From 15a8fed766812b7571d54383dfb6c4bf28adb9b1 Mon Sep 17 00:00:00 2001 From: spsobole Date: Mon, 29 May 2023 14:21:08 -0600 Subject: [PATCH] Add API handlers for snap --- context.go | 101 +++++++++++++++-------------- go.mod | 2 +- options.go | 158 +++++++++++++++++++++++++++++++++++++++++++++ server.go | 1 - service.go | 108 +++++++++++++++++++++++++++++++ service_request.go | 136 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 455 insertions(+), 51 deletions(-) create mode 100644 options.go create mode 100644 service.go create mode 100644 service_request.go diff --git a/context.go b/context.go index 0f46404..a42ee63 100644 --- a/context.go +++ b/context.go @@ -1,12 +1,11 @@ package snap import ( + "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" - "strconv" - "strings" "git.twelvetwelve.org/library/snap/auth" ) @@ -17,7 +16,8 @@ type Context struct { w http.ResponseWriter r *http.Request - vars map[string]string + vars *Vars + form *Form } type SnapContent struct { @@ -69,11 +69,21 @@ func (c *Context) ReplyWithHeaders(msg string, kv map[string]string) { } func (c *Context) ReplyObject(obj interface{}) { + compact := c.r.Header.Get("Compact") msg, err := json.Marshal(obj) if err != nil { c.Error(http.StatusInternalServerError, "Internal Server Error") return } + + if compact == "" { + var pretty bytes.Buffer + err := json.Indent(&pretty, msg, "", " ") + if err == nil { + msg = pretty.Bytes() + } + } + c.Reply(string(msg)) } @@ -133,57 +143,24 @@ func (c *Context) RenderWithMeta(tmpl string, meta map[string]string, content in c.srv.render(c.w, tmpl, &cnt) } -func (c *Context) GetVar(k string) (string, bool) { - v, ok := c.vars[k] - return v, ok -} - -func (c *Context) GetVarDefault(k string, def string) string { - v, ok := c.vars[k] - if !ok { - return def +func (c *Context) Form() (*Options, error) { + if c.form != nil { + return &Options{ + kv: c.form, + }, nil } - return v -} -func (c *Context) GetVarAsSlice(k string, delim string, def []string) []string { - v, ok := c.vars[k] - if !ok { - return def - } - r := strings.Split(v, delim) - if len(r) == 1 && r[0] == "" { - return def - } - return r -} - -func (c *Context) ParseForm() error { - return c.r.ParseForm() -} - -func (c *Context) FormValue(k string) string { - return c.r.FormValue(k) -} - -func (c *Context) FormValueUint64(k string) uint64 { - str := c.r.FormValue(k) - - val, err := strconv.ParseUint(str, 10, 64) + err := c.r.ParseForm() if err != nil { - return 0 + return nil, err + } + c.form = &Form{ + r: c.r, } - return val -} - -func (c *Context) FormValueAsSlice(k string, delim string) []string { - v := c.r.FormValue(k) - r := strings.Split(v, delim) - if len(r) == 1 && r[0] == "" { - return []string{} - } - return r + return &Options{ + kv: c.form, + }, nil } func (c *Context) Redirect(url string) { @@ -206,3 +183,29 @@ func (c *Context) QueryValueWithDefault(key string, def string) string { } return val } + +func (c *Context) ReadObject(object interface{}) error { + defer c.r.Body.Close() + decoder := json.NewDecoder(c.r.Body) + return decoder.Decode(object) +} + +func (c *Context) ParseVars() *Options { + if c.form != nil { + return &Options{ + kv: c.form, + } + } + + err := c.r.ParseForm() + if err != nil { + return nil + } + c.form = &Form{ + r: c.r, + } + + return &Options{ + kv: c.form, + } +} diff --git a/go.mod b/go.mod index 4400087..789385c 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module git.twelvetwelve.org/library/snap go 1.20 require ( + git.twelvetwelve.org/library/core v0.0.0-20230519041221-8f2da7be661d github.com/gorilla/mux v1.8.0 github.com/stretchr/testify v1.7.0 ) require ( - git.twelvetwelve.org/library/core v0.0.0-20230519041221-8f2da7be661d // indirect github.com/davecgh/go-spew v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect diff --git a/options.go b/options.go new file mode 100644 index 0000000..7b5f7ef --- /dev/null +++ b/options.go @@ -0,0 +1,158 @@ +package snap + +import ( + "fmt" + "net/url" + "strconv" + "strings" + "time" +) + +type OptionsProvider interface { + Get(key string) (string, bool) +} + +type Options struct { + kv OptionsProvider +} + + +// FormValueProvider we only care about something that can provide us the K/V lookup +type FormValueProvider interface { + FormValue(string) string +} + +// Form wraps utility functions around HTTP Form values +type Form struct { + r FormValueProvider +} + +func (f *Form) Get(k string) (string, bool) { + v := f.r.FormValue(k) + if v == "" { + return v, false + } + + return v, true +} + + +type Vars struct { + vars map[string]string +} + +func (c Vars) Get(k string) (string, bool) { + v, ok := c.vars[k] + return v, ok +} + +type Query struct { + Values url.Values +} + +func (q *Query) Get(key string) (string, bool) { + value, ok := q.Values[key] + if ok { + return value[0], true + } + + return "", false +} + + +func (o *Options) GetVar(k string) (string, bool) { + return o.kv.Get(k) +} + +func (o *Options) GetVarDefault(k string, def string) string { + v, ok := o.kv.Get(k) + if !ok { + return def + } + return v +} + +func (o *Options) GetVarAsSlice(k string, delim string, def []string) []string { + v, ok := o.kv.Get(k) + + if !ok { + return def + } + r := strings.Split(v, delim) + if len(r) == 1 && r[0] == "" { + return def + } + return r +} + + +// StringValue returns form value as a string +func (o *Options) StringValue(k string) (string,bool) { + return o.kv.Get(k) +} + +// Uint64Value returns the first value in a set as uint64 +func (o *Options) Uint64Value(k string) (uint64, bool) { + str,ok := o.kv.Get(k) + if !ok { + return 0, ok + } + + val, err := strconv.ParseUint(str, 10, 64) + if err != nil { + return 0, false + } + + return val, true +} + +// StringSlice returns multiple values assigned to the same key as a string slice +func (o *Options) StringSlice(k string, delim string) ([]string,bool) { + str,ok := o.kv.Get(k) + if !ok { + return nil, ok + } + + r := strings.Split(str, delim) + if len(r) == 1 && r[0] == "" { + return nil, false + } + return r, true +} + +// TimeValue returns a parsed time value for the specified key, +// if the key does not exist the but defaultValue was set it will return the defaultValue +// otherwise it will return the passed in time or an error if parsing failed +func (o *Options) TimeValue(key, timeFormat string, defaultValue *time.Time) time.Time { + str,ok := o.kv.Get(key) + if !ok { + if defaultValue != nil { + return *defaultValue + } + return time.Time{} + } + + t, err := time.Parse(timeFormat, str) + if err != nil { + fmt.Printf("Err: %v", err) + return time.Time{} + } + + return t +} + + + + +type CompoundOptions struct { + kvs []OptionsProvider +} + +func (c *CompoundOptions) Get(key string) (string, bool) { + for _, kv := range c.kvs { + if v, ok := kv.Get(key); ok { + return v, true + } + } + return "", false +} diff --git a/server.go b/server.go index fb39673..cba2a5f 100644 --- a/server.go +++ b/server.go @@ -57,7 +57,6 @@ func (s *Server) makeContext(auth *auth.AuthData, w http.ResponseWriter, r *http w: w, srv: s, auth: auth, - vars: mux.Vars(r), } return c } diff --git a/service.go b/service.go new file mode 100644 index 0000000..4a3d507 --- /dev/null +++ b/service.go @@ -0,0 +1,108 @@ +package snap + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + + "git.twelvetwelve.org/library/core/log" + + "github.com/gorilla/mux" +) + +type ServiceStatus struct { + Message string +} + +type ServiceResponse struct { + Code int + Message []byte +} + +func (s *ServiceResponse) WriteResponse(w http.ResponseWriter) { + fmt.Printf("Response: %d/%s\n", s.Code, s.Message) + w.WriteHeader(s.Code) + w.Write(s.Message) +} + +func (s *Server) serviceWrapper(handle func(c *ServiceRequest) *ServiceResponse) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if s.debug { + log.Debugf("request: %+v\n", r.RequestURI) + } + + c := &ServiceRequest{ + r: r, + } + if s.auth != nil { + if rec, code := s.auth.DoAuth(w, r); code == http.StatusOK { + c.auth = rec + } + } + resp := handle(c) + // discard the rest of the body content + io.Copy(ioutil.Discard, r.Body) + defer r.Body.Close() + resp.WriteResponse(w) + } +} + +func (s *Server) serviceWrapperAuthenticated(handle func(c *ServiceRequest) *ServiceResponse) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if s.debug { + log.Debugf("request: %+v\n", r.RequestURI) + } + + // cleanup content of anything sent to us + defer func() { + io.Copy(ioutil.Discard, r.Body) + r.Body.Close() + }() + + if s.auth == nil { + resp := ServiceResponse{ + Code: http.StatusForbidden, + Message: []byte("Not Authenticated"), + } + resp.WriteResponse(w) + return + } + + rec, code := s.auth.DoAuth(w, r) + switch code { + case http.StatusOK: + c := &ServiceRequest{ + r: r, + auth: rec, + } + resp := handle(c) + resp.WriteResponse(w) + return + + case http.StatusUnauthorized: + resp := ServiceResponse{ + Code: http.StatusForbidden, + Message: []byte("Not Authenticated"), + } + resp.WriteResponse(w) + return + + default: + resp := ServiceResponse{ + Code: code, + Message: []byte(http.StatusText(code)), + } + resp.WriteResponse(w) + return + } + } +} + +func (s *Server) HandleServiceFunc(path string, f func(c *ServiceRequest) *ServiceResponse) *mux.Route { + return s.router.HandleFunc(path, s.serviceWrapper(f)) +} + +func (s *Server) HandleServiceFuncWithAuth(path string, f func(c *ServiceRequest) *ServiceResponse) *mux.Route { + return s.router.HandleFunc(path, s.serviceWrapperAuthenticated(f)) +} diff --git a/service_request.go b/service_request.go new file mode 100644 index 0000000..b28d9f3 --- /dev/null +++ b/service_request.go @@ -0,0 +1,136 @@ +package snap + +import ( + "bytes" + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + + "git.twelvetwelve.org/library/snap/auth" +) + +type ServiceRequest struct { + r *http.Request + auth *auth.AuthData + vars *Vars + form *Form + query *Query +} + +func (c *ServiceRequest) GetUser() string { + if c.auth == nil { + return "" + } + return c.auth.User +} + +func (c *ServiceRequest) ReplyMessage(code int, msg string) *ServiceResponse { + return &ServiceResponse{ + Code: code, + Message: []byte(msg), + } +} + +func (c *ServiceRequest) ReplyObject(code int, obj interface{}) *ServiceResponse { + var err error + resp := &ServiceResponse{ + Code: code, + } + + resp.Message, err = json.Marshal(obj) + if err != nil { + resp.Code = http.StatusInternalServerError + resp.Message = []byte("Internal Server Error") + + } + + compact := c.r.Header.Get("Compact") + if compact == "" { + var pretty bytes.Buffer + err := json.Indent(&pretty, resp.Message, "", " ") + if err == nil { + resp.Message = pretty.Bytes() + } + } + return resp +} + +func (c *ServiceRequest) GetObject(object interface{}) error { + decoder := json.NewDecoder(c.r.Body) + return decoder.Decode(object) +} + +func (c *ServiceRequest) Form() (*Options, error) { + if c.form != nil { + return &Options{ + kv: c.form, + }, nil + } + + err := c.r.ParseForm() + if err != nil { + return nil, err + } + c.form = &Form{ + r: c.r, + } + + return &Options{ + kv: c.form, + }, nil +} + +func (c *ServiceRequest) Query() *Options { + if c.query != nil { + return &Options{ + kv: c.query, + } + } + + c.query = &Query{ + Values: c.r.URL.Query(), + } + return &Options{ + kv: c.query, + } +} + +func (c *ServiceRequest) Vars() *Options { + if c.vars != nil { + return &Options{ + kv: c.vars, + } + } + + c.vars = &Vars{ + vars: mux.Vars(c.r), + } + return &Options{ + kv: c.vars, + } +} + +// Options get compound options +func (c *ServiceRequest) Options() *Options { + opts := &CompoundOptions{} + + opts.kvs = append(opts.kvs, &Vars{ + vars: mux.Vars(c.r), + }) + + opts.kvs = append(opts.kvs, &Query{ + Values: c.r.URL.Query(), + }) + + err := c.r.ParseForm() + if err == nil { + opts.kvs = append(opts.kvs, &Form{ + r: c.r, + }) + } + + return &Options{ + kv: opts, + } +}