diff --git a/doc/api.oa3 b/doc/api.oa3 index d490999e..67c670d2 100644 --- a/doc/api.oa3 +++ b/doc/api.oa3 @@ -124,11 +124,6 @@ paths: description: permission denide '500': description: internal server error - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/NodeConfig' /admin/accounts: get: @@ -3595,6 +3590,10 @@ components: NodeConfig: type: object + required: + - domain + - publicLimit + - accountStorage properties: domain: type: string diff --git a/net/server/go.mod b/net/server/go.mod index a7c85967..b3176e93 100644 --- a/net/server/go.mod +++ b/net/server/go.mod @@ -7,6 +7,7 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.3 // indirect github.com/mattn/go-sqlite3 v1.14.9 // indirect + golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect gorm.io/driver/sqlite v1.2.6 // indirect gorm.io/gorm v1.22.4 // indirect ) diff --git a/net/server/go.sum b/net/server/go.sum index 3125734d..973a48a0 100644 --- a/net/server/go.sum +++ b/net/server/go.sum @@ -12,6 +12,8 @@ github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/sqlite v1.2.6 h1:SStaH/b+280M7C8vXeZLz/zo9cLQmIGwwj3cSj7p6l4= diff --git a/net/server/internal/api_admin.go b/net/server/internal/api_admin.go index 5b8bce07..32bee834 100644 --- a/net/server/internal/api_admin.go +++ b/net/server/internal/api_admin.go @@ -10,10 +10,12 @@ package databag import ( - "errors" + "log" + "encoding/json" "net/http" "gorm.io/gorm" - store "databag/internal/store" + "golang.org/x/crypto/bcrypt" + "databag/internal/store" ) func AddNodeAccount(w http.ResponseWriter, r *http.Request) { @@ -32,14 +34,11 @@ func GetNodeAccounts(w http.ResponseWriter, r *http.Request) { } func GetNodeClaimable(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - var config store.Config - err := store.DB.Where("config_id = ?", CONFIG_CLAIMED).First(&config).Error; - if errors.Is(err, gorm.ErrRecordNotFound) { - w.WriteHeader(http.StatusOK) - } else { + if _configured { w.WriteHeader(http.StatusNotAcceptable) + } else { + w.WriteHeader(http.StatusOK) } } @@ -59,11 +58,97 @@ func SetNodeAccount(w http.ResponseWriter, r *http.Request) { } func SetNodeClaim(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") + + // confirm node is claimable + if _configured { + w.WriteHeader(http.StatusUnauthorized) + return + } + + // extract credentials + username, password, ok := r.BasicAuth(); + if !ok { + w.WriteHeader(http.StatusBadRequest) + return + } + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + log.Printf("SetNodeClaim - failed to hash password"); + w.WriteHeader(http.StatusInternalServerError) + return + } + + // store credentials + err = store.DB.Transaction(func(tx *gorm.DB) error { + if res := tx.Create(&store.Config{ConfigId: CONFIG_USERNAME, StrValue: username}).Error; res != nil { + return res + } + if res := tx.Create(&store.Config{ConfigId: CONFIG_PASSWORD, BinValue: hashedPassword}).Error; res != nil { + return res + } + return nil; + }) + if(err != nil) { + log.Printf("SetNodeCalim - failed to store credentials"); + w.WriteHeader(http.StatusInternalServerError) + return + } + + // set global values + _adminUsername = username + _adminPassword = hashedPassword + w.WriteHeader(http.StatusOK) } func SetNodeConfig(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") + + // validate admin password + username, password, ok := r.BasicAuth(); + if !ok { + w.WriteHeader(http.StatusBadRequest) + return + } + if username != _adminUsername || bcrypt.CompareHashAndPassword(_adminPassword, []byte(password)) != nil { + log.Printf("SetNodeConfig - invalid admin credentials"); + w.WriteHeader(http.StatusUnauthorized); + return + } + + // parse node config + r.Body = http.MaxBytesReader(w, r.Body, CONFIG_BODYLIMIT) + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + var config NodeConfig; + res := dec.Decode(&config); + if res != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + // store credentials + err := store.DB.Transaction(func(tx *gorm.DB) error { + if res := tx.Create(&store.Config{ConfigId: CONFIG_DOMAIN, StrValue: config.Domain}).Error; res != nil { + return res + } + if res := tx.Create(&store.Config{ConfigId: CONFIG_PUBLICLIMIT, NumValue: config.PublicLimit}).Error; res != nil { + return res + } + if res := tx.Create(&store.Config{ConfigId: CONFIG_STORAGE, NumValue: config.AccountStorage}).Error; res != nil { + return res + } + return nil; + }) + if(err != nil) { + log.Printf("SetNodeConfig - failed to store config"); + w.WriteHeader(http.StatusInternalServerError) + return + } + + // set global values + _nodeDomain = config.Domain + _publicLimit = config.PublicLimit + _accountStorage = config.AccountStorage + w.WriteHeader(http.StatusOK) } diff --git a/net/server/internal/config_keys.go b/net/server/internal/config_keys.go index 88c15a71..371c386d 100644 --- a/net/server/internal/config_keys.go +++ b/net/server/internal/config_keys.go @@ -1,7 +1,10 @@ package databag -const CONFIG_CLAIMED = "claimed"; +const CONFIG_BODYLIMIT = 1048576 + +const CONFIG_CONFIGURED = "configured" const CONFIG_USERNAME = "username" const CONFIG_PASSWORD = "password" -const CONFIG_SATL = "salt" - +const CONFIG_DOMAIN = "domain" +const CONFIG_PUBLICLIMIT = "public_limit" +const CONFIG_STORAGE = "storage" diff --git a/net/server/internal/context.go b/net/server/internal/context.go new file mode 100644 index 00000000..4a9bbc69 --- /dev/null +++ b/net/server/internal/context.go @@ -0,0 +1,50 @@ +package databag + +import ( + "errors" + "gorm.io/gorm" + "databag/internal/store" + ) + +var _configured bool +var _adminUsername string +var _adminPassword []byte +var _nodeDomain string +var _accountStorage int64 +var _publicLimit int64 + +func getStrConfigValue(configId string, empty string) string { + var config store.Config + err := store.DB.Where("config_id = ?", config).First(&config).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return empty + } + return config.StrValue +} + +func getNumConfigValue(configId string, empty int64) int64 { + var config store.Config + err := store.DB.Where("config_id = ?", config).First(&config).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return empty + } + return config.NumValue +} + +func getBoolConfigValue(configId string, empty bool) bool { + var config store.Config + err := store.DB.Where("config_id = ?", config).First(&config).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return empty + } + return config.BoolValue +} + +func getBinConfigValue(configId string, empty []byte) []byte { + var config store.Config + err := store.DB.Where("config_id = ?", config).First(&config).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return empty + } + return config.BinValue +} diff --git a/net/server/internal/routers.go b/net/server/internal/routers.go index 628171f2..9dbaf839 100644 --- a/net/server/internal/routers.go +++ b/net/server/internal/routers.go @@ -27,6 +27,14 @@ type Routes []Route func NewRouter() *mux.Router { + // populate context + _configured = getBoolConfigValue(CONFIG_CONFIGURED, false); + _adminUsername = getStrConfigValue(CONFIG_USERNAME, ""); + _adminPassword = getBinConfigValue(CONFIG_PASSWORD, nil); + _nodeDomain = getStrConfigValue(CONFIG_DOMAIN, ""); + _publicLimit = getNumConfigValue(CONFIG_PUBLICLIMIT, 0); + _accountStorage = getNumConfigValue(CONFIG_STORAGE, 0); + router := mux.NewRouter().StrictSlash(true) for _, route := range routes { var handler http.Handler diff --git a/net/server/internal/store/schema.go b/net/server/internal/store/schema.go index 052255f0..603f20e1 100644 --- a/net/server/internal/store/schema.go +++ b/net/server/internal/store/schema.go @@ -31,6 +31,7 @@ type Config struct { StrValue string NumValue int64 BoolValue bool + BinValue []byte } type AccountToken struct { @@ -46,8 +47,7 @@ type Account struct { ID uint `gorm:"primaryKey;not null;unique;autoIncrement"` Did string `gorm:"not null"` Username string `gorm:"not null;uniqueIndex"` - Password string `gorm:"not null"` - Salt string `gorm:"not null"` + Password []byte `gorm:"not null"` Name string Description string Location string diff --git a/net/server/main_test.go b/net/server/main_test.go index 6933f401..e3d01691 100644 --- a/net/server/main_test.go +++ b/net/server/main_test.go @@ -1,38 +1,53 @@ package main import ( - "fmt" + "strings" "testing" "net/http/httptest" "encoding/base64" + "encoding/json" app "databag/internal" - store "databag/internal/store" + "databag/internal/store" ) func TestSetup(t *testing.T) { store.SetPath("file::memory:?cache=shared"); + //store.SetPath("databag.db"); Claimable(t); Claim(t); + Config(t); } func Claimable(t *testing.T) { - r := httptest.NewRequest("GET", "/claimable", nil) + r := httptest.NewRequest("GET", "/admin/claimable", nil) w := httptest.NewRecorder() - app.GetNodeClaimable(w, r); + app.GetNodeClaimable(w, r) if w.Code != 200 { - t.Errorf("server not initially claimable"); + t.Errorf("server not initially claimable") } } func Claim(t *testing.T) { auth := base64.StdEncoding.EncodeToString([]byte("admin:pass")) - r := httptest.NewRequest("GET", "/claimable", nil) + r := httptest.NewRequest("PUT", "/admin/claim", nil) r.Header.Add("Authorization","Basic " + auth) w := httptest.NewRecorder() - app.GetNodeClaimable(w, r); + app.SetNodeClaim(w, r) if w.Code != 200 { - t.Errorf("server not initially claimable"); + t.Errorf("server not initially claimable") + } +} + +func Config(t *testing.T) { + config := app.NodeConfig{Domain: "example.com", PublicLimit: 1024, AccountStorage: 4096} + auth := base64.StdEncoding.EncodeToString([]byte("admin:pass")) + body,_ := json.Marshal(config) + r := httptest.NewRequest("PUT", "/admin/config", strings.NewReader(string(body))) + r.Header.Add("Authorization","Basic " + auth) + w := httptest.NewRecorder() + app.SetNodeConfig(w, r); + if w.Code != 200 { + t.Errorf("failed to set node config") } - }