diff --git a/internal/httputil/paths.go b/internal/httputil/paths.go index 12cf59eb4..62eff0415 100644 --- a/internal/httputil/paths.go +++ b/internal/httputil/paths.go @@ -19,6 +19,7 @@ const ( PublicFederationPathPrefix = "/_matrix/federation/" PublicKeyPathPrefix = "/_matrix/key/" PublicMediaPathPrefix = "/_matrix/media/" + PublicStaticPath = "/_matrix/static/" PublicWellKnownPrefix = "/.well-known/matrix/" InternalPathPrefix = "/api/" DendriteAdminPathPrefix = "/_dendrite/" diff --git a/setup/base/base.go b/setup/base/base.go index de8f81517..6ea68119d 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -15,11 +15,14 @@ package base import ( + "bytes" "context" "crypto/tls" "database/sql" + "embed" "encoding/json" "fmt" + "html/template" "io" "net" "net/http" @@ -65,6 +68,9 @@ import ( userapiinthttp "github.com/matrix-org/dendrite/userapi/inthttp" ) +//go:embed static/*.gotmpl +var staticContent embed.FS + // BaseDendrite is a base for creating new instances of dendrite. It parses // command line flags and config, and exposes methods for creating various // resources. All errors are handled by logging then exiting, so all methods @@ -79,6 +85,7 @@ type BaseDendrite struct { PublicKeyAPIMux *mux.Router PublicMediaAPIMux *mux.Router PublicWellKnownAPIMux *mux.Router + PublicStaticMux *mux.Router InternalAPIMux *mux.Router DendriteAdminMux *mux.Router SynapseAdminMux *mux.Router @@ -250,6 +257,7 @@ func NewBaseDendrite(cfg *config.Dendrite, componentName string, options ...Base PublicKeyAPIMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicKeyPathPrefix).Subrouter().UseEncodedPath(), PublicMediaAPIMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicMediaPathPrefix).Subrouter().UseEncodedPath(), PublicWellKnownAPIMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicWellKnownPrefix).Subrouter().UseEncodedPath(), + PublicStaticMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicStaticPath).Subrouter().UseEncodedPath(), InternalAPIMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.InternalPathPrefix).Subrouter().UseEncodedPath(), DendriteAdminMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.DendriteAdminPathPrefix).Subrouter().UseEncodedPath(), SynapseAdminMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.SynapseAdminPathPrefix).Subrouter().UseEncodedPath(), @@ -405,6 +413,7 @@ func (b *BaseDendrite) configureHTTPErrors() { for _, router := range []*mux.Router{ b.PublicMediaAPIMux, b.DendriteAdminMux, b.SynapseAdminMux, b.PublicWellKnownAPIMux, + b.PublicStaticMux, } { router.NotFoundHandler = notFoundCORSHandler router.MethodNotAllowedHandler = notAllowedCORSHandler @@ -478,6 +487,11 @@ func (b *BaseDendrite) SetupAndServeHTTP( b.configureHTTPErrors() + //Redirect for Landing Page + externalRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, httputil.PublicStaticPath, http.StatusFound) + }) + internalRouter.PathPrefix(httputil.InternalPathPrefix).Handler(b.InternalAPIMux) if b.Cfg.Global.Metrics.Enabled { internalRouter.Handle("/metrics", httputil.WrapHandlerInBasicAuth(promhttp.Handler(), b.Cfg.Global.Metrics.BasicAuth)) @@ -485,6 +499,19 @@ func (b *BaseDendrite) SetupAndServeHTTP( b.ConfigureAdminEndpoints() + // Parse and execute the landing page template + tmpl := template.Must(template.ParseFS(staticContent, "static/*.gotmpl")) + landingPage := &bytes.Buffer{} + if err := tmpl.ExecuteTemplate(landingPage, "index.gotmpl", map[string]string{ + "Version": internal.VersionString(), + }); err != nil { + logrus.WithError(err).Fatal("failed to execute landing page template") + } + + b.PublicStaticMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write(landingPage.Bytes()) + }) + var clientHandler http.Handler clientHandler = b.PublicClientAPIMux if b.Cfg.Global.Sentry.Enabled { @@ -510,6 +537,7 @@ func (b *BaseDendrite) SetupAndServeHTTP( externalRouter.PathPrefix(httputil.SynapseAdminPathPrefix).Handler(b.SynapseAdminMux) externalRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(b.PublicMediaAPIMux) externalRouter.PathPrefix(httputil.PublicWellKnownPrefix).Handler(b.PublicWellKnownAPIMux) + externalRouter.PathPrefix(httputil.PublicStaticPath).Handler(b.PublicStaticMux) b.startupLock.Unlock() if internalAddr != NoListener && internalAddr != externalAddr { diff --git a/setup/base/base_test.go b/setup/base/base_test.go new file mode 100644 index 000000000..61cb530a9 --- /dev/null +++ b/setup/base/base_test.go @@ -0,0 +1,57 @@ +package base_test + +import ( + "bytes" + "embed" + "html/template" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/matrix-org/dendrite/internal" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test/testrig" + "github.com/stretchr/testify/assert" +) + +//go:embed static/*.gotmpl +var staticContent embed.FS + +func TestLandingPage(t *testing.T) { + // generate the expected result + tmpl := template.Must(template.ParseFS(staticContent, "static/*.gotmpl")) + expectedRes := &bytes.Buffer{} + err := tmpl.ExecuteTemplate(expectedRes, "index.gotmpl", map[string]string{ + "Version": internal.VersionString(), + }) + assert.NoError(t, err) + + b, _, _ := testrig.Base(nil) + defer b.Close() + + // hack: create a server and close it immediately, just to get a random port assigned + s := httptest.NewServer(nil) + s.Close() + + // start base with the listener and wait for it to be started + go b.SetupAndServeHTTP("", config.HTTPAddress(s.URL), nil, nil) + time.Sleep(time.Millisecond * 10) + + // When hitting /, we should be redirected to /_matrix/static, which should contain the landing page + req, err := http.NewRequest(http.MethodGet, s.URL, nil) + assert.NoError(t, err) + + // do the request + resp, err := s.Client().Do(req) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + // read the response + buf := &bytes.Buffer{} + _, err = buf.ReadFrom(resp.Body) + assert.NoError(t, err) + + // Using .String() for user friendly output + assert.Equal(t, expectedRes.String(), buf.String(), "response mismatch") +} diff --git a/setup/base/static/index.gotmpl b/setup/base/static/index.gotmpl new file mode 100644 index 000000000..b3c5576eb --- /dev/null +++ b/setup/base/static/index.gotmpl @@ -0,0 +1,63 @@ + + +
+Your Dendrite server is listening on this port and is ready for messages.
+To use this server you'll need a Matrix client. +
+Welcome to the Matrix universe :)
++ + + matrix.org + + +
+ +