taler-mailbox

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

mailbox.go (27836B)


      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 // configResponse returns the service configuration.
    207 //
    208 // @Summary     Get service configuration
    209 // @Description Returns service metadata including fees, message size limits, and delivery period.
    210 // @Tags        config
    211 // @Produce     json
    212 // @Success     200 {object} VersionResponse
    213 // @Router      /config [get]
    214 func (m *Mailbox) configResponse(w http.ResponseWriter, r *http.Request) {
    215 	dp, err := m.Cfg.Ini.GetDuration("mailbox", "delivery_period", 3*24*time.Hour)
    216 	if err != nil {
    217 		log.Fatal(err)
    218 	}
    219 	cfg := VersionResponse{
    220 		Version:               m.Cfg.LibtoolVersion,
    221 		Name:                  "taler-mailbox",
    222 		MessageBodyBytes:      m.MessageBodyBytes,
    223 		MessageResponseLimit:  m.MessageResponseLimit,
    224 		MonthlyFee:            m.MonthlyFee.String(),
    225 		RegistrationUpdateFee: m.RegistrationUpdateFee.String(),
    226 		DeliveryPeriod:        RelativeTime{Microseconds: uint64(dp.Microseconds())},
    227 	}
    228 	w.Header().Set("Content-Type", "application/json")
    229 	response, _ := json.Marshal(cfg)
    230 	w.Write(response)
    231 }
    232 
    233 // getMessagesResponse retrieves pending messages for a mailbox.
    234 //
    235 // @Summary     Retrieve messages
    236 // @Description Returns up to MessageResponseLimit encrypted message bodies for the given mailbox.
    237 // @Description The ETag response header contains the serial number of the first message.
    238 // @Tags        mailbox
    239 // @Produce     application/octet-stream
    240 // @Param       h_mailbox path   string true "SHA-512 hash of the mailbox signing key (Crockford base32)"
    241 // @Success     200       "One or more message bodies concatenated"
    242 // @Success     204       "No messages available"
    243 // @Failure     404       "Mailbox not found"
    244 // @Router      /{h_mailbox} [get]
    245 func (m *Mailbox) getMessagesResponse(w http.ResponseWriter, r *http.Request) {
    246 	vars := mux.Vars(r)
    247 	//to, toSet := vars["timeout_ms"]
    248 	var entries []InboxEntry
    249 	// FIXME rate limit
    250 	// FIXME timeout
    251 	// FIXME possibly limit results here
    252 	m.checkPendingMailboxRegistrationUpdates(vars["h_mailbox"])
    253 	entries, err := m.DB.GetMessages(vars["h_mailbox"], int(m.MessageResponseLimit))
    254 	if err != nil {
    255 		m.Logf(LogError, "Error getting messages: %v", err)
    256 		w.WriteHeader(http.StatusNotFound)
    257 		return
    258 	}
    259 	if len(entries) == 0 {
    260 		w.WriteHeader(http.StatusNoContent)
    261 		return
    262 	}
    263 	// Add ETag of first message ID
    264 	etag := entries[0].Serial
    265 	w.Header().Add("ETag", fmt.Sprintf("%d", etag))
    266 	for _, entry := range entries {
    267 		w.Write(entry.Body)
    268 	}
    269 }
    270 
    271 // sendMessageResponse delivers a message to a mailbox.
    272 //
    273 // @Summary     Send a message
    274 // @Description Stores an encrypted message body for the given mailbox. The body must be
    275 // @Description exactly MessageBodyBytes in size.
    276 // @Tags        mailbox
    277 // @Accept      application/octet-stream
    278 // @Param       h_mailbox path   string true "SHA-512 hash of the mailbox signing key (Crockford base32)"
    279 // @Param       body       body   string true "Encrypted message body (fixed size)"
    280 // @Success     204        "Message stored"
    281 // @Success     304        "Identical message already stored"
    282 // @Failure     400        "Missing or wrong-size body"
    283 // @Failure     402        "Payment required (free quota exceeded)"
    284 // @Failure     500
    285 // @Router      /{h_mailbox} [post]
    286 func (m *Mailbox) sendMessageResponse(w http.ResponseWriter, r *http.Request) {
    287 	vars := mux.Vars(r)
    288 	var entry InboxEntry
    289 	if r.Body == nil {
    290 		http.Error(w, "No request body", http.StatusBadRequest)
    291 		return
    292 	}
    293 	if r.ContentLength != m.MessageBodyBytes {
    294 		http.Error(w, "Wrong message size", http.StatusBadRequest)
    295 		return
    296 	}
    297 	body, err := io.ReadAll(r.Body)
    298 	if err != nil {
    299 		log.Printf("%v", err)
    300 		http.Error(w, "Cannot read body", http.StatusBadRequest)
    301 		return
    302 	}
    303 	if !m.MessageFee.IsZero() {
    304 		var count int64
    305 		count, err = m.DB.GetMessagesCount(vars["h_mailbox"])
    306 		if nil != err {
    307 			m.Logf(LogError, "Error getting messages: %v", err)
    308 			http.Error(w, "Cannot look for entries", http.StatusBadRequest)
    309 			return
    310 		}
    311 		if count >= int64(m.FreeMessageQuota) {
    312 			w.WriteHeader(http.StatusPaymentRequired)
    313 			//w.Header().Set("Taler", payto) FIXME generate payto
    314 			return
    315 		}
    316 	}
    317 	m.checkPendingMailboxRegistrationUpdates(vars["h_mailbox"])
    318 	err = m.DB.GetInboxEntryBySigningKeyAndBody(&entry, vars["h_mailbox"], body)
    319 	if err == nil {
    320 		w.WriteHeader(http.StatusNotModified)
    321 		return
    322 	}
    323 	entry.HashedSigningKey = vars["h_mailbox"]
    324 	entry.Body = body
    325 	err = m.DB.InsertInboxEntry(&entry)
    326 	if err != nil {
    327 		m.Logf(LogError, "Error storing message: %v", err)
    328 		w.WriteHeader(http.StatusInternalServerError)
    329 		return
    330 	}
    331 	w.WriteHeader(http.StatusNoContent)
    332 }
    333 
    334 // getKeysResponse returns the public key metadata for a registered mailbox.
    335 //
    336 // @Summary     Get mailbox info
    337 // @Description Returns the signing and encryption key metadata for the given mailbox.
    338 // @Tags        mailbox
    339 // @Produce     json
    340 // @Param       h_mailbox path   string true "SHA-512 hash of the mailbox signing key (Crockford base32)"
    341 // @Success     200        {object} MailboxMetadata
    342 // @Failure     404        "Mailbox not found or expired"
    343 // @Router      /info/{h_mailbox} [get]
    344 func (m *Mailbox) getKeysResponse(w http.ResponseWriter, r *http.Request) {
    345 	vars := mux.Vars(r)
    346 	var keyEntry MailboxMetadata
    347 	m.checkPendingMailboxRegistrationUpdates(vars["h_mailbox"])
    348 	err := m.DB.GetMailboxMetadataBySigningKey(&keyEntry, vars["h_mailbox"])
    349 	if err != nil {
    350 		m.Logf(LogError, "Error finding mailbox: %v", err)
    351 		w.WriteHeader(http.StatusNotFound)
    352 		return
    353 	}
    354 	m.Logf(LogDebug, "entry expires at %d, have %d", keyEntry.Expiration, time.Now().Unix())
    355 	if int64(keyEntry.Expiration.Seconds) < int64(time.Now().Unix()) {
    356 		w.WriteHeader(http.StatusNotFound)
    357 		return
    358 	}
    359 	response, _ := json.Marshal(keyEntry)
    360 	w.Write(response)
    361 }
    362 
    363 func (m *Mailbox) validateRegistrationSignature(msg MailboxRegistrationRequest) error {
    364 	var expNbo [8]byte
    365 	var signedMsg [72]byte
    366 	encPk, err := util.Base32CrockfordDecode(msg.MailboxMetadata.EncryptionKey, 32)
    367 	if err != nil {
    368 		return fmt.Errorf("unable to decode encryption key")
    369 	}
    370 	pkey, err := util.Base32CrockfordDecode(msg.MailboxMetadata.SigningKey, 32)
    371 	if err != nil {
    372 		return fmt.Errorf("unable to decode pubkey")
    373 	}
    374 	pk := ed25519.PublicKey(pkey)
    375 	sig, err := util.Base32CrockfordDecode(msg.Signature, 64)
    376 	if nil != err {
    377 		return fmt.Errorf("unable to decode signature")
    378 	}
    379 	binary.BigEndian.PutUint64(expNbo[:], msg.MailboxMetadata.Expiration.Seconds)
    380 	size := signedMsg[0:4]
    381 	binary.BigEndian.PutUint32(size, 64+4+4)
    382 	purp := signedMsg[4:8]
    383 	binary.BigEndian.PutUint32(purp, gana.TalerSignaturePurposeMailboxRegister)
    384 	h := sha512.New()
    385 	h.Write([]byte(msg.MailboxMetadata.EncryptionKeyType)) // Currently always X25519
    386 	h.Write(encPk)
    387 	h.Write(expNbo[:])
    388 	copy(signedMsg[8:], h.Sum(nil))
    389 	if !ed25519.Verify(pk, signedMsg[0:], sig) {
    390 		return fmt.Errorf("signature invalid")
    391 	}
    392 	return nil
    393 }
    394 
    395 // Check if this is a non-zero, positive amount
    396 func calculateCost(sliceCostAmount string, fixedCostAmount string, howLong time.Duration, sliceDuration time.Duration) (*talerutil.Amount, error) {
    397 	sliceCount := int(float64(howLong.Microseconds()) / float64(sliceDuration.Microseconds()))
    398 	sliceCost, err := talerutil.ParseAmount(sliceCostAmount)
    399 	if nil != err {
    400 		return nil, err
    401 	}
    402 	fixedCost, err := talerutil.ParseAmount(fixedCostAmount)
    403 	if nil != err {
    404 		return nil, err
    405 	}
    406 	sum := &talerutil.Amount{
    407 		Currency: sliceCost.Currency,
    408 		Value:    0,
    409 		Fraction: 0,
    410 	}
    411 	for range sliceCount {
    412 		sum, err = sum.Add(*sliceCost)
    413 		if nil != err {
    414 			return nil, err
    415 		}
    416 	}
    417 	sum, err = sum.Add(*fixedCost)
    418 	if nil != err {
    419 		return nil, err
    420 	}
    421 	return sum, nil
    422 }
    423 
    424 // registerMailboxResponse registers or updates a mailbox.
    425 //
    426 // @Summary     Register or update mailbox
    427 // @Description Registers a new mailbox or updates the keys/expiration of an existing one.
    428 // @Description A valid EdDSA signature over the key material must be provided.
    429 // @Tags        mailbox
    430 // @Accept      json
    431 // @Param       body body MailboxRegistrationRequest true "Registration request"
    432 // @Success     204  "Registration confirmed"
    433 // @Success     304  "Nothing changed"
    434 // @Failure     400  "Invalid request body or signature"
    435 // @Failure     402  "Payment required"
    436 // @Failure     500
    437 // @Router      /register [post]
    438 func (m *Mailbox) registerMailboxResponse(w http.ResponseWriter, r *http.Request) {
    439 	var msg MailboxRegistrationRequest
    440 	var pendingRegistration PendingMailboxRegistration
    441 	var registrationEntry MailboxMetadata
    442 	if r.Body == nil {
    443 		m.Logf(LogError, "no request body")
    444 		http.Error(w, "No request body", http.StatusBadRequest)
    445 		return
    446 	}
    447 	err := json.NewDecoder(r.Body).Decode(&msg)
    448 	if err != nil {
    449 		http.Error(w, "Malformed request body", http.StatusBadRequest)
    450 		return
    451 	}
    452 	pkey, err := util.Base32CrockfordDecode(msg.MailboxMetadata.SigningKey, 32)
    453 	if err != nil {
    454 		http.Error(w, "Public key invalid", http.StatusBadRequest)
    455 		return
    456 	}
    457 	pk := ed25519.PublicKey(pkey)
    458 	err = m.validateRegistrationSignature(msg)
    459 	if nil != err {
    460 		http.Error(w, "Signature verification failed", http.StatusBadRequest)
    461 		return
    462 	}
    463 	h := sha512.New()
    464 	h.Write(pkey)
    465 	hMailbox := util.Base32CrockfordEncode(h.Sum(nil))
    466 	pendingRegistration.HashedSigningKey = hMailbox
    467 	// Round to the nearest multiple of a month
    468 	reqExpiration := time.Unix(int64(msg.MailboxMetadata.Expiration.Seconds), 0)
    469 	now := time.Now()
    470 	reqDuration := reqExpiration.Sub(now).Round(monthDuration)
    471 	err = m.DB.GetMailboxMetadataBySigningKey(&registrationEntry, hMailbox)
    472 	if err == nil {
    473 		// This probably means the registration is modified or extended or both
    474 		entryModified := (registrationEntry.EncryptionKey != msg.MailboxMetadata.EncryptionKey)
    475 		// At least one MonthlyFee
    476 		if reqDuration.Microseconds() == 0 && !entryModified {
    477 			// Nothing changed. Return validity
    478 			w.WriteHeader(http.StatusNotModified)
    479 			return
    480 		}
    481 	} else {
    482 		// Entry does not yet exist, add but immediately expire it
    483 		registrationEntry = msg.MailboxMetadata
    484 		registrationEntry.Expiration.Seconds = uint64(time.Now().Unix() - 1)
    485 		hAddr := sha512.New()
    486 		hAddr.Write(pk)
    487 		registrationEntry.HashedSigningKey = util.Base32CrockfordEncode(hAddr.Sum(nil))
    488 		err = m.DB.InsertMailboxRegistration(&registrationEntry)
    489 		if nil != err {
    490 			m.Logf(LogError, "%v\n", err)
    491 			w.WriteHeader(http.StatusInternalServerError)
    492 			return
    493 		}
    494 	}
    495 	err = m.DB.GetPendingMailboxRegistrationBySigningKey(&pendingRegistration, hMailbox)
    496 	pendingRegistrationExists := (nil == err)
    497 	if !pendingRegistrationExists {
    498 		pendingRegistration.HashedSigningKey = hMailbox
    499 		pendingRegistration.Duration = reqDuration.Microseconds()
    500 		err = m.DB.InsertPendingMailboxRegistration(&pendingRegistration)
    501 		if nil != err {
    502 			m.Logf(LogError, "Error inserting pending registration: %v\n", err)
    503 			w.WriteHeader(http.StatusInternalServerError)
    504 			return
    505 		}
    506 		err = m.DB.GetPendingMailboxRegistrationBySigningKey(&pendingRegistration, hMailbox)
    507 		if nil != err {
    508 			m.Logf(LogError, "Error getting pending registration: %v\n", err)
    509 			w.WriteHeader(http.StatusInternalServerError)
    510 			return
    511 		}
    512 	}
    513 	// At least the update fee needs to be paid
    514 	cost, err := calculateCost(m.MonthlyFee.String(),
    515 		m.RegistrationUpdateFee.String(),
    516 		reqDuration,
    517 		monthDuration)
    518 	if err != nil {
    519 		m.Logf(LogError, "Error calculating cost: %v\n", err)
    520 		w.WriteHeader(http.StatusInternalServerError)
    521 		return
    522 	}
    523 	if !cost.IsZero() {
    524 		if len(pendingRegistration.OrderID) == 0 {
    525 			// Add new order
    526 			orderID, newOrderErr := m.Merchant.AddNewOrder(*cost, "Mailbox registration", m.BaseURL)
    527 			if newOrderErr != nil {
    528 				m.Logf(LogError, "Error adding order: %v", newOrderErr)
    529 				w.WriteHeader(http.StatusInternalServerError)
    530 				return
    531 			}
    532 			m.Logf(LogDebug, "New order ID %s for pending registration for %s", orderID, pendingRegistration.HashedSigningKey)
    533 			pendingRegistration.OrderID = orderID
    534 		}
    535 		// Check if order paid.
    536 		// FIXME: Remember that it was activated and paid
    537 		// FIXME: We probably need to handle the return code here (see gns registrar for how)
    538 		_, _, payto, paytoErr := m.Merchant.IsOrderPaid(pendingRegistration.OrderID)
    539 		if paytoErr != nil {
    540 			fmt.Println(paytoErr)
    541 			w.WriteHeader(http.StatusInternalServerError)
    542 			m.Logf(LogError, "Error checking if order is paid: %s\n", paytoErr.Error())
    543 			return
    544 		}
    545 		if len(payto) != 0 {
    546 			err = m.DB.UpdatePendingMailboxRegistrationOrderId(&pendingRegistration)
    547 			if err != nil {
    548 				m.Logf(LogError, "Error updating pending registration: %v\n", err)
    549 				w.WriteHeader(http.StatusInternalServerError)
    550 				return
    551 			}
    552 			w.WriteHeader(http.StatusPaymentRequired)
    553 			w.Header().Set("Taler", payto)
    554 			return
    555 		}
    556 	}
    557 	// Update expiration time of registration.
    558 	registrationEntry.Expiration.Seconds += uint64(reqDuration.Seconds())
    559 	_, err = m.DB.DeletePendingRegistration(&pendingRegistration)
    560 	if nil != err {
    561 		m.Logf(LogError, "Error deleting pending registration: %v\n", err)
    562 		w.WriteHeader(http.StatusInternalServerError)
    563 		return
    564 	}
    565 	err = m.DB.UpdateMailboxExpiration(&registrationEntry)
    566 	if nil != err {
    567 		m.Logf(LogError, "Error updating mailbox registration: %v\n", err)
    568 		w.WriteHeader(http.StatusInternalServerError)
    569 		return
    570 	}
    571 	w.WriteHeader(http.StatusNoContent)
    572 }
    573 
    574 func (m *Mailbox) checkPendingMailboxRegistrationUpdates(hMailbox string) {
    575 	var pendingEntry PendingMailboxRegistration
    576 	var registrationEntry MailboxMetadata
    577 	err := m.DB.GetPendingMailboxRegistrationBySigningKey(&pendingEntry, hMailbox)
    578 	if err != nil {
    579 		return
    580 	}
    581 	m.Logf(LogDebug, "Found pending registration for %s, OrderID: %s", hMailbox, pendingEntry.OrderID)
    582 	rc, orderStatus, _, paytoErr := m.Merchant.IsOrderPaid(pendingEntry.OrderID)
    583 	if nil != paytoErr {
    584 		if rc == http.StatusNotFound {
    585 			m.Logf(LogInfo, "Registration order for `%s' not found, removing\n", hMailbox)
    586 		}
    587 		_, err = m.DB.DeletePendingRegistration(&pendingEntry)
    588 		if nil != err {
    589 			m.Logf(LogInfo, "Error deleting pending registration: %v\n", err)
    590 		}
    591 		return
    592 	}
    593 	m.Logf(LogDebug, "Order status for %s is %s", pendingEntry.HashedSigningKey, orderStatus)
    594 	if merchant.OrderPaid == orderStatus {
    595 		m.Logf(LogDebug, "Order for %v appears to be paid", pendingEntry)
    596 		err = m.DB.GetMailboxMetadataBySigningKey(&registrationEntry, hMailbox)
    597 		if err == nil {
    598 			m.Logf(LogDebug, "Adding %d seconds to entry expiration", pendingEntry.Duration)
    599 			registrationEntry.Expiration.Seconds += uint64(pendingEntry.Duration)
    600 			err = m.DB.UpdateMailboxExpiration(&registrationEntry)
    601 			if nil != err {
    602 				m.Logf(LogInfo, "Error updating mailbox expiration: %v\n", err)
    603 			}
    604 			_, err = m.DB.DeletePendingRegistration(&pendingEntry)
    605 			if nil != err {
    606 				m.Logf(LogInfo, "Error deleting pending registration: %v\n", err)
    607 			}
    608 		}
    609 		return
    610 	}
    611 }
    612 
    613 // deleteMessagesResponse deletes messages from a mailbox.
    614 //
    615 // @Summary     Delete messages
    616 // @Description Deletes one or more messages starting from the serial given in the If-Match header.
    617 // @Description Requires a valid EdDSA signature in the Taler-Mailbox-Delete-Signature header.
    618 // @Tags        mailbox
    619 // @Param       mailbox    path   string true  "Crockford base32-encoded EdDSA public key of the mailbox"
    620 // @Param       count      query  int    false "Number of messages to delete (default: 1)"
    621 // @Param       If-Match   header string true  "Serial number of the first message to delete"
    622 // @Param       Taler-Mailbox-Delete-Signature header string true "EdDSA signature authorising the deletion"
    623 // @Success     204 "Messages deleted"
    624 // @Failure     400 "Missing or malformed headers/parameters"
    625 // @Failure     403 "Signature invalid"
    626 // @Failure     404 "Message with given serial not found"
    627 // @Failure     500
    628 // @Router      /{mailbox} [delete]
    629 func (m *Mailbox) deleteMessagesResponse(w http.ResponseWriter, r *http.Request) {
    630 	vars := mux.Vars(r)
    631 	etagHeader := r.Header.Get("If-Match")
    632 	if etagHeader == "" {
    633 		http.Error(w, "If-Match header missing", 400)
    634 		return
    635 	}
    636 	if strings.Contains(etagHeader, ",") {
    637 		http.Error(w, "If-Match contains multiple values", 400)
    638 		return
    639 	}
    640 	expectedETag, err := strconv.Atoi(etagHeader)
    641 	if err != nil {
    642 		http.Error(w, "If-Match contains malformed etag number", 400)
    643 		return
    644 	}
    645 	pkey, err := util.Base32CrockfordDecode(vars["mailbox"], 32)
    646 	if err != nil {
    647 		w.WriteHeader(http.StatusBadRequest)
    648 		return
    649 	}
    650 	count := 1
    651 	countStr := r.URL.Query().Get("count")
    652 	if len(countStr) > 0 {
    653 		count, err = strconv.Atoi(countStr)
    654 		if err != nil {
    655 			http.Error(w, "Malformed count parameter", http.StatusBadRequest)
    656 			w.WriteHeader(http.StatusBadRequest)
    657 			return
    658 		}
    659 	}
    660 	headerSig := r.Header["Taler-Mailbox-Delete-Signature"]
    661 	if nil == headerSig {
    662 		http.Error(w, "Missing signature", http.StatusBadRequest)
    663 		return
    664 	}
    665 	pk := ed25519.PublicKey(pkey)
    666 	sig, err := util.Base32CrockfordDecode(headerSig[0], 64)
    667 	if nil != err {
    668 		w.WriteHeader(http.StatusBadRequest)
    669 		return
    670 	}
    671 	h := sha512.New()
    672 	h.Write(pkey)
    673 	hMailbox := util.Base32CrockfordEncode(h.Sum(nil))
    674 	m.checkPendingMailboxRegistrationUpdates(hMailbox)
    675 	var signedMsg [4 * 4]byte
    676 	binary.BigEndian.PutUint32(signedMsg[0:4], 4*4)
    677 	binary.BigEndian.PutUint32(signedMsg[4:8], gana.TalerSignaturePurposeMailboxMessagesDelete)
    678 	binary.BigEndian.PutUint32(signedMsg[8:12], uint32(expectedETag))
    679 	binary.BigEndian.PutUint32(signedMsg[12:16], uint32(count))
    680 	if !ed25519.Verify(pk, signedMsg[0:], sig) {
    681 		w.WriteHeader(http.StatusForbidden)
    682 		return
    683 	}
    684 	// Check that expectedETag actually exists
    685 	var entry InboxEntry
    686 	err = m.DB.GetInboxEntryBySerial(&entry, hMailbox, int64(expectedETag))
    687 	if err != nil {
    688 		m.Logf(LogDebug, "Message to delete not found with ID %d", expectedETag)
    689 		w.WriteHeader(http.StatusNotFound)
    690 		return
    691 	}
    692 	m.Logf(LogError, "Deleting from entry %v up to %d messages\n", entry, count)
    693 	num, err := m.DB.DeleteInboxEntryBySerial(&entry, count)
    694 	if err != nil {
    695 		m.Logf(LogDebug, "Failed to delete messages: %v", err)
    696 		w.WriteHeader(http.StatusInternalServerError)
    697 		return
    698 	}
    699 	m.Logf(LogDebug, "Found matching ID (serial: %d), deleted %d messages", entry.Serial, num)
    700 	w.WriteHeader(http.StatusNoContent)
    701 }
    702 
    703 func (m *Mailbox) termsResponse(w http.ResponseWriter, r *http.Request) {
    704 	termspath := m.Cfg.Ini.GetFilename("mailbox", "default_terms_path", "terms/", m.Cfg.Datahome)
    705 	tos.ServiceTermsResponse(w, r, termspath, tos.TalerTosConfig{
    706 		DefaultFileType:    m.Cfg.Ini.GetString("mailbox", "default_doc_filetype", "text/html"),
    707 		DefaultLanguage:    m.Cfg.Ini.GetString("mailbox", "default_doc_lang", "en"),
    708 		SupportedFileTypes: strings.Split(m.Cfg.Ini.GetString("mailbox", "supported_doc_filetypes", ""), " "),
    709 	})
    710 }
    711 
    712 func (m *Mailbox) privacyResponse(w http.ResponseWriter, r *http.Request) {
    713 	pppath := m.Cfg.Ini.GetFilename("mailbox", "default_pp_path", "privacy/", m.Cfg.Datahome)
    714 	tos.PrivacyPolicyResponse(w, r, pppath, tos.TalerTosConfig{
    715 		DefaultFileType:    m.Cfg.Ini.GetString("mailbox", "default_doc_filetype", "text/html"),
    716 		DefaultLanguage:    m.Cfg.Ini.GetString("mailbox", "default_doc_lang", "en"),
    717 		SupportedFileTypes: strings.Split(m.Cfg.Ini.GetString("mailbox", "supported_doc_filetypes", ""), " "),
    718 	})
    719 }
    720 
    721 func (m *Mailbox) setupHandlers() {
    722 	m.Router = mux.NewRouter().StrictSlash(true)
    723 
    724 	/* ToS API */
    725 	m.Router.HandleFunc("/terms", m.termsResponse).Methods("GET")
    726 	m.Router.HandleFunc("/privacy", m.privacyResponse).Methods("GET")
    727 
    728 	/* Config API */
    729 	m.Router.HandleFunc("/config", m.configResponse).Methods("GET")
    730 
    731 	/* Mailbox API */
    732 	m.Router.HandleFunc("/register", m.registerMailboxResponse).Methods("POST")
    733 	m.Router.HandleFunc("/info/{h_mailbox}", m.getKeysResponse).Methods("GET")
    734 	m.Router.HandleFunc("/{h_mailbox}", m.sendMessageResponse).Methods("POST")
    735 	m.Router.HandleFunc("/{h_mailbox}", m.getMessagesResponse).Methods("GET")
    736 	m.Router.HandleFunc("/{mailbox}", m.deleteMessagesResponse).Methods("DELETE")
    737 }
    738 
    739 func (m *Mailbox) Logf(loglevel LogLevel, fmt string, args ...any) {
    740 	if loglevel < m.Cfg.Loglevel {
    741 		return
    742 	}
    743 	m.Logger.SetPrefix("taler-mailbox - " + LoglevelStringMap[loglevel] + " ")
    744 	m.Logger.Printf(fmt, args...)
    745 }
    746 
    747 // Initialize the Mailbox instance with cfgfile
    748 func (m *Mailbox) Initialize(cfg MailboxConfig) {
    749 	m.Cfg = cfg
    750 	m.Logger = log.New(os.Stdout, "taler-mailbox:", log.LstdFlags)
    751 	m.BaseURL = cfg.Ini.GetString("mailbox", "base_url", "https://example.com")
    752 	m.MessageBodyBytes = cfg.Ini.GetInt64("mailbox", "message_body_bytes", 256)
    753 	m.MessageResponseLimit = uint64(cfg.Ini.GetInt64("mailbox", "message_response_limit", 50))
    754 	monthlyFee, err := cfg.Ini.GetAmount("mailbox", "monthly_fee", &talerutil.Amount{})
    755 	if err != nil {
    756 		fmt.Printf("Failed to parse monthly fee: %v", err)
    757 		os.Exit(1)
    758 	}
    759 	m.MonthlyFee = monthlyFee
    760 	updateFee, err := cfg.Ini.GetAmount("mailbox", "registration_update_fee", &talerutil.Amount{})
    761 	if err != nil {
    762 		fmt.Printf("Failed to parse update fee: %v", err)
    763 		os.Exit(1)
    764 	}
    765 	m.RegistrationUpdateFee = updateFee
    766 	messageFee, err := cfg.Ini.GetAmount("mailbox", "message_fee", &talerutil.Amount{})
    767 	if err != nil {
    768 		fmt.Printf("Failed to parse message fee: %v", err)
    769 		os.Exit(1)
    770 	}
    771 	m.MessageFee = messageFee
    772 	m.FreeMessageQuota = uint64(cfg.Ini.GetInt64("mailbox", "free_message_quota", 0))
    773 	m.DB = cfg.DB
    774 	go func() {
    775 		for {
    776 			num, err := m.DB.DeleteStaleRegistrations(time.Now())
    777 			if err != nil {
    778 				m.Logf(LogDebug, "Error purging stale registrations: `%v'.\n", err)
    779 			}
    780 			m.Logf(LogInfo, "Cleaned up %d stale registrations.\n", num)
    781 			time.Sleep(time.Hour * 24)
    782 		}
    783 	}()
    784 	// Clean up pending
    785 	pendingExp, err := cfg.Ini.GetDuration("mailbox", "pending_registration_expiration", 24*time.Hour)
    786 	if err != nil {
    787 		fmt.Printf("Failed to parse pending registration expiration: %v", err)
    788 		os.Exit(1)
    789 	}
    790 	go func() {
    791 		for {
    792 			num, err := m.DB.DeleteStalePendingRegistrations(time.Now().Add(-pendingExp))
    793 			if err != nil {
    794 				m.Logf(LogDebug, "Error purging stale registrations: `%v'.\n", err)
    795 			}
    796 			m.Logf(LogInfo, "Cleaned up %d stale pending registrations.\n", num)
    797 			time.Sleep(pendingExp)
    798 		}
    799 	}()
    800 
    801 	m.Merchant = cfg.Merchant
    802 	if !monthlyFee.IsZero() {
    803 		merchConfig, err := m.Merchant.GetConfig()
    804 		if err != nil {
    805 			fmt.Printf("Failed to get merchant config: %v", err)
    806 			os.Exit(1)
    807 		}
    808 		currencySpec, currencySupported := merchConfig.Currencies[monthlyFee.Currency]
    809 		for !currencySupported {
    810 			fmt.Printf("Currency `%s' not supported by merchant!\n", monthlyFee.Currency)
    811 			os.Exit(1)
    812 		}
    813 		m.CurrencySpec = currencySpec
    814 	}
    815 	m.setupHandlers()
    816 }