commit 1b6093863884b746dcae66c82fb3bf748feb4b78
parent 2378b2681b8c5dbee8ae6ebbc7bcb770bdf6aeda
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date: Thu, 7 Jul 2022 21:33:41 +0200
refactoring
Diffstat:
9 files changed, 714 insertions(+), 663 deletions(-)
diff --git a/Makefile b/Makefile
@@ -3,11 +3,11 @@ all: gana server cli
SCRIPT_TARGET:=$(shell dirname $(shell go list -f '{{.Target}}' ./cmd/taldir-server))
gana:
- mkdir -p gana
+ mkdir -p pkg/gana
git submodule update --init --recursive
git submodule sync --recursive
cd contrib/gana/gnu-taler-error-codes && make taler_error_codes.go
- cp contrib/gana/gnu-taler-error-codes/taler_error_codes.go gana/
+ cp contrib/gana/gnu-taler-error-codes/taler_error_codes.go pkg/gana/
server:
go build ./cmd/taldir-server
diff --git a/cmd/taldir-cli/main.go b/cmd/taldir-cli/main.go
@@ -23,7 +23,10 @@ import (
"os"
"fmt"
"flag"
- "taler.net/taldir/util"
+ "bufio"
+ "log"
+ "taler.net/taldir/pkg/taldir"
+ "taler.net/taldir/pkg/util"
"crypto/sha512"
)
@@ -41,6 +44,11 @@ func main() {
var codeFlag = flag.String("c", "", "Activation code")
var pubkeyFlag = flag.String("p", "", "Public key")
var addressFlag = flag.String("a", "", "Address")
+ var delEntryFlag = flag.Bool("d", false, "Delete Entry for the specificied address")
+ var delValidationFlag = flag.Bool("p", false, "Delete validation for the specificied address")
+
+ var dropFlag = flag.Bool("D", false, "Drop all data in table (DANGEROUS!)")
+ var cfgFlag = flag.String("c", "", "Configuration file to use")
flag.Parse()
if *solveFlag {
if len(*codeFlag) == 0 || len(*pubkeyFlag) == 0 {
@@ -58,4 +66,40 @@ func main() {
fmt.Println(generateLink(*addressFlag, *codeFlag))
os.Exit(0)
}
+ cfgfile := "taldir.conf"
+ if len(*cfgFlag) != 0 {
+ cfgfile = *cfgFlag
+ }
+ t := taldir.Taldir{}
+ t.Initialize(cfgfile)
+ if *dropFlag {
+ fmt.Println("Really delete all data in database? [y/N]:")
+ reader := bufio.NewReader(os.Stdin)
+ char, _, err := reader.ReadRune()
+
+ if err == nil {
+ fmt.Println(char)
+ if char == 'y' {
+ t.ClearDatabase()
+ }
+ os.Exit(0)
+ }
+ os.Exit(1)
+ }
+ if *delEntryFlag {
+ if len(*addressFlag) == 0 {
+ log.Fatal("No address provided!")
+ }
+ t.DeleteEntry(*addressFlag)
+ }
+ if *delValidationFlag {
+ if len(*addressFlag) == 0 {
+ log.Fatal("No address provided!")
+ }
+ t.DeleteValidation(*addressFlag)
+ }
+
+
+ t.Run()
+
}
diff --git a/cmd/taldir-server/main.go b/cmd/taldir-server/main.go
@@ -28,36 +28,18 @@ package main
*/
import (
- "os"
- "fmt"
- "bufio"
"flag"
+ "taler.net/taldir/pkg/taldir"
)
func main() {
- var dropFlag = flag.Bool("D", false, "Drop all data in table (DANGEROUS!)")
var cfgFlag = flag.String("c", "", "Configuration file to use")
flag.Parse()
cfgfile := "taldir.conf"
if len(*cfgFlag) != 0 {
cfgfile = *cfgFlag
}
- t := Taldir{}
- clearDb := false
- if *dropFlag {
- fmt.Println("Really delete all data in database? [y/N]:")
- reader := bufio.NewReader(os.Stdin)
- char, _, err := reader.ReadRune()
-
- if err == nil {
- fmt.Println(char)
- if char == 'y' {
- clearDb = true
- }
- os.Exit(0)
- }
- os.Exit(1)
- }
- t.Initialize(cfgfile, clearDb)
+ t := taldir.Taldir{}
+ t.Initialize(cfgfile)
t.Run()
}
diff --git a/cmd/taldir-server/main_test.go b/cmd/taldir-server/main_test.go
@@ -29,11 +29,12 @@ import (
"strings"
"io/ioutil"
"log"
- "taler.net/taldir/cmd/taldir-server"
- "taler.net/taldir/util"
+ "taler.net/taldir/pkg/taldir"
+ _ "taler.net/taldir/cmd/taldir-server"
+ "taler.net/taldir/pkg/util"
)
-var t main.Taldir
+var t taldir.Taldir
var validRegisterRequest = []byte(`
{
@@ -55,7 +56,7 @@ var validRegisterRequestShort = []byte(`
func TestMain(m *testing.M) {
- t.Initialize("testdata/taldir-test.conf", true)
+ t.Initialize("testdata/taldir-test.conf")
code := m.Run()
t.ClearDatabase()
os.Exit(code)
diff --git a/cmd/taldir-server/taldir.go b/cmd/taldir-server/taldir.go
@@ -1,634 +0,0 @@
-// This file is part of taldir, the Taler Directory implementation.
-// Copyright (C) 2022 Martin Schanzenbach
-//
-// Taldir is free software: you can redistribute it and/or modify it
-// under the terms of the GNU Affero General Public License as published
-// by the Free Software Foundation, either version 3 of the License,
-// or (at your option) any later version.
-//
-// Taldir is distributed in the hope that it will be useful, but
-// WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-// Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see <http://www.gnu.org/licenses/>.
-//
-// SPDX-License-Identifier: AGPL3.0-or-later
-
-
-package main
-
-/* TODO
- - ToS API (terms, privacy) with localizions
- - ToS compression
- - ToS etag
- - Prettify QR code landing page
- - Base32: Use gnunet-go module? (currently copied)
- - OrderId processing
- - Maintenance of database: When to delete expired validations?
-*/
-
-import (
- "os"
- "os/exec"
- "time"
- "fmt"
- "log"
- "io/ioutil"
- "mime"
- "net/http"
- "html/template"
- "encoding/json"
- "github.com/gorilla/mux"
- "gorm.io/gorm"
- "encoding/base64"
- "taler.net/taldir/util"
- "taler.net/taldir/gana"
- "crypto/sha512"
- "gorm.io/driver/postgres"
- "gopkg.in/ini.v1"
- "strings"
- "github.com/skip2/go-qrcode"
- "golang.org/x/text/language"
-)
-
-type Taldir struct {
-
- // The main router
- Router *mux.Router
-
- // The main DB handle
- Db *gorm.DB
-
- // Our configuration from the config.json
- Cfg *ini.File
-
- // Map of supported validators as defined in the configuration
- Validators map[string]bool
-
- // landing page
- ValidationTpl *template.Template
-
- // The address salt
- Salt string
-
- // Request frequency
- RequestFrequency int64
-}
-
-type VersionResponse struct {
- // libtool-style representation of the Merchant protocol version, see
- // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
- // The format is "current:revision:age".
- Version string `json:"version"`
-
- // Name of the protocol.
- Name string `json:"name"` // "taler-directory"
-
- // Supported registration methods
- Methods []Method `json:"methods"`
-
- // fee for one month of registration
- MonthlyFee string `json:"monthly_fee"`
-
-}
-
-type Method struct {
-
- // Name of the method, e.g. "email" or "sms".
- Name string `json:"name"`
-
- // per challenge fee
- ChallengeFee string `json:"challenge_fee"`
-
-}
-
-type RateLimitedResponse struct {
-
- // Taler error code, TALER_EC_TALDIR_REGISTER_RATE_LIMITED.
- Code int `json:"code"`
-
- // At what frequency are new registrations allowed. FIXME: In what? Currently: In microseconds
- RequestFrequency int64 `json:"request_frequency"`
-
- // The human readable error message.
- Hint string `json:"hint"`
-}
-
-type RegisterMessage struct {
-
- // Address, in method-specific format
- Address string `json:"address"`
-
- // Public key of the user to register
- PublicKey string `json:"public_key"`
-
- // (HTTPS) endpoint URL for the inbox service for this address
- Inbox string `json:"inbox_url"`
-
- // For how long should the registration last
- Duration int64 `json:"duration"`
-
- // Order ID, if the client recently paid for this registration
- // FIXME: As an optional field, maybe we want to parse this separately
- // instead?
- // Order_id string `json:"order_id"`
-}
-
-// A mappind entry from the identity key hash to a wallet key
-// The identity key hash is sha256(sha256(identity)|salt) where identity is
-// one of the identity key types supported (e.g. email)
-type Entry struct {
-
- // ORM
- gorm.Model `json:"-"`
-
- // The salted hash (SHA512) of the hashed address (h_address)
- HsAddress string `json:"-"`
-
- // (HTTPS) endpoint URL for the inbox service for this address
- Inbox string `json:"inbox_url"`
-
- // Public key of the user to register in base32
- PublicKey string `json:"public_key"`
-
- // Time of (re)registration. In Unix epoch microseconds)
- RegisteredAt int64 `json:"-"`
-
- // How long the registration lasts in microseconds
- Duration int64 `json:"-"`
-}
-
-// A validation is created when a registration for an entry is initiated.
-// The validation stores the identity key (sha256(identity)) the secret
-// validation reference. The validation reference is sent to the identity
-// depending on the out-of-band chennel defined through the identity key type.
-type Validation struct {
-
- // ORM
- gorm.Model `json:"-"`
-
- // The hash (SHA512) of the address
- HAddress string `json:"h_address"`
-
- // For how long should the registration last
- Duration int64 `json:"duration"`
-
- // (HTTPS) endpoint URL for the inbox service for this address
- Inbox string `json:"inbox_url"`
-
- // The activation code sent to the client
- Code string `json:"activation_code"`
-
- // Public key of the user to register
- PublicKey string `json:"public_key"`
-}
-
-type ErrorDetail struct {
-
- // Numeric error code unique to the condition.
- // The other arguments are specific to the error value reported here.
- Code int `json:"code"`
-
- // Human-readable description of the error, i.e. "missing parameter", "commitment violation", ...
- // Should give a human-readable hint about the error's nature. Optional, may change without notice!
- Hint string `json:"hint,omitempty"`
-
- // Optional detail about the specific input value that failed. May change without notice!
- Detail string `json:"detail,omitempty"`
-
- // Name of the parameter that was bogus (if applicable).
- Parameter string `json:"parameter,omitempty"`
-
- // Path to the argument that was bogus (if applicable).
- Path string `json:"path,omitempty"`
-
- // Offset of the argument that was bogus (if applicable).
- Offset string `json:"offset,omitempty"`
-
- // Index of the argument that was bogus (if applicable).
- Index string `json:"index,omitempty"`
-
- // Name of the object that was bogus (if applicable).
- Object string `json:"object,omitempty"`
-
- // Name of the currency than was problematic (if applicable).
- Currency string `json:"currency,omitempty"`
-
- // Expected type (if applicable).
- TypeExpected string `json:"type_expected,omitempty"`
-
- // Type that was provided instead (if applicable).
- TypeActual string `json:"type_actual,omitempty"`
-}
-
-type ValidationConfirmation struct {
- Solution string `json:"solution"`
-}
-
-// matcher is a language.Matcher configured for all supported languages.
-var langMatcher = language.NewMatcher([]language.Tag{
- language.BritishEnglish,
- //language.Norwegian,
- language.German,
-})
-
-// 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
- hs_address := saltHAddress(vars["h_address"], t.Salt)
- var err = t.Db.First(&entry, "hs_address = ?", hs_address).Error
- if err == nil {
- w.Header().Set("Content-Type", "application/json")
- resp, _ := json.Marshal(entry)
- w.Write(resp)
- return
- }
- w.WriteHeader(http.StatusNotFound)
-}
-
-// Hashes an identity key (e.g. sha256(<email address>)) with a salt for
-// Lookup and storage.
-func saltHAddress(h_address string, salt string) string {
- h := sha512.New()
- h.Write([]byte(h_address))
- h.Write([]byte(salt))
- return util.EncodeBinaryToString(h.Sum(nil))
-}
-
-// Called by the registrant to validate the registration request. The reference ID was
-// provided "out of band" using a validation method such as email or SMS
-func (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
- }
- expectedSolution := util.GenerateSolution(validation.PublicKey, validation.Code)
- if confirm.Solution != expectedSolution {
- // FIXME how TF do we rate limit here??
- w.WriteHeader(http.StatusForbidden)
- return
- }
- // FIXME: Expire validations somewhere?
- err = t.Db.Delete(&validation).Error
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- entry.HsAddress = saltHAddress(validation.HAddress, t.Salt)
- entry.Inbox = validation.Inbox
- entry.Duration = validation.Duration
- entry.RegisteredAt = time.Now().UnixMicro()
- 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) registerRequest(w http.ResponseWriter, r *http.Request){
- vars := mux.Vars(r)
- var req RegisterMessage
- var errDetail ErrorDetail
- var validation Validation
- var entry Entry
- if r.Body == nil {
- http.Error(w, "No request body", 400)
- return
- }
- err := json.NewDecoder(r.Body).Decode(&req)
- if err != nil {
- errDetail.Code = gana.GENERIC_JSON_INVALID
- errDetail.Hint = "Unable to parse JSON"
- resp, _ := json.Marshal(errDetail)
- w.WriteHeader(400)
- w.Write(resp)
- return
- }
- if !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(404)
- w.Write(resp)
- return
- }
- h := sha512.New()
- h.Write([]byte(req.Address))
- validation.HAddress = util.EncodeBinaryToString(h.Sum(nil))
- // We first try if there is already an entry for this address which
- // is still valid and the duration is not extended.
- hs_address := saltHAddress(validation.HAddress, t.Salt)
- err = t.Db.First(&entry, "hs_address = ?", hs_address).Error
- if err == nil {
- log.Println("Entry for this address already exists..")
- lastRegValidity := entry.RegisteredAt + entry.Duration
- requestedValidity := time.Now().UnixMicro() + req.Duration
- earliestReRegistration := entry.RegisteredAt + t.RequestFrequency
- // Rate limit re-registrations.
- if time.Now().UnixMicro() < earliestReRegistration {
- w.WriteHeader(429)
- rlResponse := RateLimitedResponse{
- Code: gana.TALDIR_REGISTER_RATE_LIMITED,
- RequestFrequency: t.RequestFrequency,
- Hint: "Registration rate limit reached",
- }
- jsonResp, _ := json.Marshal(rlResponse)
- w.Write(jsonResp)
- return
- }
- // Do not allow re-registrations with shorter duration.
- if requestedValidity <= lastRegValidity {
- w.WriteHeader(200)
- // FIXME how to return how long it is already paid for??
- return
- }
- }
- err = t.Db.First(&validation, "h_address = ?", validation.HAddress).Error
- bytes := t.Cfg.Section("taldir").Key("activation_code_bytes").MustInt(16)
- validation.Code = util.GenerateCode(bytes)
- validation.Inbox = req.Inbox
- validation.Duration = req.Duration
- validation.PublicKey = req.PublicKey
- if err == nil {
- // FIXME: Validation already pending for this address
- // How should we proceed here? Expire old validations?
- log.Println("Validation for this address already exists")
- err = t.Db.Save(&validation).Error
- } else {
- err = t.Db.Create(&validation).Error
- }
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- fmt.Println("Address registration request created:", validation)
- 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.Code).Output()
- if err != nil {
- log.Println(err)
- t.Db.Delete(&validation)
- w.WriteHeader(500)
- return
- }
- w.WriteHeader(202)
- fmt.Printf("Output from method script %s is %s\n", path, out)
-}
-
-func notImplemented(w http.ResponseWriter, r *http.Request) {
- return
-}
-
-func (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)
-}
-
-func (t *Taldir) validationPage(w http.ResponseWriter, r *http.Request) {
- vars := mux.Vars(r)
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- var walletLink string
- walletLink = "taler://taldir/" + vars["h_address"] + "/" + vars["validation_code"] + "-wallet"
- var png []byte
- png, err := qrcode.Encode(walletLink, qrcode.Medium, 256)
- if err != nil {
- w.WriteHeader(500)
- return
- }
- encodedPng := base64.StdEncoding.EncodeToString(png)
-
- fullData := map[string]interface{}{
- "QRCode": template.URL("data:image/png;base64," + encodedPng),
- "WalletLink": template.URL(walletLink),
- }
- t.ValidationTpl.Execute(w, fullData)
- return
-}
-
-func (t *Taldir) ClearDatabase() {
- 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/markdown")
- 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 {
- log.Printf("Trying %s\n", termsLocation + lang.String() + ext)
- fileBytes, err := ioutil.ReadFile(termsLocation + lang.String() + ext)
- if nil == err {
- w.Header().Set("Content-Type", fileType)
- w.Write(fileBytes)
- return
- }
- }
- }
- w.WriteHeader(404)
- return
- }
- // Default document in expected/default format
- defaultLanguage := t.Cfg.Section("taldir").Key("default_doc_lang").MustString("en-US")
- extensions, _ := mime.ExtensionsByType(fileType)
- for _, ext := range extensions {
- fileBytes, err := ioutil.ReadFile(termsLocation + defaultLanguage + ext)
- if nil == err {
- w.Header().Set("Content-Type", fileType)
- w.Write(fileBytes)
- return
- }
- }
- w.WriteHeader(404)
-}
-
-func (t *Taldir) privacyResponse(w http.ResponseWriter, r *http.Request) {
- fileType := t.Cfg.Section("taldir").Key("default_doc_filetype").MustString("text/markdown")
- 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 {
- log.Printf("Trying %s\n", termsLocation + lang.String() + ext)
- fileBytes, err := ioutil.ReadFile(termsLocation + lang.String() + ext)
- if nil == err {
- w.Header().Set("Content-Type", fileType)
- w.Write(fileBytes)
- return
- }
- }
- }
- w.WriteHeader(404)
- return
- }
- // Default document in expected/default format
- defaultLanguage := t.Cfg.Section("taldir").Key("default_doc_lang").MustString("en-US")
- extensions, _ := mime.ExtensionsByType(fileType)
- for _, ext := range extensions {
- fileBytes, err := ioutil.ReadFile(termsLocation + defaultLanguage + ext)
- if nil == err {
- w.Header().Set("Content-Type", fileType)
- w.Write(fileBytes)
- return
- }
- }
- w.WriteHeader(404)
-}
-
-
-func (t *Taldir) setupHandlers() {
- t.Router = mux.NewRouter().StrictSlash(true)
-
- /* 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")
-
-
- /* 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}/{validation_code}", t.validationPage).Methods("GET")
- t.Router.HandleFunc("/{h_address}", t.validationRequest).Methods("POST")
-
-}
-
-func (t *Taldir) handleRequests() {
- log.Fatal(http.ListenAndServe(t.Cfg.Section("taldir").Key("bind_to").MustString("localhost:11000"), t.Router))
-}
-
-func (t *Taldir) Initialize(cfgfile string, clearDb bool) {
- _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
- }
-
- 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{})
- 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)
- }
- if clearDb {
- t.ClearDatabase()
- }
-
- 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")
- }
- t.RequestFrequency = t.Cfg.Section("taldir").Key("request_frequency_microseconds").MustInt64(1000)
- t.setupHandlers()
-}
-
-func (t *Taldir) Run() {
- t.handleRequests()
-}
diff --git a/go.mod b/go.mod
@@ -9,7 +9,7 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
- golang.org/x/text v0.3.7 // indirect
+ golang.org/x/text v0.3.7
gopkg.in/ini.v1 v1.66.4
gopkg.in/jcmturner/aescts.v1 v1.0.1 // indirect
gopkg.in/jcmturner/dnsutils.v1 v1.0.1 // indirect
diff --git a/pkg/taldir/taldir.go b/pkg/taldir/taldir.go
@@ -0,0 +1,658 @@
+// This file is part of taldir, the Taler Directory implementation.
+// Copyright (C) 2022 Martin Schanzenbach
+//
+// Taldir is free software: you can redistribute it and/or modify it
+// under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// Taldir is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+//
+// SPDX-License-Identifier: AGPL3.0-or-later
+
+package taldir
+
+/* TODO
+ - ToS API (terms, privacy) with localizions
+ - ToS compression
+ - ToS etag
+ - Prettify QR code landing page
+ - Base32: Use gnunet-go module? (currently copied)
+ - OrderId processing
+ - Maintenance of database: When to delete expired validations?
+*/
+
+import (
+ "os"
+ "os/exec"
+ "time"
+ "fmt"
+ "log"
+ "io/ioutil"
+ "mime"
+ "net/http"
+ "html/template"
+ "encoding/json"
+ "github.com/gorilla/mux"
+ "gorm.io/gorm"
+ "encoding/base64"
+ "taler.net/taldir/pkg/util"
+ "taler.net/taldir/pkg/gana"
+ "crypto/sha512"
+ "gorm.io/driver/postgres"
+ "gopkg.in/ini.v1"
+ "strings"
+ "github.com/skip2/go-qrcode"
+ "golang.org/x/text/language"
+)
+
+type Taldir struct {
+
+ // The main router
+ Router *mux.Router
+
+ // The main DB handle
+ Db *gorm.DB
+
+ // Our configuration from the config.json
+ Cfg *ini.File
+
+ // Map of supported validators as defined in the configuration
+ Validators map[string]bool
+
+ // landing page
+ ValidationTpl *template.Template
+
+ // The address salt
+ Salt string
+
+ // Request frequency
+ RequestFrequency int64
+}
+
+type VersionResponse struct {
+ // libtool-style representation of the Merchant protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ Version string `json:"version"`
+
+ // Name of the protocol.
+ Name string `json:"name"` // "taler-directory"
+
+ // Supported registration methods
+ Methods []Method `json:"methods"`
+
+ // fee for one month of registration
+ MonthlyFee string `json:"monthly_fee"`
+
+}
+
+type Method struct {
+
+ // Name of the method, e.g. "email" or "sms".
+ Name string `json:"name"`
+
+ // per challenge fee
+ ChallengeFee string `json:"challenge_fee"`
+
+}
+
+type RateLimitedResponse struct {
+
+ // Taler error code, TALER_EC_TALDIR_REGISTER_RATE_LIMITED.
+ Code int `json:"code"`
+
+ // At what frequency are new registrations allowed. FIXME: In what? Currently: In microseconds
+ RequestFrequency int64 `json:"request_frequency"`
+
+ // The human readable error message.
+ Hint string `json:"hint"`
+}
+
+type RegisterMessage struct {
+
+ // Address, in method-specific format
+ Address string `json:"address"`
+
+ // Public key of the user to register
+ PublicKey string `json:"public_key"`
+
+ // (HTTPS) endpoint URL for the inbox service for this address
+ Inbox string `json:"inbox_url"`
+
+ // For how long should the registration last
+ Duration int64 `json:"duration"`
+
+ // Order ID, if the client recently paid for this registration
+ // FIXME: As an optional field, maybe we want to parse this separately
+ // instead?
+ // Order_id string `json:"order_id"`
+}
+
+// A mappind entry from the identity key hash to a wallet key
+// The identity key hash is sha256(sha256(identity)|salt) where identity is
+// one of the identity key types supported (e.g. email)
+type Entry struct {
+
+ // ORM
+ gorm.Model `json:"-"`
+
+ // The salted hash (SHA512) of the hashed address (h_address)
+ HsAddress string `json:"-"`
+
+ // (HTTPS) endpoint URL for the inbox service for this address
+ Inbox string `json:"inbox_url"`
+
+ // Public key of the user to register in base32
+ PublicKey string `json:"public_key"`
+
+ // Time of (re)registration. In Unix epoch microseconds)
+ RegisteredAt int64 `json:"-"`
+
+ // How long the registration lasts in microseconds
+ Duration int64 `json:"-"`
+}
+
+// A validation is created when a registration for an entry is initiated.
+// The validation stores the identity key (sha256(identity)) the secret
+// validation reference. The validation reference is sent to the identity
+// depending on the out-of-band chennel defined through the identity key type.
+type Validation struct {
+
+ // ORM
+ gorm.Model `json:"-"`
+
+ // The hash (SHA512) of the address
+ HAddress string `json:"h_address"`
+
+ // For how long should the registration last
+ Duration int64 `json:"duration"`
+
+ // (HTTPS) endpoint URL for the inbox service for this address
+ Inbox string `json:"inbox_url"`
+
+ // The activation code sent to the client
+ Code string `json:"activation_code"`
+
+ // Public key of the user to register
+ PublicKey string `json:"public_key"`
+}
+
+type ErrorDetail struct {
+
+ // Numeric error code unique to the condition.
+ // The other arguments are specific to the error value reported here.
+ Code int `json:"code"`
+
+ // Human-readable description of the error, i.e. "missing parameter", "commitment violation", ...
+ // Should give a human-readable hint about the error's nature. Optional, may change without notice!
+ Hint string `json:"hint,omitempty"`
+
+ // Optional detail about the specific input value that failed. May change without notice!
+ Detail string `json:"detail,omitempty"`
+
+ // Name of the parameter that was bogus (if applicable).
+ Parameter string `json:"parameter,omitempty"`
+
+ // Path to the argument that was bogus (if applicable).
+ Path string `json:"path,omitempty"`
+
+ // Offset of the argument that was bogus (if applicable).
+ Offset string `json:"offset,omitempty"`
+
+ // Index of the argument that was bogus (if applicable).
+ Index string `json:"index,omitempty"`
+
+ // Name of the object that was bogus (if applicable).
+ Object string `json:"object,omitempty"`
+
+ // Name of the currency than was problematic (if applicable).
+ Currency string `json:"currency,omitempty"`
+
+ // Expected type (if applicable).
+ TypeExpected string `json:"type_expected,omitempty"`
+
+ // Type that was provided instead (if applicable).
+ TypeActual string `json:"type_actual,omitempty"`
+}
+
+type ValidationConfirmation struct {
+ Solution string `json:"solution"`
+}
+
+// matcher is a language.Matcher configured for all supported languages.
+var langMatcher = language.NewMatcher([]language.Tag{
+ language.BritishEnglish,
+ //language.Norwegian,
+ language.German,
+})
+
+// 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
+ hs_address := saltHAddress(vars["h_address"], t.Salt)
+ var err = t.Db.First(&entry, "hs_address = ?", hs_address).Error
+ if err == nil {
+ w.Header().Set("Content-Type", "application/json")
+ resp, _ := json.Marshal(entry)
+ w.Write(resp)
+ return
+ }
+ w.WriteHeader(http.StatusNotFound)
+}
+
+// Hashes an identity key (e.g. sha256(<email address>)) with a salt for
+// Lookup and storage.
+func saltHAddress(h_address string, salt string) string {
+ h := sha512.New()
+ h.Write([]byte(h_address))
+ h.Write([]byte(salt))
+ return util.EncodeBinaryToString(h.Sum(nil))
+}
+
+// Called by the registrant to validate the registration request. The reference ID was
+// provided "out of band" using a validation method such as email or SMS
+func (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
+ }
+ expectedSolution := util.GenerateSolution(validation.PublicKey, validation.Code)
+ if confirm.Solution != expectedSolution {
+ // FIXME how TF do we rate limit here??
+ w.WriteHeader(http.StatusForbidden)
+ return
+ }
+ // FIXME: Expire validations somewhere?
+ err = t.Db.Delete(&validation).Error
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ entry.HsAddress = saltHAddress(validation.HAddress, t.Salt)
+ entry.Inbox = validation.Inbox
+ entry.Duration = validation.Duration
+ entry.RegisteredAt = time.Now().UnixMicro()
+ 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) registerRequest(w http.ResponseWriter, r *http.Request){
+ vars := mux.Vars(r)
+ var req RegisterMessage
+ var errDetail ErrorDetail
+ var validation Validation
+ var entry Entry
+ if r.Body == nil {
+ http.Error(w, "No request body", 400)
+ return
+ }
+ err := json.NewDecoder(r.Body).Decode(&req)
+ if err != nil {
+ errDetail.Code = gana.GENERIC_JSON_INVALID
+ errDetail.Hint = "Unable to parse JSON"
+ resp, _ := json.Marshal(errDetail)
+ w.WriteHeader(400)
+ w.Write(resp)
+ return
+ }
+ if !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(404)
+ w.Write(resp)
+ return
+ }
+ h := sha512.New()
+ h.Write([]byte(req.Address))
+ validation.HAddress = util.EncodeBinaryToString(h.Sum(nil))
+ // We first try if there is already an entry for this address which
+ // is still valid and the duration is not extended.
+ hs_address := saltHAddress(validation.HAddress, t.Salt)
+ err = t.Db.First(&entry, "hs_address = ?", hs_address).Error
+ if err == nil {
+ log.Println("Entry for this address already exists..")
+ lastRegValidity := entry.RegisteredAt + entry.Duration
+ requestedValidity := time.Now().UnixMicro() + req.Duration
+ earliestReRegistration := entry.RegisteredAt + t.RequestFrequency
+ // Rate limit re-registrations.
+ if time.Now().UnixMicro() < earliestReRegistration {
+ w.WriteHeader(429)
+ rlResponse := RateLimitedResponse{
+ Code: gana.TALDIR_REGISTER_RATE_LIMITED,
+ RequestFrequency: t.RequestFrequency,
+ Hint: "Registration rate limit reached",
+ }
+ jsonResp, _ := json.Marshal(rlResponse)
+ w.Write(jsonResp)
+ return
+ }
+ // Do not allow re-registrations with shorter duration.
+ if requestedValidity <= lastRegValidity {
+ w.WriteHeader(200)
+ // FIXME how to return how long it is already paid for??
+ return
+ }
+ }
+ err = t.Db.First(&validation, "h_address = ?", validation.HAddress).Error
+ bytes := t.Cfg.Section("taldir").Key("activation_code_bytes").MustInt(16)
+ validation.Code = util.GenerateCode(bytes)
+ validation.Inbox = req.Inbox
+ validation.Duration = req.Duration
+ validation.PublicKey = req.PublicKey
+ if err == nil {
+ // FIXME: Validation already pending for this address
+ // How should we proceed here? Expire old validations?
+ log.Println("Validation for this address already exists")
+ err = t.Db.Save(&validation).Error
+ } else {
+ err = t.Db.Create(&validation).Error
+ }
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+ fmt.Println("Address registration request created:", validation)
+ 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.Code).Output()
+ if err != nil {
+ log.Println(err)
+ t.Db.Delete(&validation)
+ w.WriteHeader(500)
+ return
+ }
+ w.WriteHeader(202)
+ fmt.Printf("Output from method script %s is %s\n", path, out)
+}
+
+func notImplemented(w http.ResponseWriter, r *http.Request) {
+ return
+}
+
+func (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)
+}
+
+func (t *Taldir) validationPage(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ var walletLink string
+ walletLink = "taler://taldir/" + vars["h_address"] + "/" + vars["validation_code"] + "-wallet"
+ var png []byte
+ png, err := qrcode.Encode(walletLink, qrcode.Medium, 256)
+ if err != nil {
+ w.WriteHeader(500)
+ return
+ }
+ encodedPng := base64.StdEncoding.EncodeToString(png)
+
+ fullData := map[string]interface{}{
+ "QRCode": template.URL("data:image/png;base64," + encodedPng),
+ "WalletLink": template.URL(walletLink),
+ }
+ t.ValidationTpl.Execute(w, fullData)
+ return
+}
+
+func (t *Taldir) DeleteValidation(addr string) error {
+
+ var validation Validation
+ h := sha512.New()
+ h.Write([]byte(addr))
+ h_addr := util.EncodeBinaryToString(h.Sum(nil))
+ err := t.Db.First(&validation, "h_address = ?", h_addr).Error
+ if nil == err {
+ err = t.Db.Delete(&validation).Error
+ }
+ return err
+}
+
+
+func (t *Taldir) DeleteEntry(addr string) error {
+
+ var entry Entry
+ h := sha512.New()
+ h.Write([]byte(addr))
+ h_addr := util.EncodeBinaryToString(h.Sum(nil))
+ hs_address := saltHAddress(h_addr, t.Salt)
+ err := t.Db.First(&entry, "hs_address = ?", hs_address).Error
+ if nil == err {
+ err = t.Db.Delete(&entry).Error
+ }
+ return err
+}
+
+func (t *Taldir) ClearDatabase() {
+ 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/markdown")
+ 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 {
+ log.Printf("Trying %s\n", termsLocation + lang.String() + ext)
+ fileBytes, err := ioutil.ReadFile(termsLocation + lang.String() + ext)
+ if nil == err {
+ w.Header().Set("Content-Type", fileType)
+ w.Write(fileBytes)
+ return
+ }
+ }
+ }
+ w.WriteHeader(404)
+ return
+ }
+ // Default document in expected/default format
+ defaultLanguage := t.Cfg.Section("taldir").Key("default_doc_lang").MustString("en-US")
+ extensions, _ := mime.ExtensionsByType(fileType)
+ for _, ext := range extensions {
+ fileBytes, err := ioutil.ReadFile(termsLocation + defaultLanguage + ext)
+ if nil == err {
+ w.Header().Set("Content-Type", fileType)
+ w.Write(fileBytes)
+ return
+ }
+ }
+ w.WriteHeader(404)
+}
+
+func (t *Taldir) privacyResponse(w http.ResponseWriter, r *http.Request) {
+ fileType := t.Cfg.Section("taldir").Key("default_doc_filetype").MustString("text/markdown")
+ 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 {
+ log.Printf("Trying %s\n", termsLocation + lang.String() + ext)
+ fileBytes, err := ioutil.ReadFile(termsLocation + lang.String() + ext)
+ if nil == err {
+ w.Header().Set("Content-Type", fileType)
+ w.Write(fileBytes)
+ return
+ }
+ }
+ }
+ w.WriteHeader(404)
+ return
+ }
+ // Default document in expected/default format
+ defaultLanguage := t.Cfg.Section("taldir").Key("default_doc_lang").MustString("en-US")
+ extensions, _ := mime.ExtensionsByType(fileType)
+ for _, ext := range extensions {
+ fileBytes, err := ioutil.ReadFile(termsLocation + defaultLanguage + ext)
+ if nil == err {
+ w.Header().Set("Content-Type", fileType)
+ w.Write(fileBytes)
+ return
+ }
+ }
+ w.WriteHeader(404)
+}
+
+
+func (t *Taldir) setupHandlers() {
+ t.Router = mux.NewRouter().StrictSlash(true)
+
+ /* 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")
+
+
+ /* 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}/{validation_code}", t.validationPage).Methods("GET")
+ t.Router.HandleFunc("/{h_address}", t.validationRequest).Methods("POST")
+
+}
+
+func (t *Taldir) handleRequests() {
+ log.Fatal(http.ListenAndServe(t.Cfg.Section("taldir").Key("bind_to").MustString("localhost:11000"), t.Router))
+}
+
+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
+ }
+
+ 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{})
+ 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)
+ }
+
+ 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")
+ }
+ t.RequestFrequency = t.Cfg.Section("taldir").Key("request_frequency_microseconds").MustInt64(1000)
+ t.setupHandlers()
+}
+
+func (t *Taldir) Run() {
+ t.handleRequests()
+}
diff --git a/util/base32.go b/pkg/util/base32.go
diff --git a/util/helper.go b/pkg/util/helper.go