implemented rate limiting

This commit is contained in:
Maximilian Wagner
2025-12-28 00:31:39 +01:00
parent 2c05415ed1
commit 03c0e3ca77
5 changed files with 74 additions and 41 deletions

View File

@@ -1,6 +1,7 @@
package backend package backend
import ( import (
"embed"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -13,7 +14,6 @@ import (
"slices" "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"
@@ -22,9 +22,15 @@ import (
//go:embed default_config.json //go:embed default_config.json
var defaultConfig embed.FS var defaultConfig embed.FS
// There are no errors if I can't see them
func Must[T any](object T, err error) T {
return object
}
// 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 := Must(time.LoadLocation(globals.AppConfig.Timezone))
return time.Now().In(timezone) return time.Now().In(timezone)
} }
@@ -57,7 +63,7 @@ func LogLine(message string, level int) {
globals.LogFile.WriteString(logline) globals.LogFile.WriteString(logline)
} }
// Logs client info. Configured log level sets the verbosity. // Logs client info. Configured log level sets the verbosity. Logs only up to level INFO
// //
// Bad Request if err != nil // Bad Request if err != nil
func VerifyLogRequest(request *http.Request) (err error) { func VerifyLogRequest(request *http.Request) (err error) {
@@ -66,16 +72,16 @@ func VerifyLogRequest(request *http.Request) (err error) {
if client.Proxied && globals.AppConfig.LogLevelNumeric > 1 { if client.Proxied && globals.AppConfig.LogLevelNumeric > 1 {
LogLine(fmt.Sprintf( LogLine(fmt.Sprintf(
"Request: %s via %s - %s %s", "httpR: %s via %s - %s %s",
client.ClientIP, client.ClientIP,
client.Proxy, client.Proxy,
client.RequestType, client.RequestType,
client.RequestPath, client.RequestPath,
), ),
4, 2,
) )
} else { } else {
LogLine(fmt.Sprintf("Request: %s - %s %s", client.ClientIP, client.RequestType, client.RequestPath), 4) LogLine(fmt.Sprintf("httpR: %s - %s %s", client.ClientIP, client.RequestType, client.RequestPath), 2)
} }
return nil return nil
@@ -84,7 +90,7 @@ func VerifyLogRequest(request *http.Request) (err error) {
// Writes a configuration file populated with defaults // Writes a configuration file populated with defaults
func createConfigWithDefaults(path string) error { func createConfigWithDefaults(path string) error {
config, _ := defaultConfig.ReadFile("default_config.json") config := Must(defaultConfig.ReadFile("default_config.json"))
err := os.WriteFile(path, config, 0644) err := os.WriteFile(path, config, 0644)
return err return err
} }
@@ -122,7 +128,7 @@ func configReaderParse(filePath string, configOptionStorage *globals.Config) err
// populates the appconfig with default values // populates the appconfig with default values
func configPopulator(configOptionStorage *globals.Config) { func configPopulator(configOptionStorage *globals.Config) {
jsonData, _ := defaultConfig.ReadFile("default_config.json") jsonData := Must(defaultConfig.ReadFile("default_config.json"))
json.Unmarshal(jsonData, &configOptionStorage) json.Unmarshal(jsonData, &configOptionStorage)
} }
@@ -175,7 +181,6 @@ func watchLoop(watcher *fsnotify.Watcher, callOnChange func() error) {
defer watcher.Close() defer watcher.Close()
for { for {
select { select {
// Errors
case err, ok := <-watcher.Errors: case err, ok := <-watcher.Errors:
if !ok { if !ok {
LogLine(fmt.Sprintf("Watcher crashed on error, manual compile only: %s", err), 4) LogLine(fmt.Sprintf("Watcher crashed on error, manual compile only: %s", err), 4)
@@ -183,7 +188,6 @@ func watchLoop(watcher *fsnotify.Watcher, callOnChange func() error) {
} }
LogLine(fmt.Sprintf("Watcher Error: %s", err), 3) LogLine(fmt.Sprintf("Watcher Error: %s", err), 3)
// Events
case event, ok := <-watcher.Events: case event, ok := <-watcher.Events:
if !ok { if !ok {
LogLine(fmt.Sprintf("Watcher crashed on event, manual compile only: %s", event), 4) LogLine(fmt.Sprintf("Watcher crashed on event, manual compile only: %s", event), 4)
@@ -204,6 +208,7 @@ func watchLoop(watcher *fsnotify.Watcher, callOnChange func() error) {
} }
// Returns a list of all .tex files recursively found in the passed directory
func texFinder(path string) []string { func texFinder(path string) []string {
var texFiles []string var texFiles []string
err := filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error { err := filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
@@ -216,7 +221,7 @@ func texFinder(path string) []string {
return nil return nil
}) })
if err != nil { if err != nil {
LogLine("Some paths could not be traversed while search for .tex files", 3) LogLine(fmt.Sprintf("Some paths could not be traversed while search for .tex files: %s", err), 3)
} }
return texFiles return texFiles
} }
@@ -249,19 +254,40 @@ func updateExecutionTimestamp(execution *globals.LatexExecution) {
execution.TimestampRFC = GetLocalTimeRFC() execution.TimestampRFC = GetLocalTimeRFC()
} }
// Returns the necessary time to sleep for rate limiting.
//
// Requires an RFC3339 timestamp. A dog will die if anything else is passed.
func rateLimitTime(timestampRFC string) (sleepTime time.Duration) {
lastExeTime := Must(time.Parse(time.RFC3339, timestampRFC))
rateLimitInterval := Must(time.ParseDuration(fmt.Sprintf("%ds", globals.AppConfig.RateLimitSeconds)))
return time.Until(lastExeTime.Add(rateLimitInterval)).Round(time.Second)
}
// Intended to be run as goroutine // Intended to be run as goroutine
func LatexCompile() error { func LatexCompile() error {
execution := &globals.LatexExec execution := &globals.LatexExec
config := globals.AppConfig config := globals.AppConfig
// check for already running compilation
if execution.ExecutionLock.TryLock() { if execution.ExecutionLock.TryLock() {
defer execution.ExecutionLock.Unlock() defer execution.ExecutionLock.Unlock()
} else { } else {
LogLine("LaTeX execution already underway", 2) LogLine("LaTeX: Already running", 2)
return errors.New("Execution already in progress") return errors.New("Execution already in progress")
} }
LogLine("LaTeX execution started", 2) // rate limit or start right away
rateLimit := rateLimitTime(execution.TimestampRFC)
if rateLimit.Seconds() > 0 {
LogLine(fmt.Sprintf("LaTeX: Rate limit: %d seconds", int(rateLimit.Seconds())), 3)
execution.ExecutionState = globals.LatexExecutionStateLimit
time.Sleep(rateLimit)
} else {
LogLine("LaTeX: Started", 3)
execution.ExecutionState = globals.LatexExecutionStateRunning
}
// build command
var latexCommand *exec.Cmd var latexCommand *exec.Cmd
switch config.LatexEngine { switch config.LatexEngine {
case "lualatex": case "lualatex":
@@ -277,10 +303,11 @@ func LatexCompile() error {
execution.Output = []byte{} execution.Output = []byte{}
updateExecutionTimestamp(execution) updateExecutionTimestamp(execution)
LogLine("Unsupported LaTeX Engine", 4) LogLine("LaTeX: Unsupported engine", 4)
return errors.New("Unsupported LaTeX Engine") return errors.New("Unsupported LaTeX Engine")
} }
// execute command
stdout, err := latexCommand.Output() stdout, err := latexCommand.Output()
execution.Output = stdout execution.Output = stdout
if err == nil { if err == nil {
@@ -290,7 +317,7 @@ func LatexCompile() error {
} }
updateExecutionTimestamp(execution) updateExecutionTimestamp(execution)
LogLine("LaTeX execution finished", 2) LogLine("LaTeX: Finished", 3)
return err return err
} }
@@ -317,7 +344,7 @@ func isTrustedProxy(proxy string, trustedProxies []string) (trusted bool, usedPr
// resolve trustedProxies in case a DNS name was configured // resolve trustedProxies in case a DNS name was configured
for _, trustedProxyDNS := range trustedProxies { for _, trustedProxyDNS := range trustedProxies {
// get IPs of this dns name // get IPs of this dns name
trustedProxyIPs, err := net.LookupAddr(trustedProxyDNS) trustedProxyIPs, err := net.LookupHost(trustedProxyDNS)
if err != nil { continue } if err != nil { continue }
// set usedProxy to DNS name if proxy is trusted // set usedProxy to DNS name if proxy is trusted

View File

@@ -6,6 +6,7 @@
"latexEngine": "lualatex", "latexEngine": "lualatex",
"latexSourceFilePath": "./latex/Example.tex", "latexSourceFilePath": "./latex/Example.tex",
"latexOutputPath": "./latex/output", "latexOutputPath": "./latex/output",
"rateLimitSeconds": 20,
"webserverDomain": "localhost", "webserverDomain": "localhost",
"webserverPort": "8080", "webserverPort": "8080",

View File

@@ -1,11 +1,12 @@
{ {
"timezone": "Europe/Berlin", "timezone": "Europe/Berlin",
"logFilePath": "./servtex.log", "logFilePath": "./servtex.log",
"logLevel": "debug", "logLevel": "info",
"latexEngine": "lualatex", "latexEngine": "lualatex",
"latexSourceFilePath": "./testfiles/Example.tex", "latexSourceFilePath": "./testfiles/Example.tex",
"latexOutputPath": "./testfiles/output", "latexOutputPath": "./testfiles/output",
"rateLimitSeconds": 0,
"webserverDomain": "localhost", "webserverDomain": "localhost",
"webserverPort": "8080", "webserverPort": "8080",

View File

@@ -95,8 +95,9 @@ func SSEventHandler(writer http.ResponseWriter, request *http.Request) {
ssePing(&writer) ssePing(&writer)
} else { } else {
sseStatusSend(&writer) sseStatusSend(&writer)
if globals.LatexExec.ExecutionState != globals.LatexExecutionStateRunning {
sseOutputSend(&writer) sseOutputSend(&writer)
// let client keep current pdf, if compile failed }
if globals.LatexExec.ExecutionState != globals.LatexExecutionStateFailure { if globals.LatexExec.ExecutionState != globals.LatexExecutionStateFailure {
ssePDFSend(&writer) ssePDFSend(&writer)
} }

View File

@@ -18,6 +18,7 @@ type Config struct {
LatexEngine string `json:"latexEngine"` LatexEngine string `json:"latexEngine"`
LatexSourceFilePath string `json:"latexSourceFilePath"` LatexSourceFilePath string `json:"latexSourceFilePath"`
LatexOutputPath string `json:"latexOutputPath"` LatexOutputPath string `json:"latexOutputPath"`
RateLimitSeconds int `json:"rateLimitSeconds"`
// webserver // webserver
WebserverDomain string `json:"webserverDomain"` WebserverDomain string `json:"webserverDomain"`
@@ -40,6 +41,8 @@ type LatexExecution struct {
var LatexExec LatexExecution var LatexExec LatexExecution
var LatexExecutionStateSuccess = "Success" var LatexExecutionStateSuccess = "Success"
var LatexExecutionStateFailure = "Failure" var LatexExecutionStateFailure = "Failure"
var LatexExecutionStateRunning = "Running"
var LatexExecutionStateLimit = "Running (rate limited)"
// Creates a copy of a LatexExecution (without the Mutex) // Creates a copy of a LatexExecution (without the Mutex)
func (execution *LatexExecution) Copy() (exeCopy *LatexExecution) { func (execution *LatexExecution) Copy() (exeCopy *LatexExecution) {