Skip to content

Commit

Permalink
add debug api
Browse files Browse the repository at this point in the history
  • Loading branch information
zyxkad committed Feb 23, 2024
1 parent 2d6c294 commit d3726ec
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 9 deletions.
152 changes: 146 additions & 6 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,154 @@ import (
"net/http"
"strconv"
"strings"
"sync"
"time"

"runtime/pprof"

"github.com/golang-jwt/jwt/v5"
)

const (
jwtIssuer = "go-openbmclapi.dashboard.api"

clientIdCookieName = "_id"

clientIdKey = "go-openbmclapi.cluster.client.id"
tokenIdKey = "go-openbmclapi.cluster.token.id"
)

var (
tokenIdSet = make(map[string]struct{})
tokenIdSetMux sync.RWMutex
)

func (cr *Cluster) verifyDashBoardToken(token string) bool {
return false
func registerTokenId(id string) {
tokenIdSetMux.Lock()
defer tokenIdSetMux.Unlock()
tokenIdSet[id] = struct{}{}
}

func unregisterTokenId(id string) {
tokenIdSetMux.Lock()
defer tokenIdSetMux.Unlock()
delete(tokenIdSet, id)
}

func checkTokenId(id string) (ok bool) {
tokenIdSetMux.RLock()
defer tokenIdSetMux.RUnlock()
_, ok = tokenIdSet[id]
return
}

func apiGetClientId(req *http.Request) (id string) {
id, _ = req.Context().Value(clientIdKey).(string)
return
}

func (cr *Cluster) generateToken(cliId string) (string, error) {
jti, err := genRandB64(16)
if err != nil {
return "", err
}
now := time.Now()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"jti": jti,
"sub": "GOBA-tk",
"iss": jwtIssuer,
"iat": (int64)(now.Second()),
"exp": (int64)(now.Add(time.Hour * 12).Second()),
"cli": cliId,
})
tokenStr, err := token.SignedString(cr.apiHmacKey)
if err != nil {
return "", err
}
registerTokenId(jti)
return tokenStr, nil
}

func (cr *Cluster) verifyAPIToken(cliId string, token string) (id string) {
t, err := jwt.Parse(
token,
func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", t.Header["alg"])
}
return cr.apiHmacKey, nil
},
jwt.WithSubject("GOBA-tk"),
jwt.WithIssuedAt(),
jwt.WithIssuer(jwtIssuer),
)
if err != nil {
return ""
}
c, ok := t.Claims.(jwt.MapClaims)
if !ok {
return ""
}
if c["cli"] != cliId {
return ""
}
jti, ok := c["jti"].(string)
if !ok || !checkTokenId(jti) {
return ""
}
return jti
}

func (cr *Cluster) cliIdHandle(next http.Handler) http.Handler {
return (http.HandlerFunc)(func(rw http.ResponseWriter, req *http.Request) {
var id string
if cid, _ := req.Cookie(clientIdCookieName); cid != nil {
id = cid.Value
} else {
var err error
id, err = genRandB64(16)
if err != nil {
http.Error(rw, "cannot generate random number", http.StatusInternalServerError)
return
}
http.SetCookie(rw, &http.Cookie{
Name: clientIdCookieName,
Value: id,
Expires: time.Now().Add(time.Hour * 24 * 365 * 16),
Secure: true,
HttpOnly: true,
})
}
req = req.WithContext(context.WithValue(req.Context(), clientIdKey, id))
next.ServeHTTP(rw, req)
})
}

func (cr *Cluster) apiAuthHandle(next http.Handler) http.Handler {
return (http.HandlerFunc)(func(rw http.ResponseWriter, req *http.Request) {
cli := apiGetClientId(req)
if cli == "" {
writeJson(rw, http.StatusUnauthorized, Map{
"error": "client id not exists",
})
return
}
auth := req.Header.Get("Authorization")
tk, ok := strings.CutPrefix(auth, "Bearer ")
if !ok || cr.verifyDashBoardToken(tk) {
if !ok {
writeJson(rw, http.StatusUnauthorized, Map{
"error": "invalid type of authorization",
})
return
}
id := cr.verifyAPIToken(cli, tk)
if id == "" {
writeJson(rw, http.StatusUnauthorized, Map{
"error": "invalid authorization token",
})
return
}
req = req.WithContext(context.WithValue(req.Context(), tokenIdKey, id))
next.ServeHTTP(rw, req)
})
}
Expand All @@ -53,8 +182,8 @@ func (cr *Cluster) apiAuthHandleFunc(next http.HandlerFunc) http.Handler {
return cr.apiAuthHandle(next)
}

func (cr *Cluster) initAPIv0() (mux *http.ServeMux) {
mux = http.NewServeMux()
func (cr *Cluster) initAPIv0() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
writeJson(rw, http.StatusNotFound, Map{
"error": "404 not found",
Expand All @@ -74,6 +203,17 @@ func (cr *Cluster) initAPIv0() (mux *http.ServeMux) {
"enabled": cr.enabled.Load(),
})
})
mux.HandleFunc("/login", func(rw http.ResponseWriter, req *http.Request) {
cli := apiGetClientId(req)
if cli == "" {
writeJson(rw, http.StatusUnauthorized, Map{
"error": "client id not exists",
})
return
}
// TODO
rw.WriteHeader(http.StatusInternalServerError)
})
mux.Handle("/log", cr.apiAuthHandleFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
e := json.NewEncoder(rw)
Expand Down Expand Up @@ -143,7 +283,7 @@ func (cr *Cluster) initAPIv0() (mux *http.ServeMux) {
rw.WriteHeader(http.StatusOK)
p.WriteTo(rw, debug)
}))
return
return mux
}

type Map = map[string]any
Expand Down
8 changes: 6 additions & 2 deletions cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type Cluster struct {
storageTotalWeight uint
cache Cache
httpCache Cache
apiHmacKey []byte

stats Stats
hits atomic.Int32
Expand Down Expand Up @@ -161,7 +162,7 @@ func NewCluster(
return
}

func (cr *Cluster) Init(ctx context.Context) error {
func (cr *Cluster) Init(ctx context.Context) (err error) {
// Init storages
vctx := context.WithValue(ctx, ClusterCacheCtxKey, cr.cache)
for _, s := range cr.storages {
Expand All @@ -173,7 +174,10 @@ func (cr *Cluster) Init(ctx context.Context) error {
if err := cr.stats.Load(cr.dataDir); err != nil {
logErrorf("Could not load stats: %v", err)
}
return nil
if cr.apiHmacKey, err = loadOrCreateHmacKey(cr.dataDir); err != nil {
return fmt.Errorf("Cannot load hmac key: %w", err)
}
return
}

func (cr *Cluster) allocBuf(ctx context.Context) (slotId int, buf []byte, free func()) {
Expand Down
2 changes: 2 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ func (c *CacheConfig) UnmarshalYAML(n *yaml.Node) (err error) {

type DashboardConfig struct {
Enable bool `yaml:"enable"`
Username string `yaml:"username"`
Password string `yaml:"password"`
PwaName string `yaml:"pwa-name"`
PwaShortName string `yaml:"pwa-short_name"`
PwaDesc string `yaml:"pwa-description"`
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/json-iterator/go v1.1.12 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
Expand Down
2 changes: 1 addition & 1 deletion handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (w *statusResponseWriter) Write(buf []byte) (n int, err error) {
}

func (cr *Cluster) GetHandler() (handler http.Handler) {
cr.handlerAPIv0 = http.StripPrefix("/api/v0", cr.initAPIv0())
cr.handlerAPIv0 = http.StripPrefix("/api/v0", cr.cliIdHandle(cr.initAPIv0()))

handler = cr
{
Expand Down
34 changes: 34 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package main
import (
"context"
"crypto"
crand "crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/pem"
Expand All @@ -32,6 +33,7 @@ import (
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -605,3 +607,35 @@ func IsHex(s string) bool {
func HexTo256(s string) (n int) {
return hexToNumMap[s[0]]*0x10 + hexToNumMap[s[1]]
}

func genRandB64(n int) (s string, err error) {
buf := make([]byte, n)
if _, err = crand.Read(buf); err != nil {
return
}
s = base64.RawURLEncoding.EncodeToString(buf)
return
}

func loadOrCreateHmacKey(dataDir string) (key []byte, err error) {
path := filepath.Join(dataDir, "server.hmac.private_key")
buf, err := os.ReadFile(path)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return
}
var sbuf string
if sbuf, err = genRandB64(256); err != nil {
return
}
buf = ([]byte)(sbuf)
if err = os.WriteFile(path, buf, 0600); err != nil {
return
}
}
key = make([]byte, base64.RawURLEncoding.DecodedLen(len(buf)))
if _, err = base64.RawURLEncoding.Decode(key, buf); err != nil {
return
}
return
}

0 comments on commit d3726ec

Please sign in to comment.