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(®istrationEntry, "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(®istrationEntry).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(®istrationEntry).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(®istrationEntry, "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(®istrationEntry) 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 }