├── .gitignore ├── LICENSE ├── middleware ├── config.go ├── datastore.go └── auth.go ├── models ├── buckets.go └── models.go ├── main.go ├── admin └── admin.go └── gifs └── gifs.go /.gitignore: -------------------------------------------------------------------------------- 1 | giftd 2 | *.db 3 | admin.token 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Christopher Saunders 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /middleware/config.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | 8 | "github.com/zenazn/goji/web" 9 | ) 10 | 11 | const ConfigurationDB string = "configuration-db" 12 | 13 | func InitializeConfiguration(configPath string, configDb interface{}) (func(c *web.C, h http.Handler) http.Handler, error) { 14 | config := map[string]interface{}{ConfigurationDB: configDb} 15 | 16 | err := updateConfiguration(config, configPath) 17 | return configurationMiddleware(config), err 18 | } 19 | 20 | func updateConfiguration(config map[string]interface{}, configPath string) error { 21 | file, err := ioutil.ReadFile(configPath) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | var unmarshalled map[string]interface{} 27 | err = json.Unmarshal(file, &unmarshalled) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | for key, value := range unmarshalled { 33 | config[key] = value 34 | } 35 | return nil 36 | } 37 | 38 | func configurationMiddleware(config map[string]interface{}) func(c *web.C, h http.Handler) http.Handler { 39 | return func(c *web.C, h http.Handler) http.Handler { 40 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 41 | for k, v := range config { 42 | c.Env[k] = v 43 | } 44 | 45 | h.ServeHTTP(w, r) 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /models/buckets.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/boltdb/bolt" 8 | ) 9 | 10 | const protectedApisBucket string = "protected-apis" 11 | const apiClientsBucket string = "api-clients" 12 | const apiClientIdsBucket string = "api-client-ids" 13 | 14 | func dump(header string, c *bolt.Cursor) { 15 | fmt.Println("----------", header, "----------") 16 | printer := func(k, v []byte) bool { 17 | if k == nil { 18 | return false 19 | } 20 | fmt.Println(string(k), "------>", string(v)) 21 | return true 22 | } 23 | printer(c.First()) 24 | for true { 25 | if printer(c.Next()) == false { 26 | break 27 | } 28 | } 29 | } 30 | 31 | func ApiClientsBucket(tx *bolt.Tx) (*bolt.Bucket, error) { 32 | if tx.Writable() { 33 | return tx.CreateBucketIfNotExists([]byte(apiClientsBucket)) 34 | } else { 35 | bucket := tx.Bucket([]byte(apiClientsBucket)) 36 | if bucket == nil { 37 | return nil, bucketMissing(apiClientsBucket) 38 | } 39 | return bucket, nil 40 | } 41 | } 42 | 43 | func ApiClientIdsBucket(tx *bolt.Tx) (*bolt.Bucket, error) { 44 | if tx.Writable() { 45 | return tx.CreateBucketIfNotExists([]byte(apiClientIdsBucket)) 46 | } else { 47 | bucket := tx.Bucket([]byte(apiClientIdsBucket)) 48 | if bucket == nil { 49 | return nil, bucketMissing(apiClientIdsBucket) 50 | } 51 | return bucket, nil 52 | } 53 | 54 | } 55 | 56 | func ApiAccessBucket(tx *bolt.Tx) (*bolt.Bucket, error) { 57 | if tx.Writable() { 58 | return tx.CreateBucketIfNotExists([]byte(protectedApisBucket)) 59 | } else { 60 | bucket := tx.Bucket([]byte(protectedApisBucket)) 61 | if bucket == nil { 62 | return nil, bucketMissing(protectedApisBucket) 63 | } 64 | return bucket, nil 65 | } 66 | } 67 | 68 | func bucketMissing(name string) error { 69 | return errors.New(fmt.Sprintf("buckets: %s does not exist and transaction is not writable", name)) 70 | } 71 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/boltdb/bolt" 12 | "github.com/zenazn/goji" 13 | 14 | "github.com/csaunders/giftd/admin" 15 | "github.com/csaunders/giftd/gifs" 16 | "github.com/csaunders/giftd/middleware" 17 | ) 18 | 19 | const gifsDatabase string = "giftd.db" 20 | const gifsConfigDb string = "giftd-config.db" 21 | const giftdConfig string = "giftd.json" 22 | 23 | var permissions map[string]string = map[string]string{ 24 | `/gifs/[a-z]+/random`: "public", 25 | `/gifs/.{8}-.{4}-.{4}-.{4}-.{12}`: "public", 26 | `/gifs.*`: "gifs-api", 27 | `/admin.*`: "admin-api", 28 | } 29 | 30 | func dbConnect(name string) *bolt.DB { 31 | db, err := bolt.Open(name, 0600, &bolt.Options{Timeout: 1 * time.Second}) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | return db 36 | } 37 | 38 | func writePidfile(pidfile string) { 39 | if len(pidfile) > 0 { 40 | file, err := os.OpenFile(pidfile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | defer file.Close() 45 | pid := syscall.Getpid() 46 | _, err = file.Write([]byte(fmt.Sprintf("%d\n", pid))) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | } 51 | } 52 | 53 | func writeAdminToken(token string) { 54 | if len(token) <= 0 { 55 | return 56 | } 57 | file, err := os.OpenFile("admin.token", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | defer file.Close() 62 | _, err = file.Write([]byte(token)) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | } 67 | 68 | func initialize() error { 69 | var dataDir string 70 | var pidfile string 71 | flag.StringVar(&dataDir, "datadir", "/var/lib/giftd", "Location where giftd data should be stored") 72 | flag.StringVar(&pidfile, "pidfile", "", "Location to write pidfile") 73 | flag.Parse() 74 | 75 | writePidfile(pidfile) 76 | return os.Chdir(dataDir) 77 | } 78 | 79 | func setupPermissionsDb() { 80 | db := dbConnect(gifsConfigDb) 81 | defer db.Close() 82 | for path, scope := range permissions { 83 | err := middleware.SetPermissions(db, path, scope) 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | } 88 | if token, err := middleware.CreateAdministrator(db); err != nil { 89 | log.Fatal("middleware.CreateAdministrator:", err) 90 | } else { 91 | writeAdminToken(token) 92 | } 93 | } 94 | 95 | func main() { 96 | if err := initialize(); err != nil { 97 | log.Fatal(err) 98 | } 99 | setupPermissionsDb() 100 | confDb := dbConnect(gifsConfigDb) 101 | defer confDb.Close() 102 | 103 | gifs.Register("/gifs", middleware.EnvironmentDatabaseProvider) 104 | admin.Register("/admin") 105 | 106 | configMiddleware, err := middleware.InitializeConfiguration(giftdConfig, confDb) 107 | if err != nil { 108 | fmt.Println(err) 109 | } 110 | 111 | goji.Use(configMiddleware) 112 | goji.Use(middleware.APIAccessManagement) 113 | goji.Use(middleware.DatastoreLoader) 114 | goji.Serve() 115 | } 116 | -------------------------------------------------------------------------------- /models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "os" 10 | 11 | "github.com/boltdb/bolt" 12 | ) 13 | 14 | const defaultDatastore string = "giftd.db" 15 | const accessTokenSize int = 32 16 | 17 | var RecordNotFound error = errors.New("record does not exist") 18 | 19 | type set map[string]bool 20 | 21 | func (s set) add(items []string) set { 22 | for _, i := range items { 23 | s[i] = true 24 | } 25 | return s 26 | } 27 | 28 | func (s set) remove(items []string) set { 29 | for _, i := range items { 30 | if s[i] { 31 | delete(s, i) 32 | } 33 | } 34 | return s 35 | } 36 | 37 | func (s set) has(i string) bool { 38 | return s[i] 39 | } 40 | 41 | func (s set) array() []string { 42 | ary := make([]string, len(s)) 43 | idx := 0 44 | for key, _ := range s { 45 | ary[idx] = key 46 | idx++ 47 | } 48 | return ary 49 | } 50 | 51 | type Account struct { 52 | Id string `json:"id"` 53 | Token string `json:"access-token"` 54 | Datastore string `json:"datastore"` 55 | Permissions []string `json:"permissions"` 56 | } 57 | 58 | func NewAccount() (*Account, error) { 59 | var err error 60 | account := &Account{} 61 | if account.Id, err = GenUUID(); err != nil { 62 | return nil, err 63 | } 64 | if account.Token, err = generateToken(accessTokenSize); err != nil { 65 | return nil, err 66 | } 67 | if account.Datastore, err = generateDatastoreName(accessTokenSize); err != nil { 68 | return nil, err 69 | } 70 | return account, nil 71 | } 72 | 73 | func (a *Account) DatastoreName() string { 74 | if len(a.Datastore) <= 0 { 75 | return defaultDatastore 76 | } 77 | return a.Datastore 78 | } 79 | 80 | func (a *Account) SetPermissions(perms []string) { 81 | if len(perms) > 0 { 82 | a.Permissions = perms 83 | } 84 | } 85 | 86 | func (a *Account) SetDatastore(datastore string) { 87 | if len(datastore) > 0 { 88 | a.Datastore = datastore 89 | } 90 | } 91 | 92 | func (a *Account) AddPermissions(perms []string) { 93 | set := make(set).add(a.Permissions).add(perms) 94 | a.Permissions = set.array() 95 | } 96 | 97 | func (a *Account) RemovePermissions(perms []string) { 98 | set := make(set).add(a.Permissions).remove(perms) 99 | a.Permissions = set.array() 100 | } 101 | 102 | func (a *Account) HasPermission(perm string) bool { 103 | set := make(set).add(a.Permissions) 104 | return set.has(perm) 105 | } 106 | 107 | func Save(bucket *bolt.Bucket, key string, record interface{}) error { 108 | if data, err := json.Marshal(record); err != nil { 109 | return err 110 | } else { 111 | return bucket.Put([]byte(key), data) 112 | } 113 | } 114 | 115 | func Load(bucket *bolt.Bucket, key string, record interface{}) error { 116 | var data []byte 117 | var err error 118 | if data, err = LoadRaw(bucket, key); err != nil { 119 | return err 120 | } 121 | return json.Unmarshal(data, record) 122 | } 123 | 124 | func LoadRaw(bucket *bolt.Bucket, key string) ([]byte, error) { 125 | data := bucket.Get([]byte(key)) 126 | if data == nil { 127 | return []byte{}, RecordNotFound 128 | } 129 | return data, nil 130 | } 131 | 132 | func GenUUID() (string, error) { 133 | urandom, err := os.OpenFile("/dev/urandom", os.O_RDONLY, 0) 134 | if err != nil { 135 | return "", err 136 | } 137 | defer urandom.Close() 138 | b := make([]byte, 16) 139 | n, err := urandom.Read(b) 140 | 141 | if err != nil { 142 | return "", err 143 | } else if n != len(b) { 144 | return "", errors.New("Could not read a sufficient number of bytes") 145 | } 146 | uuid := fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) 147 | return uuid, nil 148 | } 149 | 150 | func generateToken(size int) (string, error) { 151 | rb := make([]byte, size) 152 | _, err := rand.Read(rb) 153 | 154 | if err != nil { 155 | return "", err 156 | } 157 | return hex.EncodeToString(rb), nil 158 | } 159 | 160 | func generateDatastoreName(size int) (string, error) { 161 | name, err := generateToken(size) 162 | if err != nil { 163 | return "", err 164 | } 165 | return name + ".db", nil 166 | } 167 | -------------------------------------------------------------------------------- /middleware/datastore.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "regexp" 8 | "sync" 9 | "time" 10 | 11 | "github.com/boltdb/bolt" 12 | "github.com/csaunders/giftd/models" 13 | "github.com/zenazn/goji/web" 14 | ) 15 | 16 | const Datastore string = "datastore" 17 | const AccountDetails string = "account-details" 18 | const AccountId string = "account_id" 19 | 20 | const sels string = "[0-9a-f]" 21 | 22 | var pattern string = fmt.Sprintf("%s{8}-%s{4}-%s{4}-%s{4}-%s{12}", sels, sels, sels, sels, sels) 23 | 24 | var pathPattern *regexp.Regexp = regexp.MustCompile(pattern) 25 | 26 | var cache map[string]*store = map[string]*store{} 27 | 28 | var storeMutex *sync.Mutex = new(sync.Mutex) 29 | 30 | func synchronized(fn func()) { 31 | storeMutex.Lock() 32 | defer storeMutex.Unlock() 33 | fn() 34 | } 35 | 36 | type store struct { 37 | Db *bolt.DB 38 | Wg *sync.WaitGroup 39 | } 40 | 41 | func datastoreCloser(name string, datastore *store) { 42 | datastore.Wg.Wait() 43 | synchronized(func() { 44 | datastore.Db.Close() 45 | if cache[name] != nil { 46 | delete(cache, name) 47 | } 48 | }) 49 | } 50 | 51 | func openDatastore(name string) (*store, error) { 52 | var datastore *store 53 | var err error 54 | synchronized(func() { 55 | datastore = cache[name] 56 | if datastore == nil { 57 | db, err := bolt.Open(name, 0600, &bolt.Options{Timeout: 1 * time.Second}) 58 | if err == nil { 59 | datastore = &store{} 60 | datastore.Db = db 61 | datastore.Wg = new(sync.WaitGroup) 62 | datastore.Wg.Add(1) 63 | cache[name] = datastore 64 | go datastoreCloser(name, datastore) 65 | } 66 | } else { 67 | datastore.Wg.Add(1) 68 | } 69 | }) 70 | 71 | return datastore, err 72 | } 73 | 74 | func datastoreNameFromEnv(c *web.C) (string, error) { 75 | account, ok := c.Env[AccountDetails].(models.Account) 76 | if !ok { 77 | return "", errors.New("No Account Information") 78 | } 79 | return account.DatastoreName(), nil 80 | } 81 | 82 | func datastoreNameFromPath(c *web.C, path string) (string, error) { 83 | var account models.Account 84 | var db *bolt.DB 85 | var ok bool 86 | if db, ok = c.Env[ConfigurationDB].(*bolt.DB); !ok { 87 | return "", errors.New("Cannot load configuration database") 88 | } 89 | err := db.View(func(tx *bolt.Tx) error { 90 | idsBucket, err := models.ApiClientIdsBucket(tx) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | uuids := pathPattern.FindAllString(path, -1) 96 | if uuids == nil { 97 | return errors.New("Cannot load database for path") 98 | } 99 | 100 | var token []byte 101 | for _, uuid := range uuids { 102 | if token = idsBucket.Get([]byte(uuid)); token != nil { 103 | break 104 | } 105 | } 106 | 107 | if token == nil { 108 | return models.RecordNotFound 109 | } 110 | 111 | clientsBucket, err := models.ApiClientsBucket(tx) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | return models.Load(clientsBucket, string(token), &account) 117 | }) 118 | 119 | return account.DatastoreName(), err 120 | } 121 | 122 | func loadDatastore(c *web.C, r *http.Request) (*store, error) { 123 | var name string 124 | var err error 125 | if name, err = datastoreNameFromEnv(c); err != nil { 126 | if name, err = datastoreNameFromPath(c, r.URL.Path); err != nil { 127 | return nil, errors.New("loadDatastore: could not find datastore") 128 | } 129 | } 130 | return openDatastore(name) 131 | } 132 | 133 | func unloadDatastore(datastore *store) { 134 | datastore.Wg.Done() 135 | } 136 | 137 | func DatastoreLoader(c *web.C, h http.Handler) http.Handler { 138 | fn := func(w http.ResponseWriter, r *http.Request) { 139 | datastore, err := loadDatastore(c, r) 140 | if err == nil { 141 | defer unloadDatastore(datastore) 142 | c.Env[Datastore] = datastore.Db 143 | 144 | h.ServeHTTP(w, r) 145 | } else { 146 | w.WriteHeader(http.StatusServiceUnavailable) 147 | w.Write([]byte("It's not you, it's us.")) 148 | w.Write([]byte(err.Error())) 149 | } 150 | } 151 | return http.HandlerFunc(fn) 152 | } 153 | 154 | type DbHandler func(db *bolt.DB, c web.C, w http.ResponseWriter, r *http.Request) 155 | type Initializer func(db *bolt.DB) error 156 | type DatabaseProvider func(init Initializer, handler DbHandler) func(c web.C, w http.ResponseWriter, r *http.Request) 157 | 158 | func EnvironmentDatabaseProvider(init Initializer, handler DbHandler) func(c web.C, w http.ResponseWriter, r *http.Request) { 159 | return func(c web.C, w http.ResponseWriter, r *http.Request) { 160 | var err error 161 | db, ok := c.Env[Datastore].(*bolt.DB) 162 | if ok { 163 | err = init(db) 164 | } 165 | 166 | if err != nil || !ok { 167 | w.WriteHeader(http.StatusServiceUnavailable) 168 | w.Write([]byte("Datastore Unavailable")) 169 | return 170 | } 171 | handler(db, c, w, r) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/boltdb/bolt" 11 | "github.com/csaunders/giftd/models" 12 | "github.com/zenazn/goji/web" 13 | ) 14 | 15 | func deny(w http.ResponseWriter) { 16 | w.WriteHeader(http.StatusUnauthorized) 17 | w.Write([]byte("Access Denied")) 18 | } 19 | 20 | func HasAdministratorToken(db *bolt.DB) (bool, error) { 21 | hasAdmin := false 22 | err := db.Update(func(tx *bolt.Tx) error { 23 | bucket, err := models.ApiClientsBucket(tx) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | bucket.ForEach(func(token, data []byte) error { 29 | fmt.Println(string(data)) 30 | if hasAdmin = strings.Contains(string(data), "admin"); hasAdmin { 31 | return errors.New("") 32 | } 33 | return nil 34 | }) 35 | return nil 36 | }) 37 | return hasAdmin, err 38 | } 39 | 40 | func generateAdministrator(db *bolt.DB) (string, error) { 41 | account, err := models.NewAccount() 42 | if err != nil { 43 | return "", err 44 | } 45 | 46 | account.AddPermissions([]string{"admin"}) 47 | err = db.Update(func(tx *bolt.Tx) error { 48 | bucket, err := models.ApiClientsBucket(tx) 49 | if err != nil { 50 | return err 51 | } 52 | return models.Save(bucket, account.Token, account) 53 | }) 54 | 55 | if err != nil { 56 | return "", err 57 | } 58 | return account.Token, nil 59 | } 60 | 61 | func SetPermissions(db *bolt.DB, path, scope string) error { 62 | _, err := regexp.Compile(path) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | return db.Update(func(tx *bolt.Tx) error { 68 | bucket, err := models.ApiAccessBucket(tx) 69 | if err != nil { 70 | return err 71 | } 72 | return bucket.Put([]byte(path), []byte(scope)) 73 | }) 74 | } 75 | 76 | func CreateAdministrator(db *bolt.DB) (string, error) { 77 | hasAdminToken, err := HasAdministratorToken(db) 78 | if err != nil { 79 | return "", err 80 | } 81 | 82 | if !hasAdminToken { 83 | return generateAdministrator(db) 84 | } 85 | return "", nil 86 | } 87 | 88 | func hasSufficientPermissions(requiredPerms, actualPerms string) bool { 89 | fmt.Println("reqd:", requiredPerms, "actual:", actualPerms) 90 | if strings.Contains(requiredPerms, "public") { 91 | return true 92 | } 93 | 94 | if strings.Contains(actualPerms, "admin") { 95 | return true 96 | } 97 | 98 | sufficientPermissions := false 99 | requiredPermsList := strings.Split(requiredPerms, ",") 100 | for _, perm := range requiredPermsList { 101 | if strings.Contains(actualPerms, perm) { 102 | sufficientPermissions = true 103 | break 104 | } 105 | } 106 | return sufficientPermissions 107 | } 108 | 109 | func permissionsFor(db *bolt.DB, token string) (string, error) { 110 | perms := "" 111 | err := db.View(func(tx *bolt.Tx) error { 112 | bucket, err := models.ApiClientsBucket(tx) 113 | if err != nil { 114 | return err 115 | } 116 | perms = string(bucket.Get([]byte(token))) 117 | return nil 118 | }) 119 | return perms, err 120 | } 121 | 122 | func canAccess(db *bolt.DB, path, perms string) bool { 123 | if strings.Contains(perms, "admin") { 124 | return true 125 | } 126 | 127 | access := false 128 | db.View(func(tx *bolt.Tx) error { 129 | bucket, err := models.ApiAccessBucket(tx) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | bucket.ForEach(func(pathPattern, requiredPerms []byte) error { 135 | re := regexp.MustCompile(string(pathPattern)) 136 | if re.MatchString(path) { 137 | access = hasSufficientPermissions(string(requiredPerms), perms) 138 | if access { 139 | return errors.New("") 140 | } 141 | } 142 | return nil 143 | }) 144 | return nil 145 | }) 146 | return access 147 | } 148 | 149 | func loadAccount(db *bolt.DB, token string, c *web.C) { 150 | db.View(func(tx *bolt.Tx) error { 151 | var account models.Account 152 | bucket, err := models.ApiClientsBucket(tx) 153 | if err != nil { 154 | return err 155 | } 156 | if err = models.Load(bucket, token, &account); err != nil { 157 | return err 158 | } 159 | c.Env[AccountDetails] = account 160 | return nil 161 | }) 162 | } 163 | 164 | func APIAccessManagement(c *web.C, h http.Handler) http.Handler { 165 | fn := func(w http.ResponseWriter, r *http.Request) { 166 | if c.Env["skipAuth"] != nil { 167 | h.ServeHTTP(w, r) 168 | return 169 | } 170 | 171 | db, ok := c.Env["configuration-db"].(*bolt.DB) 172 | if !ok { 173 | fmt.Println("No configuration database") 174 | deny(w) 175 | return 176 | } 177 | 178 | accessToken := r.Header.Get("Authorization") 179 | perms, err := permissionsFor(db, accessToken) 180 | 181 | if err != nil { 182 | deny(w) 183 | return 184 | } 185 | 186 | if canAccess(db, r.URL.Path, perms) { 187 | fmt.Println("Access Granted for", r.URL.Path) 188 | loadAccount(db, accessToken, c) 189 | h.ServeHTTP(w, r) 190 | return 191 | } 192 | 193 | deny(w) 194 | } 195 | return http.HandlerFunc(fn) 196 | } 197 | -------------------------------------------------------------------------------- /admin/admin.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/boltdb/bolt" 11 | "github.com/csaunders/giftd/middleware" 12 | "github.com/csaunders/giftd/models" 13 | "github.com/zenazn/goji" 14 | "github.com/zenazn/goji/web" 15 | ) 16 | 17 | type TokenOptions struct { 18 | AccessToken string 19 | Permissions string 20 | } 21 | 22 | func unavailable(err error, w http.ResponseWriter) error { 23 | w.WriteHeader(http.StatusServiceUnavailable) 24 | w.Write([]byte(err.Error())) 25 | return err 26 | } 27 | 28 | func notFound(w http.ResponseWriter) { 29 | w.WriteHeader(http.StatusNotFound) 30 | body, _ := json.Marshal(struct { 31 | Err string `json:"error"` 32 | }{"Resource Not Found"}) 33 | w.Write(body) 34 | } 35 | 36 | func invalid(err error, w http.ResponseWriter) { 37 | w.WriteHeader(http.StatusNotAcceptable) 38 | body, _ := json.Marshal(struct { 39 | Err string `json:"error"` 40 | }{err.Error()}) 41 | w.Write(body) 42 | } 43 | 44 | func retrieveDb(c web.C, w http.ResponseWriter) (*bolt.DB, error) { 45 | if db, ok := c.Env[middleware.ConfigurationDB].(*bolt.DB); ok { 46 | return db, nil 47 | } 48 | w.WriteHeader(http.StatusServiceUnavailable) 49 | w.Write([]byte("retrieveDb: database not available in context")) 50 | return nil, errors.New("retrieveDb: database not available in context") 51 | } 52 | 53 | func findClient(db *bolt.DB, c web.C, raw bool) (models.Account, error, []byte) { 54 | var account models.Account 55 | var rawData []byte 56 | err := db.View(func(tx *bolt.Tx) error { 57 | id := c.URLParams["id"] 58 | idsBucket, err := models.ApiClientIdsBucket(tx) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | token := idsBucket.Get([]byte(id)) 64 | if token == nil { 65 | return models.RecordNotFound 66 | } 67 | 68 | clientsBucket, err := models.ApiClientsBucket(tx) 69 | if err != nil { 70 | return err 71 | } 72 | if raw { 73 | rawData, err = models.LoadRaw(clientsBucket, string(token)) 74 | } else { 75 | err = models.Load(clientsBucket, string(token), &account) 76 | } 77 | return err 78 | }) 79 | return account, err, rawData 80 | } 81 | 82 | func saveClient(db *bolt.DB, client *models.Account) error { 83 | return db.Update(func(tx *bolt.Tx) error { 84 | clientIds, err := models.ApiClientIdsBucket(tx) 85 | if err != nil { 86 | return err 87 | } 88 | bucket, err := models.ApiClientsBucket(tx) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | clientIds.Put([]byte(client.Id), []byte(client.Token)) 94 | return models.Save(bucket, client.Token, client) 95 | }) 96 | } 97 | 98 | func modifyPermissions(db *bolt.DB, r io.Reader, account *models.Account, operation func([]string)) error { 99 | var perms struct { 100 | Permissions []string `json:"permissions"` 101 | } 102 | err := json.NewDecoder(r).Decode(&perms) 103 | if err != nil { 104 | return err 105 | } 106 | operation(perms.Permissions) 107 | return saveClient(db, account) 108 | } 109 | 110 | func listClients(c web.C, w http.ResponseWriter, r *http.Request) { 111 | db, err := retrieveDb(c, w) 112 | if err != nil { 113 | return 114 | } 115 | clientIds := []string{} 116 | err = db.View(func(tx *bolt.Tx) error { 117 | clients, err := models.ApiClientIdsBucket(tx) 118 | if err != nil { 119 | return unavailable(err, w) 120 | } 121 | clients.ForEach(func(key, value []byte) error { 122 | clientIds = append(clientIds, string(key)) 123 | return nil 124 | }) 125 | return nil 126 | }) 127 | if err == nil { 128 | data, _ := json.Marshal(struct { 129 | Ids []string `json:"ids"` 130 | }{clientIds}) 131 | w.Write(data) 132 | } 133 | } 134 | 135 | func showClient(c web.C, w http.ResponseWriter, r *http.Request) { 136 | db, err := retrieveDb(c, w) 137 | if err != nil { 138 | return 139 | } 140 | _, err, clientAccount := findClient(db, c, true) 141 | switch err { 142 | case nil: 143 | w.Write(clientAccount) 144 | case models.RecordNotFound: 145 | notFound(w) 146 | default: 147 | unavailable(err, w) 148 | } 149 | } 150 | 151 | func updateClient(c web.C, w http.ResponseWriter, r *http.Request) { 152 | db, err := retrieveDb(c, w) 153 | if err != nil { 154 | return 155 | } 156 | client, err, _ := findClient(db, c, false) 157 | if err == nil { 158 | params := struct { 159 | Datastore string `json:"datastore"` 160 | Permissions []string `json:"permissions"` 161 | }{} 162 | if err = json.NewDecoder(r.Body).Decode(¶ms); err == nil { 163 | client.SetDatastore(params.Datastore) 164 | client.SetPermissions(params.Permissions) 165 | err = saveClient(db, &client) 166 | } 167 | } 168 | if err == nil { 169 | w.WriteHeader(http.StatusAccepted) 170 | w.Write([]byte("")) 171 | } else { 172 | w.WriteHeader(http.StatusServiceUnavailable) 173 | w.Write([]byte(err.Error())) 174 | } 175 | } 176 | 177 | func addPermissions(c web.C, w http.ResponseWriter, r *http.Request) { 178 | db, err := retrieveDb(c, w) 179 | if err != nil { 180 | return 181 | } 182 | clientAccount, err, _ := findClient(db, c, false) 183 | if err == nil { 184 | err = modifyPermissions(db, r.Body, &clientAccount, (&clientAccount).AddPermissions) 185 | } 186 | switch err { 187 | case nil: 188 | w.WriteHeader(http.StatusAccepted) 189 | w.Write([]byte("")) 190 | case models.RecordNotFound: 191 | notFound(w) 192 | default: 193 | unavailable(err, w) 194 | } 195 | } 196 | 197 | func removePermissions(c web.C, w http.ResponseWriter, r *http.Request) { 198 | db, err := retrieveDb(c, w) 199 | if err != nil { 200 | return 201 | } 202 | clientAccount, err, _ := findClient(db, c, false) 203 | if err == nil { 204 | err = modifyPermissions(db, r.Body, &clientAccount, (&clientAccount).RemovePermissions) 205 | } 206 | switch err { 207 | case nil: 208 | w.WriteHeader(http.StatusAccepted) 209 | w.Write([]byte("")) 210 | case models.RecordNotFound: 211 | notFound(w) 212 | default: 213 | unavailable(err, w) 214 | } 215 | } 216 | 217 | func createClient(c web.C, w http.ResponseWriter, r *http.Request) { 218 | db, err := retrieveDb(c, w) 219 | if err != nil { 220 | return 221 | } 222 | client, err := models.NewAccount() 223 | if err == nil { 224 | err = modifyPermissions(db, r.Body, client, client.AddPermissions) 225 | } 226 | if err != nil { 227 | unavailable(err, w) 228 | return 229 | } 230 | 231 | bytes, _ := json.Marshal(client) 232 | w.Write(bytes) 233 | } 234 | 235 | func revokeClient(c web.C, w http.ResponseWriter, r *http.Request) { 236 | db, err := retrieveDb(c, w) 237 | if err != nil { 238 | return 239 | } 240 | client, err, _ := findClient(db, c, false) 241 | if err == nil { 242 | err = db.Update(func(tx *bolt.Tx) error { 243 | idsBucket, err := models.ApiClientIdsBucket(tx) 244 | if err != nil { 245 | return err 246 | } 247 | accounts, err := models.ApiClientsBucket(tx) 248 | if err != nil { 249 | return err 250 | } 251 | err = idsBucket.Delete([]byte(client.Id)) 252 | if err != nil { 253 | return err 254 | } 255 | return accounts.Delete([]byte(client.Token)) 256 | }) 257 | } 258 | 259 | switch err { 260 | case nil: 261 | w.WriteHeader(http.StatusAccepted) 262 | w.Write([]byte("")) 263 | case models.RecordNotFound: 264 | notFound(w) 265 | default: 266 | unavailable(err, w) 267 | } 268 | } 269 | 270 | func syncIds(c web.C, w http.ResponseWriter, r *http.Request) { 271 | var db *bolt.DB 272 | var ok bool 273 | if db, ok = c.Env[middleware.ConfigurationDB].(*bolt.DB); !ok { 274 | unavailable(errors.New("Cannot load configuration database"), w) 275 | return 276 | } 277 | db.Update(func(tx *bolt.Tx) error { 278 | clientsBucket, _ := models.ApiClientsBucket(tx) 279 | idsBucket, _ := models.ApiClientIdsBucket(tx) 280 | sync := func(token, record []byte) bool { 281 | if token == nil { 282 | return false 283 | } 284 | var client models.Account 285 | if err := json.Unmarshal(record, &client); err != nil { 286 | return false 287 | } 288 | fmt.Println(client.Id, "----->", client.Token) 289 | if err := idsBucket.Put([]byte(client.Id), []byte(client.Token)); err != nil { 290 | fmt.Println(err) 291 | } 292 | 293 | return true 294 | } 295 | cursor := clientsBucket.Cursor() 296 | fmt.Println("Synchronizing...") 297 | processing := sync(cursor.First()) 298 | for processing { 299 | processing = sync(cursor.Next()) 300 | } 301 | return nil 302 | }) 303 | w.WriteHeader(http.StatusAccepted) 304 | w.Write([]byte("")) 305 | } 306 | 307 | func Register(root string) { 308 | goji.Get(fmt.Sprintf("%s/accounts", root), listClients) 309 | goji.Post(fmt.Sprintf("%s/accounts", root), createClient) 310 | goji.Post(fmt.Sprintf("%s/accounts/sync", root), syncIds) 311 | goji.Get(fmt.Sprintf("%s/accounts/:id", root), showClient) 312 | goji.Put(fmt.Sprintf("%s/accounts/:id", root), updateClient) 313 | goji.Post(fmt.Sprintf("%s/accounts/:id/permissions", root), addPermissions) 314 | goji.Delete(fmt.Sprintf("%s/accounts/:id/permissions", root), removePermissions) 315 | goji.Delete(fmt.Sprintf("%s/accounts/:id", root), revokeClient) 316 | } 317 | -------------------------------------------------------------------------------- /gifs/gifs.go: -------------------------------------------------------------------------------- 1 | package gifs 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "image/gif" 9 | "io" 10 | "io/ioutil" 11 | "math" 12 | "math/rand" 13 | "net/http" 14 | "sort" 15 | "strconv" 16 | 17 | "github.com/boltdb/bolt" 18 | "github.com/csaunders/giftd/middleware" 19 | "github.com/csaunders/giftd/models" 20 | "github.com/zenazn/goji" 21 | "github.com/zenazn/goji/web" 22 | ) 23 | 24 | const root string = "giftd-gifs" 25 | const maxRandGif int = 10 26 | const namespacesBucketName string = "namespaces" 27 | 28 | type requestError struct { 29 | Error string `json:"error"` 30 | } 31 | 32 | func verifyGif(r io.Reader) ([]byte, error) { 33 | data, err := ioutil.ReadAll(r) 34 | buff := bytes.NewBuffer(data) 35 | _, err = gif.Decode(buff) 36 | if err != nil { 37 | return []byte{}, err 38 | } 39 | return data, err 40 | } 41 | 42 | func storeGif(db *bolt.DB, ns, uuid, content []byte) error { 43 | return db.Update(func(tx *bolt.Tx) error { 44 | rootBucket := tx.Bucket([]byte(root)) 45 | 46 | namespacesBucket, err := rootBucket.CreateBucketIfNotExists([]byte(namespacesBucketName)) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | bucketForNamespace, err := rootBucket.CreateBucketIfNotExists(ns) 52 | if err != nil { 53 | return err 54 | } 55 | if err = rootBucket.Put(uuid, content); err != nil { 56 | return err 57 | } 58 | if err = bucketForNamespace.Put(uuid, []byte("{}")); err != nil { 59 | return err 60 | } 61 | if err = namespacesBucket.Put(ns, []byte("{}")); err != nil { 62 | return err 63 | } 64 | 65 | return nil 66 | }) 67 | } 68 | 69 | func retrieveAndVerify(r io.Reader) ([]byte, error) { 70 | src, err := ioutil.ReadAll(r) 71 | if err != nil { 72 | return []byte{}, err 73 | } 74 | resp, err := http.Get(string(src)) 75 | if err != nil { 76 | return []byte{}, err 77 | } 78 | defer resp.Body.Close() 79 | return verifyGif(resp.Body) 80 | } 81 | 82 | func errorHandler(err error, c web.C, w http.ResponseWriter, r *http.Request) { 83 | w.WriteHeader(http.StatusServiceUnavailable) 84 | fmt.Fprintf(w, "Looks like something be misbehavin!") 85 | } 86 | 87 | func notFound(msg string, c web.C, w http.ResponseWriter, r *http.Request) { 88 | response( 89 | http.StatusNotFound, 90 | struct { 91 | Error string `json:"error"` 92 | }{msg}, 93 | c, 94 | w, 95 | r, 96 | ) 97 | } 98 | 99 | func response(code int, body interface{}, c web.C, w http.ResponseWriter, r *http.Request) { 100 | content, err := json.Marshal(body) 101 | if err != nil { 102 | errorHandler(err, c, w, r) 103 | } 104 | w.Header().Set("Content-Type", "application/json") 105 | w.WriteHeader(code) 106 | w.Write(content) 107 | } 108 | 109 | func nRandomIndiciesFor(db *bolt.DB, namespace []byte, num int) []int { 110 | var indices []int 111 | indexMap := map[int]bool{} 112 | namespaceSize := 0 113 | db.View(func(tx *bolt.Tx) error { 114 | namespaceBucket := tx.Bucket([]byte(root)).Bucket(namespace) 115 | if namespaceBucket != nil { 116 | namespaceSize = namespaceBucket.Stats().KeyN 117 | } 118 | return nil 119 | }) 120 | indices = make([]int, int(math.Min(float64(num), float64(namespaceSize)))) 121 | index := 0 122 | retries := 0 123 | for { 124 | if retries > 100 || index >= len(indices) { 125 | break 126 | } 127 | n := rand.Intn(namespaceSize) 128 | if !indexMap[n] { 129 | indices[index] = n 130 | indexMap[n] = true 131 | index++ 132 | retries = 0 133 | } else { 134 | retries++ 135 | } 136 | } 137 | sort.Sort(sort.IntSlice(indices)) 138 | return indices 139 | } 140 | 141 | func findRandomGifs(db *bolt.DB, namespace []byte, num int) ([]string, error) { 142 | indices := nRandomIndiciesFor(db, namespace, num) 143 | uuids := make([]string, len(indices)) 144 | 145 | err := db.View(func(tx *bolt.Tx) error { 146 | rootBucket := tx.Bucket([]byte(root)) 147 | bucketForNamespace := rootBucket.Bucket(namespace) 148 | if bucketForNamespace == nil { 149 | return errors.New("findRandomGifs: bucket does not exist") 150 | } 151 | cursor := bucketForNamespace.Cursor() 152 | index := 0 153 | position := 0 154 | for k, _ := cursor.First(); k != nil; k, _ = cursor.Next() { 155 | if index >= len(indices) { 156 | break 157 | } else if indices[index] == position { 158 | uuids[index] = string(k) 159 | index++ 160 | } 161 | position++ 162 | } 163 | return nil 164 | }) 165 | return uuids, err 166 | } 167 | 168 | func listNamespaces(db *bolt.DB, c web.C, w http.ResponseWriter, r *http.Request) { 169 | var body struct { 170 | Categories []string `json:"categories"` 171 | } 172 | err := db.View(func(tx *bolt.Tx) error { 173 | b := tx.Bucket([]byte(root)).Bucket([]byte(namespacesBucketName)) 174 | if b == nil { 175 | body.Categories = []string{} 176 | return nil 177 | } 178 | stats := b.Stats() 179 | c := b.Cursor() 180 | results := make([]string, stats.KeyN) 181 | i := 0 182 | 183 | for k, _ := c.First(); k != nil; k, _ = c.Next() { 184 | results[i] = string(k) 185 | i++ 186 | } 187 | 188 | body.Categories = results 189 | return nil 190 | }) 191 | 192 | if err != nil { 193 | errorHandler(err, c, w, r) 194 | return 195 | } 196 | response(http.StatusOK, body, c, w, r) 197 | } 198 | 199 | func showGif(db *bolt.DB, c web.C, w http.ResponseWriter, r *http.Request) { 200 | uuid := c.URLParams["uuid"] 201 | var content []byte 202 | err := db.View(func(tx *bolt.Tx) error { 203 | content = tx.Bucket([]byte(root)).Get([]byte(uuid)) 204 | return nil 205 | }) 206 | if err != nil { 207 | errorHandler(err, c, w, r) 208 | return 209 | } else if len(content) <= 0 { 210 | notFound(fmt.Sprintf("%s does not exist", uuid), c, w, r) 211 | return 212 | } 213 | w.Header().Set("Content-Type", "image/gif") 214 | w.Write(content) 215 | } 216 | 217 | func createGif(db *bolt.DB, c web.C, w http.ResponseWriter, r *http.Request) { 218 | namespace := c.URLParams["namespace"] 219 | var content []byte 220 | var err error 221 | switch c.URLParams["type"] { 222 | case "gif": 223 | content, err = verifyGif(r.Body) 224 | case "link": 225 | content, err = retrieveAndVerify(r.Body) 226 | default: 227 | response( 228 | http.StatusNotAcceptable, 229 | requestError{"Invalid or unspecified resource: use gif or link"}, 230 | c, w, r, 231 | ) 232 | return 233 | } 234 | 235 | if err != nil { 236 | response( 237 | http.StatusUnsupportedMediaType, 238 | requestError{"Invalid Content"}, 239 | c, w, r, 240 | ) 241 | return 242 | } 243 | 244 | uuid, err := models.GenUUID() 245 | if err != nil { 246 | errorHandler(err, c, w, r) 247 | return 248 | } 249 | 250 | err = storeGif(db, []byte(namespace), []byte(uuid), content) 251 | if err != nil { 252 | errorHandler(err, c, w, r) 253 | } else { 254 | response( 255 | http.StatusCreated, struct { 256 | UUID string `json:"uuid"` 257 | }{string(uuid)}, 258 | c, w, r, 259 | ) 260 | } 261 | } 262 | 263 | func randomGif(db *bolt.DB, c web.C, w http.ResponseWriter, r *http.Request) { 264 | namespace := c.URLParams["namespace"] 265 | account, _ := c.Env[middleware.AccountDetails].(models.Account) 266 | uuids, err := findRandomGifs(db, []byte(namespace), 1) 267 | 268 | if err != nil { 269 | errorHandler(err, c, w, r) 270 | return 271 | } 272 | http.Redirect(w, r, fmt.Sprintf("/gifs/%s/%s", account.Id, string(uuids[0])), http.StatusTemporaryRedirect) 273 | } 274 | 275 | func randomNumGifs(db *bolt.DB, c web.C, w http.ResponseWriter, r *http.Request) { 276 | namespace := c.URLParams["namespace"] 277 | account, _ := c.Env[middleware.AccountDetails].(models.Account) 278 | count, err := strconv.ParseInt(c.URLParams["count"], 10, 64) 279 | if err != nil { 280 | errorHandler(err, c, w, r) 281 | return 282 | } 283 | 284 | if int(count) > maxRandGif { 285 | response( 286 | http.StatusNotAcceptable, 287 | requestError{fmt.Sprintf("Request exceeds maximum random gif count of %d", maxRandGif)}, 288 | c, 289 | w, 290 | r, 291 | ) 292 | return 293 | } 294 | host, ok := c.Env["host"].(string) 295 | if !ok { 296 | host = "localhost:8000" 297 | } 298 | uuids, err := findRandomGifs(db, []byte(namespace), int(count)) 299 | paths := make([]string, len(uuids)) 300 | if err != nil { 301 | errorHandler(err, c, w, r) 302 | return 303 | } 304 | for i, uuid := range uuids { 305 | paths[i] = fmt.Sprintf("http://%s/gifs/%s/%s", host, account.Id, uuid) 306 | } 307 | response( 308 | http.StatusOK, 309 | struct { 310 | Locations []string `json:"locations"` 311 | }{paths}, 312 | c, 313 | w, 314 | r, 315 | ) 316 | } 317 | 318 | func reportGif(db *bolt.DB, c web.C, w http.ResponseWriter, r *http.Request) { 319 | response(http.StatusNotImplemented, requestError{"Not Implemented"}, c, w, r) 320 | } 321 | 322 | func createBucket(db *bolt.DB) error { 323 | return db.Update(func(tx *bolt.Tx) error { 324 | _, err := tx.CreateBucketIfNotExists([]byte(root)) 325 | if err != nil { 326 | return fmt.Errorf("create bucket: %s", err) 327 | } 328 | return nil 329 | }) 330 | } 331 | 332 | func Register(root string, provider middleware.DatabaseProvider) { 333 | goji.Get(fmt.Sprintf("%s", root), provider(createBucket, listNamespaces)) 334 | 335 | // Creation / Retrieval 336 | goji.Post(fmt.Sprintf("%s/:namespace/:type", root), provider(createBucket, createGif)) 337 | goji.Get(fmt.Sprintf("%s/:namespace/random", root), provider(createBucket, randomGif)) 338 | goji.Get(fmt.Sprintf("%s/:namespace/random/:count", root), provider(createBucket, randomNumGifs)) 339 | 340 | // Gif Specific 341 | goji.Get(fmt.Sprintf("%s/:account_id/:uuid", root), provider(createBucket, showGif)) 342 | goji.Delete(fmt.Sprintf("%s/:account_id/:uuid/report", root), provider(createBucket, reportGif)) 343 | } 344 | --------------------------------------------------------------------------------