logging and client info parsing changes, added trusted proxies

This commit is contained in:
Maximilian Wagner
2025-12-27 18:02:02 +01:00
parent 8dcf9fba11
commit ad3825c871
7 changed files with 211 additions and 32 deletions

2
.gitignore vendored
View File

@@ -15,6 +15,8 @@
# Custom Additions # Custom Additions
servtex servtex
config.json
testfiles/tls
*.aux *.aux
*.log *.log
*.pdf *.pdf

View File

@@ -4,16 +4,24 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"os"
"io/fs" "io/fs"
"net"
"net/http"
"os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"time" "time"
"embed"
"git.noctra.dev/noctra/servtex/globals" "git.noctra.dev/noctra/servtex/globals"
"github.com/fsnotify/fsnotify" "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 // Returns the current time in the timezone specified in the config file
func GetLocalTime() time.Time { func GetLocalTime() time.Time {
timezone, _ := time.LoadLocation(globals.AppConfig.Timezone) timezone, _ := time.LoadLocation(globals.AppConfig.Timezone)
@@ -49,6 +57,30 @@ func LogLine(message string, level int) {
globals.LogFile.WriteString(logline) 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 // Helper for configReader() that does the actual reading
// //
// Also validates the populated fields // Also validates the populated fields
@@ -56,11 +88,11 @@ func configReaderParse(filePath string, configOptionStorage *globals.Config) err
jsonData, err := os.ReadFile(filePath) jsonData, err := os.ReadFile(filePath)
if err == nil { if err == nil {
if err = json.Unmarshal(jsonData, &configOptionStorage); 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") return errors.New("Config file could not be read")
} }
} else { } 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") return errors.New("Config file does not exist")
} }
@@ -77,7 +109,7 @@ func configReaderParse(filePath string, configOptionStorage *globals.Config) err
configOptionStorage.LogLevelNumeric = 3 configOptionStorage.LogLevelNumeric = 3
LogLine("Defaulting to log level warning", 3) LogLine("Defaulting to log level warning", 3)
} }
return configValidator(*configOptionStorage) return nil
} }
// tbd what exactly happens here // tbd what exactly happens here
@@ -85,6 +117,14 @@ func configValidator(config globals.Config) error {
return nil 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 // Reads config file and stores the options in configOptionStorage
// //
// Tries the following paths // Tries the following paths
@@ -108,9 +148,15 @@ func ConfigReader(configFileName string, configOptionStorage *globals.Config) er
path := filepath.Join("~", ".config", "servtex", configFileName) path := filepath.Join("~", ".config", "servtex", configFileName)
err = configReaderParse(path, configOptionStorage) 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 // Contents copied from https://github.com/fsnotify/fsnotify/blob/main/cmd/fsnotify/watch.go
@@ -240,3 +286,88 @@ func LatexCompile() error {
return nil 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
}

View File

@@ -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": []
}

View File

@@ -1,6 +1,7 @@
{ {
"logLevel": "debug", "timezone": "Europe/Berlin"
"logFilePath": "./servtex.log", "logFilePath": "./servtex.log",
"logLevel": "debug",
"latexEngine": "lualatex", "latexEngine": "lualatex",
"latexSourceFilePath": "./testfiles/Example.tex", "latexSourceFilePath": "./testfiles/Example.tex",
@@ -12,5 +13,5 @@
"webserverPortSecure": "8443", "webserverPortSecure": "8443",
"certificatePath": "./testfiles/tls/testing.crt", "certificatePath": "./testfiles/tls/testing.crt",
"certificateKeyPath": "./testfiles/tls/testing.key", "certificateKeyPath": "./testfiles/tls/testing.key",
"timezone": "Europe/Berlin" "trustedProxies": []
} }

View File

@@ -70,30 +70,46 @@ func sseOutputSend(writer *http.ResponseWriter) {
backend.LogLine("Output Event has been sent", 1) backend.LogLine("Output Event has been sent", 1)
} }
// Server Side Event Handler // Server Side Event Handler
// //
// Sends a Ping instead of actual data when no new data available to save bandwidth // Sends a Ping instead of actual data when no new data available to save bandwidth
func SSEventHandler(writer http.ResponseWriter, request *http.Request) { 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("Content-Type", "text/event-stream")
writer.Header().Set("Cache-Control", "no-cache") writer.Header().Set("Cache-Control", "no-cache")
writer.Header().Set("Connection", "keep-alive") writer.Header().Set("Connection", "keep-alive")
ssePing(&writer) ssePing(&writer)
lastExecution := "startup"
lastExecution := ""
for range time.Tick(time.Second) { for range time.Tick(time.Second) {
select {
case <-request.Context().Done():
backend.LogLine("SSE Context Done", 1)
return
default:
if lastExecution == globals.LatexExec.TimestampRFC { if lastExecution == globals.LatexExec.TimestampRFC {
ssePing(&writer) ssePing(&writer)
} else { } else {
sseStatusSend(&writer) sseStatusSend(&writer)
ssePDFSend(&writer)
sseOutputSend(&writer) sseOutputSend(&writer)
// let client keep current pdf, if compile failed
if globals.LatexExec.ExecutionState != "Failed" {
ssePDFSend(&writer)
}
lastExecution = globals.LatexExec.TimestampRFC lastExecution = globals.LatexExec.TimestampRFC
} }
} }
}
} }
// Serves the compiled PDF file // Serves the compiled PDF file
func PDFHandler(writer http.ResponseWriter, request *http.Request) { 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") pdfPath := filepath.Join(globals.AppConfig.LatexOutputPath, "servtex.pdf")
pdf, err := os.Open(pdfPath) pdf, err := os.Open(pdfPath)
if err != nil { if err != nil {
@@ -109,12 +125,16 @@ func PDFHandler(writer http.ResponseWriter, request *http.Request) {
// Serves the main page of ServTeX // Serves the main page of ServTeX
func MainHandler(writer http.ResponseWriter, request *http.Request) { 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") writer.Header().Set("Content-Type", "text/html")
main, _ := WebFiles.ReadFile("templates/main.html") main, _ := WebFiles.ReadFile("templates/main.html")
writer.Write(main) writer.Write(main)
} }
func PDFCompile(writer http.ResponseWriter, request *http.Request) { func PDFCompile(writer http.ResponseWriter, request *http.Request) {
if err := backend.VerifyLogRequest(request); err != nil { http.Error(writer, "Bad Request", http.StatusBadRequest) }
backend.LatexCompile() backend.LatexCompile()
} }

View File

@@ -5,11 +5,14 @@ import (
"sync" "sync"
) )
var LogFile *os.File
type Config struct { type Config struct {
// general // general
Timezone string `json:"timezone"`
LogFilePath string `json:"logFilePath"`
LogLevel string `json:"logLevel"` LogLevel string `json:"logLevel"`
LogLevelNumeric int LogLevelNumeric int
LogFilePath string `json:"logFilePath"`
// latex // latex
LatexEngine string `json:"latexEngine"` LatexEngine string `json:"latexEngine"`
@@ -23,7 +26,7 @@ type Config struct {
WebserverPortSecure string `json:"webserverPortSecure"` WebserverPortSecure string `json:"webserverPortSecure"`
CertificatePath string `json:"certificatePath"` CertificatePath string `json:"certificatePath"`
CertificateKeyPath string `json:"certificateKeyPath"` CertificateKeyPath string `json:"certificateKeyPath"`
Timezone string `json:"timezone"` TrustedProxies []string `json:"trustedProxies"`
} }
var AppConfig Config var AppConfig Config
@@ -37,5 +40,12 @@ type LatexExecution struct {
} }
var LatexExec LatexExecution var LatexExec LatexExecution
var LogFile *os.File type ClientInfo struct {
ClientIP string
RequestType string
RequestPath string
Proxy string
Proxied bool
ProxyTrusted bool
}

View File

@@ -3,6 +3,4 @@
(This is an Example Document) (This is an Example Document)
Filewatcher Test Document Filewatcher Test Document
Filewatcher Test Document Filewatcher Test Document
Filewatcher Test Document
Filewatcher Test Document
\end{document} \end{document}