commit 37874b8c865ff295b1e125ee36c970fc8dedf7bd
parent 820f168bebb3c112782ecd086022fb2d8f1df3b7
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date: Wed, 6 Jul 2022 16:46:11 +0200
refactor and restructure project
Diffstat:
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))
+}
+
+