logging and client info parsing changes, added trusted proxies
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,6 +15,8 @@
|
||||
|
||||
# Custom Additions
|
||||
servtex
|
||||
config.json
|
||||
testfiles/tls
|
||||
*.aux
|
||||
*.log
|
||||
*.pdf
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
17
backend/default_config.json
Normal file
17
backend/default_config.json
Normal 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": []
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,4 @@
|
||||
(This is an Example Document)
|
||||
Filewatcher Test Document
|
||||
Filewatcher Test Document
|
||||
Filewatcher Test Document
|
||||
Filewatcher Test Document
|
||||
\end{document}
|
||||
|
||||
Reference in New Issue
Block a user