initial commit router with static files and dynamic handlers, renderers for templates, json, and xml, and beginnings of auth and cookie management
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE----- iQIzBAABCAAdFiEEkFh6dA+k/6CXFXU4O3+8IhROY5gFAmHUrL0ACgkQO3+8IhRO Y5h30BAAlukB8zh3VEE5mPV2pUfYB6e7/PY+4d5tXMTP7GSHBsU2uNrPPOyphtCX St2Nzu/cXjdVvBsJkvTPDIIApPsJjNLihU7hxJEjwwM04F2+CiCQMwSOKGysqUiL bFxnvD3ha0U2eA0VuywPmMfYPIwoARaots4J/rpy1QZb480LSpgINiswmkN0jdYe oha8b+XA+G5o4xZ7N4wemHhokcf9GJNFrfBewboED4gfJtaT01lHmXItW5oElYsn 3M867N+9K+BKmhVtHZdBbg/JFhCaHSaz15Ax5kmZNNLZlrk4q8O4yzapfIWxso5L 7cIHY5mDlRGC6h1ZiNcWGHBfpN+ZGS6EUe/gh6AhFNQ1NajvmTMuPQnDIgieTeov 78GgUXDRhNzo2n7vM/YmuVEaFd0En0TaASimUCuj9PVRrnvn9YspclcTCCT63b4i 4kCIDDsXTvGzc2akobMTuo33Pnn9XXAvAx1EuWnPX7fAsYCj16NuQydhjRUVgHre Z6ObKIc80eCQR99VYB7VS/+sZq9eFFNZmFGU0BxwlR85KntFNDzMH4b3SkN7t1ri TsZuUeLFs7AiffL+l4hRAMw+QqhLZ36g/t9egwILzzIDHOVoKIg/jusUW7MKsvVL oaBIy4o0t434v2wrbkrhTv7bcM2aKiJ2xdlT3tHeP1QIdHxHMuM= =bQSa -----END PGP SIGNATURE-----
7 files changed,
298 insertions(+),
0 deletions(-)
A
auth/auth.go
@@ -0,0 +1,19 @@
+package auth + +import ( + //nilfm.cc/git/goldbug/cookie +) + +type UserStore interface { + InitiateSession(user string, sessionId string) error + ValidateUser(user string, password string, sessionId string) (bool, error) + EndSession(user string) error +} + +func Login(user string, password string, userStore UserStore) (string, error) { + //ValidateUser (check user exists, hash and compare password) + //InitiateUserSession (generate token and assign it to the user) + //set username in cookie + //return token, nil + return "", nil +}
A
go.mod
@@ -0,0 +1,11 @@
+module nilfm.cc/git/goldbug + +go 1.17 + +require ( + github.com/gorilla/securecookie v1.1.1 + github.com/gorilla/sessions v1.2.1 + golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 +) + +
A
go.sum
@@ -0,0 +1,6 @@
+github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
A
renderer/renderer.go
@@ -0,0 +1,48 @@
+package renderer + +import ( + "net/http" + "html/template" + "encoding/json" + "encoding/xml" +) + +func Template(t string) http.Handler { + tmpl := template.Must(template.ParseFiles(t)) + + handlerFunc := func(w http.ResponseWriter, req *http.Request) { + tmpl.Execute(w, req) + } + + return http.HandlerFunc(handlerFunc) +} + +func JSON(key string) http.Handler { + handlerFunc := func(w http.ResponseWriter, req *http.Request) { + apiData := req.Context().Value(key) + + data, err := json.Marshal(apiData) + if err != nil { + panic(err.Error()) + } + w.Header().Set("Content-Type", "application/json") + w.Write(data) + } + + return http.HandlerFunc(handlerFunc) +} + +func XML(key string) http.Handler { + handlerFunc := func(w http.ResponseWriter, req *http.Request) { + apiData := req.Context().Value(key) + + data, err := xml.MarshalIndent(apiData, "", " ") + if err != nil { + panic(err.Error()) + } + w.Header().Set("Content-Type", "application/xml") + w.Write(data) + } + + return http.HandlerFunc(handlerFunc) +}
A
router/router.go
@@ -0,0 +1,173 @@
+package router + +import ( + "net/http" + "html/template" + "regexp" + "log" + "strconv" + "strings" + "path" + "os" + "errors" + "fmt" +) + +type Router struct { + /* This is the template for error pages */ + Fallback template.Template + /* Routes are only filled by using the appropriate methods. */ + routes []Route + /* StaticPaths can be filled from outside when constructing the Router. + * key = uri + * value = file path + */ + StaticPaths map[string]string +} + + +type Route struct { + path *regexp.Regexp + handlerMap map[string]http.Handler +} + +/* This represents what the server should do with a given request. */ +type ServerTask struct { + /* template and apiFmt are mutually exclusive. */ + template *template.Template + apiFmt string + + /* doWork represents serverside work to fulfill the request. + * This function can be composed any way you see fit when creating + * a route. + */ + doWork func(http.ResponseWriter, *http.Request) +} + +func (self *Router) Get(path string, h http.Handler) { + self.AddRoute("GET", path, h) +} + +func (self *Router) Post(path string, h http.Handler) { + self.AddRoute("POST", path, h) +} + +func (self *Router) Put(path string, h http.Handler) { + self.AddRoute("PUT", path, h) +} + +func (self *Router) Delete(path string, h http.Handler) { + self.AddRoute("DELETE", path, h) +} + +func (self *Router) AddRoute(method string, path string, h http.Handler) { + + exactPath := regexp.MustCompile("^" + path + "$") + + /* If the route already exists, try to add this method to the ServerTask map. */ + for _, r := range self.routes { + if r.path == exactPath { + r.handlerMap[method] = h + return + } + } + + /* Otherwise add a new route */ + self.routes = append(self.routes, Route{ + path: exactPath, + handlerMap: map[string]http.Handler{method: h}, + }) + +} + +func (self *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + /* Show the 500 error page if we panic */ + defer func() { + if r := recover(); r != nil { + log.Println("ERROR:", r) + self.ErrorPage(w, req, 500, "There was an error on the server.") + } + }() + + /* If the request matches any our StaticPaths, try to serve a file. */ + for uri, dir := range self.StaticPaths { + if req.Method == "GET" && strings.HasPrefix(req.URL.Path, uri) { + restOfUri := strings.TrimPrefix(req.URL.Path, uri) + p := path.Join(dir, restOfUri) + p = path.Clean(p) + + /* If the file exists, try to serve it. */ + info, err := os.Stat(p); + if err == nil && !info.IsDir() { + http.ServeFile(w, req, p) + /* Handle the common errors */ + } else if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrExist) { + self.ErrorPage(w, req, 404, "The requested file does not exist") + } else if errors.Is(err, os.ErrPermission) || info.IsDir() { + self.ErrorPage(w, req, 403, "Access forbidden") + /* If it's some weird error, serve a 500. */ + } else { + self.ErrorPage(w, req, 500, "Internal server error") + } + + return + } + } + + /* Otherwise, this is a normal route */ + for _, r := range self.routes { + + /* Pull the params out of the regex; + * If the path doesn't match the regex, params will be nil. + */ + params := r.Match(req) + if params == nil { + continue + } + for method, handler := range r.handlerMap { + if method == req.Method { + /* Parse the form and add the params to it */ + req.ParseForm() + ProcessParams(req, params) + /* handle the request! */ + handler.ServeHTTP(w, req); + return + } + } + } + self.ErrorPage(w, req, 404, "The page you requested does not exist!") +} + +/******************* + * Utility Methods * + *******************/ + +func ProcessParams(req *http.Request, params map[string]string) { + for key, value := range params { + req.Form.Add(key, value) + } +} + +func (self *Route) Match(r *http.Request) map[string]string { + match := self.path.FindStringSubmatch(r.URL.Path) + if match == nil { + return nil + } + + params := map[string]string{} + groupNames := self.path.SubexpNames() + + for i, group := range match { + params[groupNames[i]] = group + } + + return params +} + +func (self *Router) ErrorPage(w http.ResponseWriter, req *http.Request, code int, errMsg string) { + w.WriteHeader(code) + req.ParseForm() + req.Form.Add("ErrorCode", strconv.Itoa(code)) + req.Form.Add("ErrorMessage", errMsg) + self.Fallback.Execute(w, req) +}