diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..60d8597 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +furthur: $(shell find . -type f -name "*.go") + go build ./cmd/furthur + \ No newline at end of file diff --git a/api/auth.go b/api/auth.go new file mode 100644 index 0000000..de27201 --- /dev/null +++ b/api/auth.go @@ -0,0 +1,18 @@ +package api + +import ( + "log/slog" + "net/http" +) + +func getLoginHandleFunc(sessions SessionProvider, log *slog.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // login html page + } +} + +func postLoginHandleFunc(sessions SessionProvider, log *slog.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // login html page + } +} diff --git a/api/common.go b/api/common.go new file mode 100644 index 0000000..10dda29 --- /dev/null +++ b/api/common.go @@ -0,0 +1,38 @@ +package api + +import ( + "log/slog" + "net/http" +) + +func versionHandleFunc(log *slog.Logger, version string) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + log.Debug("version handler") + w.WriteHeader(http.StatusOK) + w.Write([]byte(version)) + } +} + +func staticHandleFunc() func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + } +} + +func sessionMiddleware(sessions SessionProvider, log *slog.Logger) func(http.HandlerFunc) http.HandlerFunc { + return func(h http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := r.Cookie("FURTHUR_SESS") + if err != nil { + log.Warn("error loading session") + http.NotFound(w, r) + return + } + if !sessions.Valid(c.Value) { + log.Warn("user provided invalid session") + http.NotFound(w, r) + return + } + h(w, r) + }) + } +} diff --git a/api/link.go b/api/link.go new file mode 100644 index 0000000..f7ff1f9 --- /dev/null +++ b/api/link.go @@ -0,0 +1,10 @@ +package api + +import ( + "log/slog" + "net/http" +) + +func linkHandleFunc(links LinkProvider, log *slog.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) {} +} diff --git a/api/manage.go b/api/manage.go new file mode 100644 index 0000000..20edf4c --- /dev/null +++ b/api/manage.go @@ -0,0 +1,50 @@ +package api + +import ( + "log/slog" + "net/http" +) + +func getManageHandleFunc(log *slog.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func getLinkListHandleFunc(links LinkProvider, log *slog.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func getLinkHandleFunc(links LinkProvider, log *slog.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func postLinkHandleFunc(links LinkProvider, log *slog.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func putLinkHandleFunc(links LinkProvider, log *slog.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func deleteLinkHandleFunc(links LinkProvider, log *slog.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func getUserListHandleFunc(users UserProvider, log *slog.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func getUserHandleFunc(sers UserProvider, log *slog.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func postUserHandleFunc(sers UserProvider, log *slog.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func putUserHandleFunc(sers UserProvider, log *slog.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) {} +} + +func deleteUserHandleFunc(sers UserProvider, log *slog.Logger) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) {} +} diff --git a/api/server.go b/api/server.go new file mode 100644 index 0000000..63688bf --- /dev/null +++ b/api/server.go @@ -0,0 +1,117 @@ +package api + +import ( + "context" + "log/slog" + "net" + "net/http" + "net/url" + "time" + + "git.yetaga.in/alazyreader/going-further/storage" +) + +type LinkProvider interface { + Link(key string) *url.URL + List() []string + Details(key string) storage.Link +} + +type SessionProvider interface { + Login(username string, password string) (string, bool) // session token, valid + Valid(token string) bool + User(token string) string // token -> username +} + +type UserProvider interface { + List() []string // usernames + Add(username string, password string) error + Update(username string, password string) error + Remove(username string) error +} + +type shutdown struct { + wait int + timeout int +} + +type Server struct { + host string + port string + version string + + users UserProvider + sessions SessionProvider + links LinkProvider + + http *http.Server + waits shutdown + + log *slog.Logger +} + +func NewServer(logger *slog.Logger, port string, version string) *Server { + return &Server{ + log: logger, + port: port, + version: version, + waits: shutdown{ + wait: 10, + timeout: 10, + }, + } +} + +// Setup returns start and stop functions for the http service +func (s *Server) Setup() (func(), func()) { + mux := http.NewServeMux() + mux.HandleFunc("GET /version", versionHandleFunc(s.log, s.version)) + + mux.HandleFunc("GET /login", getLoginHandleFunc(s.sessions, s.log)) + mux.HandleFunc("POST /login", postLoginHandleFunc(s.sessions, s.log)) + + mux.HandleFunc("GET /manage", getManageHandleFunc(s.log)) + + mux.HandleFunc("GET /manage/links", getLinkListHandleFunc(s.links, s.log)) + mux.HandleFunc("GET /manage/links/{name}", getLinkHandleFunc(s.links, s.log)) + mux.HandleFunc("POST /manage/links", postLinkHandleFunc(s.links, s.log)) + mux.HandleFunc("PUT /manage/links/{name}", putLinkHandleFunc(s.links, s.log)) + mux.HandleFunc("DELETE /manage/links/{name}", deleteLinkHandleFunc(s.links, s.log)) + + mux.HandleFunc("GET /manage/users", getUserListHandleFunc(s.users, s.log)) + mux.HandleFunc("GET /manage/users/{name}", getUserHandleFunc(s.users, s.log)) + mux.HandleFunc("POST /manage/users", postUserHandleFunc(s.users, s.log)) + mux.HandleFunc("PUT /manage/users/{name}", putUserHandleFunc(s.users, s.log)) + mux.HandleFunc("DELETE /manage/users/{name}", deleteUserHandleFunc(s.users, s.log)) + + mux.HandleFunc("GET /static/", staticHandleFunc()) + + mux.HandleFunc("GET /", linkHandleFunc(s.links, s.log)) + + s.http = &http.Server{ + Addr: net.JoinHostPort(s.host, s.port), + Handler: mux, + } + + return s.start(), s.stop() +} + +func (s *Server) start() func() { + return func() { + s.log.Info("server startup", slog.String("addr", s.http.Addr)) + if err := s.http.ListenAndServe(); err != nil && err != http.ErrServerClosed { + s.log.Error("error listening and serving", slog.String("error", err.Error())) + } + } +} +func (s *Server) stop() func() { + return func() { + s.log.Info("server shutdown") + time.Sleep(time.Duration(s.waits.wait) * time.Second) + shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Duration(s.waits.timeout)*time.Second) + defer cancel() + if err := s.http.Shutdown(shutdownCtx); err != nil { + s.log.Error("error shutting down http server", slog.String("error", err.Error())) + } + } +} diff --git a/cmd/furthur/main.go b/cmd/furthur/main.go index 38dd16d..cd39a83 100644 --- a/cmd/furthur/main.go +++ b/cmd/furthur/main.go @@ -1,3 +1,26 @@ package main -func main() {} +import ( + "log/slog" + "os" + "os/signal" + + "git.yetaga.in/alazyreader/going-further/api" +) + +func main() { + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)). + With(slog.String("application", "furthur")) + slog.SetDefault(logger) + + server := api.NewServer(logger, "8080", "v0.0.1") + start, stop := server.Setup() + + start() + + c := make(chan os.Signal, 2) + signal.Notify(c, os.Interrupt) + <-c + + stop() +} diff --git a/readme.md b/readme.md index 01089ce..408cd7a 100644 --- a/readme.md +++ b/readme.md @@ -65,11 +65,14 @@ It turns out channels are hard to reason about, but that's because _concurrency_ hard to reason about. Always remember that read/write access to a shared map _must_ be gated with a mutex. +Concurrent read-only access is safe, however. ## Go Generate ## Build tags +## Templates + ## Logging `slog` package @@ -77,7 +80,7 @@ Always remember that read/write access to a shared map _must_ be gated with a mu ## init functions and globals Don't use them! They're hard to reason about and until recent versions of go -the order they ran in was undefined, leading to subtle bugs. +the order they ran in was under-defined, leading to subtle bugs. ## Common tools diff --git a/storage/file.go b/storage/file.go new file mode 100644 index 0000000..82be054 --- /dev/null +++ b/storage/file.go @@ -0,0 +1 @@ +package storage diff --git a/storage/memory.go b/storage/memory.go new file mode 100644 index 0000000..82be054 --- /dev/null +++ b/storage/memory.go @@ -0,0 +1 @@ +package storage diff --git a/storage/struct.go b/storage/struct.go new file mode 100644 index 0000000..9a97902 --- /dev/null +++ b/storage/struct.go @@ -0,0 +1,4 @@ +package storage + +type Link struct { +}