diff --git a/build/images.go b/build/images.go index 90cc519..09caf66 100644 --- a/build/images.go +++ b/build/images.go @@ -27,6 +27,13 @@ import ( "github.com/fatih/color" ) +/** + * Builds a Containerfile for a given language. + * + * @param string lang + * @param string setup + * @return string Containerfile content + */ func ContainerFile(lang, setup string) string { header := "FROM docker.io/alpine:latest" prefix := "RUN apk update --no-cache && apk upgrade --no-cache && apk add --no-cache libc-dev musl-dev " @@ -41,16 +48,17 @@ func ContainerFile(lang, setup string) string { return fmt.Sprintf("%s\n%s%s && %s", header, prefix, setup, suffix) } -func Cleanup(tempFile string) { - if err := os.Remove("TEMP_CONTAINERFILE"); err != nil { - color.Red("Error removing temporary file: %v", err) - os.Exit(1) - } -} - +/** + * Builds images for all languages. + */ func BuildImages() { tempFile := "TEMP_CONTAINERFILE" - defer Cleanup(tempFile) + defer func() { + if err := os.Remove(tempFile); err != nil { + color.Red("Error removing temporary file: %v", err) + os.Exit(1) + } + }() var builds Builds diff --git a/config/load.go b/config/load.go index 9049d94..43ea999 100644 --- a/config/load.go +++ b/config/load.go @@ -23,6 +23,12 @@ import ( "github.com/charmbracelet/log" ) +/** + * Loads the configuration from the given path. + * + * @param path string Path to the configuration file + * @return *Config Configuration object + */ func LoadConfig(path string) *Config { var config Config if _, err := toml.DecodeFile(path, &config); err != nil { @@ -31,6 +37,12 @@ func LoadConfig(path string) *Config { return &config } +/** + * Loads the language map from the given path. + * + * @param path string Path to the language map file + * @return *server.LangMap Language map object + */ func LoadLangs(path string) *server.LangMap { var langs server.LangMap if _, err := toml.DecodeFile(path, &langs); err != nil { diff --git a/control/auth.go b/control/auth.go index ceaab79..f5a2f5e 100644 --- a/control/auth.go +++ b/control/auth.go @@ -25,6 +25,14 @@ import ( "golang.org/x/crypto/argon2" ) +/** + * Checks if the user key is valid. Runs argon2id on + * requests until a valid request is made, then caches + * the key. + * + * @param userKey string User key + * @param serverKey []string Server key + */ func (ks *KeyStore) CheckKey(userKey string, serverKey []string) bool { if cachedKey, ok := ks.cachedKey.Load().(string); ok && userKey == cachedKey { return true @@ -39,6 +47,13 @@ func (ks *KeyStore) CheckKey(userKey string, serverKey []string) bool { return false } +/** + * Initializes the key store. + * + * @param keyFile string Key file + * @return *KeyStore Key store + * @return []string Key and salt + */ func InitializeKeystore(keyFile string) (*KeyStore, []string) { file, err := os.ReadFile(keyFile) if err != nil { diff --git a/control/limit.go b/control/limit.go index a3781ce..60efa83 100644 --- a/control/limit.go +++ b/control/limit.go @@ -22,12 +22,26 @@ import ( "golang.org/x/time/rate" ) +/** + * Creates a new rate limiter. + * + * @return *RateLimiter Rate limiter object + */ func NewRateLimiter() *RateLimiter { return &RateLimiter{ clients: make(map[string]*Client), } } +/** + * Checks if a client should be rate limited or + * allowed to continue. + * + * @param ip string IP address of the client + * @param burst int Burst rate + * @param refill int Refill rate + * @return *rate.Limiter Rate limiter object + */ func (rl *RateLimiter) LimitClient(ip string, burst, refill int) *rate.Limiter { rl.mu.RLock() user, exists := rl.clients[ip] @@ -43,6 +57,10 @@ func (rl *RateLimiter) LimitClient(ip string, burst, refill int) *rate.Limiter { return user.limiter } +/** + * Starts a cleanup routine to remove old clients + * from the rate limiter. Run as a goroutine. + */ func (rl *RateLimiter) StartCleanup() { go func() { for { @@ -58,6 +76,16 @@ func (rl *RateLimiter) StartCleanup() { }() } +/** + * Checks if a client should be rate limited or + * allowed to continue. This is a wrapper around + * LimitClient and Allow. + * + * @param ip string IP address of the client + * @param burst int Burst rate + * @param refill int Refill rate + * @return bool True if the client should be rate limited + */ func (rl *RateLimiter) CheckClient(ip string, burst, refill int) bool { limiter := rl.LimitClient(ip, burst, refill) return limiter.Allow() diff --git a/main.go b/main.go index ccf68e5..48d912e 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,9 @@ import ( const VERSION = "1.6.0" +/** + * The entry point of the application. + */ func main() { logger := log.NewWithOptions(os.Stderr, log.Options{ ReportTimestamp: true, diff --git a/podman/podman.go b/podman/podman.go index 4851b69..06b1498 100644 --- a/podman/podman.go +++ b/podman/podman.go @@ -33,17 +33,43 @@ import ( "github.com/karlseguin/ccache/v3" ) +/** + * Creates a new LRU cache for caching exec results + * and a new Executor instance. + * + * @param timeout int Timeout for execution + * @param podmanPath string Path to podman executable + * @return *Executor New Executor instance + */ func NewExecutor(timeout int, podmanPath string) *Executor { cache := ccache.New(ccache.Configure[map[string]interface{}]().MaxSize(100).ItemsToPrune(10)) return &Executor{execCache: cache, timeout: timeout, podmanPath: podmanPath} } +/** + * Cleans up the temp directory. Called on SIGINT and + * SIGTERM. + */ func Cleanup() { if err := os.RemoveAll(filepath.Join(".", "run")); err != nil { log.Error("Could not clean up temp dir", "Error", err) } } +/** + * Runs the given code in a podman container. The code is + * dumped into a temp file, which is then mounted into the + * container. + * + * @param code string Code to run + * @param entry string Entry point for the container + * @param cArgs string Args for the interpreter or compiler + * @param ext string File extension of the code + * @param timeout int Timeout for execution + * @param enableCache bool Enable caching of exec results + * @return int HTTP status code + * @return map[string]interface{} Response body + */ func (ex *Executor) RunCode(code, entry, cArgs, ext string, timeout int, enableCache bool) (int, map[string]interface{}) { argsSlice := strings.Fields(cArgs) for i, arg := range argsSlice { diff --git a/routes/ping.go b/routes/ping.go index 300b9b7..66a3ce6 100644 --- a/routes/ping.go +++ b/routes/ping.go @@ -22,6 +22,12 @@ import ( "whipcode/server" ) +/** + * Ping endpoint for health checks. + * + * @param w http.ResponseWriter Response writer + * @param _ *http.Request Request object + */ func Ping(w http.ResponseWriter, _ *http.Request) { server.Send(w, http.StatusOK, []byte("pong"), "text/plain") } diff --git a/routes/run.go b/routes/run.go index 9df2a6d..8afca79 100644 --- a/routes/run.go +++ b/routes/run.go @@ -30,6 +30,13 @@ import ( "whipcode/server" ) +/** + * Helper function for accepting a string or int value. + * + * @param l *StrInt StrInt object + * @param b []byte Byte array + * @return error Error object + */ func (l *StrInt) UnmarshalJSON(b []byte) error { var intValue int if err := json.Unmarshal(b, &intValue); err == nil { @@ -46,6 +53,14 @@ func (l *StrInt) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &l.value) } +/** + * Run endpoint for running code in a container. This is + * the main endpoint for the application. + * Calls podman.RunCode + * + * @param w http.ResponseWriter Response writer + * @param r *http.Request Request object + */ func Run(w http.ResponseWriter, r *http.Request) { masterKey := r.Header.Get("X-Master-Key") diff --git a/server/middleware.go b/server/middleware.go index f3f843a..5125dd9 100644 --- a/server/middleware.go +++ b/server/middleware.go @@ -33,7 +33,20 @@ const ( ExecutorContextKey contextKey = "executor" ) +/** + * Middleware for the /run endpoint that caps the + * request body size and passes various parameters + * to the handler. + * + * @param f http.HandlerFunc Handler + * @param params ScopedMiddleWareParams Parameters + * @return http.HandlerFunc Handler + */ func ScopedMiddleWare(f http.HandlerFunc, params ScopedMiddleWareParams) http.HandlerFunc { + /** + * @param w http.ResponseWriter Response writer + * @param r *http.Request Request object + */ return func(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, int64(params.MaxBytesSize)) @@ -48,7 +61,19 @@ func ScopedMiddleWare(f http.HandlerFunc, params ScopedMiddleWareParams) http.Ha } } +/** + * Global middleware for all requests that performs + * rate limiting and host checks. + * + * @param handler http.Handler Handler + * @param params MiddleWareParams Parameters + * @return http.Handler Handler + */ func MiddleWare(handler http.Handler, params MiddleWareParams) http.Handler { + /** + * @param w http.ResponseWriter - Response writer + * @param r *http.Request - Request object + */ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { host, _, _ := net.SplitHostPort(r.RemoteAddr) details := fmt.Sprintf("%s %s %s", host, r.Method, r.URL) diff --git a/server/send.go b/server/send.go index 1289f8d..5309b4a 100644 --- a/server/send.go +++ b/server/send.go @@ -22,6 +22,15 @@ import ( "github.com/charmbracelet/log" ) +/** + * Send sends a response to the client. + * + * @param w http.ResponseWriter Response writer + * @param status int Status code to return + * @param message []byte Message to send + * @param contentType ...string Content type to + * send, defaults to application/json + */ func Send(w http.ResponseWriter, status int, message []byte, contentType ...string) { cType := "application/json" if len(contentType) > 0 { diff --git a/server/server.go b/server/server.go index 096aae9..5c3bb1d 100644 --- a/server/server.go +++ b/server/server.go @@ -24,6 +24,16 @@ import ( "github.com/charmbracelet/log" ) +/** + * Starts the server with the given port, handler, and + * TLS settings. + * + * @param port int Port to use + * @param handler http.Handler Handler to use + * @param enableTLS bool Whether to enable TLS + * @param tlsDir string Directory for the TLS files + * @param timeout int Configured execution timeout + */ func StartServer(port int, handler http.Handler, enableTLS bool, tlsDir string, timeout int) { log.Info("Starting whipcode", "Port", port, "TLS", enableTLS) diff --git a/utils/key.go b/utils/key.go index 94c49f8..d8c8a61 100644 --- a/utils/key.go +++ b/utils/key.go @@ -28,6 +28,12 @@ import ( "golang.org/x/crypto/argon2" ) +/** + * Generate a random string of a specified length. + * + * @param length int Length of the string + * @return string Random string + */ func RandomString(length int) string { bytes := make([]byte, length) if _, err := rand.Read(bytes); err != nil { @@ -37,6 +43,13 @@ func RandomString(length int) string { return hex.EncodeToString(bytes) } +/** + * Generate a form for the user to input the salt + * and key. + * + * @return string Salt + * @return string Key + */ func KeyForm() (string, string) { var salt string var key string @@ -65,6 +78,10 @@ func KeyForm() (string, string) { return strings.TrimSpace(salt), strings.TrimSpace(key) } +/** + * Generate a master key file with a salt and argon2 + * hash of the key. + */ func GenKey() { salt, key := KeyForm() diff --git a/utils/test.go b/utils/test.go index 855c13a..1821324 100644 --- a/utils/test.go +++ b/utils/test.go @@ -31,6 +31,13 @@ import ( "github.com/fatih/color" ) +/** + * Generate a form for the user to input the master + * key and port. + * + * @return string Master key + * @return string Port + */ func TestForm() (string, string) { var key string var port string @@ -76,6 +83,11 @@ func TestForm() (string, string) { return strings.TrimSpace(key), strings.TrimSpace(port) } +/** + * Run the self-test for the application. This + * will send a request for each language to the + * server with a test payload. + */ func SelfTest() { key, port := TestForm()