package main
import (
"bytes"
"crypto/rand"
"embed"
"encoding/base64"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"net/http"
"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 = []byte(fmt.Sprintf("
%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;") {
r.ParseMultipartForm(1 << 23) // max 8MB in RAM
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
}
var textLink string
var fileLink string
if textName != "" {
log.Printf("Created posting: %s", textName)
textLink = fmt.Sprintf("%s/%s", s.host, textName)
}
if fileName != "" {
log.Printf("Created posting: %s", fileName)
fileLink = fmt.Sprintf("%s/%s", s.host, fileName)
}
tmplData := map[string]string{
"TextLink": textLink,
"FileLink": fileLink,
}
if err := tmpl.ExecuteTemplate(w, "template.html", tmplData); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
} 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
if path == "/" {
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)
}
} else {
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)
}
}