about summary refs log tree commit diff
path: root/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'main.go')
-rw-r--r--main.go98
1 files changed, 98 insertions, 0 deletions
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..2019888
--- /dev/null
+++ b/main.go
@@ -0,0 +1,98 @@
+// SPDX-FileCopyrightText: V <v@anomalous.eu>
+// SPDX-License-Identifier: OSL-3.0
+
+package main // import "go.anomalous.eu/cgiserver"
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"log"
+	"net/http"
+	"net/http/cgi"
+	"os"
+	"os/signal"
+	"syscall"
+
+	"github.com/coreos/go-systemd/v22/activation"
+	"github.com/gorilla/handlers"
+)
+
+func main() {
+	// The journal already has timestamps, so let's avoid adding our own.
+	log.SetFlags(0)
+
+	// The default usage message isn't very helpful, so we provide our own.
+	flag.Usage = func() {
+		fmt.Fprintln(flag.CommandLine.Output(), "Usage: cgiserver /path/to/application.cgi")
+	}
+
+	// We didn't configure any flags, so this just serves to display an error
+	// if someone tries to pass one. It also automatically handles -h/--help.
+	flag.Parse()
+
+	if flag.NArg() != 1 {
+		flag.Usage()
+		os.Exit(1)
+	}
+	path := flag.Arg(0)
+
+	// Socket activation makes our life very easy...
+	lns, err := activation.Listeners()
+	if err != nil {
+		log.Fatal(err)
+	}
+	if len(lns) == 0 {
+		log.Print("One or more listening sockets must be configured.")
+		log.Print("See systemd.socket(5) for more information.")
+		os.Exit(1)
+	}
+
+	var handler http.Handler
+
+	// ...and Go's standard library does literally all of the hard work.
+	handler = &cgi.Handler{
+		Path: path,
+		Env:  os.Environ(), // Environment variables aren't inherited by default.
+	}
+
+	// We want to resolve X-Forwarded-For, etc.
+	handler = handlers.ProxyHeaders(handler)
+
+	// Additionally, we want to log requests.
+	handler = handlers.CombinedLoggingHandler(os.Stdout, handler)
+
+	// Catch SIGTERM so we can shutdown gracefully.
+	sig := make(chan os.Signal, 1)
+	signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
+
+	// Create a Server so we can call Shutdown() on it later.
+	srv := &http.Server{Handler: handler}
+
+	doom := make(chan error)
+	for _, ln := range lns {
+		// Loop variables are unsafe to close over with goroutines,
+		// so this just shadows it with a fresh binding.
+		ln := ln
+
+		go func() {
+			// If any one of these return, it's game over.
+			doom <- srv.Serve(ln)
+		}()
+	}
+
+	select {
+	case err = <-doom:
+		// Only the first error matters, the rest will be ErrServerClosed.
+		log.Printf("Fatal server error: %v", err)
+	case <-sig:
+		log.Print("Caught signal, shutting down")
+	}
+
+	// This will block until all existing connections are handled.
+	srv.Shutdown(context.Background())
+
+	if err != nil {
+		os.Exit(1)
+	}
+}