| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- package ccq
- import (
- "context"
- "encoding/hex"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "os"
- "strings"
- "sync"
- "github.com/certusone/wormhole/node/pkg/common"
- "github.com/certusone/wormhole/node/pkg/query"
- "github.com/wormhole-foundation/wormhole/sdk/vaa"
- "go.uber.org/zap"
- "golang.org/x/time/rate"
- "github.com/gagliardetto/solana-go"
- "gopkg.in/godo.v2/watcher/fswatch"
- )
- type (
- Config struct {
- AllowAnythingSupported bool `json:"AllowAnythingSupported"`
- DefaultRateLimit float64 `json:"DefaultRateLimit"`
- DefaultBurstSize int `json:"DefaultBurstSize"`
- Permissions []User `json:"Permissions"`
- }
- User struct {
- UserName string `json:"userName"`
- ApiKey string `json:"apiKey"`
- AllowUnsigned bool `json:"allowUnsigned"`
- AllowAnything bool `json:"allowAnything"`
- RateLimit *float64 `json:"RateLimit"`
- BurstSize *int `json:"BurstSize"`
- LogResponses bool `json:"logResponses"`
- AllowedCalls []AllowedCall `json:"allowedCalls"`
- }
- AllowedCall struct {
- EthCall *EthCall `json:"ethCall"`
- EthCallByTimestamp *EthCallByTimestamp `json:"ethCallByTimestamp"`
- EthCallWithFinality *EthCallWithFinality `json:"ethCallWithFinality"`
- SolanaAccount *SolanaAccount `json:"solAccount"`
- SolanaPda *SolanaPda `json:"solPDA"`
- }
- EthCall struct {
- Chain int `json:"chain"`
- ContractAddress string `json:"contractAddress"`
- Call string `json:"call"`
- }
- EthCallByTimestamp struct {
- Chain int `json:"chain"`
- ContractAddress string `json:"contractAddress"`
- Call string `json:"call"`
- }
- EthCallWithFinality struct {
- Chain int `json:"chain"`
- ContractAddress string `json:"contractAddress"`
- Call string `json:"call"`
- }
- SolanaAccount struct {
- Chain int `json:"chain"`
- Account string `json:"account"`
- }
- SolanaPda struct {
- Chain int `json:"chain"`
- ProgramAddress string `json:"programAddress"`
- // As a future enhancement, we may want to specify the allowed seeds.
- }
- PermissionsMap map[string]*permissionEntry
- permissionEntry struct {
- userName string
- apiKey string
- rateLimiter *rate.Limiter
- allowUnsigned bool
- allowAnything bool
- logResponses bool
- allowedCalls allowedCallsForUser // Key is something like "ethCall:2:000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6:06fdde03"
- }
- allowedCallsForUser map[string]struct{}
- Permissions struct {
- lock sync.Mutex
- env common.Environment
- permMap PermissionsMap
- fileName string
- watcher *fswatch.Watcher
- }
- )
- // NewPermissions creates a Permissions object which contains the per-user permissions.
- func NewPermissions(fileName string, env common.Environment) (*Permissions, error) {
- permMap, err := parseConfigFile(fileName, env)
- if err != nil {
- return nil, err
- }
- return &Permissions{
- permMap: permMap,
- fileName: fileName,
- }, nil
- }
- // StartWatcher creates an fswatcher to watch for updates to the permissions file and reload it when it changes.
- func (perms *Permissions) StartWatcher(ctx context.Context, logger *zap.Logger, errC chan error) {
- logger = logger.With(zap.String("component", "perms"))
- perms.watcher = fswatch.NewWatcher(perms.fileName)
- fsChan := perms.watcher.Start()
- common.RunWithScissors(ctx, errC, "perm_file_watcher", func(ctx context.Context) error {
- for {
- select {
- case <-ctx.Done():
- return nil
- case notif := <-fsChan:
- if notif.Path != perms.fileName {
- return fmt.Errorf("permissions watcher received an update for an unexpected file: %s", notif.Path)
- }
- logger.Info("the permissions file has been updated", zap.String("fileName", notif.Path), zap.Int("event", int(notif.Event)))
- perms.Reload(logger)
- }
- }
- })
- }
- // Reload reloads the permissions file.
- func (perms *Permissions) Reload(logger *zap.Logger) {
- permMap, err := parseConfigFile(perms.fileName, perms.env)
- if err != nil {
- logger.Error("failed to reload the permissions file, sticking with the old one", zap.String("fileName", perms.fileName), zap.Error(err))
- permissionFileReloadsFailure.Inc()
- return
- }
- logger.Info("successfully reloaded the permissions file, switching to it", zap.String("fileName", perms.fileName))
- perms.lock.Lock()
- perms.permMap = permMap
- perms.lock.Unlock()
- permissionFileReloadsSuccess.Inc()
- }
- // StopWatcher stops the permissions file watcher.
- func (perms *Permissions) StopWatcher() {
- if perms.watcher != nil {
- perms.watcher.Stop()
- }
- }
- // GetUserEntry returns the permissions entry for a given API key. It uses the lock to protect against updates.
- func (perms *Permissions) GetUserEntry(apiKey string) (*permissionEntry, bool) {
- perms.lock.Lock()
- defer perms.lock.Unlock()
- userEntry, exists := perms.permMap[apiKey]
- return userEntry, exists
- }
- const ETH_CALL_SIG_LENGTH = 4
- // parseConfigFile parses the permissions config file into a map keyed by API key.
- func parseConfigFile(fileName string, env common.Environment) (PermissionsMap, error) {
- jsonFile, err := os.Open(fileName)
- if err != nil {
- return nil, fmt.Errorf(`failed to open permissions file "%s": %w`, fileName, err)
- }
- defer jsonFile.Close()
- byteValue, err := io.ReadAll(jsonFile)
- if err != nil {
- return nil, fmt.Errorf(`failed to read permissions file "%s": %w`, fileName, err)
- }
- retVal, err := parseConfig(byteValue, env)
- if err != nil {
- return nil, fmt.Errorf(`failed to parse permissions file "%s": %w`, fileName, err)
- }
- return retVal, err
- }
- // parseConfig parses the permissions config from a buffer into a map keyed by API key.
- func parseConfig(byteValue []byte, env common.Environment) (PermissionsMap, error) {
- config := Config{DefaultBurstSize: 1}
- if err := json.Unmarshal(byteValue, &config); err != nil {
- return nil, fmt.Errorf(`failed to unmarshal json: %w`, err)
- }
- // According to the docs, a burst size of zero does not allow any events. We don't want that!
- if config.DefaultBurstSize == 0 {
- return nil, errors.New("the default burst size may not be zero")
- }
- if config.AllowAnythingSupported && env == common.MainNet {
- return nil, fmt.Errorf(`the "allowAnythingSupported" flag is not supported in mainnet`)
- }
- ret := make(PermissionsMap)
- userNames := map[string]struct{}{}
- for _, user := range config.Permissions {
- // Since we log user names in all our error messages, make sure they are unique.
- if _, exists := userNames[user.UserName]; exists {
- return nil, fmt.Errorf(`UserName "%s" is a duplicate`, user.UserName)
- }
- userNames[user.UserName] = struct{}{}
- apiKey := strings.ToLower(user.ApiKey)
- if _, exists := ret[apiKey]; exists {
- return nil, fmt.Errorf(`API key "%s" is a duplicate`, apiKey)
- }
- if user.AllowAnything {
- if !config.AllowAnythingSupported {
- return nil, fmt.Errorf(`UserName "%s" has "allowAnything" specified when the feature is not enabled`, user.UserName)
- }
- if len(user.AllowedCalls) != 0 {
- return nil, fmt.Errorf(`UserName "%s" has "allowedCalls" specified with "allowAnything", which is not allowed`, user.UserName)
- }
- }
- var rateLimiter *rate.Limiter
- rateLimit := config.DefaultRateLimit
- if user.RateLimit != nil {
- rateLimit = *user.RateLimit
- }
- if rateLimit != 0 {
- burstSize := config.DefaultBurstSize
- if user.BurstSize != nil {
- burstSize = *user.BurstSize
- }
- if burstSize == 0 {
- return nil, errors.New("if rate limiting is enabled, the burst size may not be zero")
- }
- rateLimiter = rate.NewLimiter(rate.Limit(rateLimit), burstSize)
- }
- // Build the list of allowed calls for this API key.
- allowedCalls := make(allowedCallsForUser)
- for _, ac := range user.AllowedCalls {
- var chain int
- var callType, contractAddressStr, callStr, callKey string
- // var contractAddressStr string
- if ac.EthCall != nil {
- callType = "ethCall"
- chain = ac.EthCall.Chain
- contractAddressStr = ac.EthCall.ContractAddress
- callStr = ac.EthCall.Call
- } else if ac.EthCallByTimestamp != nil {
- callType = "ethCallByTimestamp"
- chain = ac.EthCallByTimestamp.Chain
- contractAddressStr = ac.EthCallByTimestamp.ContractAddress
- callStr = ac.EthCallByTimestamp.Call
- } else if ac.EthCallWithFinality != nil {
- callType = "ethCallWithFinality"
- chain = ac.EthCallWithFinality.Chain
- contractAddressStr = ac.EthCallWithFinality.ContractAddress
- callStr = ac.EthCallWithFinality.Call
- } else if ac.SolanaAccount != nil {
- // We assume the account is base58, but if it starts with "0x" it should be 32 bytes of hex.
- account := ac.SolanaAccount.Account
- if strings.HasPrefix(account, "0x") {
- buf, err := hex.DecodeString(account[2:])
- if err != nil {
- return nil, fmt.Errorf(`invalid solana account hex string "%s" for user "%s": %w`, account, user.UserName, err)
- }
- if len(buf) != query.SolanaPublicKeyLength {
- return nil, fmt.Errorf(`invalid solana account hex string "%s" for user "%s, must be %d bytes`, account, user.UserName, query.SolanaPublicKeyLength)
- }
- account = solana.PublicKey(buf).String()
- } else {
- // Make sure it is valid base58.
- _, err := solana.PublicKeyFromBase58(account)
- if err != nil {
- return nil, fmt.Errorf(`solana account string "%s" for user "%s" is not valid base58: %w`, account, user.UserName, err)
- }
- }
- callKey = fmt.Sprintf("solAccount:%d:%s", ac.SolanaAccount.Chain, account)
- } else if ac.SolanaPda != nil {
- // We assume the account is base58, but if it starts with "0x" it should be 32 bytes of hex.
- pa := ac.SolanaPda.ProgramAddress
- if strings.HasPrefix(pa, "0x") {
- buf, err := hex.DecodeString(pa[2:])
- if err != nil {
- return nil, fmt.Errorf(`invalid solana program address hex string "%s" for user "%s": %w`, pa, user.UserName, err)
- }
- if len(buf) != query.SolanaPublicKeyLength {
- return nil, fmt.Errorf(`invalid solana program address hex string "%s" for user "%s, must be %d bytes`, pa, user.UserName, query.SolanaPublicKeyLength)
- }
- pa = solana.PublicKey(buf).String()
- } else {
- // Make sure it is valid base58.
- _, err := solana.PublicKeyFromBase58(pa)
- if err != nil {
- return nil, fmt.Errorf(`solana program address string "%s" for user "%s" is not valid base58: %w`, pa, user.UserName, err)
- }
- }
- callKey = fmt.Sprintf("solPDA:%d:%s", ac.SolanaPda.Chain, pa)
- } else {
- return nil, fmt.Errorf(`unsupported call type for user "%s", must be "ethCall", "ethCallByTimestamp", "ethCallWithFinality", "solAccount" or "solPDA"`, user.UserName)
- }
- if callKey == "" {
- // Convert the contract address into a standard format like "000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6".
- contractAddress := contractAddressStr
- if contractAddressStr != "*" {
- contractAddr, err := vaa.StringToAddress(contractAddressStr)
- if err != nil {
- return nil, fmt.Errorf(`invalid contract address "%s" for user "%s"`, contractAddressStr, user.UserName)
- }
- contractAddress = contractAddr.String()
- }
- // The call should be the ABI four byte hex hash of the function signature. Parse it into a standard form of "06fdde03".
- call, err := hex.DecodeString(strings.TrimPrefix(callStr, "0x"))
- if err != nil {
- return nil, fmt.Errorf(`invalid eth call "%s" for user "%s"`, callStr, user.UserName)
- }
- if len(call) != ETH_CALL_SIG_LENGTH {
- return nil, fmt.Errorf(`eth call "%s" for user "%s" has an invalid length, must be %d bytes`, callStr, user.UserName, ETH_CALL_SIG_LENGTH)
- }
- // The permission key is the chain, contract address and call formatted as a colon separated string.
- callKey = fmt.Sprintf("%s:%d:%s:%s", callType, chain, contractAddress, hex.EncodeToString(call))
- }
- if _, exists := allowedCalls[callKey]; exists {
- return nil, fmt.Errorf(`"%s" is a duplicate allowed call for user "%s"`, callKey, user.UserName)
- }
- allowedCalls[callKey] = struct{}{}
- }
- pe := &permissionEntry{
- userName: user.UserName,
- apiKey: apiKey,
- rateLimiter: rateLimiter,
- allowUnsigned: user.AllowUnsigned,
- allowAnything: user.AllowAnything,
- logResponses: user.LogResponses,
- allowedCalls: allowedCalls,
- }
- ret[apiKey] = pe
- }
- return ret, nil
- }
|