taler-mailbox

Service for asynchronous wallet-to-wallet payment messages
Log | Files | Refs | Submodules | README | LICENSE

mailbox.go (23983B)


      1 // This file is part of taler-mailbox, the Taler Mailbox implementation.
      2 // Copyright (C) 2022 Martin Schanzenbach
      3 //
      4 // Taler-mailbox is free software: you can redistribute it and/or modify it
      5 // under the terms of the GNU Affero General Public License as published
      6 // by the Free Software Foundation, either version 3 of the License,
      7 // or (at your option) any later version.
      8 //
      9 // Taler-mailbox is distributed in the hope that it will be useful, but
     10 // WITHOUT ANY WARRANTY; without even the implied warranty of
     11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     12 // Affero General Public License for more details.
     13 //
     14 // You should have received a copy of the GNU Affero General Public License
     15 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
     16 //
     17 // SPDX-License-Identifier: AGPL3.0-or-later
     18 
     19 // Package mailbox is a GNU Taler service. See https://docs.taler.net/core/api-mailbox.html
     20 package mailbox
     21 
     22 import (
     23 	"crypto/ed25519"
     24 	"crypto/sha512"
     25 	"encoding/binary"
     26 	"encoding/json"
     27 	"fmt"
     28 	"io"
     29 	"log"
     30 	"net/http"
     31 	"os"
     32 	"strconv"
     33 	"strings"
     34 	"time"
     35 
     36 	"github.com/gorilla/mux"
     37 	"github.com/schanzen/taler-go/pkg/merchant"
     38 	tos "github.com/schanzen/taler-go/pkg/rest"
     39 	talerutil "github.com/schanzen/taler-go/pkg/util"
     40 	"taler.net/taler-mailbox/internal/gana"
     41 	"taler.net/taler-mailbox/internal/util"
     42 )
     43 
     44 type LogLevel int
     45 
     46 const (
     47 	LogError LogLevel = iota
     48 	LogWarning
     49 	LogInfo
     50 	LogDebug
     51 )
     52 
     53 var LoglevelStringMap = map[LogLevel]string{
     54 	LogDebug:   "DEBUG",
     55 	LogError:   "ERROR",
     56 	LogWarning: "WARN",
     57 	LogInfo:    "INFO",
     58 }
     59 
     60 type MailboxConfig struct {
     61 	// libtool-style representation of the Mailbox protocol version, see
     62 	// https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
     63 	// The format is "current:revision:age".
     64 	LibtoolVersion string
     65 
     66 	// Version
     67 	Version string
     68 
     69 	// Data home
     70 	Datahome string
     71 
     72 	// Configuration
     73 	Ini talerutil.TalerConfiguration
     74 
     75 	// The database connection to use
     76 	DB *MailboxDatabase
     77 
     78 	// Merchant connection
     79 	Merchant merchant.Merchant
     80 
     81 	// Loglevel
     82 	Loglevel LogLevel
     83 }
     84 
     85 // Mailbox is the primary object of the Mailbox service
     86 type Mailbox struct {
     87 
     88 	// The main router
     89 	Router *mux.Router
     90 
     91 	// The database connection to use
     92 	DB *MailboxDatabase
     93 
     94 	// Our configuration from the ini
     95 	Cfg MailboxConfig
     96 
     97 	// Fixed size of message bodies
     98 	MessageBodyBytes int64 `json:"message_body_bytes"`
     99 
    100 	// Merchant object
    101 	Merchant merchant.Merchant
    102 
    103 	// Base URL
    104 	BaseURL string
    105 
    106 	// Registration fee for each validity month mailbox
    107 	MonthlyFee *talerutil.Amount
    108 
    109 	// Registration fee for registering or modifying mailbox
    110 	RegistrationUpdateFee *talerutil.Amount
    111 
    112 	// Message fee for receiving a message
    113 	MessageFee *talerutil.Amount
    114 
    115 	// The free message quota
    116 	FreeMessageQuota uint64
    117 
    118 	// How many messages will a single response
    119 	// contain at maximum.
    120 	MessageResponseLimit uint64
    121 
    122 	// Logger
    123 	Logger *log.Logger
    124 
    125 	// Currency Spec
    126 	CurrencySpec talerutil.CurrencySpecification
    127 }
    128 
    129 type RelativeTime struct {
    130 	Microseconds uint64 `json:"d_us"`
    131 }
    132 
    133 // 1 Month as Go duration
    134 const monthDuration = time.Hour * 24 * 30
    135 
    136 // VersionResponse is the JSON response of the /config endpoint
    137 type VersionResponse struct {
    138 	// libtool-style representation of the Mailbox protocol version, see
    139 	// https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
    140 	// The format is "current:revision:age".
    141 	Version string `json:"version"`
    142 
    143 	// Name of the protocol.
    144 	Name string `json:"name"` // "taler-mailbox"
    145 
    146 	// Fixed size of message bodies
    147 	MessageBodyBytes int64 `json:"message_body_bytes"`
    148 
    149 	// How long will the service store a message
    150 	// before giving up
    151 	DeliveryPeriod RelativeTime `json:"delivery_period" gorm:"embedded;embeddedPrefix:delivery_period_"`
    152 
    153 	// How many messages will a single response
    154 	// contain at maximum.
    155 	MessageResponseLimit uint64 `json:"message_response_limit"`
    156 
    157 	// How much is the cost of a single
    158 	// registration (update) of a mailbox
    159 	// May be 0 for a free update/registration.
    160 	RegistrationUpdateFee string `json:"registration_update_fee"`
    161 
    162 	// How much is the cost of a single
    163 	// registration period (30 days) of a mailbox
    164 	// May be 0 for a free registration.
    165 	MonthlyFee string `json:"monthly_fee"`
    166 
    167 	// How much is the cost to send a single
    168 	// message to a mailbox.
    169 	// May be 0 for free message sending.
    170 	MessageFee string `json:"message_fee"`
    171 
    172 	// How many messages can be send and
    173 	// are stored by the service for free.
    174 	// After the quota is reached, the
    175 	// regular message_fee applies.
    176 	// May be 0 for no free quota.
    177 	FreeMessageQuota string `json:"free_message_quota"`
    178 }
    179 
    180 type MailboxRegistrationRequest struct {
    181 
    182 	// Keys to add/update for a mailbox.
    183 	MailboxMetadata MailboxMetadata `json:"mailbox_metadata"`
    184 
    185 	// Signature by the mailbox's signing key affirming
    186 	// the update of keys, of purpose
    187 	// TALER_SIGNATURE_WALLET_MAILBOX_KEYS_UPDATE.
    188 	// The signature is created over the SHA-512 hash
    189 	// of (encryptionKeyType||encryptionKey||expiration)
    190 	Signature string `json:"signature"`
    191 }
    192 
    193 // MailboxRateLimitedResponse is the JSON response when a rate limit is hit
    194 type MailboxRateLimitedResponse struct {
    195 
    196 	// Taler error code, TALER_EC_mailbox_REGISTER_RATE_LIMITED.
    197 	Code int `json:"code"`
    198 
    199 	// When the client should retry. Currently: In microseconds
    200 	RetryDelay int64 `json:"retry_delay"`
    201 
    202 	// The human readable error message.
    203 	Hint string `json:"hint"`
    204 }
    205 
    206 func (m *Mailbox) configResponse(w http.ResponseWriter, r *http.Request) {
    207 	dp, err := m.Cfg.Ini.GetDuration("mailbox", "delivery_period", 3*24*time.Hour)
    208 	if err != nil {
    209 		log.Fatal(err)
    210 	}
    211 	cfg := VersionResponse{
    212 		Version:               m.Cfg.LibtoolVersion,
    213 		Name:                  "taler-mailbox",
    214 		MessageBodyBytes:      m.MessageBodyBytes,
    215 		MessageResponseLimit:  m.MessageResponseLimit,
    216 		MonthlyFee:            m.MonthlyFee.String(),
    217 		RegistrationUpdateFee: m.RegistrationUpdateFee.String(),
    218 		DeliveryPeriod:        RelativeTime{Microseconds: uint64(dp.Microseconds())},
    219 	}
    220 	w.Header().Set("Content-Type", "application/json")
    221 	response, _ := json.Marshal(cfg)
    222 	w.Write(response)
    223 }
    224 
    225 func (m *Mailbox) getMessagesResponse(w http.ResponseWriter, r *http.Request) {
    226 	vars := mux.Vars(r)
    227 	//to, toSet := vars["timeout_ms"]
    228 	var entries []InboxEntry
    229 	// FIXME rate limit
    230 	// FIXME timeout
    231 	// FIXME possibly limit results here
    232 	m.checkPendingMailboxRegistrationUpdates(vars["h_mailbox"])
    233 	entries, err := m.DB.GetMessages(vars["h_mailbox"], int(m.MessageResponseLimit))
    234 	if err != nil {
    235 		m.Logf(LogError, "Error getting messages: %v", err)
    236 		w.WriteHeader(http.StatusNotFound)
    237 		return
    238 	}
    239 	if len(entries) == 0 {
    240 		w.WriteHeader(http.StatusNoContent)
    241 		return
    242 	}
    243 	// Add ETag of first message ID
    244 	etag := entries[0].Serial
    245 	w.Header().Add("ETag", fmt.Sprintf("%d", etag))
    246 	for _, entry := range entries {
    247 		w.Write(entry.Body)
    248 	}
    249 }
    250 
    251 func (m *Mailbox) sendMessageResponse(w http.ResponseWriter, r *http.Request) {
    252 	vars := mux.Vars(r)
    253 	var entry InboxEntry
    254 	if r.Body == nil {
    255 		http.Error(w, "No request body", http.StatusBadRequest)
    256 		return
    257 	}
    258 	if r.ContentLength != m.MessageBodyBytes {
    259 		http.Error(w, "Wrong message size", http.StatusBadRequest)
    260 		return
    261 	}
    262 	body, err := io.ReadAll(r.Body)
    263 	if err != nil {
    264 		log.Printf("%v", err)
    265 		http.Error(w, "Cannot read body", http.StatusBadRequest)
    266 		return
    267 	}
    268 	if !m.MessageFee.IsZero() {
    269 		var count int64
    270 		count, err = m.DB.GetMessagesCount(vars["h_mailbox"])
    271 		if nil != err {
    272 			m.Logf(LogError, "Error getting messages: %v", err)
    273 			http.Error(w, "Cannot look for entries", http.StatusBadRequest)
    274 			return
    275 		}
    276 		if count >= int64(m.FreeMessageQuota) {
    277 			w.WriteHeader(http.StatusPaymentRequired)
    278 			//w.Header().Set("Taler", payto) FIXME generate payto
    279 			return
    280 		}
    281 	}
    282 	m.checkPendingMailboxRegistrationUpdates(vars["h_mailbox"])
    283 	err = m.DB.GetInboxEntryBySigningKeyAndBody(&entry, vars["h_mailbox"], body)
    284 	if err == nil {
    285 		w.WriteHeader(http.StatusNotModified)
    286 		return
    287 	}
    288 	entry.HashedSigningKey = vars["h_mailbox"]
    289 	entry.Body = body
    290 	err = m.DB.InsertInboxEntry(&entry)
    291 	if err != nil {
    292 		m.Logf(LogError, "Error storing message: %v", err)
    293 		w.WriteHeader(http.StatusInternalServerError)
    294 		return
    295 	}
    296 	w.WriteHeader(http.StatusNoContent)
    297 }
    298 
    299 func (m *Mailbox) getKeysResponse(w http.ResponseWriter, r *http.Request) {
    300 	vars := mux.Vars(r)
    301 	var keyEntry MailboxMetadata
    302 	m.checkPendingMailboxRegistrationUpdates(vars["h_mailbox"])
    303 	err := m.DB.GetMailboxMetadataBySigningKey(&keyEntry, vars["h_mailbox"])
    304 	if err != nil {
    305 		m.Logf(LogError, "Error finding mailbox: %v", err)
    306 		w.WriteHeader(http.StatusNotFound)
    307 		return
    308 	}
    309 	m.Logf(LogDebug, "entry expires at %d, have %d", keyEntry.Expiration, time.Now().Unix())
    310 	if int64(keyEntry.Expiration.Seconds) < int64(time.Now().Unix()) {
    311 		w.WriteHeader(http.StatusNotFound)
    312 		return
    313 	}
    314 	response, _ := json.Marshal(keyEntry)
    315 	w.Write(response)
    316 }
    317 
    318 func (m *Mailbox) validateRegistrationSignature(msg MailboxRegistrationRequest) error {
    319 	var expNbo [8]byte
    320 	var signedMsg [72]byte
    321 	encPk, err := util.Base32CrockfordDecode(msg.MailboxMetadata.EncryptionKey, 32)
    322 	if err != nil {
    323 		return fmt.Errorf("unable to decode encryption key")
    324 	}
    325 	pkey, err := util.Base32CrockfordDecode(msg.MailboxMetadata.SigningKey, 32)
    326 	if err != nil {
    327 		return fmt.Errorf("unable to decode pubkey")
    328 	}
    329 	pk := ed25519.PublicKey(pkey)
    330 	sig, err := util.Base32CrockfordDecode(msg.Signature, 64)
    331 	if nil != err {
    332 		return fmt.Errorf("unable to decode signature")
    333 	}
    334 	binary.BigEndian.PutUint64(expNbo[:], msg.MailboxMetadata.Expiration.Seconds)
    335 	size := signedMsg[0:4]
    336 	binary.BigEndian.PutUint32(size, 64+4+4)
    337 	purp := signedMsg[4:8]
    338 	binary.BigEndian.PutUint32(purp, gana.TalerSignaturePurposeMailboxRegister)
    339 	h := sha512.New()
    340 	h.Write([]byte(msg.MailboxMetadata.EncryptionKeyType)) // Currently always X25519
    341 	h.Write(encPk)
    342 	h.Write(expNbo[:])
    343 	copy(signedMsg[8:], h.Sum(nil))
    344 	if !ed25519.Verify(pk, signedMsg[0:], sig) {
    345 		return fmt.Errorf("signature invalid")
    346 	}
    347 	return nil
    348 }
    349 
    350 // Check if this is a non-zero, positive amount
    351 func calculateCost(sliceCostAmount string, fixedCostAmount string, howLong time.Duration, sliceDuration time.Duration) (*talerutil.Amount, error) {
    352 	sliceCount := int(float64(howLong.Microseconds()) / float64(sliceDuration.Microseconds()))
    353 	sliceCost, err := talerutil.ParseAmount(sliceCostAmount)
    354 	if nil != err {
    355 		return nil, err
    356 	}
    357 	fixedCost, err := talerutil.ParseAmount(fixedCostAmount)
    358 	if nil != err {
    359 		return nil, err
    360 	}
    361 	sum := &talerutil.Amount{
    362 		Currency: sliceCost.Currency,
    363 		Value:    0,
    364 		Fraction: 0,
    365 	}
    366 	for range sliceCount {
    367 		sum, err = sum.Add(*sliceCost)
    368 		if nil != err {
    369 			return nil, err
    370 		}
    371 	}
    372 	sum, err = sum.Add(*fixedCost)
    373 	if nil != err {
    374 		return nil, err
    375 	}
    376 	return sum, nil
    377 }
    378 
    379 func (m *Mailbox) registerMailboxResponse(w http.ResponseWriter, r *http.Request) {
    380 	var msg MailboxRegistrationRequest
    381 	var pendingRegistration PendingMailboxRegistration
    382 	var registrationEntry MailboxMetadata
    383 	if r.Body == nil {
    384 		m.Logf(LogError, "no request body")
    385 		http.Error(w, "No request body", http.StatusBadRequest)
    386 		return
    387 	}
    388 	err := json.NewDecoder(r.Body).Decode(&msg)
    389 	if err != nil {
    390 		http.Error(w, "Malformed request body", http.StatusBadRequest)
    391 		return
    392 	}
    393 	pkey, err := util.Base32CrockfordDecode(msg.MailboxMetadata.SigningKey, 32)
    394 	if err != nil {
    395 		http.Error(w, "Public key invalid", http.StatusBadRequest)
    396 		return
    397 	}
    398 	pk := ed25519.PublicKey(pkey)
    399 	err = m.validateRegistrationSignature(msg)
    400 	if nil != err {
    401 		http.Error(w, "Signature verification failed", http.StatusBadRequest)
    402 		return
    403 	}
    404 	h := sha512.New()
    405 	h.Write(pkey)
    406 	hMailbox := util.Base32CrockfordEncode(h.Sum(nil))
    407 	pendingRegistration.HashedSigningKey = hMailbox
    408 	// Round to the nearest multiple of a month
    409 	reqExpiration := time.Unix(int64(msg.MailboxMetadata.Expiration.Seconds), 0)
    410 	now := time.Now()
    411 	reqDuration := reqExpiration.Sub(now).Round(monthDuration)
    412 	err = m.DB.GetMailboxMetadataBySigningKey(&registrationEntry, hMailbox)
    413 	if err == nil {
    414 		// This probably means the registration is modified or extended or both
    415 		entryModified := (registrationEntry.EncryptionKey != msg.MailboxMetadata.EncryptionKey)
    416 		// At least one MonthlyFee
    417 		if reqDuration.Microseconds() == 0 && !entryModified {
    418 			// Nothing changed. Return validity
    419 			w.WriteHeader(http.StatusNotModified)
    420 			return
    421 		}
    422 	} else {
    423 		// Entry does not yet exist, add but immediately expire it
    424 		registrationEntry = msg.MailboxMetadata
    425 		registrationEntry.Expiration.Seconds = uint64(time.Now().Unix() - 1)
    426 		hAddr := sha512.New()
    427 		hAddr.Write(pk)
    428 		registrationEntry.HashedSigningKey = util.Base32CrockfordEncode(hAddr.Sum(nil))
    429 		err = m.DB.InsertMailboxRegistration(&registrationEntry)
    430 		if nil != err {
    431 			m.Logf(LogError, "%v\n", err)
    432 			w.WriteHeader(http.StatusInternalServerError)
    433 			return
    434 		}
    435 	}
    436 	err = m.DB.GetPendingMailboxRegistrationBySigningKey(&pendingRegistration, hMailbox)
    437 	pendingRegistrationExists := (nil == err)
    438 	if !pendingRegistrationExists {
    439 		pendingRegistration.HashedSigningKey = hMailbox
    440 		pendingRegistration.Duration = reqDuration.Microseconds()
    441 		err = m.DB.InsertPendingMailboxRegistration(&pendingRegistration)
    442 		if nil != err {
    443 			m.Logf(LogError, "Error inserting pending registration: %v\n", err)
    444 			w.WriteHeader(http.StatusInternalServerError)
    445 			return
    446 		}
    447 		err = m.DB.GetPendingMailboxRegistrationBySigningKey(&pendingRegistration, hMailbox)
    448 		if nil != err {
    449 			m.Logf(LogError, "Error getting pending registration: %v\n", err)
    450 			w.WriteHeader(http.StatusInternalServerError)
    451 			return
    452 		}
    453 	}
    454 	// At least the update fee needs to be paid
    455 	cost, err := calculateCost(m.MonthlyFee.String(),
    456 		m.RegistrationUpdateFee.String(),
    457 		reqDuration,
    458 		monthDuration)
    459 	if err != nil {
    460 		m.Logf(LogError, "Error calculating cost: %v\n", err)
    461 		w.WriteHeader(http.StatusInternalServerError)
    462 		return
    463 	}
    464 	if !cost.IsZero() {
    465 		if len(pendingRegistration.OrderID) == 0 {
    466 			// Add new order
    467 			orderID, newOrderErr := m.Merchant.AddNewOrder(*cost, "Mailbox registration", m.BaseURL)
    468 			if newOrderErr != nil {
    469 				m.Logf(LogError, "Error adding order: %v", newOrderErr)
    470 				w.WriteHeader(http.StatusInternalServerError)
    471 				return
    472 			}
    473 			m.Logf(LogDebug, "New order ID %s for pending registration for %s", orderID, pendingRegistration.HashedSigningKey)
    474 			pendingRegistration.OrderID = orderID
    475 		}
    476 		// Check if order paid.
    477 		// FIXME: Remember that it was activated and paid
    478 		// FIXME: We probably need to handle the return code here (see gns registrar for how)
    479 		_, _, payto, paytoErr := m.Merchant.IsOrderPaid(pendingRegistration.OrderID)
    480 		if paytoErr != nil {
    481 			fmt.Println(paytoErr)
    482 			w.WriteHeader(http.StatusInternalServerError)
    483 			m.Logf(LogError, "Error checking if order is paid: %s\n", paytoErr.Error())
    484 			return
    485 		}
    486 		if len(payto) != 0 {
    487 			err = m.DB.UpdatePendingMailboxRegistrationOrderId(&pendingRegistration)
    488 			if err != nil {
    489 				m.Logf(LogError, "Error updating pending registration: %v\n", err)
    490 				w.WriteHeader(http.StatusInternalServerError)
    491 				return
    492 			}
    493 			w.WriteHeader(http.StatusPaymentRequired)
    494 			w.Header().Set("Taler", payto)
    495 			return
    496 		}
    497 	}
    498 	// Update expiration time of registration.
    499 	registrationEntry.Expiration.Seconds += uint64(reqDuration.Seconds())
    500 	_, err = m.DB.DeletePendingRegistration(&pendingRegistration)
    501 	if nil != err {
    502 		m.Logf(LogError, "Error deleting pending registration: %v\n", err)
    503 		w.WriteHeader(http.StatusInternalServerError)
    504 		return
    505 	}
    506 	err = m.DB.UpdateMailboxExpiration(&registrationEntry)
    507 	if nil != err {
    508 		m.Logf(LogError, "Error updating mailbox registration: %v\n", err)
    509 		w.WriteHeader(http.StatusInternalServerError)
    510 		return
    511 	}
    512 	w.WriteHeader(http.StatusNoContent)
    513 }
    514 
    515 func (m *Mailbox) checkPendingMailboxRegistrationUpdates(hMailbox string) {
    516 	var pendingEntry PendingMailboxRegistration
    517 	var registrationEntry MailboxMetadata
    518 	err := m.DB.GetPendingMailboxRegistrationBySigningKey(&pendingEntry, hMailbox)
    519 	if err != nil {
    520 		return
    521 	}
    522 	m.Logf(LogDebug, "Found pending registration for %s, OrderID: %s", hMailbox, pendingEntry.OrderID)
    523 	rc, orderStatus, _, paytoErr := m.Merchant.IsOrderPaid(pendingEntry.OrderID)
    524 	if nil != paytoErr {
    525 		if rc == http.StatusNotFound {
    526 			m.Logf(LogInfo, "Registration order for `%s' not found, removing\n", hMailbox)
    527 		}
    528 		_, err = m.DB.DeletePendingRegistration(&pendingEntry)
    529 		if nil != err {
    530 			m.Logf(LogInfo, "Error deleting pending registration: %v\n", err)
    531 		}
    532 		return
    533 	}
    534 	m.Logf(LogDebug, "Order status for %s is %s", pendingEntry.HashedSigningKey, orderStatus)
    535 	if merchant.OrderPaid == orderStatus {
    536 		m.Logf(LogDebug, "Order for %v appears to be paid", pendingEntry)
    537 		err = m.DB.GetMailboxMetadataBySigningKey(&registrationEntry, hMailbox)
    538 		if err == nil {
    539 			m.Logf(LogDebug, "Adding %d seconds to entry expiration", pendingEntry.Duration)
    540 			registrationEntry.Expiration.Seconds += uint64(pendingEntry.Duration)
    541 			err = m.DB.UpdateMailboxExpiration(&registrationEntry)
    542 			if nil != err {
    543 				m.Logf(LogInfo, "Error updating mailbox expiration: %v\n", err)
    544 			}
    545 			_, err = m.DB.DeletePendingRegistration(&pendingEntry)
    546 			if nil != err {
    547 				m.Logf(LogInfo, "Error deleting pending registration: %v\n", err)
    548 			}
    549 		}
    550 		return
    551 	}
    552 }
    553 
    554 func (m *Mailbox) deleteMessagesResponse(w http.ResponseWriter, r *http.Request) {
    555 	vars := mux.Vars(r)
    556 	etagHeader := r.Header.Get("If-Match")
    557 	if etagHeader == "" {
    558 		http.Error(w, "If-Match header missing", 400)
    559 		return
    560 	}
    561 	if strings.Contains(etagHeader, ",") {
    562 		http.Error(w, "If-Match contains multiple values", 400)
    563 		return
    564 	}
    565 	expectedETag, err := strconv.Atoi(etagHeader)
    566 	if err != nil {
    567 		http.Error(w, "If-Match contains malformed etag number", 400)
    568 		return
    569 	}
    570 	pkey, err := util.Base32CrockfordDecode(vars["mailbox"], 32)
    571 	if err != nil {
    572 		w.WriteHeader(http.StatusBadRequest)
    573 		return
    574 	}
    575 	count := 1
    576 	countStr := r.URL.Query().Get("count")
    577 	if len(countStr) > 0 {
    578 		count, err = strconv.Atoi(countStr)
    579 		if err != nil {
    580 			http.Error(w, "Malformed count parameter", http.StatusBadRequest)
    581 			w.WriteHeader(http.StatusBadRequest)
    582 			return
    583 		}
    584 	}
    585 	headerSig := r.Header["Taler-Mailbox-Delete-Signature"]
    586 	if nil == headerSig {
    587 		http.Error(w, "Missing signature", http.StatusBadRequest)
    588 		return
    589 	}
    590 	pk := ed25519.PublicKey(pkey)
    591 	sig, err := util.Base32CrockfordDecode(headerSig[0], 64)
    592 	if nil != err {
    593 		w.WriteHeader(http.StatusBadRequest)
    594 		return
    595 	}
    596 	h := sha512.New()
    597 	h.Write(pkey)
    598 	hMailbox := util.Base32CrockfordEncode(h.Sum(nil))
    599 	m.checkPendingMailboxRegistrationUpdates(hMailbox)
    600 	var signedMsg [4 * 4]byte
    601 	binary.BigEndian.PutUint32(signedMsg[0:4], 4*4)
    602 	binary.BigEndian.PutUint32(signedMsg[4:8], gana.TalerSignaturePurposeMailboxMessagesDelete)
    603 	binary.BigEndian.PutUint32(signedMsg[8:12], uint32(expectedETag))
    604 	binary.BigEndian.PutUint32(signedMsg[12:16], uint32(count))
    605 	if !ed25519.Verify(pk, signedMsg[0:], sig) {
    606 		w.WriteHeader(http.StatusForbidden)
    607 		return
    608 	}
    609 	// Check that expectedETag actually exists
    610 	var entry InboxEntry
    611 	err = m.DB.GetInboxEntryBySerial(&entry, hMailbox, int64(expectedETag))
    612 	if err != nil {
    613 		m.Logf(LogDebug, "Message to delete not found with ID %d", expectedETag)
    614 		w.WriteHeader(http.StatusNotFound)
    615 		return
    616 	}
    617 	m.Logf(LogError, "Deleting from entry %v up to %d messages\n", entry, count)
    618 	num, err := m.DB.DeleteInboxEntryBySerial(&entry, count)
    619 	if err != nil {
    620 		m.Logf(LogDebug, "Failed to delete messages: %v", err)
    621 		w.WriteHeader(http.StatusInternalServerError)
    622 		return
    623 	}
    624 	m.Logf(LogDebug, "Found matching ID (serial: %d), deleted %d messages", entry.Serial, num)
    625 	w.WriteHeader(http.StatusNoContent)
    626 }
    627 
    628 func (m *Mailbox) termsResponse(w http.ResponseWriter, r *http.Request) {
    629 	termspath := m.Cfg.Ini.GetFilename("mailbox", "default_terms_path", "terms/", m.Cfg.Datahome)
    630 	tos.ServiceTermsResponse(w, r, termspath, tos.TalerTosConfig{
    631 		DefaultFileType:    m.Cfg.Ini.GetString("mailbox", "default_doc_filetype", "text/html"),
    632 		DefaultLanguage:    m.Cfg.Ini.GetString("mailbox", "default_doc_lang", "en"),
    633 		SupportedFileTypes: strings.Split(m.Cfg.Ini.GetString("mailbox", "supported_doc_filetypes", ""), " "),
    634 	})
    635 }
    636 
    637 func (m *Mailbox) privacyResponse(w http.ResponseWriter, r *http.Request) {
    638 	pppath := m.Cfg.Ini.GetFilename("mailbox", "default_pp_path", "privacy/", m.Cfg.Datahome)
    639 	tos.PrivacyPolicyResponse(w, r, pppath, tos.TalerTosConfig{
    640 		DefaultFileType:    m.Cfg.Ini.GetString("mailbox", "default_doc_filetype", "text/html"),
    641 		DefaultLanguage:    m.Cfg.Ini.GetString("mailbox", "default_doc_lang", "en"),
    642 		SupportedFileTypes: strings.Split(m.Cfg.Ini.GetString("mailbox", "supported_doc_filetypes", ""), " "),
    643 	})
    644 }
    645 
    646 func (m *Mailbox) setupHandlers() {
    647 	m.Router = mux.NewRouter().StrictSlash(true)
    648 
    649 	/* ToS API */
    650 	m.Router.HandleFunc("/terms", m.termsResponse).Methods("GET")
    651 	m.Router.HandleFunc("/privacy", m.privacyResponse).Methods("GET")
    652 
    653 	/* Config API */
    654 	m.Router.HandleFunc("/config", m.configResponse).Methods("GET")
    655 
    656 	/* Mailbox API */
    657 	m.Router.HandleFunc("/register", m.registerMailboxResponse).Methods("POST")
    658 	m.Router.HandleFunc("/info/{h_mailbox}", m.getKeysResponse).Methods("GET")
    659 	m.Router.HandleFunc("/{h_mailbox}", m.sendMessageResponse).Methods("POST")
    660 	m.Router.HandleFunc("/{h_mailbox}", m.getMessagesResponse).Methods("GET")
    661 	m.Router.HandleFunc("/{mailbox}", m.deleteMessagesResponse).Methods("DELETE")
    662 }
    663 
    664 func (m *Mailbox) Logf(loglevel LogLevel, fmt string, args ...any) {
    665 	if loglevel < m.Cfg.Loglevel {
    666 		return
    667 	}
    668 	m.Logger.SetPrefix("taler-mailbox - " + LoglevelStringMap[loglevel] + " ")
    669 	m.Logger.Printf(fmt, args...)
    670 }
    671 
    672 // Initialize the Mailbox instance with cfgfile
    673 func (m *Mailbox) Initialize(cfg MailboxConfig) {
    674 	m.Cfg = cfg
    675 	m.Logger = log.New(os.Stdout, "taler-mailbox:", log.LstdFlags)
    676 	m.BaseURL = cfg.Ini.GetString("mailbox", "base_url", "https://example.com")
    677 	m.MessageBodyBytes = cfg.Ini.GetInt64("mailbox", "message_body_bytes", 256)
    678 	m.MessageResponseLimit = uint64(cfg.Ini.GetInt64("mailbox", "message_response_limit", 50))
    679 	monthlyFee, err := cfg.Ini.GetAmount("mailbox", "monthly_fee", &talerutil.Amount{})
    680 	if err != nil {
    681 		fmt.Printf("Failed to parse monthly fee: %v", err)
    682 		os.Exit(1)
    683 	}
    684 	m.MonthlyFee = monthlyFee
    685 	updateFee, err := cfg.Ini.GetAmount("mailbox", "registration_update_fee", &talerutil.Amount{})
    686 	if err != nil {
    687 		fmt.Printf("Failed to parse update fee: %v", err)
    688 		os.Exit(1)
    689 	}
    690 	m.RegistrationUpdateFee = updateFee
    691 	messageFee, err := cfg.Ini.GetAmount("mailbox", "message_fee", &talerutil.Amount{})
    692 	if err != nil {
    693 		fmt.Printf("Failed to parse message fee: %v", err)
    694 		os.Exit(1)
    695 	}
    696 	m.MessageFee = messageFee
    697 	m.FreeMessageQuota = uint64(cfg.Ini.GetInt64("mailbox", "free_message_quota", 0))
    698 	m.DB = cfg.DB
    699 	go func() {
    700 		for {
    701 			num, err := m.DB.DeleteStaleRegistrations(time.Now())
    702 			if err != nil {
    703 				m.Logf(LogDebug, "Error purging stale registrations: `%v'.\n", err)
    704 			}
    705 			m.Logf(LogInfo, "Cleaned up %d stale registrations.\n", num)
    706 			time.Sleep(time.Hour * 24)
    707 		}
    708 	}()
    709 	// Clean up pending
    710 	pendingExp, err := cfg.Ini.GetDuration("mailbox", "pending_registration_expiration", 24*time.Hour)
    711 	if err != nil {
    712 		fmt.Printf("Failed to parse pending registration expiration: %v", err)
    713 		os.Exit(1)
    714 	}
    715 	go func() {
    716 		for {
    717 			num, err := m.DB.DeleteStalePendingRegistrations(time.Now().Add(-pendingExp))
    718 			if err != nil {
    719 				m.Logf(LogDebug, "Error purging stale registrations: `%v'.\n", err)
    720 			}
    721 			m.Logf(LogInfo, "Cleaned up %d stale pending registrations.\n", num)
    722 			time.Sleep(pendingExp)
    723 		}
    724 	}()
    725 
    726 	m.Merchant = cfg.Merchant
    727 	if !monthlyFee.IsZero() {
    728 		merchConfig, err := m.Merchant.GetConfig()
    729 		if err != nil {
    730 			fmt.Printf("Failed to get merchant config: %v", err)
    731 			os.Exit(1)
    732 		}
    733 		currencySpec, currencySupported := merchConfig.Currencies[monthlyFee.Currency]
    734 		for !currencySupported {
    735 			fmt.Printf("Currency `%s' not supported by merchant!\n", monthlyFee.Currency)
    736 			os.Exit(1)
    737 		}
    738 		m.CurrencySpec = currencySpec
    739 	}
    740 	m.setupHandlers()
    741 }