diff --git a/api/user.go b/api/user.go
index f6de76c..f426a54 100644
--- a/api/user.go
+++ b/api/user.go
@@ -24,6 +24,7 @@ type UserManager interface {
GetUser(id string) *User
AddUser(*User) error
RemoveUser(id string) error
+ ForEachUser(cb func(*User) error) error
UpdateUserPassword(username string, password string) error
UpdateUserPermissions(username string, permissions PermissionFlag) error
diff --git a/api/v0/auth.go b/api/v0/auth.go
index 0af1303..f16dce1 100644
--- a/api/v0/auth.go
+++ b/api/v0/auth.go
@@ -281,215 +281,3 @@ func (h *Handler) routeLogout(rw http.ResponseWriter, req *http.Request) {
h.tokens.InvalidToken(tid)
rw.WriteHeader(http.StatusNoContent)
}
-
-// var (
-// ErrUnsupportAuthType = errors.New("unsupported authorization type")
-// ErrScopeNotMatch = errors.New("scope not match")
-// ErrJTINotExists = errors.New("jti not exists")
-
-// ErrStrictPathNotMatch = errors.New("strict path not match")
-// ErrStrictQueryNotMatch = errors.New("strict query value not match")
-// )
-
-// func (cr *Cluster) getJWTKey(t *jwt.Token) (any, error) {
-// if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
-// return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"])
-// }
-// return cr.apiHmacKey, nil
-// }
-
-// const (
-// challengeTokenScope = "GOBA-challenge"
-// authTokenScope = "GOBA-auth"
-// apiTokenScope = "GOBA-API"
-// )
-
-// type challengeTokenClaims struct {
-// jwt.RegisteredClaims
-
-// Scope string `json:"scope"`
-// Action string `json:"act"`
-// }
-
-// func (cr *Cluster) generateChallengeToken(cliId string, action string) (string, error) {
-// now := time.Now()
-// exp := now.Add(time.Minute * 1)
-// token := jwt.NewWithClaims(jwt.SigningMethodHS256, &challengeTokenClaims{
-// RegisteredClaims: jwt.RegisteredClaims{
-// Subject: cliId,
-// Issuer: cr.jwtIssuer,
-// IssuedAt: jwt.NewNumericDate(now),
-// ExpiresAt: jwt.NewNumericDate(exp),
-// },
-// Scope: challengeTokenScope,
-// Action: action,
-// })
-// tokenStr, err := token.SignedString(cr.apiHmacKey)
-// if err != nil {
-// return "", err
-// }
-// return tokenStr, nil
-// }
-
-// func (cr *Cluster) verifyChallengeToken(cliId string, action string, token string) (err error) {
-// var claims challengeTokenClaims
-// if _, err = jwt.ParseWithClaims(
-// token,
-// &claims,
-// cr.getJWTKey,
-// jwt.WithSubject(cliId),
-// jwt.WithIssuedAt(),
-// jwt.WithIssuer(cr.jwtIssuer),
-// ); err != nil {
-// return
-// }
-// if claims.Scope != challengeTokenScope {
-// return ErrScopeNotMatch
-// }
-// if claims.Action != action {
-// return ErrJTINotExists
-// }
-// return
-// }
-
-// type authTokenClaims struct {
-// jwt.RegisteredClaims
-
-// Scope string `json:"scope"`
-// User string `json:"usr"`
-// }
-
-// func (cr *Cluster) generateAuthToken(cliId string, userId string) (string, error) {
-// jti, err := utils.GenRandB64(16)
-// if err != nil {
-// return "", err
-// }
-// now := time.Now()
-// exp := now.Add(time.Hour * 24)
-// token := jwt.NewWithClaims(jwt.SigningMethodHS256, &authTokenClaims{
-// RegisteredClaims: jwt.RegisteredClaims{
-// ID: jti,
-// Subject: cliId,
-// Issuer: cr.jwtIssuer,
-// IssuedAt: jwt.NewNumericDate(now),
-// ExpiresAt: jwt.NewNumericDate(exp),
-// },
-// Scope: authTokenScope,
-// User: userId,
-// })
-// tokenStr, err := token.SignedString(cr.apiHmacKey)
-// if err != nil {
-// return "", err
-// }
-// if err = cr.database.AddJTI(jti, exp); err != nil {
-// return "", err
-// }
-// return tokenStr, nil
-// }
-
-// func (cr *Cluster) verifyAuthToken(cliId string, token string) (id string, user string, err error) {
-// var claims authTokenClaims
-// if _, err = jwt.ParseWithClaims(
-// token,
-// &claims,
-// cr.getJWTKey,
-// jwt.WithSubject(cliId),
-// jwt.WithIssuedAt(),
-// jwt.WithIssuer(cr.jwtIssuer),
-// ); err != nil {
-// return
-// }
-// if claims.Scope != authTokenScope {
-// err = ErrScopeNotMatch
-// return
-// }
-// if user = claims.User; user == "" {
-// // reject old token
-// err = ErrJTINotExists
-// return
-// }
-// id = claims.ID
-// if ok, _ := cr.database.ValidJTI(id); !ok {
-// err = ErrJTINotExists
-// return
-// }
-// return
-// }
-
-// type apiTokenClaims struct {
-// jwt.RegisteredClaims
-
-// Scope string `json:"scope"`
-// User string `json:"usr"`
-// StrictPath string `json:"str-p"`
-// StrictQuery map[string]string `json:"str-q,omitempty"`
-// }
-
-// func (cr *Cluster) generateAPIToken(cliId string, userId string, path string, query map[string]string) (string, error) {
-// jti, err := utils.GenRandB64(8)
-// if err != nil {
-// return "", err
-// }
-// now := time.Now()
-// exp := now.Add(time.Minute * 10)
-// token := jwt.NewWithClaims(jwt.SigningMethodHS256, &apiTokenClaims{
-// RegisteredClaims: jwt.RegisteredClaims{
-// ID: jti,
-// Subject: cliId,
-// Issuer: cr.jwtIssuer,
-// IssuedAt: jwt.NewNumericDate(now),
-// ExpiresAt: jwt.NewNumericDate(exp),
-// },
-// Scope: apiTokenScope,
-// User: userId,
-// StrictPath: path,
-// StrictQuery: query,
-// })
-// tokenStr, err := token.SignedString(cr.apiHmacKey)
-// if err != nil {
-// return "", err
-// }
-// if err = cr.database.AddJTI(jti, exp); err != nil {
-// return "", err
-// }
-// return tokenStr, nil
-// }
-
-// func (h *Handler) verifyAPIToken(cliId string, token string, path string, query url.Values) (id string, user string, err error) {
-// var claims apiTokenClaims
-// _, err = jwt.ParseWithClaims(
-// token,
-// &claims,
-// cr.getJWTKey,
-// jwt.WithSubject(cliId),
-// jwt.WithIssuedAt(),
-// jwt.WithIssuer(cr.jwtIssuer),
-// )
-// if err != nil {
-// return
-// }
-// if claims.Scope != apiTokenScope {
-// err = ErrScopeNotMatch
-// return
-// }
-// if user = claims.User; user == "" {
-// err = ErrJTINotExists
-// return
-// }
-// id = claims.ID
-// if ok, _ := cr.database.ValidJTI(id); !ok {
-// err = ErrJTINotExists
-// return
-// }
-// if claims.StrictPath != path {
-// err = ErrStrictPathNotMatch
-// return
-// }
-// for k, v := range claims.StrictQuery {
-// if query.Get(k) != v {
-// err = ErrStrictQueryNotMatch
-// return
-// }
-// }
-// return
-// }
diff --git a/database/db.go b/database/db.go
index 594b777..8bca346 100644
--- a/database/db.go
+++ b/database/db.go
@@ -51,6 +51,15 @@ type DB interface {
// the callback should not edit the record pointer
ForEachFileRecord(cb func(*FileRecord) error) error
+ // GetUsers() []*api.User
+ // GetUser(id string) *api.User
+ // AddUser(*api.User) error
+ // RemoveUser(id string) error
+ // ForEachUser(cb func(*api.User) error) error
+ // UpdateUserPassword(username string, password string) error
+ // UpdateUserPermissions(username string, permissions api.PermissionFlag) error
+ // VerifyUserPassword(userId string, comparator func(password string) bool) error
+
GetSubscribe(user string, client string) (*api.SubscribeRecord, error)
SetSubscribe(api.SubscribeRecord) error
RemoveSubscribe(user string, client string) error
diff --git a/notify/webhook/webhook.go b/notify/webhook/webhook.go
index 44b8b02..1fa637e 100644
--- a/notify/webhook/webhook.go
+++ b/notify/webhook/webhook.go
@@ -17,7 +17,7 @@
* along with this program. If not, see .
*/
-package webpush
+package webhook
import (
"bytes"
diff --git a/runner.go b/runner.go
index 7c92a5f..41251c2 100644
--- a/runner.go
+++ b/runner.go
@@ -53,8 +53,11 @@ import (
"github.com/LiterMC/go-openbmclapi/limited"
"github.com/LiterMC/go-openbmclapi/log"
"github.com/LiterMC/go-openbmclapi/notify"
+ "github.com/LiterMC/go-openbmclapi/notify/email"
+ "github.com/LiterMC/go-openbmclapi/notify/webhook"
"github.com/LiterMC/go-openbmclapi/notify/webpush"
"github.com/LiterMC/go-openbmclapi/storage"
+ "github.com/LiterMC/go-openbmclapi/token"
"github.com/LiterMC/go-openbmclapi/utils"
)
@@ -104,21 +107,48 @@ func NewRunner() *Runner {
r.database = database.NewMemoryDB()
} else if r.database, err = database.NewSqlDB(r.Config.Database.Driver, r.Config.Database.DSN); err != nil {
log.Errorf("Cannot connect to database: %v", err)
+ os.Exit(1)
}
}
// r.userManager =
- // r.tokenManager =
- webpushPlg := new(webpush.Plugin)
- r.subManager = &subscriptionManager{
- webpushPlg: webpushPlg,
- DB: r.database,
- }
- r.notifyManager = notify.NewManager(dataDir, r.database, r.client.CachedClient(), "go-openbmclapi")
- r.storageManager = storage.NewManager(storages)
+ if apiHMACKey, err := utils.LoadOrCreateHmacKey(dataDir, "server"); err != nil {
+ log.Errorf("Cannot load HMAC key: %v", err)
+ os.Exit(1)
+ } else {
+ r.tokenManager = token.NewDBManager("go-openbmclapi", apiHMACKey, r.database)
+ }
+ {
+ r.notifyManager = notify.NewManager(dataDir, r.database, r.client.CachedClient(), "go-openbmclapi")
+ r.notifyManager.AddPlugin(new(webhook.Plugin))
+ if r.Config.Notification.EnableEmail {
+ emailPlg, err := email.NewSMTP(r.Config.Notification.EmailSMTP, r.Config.Notification.EmailSMTPEncryption,
+ r.Config.Notification.EmailSender, r.Config.Notification.EmailSenderPassword)
+ if err != nil {
+ log.Errorf("Cannot init SMTP client: %v", err)
+ os.Exit(1)
+ }
+ r.notifyManager.AddPlugin(emailPlg)
+ }
+ r.notifyManager.AddPlugin(new(email.Plugin))
+ webpushPlg := new(webpush.Plugin)
+ r.notifyManager.AddPlugin(webpushPlg)
+
+ r.subManager = &subscriptionManager{
+ webpushPlg: webpushPlg,
+ DB: r.database,
+ }
+ }
+ {
+ storages := make([]storage.Storage, len(r.Config.Storages))
+ for i, s := range r.Config.Storages {
+ storages[i] = storage.NewStorage(s)
+ }
+ r.storageManager = storage.NewManager(storages)
+ }
r.statManager = cluster.NewStatManager()
if err := r.statManager.Load(dataDir); err != nil {
- log.Errorf("Stat load failed:", err)
+ log.Errorf("Stat load failed: %v", err)
}
r.apiRateLimiter = limited.NewAPIRateMiddleWare(api.RealAddrCtxKey, "go-openbmclapi.cluster.logged.user" /* api/v0.loggedUserKey */)
return r
diff --git a/token/package.go b/token/package.go
new file mode 100644
index 0000000..470830e
--- /dev/null
+++ b/token/package.go
@@ -0,0 +1,256 @@
+/**
+ * OpenBmclAPI (Golang Edition)
+ * Copyright (C) 2023 Kevin Z
+ * All rights reserved
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package token
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+ "time"
+
+ "github.com/golang-jwt/jwt/v5"
+
+ "github.com/LiterMC/go-openbmclapi/utils"
+)
+
+var (
+ ErrUnsupportAuthType = errors.New("unsupported authorization type")
+ ErrScopeNotMatch = errors.New("scope not match")
+ ErrJTINotExists = errors.New("jti not exists")
+
+ ErrStrictPathNotMatch = errors.New("strict path not match")
+ ErrStrictQueryNotMatch = errors.New("strict query value not match")
+)
+
+const (
+ challengeTokenScope = "GOBA-challenge"
+ authTokenScope = "GOBA-auth"
+ apiTokenScope = "GOBA-API"
+)
+
+type (
+ basicTokenManager struct {
+ impl basicTokenManagerImpl
+ }
+ basicTokenManagerImpl interface {
+ Issuer() string
+ HmacKey() []byte
+ AddJTI(string, time.Time) error
+ ValidJTI(string) bool
+ }
+)
+
+type (
+ challengeTokenClaims struct {
+ jwt.RegisteredClaims
+
+ Scope string `json:"scope"`
+ Action string `json:"act"`
+ }
+
+ authTokenClaims struct {
+ jwt.RegisteredClaims
+
+ Scope string `json:"scope"`
+ User string `json:"usr"`
+ }
+
+ apiTokenClaims struct {
+ jwt.RegisteredClaims
+
+ Scope string `json:"scope"`
+ User string `json:"usr"`
+ StrictPath string `json:"str-p"`
+ StrictQuery map[string]string `json:"str-q,omitempty"`
+ }
+)
+
+func (m *basicTokenManager) getJWTKey(t *jwt.Token) (any, error) {
+ if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
+ return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"])
+ }
+ return m.impl.HmacKey(), nil
+}
+
+func (m *basicTokenManager) GenerateChallengeToken(cliId string, action string) (string, error) {
+ now := time.Now()
+ exp := now.Add(time.Minute * 1)
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, &challengeTokenClaims{
+ RegisteredClaims: jwt.RegisteredClaims{
+ Subject: cliId,
+ Issuer: m.impl.Issuer(),
+ IssuedAt: jwt.NewNumericDate(now),
+ ExpiresAt: jwt.NewNumericDate(exp),
+ },
+ Scope: challengeTokenScope,
+ Action: action,
+ })
+ tokenStr, err := token.SignedString(m.impl.HmacKey())
+ if err != nil {
+ return "", err
+ }
+ return tokenStr, nil
+}
+
+func (m *basicTokenManager) VerifyChallengeToken(cliId string, action string, token string) (err error) {
+ var claims challengeTokenClaims
+ if _, err = jwt.ParseWithClaims(
+ token,
+ &claims,
+ m.getJWTKey,
+ jwt.WithSubject(cliId),
+ jwt.WithIssuedAt(),
+ jwt.WithIssuer(m.impl.Issuer()),
+ ); err != nil {
+ return
+ }
+ if claims.Scope != challengeTokenScope {
+ return ErrScopeNotMatch
+ }
+ if claims.Action != action {
+ return ErrJTINotExists
+ }
+ return
+}
+
+func (m *basicTokenManager) GenerateAuthToken(cliId string, userId string) (string, error) {
+ jti, err := utils.GenRandB64(16)
+ if err != nil {
+ return "", err
+ }
+ now := time.Now()
+ exp := now.Add(time.Hour * 24)
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, &authTokenClaims{
+ RegisteredClaims: jwt.RegisteredClaims{
+ ID: jti,
+ Subject: cliId,
+ Issuer: m.impl.Issuer(),
+ IssuedAt: jwt.NewNumericDate(now),
+ ExpiresAt: jwt.NewNumericDate(exp),
+ },
+ Scope: authTokenScope,
+ User: userId,
+ })
+ tokenStr, err := token.SignedString(m.impl.HmacKey())
+ if err != nil {
+ return "", err
+ }
+ if err = m.impl.AddJTI(jti, exp); err != nil {
+ return "", err
+ }
+ return tokenStr, nil
+}
+
+func (m *basicTokenManager) VerifyAuthToken(cliId string, token string) (id string, user string, err error) {
+ var claims authTokenClaims
+ if _, err = jwt.ParseWithClaims(
+ token,
+ &claims,
+ m.getJWTKey,
+ jwt.WithSubject(cliId),
+ jwt.WithIssuedAt(),
+ jwt.WithIssuer(m.impl.Issuer()),
+ ); err != nil {
+ return
+ }
+ if claims.Scope != authTokenScope {
+ err = ErrScopeNotMatch
+ return
+ }
+ if user = claims.User; user == "" {
+ // reject old token
+ err = ErrJTINotExists
+ return
+ }
+ id = claims.ID
+ if ok := m.impl.ValidJTI(id); !ok {
+ err = ErrJTINotExists
+ return
+ }
+ return
+}
+
+func (m *basicTokenManager) GenerateAPIToken(cliId string, userId string, path string, query map[string]string) (string, error) {
+ jti, err := utils.GenRandB64(8)
+ if err != nil {
+ return "", err
+ }
+ now := time.Now()
+ exp := now.Add(time.Minute * 10)
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, &apiTokenClaims{
+ RegisteredClaims: jwt.RegisteredClaims{
+ ID: jti,
+ Subject: cliId,
+ Issuer: m.impl.Issuer(),
+ IssuedAt: jwt.NewNumericDate(now),
+ ExpiresAt: jwt.NewNumericDate(exp),
+ },
+ Scope: apiTokenScope,
+ User: userId,
+ StrictPath: path,
+ StrictQuery: query,
+ })
+ tokenStr, err := token.SignedString(m.impl.HmacKey())
+ if err != nil {
+ return "", err
+ }
+ if err = m.impl.AddJTI(jti, exp); err != nil {
+ return "", err
+ }
+ return tokenStr, nil
+}
+
+func (m *basicTokenManager) VerifyAPIToken(cliId string, token string, path string, query url.Values) (user string, err error) {
+ var claims apiTokenClaims
+ _, err = jwt.ParseWithClaims(
+ token,
+ &claims,
+ m.getJWTKey,
+ jwt.WithSubject(cliId),
+ jwt.WithIssuedAt(),
+ jwt.WithIssuer(m.impl.Issuer()),
+ )
+ if err != nil {
+ return
+ }
+ if claims.Scope != apiTokenScope {
+ err = ErrScopeNotMatch
+ return
+ }
+ if user = claims.User; user == "" {
+ err = ErrJTINotExists
+ return
+ }
+ if ok := m.impl.ValidJTI(claims.ID); !ok {
+ err = ErrJTINotExists
+ return
+ }
+ if claims.StrictPath != path {
+ err = ErrStrictPathNotMatch
+ return
+ }
+ for k, v := range claims.StrictQuery {
+ if query.Get(k) != v {
+ err = ErrStrictQueryNotMatch
+ return
+ }
+ }
+ return
+}
diff --git a/token/token_db.go b/token/token_db.go
new file mode 100644
index 0000000..f9e507c
--- /dev/null
+++ b/token/token_db.go
@@ -0,0 +1,67 @@
+/**
+ * OpenBmclAPI (Golang Edition)
+ * Copyright (C) 2023 Kevin Z
+ * All rights reserved
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package token
+
+import (
+ "time"
+
+ "github.com/LiterMC/go-openbmclapi/api"
+ "github.com/LiterMC/go-openbmclapi/database"
+)
+
+type DBManager struct {
+ basicTokenManager
+ db database.DB
+ issuer string
+ apiHmacKey []byte
+}
+
+var _ api.TokenManager = (*DBManager)(nil)
+
+func NewDBManager(issuer string, apiHmacKey []byte, db database.DB) *DBManager {
+ m := &DBManager{
+ db: db,
+ issuer: issuer,
+ apiHmacKey: apiHmacKey,
+ }
+ m.basicTokenManager.impl = m
+ return m
+}
+
+func (m *DBManager) Issuer() string {
+ return m.issuer
+}
+
+func (m *DBManager) HmacKey() []byte {
+ return m.apiHmacKey
+}
+
+func (m *DBManager) AddJTI(id string, expire time.Time) error {
+ return m.db.AddJTI(id, expire)
+}
+
+func (m *DBManager) ValidJTI(id string) bool {
+ ok, _ := m.db.ValidJTI(id)
+ return ok
+}
+
+func (m *DBManager) InvalidToken(id string) error {
+ return m.db.RemoveJTI(id)
+}
diff --git a/utils/crypto.go b/utils/crypto.go
index 40f7084..d1fe2bc 100644
--- a/utils/crypto.go
+++ b/utils/crypto.go
@@ -81,8 +81,8 @@ func GenRandB64(n int) (s string, err error) {
return
}
-func LoadOrCreateHmacKey(dataDir string) (key []byte, err error) {
- path := filepath.Join(dataDir, "server.hmac.private_key")
+func LoadOrCreateHmacKey(dataDir string, name string) (key []byte, err error) {
+ path := filepath.Join(dataDir, name + ".hmac.private_key")
buf, err := os.ReadFile(path)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {