From ad3825c871d677e38f5b91bc431ee74c012cac50 Mon Sep 17 00:00:00 2001 From: Maximilian Wagner Date: Sat, 27 Dec 2025 18:02:02 +0100 Subject: [PATCH] logging and client info parsing changes, added trusted proxies --- .gitignore | 2 + backend/backend.go | 143 ++++++++++++++++++++++++++++++++++-- backend/default_config.json | 17 +++++ config.json | 5 +- frontend/webserver.go | 38 +++++++--- globals/memory.go | 36 +++++---- testfiles/Example.tex | 2 - 7 files changed, 211 insertions(+), 32 deletions(-) create mode 100644 backend/default_config.json diff --git a/.gitignore b/.gitignore index 29df7c6..71ccd2d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,8 @@ # Custom Additions servtex +config.json +testfiles/tls *.aux *.log *.pdf diff --git a/backend/backend.go b/backend/backend.go index fd35b3f..bcff64b 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -4,16 +4,24 @@ import ( "encoding/json" "errors" "fmt" - "os" "io/fs" + "net" + "net/http" + "os" "os/exec" "path/filepath" + "slices" "strings" "time" + "embed" + "git.noctra.dev/noctra/servtex/globals" "github.com/fsnotify/fsnotify" ) +//go:embed default_config.json +var defaultConfig embed.FS + // Returns the current time in the timezone specified in the config file func GetLocalTime() time.Time { timezone, _ := time.LoadLocation(globals.AppConfig.Timezone) @@ -49,6 +57,30 @@ func LogLine(message string, level int) { globals.LogFile.WriteString(logline) } +// Logs client info. Configured log level sets the verbosity. +// +// Bad Request if err != nil +func VerifyLogRequest(request *http.Request) (err error) { + client, err := ProcessClientInfo(request) + if err != nil { return errors.New("Request Verification Failed") } + + if client.Proxied && globals.AppConfig.LogLevelNumeric > 1 { + LogLine(fmt.Sprintf( + "Request: %s via %s - %s %s", + client.ClientIP, + client.Proxy, + client.RequestType, + client.RequestPath, + ), + 4, + ) + } else { + LogLine(fmt.Sprintf("Request: %s - %s %s", client.ClientIP, client.RequestType, client.RequestPath), 4) + } + + return nil +} + // Helper for configReader() that does the actual reading // // Also validates the populated fields @@ -56,11 +88,11 @@ func configReaderParse(filePath string, configOptionStorage *globals.Config) err jsonData, err := os.ReadFile(filePath) if err == nil { if err = json.Unmarshal(jsonData, &configOptionStorage); err != nil { - LogLine("Configuration file is invaldi JSON", 5) + LogLine("Configuration file is invalid JSON", 5) return errors.New("Config file could not be read") } } else { - LogLine(fmt.Sprintf("Config at %s does not exist", filePath), 1) + LogLine("Configuration file does not exist", 1) return errors.New("Config file does not exist") } @@ -77,7 +109,7 @@ func configReaderParse(filePath string, configOptionStorage *globals.Config) err configOptionStorage.LogLevelNumeric = 3 LogLine("Defaulting to log level warning", 3) } - return configValidator(*configOptionStorage) + return nil } // tbd what exactly happens here @@ -85,6 +117,14 @@ func configValidator(config globals.Config) error { return nil } +func configPopulator(config globals.Config) error { + return nil +} + +// Creates the configuration file populated with defaults +func createConfigWithDefaults() { +} + // Reads config file and stores the options in configOptionStorage // // Tries the following paths @@ -108,9 +148,15 @@ func ConfigReader(configFileName string, configOptionStorage *globals.Config) er path := filepath.Join("~", ".config", "servtex", configFileName) err = configReaderParse(path, configOptionStorage) - if err == nil { return nil } + if err != nil { return err } - return err + err = configPopulator(*configOptionStorage) + if err != nil { return err } + + err = configValidator(*configOptionStorage) + if err != nil { return err } + + return nil } // Contents copied from https://github.com/fsnotify/fsnotify/blob/main/cmd/fsnotify/watch.go @@ -240,3 +286,88 @@ func LatexCompile() error { return nil } +// Returns intersection of two slices +func sliceIntersection[T comparable](left []T, right []T) (intersection []T) { + for _, leftItem := range left { + for _, rightItem := range right { + if leftItem == rightItem { + intersection = append(intersection, leftItem) + } + } + } + return intersection +} + +// Checks whether the proxy is trusted. Returns trusted status and the proxy. +// +// If X-Forwarded-For chain is passed, only the last in chain will be considered +func isTrustedProxy(proxy string, trustedProxies []string) (trusted bool, usedProxy string) { + // removes len()>0 checks + if proxy == "" { return false, "" } + // untrusted unless proven otherwise + trusted = false + + // get (last) used proxy (in chain) + if slices.Contains(trustedProxies, proxy) { + usedProxy = proxy + } else { + proxies := strings.Split(proxy, ",")[1:] + usedProxy = proxies[len(proxies)-1] + } + + // check if proxy is trusted + if slices.Contains(trustedProxies, usedProxy) { return true, usedProxy } + + // resolve trustedProxies in case a DNS name was configured + for _, trustedProxyDNS := range trustedProxies { + // get IPs of this dns name + trustedProxyIPs, err := net.LookupAddr(trustedProxyDNS) + if err != nil { continue } + + // set usedProxy to DNS name if proxy is trusted + if slices.Contains(trustedProxyIPs, usedProxy) { + trusted = true + usedProxy = trustedProxyDNS + return trusted, usedProxy + } + } + + return trusted, usedProxy +} + +// Returns client info of the request. Error indicates untrusted Proxy or unavailable client IP. +func ProcessClientInfo(request *http.Request) (client globals.ClientInfo, err error) { + client.ClientIP, _, err = net.SplitHostPort(request.RemoteAddr) + client.RequestType = request.Method + client.RequestPath = request.URL.Path + client.Proxy = "" + client.Proxied = false + client.ProxyTrusted = false + + trustedProxies := globals.AppConfig.TrustedProxies + + headerXRP := request.Header.Get("X-Real-IP") + if headerXRP != "" { + client.Proxied = true + client.ProxyTrusted, client.Proxy = isTrustedProxy(headerXRP, trustedProxies) + if !client.ProxyTrusted { + LogLine(fmt.Sprintf("Request sent via untrusted Proxy %s", client.Proxy), 4) + err = errors.New("Untrusted Proxy") + } + return client, err + } + + headerXFF := request.Header.Get("X-Forwarded-For") + if headerXFF != "" { + client.Proxied = true + client.ProxyTrusted, client.Proxy = isTrustedProxy(headerXRP, trustedProxies) + if !client.ProxyTrusted { + LogLine(fmt.Sprintf("Request sent via untrusted Proxy %s", client.Proxy), 4) + err = errors.New("Untrusted Proxy") + } + return client, err + } + + return client, err +} + diff --git a/backend/default_config.json b/backend/default_config.json new file mode 100644 index 0000000..63dbc2c --- /dev/null +++ b/backend/default_config.json @@ -0,0 +1,17 @@ +{ + "timezone": "Europe/Berlin" + "logFilePath": "./servtex.log", + "logLevel": "warning", + + "latexEngine": "lualatex", + "latexSourceFilePath": "./latex/Example.tex", + "latexOutputPath": "./latex/output", + + "webserverDomain": "localhost", + "webserverPort": "8080", + "webserverSecure": true, + "webserverPortSecure": "8443", + "certificatePath": "./tls/testing.crt", + "certificateKeyPath": "./tls/testing.key", + "trustedProxies": [] +} diff --git a/config.json b/config.json index d079421..edb0e71 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,7 @@ { - "logLevel": "debug", + "timezone": "Europe/Berlin" "logFilePath": "./servtex.log", + "logLevel": "debug", "latexEngine": "lualatex", "latexSourceFilePath": "./testfiles/Example.tex", @@ -12,5 +13,5 @@ "webserverPortSecure": "8443", "certificatePath": "./testfiles/tls/testing.crt", "certificateKeyPath": "./testfiles/tls/testing.key", - "timezone": "Europe/Berlin" + "trustedProxies": [] } diff --git a/frontend/webserver.go b/frontend/webserver.go index 3d2f3a3..23109b8 100644 --- a/frontend/webserver.go +++ b/frontend/webserver.go @@ -70,30 +70,46 @@ func sseOutputSend(writer *http.ResponseWriter) { backend.LogLine("Output Event has been sent", 1) } + + // Server Side Event Handler // // Sends a Ping instead of actual data when no new data available to save bandwidth func SSEventHandler(writer http.ResponseWriter, request *http.Request) { + if err := backend.VerifyLogRequest(request); err != nil { http.Error(writer, "Bad Request", http.StatusBadRequest) } + writer.Header().Set("Content-Type", "text/event-stream") writer.Header().Set("Cache-Control", "no-cache") writer.Header().Set("Connection", "keep-alive") ssePing(&writer) - lastExecution := "startup" + + lastExecution := "" for range time.Tick(time.Second) { - if lastExecution == globals.LatexExec.TimestampRFC { - ssePing(&writer) - } else { - sseStatusSend(&writer) - ssePDFSend(&writer) - sseOutputSend(&writer) - lastExecution = globals.LatexExec.TimestampRFC - } + select { + case <-request.Context().Done(): + backend.LogLine("SSE Context Done", 1) + return + default: + if lastExecution == globals.LatexExec.TimestampRFC { + ssePing(&writer) + } else { + sseStatusSend(&writer) + sseOutputSend(&writer) + // let client keep current pdf, if compile failed + if globals.LatexExec.ExecutionState != "Failed" { + ssePDFSend(&writer) + } + lastExecution = globals.LatexExec.TimestampRFC + } + } } } // Serves the compiled PDF file func PDFHandler(writer http.ResponseWriter, request *http.Request) { + if err := backend.VerifyLogRequest(request); err != nil { http.Error(writer, "Bad Request", http.StatusBadRequest) } + pdfPath := filepath.Join(globals.AppConfig.LatexOutputPath, "servtex.pdf") pdf, err := os.Open(pdfPath) if err != nil { @@ -109,12 +125,16 @@ func PDFHandler(writer http.ResponseWriter, request *http.Request) { // Serves the main page of ServTeX func MainHandler(writer http.ResponseWriter, request *http.Request) { + if err := backend.VerifyLogRequest(request); err != nil { http.Error(writer, "Bad Request", http.StatusBadRequest) } + writer.Header().Set("Content-Type", "text/html") main, _ := WebFiles.ReadFile("templates/main.html") writer.Write(main) } func PDFCompile(writer http.ResponseWriter, request *http.Request) { + if err := backend.VerifyLogRequest(request); err != nil { http.Error(writer, "Bad Request", http.StatusBadRequest) } + backend.LatexCompile() } diff --git a/globals/memory.go b/globals/memory.go index 7243df7..8d623b5 100644 --- a/globals/memory.go +++ b/globals/memory.go @@ -5,25 +5,28 @@ import ( "sync" ) +var LogFile *os.File + type Config struct { // general - LogLevel string `json:"logLevel"` + Timezone string `json:"timezone"` + LogFilePath string `json:"logFilePath"` + LogLevel string `json:"logLevel"` LogLevelNumeric int - LogFilePath string `json:"logFilePath"` // latex - LatexEngine string `json:"latexEngine"` - LatexSourceFilePath string `json:"latexSourceFilePath"` - LatexOutputPath string `json:"latexOutputPath"` + LatexEngine string `json:"latexEngine"` + LatexSourceFilePath string `json:"latexSourceFilePath"` + LatexOutputPath string `json:"latexOutputPath"` // webserver - WebserverDomain string `json:"webserverDomain"` - WebserverPort string `json:"webserverPort"` - WebserverSecure bool `json:"webserverSecure"` - WebserverPortSecure string `json:"webserverPortSecure"` - CertificatePath string `json:"certificatePath"` - CertificateKeyPath string `json:"certificateKeyPath"` - Timezone string `json:"timezone"` + WebserverDomain string `json:"webserverDomain"` + WebserverPort string `json:"webserverPort"` + WebserverSecure bool `json:"webserverSecure"` + WebserverPortSecure string `json:"webserverPortSecure"` + CertificatePath string `json:"certificatePath"` + CertificateKeyPath string `json:"certificateKeyPath"` + TrustedProxies []string `json:"trustedProxies"` } var AppConfig Config @@ -37,5 +40,12 @@ type LatexExecution struct { } var LatexExec LatexExecution -var LogFile *os.File +type ClientInfo struct { + ClientIP string + RequestType string + RequestPath string + Proxy string + Proxied bool + ProxyTrusted bool +} diff --git a/testfiles/Example.tex b/testfiles/Example.tex index 7f0cc2d..70e39ec 100644 --- a/testfiles/Example.tex +++ b/testfiles/Example.tex @@ -3,6 +3,4 @@ (This is an Example Document) Filewatcher Test Document Filewatcher Test Document -Filewatcher Test Document -Filewatcher Test Document \end{document}