143 lines
2.8 KiB
Go
143 lines
2.8 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"golang.org/x/crypto/xtea"
|
|
)
|
|
|
|
type userID int64
|
|
|
|
const defaultUIDKey = "dkXxWLwqUNRfYmdXRzOkZw=="
|
|
|
|
var (
|
|
uidCodecMu sync.RWMutex
|
|
uidCipher *xtea.Cipher
|
|
)
|
|
|
|
func init() {
|
|
if err := SetUIDKey(defaultUIDKey); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func SetUIDKey(encoded string) error {
|
|
key, err := base64.StdEncoding.DecodeString(strings.TrimSpace(encoded))
|
|
if err != nil {
|
|
return fmt.Errorf("invalid uid key: %w", err)
|
|
}
|
|
cipher, err := xtea.NewCipher(key)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid uid key: %w", err)
|
|
}
|
|
uidCodecMu.Lock()
|
|
uidCipher = cipher
|
|
uidCodecMu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (id *userID) UnmarshalJSON(raw []byte) error {
|
|
value, err := parseUserIDJSON(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*id = userID(value)
|
|
return nil
|
|
}
|
|
|
|
type optionalUserID struct {
|
|
value int64
|
|
set bool
|
|
}
|
|
|
|
func (id *optionalUserID) UnmarshalJSON(raw []byte) error {
|
|
if strings.TrimSpace(string(raw)) == "null" {
|
|
return nil
|
|
}
|
|
value, err := parseUserIDJSON(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id.set = true
|
|
id.value = value
|
|
return nil
|
|
}
|
|
|
|
func (id optionalUserID) ptr() *int64 {
|
|
if !id.set {
|
|
return nil
|
|
}
|
|
return &id.value
|
|
}
|
|
|
|
func parseUserIDJSON(raw []byte) (int64, error) {
|
|
var text string
|
|
if err := json.Unmarshal(raw, &text); err == nil {
|
|
return parseUserID(text)
|
|
}
|
|
return 0, fmt.Errorf("user id must be a string")
|
|
}
|
|
|
|
func parseUserID(value string) (int64, error) {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return 0, fmt.Errorf("empty user id")
|
|
}
|
|
if strings.HasPrefix(value, "usr") {
|
|
decoded, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(strings.TrimPrefix(value, "usr"))
|
|
if err != nil || len(decoded) < 8 {
|
|
return 0, fmt.Errorf("invalid user id")
|
|
}
|
|
return decryptUID(decoded[:8]), nil
|
|
}
|
|
return strconv.ParseInt(value, 10, 64)
|
|
}
|
|
|
|
func formatUserID(id int64) string {
|
|
if id == 0 {
|
|
return ""
|
|
}
|
|
encoded := encryptUID(id)
|
|
return "usr" + base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(encoded)
|
|
}
|
|
|
|
func parseUserIDList(values []userID) []int64 {
|
|
items := make([]int64, 0, len(values))
|
|
seen := make(map[int64]bool, len(values))
|
|
for _, value := range values {
|
|
id := int64(value)
|
|
if id == 0 || seen[id] {
|
|
continue
|
|
}
|
|
seen[id] = true
|
|
items = append(items, id)
|
|
}
|
|
return items
|
|
}
|
|
|
|
func decryptUID(encoded []byte) int64 {
|
|
dst := make([]byte, 8)
|
|
uidCodecMu.RLock()
|
|
cipher := uidCipher
|
|
uidCodecMu.RUnlock()
|
|
cipher.Decrypt(dst, encoded[:8])
|
|
return int64(binary.LittleEndian.Uint64(dst))
|
|
}
|
|
|
|
func encryptUID(id int64) []byte {
|
|
src := make([]byte, 8)
|
|
dst := make([]byte, 8)
|
|
binary.LittleEndian.PutUint64(src, uint64(id))
|
|
uidCodecMu.RLock()
|
|
cipher := uidCipher
|
|
uidCodecMu.RUnlock()
|
|
cipher.Encrypt(dst, src)
|
|
return dst
|
|
}
|