From 59ee9fbb237673c2d0cccad42f7adbae65852b8d Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Fri, 13 Jan 2017 11:58:39 -0800 Subject: changing ConfigCache to return interface{} --- cmd/ponzu/main.go | 2 +- system/addon/api.go | 8 ++++---- system/admin/config/config.go | 12 ++++++++++-- system/admin/handlers.go | 2 +- system/admin/server.go | 4 +++- system/api/handlers.go | 6 ++++++ system/api/server.go | 8 ++++---- system/db/cache.go | 2 +- system/db/config.go | 2 +- system/db/init.go | 2 +- system/tls/devcerts.go | 2 +- system/tls/enable.go | 2 +- 12 files changed, 34 insertions(+), 18 deletions(-) diff --git a/cmd/ponzu/main.go b/cmd/ponzu/main.go index 90ad613..2bee2e5 100644 --- a/cmd/ponzu/main.go +++ b/cmd/ponzu/main.go @@ -193,7 +193,7 @@ func main() { fmt.Println("Enabling HTTPS...") go tls.Enable() - fmt.Printf("Server listening on :%s for HTTPS requests...\n", db.ConfigCache("https_port")) + fmt.Printf("Server listening on :%s for HTTPS requests...\n", db.ConfigCache("https_port").(string)) } // save the https port the system is listening on so internal system can make diff --git a/system/addon/api.go b/system/addon/api.go index 9b54d6e..cd792aa 100644 --- a/system/addon/api.go +++ b/system/addon/api.go @@ -18,8 +18,8 @@ type QueryOptions db.QueryOptions // ContentAll retrives all items from the HTTP API within the provided namespace func ContentAll(namespace string) []byte { - host := db.ConfigCache("domain") - port := db.ConfigCache("http_port") + host := db.ConfigCache("domain").(string) + port := db.ConfigCache("http_port").(string) endpoint := "http://%s:%s/api/contents?type=%s&count=-1" URL := fmt.Sprintf(endpoint, host, port, namespace) @@ -35,8 +35,8 @@ func ContentAll(namespace string) []byte { // Query retrieves a set of content from the HTTP API based on options // and returns the total number of content in the namespace and the content func Query(namespace string, opts QueryOptions) []byte { - host := db.ConfigCache("domain") - port := db.ConfigCache("http_port") + host := db.ConfigCache("domain").(string) + port := db.ConfigCache("http_port").(string) endpoint := "http://%s:%s/api/contents?type=%s&count=%d&offset=%d&order=%s" URL := fmt.Sprintf(endpoint, host, port, namespace, opts.Count, opts.Offset, opts.Order) diff --git a/system/admin/config/config.go b/system/admin/config/config.go index 7b57dc0..c83eb32 100644 --- a/system/admin/config/config.go +++ b/system/admin/config/config.go @@ -16,6 +16,7 @@ type Config struct { AdminEmail string `json:"admin_email"` ClientSecret string `json:"client_secret"` Etag string `json:"etag"` + DisableCORS []string `json:"cors_disabled"` CacheInvalidate []string `json:"cache"` } @@ -49,7 +50,7 @@ func (c *Config) MarshalEditor() ([]byte, error) { }, editor.Field{ View: editor.Input("AdminEmail", c, map[string]string{ - "label": "Adminstrator Email (will be notified of internal system information)", + "label": "Adminstrator Email (notified of internal system information)", }), }, editor.Field{ @@ -65,7 +66,7 @@ func (c *Config) MarshalEditor() ([]byte, error) { }, editor.Field{ View: editor.Input("Etag", c, map[string]string{ - "label": "Etag Header (used for static asset cache)", + "label": "Etag Header (used to cache resources)", "disabled": "true", }), }, @@ -74,6 +75,13 @@ func (c *Config) MarshalEditor() ([]byte, error) { "type": "hidden", }), }, + editor.Field{ + View: editor.Checkbox("DisableCORS", c, map[string]string{ + "label": "Disable CORS (so only " + c.Domain + " can fetch your data)", + }, map[string]string{ + "true": "Disable", + }), + }, editor.Field{ View: editor.Checkbox("CacheInvalidate", c, map[string]string{ "label": "Invalidate cache on save", diff --git a/system/admin/handlers.go b/system/admin/handlers.go index c39fee4..59e7a66 100644 --- a/system/admin/handlers.go +++ b/system/admin/handlers.go @@ -92,7 +92,7 @@ func initHandler(res http.ResponseWriter, req *http.Request) { } // set HTTP port which should be previously added to config cache - port := db.ConfigCache("http_port") + port := db.ConfigCache("http_port").(string) req.Form.Set("http_port", port) // set initial user email as admin_email and make config diff --git a/system/admin/server.go b/system/admin/server.go index f2bf244..991f2d2 100644 --- a/system/admin/server.go +++ b/system/admin/server.go @@ -51,5 +51,7 @@ func Run() { // even if the API server is not running. Otherwise, images/files uploaded // through the editor will not load within the admin system. uploadsDir := filepath.Join(pwd, "uploads") - http.Handle("/api/uploads/", api.Record(db.CacheControl(http.StripPrefix("/api/uploads/", http.FileServer(restrict(http.Dir(uploadsDir))))))) + http.Handle("/api/uploads/", api.Record(api.CORS(db.CacheControl( + http.StripPrefix("/api/uploads/", http.FileServer( + restrict(http.Dir(uploadsDir)))))))) } diff --git a/system/api/handlers.go b/system/api/handlers.go index 1bc4fbb..0be98a4 100644 --- a/system/api/handlers.go +++ b/system/api/handlers.go @@ -254,6 +254,12 @@ func sendPreflight(res http.ResponseWriter) { // CORS wraps a HandleFunc to respond to OPTIONS requests properly func CORS(next http.HandlerFunc) http.HandlerFunc { + if db.ConfigCache("cors_disabled").([]string)[0] == "true" { + return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusForbidden) + }) + } + return db.CacheControl(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { if req.Method == http.MethodOptions { sendPreflight(res) diff --git a/system/api/server.go b/system/api/server.go index f31a748..4b8b22e 100644 --- a/system/api/server.go +++ b/system/api/server.go @@ -4,11 +4,11 @@ import "net/http" // Run adds Handlers to default http listener for API func Run() { - http.HandleFunc("/api/types", CORS(Record(typesHandler))) + http.HandleFunc("/api/types", Record(CORS(typesHandler))) - http.HandleFunc("/api/contents", CORS(Record(contentsHandler))) + http.HandleFunc("/api/contents", Record(CORS(contentsHandler))) - http.HandleFunc("/api/content", CORS(Record(contentHandler))) + http.HandleFunc("/api/content", Record(CORS(contentHandler))) - http.HandleFunc("/api/content/external", CORS(Record(externalContentHandler))) + http.HandleFunc("/api/content/external", Record(CORS(externalContentHandler))) } diff --git a/system/db/cache.go b/system/db/cache.go index 30ecf5a..0120147 100644 --- a/system/db/cache.go +++ b/system/db/cache.go @@ -11,7 +11,7 @@ import ( // CacheControl sets the default cache policy on static asset responses func CacheControl(next http.Handler) http.HandlerFunc { return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { - etag := ConfigCache("etag") + etag := ConfigCache("etag").(string) policy := fmt.Sprintf("max-age=%d, public", 60*60*24*30) res.Header().Add("ETag", etag) res.Header().Add("Cache-Control", policy) diff --git a/system/db/config.go b/system/db/config.go index 45b3952..f1d1215 100644 --- a/system/db/config.go +++ b/system/db/config.go @@ -166,6 +166,6 @@ func PutConfig(key string, value interface{}) error { // ConfigCache is a in-memory cache of the Configs for quicker lookups // 'key' is the JSON tag associated with the config field -func ConfigCache(key string) string { +func ConfigCache(key string) interface{} { return configCache.Get(key) } diff --git a/system/db/init.go b/system/db/init.go index eaf6d76..98ba056 100644 --- a/system/db/init.go +++ b/system/db/init.go @@ -71,7 +71,7 @@ func Init() { } } - clientSecret := ConfigCache("client_secret") + clientSecret := ConfigCache("client_secret").(string) if clientSecret != "" { jwt.Secret([]byte(clientSecret)) diff --git a/system/tls/devcerts.go b/system/tls/devcerts.go index f4dc18f..0554aa4 100644 --- a/system/tls/devcerts.go +++ b/system/tls/devcerts.go @@ -89,7 +89,7 @@ func setupDev() { } hosts := []string{"localhost", "0.0.0.0"} - domain := db.ConfigCache("domain") + domain := db.ConfigCache("domain").(string) if domain != "" { hosts = append(hosts, domain) } diff --git a/system/tls/enable.go b/system/tls/enable.go index f9c16d8..4279b55 100644 --- a/system/tls/enable.go +++ b/system/tls/enable.go @@ -70,7 +70,7 @@ func Enable() { setup() server := &http.Server{ - Addr: fmt.Sprintf(":%s", db.ConfigCache("https_port")), + Addr: fmt.Sprintf(":%s", db.ConfigCache("https_port").(string)), TLSConfig: &tls.Config{GetCertificate: m.GetCertificate}, } -- cgit v1.2.3 From ffa8654af317bfb05b1d38a17aa4a972d1c8056d Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Fri, 13 Jan 2017 13:13:07 -0800 Subject: adding more precise timestamp creation algorithm, and a LoadCacheConfig func to warm the config cache vs. trying to set defaults on init so the cache exists --- system/admin/handlers.go | 2 +- system/admin/upload/upload.go | 2 +- system/api/external.go | 2 +- system/db/config.go | 36 ++++++++++++++++++++++++++++++++++++ system/db/init.go | 17 +++-------------- 5 files changed, 42 insertions(+), 17 deletions(-) diff --git a/system/admin/handlers.go b/system/admin/handlers.go index 59e7a66..00add87 100644 --- a/system/admin/handlers.go +++ b/system/admin/handlers.go @@ -1533,7 +1533,7 @@ func editHandler(res http.ResponseWriter, req *http.Request) { // create a timestamp if one was not set if ts == "" { - ts := fmt.Sprintf("%d", time.Now().Unix()*1000) + ts := fmt.Sprintf("%d", int64(time.Nanosecond)*time.Now().UnixNano()/int64(time.Millisecond)) req.PostForm.Set("timestamp", ts) } diff --git a/system/admin/upload/upload.go b/system/admin/upload/upload.go index 486f55c..6b99dfc 100644 --- a/system/admin/upload/upload.go +++ b/system/admin/upload/upload.go @@ -20,7 +20,7 @@ func StoreFiles(req *http.Request) (map[string]string, error) { ts := req.FormValue("timestamp") // timestamp in milliseconds since unix epoch if ts == "" { - ts = fmt.Sprintf("%d", time.Now().Unix()*1000) // Unix() returns seconds since unix epoch + ts = fmt.Sprintf("%d", int64(time.Nanosecond)*time.Now().UnixNano()/int64(time.Millisecond)) // Unix() returns seconds since unix epoch } req.Form.Set("timestamp", ts) diff --git a/system/api/external.go b/system/api/external.go index 662fc07..5d4b302 100644 --- a/system/api/external.go +++ b/system/api/external.go @@ -61,7 +61,7 @@ func externalContentHandler(res http.ResponseWriter, req *http.Request) { return } - ts := fmt.Sprintf("%d", time.Now().Unix()*1000) + ts := fmt.Sprintf("%d", int64(time.Nanosecond)*time.Now().UnixNano()/int64(time.Millisecond)) req.PostForm.Set("timestamp", ts) req.PostForm.Set("updated", ts) diff --git a/system/db/config.go b/system/db/config.go index f1d1215..ecddd54 100644 --- a/system/db/config.go +++ b/system/db/config.go @@ -169,3 +169,39 @@ func PutConfig(key string, value interface{}) error { func ConfigCache(key string) interface{} { return configCache.Get(key) } + +// LoadCacheConfig loads the config into a cache to be accessed by ConfigCache() +func LoadCacheConfig() error { + c, err := ConfigAll() + if err != nil { + return err + } + + // convert json => map[string]interface{} => url.Values + var kv map[string]interface{} + err = json.Unmarshal(c, &kv) + if err != nil { + return err + } + + data := make(url.Values) + for k, v := range kv { + switch v.(type) { + case []string: + s := v.([]string) + for i := range s { + if i == 0 { + data.Set(k, s[i]) + } + + data.Add(k, s[i]) + } + default: + data.Set(k, fmt.Sprintf("%v", v)) + } + } + + configCache = data + + return nil +} diff --git a/system/db/init.go b/system/db/init.go index 98ba056..769137d 100644 --- a/system/db/init.go +++ b/system/db/init.go @@ -1,10 +1,8 @@ package db import ( - "encoding/json" "log" - "github.com/ponzu-cms/ponzu/system/admin/config" "github.com/ponzu-cms/ponzu/system/item" "github.com/boltdb/bolt" @@ -57,18 +55,9 @@ func Init() { } } - // seed db with configs structure if not present - b := tx.Bucket([]byte("__config")) - if b.Get([]byte("settings")) == nil { - j, err := json.Marshal(&config.Config{}) - if err != nil { - return err - } - - err = b.Put([]byte("settings"), j) - if err != nil { - return err - } + err := LoadCacheConfig() + if err != nil { + return err } clientSecret := ConfigCache("client_secret").(string) -- cgit v1.2.3 From 774f9587527e677a2d1d8a6dc53aead0a8863d36 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Fri, 13 Jan 2017 14:17:59 -0800 Subject: testing issue finding config bucket --- system/db/config.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/db/config.go b/system/db/config.go index ecddd54..31574ab 100644 --- a/system/db/config.go +++ b/system/db/config.go @@ -108,6 +108,9 @@ func ConfigAll() ([]byte, error) { val := &bytes.Buffer{} err := store.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("__config")) + if b == nil { + return fmt.Errorf("Error finding bucket: %s", "__config") + } _, err := val.Write(b.Get([]byte("settings"))) if err != nil { return err -- cgit v1.2.3 From 8340401e97d76fa3ebdf501270e54533f5f9ff27 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Fri, 13 Jan 2017 14:21:57 -0800 Subject: pulling db tx from inside outer db tx --- system/db/init.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/system/db/init.go b/system/db/init.go index 769137d..0e640b1 100644 --- a/system/db/init.go +++ b/system/db/init.go @@ -55,23 +55,23 @@ func Init() { } } - err := LoadCacheConfig() - if err != nil { - return err - } - - clientSecret := ConfigCache("client_secret").(string) - - if clientSecret != "" { - jwt.Secret([]byte(clientSecret)) - } - return nil }) if err != nil { log.Fatalln("Coudn't initialize db with buckets.", err) } + err = LoadCacheConfig() + if err != nil { + log.Fatalln("Failed to load config cache.", err) + } + + clientSecret := ConfigCache("client_secret").(string) + + if clientSecret != "" { + jwt.Secret([]byte(clientSecret)) + } + // invalidate cache on system start err = InvalidateCache() if err != nil { -- cgit v1.2.3 From d1eac138737668e102a2663ee19b15443c42c6ec Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Fri, 13 Jan 2017 14:24:54 -0800 Subject: adding print debug for config []byte value --- system/db/config.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/db/config.go b/system/db/config.go index 31574ab..6f10e65 100644 --- a/system/db/config.go +++ b/system/db/config.go @@ -180,6 +180,8 @@ func LoadCacheConfig() error { return err } + fmt.Println(string(c)) + // convert json => map[string]interface{} => url.Values var kv map[string]interface{} err = json.Unmarshal(c, &kv) -- cgit v1.2.3 From 2669a35c077e694f7200065f7f71c795327ce428 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Fri, 13 Jan 2017 14:26:41 -0800 Subject: adding stop to LoadCacheConfig if there is no config data --- system/db/config.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/system/db/config.go b/system/db/config.go index 6f10e65..1ba71c8 100644 --- a/system/db/config.go +++ b/system/db/config.go @@ -180,7 +180,10 @@ func LoadCacheConfig() error { return err } - fmt.Println(string(c)) + if c == nil { + configCache = make(url.Values) + return nil + } // convert json => map[string]interface{} => url.Values var kv map[string]interface{} -- cgit v1.2.3 From 1e745959284c7e23280ccba63ca9ae1fcf983f90 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Fri, 13 Jan 2017 14:28:36 -0800 Subject: adding stop for PutConfig if there is no config data --- system/db/config.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/system/db/config.go b/system/db/config.go index 1ba71c8..e023729 100644 --- a/system/db/config.go +++ b/system/db/config.go @@ -134,6 +134,10 @@ func PutConfig(key string, value interface{}) error { return err } + if c == nil { + return nil + } + err = json.Unmarshal(c, &kv) if err != nil { return err -- cgit v1.2.3 From 7aee4abfff1fdf588a897470da3413455d8d9f87 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Fri, 13 Jan 2017 14:35:36 -0800 Subject: adding empty config helper in case config has no data --- system/db/config.go | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/system/db/config.go b/system/db/config.go index e023729..8161492 100644 --- a/system/db/config.go +++ b/system/db/config.go @@ -135,7 +135,10 @@ func PutConfig(key string, value interface{}) error { } if c == nil { - return nil + c, err = emptyConfig() + if err != nil { + return err + } } err = json.Unmarshal(c, &kv) @@ -185,8 +188,10 @@ func LoadCacheConfig() error { } if c == nil { - configCache = make(url.Values) - return nil + c, err = emptyConfig() + if err != nil { + return err + } } // convert json => map[string]interface{} => url.Values @@ -217,3 +222,14 @@ func LoadCacheConfig() error { return nil } + +func emptyConfig() ([]byte, error) { + cfg := &config.Config{} + + data, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + return data, nil +} -- cgit v1.2.3 From 1225c90c0d0a49f91c59e119ee3748efdb0b78db Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Fri, 13 Jan 2017 14:57:09 -0800 Subject: updating config model and changing configCache to map[string]interface{} throughout codebase --- system/admin/config/config.go | 2 +- system/db/config.go | 39 +++++++++++++++------------------------ 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/system/admin/config/config.go b/system/admin/config/config.go index c83eb32..5504830 100644 --- a/system/admin/config/config.go +++ b/system/admin/config/config.go @@ -16,7 +16,7 @@ type Config struct { AdminEmail string `json:"admin_email"` ClientSecret string `json:"client_secret"` Etag string `json:"etag"` - DisableCORS []string `json:"cors_disabled"` + DisableCORS []bool `json:"cors_disabled"` CacheInvalidate []string `json:"cache"` } diff --git a/system/db/config.go b/system/db/config.go index 8161492..48da4b0 100644 --- a/system/db/config.go +++ b/system/db/config.go @@ -13,14 +13,15 @@ import ( "github.com/gorilla/schema" ) -var configCache url.Values +var configCache map[string]interface{} func init() { - configCache = make(url.Values) + configCache = make(map[string]interface{}) } // SetConfig sets key:value pairs in the db for configuration settings func SetConfig(data url.Values) error { + var j []byte err := store.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("__config")) @@ -61,7 +62,7 @@ func SetConfig(data url.Values) error { cfg.CacheInvalidate = []string{} } - j, err := json.Marshal(cfg) + j, err = json.Marshal(cfg) if err != nil { return err } @@ -77,7 +78,14 @@ func SetConfig(data url.Values) error { return err } - configCache = data + // convert json => map[string]interface{} + var kv map[string]interface{} + err = json.Unmarshal(j, &kv) + if err != nil { + return err + } + + configCache = kv return nil } @@ -177,7 +185,7 @@ func PutConfig(key string, value interface{}) error { // ConfigCache is a in-memory cache of the Configs for quicker lookups // 'key' is the JSON tag associated with the config field func ConfigCache(key string) interface{} { - return configCache.Get(key) + return configCache[key] } // LoadCacheConfig loads the config into a cache to be accessed by ConfigCache() @@ -194,31 +202,14 @@ func LoadCacheConfig() error { } } - // convert json => map[string]interface{} => url.Values + // convert json => map[string]interface{} var kv map[string]interface{} err = json.Unmarshal(c, &kv) if err != nil { return err } - data := make(url.Values) - for k, v := range kv { - switch v.(type) { - case []string: - s := v.([]string) - for i := range s { - if i == 0 { - data.Set(k, s[i]) - } - - data.Add(k, s[i]) - } - default: - data.Set(k, fmt.Sprintf("%v", v)) - } - } - - configCache = data + configCache = kv return nil } -- cgit v1.2.3 From b020490255870ad2b539c61fd162d519b7c22666 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Fri, 13 Jan 2017 15:05:57 -0800 Subject: adding test to fix bool conversion with schema --- system/admin/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/admin/config/config.go b/system/admin/config/config.go index 5504830..76d97eb 100644 --- a/system/admin/config/config.go +++ b/system/admin/config/config.go @@ -79,7 +79,7 @@ func (c *Config) MarshalEditor() ([]byte, error) { View: editor.Checkbox("DisableCORS", c, map[string]string{ "label": "Disable CORS (so only " + c.Domain + " can fetch your data)", }, map[string]string{ - "true": "Disable", + "on": "Disable", }), }, editor.Field{ -- cgit v1.2.3 From af31558e3d90b887758d9266c5ba80f0d9eaf3dc Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Fri, 13 Jan 2017 15:27:20 -0800 Subject: changing config cors field type --- system/admin/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/admin/config/config.go b/system/admin/config/config.go index 76d97eb..a028ebf 100644 --- a/system/admin/config/config.go +++ b/system/admin/config/config.go @@ -16,7 +16,7 @@ type Config struct { AdminEmail string `json:"admin_email"` ClientSecret string `json:"client_secret"` Etag string `json:"etag"` - DisableCORS []bool `json:"cors_disabled"` + DisableCORS bool `json:"cors_disabled"` CacheInvalidate []string `json:"cache"` } -- cgit v1.2.3 From 317dcbdda41cafbd5a330602b38b9a52fab5ec6f Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Fri, 13 Jan 2017 15:48:38 -0800 Subject: adding test in CORS middleware in fix for start --- system/admin/config/config.go | 2 +- system/admin/handlers.go | 2 +- system/admin/server.go | 3 +++ system/api/handlers.go | 9 ++++----- system/api/server.go | 7 ++++++- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/system/admin/config/config.go b/system/admin/config/config.go index a028ebf..ba12515 100644 --- a/system/admin/config/config.go +++ b/system/admin/config/config.go @@ -79,7 +79,7 @@ func (c *Config) MarshalEditor() ([]byte, error) { View: editor.Checkbox("DisableCORS", c, map[string]string{ "label": "Disable CORS (so only " + c.Domain + " can fetch your data)", }, map[string]string{ - "on": "Disable", + "true": "Disable", }), }, editor.Field{ diff --git a/system/admin/handlers.go b/system/admin/handlers.go index 00add87..2bea356 100644 --- a/system/admin/handlers.go +++ b/system/admin/handlers.go @@ -1533,7 +1533,7 @@ func editHandler(res http.ResponseWriter, req *http.Request) { // create a timestamp if one was not set if ts == "" { - ts := fmt.Sprintf("%d", int64(time.Nanosecond)*time.Now().UnixNano()/int64(time.Millisecond)) + ts = fmt.Sprintf("%d", int64(time.Nanosecond)*time.Now().UnixNano()/int64(time.Millisecond)) req.PostForm.Set("timestamp", ts) } diff --git a/system/admin/server.go b/system/admin/server.go index 991f2d2..8b759e4 100644 --- a/system/admin/server.go +++ b/system/admin/server.go @@ -1,6 +1,7 @@ package admin import ( + "fmt" "log" "net/http" "os" @@ -54,4 +55,6 @@ func Run() { http.Handle("/api/uploads/", api.Record(api.CORS(db.CacheControl( http.StripPrefix("/api/uploads/", http.FileServer( restrict(http.Dir(uploadsDir)))))))) + + fmt.Println("Admin routes registered.") } diff --git a/system/api/handlers.go b/system/api/handlers.go index 0be98a4..de7fbcb 100644 --- a/system/api/handlers.go +++ b/system/api/handlers.go @@ -254,13 +254,12 @@ func sendPreflight(res http.ResponseWriter) { // CORS wraps a HandleFunc to respond to OPTIONS requests properly func CORS(next http.HandlerFunc) http.HandlerFunc { - if db.ConfigCache("cors_disabled").([]string)[0] == "true" { - return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + return db.CacheControl(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + if db.ConfigCache("cors_disabled").(bool) == true { res.WriteHeader(http.StatusForbidden) - }) - } + return + } - return db.CacheControl(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { if req.Method == http.MethodOptions { sendPreflight(res) return diff --git a/system/api/server.go b/system/api/server.go index 4b8b22e..e2af125 100644 --- a/system/api/server.go +++ b/system/api/server.go @@ -1,6 +1,9 @@ package api -import "net/http" +import ( + "fmt" + "net/http" +) // Run adds Handlers to default http listener for API func Run() { @@ -11,4 +14,6 @@ func Run() { http.HandleFunc("/api/content", Record(CORS(contentHandler))) http.HandleFunc("/api/content/external", Record(CORS(externalContentHandler))) + + fmt.Println("API routes registered.") } -- cgit v1.2.3 From 76cd401c05df952de6b305f37a086aa3563f7708 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Fri, 13 Jan 2017 15:52:25 -0800 Subject: checking which services by default are running --- cmd/ponzu/main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/ponzu/main.go b/cmd/ponzu/main.go index 2bee2e5..35b9e8d 100644 --- a/cmd/ponzu/main.go +++ b/cmd/ponzu/main.go @@ -110,6 +110,7 @@ func main() { } case "run": + fmt.Println("Running..") var addTLS string if https { addTLS = "--https" @@ -128,6 +129,8 @@ func main() { services = "admin,api" } + fmt.Println("services:", services) + serve := exec.Command("./ponzu-server", fmt.Sprintf("--port=%d", port), fmt.Sprintf("--httpsport=%d", httpsport), -- cgit v1.2.3 From dfb1d373e6c61da8a28f919f34517350b6b79ef2 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Fri, 13 Jan 2017 15:56:59 -0800 Subject: adding more print debug to check problem --- cmd/ponzu/main.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cmd/ponzu/main.go b/cmd/ponzu/main.go index 35b9e8d..5d8fbef 100644 --- a/cmd/ponzu/main.go +++ b/cmd/ponzu/main.go @@ -153,21 +153,30 @@ func main() { os.Exit(1) } + fmt.Println("serve command executed.") + case "serve", "s": db.Init() defer db.Close() + fmt.Println("called db.Init()") analytics.Init() defer analytics.Close() + fmt.Println("called analytics.Init()") if len(args) > 1 { services := strings.Split(args[1], ",") + fmt.Println("configured to start services:", services) for i := range services { if services[i] == "api" { api.Run() + fmt.Println("called api.Run()") + } else if services[i] == "admin" { admin.Run() + fmt.Println("called admin.Run()") + } else { fmt.Println("To execute 'ponzu serve', you must specify which service to run.") fmt.Println("$ ponzu --help") @@ -179,8 +188,9 @@ func main() { // save the https port the system is listening on err := db.PutConfig("https_port", fmt.Sprintf("%d", httpsport)) if err != nil { - log.Fatalln("System failed to save config. Please try to run again.") + log.Fatalln("System failed to save config. Please try to run again.", err) } + fmt.Println("called db.PutConfig('https_port')") // cannot run production HTTPS and development HTTPS together if devhttps { @@ -203,10 +213,12 @@ func main() { // HTTP api calls while in dev or production w/o adding more cli flags err = db.PutConfig("http_port", fmt.Sprintf("%d", port)) if err != nil { - log.Fatalln("System failed to save config. Please try to run again.") + log.Fatalln("System failed to save config. Please try to run again.", err) } + fmt.Println("called db.PutConfig('http_port')") log.Fatalln(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) + fmt.Println("called http.ListenAndServe()") case "": fmt.Println(usage) -- cgit v1.2.3 From dd3bd260d907bcb42b3a85a5972e387c4c8f805b Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Sun, 15 Jan 2017 19:42:14 -0800 Subject: testing func line unsplit --- system/admin/server.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/system/admin/server.go b/system/admin/server.go index 8b759e4..25ff52c 100644 --- a/system/admin/server.go +++ b/system/admin/server.go @@ -52,9 +52,7 @@ func Run() { // even if the API server is not running. Otherwise, images/files uploaded // through the editor will not load within the admin system. uploadsDir := filepath.Join(pwd, "uploads") - http.Handle("/api/uploads/", api.Record(api.CORS(db.CacheControl( - http.StripPrefix("/api/uploads/", http.FileServer( - restrict(http.Dir(uploadsDir)))))))) + http.Handle("/api/uploads/", api.Record(api.CORS(db.CacheControl(http.StripPrefix("/api/uploads/", http.FileServer(restrict(http.Dir(uploadsDir)))))))) fmt.Println("Admin routes registered.") } -- cgit v1.2.3 From d0f498e05cbe01a3b80a7b500334b55e6dd05e85 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Mon, 16 Jan 2017 09:21:03 -0800 Subject: updated usage to reflect copy on repository readme --- cmd/ponzu/usage.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cmd/ponzu/usage.go b/cmd/ponzu/usage.go index 2dca46d..ee69cf4 100644 --- a/cmd/ponzu/usage.go +++ b/cmd/ponzu/usage.go @@ -10,11 +10,10 @@ var year = fmt.Sprintf("%d", time.Now().Year()) var usageHeader = ` $ ponzu [flags] command -Ponzu is a powerful and efficient open-source "Content-as-a-Service" system -framework and CMS. It provides automatic, free, and secure HTTP/2 over TLS -(certificates obtained via Let's Encrypt - https://letsencrypt.org), a useful -CMS and scaffolding to generate content editors, and a fast HTTP API on which -to build modern applications. +Ponzu is a powerful and efficient open-source HTTP server framework and CMS. It +provides automatic, free, and secure HTTP/2 over TLS (certificates obtained via +[Let's Encrypt](https://letsencrypt.org)), a useful CMS and scaffolding to +generate content editors, and a fast HTTP API on which to build modern applications. Ponzu is released under the BSD-3-Clause license (see LICENSE). (c) ` + year + ` Boss Sauce Creative, LLC -- cgit v1.2.3 From f1fa430c6a9c97fa36a9599746eca0f9a474073b Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Mon, 16 Jan 2017 09:33:44 -0800 Subject: adding generator option to select what to generate --- cmd/ponzu/main.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/cmd/ponzu/main.go b/cmd/ponzu/main.go index 5d8fbef..b07a249 100644 --- a/cmd/ponzu/main.go +++ b/cmd/ponzu/main.go @@ -80,7 +80,7 @@ func main() { case "new": if len(args) < 2 { - fmt.Println(usage) + fmt.Println(usageNew) os.Exit(0) } @@ -91,15 +91,19 @@ func main() { } case "generate", "gen", "g": - if len(args) < 2 { - flag.PrintDefaults() + if len(args) < 3 { + fmt.Println(usageGenerate) os.Exit(0) } - err := generateContentType(args[1:]) - if err != nil { - fmt.Println(err) - os.Exit(1) + // check what we are asked to generate + switch args[2] { + case "content", "c": + err := generateContentType(args[2:]) + if err != nil { + fmt.Println(err) + os.Exit(1) + } } case "build": -- cgit v1.2.3 From 920a30b1ee400da06530e39997c6545c260736fe Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Mon, 16 Jan 2017 09:53:50 -0800 Subject: update readme and usage, debug print fmt --- README.md | 21 ++++++++------------- cmd/ponzu/main.go | 1 + cmd/ponzu/usage.go | 11 +++-------- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 3df2090..0c91bc0 100644 --- a/README.md +++ b/README.md @@ -80,22 +80,17 @@ Errors will be reported, but successful commands return nothing. ### generate, gen, g -Generate a content type file with boilerplate code to implement -the editor.Editable interface. Must be given one (1) parameter of -the name of the type for the new content. The fields following a -type determine the field names and types of the content struct to -be generated. These must be in the following format: -fieldName:"T" +Generate boilerplate code for various Ponzu components, such as `content`. Example: ```bash - struct fields and built-in types... - | - v -$ ponzu gen review title:"string" body:"string" rating:"int" tags:"[]string" - ^ - | - struct type + generator struct fields and built-in types... + | | + v v +$ ponzu gen content review title:"string" body:"string" rating:"int" tags:"[]string" + ^ + | + struct type ``` The command above will generate the file `content/review.go` with boilerplate diff --git a/cmd/ponzu/main.go b/cmd/ponzu/main.go index b07a249..d2ec4d8 100644 --- a/cmd/ponzu/main.go +++ b/cmd/ponzu/main.go @@ -99,6 +99,7 @@ func main() { // check what we are asked to generate switch args[2] { case "content", "c": + fmt.Println(args, "|", args[2]) err := generateContentType(args[2:]) if err != nil { fmt.Println(err) diff --git a/cmd/ponzu/usage.go b/cmd/ponzu/usage.go index ee69cf4..2d7a3b8 100644 --- a/cmd/ponzu/usage.go +++ b/cmd/ponzu/usage.go @@ -54,17 +54,12 @@ new : ` var usageGenerate = ` -generate, gen, g : +generate, gen, g : - Generate a content type file with boilerplate code to implement - the editor.Editable interface. Must be given one (1) parameter of - the name of the type for the new content. The fields following a - type determine the field names and types of the content struct to - be generated. These must be in the following format: - fieldName:"T" + Generate boilerplate code for various Ponzu components, such as 'content'. Example: - $ ponzu gen review title:"string" body:"string" rating:"int" tags:"[]string" + $ ponzu gen content review title:"string" body:"string" rating:"int" tags:"[]string" The command above will generate a file 'content/review.go' with boilerplate methods, as well as struct definition, and cooresponding field tags like: -- cgit v1.2.3 From 89a43c6734a4375b4c6d5b76034020505965baac Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Mon, 16 Jan 2017 10:34:51 -0800 Subject: removing fmt debug and changing index of args to match generator --- cmd/ponzu/main.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/ponzu/main.go b/cmd/ponzu/main.go index d2ec4d8..e9c4354 100644 --- a/cmd/ponzu/main.go +++ b/cmd/ponzu/main.go @@ -97,14 +97,16 @@ func main() { } // check what we are asked to generate - switch args[2] { + switch args[1] { case "content", "c": - fmt.Println(args, "|", args[2]) err := generateContentType(args[2:]) if err != nil { fmt.Println(err) os.Exit(1) } + default: + msg := fmt.Sprintf("Generator '%s' is not implemented.", args[1]) + fmt.Println(msg) } case "build": -- cgit v1.2.3 From cb4f6cd182bf538b2b7c297e56d6f7665607d19f Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Mon, 16 Jan 2017 10:35:15 -0800 Subject: adding cors helper func --- system/api/handlers.go | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/system/api/handlers.go b/system/api/handlers.go index de7fbcb..071a373 100644 --- a/system/api/handlers.go +++ b/system/api/handlers.go @@ -234,10 +234,14 @@ func toJSON(data []string) ([]byte, error) { // sendData() should be used any time you want to communicate // data back to a foreign client func sendData(res http.ResponseWriter, data []byte, code int) { - res.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type") - res.Header().Set("Access-Control-Allow-Origin", "*") + res, cors := responseWithCORS(res) + if !cors { + return + } + res.Header().Set("Content-Type", "application/json") res.WriteHeader(code) + _, err := res.Write(data) if err != nil { log.Println("Error writing to response in sendData") @@ -252,14 +256,23 @@ func sendPreflight(res http.ResponseWriter) { return } +func responseWithCORS(res http.ResponseWriter) (http.ResponseWriter, bool) { + if db.ConfigCache("cors_disabled").(bool) == true { + // disallow request + res.WriteHeader(http.StatusForbidden) + return res, false + } + + // apply 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 HandleFunc 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) { - if db.ConfigCache("cors_disabled").(bool) == true { - res.WriteHeader(http.StatusForbidden) - return - } - if req.Method == http.MethodOptions { sendPreflight(res) return -- cgit v1.2.3 From 957926591fd8fc2adacf04f410e5127dec259c85 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Mon, 16 Jan 2017 10:56:48 -0800 Subject: clear up debug artifacts and add info to console messages --- cmd/ponzu/main.go | 15 ++------------- system/admin/server.go | 3 --- system/api/handlers.go | 2 +- system/api/server.go | 7 +------ system/db/content.go | 3 --- 5 files changed, 4 insertions(+), 26 deletions(-) diff --git a/cmd/ponzu/main.go b/cmd/ponzu/main.go index e9c4354..e90d318 100644 --- a/cmd/ponzu/main.go +++ b/cmd/ponzu/main.go @@ -117,7 +117,6 @@ func main() { } case "run": - fmt.Println("Running..") var addTLS string if https { addTLS = "--https" @@ -136,8 +135,6 @@ func main() { services = "admin,api" } - fmt.Println("services:", services) - serve := exec.Command("./ponzu-server", fmt.Sprintf("--port=%d", port), fmt.Sprintf("--httpsport=%d", httpsport), @@ -160,29 +157,22 @@ func main() { os.Exit(1) } - fmt.Println("serve command executed.") - case "serve", "s": db.Init() defer db.Close() - fmt.Println("called db.Init()") analytics.Init() defer analytics.Close() - fmt.Println("called analytics.Init()") if len(args) > 1 { services := strings.Split(args[1], ",") - fmt.Println("configured to start services:", services) for i := range services { if services[i] == "api" { api.Run() - fmt.Println("called api.Run()") } else if services[i] == "admin" { admin.Run() - fmt.Println("called admin.Run()") } else { fmt.Println("To execute 'ponzu serve', you must specify which service to run.") @@ -197,7 +187,6 @@ func main() { if err != nil { log.Fatalln("System failed to save config. Please try to run again.", err) } - fmt.Println("called db.PutConfig('https_port')") // cannot run production HTTPS and development HTTPS together if devhttps { @@ -222,10 +211,10 @@ func main() { if err != nil { log.Fatalln("System failed to save config. Please try to run again.", err) } - fmt.Println("called db.PutConfig('http_port')") + fmt.Printf("Server listening on :%d for HTTP requests...\n", port) + fmt.Println("\nvisit `/admin` to get started.") log.Fatalln(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) - fmt.Println("called http.ListenAndServe()") case "": fmt.Println(usage) diff --git a/system/admin/server.go b/system/admin/server.go index 25ff52c..11bfe6f 100644 --- a/system/admin/server.go +++ b/system/admin/server.go @@ -1,7 +1,6 @@ package admin import ( - "fmt" "log" "net/http" "os" @@ -53,6 +52,4 @@ func Run() { // through the editor will not load within the admin system. uploadsDir := filepath.Join(pwd, "uploads") http.Handle("/api/uploads/", api.Record(api.CORS(db.CacheControl(http.StripPrefix("/api/uploads/", http.FileServer(restrict(http.Dir(uploadsDir)))))))) - - fmt.Println("Admin routes registered.") } diff --git a/system/api/handlers.go b/system/api/handlers.go index 071a373..2473c24 100644 --- a/system/api/handlers.go +++ b/system/api/handlers.go @@ -231,7 +231,7 @@ func toJSON(data []string) ([]byte, error) { return buf.Bytes(), nil } -// sendData() should be used any time you want to communicate +// sendData should be used any time you want to communicate // data back to a foreign client func sendData(res http.ResponseWriter, data []byte, code int) { res, cors := responseWithCORS(res) diff --git a/system/api/server.go b/system/api/server.go index e2af125..4b8b22e 100644 --- a/system/api/server.go +++ b/system/api/server.go @@ -1,9 +1,6 @@ package api -import ( - "fmt" - "net/http" -) +import "net/http" // Run adds Handlers to default http listener for API func Run() { @@ -14,6 +11,4 @@ func Run() { http.HandleFunc("/api/content", Record(CORS(contentHandler))) http.HandleFunc("/api/content/external", Record(CORS(externalContentHandler))) - - fmt.Println("API routes registered.") } diff --git a/system/db/content.go b/system/db/content.go index b8d9cb8..010e5cb 100644 --- a/system/db/content.go +++ b/system/db/content.go @@ -450,13 +450,11 @@ func SortContent(namespace string) { bname := []byte(namespace + "__sorted") err := tx.DeleteBucket(bname) if err != nil && err != bolt.ErrBucketNotFound { - fmt.Println("Error in DeleteBucket") return err } b, err := tx.CreateBucketIfNotExists(bname) if err != nil { - fmt.Println("Error in CreateBucketIfNotExists") return err } @@ -465,7 +463,6 @@ func SortContent(namespace string) { cid := fmt.Sprintf("%d:%d", i, posts[i].Time()) err = b.Put([]byte(cid), bb[i]) if err != nil { - fmt.Println("Error in Put") return err } } -- cgit v1.2.3 From b67aa10ca519107ae43520aa46b45601549a7332 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Mon, 16 Jan 2017 12:46:34 -0800 Subject: minor optimization in db Updates, added gzip responses for API requests, added better CORS control --- system/api/handlers.go | 54 +++++++++++++++++++++++++++------- system/api/server.go | 4 +-- system/db/addon.go | 22 +++++++++++--- system/db/config.go | 78 ++++++++++++++++++++++++++------------------------ system/db/content.go | 46 ++++++++++++++++++++++------- system/db/init.go | 3 ++ system/db/user.go | 20 ++++++++++++- 7 files changed, 162 insertions(+), 65 deletions(-) diff --git a/system/api/handlers.go b/system/api/handlers.go index 2473c24..f073e16 100644 --- a/system/api/handlers.go +++ b/system/api/handlers.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "compress/gzip" "encoding/json" "log" "net/http" @@ -13,6 +14,7 @@ import ( "github.com/ponzu-cms/ponzu/system/item" ) +// deprecating from API, but going to provide code here in case someone wants it func typesHandler(res http.ResponseWriter, req *http.Request) { var types = []string{} for t, fn := range item.Types { @@ -27,7 +29,7 @@ func typesHandler(res http.ResponseWriter, req *http.Request) { return } - sendData(res, j, http.StatusOK) + sendData(res, req, j, http.StatusOK) } func contentsHandler(res http.ResponseWriter, req *http.Request) { @@ -91,7 +93,7 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) { return } - sendData(res, j, http.StatusOK) + sendData(res, req, j, http.StatusOK) } func contentHandler(res http.ResponseWriter, req *http.Request) { @@ -134,7 +136,7 @@ func contentHandler(res http.ResponseWriter, req *http.Request) { return } - sendData(res, j, http.StatusOK) + sendData(res, req, j, http.StatusOK) } func contentHandlerBySlug(res http.ResponseWriter, req *http.Request) { @@ -171,7 +173,7 @@ func contentHandlerBySlug(res http.ResponseWriter, req *http.Request) { return } - sendData(res, j, http.StatusOK) + sendData(res, req, j, http.StatusOK) } func hide(it interface{}, res http.ResponseWriter, req *http.Request) bool { @@ -233,8 +235,8 @@ func toJSON(data []string) ([]byte, error) { // sendData should be used any time you want to communicate // data back to a foreign client -func sendData(res http.ResponseWriter, data []byte, code int) { - res, cors := responseWithCORS(res) +func sendData(res http.ResponseWriter, req *http.Request, data []byte, code int) { + res, cors := responseWithCORS(res, req) if !cors { return } @@ -256,21 +258,34 @@ func sendPreflight(res http.ResponseWriter) { return } -func responseWithCORS(res http.ResponseWriter) (http.ResponseWriter, bool) { +func responseWithCORS(res http.ResponseWriter, req *http.Request) (http.ResponseWriter, bool) { if db.ConfigCache("cors_disabled").(bool) == true { + // check origin matches config domain and Allow + domain := db.ConfigCache("domain").(string) + + // currently, this will check for exact match. will need feedback to + // determine if subdomains should be allowed or allow multiple domains + // in config + if req.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 CORS headers and return + // 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 HandleFunc to respond to OPTIONS requests properly +// 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) { if req.Method == http.MethodOptions { @@ -282,7 +297,7 @@ func CORS(next http.HandlerFunc) http.HandlerFunc { })) } -// Record wraps a HandleFunc to record API requests for analytical purposes +// 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) @@ -290,3 +305,22 @@ func Record(next http.HandlerFunc) http.HandlerFunc { 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) { + // check if req header content-encoding supports gzip + if strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") { + // gzip response data + gzres, err := gzip.NewWriter(res) + if err != nil { + log.Println("Error creating gzip writer in Gzip middleware.") + next.ServeHTTP(res, req) + } + + next.ServeHTTP(gzres, req) + } + + next.ServeHTTP(res, req) + }) +} diff --git a/system/api/server.go b/system/api/server.go index 4b8b22e..0bdc48a 100644 --- a/system/api/server.go +++ b/system/api/server.go @@ -4,11 +4,9 @@ import "net/http" // Run adds Handlers to default http listener for API func Run() { - http.HandleFunc("/api/types", Record(CORS(typesHandler))) - http.HandleFunc("/api/contents", Record(CORS(contentsHandler))) http.HandleFunc("/api/content", Record(CORS(contentHandler))) - http.HandleFunc("/api/content/external", Record(CORS(externalContentHandler))) + http.HandleFunc("/api/content/external", Record(externalContentHandler)) } diff --git a/system/db/addon.go b/system/db/addon.go index f4621fa..a145293 100644 --- a/system/db/addon.go +++ b/system/db/addon.go @@ -24,6 +24,9 @@ func Addon(key string) ([]byte, error) { err := store.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("__addons")) + if b == nil { + return bolt.ErrBucketNotFound + } val := b.Get([]byte(key)) @@ -56,12 +59,16 @@ func SetAddon(data url.Values, kind interface{}) error { v, err := json.Marshal(kind) + k := data.Get("addon_reverse_dns") + if k == "" { + name := data.Get("addon_name") + return fmt.Errorf(`Addon "%s" has no identifier to use as key.`, name) + } + err = store.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("__addons")) - k := data.Get("addon_reverse_dns") - if k == "" { - name := data.Get("addon_name") - return fmt.Errorf(`Addon "%s" has no identifier to use as key.`, name) + if b == nil { + return bolt.ErrBucketNotFound } err := b.Put([]byte(k), v) @@ -84,6 +91,10 @@ func AddonAll() [][]byte { err := store.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("__addons")) + if b == nil { + return bolt.ErrBucketNotFound + } + err := b.ForEach(func(k, v []byte) error { all = append(all, v) @@ -107,6 +118,9 @@ func AddonAll() [][]byte { func DeleteAddon(key string) error { err := store.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("__addons")) + if b == nil { + bolt.ErrBucketNotFound + } if err := b.Delete([]byte(key)); err != nil { return err diff --git a/system/db/config.go b/system/db/config.go index 48da4b0..5a93353 100644 --- a/system/db/config.go +++ b/system/db/config.go @@ -22,49 +22,53 @@ func init() { // SetConfig sets key:value pairs in the db for configuration settings func SetConfig(data url.Values) error { var j []byte - err := store.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("__config")) - // check for any multi-value fields (ex. checkbox fields) - // and correctly format for db storage. Essentially, we need - // fieldX.0: value1, fieldX.1: value2 => fieldX: []string{value1, value2} - var discardKeys []string - for k, v := range data { - if strings.Contains(k, ".") { - key := strings.Split(k, ".")[0] - - if data.Get(key) == "" { - data.Set(key, v[0]) - } else { - data.Add(key, v[0]) - } - - discardKeys = append(discardKeys, k) + // check for any multi-value fields (ex. checkbox fields) + // and correctly format for db storage. Essentially, we need + // fieldX.0: value1, fieldX.1: value2 => fieldX: []string{value1, value2} + var discardKeys []string + for k, v := range data { + if strings.Contains(k, ".") { + key := strings.Split(k, ".")[0] + + if data.Get(key) == "" { + data.Set(key, v[0]) + } else { + data.Add(key, v[0]) } - } - for _, discardKey := range discardKeys { - data.Del(discardKey) + discardKeys = append(discardKeys, k) } + } - cfg := &config.Config{} - dec := schema.NewDecoder() - dec.SetAliasTag("json") // allows simpler struct tagging when creating a content type - dec.IgnoreUnknownKeys(true) // will skip over form values submitted, but not in struct - err := dec.Decode(cfg, data) - if err != nil { - return err - } + for _, discardKey := range discardKeys { + data.Del(discardKey) + } - // check for "invalidate" value to reset the Etag - if len(cfg.CacheInvalidate) > 0 && cfg.CacheInvalidate[0] == "invalidate" { - cfg.Etag = NewEtag() - cfg.CacheInvalidate = []string{} - } + cfg := &config.Config{} + dec := schema.NewDecoder() + dec.SetAliasTag("json") // allows simpler struct tagging when creating a content type + dec.IgnoreUnknownKeys(true) // will skip over form values submitted, but not in struct + err := dec.Decode(cfg, data) + if err != nil { + return err + } - j, err = json.Marshal(cfg) - if err != nil { - return err + // check for "invalidate" value to reset the Etag + if len(cfg.CacheInvalidate) > 0 && cfg.CacheInvalidate[0] == "invalidate" { + cfg.Etag = NewEtag() + cfg.CacheInvalidate = []string{} + } + + j, err = json.Marshal(cfg) + if err != nil { + return err + } + + err = store.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte("__config")) + if b == nil { + return bolt.ErrBucketNotFound } err = b.Put([]byte("settings"), j) @@ -117,7 +121,7 @@ func ConfigAll() ([]byte, error) { err := store.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("__config")) if b == nil { - return fmt.Errorf("Error finding bucket: %s", "__config") + return bolt.ErrBucketNotFound } _, err := val.Write(b.Get([]byte("settings"))) if err != nil { diff --git a/system/db/content.go b/system/db/content.go index 010e5cb..d9096ae 100644 --- a/system/db/content.go +++ b/system/db/content.go @@ -49,17 +49,17 @@ func update(ns, id string, data url.Values) (int, error) { return 0, err } + j, err := postToJSON(ns, data) + if err != nil { + return 0, err + } + err = store.Update(func(tx *bolt.Tx) error { b, err := tx.CreateBucketIfNotExists([]byte(ns + specifier)) if err != nil { return err } - j, err := postToJSON(ns, data) - if err != nil { - return err - } - err = b.Put([]byte(fmt.Sprintf("%d", cid)), j) if err != nil { return err @@ -134,6 +134,10 @@ func insert(ns string, data url.Values) (int, error) { // store the slug,type:id in contentIndex if public content if specifier == "" { ci := tx.Bucket([]byte("__contentIndex")) + if ci == nil { + return bolt.ErrBucketNotFound + } + k := []byte(data.Get("slug")) v := []byte(fmt.Sprintf("%s:%d", ns, effectedID)) err := ci.Put(k, v) @@ -168,7 +172,12 @@ func DeleteContent(target string, data url.Values) error { ns, id := t[0], t[1] err := store.Update(func(tx *bolt.Tx) error { - err := tx.Bucket([]byte(ns)).Delete([]byte(id)) + b := tx.Bucket([]byte(ns)) + if b == nil { + return bolt.ErrBucketNotFound + } + + err := b.Delete([]byte(id)) if err != nil { return err } @@ -176,7 +185,12 @@ func DeleteContent(target string, data url.Values) error { // if content has a slug, also delete it from __contentIndex slug := data.Get("slug") if slug != "" { - err := tx.Bucket([]byte("__contentIndex")).Delete([]byte(slug)) + ci := tx.Bucket([]byte("__contentIndex")) + if ci == nil { + return bolt.ErrBucketNotFound + } + + err := ci.Delete([]byte(slug)) if err != nil { return err } @@ -212,6 +226,10 @@ func Content(target string) ([]byte, error) { val := &bytes.Buffer{} err := store.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(ns)) + if b == nil { + return bolt.ErrBucketNotFound + } + _, err := val.Write(b.Get([]byte(id))) if err != nil { log.Println(err) @@ -235,6 +253,9 @@ func ContentBySlug(slug string) (string, []byte, error) { var t, id string err := store.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("__contentIndex")) + if b == nil { + return bolt.ErrBucketNotFound + } idx := b.Get([]byte(slug)) if idx != nil { @@ -248,6 +269,9 @@ func ContentBySlug(slug string) (string, []byte, error) { } c := tx.Bucket([]byte(t)) + if c == nil { + return bolt.ErrBucketNotFound + } _, err := val.Write(c.Get([]byte(id))) if err != nil { return err @@ -267,9 +291,8 @@ func ContentAll(namespace string) [][]byte { var posts [][]byte store.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(namespace)) - if b == nil { - return nil + return bolt.ErrBucketNotFound } numKeys := b.Stats().KeyN @@ -313,7 +336,7 @@ func Query(namespace string, opts QueryOptions) (int, [][]byte) { store.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(namespace)) if b == nil { - return nil + return bolt.ErrBucketNotFound } c := b.Cursor() @@ -535,6 +558,9 @@ func checkSlugForDuplicate(slug string) (string, error) { // check for existing slug in __contentIndex err := store.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("__contentIndex")) + if b == nil { + return bolt.ErrBucketNotFound + } original := slug exists := true i := 0 diff --git a/system/db/init.go b/system/db/init.go index 0e640b1..9125d3b 100644 --- a/system/db/init.go +++ b/system/db/init.go @@ -92,6 +92,9 @@ func SystemInitComplete() bool { err := store.View(func(tx *bolt.Tx) error { users := tx.Bucket([]byte("__users")) + if users == nil { + return bolt.ErrBucketNotFound + } err := users.ForEach(func(k, v []byte) error { complete = true diff --git a/system/db/user.go b/system/db/user.go index 02fda95..164ae7b 100644 --- a/system/db/user.go +++ b/system/db/user.go @@ -26,6 +26,9 @@ func SetUser(usr *user.User) (int, error) { err := store.Update(func(tx *bolt.Tx) error { email := []byte(usr.Email) users := tx.Bucket([]byte("__users")) + if users == nil { + return bolt.ErrBucketNotFound + } // check if user is found by email, fail if nil exists := users.Get(email) @@ -69,6 +72,9 @@ func UpdateUser(usr, updatedUsr *user.User) error { err := store.Update(func(tx *bolt.Tx) error { users := tx.Bucket([]byte("__users")) + if users == nil { + return bolt.ErrBucketNotFound + } // check if user is found by email, fail if nil exists := users.Get([]byte(usr.Email)) @@ -110,6 +116,10 @@ func UpdateUser(usr, updatedUsr *user.User) error { func DeleteUser(email string) error { err := store.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("__users")) + if b == nil { + return bolt.ErrBucketNotFound + } + err := b.Delete([]byte(email)) if err != nil { return err @@ -129,6 +139,10 @@ func User(email string) ([]byte, error) { val := &bytes.Buffer{} err := store.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("__users")) + if b == nil { + return bolt.ErrBucketNotFound + } + usr := b.Get([]byte(email)) _, err := val.Write(usr) @@ -154,6 +168,10 @@ func UserAll() ([][]byte, error) { var users [][]byte err := store.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("__users")) + if b == nil { + return bolt.ErrBucketNotFound + } + err := b.ForEach(func(k, v []byte) error { users = append(users, v) return nil @@ -230,7 +248,7 @@ func RecoveryKey(email string) (string, error) { err := store.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("__recoveryKeys")) if b == nil { - return errors.New("No database found for checking keys.") + return bolt.ErrBucketNotFound } _, err := key.Write(b.Get([]byte(email))) -- cgit v1.2.3 From 1e249d23b741ce7f0908fd0160184fac184439a9 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Mon, 16 Jan 2017 13:04:08 -0800 Subject: fixing cors change and gzip response writer --- system/api/handlers.go | 36 +++++++++++++++++++++++++++++++----- system/db/addon.go | 2 +- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/system/api/handlers.go b/system/api/handlers.go index f073e16..5bdb7cd 100644 --- a/system/api/handlers.go +++ b/system/api/handlers.go @@ -4,8 +4,10 @@ import ( "bytes" "compress/gzip" "encoding/json" + "io" "log" "net/http" + "net/url" "strconv" "strings" @@ -262,11 +264,19 @@ func responseWithCORS(res http.ResponseWriter, req *http.Request) (http.Response if db.ConfigCache("cors_disabled").(bool) == true { // check origin matches config domain and Allow 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 + } + + 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 req.Origin == domain { + 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) @@ -312,10 +322,9 @@ func Gzip(next http.HandlerFunc) http.HandlerFunc { // check if req header content-encoding supports gzip if strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") { // gzip response data - gzres, err := gzip.NewWriter(res) - if err != nil { - log.Println("Error creating gzip writer in Gzip middleware.") - next.ServeHTTP(res, req) + gzres := gzipResponseWriter{ + rw: res, + gw: gzip.NewWriter(res), } next.ServeHTTP(gzres, req) @@ -324,3 +333,20 @@ func Gzip(next http.HandlerFunc) http.HandlerFunc { next.ServeHTTP(res, req) }) } + +type gzipResponseWriter struct { + rw http.ResponseWriter + gw io.Writer +} + +func (gzw gzipResponseWriter) Header() http.Header { + return gzw.rw.Header() +} + +func (gzw gzipResponseWriter) Write(p []byte) (int, error) { + return gzw.gw.Write(p) +} + +func (gzw gzipResponseWriter) WriteHeader(code int) { + gzw.rw.WriteHeader(code) +} diff --git a/system/db/addon.go b/system/db/addon.go index a145293..0f63405 100644 --- a/system/db/addon.go +++ b/system/db/addon.go @@ -119,7 +119,7 @@ func DeleteAddon(key string) error { err := store.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("__addons")) if b == nil { - bolt.ErrBucketNotFound + return bolt.ErrBucketNotFound } if err := b.Delete([]byte(key)); err != nil { -- cgit v1.2.3 From d2647904359bc79090583a66197768eb7de462b3 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Mon, 16 Jan 2017 15:17:49 -0800 Subject: fix gzip middleware and remove code from sendData func --- system/api/handlers.go | 51 ++++++++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/system/api/handlers.go b/system/api/handlers.go index 5bdb7cd..c3421f9 100644 --- a/system/api/handlers.go +++ b/system/api/handlers.go @@ -4,7 +4,6 @@ import ( "bytes" "compress/gzip" "encoding/json" - "io" "log" "net/http" "net/url" @@ -31,7 +30,7 @@ func typesHandler(res http.ResponseWriter, req *http.Request) { return } - sendData(res, req, j, http.StatusOK) + sendData(res, req, j) } func contentsHandler(res http.ResponseWriter, req *http.Request) { @@ -95,7 +94,7 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) { return } - sendData(res, req, j, http.StatusOK) + sendData(res, req, j) } func contentHandler(res http.ResponseWriter, req *http.Request) { @@ -138,7 +137,7 @@ func contentHandler(res http.ResponseWriter, req *http.Request) { return } - sendData(res, req, j, http.StatusOK) + sendData(res, req, j) } func contentHandlerBySlug(res http.ResponseWriter, req *http.Request) { @@ -175,7 +174,7 @@ func contentHandlerBySlug(res http.ResponseWriter, req *http.Request) { return } - sendData(res, req, j, http.StatusOK) + sendData(res, req, j) } func hide(it interface{}, res http.ResponseWriter, req *http.Request) bool { @@ -237,14 +236,9 @@ func toJSON(data []string) ([]byte, error) { // 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, code int) { - res, cors := responseWithCORS(res, req) - if !cors { - return - } - +func sendData(res http.ResponseWriter, req *http.Request, data []byte) { res.Header().Set("Content-Type", "application/json") - res.WriteHeader(code) + res.Header().Set("Vary", "Accept-Encoding") _, err := res.Write(data) if err != nil { @@ -262,7 +256,7 @@ func sendPreflight(res http.ResponseWriter) { func responseWithCORS(res http.ResponseWriter, req *http.Request) (http.ResponseWriter, bool) { if db.ConfigCache("cors_disabled").(bool) == true { - // check origin matches config domain and Allow + // check origin matches config domain domain := db.ConfigCache("domain").(string) origin := req.Header.Get("Origin") u, err := url.Parse(origin) @@ -271,6 +265,11 @@ func responseWithCORS(res http.ResponseWriter, req *http.Request) (http.Response 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 @@ -298,6 +297,11 @@ func responseWithCORS(res http.ResponseWriter, req *http.Request) (http.Response // 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 @@ -322,12 +326,12 @@ func Gzip(next http.HandlerFunc) http.HandlerFunc { // check if req header content-encoding supports gzip if strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") { // gzip response data - gzres := gzipResponseWriter{ - rw: res, - gw: gzip.NewWriter(res), - } + res.Header().Set("Content-Encoding", "gzip") + gzres := gzipResponseWriter{res, gzip.NewWriter(res)} next.ServeHTTP(gzres, req) + + return } next.ServeHTTP(res, req) @@ -335,18 +339,11 @@ func Gzip(next http.HandlerFunc) http.HandlerFunc { } type gzipResponseWriter struct { - rw http.ResponseWriter - gw io.Writer -} - -func (gzw gzipResponseWriter) Header() http.Header { - return gzw.rw.Header() + http.ResponseWriter + gw *gzip.Writer } func (gzw gzipResponseWriter) Write(p []byte) (int, error) { + defer gzw.gw.Close() return gzw.gw.Write(p) } - -func (gzw gzipResponseWriter) WriteHeader(code int) { - gzw.rw.WriteHeader(code) -} -- cgit v1.2.3 From a645a66ecd2d68cb6719cb22bdb91a0480be8659 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Mon, 16 Jan 2017 15:21:52 -0800 Subject: adding gzip compression to api endpoints --- system/api/server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/api/server.go b/system/api/server.go index 0bdc48a..8e103c4 100644 --- a/system/api/server.go +++ b/system/api/server.go @@ -4,9 +4,9 @@ import "net/http" // Run adds Handlers to default http listener for API func Run() { - http.HandleFunc("/api/contents", Record(CORS(contentsHandler))) + http.HandleFunc("/api/contents", Record(CORS(Gzip(contentsHandler)))) - http.HandleFunc("/api/content", Record(CORS(contentHandler))) + http.HandleFunc("/api/content", Record(CORS(Gzip(contentHandler)))) http.HandleFunc("/api/content/external", Record(externalContentHandler)) } -- cgit v1.2.3 From 61ab44352906ae598fce12e47dbd7af9d17988cf Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Mon, 16 Jan 2017 15:40:24 -0800 Subject: adding gzip_disabled config setting --- system/admin/config/config.go | 8 ++++++++ system/api/handlers.go | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/system/admin/config/config.go b/system/admin/config/config.go index ba12515..3605514 100644 --- a/system/admin/config/config.go +++ b/system/admin/config/config.go @@ -17,6 +17,7 @@ type Config struct { ClientSecret string `json:"client_secret"` Etag string `json:"etag"` DisableCORS bool `json:"cors_disabled"` + DisableGZIP bool `json:"gzip_disabled"` CacheInvalidate []string `json:"cache"` } @@ -82,6 +83,13 @@ func (c *Config) MarshalEditor() ([]byte, error) { "true": "Disable", }), }, + editor.Field{ + View: editor.Checkbox("DisableGZIP", c, map[string]string{ + "label": "Disable GZIP (will increase server speed, but also bandwidth)", + }, map[string]string{ + "true": "Disable", + }), + }, editor.Field{ View: editor.Checkbox("CacheInvalidate", c, map[string]string{ "label": "Invalidate cache on save", diff --git a/system/api/handlers.go b/system/api/handlers.go index c3421f9..9292e15 100644 --- a/system/api/handlers.go +++ b/system/api/handlers.go @@ -323,6 +323,11 @@ func Record(next http.HandlerFunc) http.HandlerFunc { // 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 @@ -330,7 +335,6 @@ func Gzip(next http.HandlerFunc) http.HandlerFunc { gzres := gzipResponseWriter{res, gzip.NewWriter(res)} next.ServeHTTP(gzres, req) - return } -- cgit v1.2.3 From 316d456cd990182ba7136a3550f6d6e3e9b4b960 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Mon, 16 Jan 2017 15:44:31 -0800 Subject: adding different identifier to checkbox in config --- system/admin/config/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/admin/config/config.go b/system/admin/config/config.go index 3605514..0d55700 100644 --- a/system/admin/config/config.go +++ b/system/admin/config/config.go @@ -80,14 +80,14 @@ func (c *Config) MarshalEditor() ([]byte, error) { View: editor.Checkbox("DisableCORS", c, map[string]string{ "label": "Disable CORS (so only " + c.Domain + " can fetch your data)", }, map[string]string{ - "true": "Disable", + "true": "Disable CORS", }), }, editor.Field{ View: editor.Checkbox("DisableGZIP", c, map[string]string{ "label": "Disable GZIP (will increase server speed, but also bandwidth)", }, map[string]string{ - "true": "Disable", + "true": "Disable GZIP", }), }, editor.Field{ -- cgit v1.2.3 From aebdb6778967aec0f05fe7c4da3863c3778907d6 Mon Sep 17 00:00:00 2001 From: Steve Manuel Date: Mon, 16 Jan 2017 17:42:54 -0800 Subject: update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0c91bc0..8a02c02 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ rapid development, but need a fast JSON response in a high-concurrency environme **Because you want to turn this:** ```bash -$ ponzu generate song title:"string" artist:"string" rating:"int" opinion:"string" spotify_url:"string" +$ ponzu generate content song title:"string" artist:"string" rating:"int" opinion:"string" spotify_url:"string" ``` **Into this:** -- cgit v1.2.3