diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..868ea14 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +* +!__build/indieauth-server diff --git a/.forgejo/workflows/workflow.yaml b/.forgejo/workflows/workflow.yaml index 511e196..bf38dc8 100644 --- a/.forgejo/workflows/workflow.yaml +++ b/.forgejo/workflows/workflow.yaml @@ -28,6 +28,10 @@ jobs: uses: https://codeflow.dananglin.me.uk/actions/mage-ci@main with: target: gosec + - name: Run go vet + uses: https://codeflow.dananglin.me.uk/actions/mage-ci@main + with: + target: govet style: name: Style diff --git a/.gitignore b/.gitignore index e200850..c724971 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /__build/* !__build/.gitkeep +/.environment/* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c71907a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM gcr.io/distroless/static-debian12 + +COPY ./__build/indieauth-server /indieauth-server + +ENTRYPOINT ["/indieauth-server"] diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..8684b00 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,38 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" +) + +type Config struct { + BindAddress string `json:"bindAddress"` + Port int32 `json:"port"` + Domain string `json:"domain"` +} + +func NewConfig(path string) (Config, error) { + path = filepath.Clean(path) + + data, err := os.ReadFile(path) + if err != nil { + return Config{}, fmt.Errorf( + "unable to read the config from %q: %w", + path, + err, + ) + } + + var cfg Config + + if err := json.Unmarshal(data, &cfg); err != nil { + return Config{}, fmt.Errorf( + "unable to decode the JSON data: %w", + err, + ) + } + + return cfg, nil +} diff --git a/internal/executors/serve.go b/internal/executors/serve.go index f4cac2e..1d373f6 100644 --- a/internal/executors/serve.go +++ b/internal/executors/serve.go @@ -3,18 +3,15 @@ package executors import ( "flag" "fmt" - "log/slog" - "net/http" - "time" - "codeflow.dananglin.me.uk/apollo/indieauth-server/internal/info" - "codeflow.dananglin.me.uk/apollo/indieauth-server/internal/router" + "codeflow.dananglin.me.uk/apollo/indieauth-server/internal/config" + "codeflow.dananglin.me.uk/apollo/indieauth-server/internal/server" ) type serveExecutor struct { *flag.FlagSet - address string + configFile string } func executeServeCommand(args []string) error { @@ -24,7 +21,7 @@ func executeServeCommand(args []string) error { FlagSet: flag.NewFlagSet(executorName, flag.ExitOnError), } - executor.StringVar(&executor.address, "address", "0.0.0.0:8080", "The address that the server will listen on") + executor.StringVar(&executor.configFile, "config", "", "The path to the config file") if err := executor.Parse(args); err != nil { return fmt.Errorf("(%s) flag parsing error: %w", executorName, err) @@ -38,18 +35,12 @@ func executeServeCommand(args []string) error { } func (e *serveExecutor) execute() error { - server := http.Server{ - Addr: e.address, - Handler: router.NewServeMux(), - ReadHeaderTimeout: 1 * time.Second, - } - - slog.Info(info.ApplicationName+" is listening for web requests", "address", e.address) - - err := server.ListenAndServe() + cfg, err := config.NewConfig(e.configFile) if err != nil { - return fmt.Errorf("error running the server: %w", err) + return fmt.Errorf("unable to load the config: %w", err) } - return nil + srv := server.NewServer(cfg) + + return srv.Serve() } diff --git a/internal/router/router.go b/internal/router/router.go deleted file mode 100644 index 0c2cae6..0000000 --- a/internal/router/router.go +++ /dev/null @@ -1,9 +0,0 @@ -package router - -import "net/http" - -func NewServeMux() *http.ServeMux { - mux := http.NewServeMux() - - return mux -} diff --git a/internal/server/responses.go b/internal/server/responses.go new file mode 100644 index 0000000..8341792 --- /dev/null +++ b/internal/server/responses.go @@ -0,0 +1,22 @@ +package server + +import ( + "encoding/json" + "log/slog" + "net/http" +) + +func sendJSONResponse(w http.ResponseWriter, statusCode int, payload any) { + data, err := json.Marshal(payload) + if err != nil { + slog.Error("Error marshalling the response to JSON", "error", err.Error()) + + w.WriteHeader(http.StatusInternalServerError) + + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + _, _ = w.Write(data) +} diff --git a/internal/server/router.go b/internal/server/router.go new file mode 100644 index 0000000..28366ea --- /dev/null +++ b/internal/server/router.go @@ -0,0 +1,36 @@ +package server + +import ( + "fmt" + "net/http" + + "codeflow.dananglin.me.uk/apollo/indieauth-server/internal/config" +) + +func newMux(cfg config.Config) *http.ServeMux { + mux := http.NewServeMux() + + mux.HandleFunc("GET /.well-known/oauth-authorization-server", metadataHandleFunc(cfg.Domain)) + + return mux +} + +func metadataHandleFunc(domain string) http.HandlerFunc { + return func(writer http.ResponseWriter, _ *http.Request) { + metadata := struct { + Issuer string `json:"issuer"` + AuthorizationEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + ServiceDocumentation string `json:"service_documentation"` + CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"` + }{ + Issuer: fmt.Sprintf("https://%s/", domain), + AuthorizationEndpoint: fmt.Sprintf("https://%s/auth", domain), + TokenEndpoint: fmt.Sprintf("https://%s/token", domain), + ServiceDocumentation: "https://indieauth.spec.indieweb.org", + CodeChallengeMethodsSupported: []string{"S256"}, + } + + sendJSONResponse(writer, http.StatusOK, metadata) + } +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..aa9d7c6 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,41 @@ +package server + +import ( + "fmt" + "log/slog" + "net/http" + "time" + + "codeflow.dananglin.me.uk/apollo/indieauth-server/internal/config" + "codeflow.dananglin.me.uk/apollo/indieauth-server/internal/info" +) + +type Server struct { + httpServer *http.Server +} + +func NewServer(cfg config.Config) *Server { + address := fmt.Sprintf("%s:%d", cfg.BindAddress, cfg.Port) + + httpServer := http.Server{ + Addr: address, + ReadHeaderTimeout: 1 * time.Second, + Handler: newMux(cfg), + } + + server := Server{ + httpServer: &httpServer, + } + + return &server +} + +func (s *Server) Serve() error { + slog.Info(info.ApplicationName+" is now ready to serve web requests", "address", s.httpServer.Addr) + + if err := s.httpServer.ListenAndServe(); err != nil { + return fmt.Errorf("error running the server: %w", err) + } + + return nil +} diff --git a/magefiles/mage.go b/magefiles/mage.go index cdd1d29..4bbc0e6 100644 --- a/magefiles/mage.go +++ b/magefiles/mage.go @@ -55,10 +55,12 @@ func Lint() error { return sh.RunV("golangci-lint", "run", "--color", "always") } +// Gosec runs gosec against the code. func Gosec() error { return sh.RunV("gosec", "./...") } +// Staticcheck runs staticcheck against the code. func Staticcheck() error { return sh.RunV("staticcheck", "./...") } @@ -90,6 +92,11 @@ func Gofmt() error { return nil } +// Govet runs go vet against the code. +func Govet() error { + return sh.RunV("go", "vet", "./...") +} + // Build build the executable. // To rebuild packages that are already up-to-date set PROJECT_BUILD_REBUILD_ALL=1 // To enable verbose mode set PROJECT_BUILD_VERBOSE=1