filehole/main.go

307 lines
8.9 KiB
Go

package main
import (
"bytes"
"crypto/rand"
_ "embed"
"errors"
"flag"
"html"
"html/template"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/boltdb/bolt"
"github.com/gabriel-vasile/mimetype"
"github.com/landlock-lsm/go-landlock/landlock"
)
func shortID(length int64) string {
const CHARS = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ123456789"
ll := len(CHARS)
b := make([]byte, length)
rand.Read(b) // generates len(b) random bytes
for i := int64(0); i < length; i++ {
b[i] = CHARS[int(b[i])%ll]
}
return string(b)
}
var db *bolt.DB
func (fh FileholeServer) GalleryHandler(w http.ResponseWriter, r *http.Request) {
v := mux.Vars(r)
w.Write([]byte(`<!DOCTYPE html><html><head><style>body { background-color: black; color: white; }</style></head><body>`))
for _, i := range strings.Split(v["files"], ",") {
link := fh.PublicUrl + `/u/` + i
w.Write([]byte(`<p>` + html.EscapeString(i) + `</p><a href="` + html.EscapeString(link) + `">` + `<img width=500em src="` + html.EscapeString(link) + `"></img></a>`))
}
w.Write([]byte(`</body></html>`))
}
func (fh FileholeServer) UploadHandler(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, fh.UploadLimit) // Make sure we don't fuck up and read too much
multipReader, err := r.MultipartReader()
var UploadProperties struct {
MimeType *mimetype.MIME
Filename string
TempFile string
Expiry int64
UrlLen int64
}
// Our defaults
UploadProperties.Expiry = 86400
UploadProperties.UrlLen = 24
parts := 0
shouldUpload := false
for {
parts += 1
if parts > 55 {
log.Debug().Err(err).Msg("too many parts in multipart form")
http.Error(w, "too many parts in multipart form", http.StatusBadRequest)
return
}
if p, err := multipReader.NextPart(); errors.Is(err, io.EOF) {
log.Debug().Msg("iterated all parts successfully")
break
} else if err != nil {
log.Debug().Err(err).Msg("error in getting next part of multipart")
break
} else {
log.Debug().Str("filename", p.FileName()).Str("formname", p.FormName()).Msg("multipReader next")
switch p.FormName() {
case "url_len":
if urlLenBytes, err := io.ReadAll(io.LimitReader(p, 55)); err != nil {
log.Debug().Err(err).Msg("Error reading url_len bytes")
break
} else {
// url_len sanitize
inpUrlLen := string(urlLenBytes)
UploadProperties.UrlLen, err = strconv.ParseInt(inpUrlLen, 10, 64)
if err != nil {
UploadProperties.UrlLen = 24
}
if UploadProperties.UrlLen < 5 || UploadProperties.UrlLen > 236 {
w.Write([]byte("url_len needs to be between 5 and 236\n"))
return
}
}
case "expiry":
if expiryBytes, err := io.ReadAll(io.LimitReader(p, 55)); err != nil {
log.Debug().Err(err).Msg("Error reading expiry bytes")
break
} else {
inpExpiry := string(expiryBytes)
UploadProperties.Expiry, err = strconv.ParseInt(inpExpiry, 10, 64)
if err != nil {
UploadProperties.Expiry = 86400
}
if UploadProperties.Expiry < 5 || UploadProperties.Expiry > 432000 {
w.Write([]byte("expiry needs to be between 5 and 432000\n"))
return
}
}
case "file":
fuckYou := make([]byte, 512)
n, err := p.Read(fuckYou)
if n < 512 {
// really small file, don't make an error, but don't allow it to read into the uninitialized part of the buffer
fuckYou = fuckYou[0:n]
} else if err != nil {
http.Error(w, "error detecting the mime type of your file", http.StatusInternalServerError)
return
}
UploadProperties.MimeType = mimetype.Detect(fuckYou)
log.Info().Stringer("mtype", UploadProperties.MimeType).Msg("Detected mime type")
tempFile, err := os.CreateTemp(fh.BufferDir, "")
if err != nil {
log.Debug().Err(err).Msg("failed to create temp file for buffering upload")
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
UploadProperties.TempFile = tempFile.Name()
if n, err := io.Copy(tempFile, bytes.NewReader(fuckYou)); err != nil {
log.Debug().Str("tempFile", tempFile.Name()).Int64("n", n).Msg("failed to copy mime portion of file to disk")
http.Error(w, "internal server error", http.StatusInternalServerError)
}
if n, err := io.Copy(tempFile, p); err != nil {
log.Debug().Str("tempFile", tempFile.Name()).Int64("n", n).Msg("failed to copy rest of file to disk")
}
shouldUpload = true
default:
break
}
}
}
if shouldUpload {
name := shortID(UploadProperties.UrlLen) + UploadProperties.MimeType.Extension()
os.Rename(UploadProperties.TempFile, fh.StorageDir+"/"+name)
if err = db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("expiry"))
return b.Put([]byte(name), []byte(strconv.FormatInt(time.Now().Unix()+UploadProperties.Expiry, 10)))
}); err != nil {
log.Error().Err(err).Msg("Failed to put expiry")
}
w.Write([]byte(fh.PublicUrl + "/u/" + name + "\n"))
}
}
//go:embed index.html
var indexPage []byte
type FileholeServer struct {
Bind string
MetadataFile string
StorageDir string
BufferDir string
PublicUrl string
SiteName string
Debug bool
UploadLimit int64
}
func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
getEnv := func(key string, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
fh := FileholeServer{}
flag.StringVar(&fh.Bind, "bind", getEnv("FH_BIND", "127.0.0.1:8000"), "Address to bind ENV: FH_BIND")
flag.StringVar(&fh.MetadataFile, "metadata-path", getEnv("FH_METADATA_FILE", "./filehole.db"), "File metadata storage KV store filename ENV: FH_METADATA_FILE")
flag.StringVar(&fh.StorageDir, "storage-dir", getEnv("FH_STORAGE_DIR", "./data"), "Data storage folder ENV: FH_STORAGE_DIR")
flag.StringVar(&fh.BufferDir, "buffer-dir", getEnv("FH_BUFFER_DIR", "./buffer"), "Buffer folder for uploads ENV: FH_STORAGE_DIR")
flag.StringVar(&fh.PublicUrl, "public-url", getEnv("FH_PUBLIC_URL", "https://filehole.org"), "Internet facing URL of the base of the site ENV: FH_PUBLIC_URL")
flag.StringVar(&fh.SiteName, "site-name", getEnv("FH_SITE_NAME", "Filehole"), "User facing website branding ENV: FH_SITE_NAME")
fh.Debug = os.Getenv("FH_DEBUG") != ""
flag.BoolVar(&fh.Debug, "debug", fh.Debug, "Enable debug logging for development ENV: FH_DEBUG")
const DEFAULT_UPLOAD_LIMIT = 1024 * 1024 * 1024
if env_fh_upload_limit, exists := os.LookupEnv("FH_UPLOAD_LIMIT"); exists {
var err error
if fh.UploadLimit, err = strconv.ParseInt(env_fh_upload_limit, 10, 64); err != nil {
log.Error().Err(err).Msg("Could not parse FH_UPLOAD_LIMIT as a uint64. Defaulting to 1GiB.")
fh.UploadLimit = DEFAULT_UPLOAD_LIMIT
}
} else {
fh.UploadLimit = DEFAULT_UPLOAD_LIMIT
}
flag.Int64Var(&fh.UploadLimit, "upload-limit", fh.UploadLimit, "Max allowed size for a HTTP request in bytes ENV: FH_UPLOAD_LIMIT")
flag.Parse()
if fh.Debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
log.Warn().Msg("Debug logging is enabled")
}
var err error
db, err = bolt.Open(fh.MetadataFile, 0600, nil)
if err != nil {
log.Fatal().Err(err).Msg("dangerous database activity")
}
db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte("expiry"))
if err != nil {
log.Fatal().Err(err).Msg("Error creating expiry bucket")
return err
}
return nil
})
// Directories should already exist, we will try to make them
if err := os.Mkdir(fh.StorageDir, os.ModePerm); !errors.Is(err, os.ErrExist) {
log.Fatal().Err(err).Msg("Failed to create storage directory")
}
if err := os.Mkdir(fh.BufferDir, os.ModePerm); !errors.Is(err, os.ErrExist) {
log.Fatal().Err(err).Msg("Failed to create buffer directory")
}
// We actually need to landlock after creating all the files we reference
// in the landlock or it will fail
err = landlock.V2.BestEffort().RestrictPaths(
landlock.RWDirs(fh.StorageDir),
landlock.RWFiles(fh.MetadataFile),
)
if err != nil {
log.Error().Err(err).Msg("Could not landlock")
}
// Test if landlock actually works on whatever fucked kernel you're
// probably using
_, err = os.Open("/etc/passwd")
if err == nil {
log.Error().Msg("Landlock failed, could open /etc/passwd")
} else {
log.Info().Err(err).Msg("Landlocked")
}
r := mux.NewRouter()
// Serve multiple images in a gallery
r.HandleFunc("/g/{files}", fh.GalleryHandler)
// Serve files from data dir statically
r.PathPrefix("/u/").Handler(http.StripPrefix("/u/", NoDirectoryList(http.FileServer(http.Dir(fh.StorageDir)))))
r.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
t, _ := template.New("index").Parse(string(indexPage))
t.Execute(w, map[string]interface{}{
"PublicUrl": fh.PublicUrl,
"SiteName": fh.SiteName,
"Debug": fh.Debug,
})
}).Methods("GET")
r.HandleFunc("/", fh.UploadHandler).Methods("POST")
http.Handle("/", r)
go ExpiryDoer()
http.ListenAndServe(fh.Bind, r)
db.Close()
}