ソースを参照

WIP generation URLs

Frederic G. MARAND 6 年 前
コミット
7e62df9e50

+ 4 - 2
.gitignore

@@ -5,8 +5,10 @@
 *.o
 *.o
 *.a
 *.a
 *.so
 *.so
-cmd/kurz/kurz
-cmd/kurzd/kurzd
+/cmd/kurz/kurz
+/cmd/kurzd/kurzd
+/kurz
+/kurzd
 doc/*/*.svg
 doc/*/*.svg
 /cover*
 /cover*
 
 

+ 1 - 1
.idea/runConfigurations/Kurzd_serve.xml

@@ -1,7 +1,7 @@
 <component name="ProjectRunConfigurationManager">
 <component name="ProjectRunConfigurationManager">
   <configuration default="false" name="Kurzd serve" type="GoApplicationRunConfiguration" factoryName="Go Application" singleton="true" show_console_on_std_err="true" show_console_on_std_out="true">
   <configuration default="false" name="Kurzd serve" type="GoApplicationRunConfiguration" factoryName="Go Application" singleton="true" show_console_on_std_err="true" show_console_on_std_out="true">
     <module name="kurz" />
     <module name="kurz" />
-    <working_directory value="$PROJECT_DIR$/cmd/kurzd" />
+    <working_directory value="$PROJECT_DIR$" />
     <go_parameters value="-o kurzd" />
     <go_parameters value="-o kurzd" />
     <parameters value="serve" />
     <parameters value="serve" />
     <kind value="PACKAGE" />
     <kind value="PACKAGE" />

+ 12 - 0
.idea/runConfigurations/Test_Web.xml

@@ -0,0 +1,12 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="Test Web" type="GoTestRunConfiguration" factoryName="Go Test">
+    <module name="kurz" />
+    <working_directory value="$PROJECT_DIR$/" />
+    <framework value="gotest" />
+    <kind value="PACKAGE" />
+    <package value="code.osinet.fr/fgm/kurz/web" />
+    <directory value="$PROJECT_DIR$" />
+    <pattern value="./..." />
+    <method v="2" />
+  </configuration>
+</component>

+ 6 - 1
api/api.go

@@ -21,7 +21,7 @@ Code 451 MAY be replaced by 403, for example when legal censorship includes a
 gag order, super-injunction (UK), National security letter (US) or similar
 gag order, super-injunction (UK), National security letter (US) or similar
 mechanisms.
 mechanisms.
 
 
-These routes are exposed by running SetupRoutes(address), which is enough to
+These routes are exposed by running SetupRoutes(listenAddress), which is enough to
 configure the Kurz domain API. Be sure to also configure the domain SPI to have
 configure the Kurz domain API. Be sure to also configure the domain SPI to have
 a complete application.
 a complete application.
 */
 */
@@ -29,6 +29,7 @@ package api
 
 
 import (
 import (
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
+	"net/url"
 )
 )
 
 
 // Short is the type of responses provided by the Web API from a target.
 // Short is the type of responses provided by the Web API from a target.
@@ -71,3 +72,7 @@ func SetupRoutes(router *mux.Router) {
 		Methods("POST").
 		Methods("POST").
 		Name(RoutePostTarget)
 		Name(RoutePostTarget)
 }
 }
+
+func URLFromRoute(name string, args map[string]string) url.URL {
+	return url.URL{}
+}

+ 8 - 6
cmd/kurzd/dist.config.yml

@@ -1,18 +1,20 @@
 api:
 api:
-  address: ":8000"
+  listenAddress: ":8000"
 database:
 database:
   # Code likely not to work on another engine because it adds ?parseTime=true
   # Code likely not to work on another engine because it adds ?parseTime=true
   driver: mysql
   driver: mysql
   dsn: <user>:<pass>@[<server>]/<database>
   dsn: <user>:<pass>@[<server>]/<database>
   test_dsn: <user>:<pass>@[<server>]/<database>
   test_dsn: <user>:<pass>@[<server>]/<database>
 web:
 web:
-  address: ":3000"
-  # May be absolute or relative to the "kurzd" binary.
-  assetsPath: ../../web/public
+  # May be absolute or relative to the current directory.
+  assetsPath: web/public
   # The initial version cache buster on Kurz start. Will probably not remain in config.
   # The initial version cache buster on Kurz start. Will probably not remain in config.
   assetsVersion: 1
   assetsVersion: 1
-  fullyQualifiedSiteBaseURL: &fqsu http://localhost:3000
-  fullyQualifiedAssetsBaseURL: *fqsu
+  listenAddress: ":3000"
   # Delay is in seconds
   # Delay is in seconds
   refreshDelay: 5
   refreshDelay: 5
+  # Used to build absolute URLs.
+  siteBaseURL: &sbu http://localhost:3000
   siteName: "Kurz"
   siteName: "Kurz"
+  # Lines needs to be below siteBaseURL for the YAML reference to work.
+  assetsBaseURL: *sbu

+ 1 - 0
cmd/kurzd/rootCmd.go

@@ -14,6 +14,7 @@ var cmd = &cobra.Command{
 	Long: `kurzd is the actual engine for Kurz. Use it as the server for your Kurz clients.
 	Long: `kurzd is the actual engine for Kurz. Use it as the server for your Kurz clients.
 Configure it by copying dist.config.yml to ~/.kurz/config.yml and editing the copy.`,
 Configure it by copying dist.config.yml to ~/.kurz/config.yml and editing the copy.`,
 }
 }
+
 var verbose bool
 var verbose bool
 
 
 func initConfig() {
 func initConfig() {

+ 20 - 8
cmd/kurzd/serve.go

@@ -1,16 +1,18 @@
 package main
 package main
 
 
 import (
 import (
-	"code.osinet.fr/fgm/kurz/web"
 	"context"
 	"context"
 	"database/sql"
 	"database/sql"
-	"github.com/spf13/viper"
+	"fmt"
 	"log"
 	"log"
 	"net/http"
 	"net/http"
 	"os"
 	"os"
 	"os/signal"
 	"os/signal"
 	"time"
 	"time"
 
 
+	"code.osinet.fr/fgm/kurz/web"
+	"github.com/spf13/viper"
+
 	"code.osinet.fr/fgm/kurz/api"
 	"code.osinet.fr/fgm/kurz/api"
 	"code.osinet.fr/fgm/kurz/domain"
 	"code.osinet.fr/fgm/kurz/domain"
 	"code.osinet.fr/fgm/kurz/infrastructure"
 	"code.osinet.fr/fgm/kurz/infrastructure"
@@ -63,27 +65,37 @@ func ensureInfrastructure(db *sql.DB) *sql.DB {
 func serveHandler(_ *cobra.Command, args []string) {
 func serveHandler(_ *cobra.Command, args []string) {
 	db = ensureInfrastructure(db)
 	db = ensureInfrastructure(db)
 	defer db.Close()
 	defer db.Close()
+	cwd, err := os.Getwd()
+	if err != nil {
+		panic(err)
+	}
+	fmt.Printf("Server initializing in %s\n", cwd)
 
 
-	// This default is the relative position of the assets from the kurzd binary during development.
-	viper.SetDefault("web.assetsPath", "../../web/public")
+	// This default is the relative position of the assets from the project root during development.
+	viper.SetDefault("web.assetsPath", "web/public")
 	assetsPath := viper.Get("web.assetsPath").(string)
 	assetsPath := viper.Get("web.assetsPath").(string)
 
 
+	siteBaseURL := viper.Get("web.siteBaseUrl").(string)
+
+	viper.SetDefault("web.assetsBaseURL", siteBaseURL)
+	assetsBaseURL := viper.Get("web.assetsBaseUrl").(string)
+
 	webConfig := viper.Get("web").(map[string]interface{})
 	webConfig := viper.Get("web").(map[string]interface{})
 	router := mux.NewRouter()
 	router := mux.NewRouter()
 	api.SetupRoutes(router)
 	api.SetupRoutes(router)
-	web.SetupRoutes(router, assetsPath)
+	web.SetupRoutes(router, siteBaseURL, assetsBaseURL, assetsPath)
 	web.BuildGlobals(webConfig)
 	web.BuildGlobals(webConfig)
 	http.Handle("/", router)
 	http.Handle("/", router)
 
 
-	address := viper.Get("web.address").(string)
+	listenAddress := viper.Get("web.listenAddress").(string)
 
 
 	// Start a server that can handle a SIGINT to shutdown.
 	// Start a server that can handle a SIGINT to shutdown.
 	stop := make(chan os.Signal, 1)
 	stop := make(chan os.Signal, 1)
 	signal.Notify(stop, os.Interrupt)
 	signal.Notify(stop, os.Interrupt)
 
 
-	server := &http.Server{Addr: address, Handler: router}
+	server := &http.Server{Addr: listenAddress, Handler: router}
 	go func() {
 	go func() {
-		log.Printf("Listening on %s", address)
+		log.Printf("Listening on %s, exposed on %s", listenAddress, siteBaseURL)
 		err := server.ListenAndServe()
 		err := server.ListenAndServe()
 		log.Fatal(err)
 		log.Fatal(err)
 	}()
 	}()

+ 1 - 0
domain/domain_api.go

@@ -1,6 +1,7 @@
 package domain
 package domain
 
 
 func GetTargetURL(shortURL string) (target string, err error) {
 func GetTargetURL(shortURL string) (target string, err error) {
+	return "", MakeError(TargetCensoredError, "censored")
 	su := ShortURL{URL: URL(shortURL)}
 	su := ShortURL{URL: URL(shortURL)}
 	tu, err := shortURLRepository.GetTarget(su)
 	tu, err := shortURLRepository.GetTarget(su)
 	if err != nil {
 	if err != nil {

+ 32 - 11
web/get_short.go

@@ -1,6 +1,7 @@
 package web
 package web
 
 
 import (
 import (
+	"fmt"
 	"io"
 	"io"
 	"net/http"
 	"net/http"
 	"strings"
 	"strings"
@@ -12,9 +13,9 @@ import (
 /*
 /*
 Template variables:
 Template variables:
 
 
-  - FullyQualifiedAssetsBaseURL http://plusvite-cdn.osinet.eu
+  - assetsBaseURL http://plusvite-cdn.osinet.eu
   - FullyQualifiedShortURL      (short string)
   - FullyQualifiedShortURL      (short string)
-  - FullyQualifiedSiteURL       http://plusvite.net/
+  - SiteBaseURL       http://plusvite.net/
   - FullyQualifiedTargetURL     (target string)
   - FullyQualifiedTargetURL     (target string)
   - RefreshDelay                (seconds int)
   - RefreshDelay                (seconds int)
   - SiteName                    PlusVite
   - SiteName                    PlusVite
@@ -22,7 +23,7 @@ Template variables:
 
 
 type bytes = []byte
 type bytes = []byte
 
 
-func build403(w http.ResponseWriter, short string) error {
+func build403(w http.ResponseWriter, short string) {
 	data := struct {
 	data := struct {
 		FullyQualifiedShortURL string
 		FullyQualifiedShortURL string
 		Globals
 		Globals
@@ -39,11 +40,9 @@ func build403(w http.ResponseWriter, short string) error {
 		w.WriteHeader(http.StatusForbidden)
 		w.WriteHeader(http.StatusForbidden)
 		io.Copy(w, strings.NewReader(sw.String()))
 		io.Copy(w, strings.NewReader(sw.String()))
 	}
 	}
-
-	return err
 }
 }
 
 
-func build404(w http.ResponseWriter, short string) error {
+func build404(w http.ResponseWriter, short string) {
 	data := struct {
 	data := struct {
 		FullyQualifiedShortURL string
 		FullyQualifiedShortURL string
 		Globals
 		Globals
@@ -60,15 +59,37 @@ func build404(w http.ResponseWriter, short string) error {
 		w.WriteHeader(http.StatusNotFound)
 		w.WriteHeader(http.StatusNotFound)
 		io.Copy(w, strings.NewReader(sw.String()))
 		io.Copy(w, strings.NewReader(sw.String()))
 	}
 	}
-	return err
 }
 }
 
 
-func build451(w http.ResponseWriter, short string) error {
-	return nil
+func build451(w http.ResponseWriter, router *mux.Router, short string) {
+	hr := router.Get(RouteGetRoot)
+	_, err := hr.URL()
+	if err != nil {
+		fmt.Println(err)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	data := struct {
+		FullyQualifiedShortURL string
+		Globals
+	}{
+		short,
+		globals,
+	}
+
+	sw := &strings.Builder{}
+	err = tmpl.ExecuteTemplate(sw, "404", data)
+	if err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+	} else {
+		w.WriteHeader(http.StatusNotFound)
+		io.Copy(w, strings.NewReader(sw.String()))
+	}
 }
 }
 
 
 // handleGetShort handles path /<short>
 // handleGetShort handles path /<short>
-func handleGetShort(w http.ResponseWriter, r *http.Request) {
+func handleGetShort(w http.ResponseWriter, r *http.Request, router *mux.Router) {
 	short, ok := mux.Vars(r)["short"]
 	short, ok := mux.Vars(r)["short"]
 	if !ok {
 	if !ok {
 		w.WriteHeader(http.StatusBadRequest)
 		w.WriteHeader(http.StatusBadRequest)
@@ -100,7 +121,7 @@ func handleGetShort(w http.ResponseWriter, r *http.Request) {
 		build403(w, short)
 		build403(w, short)
 
 
 	case domain.TargetCensoredError:
 	case domain.TargetCensoredError:
-		build451(w, short)
+		build451(w, router, short)
 
 
 	default:
 	default:
 		// TargetInvalid is not supposed to happen in this case, so it is an internal error too.
 		// TargetInvalid is not supposed to happen in this case, so it is an internal error too.

+ 5 - 5
web/templates/201.gohtml

@@ -3,9 +3,9 @@
 <html lang="fr" dir="ltr">
 <html lang="fr" dir="ltr">
 <head>
 <head>
     <title>{{.SiteName}}: vers {{.FullyQualifiedTargetURL}}</title>
     <title>{{.SiteName}}: vers {{.FullyQualifiedTargetURL}}</title>
-    <link rel="shortcut icon" href="{{.FullyQualifiedAssetsBaseURL}}/favicon.ico?v=1">
-    <link rel="stylesheet" type="text/css" href="{{.FullyQualifiedAssetsBaseURL}}/public/css/plusvite.css?v={{.AssetsVersion}}" />
-    <link rel="stylesheet" type="text/css" href="{{.FullyQualifiedAssetsBaseURL}}/public/css/riff.css?v={{.AssetsVersion}}" />
+    <link rel="shortcut icon" href="{{.AssetsBaseURL}}/favicon.ico?v=1">
+    <link rel="stylesheet" type="text/css" href="{{.AssetsBaseURL}}/public/css/plusvite.css?v={{.AssetsVersion}}" />
+    <link rel="stylesheet" type="text/css" href="{{.AssetsBaseURL}}/public/css/riff.css?v={{.AssetsVersion}}" />
     {{template "analytics" .}}
     {{template "analytics" .}}
     {{template "inlinecss"}}
     {{template "inlinecss"}}
 </head>
 </head>
@@ -18,10 +18,10 @@
         <p>en utilisant le nouvel URL</p>
         <p>en utilisant le nouvel URL</p>
         <pre><a href="{{.FullyQualifiedShortURL}}">{{.FullyQualifiedShortURL}}</a></pre>
         <pre><a href="{{.FullyQualifiedShortURL}}">{{.FullyQualifiedShortURL}}</a></pre>
         <p>pour y faire référence.</p>
         <p>pour y faire référence.</p>
-        <p><a href="{{.FullyQualifiedSiteURL}}">Retour</a></p>
+        <p><a href="{{.SiteBaseURL}}">Retour</a></p>
     </div>
     </div>
 
 
     {{template "footer"}}
     {{template "footer"}}
 </body>
 </body>
 </html>
 </html>
-{{end}}
+{{end}}

+ 6 - 6
web/templates/403.gohtml

@@ -3,12 +3,12 @@
 <html lang="fr" dir="ltr">
 <html lang="fr" dir="ltr">
 <head>
 <head>
     <title>{{.SiteName}}: {{.FullyQualifiedShortURL}}</title>
     <title>{{.SiteName}}: {{.FullyQualifiedShortURL}}</title>
-    <link rel="shortcut icon" href="{{.FullyQualifiedAssetsBaseURL}}/favicon.ico?v=1">
-    <link rel="stylesheet" type="text/css" href="{{.FullyQualifiedAssetsBaseURL}}/public/css/plusvite.css?v={{.AssetsVersion}}" />
-    <link rel="stylesheet" type="text/css" href="{{.FullyQualifiedAssetsBaseURL}}/public/css/riff.css?v={{.AssetsVersion}}" />
+    <link rel="shortcut icon" href="{{.AssetsBaseURL}}/favicon.ico?v=1">
+    <link rel="stylesheet" type="text/css" href="{{.AssetsBaseURL}}/public/css/plusvite.css?v={{.AssetsVersion}}" />
+    <link rel="stylesheet" type="text/css" href="{{.AssetsBaseURL}}/public/css/riff.css?v={{.AssetsVersion}}" />
     {{template "analytics" .}}
     {{template "analytics" .}}
     {{template "inlinecss"}}
     {{template "inlinecss"}}
-    <meta http-equiv="Refresh" content="{{.RefreshDelay}};{{.FullyQualifiedSiteURL}}" />
+    <meta http-equiv="Refresh" content="{{.RefreshDelay}};{{.SiteBaseURL}}" />
 </head>
 </head>
 
 
 <body>
 <body>
@@ -16,10 +16,10 @@
         <h1>{{.FullyQualifiedShortURL}}</h1>
         <h1>{{.FullyQualifiedShortURL}}</h1>
         <p>L'URL de destination est valide mais a été bloqué par son titulaire.</p>
         <p>L'URL de destination est valide mais a été bloqué par son titulaire.</p>
         <p>Dans quelques secondes, vous serez redirigé vers la page d'accueil du site.</p>
         <p>Dans quelques secondes, vous serez redirigé vers la page d'accueil du site.</p>
-        <p><a href="{{.FullyQualifiedSiteURL}}">Retour</a></p>
+        <p><a href="{{.SiteBaseURL}}">Retour</a></p>
     </div>
     </div>
 
 
     {{template "footer"}}
     {{template "footer"}}
 </body>
 </body>
 </html>
 </html>
-{{end}}
+{{end}}

+ 7 - 7
web/templates/404.gohtml

@@ -3,23 +3,23 @@
 <html lang="fr" dir="ltr">
 <html lang="fr" dir="ltr">
 <head>
 <head>
     <title>{{.SiteName}}: {{.FullyQualifiedShortURL}}</title>
     <title>{{.SiteName}}: {{.FullyQualifiedShortURL}}</title>
-    <link rel="shortcut icon" href="{{.FullyQualifiedAssetsBaseURL}}/favicon.ico?v=1">
-    <link rel="stylesheet" type="text/css" href="{{.FullyQualifiedAssetsBaseURL}}/public/css/plusvite.css?v={{.Globals.AssetsVersion}}" />
-    <link rel="stylesheet" type="text/css" href="{{.FullyQualifiedAssetsBaseURL}}/public/css/riff.css?v={{.Globals.AssetsVersion}}" />
+    <link rel="shortcut icon" href="{{.AssetsBaseURL}}/favicon.ico?v=1">
+    <link rel="stylesheet" type="text/css" href="{{.AssetsBaseURL}}/public/css/plusvite.css?v={{.Globals.AssetsVersion}}" />
+    <link rel="stylesheet" type="text/css" href="{{.AssetsBaseURL}}/public/css/riff.css?v={{.Globals.AssetsVersion}}" />
     {{template "analytics" .}}
     {{template "analytics" .}}
     {{template "inlinecss"}}
     {{template "inlinecss"}}
-    <meta http-equiv="Refresh" content="{{.RefreshDelay}};{{.FullyQualifiedSiteURL}}" />
+    <meta http-equiv="Refresh" content="{{.RefreshDelay}};{{.SiteBaseURL}}" />
 </head>
 </head>
 
 
 <body>
 <body>
     <div id="box">
     <div id="box">
         <h1>{{.FullyQualifiedShortURL}}</h1>
         <h1>{{.FullyQualifiedShortURL}}</h1>
-        <p>Ceci n'est pas un URL <a href="{{.FullyQualifiedSiteURL}}">{{.SiteName}}</a> valide.</p>
+        <p>Ceci n'est pas un URL <a href="{{.SiteBaseURL}}">{{.SiteName}}</a> valide.</p>
         <p>Dans quelques secondes, vous serez redirigé vers la page d'accueil du site.</p>
         <p>Dans quelques secondes, vous serez redirigé vers la page d'accueil du site.</p>
-        <p><a href="{{.FullyQualifiedSiteURL}}">Retour</a></p>
+        <p><a href="{{.SiteBaseURL}}">Retour</a></p>
     </div>
     </div>
 
 
     {{template "footer"}}
     {{template "footer"}}
 </body>
 </body>
 </html>
 </html>
-{{end}}
+{{end}}

+ 6 - 6
web/templates/451.gohtml

@@ -3,12 +3,12 @@
 <html lang="fr" dir="ltr">
 <html lang="fr" dir="ltr">
 <head>
 <head>
     <title>{{.SiteName}}: {{.FullyQualifiedShortURL}}</title>
     <title>{{.SiteName}}: {{.FullyQualifiedShortURL}}</title>
-    <link rel="shortcut icon" href="{{.FullyQualifiedAssetsBaseURL}}/favicon.ico?v=1">
-    <link rel="stylesheet" type="text/css" href="{{.FullyQualifiedAssetsBaseURL}}/public/css/plusvite.css?v={{.AssetsVersion}}" />
-    <link rel="stylesheet" type="text/css" href="{{.FullyQualifiedAssetsBaseURL}}/public/css/riff.css?v={{.AssetsVersion}}" />
+    <link rel="shortcut icon" href="{{.AssetsBaseURL}}/favicon.ico?v=1">
+    <link rel="stylesheet" type="text/css" href="{{.AssetsBaseURL}}/public/css/plusvite.css?v={{.AssetsVersion}}" />
+    <link rel="stylesheet" type="text/css" href="{{.AssetsBaseURL}}/public/css/riff.css?v={{.AssetsVersion}}" />
     {{template "analytics" .}}
     {{template "analytics" .}}
     {{template "inlinecss"}}
     {{template "inlinecss"}}
-    <meta http-equiv="Refresh" content="{{.RefreshDelay}};{{.FullyQualifiedSiteURL}}" />
+    <meta http-equiv="Refresh" content="{{.RefreshDelay}};{{.SiteBaseURL}}" />
 </head>
 </head>
 
 
 <body>
 <body>
@@ -16,10 +16,10 @@
         <h1>{{.FullyQualifiedShortURL}}</h1>
         <h1>{{.FullyQualifiedShortURL}}</h1>
         <p>L'URL de destination est valide mais a été bloqué par décision légale.</p>
         <p>L'URL de destination est valide mais a été bloqué par décision légale.</p>
         <p>Dans quelques secondes, vous serez redirigé vers la page d'accueil du site.</p>
         <p>Dans quelques secondes, vous serez redirigé vers la page d'accueil du site.</p>
-        <p><a href="{{.FullyQualifiedSiteURL}}">Retour</a></p>
+        <p><a href="{{.SiteBaseURL}}">Retour</a></p>
     </div>
     </div>
 
 
     {{template "footer"}}
     {{template "footer"}}
 </body>
 </body>
 </html>
 </html>
-{{end}}
+{{end}}

+ 3 - 3
web/templates/home.gohtml

@@ -3,9 +3,9 @@
 <html lang="fr" dir="ltr">
 <html lang="fr" dir="ltr">
 <head>
 <head>
     <title>Plus Vite sur le Net: service gratuit d'URLs raccourcis</title>
     <title>Plus Vite sur le Net: service gratuit d'URLs raccourcis</title>
-    <link rel="shortcut icon" href="{{.FullyQualifiedAssetsBaseURL}}/favicon.ico?v=1">
-    <link rel="stylesheet" type="text/css" href="{{.FullyQualifiedAssetsBaseURL}}/public/css/plusvite.css?v={{.AssetsVersion}}" />
-    <link rel="stylesheet" type="text/css" href="{{.FullyQualifiedAssetsBaseURL}}/public/css/riff.css?v={{.AssetsVersion}}" />
+    <link rel="shortcut icon" href="{{.AssetsBaseURL}}/favicon.ico?v=1">
+    <link rel="stylesheet" type="text/css" href="{{.AssetsBaseURL}}/public/css/plusvite.css?v={{.AssetsVersion}}" />
+    <link rel="stylesheet" type="text/css" href="{{.AssetsBaseURL}}/public/css/riff.css?v={{.AssetsVersion}}" />
     {{template "analytics" .}}
     {{template "analytics" .}}
     {{template "inlinecss"}}
     {{template "inlinecss"}}
 </head>
 </head>

+ 49 - 15
web/web.go

@@ -1,7 +1,7 @@
 /*
 /*
 The Kurz Web UI exposes HTTP routes for browsers, route names to access them, and types for the requests.
 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 SetupRoutes(address), which is enough to
+These routes are exposed by running SetupRoutes(listenAddress), which is enough to
 configure the Kurz domain API. Be sure to also configure the domain SPI to have
 configure the Kurz domain API. Be sure to also configure the domain SPI to have
 a complete application.
 a complete application.
 */
 */
@@ -10,6 +10,7 @@ package web
 import (
 import (
 	"html/template"
 	"html/template"
 	"net/http"
 	"net/http"
+	"net/url"
 	"path/filepath"
 	"path/filepath"
 
 
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
@@ -31,20 +32,20 @@ const (
 	HtmlTypeRegex = HtmlType
 	HtmlTypeRegex = HtmlType
 )
 )
 
 
-var tmpl *template.Template
-
 type Globals struct {
 type Globals struct {
-	AssetsVersion               int
-	FullyQualifiedAssetsBaseURL string
-	FullyQualifiedSiteBaseURL   string
-	RefreshDelay                int
-	SiteName                    string
+	AssetsBaseURL string
+	AssetsVersion int
+	AssetsPath    string
+	SiteBaseURL   string
+	RefreshDelay  int
+	SiteName      string
 }
 }
 
 
 var globals Globals
 var globals Globals
+var tmpl *template.Template
 
 
 // SetupRoutes() configures Web UI routes on the passed mux.Router.
 // SetupRoutes() configures Web UI routes on the passed mux.Router.
-func SetupRoutes(router *mux.Router, configAssetsPath string) {
+func SetupRoutes(router *mux.Router, configSiteBaseURL, configAssetsBaseURL, configAssetsPath string) {
 	const assetsPrefix = "/public"
 	const assetsPrefix = "/public"
 	absAssetsDir, err := filepath.Abs(configAssetsPath)
 	absAssetsDir, err := filepath.Abs(configAssetsPath)
 	if err != nil {
 	if err != nil {
@@ -55,7 +56,9 @@ func SetupRoutes(router *mux.Router, configAssetsPath string) {
 	router.Handle("/favicon.ico", fs)
 	router.Handle("/favicon.ico", fs)
 
 
 	// BUG(fgm): improve Accept header matchers once https://github.com/golang/go/issues/19307 is completed.
 	// BUG(fgm): improve Accept header matchers once https://github.com/golang/go/issues/19307 is completed.
-	router.HandleFunc("/{short}", handleGetShort).
+	router.HandleFunc("/{short}", func(w http.ResponseWriter, r *http.Request) {
+		handleGetShort(w, r, router)
+	}).
 		Methods("GET", "HEAD").
 		Methods("GET", "HEAD").
 		Name(RouteGetShort)
 		Name(RouteGetShort)
 	router.HandleFunc("/", handlePostTarget).
 	router.HandleFunc("/", handlePostTarget).
@@ -82,10 +85,41 @@ func SetupRoutes(router *mux.Router, configAssetsPath string) {
 func BuildGlobals(c map[string]interface{}) {
 func BuildGlobals(c map[string]interface{}) {
 	// Note: keys in viper are lower-cased.
 	// Note: keys in viper are lower-cased.
 	globals = Globals{
 	globals = Globals{
-		AssetsVersion:               c["assetsversion"].(int),
-		FullyQualifiedAssetsBaseURL: c["fullyqualifiedassetsbaseurl"].(string),
-		FullyQualifiedSiteBaseURL:   c["fullyqualifiedsitebaseurl"].(string),
-		RefreshDelay:                c["refreshdelay"].(int),
-		SiteName:                    c["sitename"].(string),
+		AssetsBaseURL: c["assetsbaseurl"].(string),
+		AssetsPath:    c["assetspath"].(string),
+		AssetsVersion: c["assetsversion"].(int),
+		RefreshDelay:  c["refreshdelay"].(int),
+		SiteBaseURL:   c["sitebaseurl"].(string),
+		SiteName:      c["sitename"].(string),
+	}
+}
+
+/*
+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 {
+	return ""
+}
+
+/*
+URLFromRoute generates absolute URLs for assets.
+
+To build URLs for routes, use URLFromRoute().
+
+  - ns: the assets namespace. One of "js", "css', "images".
+  - path: the asset path relative to the project root
+ */
+func URLForAsset(ns string, path string) string {
+	base, err := url.Parse(globals.AssetsBaseURL)
+	if err != nil {
+		panic(err)
 	}
 	}
+	base.Path = path
+	res := base.String()
+	return res
 }
 }

+ 38 - 0
web/web_test.go

@@ -0,0 +1,38 @@
+package web
+
+import (
+	"testing"
+)
+
+// Not a Test function, a test helper.
+func testSetupGlobals() {
+	globals = Globals{
+		AssetsBaseURL: "https://cdn.osinet.fr",
+		AssetsVersion: 1,
+		AssetsPath:    "web/public",
+		SiteBaseURL:   "http://sut.kurz.osinet.fr",
+		RefreshDelay:  1,
+		SiteName:      "Kurz test site",
+	}
+}
+
+func TestURLForAsset(t *testing.T) {
+	testSetupGlobals()
+	checks := [][4]string{
+		// Name, NS, Path, Expected.
+		{"empty css path", "css", "", "https://cdn.osinet.fr/"},
+		{"root css path", "css", "/", "https://cdn.osinet.fr/"},
+	}
+	for _, check := range checks {
+		name := check[0]
+		ns := check[1]
+		path := check[2]
+		expected := check[3]
+		t.Run(name, func (t *testing.T) {
+			actual := URLForAsset(ns, path)
+			if actual != expected {
+				t.Fail()
+			}
+		})
+	}
+}