Skip to content

Commit

Permalink
fix(server): env vars to change html title and favicon (#390)
Browse files Browse the repository at this point in the history
  • Loading branch information
rot1024 committed Mar 23, 2023
1 parent e6e79e3 commit a9910cb
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 9 deletions.
2 changes: 2 additions & 0 deletions server/internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ func initEcho(ctx context.Context, cfg *ServerConfig) *echo.Echo {
WebConfig: cfg.Config.WebConfig(),
AuthConfig: cfg.Config.AuthForWeb(),
HostPattern: cfg.Config.Published.Host,
Title: cfg.Config.Web_Title,
FaviconURL: cfg.Config.Web_FaviconURL,
FS: nil,
}).Handler(e)

Expand Down
2 changes: 2 additions & 0 deletions server/internal/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ type Config struct {
Web_App_Disabled bool
Web map[string]string
Web_Config JSON
Web_Title string
Web_FaviconURL string
SignupSecret string
SignupDisabled bool
// auth
Expand Down
37 changes: 33 additions & 4 deletions server/internal/app/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/reearth/reearthx/log"
"github.com/spf13/afero"
)

Expand All @@ -15,6 +16,8 @@ type WebHandler struct {
WebConfig map[string]any
AuthConfig *AuthConfig
HostPattern string
Title string
FaviconURL string
FS afero.Fs
}

Expand All @@ -26,10 +29,31 @@ func (w *WebHandler) Handler(e *echo.Echo) {
if w.FS == nil {
w.FS = afero.NewOsFs()
}

if _, err := w.FS.Stat("web"); err != nil {
return // web won't be delivered
}

// favicon
var favicon []byte
var faviconPath string
var err error
if w.FaviconURL != "" {
favicon, err = fetchFavicon(w.FaviconURL)
if err != nil {
log.Errorf("web: failed to fetch favicon from %s", w.FaviconURL)
return
}
faviconPath = "/favicon.ico"
}

// fs
hfs, err := NewRewriteHTMLFS(w.FS, "web", w.Title, faviconPath)
if err != nil {
log.Errorf("web: failed to init fs: %v", err)
return
}

e.Logger.Info("web: web directory will be delivered\n")

config := map[string]any{}
Expand All @@ -52,24 +76,29 @@ func (w *WebHandler) Handler(e *echo.Echo) {
config[k] = v
}

m := middleware.StaticWithConfig(middleware.StaticConfig{
static := middleware.StaticWithConfig(middleware.StaticConfig{
Root: "web",
Index: "index.html",
Browse: false,
HTML5: true,
Filesystem: afero.NewHttpFs(w.FS),
Filesystem: hfs,
})
notFound := func(c echo.Context) error { return echo.ErrNotFound }

e.GET("/reearth_config.json", func(c echo.Context) error {
return c.JSON(http.StatusOK, config)
})
e.GET("/data.json", PublishedData(w.HostPattern, false))
if favicon != nil && faviconPath != "" {
e.GET(faviconPath, func(c echo.Context) error {
return c.Blob(http.StatusOK, "image/vnd.microsoft.icon", favicon)
})
}
e.GET("/index.html", func(c echo.Context) error {
return c.Redirect(http.StatusPermanentRedirect, "/")
})
e.GET("/", notFound, PublishedIndexMiddleware(w.HostPattern, false, w.AppDisabled), m)
e.GET("*", notFound, m)
e.GET("/", notFound, PublishedIndexMiddleware(w.HostPattern, false, w.AppDisabled), static)
e.GET("*", notFound, static)
}

func (w *WebHandler) hostWithSchema() string {
Expand Down
150 changes: 150 additions & 0 deletions server/internal/app/web_index.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package app

import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"path"
"regexp"
"time"

"github.com/spf13/afero"
)

var reTitle = regexp.MustCompile("<title>(.+?)</title>")
var reFavicon = regexp.MustCompile("<link rel=\"icon\" href=\"(.+?)\"")

func rewriteHTML(html, title, favicon string) string {
if title != "" {
html = reTitle.ReplaceAllString(html, fmt.Sprintf("<title>%s</title>", title))
}
if favicon != "" {
html = reFavicon.ReplaceAllString(html, fmt.Sprintf("<link rel=\"icon\" href=\"%s\"", favicon))
}
return html
}

type AdapterFS struct {
FSU afero.Fs
FS afero.Fs
}

var _ afero.Fs = (*AdapterFS)(nil)

func (fs *AdapterFS) Create(name string) (afero.File, error) {
return fs.FS.Create(name)
}

func (fs *AdapterFS) Mkdir(name string, perm os.FileMode) error {
return fs.FS.Mkdir(name, perm)
}

func (fs *AdapterFS) MkdirAll(path string, perm os.FileMode) error {
return fs.FS.MkdirAll(path, perm)
}

func (fs *AdapterFS) Open(name string) (afero.File, error) {
if f, err := fs.FSU.Open(name); err == nil {
return f, nil
} else if !os.IsNotExist(err) {
return nil, err
}
return fs.FS.Open(name)
}

func (fs *AdapterFS) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
if f, err := fs.FSU.OpenFile(name, flag, perm); err == nil {
return f, nil
} else if !os.IsNotExist(err) {
return nil, err
}
return fs.FS.OpenFile(name, flag, perm)
}

func (fs *AdapterFS) Remove(name string) error {
return fs.FS.Remove(name)
}

func (fs *AdapterFS) RemoveAll(path string) error {
return fs.FS.RemoveAll(path)
}

func (fs *AdapterFS) Rename(oldname, newname string) error {
return fs.FS.Rename(oldname, newname)
}

func (fs *AdapterFS) Stat(name string) (os.FileInfo, error) {
if f, err := fs.FSU.Stat(name); err == nil {
return f, nil
} else if !os.IsNotExist(err) {
return nil, err
}
return fs.FS.Stat(name)
}

func (fs *AdapterFS) Name() string {
return "adapter"
}

func (fs *AdapterFS) Chmod(name string, mode os.FileMode) error {
return fs.FS.Chmod(name, mode)
}

func (fs *AdapterFS) Chown(name string, uid, gid int) error {
return fs.FS.Chown(name, uid, gid)
}

func (fs *AdapterFS) Chtimes(name string, atime time.Time, mtime time.Time) error {
return fs.FS.Chtimes(name, atime, mtime)
}

func NewRewriteHTMLFS(f afero.Fs, base, title, favicon string) (http.FileSystem, error) {
index, err := afero.ReadFile(f, path.Join(base, "index.html"))
if err != nil {
return nil, err
}

indexs := rewriteHTML(string(index), title, favicon)
mfs := afero.NewMemMapFs()
if err := afero.WriteFile(mfs, path.Join(base, "index.html"), []byte(indexs), 0666); err != nil {
return nil, err
}

published, err := afero.ReadFile(f, path.Join(base, "published.html"))
if err != nil {
return nil, err
}

indexps := rewriteHTML(string(published), title, favicon)
if err := afero.WriteFile(mfs, path.Join(base, "published.html"), []byte(indexps), 0666); err != nil {
return nil, err
}

afs := &AdapterFS{FSU: mfs, FS: f}
return afero.NewHttpFs(afs), nil
}

func fetchFavicon(url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status code is %d", res.StatusCode)
}
defer func() {
_ = res.Body.Close()
}()
b := &bytes.Buffer{}
_, _ = io.Copy(b, res.Body)
return b.Bytes(), nil
}
38 changes: 38 additions & 0 deletions server/internal/app/web_index_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package app

import (
"testing"

"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
)

func TestRewriteHTML(t *testing.T) {
html := `<head><title>XXX</title><meta charset="utf8" /><link rel="icon" href="aaa" /></head>`
assert.Equal(t, `<head><title>hoge</title><meta charset="utf8" /><link rel="icon" href="favicon" /></head>`, rewriteHTML(html, "hoge", "favicon"))
}

func TestAdapterFS(t *testing.T) {
fs1 := afero.NewMemMapFs()
_ = afero.WriteFile(fs1, "aaaa", []byte("aaa"), 0666)
fs2 := afero.NewMemMapFs()
_ = afero.WriteFile(fs2, "aaaa", []byte("xxx"), 0666)
_ = afero.WriteFile(fs2, "bbbb", []byte("bbb"), 0666)

a := &AdapterFS{
FSU: fs1,
FS: fs2,
}

d, err := afero.ReadFile(a, "aaaa")
assert.NoError(t, err)
assert.Equal(t, "aaa", string(d))

d, err = afero.ReadFile(a, "bbbb")
assert.NoError(t, err)
assert.Equal(t, "bbb", string(d))

d, err = afero.ReadFile(a, "ccc")
assert.ErrorContains(t, err, "file does not exist")
assert.Nil(t, d)
}
25 changes: 20 additions & 5 deletions server/internal/app/web_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"testing"

"github.com/jarcoal/httpmock"
"github.com/labstack/echo/v4"
"github.com/reearth/reearth/server/internal/adapter"
"github.com/reearth/reearth/server/internal/infrastructure/fs"
Expand All @@ -22,7 +23,12 @@ import (
)

func TestWeb(t *testing.T) {
const indexHTML = `<html><head><meta charset="utf-8" /><title>Re:Earth</title></head></html>`
httpmock.Activate()
defer httpmock.Deactivate()
httpmock.RegisterResponder("GET", "https://example.com/favicon.ico", httpmock.NewBytesResponder(http.StatusOK, []byte("icon")))

const indexHTML = `<html><head><meta charset="utf-8" /><title>Re:Earth</title><link rel="icon" href="favicon.ico" /></head></html>`
const indexHTML2 = `<html><head><meta charset="utf-8" /><title>title</title><link rel="icon" href="/favicon.ico" /></head></html>`
const publishedHTML = `<html><head><meta charset="utf-8" /><title>Re:Earth Published</title></head></html>`
const testJS = `console.log("hello, world");`
const dataJSON = `{"data":"data"}`
Expand Down Expand Up @@ -73,7 +79,7 @@ func TestWeb(t *testing.T) {
name: "invalid path should serve index.html",
path: "/not_found.js",
statusCode: http.StatusOK,
body: indexHTML,
body: indexHTML2,
contentType: "text/html; charset=utf-8",
},
{
Expand Down Expand Up @@ -109,7 +115,7 @@ func TestWeb(t *testing.T) {
name: "index file without host",
path: "/",
statusCode: http.StatusOK,
body: indexHTML,
body: indexHTML2,
contentType: "text/html; charset=utf-8",
},
{
Expand All @@ -123,7 +129,7 @@ func TestWeb(t *testing.T) {
path: "/",
host: "aaa.example2.com",
statusCode: http.StatusOK,
body: indexHTML,
body: indexHTML2,
contentType: "text/html; charset=utf-8",
},
{
Expand All @@ -145,12 +151,19 @@ func TestWeb(t *testing.T) {
assert.Contains(t, body, "DESC")
},
},
{
name: "favicon",
path: "/favicon.ico",
statusCode: http.StatusOK,
body: "icon",
contentType: "image/vnd.microsoft.icon",
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// t.Parallel()

e := echo.New()
e.HTTPErrorHandler = func(err error, c echo.Context) {
Expand Down Expand Up @@ -180,6 +193,8 @@ func TestWeb(t *testing.T) {
},
HostPattern: `{}.example.com`,
FS: mfs,
Title: "title",
FaviconURL: "https://example.com/favicon.ico",
}).Handler(e)

r := httptest.NewRequest("GET", tt.path, nil)
Expand Down

0 comments on commit a9910cb

Please sign in to comment.