Merge "OWNERS += implr"
diff --git a/hswaw/laserproxy/BUILD.bazel b/hswaw/laserproxy/BUILD.bazel
new file mode 100644
index 0000000..ca8bfb1
--- /dev/null
+++ b/hswaw/laserproxy/BUILD.bazel
@@ -0,0 +1,23 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "locker.go",
+        "main.go",
+        "proxy.go",
+    ],
+    importpath = "code.hackerspace.pl/hscloud/hswaw/laserproxy",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//go/mirko:go_default_library",
+        "//hswaw/laserproxy/tpl:go_default_library",
+        "@com_github_golang_glog//:go_default_library",
+    ],
+)
+
+go_binary(
+    name = "laserproxy",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
diff --git a/hswaw/laserproxy/README.md b/hswaw/laserproxy/README.md
new file mode 100644
index 0000000..3a2e8db
--- /dev/null
+++ b/hswaw/laserproxy/README.md
@@ -0,0 +1,17 @@
+hswaw ruida laser proxy
+=======================
+
+This is a layer 7 proxy to access the Warsaw Hackerspace laser from the main LAN/wifi.
+
+For more information about actually accessing the lasercutter in the space, see [the wiki entry](https://wiki.hackerspace.pl/infra:tools:lasercutter). The rest of this file will describe the software itself.
+
+Architecture
+------------
+
+The laserproxy software is a single Go binary that runs on Customs, which has access to both the Hackerspace LAN and the laser network. It proxies UDP traffic from lasercutter users to the lasercutter itself.
+
+Only one user is allowed at a time - to implement this mutual exclusion, a Locker worker manages a single (address, note) tuple that is currently allowed to proxy traffic, and to which return traffic is forwarded back. The Locker also maintians a deadline. After the deadline expires, the lock is automatically released. A user can also release their lock ahead of time.
+
+A lock is taken through a web interface by the user that wants to access the laser.
+
+When a lock is taken, the Locker will notify a Proxy worker about this address. The Proxy will then perform the UDP proxying. As traffic is proxied, the Proxy will send bump updates to the Locker to extend the lock deadline.
diff --git a/hswaw/laserproxy/locker.go b/hswaw/laserproxy/locker.go
new file mode 100644
index 0000000..044e832
--- /dev/null
+++ b/hswaw/laserproxy/locker.go
@@ -0,0 +1,131 @@
+package main
+
+import (
+	"context"
+	"time"
+
+	"github.com/golang/glog"
+)
+
+type lockCtrl struct {
+	getCurrent *lockCtrlGetCurrent
+	take *lockCtrlTake
+	release *lockCtrlRelease
+	subscribe *lockCtrlSubscribe
+	bump *lockCtrlBump
+}
+
+type lockCtrlGetCurrent struct {
+	res chan *lockResCurrent
+}
+
+type lockCtrlTake struct {
+	note string
+	addr string
+	prev string
+	res chan bool
+}
+
+type lockCtrlRelease struct {
+	addr string
+	force bool
+	res chan struct{}
+}
+
+type lockCtrlSubscribe struct {
+	subscriber chan *lockUpdate
+}
+
+type lockCtrlBump struct {
+	addr string
+}
+
+type lockResCurrent struct {
+	note string
+	addr string
+	deadline time.Time
+}
+
+type lockUpdate struct {
+	note string
+	addr string
+}
+
+func (s *service) runLocker(ctx context.Context, ctrlC chan *lockCtrl) {
+	glog.Infof("Locker starting...")
+
+	var curNote string
+	var curAddr string
+	var curDeadline time.Time
+	var subscribers []chan *lockUpdate
+
+	notify := func() {
+		for _, sub := range subscribers {
+			go func() {
+				sub <- &lockUpdate{
+					note: curNote,
+					addr: curAddr,
+				}
+			}()
+		}
+	}
+
+	t := time.NewTicker(5 * time.Second)
+	defer t.Stop()
+
+	for {
+		select {
+		case <-ctx.Done():
+			err := ctx.Err()
+			glog.Errorf("Locker stoppped: %v", err)
+			return
+		case <-t.C:
+			if curAddr != "" && time.Now().After(curDeadline) {
+				glog.Infof("Expiring lock")
+				curAddr = ""
+				curNote = ""
+				notify()
+			}
+		case ctrl := <-ctrlC:
+			switch {
+			case ctrl.take != nil:
+				won := false
+				if curAddr == ctrl.take.prev {
+					won = true
+					curNote = ctrl.take.note
+					curAddr = ctrl.take.addr
+					curDeadline = time.Now().Add(15 * time.Minute)
+					notify()
+					glog.Infof("Lock taken by %q %q", curNote, curAddr)
+				}
+				go func() {
+					ctrl.take.res <- won
+				}()
+			case ctrl.release != nil:
+				if curAddr == ctrl.release.addr || ctrl.release.force {
+					curAddr = ""
+					curNote = ""
+					notify()
+					glog.Infof("Lock relased")
+				}
+				go func() {
+					ctrl.release.res <- struct{}{}
+				}()
+			case ctrl.getCurrent != nil:
+				go func() {
+					ctrl.getCurrent.res <- &lockResCurrent{
+						note: curNote,
+						addr: curAddr,
+						deadline: curDeadline,
+					}
+				}()
+			case ctrl.bump != nil:
+				if curAddr != "" {
+					curDeadline = time.Now().Add(15 * time.Minute)
+				}
+			case ctrl.subscribe != nil:
+				subscribers = append(subscribers, ctrl.subscribe.subscriber)
+			}
+		}
+	}
+}
diff --git a/hswaw/laserproxy/main.go b/hswaw/laserproxy/main.go
new file mode 100644
index 0000000..07d7bcd
--- /dev/null
+++ b/hswaw/laserproxy/main.go
@@ -0,0 +1,205 @@
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"html/template"
+	"net"
+	"net/http"
+	"math/rand"
+	"strings"
+	"time"
+
+	"github.com/golang/glog"
+
+	mirko "code.hackerspace.pl/hscloud/go/mirko"
+	"code.hackerspace.pl/hscloud/hswaw/laserproxy/tpl"
+)
+
+var (
+	flagLaserAddress = "10.11.0.10"
+	flagLaserNetworkAddress = "10.11.0.1"
+	flagClientNetworkAddress = "10.8.1.2"
+	flagWebAddress = "127.0.0.1:8080"
+
+	tplIndex = template.Must(template.New("index.html").Parse(tpl.MustAssetString("index.html")))
+)
+
+type service struct {
+	lockCtrl chan *lockCtrl
+}
+
+func main() {
+	flag.StringVar(&flagLaserAddress, "laser_address", flagLaserAddress, "Address of Ruida controller on laser network")
+	flag.StringVar(&flagLaserNetworkAddress, "laser_network", flagLaserNetworkAddress, "Address of proxy on laser network")
+	flag.StringVar(&flagClientNetworkAddress, "client_network", flagClientNetworkAddress, "Address of proxy on client network")
+	flag.StringVar(&flagWebAddress, "web_address", flagWebAddress, "Address and port to listen on for public web connections")
+
+	flag.Parse()
+	m := mirko.New()
+	if err := m.Listen(); err != nil {
+		glog.Exitf("Could not listen: %v", err)
+	}
+
+	lisLaser, err := net.ListenPacket("udp", fmt.Sprintf("%s:40200", flagLaserNetworkAddress))
+	if err != nil {
+		glog.Fatalf("could not listen on laser network: %v", err)
+	}
+	defer lisLaser.Close()
+
+	lisClient, err := net.ListenPacket("udp", fmt.Sprintf("%s:50200", flagClientNetworkAddress))
+	if err != nil {
+		glog.Fatalf("could not listen on client network: %v", err)
+	}
+	defer lisClient.Close()
+
+	laserIP := net.ParseIP(flagLaserAddress)
+	if laserIP == nil {
+		glog.Fatalf("could not parse laser IP address %q", flagLaserAddress)
+	}
+	laserAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:50200", laserIP.String()))
+	if err != nil {
+		glog.Fatalf("could not make laser UDP address: %v", err)
+	}
+
+	ctx := m.Context()
+	s := &service{
+		lockCtrl: make(chan *lockCtrl),
+	}
+	updates := make(chan *lockUpdate)
+	go s.runProxy(ctx, updates, laserAddr, lisLaser, lisClient)
+	go s.runLocker(ctx, s.lockCtrl)
+	s.lockCtrl <- &lockCtrl{
+		subscribe: &lockCtrlSubscribe{
+			subscriber: updates,
+		},
+	}
+
+	mux := http.NewServeMux()
+	mux.HandleFunc("/", s.handlerIndex)
+	mux.HandleFunc("/take", s.handlerTake)
+	mux.HandleFunc("/release", s.handlerRelease)
+	mux.HandleFunc("/force", s.handlerForce)
+	httpSrv := &http.Server{Addr: flagWebAddress, Handler: mux}
+
+	glog.Infof("Listening for web connections on %q...", flagWebAddress)
+	go func() {
+		if err := httpSrv.ListenAndServe(); err != nil {
+			glog.Error(err)
+		}
+	}()
+
+	if err := m.Serve(); err != nil {
+		glog.Exitf("Could not run: %v", err)
+	}
+
+	glog.Info("Running.")
+	<-m.Done()
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	httpSrv.Shutdown(ctx)
+}
+
+var (
+	sampleNames = []string{
+		"enleth", "radex", "qdot", "hans acker", "makłowicz",
+	}
+)
+
+func remoteAddr(r *http.Request) string {
+	if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
+		return strings.Split(fwd, ":")[0]
+	}
+	return strings.Split(r.RemoteAddr, ":")[0]
+}
+
+func (s *service) handlerIndex(w http.ResponseWriter, r *http.Request) {
+	res := make(chan *lockResCurrent)
+	s.lockCtrl <- &lockCtrl{
+		getCurrent: &lockCtrlGetCurrent{
+			res: res,
+		},
+	}
+	cur := <-res
+
+	err := tplIndex.Execute(w, struct {
+		You bool
+		CurrentAddress string
+		CurrentNote string
+		CurrentDeadline string
+		SampleName string
+	}{
+		You: cur.addr == remoteAddr(r),
+		CurrentAddress: cur.addr,
+		CurrentNote: cur.note,
+		CurrentDeadline: fmt.Sprintf("%d minute(s)", int(cur.deadline.Sub(time.Now()).Minutes())),
+		SampleName: sampleNames[rand.Intn(len(sampleNames))],
+	})
+	if err != nil {
+		glog.Errorf("rendering template: %v", err)
+	}
+}
+
+func (s *service) handlerTake(w http.ResponseWriter, r *http.Request) {
+	if r.Method != "POST" {
+		return
+	}
+	r.ParseForm()
+	who := r.Form.Get("who")
+	prev := r.Form.Get("prev")
+	if who == "" {
+		fmt.Fprintf(w, "sorry, who are you? please specify a name")
+		return
+	}
+
+	res := make(chan bool)
+	take := &lockCtrlTake{
+		note: who,
+		addr: remoteAddr(r),
+		prev: prev,
+		res: res,
+	}
+	s.lockCtrl <- &lockCtrl{
+		take: take,
+	}
+	won := <-res
+
+	if won {
+		http.Redirect(w, r, "/", 302)
+	} else {
+		fmt.Fprintf(w, "lock not taken")
+	}
+}
+
+func (s *service) handlerRelease(w http.ResponseWriter, r *http.Request) {
+	if r.Method != "POST" {
+		return
+	}
+	res := make(chan struct{})
+	s.lockCtrl <- &lockCtrl{
+		release: &lockCtrlRelease{
+			addr: remoteAddr(r),
+			res: res,
+		},
+	}
+	<-res
+
+	http.Redirect(w, r, "/", 302)
+}
+
+func (s *service) handlerForce(w http.ResponseWriter, r *http.Request) {
+	if r.Method != "POST" {
+		return
+	}
+	res := make(chan struct{})
+	s.lockCtrl <- &lockCtrl{
+		release: &lockCtrlRelease{
+			force: true,
+			res: res,
+		},
+	}
+	<-res
+
+	http.Redirect(w, r, "/", 302)
+}
diff --git a/hswaw/laserproxy/proxy.go b/hswaw/laserproxy/proxy.go
new file mode 100644
index 0000000..21f9399
--- /dev/null
+++ b/hswaw/laserproxy/proxy.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+	"context"
+	"net"
+	"strings"
+
+	"github.com/golang/glog"
+)
+
+type packetFrom struct {
+	addr net.Addr
+	data []byte
+}
+
+func (s *service) runProxy(ctx context.Context, updates chan *lockUpdate, laserAddr net.Addr, laserNet, clientNet net.PacketConn) {
+	glog.Infof("Proxy starting... (laser: %v, laser network: %v, client network: %v)", laserAddr, laserNet, clientNet)
+
+
+	pipe := func(conn net.PacketConn, C chan *packetFrom) {
+		for {
+			buf := make([]byte, 1500)
+			n, addr, err := conn.ReadFrom(buf)
+			if err != nil {
+				glog.Errorf("pipe failed: %v", err)
+			}
+			C <- &packetFrom{ addr, buf[:n] }
+		}
+	}
+
+	laserC := make(chan *packetFrom)
+	go pipe(laserNet, laserC)
+	clientC := make(chan *packetFrom)
+	go pipe(clientNet, clientC)
+
+	var allowedClient string
+	var curClient *net.Addr
+	for {
+		select {
+		case <-ctx.Done():
+			err := ctx.Err()
+			glog.Errorf("Proxy stopped: %v", err)
+			return
+		case u := <-updates:
+			allowedClient = u.addr
+			glog.Infof("New allowed client: %q", allowedClient)
+		case p := <-laserC:
+			s.lockCtrl <- &lockCtrl{
+				bump: &lockCtrlBump{},
+			}
+			if curClient == nil {
+				glog.Warningf("Packet from laser without client connected, dropping.")
+				break
+			}
+			clientNet.WriteTo(p.data, *curClient)
+		case p := <-clientC:
+			s.lockCtrl <- &lockCtrl{
+				bump: &lockCtrlBump{},
+			}
+			if strings.Split(p.addr.String(), ":")[0] == allowedClient {
+				curClient = &p.addr
+				laserNet.WriteTo(p.data, laserAddr)
+			} else {
+				glog.Infof("Rejecting packet from %s", p.addr.String())
+			}
+		}
+	}
+}
+
diff --git a/hswaw/laserproxy/tpl/BUILD.bazel b/hswaw/laserproxy/tpl/BUILD.bazel
new file mode 100644
index 0000000..10e3740
--- /dev/null
+++ b/hswaw/laserproxy/tpl/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@io_bazel_rules_go//extras:bindata.bzl", "bindata")
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+bindata(
+    name = "tpl_bindata",
+    srcs = glob(["*"]),
+    extra_args = ["."],
+    package = "tpl",
+)
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        ":tpl_bindata",  # keep
+    ],
+    importpath = "code.hackerspace.pl/hscloud/hswaw/laserproxy/tpl",  # keep
+    visibility = ["//hswaw/laserproxy:__subpackages__"],
+)
diff --git a/hswaw/laserproxy/tpl/index.html b/hswaw/laserproxy/tpl/index.html
new file mode 100644
index 0000000..2bd93e7
--- /dev/null
+++ b/hswaw/laserproxy/tpl/index.html
@@ -0,0 +1,52 @@
+<!doctype html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8">
+        <title>laserproxy</title>
+    </head>
+    <body>
+        <h1>warsaw hackerspace SOKÓŁ proxy</h1>
+        {{ if eq .CurrentAddress "" }}
+        <p>
+            Currently <b>not</b> in use by anyone. Wanna lase something?
+        </p>
+        <form action="/take" method="POST">
+            I am <input id="persist" type="text" name="who" placeholder="{{ .SampleName }}" /> and want to <input type="submit" value="use the laser" /> over the network.
+        </form>
+        {{ else if .You }}
+        <p>
+            Currently in use by <b>you</b> ({{ .CurrentNote }}, {{ .CurrentAddress }}). <b>Expires in {{ .CurrentDeadline }}.</b> This deadline will automatically extend as long as the laser is actively used.
+        </p>
+        <p>
+            To cut something, use LightBurn, and point it as <b>10.8.1.2</b> (as a 'Ruida' Ethernet/LAN controller).
+        </p>
+        <form action="/release" method="POST">
+            <input type="submit" value="I'm done with the laser." />
+        </form>
+        {{ else }}
+        <p>
+            Currently in use by '{{ .CurrentNote }}' ({{ .CurrentAddress }}). <b>Expires in {{ .CurrentDeadline }}</b>.
+        </p>
+        <form action="/force" method="POST">
+            I need to use the laser now and I can't ask the current user ({{ .CurrentNote }}) to release the lock. <input type="submit" value="I want to forcefully release the lock" />, and am aware of the possible consequences of that.
+        </form>
+        {{ end }}
+        <p>
+            <b>Confused by this?</b> See our <a href="https://wiki.hackerspace.pl/infra:tools:lasercutter">wiki entry about how to use the laser</a>.
+        </p>
+
+        <script>
+let element = document.querySelector("#persist");
+if (element !== null) {
+    let existing = localStorage.getItem("hacker");
+    if (existing !== "" && existing !== null) {
+        element.value = existing;
+    }
+    element.addEventListener('change', (event) => {
+        let value = event.target.value;
+        localStorage.setItem("hacker", value);
+    });
+}
+        </script>
+    </body>
+</html>