Merge fix/bimi-per-domain-urls: each domain's BIMI logo under its own URL, and the DNS publisher wired
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
aea4c1ee5e
7 files changed
+173
-21
No files matched your search
@@ -226,7 +226,12 @@ everyone who sends from it. Publishing one needs three things, and the console
|
|||||||
file, DMARC, the BIMI record in DNS, the public URL of the logo, the
|
file, DMARC, the BIMI record in DNS, the public URL of the logo, the
|
||||||
certificate and its public URL, and names the one that is wrong.
|
certificate and its public URL, and names the one that is wrong.
|
||||||
|
|
||||||
The logo is served from `https://<mail host>/.well-known/bimi/default.svg`.
|
The logo is served from `https://<mail host>/.well-known/bimi/<domain>/default.svg`
|
||||||
|
and the certificate from `.../<domain>/vmc.pem`: one mail host, a path per
|
||||||
|
domain, so every domain the server hosts can publish its own. (The earlier
|
||||||
|
`/.well-known/bimi/default.svg` chose the domain from the host name, which could
|
||||||
|
name only the primary domain; every other domain's record pointed at a 404. It
|
||||||
|
still answers, for records already published.)
|
||||||
|
|
||||||
**The DNS record follows the picture.** With a Cloudflare token configured, the
|
**The DNS record follows the picture.** With a Cloudflare token configured, the
|
||||||
`default._bimi.<domain>` TXT record is part of what the DNS reconciler
|
`default._bimi.<domain>` TXT record is part of what the DNS reconciler
|
||||||
|
|||||||
@@ -22,15 +22,15 @@ func bimiPublication(ctx context.Context, repo *postgres.BimiRepository, cfg con
|
|||||||
// Unknown is not "no logo": returning an empty publication here would
|
// Unknown is not "no logo": returning an empty publication here would
|
||||||
// have the sync delete the record over a database hiccup.
|
// have the sync delete the record over a database hiccup.
|
||||||
log.Printf("BIMI: could not read the logo for %s, leaving its record alone: %v", domain, err)
|
log.Printf("BIMI: could not read the logo for %s, leaving its record alone: %v", domain, err)
|
||||||
return bimi.Publication{LogoURL: httppresentation.BimiLogoURL(cfg.PrimaryMailHost)}
|
return bimi.Publication{LogoURL: httppresentation.BimiLogoURL(cfg.PrimaryMailHost, domain)}
|
||||||
}
|
}
|
||||||
if logo == nil {
|
if logo == nil {
|
||||||
return bimi.Publication{}
|
return bimi.Publication{}
|
||||||
}
|
}
|
||||||
return bimi.Publish(
|
return bimi.Publish(
|
||||||
logo.SVG, logo.VmcPEM, logo.VmcURL, domain,
|
logo.SVG, logo.VmcPEM, logo.VmcURL, domain,
|
||||||
httppresentation.BimiLogoURL(cfg.PrimaryMailHost),
|
httppresentation.BimiLogoURL(cfg.PrimaryMailHost, domain),
|
||||||
httppresentation.BimiVmcURL(cfg.PrimaryMailHost),
|
httppresentation.BimiVmcURL(cfg.PrimaryMailHost, domain),
|
||||||
time.Now(),
|
time.Now(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -53,8 +54,12 @@ type bimiAPI struct {
|
|||||||
publish func(domain string)
|
publish func(domain string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetBimiPublisher wires what happens after a logo or certificate changes.
|
// SetBimiPublisher wires what happens after a logo or certificate changes. It
|
||||||
|
// may be called before or after SetBimiAPI: it was once called before, found no
|
||||||
|
// BIMI API to attach to, and was dropped without a word, so saving a logo never
|
||||||
|
// published anything.
|
||||||
func (r *Router) SetBimiPublisher(publish func(domain string)) {
|
func (r *Router) SetBimiPublisher(publish func(domain string)) {
|
||||||
|
r.bimiPublish = publish
|
||||||
if r.bimi != nil {
|
if r.bimi != nil {
|
||||||
r.bimi.publish = publish
|
r.bimi.publish = publish
|
||||||
}
|
}
|
||||||
@@ -81,6 +86,7 @@ func (r *Router) SetBimiAPI(store BimiStore, sessionSecret, defaultDomain, mailH
|
|||||||
now: time.Now,
|
now: time.Now,
|
||||||
dns: netResolver{},
|
dns: netResolver{},
|
||||||
fetch: newBimiFetcher(),
|
fetch: newBimiFetcher(),
|
||||||
|
publish: r.bimiPublish,
|
||||||
}
|
}
|
||||||
if sessionSecret != "" {
|
if sessionSecret != "" {
|
||||||
r.bimi.sessions = NewWebSessionVerifier(sessionSecret)
|
r.bimi.sessions = NewWebSessionVerifier(sessionSecret)
|
||||||
@@ -98,12 +104,15 @@ func (r *Router) handleBimiLogo(w http.ResponseWriter, req *http.Request) {
|
|||||||
http.NotFound(w, req)
|
http.NotFound(w, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
r.serveBimiLogo(w, req, r.bimi.domainFor(req))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) serveBimiLogo(w http.ResponseWriter, req *http.Request, domain string) {
|
||||||
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
||||||
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "GET required")
|
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "GET required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domain := r.bimi.domainFor(req)
|
|
||||||
logo, err := r.bimi.store.Load(req.Context(), domain)
|
logo, err := r.bimi.store.Load(req.Context(), domain)
|
||||||
if err != nil || logo == nil {
|
if err != nil || logo == nil {
|
||||||
http.NotFound(w, req)
|
http.NotFound(w, req)
|
||||||
@@ -202,12 +211,16 @@ func (r *Router) handleBimiCertificate(w http.ResponseWriter, req *http.Request)
|
|||||||
http.NotFound(w, req)
|
http.NotFound(w, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
r.serveBimiCertificate(w, req, r.bimi.domainFor(req))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) serveBimiCertificate(w http.ResponseWriter, req *http.Request, domain string) {
|
||||||
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
||||||
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "GET required")
|
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "GET required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logo, err := r.bimi.store.Load(req.Context(), r.bimi.domainFor(req))
|
logo, err := r.bimi.store.Load(req.Context(), domain)
|
||||||
if err != nil || logo == nil || len(logo.VmcPEM) == 0 {
|
if err != nil || logo == nil || len(logo.VmcPEM) == 0 {
|
||||||
http.NotFound(w, req)
|
http.NotFound(w, req)
|
||||||
return
|
return
|
||||||
@@ -285,7 +298,7 @@ func (r *Router) handleBimiCheck(w http.ResponseWriter, req *http.Request) {
|
|||||||
// sync writes it, so "the record is wrong" means it differs from what the
|
// sync writes it, so "the record is wrong" means it differs from what the
|
||||||
// reconciler would publish.
|
// reconciler would publish.
|
||||||
publication := bimi.Publish(logo.SVG, logo.VmcPEM, logo.VmcURL, domain,
|
publication := bimi.Publish(logo.SVG, logo.VmcPEM, logo.VmcURL, domain,
|
||||||
BimiLogoURL(r.bimi.mailHost), BimiVmcURL(r.bimi.mailHost), r.bimi.now())
|
BimiLogoURL(r.bimi.mailHost, domain), BimiVmcURL(r.bimi.mailHost, domain), r.bimi.now())
|
||||||
in := bimi.Inputs{
|
in := bimi.Inputs{
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
LogoURL: publication.LogoURL,
|
LogoURL: publication.LogoURL,
|
||||||
@@ -321,7 +334,7 @@ func (a *bimiAPI) get(w http.ResponseWriter, req *http.Request, domain string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
publication := bimi.Publish(logo.SVG, logo.VmcPEM, logo.VmcURL, domain,
|
publication := bimi.Publish(logo.SVG, logo.VmcPEM, logo.VmcURL, domain,
|
||||||
BimiLogoURL(a.mailHost), BimiVmcURL(a.mailHost), a.now())
|
BimiLogoURL(a.mailHost, domain), BimiVmcURL(a.mailHost, domain), a.now())
|
||||||
writeJSON(w, http.StatusOK, map[string]any{
|
writeJSON(w, http.StatusOK, map[string]any{
|
||||||
"configured": true,
|
"configured": true,
|
||||||
"etag": logo.ETag,
|
"etag": logo.ETag,
|
||||||
@@ -333,8 +346,8 @@ func (a *bimiAPI) get(w http.ResponseWriter, req *http.Request, domain string) {
|
|||||||
"vmc_url": logo.VmcURL,
|
"vmc_url": logo.VmcURL,
|
||||||
"bytes": len(logo.SVG),
|
"bytes": len(logo.SVG),
|
||||||
"svg": string(logo.SVG),
|
"svg": string(logo.SVG),
|
||||||
"logo_url": BimiLogoURL(a.mailHost),
|
"logo_url": BimiLogoURL(a.mailHost, domain),
|
||||||
"certificate_url": BimiVmcURL(a.mailHost),
|
"certificate_url": BimiVmcURL(a.mailHost, domain),
|
||||||
"certificate": a.describeCertificate(domain, logo),
|
"certificate": a.describeCertificate(domain, logo),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -487,12 +500,45 @@ func (a *bimiAPI) put(w http.ResponseWriter, req *http.Request, domain string) {
|
|||||||
writeJSON(w, http.StatusOK, response)
|
writeJSON(w, http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BimiLogoURL is where a domain's mark is served from.
|
// domainName accepts what a mail domain can be, and nothing a path could use to
|
||||||
func BimiLogoURL(mailHost string) string {
|
// reach anything else.
|
||||||
return fmt.Sprintf("https://%s/.well-known/bimi/default.svg", mailHost)
|
var domainName = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$`)
|
||||||
|
|
||||||
|
// handleBimiPerDomain serves /.well-known/bimi/<domain>/default.svg and
|
||||||
|
// /.well-known/bimi/<domain>/vmc.pem.
|
||||||
|
//
|
||||||
|
// The legacy paths pick the domain from the host the request arrived on, which
|
||||||
|
// can name only the one domain whose mail host that is. The record a domain
|
||||||
|
// publishes names a URL on the deployment's mail host whatever the domain is,
|
||||||
|
// so every domain other than the primary one pointed at a logo that was not its
|
||||||
|
// own and got a 404.
|
||||||
|
func (r *Router) handleBimiPerDomain(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if r.bimi == nil {
|
||||||
|
http.NotFound(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parts := strings.Split(strings.TrimPrefix(req.URL.Path, "/.well-known/bimi/"), "/")
|
||||||
|
if len(parts) != 2 || !domainName.MatchString(strings.ToLower(parts[0])) {
|
||||||
|
http.NotFound(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
domain := strings.ToLower(parts[0])
|
||||||
|
switch parts[1] {
|
||||||
|
case "default.svg":
|
||||||
|
r.serveBimiLogo(w, req, domain)
|
||||||
|
case "vmc.pem":
|
||||||
|
r.serveBimiCertificate(w, req, domain)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, req)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BimiVmcURL is where the certificate held here is served from.
|
// BimiLogoURL is where a domain's mark is served from.
|
||||||
func BimiVmcURL(mailHost string) string {
|
func BimiLogoURL(mailHost, domain string) string {
|
||||||
return fmt.Sprintf("https://%s/.well-known/bimi/vmc.pem", mailHost)
|
return fmt.Sprintf("https://%s/.well-known/bimi/%s/default.svg", mailHost, strings.ToLower(domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
// BimiVmcURL is where the certificate held for a domain is served from.
|
||||||
|
func BimiVmcURL(mailHost, domain string) string {
|
||||||
|
return fmt.Sprintf("https://%s/.well-known/bimi/%s/vmc.pem", mailHost, strings.ToLower(domain))
|
||||||
}
|
}
|
||||||
@@ -92,6 +92,7 @@ func newBimiRouter(store *memoryBimiStore) *Router {
|
|||||||
}
|
}
|
||||||
r.mux.HandleFunc("/.well-known/bimi/default.svg", r.handleBimiLogo)
|
r.mux.HandleFunc("/.well-known/bimi/default.svg", r.handleBimiLogo)
|
||||||
r.mux.HandleFunc("/.well-known/bimi/vmc.pem", r.handleBimiCertificate)
|
r.mux.HandleFunc("/.well-known/bimi/vmc.pem", r.handleBimiCertificate)
|
||||||
|
r.mux.HandleFunc("/.well-known/bimi/", r.handleBimiPerDomain)
|
||||||
r.mux.HandleFunc("/api/v1/admin/bimi", r.handleBimiAdmin)
|
r.mux.HandleFunc("/api/v1/admin/bimi", r.handleBimiAdmin)
|
||||||
r.mux.HandleFunc("/api/v1/admin/bimi/certificate", r.handleBimiCertificateAdmin)
|
r.mux.HandleFunc("/api/v1/admin/bimi/certificate", r.handleBimiCertificateAdmin)
|
||||||
r.mux.HandleFunc("/api/v1/admin/bimi/check", r.handleBimiCheck)
|
r.mux.HandleFunc("/api/v1/admin/bimi/check", r.handleBimiCheck)
|
||||||
@@ -195,7 +196,7 @@ func TestBimiAcceptsTheRealCertificateAndServesIt(t *testing.T) {
|
|||||||
|
|
||||||
// The console reads it back, with the URLs the DNS record should carry
|
// The console reads it back, with the URLs the DNS record should carry
|
||||||
got := jsonBody(t, bimiCall(r, http.MethodGet, "/api/v1/admin/bimi?domain=ebay.com", "", true))
|
got := jsonBody(t, bimiCall(r, http.MethodGet, "/api/v1/admin/bimi?domain=ebay.com", "", true))
|
||||||
if got["logo_url"] != "https://mail.ebay.com/.well-known/bimi/default.svg" || got["certificate_url"] != "https://mail.ebay.com/.well-known/bimi/vmc.pem" {
|
if got["logo_url"] != "https://mail.ebay.com/.well-known/bimi/ebay.com/default.svg" || got["certificate_url"] != "https://mail.ebay.com/.well-known/bimi/ebay.com/vmc.pem" {
|
||||||
t.Errorf("urls = %v / %v", got["logo_url"], got["certificate_url"])
|
t.Errorf("urls = %v / %v", got["logo_url"], got["certificate_url"])
|
||||||
}
|
}
|
||||||
if got["certificate"].(map[string]any)["present"] != true {
|
if got["certificate"].(map[string]any)["present"] != true {
|
||||||
@@ -409,7 +410,7 @@ func TestBimiCheckExpectsWhatTheReconcilerPublishes(t *testing.T) {
|
|||||||
|
|
||||||
r.bimi.dns = fakeBimiDNS{
|
r.bimi.dns = fakeBimiDNS{
|
||||||
"_dmarc.ebay.com": {"v=DMARC1; p=reject"},
|
"_dmarc.ebay.com": {"v=DMARC1; p=reject"},
|
||||||
"default._bimi.ebay.com": {"v=BIMI1; l=https://mail.ebay.com/.well-known/bimi/default.svg;"},
|
"default._bimi.ebay.com": {"v=BIMI1; l=https://mail.ebay.com/.well-known/bimi/ebay.com/default.svg;"},
|
||||||
}
|
}
|
||||||
body := jsonBody(t, bimiCall(r, http.MethodGet, "/api/v1/admin/bimi/check?domain=ebay.com", "", true))
|
body := jsonBody(t, bimiCall(r, http.MethodGet, "/api/v1/admin/bimi/check?domain=ebay.com", "", true))
|
||||||
|
|
||||||
@@ -439,12 +440,12 @@ func TestBimiGetReportsTheRecordItWouldPublish(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
got := read()
|
got := read()
|
||||||
if got["dns_record"] != "v=BIMI1; l=https://mail.ebay.com/.well-known/bimi/default.svg;" || got["certificate_withheld"] != "" {
|
if got["dns_record"] != "v=BIMI1; l=https://mail.ebay.com/.well-known/bimi/ebay.com/default.svg;" || got["certificate_withheld"] != "" {
|
||||||
t.Fatalf("logo only: %v", got)
|
t.Fatalf("logo only: %v", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
putCertificate(r, fixture(t, "ebay.pem"))
|
putCertificate(r, fixture(t, "ebay.pem"))
|
||||||
if got := read(); got["dns_record"] != "v=BIMI1; l=https://mail.ebay.com/.well-known/bimi/default.svg; a=https://mail.ebay.com/.well-known/bimi/vmc.pem;" {
|
if got := read(); got["dns_record"] != "v=BIMI1; l=https://mail.ebay.com/.well-known/bimi/ebay.com/default.svg; a=https://mail.ebay.com/.well-known/bimi/ebay.com/vmc.pem;" {
|
||||||
t.Fatalf("with its certificate: %v", got["dns_record"])
|
t.Fatalf("with its certificate: %v", got["dns_record"])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,3 +456,83 @@ func TestBimiGetReportsTheRecordItWouldPublish(t *testing.T) {
|
|||||||
t.Fatalf("after replacing the logo: %v / %v", got["dns_record"], got["certificate_withheld"])
|
t.Fatalf("after replacing the logo: %v / %v", got["dns_record"], got["certificate_withheld"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The bug this pins: every domain's record named the deployment's own mail host,
|
||||||
|
// and the host chose the domain, so only the primary domain's logo was ever
|
||||||
|
// found. A second domain's logo has to be served under its own name.
|
||||||
|
func TestBimiServesEachDomainsLogoUnderItsOwnName(t *testing.T) {
|
||||||
|
store := &memoryBimiStore{logos: map[string]*BimiLogo{
|
||||||
|
"ebay.com": {SVG: fixture(t, "ebay.svg"), ETag: "e1"},
|
||||||
|
"cnn.com": {SVG: fixture(t, "cnn.svg"), ETag: "c1", VmcPEM: fixture(t, "ebay.pem")},
|
||||||
|
}}
|
||||||
|
r := newBimiRouter(store) // its default domain, and the host these requests arrive on, is ebay.com
|
||||||
|
|
||||||
|
logo := bimiCall(r, http.MethodGet, "/.well-known/bimi/cnn.com/default.svg", "", false)
|
||||||
|
if logo.Code != http.StatusOK || logo.Body.String() != string(fixture(t, "cnn.svg")) {
|
||||||
|
t.Fatalf("the second domain's logo: %d", logo.Code)
|
||||||
|
}
|
||||||
|
if got := logo.Header().Get("Content-Type"); got != "image/svg+xml" {
|
||||||
|
t.Errorf("content type = %q", got)
|
||||||
|
}
|
||||||
|
if rec := bimiCall(r, http.MethodGet, "/.well-known/bimi/cnn.com/vmc.pem", "", false); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "BEGIN CERTIFICATE") {
|
||||||
|
t.Errorf("the second domain's certificate: %d", rec.Code)
|
||||||
|
}
|
||||||
|
// Case does not matter in a host name
|
||||||
|
if rec := bimiCall(r, http.MethodGet, "/.well-known/bimi/CNN.com/default.svg", "", false); rec.Code != http.StatusOK {
|
||||||
|
t.Errorf("upper case: %d", rec.Code)
|
||||||
|
}
|
||||||
|
// The legacy path still answers with the host's own domain
|
||||||
|
if rec := bimiCall(r, http.MethodGet, "/.well-known/bimi/default.svg", "", false); rec.Code != http.StatusOK || rec.Body.String() != string(fixture(t, "ebay.svg")) {
|
||||||
|
t.Errorf("legacy path: %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBimiPerDomainPathsRefuseWhatIsNotADomainLogo(t *testing.T) {
|
||||||
|
store := &memoryBimiStore{logos: map[string]*BimiLogo{"ebay.com": {SVG: fixture(t, "ebay.svg"), ETag: "e1"}}}
|
||||||
|
r := newBimiRouter(store)
|
||||||
|
for _, path := range []string{
|
||||||
|
"/.well-known/bimi/nobody.test/default.svg", // a domain with no logo
|
||||||
|
"/.well-known/bimi/ebay.com/other.svg", // not a document we serve
|
||||||
|
"/.well-known/bimi/ebay.com/", // no document
|
||||||
|
"/.well-known/bimi/ebay.com/x/default.svg", // extra segment
|
||||||
|
"/.well-known/bimi/ebay.com/default.svg/x",
|
||||||
|
"/.well-known/bimi/-bad.com/default.svg", // not a host name
|
||||||
|
"/.well-known/bimi/a_b.com/default.svg",
|
||||||
|
"/.well-known/bimi/localhost/default.svg", // a single label is not a mail domain
|
||||||
|
"/.well-known/bimi/%2e%2e/default.svg",
|
||||||
|
} {
|
||||||
|
if rec := bimiCall(r, http.MethodGet, path, "", false); rec.Code != http.StatusNotFound {
|
||||||
|
t.Errorf("%s answered %d, want 404", path, rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rec := bimiCall(r, http.MethodPost, "/.well-known/bimi/ebay.com/default.svg", "", false); rec.Code != http.StatusMethodNotAllowed {
|
||||||
|
t.Errorf("POST answered %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The wiring bug: the publisher was handed over before the BIMI API existed and
|
||||||
|
// was dropped. Either order has to work.
|
||||||
|
func TestBimiPublisherSurvivesTheOrderItIsWiredIn(t *testing.T) {
|
||||||
|
store := &memoryBimiStore{logos: map[string]*BimiLogo{}}
|
||||||
|
publisher := func(string) {}
|
||||||
|
|
||||||
|
before := &Router{mux: http.NewServeMux()}
|
||||||
|
before.SetBimiPublisher(publisher)
|
||||||
|
before.SetBimiAPI(store, "secret", "ebay.com", "mail.ebay.com")
|
||||||
|
if before.bimi.publish == nil {
|
||||||
|
t.Error("a publisher set before the API was lost")
|
||||||
|
}
|
||||||
|
|
||||||
|
after := &Router{mux: http.NewServeMux()}
|
||||||
|
after.SetBimiAPI(store, "secret", "ebay.com", "mail.ebay.com")
|
||||||
|
after.SetBimiPublisher(publisher)
|
||||||
|
if after.bimi.publish == nil {
|
||||||
|
t.Error("a publisher set after the API was lost")
|
||||||
|
}
|
||||||
|
|
||||||
|
none := &Router{mux: http.NewServeMux()}
|
||||||
|
none.SetBimiAPI(store, "secret", "ebay.com", "mail.ebay.com")
|
||||||
|
if none.bimi.publish != nil {
|
||||||
|
t.Error("with no DNS provider there is nothing to publish to")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ type Router struct {
|
|||||||
dnsReconciler DomainReconciler
|
dnsReconciler DomainReconciler
|
||||||
dnsStatus DnsStatusWriter
|
dnsStatus DnsStatusWriter
|
||||||
bimi *bimiAPI
|
bimi *bimiAPI
|
||||||
|
bimiPublish func(domain string)
|
||||||
// degradedFunc reports a condition that leaves the service running but
|
// degradedFunc reports a condition that leaves the service running but
|
||||||
// not fit for production, the clearest case being a self-signed
|
// not fit for production, the clearest case being a self-signed
|
||||||
// certificate standing in for one Traefik never issued
|
// certificate standing in for one Traefik never issued
|
||||||
@@ -81,6 +82,7 @@ func (r *Router) registerRoutes() {
|
|||||||
r.mux.HandleFunc("/.well-known/security.txt", r.handleSecurityTxt)
|
r.mux.HandleFunc("/.well-known/security.txt", r.handleSecurityTxt)
|
||||||
r.mux.HandleFunc("/.well-known/bimi/default.svg", r.handleBimiLogo)
|
r.mux.HandleFunc("/.well-known/bimi/default.svg", r.handleBimiLogo)
|
||||||
r.mux.HandleFunc("/.well-known/bimi/vmc.pem", r.handleBimiCertificate)
|
r.mux.HandleFunc("/.well-known/bimi/vmc.pem", r.handleBimiCertificate)
|
||||||
|
r.mux.HandleFunc("/.well-known/bimi/", r.handleBimiPerDomain)
|
||||||
r.mux.HandleFunc("/api/v1/admin/bimi", r.handleBimiAdmin)
|
r.mux.HandleFunc("/api/v1/admin/bimi", r.handleBimiAdmin)
|
||||||
r.mux.HandleFunc("/api/v1/admin/bimi/certificate", r.handleBimiCertificateAdmin)
|
r.mux.HandleFunc("/api/v1/admin/bimi/certificate", r.handleBimiCertificateAdmin)
|
||||||
r.mux.HandleFunc("/api/v1/admin/bimi/check", r.handleBimiCheck)
|
r.mux.HandleFunc("/api/v1/admin/bimi/check", r.handleBimiCheck)
|
||||||
|
|||||||
@@ -36,6 +36,16 @@ const nextConfig: NextConfig = {
|
|||||||
source: "/.well-known/bimi/default.svg",
|
source: "/.well-known/bimi/default.svg",
|
||||||
destination: `${PROTOCOLS_SERVICE_URL}/.well-known/bimi/default.svg`,
|
destination: `${PROTOCOLS_SERVICE_URL}/.well-known/bimi/default.svg`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// A record names its own domain in the path, so every domain the server
|
||||||
|
// hosts can publish its own logo and certificate from this one host.
|
||||||
|
source: "/.well-known/bimi/:domain/default.svg",
|
||||||
|
destination: `${PROTOCOLS_SERVICE_URL}/.well-known/bimi/:domain/default.svg`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/.well-known/bimi/:domain/vmc.pem",
|
||||||
|
destination: `${PROTOCOLS_SERVICE_URL}/.well-known/bimi/:domain/vmc.pem`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// The BIMI certificate (VMC/CMC) the DNS record's a= tag points at,
|
// The BIMI certificate (VMC/CMC) the DNS record's a= tag points at,
|
||||||
// fetched the same way as the logo above.
|
// fetched the same way as the logo above.
|
||||||
|
|||||||
@@ -66,4 +66,12 @@ describe("the routes the protocols service owns", () => {
|
|||||||
expect(config).toContain("/.well-known/bimi/vmc.pem");
|
expect(config).toContain("/.well-known/bimi/vmc.pem");
|
||||||
expect(config).toMatch(/bimi\/vmc\.pem[\s\S]{0,200}PROTOCOLS_SERVICE_URL/);
|
expect(config).toMatch(/bimi\/vmc\.pem[\s\S]{0,200}PROTOCOLS_SERVICE_URL/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Each domain's record names its own logo and certificate under its own name.
|
||||||
|
// Without these rewrites only the host's primary domain had a logo that could
|
||||||
|
// be fetched; every other domain's record pointed at a 404.
|
||||||
|
it("routes the per-domain BIMI logo and certificate to the service", () => {
|
||||||
|
expect(config).toMatch(/bimi\/:domain\/default\.svg[\s\S]{0,200}PROTOCOLS_SERVICE_URL[^\n]*:domain\/default\.svg/);
|
||||||
|
expect(config).toMatch(/bimi\/:domain\/vmc\.pem[\s\S]{0,200}PROTOCOLS_SERVICE_URL[^\n]*:domain\/vmc\.pem/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user