go-ssb-room/web/handlers/http.go

375 lines
10 KiB
Go
Raw Normal View History

2021-02-09 11:53:33 +00:00
// SPDX-License-Identifier: MIT
2021-02-04 10:36:02 +00:00
package handlers
import (
2021-02-15 16:21:06 +00:00
"errors"
2021-02-04 10:36:02 +00:00
"fmt"
"html/template"
2021-02-04 10:36:02 +00:00
"net/http"
"net/url"
"strconv"
2021-02-16 11:18:01 +00:00
"time"
2021-02-04 10:36:02 +00:00
"github.com/go-kit/kit/log/level"
2021-02-16 10:20:38 +00:00
"github.com/gorilla/csrf"
2021-02-08 11:57:14 +00:00
"github.com/gorilla/sessions"
"github.com/russross/blackfriday/v2"
2021-02-08 11:57:14 +00:00
"go.mindeco.de/http/auth"
2021-02-04 10:36:02 +00:00
"go.mindeco.de/http/render"
"go.mindeco.de/logging"
2021-02-04 10:36:02 +00:00
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
"github.com/ssb-ngi-pointer/go-ssb-room/web"
"github.com/ssb-ngi-pointer/go-ssb-room/web/handlers/admin"
roomsAuth "github.com/ssb-ngi-pointer/go-ssb-room/web/handlers/auth"
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
2021-03-22 14:25:00 +00:00
"github.com/ssb-ngi-pointer/go-ssb-room/web/members"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
2021-02-04 10:36:02 +00:00
)
2021-02-25 10:14:39 +00:00
var HTMLTemplates = []string{
"landing/index.tmpl",
"landing/about.tmpl",
2021-03-15 11:25:07 +00:00
"aliases-resolved.html",
"invite/consumed.tmpl",
"invite/facade.tmpl",
2021-02-23 19:23:50 +00:00
"notice/list.tmpl",
"notice/show.tmpl",
"error.tmpl",
}
// Databases is an options stuct for the required databases of the web handlers
type Databases struct {
2021-03-19 10:19:01 +00:00
Aliases roomdb.AliasesService
2021-03-10 15:44:46 +00:00
AuthFallback roomdb.AuthFallbackService
AuthWithSSB roomdb.AuthWithSSBService
2021-03-19 13:02:19 +00:00
DeniedKeys roomdb.DeniedKeysService
2021-03-19 10:19:01 +00:00
Invites roomdb.InvitesService
2021-03-10 15:44:46 +00:00
Notices roomdb.NoticesService
2021-03-19 10:19:01 +00:00
Members roomdb.MembersService
2021-03-10 15:44:46 +00:00
PinnedNotices roomdb.PinnedNoticesService
}
2021-02-04 16:21:21 +00:00
// New initializes the whole web stack for rooms, with all the sub-modules and routing.
2021-02-08 11:57:14 +00:00
func New(
logger logging.Interface,
2021-02-08 11:57:14 +00:00
repo repo.Interface,
netInfo network.ServerEndpointDetails,
roomState *roomstate.Manager,
roomEndpoints network.Endpoints,
bridge *signinwithssb.SignalBridge,
dbs Databases,
2021-02-08 11:57:14 +00:00
) (http.Handler, error) {
m := router.CompleteApp()
2021-02-04 10:36:02 +00:00
locHelper, err := i18n.New(repo)
if err != nil {
return nil, err
}
r, err := render.New(web.Templates,
render.SetLogger(logger),
render.BaseTemplates("base.tmpl", "menu.tmpl"),
2021-02-08 11:57:14 +00:00
render.AddTemplates(concatTemplates(
2021-02-25 10:14:39 +00:00
HTMLTemplates,
2021-02-08 16:47:42 +00:00
roomsAuth.HTMLTemplates,
2021-02-08 11:57:14 +00:00
admin.HTMLTemplates,
)...),
2021-02-22 15:20:26 +00:00
render.ErrorTemplate("error.tmpl"),
2021-02-04 11:00:12 +00:00
render.FuncMap(web.TemplateFuncs(m)),
2021-02-04 16:21:21 +00:00
// TODO: move these to the i18n helper pkg
render.InjectTemplateFunc("i18npl", func(r *http.Request) interface{} {
2021-02-15 16:21:06 +00:00
loc := i18n.LocalizerFromRequest(locHelper, r)
return loc.LocalizePlurals
}),
render.InjectTemplateFunc("i18n", func(r *http.Request) interface{} {
2021-02-15 16:21:06 +00:00
loc := i18n.LocalizerFromRequest(locHelper, r)
return loc.LocalizeSimple
}),
render.InjectTemplateFunc("current_page_is", func(r *http.Request) interface{} {
return func(routeName string) bool {
route := m.Get(routeName)
if route == nil {
return false
}
url, err := route.URLPath()
if err != nil {
return false
}
return r.RequestURI == url.Path
}
}),
render.InjectTemplateFunc("urlToNotice", func(r *http.Request) interface{} {
return func(name string) *url.URL {
2021-03-10 15:44:46 +00:00
noticeName := roomdb.PinnedNoticeName(name)
if !noticeName.Valid() {
return nil
}
notice, err := dbs.PinnedNotices.Get(r.Context(), noticeName, "en-GB")
if err != nil {
return nil
}
route := m.GetRoute(router.CompleteNoticeShow)
if route == nil {
return nil
}
u, err := route.URLPath()
if err != nil {
return nil
}
noticeID := strconv.FormatInt(notice.ID, 10)
q := u.Query()
q.Add("id", noticeID)
u.RawQuery = q.Encode()
return u
}
}),
2021-03-22 14:25:00 +00:00
render.InjectTemplateFunc("is_logged_in", members.TemplateHelper()),
2021-02-04 10:36:02 +00:00
)
if err != nil {
return nil, fmt.Errorf("web Handler: failed to create renderer: %w", err)
}
2021-02-08 16:47:42 +00:00
cookieCodec, err := web.LoadOrCreateCookieSecrets(repo)
if err != nil {
return nil, err
}
cookieStore := &sessions.CookieStore{
2021-02-08 16:47:42 +00:00
Codecs: cookieCodec,
2021-02-08 11:57:14 +00:00
Options: &sessions.Options{
Path: "/",
2021-02-16 11:18:01 +00:00
MaxAge: 2 * 60 * 60, // two hours in seconds // TODO: configure
2021-02-08 11:57:14 +00:00
},
}
2021-02-15 16:21:06 +00:00
// TODO: this is just the error handler for http/auth, not render
2021-02-09 12:40:57 +00:00
authErrH := func(rw http.ResponseWriter, req *http.Request, err error, code int) {
2021-02-15 16:21:06 +00:00
var ih = i18n.LocalizerFromRequest(locHelper, req)
// default, unlocalized message
2021-02-09 12:40:57 +00:00
msg := err.Error()
// localize some specific error messages
2021-02-15 16:21:06 +00:00
var (
2021-03-10 15:44:46 +00:00
aa roomdb.ErrAlreadyAdded
2021-02-15 16:21:06 +00:00
)
switch {
case err == auth.ErrBadLogin:
msg = ih.LocalizeSimple("AuthErrorBadLogin")
2021-03-10 15:44:46 +00:00
case errors.Is(err, roomdb.ErrNotFound):
2021-02-15 16:21:06 +00:00
msg = ih.LocalizeSimple("ErrorNotFound")
case errors.As(err, &aa):
msg = ih.LocalizeSimple("ErrorAlreadyAdded")
2021-02-09 12:40:57 +00:00
}
r.HTML("error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
2021-02-09 12:40:57 +00:00
return errorTemplateData{
Err: msg,
// TODO: localize?
Status: http.StatusText(code),
StatusCode: code,
}, nil
}).ServeHTTP(rw, req)
}
notAuthorizedH := r.HTML("error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
2021-02-08 11:57:14 +00:00
statusCode := http.StatusUnauthorized
rw.WriteHeader(statusCode)
return errorTemplateData{
statusCode,
"Unauthorized",
"you are not authorized to access the requested site",
}, nil
})
authWithPassword, err := auth.NewHandler(dbs.AuthFallback,
auth.SetStore(cookieStore),
2021-02-09 12:40:57 +00:00
auth.SetErrorHandler(authErrH),
2021-02-08 11:57:14 +00:00
auth.SetNotAuthorizedHandler(notAuthorizedH),
2021-02-16 11:18:01 +00:00
auth.SetLifetime(2*time.Hour), // TODO: configure
2021-02-08 11:57:14 +00:00
)
if err != nil {
return nil, fmt.Errorf("web Handler: failed to init fallback auth system: %w", err)
}
2021-02-16 10:20:38 +00:00
// Cross Site Request Forgery prevention middleware
csrfKey, err := web.LoadOrCreateCSRFSecret(repo)
if err != nil {
return nil, err
}
CSRF := csrf.Protect(csrfKey,
csrf.ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
err := csrf.FailureReason(req)
// TODO: localize error?
r.Error(w, req, http.StatusForbidden, err)
})),
)
2021-02-11 15:43:19 +00:00
// this router is a bit of a qurik
// TODO: explain problem between gorilla/mux named routers and authentication
mainMux := &http.ServeMux{}
// start hooking up handlers to the router
authWithSSB := roomsAuth.NewWithSSBHandler(
m,
r,
netInfo,
roomEndpoints,
dbs.Aliases,
dbs.Members,
dbs.AuthWithSSB,
cookieStore,
bridge,
)
2021-03-26 15:30:57 +00:00
m.Get(router.AuthLogin).Handler(r.StaticHTML("auth/decide_method.tmpl"))
m.Get(router.AuthFallbackFinalize).HandlerFunc(authWithPassword.Authorize)
m.Get(router.AuthFallbackLogin).Handler(r.HTML("auth/fallback_sign_in.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
return map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(req),
}, nil
}))
2021-02-08 11:57:14 +00:00
2021-03-25 17:38:21 +00:00
m.Get(router.AuthLogout).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
err = authWithSSB.Logout(w, req)
if err != nil {
level.Warn(logging.FromContext(req.Context())).Log("err", err)
}
authWithPassword.Logout(w, req)
})
adminHandler := admin.Handler(
netInfo.Domain,
r,
2021-03-03 12:58:06 +00:00
roomState,
admin.Databases{
Aliases: dbs.Aliases,
2021-03-19 13:02:19 +00:00
DeniedKeys: dbs.DeniedKeys,
Invites: dbs.Invites,
Notices: dbs.Notices,
2021-03-19 13:02:19 +00:00
Members: dbs.Members,
PinnedNotices: dbs.PinnedNotices,
},
)
mainMux.Handle("/admin/", members.AuthenticateFromContext(r)(adminHandler))
2021-02-04 10:36:02 +00:00
m.Get(router.CompleteIndex).Handler(r.HTML("landing/index.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
2021-03-10 15:44:46 +00:00
notice, err := dbs.PinnedNotices.Get(req.Context(), roomdb.NoticeDescription, "en-GB")
if err != nil {
return nil, fmt.Errorf("failed to find description: %w", err)
}
markdown := blackfriday.Run([]byte(notice.Content), blackfriday.WithNoExtensions())
return noticeShowData{
ID: notice.ID,
Title: notice.Title,
Content: template.HTML(markdown),
Language: notice.Language,
}, nil
}))
m.Get(router.CompleteAbout).Handler(r.StaticHTML("landing/about.tmpl"))
2021-02-04 10:36:02 +00:00
var nh = noticeHandler{
notices: dbs.Notices,
pinned: dbs.PinnedNotices,
}
m.Get(router.CompleteNoticeList).Handler(r.HTML("notice/list.tmpl", nh.list))
m.Get(router.CompleteNoticeShow).Handler(r.HTML("notice/show.tmpl", nh.show))
2021-03-15 11:25:07 +00:00
var ah = aliasHandler{
r: r,
db: dbs.Aliases,
roomEndpoint: netInfo,
2021-03-15 11:25:07 +00:00
}
m.Get(router.CompleteAliasResolve).HandlerFunc(ah.resolve)
var ih = inviteHandler{
render: r,
invites: dbs.Invites,
networkInfo: netInfo,
}
m.Get(router.CompleteInviteFacade).Handler(r.HTML("invite/facade.tmpl", ih.presentFacade))
m.Get(router.CompleteInviteConsume).HandlerFunc(ih.consume)
m.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets)))
2021-02-04 16:46:51 +00:00
m.NotFoundHandler = r.HTML("error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
2021-02-08 11:57:14 +00:00
rw.WriteHeader(http.StatusNotFound)
2021-02-15 16:21:06 +00:00
msg := i18n.LocalizerFromRequest(locHelper, req).LocalizeSimple("PageNotFound")
return errorTemplateData{http.StatusNotFound, "Not Found", msg}, nil
2021-02-04 10:36:02 +00:00
})
2021-02-11 15:43:19 +00:00
mainMux.Handle("/", m)
urlTo := web.NewURLTo(m)
consumeURL := urlTo(router.CompleteInviteConsume)
// apply HTTP middleware
middlewares := []func(http.Handler) http.Handler{
logging.InjectHandler(logger),
members.ContextInjecter(dbs.Members, authWithPassword, authWithSSB),
CSRF,
// We disable CSRF for certain requests that are done by apps
// only if they already contain some secret (like invite consumption)
func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ct := req.Header.Get("Content-Type")
if req.URL.Path == consumeURL.Path && ct == "application/json" {
next.ServeHTTP(w, csrf.UnsafeSkipCheck(req))
return
}
next.ServeHTTP(w, req)
})
},
}
if !web.Production {
middlewares = append(middlewares, r.GetReloader())
}
var finalHandler http.Handler = mainMux
for _, applyMiddleware := range middlewares {
finalHandler = applyMiddleware(finalHandler)
2021-02-08 11:57:14 +00:00
}
return finalHandler, nil
2021-02-04 10:36:02 +00:00
}
2021-02-08 11:57:14 +00:00
// utils
type errorTemplateData struct {
StatusCode int
Status string
Err string
}
func concatTemplates(lst ...[]string) []string {
var catted []string
for _, tpls := range lst {
for _, t := range tpls {
catted = append(catted, t)
}
}
return catted
}