package contented import ( "embed" "encoding/json" "io/fs" "log" "net/http" "os" "regexp" "strings" "time" "github.com/mxk/go-flowrate/flowrate" bolt "go.etcd.io/bbolt" ) //go:embed static var staticAssets embed.FS var SERVER_HEADER string = `contented/0.0.0-dev` const DEFAULT_MAX_CONCURRENT_THUMBS = 16 type ServerPublicProperties struct { AppTitle string MaxUploadBytes int64 CanonicalBaseURL string } type ServerOptions struct { DataDirectory string DBPath string DiskFilesWorldReadable bool BandwidthLimit int64 TrustXForwardedFor bool EnableHomepage bool MaxConcurrentThumbs int ServerPublicProperties } func (this *ServerOptions) FileMode() os.FileMode { if this.DiskFilesWorldReadable { return 0644 } else { return 0600 } } type Server struct { opts ServerOptions db *bolt.DB startTime time.Time thumbnailSem chan struct{} metadataBucket []byte staticDir fs.FS // interface } func NewServer(opts *ServerOptions) (*Server, error) { s := &Server{ opts: *opts, metadataBucket: []byte(`METADATA`), startTime: time.Now(), } if s.opts.MaxConcurrentThumbs <= 0 { s.opts.MaxConcurrentThumbs = DEFAULT_MAX_CONCURRENT_THUMBS // default log.Printf("Allowing %d concurrent thumbnails", s.opts.MaxConcurrentThumbs) } s.staticDir, _ = fs.Sub(staticAssets, `static`) // can't fail // "fill" the thumbnailer semaphore s.thumbnailSem = make(chan struct{}, s.opts.MaxConcurrentThumbs) for i := 0; i < s.opts.MaxConcurrentThumbs; i += 1 { s.thumbnailSem <- struct{}{} } b, err := bolt.Open(opts.DBPath, 0644, bolt.DefaultOptions) if err != nil { return nil, err } err = b.Update(func(tx *bolt.Tx) error { _, err := tx.CreateBucketIfNotExists(s.metadataBucket) return err }) if err != nil { return nil, err } s.db = b return s, nil } func (this *Server) serveJsonObject(w http.ResponseWriter, o interface{}) { jb, err := json.Marshal(o) if err != nil { log.Println(err.Error()) http.Error(w, "Internal error", 500) return } w.Header().Set(`Content-Type`, `application/json`) w.WriteHeader(200) w.Write(jb) } func (this *Server) handleAbout(w http.ResponseWriter) { this.serveJsonObject(w, this.opts.ServerPublicProperties) } func (this *Server) remoteIP(r *http.Request) string { if this.opts.TrustXForwardedFor { if xff := r.Header.Get("X-Forwarded-For"); len(xff) > 0 { return xff } } return strings.TrimRight(strings.TrimRight(r.RemoteAddr, "0123456789"), ":") } const ( downloadUrlPrefix = `/get/` metadataUrlPrefix = `/info/` previewUrlPrefix = `/p/` ) var rxThumbUrl = regexp.MustCompile(`^/thumb/(.)/(.*)$`) func (this *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set(`Server`, SERVER_HEADER) w.Header().Set(`Access-Control-Allow-Origin`, `*`) // Blanket allow CORS if this.opts.MaxUploadBytes > 0 { r.Body = http.MaxBytesReader(w, r.Body, this.opts.MaxUploadBytes) } if this.opts.BandwidthLimit > 0 { r.Body = flowrate.NewReader(r.Body, this.opts.BandwidthLimit) } // if r.Method == "GET" && strings.HasPrefix(r.URL.Path, downloadUrlPrefix) { this.handleView(w, r, r.URL.Path[len(downloadUrlPrefix):]) } else if r.Method == "GET" && strings.HasPrefix(r.URL.Path, metadataUrlPrefix) { this.handleInformation(w, r.URL.Path[len(metadataUrlPrefix):]) } else if r.Method == "GET" && strings.HasPrefix(r.URL.Path, previewUrlPrefix) { this.handlePreview(w, r.URL.Path[len(previewUrlPrefix):]) } else if r.Method == "GET" && rxThumbUrl.MatchString(r.URL.Path) { parts := rxThumbUrl.FindStringSubmatch(r.URL.Path) this.handleThumb(w, r, parts[1][0], parts[2]) } else if r.Method == "GET" && r.URL.Path == `/about` { this.handleAbout(w) } else if r.Method == "POST" && r.URL.Path == `/upload` { this.handleUpload(w, r) } else if r.Method == "OPTIONS" { // Blanket allow (headers already set) w.WriteHeader(200) } else if r.Method == "GET" { // Conditionally block homepage access if !this.opts.EnableHomepage && (r.URL.Path == `/index.html` || r.URL.Path == `/`) { http.Error(w, "Not found", 404) return } // Serve static html/css/js assets // http.FileServer transparently redirects index.html->/ internally http.FileServer(http.FS(this.staticDir)).ServeHTTP(w, r) } else { http.Error(w, "Not found", 404) } }