package main import ( "crypto/subtle" "embed" "encoding/json" "errors" "fmt" "io" "net" "net/http" "os/exec" "runtime" "strings" "time" ) //go:embed web/index.html var assets embed.FS func (a *App) gui() error { ln, err := net.Listen("tcp4", "127.0.0.1:0") if err != nil { return err } token := newID() + newID() origin := "http://" + ln.Addr().String() mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; frame-ancestors 'none'; connect-src 'self'") if r.Host != ln.Addr().String() { http.Error(w, "invalid host", 403) return } if r.URL.Path == "/" { if r.Method != "GET" { http.Error(w, "method", 405) return } b, _ := assets.ReadFile("web/index.html") w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write(b) return } if subtle.ConstantTimeCompare([]byte(r.Header.Get("Authorization")), []byte("Bearer "+token)) != 1 { http.Error(w, "unauthorized", 401) return } if o := r.Header.Get("Origin"); o != "" && o != origin { http.Error(w, "origin", 403) return } w.Header().Set("Content-Type", "application/json") var result any = map[string]bool{"ok": true} var err error if r.Method == "GET" && r.URL.Path == "/api/state" { a.mu.Lock() s := a.settings a.mu.Unlock() result = struct { Settings Settings `json:"settings"` Status Status `json:"status"` }{s, a.engine.status()} } else if r.Method == "POST" { r.Body = http.MaxBytesReader(w, r.Body, 1024*1024) dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() decode := func(v any) error { if e := dec.Decode(v); e != nil { return e } if e := dec.Decode(new(any)); e != io.EOF { return errors.New("unexpected trailing JSON") } return nil } switch r.URL.Path { case "/api/save": var p Profile if err = decode(&p); err == nil { err = a.save(p) } case "/api/delete", "/api/connect": var p struct { ID string `json:"id"` } if err = decode(&p); err == nil { if r.URL.Path == "/api/delete" { err = a.remove(p.ID) } else { err = a.connect(p.ID) } } case "/api/disconnect": a.engine.stop() if s := a.engine.status(); s.Error != "" { err = errors.New(s.Error) } case "/api/options": var o struct { Autorun bool `json:"autorun"` OnBoot bool `json:"onBoot"` } if err = decode(&o); err == nil { err = a.setOptions(o.Autorun, o.OnBoot) } default: http.NotFound(w, r) return } } else { http.NotFound(w, r) return } if err != nil { w.WriteHeader(400) result = map[string]string{"error": err.Error()} } _ = json.NewEncoder(w).Encode(result) }) server := &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 20 * time.Second, WriteTimeout: 45 * time.Second, IdleTimeout: 60 * time.Second} defer server.Close() url := origin + "/#" + token fmt.Println("GUI:", url, "\nKeep this process running. Ctrl+C stops the VPN.") var c *exec.Cmd switch runtime.GOOS { case "windows": c = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) case "darwin": c = exec.Command("open", url) default: if strings.Contains(runtime.GOOS, "android") { c = exec.Command("termux-open-url", url) } else { c = exec.Command("xdg-open", url) } } if err = c.Start(); err != nil { fmt.Println("Open the GUI URL in your browser:", err) } else { go c.Wait() } return server.Serve(ln) }