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
|
# Custom Additions
|
||||||
servtex
|
servtex
|
||||||
|
config.json
|
||||||
|
testfiles/tls
|
||||||
*.aux
|
*.aux
|
||||||
*.log
|
*.log
|
||||||
*.pdf
|
*.pdf
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
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",
|
"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": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user