package main import ( "crypto/rand" "embed" "encoding/binary" "flag" "fmt" "html/template" "io" "log" "net/http" "strings" "sync" "time" ) var ( port = flag.Int("port", 42069, "port to listen on") secretsFile = flag.String("secrets", "secrets", "path to the secrets file") notepadDir = flag.String("notepad", "notepad", "directory to save user notes") selfURL = flag.String("self-url", "http://localhost:42069", "base URL of the server") serverStartTime = time.Now() ) var ( //go:embed templates/404.html.template notFoundString string notFoundTemplate = template.Must(template.New("notFound").Parse(notFoundString)) ) func notFound(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) type args struct { Path string } if err := notFoundTemplate.Execute(w, args{Path: r.URL.Path}); err != nil { log.Printf("Warning: notFound: %s", err) } } var ( //go:embed templates/outline.html.template outlineString string outlineTemplate = template.Must(template.New("outline").Parse(outlineString)) //go:embed templates/index.html.template indexString string indexTemplate = template.Must(template.Must(outlineTemplate.Clone()).New("body").Parse(indexString)).Lookup("outline") ) func index(w http.ResponseWriter, _ *http.Request) { if err := indexTemplate.Execute(w, nil); err != nil { log.Printf("Warning: index: %s", err) } } var ( //go:embed templates/pong.html.template pongString string pongTemplate = template.Must(template.New("pong").Parse(pongString)) ) func pong(w http.ResponseWriter, _ *http.Request) { if err := pongTemplate.Execute(w, nil); err != nil { log.Printf("Warning: pong: %s", err) } } //go:embed static var staticFiles embed.FS func serveStaticFile(w http.ResponseWriter, r *http.Request, path string) { f, err := staticFiles.Open(path) if err != nil { notFound(w, r) return } defer f.Close() http.ServeContent(w, r, path, serverStartTime, f.(io.ReadSeeker)) } func static(w http.ResponseWriter, r *http.Request) { serveStaticFile(w, r, strings.TrimPrefix(r.URL.Path, "/")) } func favicon(w http.ResponseWriter, r *http.Request) { serveStaticFile(w, r, "static/favicon.ico") } var ( //go:embed wordlist.txt wordListString string wordList = strings.Split(strings.TrimSuffix(wordListString, "\n"), "\n") //go:embed templates/wormhole.html.template wormholeTemplateString string wormholeTemplate = template.Must(template.Must(outlineTemplate.Clone()).New("body").Parse(wormholeTemplateString)).Lookup("outline") ) type wormholeTemplateArgs struct { Self string Hole string } func wormhole(w http.ResponseWriter, r *http.Request) { buf := make([]byte, 20) rand.Read(buf) words := make([]string, 10) for i := range words { words[i] = wordList[binary.NativeEndian.Uint16(buf[2*i:])&0x1fff] } if err := wormholeTemplate.Execute(w, wormholeTemplateArgs{Self: *selfURL, Hole: strings.Join(words, "-")}); err != nil { log.Printf("Warning: wormhole: %s", err) } } type wormholeConn struct { done chan struct{} w http.ResponseWriter } var wormholeConnsMu sync.Mutex var wormholeConns = make(map[string]*wormholeConn) var wormholeNotifyMu sync.Mutex var wormholeNotify = make(map[string]chan struct{}) func wormholeSend(w http.ResponseWriter, r *http.Request) { reader, err := r.MultipartReader() if err != nil { http.Error(w, "not a multipart/form-data request", http.StatusBadRequest) return } for { part, err := reader.NextPart() if err != nil { break } if part.FormName() != "file" { continue } hole := r.PathValue("hole") wormholeConnsMu.Lock() conn := wormholeConns[hole] delete(wormholeConns, hole) wormholeConnsMu.Unlock() if conn == nil { http.Error(w, "no such connection", http.StatusBadRequest) return } defer close(conn.done) conn.w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+part.FileName()) if _, err := io.Copy(conn.w, part); err != nil { http.Error(w, fmt.Sprintf("Error during copy: %s", err), http.StatusServiceUnavailable) return } fmt.Fprintf(w, "uploaded!") return } http.Error(w, "file not found", http.StatusBadRequest) } func wormholeRecv(w http.ResponseWriter, r *http.Request) { ctx := r.Context() hole := r.PathValue("hole") conn := &wormholeConn{ done: make(chan struct{}), w: w, } wormholeConnsMu.Lock() wormholeConns[hole] = conn wormholeConnsMu.Unlock() defer func() { wormholeConnsMu.Lock() delete(wormholeConns, hole) wormholeConnsMu.Unlock() }() wormholeNotifyMu.Lock() notify := wormholeNotify[hole] wormholeNotifyMu.Unlock() if notify == nil { http.Error(w, "no such connection", http.StatusBadRequest) return } select { case notify <- struct{}{}: default: http.Error(w, "connection not ready", http.StatusBadRequest) return } select { case <-ctx.Done(): case <-conn.done: } } func wormholeReady(w http.ResponseWriter, r *http.Request) { ctx := r.Context() hole := r.PathValue("hole") notify := make(chan struct{}) wormholeNotifyMu.Lock() wormholeNotify[hole] = notify wormholeNotifyMu.Unlock() defer func() { wormholeNotifyMu.Lock() delete(wormholeNotify, hole) wormholeNotifyMu.Unlock() }() w.Header().Set("Content-Type", "text/event-stream") select { case <-ctx.Done(): return case <-notify: } fmt.Fprintf(w, "event: ready\ndata:\n\n") } func main() { flag.Parse() http.HandleFunc("GET /pong", pong) http.HandleFunc("GET /wormhole", wormhole) http.HandleFunc("POST /wormhole/{hole}", wormholeSend) http.HandleFunc("GET /wormhole/{hole}", wormholeRecv) http.HandleFunc("GET /wormhole/{hole}/ready", wormholeReady) http.HandleFunc("GET /static/", static) http.HandleFunc("GET /favicon.ico", favicon) http.HandleFunc("GET /{$}", index) http.HandleFunc("GET /", notFound) addr := fmt.Sprintf(":%d", *port) server := &http.Server{ Addr: addr, ReadTimeout: 10 * time.Minute, WriteTimeout: time.Minute, } log.Printf("Listening on %q", addr) log.Fatal(server.ListenAndServe()) }