taldir

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

commit 194002465a2502bdf2ec815289d00b97757f9973
parent 07b4475b461fcecec37da5d5878479660167b59f
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date:   Mon, 18 Jul 2022 12:55:18 +0200

go fmt

Diffstat:
Mcmd/taldir-cli/main.go | 75+++++++++++++++++++++++++++++++++++++--------------------------------------
Mcmd/taldir-server/main.go | 32++++++++++++++++----------------
Mcmd/taldir-server/main_test.go | 428+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mpkg/rest/taldir.go | 1224+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mpkg/taler/merchant.go | 233+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mpkg/util/helper.go | 94+++++++++++++++++++++++++++++++++++++++----------------------------------------
6 files changed, 1033 insertions(+), 1053 deletions(-)

diff --git a/cmd/taldir-cli/main.go b/cmd/taldir-cli/main.go @@ -16,52 +16,51 @@ // // SPDX-License-Identifier: AGPL3.0-or-later - package main import ( - "os" - "fmt" - "flag" - "taler.net/taldir/pkg/rest" - "taler.net/taldir/pkg/util" - gnunetutil "git.gnunet.org/gnunet-go.git/pkg/util" - "crypto/sha512" + "crypto/sha512" + "flag" + "fmt" + gnunetutil "git.gnunet.org/gnunet-go.git/pkg/util" + "os" + "taler.net/taldir/pkg/rest" + "taler.net/taldir/pkg/util" ) // Generates a link from a challenge and address func generateLink(host string, addr string, challenge string) string { - h := sha512.New() - h.Write([]byte(addr)) - h_addr := gnunetutil.EncodeBinaryToString(h.Sum(nil)) - return host + "/register/" + h_addr + "/" + challenge + h := sha512.New() + h.Write([]byte(addr)) + h_addr := gnunetutil.EncodeBinaryToString(h.Sum(nil)) + return host + "/register/" + h_addr + "/" + challenge } func main() { - var solveFlag = flag.Bool("s", false, "Provide a solution for the challenge/pubkey") - var linkFlag = flag.Bool("l", false, "Provide a link for activation") - var challengeFlag = flag.String("c", "", "Activation challenge") - var pubkeyFlag = flag.String("p", "", "Public key") - var addressFlag = flag.String("a", "", "Address") - flag.Parse() - cfgfile := "taldir.conf" - t := taldir.Taldir{} - t.Initialize(cfgfile) - host := t.Cfg.Section("taldir").Key("host").MustString("http://localhost") - if *solveFlag { - if len(*challengeFlag) == 0 || len(*pubkeyFlag) == 0 { - fmt.Println("You need to provide an activation challenge and a public key to generate a solution") - os.Exit(1) - } - fmt.Println(util.GenerateSolution(*pubkeyFlag, *challengeFlag)) - os.Exit(0) - } - if *linkFlag { - if len(*challengeFlag) == 0 || len(*addressFlag) == 0 { - fmt.Println("You need to provide an activation challenge and an address to generate a link") - os.Exit(1) - } - fmt.Println(generateLink(host, *addressFlag, *challengeFlag)) - os.Exit(0) - } + var solveFlag = flag.Bool("s", false, "Provide a solution for the challenge/pubkey") + var linkFlag = flag.Bool("l", false, "Provide a link for activation") + var challengeFlag = flag.String("c", "", "Activation challenge") + var pubkeyFlag = flag.String("p", "", "Public key") + var addressFlag = flag.String("a", "", "Address") + flag.Parse() + cfgfile := "taldir.conf" + t := taldir.Taldir{} + t.Initialize(cfgfile) + host := t.Cfg.Section("taldir").Key("host").MustString("http://localhost") + if *solveFlag { + if len(*challengeFlag) == 0 || len(*pubkeyFlag) == 0 { + fmt.Println("You need to provide an activation challenge and a public key to generate a solution") + os.Exit(1) + } + fmt.Println(util.GenerateSolution(*pubkeyFlag, *challengeFlag)) + os.Exit(0) + } + if *linkFlag { + if len(*challengeFlag) == 0 || len(*addressFlag) == 0 { + fmt.Println("You need to provide an activation challenge and an address to generate a link") + os.Exit(1) + } + fmt.Println(generateLink(host, *addressFlag, *challengeFlag)) + os.Exit(0) + } } diff --git a/cmd/taldir-server/main.go b/cmd/taldir-server/main.go @@ -16,7 +16,6 @@ // // SPDX-License-Identifier: AGPL3.0-or-later - package main /* TODO @@ -28,27 +27,28 @@ package main */ import ( - "flag" - "net/http" - "log" - "taler.net/taldir/pkg/rest" + "flag" + "log" + "net/http" + + taldir "taler.net/taldir/pkg/rest" ) var t taldir.Taldir func handleRequests() { - log.Fatal(http.ListenAndServe(t.Cfg.Section("taldir").Key("bind_to").MustString("localhost:11000"), t.Router)) + log.Fatal(http.ListenAndServe(t.Cfg.Section("taldir").Key("bind_to").MustString("localhost:11000"), t.Router)) } func main() { - var cfgFlag = flag.String("c", "", "Configuration file to use") - - flag.Parse() - cfgfile := "taldir.conf" - if len(*cfgFlag) != 0 { - cfgfile = *cfgFlag - } - t := taldir.Taldir{} - t.Initialize(cfgfile) - handleRequests() + var cfgFlag = flag.String("c", "", "Configuration file to use") + + flag.Parse() + cfgfile := "taldir.conf" + if len(*cfgFlag) != 0 { + cfgfile = *cfgFlag + } + t := taldir.Taldir{} + t.Initialize(cfgfile) + handleRequests() } diff --git a/cmd/taldir-server/main_test.go b/cmd/taldir-server/main_test.go @@ -16,23 +16,22 @@ // // SPDX-License-Identifier: AGPL3.0-or-later - package main_test import ( - "os" - "testing" - "net/http" - "net/http/httptest" - "crypto/sha512" - "bytes" - "strings" - "io/ioutil" - "taler.net/taldir/pkg/rest" - _ "taler.net/taldir/cmd/taldir-server" - gnunetutil "git.gnunet.org/gnunet-go.git/pkg/util" - "github.com/jarcoal/httpmock" - "taler.net/taldir/pkg/util" + "bytes" + "crypto/sha512" + gnunetutil "git.gnunet.org/gnunet-go.git/pkg/util" + "github.com/jarcoal/httpmock" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strings" + _ "taler.net/taldir/cmd/taldir-server" + "taler.net/taldir/pkg/rest" + "taler.net/taldir/pkg/util" + "testing" ) var t taldir.Taldir @@ -70,253 +69,248 @@ var newOrderStatusUnpaidMockResponse = ` } ` - - - func TestMain(m *testing.M) { - t.Initialize("testdata/taldir-test.conf") - code := m.Run() - t.ClearDatabase() - os.Exit(code) + t.Initialize("testdata/taldir-test.conf") + code := m.Run() + t.ClearDatabase() + os.Exit(code) } func getHAddress(addr string) string { - h := sha512.New() - h.Write([]byte(addr)) - return gnunetutil.EncodeBinaryToString(h.Sum(nil)) + h := sha512.New() + h.Write([]byte(addr)) + return gnunetutil.EncodeBinaryToString(h.Sum(nil)) } func TestNoEntry(s *testing.T) { - t.ClearDatabase() + t.ClearDatabase() - h_addr := getHAddress("jdoe@example.com") - req, _ := http.NewRequest("GET", "/" + h_addr, nil) - response := executeRequest(req) + h_addr := getHAddress("jdoe@example.com") + req, _ := http.NewRequest("GET", "/"+h_addr, nil) + response := executeRequest(req) - if http.StatusNotFound != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusNotFound, response.Code) - } + if http.StatusNotFound != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusNotFound, response.Code) + } } func executeRequest(req *http.Request) *httptest.ResponseRecorder { - rr := httptest.NewRecorder() - t.Router.ServeHTTP(rr, req) - return rr + rr := httptest.NewRecorder() + t.Router.ServeHTTP(rr, req) + return rr } func TestRegisterRequest(s *testing.T) { - t.ClearDatabase() - - req, _ := http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) - response := executeRequest(req) - - if http.StatusAccepted != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) - } - file, err := os.Open("validation_code") - if err != nil { - s.Errorf("No validation code file found!\n") - } - code, err := ioutil.ReadAll(file) - if err != nil { - s.Errorf("Error reading validation code file contents!\n") - } - h_addr := getHAddress("abc@test") - trimCode := strings.Trim(string(code), " \r\n") - solution := util.GenerateSolution("000G006XE97PTWV3B7AJNCRQZA6BF26HPV3XZ07293FMY7KD4181946A90", trimCode) - solutionJSON := "{\"solution\": \"" + solution + "\"}" - req, _ = http.NewRequest("POST", "/" + h_addr, bytes.NewBuffer([]byte(solutionJSON))) - response = executeRequest(req) - if http.StatusNoContent != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusNoContent, response.Code) - } + t.ClearDatabase() + + req, _ := http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) + response := executeRequest(req) + + if http.StatusAccepted != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) + } + file, err := os.Open("validation_code") + if err != nil { + s.Errorf("No validation code file found!\n") + } + code, err := ioutil.ReadAll(file) + if err != nil { + s.Errorf("Error reading validation code file contents!\n") + } + h_addr := getHAddress("abc@test") + trimCode := strings.Trim(string(code), " \r\n") + solution := util.GenerateSolution("000G006XE97PTWV3B7AJNCRQZA6BF26HPV3XZ07293FMY7KD4181946A90", trimCode) + solutionJSON := "{\"solution\": \"" + solution + "\"}" + req, _ = http.NewRequest("POST", "/"+h_addr, bytes.NewBuffer([]byte(solutionJSON))) + response = executeRequest(req) + if http.StatusNoContent != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusNoContent, response.Code) + } } func TestRegisterQRPageRequest(s *testing.T) { - t.ClearDatabase() - - req, _ := http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) - response := executeRequest(req) - - if http.StatusAccepted != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) - } - req, _ = http.NewRequest("GET", "/register/NonSenseAddr/NonSenseCode", nil) - response = executeRequest(req) - if http.StatusNotFound != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusNotFound, response.Code) - } - - file, err := os.Open("validation_code") - if err != nil { - s.Errorf("No validation code file found!\n") - } - code, err := ioutil.ReadAll(file) - if err != nil { - s.Errorf("Error reading validation code file contents!\n") - } - h_addr := getHAddress("abc@test") - trimCode := strings.Trim(string(code), " \r\n") - req, _ = http.NewRequest("GET", "/register/" + h_addr + "/" + trimCode, nil) - response = executeRequest(req) - if http.StatusOK != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusOK, response.Code) - } + t.ClearDatabase() + + req, _ := http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) + response := executeRequest(req) + + if http.StatusAccepted != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) + } + req, _ = http.NewRequest("GET", "/register/NonSenseAddr/NonSenseCode", nil) + response = executeRequest(req) + if http.StatusNotFound != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusNotFound, response.Code) + } + + file, err := os.Open("validation_code") + if err != nil { + s.Errorf("No validation code file found!\n") + } + code, err := ioutil.ReadAll(file) + if err != nil { + s.Errorf("Error reading validation code file contents!\n") + } + h_addr := getHAddress("abc@test") + trimCode := strings.Trim(string(code), " \r\n") + req, _ = http.NewRequest("GET", "/register/"+h_addr+"/"+trimCode, nil) + response = executeRequest(req) + if http.StatusOK != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusOK, response.Code) + } } - func TestReRegisterRequest(s *testing.T) { - t.ClearDatabase() - - req, _ := http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) - response := executeRequest(req) - - if http.StatusAccepted != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) - } - file, err := os.Open("validation_code") - if err != nil { - s.Errorf("No validation code file found!\n") - } - code, err := ioutil.ReadAll(file) - if err != nil { - s.Errorf("Error reading validation code file contents!\n") - } - h_addr := getHAddress("abc@test") - trimCode := strings.Trim(string(code), " \r\n") - solution := util.GenerateSolution("000G006XE97PTWV3B7AJNCRQZA6BF26HPV3XZ07293FMY7KD4181946A90", trimCode) - solutionJSON := "{\"solution\": \"" + solution + "\"}" - req, _ = http.NewRequest("POST", "/" + h_addr, bytes.NewBuffer([]byte(solutionJSON))) - response = executeRequest(req) - if http.StatusNoContent != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusNoContent, response.Code) - } - req, _ = http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequestUnmodified)) - response = executeRequest(req) - - if http.StatusOK != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusOK, response.Code) - } + t.ClearDatabase() + + req, _ := http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) + response := executeRequest(req) + + if http.StatusAccepted != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) + } + file, err := os.Open("validation_code") + if err != nil { + s.Errorf("No validation code file found!\n") + } + code, err := ioutil.ReadAll(file) + if err != nil { + s.Errorf("Error reading validation code file contents!\n") + } + h_addr := getHAddress("abc@test") + trimCode := strings.Trim(string(code), " \r\n") + solution := util.GenerateSolution("000G006XE97PTWV3B7AJNCRQZA6BF26HPV3XZ07293FMY7KD4181946A90", trimCode) + solutionJSON := "{\"solution\": \"" + solution + "\"}" + req, _ = http.NewRequest("POST", "/"+h_addr, bytes.NewBuffer([]byte(solutionJSON))) + response = executeRequest(req) + if http.StatusNoContent != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusNoContent, response.Code) + } + req, _ = http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequestUnmodified)) + response = executeRequest(req) + + if http.StatusOK != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusOK, response.Code) + } } func TestReRegisterRequestTooMany(s *testing.T) { - t.ClearDatabase() + t.ClearDatabase() - req, _ := http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) - response := executeRequest(req) + req, _ := http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) + response := executeRequest(req) - if http.StatusAccepted != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) - } - req, _ = http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) - response = executeRequest(req) + if http.StatusAccepted != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) + } + req, _ = http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) + response = executeRequest(req) - if http.StatusAccepted != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) - } - req, _ = http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) - response = executeRequest(req) + if http.StatusAccepted != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) + } + req, _ = http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) + response = executeRequest(req) - if http.StatusAccepted != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) - } - req, _ = http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) - response = executeRequest(req) + if http.StatusAccepted != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) + } + req, _ = http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) + response = executeRequest(req) - if http.StatusTooManyRequests != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusTooManyRequests, response.Code) - } + if http.StatusTooManyRequests != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusTooManyRequests, response.Code) + } } func TestSolutionRequestTooMany(s *testing.T) { - t.ClearDatabase() - - req, _ := http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) - response := executeRequest(req) - - if http.StatusAccepted != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) - } - h_addr := getHAddress("abc@test") - solution := util.GenerateSolution("000G006XE97PTWV3B7AJNCRQZA6BF26HPV3XZ07293FMY7KD4181946A90", "wrongSolution") - solutionJSON := "{\"solution\": \"" + solution + "\"}" - req, _ = http.NewRequest("POST", "/" + h_addr, bytes.NewBuffer([]byte(solutionJSON))) - response = executeRequest(req) - if http.StatusForbidden != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusForbidden, response.Code) - } - req, _ = http.NewRequest("POST", "/" + h_addr, bytes.NewBuffer([]byte(solutionJSON))) - response = executeRequest(req) - if http.StatusForbidden != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusForbidden, response.Code) - } - req, _ = http.NewRequest("POST", "/" + h_addr, bytes.NewBuffer([]byte(solutionJSON))) - response = executeRequest(req) - if http.StatusForbidden != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusForbidden, response.Code) - } - req, _ = http.NewRequest("POST", "/" + h_addr, bytes.NewBuffer([]byte(solutionJSON))) - response = executeRequest(req) - if http.StatusTooManyRequests != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusTooManyRequests, response.Code) - } + t.ClearDatabase() + + req, _ := http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) + response := executeRequest(req) + + if http.StatusAccepted != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) + } + h_addr := getHAddress("abc@test") + solution := util.GenerateSolution("000G006XE97PTWV3B7AJNCRQZA6BF26HPV3XZ07293FMY7KD4181946A90", "wrongSolution") + solutionJSON := "{\"solution\": \"" + solution + "\"}" + req, _ = http.NewRequest("POST", "/"+h_addr, bytes.NewBuffer([]byte(solutionJSON))) + response = executeRequest(req) + if http.StatusForbidden != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusForbidden, response.Code) + } + req, _ = http.NewRequest("POST", "/"+h_addr, bytes.NewBuffer([]byte(solutionJSON))) + response = executeRequest(req) + if http.StatusForbidden != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusForbidden, response.Code) + } + req, _ = http.NewRequest("POST", "/"+h_addr, bytes.NewBuffer([]byte(solutionJSON))) + response = executeRequest(req) + if http.StatusForbidden != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusForbidden, response.Code) + } + req, _ = http.NewRequest("POST", "/"+h_addr, bytes.NewBuffer([]byte(solutionJSON))) + response = executeRequest(req) + if http.StatusTooManyRequests != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusTooManyRequests, response.Code) + } } func TestRegisterRequestWrongPubkey(s *testing.T) { - t.ClearDatabase() - - req, _ := http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) - response := executeRequest(req) - - if http.StatusAccepted != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) - } - file, err := os.Open("validation_code") - if err != nil { - s.Errorf("No validation code file found!\n") - } - code, err := ioutil.ReadAll(file) - if err != nil { - s.Errorf("Error reading validation code file contents!\n") - } - h_addr := getHAddress("abc@test") - trimCode := strings.Trim(string(code), " \r\n") - solution := util.GenerateSolution("000G006XE97PTWV3B7AJNCRQZA6BF26HPV3XZ07293FMY7KD4181946AA0", trimCode) - solutionJSON := "{\"solution\": \"" + solution + "\"}" - req, _ = http.NewRequest("POST", "/" + h_addr, bytes.NewBuffer([]byte(solutionJSON))) - response = executeRequest(req) - if http.StatusForbidden != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusForbidden, response.Code) - } + t.ClearDatabase() + + req, _ := http.NewRequest("POST", "/register/test", bytes.NewBuffer(validRegisterRequest)) + response := executeRequest(req) + + if http.StatusAccepted != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusAccepted, response.Code) + } + file, err := os.Open("validation_code") + if err != nil { + s.Errorf("No validation code file found!\n") + } + code, err := ioutil.ReadAll(file) + if err != nil { + s.Errorf("Error reading validation code file contents!\n") + } + h_addr := getHAddress("abc@test") + trimCode := strings.Trim(string(code), " \r\n") + solution := util.GenerateSolution("000G006XE97PTWV3B7AJNCRQZA6BF26HPV3XZ07293FMY7KD4181946AA0", trimCode) + solutionJSON := "{\"solution\": \"" + solution + "\"}" + req, _ = http.NewRequest("POST", "/"+h_addr, bytes.NewBuffer([]byte(solutionJSON))) + response = executeRequest(req) + if http.StatusForbidden != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusForbidden, response.Code) + } } - func TestUnsupportedMethod(s *testing.T) { - t.ClearDatabase() + t.ClearDatabase() - req, _ := http.NewRequest("POST", "/register/email", bytes.NewBuffer(validRegisterRequest)) - response := executeRequest(req) + req, _ := http.NewRequest("POST", "/register/email", bytes.NewBuffer(validRegisterRequest)) + response := executeRequest(req) - if http.StatusNotFound != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusNotFound, response.Code) - } + if http.StatusNotFound != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusNotFound, response.Code) + } } func TestPaymentRequiredMethod(s *testing.T) { - t.ClearDatabase() - t.MonthlyFee = "KUDOS:5" - httpmock.Activate() - defer httpmock.DeactivateAndReset() - req, _ := http.NewRequest("POST", "/register/test-cost", bytes.NewBuffer(validRegisterRequest)) - httpmock.RegisterResponder("POST", "http://merchant.taldir/instances/myInstance/private/orders", httpmock.NewStringResponder(200, newOrderMockResponse)) - httpmock.RegisterResponder("GET", "http://merchant.taldir/instances/myInstance/private/orders/testOrder1234", httpmock.NewStringResponder(200, newOrderStatusUnpaidMockResponse)) - - response := executeRequest(req) - t.MonthlyFee = "KUDOS:0" - if http.StatusPaymentRequired != response.Code { - s.Errorf("Expected response code %d. Got %d\n", http.StatusPaymentRequired, response.Code) - } + t.ClearDatabase() + t.MonthlyFee = "KUDOS:5" + httpmock.Activate() + defer httpmock.DeactivateAndReset() + req, _ := http.NewRequest("POST", "/register/test-cost", bytes.NewBuffer(validRegisterRequest)) + httpmock.RegisterResponder("POST", "http://merchant.taldir/instances/myInstance/private/orders", httpmock.NewStringResponder(200, newOrderMockResponse)) + httpmock.RegisterResponder("GET", "http://merchant.taldir/instances/myInstance/private/orders/testOrder1234", httpmock.NewStringResponder(200, newOrderStatusUnpaidMockResponse)) + + response := executeRequest(req) + t.MonthlyFee = "KUDOS:0" + if http.StatusPaymentRequired != response.Code { + s.Errorf("Expected response code %d. Got %d\n", http.StatusPaymentRequired, response.Code) + } } diff --git a/pkg/rest/taldir.go b/pkg/rest/taldir.go @@ -19,137 +19,134 @@ package taldir /* TODO - - ToS compression - - ToS etag +- ToS compression +- ToS etag */ import ( - "os" - "os/exec" - "time" - "fmt" - "log" - "io/ioutil" - "mime" - "net/http" - "html/template" - "encoding/json" - "github.com/gorilla/mux" - "gorm.io/gorm" - "gorm.io/gorm/logger" - "encoding/base64" - "taler.net/taldir/pkg/util" - "taler.net/taldir/pkg/gana" - "taler.net/taldir/pkg/taler" - "crypto/sha512" - "gorm.io/driver/postgres" - "gopkg.in/ini.v1" - "strings" - "github.com/skip2/go-qrcode" - gnunetutil "git.gnunet.org/gnunet-go.git/pkg/util" - "golang.org/x/text/language" + "crypto/sha512" + "encoding/base64" + "encoding/json" + "fmt" + gnunetutil "git.gnunet.org/gnunet-go.git/pkg/util" + "github.com/gorilla/mux" + "github.com/skip2/go-qrcode" + "golang.org/x/text/language" + "gopkg.in/ini.v1" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "html/template" + "io/ioutil" + "log" + "mime" + "net/http" + "os" + "os/exec" + "strings" + "taler.net/taldir/pkg/gana" + "taler.net/taldir/pkg/taler" + "taler.net/taldir/pkg/util" + "time" ) // Taldir is the primary object of the Taldir service type Taldir struct { - // The main router - Router *mux.Router + // The main router + Router *mux.Router - // The main DB handle - Db *gorm.DB + // The main DB handle + Db *gorm.DB - // Our configuration from the config.json - Cfg *ini.File + // Our configuration from the config.json + Cfg *ini.File - // Map of supported validators as defined in the configuration - Validators map[string]bool + // Map of supported validators as defined in the configuration + Validators map[string]bool - // landing page - ValidationTpl *template.Template + // landing page + ValidationTpl *template.Template - // The address salt - Salt string + // The address salt + Salt string - // The timeframe for the validation requests - ValidationTimeframe time.Duration + // The timeframe for the validation requests + ValidationTimeframe time.Duration - // How often may a challenge be requested - ValidationInitiationMax int64 + // How often may a challenge be requested + ValidationInitiationMax int64 - // How often may a solution be attempted (in the given timeframe) - SolutionAttemptsMax int + // How often may a solution be attempted (in the given timeframe) + SolutionAttemptsMax int - // The timeframe for the above solution attempts - SolutionTimeframe time.Duration + // The timeframe for the above solution attempts + SolutionTimeframe time.Duration + // Challenge length in bytes before encoding + ChallengeBytes int - // Challenge length in bytes before encoding - ChallengeBytes int + // Merchant object + Merchant taler.Merchant - // Merchant object - Merchant taler.Merchant - - // Monthly fee amount - MonthlyFee string + // Monthly fee amount + MonthlyFee string } // VersionResponse is the JSON response of the /config enpoint 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" + // 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"` - // Supported registration methods - Methods []Method `json:"methods"` + // Name of the protocol. + Name string `json:"name"` // "taler-directory" - // fee for one month of registration - MonthlyFee string `json:"monthly_fee"` + // Supported registration methods + Methods []Method `json:"methods"` + // fee for one month of registration + MonthlyFee string `json:"monthly_fee"` } // Method is part of the VersionResponse and contains a supported validator type Method struct { - // Name of the method, e.g. "email" or "sms". - Name string `json:"name"` - - // per challenge fee - ChallengeFee string `json:"challenge_fee"` + // Name of the method, e.g. "email" or "sms". + Name string `json:"name"` + // per challenge fee + ChallengeFee string `json:"challenge_fee"` } // RateLimitedResponse is the JSON response when a rate limit is hit type RateLimitedResponse struct { - // Taler error code, TALER_EC_TALDIR_REGISTER_RATE_LIMITED. - Code int `json:"code"` + // 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"` + // 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"` + // The human readable error message. + Hint string `json:"hint"` } // RegisterMessage is the JSON paylaod when a registration is requested type RegisterMessage struct { - // Address, in method-specific format - Address string `json:"address"` + // Address, in method-specific format + Address string `json:"address"` - // Public key of the user to register - PublicKey string `json:"public_key"` + // 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"` + // (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"` + // For how long should the registration last + Duration int64 `json:"duration"` } // Entry is a mapping from the identity key hash to a wallet key @@ -157,20 +154,20 @@ type RegisterMessage struct { // one of the identity key types supported (e.g. an email address) type entry struct { - // ORM - gorm.Model `json:"-"` + // ORM + gorm.Model `json:"-"` - // The salted hash (SHA512) of the hashed address (h_address) - HsAddress string `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"` + // (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"` + // Public key of the user to register in base32 + PublicKey string `json:"public_key"` - // How long the registration lasts in microseconds - Duration time.Duration `json:"-"` + // How long the registration lasts in microseconds + Duration time.Duration `json:"-"` } // Validation is the object created when a registration for an entry is initiated. @@ -179,86 +176,85 @@ type entry struct { // depending on the out-of-band chennel defined through the identity key type. type validation struct { - // ORM - gorm.Model `json:"-"` + // ORM + gorm.Model `json:"-"` - // The hash (SHA512) of the address - HAddress string `json:"h_address"` + // The hash (SHA512) of the address + HAddress string `json:"h_address"` - // For how long should the registration last - Duration int64 `json:"duration"` + // 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"` + // (HTTPS) endpoint URL for the inbox service for this address + Inbox string `json:"inbox_url"` - // The activation code sent to the client - Challenge string `json:"-"` + // The activation code sent to the client + Challenge string `json:"-"` - // The challenge has been sent already - ChallengeSent bool `json:"-"` + // The challenge has been sent already + ChallengeSent bool `json:"-"` - // Public key of the user to register - PublicKey string `json:"public_key"` + // Public key of the user to register + PublicKey string `json:"public_key"` - // How often was a solution for this validation tried - SolutionAttemptCount int + // How often was a solution for this validation tried + SolutionAttemptCount int - // The beginning of the last solution timeframe - LastSolutionTimeframeStart time.Time + // The beginning of the last solution timeframe + LastSolutionTimeframeStart time.Time - // The order ID associated with this validation - OrderID string `json:"-"` + // The order ID associated with this validation + OrderID string `json:"-"` } - // ErrorDetail is the detailed error payload returned from Taldir endpoints 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"` + // 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"` + // 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"` + // 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"` + // 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"` + // 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"` + // 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"` + // 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 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"` + // Name of the currency than was problematic (if applicable). + Currency string `json:"currency,omitempty"` - // Expected type (if applicable). - TypeExpected string `json:"type_expected,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 that was provided instead (if applicable). + TypeActual string `json:"type_actual,omitempty"` } // ValidationConfirmation is the payload sent by the client t complete a // registration. type ValidationConfirmation struct { - // The solution is the SHA-512 hash of the challenge value - // chosen by TalDir (encoded as string just as given in the URL, but - // excluding the 0-termination) concatenated with the binary 32-byte - // value representing the wallet's EdDSA public key. - // The hash is provided as string in Crockford base32 encoding. - Solution string `json:"solution"` + // The solution is the SHA-512 hash of the challenge value + // chosen by TalDir (encoded as string just as given in the URL, but + // excluding the 0-termination) concatenated with the binary 32-byte + // value representing the wallet's EdDSA public key. + // The hash is provided as string in Crockford base32 encoding. + Solution string `json:"solution"` } // NOTE: Go stores durations as nanoseconds. TalDir usually operates on microseconds @@ -267,520 +263,518 @@ const monthDurationUs = 2592000000000 // 1 Month as Go duration const monthDuration = time.Duration(monthDurationUs * 1000) - // Primary lookup function. // Allows the caller to query a wallet key using the hash(!) of the // identity, e.g. SHA512(<email address>) -func (t *Taldir) getSingleEntry(w http.ResponseWriter, r *http.Request){ - vars := mux.Vars(r) - var entry entry - hsAddress := saltHAddress(vars["h_address"], t.Salt) - var err = t.Db.First(&entry, "hs_address = ?", hsAddress).Error - if err == nil { - w.Header().Set("Content-Type", "application/json") - resp, _ := json.Marshal(entry) - w.Write(resp) - return - } - w.WriteHeader(http.StatusNotFound) +func (t *Taldir) getSingleEntry(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + var entry entry + hsAddress := saltHAddress(vars["h_address"], t.Salt) + var err = t.Db.First(&entry, "hs_address = ?", hsAddress).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(hAddress string, salt string) string { - h := sha512.New() - h.Write([]byte(hAddress)) - h.Write([]byte(salt)) - return gnunetutil.EncodeBinaryToString(h.Sum(nil)) + h := sha512.New() + h.Write([]byte(hAddress)) + h.Write([]byte(salt)) + return gnunetutil.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 (t *Taldir) 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 = t.Db.First(&validation, "h_address = ?", vars["h_address"]).Error - if err != nil { - w.WriteHeader(http.StatusNotFound) - return - } - validation.SolutionAttemptCount++ - if validation.LastSolutionTimeframeStart.Add(t.SolutionTimeframe).After(time.Now()) { - if validation.SolutionAttemptCount > t.SolutionAttemptsMax { - w.WriteHeader(429) - return - } - } else { - log.Println("New solution timeframe set.") - validation.LastSolutionTimeframeStart = time.Now() - validation.SolutionAttemptCount = 1 - } - t.Db.Save(&validation) - expectedSolution := util.GenerateSolution(validation.PublicKey, validation.Challenge) - if confirm.Solution != expectedSolution { - w.WriteHeader(http.StatusForbidden) - return - } - err = t.Db.Delete(&validation).Error - if err != nil { - log.Fatalf("Error deleting validation") - w.WriteHeader(http.StatusInternalServerError) - return - } - entry.HsAddress = saltHAddress(validation.HAddress, t.Salt) - entry.Inbox = validation.Inbox - tmpDuration := (entry.Duration.Microseconds() + validation.Duration) * 1000 - entry.Duration = time.Duration(tmpDuration) - entry.PublicKey = validation.PublicKey - err = t.Db.First(&entry, "hs_address = ?", entry.HsAddress).Error - if err == nil { - t.Db.Save(&entry) - } else { - err = t.Db.Create(&entry).Error - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - } - w.WriteHeader(http.StatusNoContent) +func (t *Taldir) 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 = t.Db.First(&validation, "h_address = ?", vars["h_address"]).Error + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + validation.SolutionAttemptCount++ + if validation.LastSolutionTimeframeStart.Add(t.SolutionTimeframe).After(time.Now()) { + if validation.SolutionAttemptCount > t.SolutionAttemptsMax { + w.WriteHeader(429) + return + } + } else { + log.Println("New solution timeframe set.") + validation.LastSolutionTimeframeStart = time.Now() + validation.SolutionAttemptCount = 1 + } + t.Db.Save(&validation) + expectedSolution := util.GenerateSolution(validation.PublicKey, validation.Challenge) + if confirm.Solution != expectedSolution { + w.WriteHeader(http.StatusForbidden) + return + } + err = t.Db.Delete(&validation).Error + if err != nil { + log.Fatalf("Error deleting validation") + w.WriteHeader(http.StatusInternalServerError) + return + } + entry.HsAddress = saltHAddress(validation.HAddress, t.Salt) + entry.Inbox = validation.Inbox + tmpDuration := (entry.Duration.Microseconds() + validation.Duration) * 1000 + entry.Duration = time.Duration(tmpDuration) + entry.PublicKey = validation.PublicKey + err = t.Db.First(&entry, "hs_address = ?", entry.HsAddress).Error + if err == nil { + t.Db.Save(&entry) + } else { + err = t.Db.Create(&entry).Error + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + } + w.WriteHeader(http.StatusNoContent) } func (t *Taldir) isRateLimited(hAddress string) (bool, error) { - var validations []validation - res := t.Db.Where("h_address = ?", hAddress).Find(&validations) - // NOTE: Check rate limit - if res.Error == nil { - // Limit re-initiation attempts to ValidationInitiationMax times - // within the expiration timeframe of a validation. - return res.RowsAffected >= t.ValidationInitiationMax, nil - } - return false, nil + var validations []validation + res := t.Db.Where("h_address = ?", hAddress).Find(&validations) + // NOTE: Check rate limit + if res.Error == nil { + // Limit re-initiation attempts to ValidationInitiationMax times + // within the expiration timeframe of a validation. + return res.RowsAffected >= t.ValidationInitiationMax, nil + } + return false, nil } -func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request){ - vars := mux.Vars(r) - var req RegisterMessage - var errDetail ErrorDetail - var validation validation - var entry entry - // Check if this validation method is supported or not. - if !t.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(http.StatusNotFound) - w.Write(resp) - return - } - 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 - } - - // Setup validation object. Retrieve object from DB if it already - // exists. - h := sha512.New() - h.Write([]byte(req.Address)) - hAddress := gnunetutil.EncodeBinaryToString(h.Sum(nil)) - validation.HAddress = hAddress - hsAddress := saltHAddress(validation.HAddress, t.Salt) - err = t.Db.First(&entry, "hs_address = ?", hsAddress).Error - // Round to the nearest multiple of a month - reqDuration := time.Duration(req.Duration * 1000) - reqDuration = reqDuration.Round(monthDuration) - if err == nil { - // Check if this entry is to be modified or extended - entryModified := (req.Inbox != entry.Inbox) || - (req.PublicKey != entry.PublicKey) - entryValidity := entry.CreatedAt.Add(entry.Duration) - // NOTE: The extension must be at least one month - if (reqDuration.Microseconds() == 0) && !entryModified { - // Nothing changed. Return validity - w.WriteHeader(http.StatusOK) - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(fmt.Sprintf("{\"valid_for\": %d}", time.Until(entryValidity).Microseconds()))) - return - } - } - rateLimited, err := t.isRateLimited(hAddress) - if nil != err { - log.Printf("Error checking rate limit! %w", err) - w.WriteHeader(http.StatusInternalServerError) - return - } else if rateLimited { - w.WriteHeader(http.StatusTooManyRequests) - rlResponse := RateLimitedResponse{ - Code: gana.TALDIR_REGISTER_RATE_LIMITED, - RequestFrequency: t.ValidationTimeframe.Microseconds() / t.ValidationInitiationMax, - Hint: "Registration rate limit reached", - } - jsonResp, _ := json.Marshal(rlResponse) - w.Write(jsonResp) - t.Db.Delete(&validation) - return - } - err = t.Db.First(&validation, "h_address = ? AND public_key = ? AND inbox = ? AND duration = ?", - hAddress, req.PublicKey, req.Inbox, reqDuration).Error - validationExists := (nil == err) - // FIXME: Always set new challenge? - validation.Challenge = util.GenerateChallenge(t.ChallengeBytes) - if !validationExists { - validation.Inbox = req.Inbox - validation.PublicKey = req.PublicKey - validation.SolutionAttemptCount = 0 - validation.LastSolutionTimeframeStart = time.Now() - validation.Duration = reqDuration.Microseconds() - } - - fixedCost := t.Cfg.Section("taldir-" + vars["method"]).Key("challenge_fee").MustString("KUDOS:0") - sliceDuration := time.Duration(validation.Duration * 1000) - cost, err := util.CalculateCost(t.MonthlyFee, - fixedCost, - sliceDuration, - monthDuration) - if err != nil { - fmt.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - if !cost.IsZero() { - if len(validation.OrderID) == 0 { - // Add new order for new validations - orderID, newOrderErr := t.Merchant.AddNewOrder(*cost) - if newOrderErr != nil { - fmt.Println(newOrderErr) - w.WriteHeader(http.StatusInternalServerError) - return - } - validation.OrderID = orderID - } - - // Check if order paid. - // FIXME: Remember that it was activated and paid - payto, paytoErr := t.Merchant.IsOrderPaid(validation.OrderID) - if paytoErr != nil { - fmt.Println(paytoErr) - w.WriteHeader(http.StatusInternalServerError) - log.Println(paytoErr) - return - } - if len(payto) != 0 { - t.Db.Save(&validation) - w.WriteHeader(http.StatusPaymentRequired) - w.Header().Set("Taler", payto) // FIXME no idea what to do with this. - return - } - // In this case, this order was paid - } - if validationExists { - err = t.Db.Save(&validation).Error - } else { - err = t.Db.Create(&validation).Error - } - if err != nil { - log.Println(err) - w.WriteHeader(500) - return - } - // Some validation methods are costly - // Require explicit whitelisting for a resend. - if validation.ChallengeSent && - !t.Cfg.Section("taldir-" + vars["method"]).Key("allow_resend").MustBool(false) { - w.WriteHeader(202) - return - } - if !t.Cfg.Section("taldir-" + vars["method"]).HasKey("command") { - log.Fatal(err) - t.Db.Delete(&validation) - w.WriteHeader(500) - return - } - command := t.Cfg.Section("taldir-" + vars["method"]).Key("command").String() - path, err := exec.LookPath(command) - if err != nil { - log.Println(err) - t.Db.Delete(&validation) - w.WriteHeader(500) - return - } - out, err := exec.Command(path, req.Address, validation.Challenge).Output() - if err != nil { - log.Printf("%s, %w", out, err) - t.Db.Delete(&validation) - w.WriteHeader(500) - return - } - validation.ChallengeSent = true - w.WriteHeader(202) +func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + var req RegisterMessage + var errDetail ErrorDetail + var validation validation + var entry entry + // Check if this validation method is supported or not. + if !t.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(http.StatusNotFound) + w.Write(resp) + return + } + 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 + } + + // Setup validation object. Retrieve object from DB if it already + // exists. + h := sha512.New() + h.Write([]byte(req.Address)) + hAddress := gnunetutil.EncodeBinaryToString(h.Sum(nil)) + validation.HAddress = hAddress + hsAddress := saltHAddress(validation.HAddress, t.Salt) + err = t.Db.First(&entry, "hs_address = ?", hsAddress).Error + // Round to the nearest multiple of a month + reqDuration := time.Duration(req.Duration * 1000) + reqDuration = reqDuration.Round(monthDuration) + if err == nil { + // Check if this entry is to be modified or extended + entryModified := (req.Inbox != entry.Inbox) || + (req.PublicKey != entry.PublicKey) + entryValidity := entry.CreatedAt.Add(entry.Duration) + // NOTE: The extension must be at least one month + if (reqDuration.Microseconds() == 0) && !entryModified { + // Nothing changed. Return validity + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(fmt.Sprintf("{\"valid_for\": %d}", time.Until(entryValidity).Microseconds()))) + return + } + } + rateLimited, err := t.isRateLimited(hAddress) + if nil != err { + log.Printf("Error checking rate limit! %w", err) + w.WriteHeader(http.StatusInternalServerError) + return + } else if rateLimited { + w.WriteHeader(http.StatusTooManyRequests) + rlResponse := RateLimitedResponse{ + Code: gana.TALDIR_REGISTER_RATE_LIMITED, + RequestFrequency: t.ValidationTimeframe.Microseconds() / t.ValidationInitiationMax, + Hint: "Registration rate limit reached", + } + jsonResp, _ := json.Marshal(rlResponse) + w.Write(jsonResp) + t.Db.Delete(&validation) + return + } + err = t.Db.First(&validation, "h_address = ? AND public_key = ? AND inbox = ? AND duration = ?", + hAddress, req.PublicKey, req.Inbox, reqDuration).Error + validationExists := (nil == err) + // FIXME: Always set new challenge? + validation.Challenge = util.GenerateChallenge(t.ChallengeBytes) + if !validationExists { + validation.Inbox = req.Inbox + validation.PublicKey = req.PublicKey + validation.SolutionAttemptCount = 0 + validation.LastSolutionTimeframeStart = time.Now() + validation.Duration = reqDuration.Microseconds() + } + + fixedCost := t.Cfg.Section("taldir-" + vars["method"]).Key("challenge_fee").MustString("KUDOS:0") + sliceDuration := time.Duration(validation.Duration * 1000) + cost, err := util.CalculateCost(t.MonthlyFee, + fixedCost, + sliceDuration, + monthDuration) + if err != nil { + fmt.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + if !cost.IsZero() { + if len(validation.OrderID) == 0 { + // Add new order for new validations + orderID, newOrderErr := t.Merchant.AddNewOrder(*cost) + if newOrderErr != nil { + fmt.Println(newOrderErr) + w.WriteHeader(http.StatusInternalServerError) + return + } + validation.OrderID = orderID + } + + // Check if order paid. + // FIXME: Remember that it was activated and paid + payto, paytoErr := t.Merchant.IsOrderPaid(validation.OrderID) + if paytoErr != nil { + fmt.Println(paytoErr) + w.WriteHeader(http.StatusInternalServerError) + log.Println(paytoErr) + return + } + if len(payto) != 0 { + t.Db.Save(&validation) + w.WriteHeader(http.StatusPaymentRequired) + w.Header().Set("Taler", payto) // FIXME no idea what to do with this. + return + } + // In this case, this order was paid + } + if validationExists { + err = t.Db.Save(&validation).Error + } else { + err = t.Db.Create(&validation).Error + } + if err != nil { + log.Println(err) + w.WriteHeader(500) + return + } + // Some validation methods are costly + // Require explicit whitelisting for a resend. + if validation.ChallengeSent && + !t.Cfg.Section("taldir-"+vars["method"]).Key("allow_resend").MustBool(false) { + w.WriteHeader(202) + return + } + if !t.Cfg.Section("taldir-" + vars["method"]).HasKey("command") { + log.Fatal(err) + t.Db.Delete(&validation) + w.WriteHeader(500) + return + } + command := t.Cfg.Section("taldir-" + vars["method"]).Key("command").String() + path, err := exec.LookPath(command) + if err != nil { + log.Println(err) + t.Db.Delete(&validation) + w.WriteHeader(500) + return + } + out, err := exec.Command(path, req.Address, validation.Challenge).Output() + if err != nil { + log.Printf("%s, %w", out, err) + t.Db.Delete(&validation) + w.WriteHeader(500) + return + } + validation.ChallengeSent = true + w.WriteHeader(202) } func (t *Taldir) configResponse(w http.ResponseWriter, r *http.Request) { - meths := []Method{} - i := 0 - for key := range t.Validators { - var meth Method - meth.Name = key - meth.ChallengeFee = t.Cfg.Section("taldir-" + key).Key("challenge_fee").MustString("KUDOS:1") - i++ - meths = append(meths, meth) - } - cfg := VersionResponse{ - Version: "0:0:0", - Name: "taler-directory", - MonthlyFee: t.Cfg.Section("taldir").Key("monthly_fee").MustString("KUDOS:1"), - Methods: meths, - } - w.Header().Set("Content-Type", "application/json") - response, _ := json.Marshal(cfg) - w.Write(response) + meths := []Method{} + i := 0 + for key := range t.Validators { + var meth Method + meth.Name = key + meth.ChallengeFee = t.Cfg.Section("taldir-" + key).Key("challenge_fee").MustString("KUDOS:1") + i++ + meths = append(meths, meth) + } + cfg := VersionResponse{ + Version: "0:0:0", + Name: "taler-directory", + MonthlyFee: t.Cfg.Section("taldir").Key("monthly_fee").MustString("KUDOS:1"), + Methods: meths, + } + w.Header().Set("Content-Type", "application/json") + response, _ := json.Marshal(cfg) + w.Write(response) } func (t *Taldir) validationPage(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - var walletLink string - var png []byte - var validation validation - - err := t.Db.First(&validation, "h_address = ?", vars["h_address"]).Error - w.Header().Set("Content-Type", "text/html; charset=utf-8") - if err != nil { - // This validation does not exist. - w.WriteHeader(404) - return - } - - walletLink = "taler://taldir/" + vars["h_address"] + "/" + vars["challenge"] + "-wallet" - 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), - } - t.ValidationTpl.Execute(w, fullData) - return + vars := mux.Vars(r) + var walletLink string + var png []byte + var validation validation + + err := t.Db.First(&validation, "h_address = ?", vars["h_address"]).Error + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err != nil { + // This validation does not exist. + w.WriteHeader(404) + return + } + + walletLink = "taler://taldir/" + vars["h_address"] + "/" + vars["challenge"] + "-wallet" + 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), + } + t.ValidationTpl.Execute(w, fullData) + return } // ClearDatabase nukes the database (for tests) func (t *Taldir) ClearDatabase() { - t.Db.Where("1 = 1").Delete(&entry{}) - t.Db.Where("1 = 1").Delete(&validation{}) + t.Db.Where("1 = 1").Delete(&entry{}) + t.Db.Where("1 = 1").Delete(&validation{}) } func (t *Taldir) termsResponse(w http.ResponseWriter, r *http.Request) { - fileType := t.Cfg.Section("taldir").Key("default_doc_filetype").MustString("text/html") - termsLocation := t.Cfg.Section("taldir").Key("default_tos_path").MustString("terms/") - for _, typ := range r.Header["Accept"] { - for _, a := range strings.Split(t.Cfg.Section("taldir").Key("supported_doc_filetypes").String(), " ") { - if typ == a { - fileType = a - } - } - } - - if len(r.Header.Get("Accept-Language")) != 0 { - acceptLangs, _, _ := language.ParseAcceptLanguage(r.Header.Get("Accept-Language")) - for _, lang := range acceptLangs { - extensions, _ := mime.ExtensionsByType(fileType) - for _, ext := range extensions { - docFile := fmt.Sprintf("%s/%s/0%s", termsLocation, lang.String(), ext) - log.Printf("Trying %s\n", docFile) - fileBytes, err := ioutil.ReadFile(docFile) - if nil == err { - w.Header().Set("Content-Type", fileType) - w.Write(fileBytes) - return - } - } - } - } - // Default document in expected/default format - defaultLanguage := t.Cfg.Section("taldir").Key("default_doc_lang").MustString("en") - extensions, _ := mime.ExtensionsByType(fileType) - for _, ext := range extensions { - docFile := fmt.Sprintf("%s/%s/0%s", termsLocation, defaultLanguage, ext) - log.Println("Trying " + docFile) - fileBytes, err := ioutil.ReadFile(docFile) - if nil == err { - w.Header().Set("Content-Type", fileType) - w.Write(fileBytes) - return - } - } - w.WriteHeader(http.StatusNotFound) + fileType := t.Cfg.Section("taldir").Key("default_doc_filetype").MustString("text/html") + termsLocation := t.Cfg.Section("taldir").Key("default_tos_path").MustString("terms/") + for _, typ := range r.Header["Accept"] { + for _, a := range strings.Split(t.Cfg.Section("taldir").Key("supported_doc_filetypes").String(), " ") { + if typ == a { + fileType = a + } + } + } + + if len(r.Header.Get("Accept-Language")) != 0 { + acceptLangs, _, _ := language.ParseAcceptLanguage(r.Header.Get("Accept-Language")) + for _, lang := range acceptLangs { + extensions, _ := mime.ExtensionsByType(fileType) + for _, ext := range extensions { + docFile := fmt.Sprintf("%s/%s/0%s", termsLocation, lang.String(), ext) + log.Printf("Trying %s\n", docFile) + fileBytes, err := ioutil.ReadFile(docFile) + if nil == err { + w.Header().Set("Content-Type", fileType) + w.Write(fileBytes) + return + } + } + } + } + // Default document in expected/default format + defaultLanguage := t.Cfg.Section("taldir").Key("default_doc_lang").MustString("en") + extensions, _ := mime.ExtensionsByType(fileType) + for _, ext := range extensions { + docFile := fmt.Sprintf("%s/%s/0%s", termsLocation, defaultLanguage, ext) + log.Println("Trying " + docFile) + fileBytes, err := ioutil.ReadFile(docFile) + if nil == err { + w.Header().Set("Content-Type", fileType) + w.Write(fileBytes) + return + } + } + w.WriteHeader(http.StatusNotFound) } func (t *Taldir) privacyResponse(w http.ResponseWriter, r *http.Request) { - fileType := t.Cfg.Section("taldir").Key("default_doc_filetype").MustString("text/html") - termsLocation := t.Cfg.Section("taldir").Key("default_pp_path").MustString("privacy/") - for _, typ := range r.Header["Accept"] { - for _, a := range strings.Split(t.Cfg.Section("taldir").Key("supported_doc_filetypes").String(), " ") { - if typ == a { - fileType = a - } - } - } - - if len(r.Header.Get("Accept-Language")) != 0 { - acceptLangs, _, _ := language.ParseAcceptLanguage(r.Header.Get("Accept-Language")) - for _, lang := range acceptLangs { - extensions, _ := mime.ExtensionsByType(fileType) - for _, ext := range extensions { - docFile := fmt.Sprintf("%s/%s/0%s", termsLocation, lang.String(), ext) - log.Printf("Trying %s\n", docFile) - fileBytes, err := ioutil.ReadFile(docFile) - if nil == err { - w.Header().Set("Content-Type", fileType) - w.Write(fileBytes) - return - } - } - } - } - // Default document in expected/default format - defaultLanguage := t.Cfg.Section("taldir").Key("default_doc_lang").MustString("en") - extensions, _ := mime.ExtensionsByType(fileType) - for _, ext := range extensions { - docFile := fmt.Sprintf("%s/%s/0%s", termsLocation, defaultLanguage, ext) - fileBytes, err := ioutil.ReadFile(docFile) - if nil == err { - w.Header().Set("Content-Type", fileType) - w.Write(fileBytes) - return - } - } - w.WriteHeader(http.StatusNotFound) + fileType := t.Cfg.Section("taldir").Key("default_doc_filetype").MustString("text/html") + termsLocation := t.Cfg.Section("taldir").Key("default_pp_path").MustString("privacy/") + for _, typ := range r.Header["Accept"] { + for _, a := range strings.Split(t.Cfg.Section("taldir").Key("supported_doc_filetypes").String(), " ") { + if typ == a { + fileType = a + } + } + } + + if len(r.Header.Get("Accept-Language")) != 0 { + acceptLangs, _, _ := language.ParseAcceptLanguage(r.Header.Get("Accept-Language")) + for _, lang := range acceptLangs { + extensions, _ := mime.ExtensionsByType(fileType) + for _, ext := range extensions { + docFile := fmt.Sprintf("%s/%s/0%s", termsLocation, lang.String(), ext) + log.Printf("Trying %s\n", docFile) + fileBytes, err := ioutil.ReadFile(docFile) + if nil == err { + w.Header().Set("Content-Type", fileType) + w.Write(fileBytes) + return + } + } + } + } + // Default document in expected/default format + defaultLanguage := t.Cfg.Section("taldir").Key("default_doc_lang").MustString("en") + extensions, _ := mime.ExtensionsByType(fileType) + for _, ext := range extensions { + docFile := fmt.Sprintf("%s/%s/0%s", termsLocation, defaultLanguage, ext) + fileBytes, err := ioutil.ReadFile(docFile) + if nil == err { + w.Header().Set("Content-Type", fileType) + w.Write(fileBytes) + return + } + } + w.WriteHeader(http.StatusNotFound) } - func (t *Taldir) setupHandlers() { - t.Router = mux.NewRouter().StrictSlash(true) + t.Router = mux.NewRouter().StrictSlash(true) - /* ToS API */ - t.Router.HandleFunc("/terms", t.termsResponse).Methods("GET") - t.Router.HandleFunc("/privacy", t.privacyResponse).Methods("GET") + /* ToS API */ + t.Router.HandleFunc("/terms", t.termsResponse).Methods("GET") + t.Router.HandleFunc("/privacy", t.privacyResponse).Methods("GET") - /* Config API */ - t.Router.HandleFunc("/config", t.configResponse).Methods("GET") + /* Config API */ + t.Router.HandleFunc("/config", t.configResponse).Methods("GET") - /* Aissets HTML */ - t.Router.PathPrefix("/css").Handler(http.StripPrefix("/css", http.FileServer(http.Dir("./static/css")))) + /* Aissets HTML */ + t.Router.PathPrefix("/css").Handler(http.StripPrefix("/css", http.FileServer(http.Dir("./static/css")))) - /* Registration API */ - t.Router.HandleFunc("/{h_address}", t.getSingleEntry).Methods("GET") - t.Router.HandleFunc("/register/{method}", t.registerRequest).Methods("POST") - t.Router.HandleFunc("/register/{h_address}/{challenge}", t.validationPage).Methods("GET") - t.Router.HandleFunc("/{h_address}", t.validationRequest).Methods("POST") + /* Registration API */ + t.Router.HandleFunc("/{h_address}", t.getSingleEntry).Methods("GET") + t.Router.HandleFunc("/register/{method}", t.registerRequest).Methods("POST") + t.Router.HandleFunc("/register/{h_address}/{challenge}", t.validationPage).Methods("GET") + t.Router.HandleFunc("/{h_address}", t.validationRequest).Methods("POST") } // Initialize the Taldir instance with cfgfile func (t *Taldir) Initialize(cfgfile string) { - _cfg, err := ini.Load(cfgfile) - if err != nil { - fmt.Printf("Failed to read config: %v", err) - os.Exit(1) - } - t.Cfg = _cfg - if t.Cfg.Section("taldir").Key("production").MustBool(false) { - fmt.Println("Production mode enabled") - } - - t.Validators = make(map[string]bool) - for _, a := range strings.Split(t.Cfg.Section("taldir").Key("validators").String(), " ") { - t.Validators[a] = true - } - t.ChallengeBytes = t.Cfg.Section("taldir").Key("challenge_bytes").MustInt(16) - t.ValidationInitiationMax = t.Cfg.Section("taldir").Key("validation_initiation_max").MustInt64(3) - t.SolutionAttemptsMax = t.Cfg.Section("taldir").Key("solution_attempt_max").MustInt(3) - - validationTTLStr := t.Cfg.Section("taldir").Key("validation_timeframe").MustString("5m") - t.ValidationTimeframe, err = time.ParseDuration(validationTTLStr) - if err != nil { - log.Fatal(err) - } - - retryTimeframeStr := t.Cfg.Section("taldir").Key("solution_attempt_timeframe").MustString("1h") - t.SolutionTimeframe, err = time.ParseDuration(retryTimeframeStr) - if err != nil { - log.Fatal(err) - } - t.MonthlyFee = t.Cfg.Section("taldir").Key("monthly_fee").MustString("KUDOS:0") - - psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", - t.Cfg.Section("taldir-pq").Key("host").MustString("localhost"), - t.Cfg.Section("taldir-pq").Key("port").MustInt64(5432), - t.Cfg.Section("taldir-pq").Key("user").MustString("taldir"), - t.Cfg.Section("taldir-pq").Key("password").MustString("secret"), - t.Cfg.Section("taldir-pq").Key("db_name").MustString("taldir")) - _db, err := gorm.Open(postgres.Open(psqlconn), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Silent), - }) - if err != nil { - panic(err) - } - t.Db = _db - if err := t.Db.AutoMigrate(&entry{}); err != nil { - panic(err) - } - if err := t.Db.AutoMigrate(&validation{}); err != nil { - panic(err) - } - - // Clean up validations - validationExpStr := t.Cfg.Section("taldir").Key("validation_expiration").MustString("24h") - validationExp, err := time.ParseDuration(validationExpStr) - if err != nil { - log.Fatal(err) - } - go func() { - for true { - tx := t.Db.Where("created_at < ?", time.Now().Add(-validationExp)).Delete(&validation{}) - log.Printf("Cleaned up %d stale validations.\n", tx.RowsAffected) - time.Sleep(validationExp) - } - }() - validationLandingTplFile := t.Cfg.Section("taldir").Key("validation_landing").MustString("templates/validation_landing.html") - t.ValidationTpl, err = template.ParseFiles(validationLandingTplFile) - if err != nil { - fmt.Println(err) - } - t.Salt = os.Getenv("TALDIR_SALT") - if "" == t.Salt { - t.Salt = t.Cfg.Section("taldir").Key("salt").MustString("ChangeMe") - } - merchURL := t.Cfg.Section("taldir").Key("merchant_baseurl_private").MustString("http://merchant.taldir/instances/myInstance") - merchToken := t.Cfg.Section("taldir").Key("merchant_token").MustString("secretAccessToken") - t.Merchant = taler.NewMerchant(merchURL, merchToken) - t.setupHandlers() + _cfg, err := ini.Load(cfgfile) + if err != nil { + fmt.Printf("Failed to read config: %v", err) + os.Exit(1) + } + t.Cfg = _cfg + if t.Cfg.Section("taldir").Key("production").MustBool(false) { + fmt.Println("Production mode enabled") + } + + t.Validators = make(map[string]bool) + for _, a := range strings.Split(t.Cfg.Section("taldir").Key("validators").String(), " ") { + t.Validators[a] = true + } + t.ChallengeBytes = t.Cfg.Section("taldir").Key("challenge_bytes").MustInt(16) + t.ValidationInitiationMax = t.Cfg.Section("taldir").Key("validation_initiation_max").MustInt64(3) + t.SolutionAttemptsMax = t.Cfg.Section("taldir").Key("solution_attempt_max").MustInt(3) + + validationTTLStr := t.Cfg.Section("taldir").Key("validation_timeframe").MustString("5m") + t.ValidationTimeframe, err = time.ParseDuration(validationTTLStr) + if err != nil { + log.Fatal(err) + } + + retryTimeframeStr := t.Cfg.Section("taldir").Key("solution_attempt_timeframe").MustString("1h") + t.SolutionTimeframe, err = time.ParseDuration(retryTimeframeStr) + if err != nil { + log.Fatal(err) + } + t.MonthlyFee = t.Cfg.Section("taldir").Key("monthly_fee").MustString("KUDOS:0") + + psqlconn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", + t.Cfg.Section("taldir-pq").Key("host").MustString("localhost"), + t.Cfg.Section("taldir-pq").Key("port").MustInt64(5432), + t.Cfg.Section("taldir-pq").Key("user").MustString("taldir"), + t.Cfg.Section("taldir-pq").Key("password").MustString("secret"), + t.Cfg.Section("taldir-pq").Key("db_name").MustString("taldir")) + _db, err := gorm.Open(postgres.Open(psqlconn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) + if err != nil { + panic(err) + } + t.Db = _db + if err := t.Db.AutoMigrate(&entry{}); err != nil { + panic(err) + } + if err := t.Db.AutoMigrate(&validation{}); err != nil { + panic(err) + } + + // Clean up validations + validationExpStr := t.Cfg.Section("taldir").Key("validation_expiration").MustString("24h") + validationExp, err := time.ParseDuration(validationExpStr) + if err != nil { + log.Fatal(err) + } + go func() { + for true { + tx := t.Db.Where("created_at < ?", time.Now().Add(-validationExp)).Delete(&validation{}) + log.Printf("Cleaned up %d stale validations.\n", tx.RowsAffected) + time.Sleep(validationExp) + } + }() + validationLandingTplFile := t.Cfg.Section("taldir").Key("validation_landing").MustString("templates/validation_landing.html") + t.ValidationTpl, err = template.ParseFiles(validationLandingTplFile) + if err != nil { + fmt.Println(err) + } + t.Salt = os.Getenv("TALDIR_SALT") + if "" == t.Salt { + t.Salt = t.Cfg.Section("taldir").Key("salt").MustString("ChangeMe") + } + merchURL := t.Cfg.Section("taldir").Key("merchant_baseurl_private").MustString("http://merchant.taldir/instances/myInstance") + merchToken := t.Cfg.Section("taldir").Key("merchant_token").MustString("secretAccessToken") + t.Merchant = taler.NewMerchant(merchURL, merchToken) + t.setupHandlers() } diff --git a/pkg/taler/merchant.go b/pkg/taler/merchant.go @@ -1,161 +1,156 @@ package taler import ( - "net/http" - "encoding/json" - "bytes" - "fmt" - "errors" - "io/ioutil" - talerutil "taler.net/taler-go.git/pkg/util" + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + talerutil "taler.net/taler-go.git/pkg/util" ) type PostOrderRequest struct { - // The order must at least contain the minimal - // order detail, but can override all. - order MinimalOrderDetail - - // If set, the backend will then set the refund deadline to the current - // time plus the specified delay. If it's not set, refunds will not be - // possible. - RefundDelay int64 `json:"refund_delay,omitempty"` - - // Specifies the payment target preferred by the client. Can be used - // to select among the various (active) wire methods supported by the instance. - PaymentTarget string `json:"payment_target,omitempty"` - - // Specifies that some products are to be included in the - // order from the inventory. For these inventory management - // is performed (so the products must be in stock) and - // details are completed from the product data of the backend. - // FIXME: Not sure we actually need this for now - //InventoryProducts []MinimalInventoryProduct `json:"inventory_products,omitempty"` - - // Specifies a lock identifier that was used to - // lock a product in the inventory. Only useful if - // inventory_products is set. Used in case a frontend - // reserved quantities of the individual products while - // the shopping cart was being built. Multiple UUIDs can - // be used in case different UUIDs were used for different - // products (i.e. in case the user started with multiple - // shopping sessions that were combined during checkout). - LockUuids []string `json:"lock_uuids"` - - // Should a token for claiming the order be generated? - // False can make sense if the ORDER_ID is sufficiently - // high entropy to prevent adversarial claims (like it is - // if the backend auto-generates one). Default is 'true'. - CreateToken bool `json:"create_token,omitempty"` - + // The order must at least contain the minimal + // order detail, but can override all. + order MinimalOrderDetail + + // If set, the backend will then set the refund deadline to the current + // time plus the specified delay. If it's not set, refunds will not be + // possible. + RefundDelay int64 `json:"refund_delay,omitempty"` + + // Specifies the payment target preferred by the client. Can be used + // to select among the various (active) wire methods supported by the instance. + PaymentTarget string `json:"payment_target,omitempty"` + + // Specifies that some products are to be included in the + // order from the inventory. For these inventory management + // is performed (so the products must be in stock) and + // details are completed from the product data of the backend. + // FIXME: Not sure we actually need this for now + //InventoryProducts []MinimalInventoryProduct `json:"inventory_products,omitempty"` + + // Specifies a lock identifier that was used to + // lock a product in the inventory. Only useful if + // inventory_products is set. Used in case a frontend + // reserved quantities of the individual products while + // the shopping cart was being built. Multiple UUIDs can + // be used in case different UUIDs were used for different + // products (i.e. in case the user started with multiple + // shopping sessions that were combined during checkout). + LockUuids []string `json:"lock_uuids"` + + // Should a token for claiming the order be generated? + // False can make sense if the ORDER_ID is sufficiently + // high entropy to prevent adversarial claims (like it is + // if the backend auto-generates one). Default is 'true'. + CreateToken bool `json:"create_token,omitempty"` } type MinimalOrderDetail struct { - // Amount to be paid by the customer. - Amount string + // Amount to be paid by the customer. + Amount string - // Short summary of the order. - Summary string; + // Short summary of the order. + Summary string } - // NOTE: Part of the above but optional type FulfillmentMetadata struct { - // See documentation of fulfillment_url in ContractTerms. - // Either fulfillment_url or fulfillment_message must be specified. - FulfillmentUrl string `json:"fulfillment_url,omitempty"` + // See documentation of fulfillment_url in ContractTerms. + // Either fulfillment_url or fulfillment_message must be specified. + FulfillmentUrl string `json:"fulfillment_url,omitempty"` - // See documentation of fulfillment_message in ContractTerms. - // Either fulfillment_url or fulfillment_message must be specified. - FulfillmentMessage string `json:"fulfillment_message,omitempty"` + // See documentation of fulfillment_message in ContractTerms. + // Either fulfillment_url or fulfillment_message must be specified. + FulfillmentMessage string `json:"fulfillment_message,omitempty"` } type PostOrderResponse struct { - // Order ID of the response that was just created. - OrderId string `json:"order_id"` + // Order ID of the response that was just created. + OrderId string `json:"order_id"` } type PostOrderResponseToken struct { - // Token that authorizes the wallet to claim the order. - // Provided only if "create_token" was set to 'true' - // in the request. - Token string + // Token that authorizes the wallet to claim the order. + // Provided only if "create_token" was set to 'true' + // in the request. + Token string } type CheckPaymentStatusResponse struct { - // Status of the order - OrderStatus string `json:"order_status"` + // Status of the order + OrderStatus string `json:"order_status"` } type CheckPaymentPaytoResponse struct { - // Status of the order - TalerPayUri string `json:"taler_pay_uri"` + // Status of the order + TalerPayUri string `json:"taler_pay_uri"` } - - type Merchant struct { - // The host of this merchant - BaseUrlPrivate string - - // The access token to use for the private API - AccessToken string + // The host of this merchant + BaseUrlPrivate string + // The access token to use for the private API + AccessToken string } func NewMerchant(merchBaseUrlPrivate string, merchAccessToken string) Merchant { - return Merchant{ - BaseUrlPrivate: merchBaseUrlPrivate, - AccessToken: merchAccessToken, - } + return Merchant{ + BaseUrlPrivate: merchBaseUrlPrivate, + AccessToken: merchAccessToken, + } } func (m *Merchant) IsOrderPaid(orderId string) (string, error) { - var orderPaidResponse CheckPaymentStatusResponse - var paytoResponse CheckPaymentPaytoResponse - resp, err := http.Get(m.BaseUrlPrivate + "/private/orders/" + orderId) - if nil != err { - return "", err - } - defer resp.Body.Close() - if http.StatusOK != resp.StatusCode { - message := fmt.Sprintf("Expected response code %d. Got %d", http.StatusOK, resp.StatusCode) - return "", errors.New(message) - } - respData, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", err - } - err = json.NewDecoder(bytes.NewReader(respData)).Decode(&orderPaidResponse) - if err != nil { - return "", err - } - if orderPaidResponse.OrderStatus != "paid" { - err = json.NewDecoder(bytes.NewReader(respData)).Decode(&paytoResponse) - return paytoResponse.TalerPayUri, err - } - return "", nil + var orderPaidResponse CheckPaymentStatusResponse + var paytoResponse CheckPaymentPaytoResponse + resp, err := http.Get(m.BaseUrlPrivate + "/private/orders/" + orderId) + if nil != err { + return "", err + } + defer resp.Body.Close() + if http.StatusOK != resp.StatusCode { + message := fmt.Sprintf("Expected response code %d. Got %d", http.StatusOK, resp.StatusCode) + return "", errors.New(message) + } + respData, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + err = json.NewDecoder(bytes.NewReader(respData)).Decode(&orderPaidResponse) + if err != nil { + return "", err + } + if orderPaidResponse.OrderStatus != "paid" { + err = json.NewDecoder(bytes.NewReader(respData)).Decode(&paytoResponse) + return paytoResponse.TalerPayUri, err + } + return "", nil } func (m *Merchant) AddNewOrder(cost talerutil.Amount) (string, error) { - var newOrder PostOrderRequest - var orderDetail MinimalOrderDetail - var orderResponse PostOrderResponse - orderDetail.Amount = cost.String() - // FIXME get from cfg - orderDetail.Summary = "This is an order to a TalDir registration" - newOrder.order = orderDetail - reqString, _ := json.Marshal(newOrder) - resp, err := http.Post(m.BaseUrlPrivate + "/private/orders", "application/json", bytes.NewBuffer(reqString)) - - if nil != err { - return "", err - } - defer resp.Body.Close() - if http.StatusOK != resp.StatusCode { - message := fmt.Sprintf("Expected response code %d. Got %d", http.StatusOK, resp.StatusCode) - return "", errors.New(message) - } - err = json.NewDecoder(resp.Body).Decode(&orderResponse) - return orderResponse.OrderId, err + var newOrder PostOrderRequest + var orderDetail MinimalOrderDetail + var orderResponse PostOrderResponse + orderDetail.Amount = cost.String() + // FIXME get from cfg + orderDetail.Summary = "This is an order to a TalDir registration" + newOrder.order = orderDetail + reqString, _ := json.Marshal(newOrder) + resp, err := http.Post(m.BaseUrlPrivate+"/private/orders", "application/json", bytes.NewBuffer(reqString)) + + if nil != err { + return "", err + } + defer resp.Body.Close() + if http.StatusOK != resp.StatusCode { + message := fmt.Sprintf("Expected response code %d. Got %d", http.StatusOK, resp.StatusCode) + return "", errors.New(message) + } + err = json.NewDecoder(resp.Body).Decode(&orderResponse) + return orderResponse.OrderId, err } diff --git a/pkg/util/helper.go b/pkg/util/helper.go @@ -16,67 +16,65 @@ // // SPDX-License-Identifier: AGPL3.0-or-later - package util import ( - "fmt" - "crypto/sha512" - "math/rand" - "time" - gnunetutil "git.gnunet.org/gnunet-go.git/pkg/util" - talerutil "taler.net/taler-go.git/pkg/util" + "crypto/sha512" + "fmt" + gnunetutil "git.gnunet.org/gnunet-go.git/pkg/util" + "math/rand" + talerutil "taler.net/taler-go.git/pkg/util" + "time" ) - // Generates a solution from a challenge and pubkey func GenerateSolution(pubkeyEncoded string, challenge string) string { - pubkey, err := gnunetutil.DecodeStringToBinary(pubkeyEncoded, 36) - if err != nil { - fmt.Println("error decoding pubkey:", err) - return "" - } - h := sha512.New() - h.Write([]byte(challenge)) - h.Write(pubkey) - return gnunetutil.EncodeBinaryToString(h.Sum(nil)) + pubkey, err := gnunetutil.DecodeStringToBinary(pubkeyEncoded, 36) + if err != nil { + fmt.Println("error decoding pubkey:", err) + return "" + } + h := sha512.New() + h.Write([]byte(challenge)) + h.Write(pubkey) + return gnunetutil.EncodeBinaryToString(h.Sum(nil)) } // Generates random reference token used in the validation flow. func GenerateChallenge(bytes int) string { - randBytes := make([]byte, bytes) - _, err := rand.Read(randBytes) - if err != nil { - panic(err) - } - return gnunetutil.EncodeBinaryToString(randBytes) + randBytes := make([]byte, bytes) + _, err := rand.Read(randBytes) + if err != nil { + panic(err) + } + return gnunetutil.EncodeBinaryToString(randBytes) } // Check if this is a non-zero, positive amount func CalculateCost(sliceCostAmount string, fixedCostAmount string, howLong time.Duration, sliceDuration time.Duration) (*talerutil.Amount, error) { - sliceCount := int(float64(howLong.Microseconds()) / float64(sliceDuration.Microseconds())) - sliceCost, err := talerutil.ParseAmount(sliceCostAmount) - if nil != err { - return nil, err - } - fixedCost, err := talerutil.ParseAmount(fixedCostAmount) - if nil != err { - return nil, err - } - sum := &talerutil.Amount{ - Currency: sliceCost.Currency, - Value: 0, - Fraction: 0, - } - for i := 0; i < sliceCount; i++ { - sum, err = sum.Add(*sliceCost) - if nil != err { - return nil, err - } - } - sum, err = sum.Add(*fixedCost) - if nil != err { - return nil, err - } - return sum, nil + sliceCount := int(float64(howLong.Microseconds()) / float64(sliceDuration.Microseconds())) + sliceCost, err := talerutil.ParseAmount(sliceCostAmount) + if nil != err { + return nil, err + } + fixedCost, err := talerutil.ParseAmount(fixedCostAmount) + if nil != err { + return nil, err + } + sum := &talerutil.Amount{ + Currency: sliceCost.Currency, + Value: 0, + Fraction: 0, + } + for i := 0; i < sliceCount; i++ { + sum, err = sum.Add(*sliceCost) + if nil != err { + return nil, err + } + } + sum, err = sum.Add(*fixedCost) + if nil != err { + return nil, err + } + return sum, nil }