bluebell/serve/serve.go
2024-08-16 16:48:50 +01:00

97 lines
2.2 KiB
Go

package serve
import (
"context"
"github.com/1f349/bluebell/conf"
"github.com/1f349/bluebell/database"
"github.com/julienschmidt/httprouter"
"github.com/spf13/afero"
"io"
"net"
"net/http"
"os"
"path"
"path/filepath"
"strings"
)
var (
indexBranches = []string{
"main",
"master",
}
indexFiles = []func(p string) string{
func(p string) string { return path.Join(p, "index.html") },
func(p string) string { return p + ".html" },
func(p string) string { return p },
}
)
type sitesQueries interface {
GetSiteByDomain(ctx context.Context, domain string) (database.Site, error)
}
func New(storage afero.Fs, db sitesQueries, domain string) *Handler {
return &Handler{storage, db, domain}
}
type Handler struct {
storageFs afero.Fs
db sitesQueries
domain string
}
func (h *Handler) Handle(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
host, _, err := net.SplitHostPort(req.Host)
if err != nil {
http.Error(rw, "Bad Gateway", http.StatusBadGateway)
return
}
site, ok := strings.CutSuffix(host, "."+h.domain)
if !ok {
http.Error(rw, "Bad Gateway", http.StatusBadGateway)
return
}
site = conf.SlugFromDomain(site)
branch := req.URL.User.Username()
if branch == "" {
for _, i := range indexBranches {
if h.tryServePath(rw, site, i, req.URL.Path) {
return
}
}
} else if h.tryServePath(rw, site, branch, req.URL.Path) {
return
}
http.Error(rw, "404 Not Found", http.StatusNotFound)
}
func (h *Handler) tryServePath(rw http.ResponseWriter, site, branch, p string) bool {
for _, i := range indexFiles {
if h.tryServeFile(rw, site, branch, i(p)) {
return true
}
}
return false
}
func (h *Handler) tryServeFile(rw http.ResponseWriter, site, branch, p string) bool {
// prevent path traversal
if strings.Contains(site, "..") || strings.Contains(branch, "..") || strings.Contains(p, "..") {
http.Error(rw, "400 Bad Request", http.StatusBadRequest)
return true
}
open, err := h.storageFs.Open(filepath.Join(site, branch, p))
switch {
case err == nil:
rw.WriteHeader(http.StatusOK)
_, _ = io.Copy(rw, open)
case os.IsNotExist(err):
// check next path
return false
default:
http.Error(rw, "500 Internal Server Error", http.StatusInternalServerError)
}
return true
}