commit a84002ca1ad4f70cc7f6c5105424f7a8ab1af6d8
parent 86fb4f92dd0a6624e3cd7e7f25781c011483c642
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date: Wed, 6 Dec 2023 14:52:13 +0100
Towards paying with the taler demo
Diffstat:
6 files changed, 207 insertions(+), 87 deletions(-)
diff --git a/gns-registrar.conf b/gns-registrar.conf
@@ -9,8 +9,8 @@ default_doc_lang = en
default_tos_path = terms/
default_pp_path = privacy/
supported_doc_filetypes = text/html application/pdf application/epub application/xml text/plain
-merchant_baseurl_private = http://merchant.taldir/instances/myInstance
-merchant_token = superSecretToken
+merchant_baseurl_private = https://backend.demo.taler.net
+merchant_token = sandbox
registrar_landing = web/templates/landing.html
registrar_name = web/templates/name.html
registration_failed = templates/registration_failed.html
diff --git a/go.mod b/go.mod
@@ -4,6 +4,7 @@ go 1.18
require (
github.com/gorilla/mux v1.8.0 // indirect
+ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
- taler.net/taler-go.git v0.0.0-20220719135513-36eb87bf37a3 // indirect
+ taler.net/taler-go.git v0.0.0-20231206131418-346a54653b41 // indirect
)
diff --git a/pkg/rest/gnsregistrar.go b/pkg/rest/gnsregistrar.go
@@ -20,7 +20,9 @@ package gnsregistrar
import (
"bytes"
+ "encoding/base64"
"encoding/json"
+ "errors"
"fmt"
"html/template"
"io"
@@ -28,15 +30,19 @@ import (
"os"
"time"
+ "github.com/skip2/go-qrcode"
"github.com/gorilla/mux"
"gopkg.in/ini.v1"
"taler.net/taler-go.git/pkg/merchant"
+ talerutil "taler.net/taler-go.git/pkg/util"
)
type RegistrationMetadata struct {
Expiration uint64 `json:"expiration"`
Paid bool `json:"paid"`
+ OrderID string `json:"order_id"`
+ NeedsPaymentUntil time.Time `json:"needs_payment_until"`
}
type IdentityInfo struct {
@@ -85,8 +91,8 @@ type Registrar struct {
// name page
NameTpl *template.Template
- // all names page
- AllNamesTpl *template.Template
+ // buy names page
+ BuyTpl *template.Template
// Merchant object
Merchant merchant.Merchant
@@ -97,6 +103,9 @@ type Registrar struct {
// Registration expiration (NOT record expiration!)
RelativeRegistrationExpiration time.Duration
+ // Payment expiration (time you have to pay for registration)
+ PaymentExpiration time.Duration
+
// Name of our root zone
RootZoneName string
@@ -113,10 +122,8 @@ type Registrar struct {
RegistrationPolicy string
// Cost for a registration
- RegistrationCost uint64
+ RegistrationCost *talerutil.Amount
- // Currency to use
- RegistrationCostCurrency string
}
type VersionResponse struct {
@@ -149,7 +156,7 @@ func (t *Registrar) landingPage(w http.ResponseWriter, r *http.Request) {
func (t *Registrar) registerName(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
var namestoreRequest NamestoreRecord
- var delegationRecord RecordData
+ var delegationRecord RecordData
var metadataRecord RecordData
var gnunetError GnunetError
var registrationMetadata RegistrationMetadata
@@ -173,45 +180,193 @@ func (t *Registrar) registerName(w http.ResponseWriter, r *http.Request) {
Paid: false,
Expiration: uint64(time.Now().Add(t.RelativeRegistrationExpiration).UnixMicro()),
}
- metadataRecordValue, err := json.Marshal(registrationMetadata)
+ metadataRecordValue, err := json.Marshal(registrationMetadata)
if nil != err {
- http.Redirect(w, r, "/name?label="+vars["label"] + "&error=Registration failed", http.StatusSeeOther)
+ http.Redirect(w, r, "/name?label="+vars["label"] + "&error=Registration failed", http.StatusSeeOther)
return
}
metadataRecord.Value = string(metadataRecordValue)
namestoreRequest.RecordName = vars["label"]
namestoreRequest.Records = []RecordData{delegationRecord,metadataRecord}
- reqString, _ := json.Marshal(namestoreRequest)
- // FIXME handle errors here
- fmt.Println(namestoreRequest)
+ reqString, _ := json.Marshal(namestoreRequest)
+ // FIXME handle errors here
+ fmt.Println(namestoreRequest)
resp, err := http.Post(t.GnunetUrl+"/namestore/" + t.RootZoneName, "application/json", bytes.NewBuffer(reqString))
- resp.Body.Close()
+ resp.Body.Close()
if http.StatusNoContent != resp.StatusCode {
fmt.Printf("Got error: %d\n", resp.StatusCode)
json.NewDecoder(resp.Body).Decode(&gnunetError)
fmt.Println(gnunetError.Code)
fmt.Println(err)
- http.Redirect(w, r, "/name?label="+vars["label"] + "&error=" + gnunetError.Description, http.StatusSeeOther)
+ http.Redirect(w, r, "/name?label="+vars["label"] + "&error=" + gnunetError.Description, http.StatusSeeOther)
return
}
http.Redirect(w, r, "/name/"+ vars["label"] + "?registered=true", http.StatusSeeOther)
return
}
-
func (t *Registrar) searchPage(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
http.Redirect(w, r, "/name/"+r.URL.Query().Get("label"), http.StatusSeeOther)
return
}
+func (t *Registrar) expireRegistration(label string) (error) {
+ var gnunetError GnunetError
+ client := &http.Client{}
+ req, _ := http.NewRequest(http.MethodDelete,t.GnunetUrl+"/namestore/" + t.RootZoneName + "/" + label, nil)
+ resp, err := client.Do(req)
+ resp.Body.Close()
+ if err != nil {
+ return err
+ }
+ if http.StatusNotFound == resp.StatusCode {
+ return nil
+ }
+ if http.StatusNoContent != resp.StatusCode {
+ fmt.Printf("Got error: %d\n", resp.StatusCode)
+ err = json.NewDecoder(resp.Body).Decode(&gnunetError)
+ return errors.New("GNUnet REST API error: " + gnunetError.Description)
+ }
+ return nil
+}
+
+func (t *Registrar) setupRegistrationMetadataBeforePayment(label string, zkey string, orderId string, paymentUntil time.Time) (error) {
+ var namestoreRequest NamestoreRecord
+ var delegationRecord RecordData
+ var metadataRecord RecordData
+ var gnunetError GnunetError
+ var registrationMetadata RegistrationMetadata
+ delegationRecord.IsPrivate = true // Private until payment is through
+ delegationRecord.IsRelativeExpiration = true
+ delegationRecord.IsSupplemental = false
+ delegationRecord.IsMaintenance = false
+ delegationRecord.IsShadow = false
+ delegationRecord.RecordType = "PKEY" // FIXME get from value
+ delegationRecord.RelativeExpiration = uint64(t.RelativeDelegationExpiration.Microseconds())
+ delegationRecord.Value = zkey
+ metadataRecord.IsPrivate = true
+ metadataRecord.IsRelativeExpiration = true
+ metadataRecord.IsSupplemental = false
+ metadataRecord.IsMaintenance = true
+ metadataRecord.IsShadow = false
+ metadataRecord.RecordType = "TXT" // FIXME use new recory type
+ metadataRecord.RelativeExpiration = uint64(t.RelativeDelegationExpiration.Microseconds())
+ registrationMetadata = RegistrationMetadata{
+ Paid: false,
+ OrderID: orderId,
+ NeedsPaymentUntil: paymentUntil,
+ Expiration: uint64(time.Now().Add(t.RelativeRegistrationExpiration).UnixMicro()),
+ }
+ metadataRecordValue, err := json.Marshal(registrationMetadata)
+ if nil != err {
+ return err
+ }
+ metadataRecord.Value = string(metadataRecordValue)
+ namestoreRequest.RecordName = label
+ namestoreRequest.Records = []RecordData{delegationRecord,metadataRecord}
+ reqString, _ := json.Marshal(namestoreRequest)
+ // FIXME handle errors here
+ fmt.Println(namestoreRequest)
+ resp, err := http.Post(t.GnunetUrl+"/namestore/" + t.RootZoneName, "application/json", bytes.NewBuffer(reqString))
+ resp.Body.Close()
+ if http.StatusNoContent != resp.StatusCode {
+ fmt.Printf("Got error: %d\n", resp.StatusCode)
+ err = json.NewDecoder(resp.Body).Decode(&gnunetError)
+ return errors.New("GNUnet REST API error: " + gnunetError.Description)
+ }
+ return nil
+}
+
func (t *Registrar) buyPage(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
- if t.RegistrationCost == 0 {
+ /*if t.RegistrationCost == 0 {
http.Redirect(w, r, "/name/"+vars["label"]+"/register?zkey="+r.URL.Query().Get("zkey"), http.StatusSeeOther)
return
+ }*/
+ var metadataResponse RegistrationMetadata
+ var namestoreResponse NamestoreRecord
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ var metadataExists = false
+ // FIXME redirect back if label empty
+ resp, err := http.Get(t.GnunetUrl + "/namestore/" + t.RootZoneName + "/" + vars["label"] + "?include_maintenance=yes")
+ if err != nil {
+ fmt.Printf("Failed to get zone contents")
+ return
+ }
+ defer resp.Body.Close()
+ if http.StatusOK == resp.StatusCode {
+ respData, err := io.ReadAll(resp.Body)
+ if err != nil {
+ fmt.Printf("Failed to get zone contents" + err.Error())
+ return
+ }
+ err = json.NewDecoder(bytes.NewReader(respData)).Decode(&namestoreResponse)
+ if err != nil {
+ fmt.Printf("Failed to get zone contents" + err.Error())
+ return
+ }
+ for _, record := range namestoreResponse.Records {
+ if record.RecordType == "PKEY" {
+ continue
+ }
+ if record.RecordType == "TXT" {
+ err = json.Unmarshal([]byte(record.Value), &metadataResponse)
+ if err != nil {
+ fmt.Printf("Failed to get zone contents" + err.Error())
+ return
+ }
+ metadataExists = true
+ }
+ }
+ } else {
+ fmt.Printf("Failed to get zone contents" + err.Error())
+ return
+ }
+ var errorMsg = ""
+ // FIXME check for order expiration
+ if metadataExists {
+ if metadataResponse.Paid {
+ http.Redirect(w, r, "/name/" +vars["label"] + "?error=Registration failed: Already paid", http.StatusSeeOther)
+ return
+ }
+ if !time.Now().After(metadataResponse.NeedsPaymentUntil) {
+ http.Redirect(w, r, "/name/" + vars["label"] + "?error=Registration failed: Name is being paid", http.StatusSeeOther)
+ return
+ }
+ t.expireRegistration(vars["label"])
+ }
+ orderID, newOrderErr := t.Merchant.AddNewOrder(*t.RegistrationCost, "GNS registrar name registration", "/name/" + vars["label"])
+ if newOrderErr != nil {
+ fmt.Println(newOrderErr)
+ http.Redirect(w, r, "/name/"+vars["label"] + "?error=Registration failed: Unable to create order", http.StatusSeeOther)
+ return
}
- http.Redirect(w, r, "/name/"+vars["label"]+"?error=Not implemented", http.StatusSeeOther)
+ metadataResponse.OrderID = orderID
+
+ payto, paytoErr := t.Merchant.IsOrderPaid(metadataResponse.OrderID)
+ if paytoErr != nil {
+ http.Redirect(w, r, "/name/"+vars["label"] + "?error=Registration failed: Error getting payment data", http.StatusSeeOther)
+ return
+ }
+ qrPng, qrErr := qrcode.Encode(payto, qrcode.Medium, 256)
+ if qrErr != nil {
+ http.Redirect(w, r, "/name/"+vars["label"] + "?error=Registration failed: Error generating QR code", http.StatusSeeOther)
+ return
+ }
+ paymentUntil := time.Now().Add(t.PaymentExpiration)
+ err = t.setupRegistrationMetadataBeforePayment(vars["label"], vars["zkey"], orderID, paymentUntil)
+ encodedPng := base64.StdEncoding.EncodeToString(qrPng)
+ fullData := map[string]interface{}{
+ "qrCode": template.URL("data:image/png;base64," + encodedPng),
+ "payto": template.URL(payto),
+ "fulfillmentUrl": template.URL("/name/" + vars["label"]),
+ "label": vars["label"],
+ "error": errorMsg,
+ "cost": t.RegistrationCost,
+ "suffixHint": t.SuffixHint,
+ }
+ t.BuyTpl.Execute(w, fullData)
return
}
@@ -264,7 +419,6 @@ func (t *Registrar) namePage(w http.ResponseWriter, r *http.Request) {
"label": vars["label"],
"error": r.URL.Query().Get("error"),
"cost": t.RegistrationCost,
- "currency": t.RegistrationCostCurrency,
"available": available,
"currentValue": value,
"suffixHint": t.SuffixHint,
@@ -276,34 +430,6 @@ func (t *Registrar) namePage(w http.ResponseWriter, r *http.Request) {
return
}
-func (t *Registrar) allNamesPage(w http.ResponseWriter, r *http.Request) {
- var namestoreResponse []NamestoreRecord
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- resp, err := http.Get(t.GnunetUrl + "/namestore/" + t.RootZoneName)
- if err != nil {
- fmt.Printf("Failed to get zone contents")
- return
- }
- defer resp.Body.Close()
- if http.StatusOK != resp.StatusCode {
- fmt.Printf("Failed to get zone contents. Retcode=%d", resp.StatusCode)
- return
- }
- respData, err := io.ReadAll(resp.Body)
- if err != nil {
- fmt.Printf("Failed to get zone contents" + err.Error())
- return
- }
- err = json.NewDecoder(bytes.NewReader(respData)).Decode(&namestoreResponse)
- if err != nil {
- fmt.Printf("Failed to get zone contents" + err.Error())
- return
- }
- fmt.Println(namestoreResponse)
- t.AllNamesTpl.Execute(w, namestoreResponse)
- return
-}
-
func (t *Registrar) setupHandlers() {
t.Router = mux.NewRouter().StrictSlash(true)
@@ -312,7 +438,6 @@ func (t *Registrar) setupHandlers() {
t.Router.HandleFunc("/name/{label}/buy", t.buyPage).Methods("GET")
t.Router.HandleFunc("/name/{label}/register", t.registerName).Methods("GET")
t.Router.HandleFunc("/search", t.searchPage).Methods("GET")
- t.Router.HandleFunc("/names", t.allNamesPage).Methods("GET")
/* ToS API */
// t.Router.HandleFunc("/terms", t.termsResponse).Methods("GET")
@@ -347,21 +472,21 @@ func (t *Registrar) Initialize(cfgfile string) {
if err != nil {
fmt.Println(err)
}
- allNamesTplFile := t.Cfg.Section("gns-registrar").Key("registrar_all_names").MustString("web/templates/names.html")
- t.AllNamesTpl, err = template.ParseFiles(allNamesTplFile)
+ buyTplFile := t.Cfg.Section("gns-registrar").Key("buy_template").MustString("web/templates/buy.html")
+ t.BuyTpl, err = template.ParseFiles(buyTplFile)
if err != nil {
fmt.Println(err)
}
- t.RegistrationCost = t.Cfg.Section("gns-registrar").Key("registration_cost").MustUint64(0)
+ paymentExp := t.Cfg.Section("gns-registrar").Key("payment_required_expiration").MustString("48h")
recordExp := t.Cfg.Section("gns-registrar").Key("relative_delegation_expiration").MustString("24h")
registrationExp := t.Cfg.Section("gns-registrar").Key("registration_expiration").MustString("8760h")
- fmt.Println(recordExp)
- fmt.Println(registrationExp)
t.RelativeRegistrationExpiration, _ = time.ParseDuration(registrationExp)
t.RelativeDelegationExpiration, _ = time.ParseDuration(recordExp)
+ t.PaymentExpiration, _ = time.ParseDuration(paymentExp)
fmt.Println(t.RelativeDelegationExpiration)
fmt.Println(t.RelativeRegistrationExpiration)
- t.RegistrationCostCurrency = t.Cfg.Section("gns-registrar").Key("registration_cost_currency").MustString("EUR")
+ costStr := t.Cfg.Section("gns-registrar").Key("registration_cost").MustString("KUDOS:13")
+ t.RegistrationCost, err = talerutil.ParseAmount(costStr)
t.SuffixHint = t.Cfg.Section("gns-registrar").Key("suffix_hint").MustString("example.alt")
t.RootZoneName = t.Cfg.Section("gns-registrar").Key("root_zone_name").MustString("test")
t.GnunetUrl = t.Cfg.Section("gns-registrar").Key("gnunet_baseurl_private").MustString("http://localhost:7776")
@@ -390,8 +515,8 @@ func (t *Registrar) Initialize(cfgfile string) {
fmt.Printf("Failed to get zone contents" + err.Error())
os.Exit(1)
}
- merchURL := t.Cfg.Section("gns-registrar").Key("merchant_baseurl_private").MustString("http://merchant.gnsregistrar/instances/myInstance")
- merchToken := t.Cfg.Section("gns-registrar").Key("merchant_token").MustString("secretAccessToken")
+ merchURL := t.Cfg.Section("gns-registrar").Key("merchant_baseurl_private").MustString("https://backend.demo.taler.net")
+ merchToken := t.Cfg.Section("gns-registrar").Key("merchant_token").MustString("sandbox")
t.Merchant = merchant.NewMerchant(merchURL, merchToken)
t.setupHandlers()
}
diff --git a/web/templates/buy.html b/web/templates/buy.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <!-- Required meta tags -->
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <link href="/css/bootstrap.min.css" rel="stylesheet">
+ <title>Buy</title>
+ </head>
+ <body>
+ <div class ="container text-center">
+ <h1 class="mb-3">To register your name <i class="text-primary">{{.label}}</i>.<i class="text-secondary">{{.suffixHint}}</i> please pay here:</h1>
+
+ <a href="{{.payto}}" class="btn btn-success mb-3">Pay with TALER</a><br/>
+ Alternatively, you can pay using your mobile wallet by scanning the QR code below:<br/>
+ <a href="{{.fulfillmentUrl}}">
+ <img class="qr" src="{{.qrCode}}"/>
+ </a><br/>
+ After you pay with a mobile wallet, please click on the QR code to finalize your registration.
+ </div>
+ </body>
+</html>
diff --git a/web/templates/name.html b/web/templates/name.html
@@ -31,14 +31,10 @@
<h1 class="mb-5"><i class="text-primary">{{.label}}</i> is still <span class="text-success">available</span> for registration.</h1>
<form action="/name/{{.label}}/buy" method="get" class="align-items-center">
<div class="input-group mb-2 w-75">
- <span class="input-group-text" id="reg-prefix"><i class="text-primary">{{.label}}</i>.<i class="text-secondary">{{.suffixHint}}</i>: </span>
+ <span class="input-group-text" id="reg-prefix"><i class="text-primary">{{.label}}</i>.<i class="text-secondary">{{.suffixHint}}</i>: </span>
<input type="hidden" name="label" value="{{.label}}">
<input name="zkey" class="form-control form-control-lg" maxlength="63" type="text" placeholder="Enter your zone key here!" required autofocus>
- {{if eq .cost 0}}
- <input class="btn btn-primary" type="submit" value="Register for free">
- {{else}}
- <input class="btn btn-primary" type="submit" value="Register for {{.cost}} {{.currency}}">
- {{end}}
+ <input class="btn btn-primary" type="submit" value="Register for {{.cost}}">
</div>
</form>
{{else}}
@@ -54,10 +50,6 @@
</form>
<form action="/renew" method="get" class="align-items-center">
<span>Registration valid until: <i>{{.registeredUntil}}</i></span><br/>
- {{if .modificationAllowed}}
- <span>Registration extension cost: <i>23 EUR</i></span><br/>
- <input class="btn btn-primary" type="submit" value="Extend registration">
- {{end}}
</form>
{{end}}
<a class="btn btn-primary mt-5" href="/">Back</a>
diff --git a/web/templates/names.html b/web/templates/names.html
@@ -1,20 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
- <head>
- <!-- Required meta tags -->
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
- <link href="/css/style.css" rel="stylesheet">
- <title>All Names Overview</title>
- </head>
- <body>
- <div class="names">
- <!-- FIXME: Probably make this a table -->
- <ul>
- {{range $val := .}}
- <li>{{$val.RecordName}}: {{(index $val.Records 0).Value}}</li>
- {{end}}
- </ul>
- </div>
- </body>
-</html>