go-ssb-room/web/utils.go
2021-05-10 13:52:45 +02:00

195 lines
5.9 KiB
Go

// SPDX-License-Identifier: MIT
package web
import (
"fmt"
"html/template"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/dustin/go-humanize"
"github.com/go-kit/kit/log/level"
"github.com/gorilla/mux"
"github.com/gorilla/securecookie"
"go.mindeco.de/logging"
"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/roomdb"
refs "go.mindeco.de/ssb-refs"
)
// TemplateFuncs returns a map of template functions
func TemplateFuncs(m *mux.Router, netInfo network.ServerEndpointDetails) template.FuncMap {
return template.FuncMap{
"human_time": func(when time.Time) string {
return humanize.Time(when)
},
"urlTo": NewURLTo(m, netInfo),
"inc": func(i int) int { return i + 1 },
}
}
type URLMaker func(string, ...interface{}) *url.URL
// NewURLTo returns a template helper function for a router.
// It is usually called with one parameter, the route name, which should be defined in the router package.
// If it's called with more then one, it has a to be a pair of two values. (1, 3, 5, 7, etc.)
// The first value of such a pair is the placeholder name in the router (i.e. in '/our/routes/{id:[0-9]+}/test' it would be id )
// and the 2nd value is the actual value that should be put in place of the placeholder.
func NewURLTo(appRouter *mux.Router, netInfo network.ServerEndpointDetails) URLMaker {
l := logging.Logger("helper.URLTo") // TOOD: inject in a scoped way
return func(routeName string, ps ...interface{}) *url.URL {
route := appRouter.Get(routeName)
if route == nil {
// TODO: https://github.com/ssb-ngi-pointer/go-ssb-room/issues/35 for a
// for reference, see https://github.com/ssb-ngi-pointer/go-ssb-room/pull/64
// level.Warn(l).Log("msg", "no such route", "route", routeName, "params", fmt.Sprintf("%v", ps))
return &url.URL{}
}
if len(ps)%2 != 0 {
level.Warn(l).Log("msg", "expected even number of params (name-value pairs)", "route", routeName, "params", fmt.Sprintf("%v", ps))
return &url.URL{}
}
var params []string
for _, p := range ps {
switch v := p.(type) {
case string:
params = append(params, v)
case int:
params = append(params, strconv.Itoa(v))
case int64:
params = append(params, strconv.FormatInt(v, 10))
case refs.FeedRef:
params = append(params, v.Ref())
case roomdb.PinnedNoticeName:
params = append(params, string(v))
default:
level.Error(l).Log("msg", "invalid param type", "param", fmt.Sprintf("%T", p), "route", routeName)
return &url.URL{}
}
}
// named vars in routes don't work because we cant use the mux.router with middleware correctly
u, err := route.URLPath()
if err != nil {
level.Error(l).Log("msg", "failed to create URL",
"route", routeName,
"params", params,
"error", err)
return &url.URL{}
}
urlVals := u.Query()
n := len(params)
for i := 0; i < n; i += 2 {
key, value := strings.ToLower(params[i]), params[i+1]
urlVals.Set(key, value)
}
u.RawQuery = urlVals.Encode()
u.Host = netInfo.Domain
u.Scheme = "https"
if netInfo.Development {
u.Scheme = "http"
u.Host += fmt.Sprintf(":%d", netInfo.PortHTTPS)
}
return u
}
}
// LoadOrCreateCookieSecrets either parses the bytes from $repo/web/cookie-secret or creates a new file with suitable keys in it
func LoadOrCreateCookieSecrets(repo repo.Interface) ([]securecookie.Codec, error) {
secretPath := repo.GetPath("web", "cookie-secret")
err := os.MkdirAll(filepath.Dir(secretPath), 0700)
if err != nil && !os.IsExist(err) {
return nil, fmt.Errorf("failed to create folder for cookie secret: %w", err)
}
// load the existing data
secrets, err := ioutil.ReadFile(secretPath)
if err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("failed to load cookie secrets: %w", err)
}
// create new keys, save them and return the codec
hashKey := securecookie.GenerateRandomKey(32)
blockKey := securecookie.GenerateRandomKey(32)
data := append(hashKey, blockKey...)
err = ioutil.WriteFile(secretPath, data, 0600)
if err != nil {
return nil, err
}
sc := securecookie.CodecsFromPairs(hashKey, blockKey)
return sc, nil
}
// secrets should contain multiple of 64byte (to enable key rotation as supported by gorilla)
if n := len(secrets); n%64 != 0 {
return nil, fmt.Errorf("expected multiple of 64bytes in cookie secret file but got: %d", n)
}
// range over the secrets []byte in chunks of 64 bytes
// and slice it into 32byte pairs
var pairs [][]byte
// the increment/next part (which usually is i++)
// is the multiple comma assigment (a,b = b+1,a-1)
// so chunk is the next 64 bytes and then it slices of the first 64 bytes of secrets for the next iteration
for chunk := secrets[:64]; len(secrets) >= 64; chunk, secrets = secrets[:64], secrets[64:] {
pairs = append(pairs,
chunk[0:32], // hash key
chunk[32:64], // block key
)
}
sc := securecookie.CodecsFromPairs(pairs...)
return sc, nil
}
const csrfKeyLength = 32
// LoadOrCreateCSRFSecret either loads the bytes from $repo/web/csrf-secret or creates a new file with suitable keys in it
func LoadOrCreateCSRFSecret(repo repo.Interface) ([]byte, error) {
secretPath := repo.GetPath("web", "csrf-secret")
err := os.MkdirAll(filepath.Dir(secretPath), 0700)
if err != nil && !os.IsExist(err) {
return nil, fmt.Errorf("failed to create folder for csrf secret: %w", err)
}
// load the existing data
secret, err := ioutil.ReadFile(secretPath)
if err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("failed to load csrf secrets: %w", err)
}
// create a new key, save and return it
freshKey := securecookie.GenerateRandomKey(csrfKeyLength)
err = ioutil.WriteFile(secretPath, freshKey, 0600)
if err != nil {
return nil, err
}
return freshKey, nil
}
if n := len(secret); csrfKeyLength != n {
return nil, fmt.Errorf("expected %d bytes csrf secert but got %d", csrfKeyLength, n)
}
return secret, nil
}