Files
fnx_web/webcontroller/web_controller.go

143 lines
4.5 KiB
Go
Raw Normal View History

package webcontroller
import (
2018-06-23 21:17:53 +02:00
"errors"
2020-01-31 19:16:20 +01:00
"fmt"
2020-07-29 16:29:25 +02:00
"html/template"
"net/http"
2021-03-04 17:10:59 +01:00
"net/http/httputil"
"net/url"
2020-01-31 19:16:20 +01:00
"os"
2019-09-18 22:30:29 +02:00
"strings"
2021-11-16 15:20:15 +01:00
"fornaxian.tech/log"
2021-03-10 20:13:32 +01:00
"fornaxian.tech/pixeldrain_api_client/pixelapi"
"github.com/julienschmidt/httprouter"
)
2022-11-07 18:10:06 +01:00
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"`
2022-11-07 18:10:06 +01:00
}
2018-07-09 23:19:16 +02:00
// WebController controls how requests are handled and makes sure they have
// proper context when running
type WebController struct {
tpl *template.Template
config Config
2018-09-19 22:26:52 +02:00
2022-11-07 18:10:06 +01:00
// Server hostname, displayed in the footer of every web page
2020-01-31 19:16:20 +01:00
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
2021-03-10 20:13:32 +01:00
api pixelapi.PixelAPI
}
2018-07-09 23:19:16 +02:00
// 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))
}
2022-11-07 18:10:06 +01:00
wc = &WebController{
config: conf,
hostname: hostname,
api: pixelapi.New(conf.APIURLInternal),
}
2022-11-07 18:10:06 +01:00
if conf.APISocketPath != "" {
wc.api = wc.api.UnixSocketPath(conf.APISocketPath)
}
wc.tpl = wc.parseTemplate()
2020-01-31 19:16:20 +01:00
// Serve static files
2022-11-07 18:10:06 +01:00
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/main.js" {
w.Header().Set("Cache-Control", "no-cache")
} else {
w.Header().Set("Cache-Control", "public, max-age=31536000")
}
2019-02-18 14:17:31 +01:00
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)
2019-05-30 09:29:50 +02:00
// 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)
2019-05-30 09:29:50 +02:00
2022-11-07 18:10:06 +01:00
if conf.ProxyAPIRequests {
remoteURL, err := url.Parse(strings.TrimSuffix(conf.APIURLInternal, "/api"))
2021-03-04 17:10:59 +01:00
if err != nil {
2022-11-07 18:10:06 +01:00
panic(fmt.Errorf("failed to parse reverse proxy URL '%s': %w", conf.APIURLInternal, err))
2021-03-04 17:10:59 +01:00
}
2021-11-02 10:17:10 +01:00
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)
}
2021-03-04 17:10:59 +01:00
}
// 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))
2025-10-09 15:48:23 +02:00
})
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 {
2024-09-10 00:00:12 +02:00
http.Redirect(
w, r,
"https://"+hostname+r.URL.String(),
2024-09-10 00:00:12 +02:00
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)
}
}
2018-06-23 21:17:53 +02:00
func (wc *WebController) getAPIKey(r *http.Request) (key string, err error) {
2026-09-02 00:10:51 +02:00
if cookie, err := r.Cookie("nova_auth_key"); err == nil {
2021-11-02 10:33:07 +01:00
if len(cookie.Value) == 36 {
2018-06-23 21:17:53 +02:00
return cookie.Value, nil
}
}
2026-06-10 23:53:03 +02:00
return "", errors.New("not a valid Nova authentication cookie")
}