package main import ( "bytes" "crypto/rand" "embed" "encoding/base64" "encoding/json" "fmt" "html/template" "io" "log" "net/http" "net/url" "os" "path/filepath" "strings" "github.com/gabriel-vasile/mimetype" "github.com/robert-nix/ansihtml" flag "github.com/spf13/pflag" ) const ( MetadataExt = ".metadata" DefaultMimeType = "application/octet-stream" DefaultMaxSize = (10 << 23) // 83MB ) type Server struct { data string host string maxSize int64 } type Metadata struct { ContentType string `json:"contentType"` } //go:embed template.html var static embed.FS var tmpl = template.Must(template.ParseFS(static, "*.html")) func detectMimetype(input io.Reader) (mimeType string, fileext string, recycled io.Reader, err error) { header := bytes.NewBuffer(nil) mtype, err := mimetype.DetectReader(io.TeeReader(input, header)) recycled = io.MultiReader(header, input) return mtype.String(), mtype.Extension(), recycled, err } func serveAnsiHtml(w http.ResponseWriter, filename string) { f, err := os.Open(filename) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.Header().Add("content-type", "text/html") data, err := io.ReadAll(f) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } data = ansihtml.ConvertToHTML(data) data = fmt.Appendf(nil, "
%s
", strings.ReplaceAll(string(data), "\n", "
")) w.Write(data) } func writeMetadata(filename string, m *Metadata) error { f, err := os.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) if err != nil { return err } j := json.NewEncoder(f) return j.Encode(m) } func readMetadata(filename string) (*Metadata, error) { f, err := os.Open(filename) if err != nil { return nil, err } var m Metadata j := json.NewDecoder(f) if err := j.Decode(&m); err != nil { return nil, err } return &m, nil } func (s *Server) writeMetadata(name string, m *Metadata) error { filename := filepath.Join(s.data, name+MetadataExt) return writeMetadata(filename, m) } func (s *Server) randomFile(ext string) (*os.File, string, error) { var random [16]byte _, err := io.ReadFull(rand.Reader, random[:]) if err != nil { return nil, "", err } name := base64.RawURLEncoding.EncodeToString(random[:]) + ext filename := filepath.Join(s.data, name) f, err := os.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) if err != nil { return nil, "", err } return f, name, nil } func (s *Server) createPosting(rd io.Reader, mimeType string) (string, error) { var ext string if mimeType == "" || mimeType == "auto" { mimeType, ext, rd, _ = detectMimetype(rd) } else { mime := mimetype.Lookup(mimeType) if mime != nil { ext = mime.Extension() } } f, name, err := s.randomFile(ext) if err != nil { return "", fmt.Errorf("randomFile(): %w", err) } defer f.Close() if _, err := io.Copy(f, rd); err != nil { return "", fmt.Errorf("io.Copy(): %w", err) } f.Sync() m := Metadata{ContentType: mimeType} if err = s.writeMetadata(name, &m); err != nil { return "", fmt.Errorf("writeMetadata(): %w", err) } return name, nil } func (s *Server) postText(r *http.Request) (string, error) { text := r.Form.Get("text") if text == "" { return "", nil } mimeType := r.Form.Get("text_mimetype") return s.createPosting(strings.NewReader(text), mimeType) } func (s *Server) postFile(r *http.Request) (string, error) { mimeType := r.Form.Get("file_mimetype") file, _, err := r.FormFile("file") if err != nil { return "", err } defer file.Close() return s.createPosting(file, mimeType) } func (s *Server) servePosting(w http.ResponseWriter, r *http.Request, name string) { filename := filepath.Join(s.data, name) m, err := readMetadata(filename + MetadataExt) if err != nil { log.Print("readMetadata(): ", err) w.WriteHeader(http.StatusNotFound) return } h := w.Header() switch m.ContentType { case "text/x-ansi": serveAnsiHtml(w, filename) return case "text/x-url": location, err := os.ReadFile(filename) if err != nil { log.Print("read(): ", err) w.WriteHeader(http.StatusNotFound) return } w.Header().Add("location", string(location)) w.WriteHeader(http.StatusFound) return case "": m.ContentType = "application/octet-stream" default: } h.Add("content-type", m.ContentType) http.ServeFile(w, r, filename) } func (s *Server) post(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { return } r.Body = http.MaxBytesReader(w, r.Body, s.maxSize) // impose maximum request size mimeType := r.Header.Get("content-type") if strings.HasPrefix(mimeType, "multipart/form-data;") { // max 8MB in RAM for multipart parsing if err := r.ParseMultipartForm(1 << 23); err != nil { log.Print("Error parse multipart form: ", err) w.WriteHeader(http.StatusInternalServerError) return } textName, err := s.postText(r) if err != nil { log.Print("Error text upload: ", err) } fileName, err := s.postFile(r) if err != nil { log.Print("Error file upload: ", err) } if fileName == "" && textName == "" { w.WriteHeader(http.StatusInternalServerError) return } log.Printf("Created posting: text=%s, file=%s", textName, fileName) location := fmt.Sprintf("/link?text=%s&file=%s", url.QueryEscape(textName), url.QueryEscape(fileName)) w.Header().Add("location", location) w.WriteHeader(http.StatusFound) } else { if mimeType == "application/x-www-form-urlencoded" { mimeType = "auto" } name, err := s.createPosting(r.Body, mimeType) if err != nil { log.Print("failed to create file: ", err) w.WriteHeader(http.StatusInternalServerError) return } log.Printf("Created posting: %s", name) w.WriteHeader(http.StatusCreated) fmt.Fprintf(w, "%s/%s\n", s.host, name) } } func (s *Server) get(w http.ResponseWriter, r *http.Request) { path := r.URL.Path switch path { case "/": w.Header().Add("content-type", "text/html; charset=UTF-8") if err := tmpl.ExecuteTemplate(w, "template.html", map[string]any{"Form": true}); err != nil { w.WriteHeader(http.StatusInternalServerError) } case "/link": tmplData := map[string]string{ "Host": s.host, "TextLink": r.FormValue("text"), "FileLink": r.FormValue("file"), } if err := tmpl.ExecuteTemplate(w, "template.html", tmplData); err != nil { w.WriteHeader(http.StatusInternalServerError) } default: s.servePosting(w, r, filepath.Base(path)) } } func (s *Server) Index(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: s.get(w, r) case http.MethodPost: s.post(w, r) default: w.WriteHeader(http.StatusMethodNotAllowed) } } func main() { var ( addr string data string host string maxSize uint32 ) flag.StringVarP(&addr, "addr", "a", "0.0.0.0:9000", "The address to bind to") flag.StringVarP(&data, "data-path", "d", "./data", "The path to the data") flag.StringVarP(&host, "host", "h", "", "The host url, e.g. http://127.0.0.1") flag.Uint32VarP(&maxSize, "max-size", "", DefaultMaxSize, "The maximum file upload size") flag.Parse() datadir, err := filepath.Abs(data) if err != nil { log.Fatal(err) } s := &Server{ data: datadir, host: host, maxSize: int64(maxSize), } mux := http.NewServeMux() mux.HandleFunc("/", s.Index) log.Printf("Listening at '%s'...", addr) if err := http.ListenAndServe(addr, mux); err != nil { log.Fatal(err) } }