taldir

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

commit 37874b8c865ff295b1e125ee36c970fc8dedf7bd
parent 820f168bebb3c112782ecd086022fb2d8f1df3b7
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date:   Wed,  6 Jul 2022 16:46:11 +0200

refactor and restructure project

Diffstat:
AMakefile | 7+++++++
Acmd/taldir-cli/main.go | 42++++++++++++++++++++++++++++++++++++++++++
Acmd/taldir-server/main.go | 519+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtaldir-validate-twitter.sh | 2+-
Dtaldir.go | 562-------------------------------------------------------------------------------
Mtest.sh | 2+-
Autil/helper.go | 22++++++++++++++++++++++
7 files changed, 592 insertions(+), 564 deletions(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,7 @@ +all: server cli + +server: + go build ./cmd/taldir-server + +cli: + go build ./cmd/taldir-cli diff --git a/cmd/taldir-cli/main.go b/cmd/taldir-cli/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "os" + "fmt" + "flag" + "taler.net/taldir/util" + "crypto/sha512" +) + +// Generates a link from a code and address +func generateLink(addr string, code string) string { + h := sha512.New() + h.Write([]byte(addr)) + h_addr := util.EncodeBinaryToString(h.Sum(nil)) + return "taler://taldir/" + h_addr + "/" + code + "-wallet" +} + +func main() { + var solveFlag = flag.Bool("s", false, "Provide a solution for the code/pubkey") + var linkFlag = flag.Bool("l", false, "Provide a link for activation") + var codeFlag = flag.String("c", "", "Activation code") + var pubkeyFlag = flag.String("p", "", "Public key") + var addressFlag = flag.String("a", "", "Address") + flag.Parse() + if *solveFlag { + if len(*codeFlag) == 0 || len(*pubkeyFlag) == 0 { + fmt.Println("You need to provide an activation code and a public key to generate a solution") + os.Exit(1) + } + fmt.Println(util.GenerateSolution(*pubkeyFlag, *codeFlag)) + os.Exit(0) + } + if *linkFlag { + if len(*codeFlag) == 0 || len(*addressFlag) == 0 { + fmt.Println("You need to provide an activation code and an address to generate a link") + os.Exit(1) + } + fmt.Println(generateLink(*addressFlag, *codeFlag)) + os.Exit(0) + } +} diff --git a/cmd/taldir-server/main.go b/cmd/taldir-server/main.go @@ -0,0 +1,519 @@ +package main + +/* TODO + - ToS API (terms, privacy) with localizations + - Prettify QR code landing page + - Base32: Use gnunet-go module? (currently copied) + - OrderId processing + - Maintenance of database: When to delete expired validations? +*/ + +import ( + "os" + "os/exec" + "bufio" + "time" + "fmt" + "log" + "flag" + "net/http" + "html/template" + "encoding/json" + "github.com/gorilla/mux" + "gorm.io/gorm" + "encoding/base64" + "taler.net/taldir/util" + "math/rand" + "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. sha256(<email address>) +func getSingleEntry(w http.ResponseWriter, r *http.Request){ + vars := mux.Vars(r) + var entry Entry + //identityKeyHash := hashIdentityKey(vars["identity_key"]) + 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) +} + + +// Generates random reference token used in the validation flow. +func generateToken() string { + randBytes := make([]byte, 32) + _, err := rand.Read(randBytes) + if err != nil { + panic(err) + } + return util.EncodeBinaryToString(randBytes) +} + +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 = 1006 //TALER_EC_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 = 3 //TALER_EC_NOT_IMPLEMENTED + 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: 23, //FIXME TALER_EC_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 + if err == nil { + // FIXME: Validation already pending for this address + // How should we proceed here? Expire old validations? + w.WriteHeader(202) + return + } else { + validation.Code = generateToken() + validation.Inbox = req.Inbox + validation.Duration = req.Duration + validation.PublicKey = req.PublicKey + 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() + out, err := exec.Command(command, req.Address, validation.Code).Output() + if err != nil { + log.Fatal(err) + db.Delete(&validation) + w.WriteHeader(500) + return + } + w.WriteHeader(202) + fmt.Printf("Output from method script is %s\n", 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("/directory/{identity_key}", returnSingleEntry).Methods("GET") + //myRouter.HandleFunc("/validation/{reference}", validateSingleEntry).Methods("GET") + 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() { + _cfg, err := ini.Load("taldir.conf") + 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") + } + var dropFlag = flag.Bool("D", false, "Drop all data in table (DANGEROUS!)") + flag.Parse() + 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) + } + if *dropFlag { + fmt.Println("Really delete all data in database? [y/N]:") + reader := bufio.NewReader(os.Stdin) + char, _, err := reader.ReadRune() + + 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{}) + } + os.Exit(0) + } + os.Exit(1) + } + + validationTpl, err = template.ParseFiles("templates/validation_landing.html") + if err != nil { + fmt.Println(err) + } + handleRequests() +} diff --git a/taldir-validate-twitter.sh b/taldir-validate-twitter.sh @@ -7,6 +7,6 @@ # TWITTER_USER=$1 CODE=$2 -LINK=$(./taldir -l -a $1 -c $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.go b/taldir.go @@ -1,562 +0,0 @@ -package main - -/* TODO - - ToS API (terms, privacy) with localizations - - Prettify QR code landing page - - Base32: Use gnunet-go module? (currently copied) - - OrderId processing - - Maintenance of database: When to delete expired validations? -*/ - -import ( - "os" - "os/exec" - "bufio" - "time" - "fmt" - "log" - "flag" - "net/http" - "html/template" - "encoding/json" - "github.com/gorilla/mux" - "gorm.io/gorm" - "encoding/base64" - "taler.net/taldir/util" - "math/rand" - "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. sha256(<email address>) -func getSingleEntry(w http.ResponseWriter, r *http.Request){ - vars := mux.Vars(r) - var entry Entry - //identityKeyHash := hashIdentityKey(vars["identity_key"]) - 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 := 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) -} - - -// Generates random reference token used in the validation flow. -func generateToken() string { - randBytes := make([]byte, 32) - _, err := rand.Read(randBytes) - if err != nil { - panic(err) - } - return util.EncodeBinaryToString(randBytes) -} - -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 = 1006 //TALER_EC_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 = 3 //TALER_EC_NOT_IMPLEMENTED - 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: 23, //FIXME TALER_EC_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 - if err == nil { - // FIXME: Validation already pending for this address - // How should we proceed here? Expire old validations? - w.WriteHeader(202) - return - } else { - validation.Code = generateToken() - validation.Inbox = req.Inbox - validation.Duration = req.Duration - validation.PublicKey = req.PublicKey - 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() - out, err := exec.Command(command, req.Address, validation.Code).Output() - if err != nil { - log.Fatal(err) - db.Delete(&validation) - w.WriteHeader(500) - return - } - w.WriteHeader(202) - fmt.Printf("Output from method script is %s\n", 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 -} - -// Generates a link from a code and address -func generateLink(addr string, code string) string { - h := sha512.New() - h.Write([]byte(addr)) - h_addr := util.EncodeBinaryToString(h.Sum(nil)) - return "taler://taldir/" + h_addr + "/" + code + "-wallet" -} - -// Generates a solution from a code and pubkey -func generateSolution(pubkeyEncoded string, code string) string { - pubkey, err := util.DecodeStringToBinary(pubkeyEncoded, 36) - if err != nil { - fmt.Println("error decoding pubkey:", err) - return "" - } - h := sha512.New() - h.Write([]byte(code)) - h.Write(pubkey) - return util.EncodeBinaryToString(h.Sum(nil)) -} - - -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("/directory/{identity_key}", returnSingleEntry).Methods("GET") - //myRouter.HandleFunc("/validation/{reference}", validateSingleEntry).Methods("GET") - 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() { - _cfg, err := ini.Load("taldir.conf") - 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") - } - var solveFlag = flag.Bool("s", false, "Provide a solution for the code/pubkey") - var linkFlag = flag.Bool("l", false, "Provide a link for activation") - var codeFlag = flag.String("c", "", "Activation code") - var pubkeyFlag = flag.String("p", "", "Public key") - var addressFlag = flag.String("a", "", "Address") - var dropFlag = flag.Bool("D", false, "Drop all data in table (DANGEROUS!)") - flag.Parse() - if *solveFlag { - if len(*codeFlag) == 0 || len(*pubkeyFlag) == 0 { - fmt.Println("You need to provide an activation code and a public key to generate a solution") - os.Exit(1) - } - fmt.Println(generateSolution(*pubkeyFlag, *codeFlag)) - os.Exit(0) - } - validators = make(map[string]bool) - for _, a := range strings.Split(cfg.Section("taldir").Key("validators").String(), " ") { - validators[a] = true - } - if *linkFlag { - if len(*codeFlag) == 0 || len(*addressFlag) == 0 { - fmt.Println("You need to provide an activation code and an address to generate a link") - os.Exit(1) - } - fmt.Println(generateLink(*addressFlag, *codeFlag)) - os.Exit(0) - } - 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) - } - if *dropFlag { - fmt.Println("Really delete all data in database? [y/N]:") - reader := bufio.NewReader(os.Stdin) - char, _, err := reader.ReadRune() - - 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{}) - } - os.Exit(0) - } - os.Exit(1) - } - - validationTpl, err = template.ParseFiles("templates/validation_landing.html") - if err != nil { - fmt.Println(err) - } - handleRequests() -} diff --git a/test.sh b/test.sh @@ -8,7 +8,7 @@ H_ADDRESS=`echo -n abc@test | openssl dgst -binary -sha512 | gnunet-base32` echo "Code: $CODE; Address: $H_ADDRESS" # Validate # echo localhost:11000/register/$H_ADDRESS/$CODE -SOLUTION=$(./taldir -s -c ${CODE} -p ${PUBKEY}) +SOLUTION=$(./taldir-cli -s -c ${CODE} -p ${PUBKEY}) echo "Solution: $SOLUTION" curl -v localhost:11000/$H_ADDRESS --data "{\"solution\": \"${SOLUTION}\"}" # Get mapping diff --git a/util/helper.go b/util/helper.go @@ -0,0 +1,22 @@ +package util + +import ( + "fmt" + "crypto/sha512" +) + + +// Generates a solution from a code and pubkey +func GenerateSolution(pubkeyEncoded string, code string) string { + pubkey, err := DecodeStringToBinary(pubkeyEncoded, 36) + if err != nil { + fmt.Println("error decoding pubkey:", err) + return "" + } + h := sha512.New() + h.Write([]byte(code)) + h.Write(pubkey) + return EncodeBinaryToString(h.Sum(nil)) +} + +