Obsah
1. Standardní šablonovací systém jazyka Go a šablony HTML stránek
2. Textové šablony vs. HTML šablony
3. Krátké zopakování – jednoduchá textová šablona a výsledek po její aplikaci
4. Balíček text/template a HTML šablona
5. Použití balíčku html/template namísto text/template
6. Problematika XSS a balíčku text/template
7. Zabránění XSS aplikací šablony přes balíček html/template
8. Kontext, ve kterém se data do HTML stránky vkládají při aplikaci šablony
9. HTML šablony a HTTP server realizovaný v jazyce Go
10. Ukázka jednoduchého serveru, který poskytuje pouze statické stránky
11. HTTP server a aplikace HTML šablon
12. Benchmark: rychlost načtení a aplikace šablon
13. Vytvoření šablon pouze při inicializaci HTTP serveru
15. Příklad detekce změn v souborech
16. HTTP server detekující změny šablon
17. Úplný zdrojový kód serveru
19. Repositář s demonstračními příklady
1. Standardní šablonovací systém jazyka Go a šablony HTML stránek
Na dva předchozí články [1] [2], v nichž jsme si ukázali možnosti standardního šablonovacího systému programovacího jazyka Go, dnes navážeme. Ukážeme si totiž, jakým způsobem je možné použít HTML šablony. Ve skutečnosti totiž ve standardní knihovně jazyka Go existuje kromě standardního šablonovacího systému text/template i odvozená varianta nazvaná html/template. Ta je určena pro aplikaci šablon HTML stránek. Ovšem ve skutečnosti se nemusí jednat pouze o čisté HTML, protože šablonovací systém dokáže pracovat například i s CSS (tedy s kaskádními styly).
Ve druhé části dnešního článku si ukážeme, jak lze šablony použít přímo v backendu, tedy ve webovém serveru, který bude generovat HTML stránky či jejich části a posílat je klientovi (nebude se tedy jednat o dnes tak populární SPA). Taktéž si vysvětlíme, jak zařídit, aby se šablony nemusely načítat při každém požadavku klienta a současně aby bylo možné šablony měnit za běhu serveru (což je velmi užitečné, a to nejenom při vývoji).
2. Textové šablony vs. HTML šablony
Jak se však od sebe vlastně odlišuje balíček text/template od balíčku html/template? Šablonovací systém implementovaný v balíčku text/template jednoduše aplikuje šablonu na předaná data, přičemž se nesnaží žádným způsobem „rozumět“ šabloně či datům – předpokládá se, že jak samotná šablona, tak i předávaná data jsou v takové podobě, že nedojde k žádným problematickým jevům – například že se „rozhodí“ výsledný dokument kvůli nějakému znaku se speciálním významem, který je součástí dat.
U balíčku html/template je tomu jinak, protože se předpokládá, že výsledkem aplikace šablony má být HTML stránka, jež bude zobrazena ve webovém prohlížeči uživatele. A současně se předpokládá, že data mohou pocházet z neověřeného zdroje (například mohou být zadána potenciálním útočníkem). Balíček html/template je tedy navržen takovým způsobem, aby vhodným způsobem upravil data před jejich vložením do šablony tak, aby výsledná HTML stránka byla (do značné míry) korektní. Cílem je zabránit jak „pouhému“ špatnému zobrazení stránky (například pokud do dat někdo omylem vloží neuzavřený HTML prvek), tak i útokům typu XSS.
3. Krátké zopakování – jednoduchá textová šablona a výsledek po její aplikaci
Připomeňme si, jak může vypadat jednoduchá (čistě textová) šablona, která je zpracovatelná balíčkem text/template. Konkrétně se jedná o šablonu, která je aplikovatelná na pole (resp. řez) obsahující záznamy s prvky pojmenovanými Name, Surname a Popularity. Při aplikaci šablony se postupně prochází jednotlivými záznamy a pro každý záznam se vygenerují tři textové řádky (poslední řádek bude prázdný). Povšimněte si, že přímo ze šablony je možné volat například standardní funkci str.Printf popř. použít podmínky:
-------------------------------------------------------------------- {{range .}}Jméno {{printf "%-15s" .Name}} {{printf "%-15s" .Surname}} {{if gt .Popularity 0}} Popularita {{printf "%2d" .Popularity}} {{end}} {{end}} --------------------------------------------------------------------
Šablonu budeme aplikovat na pole/řez se záznamy typu:
type Role struct { Name string Surname string Popularity int }
Konkrétně se bude jednat o následující záznamy:
roles := []Role{ Role{"Eliška", "Najbrtová", 4}, Role{"Jenny", "Suk", 3}, Role{"Anička", "Šafářová", 0}, Role{"Sváťa", "Pulec", 3}, Role{"Blažej", "Motyčka", 8}, Role{"Eda", "Wasserfall", 0}, Role{"Přemysl", "Hájek", 10}, }
Podívejme se nyní na úplný zdrojový kód demonstračního příkladu, v němž je implementováno načtení šablony i její aplikace na předaná data:
package main import ( "os" "text/template" ) const ( templateFilename = "template17.txt" ) // datový typ, jehož prvky budou vypisovány v šabloně type Role struct { Name string Surname string Popularity int } func main() { // vytvoření nové šablony tmpl := template.Must(template.ParseFiles(templateFilename)) // tyto hodnoty budou použity při aplikaci šablony roles := []Role{ Role{"Eliška", "Najbrtová", 4}, Role{"Jenny", "Suk", 3}, Role{"Anička", "Šafářová", 0}, Role{"Sváťa", "Pulec", 3}, Role{"Blažej", "Motyčka", 8}, Role{"Eda", "Wasserfall", 0}, Role{"Přemysl", "Hájek", 10}, } // aplikace šablony - přepis hodnot err := tmpl.Execute(os.Stdout, roles) if err != nil { panic(err) } }
Výsledek získaný po spuštění tohoto demonstračního příkladu by měl vypadat následovně:
-------------------------------------------------------------------- Jméno Eliška Najbrtová Popularita 4 Jméno Jenny Suk Popularita 3 Jméno Anička Šafářová Jméno Sváťa Pulec Popularita 3 Jméno Blažej Motyčka Popularita 8 Jméno Eda Wasserfall Jméno Přemysl Hájek Popularita 10 --------------------------------------------------------------------
Úplný zdrojový kód demonstračního příkladu z této kapitoly je dostupný na adrese https://github.com/tisnik/go-root/blob/master/article80/template17.go.
4. Balíček text/template a HTML šablona
Samozřejmě nám nic nebrání v tom použít balíček text/template společně s HTML šablonou. Můžeme například předchozí textovou šablonu nahradit za šablonu, která obsahuje kostru HTML stránky. Díky tomu, že HTML používá jiné speciální znaky než šablonovací systém Go, je realizace takové šablony snadná:
<html> <head> <title>Role ve hře Švestka</title> </head> <body> <h1>Role ve hře Švestka</h1> <table> <tr><th>Jméno</th><th>Příjmení</th><th>Popularita</th></tr> {{range .}} <tr><td>{{.Name}}</td><td>{{.Surname}}</td><td>{{if gt .Popularity 0}}{{.Popularity}}{{else}}×{{end}}</td></tr> {{end}} </table> </body> </html>
Samotný zdrojový kód příkladu se – až na odlišné jméno šablony – nebude odlišovat od příkladu, který byl popsán v předchozí kapitole:
package main import ( "os" "text/template" ) const ( templateFilename = "html_template01.htm" ) // datový typ, jehož prvky budou vypisovány v šabloně type Role struct { Name string Surname string Popularity int } func main() { // vytvoření nové šablony tmpl := template.Must(template.ParseFiles(templateFilename)) // tyto hodnoty budou použity při aplikaci šablony roles := []Role{ Role{"Eliška", "Najbrtová", 4}, Role{"Jenny", "Suk", 3}, Role{"Anička", "Šafářová", 0}, Role{"Sváťa", "Pulec", 3}, Role{"Blažej", "Motyčka", 8}, Role{"Eda", "Wasserfall", 0}, Role{"Přemysl", "Hájek", 10}, } // aplikace šablony - přepis hodnot err := tmpl.Execute(os.Stdout, roles) if err != nil { panic(err) } }
Výsledek, tedy vygenerovaná HTML stránka, odpovídá očekávání:
<html> <head> <title>Role ve hře Švestka</title> </head> <body> <h1>Role ve hře Švestka</h1> <table> <tr><th>Jméno</th><th>Příjmení</th><th>Popularita</th></tr> <tr><td>Eliška</td><td>Najbrtová</td><td>4</td></tr> <tr><td>Jenny</td><td>Suk</td><td>3</td></tr> <tr><td>Anička</td><td>Šafářová</td><td>×</td></tr> <tr><td>Sváťa</td><td>Pulec</td><td>3</td></tr> <tr><td>Blažej</td><td>Motyčka</td><td>8</td></tr> <tr><td>Eda</td><td>Wasserfall</td><td>×</td></tr> <tr><td>Přemysl</td><td>Hájek</td><td>10</td></tr> </table> </body> </html>
Obrázek 1: Vygenerovaná HTML stránka po zobrazení ve webovém prohlížeči.
5. Použití balíčku html/template namísto text/template
Náhrada balíčku text/template za balíček html/template je triviální, protože se jedná pouze o změnu jména balíčku, který je importován. Konkrétně namísto zvýrazněného importu:
import ( "os" "text/template" )
použijeme:
import ( "html/template" "os" )
Žádné další změny není nutné ve zdrojovém kódu provádět, o čemž se ostatně můžeme velmi snadno přesvědčit:
package main import ( "html/template" "os" ) const ( templateFilename = "html_template02.htm" ) // datový typ, jehož prvky budou vypisovány v šabloně type Role struct { Name string Surname string Popularity int } func main() { // vytvoření nové šablony tmpl := template.Must(template.ParseFiles(templateFilename)) // tyto hodnoty budou použity při aplikaci šablony roles := []Role{ Role{"Eliška", "Najbrtová", 4}, Role{"Jenny", "Suk", 3}, Role{"Anička", "Šafářová", 0}, Role{"Sváťa", "Pulec", 3}, Role{"Blažej", "Motyčka", 8}, Role{"Eda", "Wasserfall", 0}, Role{"Přemysl", "Hájek", 10}, } // aplikace šablony - přepis hodnot err := tmpl.Execute(os.Stdout, roles) if err != nil { panic(err) } }
Použitá šablona se taktéž nezmění:
<html> <head> <title>Role ve hře Švestka</title> </head> <body> <h1>Role ve hře Švestka</h1> <table> <tr><th>Jméno</th><th>Příjmení</th><th>Popularita</th></tr> {{range .}} <tr><td>{{.Name}}</td><td>{{.Surname}}</td><td>{{if gt .Popularity 0}}{{.Popularity}}{{else}}×{{end}}</td></tr> {{end}} </table> </body> </html>
Výsledek aplikace šablony:
<html> <head> <title>Role ve hře Švestka</title> </head> <body> <h1>Role ve hře Švestka</h1> <table> <tr><th>Jméno</th><th>Příjmení</th><th>Popularita</th></tr> <tr><td>Eliška</td><td>Najbrtová</td><td>4</td></tr> <tr><td>Jenny</td><td>Suk</td><td>3</td></tr> <tr><td>Anička</td><td>Šafářová</td><td>×</td></tr> <tr><td>Sváťa</td><td>Pulec</td><td>3</td></tr> <tr><td>Blažej</td><td>Motyčka</td><td>8</td></tr> <tr><td>Eda</td><td>Wasserfall</td><td>×</td></tr> <tr><td>Přemysl</td><td>Hájek</td><td>10</td></tr> </table> </body> </html>
Obrázek 2: Vygenerovaná HTML stránka po zobrazení ve webovém prohlížeči (naprosto shodná s prvním HTML stránkou).
6. Problematika XSS a balíčku text/template
V úvodních kapitolách jsme si řekli, že balíček html/template se od balíčku text/template liší především v tom ohledu, že zabraňuje tomu, aby se do výsledné HTML stránky (popř. do CSS souboru) vložily při aplikaci šablony takové údaje, které by mohly vést ke XSS (Cross-site scripting). Ostatně velmi jednoduché XSS si můžeme vytvořit sami. Nepatrně upravíme záznamy, na které bude aplikována šablona. Původní záznamy vypadaly takto:
roles := []Role{ Role{"Eliška", "Najbrtová", 4}, Role{"Jenny", "Suk", 3}, Role{"Anička", "Šafářová", 0}, Role{"Sváťa", "Pulec", 3}, Role{"Blažej", "Motyčka", 8}, Role{"Eda", "Wasserfall", 0}, Role{"Přemysl", "Hájek", 10}, }
V nové verzi záznamů je v jednom řetězci uložena HTML značka s kódem v JavaScriptu:
roles := []Role{ Role{"Eliška", "Najbrtová", 4}, Role{"Jenny", "Suk", 3}, Role{"Anička", "Šafářová", 0}, Role{"Sváťa", "Pulec", 3}, Role{"Blažej", "<script>alert('you have been pwned')</script>", 8}, Role{"Eda", "Wasserfall", 0}, Role{"Přemysl", "Hájek", 10}, }
Nejprve použijeme běžnou šablonu aplikovanou přes balíček text/template. Výsledkem bude tato HTML stránka. Povšimněte si, že značka script se stala nedílnou součástí této stránky:
<html> <head> <title>Role ve hře Švestka</title> </head> <body> <h1>Role ve hře Švestka</h1> <table> <tr><th>Jméno</th><th>Příjmení</th><th>Popularita</th></tr> <tr><td>Eliška</td><td>Najbrtová</td><td>4</td></tr> <tr><td>Jenny</td><td>Suk</td><td>3</td></tr> <tr><td>Anička</td><td>Šafářová</td><td>×</td></tr> <tr><td>Sváťa</td><td>Pulec</td><td>3</td></tr> <tr><td>Blažej</td><td><script>alert('you have been pwned')</script></td><td>8</td></tr> <tr><td>Eda</td><td>Wasserfall</td><td>×</td></tr> <tr><td>Přemysl</td><td>Hájek</td><td>10</td></tr> </table> </body> </html>
A v důsledku toho se po otevření stránky v prohlížeči zobrazí výsledek činnosti tohoto skriptu:
Obrázek 3: Toto s velkou pravděpodobností není očekávané chování.
Pro úplnost doplníme jak zdrojový kód, tak i použitou šablonu, i když se prakticky neliší od předchozích příkladů:
package main import ( "os" "text/template" ) const ( templateFilename = "html_template03.htm" ) // datový typ, jehož prvky budou vypisovány v šabloně type Role struct { Name string Surname string Popularity int } func main() { // vytvoření nové šablony tmpl := template.Must(template.ParseFiles(templateFilename)) // tyto hodnoty budou použity při aplikaci šablony roles := []Role{ Role{"Eliška", "Najbrtová", 4}, Role{"Jenny", "Suk", 3}, Role{"Anička", "Šafářová", 0}, Role{"Sváťa", "Pulec", 3}, Role{"Blažej", "<script>alert('you have been pwned')</script>", 8}, Role{"Eda", "Wasserfall", 0}, Role{"Přemysl", "Hájek", 10}, } // aplikace šablony - přepis hodnot err := tmpl.Execute(os.Stdout, roles) if err != nil { panic(err) } }
<html> <head> <title>Role ve hře Švestka</title> </head> <body> <h1>Role ve hře Švestka</h1> <table> <tr><th>Jméno</th><th>Příjmení</th><th>Popularita</th></tr> {{range .}} <tr><td>{{.Name}}</td><td>{{.Surname}}</td><td>{{if gt .Popularity 0}}{{.Popularity}}{{else}}×{{end}}</td></tr> {{end}} </table> </body> </html>
7. Zabránění XSS aplikací šablony přes balíček html/template
V případě, že namísto aplikace šablony nabízené balíčkem text/template využijeme balíček html/template, bude výše uvedenému XSS zabráněno, protože nyní se šablonovací systém bude snažit porozumět kontextu, ve kterém data do šablony vkládá. Ostatně si to můžeme velmi snadno ověřit:
package main import ( "html/template" "os" ) const ( templateFilename = "html_template04.htm" ) // datový typ, jehož prvky budou vypisovány v šabloně type Role struct { Name string Surname string Popularity int } func main() { // vytvoření nové šablony tmpl := template.Must(template.ParseFiles(templateFilename)) // tyto hodnoty budou použity při aplikaci šablony roles := []Role{ Role{"Eliška", "Najbrtová", 4}, Role{"Jenny", "Suk", 3}, Role{"Anička", "Šafářová", 0}, Role{"Sváťa", "Pulec", 3}, Role{"Blažej", "<script>alert('you have been pwned')</script>", 8}, Role{"Eda", "Wasserfall", 0}, Role{"Přemysl", "Hájek", 10}, } // aplikace šablony - přepis hodnot err := tmpl.Execute(os.Stdout, roles) if err != nil { panic(err) } }
Výsledek bude nyní vypadat následovně:
Role ve hře Švestka Role ve hře Švestka
Jméno | Příjmení | Popularita |
---|---|---|
Eliška | Najbrtová | 4 |
Jenny | Suk | 3 |
Anička | Šafářová | × |
Sváťa | Pulec | 3 |
Blažej | <script>alert(‚you have been pwned‘)</script> | 8 |
Eda | Wasserfall | × |
Přemysl | Hájek | 10 |
Nyní již bude zobrazení HTML stránky korektní, resp. přesněji řečeno k×XSS nedošlo:
Obrázek 4: Korektní zobrazení HTML stránky.
8. Kontext, ve kterém se data do HTML stránky vkládají při aplikaci šablony
Při aplikaci šablony balíčkem html/template dokáže šablonovací systém rozeznat kontext, ve kterém se data do HTML stránky vkládají. Například je rozeznáváno, zda mají být data uložena do HTML značek (což jsme si ukázali výše) nebo do odkazů (URL). Týká se to i detektoru XSS. V následujícím demonstračním příkladu je ukázáno, jak se stejné vstupní údaje do výsledné HTML stránky propíšou odlišně, pokud se bude jednat o běžný text nebo o část URL.
Samotný program pro aplikaci šablony na sedm záznamů představujících vstupní data vypadá stejně, jako tomu bylo i v předchozí kapitole:
package main import ( "html/template" "os" ) const ( templateFilename = "html_template05.htm" ) // datový typ, jehož prvky budou vypisovány v šabloně type Role struct { Name string Surname string Popularity int } func main() { // vytvoření nové šablony tmpl := template.Must(template.ParseFiles(templateFilename)) // tyto hodnoty budou použity při aplikaci šablony roles := []Role{ Role{"Eliška", "Najbrtová", 4}, Role{"Jenny", "Suk", 3}, Role{"Anička", "Šafářová", 0}, Role{"Sváťa", "Pulec", 3}, Role{"Blažej", "Motyčka", 8}, Role{"Eda", "Wasserfall", 0}, Role{"Přemysl", "Hájek", 10}, } // aplikace šablony - přepis hodnot err := tmpl.Execute(os.Stdout, roles) if err != nil { panic(err) } }
Odlišná je však samotná šablona. Povšimněte si, že nyní se atributy (položky) Name a Surname používají při konstrukci odkazu a posléze i přímo pro vložení jména postavy do tabulky:
Role ve hře Švestka Role ve hře Švestka {{range .}} {{end}}
Jméno | Příjmení | Popularita |
---|---|---|
{{.Name}} | {{.Surname}} | {{if gt .Popularity 0}}{{.Popularity}}{{else}}×{{end}} |
Po aplikaci šablony je patrné, že odkazy obsahují jinak zakódovaná jména (+ příjmení), než samotné buňky tabulky. Podobně by se postupovalo i při detekci XSS:
Role ve hře Švestka Role ve hře Švestka
Jméno | Příjmení | Popularita |
---|---|---|
Eliška | Najbrtová | 4 |
Jenny | Suk | 3 |
Anička | Šafářová | × |
Sváťa | Pulec | 3 |
Blažej | Motyčka | 8 |
Eda | Wasserfall | × |
Přemysl | Hájek | 10 |
Obrázek 5: Výsledná stránka po svém zobrazení v HTML prohlížeči.
9. HTML šablony a HTTP server realizovaný v jazyce Go
Šablony, jejichž výsledkem má být HTML stránka, lze použít například při generování statických webů. Ovšem mnohem zajímavější je využití šablon ve chvíli, kdy je přímo v jazyce Go implementován HTTP server (a pro tyto účely se jazyk Go používá velmi často). V následujících kapitolách si ukážeme několik příkladů HTTP serverů:
- HTTP server, který posílá (vybrané) statické HTML stránky popř. další typy souborů. Tyto stránky mohou vzniknout například aplikací šablon.
- HTTP server, který posílá dynamicky vytvořenou HTML stránku na základě šablony. Přitom je šablona vždy (tj. pro každý požadavek) znovu načtena ze souboru, což se hodí zejména při vývoji.
- HTTP server, který taktéž posílá dynamicky vytvořenou HTML stránku, ovšem samotná šablona je načtena jedenkrát, konkrétně během inicializace serveru. Ovšem šablona je pochopitelně znovu a znovu aplikována pro každý požadavek. Jedná se sice o jednoduché řešení, ovšem v praxi nemusí vždy vyhovovat.
- A konečně HTTP server, který šablonu načte ve chvíli, kdy je změněna. Jedná se sice o nejsložitější řešení, ovšem taktéž nejpraktičtější – šablonu není nutné znovunačítat při každém požadavku od uživatele, ovšem na druhou stranu je (většinou) vhodné reagovat na to, že šablonu někdo modifikuje.
10. Ukázka jednoduchého serveru, který poskytuje pouze statické stránky
Programovací jazyk Go obsahuje podporu pro tvorbu HTTP serverů přímo ve standardní knihovně, konkrétně v balíčku net/http. Vytvoření skutečného a plnohodnotného serveru je v tomto případě otázkou několika řádků zdrojového kódu. Základem je funkce HandleFunc, která nám umožňuje zaregistrovat obslužnou funkci (handler) v případě, že je server volán s určitým URL (endpointem). Můžeme si například zaregistrovat handler pro endpoint /:
http.HandleFunc("/", mainEndpoint)
Hlavička funkce HandleFunc přitom vypadá následovně:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
Povšimněte si, že druhým parametrem této funkce je jiná funkce (onen handler) s hlavičkou:
func MujHandler(ResponseWriter, *Request)
Konkrétně může implementace našeho handleru poslat na výstup (typu ResponseWriter) jednoduchý text, který bude zaslán klientovi v celé HTTP odpovědi (s hlavičkami, stavovým kódem, délkou atd. atd.):
func mainEndpoint(writer http.ResponseWriter, request *http.Request) { io.WriteString(writer, "Hello world!\n") }
Popř. můžeme klientovi poslat stránku načtenou ze souboru (zde prozatím velmi jednoduše – bez vyrovnávací paměti):
func sendStaticPage(writer http.ResponseWriter, filename string) { log.Printf("Sending static file %s", filename) body, err := ioutil.ReadFile(filename) if err == nil { fmt.Fprint(writer, string(body)) } else { writer.WriteHeader(http.StatusNotFound) fmt.Fprint(writer, "Not found!") } } func staticPage(filename string) func(writer http.ResponseWriter, request *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { sendStaticPage(writer, filename) } }
Existuje i snadnější možnost kombinující handler, ovšem upravený tak, aby přímo předávat statický obsah:
func filesEndpoint(writer http.ResponseWriter, request *http.Request) { url := request.URL.Path[len("/files/"):] println("Serving file from URL: " + url) http.ServeFile(writer, request, url) }
Pro čistě statické soubory je ovšem výhodnější použít http.FileServer a odstranit tak značnou část předchozího kódu:
fileServer := http.FileServer(http.Dir("./www")) http.Handle("/resources/", http.StripPrefix("/resources", fileServer))
Následně již stačí server spustit na určeném portu:
http.ListenAndServe(":8000", nil)
Úplná implementace jednoduchého HTTP serveru může vypadat takto:
package main import ( "fmt" "io/ioutil" "log" "net/http" ) func sendStaticPage(writer http.ResponseWriter, filename string) { log.Printf("Sending static file %s", filename) body, err := ioutil.ReadFile(filename) if err == nil { fmt.Fprint(writer, string(body)) } else { writer.WriteHeader(http.StatusNotFound) fmt.Fprint(writer, "Not found!") } } func staticPage(filename string) func(writer http.ResponseWriter, request *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { sendStaticPage(writer, filename) } } func main() { const address = ":8080" log.Printf("Starting server on address %s", address) http.HandleFunc("/", staticPage("index.html")) http.HandleFunc("/missing", staticPage("missing.html")) http.ListenAndServe(address, nil) }
Obrázek 6: Otestování HTTP serveru v prohlížeči.
11. HTTP server a aplikace HTML šablon
Nyní již můžeme spojit znalosti o šablonovacím systému se znalostmi o tvorbě HTTP serverů. V nové variantě HTTP serveru zaregistrujeme handler vyvolaný při přístupu na koncový bod /roles:
http.HandleFunc("/roles", rolesHandler("html_template05.htm", roles))
Můžeme vidět, že tento handler bude volán s uvedením jména souboru se šablonou a současně i s daty, které se mají šabloně předat. Handler tedy provede načtení šablony, aplikaci šablony a výsledek (což bude obsah dynamicky generované HTML stránky) pošle uživateli:
func rolesHandler(templateFilename string, roles []Role) func(writer http.ResponseWriter, request *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { log.Printf("Constructing template from file %s", templateFilename) // vytvoření nové šablony tmpl, err := template.ParseFiles(templateFilename) if err != nil { writer.WriteHeader(http.StatusInternalServerError) log.Printf("Template can't be constructed: %v", err) return } log.Printf("Application template for %d data records", len(roles)) // aplikace šablony - přepis hodnot err = tmpl.Execute(writer, roles) if err != nil { writer.WriteHeader(http.StatusInternalServerError) log.Printf("Error executing template: %v", err) return } } }
Úplný zdrojový kód takto upraveného HTTP serveru vypadá následovně:
package main import ( "fmt" "html/template" "io/ioutil" "log" "net/http" ) // datový typ, jehož prvky budou vypisovány v šabloně type Role struct { Name string Surname string Popularity int } func sendStaticPage(writer http.ResponseWriter, filename string) { log.Printf("Sending static file %s", filename) body, err := ioutil.ReadFile(filename) if err == nil { fmt.Fprint(writer, string(body)) } else { writer.WriteHeader(http.StatusNotFound) fmt.Fprint(writer, "Not found!") } } func staticPage(filename string) func(writer http.ResponseWriter, request *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { sendStaticPage(writer, filename) } } func rolesHandler(templateFilename string, roles []Role) func(writer http.ResponseWriter, request *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { log.Printf("Constructing template from file %s", templateFilename) // vytvoření nové šablony tmpl, err := template.ParseFiles(templateFilename) if err != nil { writer.WriteHeader(http.StatusInternalServerError) log.Printf("Template can't be constructed: %v", err) return } log.Printf("Application template for %d data records", len(roles)) // aplikace šablony - přepis hodnot err = tmpl.Execute(writer, roles) if err != nil { writer.WriteHeader(http.StatusInternalServerError) log.Printf("Error executing template: %v", err) return } } } func main() { // tyto hodnoty budou použity při aplikaci šablony roles := []Role{ Role{"Eliška", "Najbrtová", 4}, Role{"Jenny", "Suk", 3}, Role{"Anička", "Šafářová", 0}, Role{"Sváťa", "Pulec", 3}, Role{"Blažej", "Motyčka", 8}, Role{"Eda", "Wasserfall", 0}, Role{"Přemysl", "Hájek", 10}, } const address = ":8080" log.Printf("Starting server on address %s", address) http.HandleFunc("/", staticPage("index.html")) http.HandleFunc("/missing", staticPage("missing.html")) http.HandleFunc("/roles", rolesHandler("html_template05.htm", roles)) http.ListenAndServe(address, nil) }
12. Benchmark: rychlost načtení a aplikace šablon
Varianta HTTP serveru ukázaná v předchozí kapitole načítala a aplikovala šablonu v každém požadavku. Bylo by tedy dobré vědět, jak rychlé jsou vlastně tyto dvě operace a jaké odezvy popř. propustnost (throughput) můžeme od takto navrženého serveru očekávat. Přiblížení k reálným číslům nám může dát benchmark naprogramovaný s využitím standardního balíčku testing. Nejprve vytvoříme hlavní modul s funkcemi, které budeme v benchmarku spouštět; konkrétně s funkcemi pro načtení šablony a pro její aplikaci:
package main import ( "bytes" "fmt" "html/template" ) const ( templateFilename = "html_template05.htm" ) // datový typ, jehož prvky budou vypisovány v šabloně type Role struct { Name string Surname string Popularity int } func readTemplate(templateFilename string) *template.Template { // vytvoření nové šablony return template.Must(template.ParseFiles(templateFilename)) } func applyTemplate(tmpl *template.Template, roles []Role) int { buffer := new(bytes.Buffer) // aplikace šablony - přepis hodnot err := tmpl.Execute(buffer, roles) if err != nil { panic(err) } return buffer.Len() }
Tyto dvě funkce budeme spouštět z benchmarku. Pro zajímavost si otestujeme i rychlost provádění obou zmíněných operací. Samotný benchmark může vypadat následovně:
package main import ( "testing" ) func BenchmarkReadTemplate(b *testing.B) { for i := 0; i < b.N; i++ { tmpl := readTemplate("./html_template05.htm") if tmpl == nil { b.Fatal("Template was not created") } } } func BenchmarkApplyTemplate(b *testing.B) { // tyto hodnoty budou použity při aplikaci šablony roles := []Role{ Role{"Eliška", "Najbrtová", 4}, Role{"Jenny", "Suk", 3}, Role{"Anička", "Šafářová", 0}, Role{"Sváťa", "Pulec", 3}, Role{"Blažej", "Motyčka", 8}, Role{"Eda", "Wasserfall", 0}, Role{"Přemysl", "Hájek", 10}, } // načtení šablony (jen jedenkrát) tmpl := readTemplate("./html_template05.htm") // samotný benchmark for i := 0; i < b.N; i++ { length := applyTemplate(tmpl, roles) if length <= 0 { b.Fatal("Don't work") } } } func BenchmarkReadAndApplyTemplate(b *testing.B) { // tyto hodnoty budou použity při aplikaci šablony roles := []Role{ Role{"Eliška", "Najbrtová", 4}, Role{"Jenny", "Suk", 3}, Role{"Anička", "Šafářová", 0}, Role{"Sváťa", "Pulec", 3}, Role{"Blažej", "Motyčka", 8}, Role{"Eda", "Wasserfall", 0}, Role{"Přemysl", "Hájek", 10}, } // samotný benchmark for i := 0; i < b.N; i++ { tmpl := readTemplate("./html_template05.htm") if tmpl == nil { b.Fatal("Template was not created") } length := applyTemplate(tmpl, roles) if length <= 0 { b.Fatal("Don't work") } } }
Benchmark spustíme tímto příkazem:
$ go test -bench=.
Výsledky dosažené na notebooku s mikroprocesorem i7:
goos: linux goarch: amd64 pkg: template cpu: Intel(R) Core(TM) i7-8665U CPU @ 1.90GHz BenchmarkReadTemplate-8 44815 26499 ns/op BenchmarkApplyTemplate-8 22891 53443 ns/op BenchmarkReadAndApplyTemplate-8 10000 122381 ns/op PASS ok template 4.451s
13. Vytvoření šablon pouze při inicializaci HTTP serveru
V případě, že šablony nebudou měněny často, lze čas vyřizování požadavků zkrátit, a to takovým způsobem, že se šablony načtou již při inicializaci serveru. Poté již bude docházet pouze k jejich aplikaci. V případě, že se šablona změní (tedy pokud někdo modifikuje její soubor), nebude tato změna HTTP serverem vůbec reflektována.
Ve zdrojovém kódu je nutné změnit handler rolesHandler, a to takovým způsobem, že se mu namísto jména souboru se šablonou předá přímo již načtená šablona:
package main import ( "fmt" "html/template" "io/ioutil" "log" "net/http" ) // datový typ, jehož prvky budou vypisovány v šabloně type Role struct { Name string Surname string Popularity int } func sendStaticPage(writer http.ResponseWriter, filename string) { log.Printf("Sending static file %s", filename) body, err := ioutil.ReadFile(filename) if err == nil { fmt.Fprint(writer, string(body)) } else { writer.WriteHeader(http.StatusNotFound) fmt.Fprint(writer, "Not found!") } } func staticPage(filename string) func(writer http.ResponseWriter, request *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { sendStaticPage(writer, filename) } } func rolesHandler(tmpl *template.Template, roles []Role) func(writer http.ResponseWriter, request *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { log.Printf("Application template for %d data records", len(roles)) // aplikace šablony - přepis hodnot err := tmpl.Execute(writer, roles) if err != nil { writer.WriteHeader(http.StatusInternalServerError) log.Printf("Error executing template: %v", err) return } } } func main() { const templateFilename = "html_template05.htm" // tyto hodnoty budou použity při aplikaci šablony roles := []Role{ Role{"Eliška", "Najbrtová", 4}, Role{"Jenny", "Suk", 3}, Role{"Anička", "Šafářová", 0}, Role{"Sváťa", "Pulec", 3}, Role{"Blažej", "Motyčka", 8}, Role{"Eda", "Wasserfall", 0}, Role{"Přemysl", "Hájek", 10}, } log.Printf("Constructing template from file %s", templateFilename) // vytvoření nové šablony tmpl, err := template.ParseFiles(templateFilename) if err != nil { log.Fatalf("Template can't be constructed: %v", err) return } const address = ":8080" log.Printf("Starting server on address %s", address) http.HandleFunc("/", staticPage("index.html")) http.HandleFunc("/missing", staticPage("missing.html")) http.HandleFunc("/roles", rolesHandler(tmpl, roles)) http.ListenAndServe(address, nil) }
14. Detekce modifikace šablon
Prozatím jsme si ukázali dvě varianty HTTP serverů. V první variantě byla šablona načítána pro každý požadavek (a tudíž se reflektovaly všechny změny, ovšem na úkor neustále prováděných načítání); ve variantě druhé se pak šablona načetla při inicializaci serveru a její následné úpravy vůbec nebyly reflektovány. V praxi by však bylo vhodné oba přístupy nějakým způsobem zkombinovat, konkrétně dosáhnout toho, aby změny šablon byly reflektovány, ale aby šablony nebylo nutné načítat při každém zpracování požadavku. Řešení pochopitelně existuje a je založeno na sledování změn souborů se šablonou. Teoreticky by bylo možné využít standardní balíček os s funkcí Stat a sledovat čas modifikace souborů, ovšem výhodnější bude využít možnosti nabízené jádrem systému – to totiž umožňuje sledovat změny provedené ve sledovaných souborech (kde soubor je v tomto případě na Linuxu identifikován i-uzlem).
Tato funkcionalita je implementovaná v knihovně fsnotify/fsnotify, kterou využijeme v dalších dvou příkladech. Nejdříve je nutné vytvořit projekt, který tuto knihovnu využívá, takže po příkazu go mod init provedeme úpravu tohoto souboru:
module fsnotify-test go 1.17 require ( github.com/fsnotify/fsnotify v1.5.1 // indirect golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect )
15. Příklad detekce změn v souborech
Knihovna fsnotify/fsnotify je založena na sledování změn v souboru, který je identifikován svým i-uzlem. To ovšem znamená, že není možné sledovat přímo konkrétní soubor se šablonou, protože textové editory při ukládání typicky původní soubor přejmenují a změny uloží do souboru nového. Sledovat je tedy nutné změny v adresáři, v němž se soubor nachází. Zajímat nás budou zápisy do tohoto speciálního souboru (adresáře jsou z pohledu systému taktéž soubory). Pokud k zápisu dojde, zjistíme jméno změněného souboru přímo z události, kterou knihovna fsnotify/fsnotify vytvoří:
select { case event, ok := <-watcher.Events: if !ok { return } log.Println("event:", event) if event.Op&fsnotify.Write == fsnotify.Write { log.Println("modified file:", event.Name) if filename == event.Name { log.Println("Template change detected") } } case err, ok := <-watcher.Errors: if !ok { return } log.Println("error:", err) }
Program, který bude sledovat změny souboru html_template06.htm může vypadat následovně:
package main import ( "log" "github.com/fsnotify/fsnotify" ) const templateFilename = "./html_template06.htm" func startWatcher(directory string, filename string) { log.Print("Starting watcher") watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatal(err) } defer watcher.Close() log.Printf("Watching directory %s", directory) err = watcher.Add(directory) if err != nil { log.Fatal(err) } for { select { case event, ok := <-watcher.Events: if !ok { return } log.Println("event:", event) if event.Op&fsnotify.Write == fsnotify.Write { log.Println("modified file:", event.Name) if filename == event.Name { log.Println("Template change detected") } } case err, ok := <-watcher.Errors: if !ok { return } log.Println("error:", err) } } } func main() { startWatcher(".", templateFilename) }
16. HTTP server detekující změny šablon
Knihovnu fsnotify/fsnotify můžeme integrovat do HTTP serveru a zajistit tak, aby se šablony načetly až ve chvíli, kdy skutečně dojde k jejich změně. Samotný detektor změn bude spuštěn ve vlastní gorutině a informace o změněných souborech bude posílat do kanálu changed:
changed := make(chan string) go startWatcher(".", templateFilename, changed)
Samotná implementace gorutiny vychází z příkladu uvedeného výše. Přidán je pouze kanál, do něhož jsou posílána jména změněných souborů:
func startWatcher(directory string, filename string, changed chan string) { log.Print("Starting watcher") watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatal(err) } defer watcher.Close() log.Printf("Watching directory %s", directory) err = watcher.Add(directory) if err != nil { log.Fatal(err) } for { select { case event, ok := <-watcher.Events: if !ok { return } log.Println("event:", event) if event.Op&fsnotify.Write == fsnotify.Write { log.Println("modified file:", event.Name) if filename == event.Name { log.Println("Template change detected") changed <- filename } } case err, ok := <-watcher.Errors: if !ok { return } log.Println("error:", err) } } }
Změnit se ovšem musí i samotný handler, který nejprve otestuje, jestli došlo k modifikaci šablony a pokud ano, tuto šablonu načte (nekontroluje se však jméno souboru – tuto část resp. podmínku je však triviální přidat):
func rolesHandler(tmpl *template.Template, roles []Role, changed chan string) func(writer http.ResponseWriter, request *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { select { case filename := <-changed: log.Printf("Have to reload template from file %s", filename) tmpl = loadTemplate(filename) default: log.Print("Using old template") } log.Printf("Application template for %d data records", len(roles)) // aplikace šablony - přepis hodnot err := tmpl.Execute(writer, roles) if err != nil { writer.WriteHeader(http.StatusInternalServerError) log.Printf("Error executing template: %v", err) return } } }
17. Úplný zdrojový kód serveru
Úplný zdrojový kód serveru popsaného v předchozí kapitole bude vypadat následovně:
package main import ( "fmt" "html/template" "io/ioutil" "log" "net/http" "github.com/fsnotify/fsnotify" ) const templateFilename = "./html_template06.htm" // datový typ, jehož prvky budou vypisovány v šabloně type Role struct { Name string Surname string Popularity int } func sendStaticPage(writer http.ResponseWriter, filename string) { log.Printf("Sending static file %s", filename) body, err := ioutil.ReadFile(filename) if err == nil { fmt.Fprint(writer, string(body)) } else { writer.WriteHeader(http.StatusNotFound) fmt.Fprint(writer, "Not found!") } } func staticPage(filename string) func(writer http.ResponseWriter, request *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { sendStaticPage(writer, filename) } } func rolesHandler(tmpl *template.Template, roles []Role, changed chan string) func(writer http.ResponseWriter, request *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { select { case filename := <-changed: log.Printf("Have to reload template from file %s", filename) tmpl = loadTemplate(filename) default: log.Print("Using old template") } log.Printf("Application template for %d data records", len(roles)) // aplikace šablony - přepis hodnot err := tmpl.Execute(writer, roles) if err != nil { writer.WriteHeader(http.StatusInternalServerError) log.Printf("Error executing template: %v", err) return } } } func loadTemplate(templateFilename string) *template.Template { log.Printf("Constructing template from file %s", templateFilename) // vytvoření nové šablony tmpl, err := template.ParseFiles(templateFilename) if err != nil { log.Fatalf("Template can't be constructed: %v", err) return nil } return tmpl } func startWatcher(directory string, filename string, changed chan string) { log.Print("Starting watcher") watcher, err := fsnotify.NewWatcher() if err != nil { log.Fatal(err) } defer watcher.Close() log.Printf("Watching directory %s", directory) err = watcher.Add(directory) if err != nil { log.Fatal(err) } for { select { case event, ok := <-watcher.Events: if !ok { return } log.Println("event:", event) if event.Op&fsnotify.Write == fsnotify.Write { log.Println("modified file:", event.Name) if filename == event.Name { log.Println("Template change detected") changed <- filename } } case err, ok := <-watcher.Errors: if !ok { return } log.Println("error:", err) } } } func main() { // tyto hodnoty budou použity při aplikaci šablony roles := []Role{ Role{"Eliška", "Najbrtová", 4}, Role{"Jenny", "Suk", 3}, Role{"Anička", "Šafářová", 0}, Role{"Sváťa", "Pulec", 3}, Role{"Blažej", "Motyčka", 8}, Role{"Eda", "Wasserfall", 0}, Role{"Přemysl", "Hájek", 10}, } changed := make(chan string) go startWatcher(".", templateFilename, changed) tmpl := loadTemplate(templateFilename) const address = ":8080" log.Printf("Starting server on address %s", address) http.HandleFunc("/", staticPage("index.html")) http.HandleFunc("/missing", staticPage("missing.html")) http.HandleFunc("/roles", rolesHandler(tmpl, roles, changed)) http.ListenAndServe(address, nil) }
18. Závěr
Většinou zjistíte, že neustálé načítání šablon aplikaci nijak zásadně nezpomaluje, přesněji řečeno že onen pověstný bottleneck se nachází v jiné části HTTP serveru. Popř. si infrastruktura vyžádá, že změny šablon vůbec nemohou být provedeny bez „otočení“ uzlu s HTTP serverem. Nicméně i přesto může být užitečné tuto část HTTP serveru (resp. celé služby) sledovat a například exportovat formou metrik pro Prometheus.
19. Repositář s demonstračními příklady
Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do nového Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně stovku kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
# | Příklad/soubor | Stručný popis | Cesta |
---|---|---|---|
1 | text_template01.go | aplikace textové šablony na pole/řez záznamů | https://github.com/tisnik/go-root/blob/master/article87/text_template01.go |
2 | text_template01.txt | textová šablona použitá v tomto demonstračním příkladu | https://github.com/tisnik/go-root/blob/master/article87/text_template01.txt |
3 | html_template01.go | aplikace HTML šablony balíčkem text/template | https://github.com/tisnik/go-root/blob/master/article87/html_template01.go |
4 | html_template01.htm | HTML šablona použitá v tomto demonstračním příkladu | https://github.com/tisnik/go-root/blob/master/article87/html_template01.htm |
5 | html_template02.go | aplikace HTML šablony balíčkem html/template | https://github.com/tisnik/go-root/blob/master/article87/html_template02.go |
6 | html_template02.htm | HTML šablona použitá v tomto demonstračním příkladu | https://github.com/tisnik/go-root/blob/master/article87/html_template02.htm |
7 | html_template03.go | aplikace HTML šablony s daty obsahujícími přípravu pro XSS | https://github.com/tisnik/go-root/blob/master/article87/html_template03.go |
8 | html_template03.htm | HTML šablona použitá v tomto demonstračním příkladu | https://github.com/tisnik/go-root/blob/master/article87/html_template03.htm |
9 | html_template04.go | aplikace HTML šablony s daty obsahujícími přípravu pro XSS | https://github.com/tisnik/go-root/blob/master/article87/html_template04.go |
10 | html_template04.htm | HTML šablona použitá v tomto demonstračním příkladu | https://github.com/tisnik/go-root/blob/master/article87/html_template04.htm |
11 | html_template05.go | vkládání dat do HTML stránky v různém kontextu | https://github.com/tisnik/go-root/blob/master/article87/html_template05.go |
12 | html_template05.htm | HTML šablona použitá v tomto demonstračním příkladu | https://github.com/tisnik/go-root/blob/master/article87/html_template05.htm |
13 | static_pages.go | HTTP server se statickými stránkami | https://github.com/tisnik/go-root/blob/master/article87/static_pages.go |
14 | template_pages.go | HTTP server aplikující šablony na předaná data | https://github.com/tisnik/go-root/blob/master/article87/template_pages.go |
15 | template_pages2.go | HTTP server aplikující šablony na předaná data, cache šablon | https://github.com/tisnik/go-root/blob/master/article87/template_pages2.go |
16 | fsnotify/ | detekce změny souboru s využitím knihovny fsnotify/fsnotify | https://github.com/tisnik/go-root/blob/master/article87/fsnotify/ |
17 | template_pages3/ | implementace HTTP serveru, který dokáže automaticky načíst upravené šablony | https://github.com/tisnik/go-root/blob/master/article87/template_pages3/ |
18 | benchmark/ | benchmark měřící rychlost načítání šablon i aplikace šablon | https://github.com/tisnik/go-root/blob/master/article87/benchmark/ |
20. Odkazy na Internetu
- Dokumentace ke knihovně fsnotify
https://pkg.go.dev/github.com/fsnotify/fsnotify - Cross-site scripting
https://en.wikipedia.org/wiki/Cross-site_scripting - 5 Real-World Cross Site Scripting Examples
https://websitesecuritystore.com/blog/real-world-cross-site-scripting-examples/ - Cross Site Scripting (XSS) Attack Tutorial with Examples, Types & Prevention
https://www.softwaretestinghelp.com/cross-site-scripting-xss-attack-test/ - Dokumentace ke standardní knihovně jazyka Go
https://pkg.go.dev/std - Awesome Go
https://awesome-go.com/ - Template Engines for Go
https://awesome-go.com/#template-engines - Mail merge
https://en.wikipedia.org/wiki/Mail_merge - Template processor
https://en.wikipedia.org/wiki/Template_processor - Text/template
https://pkg.go.dev/text/template - Go Template Engines
https://go.libhunt.com/categories/556-template-engines - Template Engines
https://reposhub.com/go/template-engines - GoLang Templating Made Easy
https://awkwardferny.medium.com/golang-templating-made-easy-4d69d663c558 - Templates in GoLang
https://golangdocs.com/templates-in-golang - What are the best template engines for Go apart from „html/template“?
https://www.quora.com/What-are-the-best-template-engines-for-Go-apart-from-html-template?share=1 - Ace – HTML template engine for Go
https://github.com/yosssi/ace - amber
https://github.com/eknkc/amber - quicktemplate
https://github.com/valyala/quicktemplate - Šablonovací systém ace
https://github.com/yosssi/ace - Šablonovací systém amber
https://github.com/eknkc/amber - Šablonovací systém damsel
https://github.com/dskinner/damsel - Šablonovací systém ego
https://github.com/benbjohnson/ego - Šablonovací systém extemplate
https://github.com/dannyvankooten/extemplate - Šablonovací systém fasttemplate
https://github.com/valyala/fasttemplate - Šablonovací systém gofpdf
https://github.com/jung-kurt/gofpdf - Šablonovací systém gospin
https://github.com/m1/gospin - Šablonovací systém goview
https://github.com/foolin/goview