Files
fnx_web/webcontroller/web_controller.go

464 lines
17 KiB
Go
Raw Normal View History

package webcontroller
import (
2020-07-29 16:29:25 +02:00
"bytes"
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"
2020-02-04 19:37:46 +01:00
"time"
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"
"fornaxian.tech/util"
"github.com/julienschmidt/httprouter"
2020-07-29 16:29:25 +02:00
blackfriday "github.com/russross/blackfriday/v2"
)
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"`
SessionCookieDomain string `toml:"session_cookie_domain"`
ResourceDir string `toml:"resource_dir"`
DebugMode bool `toml:"debug_mode"`
ProxyAPIRequests bool `toml:"proxy_api_requests"`
MaintenanceMode bool `toml:"maintenance_mode"`
}
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 {
2019-12-23 23:56:57 +01:00
templates *TemplateManager
2022-11-07 18:10:06 +01:00
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
2018-09-19 22:26:52 +02:00
// page-specific variables
captchaSiteKey string
2020-02-04 19:37:46 +01:00
httpClient *http.Client
2020-02-21 14:23:29 +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-07-09 23:19:16 +02:00
// New initializes a new WebController by registering all the request handlers
// and parsing all templates in the resource directory
2022-11-07 18:10:06 +01:00
func New(r *httprouter.Router, prefix string, conf Config) (wc *WebController) {
2020-01-31 19:16:20 +01:00
var err error
2019-12-30 13:00:00 +01:00
wc = &WebController{
2022-11-07 18:10:06 +01:00
config: conf,
httpClient: &http.Client{Timeout: time.Minute * 10},
api: pixelapi.New(conf.APIURLInternal),
}
2022-11-07 18:10:06 +01:00
if conf.APISocketPath != "" {
wc.api = wc.api.UnixSocketPath(conf.APISocketPath)
}
wc.templates = NewTemplateManager(conf.ResourceDir, conf.APIURLExternal, conf.DebugMode)
wc.templates.ParseTemplates(false)
2020-01-31 19:16:20 +01:00
if wc.hostname, err = os.Hostname(); err != nil {
2021-06-29 12:08:31 +02:00
panic(fmt.Errorf("could not get hostname: %s", err))
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
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(prefix+"/res/*filepath", resourceHandler)
r.OPTIONS(prefix+"/res/*filepath", resourceHandler)
r.GET(prefix+"/res/*filepath", resourceHandler)
2019-05-30 09:29:50 +02:00
// Static assets
r.GET(prefix+"/favicon.ico" /* */, wc.serveFile("/favicon.ico"))
r.GET(prefix+"/robots.txt" /* */, wc.serveFile("/robots.txt"))
2019-05-30 09:29:50 +02:00
2022-11-07 18:10:06 +01:00
if conf.MaintenanceMode {
2019-05-30 09:29:50 +02:00
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
2024-09-24 00:12:41 +02:00
wc.templates.Run(w, r, "maintenance", wc.newTemplateData(w, r))
2019-05-30 09:29:50 +02:00
})
return wc
}
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)
2021-03-04 17:10:59 +01:00
prox.Transport = wc.httpClient.Transport
2021-11-02 10:17:10 +01:00
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)
}
r.Handle("OPTIONS", "/api/*p", proxyHandler)
r.Handle("POST", "/api/*p", proxyHandler)
r.Handle("GET", "/api/*p", proxyHandler)
r.Handle("PUT", "/api/*p", proxyHandler)
r.Handle("PATCH", "/api/*p", proxyHandler)
r.Handle("DELETE", "/api/*p", proxyHandler)
2021-03-04 17:10:59 +01:00
}
r.NotFound = http.HandlerFunc(wc.serveNotFound)
// Request method shorthands. These help keep the array of handlers aligned
2022-06-07 14:43:01 +02:00
const PST, GET = "POST", "GET"
// Loop over the handlers and register all of them in the router
for _, h := range []struct {
method string // HTTP request method this API handler uses
path string // The URL path this API will be registered on
handler httprouter.Handle // The function to run when this API is called
}{
// General navigation
2024-11-18 17:09:27 +01:00
{GET, "" /* */, wc.serveLandingPage()},
{GET, "home" /* */, wc.serveTemplate("home", handlerOpts{})},
2021-09-21 22:47:13 +02:00
{GET, "api" /* */, wc.serveMarkdown("api.md", handlerOpts{})},
2024-06-13 21:17:41 +02:00
{GET, "history" /* */, wc.serveTemplate("upload_history", handlerOpts{})},
2021-11-02 10:17:10 +01:00
{GET, "u/:id" /* */, wc.serveFileViewer},
{GET, "u/:id/preview" /* */, wc.serveFilePreview},
2021-11-02 10:17:10 +01:00
{GET, "l/:id" /* */, wc.serveListViewer},
2021-02-23 16:50:13 +01:00
{GET, "d/*path" /* */, wc.serveDirectory},
{GET, "t" /* */, wc.serveTemplate("text_upload", handlerOpts{})},
2021-03-09 17:00:43 +01:00
{GET, "donation" /* */, wc.serveMarkdown("donation.md", handlerOpts{})},
{GET, "widgets" /* */, wc.serveTemplate("widgets", handlerOpts{})},
{GET, "about" /* */, wc.serveMarkdown("about.md", handlerOpts{})},
{GET, "appearance" /* */, wc.serveTemplate("appearance", handlerOpts{})},
{GET, "hosting" /* */, wc.serveMarkdown("hosting.md", handlerOpts{})},
{GET, "acknowledgements" /**/, wc.serveMarkdown("acknowledgements.md", handlerOpts{})},
{GET, "business" /* */, wc.serveMarkdown("business.md", handlerOpts{})},
{GET, "limits" /* */, wc.serveMarkdown("limits.md", handlerOpts{})},
2023-01-30 11:58:46 +01:00
{GET, "abuse" /* */, wc.serveMarkdown("abuse.md", handlerOpts{})},
2024-02-06 17:14:40 +01:00
{GET, "filesystem" /* */, wc.serveMarkdown("filesystem.md", handlerOpts{})},
2024-03-06 18:35:40 +01:00
{GET, "100_gigabit_ethernet", wc.serveMarkdown("100_gigabit_ethernet.md", handlerOpts{NoExec: true})},
2021-03-09 17:00:43 +01:00
{GET, "apps" /* */, wc.serveTemplate("apps", handlerOpts{})},
2024-02-19 19:49:34 +01:00
{GET, "speedtest" /* */, wc.serveTemplate("speedtest", handlerOpts{})},
// User account pages
2021-03-09 17:00:43 +01:00
{GET, "register" /* */, wc.serveForm(wc.registerForm, handlerOpts{NoEmbed: true})},
{PST, "register" /* */, wc.serveForm(wc.registerForm, handlerOpts{NoEmbed: true})},
{GET, "login" /* */, wc.serveForm(wc.loginForm, handlerOpts{NoEmbed: true})},
{PST, "login" /* */, wc.serveForm(wc.loginForm, handlerOpts{NoEmbed: true})},
{GET, "password_reset" /* */, wc.serveForm(wc.passwordResetForm, handlerOpts{NoEmbed: true})},
{PST, "password_reset" /* */, wc.serveForm(wc.passwordResetForm, handlerOpts{NoEmbed: true})},
{GET, "logout" /* */, wc.serveTemplate("logout", handlerOpts{Auth: true, NoEmbed: true})},
2021-03-08 15:30:05 +01:00
{PST, "logout" /* */, wc.serveLogout},
2021-03-09 17:00:43 +01:00
{GET, "user/filemanager" /* */, wc.serveTemplate("file_manager", handlerOpts{Auth: true})},
2021-03-08 15:30:05 +01:00
{GET, "user/export/files" /**/, wc.serveUserExportFiles},
{GET, "user/export/lists" /**/, wc.serveUserExportLists},
// User account settings
2025-03-20 19:39:15 +01:00
{GET, "user" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true, NoEmbed: true})},
{GET, "user/home" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true, NoEmbed: true})},
{GET, "user/settings" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true, NoEmbed: true})},
{GET, "user/sharing" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true, NoEmbed: true})},
{GET, "user/sharing/*p" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true, NoEmbed: true})},
{GET, "user/api_keys" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true, NoEmbed: true})},
{GET, "user/activity" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true, NoEmbed: true})},
{GET, "user/connect_app" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true, NoEmbed: true})},
{GET, "user/transactions" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true, NoEmbed: true})},
{GET, "user/subscription" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true, NoEmbed: true})},
{GET, "user/prepaid" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true, NoEmbed: true})},
{GET, "user/prepaid/*p" /* */, wc.serveTemplate("user_home", handlerOpts{Auth: true, NoEmbed: true})},
{GET, "user/confirm_email" /* */, wc.serveEmailConfirm},
2021-03-09 17:00:43 +01:00
{GET, "user/password_reset_confirm" /**/, wc.serveForm(wc.passwordResetConfirmForm, handlerOpts{NoEmbed: true})},
{PST, "user/password_reset_confirm" /**/, wc.serveForm(wc.passwordResetConfirmForm, handlerOpts{NoEmbed: true})},
// Admin settings
{GET, "admin" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/status" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/block_files" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/email_reporters" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/abuse_reports" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/ip_bans" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
2024-06-14 17:21:31 +02:00
{GET, "admin/user_bans" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/user_management" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/mollie_settlements" /**/, wc.serveTemplate("admin", handlerOpts{Auth: true})},
2024-01-31 18:43:08 +01:00
{GET, "admin/paypal_taxes" /* */, wc.serveTemplate("admin", handlerOpts{Auth: true})},
{GET, "admin/globals" /* */, wc.serveForm(wc.adminGlobalsForm, handlerOpts{Auth: true})},
{PST, "admin/globals" /* */, wc.serveForm(wc.adminGlobalsForm, handlerOpts{Auth: true})},
2021-01-12 14:07:55 +01:00
// Misc
{GET, "misc/sharex/pixeldrain.com.sxcu", wc.serveShareXConfig},
2022-06-07 14:43:01 +02:00
{GET, "theme.css", wc.themeHandler},
} {
r.Handle(h.method, prefix+"/"+h.path, middleware(h.handler))
2024-09-24 00:12:41 +02:00
// Also support HEAD requests
if h.method == GET {
r.HEAD(prefix+"/"+h.path, middleware(h.handler))
}
}
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
2024-09-10 00:00:12 +02:00
if strings.HasPrefix(r.Host, "www.") {
http.Redirect(
w, r,
"https://"+strings.TrimPrefix(r.Host, "www.")+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)
}
}
2021-03-09 17:00:43 +01:00
type handlerOpts struct {
Auth bool
NoEmbed bool
2024-03-06 18:35:40 +01:00
NoExec bool
2021-03-09 17:00:43 +01:00
}
2024-11-18 17:09:27 +01:00
func (wc *WebController) serveLandingPage() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var td = wc.newTemplateData(w, r)
var template = "home"
// If the user is logged in, run user home template
if td.Authenticated {
template = "user_home"
}
if err := wc.templates.Run(w, r, template, td); err != nil && !util.IsNetError(err) {
log.Error("Error executing template '%s': %s", template, err)
}
}
}
2021-03-09 17:00:43 +01:00
func (wc *WebController) serveTemplate(tpl string, opts handlerOpts) httprouter.Handle {
2021-03-08 15:30:05 +01:00
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
2021-03-09 17:00:43 +01:00
if opts.NoEmbed {
w.Header().Set("X-Frame-Options", "DENY")
}
var td = wc.newTemplateData(w, r)
if opts.Auth && !td.Authenticated {
2018-07-09 21:41:17 +02:00
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
2024-09-24 00:12:41 +02:00
err := wc.templates.Run(w, r, tpl, td)
if err != nil && !util.IsNetError(err) {
log.Error("Error executing template '%s': %s", tpl, err)
}
}
}
2021-03-09 17:00:43 +01:00
func (wc *WebController) serveMarkdown(tpl string, opts handlerOpts) httprouter.Handle {
2020-07-29 16:29:25 +02:00
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var err error
2021-03-09 17:00:43 +01:00
if opts.NoEmbed {
w.Header().Set("X-Frame-Options", "DENY")
}
2020-07-29 16:29:25 +02:00
var tpld = wc.newTemplateData(w, r)
2021-03-09 17:00:43 +01:00
if opts.Auth && !tpld.Authenticated {
2020-07-29 16:29:25 +02:00
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Execute the raw markdown template and save the result in a buffer
var tplBuf bytes.Buffer
2024-09-24 00:12:41 +02:00
err = wc.templates.Run(&tplBuf, r, tpl, tpld)
if err != nil && !util.IsNetError(err) {
2020-07-29 16:29:25 +02:00
log.Error("Error executing template '%s': %s", tpl, err)
return
}
// Parse the markdown document and save the resulting HTML in a buffer
renderer := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
2024-06-27 18:46:37 +02:00
Flags: blackfriday.CommonHTMLFlags | blackfriday.TOC,
2020-07-29 16:29:25 +02:00
})
// We parse the markdown document, walk through the nodes. Extract the
// title of the document, and the rest of the nodes are rendered like
// normal
var mdBuf bytes.Buffer
2024-06-27 18:46:37 +02:00
2020-07-29 16:29:25 +02:00
blackfriday.New(
blackfriday.WithRenderer(renderer),
blackfriday.WithExtensions(blackfriday.CommonExtensions|blackfriday.AutoHeadingIDs),
2020-07-29 16:29:25 +02:00
).Parse(
tplBuf.Bytes(),
).Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
// Capture the title of the document so we can put it at the top of
// the template and in the metadata. When entering a h1 node the
2024-06-27 18:46:37 +02:00
// first child will be the title of the document. Save that value
2020-07-29 16:29:25 +02:00
if node.Type == blackfriday.Heading && node.HeadingData.Level == 1 {
2024-06-27 18:46:37 +02:00
tpld.Title = string(node.FirstChild.Literal)
return blackfriday.SkipChildren
2020-07-29 16:29:25 +02:00
}
2024-06-27 18:46:37 +02:00
// If this text node contains solely the text "[TOC]" then we render
// the table of contents
if node.Type == blackfriday.Text && bytes.Equal(node.Literal, []byte("[TOC]")) {
// Find the document node and render its TOC
for parent := node.Parent; ; parent = parent.Parent {
if parent.Type == blackfriday.Document {
renderer.RenderHeader(&mdBuf, parent)
return blackfriday.SkipChildren
}
}
2020-07-29 16:29:25 +02:00
}
return renderer.RenderNode(&mdBuf, node, entering)
})
// Pass the buffer's parsed contents to the wrapper template
tpld.Other = template.HTML(mdBuf.Bytes())
// Execute the wrapper template
2024-09-24 00:12:41 +02:00
err = wc.templates.Run(w, r, "markdown_wrapper", tpld)
if err != nil && !util.IsNetError(err) {
2020-07-29 16:29:25 +02:00
log.Error("Error executing template '%s': %s", tpl, err)
}
}
}
func (wc *WebController) serveFile(path string) httprouter.Handle {
return func(
w http.ResponseWriter,
r *http.Request,
p httprouter.Params,
) {
2022-11-07 18:10:06 +01:00
http.ServeFile(w, r, wc.config.ResourceDir+"/static"+path)
}
}
func (wc *WebController) serveForm(
2019-12-23 23:56:57 +01:00
handler func(*TemplateData, *http.Request) Form,
2021-03-09 17:00:43 +01:00
opts handlerOpts,
) httprouter.Handle {
return func(
w http.ResponseWriter,
r *http.Request,
p httprouter.Params,
) {
2021-03-09 17:00:43 +01:00
if opts.NoEmbed {
w.Header().Set("X-Frame-Options", "DENY")
}
var td = wc.newTemplateData(w, r)
2021-03-09 17:00:43 +01:00
if opts.Auth && !td.Authenticated {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// The handler retuns the form which will be rendered
td.Form = handler(td, r)
2020-07-31 21:21:14 +02:00
td.Title = td.Form.Title
2020-06-07 21:12:48 +02:00
td.Form.Username = td.User.Username
// Execute the extra actions if any
if td.Form.Extra.SetCookie != nil {
2020-02-05 11:56:08 +01:00
w.Header().Del("Set-Cookie")
http.SetCookie(w, td.Form.Extra.SetCookie)
}
if td.Form.Extra.RedirectTo != "" {
http.Redirect(w, r, td.Form.Extra.RedirectTo, http.StatusSeeOther)
log.Debug("redirect: %s", td.Form.Extra.RedirectTo)
return // Don't need to render a form if the user is redirected
}
// Remove the recaptcha field if captcha is disabled
if wc.captchaKey() == "none" {
for i, field := range td.Form.Fields {
2019-12-23 23:56:57 +01:00
if field.Type == FieldTypeCaptcha {
td.Form.Fields = append(
td.Form.Fields[:i],
td.Form.Fields[i+1:]...,
)
}
}
}
// Clear the entered values if the request was successful
if td.Form.SubmitSuccess {
w.WriteHeader(http.StatusOK)
for i, field := range td.Form.Fields {
field.EnteredValue = ""
td.Form.Fields[i] = field
}
2019-06-04 22:33:56 +02:00
} else if td.Form.Submitted {
w.WriteHeader(http.StatusBadRequest)
}
2024-09-24 00:12:41 +02:00
err := wc.templates.Run(w, r, "form_page", td)
if err != nil && !util.IsNetError(err) {
log.Error("Error executing form page: %s", err)
}
}
}
2023-05-19 21:45:42 +02:00
func (wc *WebController) serveForbidden(w http.ResponseWriter, r *http.Request) {
log.Debug("Forbidden: %s", r.URL)
w.WriteHeader(http.StatusForbidden)
2024-09-24 00:12:41 +02:00
wc.templates.Run(w, r, "403", wc.newTemplateData(w, r))
2023-05-19 21:45:42 +02:00
}
func (wc *WebController) serveNotFound(w http.ResponseWriter, r *http.Request) {
log.Debug("Not Found: %s", r.URL)
2018-10-04 23:36:34 +02:00
w.WriteHeader(http.StatusNotFound)
2024-09-24 00:12:41 +02:00
wc.templates.Run(w, r, "404", wc.newTemplateData(w, r))
2018-06-23 21:17:53 +02:00
}
2024-06-13 21:17:41 +02:00
func (wc *WebController) serveUnavailableForLegalReasons(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnavailableForLegalReasons)
2024-09-24 00:12:41 +02:00
wc.templates.Run(w, r, "451", wc.newTemplateData(w, r))
2024-06-13 21:17:41 +02:00
}
2018-06-23 21:17:53 +02:00
func (wc *WebController) getAPIKey(r *http.Request) (key string, err error) {
if cookie, err := r.Cookie("pd_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
}
}
return "", errors.New("not a valid pixeldrain authentication cookie")
}
2019-03-28 10:47:27 +01:00
func (wc *WebController) captchaKey() string {
// This only runs on the first request
if wc.captchaSiteKey == "" {
2021-03-10 20:13:32 +01:00
capt, err := wc.api.GetMiscRecaptcha()
2019-03-28 10:47:27 +01:00
if err != nil {
log.Error("Error getting recaptcha key: %s", err)
return ""
}
if capt.SiteKey == "" {
wc.captchaSiteKey = "none"
} else {
wc.captchaSiteKey = capt.SiteKey
}
}
return wc.captchaSiteKey
}