package main import ( "crypto/subtle" "flag" "fmt" "io" "log" "net/http" "os" "path/filepath" "strings" ) func runServer(argv []string) int { fs := flag.NewFlagSet("serve", flag.ContinueOnError) addr := fs.String("addr", envAddr(), "listen address") if err := fs.Parse(argv); err != nil { return 2 } srv := &server{token: envToken(), maxSize: envMaxSize()} mux := http.NewServeMux() mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, "ok\n") }) mux.HandleFunc("POST /open", srv.handleOpen) mux.HandleFunc("POST /file", srv.handleFile) log.Printf("open-proxy serving on %s (token=%v)", *addr, srv.token != "") if err := http.ListenAndServe(*addr, srv.auth(mux)); err != nil { fmt.Fprintf(os.Stderr, "open-proxy serve: %v\n", err) return 1 } return 0 } type server struct { token string maxSize int64 } func (s *server) auth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if s.token != "" { got := r.Header.Get(tokenHeader) if subtle.ConstantTimeCompare([]byte(got), []byte(s.token)) != 1 { http.Error(w, "forbidden", http.StatusForbidden) return } } next.ServeHTTP(w, r) }) } func (s *server) handleOpen(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(io.LimitReader(r.Body, 64<<10)) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } target := strings.TrimSpace(string(body)) if target == "" { http.Error(w, "empty target", http.StatusBadRequest) return } if err := hostOpen(target); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } log.Printf("open url/string: %s", target) _, _ = io.WriteString(w, "ok\n") } func (s *server) handleFile(w http.ResponseWriter, r *http.Request) { name := sanitizeName(r.Header.Get(fileNameHeader)) dir, err := os.MkdirTemp("", "open-proxy-") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } dst := filepath.Join(dir, name) f, err := os.Create(dst) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } n, err := io.Copy(f, io.LimitReader(r.Body, s.maxSize)) cerr := f.Close() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if cerr != nil { http.Error(w, cerr.Error(), http.StatusInternalServerError) return } if err := hostOpen(dst); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } log.Printf("open file: %s (%d bytes)", dst, n) w.Header().Set(pathHeaderField, dst) _, _ = fmt.Fprintf(w, "%s\n", dst) } // sanitizeName reduces a client-supplied filename to a safe basename, guarding // against path traversal and empty values. func sanitizeName(name string) string { name = filepath.Base(filepath.FromSlash(name)) if name == "" || name == "." || name == string(filepath.Separator) || name == ".." { return "opened-file" } return name }