generated from templates/go-generic
feat: add metadata endpoint #2
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
|
uses: https://codeflow.dananglin.me.uk/actions/mage-ci@main
|
||||||
with:
|
with:
|
||||||
target: gosec
|
target: gosec
|
||||||
|
- name: Run go vet
|
||||||
|
uses: https://codeflow.dananglin.me.uk/actions/mage-ci@main
|
||||||
|
with:
|
||||||
|
target: govet
|
||||||
|
|
||||||
style:
|
style:
|
||||||
name: Style
|
name: Style
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
/__build/*
|
/__build/*
|
||||||
!__build/.gitkeep
|
!__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 (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"codeflow.dananglin.me.uk/apollo/indieauth-server/internal/info"
|
"codeflow.dananglin.me.uk/apollo/indieauth-server/internal/config"
|
||||||
"codeflow.dananglin.me.uk/apollo/indieauth-server/internal/router"
|
"codeflow.dananglin.me.uk/apollo/indieauth-server/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
type serveExecutor struct {
|
type serveExecutor struct {
|
||||||
*flag.FlagSet
|
*flag.FlagSet
|
||||||
|
|
||||||
address string
|
configFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeServeCommand(args []string) error {
|
func executeServeCommand(args []string) error {
|
||||||
|
@ -24,7 +21,7 @@ func executeServeCommand(args []string) error {
|
||||||
FlagSet: flag.NewFlagSet(executorName, flag.ExitOnError),
|
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 {
|
if err := executor.Parse(args); err != nil {
|
||||||
return fmt.Errorf("(%s) flag parsing error: %w", executorName, err)
|
return fmt.Errorf("(%s) flag parsing error: %w", executorName, err)
|
||||||
|
@ -38,18 +35,12 @@ func executeServeCommand(args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *serveExecutor) execute() error {
|
func (e *serveExecutor) execute() error {
|
||||||
server := http.Server{
|
cfg, err := config.NewConfig(e.configFile)
|
||||||
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()
|
|
||||||
if err != nil {
|
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")
|
return sh.RunV("golangci-lint", "run", "--color", "always")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gosec runs gosec against the code.
|
||||||
func Gosec() error {
|
func Gosec() error {
|
||||||
return sh.RunV("gosec", "./...")
|
return sh.RunV("gosec", "./...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Staticcheck runs staticcheck against the code.
|
||||||
func Staticcheck() error {
|
func Staticcheck() error {
|
||||||
return sh.RunV("staticcheck", "./...")
|
return sh.RunV("staticcheck", "./...")
|
||||||
}
|
}
|
||||||
|
@ -90,6 +92,11 @@ func Gofmt() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Govet runs go vet against the code.
|
||||||
|
func Govet() error {
|
||||||
|
return sh.RunV("go", "vet", "./...")
|
||||||
|
}
|
||||||
|
|
||||||
// Build build the executable.
|
// Build build the executable.
|
||||||
// To rebuild packages that are already up-to-date set PROJECT_BUILD_REBUILD_ALL=1
|
// To rebuild packages that are already up-to-date set PROJECT_BUILD_REBUILD_ALL=1
|
||||||
// To enable verbose mode set PROJECT_BUILD_VERBOSE=1
|
// To enable verbose mode set PROJECT_BUILD_VERBOSE=1
|
||||||
|
|
Loading…
Reference in a new issue