Start X11 and web frontends for xC
For this, we needed a wire protocol. After surveying available options, it was decided to implement an XDR-like protocol code generator in portable AWK. It now has two backends, per each of: - xF, the X11 frontend, is in C, and is meant to be the primary user interface in the future. - xP, the web frontend, relies on a protocol proxy written in Go, and is meant for use on-the-go (no pun intended). They are very much work-in-progress proofs of concept right now, and the relay protocol is certain to change.
This commit is contained in:
		
							
								
								
									
										3
									
								
								xP/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								xP/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
/xP
 | 
			
		||||
/proto.go
 | 
			
		||||
/public/mithril.js
 | 
			
		||||
							
								
								
									
										14
									
								
								xP/Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								xP/Makefile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
.POSIX:
 | 
			
		||||
.SUFFIXES:
 | 
			
		||||
 | 
			
		||||
outputs = xP proto.go public/mithril.js
 | 
			
		||||
all: $(outputs)
 | 
			
		||||
 | 
			
		||||
xP: xP.go proto.go
 | 
			
		||||
	go build -o $@
 | 
			
		||||
proto.go: ../xC-gen-proto.awk ../xC-gen-proto-go.awk ../xC-proto
 | 
			
		||||
	awk -f ../xC-gen-proto.awk -f ../xC-gen-proto-go.awk ../xC-proto > $@
 | 
			
		||||
public/mithril.js:
 | 
			
		||||
	curl -Lo $@ https://unpkg.com/mithril/mithril.js
 | 
			
		||||
clean:
 | 
			
		||||
	rm -f $(outputs)
 | 
			
		||||
							
								
								
									
										5
									
								
								xP/go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								xP/go.mod
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
module janouch.name/xK
 | 
			
		||||
 | 
			
		||||
go 1.18
 | 
			
		||||
 | 
			
		||||
require golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d
 | 
			
		||||
							
								
								
									
										2
									
								
								xP/go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								xP/go.sum
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
 | 
			
		||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 | 
			
		||||
							
								
								
									
										109
									
								
								xP/public/xP.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								xP/public/xP.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
body {
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	font-family: sans-serif;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.xP {
 | 
			
		||||
	height: 100vh;
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: column;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.title, .status {
 | 
			
		||||
	background: #f8f8f8;
 | 
			
		||||
	border-bottom: 1px solid #ccc;
 | 
			
		||||
	padding: .05rem .3rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.middle {
 | 
			
		||||
	flex: auto;
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: row;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.list {
 | 
			
		||||
	overflow-y: auto;
 | 
			
		||||
	border-right: 1px solid #ccc;
 | 
			
		||||
	min-width: 10rem;
 | 
			
		||||
}
 | 
			
		||||
.item {
 | 
			
		||||
	padding: .05rem .3rem;
 | 
			
		||||
	cursor: default;
 | 
			
		||||
}
 | 
			
		||||
.item.active {
 | 
			
		||||
	font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Only Firefox currently supports align-content: safe end, thus this. */
 | 
			
		||||
.buffer-container {
 | 
			
		||||
	flex: auto;
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: column;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
.filler {
 | 
			
		||||
	flex: auto;
 | 
			
		||||
}
 | 
			
		||||
.buffer {
 | 
			
		||||
	display: grid;
 | 
			
		||||
	grid-template-columns: max-content auto;
 | 
			
		||||
	overflow-y: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.date {
 | 
			
		||||
	padding: .3rem;
 | 
			
		||||
	grid-column: span 2;
 | 
			
		||||
	font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
.time {
 | 
			
		||||
	padding: .1rem .3rem;
 | 
			
		||||
	background: #f8f8f8;
 | 
			
		||||
	color: #bbb;
 | 
			
		||||
	border-right: 1px solid #ccc;
 | 
			
		||||
}
 | 
			
		||||
.mark {
 | 
			
		||||
	padding-right: .3rem;
 | 
			
		||||
	text-align: center;
 | 
			
		||||
	display: inline-block;
 | 
			
		||||
	min-width: 2rem;
 | 
			
		||||
}
 | 
			
		||||
.mark.error {
 | 
			
		||||
	color: red;
 | 
			
		||||
}
 | 
			
		||||
.mark.join {
 | 
			
		||||
	color: green;
 | 
			
		||||
}
 | 
			
		||||
.mark.part {
 | 
			
		||||
	color: red;
 | 
			
		||||
}
 | 
			
		||||
.content {
 | 
			
		||||
	padding: .1rem .3rem;
 | 
			
		||||
	white-space: pre-wrap;
 | 
			
		||||
}
 | 
			
		||||
.content span.b {
 | 
			
		||||
	font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
.content span.i {
 | 
			
		||||
	font-style: italic;
 | 
			
		||||
}
 | 
			
		||||
.content span.u {
 | 
			
		||||
	text-decoration: underline;
 | 
			
		||||
}
 | 
			
		||||
.content span.s {
 | 
			
		||||
	text-decoration: line-through;
 | 
			
		||||
}
 | 
			
		||||
.content span.m {
 | 
			
		||||
	font-family: monospace;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.status {
 | 
			
		||||
	border-top: 2px solid #fff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
textarea {
 | 
			
		||||
	padding: .05rem .3rem;
 | 
			
		||||
	font-family: inherit;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										188
									
								
								xP/public/xP.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								xP/public/xP.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,188 @@
 | 
			
		||||
// TODO: Probably reset state on disconnect, and indicate to user.
 | 
			
		||||
let socket = new WebSocket(proxy)
 | 
			
		||||
 | 
			
		||||
let commandSeq = 0
 | 
			
		||||
function send(command) {
 | 
			
		||||
	socket.send(JSON.stringify({commandSeq: ++commandSeq, data: command}))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
socket.onopen = function(event) {
 | 
			
		||||
	send({command: 'Hello', version: 1})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let buffers = new Map()
 | 
			
		||||
let bufferCurrent = undefined
 | 
			
		||||
 | 
			
		||||
socket.onmessage = function(event) {
 | 
			
		||||
	console.log(event.data)
 | 
			
		||||
 | 
			
		||||
	let e = JSON.parse(event.data).data
 | 
			
		||||
	switch (e.event) {
 | 
			
		||||
	case 'BufferUpdate':
 | 
			
		||||
	{
 | 
			
		||||
		let b = buffers.get(e.bufferName)
 | 
			
		||||
		if (b === undefined) {
 | 
			
		||||
			b = {lines: []}
 | 
			
		||||
			buffers.set(e.bufferName, b)
 | 
			
		||||
		}
 | 
			
		||||
		// TODO: Update any buffer properties.
 | 
			
		||||
		break
 | 
			
		||||
	}
 | 
			
		||||
	case 'BufferRename':
 | 
			
		||||
		buffers.set(e.new, buffers.get(e.bufferName))
 | 
			
		||||
		buffers.delete(e.bufferName)
 | 
			
		||||
		break
 | 
			
		||||
	case 'BufferRemove':
 | 
			
		||||
		buffers.delete(e.bufferName)
 | 
			
		||||
		break
 | 
			
		||||
	case 'BufferActivate':
 | 
			
		||||
		bufferCurrent = e.bufferName
 | 
			
		||||
		// TODO: Somehow scroll to the end of it immediately.
 | 
			
		||||
		// TODO: Focus the textarea.
 | 
			
		||||
		break
 | 
			
		||||
	case 'BufferLine':
 | 
			
		||||
	{
 | 
			
		||||
		let b = buffers.get(e.bufferName)
 | 
			
		||||
		if (b !== undefined)
 | 
			
		||||
			b.lines.push({when: e.when, rendition: e.rendition, items: e.items})
 | 
			
		||||
		break
 | 
			
		||||
	}
 | 
			
		||||
	case 'BufferClear':
 | 
			
		||||
	{
 | 
			
		||||
		let b = buffers.get(e.bufferName)
 | 
			
		||||
		if (b !== undefined)
 | 
			
		||||
			b.lines.length = 0
 | 
			
		||||
		break
 | 
			
		||||
	}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m.redraw()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let BufferList = {
 | 
			
		||||
	view: vnode => {
 | 
			
		||||
		let items = []
 | 
			
		||||
		buffers.forEach((b, name) => {
 | 
			
		||||
			let attrs = {
 | 
			
		||||
				onclick: e => {
 | 
			
		||||
					send({command: 'BufferActivate', bufferName: name})
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
			if (name == bufferCurrent)
 | 
			
		||||
				attrs.class = 'active'
 | 
			
		||||
			items.push(m('.item', attrs, name))
 | 
			
		||||
		})
 | 
			
		||||
		return m('.list', {}, items)
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let Content = {
 | 
			
		||||
	view: vnode => {
 | 
			
		||||
		let line = vnode.children[0]
 | 
			
		||||
		let content = []
 | 
			
		||||
		switch (line.rendition) {
 | 
			
		||||
		case 'Indent': content.push(m('span.mark',       {}, ''));  break
 | 
			
		||||
		case 'Status': content.push(m('span.mark',       {}, '–')); break
 | 
			
		||||
		case 'Error':  content.push(m('span.mark.error', {}, '⚠')); break
 | 
			
		||||
		case 'Join':   content.push(m('span.mark.join',  {}, '→')); break
 | 
			
		||||
		case 'Part':   content.push(m('span.mark.part',  {}, '←')); break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let classes = new Set()
 | 
			
		||||
		let flip = c => {
 | 
			
		||||
			if (classes.has(c))
 | 
			
		||||
				classes.delete(c)
 | 
			
		||||
			else
 | 
			
		||||
				classes.add(c)
 | 
			
		||||
		}
 | 
			
		||||
		line.items.forEach(item => {
 | 
			
		||||
			// TODO: Colours.
 | 
			
		||||
			switch (item.kind) {
 | 
			
		||||
			case 'Text':
 | 
			
		||||
				// TODO: Detect and transform links.
 | 
			
		||||
				content.push(m('span', {
 | 
			
		||||
					class: Array.from(classes.keys()).join(' '),
 | 
			
		||||
				}, item.text))
 | 
			
		||||
				break
 | 
			
		||||
			case 'Reset':
 | 
			
		||||
				classes.clear()
 | 
			
		||||
				break
 | 
			
		||||
			case 'FlipBold':       flip('b'); break
 | 
			
		||||
			case 'FlipItalic':     flip('i'); break
 | 
			
		||||
			case 'FlipUnderline':  flip('u'); break
 | 
			
		||||
			case 'FlipInverse':    flip('i'); break
 | 
			
		||||
			case 'FlipCrossedOut': flip('s'); break
 | 
			
		||||
			case 'FlipMonospace':  flip('m'); break
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
		return m('.content', {}, content)
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let Buffer = {
 | 
			
		||||
	view: vnode => {
 | 
			
		||||
		let lines = []
 | 
			
		||||
		let b = buffers.get(bufferCurrent)
 | 
			
		||||
		if (b === undefined)
 | 
			
		||||
			return
 | 
			
		||||
 | 
			
		||||
		let lastDateMark = undefined
 | 
			
		||||
		b.lines.forEach(line => {
 | 
			
		||||
			let date = new Date(line.when * 1000)
 | 
			
		||||
			let dateMark = date.toLocaleDateString()
 | 
			
		||||
			if (dateMark !== lastDateMark) {
 | 
			
		||||
				lines.push(m('.date', {}, dateMark))
 | 
			
		||||
				lastDateMark = dateMark
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			lines.push(m('.time', {}, date.toLocaleTimeString()))
 | 
			
		||||
			lines.push(m(Content, {}, line))
 | 
			
		||||
		})
 | 
			
		||||
		return m('.buffer-container', {}, [
 | 
			
		||||
			m('.filler'),
 | 
			
		||||
			m('.buffer', {}, lines),
 | 
			
		||||
		])
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: This should be remembered across buffer switches,
 | 
			
		||||
// and we'll probably have to intercept /all/ key presses.
 | 
			
		||||
let Input = {
 | 
			
		||||
	view: vnode => {
 | 
			
		||||
		return m('textarea', {
 | 
			
		||||
			rows: 1,
 | 
			
		||||
			onkeydown: e => {
 | 
			
		||||
				// TODO: And perhaps on other actions, too.
 | 
			
		||||
				send({command: 'Active'})
 | 
			
		||||
				if (e.keyCode !== 13)
 | 
			
		||||
					return
 | 
			
		||||
 | 
			
		||||
				send({
 | 
			
		||||
					command: 'BufferInput',
 | 
			
		||||
					bufferName: bufferCurrent,
 | 
			
		||||
					text: e.currentTarget.value,
 | 
			
		||||
				})
 | 
			
		||||
				e.preventDefault()
 | 
			
		||||
				e.currentTarget.value = ''
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let Main = {
 | 
			
		||||
	view: vnode => {
 | 
			
		||||
		return m('.xP', {}, [
 | 
			
		||||
			m('.title', {}, "xP"),
 | 
			
		||||
			m('.middle', {}, [m(BufferList), m(Buffer)]),
 | 
			
		||||
			m('.status', {}, bufferCurrent),
 | 
			
		||||
			m(Input),
 | 
			
		||||
		])
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: Buffer names should work as routes.
 | 
			
		||||
window.addEventListener('load', () => {
 | 
			
		||||
	m.route(document.body, '/', {
 | 
			
		||||
		'/': Main,
 | 
			
		||||
	})
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										2
									
								
								xP/xP.example.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								xP/xP.example.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										186
									
								
								xP/xP.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								xP/xP.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,186 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/net/websocket"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	addressBind    string
 | 
			
		||||
	addressConnect string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func clientToRelay(
 | 
			
		||||
	ctx context.Context, ws *websocket.Conn, conn net.Conn) bool {
 | 
			
		||||
	var j string
 | 
			
		||||
	if err := websocket.Message.Receive(ws, &j); err != nil {
 | 
			
		||||
		log.Println("Command receive failed: " + err.Error())
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Printf("?> %s\n", j)
 | 
			
		||||
 | 
			
		||||
	var m RelayCommandMessage
 | 
			
		||||
	if err := json.Unmarshal([]byte(j), &m); err != nil {
 | 
			
		||||
		log.Println("Command unmarshalling failed: " + err.Error())
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	b, ok := m.AppendTo(make([]byte, 4))
 | 
			
		||||
	if !ok {
 | 
			
		||||
		log.Println("Command serialization failed")
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	binary.BigEndian.PutUint32(b[:4], uint32(len(b)-4))
 | 
			
		||||
	if _, err := conn.Write(b); err != nil {
 | 
			
		||||
		log.Println("Command send failed: " + err.Error())
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Printf("-> %v\n", b)
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func relayToClient(
 | 
			
		||||
	ctx context.Context, ws *websocket.Conn, conn net.Conn) bool {
 | 
			
		||||
	var length uint32
 | 
			
		||||
	if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
 | 
			
		||||
		log.Println("Event receive failed: " + err.Error())
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	b := make([]byte, length)
 | 
			
		||||
	if _, err := io.ReadFull(conn, b); err != nil {
 | 
			
		||||
		log.Println("Event receive failed: " + err.Error())
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Printf("<? %v\n", b)
 | 
			
		||||
 | 
			
		||||
	var m RelayEventMessage
 | 
			
		||||
	if after, ok := m.ConsumeFrom(b); !ok {
 | 
			
		||||
		log.Println("Event deserialization failed")
 | 
			
		||||
		return false
 | 
			
		||||
	} else if len(after) != 0 {
 | 
			
		||||
		log.Println("Event deserialization failed: trailing data")
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	j, err := json.Marshal(&m)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Println("Event marshalling failed: " + err.Error())
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if err := websocket.Message.Send(ws, string(j)); err != nil {
 | 
			
		||||
		log.Println("Event send failed: " + err.Error())
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Printf("<- %s\n", j)
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func errorToClient(ws *websocket.Conn, err error) bool {
 | 
			
		||||
	j, err := json.Marshal(&RelayEventMessage{
 | 
			
		||||
		EventSeq: 0,
 | 
			
		||||
		Data: RelayEventData{
 | 
			
		||||
			Interface: RelayEventDataError{
 | 
			
		||||
				Event:      RelayEventError,
 | 
			
		||||
				CommandSeq: 0,
 | 
			
		||||
				Error:      err.Error(),
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Println("Event marshalling failed: " + err.Error())
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	if err := websocket.Message.Send(ws, string(j)); err != nil {
 | 
			
		||||
		log.Println("Event send failed: " + err.Error())
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func handleWebSocket(ws *websocket.Conn) {
 | 
			
		||||
	conn, err := net.Dial("tcp", addressConnect)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		errorToClient(ws, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// We don't need to intervene, so it's just two separate pipes so far.
 | 
			
		||||
	ctx, cancel := context.WithCancel(ws.Request().Context())
 | 
			
		||||
	go func() {
 | 
			
		||||
		for clientToRelay(ctx, ws, conn) {
 | 
			
		||||
		}
 | 
			
		||||
		cancel()
 | 
			
		||||
	}()
 | 
			
		||||
	go func() {
 | 
			
		||||
		for relayToClient(ctx, ws, conn) {
 | 
			
		||||
		}
 | 
			
		||||
		cancel()
 | 
			
		||||
	}()
 | 
			
		||||
	<-ctx.Done()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var staticHandler = http.FileServer(http.Dir("."))
 | 
			
		||||
 | 
			
		||||
var page = template.Must(template.New("/").Parse(`<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
	<title>xP</title>
 | 
			
		||||
	<meta charset="utf-8" />
 | 
			
		||||
	<link rel="stylesheet" href="xP.css" />
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
	<script src="mithril.js">
 | 
			
		||||
	</script>
 | 
			
		||||
	<script>
 | 
			
		||||
	let proxy = '{{ . }}'
 | 
			
		||||
	</script>
 | 
			
		||||
	<script src="xP.js">
 | 
			
		||||
	</script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>`))
 | 
			
		||||
 | 
			
		||||
func handleDefault(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if r.URL.Path != "/" {
 | 
			
		||||
		staticHandler.ServeHTTP(w, r)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wsURI := fmt.Sprintf("ws://%s/ws", r.Host)
 | 
			
		||||
	if err := page.Execute(w, wsURI); err != nil {
 | 
			
		||||
		log.Println("Template execution failed: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	if len(os.Args) != 3 {
 | 
			
		||||
		log.Fatalf("usage: %s BIND CONNECT\n", os.Args[0])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	addressBind, addressConnect = os.Args[1], os.Args[2]
 | 
			
		||||
 | 
			
		||||
	http.Handle("/ws", websocket.Handler(handleWebSocket))
 | 
			
		||||
	http.Handle("/", http.HandlerFunc(handleDefault))
 | 
			
		||||
 | 
			
		||||
	s := &http.Server{
 | 
			
		||||
		Addr:           addressBind,
 | 
			
		||||
		ReadTimeout:    60 * time.Second,
 | 
			
		||||
		WriteTimeout:   60 * time.Second,
 | 
			
		||||
		MaxHeaderBytes: 32 << 10,
 | 
			
		||||
	}
 | 
			
		||||
	log.Fatal(s.ListenAndServe())
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user