This commit is contained in:
GitProtogen
2026-03-10 15:43:47 +01:00
parent a8151df167
commit f8b43cc54f
3 changed files with 123 additions and 11 deletions
+11 -1
View File
@@ -2,4 +2,14 @@ module go-socket
go 1.26 go 1.26
require nhooyr.io/websocket v1.8.17 require (
github.com/redis/go-redis/v9 v9.18.0
nhooyr.io/websocket v1.8.17
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
)
+24
View File
@@ -1,2 +1,26 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
+88 -10
View File
@@ -1,23 +1,101 @@
package main package main
import ( import (
_ "context" "context"
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
_ "fmt" "fmt"
_ "time" "time"
"github.com/redis/go-redis/v9"
) )
func hashToken(token string) string { var rdb *redis.Client
var hash = sha256.Sum256([]byte(token))
return hex.EncodeToString(hash[:]) func init() {
rdb = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
} }
func generateToken() string { func getAndSaveTokenFor(ctx context.Context, userID string, timeToDie time.Duration) string {
var bytes = make([]byte, 32) bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil { if _, err := rand.Read(bytes); err != nil {
panic(err) panic(fmt.Sprintf("failed to generate random bytes: %v", err))
} }
return hex.EncodeToString(bytes) token := hex.EncodeToString(bytes)
tokenHash := hashToken(token)
tokenKey := fmt.Sprintf("token:%s", tokenHash)
userKey := fmt.Sprintf("user_tokens:%s", userID)
now := time.Now()
expiresAt := now.Add(timeToDie)
pipe := rdb.Pipeline()
pipe.Set(ctx, tokenKey, userID, timeToDie)
// score = expiration unix timestamp
pipe.ZAdd(ctx, userKey, redis.Z{Score: float64(expiresAt.Unix()), Member: tokenHash})
// remove already-expired members
pipe.ZRemRangeByScore(ctx, userKey, "-inf", fmt.Sprintf("%d", now.Unix()))
// set key expiry to latest possible token death
pipe.ExpireAt(ctx, userKey, expiresAt)
if _, err := pipe.Exec(ctx); err != nil {
panic(fmt.Sprintf("failed to execute redis pipeline: %v", err))
}
return token
}
func hashToken(plaintext string) string {
h := sha256.Sum256([]byte(plaintext))
return hex.EncodeToString(h[:])
}
// GetUserID resolves a plaintext token to its owning userID.
// Returns an error if the token is missing or expired.
func GetUserID(ctx context.Context, plaintext string) (string, error) {
key := fmt.Sprintf("token:%s", hashToken(plaintext))
userID, err := rdb.Get(ctx, key).Result()
if err == redis.Nil {
return "", fmt.Errorf("token not found or expired")
}
return userID, err
}
// RevokeToken deletes a single token by its plaintext value.
func RevokeToken(ctx context.Context, plaintext, userID string) error {
hashed := hashToken(plaintext)
tokenKey := fmt.Sprintf("token:%s", hashed)
userKey := fmt.Sprintf("user_tokens:%s", userID)
pipe := rdb.Pipeline()
pipe.Del(ctx, tokenKey)
pipe.SRem(ctx, userKey, hashed)
_, err := pipe.Exec(ctx)
return err
}
// RevokeAllUserTokens deletes every token belonging to a userID.
func RevokeAllUserTokens(ctx context.Context, userID string) error {
userKey := fmt.Sprintf("user_tokens:%s", userID)
hashedTokens, err := rdb.SMembers(ctx, userKey).Result()
if err != nil {
return err
}
pipe := rdb.Pipeline()
for _, hashed := range hashedTokens {
pipe.Del(ctx, fmt.Sprintf("token:%s", hashed))
}
pipe.Del(ctx, userKey)
_, err = pipe.Exec(ctx)
return err
} }