implemented rate limiting
This commit is contained in:
@@ -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,35 +181,34 @@ 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)
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
LogLine(fmt.Sprintf("Watcher Error: %s", err), 3)
|
|
||||||
|
|
||||||
// Events
|
|
||||||
case event, ok := <-watcher.Events:
|
|
||||||
if !ok {
|
|
||||||
LogLine(fmt.Sprintf("Watcher crashed on event, manual compile only: %s", event), 4)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
LogLine(fmt.Sprintf("File change noticed: %s", event.Name), 2)
|
|
||||||
|
|
||||||
err := callOnChange()
|
|
||||||
if err != nil {
|
|
||||||
LogLine(fmt.Sprintf("Latex compilation failed: %s", err), 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = watcher.Add(event.Name); err != nil {
|
|
||||||
LogLine(fmt.Sprintf("File could not be re-added to watcher: %s", event.Name), 4)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
LogLine(fmt.Sprintf("Watcher Error: %s", err), 3)
|
||||||
|
|
||||||
|
case event, ok := <-watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
LogLine(fmt.Sprintf("Watcher crashed on event, manual compile only: %s", event), 4)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
LogLine(fmt.Sprintf("File change noticed: %s", event.Name), 2)
|
||||||
|
|
||||||
|
err := callOnChange()
|
||||||
|
if err != nil {
|
||||||
|
LogLine(fmt.Sprintf("Latex compilation failed: %s", err), 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = watcher.Add(event.Name); err != nil {
|
||||||
|
LogLine(fmt.Sprintf("File could not be re-added to watcher: %s", event.Name), 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -95,8 +95,9 @@ func SSEventHandler(writer http.ResponseWriter, request *http.Request) {
|
|||||||
ssePing(&writer)
|
ssePing(&writer)
|
||||||
} else {
|
} else {
|
||||||
sseStatusSend(&writer)
|
sseStatusSend(&writer)
|
||||||
sseOutputSend(&writer)
|
if globals.LatexExec.ExecutionState != globals.LatexExecutionStateRunning {
|
||||||
// let client keep current pdf, if compile failed
|
sseOutputSend(&writer)
|
||||||
|
}
|
||||||
if globals.LatexExec.ExecutionState != globals.LatexExecutionStateFailure {
|
if globals.LatexExec.ExecutionState != globals.LatexExecutionStateFailure {
|
||||||
ssePDFSend(&writer)
|
ssePDFSend(&writer)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user