taler-mailbox

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

mailbox.go (24886B)


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