diff --git a/.dockerignore b/.dockerignore index 5a71055bc..34284bb55 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ test/ bin/cql* +conf/testnet*/*.keystore *.cover.out diff --git a/blockproducer/metastate.go b/blockproducer/metastate.go index 2473d899b..c21c68e49 100644 --- a/blockproducer/metastate.go +++ b/blockproducer/metastate.go @@ -825,13 +825,6 @@ func (s *metaState) updatePermission(tx *types.UpdatePermission) (err error) { }).WithError(ErrDatabaseNotFound).Error("unexpected error in updatePermission") return ErrDatabaseNotFound } - if !tx.Permission.IsValid() { - log.WithFields(log.Fields{ - "permission": tx.Permission, - "dbID": tx.TargetSQLChain.DatabaseID(), - }).WithError(ErrInvalidPermission).Error("unexpected error in updatePermission") - return ErrInvalidPermission - } // check whether sender has super privilege and find targetUser numOfSuperUsers := 0 diff --git a/blockproducer/metastate_test.go b/blockproducer/metastate_test.go index 80620e5a0..c290fc14d 100644 --- a/blockproducer/metastate_test.go +++ b/blockproducer/metastate_test.go @@ -1037,10 +1037,11 @@ func TestMetaState(t *testing.T) { err = up.Sign(privKey1) So(err, ShouldBeNil) err = ms.apply(&up) - So(errors.Cause(err), ShouldEqual, ErrInvalidPermission) + So(err, ShouldBeNil) // test permission update // addr1(admin) update addr3 as admin up.TargetUser = addr3 + up.Nonce++ up.Permission = types.UserPermissionFromRole(types.Admin) err = up.Sign(privKey1) So(err, ShouldBeNil) @@ -1073,7 +1074,7 @@ func TestMetaState(t *testing.T) { err = ms.apply(&up) So(errors.Cause(err), ShouldEqual, ErrNoSuperUserLeft) // addr1(read) update addr3(admin) fail - up.Nonce = cd1.Nonce + 2 + up.Nonce = cd1.Nonce + 3 err = up.Sign(privKey1) So(err, ShouldBeNil) err = ms.apply(&up) @@ -1313,7 +1314,7 @@ func TestMetaState(t *testing.T) { invalidIk3 := &types.IssueKeys{ IssueKeysHeader: types.IssueKeysHeader{ TargetSQLChain: dbAccount, - Nonce: 3, + Nonce: 4, }, } err = invalidIk3.Sign(privKey1) diff --git a/cmd/cql-faucet/api.go b/cmd/cql-faucet/api.go index 9fca5ad0a..4fa78920a 100644 --- a/cmd/cql-faucet/api.go +++ b/cmd/cql-faucet/api.go @@ -20,142 +20,429 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "regexp" + "strconv" "time" + "github.com/gorilla/handlers" "github.com/gorilla/mux" + "github.com/pkg/errors" + pi "github.com/CovenantSQL/CovenantSQL/blockproducer/interfaces" + "github.com/CovenantSQL/CovenantSQL/client" + "github.com/CovenantSQL/CovenantSQL/crypto" + "github.com/CovenantSQL/CovenantSQL/crypto/asymmetric" + "github.com/CovenantSQL/CovenantSQL/crypto/hash" + "github.com/CovenantSQL/CovenantSQL/crypto/kms" + "github.com/CovenantSQL/CovenantSQL/proto" + "github.com/CovenantSQL/CovenantSQL/route" + "github.com/CovenantSQL/CovenantSQL/rpc" + "github.com/CovenantSQL/CovenantSQL/types" "github.com/CovenantSQL/CovenantSQL/utils/log" ) const ( - argAddress = "address" - argMediaURL = "media_url" - argApplicationID = "id" + argAccount = "account" + argEmail = "email" + argDatabase = "db" + argTx = "tx" + argNodeCount = "node_count" ) var ( - apiTimeout = time.Second * 10 - regexAddress = regexp.MustCompile("^[a-zA-Z0-9]{64}$") - regexMediaURL = regexp.MustCompile("^(http|ftp|https)://([\\w\\-_]+(?:(?:\\.[\\w\\-_]+)+))([\\w\\-\\.,@?^=%&:/~\\+#]*[\\w\\-\\@?^=%&/~\\+#])?$") - regexApplicationID = regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$") + apiTimeout = time.Minute * 10 + regexAccount = regexp.MustCompile("^[a-zA-Z0-9]{64}$") ) +func jsonContentType(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + // test if request is post + if r.Method == http.MethodPost && + r.Header.Get("Content-Type") == "application/json" && + r.Body != nil { + // parse json and set to form in request + var d map[string]interface{} + + if err := json.NewDecoder(r.Body).Decode(&d); err != nil { + // decode failed + log.WithError(err).Warning("decode request failed") + } else { + // fill data to new form + r.Form = make(url.Values) + + for k, v := range d { + r.Form.Set(k, fmt.Sprintf("%v", v)) + } + + r.PostForm = r.Form + } + } + + next.ServeHTTP(rw, r) + }) +} + func sendResponse(code int, success bool, msg interface{}, data interface{}, rw http.ResponseWriter) { msgStr := "ok" if msg != nil { msgStr = fmt.Sprint(msg) } - // cors support - rw.Header().Set("Access-Control-Allow-Origin", "*") rw.WriteHeader(code) - json.NewEncoder(rw).Encode(map[string]interface{}{ + _ = json.NewEncoder(rw).Encode(map[string]interface{}{ "status": msgStr, "success": success, "data": data, }) } -func corsHandler(rw http.ResponseWriter, r *http.Request) { - rw.Header().Set("Access-Control-Allow-Origin", "*") - rw.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") - rw.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization") - rw.WriteHeader(http.StatusOK) - rw.Write([]byte{}) +type service struct { + p *Persistence + addr proto.AccountAddress } -type tokenDispenser struct { - p *Persistence +func (d *service) parseAccountAddress(account string) (addr proto.AccountAddress, err error) { + var h *hash.Hash + + if h, err = hash.NewHashFromStr(account); err != nil { + return + } + + addr = proto.AccountAddress(*h) + return } -func (d *tokenDispenser) poll(rw http.ResponseWriter, r *http.Request) { +func (d *service) applyToken(rw http.ResponseWriter, r *http.Request) { // get args - applicationID := r.FormValue(argApplicationID) - address := r.FormValue(argAddress) + var ( + account = r.FormValue(argAccount) + email = r.FormValue(argEmail) + err error + applicationID string + txHash hash.Hash + ) // validate args - if !regexAddress.MatchString(address) { - sendResponse(http.StatusBadRequest, false, ErrInvalidAddress.Error(), nil, rw) + if !regexAccount.MatchString(account) { + // error + sendResponse(http.StatusBadRequest, false, ErrInvalidAccount.Error(), nil, rw) return } - if !regexApplicationID.MatchString(applicationID) { - sendResponse(http.StatusBadRequest, false, ErrInvalidApplicationID.Error(), nil, rw) + // check limits + if err = d.p.checkAccountLimit(account); err != nil { + sendResponse(http.StatusTooManyRequests, false, err.Error(), nil, rw) return } - if r, err := d.p.queryState(address, applicationID); err != nil { - // error - sendResponse(http.StatusBadRequest, false, err.Error(), nil, rw) - } else { - // build response - sendResponse(http.StatusOK, true, nil, map[string]interface{}{ - "id": r.applicationID, - "state": int(r.state), - "state_desc": r.state.String(), - "reason": r.failReason, - }, rw) + if err = d.p.checkEmailLimit(email); err != nil { + sendResponse(http.StatusTooManyRequests, false, err.Error(), nil, rw) + return + } + + // account address + if accountAddr, err := d.parseAccountAddress(account); err != nil { + sendResponse(http.StatusBadRequest, false, ErrInvalidAccount.Error(), nil, rw) + return + } else if txHash, err = client.TransferToken(accountAddr, uint64(d.p.tokenAmount), types.Particle); err != nil { + // send token + sendResponse(http.StatusInternalServerError, false, err.Error(), nil, rw) + return + } + + // add record + if applicationID, err = d.p.addRecord(account, email); err != nil { + sendResponse(http.StatusInternalServerError, false, err.Error(), nil, rw) + return } + sendResponse(http.StatusOK, true, nil, map[string]interface{}{ + "id": applicationID, + "tx": txHash.String(), + "amount": d.p.tokenAmount, + }, rw) + return } -func (d *tokenDispenser) application(rw http.ResponseWriter, r *http.Request) { +func (d *service) getBalance(rw http.ResponseWriter, r *http.Request) { // get args - address := r.FormValue(argAddress) - mediaURL := r.FormValue(argMediaURL) + account := r.FormValue(argAccount) - // validate args - if !regexAddress.MatchString(address) { + if !regexAccount.MatchString(account) { // error - sendResponse(http.StatusBadRequest, false, ErrInvalidAddress.Error(), nil, rw) + sendResponse(http.StatusBadRequest, false, ErrInvalidAccount.Error(), nil, rw) return } - if !regexMediaURL.MatchString(mediaURL) { - // error - sendResponse(http.StatusBadRequest, false, ErrInvalidURL, nil, rw) + // get account balance + var ( + req = new(types.QueryAccountTokenBalanceReq) + resp = new(types.QueryAccountTokenBalanceResp) + err error + ) + + if req.Addr, err = d.parseAccountAddress(account); err != nil { + sendResponse(http.StatusBadRequest, false, ErrInvalidAccount.Error(), nil, rw) + return + } + + if err = rpc.RequestBP(route.MCCQueryAccountTokenBalance.String(), req, resp); err != nil { + sendResponse(http.StatusInternalServerError, false, err.Error(), nil, rw) + return + } + + sendResponse(http.StatusOK, true, nil, map[string]interface{}{"balance": resp.Balance}, rw) +} + +func (d *service) createDB(rw http.ResponseWriter, r *http.Request) { + // get args + account := r.FormValue(argAccount) + rawNodeCount := r.FormValue(argNodeCount) + nodeCount := uint16(1) + + if !regexAccount.MatchString(account) { + sendResponse(http.StatusBadRequest, false, ErrInvalidAccount.Error(), nil, rw) return } - if applicationID, err := d.p.enqueueApplication(address, mediaURL); err != nil { - var status = http.StatusBadRequest - if err == ErrAddressQuotaExceeded || err == ErrAccountQuotaExceeded { - status = http.StatusTooManyRequests - } else if err == ErrEnqueueApplication { - status = http.StatusInternalServerError + if rawNodeCount != "" { + if tempNodeCount, _ := strconv.Atoi(rawNodeCount); tempNodeCount > 0 { + nodeCount = uint16(tempNodeCount) } - sendResponse(status, false, err.Error(), nil, rw) - } else { - sendResponse(http.StatusOK, true, nil, map[string]interface{}{ - "id": applicationID, - }, rw) } - return + var ( + addr proto.AccountAddress + txCreateHash hash.Hash + txCreateState pi.TransactionState + dsn string + dbID proto.DatabaseID + dbAccountAddr proto.AccountAddress + err error + cfg *client.Config + txUpdatePermHash hash.Hash + ) + + if addr, err = d.parseAccountAddress(account); err != nil { + sendResponse(http.StatusBadRequest, false, ErrInvalidAccount.Error(), nil, rw) + return + } + + meta := client.ResourceMeta{} + meta.Node = nodeCount + + if txCreateHash, dsn, err = client.Create(meta); err != nil { + sendResponse(http.StatusInternalServerError, false, err.Error(), nil, rw) + return + } + + if cfg, err = client.ParseDSN(dsn); err != nil { + sendResponse(http.StatusInternalServerError, false, err.Error(), nil, rw) + return + } + + dbID = proto.DatabaseID(cfg.DatabaseID) + + if txCreateState, err = client.WaitTxConfirmation(r.Context(), txCreateHash); err != nil { + sendResponse(http.StatusInternalServerError, false, err.Error(), nil, rw) + return + } else if txCreateState != pi.TransactionStateConfirmed { + sendResponse(http.StatusInternalServerError, false, "create database failed", nil, rw) + return + } + + if dbAccountAddr, err = dbID.AccountAddress(); err != nil { + sendResponse(http.StatusInternalServerError, false, err.Error(), nil, rw) + return + } + + // update permission, add current user as admin + if txUpdatePermHash, err = client.UpdatePermission( + addr, dbAccountAddr, types.UserPermissionFromRole(types.Admin)); err != nil { + sendResponse(http.StatusInternalServerError, false, err.Error(), nil, rw) + return + } + + sendResponse(http.StatusOK, true, nil, map[string]interface{}{ + "tx_create": txCreateHash.String(), + "tx_update_permission": txUpdatePermHash.String(), + "db": dbID, + }, rw) } -func startAPI(v *Verifier, p *Persistence, listenAddr string) (server *http.Server, err error) { +func (d *service) getDBBalance(rw http.ResponseWriter, r *http.Request) { + // get args + account := r.FormValue(argAccount) + dbID := r.FormValue(argDatabase) + + if !regexAccount.MatchString(account) { + sendResponse(http.StatusBadRequest, false, ErrInvalidAccount.Error(), nil, rw) + return + } + + var ( + addr proto.AccountAddress + req = new(types.QuerySQLChainProfileReq) + resp = new(types.QuerySQLChainProfileResp) + err error + ) + + if addr, err = d.parseAccountAddress(account); err != nil { + sendResponse(http.StatusBadRequest, false, ErrInvalidAccount.Error(), nil, rw) + return + } + + req.DBID = proto.DatabaseID(dbID) + + if err = rpc.RequestBP(route.MCCQuerySQLChainProfile.String(), req, resp); err != nil { + sendResponse(http.StatusInternalServerError, false, err.Error(), nil, rw) + return + } + + for _, user := range resp.Profile.Users { + if user.Address == addr { + sendResponse(http.StatusOK, true, nil, map[string]interface{}{ + "deposit": user.Deposit, + "arrears": user.Arrears, + "advance_payment": user.AdvancePayment, + }, rw) + return + } + } + + sendResponse(http.StatusBadRequest, false, ErrInvalidDatabase.Error(), nil, rw) +} + +func (d *service) privatizeDB(rw http.ResponseWriter, r *http.Request) { + // get args + account := r.FormValue(argAccount) + rawDBID := r.FormValue(argDatabase) + + if !regexAccount.MatchString(account) { + sendResponse(http.StatusBadRequest, false, ErrInvalidAccount.Error(), nil, rw) + return + } + + if !regexAccount.MatchString(rawDBID) { + sendResponse(http.StatusBadRequest, false, ErrInvalidDatabase.Error(), nil, rw) + return + } + + var ( + addr proto.AccountAddress + dbID = proto.DatabaseID(rawDBID) + dbAccountAddr proto.AccountAddress + req = new(types.QuerySQLChainProfileReq) + resp = new(types.QuerySQLChainProfileResp) + err error + txHash hash.Hash + ) + + if addr, err = d.parseAccountAddress(account); err != nil { + sendResponse(http.StatusBadRequest, false, ErrInvalidAccount.Error(), nil, rw) + return + } + + req.DBID = dbID + + if err = rpc.RequestBP(route.MCCQuerySQLChainProfile.String(), req, resp); err != nil { + sendResponse(http.StatusInternalServerError, false, ErrInvalidDatabase.Error(), nil, rw) + return + } + + // check current account existence + found := false + + for _, user := range resp.Profile.Users { + if user.Address == addr && user.Permission.HasSuperPermission() { + found = true + break + } + } + + if !found { + sendResponse(http.StatusBadRequest, false, ErrInvalidDatabase.Error(), nil, rw) + return + } + + if dbAccountAddr, err = dbID.AccountAddress(); err != nil { + sendResponse(http.StatusInternalServerError, false, err.Error(), nil, rw) + return + } + + if txHash, err = client.UpdatePermission(d.addr, dbAccountAddr, types.UserPermissionFromRole(types.Void)); err != nil { + sendResponse(http.StatusInternalServerError, false, err.Error(), nil, rw) + return + } + + sendResponse(http.StatusOK, true, nil, map[string]interface{}{"tx": txHash}, rw) +} + +func (d *service) waitTx(rw http.ResponseWriter, r *http.Request) { + // get args + tx := r.FormValue(argTx) + + var ( + txHash *hash.Hash + err error + txState pi.TransactionState + ) + + if txHash, err = hash.NewHashFromStr(tx); err != nil { + sendResponse(http.StatusBadRequest, false, err.Error(), nil, rw) + return + } + + if txState, err = client.WaitTxConfirmation(r.Context(), *txHash); err != nil { + sendResponse(http.StatusInternalServerError, false, err.Error(), nil, rw) + return + } + + sendResponse(http.StatusOK, false, nil, map[string]interface{}{"state": txState.String()}, rw) +} + +func startAPI(p *Persistence, listenAddr string) (server *http.Server, err error) { router := mux.NewRouter() router.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { sendResponse(http.StatusOK, true, nil, nil, rw) }).Methods("GET") - dispenser := &tokenDispenser{ - p: p, + var ( + addr proto.AccountAddress + pk *asymmetric.PublicKey + ) + + if pk, err = kms.GetLocalPublicKey(); err != nil { + err = errors.Wrapf(err, "get faucet account address failed") + return + } else if addr, err = crypto.PubKeyHash(pk); err != nil { + err = errors.Wrapf(err, "convert account address failed") + return + } + + service := &service{ + p: p, + addr: addr, } v1Router := router.PathPrefix("/v1").Subrouter() - v1Router.HandleFunc("/faucet", dispenser.application).Methods("POST") - v1Router.HandleFunc("/faucet", dispenser.poll).Methods("GET") - v1Router.HandleFunc("/faucet", corsHandler).Methods("OPTIONS") + v1Router.Use(jsonContentType) + v1Router.HandleFunc("/apply_token", service.applyToken).Methods("POST") + v1Router.HandleFunc("/account_balance", service.getBalance).Methods("GET", "POST") + v1Router.HandleFunc("/db_balance", service.getDBBalance).Methods("GET", "POST") + v1Router.HandleFunc("/create_database", service.createDB).Methods("POST") + v1Router.HandleFunc("/privatize", service.privatizeDB).Methods("POST") + v1Router.HandleFunc("/wait_tx", service.waitTx).Methods("GET", "POST") server = &http.Server{ Addr: listenAddr, WriteTimeout: apiTimeout, ReadTimeout: apiTimeout, IdleTimeout: apiTimeout, - Handler: router, + Handler: handlers.CORS( + handlers.AllowedHeaders([]string{"Content-Type"}), + )(router), } go func() { diff --git a/cmd/cql-faucet/config.go b/cmd/cql-faucet/config.go index 00e0cb2ed..b43b2a48c 100644 --- a/cmd/cql-faucet/config.go +++ b/cmd/cql-faucet/config.go @@ -18,25 +18,21 @@ package main import ( "io/ioutil" - "time" yaml "gopkg.in/yaml.v2" "github.com/CovenantSQL/CovenantSQL/utils/log" ) -// Config defines the configurable options for faucet application backend. +// Config defines the configurable options for faucet applyToken backend. type Config struct { // faucet server related - ListenAddr string `yaml:"ListenAddr"` - URLRequired string `yaml:"URLRequired"` // can be a part of a valid url - ContentRequired []string `yaml:"ContentRequired"` - FaucetAmount int64 `yaml:"FaucetAmount"` - DatabaseID string `yaml:"DatabaseID"` // database id for persistence - LocalDatabase bool `yaml:"UseLocalDatabase"` // use local sqlite3 database for persistence - AddressDailyQuota uint `yaml:"AddressDailyQuota"` - AccountDailyQuota uint `yaml:"AccountDailyQuota"` - VerificationInterval time.Duration `yaml:"VerificationInterval"` + ListenAddr string `yaml:"ListenAddr"` + FaucetAmount int64 `yaml:"FaucetAmount"` + DatabaseID string `yaml:"DatabaseID"` // database id for persistence + LocalDatabase bool `yaml:"UseLocalDatabase"` // use local sqlite3 database for persistence + AddressDailyQuota uint `yaml:"AddressDailyQuota"` + AccountDailyQuota uint `yaml:"AccountDailyQuota"` } type confWrapper struct { @@ -44,7 +40,7 @@ type confWrapper struct { } // LoadConfig load the common covenantsql client config again for extra faucet config. -func LoadConfig(configPath string) (config *Config, err error) { +func LoadConfig(listenAddr string, configPath string) (config *Config, err error) { var configBytes []byte if configBytes, err = ioutil.ReadFile(configPath); err != nil { log.WithError(err).Error("read config file failed") @@ -66,27 +62,25 @@ func LoadConfig(configPath string) (config *Config, err error) { config = configWrapper.Faucet // validate config - if config.ListenAddr == "" { - err = ErrInvalidFaucetConfig - log.Error("ListenAddr is not defined in faucet config") - return + if listenAddr != "" { + config.ListenAddr = listenAddr } - if config.URLRequired == "" && len(config.ContentRequired) == 0 { + if config.ListenAddr == "" { err = ErrInvalidFaucetConfig - log.Error("at least one URL/Content config for faucet application is required") + log.Error("ListenAddr is not defined in faucet config") return } if config.DatabaseID == "" { err = ErrInvalidFaucetConfig - log.Error("a database id is required for faucet application persistence") + log.Error("a database id is required for faucet applyToken persistence") return } if config.FaucetAmount <= 0 { err = ErrInvalidFaucetConfig - log.Error("a positive faucet amount is required for every application") + log.Error("a positive faucet amount is required for every applyToken") return } @@ -103,11 +97,5 @@ func LoadConfig(configPath string) (config *Config, err error) { return } - if config.VerificationInterval.Nanoseconds() <= 0 { - log.Warning("a valid VerificationInterval is required, 30 seconds assumed") - - config.VerificationInterval = 30 * time.Second - } - return } diff --git a/cmd/cql-faucet/errors.go b/cmd/cql-faucet/errors.go index 0e8f8e609..2b0c83684 100644 --- a/cmd/cql-faucet/errors.go +++ b/cmd/cql-faucet/errors.go @@ -21,22 +21,16 @@ import "errors" var ( // user errors - // ErrInvalidURL represents the invalid media url error. - ErrInvalidURL = errors.New("INVALID_URL") - // ErrInvalidAddress represents address is not a valid test net address. - ErrInvalidAddress = errors.New("INVALID_ADDRESS") - // ErrInvalidApplicationID represents the application id provided is invalid. - ErrInvalidApplicationID = errors.New("INVALID_APPLICATION_ID") - // ErrAccountQuotaExceeded represents the applicant has exceeded the account daily application quota. + // ErrInvalidAccount represents account is not a valid account. + ErrInvalidAccount = errors.New("INVALID_ADDRESS") + // ErrInvalidDatabase represents database id is not valid. + ErrInvalidDatabase = errors.New("INVALID_DATABASE") + // ErrAccountQuotaExceeded represents the applicant has exceeded the account daily applyToken quota. ErrAccountQuotaExceeded = errors.New("ACCOUNT_QUOTA_EXCEEDED") - // ErrAddressQuotaExceeded represents the applicant has exceeded the address daily application quota. - ErrAddressQuotaExceeded = errors.New("ADDRESS_QUOTA_EXCEEDED") - // ErrEnqueueApplication represents failing to enqueue the application request. - ErrEnqueueApplication = errors.New("ENQUEUE_FAILED") - // ErrRequiredContentNotExists represents invalid application which contains no advertising content. - ErrRequiredContentNotExists = errors.New("NO_REQUIRED_CONTENT") - // ErrRequiredURLNotExists represents invalid application which contains no advertising url. - ErrRequiredURLNotExists = errors.New("NO_REQUIRED_LINK") + // ErrEmailQuotaExceeded represents the applicant has exceeded the account daily applyToken quota. + ErrEmailQuotaExceeded = errors.New("EMAIL_QUOTA_EXCEEDED") + // ErrEnqueueApplication represents failing to enqueue the applyToken request. + ErrEnqueueApplication = errors.New("ADD_RECORD_FAILED") // system errors diff --git a/cmd/cql-faucet/main.go b/cmd/cql-faucet/main.go index 6debac8fd..5a7ac72cf 100644 --- a/cmd/cql-faucet/main.go +++ b/cmd/cql-faucet/main.go @@ -35,12 +35,14 @@ const name = "cql-faucet" var ( version = "unknown" + listenAddr string configFile string password string showVersion bool ) func init() { + flag.StringVar(&listenAddr, "listen", "", "API listen addr (will override settings in config file") flag.StringVar(&configFile, "config", "~/.cql/config.yaml", "Configuration file for covenantsql") flag.StringVar(&password, "password", "", "Master key password for covenantsql") flag.BoolVar(&asymmetric.BypassSignature, "bypass-signature", false, @@ -73,7 +75,7 @@ func main() { // load faucet config from same config file var cfg *Config - if cfg, err = LoadConfig(configFile); err != nil { + if cfg, err = LoadConfig(listenAddr, configFile); err != nil { log.WithError(err).Error("read faucet config failed") os.Exit(-1) return @@ -82,22 +84,13 @@ func main() { // init persistence var p *Persistence if p, err = NewPersistence(cfg); err != nil { - log.Errorf("") + log.WithError(err).Error("init persistence storage failed") return } - // init verifier - var v *Verifier - if v, err = NewVerifier(cfg, p); err != nil { - return - } - - // start verifier - go v.run() - // init faucet api var server *http.Server - if server, err = startAPI(v, p, cfg.ListenAddr); err != nil { + if server, err = startAPI(p, cfg.ListenAddr); err != nil { return } @@ -105,9 +98,6 @@ func main() { <-utils.WaitForExit() - // stop verifier - v.stop() - // stop faucet api ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() diff --git a/cmd/cql-faucet/persistence.go b/cmd/cql-faucet/persistence.go index 7b9ef9d5f..b48f6c5a6 100644 --- a/cmd/cql-faucet/persistence.go +++ b/cmd/cql-faucet/persistence.go @@ -32,39 +32,6 @@ import ( _ "github.com/CovenantSQL/go-sqlite3-encrypt" ) -// State defines the token application request state. -type State int - -const ( - // StateApplication represents application request initial state. - StateApplication State = iota - // StateVerified represents the application request has already been verified. - StateVerified - // StateDispensed represents the application request has been fulfilled and tokens are dispensed. - StateDispensed - // StateFailed represents the application is invalid or maybe quota exceeded. - StateFailed - // StateUnknown represents invalid state - StateUnknown -) - -func (s State) String() string { - switch s { - case StateApplication: - return "StateApplication" - case StateVerified: - return "StateVerified" - case StateDispensed: - return "StateDispensed" - case StateFailed: - return "StateFailed" - case StateUnknown: - return "StateUnknown" - } - - return "" -} - // Persistence defines the persistence api for faucet service. type Persistence struct { db *sql.DB @@ -75,34 +42,28 @@ type Persistence struct { // applicationRecord defines single record for verification. type applicationRecord struct { - rowID int64 - applicationID string - platform string - address string - mediaURL string - account string - state State - tokenAmount int64 // covenantsql could store uint64 value, use int64 instead - failReason string + id string + rowID int64 + account string + email string + tokenAmount int64 // covenantsql could not store uint64 value, use int64 instead + createTime time.Time } func (r *applicationRecord) asMap() (result map[string]interface{}) { result = make(map[string]interface{}) + result["id"] = r.id result["rowID"] = r.rowID - result["applicationID"] = r.applicationID - result["platform"] = r.platform - result["address"] = r.address - result["mediaURL"] = r.mediaURL result["account"] = r.account - result["state"] = r.state.String() + result["email"] = r.email result["tokenAmount"] = r.tokenAmount - result["failReason"] = r.failReason + result["createTime"] = r.createTime.String() return } -// NewPersistence returns a new application persistence api. +// NewPersistence returns a new applyToken persistence api. func NewPersistence(faucetCfg *Config) (p *Persistence, err error) { p = &Persistence{ accountDailyQuota: faucetCfg.AccountDailyQuota, @@ -135,28 +96,23 @@ func NewPersistence(faucetCfg *Config) (p *Persistence, err error) { func (p *Persistence) initDB() (err error) { _, err = p.db.ExecContext(context.Background(), `CREATE TABLE IF NOT EXISTS faucet_records ( - id string unique, - platform string, - account string, - url string, - address string, - state int, + id text unique, + account text, + email text, amount bigint, - reason string, ctime datetime )`) return } -func (p *Persistence) checkAccountLimit(platform string, account string) (err error) { - // TODO, consider cache the limits in memory? +func (p *Persistence) checkAccountLimit(account string) (err error) { timeOfDayStart := time.Now().UTC().Format("2006-01-02 00:00:00") // account limit check row := p.db.QueryRowContext(context.Background(), `SELECT COUNT(1) AS cnt FROM faucet_records - WHERE ctime >= ? AND platform = ? AND account = ? AND state IN (?, ?, ?)`, - timeOfDayStart, platform, account, StateApplication, StateVerified, StateDispensed) + WHERE ctime >= ? AND account = ?`, + timeOfDayStart, account) var result uint @@ -167,25 +123,21 @@ func (p *Persistence) checkAccountLimit(platform string, account string) (err er if result >= p.accountDailyQuota { // quota exceeded - log.WithFields(log.Fields{ - "account": account, - "platform": platform, - }).Error("daily account quota exceeded") + log.WithField("account", account).Error("daily account quota exceeded") return ErrAccountQuotaExceeded } return } -func (p *Persistence) checkAddressLimit(address string) (err error) { - // TODO, consider cache the limits in memory? +func (p *Persistence) checkEmailLimit(email string) (err error) { timeOfDayStart := time.Now().UTC().Format("2006-01-02 00:00:00") // account limit check row := p.db.QueryRowContext(context.Background(), `SELECT COUNT(1) AS cnt FROM faucet_records - WHERE ctime >= ? AND address = ? AND state IN (?, ?, ?)`, - timeOfDayStart, address, StateApplication, StateVerified, StateDispensed) + WHERE ctime >= ? AND email = ?`, + timeOfDayStart, email) var result uint @@ -196,142 +148,38 @@ func (p *Persistence) checkAddressLimit(address string) (err error) { if result >= p.addressDailyQuota { // quota exceeded - log.WithFields(log.Fields{ - "address": address, - }).Error("daily address quota exceeded") - return ErrAddressQuotaExceeded + log.WithField("email", email).Error("daily email quota exceeded") + return ErrEmailQuotaExceeded } return } -// enqueueApplication record a new token application to CovenantSQL database. -func (p *Persistence) enqueueApplication(address string, mediaURL string) (applicationID string, err error) { - // resolve account name in address - var meta urlMeta - meta, err = extractPlatformInURL(mediaURL) - if err != nil { - log.WithFields(log.Fields{ - "address": address, - "mediaURL": mediaURL, - }).Errorf("enqueue application with invalid url: %v", err) - return - } - - // check limits - if err = p.checkAccountLimit(meta.platform, meta.account); err != nil { - return - } - if err = p.checkAddressLimit(address); err != nil { - return - } - +// addRecord record a new token applyToken to CovenantSQL database. +func (p *Persistence) addRecord(account string, email string) (applicationID string, err error) { // generate uuid applicationID = uuid.Must(uuid.NewV4()).String() + now := time.Now().UTC().Format("2006-01-02 15:04:05") // enqueue _, err = p.db.ExecContext(context.Background(), `INSERT INTO faucet_records ( id, - platform, account, - url, - address, - state, + email, amount, - reason, ctime - ) VALUES (?, ?, ?, ?, ?, ?, ?, '', CURRENT_TIMESTAMP)`, - applicationID, meta.platform, meta.account, mediaURL, address, StateApplication, p.tokenAmount) + ) VALUES (?, ?, ?, ?, ?)`, + applicationID, account, email, p.tokenAmount, now) if err != nil { log.WithFields(log.Fields{ - "address": address, - "mediaURL": mediaURL, - }).Errorf("enqueue application failed: %v", err) + "account": account, + "email": email, + }).Errorf("enqueue applyToken failed: %v", err) err = ErrEnqueueApplication } return } - -// queryState returns faucet application state. -func (p *Persistence) queryState(address string, applicationID string) (record *applicationRecord, err error) { - row := p.db.QueryRowContext(context.Background(), - `SELECT id, rowid, platform, address, url, account, state, amount, reason FROM faucet_records WHERE - address = ? AND id = ? LIMIT 1`, address, applicationID) - - record = &applicationRecord{} - err = row.Scan(&record.applicationID, &record.rowID, &record.platform, &record.address, &record.mediaURL, - &record.account, &record.state, &record.tokenAmount, &record.failReason) - - return -} - -// getRecords fetch records need to be processed. -func (p *Persistence) getRecords(startRowID int64, platform string, state State, limitCount int) (records []*applicationRecord, err error) { - var rows *sql.Rows - - args := make([]interface{}, 0) - baseSQL := "SELECT id, rowid, platform, address, url, account, state, amount FROM faucet_records WHERE 1=1 " - - if startRowID > 0 { - baseSQL += " AND rowid >= ? " - args = append(args, startRowID) - } - if platform != "" { - baseSQL += " AND platform = ? " - args = append(args, platform) - } - if state != StateUnknown { - baseSQL += " AND state = ? " - args = append(args, state) - } - if limitCount > 0 { - baseSQL += " LIMIT ?" - args = append(args, limitCount) - } - - rows, err = p.db.QueryContext(context.Background(), baseSQL, args...) - - for rows.Next() { - r := &applicationRecord{} - - if err = rows.Scan(&r.applicationID, &r.rowID, &r.platform, &r.address, &r.mediaURL, - &r.account, &r.state, &r.tokenAmount); err != nil { - return - } - - records = append(records, r) - } - - return -} - -// updateRecord updates application record. -func (p *Persistence) updateRecord(record *applicationRecord) (err error) { - _, err = p.db.ExecContext(context.Background(), - `UPDATE faucet_records SET - id = ?, - platform = ?, - address = ?, - url = ?, - account = ?, - state = ?, - reason = ?, - amount = ? - WHERE rowid = ?`, - record.applicationID, - record.platform, - record.address, - record.mediaURL, - record.account, - int(record.state), - record.failReason, - record.tokenAmount, - record.rowID, - ) - - return -} diff --git a/cmd/cql-faucet/resolver.go b/cmd/cql-faucet/resolver.go deleted file mode 100644 index 8bfabcf89..000000000 --- a/cmd/cql-faucet/resolver.go +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2018 The CovenantSQL Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package main - -import ( - "net/url" - "strings" -) - -const ( - platformFacebook = "facebook" - platformTwitter = "twitter" - platformWeibo = "weibo" -) - -type urlMeta struct { - platform string - account string -} - -func extractPlatformInURL(mediaURL string) (meta urlMeta, err error) { - if !strings.HasPrefix(mediaURL, "http") { - mediaURL = "http://" + mediaURL - } - - u, err := url.Parse(mediaURL) - if strings.Contains(u.Hostname(), "facebook") { - // facebook - meta.platform = platformFacebook - pathSegs := strings.Split(u.Path, "/") - // account in first path seg - if len(pathSegs) >= 2 { - meta.account = pathSegs[1] - } - } else if strings.Contains(u.Hostname(), "twitter") { - // twitter - meta.platform = platformTwitter - pathSegs := strings.Split(u.Path, "/") - // account in first path seg - if len(pathSegs) >= 2 { - meta.account = pathSegs[1] - } - } else if strings.Contains(u.Hostname(), "weibo") { - // weibo - meta.platform = platformWeibo - pathSegs := strings.Split(u.Path, "/") - // account in first path seg - if len(pathSegs) >= 2 { - meta.account = pathSegs[1] - } - } else { - err = ErrInvalidURL - } - - return -} diff --git a/cmd/cql-faucet/rpc.go b/cmd/cql-faucet/rpc.go deleted file mode 100644 index 6598df633..000000000 --- a/cmd/cql-faucet/rpc.go +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2018 The CovenantSQL Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package main - -import ( - "github.com/CovenantSQL/CovenantSQL/proto" - "github.com/CovenantSQL/CovenantSQL/rpc" -) - -func requestBP(method string, req interface{}, resp interface{}) (err error) { - var bp proto.NodeID - if bp, err = rpc.GetCurrentBP(); err != nil { - return err - } - return rpc.NewCaller().CallNode(bp, method, req, resp) -} diff --git a/cmd/cql-faucet/verifier.go b/cmd/cql-faucet/verifier.go deleted file mode 100644 index bb7090d2c..000000000 --- a/cmd/cql-faucet/verifier.go +++ /dev/null @@ -1,452 +0,0 @@ -/* - * Copyright 2018 The CovenantSQL Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package main - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "net/http" - "net/url" - "regexp" - "strings" - "sync" - "time" - - "github.com/CovenantSQL/xurls" - "github.com/dyatlov/go-opengraph/opengraph" - - "github.com/CovenantSQL/CovenantSQL/crypto" - "github.com/CovenantSQL/CovenantSQL/crypto/asymmetric" - "github.com/CovenantSQL/CovenantSQL/crypto/kms" - "github.com/CovenantSQL/CovenantSQL/proto" - "github.com/CovenantSQL/CovenantSQL/route" - pt "github.com/CovenantSQL/CovenantSQL/types" - "github.com/CovenantSQL/CovenantSQL/utils/log" -) - -var ( - regexpTextContent = regexp.MustCompile("(?i)\"text\"\\s*:\\s*(\".+\")\\s*,\\s*") - medClient = &http.Client{} - locClient = &http.Client{ - CheckRedirect: func(_ *http.Request, _ []*http.Request) error { - return http.ErrUseLastResponse - }, - } -) - -const ( - uaPC = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.9 Safari/537.36" - uaMobile = "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1" - uaCurl = "curl/7.54.0" - retryCount = 10 - retryTime = time.Second - verificationPerRound = 100 - dispensePerRound = 100 -) - -// Verifier defines the social media post content verifier. -type Verifier struct { - // settings - interval time.Duration - lastVerified int64 - lastDispensed int64 - contentRequired []string - urlRequired string - vaultAddress proto.AccountAddress - privateKey *asymmetric.PrivateKey - publicKey *asymmetric.PublicKey - - // persistence - p *Persistence - - stopCh chan struct{} -} - -// NewVerifier returns a new verifier instance. -func NewVerifier(cfg *Config, p *Persistence) (v *Verifier, err error) { - v = &Verifier{ - interval: cfg.VerificationInterval, - lastVerified: 0, - lastDispensed: 0, - contentRequired: cfg.ContentRequired, - urlRequired: cfg.URLRequired, - p: p, - stopCh: make(chan struct{}), - } - - if v.publicKey, err = kms.GetLocalPublicKey(); err != nil { - return - } - - if v.privateKey, err = kms.GetLocalPrivateKey(); err != nil { - return - } - - // generate source account address - if v.vaultAddress, err = crypto.PubKeyHash(v.publicKey); err != nil { - return - } - - log.WithField("vault", v.vaultAddress.String()).Info("init verifier") - - return -} - -func (v *Verifier) run() { - for { - log.Info("begin verification iteration") - - // fetch records - v.verify() - - // dispense - v.dispense() - - log.Info("end verification iteration") - - select { - case <-time.After(v.interval): - case <-v.stopCh: - return - } - } -} - -func (v *Verifier) stop() { - select { - case <-v.stopCh: - default: - close(v.stopCh) - } -} - -func (v *Verifier) verify() { - wg := &sync.WaitGroup{} - ch := make(chan int64, 3) - runTask := func(wg *sync.WaitGroup, ch chan int64, f func() (int64, error)) { - defer wg.Done() - verified, err := f() - if err != nil { - log.WithError(err).Warning("verify application failed") - ch <- verified - } - } - - wg.Add(1) - go runTask(wg, ch, v.verifyFacebook) - wg.Add(1) - go runTask(wg, ch, v.verifyTwitter) - wg.Add(1) - go runTask(wg, ch, v.verifyWeibo) - - wg.Wait() - close(ch) - - for verified := range ch { - if verified >= v.lastVerified { - v.lastVerified = verified - } - } -} - -func (v *Verifier) verifyFacebook() (verified int64, err error) { - var records []*applicationRecord - if records, err = v.p.getRecords(v.lastVerified, platformFacebook, StateApplication, verificationPerRound); err != nil { - return - } - - // check records - return v.doVerify(records, verifyFacebook) -} - -func (v *Verifier) verifyTwitter() (verified int64, err error) { - var records []*applicationRecord - if records, err = v.p.getRecords(v.lastVerified, platformTwitter, StateApplication, verificationPerRound); err != nil { - return - } - - // check records - return v.doVerify(records, verifyTwitter) -} - -func (v *Verifier) verifyWeibo() (verified int64, err error) { - var records []*applicationRecord - if records, err = v.p.getRecords(v.lastVerified, platformWeibo, StateApplication, verificationPerRound); err != nil { - return - } - - // check records - return v.doVerify(records, verifyWeibo) -} - -func (v *Verifier) dispense() (err error) { - var records []*applicationRecord - if records, err = v.p.getRecords(v.lastDispensed, "", StateVerified, dispensePerRound); err != nil { - return - } - - // dispense - for _, record := range records { - if err = v.dispenseOne(record); err != nil { - return - } - } - - return -} - -func (v *Verifier) dispenseOne(r *applicationRecord) (err error) { - balanceReq := &pt.QueryAccountTokenBalanceReq{} - balanceRes := &pt.QueryAccountTokenBalanceResp{} - balanceReq.Addr = v.vaultAddress - balanceReq.TokenType = pt.Particle - - // get current balance - if err = requestBP(route.MCCQueryAccountTokenBalance.String(), balanceReq, balanceRes); err != nil { - log.WithError(err).Warning("get account balance failed") - } else { - log.WithField("balance", balanceRes.Balance).Info("get account balance") - } - - // allocate nonce - nonceReq := &pt.NextAccountNonceReq{} - nonceResp := &pt.NextAccountNonceResp{} - nonceReq.Addr = v.vaultAddress - - if err = requestBP(route.MCCNextAccountNonce.String(), nonceReq, nonceResp); err != nil { - // allocate nonce failed - log.WithError(err).Warning("allocate nonce for transaction failed") - return - } - - // decode target account address - var targetAddress proto.AccountAddress - - req := &pt.AddTxReq{TTL: 1} - resp := &pt.AddTxResp{} - req.Tx = pt.NewTransfer( - &pt.TransferHeader{ - Sender: v.vaultAddress, - Receiver: targetAddress, - Nonce: nonceResp.Nonce, - Amount: uint64(r.tokenAmount), - }, - ) - if err = req.Tx.Sign(v.privateKey); err != nil { - // sign failed? - return - } - - if err = requestBP(route.MCCAddTx.String(), req, resp); err != nil { - // add transaction failed, try again - log.WithError(err).Warning("send transaction failed") - - return - } - - // save dispense result - r.state = StateDispensed - - if err = v.p.updateRecord(r); err != nil { - // failed - return - } - - log.WithFields(log.Fields(r.asMap())).Info("dispensed application record") - - return -} - -func (v *Verifier) doVerify(records []*applicationRecord, verifyFunc func(string, []string, string) error) (verified int64, err error) { - for _, r := range records { - if err = verifyFunc(r.mediaURL, v.contentRequired, v.urlRequired); err != nil { - r.failReason = err.Error() - r.state = StateFailed - } else { - r.state = StateVerified - } - - if err = v.p.updateRecord(r); err != nil { - // failed - return - } - - log.WithFields(log.Fields(r.asMap())).Info("verified application record") - - verified = r.rowID - } - - return -} - -func verifyFacebook(mediaURL string, contentRequired []string, urlRequired string) (err error) { - var resp string - resp, err = makeRequest(mediaURL, uaPC, retryCount) - if err != nil { - return - } - og := opengraph.NewOpenGraph() - if err = og.ProcessHTML(strings.NewReader(resp)); err != nil { - return - } - - // description contains sharing content - if !containsOneOf(og.Description, contentRequired) { - return ErrRequiredContentNotExists - } - if !strings.Contains(og.Description, urlRequired) { - return ErrRequiredURLNotExists - } - - return nil -} - -func verifyTwitter(mediaURL string, contentRequired []string, urlRequired string) (err error) { - var resp string - resp, err = makeRequest(mediaURL, uaPC, retryCount) - if err != nil { - return - } - og := opengraph.NewOpenGraph() - if err = og.ProcessHTML(strings.NewReader(resp)); err != nil { - return - } - - // description contains sharing content - if !containsOneOf(og.Description, contentRequired) { - return ErrRequiredContentNotExists - } - - // check url - if err = containsURL(og.Description, urlRequired, retryCount); err != nil { - return err - } - - return nil -} - -func verifyWeibo(mediaURL string, contentRequired []string, urlRequired string) (err error) { - var resp string - resp, err = makeRequest(mediaURL, uaMobile, retryCount) - if err != nil { - return - } - // extract text fields - matches := regexpTextContent.FindStringSubmatch(resp) - if len(matches) <= 1 { - // parser err - return ErrRequiredContentNotExists - } - - // unquote json - var textContent string - if err = json.Unmarshal([]byte(matches[1]), &textContent); err != nil { - return - } - - // test - if !containsOneOf(textContent, contentRequired) { - return ErrRequiredContentNotExists - } - if !strings.Contains(textContent, urlRequired) { - return ErrRequiredURLNotExists - } - - return nil -} - -func containsOneOf(content string, contentRequired []string) bool { - log.WithFields(log.Fields{ - "provided": content, - "required": contentRequired, - }).Info("matching content") - for _, v := range contentRequired { - if strings.Contains(content, v) { - return true - } - } - return false -} - -func containsURL(content string, url string, retry int) (err error) { - // extract all urls in string and send test request - urls := xurls.Strict().FindAllString(content, -1) - - for _, shortedURL := range urls { - if strings.Contains(shortedURL, url) { - return nil - } - - if redirectURL, err := locationRequest(shortedURL, uaCurl, retry); err == nil { - if strings.Contains(redirectURL, url) { - return nil - } - } - } - - return ErrRequiredURLNotExists -} - -func makeRequest(reqURL string, ua string, retry int) (response string, err error) { - var req *http.Request - req, err = http.NewRequest("GET", reqURL, bytes.NewReader([]byte{})) - req.Header.Add("User-Agent", ua) - - for i := retry; i >= 0; i-- { - var resp *http.Response - resp, err = medClient.Do(req) - - if err == nil { - defer resp.Body.Close() - var resBytes []byte - if resBytes, err = ioutil.ReadAll(resp.Body); err == nil { - response = string(resBytes) - return - } - } - - time.Sleep(retryTime) - } - - return - -} - -func locationRequest(reqURL string, ua string, retry int) (redirectURL string, err error) { - var req *http.Request - req, err = http.NewRequest("HEAD", reqURL, bytes.NewReader([]byte{})) - req.Header.Add("User-Agent", ua) - - for i := retry; i >= 0; i-- { - var resp *http.Response - resp, err = locClient.Do(req) - - if err == nil { - defer resp.Body.Close() - var urlObj *url.URL - if urlObj, err = resp.Location(); err == nil { - redirectURL = urlObj.String() - return - } - } - - time.Sleep(retryTime) - } - - return -} diff --git a/cmd/cql-faucet/verifier_test.go b/cmd/cql-faucet/verifier_test.go deleted file mode 100644 index 4c19e673d..000000000 --- a/cmd/cql-faucet/verifier_test.go +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2018 The CovenantSQL Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package main - -import ( - "testing" - - . "github.com/smartystreets/goconvey/convey" -) - -func TestVerifyFacebook(t *testing.T) { - Convey("", t, func() { - var err error - err = verifyFacebook("https://www.facebook.com/hupili/posts/1700877176661446", - []string{"xxx", "Initium Media"}, "https://github.com/initiumlab/beijinguprooted") - So(err, ShouldBeNil) - err = verifyFacebook("facebook.com/dualipaofficial/posts/1832797603472815", - []string{"xxx", "ELECTRICITY"}, "http://www.baidu.com") - So(err, ShouldNotBeNil) - err = verifyFacebook("facebook.com/dualipaofficial/posts/1832797603472815", - []string{"xxx", "哈哈"}, "http://smarturl.it/SilkCityElectricity/youtube") - So(err, ShouldNotBeNil) - }) -} - -func TestVerifyTwitter(t *testing.T) { - Convey("", t, func() { - var err error - err = verifyTwitter("https://twitter.com/tualatrix/status/1040460103898394624", - []string{"xxx", "好奇心日报"}, "http://m.qdaily.com") - So(err, ShouldBeNil) - err = verifyTwitter("https://twitter.com/Fenng/status/1040487918995791873", - []string{"xxx", "阿里巴巴"}, "http://www.baidu.com") - So(err, ShouldNotBeNil) - err = verifyTwitter("https://twitter.com/Fenng/status/1040487918995791873", - []string{"xxx", "百度"}, "https://twitter.com") - So(err, ShouldNotBeNil) - }) -} - -func TestVerifyWeibo(t *testing.T) { - Convey("", t, func() { - var err error - err = verifyWeibo("https://weibo.com/2104296457/GzhcXuPNB", - []string{"xxx", "Mavic"}, "https://www.chiphell.com") - So(err, ShouldBeNil) - err = verifyWeibo("https://weibo.com/2104296457/Gz8vO2gOc", - []string{"xxx", "卡西欧"}, "http://www.baidu.com") - So(err, ShouldNotBeNil) - err = verifyWeibo("https://weibo.com/2104296457/Gz8vO2gOc", - []string{"xxx", "哈哈"}, "https://www.chiphell.com") - So(err, ShouldNotBeNil) - }) -} diff --git a/worker/dbms.go b/worker/dbms.go index 9ea0622d8..266632c41 100644 --- a/worker/dbms.go +++ b/worker/dbms.go @@ -295,10 +295,12 @@ func (dbms *DBMS) UpdatePermission(dbID proto.DatabaseID, user proto.AccountAddr } else { exist := false for _, u := range profile.Users { - u.Address = user - u.Permission = permStat.Permission - u.Status = permStat.Status - exist = true + if u.Address == user { + u.Permission = permStat.Permission + u.Status = permStat.Status + exist = true + break + } } if !exist { profile.Users = append(profile.Users, &types.SQLChainUser{