2018-06-17 16:15:58 +02:00
|
|
|
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"
|
2018-06-17 16:15:58 +02:00
|
|
|
"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"
|
2018-06-17 16:15:58 +02:00
|
|
|
|
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"
|
2018-06-17 16:15:58 +02:00
|
|
|
"github.com/julienschmidt/httprouter"
|
|
|
|
|
)
|
|
|
|
|
|
2022-11-07 18:10:06 +01:00
|
|
|
type Config struct {
|
2026-10-01 02:06:00 +02:00
|
|
|
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
|
2018-06-17 16:15:58 +02:00
|
|
|
type WebController struct {
|
2026-10-01 02:06:00 +02:00
|
|
|
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
|
|
|
|
|
|
2021-01-05 00:00:46 +01:00
|
|
|
// 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-06-17 16:15:58 +02:00
|
|
|
}
|
|
|
|
|
|
2018-07-09 23:19:16 +02:00
|
|
|
// New initializes a new WebController by registering all the request handlers
|
2026-10-01 02:06:00 +02:00
|
|
|
// 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))
|
2018-06-17 16:15:58 +02:00
|
|
|
}
|
2022-11-07 18:10:06 +01:00
|
|
|
|
2026-10-01 02:06:00 +02: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)
|
|
|
|
|
}
|
2026-10-01 02:06:00 +02:00
|
|
|
wc.tpl = wc.parseTemplate()
|
2020-01-31 19:16:20 +01:00
|
|
|
|
2018-06-17 16:15:58 +02:00
|
|
|
// Serve static files
|
2022-11-07 18:10:06 +01:00
|
|
|
var fs = http.FileServer(http.Dir(conf.ResourceDir + "/static"))
|
2021-11-29 16:13:19 +01:00
|
|
|
var resourceHandler = func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
2026-10-01 02:06:00 +02:00
|
|
|
// 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")
|
|
|
|
|
}
|
2019-02-18 14:17:31 +01:00
|
|
|
r.URL.Path = p.ByName("filepath")
|
|
|
|
|
fs.ServeHTTP(w, r)
|
2021-11-29 16:13:19 +01:00
|
|
|
}
|
2026-10-01 02:06:00 +02:00
|
|
|
r.HEAD("/res/*filepath", resourceHandler)
|
|
|
|
|
r.OPTIONS("/res/*filepath", resourceHandler)
|
|
|
|
|
r.GET("/res/*filepath", resourceHandler)
|
2019-05-30 09:29:50 +02:00
|
|
|
|
2026-10-01 02:06:00 +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)
|
|
|
|
|
}
|
2026-10-01 02:06:00 +02:00
|
|
|
for _, method := range []string{"OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE"} {
|
|
|
|
|
r.Handle(method, "/api/*p", proxyHandler)
|
|
|
|
|
}
|
2021-03-04 17:10:59 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-01 02:06:00 +02: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
|
|
|
})
|
2026-10-01 02:06:00 +02:00
|
|
|
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { page(w, r, nil) })
|
2018-06-17 16:15:58 +02:00
|
|
|
|
2026-10-01 02:06:00 +02:00
|
|
|
// Filesystem pages embed the requested node, for link previews
|
|
|
|
|
var directory = middleware(wc.serveDirectory)
|
|
|
|
|
r.GET("/d/*path", directory)
|
|
|
|
|
r.HEAD("/d/*path", directory)
|
2020-07-20 16:57:43 +02:00
|
|
|
|
2018-06-17 16:15:58 +02:00
|
|
|
return wc
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-04 21:14:55 +02:00
|
|
|
func middleware(handle httprouter.Handle) httprouter.Handle {
|
|
|
|
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
2023-09-14 16:59:46 +02:00
|
|
|
// Redirect the user to the correct domain
|
2025-09-24 15:37:57 +02:00
|
|
|
if hostname, found := strings.CutPrefix(r.Host, "www."); found {
|
2024-09-10 00:00:12 +02:00
|
|
|
http.Redirect(
|
|
|
|
|
w, r,
|
2025-09-24 15:37:57 +02:00
|
|
|
"https://"+hostname+r.URL.String(),
|
2024-09-10 00:00:12 +02:00
|
|
|
http.StatusMovedPermanently,
|
|
|
|
|
)
|
2023-09-14 16:59:46 +02:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-04 21:14:55 +02:00
|
|
|
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")
|
2018-06-17 16:15:58 +02:00
|
|
|
}
|