taler-mailbox

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

commit 86b9901df6fdde71e29c6aef91639f7d4311c942
parent f44ce98e7221494ec966372c14e6c955cf14cc26
Author: Yannick Rehberger <yr@ityreh.de>
Date:   Tue, 21 Apr 2026 19:58:30 +0200

add openapi spec

Diffstat:
M.gitignore | 1+
MMakefile.in | 5++++-
MREADME.md | 10++++++++++
Mcmd/mailbox-server/main.go | 6++++++
Mgo.sum | 52++++++++++++++++++++++++++++++++++++++++++++++++----
Mpkg/rest/mailbox.go | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 144 insertions(+), 5 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,3 +1,4 @@ taler-mailbox config.status Makefile +swagger.json diff --git a/Makefile.in b/Makefile.in @@ -54,4 +54,7 @@ gana: dist: git archive --format=tar.gz -o taler-mailbox-${GITVER}.tar.gz --prefix=taler-mailbox-${GITVER}/ HEAD -.PHONY: all gana format check uninstall install server tools dist +openapi: + swag init -g main.go --dir ./cmd/mailbox-server,./pkg/rest --outputTypes json --output . -v3.1 + +.PHONY: all gana format check uninstall install server tools dist openapi diff --git a/README.md b/README.md @@ -25,6 +25,16 @@ Make sure that the database (Postgres) `taler-mailbox-test` exists and your user $ make format ``` +# Generate OpenAPI Spec + +You can generate an openapi spec from code to `./swagger.json`: + +``` +$ go install github.com/swaggo/swag/v2/cmd/swag@latest +$ ./configure --prefix=PREFIX +$ make openapi +``` + # Funding This project is funded through [NGI TALER Fund](https://nlnet.nl/taler), a fund established by [NLnet](https://nlnet.nl) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu) program. Learn more at the [NLnet project page](https://nlnet.nl/project/TALER-LookupService). diff --git a/cmd/mailbox-server/main.go b/cmd/mailbox-server/main.go @@ -16,6 +16,12 @@ // // SPDX-License-Identifier: AGPL3.0-or-later +// @title Taler Mailbox API +// @description The Taler Mailbox service provides encrypted message delivery to wallets identified by their public key. +// @contact.url https://taler.net +// @license.name AGPL-3.0-or-later +// @license.url https://www.gnu.org/licenses/agpl-3.0.html +// @BasePath / package main import ( diff --git a/go.sum b/go.sum @@ -1,12 +1,33 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= -github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo= -github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -17,21 +38,44 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= rsc.io/getopt v0.0.0-20170811000552-20be20937449 h1:UukjJOsjQH0DIuyyrcod6CXHS6cdaMMuJmrt+SN1j4A= diff --git a/pkg/rest/mailbox.go b/pkg/rest/mailbox.go @@ -203,6 +203,14 @@ type MailboxRateLimitedResponse struct { Hint string `json:"hint"` } +// configResponse returns the service configuration. +// +// @Summary Get service configuration +// @Description Returns service metadata including fees, message size limits, and delivery period. +// @Tags config +// @Produce json +// @Success 200 {object} VersionResponse +// @Router /config [get] func (m *Mailbox) configResponse(w http.ResponseWriter, r *http.Request) { dp, err := m.Cfg.Ini.GetDuration("mailbox", "delivery_period", 3*24*time.Hour) if err != nil { @@ -222,6 +230,18 @@ func (m *Mailbox) configResponse(w http.ResponseWriter, r *http.Request) { w.Write(response) } +// getMessagesResponse retrieves pending messages for a mailbox. +// +// @Summary Retrieve messages +// @Description Returns up to MessageResponseLimit encrypted message bodies for the given mailbox. +// @Description The ETag response header contains the serial number of the first message. +// @Tags mailbox +// @Produce application/octet-stream +// @Param h_mailbox path string true "SHA-512 hash of the mailbox signing key (Crockford base32)" +// @Success 200 "One or more message bodies concatenated" +// @Success 204 "No messages available" +// @Failure 404 "Mailbox not found" +// @Router /{h_mailbox} [get] func (m *Mailbox) getMessagesResponse(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) //to, toSet := vars["timeout_ms"] @@ -248,6 +268,21 @@ func (m *Mailbox) getMessagesResponse(w http.ResponseWriter, r *http.Request) { } } +// sendMessageResponse delivers a message to a mailbox. +// +// @Summary Send a message +// @Description Stores an encrypted message body for the given mailbox. The body must be +// @Description exactly MessageBodyBytes in size. +// @Tags mailbox +// @Accept application/octet-stream +// @Param h_mailbox path string true "SHA-512 hash of the mailbox signing key (Crockford base32)" +// @Param body body string true "Encrypted message body (fixed size)" +// @Success 204 "Message stored" +// @Success 304 "Identical message already stored" +// @Failure 400 "Missing or wrong-size body" +// @Failure 402 "Payment required (free quota exceeded)" +// @Failure 500 +// @Router /{h_mailbox} [post] func (m *Mailbox) sendMessageResponse(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) var entry InboxEntry @@ -296,6 +331,16 @@ func (m *Mailbox) sendMessageResponse(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } +// getKeysResponse returns the public key metadata for a registered mailbox. +// +// @Summary Get mailbox info +// @Description Returns the signing and encryption key metadata for the given mailbox. +// @Tags mailbox +// @Produce json +// @Param h_mailbox path string true "SHA-512 hash of the mailbox signing key (Crockford base32)" +// @Success 200 {object} MailboxMetadata +// @Failure 404 "Mailbox not found or expired" +// @Router /info/{h_mailbox} [get] func (m *Mailbox) getKeysResponse(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) var keyEntry MailboxMetadata @@ -376,6 +421,20 @@ func calculateCost(sliceCostAmount string, fixedCostAmount string, howLong time. return sum, nil } +// registerMailboxResponse registers or updates a mailbox. +// +// @Summary Register or update mailbox +// @Description Registers a new mailbox or updates the keys/expiration of an existing one. +// @Description A valid EdDSA signature over the key material must be provided. +// @Tags mailbox +// @Accept json +// @Param body body MailboxRegistrationRequest true "Registration request" +// @Success 204 "Registration confirmed" +// @Success 304 "Nothing changed" +// @Failure 400 "Invalid request body or signature" +// @Failure 402 "Payment required" +// @Failure 500 +// @Router /register [post] func (m *Mailbox) registerMailboxResponse(w http.ResponseWriter, r *http.Request) { var msg MailboxRegistrationRequest var pendingRegistration PendingMailboxRegistration @@ -551,6 +610,22 @@ func (m *Mailbox) checkPendingMailboxRegistrationUpdates(hMailbox string) { } } +// deleteMessagesResponse deletes messages from a mailbox. +// +// @Summary Delete messages +// @Description Deletes one or more messages starting from the serial given in the If-Match header. +// @Description Requires a valid EdDSA signature in the Taler-Mailbox-Delete-Signature header. +// @Tags mailbox +// @Param mailbox path string true "Crockford base32-encoded EdDSA public key of the mailbox" +// @Param count query int false "Number of messages to delete (default: 1)" +// @Param If-Match header string true "Serial number of the first message to delete" +// @Param Taler-Mailbox-Delete-Signature header string true "EdDSA signature authorising the deletion" +// @Success 204 "Messages deleted" +// @Failure 400 "Missing or malformed headers/parameters" +// @Failure 403 "Signature invalid" +// @Failure 404 "Message with given serial not found" +// @Failure 500 +// @Router /{mailbox} [delete] func (m *Mailbox) deleteMessagesResponse(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) etagHeader := r.Header.Get("If-Match")