generated from templates/go-generic
feat: add metadata endpoint
Add the metadata endpoint so that clients can get the server's authorization and token endpoints. Changes: - Add a config object to allow administrators to configure the binding address, binding port and domain name. - Add a target in the magefiles and a step in the CI workflow to run go vet. - Add the Dockerfile. - Replace the --address flag with the --config flag. - Add the metadata endpoint to the router to return a JSON document containing the URLs to the metadata and token endpoints.
This commit is contained in:
parent
4fdad2d805
commit
7d0336d3db
11 changed files with 165 additions and 27 deletions
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!__build/indieauth-server
|
|
@ -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
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
/__build/*
|
||||
!__build/.gitkeep
|
||||
/.environment/*
|
||||
|
|
5
Dockerfile
Normal file
5
Dockerfile
Normal file
|
@ -0,0 +1,5 @@
|
|||
FROM gcr.io/distroless/static-debian12
|
||||
|
||||
COPY ./__build/indieauth-server /indieauth-server
|
||||
|
||||
ENTRYPOINT ["/indieauth-server"]
|
38
internal/config/config.go
Normal file
38
internal/config/config.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
package router
|
||||
|
||||
import "net/http"
|
||||
|
||||
func NewServeMux() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
return mux
|
||||
}
|
22
internal/server/responses.go
Normal file
22
internal/server/responses.go
Normal file
|
@ -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)
|
||||
}
|
36
internal/server/router.go
Normal file
36
internal/server/router.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
41
internal/server/server.go
Normal file
41
internal/server/server.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue