diff --git a/.github/workflows/action-push.yml b/.github/workflows/action-push.yml index f96f3d9..e506ea3 100644 --- a/.github/workflows/action-push.yml +++ b/.github/workflows/action-push.yml @@ -121,4 +121,12 @@ jobs: push: true tags: | ghcr.io/metalbear-co/mirrord-go-statfs:latest - + - name: go-server - build and push + uses: docker/build-push-action@v3 + with: + context: go-server + file: go-server/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ghcr.io/metalbear-co/mirrord-go-server:latest diff --git a/go-server/Dockerfile b/go-server/Dockerfile new file mode 100644 index 0000000..6c33435 --- /dev/null +++ b/go-server/Dockerfile @@ -0,0 +1,20 @@ +FROM golang:latest AS build + +WORKDIR /app + +COPY go.mod . +COPY go.sum . + +RUN go mod download + +COPY main.go . + +RUN CGO_ENABLED=0 go build main.go + +FROM alpine:latest AS run + +COPY --from=build /app /app + +WORKDIR /app + +CMD ["/app/main"] diff --git a/go-server/README.md b/go-server/README.md new file mode 100644 index 0000000..0229e3f --- /dev/null +++ b/go-server/README.md @@ -0,0 +1,11 @@ +Simple HTTP/HTTPS server written in Go. + +Always responds with 200 OK. + +# Configuration: +* `SERVER_MESSAGE` - message to respond with. Optional, defaults to `Hello from remote!`. +* `SERVER_MODE` - either `HTTP` or `HTTPS`. Optional, defaults to `HTTP`. +* `SERVER_PORT` - port to listen on. Optional, defaults to `80` in the `HTTP` mode and `443` in the `HTTPS` mode. +* `TLS_SERVER_CERT` - path to a PEM file containing certificate chain to use for server authentication (required in the `HTTPS` mode). +* `TLS_SERVER_KEY` - path to a PEM file containing private key to use with the certificate chain from `TLS_SERVER_CERT` (required in the `HTTPS` mode). +* `TLS_CLIENT_ROOTS` - path to a PEM file containing certificates to use as trusted roots for client authentication. Optional, if mode is `HTTPS` and this variable is not provided, the server will not offer client authentication. diff --git a/go-server/go.mod b/go-server/go.mod new file mode 100644 index 0000000..9bea86b --- /dev/null +++ b/go-server/go.mod @@ -0,0 +1,5 @@ +module go-server + +go 1.23.4 + +require github.com/sethvargo/go-envconfig v1.1.1 diff --git a/go-server/go.sum b/go-server/go.sum new file mode 100644 index 0000000..6e84ba1 --- /dev/null +++ b/go-server/go.sum @@ -0,0 +1,2 @@ +github.com/sethvargo/go-envconfig v1.1.1 h1:JDu8Q9baIzJf47NPkzhIB6aLYL0vQ+pPypoYrejS9QY= +github.com/sethvargo/go-envconfig v1.1.1/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw= diff --git a/go-server/main.go b/go-server/main.go new file mode 100644 index 0000000..271b7dd --- /dev/null +++ b/go-server/main.go @@ -0,0 +1,139 @@ +package main + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "github.com/sethvargo/go-envconfig" + "log" + "net/http" + "os" +) + +type ServerConfig struct { + Message string `env:"SERVER_MESSAGE"` + Mode string `env:"SERVER_MODE"` + Port string `env:"SERVER_PORT"` + ServerCert string `env:"TLS_SERVER_CERT"` + ServerKey string `env:"TLS_SERVER_KEY"` + ClientRoots string `env:"TLS_CLIENT_ROOTS"` +} + +func makeTlsConfig(serverConfig *ServerConfig) *tls.Config { + if serverConfig.Mode == "HTTP" { + return nil + } + + cfg := &tls.Config{ + MinVersion: tls.VersionTLS12, + CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + }, + NextProtos: []string{"h2", "http/1.1", "http/1.0"}, + } + + if serverConfig.ClientRoots != "" { + cfg.ClientAuth = tls.RequireAndVerifyClientCert + cfg.ClientCAs = loadClientCerts(serverConfig.ClientRoots) + } else { + cfg.ClientAuth = tls.NoClientCert + } + + return cfg +} + +func loadClientCerts(path string) *x509.CertPool { + certPool := x509.NewCertPool() + + rawPem, err := os.ReadFile(path) + if err != nil { + log.Fatalf("Error reading client allowed roots (%s): %v\n", path, err) + } + + var added uint = 0 + for { + var certDERBlock *pem.Block + certDERBlock, rawPem = pem.Decode(rawPem) + if certDERBlock == nil { + break + } + cert, err := x509.ParseCertificate(certDERBlock.Bytes) + if err != nil { + log.Fatalf("Error parsing X509 certificate (%s): %v\n", path, err) + } + + certPool.AddCert(cert) + added += 1 + } + + log.Printf("Loaded client cert pool (%s): %v certificates\n", path, added) + + return certPool +} + +func main() { + ctx := context.Background() + + var c ServerConfig + if err := envconfig.Process(ctx, &c); err != nil { + log.Fatalf("Failed to read configuration: %v\n", err) + } + + if c.Mode == "" { + c.Mode = "HTTP" + } else if c.Mode != "HTTP" && c.Mode != "HTTPS" { + log.Fatalf("Invalid mode: %s, expected either HTTP or HTTPS\n", c.Mode) + } + + if c.Message == "" { + c.Message = "Hello from remote!" + } + + if c.Port == "" { + if c.Mode == "HTTP" { + c.Port = "80" + } else { + c.Port = "443" + } + } + + log.Printf("Resolved server configuration: %+v\n", c) + + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + log.Printf("Got a %s %s request from %s\n", req.Proto, req.Method, req.RemoteAddr) + + if c.Mode == "HTTPS" { + w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains") + } + + w.Header().Add("Content-Type", "text/plain") + + _, _ = w.Write([]byte(c.Message)) + }) + + srv := &http.Server{ + Addr: fmt.Sprintf(":%s", c.Port), + Handler: mux, + TLSConfig: makeTlsConfig(&c), + } + + var err error + if c.Mode == "HTTP" { + err = srv.ListenAndServe() + } else { + err = srv.ListenAndServeTLS(c.ServerCert, c.ServerKey) + } + + if err != nil { + log.Fatalf("Server failed: %v\n", err) + } +}