all repos — quartzgun @ 756a0739fd43a53fc4c5df24c96c429a617e9591

lightweight web framework in go

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-----
commit

756a0739fd43a53fc4c5df24c96c429a617e9591

7 files changed, 298 insertions(+), 0 deletions(-)

jump to
A .gitignore

@@ -0,0 +1,3 @@

+goldbug* +static/ +templates/
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 cookie/cookie.go

@@ -0,0 +1,38 @@

+package cookie + +import ( + "net/http" + "crypto/rand" + "time" +) + +var availableChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ@!.#$_" + +func GenToken(length int) string { + ll := len(availableChars) + b := make([]byte, length) + rand.Read(b) + for i := 0; i < length; i++ { + b[i] = availableChars[int(b[i])%ll] + } + return string(b) +} + +func StoreToken(field string, token string, w http.ResponseWriter, hrs int) { + cookie := http.Cookie{ + Name: field, + Value: token, + Expires: time.Now().Add(time.Duration(hrs) * time.Hour), + } + + http.SetCookie(w, &cookie) +} + +func GetToken(field string, req *http.Request) (string, error) { + c, err := req.Cookie(field) + if err != nil { + return c.Value, nil + } else { + return "", err + } +}
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) +}