Files
opencloud/web/pkg/service/v0/service.go
2021-10-14 17:03:47 +02:00

224 lines
5.6 KiB
Go

package svc
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/web/pkg/assets"
"github.com/owncloud/ocis/web/pkg/config"
)
var (
// ErrConfigInvalid is returned when the config parse is invalid.
ErrConfigInvalid = `Invalid or missing config`
)
// Service defines the extension handlers.
type Service interface {
ServeHTTP(http.ResponseWriter, *http.Request)
Config(http.ResponseWriter, *http.Request)
}
// NewService returns a service implementation for Service.
func NewService(opts ...Option) Service {
options := newOptions(opts...)
m := chi.NewMux()
m.Use(options.Middleware...)
svc := Web{
logger: options.Logger,
config: options.Config,
mux: m,
}
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
r.Get("/config.json", svc.Config)
r.Mount("/", svc.Static(options.Config.HTTP.CacheTTL))
})
return svc
}
// Web defines implements the business logic for Service.
type Web struct {
logger log.Logger
config *config.Config
mux *chi.Mux
}
// ServeHTTP implements the Service interface.
func (p Web) ServeHTTP(w http.ResponseWriter, r *http.Request) {
p.mux.ServeHTTP(w, r)
}
func (p Web) getPayload() (payload []byte, err error) {
if p.config.Web.Path == "" {
// render dynamically using config
// provide default ocis-web options
if p.config.Web.Config.Options == nil {
p.config.Web.Config.Options = make(map[string]interface{})
p.config.Web.Config.Options["hideSearchBar"] = true
}
// build theme url
if themeServer, err := url.Parse(p.config.Web.ThemeServer); err == nil {
p.config.Web.Config.Theme = themeServer.String() + p.config.Web.ThemePath
} else {
p.config.Web.Config.Theme = p.config.Web.ThemePath
}
if p.config.Web.Config.ExternalApps == nil {
p.config.Web.Config.ExternalApps = []config.ExternalApp{
{
ID: "settings",
Path: "/settings.js",
},
{
ID: "accounts",
Path: "/accounts.js",
},
}
}
// make apps render as empty array if it is empty
// TODO remove once https://github.com/golang/go/issues/27589 is fixed
if len(p.config.Web.Config.Apps) == 0 {
p.config.Web.Config.Apps = make([]string, 0)
}
return json.Marshal(p.config.Web.Config)
}
// try loading from file
if _, err = os.Stat(p.config.Web.Path); os.IsNotExist(err) {
p.logger.Fatal().
Err(err).
Str("config", p.config.Web.Path).
Msg("web config doesn't exist")
}
payload, err = ioutil.ReadFile(p.config.Web.Path)
if err != nil {
p.logger.Fatal().
Err(err).
Str("config", p.config.Web.Path).
Msg("failed to read custom config")
}
return
}
// Config implements the Service interface.
func (p Web) Config(w http.ResponseWriter, r *http.Request) {
payload, err := p.getPayload()
if err != nil {
http.Error(w, ErrConfigInvalid, http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(payload); err != nil {
p.logger.Error().Err(err).Msg("could not write config response")
}
}
// Static simply serves all static files.
func (p Web) Static(ttl int) http.HandlerFunc {
rootWithSlash := p.config.HTTP.Root
if !strings.HasSuffix(rootWithSlash, "/") {
rootWithSlash = rootWithSlash + "/"
}
assets := assets.New(
assets.Logger(p.logger),
assets.Config(p.config),
)
notFoundFunc := func(w http.ResponseWriter, r *http.Request) {
// TODO: replace the redirect with a not found page containing a link to the Web UI
http.Redirect(w, r, rootWithSlash, http.StatusTemporaryRedirect)
}
static := http.StripPrefix(
rootWithSlash,
interceptNotFound(
http.FileServer(assets),
notFoundFunc,
),
)
// TODO: investigate broken caching - https://github.com/owncloud/ocis/issues/1094
// we don't have a last modification date of the static assets, so we use the service start date
//lastModified := time.Now().UTC().Format(http.TimeFormat)
//expires := time.Now().Add(time.Second * time.Duration(ttl)).UTC().Format(http.TimeFormat)
return func(w http.ResponseWriter, r *http.Request) {
if rootWithSlash != "/" && r.URL.Path == p.config.HTTP.Root {
http.Redirect(
w,
r,
rootWithSlash,
http.StatusMovedPermanently,
)
return
}
if r.URL.Path != rootWithSlash && strings.HasSuffix(r.URL.Path, "/") {
notFoundFunc(w, r)
return
}
// TODO: investigate broken caching - https://github.com/owncloud/ocis/issues/1094
//w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%s, must-revalidate", strconv.Itoa(ttl)))
//w.Header().Set("Expires", expires)
//w.Header().Set("Last-Modified", lastModified)
w.Header().Set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
w.Header().Set("SameSite", "Strict")
static.ServeHTTP(w, r)
}
}
func interceptNotFound(h http.Handler, notFoundFunc func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
notFoundInterceptor := &NotFoundInterceptor{ResponseWriter: w}
h.ServeHTTP(notFoundInterceptor, r)
if notFoundInterceptor.status == http.StatusNotFound {
notFoundFunc(w, r)
}
}
}
type NotFoundInterceptor struct {
http.ResponseWriter
status int
}
func (w *NotFoundInterceptor) WriteHeader(status int) {
w.status = status
if status != http.StatusNotFound {
w.ResponseWriter.WriteHeader(status)
}
}
func (w *NotFoundInterceptor) Write(p []byte) (int, error) {
if w.status != http.StatusNotFound {
return w.ResponseWriter.Write(p)
}
return len(p), nil
}