rushlink/views.go

301 lines
8.5 KiB
Go
Raw Normal View History

2019-11-09 15:50:12 +01:00
package rushlink
2019-09-15 19:51:54 +02:00
//go:generate go-bindata -pkg $GOPACKAGE -prefix assets/ assets/...
2019-09-15 19:51:54 +02:00
import (
2019-09-15 22:54:07 +02:00
"bytes"
2019-09-15 19:51:54 +02:00
"fmt"
html "html/template"
"io"
"log"
"net/http"
"path/filepath"
"regexp"
"runtime/debug"
"sort"
"strconv"
"strings"
text "text/template"
2019-12-16 11:51:41 +01:00
"time"
2019-09-15 19:51:54 +02:00
"github.com/pkg/errors"
)
2019-12-17 11:13:32 +01:00
const defaultScheme = "http"
2019-12-15 12:36:48 +01:00
2019-09-15 19:51:54 +02:00
// Plain text templates
2019-09-19 21:42:01 +02:00
var textBaseTemplate *text.Template = text.Must(text.New("").Parse(string(MustAsset("templates/txt/base.txt.tmpl"))))
var htmlBaseTemplate *html.Template = html.Must(html.New("").Parse(string(MustAsset("templates/html/base.html.tmpl"))))
2019-09-15 19:51:54 +02:00
// Template collections
2021-05-07 10:17:17 +02:00
var textTemplates = make(map[string]*text.Template)
var htmlTemplates = make(map[string]*html.Template)
2019-09-15 19:51:54 +02:00
// Used by resolveResponseContentType
var acceptHeaderMediaRangeRegex = regexp.MustCompile(`^\s*([^()<>@,;:\\"/\[\]?.=]+)/([^()<>@,;:\\"/\[\]?.=]+)\s*$`)
var acceptHeaderAcceptParamsRegex = regexp.MustCompile(`^\s*(\w+)=([A-Za-z0-9.-]+)\s*$`)
2019-09-15 19:51:54 +02:00
var acceptHeaderWeight = regexp.MustCompile(`^\s*q=0(?:\.([0-9]{0,3}))|1(?:\.0{0,3})\s*$`)
// HTML templates
func init() {
for _, tmplPath := range AssetNames() {
2019-09-15 21:34:41 +02:00
if mustMatch("templates/txt/*.txt.tmpl", tmplPath) {
2019-09-19 21:42:01 +02:00
base := text.Must(textBaseTemplate.Clone())
tmpl, err := base.Parse(string(MustAsset(tmplPath)))
if err != nil {
panic(errors.Wrapf(err, "parsing %v", tmplPath))
}
2019-09-15 19:51:54 +02:00
tmplName := strings.TrimSuffix(filepath.Base(tmplPath), ".txt.tmpl")
2019-09-19 21:42:01 +02:00
textTemplates[tmplName] = tmpl
2019-09-15 19:51:54 +02:00
continue
}
2019-09-15 21:34:41 +02:00
if mustMatch("templates/html/*.html.tmpl", tmplPath) {
2019-09-19 21:42:01 +02:00
base := html.Must(htmlBaseTemplate.Clone())
tmpl, err := base.Parse(string(MustAsset(tmplPath)))
if err != nil {
panic(errors.Wrapf(err, "parsing %v", tmplPath))
}
2019-09-15 19:51:54 +02:00
tmplName := strings.TrimSuffix(filepath.Base(tmplPath), ".html.tmpl")
2019-09-19 21:42:01 +02:00
htmlTemplates[tmplName] = tmpl
2019-09-15 19:51:54 +02:00
continue
}
}
2019-09-15 21:34:41 +02:00
// Sanity check. Both maps should not be empty
2019-09-19 21:42:01 +02:00
if len(textTemplates) == 0 || len(htmlTemplates) == 0 {
2019-09-15 21:34:41 +02:00
panic("template loading failed")
}
2019-09-15 19:51:54 +02:00
}
func mustMatch(pattern, name string) bool {
m, err := filepath.Match(pattern, name)
if err != nil {
panic("%v error in call to mustMatch")
}
return m
}
func mapExtend(m map[string]interface{}, key string, value interface{}) {
if m[key] != nil {
return
}
m[key] = value
}
2019-12-16 11:51:41 +01:00
func (rl *rushlink) renderStatic(w http.ResponseWriter, r *http.Request, path string) {
var modTime time.Time
if info, err := AssetInfo(path); err == nil {
2019-12-16 11:51:41 +01:00
modTime = info.ModTime()
}
contents, err := Asset(path)
if err != nil {
rl.renderError(w, r, http.StatusNotFound, err.Error())
return
}
http.ServeContent(w, r, path, modTime, bytes.NewReader(contents))
}
2020-05-11 22:45:56 +02:00
func (rl *rushlink) render(w http.ResponseWriter, r *http.Request, status int, tmplName string, data map[string]interface{}) {
2019-09-15 19:51:54 +02:00
contentType, err := resolveResponseContentType(r, []string{"text/plain", "text/html"})
if err != nil {
w.WriteHeader(http.StatusNotAcceptable)
fmt.Fprintf(w, "error parsing Accept header: %v\n", err)
return
2019-09-15 19:51:54 +02:00
}
2019-09-21 13:11:38 +02:00
// Add the request to the template data
2019-12-17 11:13:32 +01:00
mapExtend(data, "RootURL", rl.resolveRootURL(r))
mapExtend(data, "Request", r)
2019-09-21 13:11:38 +02:00
2019-09-15 19:51:54 +02:00
switch contentType {
case "text/plain":
w.Header().Set("Content-Type", "text/plain")
2019-09-19 21:42:01 +02:00
tmpl := textTemplates[tmplName]
2019-09-15 19:51:54 +02:00
if tmpl == nil {
2019-09-19 21:42:01 +02:00
err = fmt.Errorf("'%v' not in textTemplates", tmplName)
2019-09-15 19:51:54 +02:00
break
}
if status != 0 {
w.WriteHeader(status)
}
if r.Method != "HEAD" {
err = tmpl.Execute(w, data)
}
2019-09-15 19:51:54 +02:00
case "text/html":
w.Header().Set("Content-Type", "text/html")
2019-09-19 21:42:01 +02:00
tmpl := htmlTemplates[tmplName]
2019-09-15 19:51:54 +02:00
if tmpl == nil {
2019-09-19 21:42:01 +02:00
err = fmt.Errorf("'%v' not in htmlTemplates", tmplName)
2019-09-15 19:51:54 +02:00
break
}
2019-09-15 22:54:07 +02:00
// Construct a (lazy) plain-text view for inclusion in <pre>
2019-09-21 13:11:38 +02:00
data["Pre"] = func() string {
2019-09-19 21:42:01 +02:00
tmpl := textTemplates[tmplName]
2019-09-15 22:54:07 +02:00
if tmpl == nil {
2019-09-19 21:42:01 +02:00
panic(fmt.Errorf("'%v' not in textTemplates", tmplName))
2019-09-15 22:54:07 +02:00
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
panic(err)
}
return buf.String()
}
2020-05-11 22:45:56 +02:00
if status != 0 {
w.WriteHeader(status)
}
if r.Method != "HEAD" {
err = tmpl.Execute(w, data)
2019-12-15 07:13:49 +01:00
}
2019-09-15 19:51:54 +02:00
default:
2019-09-15 22:54:07 +02:00
// Fall back to plain text without template
2019-09-15 19:51:54 +02:00
w.WriteHeader(http.StatusNotAcceptable)
io.WriteString(w, "could not resolve an acceptable content-type\n")
}
if err != nil {
panic(err)
}
}
2019-09-15 21:34:41 +02:00
func (rl *rushlink) renderError(w http.ResponseWriter, r *http.Request, status int, msg string) {
2020-05-11 22:45:56 +02:00
rl.render(w, r, status, "error", map[string]interface{}{"Message": msg})
2019-09-15 21:34:41 +02:00
}
func (rl *rushlink) renderInternalServerError(w http.ResponseWriter, r *http.Request, err interface{}) {
2019-09-15 21:34:41 +02:00
msg := fmt.Sprintf("internal server error: %v", err)
rl.renderError(w, r, http.StatusInternalServerError, msg)
2019-09-15 21:34:41 +02:00
}
2019-12-15 12:36:48 +01:00
2019-12-17 11:13:32 +01:00
// resolveRootURL constructs the `scheme://host` part of rushlinks public API.
2019-12-15 12:36:48 +01:00
//
2019-12-17 11:13:32 +01:00
// If the `--root_url` flag is set, it will return that URL.
2019-12-15 12:36:48 +01:00
// Otherwise, this function will return 'https://{Host}', where `{Host}` is
// the value provided by the client in the HTTP `Host` header. This value may
// be invalid, but it is impossible to handle this error (because we *cannot*
// know the real host).
2019-12-17 11:13:32 +01:00
func (rl *rushlink) resolveRootURL(r *http.Request) string {
rlHost := rl.RootURL()
2019-12-15 12:36:48 +01:00
if rlHost != nil {
2019-12-17 11:13:32 +01:00
// Root URL overridden by command line arguments
2019-12-15 12:36:48 +01:00
return rlHost.String()
}
2019-12-17 11:13:32 +01:00
// Guess scheme
scheme := defaultScheme
forwardedScheme := r.Header.Get("X-Forwarded-Proto")
switch forwardedScheme {
case "http":
scheme = "http"
case "https":
scheme = "https"
}
// Guess host
host := r.Host
if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" {
host = forwardedHost
}
return scheme + "://" + host
2019-12-15 12:36:48 +01:00
}
2019-09-15 19:51:54 +02:00
// Try to resolve the preferred content-type for the response to this request.
//
// This is done by reading from the `types` argument. If one of them matches
// the preferences supplied by the client in their Accept header, we will
// return that one. We will take the clients preferences into account.
//
// Iff no match could be found, this function will return an empty string, and
// the caller should probably respond with a 406 Not Acceptable status code.
// Iff the Accept header was invalid, we will return an error. In this case,
// the situation calls for a 400 Bad Request.
func resolveResponseContentType(r *http.Request, types []string) (string, error) {
// Ref: https://tools.ietf.org/html/rfc7231#section-5.3.2
if len(types) == 0 {
return "", nil
}
acceptHeader := strings.TrimSpace(r.Header.Get("Accept"))
2019-09-15 19:51:54 +02:00
if acceptHeader == "" {
return types[0], nil
}
type AcceptValue struct {
Type string
Subtype string
Weight int
}
avStrings := strings.Split(acceptHeader, ",")
avs := make([]AcceptValue, len(avStrings))
for i, avString := range avStrings {
av := AcceptValue{Weight: 1000}
choiceParts := strings.Split(avString, ";")
mediaRange := acceptHeaderMediaRangeRegex.FindStringSubmatch(choiceParts[0])
if mediaRange == nil {
return "", fmt.Errorf("bad media-range ('%v')", choiceParts[0])
2019-09-15 19:51:54 +02:00
}
av.Type = mediaRange[1]
av.Subtype = mediaRange[2]
// Go through the rest to see if there is a q=... parameter
for choiceParts = choiceParts[1:]; len(choiceParts) > 0; choiceParts = choiceParts[1:] {
// Try to parse the weight param
weight := acceptHeaderWeight.FindStringSubmatch(choiceParts[0])
if weight != nil {
if weight[1] == "" {
av.Weight = 0
} else {
var err error
av.Weight, err = strconv.Atoi((weight[1] + "000")[:3])
if err != nil {
log.Println("error: unreachable statement")
debug.PrintStack()
av.Weight = 1000 // Reset to default value
}
}
break
}
// Check if this parameter is still invalid in any case
acceptParams := acceptHeaderAcceptParamsRegex.FindStringSubmatchIndex(choiceParts[0])
if acceptParams == nil {
return "", fmt.Errorf("bad accept-params ('%v')", choiceParts[0])
2019-09-15 19:51:54 +02:00
}
}
avs[i] = av
}
sort.SliceStable(avs, func(i, j int) bool {
if avs[i].Weight > avs[j].Weight {
return true
}
if avs[i].Type != "*" && avs[j].Type == "*" {
return true
}
if avs[i].Subtype != "*" && avs[j].Subtype == "*" {
return true
}
return false
})
avArgs := make([]AcceptValue, len(types))
for i, fulltype := range types {
split := strings.Split(fulltype, "/")
if len(split) == 1 {
avArgs[i] = AcceptValue{Type: split[0]}
} else {
avArgs[i] = AcceptValue{Type: split[0], Subtype: split[1]}
}
}
for _, av := range avs {
for j, avArg := range avArgs {
if !(av.Type == avArg.Type || av.Type == "*" || avArg.Type == "*") {
continue
}
if !(av.Subtype == avArg.Subtype || av.Subtype == "*" || avArg.Subtype == "*") {
continue
}
return types[j], nil
}
}
return "", nil
}