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" "github.com/pkg/errors" ) // 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, 0) var htmlTemplates = make(map[string]*html.Template, 0) // 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 parseFail(tmplName string, err error) { panic(errors.Wrapf(err, "parsing of %v failed", tmplName)) } func render(w http.ResponseWriter, r *http.Request, 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) } // Add the request to the template data 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 } 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()
		}
		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 renderError(w http.ResponseWriter, r *http.Request, status int, msg string) {
	w.WriteHeader(status)
	render(w, r, "error", map[string]interface{}{"Message": msg})
}

func renderInternalServerError(w http.ResponseWriter, r *http.Request, err interface{}) {
	msg := fmt.Sprintf("internal server error: %v", err)
	renderError(w, r, http.StatusInternalServerError, msg)
}

// 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 := 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
}