diff --git a/build/Dockerfile b/build/Dockerfile index 0bd4cef..c1ce97c 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -23,7 +23,7 @@ WORKDIR / COPY --from=builder /app/home /home -VOLUME ["/content", "/theme"] +VOLUME ["/content", "/theme", "/static"] EXPOSE 3000 diff --git a/internal/domain/config.go b/internal/domain/config.go index d80c371..9072a4e 100644 --- a/internal/domain/config.go +++ b/internal/domain/config.go @@ -11,6 +11,7 @@ type Config struct { ContentDir string `env:"CONTENT_DIR" envDefault:"content/"` Host string `env:"HOST" envDefault:"0.0.0.0"` ThemeDir string `env:"THEME_DIR" envDefault:"theme/"` + StaticDir string `env:"STATIC_DIR" envDefault:"static/"` Port uint16 `env:"PORT" envDefault:"3000"` } @@ -21,6 +22,7 @@ func TestConfig(tb testing.TB) *Config { ContentDir: "testdata/content/", Host: "0.0.0.0", ThemeDir: "testdata/theme/", + StaticDir: "testdata/static/", Port: 3000, } } diff --git a/main.go b/main.go index 52bc7ca..7f39afa 100644 --- a/main.go +++ b/main.go @@ -63,20 +63,23 @@ var cpuProfilePath, memProfilePath string func NewApp(ctx context.Context, config *domain.Config) (*App, error) { themeDir := os.DirFS(config.ThemeDir) contentDir := os.DirFS(config.ContentDir) - statics := staticfsrepo.NewFileServerStaticRepository(contentDir) + resources := staticfsrepo.NewFileServerStaticRepository(contentDir) sites := sitefsrepo.NewFileSystemSiteRepository(contentDir) - siter := siteucase.NewSiteUseCase(sites, statics) + siter := siteucase.NewSiteUseCase(sites, resources) funcMap, err := templateutil.New(themeDir, siter) if err != nil { logger.Fatalln("cannot setup template.FuncMap for templates: %w", err) } - staticer := staticucase.NewStaticUseCase(statics) + staticDir := os.DirFS(config.StaticDir) + statics := staticfsrepo.NewFileServerStaticRepository(staticDir) + // TODO(toby3d): use exists static use case or split that on static and resource modules? + resourcer := staticucase.NewStaticUseCase(resources) themes := themefsrepo.NewFileSystemThemeRepository(themeDir, funcMap) themer := themeucase.NewThemeUseCase(themes) pages := pagefsrepo.NewFileSystemPageRepository(contentDir) - pager := pageucase.NewPageUseCase(pages, statics) + pager := pageucase.NewPageUseCase(pages, resources) matcher := language.NewMatcher(message.DefaultCatalog.Languages()) @@ -98,32 +101,18 @@ func NewApp(ctx context.Context, config *domain.Config) (*App, error) { return } - lang := domain.NewLanguage(head) - if lang == domain.LanguageUnd { - // WARN(toby3d): fetch static resources from separated static directory instead of - // $HOME_CONTENT_DIR? - // - // Looks like what current logic is insecure, because resource from private page in - // content directory '/en/page/file.jpg' by lower use case execution can be accessed - // here by URL '/page/file.jpg'. - res, err := staticer.Do(r.Context(), r.URL.Path) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - http.Error(w, err.Error(), http.StatusNotFound) - - return - } - - http.Error(w, err.Error(), http.StatusInternalServerError) - - return - } - - http.ServeContent(w, r, res.Name(), domain.ResourceModTime(res), res) + // TODO(toby3d): use exists static use case or split that on static and resource modules? + // INFO(toby3d): any static file is public and unprotected by design, so it's safe to search it + // first before deep down to any page or it's resource which might be secured by middleware or + // something else. + static, err := statics.Get(r.Context(), strings.TrimPrefix(r.URL.Path, "/")) + if err == nil { + http.ServeContent(w, r, static.Name(), domain.ResourceModTime(static), static) return } + lang := domain.NewLanguage(head) r.URL.Path = tail s, err := siter.Do(r.Context(), lang) @@ -141,7 +130,7 @@ func NewApp(ctx context.Context, config *domain.Config) (*App, error) { return } - res, err := staticer.Do(r.Context(), r.URL.Path) + res, err := resourcer.Do(r.Context(), r.URL.Path) if err != nil { if errors.Is(err, fs.ErrNotExist) { http.Error(w, err.Error(), http.StatusNotFound) @@ -225,6 +214,7 @@ func main() { for _, dir := range []*string{ &config.ContentDir, &config.ThemeDir, + &config.StaticDir, } { if *dir, err = filepath.Abs(filepath.Clean(*dir)); err != nil { logger.Fatalf("cannot format '%s' into absolute path: %s", *dir, err)