Remove all legacy pixeldrain pages, handlers and assets. Remove old theming engine. Make logo responsive to the accent colour. Lots of other stuff.
143 lines
4.5 KiB
Go
143 lines
4.5 KiB
Go
package webcontroller
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
"fornaxian.tech/log"
|
|
"fornaxian.tech/pixeldrain_api_client/pixelapi"
|
|
"github.com/julienschmidt/httprouter"
|
|
)
|
|
|
|
type Config struct {
|
|
APIURLExternal string `toml:"api_url_external"`
|
|
APIURLInternal string `toml:"api_url_internal"`
|
|
APISocketPath string `toml:"api_socket_path"`
|
|
ResourceDir string `toml:"resource_dir"`
|
|
DebugMode bool `toml:"debug_mode"`
|
|
ProxyAPIRequests bool `toml:"proxy_api_requests"`
|
|
}
|
|
|
|
// WebController controls how requests are handled and makes sure they have
|
|
// proper context when running
|
|
type WebController struct {
|
|
tpl *template.Template
|
|
config Config
|
|
|
|
// Server hostname, displayed in the footer of every web page
|
|
hostname string
|
|
|
|
// API client to use for all requests. If the user is authenticated you
|
|
// should call Login() on this object. Calling Login will create a copy and
|
|
// not alter the original PixelAPI, but it will use the same HTTP Transport
|
|
api pixelapi.PixelAPI
|
|
}
|
|
|
|
// New initializes a new WebController by registering all the request handlers
|
|
// and parsing the page template
|
|
func New(r *httprouter.Router, conf Config) (wc *WebController) {
|
|
hostname, err := os.Hostname()
|
|
if err != nil {
|
|
panic(fmt.Errorf("could not get hostname: %s", err))
|
|
}
|
|
|
|
wc = &WebController{
|
|
config: conf,
|
|
hostname: hostname,
|
|
api: pixelapi.New(conf.APIURLInternal),
|
|
}
|
|
if conf.APISocketPath != "" {
|
|
wc.api = wc.api.UnixSocketPath(conf.APISocketPath)
|
|
}
|
|
wc.tpl = wc.parseTemplate()
|
|
|
|
// Serve static files
|
|
var fs = http.FileServer(http.Dir(conf.ResourceDir + "/static"))
|
|
var resourceHandler = func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
// Cache resources for a year. Except the Svelte entrypoint, it keeps its
|
|
// name when it changes. The chunks it imports have a hash in their name
|
|
if p.ByName("filepath") == "/svelte/wrap.js" {
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
} else {
|
|
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
|
}
|
|
r.URL.Path = p.ByName("filepath")
|
|
fs.ServeHTTP(w, r)
|
|
}
|
|
r.HEAD("/res/*filepath", resourceHandler)
|
|
r.OPTIONS("/res/*filepath", resourceHandler)
|
|
r.GET("/res/*filepath", resourceHandler)
|
|
|
|
// Browsers and crawlers look for these in the root
|
|
var rootFile = func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fs.ServeHTTP(w, r) }
|
|
r.GET("/favicon.ico", rootFile)
|
|
r.GET("/robots.txt", rootFile)
|
|
|
|
if conf.ProxyAPIRequests {
|
|
remoteURL, err := url.Parse(strings.TrimSuffix(conf.APIURLInternal, "/api"))
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to parse reverse proxy URL '%s': %w", conf.APIURLInternal, err))
|
|
}
|
|
|
|
log.Info("Starting API proxy to %s", remoteURL)
|
|
var prox = httputil.NewSingleHostReverseProxy(remoteURL)
|
|
var proxyHandler = func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
log.Info("Proxying request to %s", r.URL)
|
|
r.Host = remoteURL.Host
|
|
r.Header.Set("Origin", remoteURL.String())
|
|
prox.ServeHTTP(w, r)
|
|
}
|
|
for _, method := range []string{"OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE"} {
|
|
r.Handle(method, "/api/*p", proxyHandler)
|
|
}
|
|
}
|
|
|
|
// Every other path is a page of the app, which shows its own not found
|
|
// message for paths it doesn't know
|
|
var page = middleware(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
|
w.Header().Set("X-Frame-Options", "DENY")
|
|
wc.renderPage(w, r, wc.newTemplateData(r))
|
|
})
|
|
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { page(w, r, nil) })
|
|
|
|
// Filesystem pages embed the requested node, for link previews
|
|
var directory = middleware(wc.serveDirectory)
|
|
r.GET("/d/*path", directory)
|
|
r.HEAD("/d/*path", directory)
|
|
|
|
return wc
|
|
}
|
|
|
|
func middleware(handle httprouter.Handle) httprouter.Handle {
|
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
|
// Redirect the user to the correct domain
|
|
if hostname, found := strings.CutPrefix(r.Host, "www."); found {
|
|
http.Redirect(
|
|
w, r,
|
|
"https://"+hostname+r.URL.String(),
|
|
http.StatusMovedPermanently,
|
|
)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Strict-Transport-Security", "max-age=31536000")
|
|
w.Header().Set("X-Clacks-Overhead", "GNU Terry Pratchett")
|
|
handle(w, r, p)
|
|
}
|
|
}
|
|
|
|
func (wc *WebController) getAPIKey(r *http.Request) (key string, err error) {
|
|
if cookie, err := r.Cookie("nova_auth_key"); err == nil {
|
|
if len(cookie.Value) == 36 {
|
|
return cookie.Value, nil
|
|
}
|
|
}
|
|
return "", errors.New("not a valid Nova authentication cookie")
|
|
}
|