package rushlink //go:generate go-bindata -pkg $GOPACKAGE -prefix assets/ assets/... import ( "bytes" "fmt" html "html/template" "io" "log" "net/http" "path/filepath" "regexp" "runtime/debug" "sort" "strconv" "strings" text "text/template" "time" "github.com/pkg/errors" ) const defaultScheme = "http" // Plain text templates 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")))) // Template collections var textTemplates = make(map[string]*text.Template) var htmlTemplates = make(map[string]*html.Template) // Used by resolveResponseContentType var acceptHeaderMediaRangeRegex = regexp.MustCompile(`^\s*([^()<>@,;:\\"/\[\]?.=]+)/([^()<>@,;:\\"/\[\]?.=]+)\s*$`) var acceptHeaderAcceptParamsRegex = regexp.MustCompile(`^\s*(\w+)=([A-Za-z0-9.-]+)\s*$`) var acceptHeaderWeight = regexp.MustCompile(`^\s*q=0(?:\.([0-9]{0,3}))|1(?:\.0{0,3})\s*$`) // HTML templates func init() { for _, tmplPath := range AssetNames() { if mustMatch("templates/txt/*.txt.tmpl", tmplPath) { base := text.Must(textBaseTemplate.Clone()) tmpl, err := base.Parse(string(MustAsset(tmplPath))) if err != nil { panic(errors.Wrapf(err, "parsing %v", tmplPath)) } tmplName := strings.TrimSuffix(filepath.Base(tmplPath), ".txt.tmpl") textTemplates[tmplName] = tmpl continue } if mustMatch("templates/html/*.html.tmpl", tmplPath) { base := html.Must(htmlBaseTemplate.Clone()) tmpl, err := base.Parse(string(MustAsset(tmplPath))) if err != nil { panic(errors.Wrapf(err, "parsing %v", tmplPath)) } tmplName := strings.TrimSuffix(filepath.Base(tmplPath), ".html.tmpl") htmlTemplates[tmplName] = tmpl continue } } // Sanity check. Both maps should not be empty if len(textTemplates) == 0 || len(htmlTemplates) == 0 { panic("template loading failed") } } 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 } func (rl *rushlink) renderStatic(w http.ResponseWriter, r *http.Request, path string) { var modTime time.Time if info, err := AssetInfo(path); err == nil { 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)) } func (rl *rushlink) render(w http.ResponseWriter, r *http.Request, status int, tmplName string, data map[string]interface{}) { 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 } // Add the request to the template data mapExtend(data, "RootURL", rl.resolveRootURL(r)) mapExtend(data, "Request", r) switch contentType { case "text/plain": w.Header().Set("Content-Type", "text/plain") tmpl := textTemplates[tmplName] if tmpl == nil { err = fmt.Errorf("'%v' not in textTemplates", tmplName) break } if status != 0 { w.WriteHeader(status) } if r.Method != "HEAD" { err = tmpl.Execute(w, data) } case "text/html": w.Header().Set("Content-Type", "text/html") tmpl := htmlTemplates[tmplName] if tmpl == nil { err = fmt.Errorf("'%v' not in htmlTemplates", tmplName) break } // Construct a (lazy) plain-text view for inclusion in
data["Pre"] = func() string { tmpl := textTemplates[tmplName] if tmpl == nil { panic(fmt.Errorf("'%v' not in textTemplates", tmplName)) } var buf bytes.Buffer if err := tmpl.Execute(&buf, data); err != nil { panic(err) } return buf.String() } if status != 0 { w.WriteHeader(status) } if r.Method != "HEAD" { err = tmpl.Execute(w, data) } default: // Fall back to plain text without template w.WriteHeader(http.StatusNotAcceptable) io.WriteString(w, "could not resolve an acceptable content-type\n") } if err != nil { panic(err) } } func (rl *rushlink) renderError(w http.ResponseWriter, r *http.Request, status int, msg string) { rl.render(w, r, status, "error", map[string]interface{}{"Message": msg}) } func (rl *rushlink) renderInternalServerError(w http.ResponseWriter, r *http.Request, err interface{}) { msg := fmt.Sprintf("internal server error: %v", err) rl.renderError(w, r, http.StatusInternalServerError, msg) } // resolveRootURL constructs the `scheme://host` part of rushlinks public API. // // If the `--root_url` flag is set, it will return that URL. // 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). func (rl *rushlink) resolveRootURL(r *http.Request) string { rlHost := rl.RootURL() if rlHost != nil { // Root URL overridden by command line arguments return rlHost.String() } // 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 } // 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")) 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]) } 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]) } } 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 }