taldir

Directory service to resolve wallet mailboxes by messenger addresses
Log | Files | Refs | Submodules | README | LICENSE

commit 5a70d6d172f2f9330276830f415e62ae32ae032c
parent eac62d894b2e185fdaecb4a62e274951a52a27eb
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date:   Wed,  6 Jul 2022 22:23:48 +0200

Refactor for better testing

Diffstat:
Mcmd/taldir-server/main.go | 487+------------------------------------------------------------------------------
Mscripts/taldir-validate-twitter | 13+++++++++++--
Mtaldir.conf | 2+-
3 files changed, 18 insertions(+), 484 deletions(-)

diff --git a/cmd/taldir-server/main.go b/cmd/taldir-server/main.go @@ -29,445 +29,11 @@ package main import ( "os" - "os/exec" - "bufio" - "time" "fmt" - "log" + "bufio" "flag" - "net/http" - "html/template" - "encoding/json" - "github.com/gorilla/mux" - "gorm.io/gorm" - "encoding/base64" - "taler.net/taldir/util" - "taler.net/taldir/gana" - "crypto/sha512" - "gorm.io/driver/postgres" - "gopkg.in/ini.v1" - "strings" - "github.com/skip2/go-qrcode" ) -type VersionResponse struct { - // libtool-style representation of the Merchant protocol version, see - // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning - // The format is "current:revision:age". - Version string `json:"version"` - - // Name of the protocol. - Name string `json:"name"` // "taler-directory" - - // Supported registration methods - Methods []Method `json:"methods"` - - // fee for one month of registration - MonthlyFee string `json:"monthly_fee"` - -} - -type Method struct { - - // Name of the method, e.g. "email" or "sms". - Name string `json:"name"` - - // per challenge fee - ChallengeFee string `json:"challenge_fee"` - -} - -type RateLimitedResponse struct { - - // Taler error code, TALER_EC_TALDIR_REGISTER_RATE_LIMITED. - Code int `json:"code"` - - // At what frequency are new registrations allowed. FIXME: In what? Currently: In microseconds - RequestFrequency int64 `json:"request_frequency"` - - // The human readable error message. - Hint string `json:"hint"` -} - -type RegisterMessage struct { - - // Address, in method-specific format - Address string `json:"address"` - - // Public key of the user to register - PublicKey string `json:"public_key"` - - // (HTTPS) endpoint URL for the inbox service for this address - Inbox string `json:"inbox_url"` - - // For how long should the registration last - Duration int64 `json:"duration"` - - // Order ID, if the client recently paid for this registration - // FIXME: As an optional field, maybe we want to parse this separately - // instead? - // Order_id string `json:"order_id"` -} - -// A mappind entry from the identity key hash to a wallet key -// The identity key hash is sha256(sha256(identity)|salt) where identity is -// one of the identity key types supported (e.g. email) -type Entry struct { - - // ORM - gorm.Model `json:"-"` - - // The salted hash (SHA512) of the hashed address (h_address) - HsAddress string `json:"-"` - - // (HTTPS) endpoint URL for the inbox service for this address - Inbox string `json:"inbox_url"` - - // Public key of the user to register in base32 - PublicKey string `json:"public_key"` - - // Time of (re)registration. In Unix epoch microseconds) - RegisteredAt int64 `json:"-"` - - // How long the registration lasts in microseconds - Duration int64 `json:"-"` -} - -// A validation is created when a registration for an entry is initiated. -// The validation stores the identity key (sha256(identity)) the secret -// validation reference. The validation reference is sent to the identity -// depending on the out-of-band chennel defined through the identity key type. -type Validation struct { - - // ORM - gorm.Model `json:"-"` - - // The hash (SHA512) of the address - HAddress string `json:"h_address"` - - // For how long should the registration last - Duration int64 `json:"duration"` - - // (HTTPS) endpoint URL for the inbox service for this address - Inbox string `json:"inbox_url"` - - // The activation code sent to the client - Code string `json:"activation_code"` - - // Public key of the user to register - PublicKey string `json:"public_key"` -} - -type ErrorDetail struct { - - // Numeric error code unique to the condition. - // The other arguments are specific to the error value reported here. - Code int `json:"code"` - - // Human-readable description of the error, i.e. "missing parameter", "commitment violation", ... - // Should give a human-readable hint about the error's nature. Optional, may change without notice! - Hint string `json:"hint,omitempty"` - - // Optional detail about the specific input value that failed. May change without notice! - Detail string `json:"detail,omitempty"` - - // Name of the parameter that was bogus (if applicable). - Parameter string `json:"parameter,omitempty"` - - // Path to the argument that was bogus (if applicable). - Path string `json:"path,omitempty"` - - // Offset of the argument that was bogus (if applicable). - Offset string `json:"offset,omitempty"` - - // Index of the argument that was bogus (if applicable). - Index string `json:"index,omitempty"` - - // Name of the object that was bogus (if applicable). - Object string `json:"object,omitempty"` - - // Name of the currency than was problematic (if applicable). - Currency string `json:"currency,omitempty"` - - // Expected type (if applicable). - TypeExpected string `json:"type_expected,omitempty"` - - // Type that was provided instead (if applicable). - TypeActual string `json:"type_actual,omitempty"` -} - -type ValidationConfirmation struct { - Solution string `json:"solution"` -} - -// The main DB handle -var db *gorm.DB - -// Our configuration from the config.json -var cfg *ini.File - -// Map of supported validators as defined in the configuration -var validators map[string]bool - -// landing page -var validationTpl *template.Template - -// Primary lookup function. -// Allows the caller to query a wallet key using the hash(!) of the -// identity, e.g. SHA512(<email address>) -func getSingleEntry(w http.ResponseWriter, r *http.Request){ - vars := mux.Vars(r) - var entry Entry - hs_address := saltHAddress(vars["h_address"]) - var err = db.First(&entry, "hs_address = ?", hs_address).Error - if err == nil { - w.Header().Set("Content-Type", "application/json") - resp, _ := json.Marshal(entry) - w.Write(resp) - return - } - w.WriteHeader(http.StatusNotFound) -} - -// Hashes an identity key (e.g. sha256(<email address>)) with a salt for -// Lookup and storage. -func saltHAddress(h_address string) string { - salt := os.Getenv("TALDIR_SALT") - if "" == salt { - salt = cfg.Section("taldir").Key("salt").MustString("ChangeMe") - } - h := sha512.New() - h.Write([]byte(h_address)) - h.Write([]byte(salt)) - return util.EncodeBinaryToString(h.Sum(nil)) -} - -// Called by the registrant to validate the registration request. The reference ID was -// provided "out of band" using a validation method such as email or SMS -func validationRequest(w http.ResponseWriter, r *http.Request){ - vars := mux.Vars(r) - var entry Entry - var validation Validation - var confirm ValidationConfirmation - var errDetail ErrorDetail - if r.Body == nil { - http.Error(w, "No request body", 400) - return - } - err := json.NewDecoder(r.Body).Decode(&confirm) - if err != nil { - errDetail.Code = 1006 //TALER_EC_JSON_INVALID - errDetail.Hint = "Unable to parse JSON" - resp, _ := json.Marshal(errDetail) - w.WriteHeader(400) - w.Write(resp) - return - } - err = db.First(&validation, "h_address = ?", vars["h_address"]).Error - if err != nil { - w.WriteHeader(http.StatusNotFound) - return - } - expectedSolution := util.GenerateSolution(validation.PublicKey, validation.Code) - if confirm.Solution != expectedSolution { - // FIXME how TF do we rate limit here?? - w.WriteHeader(http.StatusForbidden) - return - } - // FIXME: Expire validations somewhere? - err = db.Delete(&validation).Error - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - entry.HsAddress = saltHAddress(validation.HAddress) - entry.Inbox = validation.Inbox - entry.Duration = validation.Duration - entry.RegisteredAt = time.Now().UnixMicro() - entry.PublicKey = validation.PublicKey - err = db.First(&entry, "hs_address = ?", entry.HsAddress).Error - if err == nil { - db.Save(&entry) - } else { - err = db.Create(&entry).Error - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - } - w.WriteHeader(http.StatusNoContent) -} - - -func registerRequest(w http.ResponseWriter, r *http.Request){ - vars := mux.Vars(r) - var req RegisterMessage - var errDetail ErrorDetail - var validation Validation - var entry Entry - if r.Body == nil { - http.Error(w, "No request body", 400) - return - } - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - errDetail.Code = gana.GENERIC_JSON_INVALID - errDetail.Hint = "Unable to parse JSON" - resp, _ := json.Marshal(errDetail) - w.WriteHeader(400) - w.Write(resp) - return - } - if !validators[vars["method"]] { - errDetail.Code = gana.TALDIR_METHOD_NOT_SUPPORTED - errDetail.Hint = "Unsupported method" - errDetail.Detail = "Given method: " + vars["method"] - resp, _ := json.Marshal(errDetail) - w.WriteHeader(404) - w.Write(resp) - return - } - h := sha512.New() - h.Write([]byte(req.Address)) - validation.HAddress = util.EncodeBinaryToString(h.Sum(nil)) - // We first try if there is already an entry for this address which - // is still valid and the duration is not extended. - hs_address := saltHAddress(validation.HAddress) - err = db.First(&entry, "hs_address = ?", hs_address).Error - if err != nil { - lastRegValidity := entry.RegisteredAt + entry.Duration - requestedValidity := time.Now().UnixMicro() + req.Duration - reqFrequency := cfg.Section("taldir").Key("request_frequency").MustInt64(1000) - earliestReRegistration := entry.RegisteredAt + reqFrequency - // Rate limit re-registrations. - if time.Now().UnixMicro() < earliestReRegistration { - w.WriteHeader(429) - rlResponse := RateLimitedResponse{ - Code: gana.TALDIR_REGISTER_RATE_LIMITED, - RequestFrequency: reqFrequency, - Hint: "Registration rate limit reached", - } - jsonResp, _ := json.Marshal(rlResponse) - w.Write(jsonResp) - return - } - // Do not allow re-registrations with shorter duration. - if requestedValidity <= lastRegValidity { - w.WriteHeader(200) - // FIXME how to return how long it is already paid for?? - return - } - } - err = db.First(&validation, "h_address = ?", validation.HAddress).Error - validation.Code = util.GenerateCode() - validation.Inbox = req.Inbox - validation.Duration = req.Duration - validation.PublicKey = req.PublicKey - if err == nil { - // FIXME: Validation already pending for this address - // How should we proceed here? Expire old validations? - log.Println("Validation for this address already exists") - err = db.Save(&validation).Error - } else { - err = db.Create(&validation).Error - } - if err != nil { - // FIXME: API needs 400 error codes in such cases - w.WriteHeader(http.StatusInternalServerError) - return - } - fmt.Println("Address registration request created:", validation) - if !cfg.Section("taldir-" + vars["method"]).HasKey("command") { - log.Fatal(err) - db.Delete(&validation) - w.WriteHeader(500) - return - } - command := cfg.Section("taldir-" + vars["method"]).Key("command").String() - path, err := exec.LookPath(command) - if err != nil { - log.Println(err) - db.Delete(&validation) - w.WriteHeader(500) - return - } - out, err := exec.Command(path, req.Address, validation.Code).Output() - if err != nil { - log.Println(err) - db.Delete(&validation) - w.WriteHeader(500) - return - } - w.WriteHeader(202) - fmt.Printf("Output from method script %s is %s\n", path, out) -} - -func notImplemented(w http.ResponseWriter, r *http.Request) { - return -} - -func configResponse(w http.ResponseWriter, r *http.Request) { - meths := []Method{} - i := 0 - for key, _ := range validators { - var meth Method - meth.Name = key - meth.ChallengeFee = cfg.Section("taldir-" + key).Key("challenge_fee").MustString("1 Kudos") - i++ - meths = append(meths, meth) - } - cfg := VersionResponse{ - Version: "0:0:0", - Name: "taler-directory", - MonthlyFee: cfg.Section("taldir").Key("monthly_fee").MustString("1 Kudos"), - Methods: meths, - } - w.Header().Set("Content-Type", "application/json") - response, _ := json.Marshal(cfg) - w.Write(response) -} - -func validationPage(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - w.Header().Set("Content-Type", "text/html; charset=utf-8") - var walletLink string - walletLink = "taler://taldir/" + vars["h_address"] + "/" + vars["validation_code"] + "-wallet" - var png []byte - png, err := qrcode.Encode(walletLink, qrcode.Medium, 256) - if err != nil { - w.WriteHeader(500) - return - } - encodedPng := base64.StdEncoding.EncodeToString(png) - - fullData := map[string]interface{}{ - "QRCode": template.URL("data:image/png;base64," + encodedPng), - "WalletLink": template.URL(walletLink), - } - validationTpl.Execute(w, fullData) - return -} - -func handleRequests() { - myRouter := mux.NewRouter().StrictSlash(true) - - /* ToS API */ - myRouter.HandleFunc("/terms", notImplemented).Methods("GET") - myRouter.HandleFunc("/privacy", notImplemented).Methods("GET") - - /* Config API */ - myRouter.HandleFunc("/config", configResponse).Methods("GET") - - - /* Registration API */ - myRouter.HandleFunc("/{h_address}", getSingleEntry).Methods("GET") - myRouter.HandleFunc("/register/{method}", registerRequest).Methods("POST") - myRouter.HandleFunc("/register/{h_address}/{validation_code}", validationPage).Methods("GET") - myRouter.HandleFunc("/{h_address}", validationRequest).Methods("POST") - - log.Fatal(http.ListenAndServe(cfg.Section("taldir").Key("bind_to").MustString("localhost:11000"), myRouter)) -} - func main() { var dropFlag = flag.Bool("D", false, "Drop all data in table (DANGEROUS!)") var cfgFlag = flag.String("c", "", "Configuration file to use") @@ -476,42 +42,8 @@ func main() { if len(*cfgFlag) != 0 { cfgfile = *cfgFlag } - _cfg, err := ini.Load(cfgfile) - if err != nil { - fmt.Printf("Failed to read config: %v", err) - os.Exit(1) - } - cfg = _cfg - if cfg.Section("taldir").Key("production").MustBool(false) { - fmt.Println("Production mode enabled") - } - - validators = make(map[string]bool) - for _, a := range strings.Split(cfg.Section("taldir").Key("validators").String(), " ") { - validators[a] = true - } - validators = make(map[string]bool) - for _, a := range strings.Split(cfg.Section("taldir").Key("validators").String(), " ") { - validators[a] = true - } - - psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", - cfg.Section("taldir-pq").Key("host").MustString("localhost"), - cfg.Section("taldir-pq").Key("port").MustInt64(5432), - cfg.Section("taldir-pq").Key("user").MustString("taldir"), - cfg.Section("taldir-pq").Key("password").MustString("secret"), - cfg.Section("taldir-pq").Key("db_name").MustString("taldir")) - _db, err := gorm.Open(postgres.Open(psqlconn), &gorm.Config{}) - if err != nil { - panic(err) - } - db = _db - if err := db.AutoMigrate(&Entry{}); err != nil { - panic(err) - } - if err := db.AutoMigrate(&Validation{}); err != nil { - panic(err) - } + t := Taldir{} + clearDb := false if *dropFlag { fmt.Println("Really delete all data in database? [y/N]:") reader := bufio.NewReader(os.Stdin) @@ -520,19 +52,12 @@ func main() { if err == nil { fmt.Println(char) if char == 'y' { - fmt.Println("Deleting entries...") - db.Where("1 = 1").Delete(&Entry{}) - fmt.Println("Deleting validations...") - db.Where("1 = 1").Delete(&Validation{}) + clearDb = true } os.Exit(0) } os.Exit(1) } - - validationTpl, err = template.ParseFiles("templates/validation_landing.html") - if err != nil { - fmt.Println(err) - } - handleRequests() + t.Initialize(cfgfile, clearDb) + t.Run() } diff --git a/scripts/taldir-validate-twitter b/scripts/taldir-validate-twitter @@ -1,3 +1,12 @@ #!/bin/bash -echo $1 $2 -echo $2 > validation_code +# +# IMPORTANT: Before this can be used, as the taldir service user +# you need to authorize this CLI app for the taldir twitter account. +# e.g.: +# $ t authorize +# +TWITTER_USER=$1 +CODE=$2 +LINK=$(taldir-cli -l -a $1 -c $2) +MESSAGE="Follow this link to complete your Taldir registration: $LINK" +t dm $TWITTER_USER $MESSAGE diff --git a/taldir.conf b/taldir.conf @@ -1,6 +1,6 @@ [taldir] production = false -validators = "email phone test" +validators = "twitter test" host = "https://taldir.net" bind_to = "localhost:11000" salt = "ChangeMe"