ui.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. /*
  2. The Kurz Web UI exposes HTTP routes for browsers, route names to access them, and types for the requests.
  3. These routes are exposed by running SetupUI(listenAddress), which is enough to
  4. configure the Kurz domain API. Be sure to also configure the domain SPI to have
  5. a complete application.
  6. */
  7. package ui
  8. import (
  9. "code.osinet.fr/fgm/kurz/web/i18n"
  10. "errors"
  11. "fmt"
  12. "html/template"
  13. "log"
  14. "net/http"
  15. "net/url"
  16. "path"
  17. "path/filepath"
  18. "strconv"
  19. "strings"
  20. "github.com/gorilla/mux"
  21. "github.com/gorilla/sessions"
  22. "github.com/spf13/cast"
  23. )
  24. // Route names.
  25. const (
  26. RouteGetRoot = "kurz.web.get_root"
  27. RouteGetShort = "kurz.web.get_short"
  28. RoutePostTarget = "kurz.web.post_target"
  29. )
  30. // Content types.
  31. const (
  32. HtmlFormType = "application/x-www-form-urlencoded"
  33. // HtmlType is the MIME HTML type.
  34. HtmlType = "text/html"
  35. // HtmlTypeRegex is a regex matching the MIME HTML type anywhere
  36. HtmlTypeRegex = HtmlType
  37. )
  38. type Globals struct {
  39. AssetsBaseURL string
  40. AssetsVersion int
  41. AssetsPath string
  42. SessionKey []byte
  43. SessionName string
  44. SiteBaseURL string
  45. RefreshDelay int
  46. SiteName string
  47. }
  48. var globals Globals
  49. var router mux.Router
  50. var store *sessions.CookieStore
  51. var tmpl *template.Template
  52. // SetupUI() configures Web UI routes on the passed mux.Router.
  53. func SetupUI(router *mux.Router, configAssetsPath string) {
  54. // Set up asset routes first, for them to have priority over possibly matching short URLs.
  55. setupAssetRoutes(configAssetsPath, router)
  56. setupControllerRoutes(router)
  57. setupTemplates(configAssetsPath)
  58. }
  59. func setupAssetRoutes(configAssetsPath string, router *mux.Router) {
  60. absAssetsDir, err := filepath.Abs(configAssetsPath)
  61. if err != nil {
  62. panic(err)
  63. }
  64. log.Printf("Serving assets from %s\n", absAssetsDir)
  65. fs := http.FileServer(http.Dir(absAssetsDir))
  66. router.Handle("/favicon.ico", fs)
  67. for _, prefix := range []string{"css", "js", "images"} {
  68. router.PathPrefix("/" + prefix).Handler(fs)
  69. }
  70. }
  71. func setupControllerRoutes(gmux *mux.Router) {
  72. router = *gmux
  73. // BUG(fgm): improve Accept header matchers once https://github.com/golang/go/issues/19307 is completed.
  74. gmux.Handle("/{short}", i18n.WithLocalizer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  75. handleGetShort(w, r, gmux)
  76. }))).
  77. Methods("GET", "HEAD").
  78. Name(RouteGetShort)
  79. gmux.Handle("/", i18n.WithLocalizer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  80. handlePostTarget(w, r, gmux)
  81. }))).
  82. HeadersRegexp("Accept", HtmlTypeRegex).
  83. Headers("Content-Type", HtmlFormType).
  84. Methods("POST").
  85. Name(RoutePostTarget)
  86. gmux.Handle("/", i18n.WithLocalizer(http.HandlerFunc(handleGetRoot))).
  87. Methods("GET", "HEAD").
  88. Name(RouteGetRoot)
  89. }
  90. func setupTemplates(configAssetsPath string) {
  91. base, _ := filepath.Abs(configAssetsPath + "/../templates/")
  92. layout := base + "/layout"
  93. funcMap := template.FuncMap{
  94. "asset": URLForAsset,
  95. "path": urlFromRouteVariadic,
  96. }
  97. tmpl = template.Must(template.New("kurz").
  98. Funcs(funcMap).
  99. ParseFiles(
  100. base+"/201.gohtml",
  101. base+"/404.gohtml",
  102. base+"/409.gohtml",
  103. base+"/home.gohtml",
  104. layout+"/analytics.gohtml",
  105. layout+"/flashes.gohtml",
  106. layout+"/footer.gohtml",
  107. layout+"/inlinecss.gohtml",
  108. ))
  109. }
  110. func SetupGlobals(c map[string]interface{}) {
  111. // Note: keys in viper are lower-cased.
  112. globals = Globals{
  113. AssetsBaseURL: strings.Trim(c["assetsbaseurl"].(string), "/"),
  114. AssetsPath: c["assetspath"].(string),
  115. AssetsVersion: c["assetsversion"].(int),
  116. RefreshDelay: c["refreshdelay"].(int),
  117. SessionKey: []byte(c["sessionkey"].(string)),
  118. SessionName: c["sessionname"].(string),
  119. SiteBaseURL: strings.Trim(c["sitebaseurl"].(string), "/"),
  120. SiteName: c["sitename"].(string),
  121. }
  122. store = sessions.NewCookieStore(globals.SessionKey)
  123. }
  124. /*
  125. URLFromRoute generates absolute URLs for named routes.
  126. To build URLs for assets, use URLForAsset().
  127. - ns: the assets namespace. One of "js", "css', "images".
  128. - path: the asset path relative to the project root
  129. */
  130. func URLFromRoute(router *mux.Router, name string, params map[string]string) (string, error) {
  131. route := router.Get(name)
  132. if route == nil {
  133. err := errors.New(fmt.Sprintf("Error building unregistered route %s\n", name))
  134. return "", err
  135. }
  136. pairs := mapToPairs(params)
  137. url, err := route.URL(pairs...)
  138. if err != nil {
  139. err = errors.New(fmt.Sprintf("Error building route %s: %s\n", name, err))
  140. return "", err
  141. }
  142. // Don't use path.Join, it would clobber the scheme://domain format.
  143. // SiteBaseURL is "/"-trimmed on setup.
  144. fqsu := globals.SiteBaseURL + url.String()
  145. return fqsu, nil
  146. }
  147. func urlFromRouteVariadic(name string, paramPairs ...interface{}) (string, error) {
  148. if len(paramPairs)%2 != 0 {
  149. return "", errors.New("needs an even number of arguments")
  150. }
  151. var params = make(map[string]string, len(paramPairs)/2)
  152. var k string
  153. for i, r := range paramPairs {
  154. if i%2 == 0 {
  155. // If i is even, this is the key for the next value.
  156. k = r.(string)
  157. } else {
  158. // Else it is a value, so store it with the key we just got previously.
  159. params[k] = cast.ToString(r)
  160. }
  161. }
  162. return URLFromRoute(&router, name, params)
  163. }
  164. /*
  165. URLForAsset generates absolute URLs for assets.
  166. To build URLs for routes, use URLFromRoute().
  167. - ns: the assets namespace. One of "", "js", "css', "images". "" is only expected
  168. to be used for "favicon.ico".
  169. - path: the asset path relative to the project root
  170. */
  171. func URLForAsset(ns string, assetPath string) (string, error) {
  172. if ns != "" && ns != "css" && ns != "js" && ns != "images" {
  173. return "", errors.New("invalid asset namespace: " + ns)
  174. }
  175. version := globals.AssetsVersion
  176. base, err := url.Parse(globals.AssetsBaseURL)
  177. if err != nil {
  178. panic(err)
  179. }
  180. // Handles "" cleanly, and doesn't use a "\" on windows, unlike filepath.Join.
  181. base.Path = path.Join(base.Path, ns, assetPath)
  182. // No need to url.QueryEscape() since this format is query-clean by construction.
  183. base.RawQuery = "v=" + strconv.Itoa(version)
  184. res := base.String()
  185. return res, nil
  186. }