commit 756a0739fd43a53fc4c5df24c96c429a617e9591 Author: Derek Stevens Date: Tue Jan 4 13:23:25 2022 -0700 initial commit router with static files and dynamic handlers, renderers for templates, json, and xml, and beginnings of auth and cookie management diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a468a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +goldbug* +static/ +templates/ diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..17b6ad3 --- /dev/null +++ b/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 +} diff --git a/cookie/cookie.go b/cookie/cookie.go new file mode 100644 index 0000000..935ae46 --- /dev/null +++ b/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 + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..938ef52 --- /dev/null +++ b/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 +) + + diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..28c6029 --- /dev/null +++ b/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= diff --git a/renderer/renderer.go b/renderer/renderer.go new file mode 100644 index 0000000..79860cc --- /dev/null +++ b/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) +} diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..35d17f6 --- /dev/null +++ b/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) +}