mirror of
https://github.com/mjl-/mox.git
synced 2025-07-12 17:04:39 +03:00
improve http request handling for internal services and multiple domains
per listener, you could enable the admin/account/webmail/webapi handlers. but that would serve those services on their configured paths (/admin/, /, /webmail/, /webapi/) on all domains mox would be webserving, including any non-mail domains. so your www.example/admin/ would be serving the admin web interface, with no way to disabled that. with this change, the admin interface is only served on requests to (based on Host header): - ip addresses - the listener host name (explicitly configured in the listener, with fallback to global hostname) - "localhost" (for ssh tunnel/forwarding scenario's) the account/webmail/webapi interfaces are served on the same domains as the admin interface, and additionally: - the client settings domains, as optionally configured in each Domain in domains.conf. typically "mail.<yourdomain>". this means the internal services are no longer served on other domains configured in the webserver, e.g. www.example.org/admin/ will not be handled specially. the order of evaluation of routes/services is also changed: before this change, the internal handlers would always be evaluated first. with this change, only the system handlers for MTA-STS/autoconfig/ACME-validation will be evaluated first. then the webserver handlers. and finally the internal services (admin/account/webmail/webapi). this allows an admin to configure overrides for some of the domains (per hostname-matching rules explained above) that would normally serve these services. webserver handlers can now be configured that pass the request to an internal service: in addition to the existing static/redirect/forward config options, there is now an "internal" config option, naming the service (admin/account/webmail/webapi) for handling the request. this allows enabling the internal services on custom domains. for issue #160 by TragicLifeHu, thanks for reporting!
This commit is contained in:
652
http/web.go
652
http/web.go
@ -351,37 +351,40 @@ func (w *loggingWriter) Done() {
|
||||
pkglog.WithContext(w.R.Context()).Debugx("http request", err, attrs...)
|
||||
}
|
||||
|
||||
// Set some http headers that should prevent potential abuse. Better safe than sorry.
|
||||
func safeHeaders(fn http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h := w.Header()
|
||||
h.Set("X-Frame-Options", "deny")
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("Content-Security-Policy", "default-src 'self' 'unsafe-inline' data:")
|
||||
h.Set("Referrer-Policy", "same-origin")
|
||||
fn.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// Built-in handlers, e.g. mta-sts and autoconfig.
|
||||
type pathHandler struct {
|
||||
Name string // For logging/metrics.
|
||||
HostMatch func(dom dns.Domain) bool // If not nil, called to see if domain of requests matches. Only called if requested host is a valid domain.
|
||||
Path string // Path to register, like on http.ServeMux.
|
||||
Name string // For logging/metrics.
|
||||
HostMatch func(host dns.IPDomain) bool // If not nil, called to see if domain of requests matches. Host can be zero value for invalid domain/ip.
|
||||
Path string // Path to register, like on http.ServeMux.
|
||||
Handler http.Handler
|
||||
}
|
||||
type serve struct {
|
||||
Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https).
|
||||
TLSConfig *tls.Config
|
||||
PathHandlers []pathHandler // Sorted, longest first.
|
||||
Webserver bool // Whether serving WebHandler. PathHandlers are always evaluated before WebHandlers.
|
||||
Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https).
|
||||
TLSConfig *tls.Config
|
||||
|
||||
// SystemHandlers are for MTA-STS, autoconfig, ACME validation. They can't be
|
||||
// overridden by WebHandlers. WebHandlers are evaluated next, and the internal
|
||||
// service handlers from Listeners in mox.conf (for admin, account, webmail, webapi
|
||||
// interfaces) last. WebHandlers can also pass requests to the internal servers.
|
||||
// This order allows admins to serve other content on domains serving the mox.conf
|
||||
// internal services.
|
||||
SystemHandlers []pathHandler // Sorted, longest first.
|
||||
Webserver bool
|
||||
ServiceHandlers []pathHandler // Sorted, longest first.
|
||||
}
|
||||
|
||||
// Handle registers a named handler for a path and optional host. If path ends with
|
||||
// a slash, it is used as prefix match, otherwise a full path match is required. If
|
||||
// hostOpt is set, only requests to those host are handled by this handler.
|
||||
func (s *serve) Handle(name string, hostMatch func(dns.Domain) bool, path string, fn http.Handler) {
|
||||
s.PathHandlers = append(s.PathHandlers, pathHandler{name, hostMatch, path, fn})
|
||||
// SystemHandle registers a named system handler for a path and optional host. If
|
||||
// path ends with a slash, it is used as prefix match, otherwise a full path match
|
||||
// is required. If hostOpt is set, only requests to those host are handled by this
|
||||
// handler.
|
||||
func (s *serve) SystemHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
|
||||
s.SystemHandlers = append(s.SystemHandlers, pathHandler{name, hostMatch, path, fn})
|
||||
}
|
||||
|
||||
// Like SystemHandle, but for internal services "admin", "account", "webmail",
|
||||
// "webapi" configured in the mox.conf Listener.
|
||||
func (s *serve) ServiceHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
|
||||
s.ServiceHandlers = append(s.ServiceHandlers, pathHandler{name, hostMatch, path, fn})
|
||||
}
|
||||
|
||||
var (
|
||||
@ -452,28 +455,44 @@ func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path += "/"
|
||||
}
|
||||
|
||||
var dom dns.Domain
|
||||
host := r.Host
|
||||
nhost, _, err := net.SplitHostPort(host)
|
||||
if err == nil {
|
||||
host = nhost
|
||||
}
|
||||
// host could be an IP, some handles may match, not an error.
|
||||
dom, domErr := dns.ParseDomain(host)
|
||||
ipdom := dns.IPDomain{IP: net.ParseIP(host)}
|
||||
if ipdom.IP == nil {
|
||||
dom, domErr := dns.ParseDomain(host)
|
||||
if domErr == nil {
|
||||
ipdom = dns.IPDomain{Domain: dom}
|
||||
}
|
||||
}
|
||||
|
||||
for _, h := range s.PathHandlers {
|
||||
if h.HostMatch != nil && (domErr != nil || !h.HostMatch(dom)) {
|
||||
continue
|
||||
handle := func(h pathHandler) bool {
|
||||
if h.HostMatch != nil && !h.HostMatch(ipdom) {
|
||||
return false
|
||||
}
|
||||
if r.URL.Path == h.Path || strings.HasSuffix(h.Path, "/") && strings.HasPrefix(r.URL.Path, h.Path) {
|
||||
nw.Handler = h.Name
|
||||
nw.Compress = true
|
||||
h.Handler.ServeHTTP(nw, r)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, h := range s.SystemHandlers {
|
||||
if handle(h) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if s.Webserver && domErr == nil {
|
||||
if WebHandle(nw, r, dom) {
|
||||
if s.Webserver {
|
||||
if WebHandle(nw, r, ipdom) {
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, h := range s.ServiceHandlers {
|
||||
if handle(h) {
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -481,284 +500,31 @@ func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(nw, r)
|
||||
}
|
||||
|
||||
func redirectToTrailingSlash(srv *serve, hostMatch func(dns.IPDomain) bool, name, path string) {
|
||||
// Helpfully redirect user to version with ending slash.
|
||||
if path != "/" && strings.HasSuffix(path, "/") {
|
||||
handler := mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, path, http.StatusSeeOther)
|
||||
}))
|
||||
srv.ServiceHandle(name, hostMatch, path[:len(path)-1], handler)
|
||||
}
|
||||
}
|
||||
|
||||
// Listen binds to sockets for HTTP listeners, including those required for ACME to
|
||||
// generate TLS certificates. It stores the listeners so Serve can start serving them.
|
||||
func Listen() {
|
||||
redirectToTrailingSlash := func(srv *serve, name, path string) {
|
||||
// Helpfully redirect user to version with ending slash.
|
||||
if path != "/" && strings.HasSuffix(path, "/") {
|
||||
handler := safeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, path, http.StatusSeeOther)
|
||||
}))
|
||||
srv.Handle(name, nil, path[:len(path)-1], handler)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize listeners in deterministic order for the same potential error
|
||||
// messages.
|
||||
names := maps.Keys(mox.Conf.Static.Listeners)
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
l := mox.Conf.Static.Listeners[name]
|
||||
|
||||
portServe := map[int]*serve{}
|
||||
|
||||
var ensureServe func(https bool, port int, kind string) *serve
|
||||
ensureServe = func(https bool, port int, kind string) *serve {
|
||||
s := portServe[port]
|
||||
if s == nil {
|
||||
s = &serve{nil, nil, nil, false}
|
||||
portServe[port] = s
|
||||
}
|
||||
s.Kinds = append(s.Kinds, kind)
|
||||
if https && l.TLS.ACME != "" {
|
||||
s.TLSConfig = l.TLS.ACMEConfig
|
||||
} else if https {
|
||||
s.TLSConfig = l.TLS.Config
|
||||
if l.TLS.ACME != "" {
|
||||
tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
|
||||
ensureServe(true, tlsport, "acme-tls-alpn-01")
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
|
||||
port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
|
||||
ensureServe(true, port, "acme-tls-alpn-01")
|
||||
}
|
||||
|
||||
if l.AccountHTTP.Enabled {
|
||||
port := config.Port(l.AccountHTTP.Port, 80)
|
||||
path := "/"
|
||||
if l.AccountHTTP.Path != "" {
|
||||
path = l.AccountHTTP.Path
|
||||
}
|
||||
srv := ensureServe(false, port, "account-http at "+path)
|
||||
handler := safeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
|
||||
srv.Handle("account", nil, path, handler)
|
||||
redirectToTrailingSlash(srv, "account", path)
|
||||
}
|
||||
if l.AccountHTTPS.Enabled {
|
||||
port := config.Port(l.AccountHTTPS.Port, 443)
|
||||
path := "/"
|
||||
if l.AccountHTTPS.Path != "" {
|
||||
path = l.AccountHTTPS.Path
|
||||
}
|
||||
srv := ensureServe(true, port, "account-https at "+path)
|
||||
handler := safeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
|
||||
srv.Handle("account", nil, path, handler)
|
||||
redirectToTrailingSlash(srv, "account", path)
|
||||
}
|
||||
|
||||
if l.AdminHTTP.Enabled {
|
||||
port := config.Port(l.AdminHTTP.Port, 80)
|
||||
path := "/admin/"
|
||||
if l.AdminHTTP.Path != "" {
|
||||
path = l.AdminHTTP.Path
|
||||
}
|
||||
srv := ensureServe(false, port, "admin-http at "+path)
|
||||
handler := safeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
|
||||
srv.Handle("admin", nil, path, handler)
|
||||
redirectToTrailingSlash(srv, "admin", path)
|
||||
}
|
||||
if l.AdminHTTPS.Enabled {
|
||||
port := config.Port(l.AdminHTTPS.Port, 443)
|
||||
path := "/admin/"
|
||||
if l.AdminHTTPS.Path != "" {
|
||||
path = l.AdminHTTPS.Path
|
||||
}
|
||||
srv := ensureServe(true, port, "admin-https at "+path)
|
||||
handler := safeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
|
||||
srv.Handle("admin", nil, path, handler)
|
||||
redirectToTrailingSlash(srv, "admin", path)
|
||||
}
|
||||
|
||||
maxMsgSize := l.SMTPMaxMessageSize
|
||||
if maxMsgSize == 0 {
|
||||
maxMsgSize = config.DefaultMaxMsgSize
|
||||
}
|
||||
|
||||
if l.WebAPIHTTP.Enabled {
|
||||
port := config.Port(l.WebAPIHTTP.Port, 80)
|
||||
path := "/webapi/"
|
||||
if l.WebAPIHTTP.Path != "" {
|
||||
path = l.WebAPIHTTP.Path
|
||||
}
|
||||
srv := ensureServe(false, port, "webapi-http at "+path)
|
||||
handler := safeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
|
||||
srv.Handle("webapi", nil, path, handler)
|
||||
redirectToTrailingSlash(srv, "webapi", path)
|
||||
}
|
||||
if l.WebAPIHTTPS.Enabled {
|
||||
port := config.Port(l.WebAPIHTTPS.Port, 443)
|
||||
path := "/webapi/"
|
||||
if l.WebAPIHTTPS.Path != "" {
|
||||
path = l.WebAPIHTTPS.Path
|
||||
}
|
||||
srv := ensureServe(true, port, "webapi-https at "+path)
|
||||
handler := safeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
|
||||
srv.Handle("webapi", nil, path, handler)
|
||||
redirectToTrailingSlash(srv, "webapi", path)
|
||||
}
|
||||
|
||||
if l.WebmailHTTP.Enabled {
|
||||
port := config.Port(l.WebmailHTTP.Port, 80)
|
||||
path := "/webmail/"
|
||||
if l.WebmailHTTP.Path != "" {
|
||||
path = l.WebmailHTTP.Path
|
||||
}
|
||||
srv := ensureServe(false, port, "webmail-http at "+path)
|
||||
var accountPath string
|
||||
if l.AccountHTTP.Enabled {
|
||||
accountPath = "/"
|
||||
if l.AccountHTTP.Path != "" {
|
||||
accountPath = l.AccountHTTP.Path
|
||||
}
|
||||
}
|
||||
handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
|
||||
srv.Handle("webmail", nil, path, handler)
|
||||
redirectToTrailingSlash(srv, "webmail", path)
|
||||
}
|
||||
if l.WebmailHTTPS.Enabled {
|
||||
port := config.Port(l.WebmailHTTPS.Port, 443)
|
||||
path := "/webmail/"
|
||||
if l.WebmailHTTPS.Path != "" {
|
||||
path = l.WebmailHTTPS.Path
|
||||
}
|
||||
srv := ensureServe(true, port, "webmail-https at "+path)
|
||||
var accountPath string
|
||||
if l.AccountHTTPS.Enabled {
|
||||
accountPath = "/"
|
||||
if l.AccountHTTPS.Path != "" {
|
||||
accountPath = l.AccountHTTPS.Path
|
||||
}
|
||||
}
|
||||
handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
|
||||
srv.Handle("webmail", nil, path, handler)
|
||||
redirectToTrailingSlash(srv, "webmail", path)
|
||||
}
|
||||
|
||||
if l.MetricsHTTP.Enabled {
|
||||
port := config.Port(l.MetricsHTTP.Port, 8010)
|
||||
srv := ensureServe(false, port, "metrics-http")
|
||||
srv.Handle("metrics", nil, "/metrics", safeHeaders(promhttp.Handler()))
|
||||
srv.Handle("metrics", nil, "/", safeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
} else if r.Method != "GET" {
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
|
||||
})))
|
||||
}
|
||||
if l.AutoconfigHTTPS.Enabled {
|
||||
port := config.Port(l.AutoconfigHTTPS.Port, 443)
|
||||
srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https")
|
||||
autoconfigMatch := func(dom dns.Domain) bool {
|
||||
// Thunderbird requests an autodiscovery URL at the email address domain name, so
|
||||
// autoconfig prefix is optional.
|
||||
if strings.HasPrefix(dom.ASCII, "autoconfig.") {
|
||||
dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
|
||||
dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
|
||||
}
|
||||
// Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
|
||||
// use the mail server's host name.
|
||||
if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
|
||||
return true
|
||||
}
|
||||
dc, ok := mox.Conf.Domain(dom)
|
||||
return ok && !dc.ReportsOnly
|
||||
}
|
||||
srv.Handle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", safeHeaders(http.HandlerFunc(autoconfHandle)))
|
||||
srv.Handle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", safeHeaders(http.HandlerFunc(autodiscoverHandle)))
|
||||
srv.Handle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", safeHeaders(http.HandlerFunc(mobileconfigHandle)))
|
||||
srv.Handle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", safeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
|
||||
}
|
||||
if l.MTASTSHTTPS.Enabled {
|
||||
port := config.Port(l.MTASTSHTTPS.Port, 443)
|
||||
srv := ensureServe(!l.MTASTSHTTPS.NonTLS, port, "mtasts-https")
|
||||
mtastsMatch := func(dom dns.Domain) bool {
|
||||
// todo: may want to check this against the configured domains, could in theory be just a webserver.
|
||||
return strings.HasPrefix(dom.ASCII, "mta-sts.")
|
||||
}
|
||||
srv.Handle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", safeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
|
||||
}
|
||||
if l.PprofHTTP.Enabled {
|
||||
// Importing net/http/pprof registers handlers on the default serve mux.
|
||||
port := config.Port(l.PprofHTTP.Port, 8011)
|
||||
if _, ok := portServe[port]; ok {
|
||||
pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
|
||||
}
|
||||
srv := &serve{[]string{"pprof-http"}, nil, nil, false}
|
||||
portServe[port] = srv
|
||||
srv.Handle("pprof", nil, "/", http.DefaultServeMux)
|
||||
}
|
||||
if l.WebserverHTTP.Enabled {
|
||||
port := config.Port(l.WebserverHTTP.Port, 80)
|
||||
srv := ensureServe(false, port, "webserver-http")
|
||||
srv.Webserver = true
|
||||
}
|
||||
if l.WebserverHTTPS.Enabled {
|
||||
port := config.Port(l.WebserverHTTPS.Port, 443)
|
||||
srv := ensureServe(true, port, "webserver-https")
|
||||
srv.Webserver = true
|
||||
}
|
||||
|
||||
if l.TLS != nil && l.TLS.ACME != "" {
|
||||
m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
|
||||
|
||||
// If we are listening on port 80 for plain http, also register acme http-01
|
||||
// validation handler.
|
||||
if srv, ok := portServe[80]; ok && srv.TLSConfig == nil {
|
||||
srv.Kinds = append(srv.Kinds, "acme-http-01")
|
||||
srv.Handle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
|
||||
}
|
||||
|
||||
hosts := map[dns.Domain]struct{}{
|
||||
mox.Conf.Static.HostnameDomain: {},
|
||||
}
|
||||
if l.HostnameDomain.ASCII != "" {
|
||||
hosts[l.HostnameDomain] = struct{}{}
|
||||
}
|
||||
// All domains are served on all listeners. Gather autoconfig hostnames to ensure
|
||||
// presence of TLS certificates for.
|
||||
for _, name := range mox.Conf.Domains() {
|
||||
if dom, err := dns.ParseDomain(name); err != nil {
|
||||
pkglog.Errorx("parsing domain from config", err)
|
||||
} else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly {
|
||||
// Do not gather autoconfig name if we aren't accepting email for this domain.
|
||||
continue
|
||||
}
|
||||
|
||||
autoconfdom, err := dns.ParseDomain("autoconfig." + name)
|
||||
if err != nil {
|
||||
pkglog.Errorx("parsing domain from config for autoconfig", err)
|
||||
} else {
|
||||
hosts[autoconfdom] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
ensureManagerHosts[m] = hosts
|
||||
}
|
||||
portServe := portServes(l)
|
||||
|
||||
ports := maps.Keys(portServe)
|
||||
sort.Ints(ports)
|
||||
for _, port := range ports {
|
||||
srv := portServe[port]
|
||||
sort.Slice(srv.PathHandlers, func(i, j int) bool {
|
||||
a := srv.PathHandlers[i].Path
|
||||
b := srv.PathHandlers[j].Path
|
||||
if len(a) == len(b) {
|
||||
// For consistent order.
|
||||
return a < b
|
||||
}
|
||||
// Longest paths first.
|
||||
return len(a) > len(b)
|
||||
})
|
||||
for _, ip := range l.IPs {
|
||||
listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv)
|
||||
}
|
||||
@ -766,6 +532,300 @@ func Listen() {
|
||||
}
|
||||
}
|
||||
|
||||
func portServes(l config.Listener) map[int]*serve {
|
||||
portServe := map[int]*serve{}
|
||||
|
||||
// For system/services, we serve on host localhost too, for ssh tunnel scenario's.
|
||||
localhost := dns.Domain{ASCII: "localhost"}
|
||||
|
||||
ldom := l.HostnameDomain
|
||||
if l.Hostname == "" {
|
||||
ldom = mox.Conf.Static.HostnameDomain
|
||||
}
|
||||
listenerHostMatch := func(host dns.IPDomain) bool {
|
||||
if host.IsIP() {
|
||||
return true
|
||||
}
|
||||
return host.Domain == ldom || host.Domain == localhost
|
||||
}
|
||||
accountHostMatch := func(host dns.IPDomain) bool {
|
||||
if listenerHostMatch(host) {
|
||||
return true
|
||||
}
|
||||
return mox.Conf.IsClientSettingsDomain(host.Domain)
|
||||
}
|
||||
|
||||
var ensureServe func(https bool, port int, kind string) *serve
|
||||
ensureServe = func(https bool, port int, kind string) *serve {
|
||||
s := portServe[port]
|
||||
if s == nil {
|
||||
s = &serve{nil, nil, nil, false, nil}
|
||||
portServe[port] = s
|
||||
}
|
||||
s.Kinds = append(s.Kinds, kind)
|
||||
if https && l.TLS.ACME != "" {
|
||||
s.TLSConfig = l.TLS.ACMEConfig
|
||||
} else if https {
|
||||
s.TLSConfig = l.TLS.Config
|
||||
if l.TLS.ACME != "" {
|
||||
tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
|
||||
ensureServe(true, tlsport, "acme-tls-alpn-01")
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
|
||||
port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
|
||||
ensureServe(true, port, "acme-tls-alpn-01")
|
||||
}
|
||||
|
||||
if l.AccountHTTP.Enabled {
|
||||
port := config.Port(l.AccountHTTP.Port, 80)
|
||||
path := "/"
|
||||
if l.AccountHTTP.Path != "" {
|
||||
path = l.AccountHTTP.Path
|
||||
}
|
||||
srv := ensureServe(false, port, "account-http at "+path)
|
||||
handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
|
||||
srv.ServiceHandle("account", accountHostMatch, path, handler)
|
||||
redirectToTrailingSlash(srv, accountHostMatch, "account", path)
|
||||
}
|
||||
if l.AccountHTTPS.Enabled {
|
||||
port := config.Port(l.AccountHTTPS.Port, 443)
|
||||
path := "/"
|
||||
if l.AccountHTTPS.Path != "" {
|
||||
path = l.AccountHTTPS.Path
|
||||
}
|
||||
srv := ensureServe(true, port, "account-https at "+path)
|
||||
handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
|
||||
srv.ServiceHandle("account", accountHostMatch, path, handler)
|
||||
redirectToTrailingSlash(srv, accountHostMatch, "account", path)
|
||||
}
|
||||
|
||||
if l.AdminHTTP.Enabled {
|
||||
port := config.Port(l.AdminHTTP.Port, 80)
|
||||
path := "/admin/"
|
||||
if l.AdminHTTP.Path != "" {
|
||||
path = l.AdminHTTP.Path
|
||||
}
|
||||
srv := ensureServe(false, port, "admin-http at "+path)
|
||||
handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
|
||||
srv.ServiceHandle("admin", listenerHostMatch, path, handler)
|
||||
redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
|
||||
}
|
||||
if l.AdminHTTPS.Enabled {
|
||||
port := config.Port(l.AdminHTTPS.Port, 443)
|
||||
path := "/admin/"
|
||||
if l.AdminHTTPS.Path != "" {
|
||||
path = l.AdminHTTPS.Path
|
||||
}
|
||||
srv := ensureServe(true, port, "admin-https at "+path)
|
||||
handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
|
||||
srv.ServiceHandle("admin", listenerHostMatch, path, handler)
|
||||
redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
|
||||
}
|
||||
|
||||
maxMsgSize := l.SMTPMaxMessageSize
|
||||
if maxMsgSize == 0 {
|
||||
maxMsgSize = config.DefaultMaxMsgSize
|
||||
}
|
||||
|
||||
if l.WebAPIHTTP.Enabled {
|
||||
port := config.Port(l.WebAPIHTTP.Port, 80)
|
||||
path := "/webapi/"
|
||||
if l.WebAPIHTTP.Path != "" {
|
||||
path = l.WebAPIHTTP.Path
|
||||
}
|
||||
srv := ensureServe(false, port, "webapi-http at "+path)
|
||||
handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
|
||||
srv.ServiceHandle("webapi", accountHostMatch, path, handler)
|
||||
redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
|
||||
}
|
||||
if l.WebAPIHTTPS.Enabled {
|
||||
port := config.Port(l.WebAPIHTTPS.Port, 443)
|
||||
path := "/webapi/"
|
||||
if l.WebAPIHTTPS.Path != "" {
|
||||
path = l.WebAPIHTTPS.Path
|
||||
}
|
||||
srv := ensureServe(true, port, "webapi-https at "+path)
|
||||
handler := mox.SafeHeaders(http.StripPrefix(path[:len(path)-1], webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
|
||||
srv.ServiceHandle("webapi", accountHostMatch, path, handler)
|
||||
redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
|
||||
}
|
||||
|
||||
if l.WebmailHTTP.Enabled {
|
||||
port := config.Port(l.WebmailHTTP.Port, 80)
|
||||
path := "/webmail/"
|
||||
if l.WebmailHTTP.Path != "" {
|
||||
path = l.WebmailHTTP.Path
|
||||
}
|
||||
srv := ensureServe(false, port, "webmail-http at "+path)
|
||||
var accountPath string
|
||||
if l.AccountHTTP.Enabled {
|
||||
accountPath = "/"
|
||||
if l.AccountHTTP.Path != "" {
|
||||
accountPath = l.AccountHTTP.Path
|
||||
}
|
||||
}
|
||||
handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
|
||||
srv.ServiceHandle("webmail", accountHostMatch, path, handler)
|
||||
redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
|
||||
}
|
||||
if l.WebmailHTTPS.Enabled {
|
||||
port := config.Port(l.WebmailHTTPS.Port, 443)
|
||||
path := "/webmail/"
|
||||
if l.WebmailHTTPS.Path != "" {
|
||||
path = l.WebmailHTTPS.Path
|
||||
}
|
||||
srv := ensureServe(true, port, "webmail-https at "+path)
|
||||
var accountPath string
|
||||
if l.AccountHTTPS.Enabled {
|
||||
accountPath = "/"
|
||||
if l.AccountHTTPS.Path != "" {
|
||||
accountPath = l.AccountHTTPS.Path
|
||||
}
|
||||
}
|
||||
handler := http.StripPrefix(path[:len(path)-1], http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
|
||||
srv.ServiceHandle("webmail", accountHostMatch, path, handler)
|
||||
redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
|
||||
}
|
||||
|
||||
if l.MetricsHTTP.Enabled {
|
||||
port := config.Port(l.MetricsHTTP.Port, 8010)
|
||||
srv := ensureServe(false, port, "metrics-http")
|
||||
srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler()))
|
||||
srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
} else if r.Method != "GET" {
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
|
||||
})))
|
||||
}
|
||||
if l.AutoconfigHTTPS.Enabled {
|
||||
port := config.Port(l.AutoconfigHTTPS.Port, 443)
|
||||
srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https")
|
||||
autoconfigMatch := func(ipdom dns.IPDomain) bool {
|
||||
dom := ipdom.Domain
|
||||
if dom.IsZero() {
|
||||
return false
|
||||
}
|
||||
// Thunderbird requests an autodiscovery URL at the email address domain name, so
|
||||
// autoconfig prefix is optional.
|
||||
if strings.HasPrefix(dom.ASCII, "autoconfig.") {
|
||||
dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
|
||||
dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
|
||||
}
|
||||
// Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
|
||||
// use the mail server's host name.
|
||||
if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
|
||||
return true
|
||||
}
|
||||
dc, ok := mox.Conf.Domain(dom)
|
||||
return ok && !dc.ReportsOnly
|
||||
}
|
||||
srv.SystemHandle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", mox.SafeHeaders(http.HandlerFunc(autoconfHandle)))
|
||||
srv.SystemHandle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", mox.SafeHeaders(http.HandlerFunc(autodiscoverHandle)))
|
||||
srv.SystemHandle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", mox.SafeHeaders(http.HandlerFunc(mobileconfigHandle)))
|
||||
srv.SystemHandle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", mox.SafeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
|
||||
}
|
||||
if l.MTASTSHTTPS.Enabled {
|
||||
port := config.Port(l.MTASTSHTTPS.Port, 443)
|
||||
srv := ensureServe(!l.MTASTSHTTPS.NonTLS, port, "mtasts-https")
|
||||
mtastsMatch := func(ipdom dns.IPDomain) bool {
|
||||
// todo: may want to check this against the configured domains, could in theory be just a webserver.
|
||||
dom := ipdom.Domain
|
||||
if dom.IsZero() {
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(dom.ASCII, "mta-sts.")
|
||||
}
|
||||
srv.SystemHandle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", mox.SafeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
|
||||
}
|
||||
if l.PprofHTTP.Enabled {
|
||||
// Importing net/http/pprof registers handlers on the default serve mux.
|
||||
port := config.Port(l.PprofHTTP.Port, 8011)
|
||||
if _, ok := portServe[port]; ok {
|
||||
pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
|
||||
}
|
||||
srv := &serve{[]string{"pprof-http"}, nil, nil, false, nil}
|
||||
portServe[port] = srv
|
||||
srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
|
||||
}
|
||||
if l.WebserverHTTP.Enabled {
|
||||
port := config.Port(l.WebserverHTTP.Port, 80)
|
||||
srv := ensureServe(false, port, "webserver-http")
|
||||
srv.Webserver = true
|
||||
}
|
||||
if l.WebserverHTTPS.Enabled {
|
||||
port := config.Port(l.WebserverHTTPS.Port, 443)
|
||||
srv := ensureServe(true, port, "webserver-https")
|
||||
srv.Webserver = true
|
||||
}
|
||||
|
||||
if l.TLS != nil && l.TLS.ACME != "" {
|
||||
m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
|
||||
|
||||
// If we are listening on port 80 for plain http, also register acme http-01
|
||||
// validation handler.
|
||||
if srv, ok := portServe[80]; ok && srv.TLSConfig == nil {
|
||||
srv.Kinds = append(srv.Kinds, "acme-http-01")
|
||||
srv.SystemHandle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
|
||||
}
|
||||
|
||||
hosts := map[dns.Domain]struct{}{
|
||||
mox.Conf.Static.HostnameDomain: {},
|
||||
}
|
||||
if l.HostnameDomain.ASCII != "" {
|
||||
hosts[l.HostnameDomain] = struct{}{}
|
||||
}
|
||||
// All domains are served on all listeners. Gather autoconfig hostnames to ensure
|
||||
// presence of TLS certificates for.
|
||||
for _, name := range mox.Conf.Domains() {
|
||||
if dom, err := dns.ParseDomain(name); err != nil {
|
||||
pkglog.Errorx("parsing domain from config", err)
|
||||
} else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly {
|
||||
// Do not gather autoconfig name if we aren't accepting email for this domain.
|
||||
continue
|
||||
}
|
||||
|
||||
autoconfdom, err := dns.ParseDomain("autoconfig." + name)
|
||||
if err != nil {
|
||||
pkglog.Errorx("parsing domain from config for autoconfig", err)
|
||||
} else {
|
||||
hosts[autoconfdom] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
ensureManagerHosts[m] = hosts
|
||||
}
|
||||
|
||||
for _, srv := range portServe {
|
||||
sortPathHandlers(srv.SystemHandlers)
|
||||
sortPathHandlers(srv.ServiceHandlers)
|
||||
}
|
||||
|
||||
return portServe
|
||||
}
|
||||
|
||||
func sortPathHandlers(l []pathHandler) {
|
||||
sort.Slice(l, func(i, j int) bool {
|
||||
a := l[i].Path
|
||||
b := l[j].Path
|
||||
if len(a) == len(b) {
|
||||
// For consistent order.
|
||||
return a < b
|
||||
}
|
||||
// Longest paths first.
|
||||
return len(a) > len(b)
|
||||
})
|
||||
}
|
||||
|
||||
// functions to be launched in goroutine that will serve on a listener.
|
||||
var servers []func()
|
||||
|
||||
|
@ -6,10 +6,8 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mjl-/mox/dns"
|
||||
"github.com/mjl-/mox/mox-"
|
||||
)
|
||||
|
||||
@ -19,20 +17,8 @@ func TestServeHTTP(t *testing.T) {
|
||||
mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
|
||||
mox.MustLoadConfig(true, false)
|
||||
|
||||
srv := &serve{
|
||||
PathHandlers: []pathHandler{
|
||||
{
|
||||
HostMatch: func(dom dns.Domain) bool {
|
||||
return strings.HasPrefix(dom.ASCII, "mta-sts.")
|
||||
},
|
||||
Path: "/.well-known/mta-sts.txt",
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("mta-sts!"))
|
||||
}),
|
||||
},
|
||||
},
|
||||
Webserver: true,
|
||||
}
|
||||
portSrvs := portServes(mox.Conf.Static.Listeners["local"])
|
||||
srv := portSrvs[80]
|
||||
|
||||
test := func(method, target string, expCode int, expContent string, expHeaders map[string]string) {
|
||||
t.Helper()
|
||||
@ -43,22 +29,22 @@ func TestServeHTTP(t *testing.T) {
|
||||
srv.ServeHTTP(rw, req)
|
||||
resp := rw.Result()
|
||||
if resp.StatusCode != expCode {
|
||||
t.Fatalf("got statuscode %d, expected %d", resp.StatusCode, expCode)
|
||||
t.Errorf("got statuscode %d, expected %d", resp.StatusCode, expCode)
|
||||
}
|
||||
if expContent != "" {
|
||||
s := rw.Body.String()
|
||||
if s != expContent {
|
||||
t.Fatalf("got response data %q, expected %q", s, expContent)
|
||||
t.Errorf("got response data %q, expected %q", s, expContent)
|
||||
}
|
||||
}
|
||||
for k, v := range expHeaders {
|
||||
if xv := resp.Header.Get(k); xv != v {
|
||||
t.Fatalf("got %q for header %q, expected %q", xv, k, v)
|
||||
t.Errorf("got %q for header %q, expected %q", xv, k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("GET", "http://mta-sts.mox.example/.well-known/mta-sts.txt", http.StatusOK, "mta-sts!", nil)
|
||||
test("GET", "http://mta-sts.mox.example/.well-known/mta-sts.txt", http.StatusOK, "version: STSv1\nmode: enforce\nmax_age: 86400\nmx: mox.example\n", nil)
|
||||
test("GET", "http://mox.example/.well-known/mta-sts.txt", http.StatusNotFound, "", nil) // mta-sts endpoint not in this domain.
|
||||
test("GET", "http://mta-sts.mox.example/static/", http.StatusNotFound, "", nil) // static not served on this domain.
|
||||
test("GET", "http://mta-sts.mox.example/other", http.StatusNotFound, "", nil)
|
||||
@ -66,4 +52,24 @@ func TestServeHTTP(t *testing.T) {
|
||||
test("GET", "http://mox.example/static/index.html", http.StatusOK, "html\n", map[string]string{"X-Test": "mox"})
|
||||
test("GET", "http://mox.example/static/dir/", http.StatusOK, "", map[string]string{"X-Test": "mox"}) // Dir listing.
|
||||
test("GET", "http://mox.example/other", http.StatusNotFound, "", nil)
|
||||
|
||||
// Webmail on IP, localhost, mail host, clientsettingsdomain, not others.
|
||||
test("GET", "http://127.0.0.1/webmail/", http.StatusOK, "", nil)
|
||||
test("GET", "http://localhost/webmail/", http.StatusOK, "", nil)
|
||||
test("GET", "http://mox.example/webmail/", http.StatusOK, "", nil)
|
||||
test("GET", "http://mail.mox.example/webmail/", http.StatusOK, "", nil)
|
||||
test("GET", "http://mail.other.example/webmail/", http.StatusNotFound, "", nil)
|
||||
test("GET", "http://remotehost/webmail/", http.StatusNotFound, "", nil)
|
||||
|
||||
// admin on IP, localhost, mail host, not clientsettingsdomain.
|
||||
test("GET", "http://127.0.0.1/admin/", http.StatusOK, "", nil)
|
||||
test("GET", "http://localhost/admin/", http.StatusOK, "", nil)
|
||||
test("GET", "http://mox.example/admin/", http.StatusPermanentRedirect, "", nil) // Override by WebHandler.
|
||||
test("GET", "http://mail.mox.example/admin/", http.StatusNotFound, "", nil)
|
||||
|
||||
// account is off.
|
||||
test("GET", "http://127.0.0.1/", http.StatusNotFound, "", nil)
|
||||
test("GET", "http://localhost/", http.StatusNotFound, "", nil)
|
||||
test("GET", "http://mox.example/", http.StatusNotFound, "", nil)
|
||||
test("GET", "http://mail.mox.example/", http.StatusNotFound, "", nil)
|
||||
}
|
||||
|
@ -46,13 +46,13 @@ func recvid(r *http.Request) string {
|
||||
// WebHandle runs after the built-in handlers for mta-sts, autoconfig, etc.
|
||||
// If no handler matched, false is returned.
|
||||
// WebHandle sets w.Name to that of the matching handler.
|
||||
func WebHandle(w *loggingWriter, r *http.Request, host dns.Domain) (handled bool) {
|
||||
func WebHandle(w *loggingWriter, r *http.Request, host dns.IPDomain) (handled bool) {
|
||||
conf := mox.Conf.DynamicConfig()
|
||||
redirects := conf.WebDNSDomainRedirects
|
||||
handlers := conf.WebHandlers
|
||||
|
||||
for from, to := range redirects {
|
||||
if host != from {
|
||||
if host.Domain != from {
|
||||
continue
|
||||
}
|
||||
u := r.URL
|
||||
@ -64,7 +64,7 @@ func WebHandle(w *loggingWriter, r *http.Request, host dns.Domain) (handled bool
|
||||
}
|
||||
|
||||
for _, h := range handlers {
|
||||
if host != h.DNSDomain {
|
||||
if host.Domain != h.DNSDomain {
|
||||
continue
|
||||
}
|
||||
loc := h.Path.FindStringIndex(r.URL.Path)
|
||||
@ -99,6 +99,10 @@ func WebHandle(w *loggingWriter, r *http.Request, host dns.Domain) (handled bool
|
||||
w.Handler = h.Name
|
||||
return true
|
||||
}
|
||||
if h.WebInternal != nil && HandleInternal(h.WebInternal, w, r) {
|
||||
w.Handler = h.Name
|
||||
return true
|
||||
}
|
||||
}
|
||||
w.Compress = false
|
||||
return false
|
||||
@ -396,6 +400,12 @@ func HandleRedirect(h *config.WebRedirect, w http.ResponseWriter, r *http.Reques
|
||||
return true
|
||||
}
|
||||
|
||||
// HandleInternal passes the request to an internal service.
|
||||
func HandleInternal(h *config.WebInternal, w http.ResponseWriter, r *http.Request) (handled bool) {
|
||||
h.Handler.ServeHTTP(w, r)
|
||||
return true
|
||||
}
|
||||
|
||||
// HandleForward handles a request by forwarding it to another webserver and
|
||||
// passing the response on. I.e. a reverse proxy. It handles websocket
|
||||
// connections by monitoring the websocket handshake and then just passing along the
|
||||
|
@ -134,6 +134,10 @@ func TestWebserver(t *testing.T) {
|
||||
|
||||
test("GET", "http://mox.example/bogus", nil, http.StatusNotFound, "", nil) // path not registered.
|
||||
test("GET", "http://bogus.mox.example/static/", nil, http.StatusNotFound, "", nil) // domain not registered.
|
||||
test("GET", "http://mox.example/xadmin/", nil, http.StatusOK, "", nil) // internal admin service
|
||||
test("GET", "http://mox.example/xaccount/", nil, http.StatusOK, "", nil) // internal account service
|
||||
test("GET", "http://mox.example/xwebmail/", nil, http.StatusOK, "", nil) // internal webmail service
|
||||
test("GET", "http://mox.example/xwebapi/v0/", nil, http.StatusOK, "", nil) // internal webapi service
|
||||
|
||||
npaths := len(staticgzcache.paths)
|
||||
if npaths != 1 {
|
||||
@ -335,5 +339,4 @@ func TestWebsocket(t *testing.T) {
|
||||
w.WriteHeader(http.StatusSwitchingProtocols)
|
||||
})
|
||||
test("GET", wsreqhdrs, http.StatusSwitchingProtocols, wsresphdrs)
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user