diff options
author | Steve Manuel <nilslice@gmail.com> | 2017-02-13 09:18:36 -0800 |
---|---|---|
committer | Steve Manuel <nilslice@gmail.com> | 2017-02-13 09:18:36 -0800 |
commit | f47826071f1e6d7b048d1304d8435a347f9b412a (patch) | |
tree | 1990862ba77dfce49325cc39c017a76a97c366c5 | |
parent | 6ea648ae4306e64f5562ca363e0cdea494ba8d46 (diff) |
adding item.Omittable interface for field-level omission of data in responses, implementation in handler and separating source into individual files for other interface impls
-rw-r--r-- | system/api/cors.go | 74 | ||||
-rw-r--r-- | system/api/gzip.go | 60 | ||||
-rw-r--r-- | system/api/handlers.go | 212 | ||||
-rw-r--r-- | system/api/hide.go | 27 | ||||
-rw-r--r-- | system/api/json.go | 57 | ||||
-rw-r--r-- | system/api/omit.go | 34 | ||||
-rw-r--r-- | system/api/record.go | 16 | ||||
-rw-r--r-- | system/item/item.go | 8 |
8 files changed, 292 insertions, 196 deletions
diff --git a/system/api/cors.go b/system/api/cors.go new file mode 100644 index 0000000..249a378 --- /dev/null +++ b/system/api/cors.go @@ -0,0 +1,74 @@ +package api + +import ( + "log" + "net/http" + "net/url" + + "github.com/ponzu-cms/ponzu/system/db" +) + +// sendPreflight is used to respond to a cross-origin "OPTIONS" request +func sendPreflight(res http.ResponseWriter) { + res.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type") + res.Header().Set("Access-Control-Allow-Origin", "*") + res.WriteHeader(200) + return +} + +func responseWithCORS(res http.ResponseWriter, req *http.Request) (http.ResponseWriter, bool) { + if db.ConfigCache("cors_disabled").(bool) == true { + // check origin matches config domain + domain := db.ConfigCache("domain").(string) + origin := req.Header.Get("Origin") + u, err := url.Parse(origin) + if err != nil { + log.Println("Error parsing URL from request Origin header:", origin) + return res, false + } + + // hack to get dev environments to bypass cors since u.Host (below) will + // be empty, based on Go's url.Parse function + if domain == "localhost" { + domain = "" + } + origin = u.Host + + // currently, this will check for exact match. will need feedback to + // determine if subdomains should be allowed or allow multiple domains + // in config + if origin == domain { + // apply limited CORS headers and return + res.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type") + res.Header().Set("Access-Control-Allow-Origin", domain) + return res, true + } + + // disallow request + res.WriteHeader(http.StatusForbidden) + return res, false + } + + // apply full CORS headers and return + res.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type") + res.Header().Set("Access-Control-Allow-Origin", "*") + + return res, true +} + +// CORS wraps a HandlerFunc to respond to OPTIONS requests properly +func CORS(next http.HandlerFunc) http.HandlerFunc { + return db.CacheControl(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res, cors := responseWithCORS(res, req) + if !cors { + return + } + + if req.Method == http.MethodOptions { + sendPreflight(res) + return + } + + next.ServeHTTP(res, req) + })) +} diff --git a/system/api/gzip.go b/system/api/gzip.go new file mode 100644 index 0000000..9f50cf7 --- /dev/null +++ b/system/api/gzip.go @@ -0,0 +1,60 @@ +package api + +import ( + "compress/gzip" + "net/http" + "strings" + + "github.com/ponzu-cms/ponzu/system/db" +) + +// Gzip wraps a HandlerFunc to compress responses when possible +func Gzip(next http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + if db.ConfigCache("gzip_disabled").(bool) == true { + next.ServeHTTP(res, req) + return + } + + // check if req header content-encoding supports gzip + if strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") { + // gzip response data + res.Header().Set("Content-Encoding", "gzip") + var gzres gzipResponseWriter + if pusher, ok := res.(http.Pusher); ok { + gzres = gzipResponseWriter{res, pusher, gzip.NewWriter(res)} + } else { + gzres = gzipResponseWriter{res, nil, gzip.NewWriter(res)} + } + + next.ServeHTTP(gzres, req) + return + } + + next.ServeHTTP(res, req) + }) +} + +type gzipResponseWriter struct { + http.ResponseWriter + pusher http.Pusher + + gw *gzip.Writer +} + +func (gzw gzipResponseWriter) Write(p []byte) (int, error) { + defer gzw.gw.Close() + return gzw.gw.Write(p) +} + +func (gzw gzipResponseWriter) Push(target string, opts *http.PushOptions) error { + if opts == nil { + opts = &http.PushOptions{ + Header: make(http.Header), + } + } + + opts.Header.Set("Accept-Encoding", "gzip") + + return gzw.pusher.Push(target, opts) +} diff --git a/system/api/handlers.go b/system/api/handlers.go index 91b10f9..2c234ed 100644 --- a/system/api/handlers.go +++ b/system/api/handlers.go @@ -1,16 +1,12 @@ package api import ( - "bytes" - "compress/gzip" "encoding/json" "log" "net/http" - "net/url" "strconv" "strings" - "github.com/ponzu-cms/ponzu/system/api/analytics" "github.com/ponzu-cms/ponzu/system/db" "github.com/ponzu-cms/ponzu/system/item" ) @@ -94,6 +90,12 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) { return } + j, err = omit(it(), res, req, &j) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + sendData(res, req, j) } @@ -137,6 +139,12 @@ func contentHandler(res http.ResponseWriter, req *http.Request) { return } + j, err = omit(pt(), res, req, &j) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + sendData(res, req, j) } @@ -174,199 +182,11 @@ func contentHandlerBySlug(res http.ResponseWriter, req *http.Request) { return } - sendData(res, req, j) -} - -func hide(it interface{}, res http.ResponseWriter, req *http.Request) bool { - // check if should be hidden - if h, ok := it.(item.Hideable); ok { - err := h.Hide(res, req) - if err == item.ErrAllowHiddenItem { - return false - } - - if err != nil { - res.WriteHeader(http.StatusInternalServerError) - return true - } - - res.WriteHeader(http.StatusNotFound) - return true - } - - return false -} - -func fmtJSON(data ...json.RawMessage) ([]byte, error) { - var msg = []json.RawMessage{} - for _, d := range data { - msg = append(msg, d) - } - - resp := map[string][]json.RawMessage{ - "data": msg, - } - - var buf = &bytes.Buffer{} - enc := json.NewEncoder(buf) - err := enc.Encode(resp) + j, err = omit(it(), res, req, &j) if err != nil { - log.Println("Failed to encode data to JSON:", err) - return nil, err - } - - return buf.Bytes(), nil -} - -func toJSON(data []string) ([]byte, error) { - var buf = &bytes.Buffer{} - enc := json.NewEncoder(buf) - resp := map[string][]string{ - "data": data, - } - - err := enc.Encode(resp) - if err != nil { - log.Println("Failed to encode data to JSON:", err) - return nil, err - } - - return buf.Bytes(), nil -} - -// sendData should be used any time you want to communicate -// data back to a foreign client -func sendData(res http.ResponseWriter, req *http.Request, data []byte) { - res.Header().Set("Content-Type", "application/json") - res.Header().Set("Vary", "Accept-Encoding") - - _, err := res.Write(data) - if err != nil { - log.Println("Error writing to response in sendData") - } -} - -// sendPreflight is used to respond to a cross-origin "OPTIONS" request -func sendPreflight(res http.ResponseWriter) { - res.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type") - res.Header().Set("Access-Control-Allow-Origin", "*") - res.WriteHeader(200) - return -} - -func responseWithCORS(res http.ResponseWriter, req *http.Request) (http.ResponseWriter, bool) { - if db.ConfigCache("cors_disabled").(bool) == true { - // check origin matches config domain - domain := db.ConfigCache("domain").(string) - origin := req.Header.Get("Origin") - u, err := url.Parse(origin) - if err != nil { - log.Println("Error parsing URL from request Origin header:", origin) - return res, false - } - - // hack to get dev environments to bypass cors since u.Host (below) will - // be empty, based on Go's url.Parse function - if domain == "localhost" { - domain = "" - } - origin = u.Host - - // currently, this will check for exact match. will need feedback to - // determine if subdomains should be allowed or allow multiple domains - // in config - if origin == domain { - // apply limited CORS headers and return - res.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type") - res.Header().Set("Access-Control-Allow-Origin", domain) - return res, true - } - - // disallow request - res.WriteHeader(http.StatusForbidden) - return res, false - } - - // apply full CORS headers and return - res.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type") - res.Header().Set("Access-Control-Allow-Origin", "*") - - return res, true -} - -// CORS wraps a HandlerFunc to respond to OPTIONS requests properly -func CORS(next http.HandlerFunc) http.HandlerFunc { - return db.CacheControl(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - res, cors := responseWithCORS(res, req) - if !cors { - return - } - - if req.Method == http.MethodOptions { - sendPreflight(res) - return - } - - next.ServeHTTP(res, req) - })) -} - -// Record wraps a HandlerFunc to record API requests for analytical purposes -func Record(next http.HandlerFunc) http.HandlerFunc { - return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - go analytics.Record(req) - - next.ServeHTTP(res, req) - }) -} - -// Gzip wraps a HandlerFunc to compress responses when possible -func Gzip(next http.HandlerFunc) http.HandlerFunc { - return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - if db.ConfigCache("gzip_disabled").(bool) == true { - next.ServeHTTP(res, req) - return - } - - // check if req header content-encoding supports gzip - if strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") { - // gzip response data - res.Header().Set("Content-Encoding", "gzip") - var gzres gzipResponseWriter - if pusher, ok := res.(http.Pusher); ok { - gzres = gzipResponseWriter{res, pusher, gzip.NewWriter(res)} - } else { - gzres = gzipResponseWriter{res, nil, gzip.NewWriter(res)} - } - - next.ServeHTTP(gzres, req) - return - } - - next.ServeHTTP(res, req) - }) -} - -type gzipResponseWriter struct { - http.ResponseWriter - pusher http.Pusher - - gw *gzip.Writer -} - -func (gzw gzipResponseWriter) Write(p []byte) (int, error) { - defer gzw.gw.Close() - return gzw.gw.Write(p) -} - -func (gzw gzipResponseWriter) Push(target string, opts *http.PushOptions) error { - if opts == nil { - opts = &http.PushOptions{ - Header: make(http.Header), - } + res.WriteHeader(http.StatusInternalServerError) + return } - opts.Header.Set("Accept-Encoding", "gzip") - - return gzw.pusher.Push(target, opts) + sendData(res, req, j) } diff --git a/system/api/hide.go b/system/api/hide.go new file mode 100644 index 0000000..eed2c8b --- /dev/null +++ b/system/api/hide.go @@ -0,0 +1,27 @@ +package api + +import ( + "net/http" + + "github.com/ponzu-cms/ponzu/system/item" +) + +func hide(it interface{}, res http.ResponseWriter, req *http.Request) bool { + // check if should be hidden + if h, ok := it.(item.Hideable); ok { + err := h.Hide(res, req) + if err == item.ErrAllowHiddenItem { + return false + } + + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return true + } + + res.WriteHeader(http.StatusNotFound) + return true + } + + return false +} diff --git a/system/api/json.go b/system/api/json.go new file mode 100644 index 0000000..e9d448e --- /dev/null +++ b/system/api/json.go @@ -0,0 +1,57 @@ +package api + +import ( + "bytes" + "encoding/json" + "log" + "net/http" +) + +func fmtJSON(data ...json.RawMessage) ([]byte, error) { + var msg = []json.RawMessage{} + for _, d := range data { + msg = append(msg, d) + } + + resp := map[string][]json.RawMessage{ + "data": msg, + } + + var buf = &bytes.Buffer{} + enc := json.NewEncoder(buf) + err := enc.Encode(resp) + if err != nil { + log.Println("Failed to encode data to JSON:", err) + return nil, err + } + + return buf.Bytes(), nil +} + +func toJSON(data []string) ([]byte, error) { + var buf = &bytes.Buffer{} + enc := json.NewEncoder(buf) + resp := map[string][]string{ + "data": data, + } + + err := enc.Encode(resp) + if err != nil { + log.Println("Failed to encode data to JSON:", err) + return nil, err + } + + return buf.Bytes(), nil +} + +// sendData should be used any time you want to communicate +// data back to a foreign client +func sendData(res http.ResponseWriter, req *http.Request, data []byte) { + res.Header().Set("Content-Type", "application/json") + res.Header().Set("Vary", "Accept-Encoding") + + _, err := res.Write(data) + if err != nil { + log.Println("Error writing to response in sendData") + } +} diff --git a/system/api/omit.go b/system/api/omit.go new file mode 100644 index 0000000..46e172c --- /dev/null +++ b/system/api/omit.go @@ -0,0 +1,34 @@ +package api + +import ( + "log" + "net/http" + + "github.com/ponzu-cms/ponzu/system/item" + + "github.com/tidwall/sjson" +) + +func omit(it interface{}, res http.ResponseWriter, req *http.Request, data *[]byte) ([]byte, error) { + // is it Omittable + om, ok := it.(item.Omittable) + if !ok { + return *data, nil + } + + // get fields to omit from json data + fields := om.Omit() + + // remove each field from json, all responses contain json object(s) in top-level "data" array + var omitted []byte + for i := range fields { + var err error + omitted, err = sjson.DeleteBytes(*data, "data."+fields[i]) + if err != nil { + log.Println("Erorr omitting field:", fields[i], "from item.Omittable:", it) + return nil, err + } + } + + return omitted, nil +} diff --git a/system/api/record.go b/system/api/record.go new file mode 100644 index 0000000..93d4aaf --- /dev/null +++ b/system/api/record.go @@ -0,0 +1,16 @@ +package api + +import ( + "net/http" + + "github.com/ponzu-cms/ponzu/system/api/analytics" +) + +// Record wraps a HandlerFunc to record API requests for analytical purposes +func Record(next http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + go analytics.Record(req) + + next.ServeHTTP(res, req) + }) +} diff --git a/system/item/item.go b/system/item/item.go index f35d7f6..4a219c8 100644 --- a/system/item/item.go +++ b/system/item/item.go @@ -71,6 +71,14 @@ type Pushable interface { Push() []string } +// Omittable lets a user define certin fields within a content struct to remove +// from an API response. Helpful when you want data in the CMS, but not entirely +// shown or available from the content API. All items in the slice should be the +// json tag names of the struct fields to which they coorespond. +type Omittable interface { + Omit() []string +} + // Item should only be embedded into content type structs. type Item struct { UUID uuid.UUID `json:"uuid"` |