123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- /*
- The Kurz Web UI exposes HTTP routes for browsers, route names to access them, and types for the requests.
- These routes are exposed by running SetupUI(listenAddress), which is enough to
- configure the Kurz domain API. Be sure to also configure the domain SPI to have
- a complete application.
- */
- package ui
- import (
- "code.osinet.fr/fgm/kurz/web/i18n"
- "errors"
- "fmt"
- "html/template"
- "log"
- "net/http"
- "net/url"
- "path"
- "path/filepath"
- "strconv"
- "strings"
- "github.com/gorilla/mux"
- "github.com/gorilla/sessions"
- "github.com/spf13/cast"
- )
- // Route names.
- const (
- RouteGetRoot = "kurz.web.get_root"
- RouteGetShort = "kurz.web.get_short"
- RoutePostTarget = "kurz.web.post_target"
- )
- // Content types.
- const (
- HtmlFormType = "application/x-www-form-urlencoded"
- // HtmlType is the MIME HTML type.
- HtmlType = "text/html"
- // HtmlTypeRegex is a regex matching the MIME HTML type anywhere
- HtmlTypeRegex = HtmlType
- )
- type Globals struct {
- AssetsBaseURL string
- AssetsVersion int
- AssetsPath string
- SessionKey []byte
- SessionName string
- SiteBaseURL string
- RefreshDelay int
- SiteName string
- }
- var globals Globals
- var router mux.Router
- var store *sessions.CookieStore
- var tmpl *template.Template
- // SetupUI() configures Web UI routes on the passed mux.Router.
- func SetupUI(router *mux.Router, configAssetsPath string) {
- // Set up asset routes first, for them to have priority over possibly matching short URLs.
- setupAssetRoutes(configAssetsPath, router)
- setupControllerRoutes(router)
- setupTemplates(configAssetsPath)
- }
- func setupAssetRoutes(configAssetsPath string, router *mux.Router) {
- absAssetsDir, err := filepath.Abs(configAssetsPath)
- if err != nil {
- panic(err)
- }
- log.Printf("Serving assets from %s\n", absAssetsDir)
- fs := http.FileServer(http.Dir(absAssetsDir))
- router.Handle("/favicon.ico", fs)
- for _, prefix := range []string{"css", "js", "images"} {
- router.PathPrefix("/" + prefix).Handler(fs)
- }
- }
- func setupControllerRoutes(gmux *mux.Router) {
- router = *gmux
- // BUG(fgm): improve Accept header matchers once https://github.com/golang/go/issues/19307 is completed.
- gmux.Handle("/{short}", i18n.WithLocalizer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- handleGetShort(w, r, gmux)
- }))).
- Methods("GET", "HEAD").
- Name(RouteGetShort)
- gmux.Handle("/", i18n.WithLocalizer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- handlePostTarget(w, r, gmux)
- }))).
- HeadersRegexp("Accept", HtmlTypeRegex).
- Headers("Content-Type", HtmlFormType).
- Methods("POST").
- Name(RoutePostTarget)
- gmux.Handle("/", i18n.WithLocalizer(http.HandlerFunc(handleGetRoot))).
- Methods("GET", "HEAD").
- Name(RouteGetRoot)
- }
- func setupTemplates(configAssetsPath string) {
- base, _ := filepath.Abs(configAssetsPath + "/../templates/")
- layout := base + "/layout"
- funcMap := template.FuncMap{
- "asset": URLForAsset,
- "path": urlFromRouteVariadic,
- }
- tmpl = template.Must(template.New("kurz").
- Funcs(funcMap).
- ParseFiles(
- base+"/201.gohtml",
- base+"/404.gohtml",
- base+"/409.gohtml",
- base+"/home.gohtml",
- layout+"/analytics.gohtml",
- layout+"/flashes.gohtml",
- layout+"/footer.gohtml",
- layout+"/inlinecss.gohtml",
- ))
- }
- func SetupGlobals(c map[string]interface{}) {
- // Note: keys in viper are lower-cased.
- globals = Globals{
- AssetsBaseURL: strings.Trim(c["assetsbaseurl"].(string), "/"),
- AssetsPath: c["assetspath"].(string),
- AssetsVersion: c["assetsversion"].(int),
- RefreshDelay: c["refreshdelay"].(int),
- SessionKey: []byte(c["sessionkey"].(string)),
- SessionName: c["sessionname"].(string),
- SiteBaseURL: strings.Trim(c["sitebaseurl"].(string), "/"),
- SiteName: c["sitename"].(string),
- }
- store = sessions.NewCookieStore(globals.SessionKey)
- }
- /*
- URLFromRoute generates absolute URLs for named routes.
- To build URLs for assets, use URLForAsset().
- - ns: the assets namespace. One of "js", "css', "images".
- - path: the asset path relative to the project root
- */
- func URLFromRoute(router *mux.Router, name string, params map[string]string) (string, error) {
- route := router.Get(name)
- if route == nil {
- err := errors.New(fmt.Sprintf("Error building unregistered route %s\n", name))
- return "", err
- }
- pairs := mapToPairs(params)
- url, err := route.URL(pairs...)
- if err != nil {
- err = errors.New(fmt.Sprintf("Error building route %s: %s\n", name, err))
- return "", err
- }
- // Don't use path.Join, it would clobber the scheme://domain format.
- // SiteBaseURL is "/"-trimmed on setup.
- fqsu := globals.SiteBaseURL + url.String()
- return fqsu, nil
- }
- func urlFromRouteVariadic(name string, paramPairs ...interface{}) (string, error) {
- if len(paramPairs)%2 != 0 {
- return "", errors.New("needs an even number of arguments")
- }
- var params = make(map[string]string, len(paramPairs)/2)
- var k string
- for i, r := range paramPairs {
- if i%2 == 0 {
- // If i is even, this is the key for the next value.
- k = r.(string)
- } else {
- // Else it is a value, so store it with the key we just got previously.
- params[k] = cast.ToString(r)
- }
- }
- return URLFromRoute(&router, name, params)
- }
- /*
- URLForAsset generates absolute URLs for assets.
- To build URLs for routes, use URLFromRoute().
- - ns: the assets namespace. One of "", "js", "css', "images". "" is only expected
- to be used for "favicon.ico".
- - path: the asset path relative to the project root
- */
- func URLForAsset(ns string, assetPath string) (string, error) {
- if ns != "" && ns != "css" && ns != "js" && ns != "images" {
- return "", errors.New("invalid asset namespace: " + ns)
- }
- version := globals.AssetsVersion
- base, err := url.Parse(globals.AssetsBaseURL)
- if err != nil {
- panic(err)
- }
- // Handles "" cleanly, and doesn't use a "\" on windows, unlike filepath.Join.
- base.Path = path.Join(base.Path, ns, assetPath)
- // No need to url.QueryEscape() since this format is query-clean by construction.
- base.RawQuery = "v=" + strconv.Itoa(version)
- res := base.String()
- return res, nil
- }
|