URL shortener and file dump for hashru.link https://hashru.link
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

307 lines
8.6 KiB

  1. package rushlink
  2. //go:generate go-bindata -pkg $GOPACKAGE -prefix assets/ assets/...
  3. import (
  4. "bytes"
  5. "fmt"
  6. html "html/template"
  7. "io"
  8. "log"
  9. "net/http"
  10. "path/filepath"
  11. "regexp"
  12. "runtime/debug"
  13. "sort"
  14. "strconv"
  15. "strings"
  16. text "text/template"
  17. "time"
  18. "github.com/pkg/errors"
  19. )
  20. const defaultScheme = "http"
  21. // Plain text templates
  22. var textBaseTemplate *text.Template = text.Must(text.New("").Parse(string(MustAsset("templates/txt/base.txt.tmpl"))))
  23. var htmlBaseTemplate *html.Template = html.Must(html.New("").Parse(string(MustAsset("templates/html/base.html.tmpl"))))
  24. // Template collections
  25. var textTemplates = make(map[string]*text.Template, 0)
  26. var htmlTemplates = make(map[string]*html.Template, 0)
  27. // Used by resolveResponseContentType
  28. var acceptHeaderMediaRangeRegex = regexp.MustCompile(`^\s*([^()<>@,;:\\"/\[\]?.=]+)/([^()<>@,;:\\"/\[\]?.=]+)\s*$`)
  29. var acceptHeaderAcceptParamsRegex = regexp.MustCompile(`^\s*(\w+)=([A-Za-z0-9.-]+)\s*$`)
  30. var acceptHeaderWeight = regexp.MustCompile(`^\s*q=0(?:\.([0-9]{0,3}))|1(?:\.0{0,3})\s*$`)
  31. // HTML templates
  32. func init() {
  33. for _, tmplPath := range AssetNames() {
  34. if mustMatch("templates/txt/*.txt.tmpl", tmplPath) {
  35. base := text.Must(textBaseTemplate.Clone())
  36. tmpl, err := base.Parse(string(MustAsset(tmplPath)))
  37. if err != nil {
  38. panic(errors.Wrapf(err, "parsing %v", tmplPath))
  39. }
  40. tmplName := strings.TrimSuffix(filepath.Base(tmplPath), ".txt.tmpl")
  41. textTemplates[tmplName] = tmpl
  42. continue
  43. }
  44. if mustMatch("templates/html/*.html.tmpl", tmplPath) {
  45. base := html.Must(htmlBaseTemplate.Clone())
  46. tmpl, err := base.Parse(string(MustAsset(tmplPath)))
  47. if err != nil {
  48. panic(errors.Wrapf(err, "parsing %v", tmplPath))
  49. }
  50. tmplName := strings.TrimSuffix(filepath.Base(tmplPath), ".html.tmpl")
  51. htmlTemplates[tmplName] = tmpl
  52. continue
  53. }
  54. }
  55. // Sanity check. Both maps should not be empty
  56. if len(textTemplates) == 0 || len(htmlTemplates) == 0 {
  57. panic("template loading failed")
  58. }
  59. }
  60. func mustMatch(pattern, name string) bool {
  61. m, err := filepath.Match(pattern, name)
  62. if err != nil {
  63. panic("%v error in call to mustMatch")
  64. }
  65. return m
  66. }
  67. func parseFail(tmplName string, err error) {
  68. panic(errors.Wrapf(err, "parsing of %v failed", tmplName))
  69. }
  70. func mapExtend(m map[string]interface{}, key string, value interface{}) {
  71. if m[key] != nil {
  72. return
  73. }
  74. m[key] = value
  75. }
  76. func (rl *rushlink) renderStatic(w http.ResponseWriter, r *http.Request, path string) {
  77. var modTime time.Time
  78. if info, err := AssetInfo(path); err == nil {
  79. modTime = info.ModTime()
  80. }
  81. contents, err := Asset(path)
  82. if err != nil {
  83. rl.renderError(w, r, http.StatusNotFound, err.Error())
  84. return
  85. }
  86. http.ServeContent(w, r, path, modTime, bytes.NewReader(contents))
  87. }
  88. func (rl *rushlink) render(w http.ResponseWriter, r *http.Request, status int, tmplName string, data map[string]interface{}) {
  89. contentType, err := resolveResponseContentType(r, []string{"text/plain", "text/html"})
  90. if err != nil {
  91. w.WriteHeader(http.StatusNotAcceptable)
  92. fmt.Fprintf(w, "error parsing Accept header: %v\n", err)
  93. return
  94. }
  95. // Add the request to the template data
  96. mapExtend(data, "RootURL", rl.resolveRootURL(r))
  97. mapExtend(data, "Request", r)
  98. switch contentType {
  99. case "text/plain":
  100. w.Header().Set("Content-Type", "text/plain")
  101. tmpl := textTemplates[tmplName]
  102. if tmpl == nil {
  103. err = fmt.Errorf("'%v' not in textTemplates", tmplName)
  104. break
  105. }
  106. if status != 0 {
  107. w.WriteHeader(status)
  108. }
  109. if r.Method != "HEAD" {
  110. err = tmpl.Execute(w, data)
  111. }
  112. case "text/html":
  113. w.Header().Set("Content-Type", "text/html")
  114. tmpl := htmlTemplates[tmplName]
  115. if tmpl == nil {
  116. err = fmt.Errorf("'%v' not in htmlTemplates", tmplName)
  117. break
  118. }
  119. // Construct a (lazy) plain-text view for inclusion in <pre>
  120. data["Pre"] = func() string {
  121. tmpl := textTemplates[tmplName]
  122. if tmpl == nil {
  123. panic(fmt.Errorf("'%v' not in textTemplates", tmplName))
  124. }
  125. var buf bytes.Buffer
  126. if err := tmpl.Execute(&buf, data); err != nil {
  127. panic(err)
  128. }
  129. return buf.String()
  130. }
  131. if status != 0 {
  132. w.WriteHeader(status)
  133. }
  134. if r.Method != "HEAD" {
  135. err = tmpl.Execute(w, data)
  136. }
  137. default:
  138. // Fall back to plain text without template
  139. w.WriteHeader(http.StatusNotAcceptable)
  140. io.WriteString(w, "could not resolve an acceptable content-type\n")
  141. }
  142. if err != nil {
  143. panic(err)
  144. }
  145. }
  146. func (rl *rushlink) renderError(w http.ResponseWriter, r *http.Request, status int, msg string) {
  147. rl.render(w, r, status, "error", map[string]interface{}{"Message": msg})
  148. }
  149. func (rl *rushlink) renderInternalServerError(w http.ResponseWriter, r *http.Request, err interface{}) {
  150. msg := fmt.Sprintf("internal server error: %v", err)
  151. rl.renderError(w, r, http.StatusInternalServerError, msg)
  152. }
  153. // resolveRootURL constructs the `scheme://host` part of rushlinks public API.
  154. //
  155. // If the `--root_url` flag is set, it will return that URL.
  156. // Otherwise, this function will return 'https://{Host}', where `{Host}` is
  157. // the value provided by the client in the HTTP `Host` header. This value may
  158. // be invalid, but it is impossible to handle this error (because we *cannot*
  159. // know the real host).
  160. func (rl *rushlink) resolveRootURL(r *http.Request) string {
  161. rlHost := rl.RootURL()
  162. if rlHost != nil {
  163. // Root URL overridden by command line arguments
  164. return rlHost.String()
  165. }
  166. // Guess scheme
  167. scheme := defaultScheme
  168. forwardedScheme := r.Header.Get("X-Forwarded-Proto")
  169. switch forwardedScheme {
  170. case "http":
  171. scheme = "http"
  172. break
  173. case "https":
  174. scheme = "https"
  175. break
  176. }
  177. // Guess host
  178. host := r.Host
  179. if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" {
  180. host = forwardedHost
  181. }
  182. return scheme + "://" + host
  183. }
  184. // Try to resolve the preferred content-type for the response to this request.
  185. //
  186. // This is done by reading from the `types` argument. If one of them matches
  187. // the preferences supplied by the client in their Accept header, we will
  188. // return that one. We will take the clients preferences into account.
  189. //
  190. // Iff no match could be found, this function will return an empty string, and
  191. // the caller should probably respond with a 406 Not Acceptable status code.
  192. // Iff the Accept header was invalid, we will return an error. In this case,
  193. // the situation calls for a 400 Bad Request.
  194. func resolveResponseContentType(r *http.Request, types []string) (string, error) {
  195. // Ref: https://tools.ietf.org/html/rfc7231#section-5.3.2
  196. if len(types) == 0 {
  197. return "", nil
  198. }
  199. acceptHeader := strings.TrimSpace(r.Header.Get("Accept"))
  200. if acceptHeader == "" {
  201. return types[0], nil
  202. }
  203. type AcceptValue struct {
  204. Type string
  205. Subtype string
  206. Weight int
  207. }
  208. avStrings := strings.Split(acceptHeader, ",")
  209. avs := make([]AcceptValue, len(avStrings))
  210. for i, avString := range avStrings {
  211. av := AcceptValue{Weight: 1000}
  212. choiceParts := strings.Split(avString, ";")
  213. mediaRange := acceptHeaderMediaRangeRegex.FindStringSubmatch(choiceParts[0])
  214. if mediaRange == nil {
  215. return "", fmt.Errorf("bad media-range ('%v')", choiceParts[0])
  216. }
  217. av.Type = mediaRange[1]
  218. av.Subtype = mediaRange[2]
  219. // Go through the rest to see if there is a q=... parameter
  220. for choiceParts = choiceParts[1:]; len(choiceParts) > 0; choiceParts = choiceParts[1:] {
  221. // Try to parse the weight param
  222. weight := acceptHeaderWeight.FindStringSubmatch(choiceParts[0])
  223. if weight != nil {
  224. if weight[1] == "" {
  225. av.Weight = 0
  226. } else {
  227. var err error
  228. av.Weight, err = strconv.Atoi((weight[1] + "000")[:3])
  229. if err != nil {
  230. log.Println("error: unreachable statement")
  231. debug.PrintStack()
  232. av.Weight = 1000 // Reset to default value
  233. }
  234. }
  235. break
  236. }
  237. // Check if this parameter is still invalid in any case
  238. acceptParams := acceptHeaderAcceptParamsRegex.FindStringSubmatchIndex(choiceParts[0])
  239. if acceptParams == nil {
  240. return "", fmt.Errorf("bad accept-params ('%v')", choiceParts[0])
  241. }
  242. }
  243. avs[i] = av
  244. }
  245. sort.SliceStable(avs, func(i, j int) bool {
  246. if avs[i].Weight > avs[j].Weight {
  247. return true
  248. }
  249. if avs[i].Type != "*" && avs[j].Type == "*" {
  250. return true
  251. }
  252. if avs[i].Subtype != "*" && avs[j].Subtype == "*" {
  253. return true
  254. }
  255. return false
  256. })
  257. avArgs := make([]AcceptValue, len(types))
  258. for i, fulltype := range types {
  259. split := strings.Split(fulltype, "/")
  260. if len(split) == 1 {
  261. avArgs[i] = AcceptValue{Type: split[0]}
  262. } else {
  263. avArgs[i] = AcceptValue{Type: split[0], Subtype: split[1]}
  264. }
  265. }
  266. for _, av := range avs {
  267. for j, avArg := range avArgs {
  268. if !(av.Type == avArg.Type || av.Type == "*" || avArg.Type == "*") {
  269. continue
  270. }
  271. if !(av.Subtype == avArg.Subtype || av.Subtype == "*" || avArg.Subtype == "*") {
  272. continue
  273. }
  274. return types[j], nil
  275. }
  276. }
  277. return "", nil
  278. }