shrt.go in go-shrt

at master

1// See LICENSE file for copyright and license details
2
3// Package shrt implements a simple (perhaps simplistic) URL
4// shortener. It also handles go-get requests.
5//
6// Shortlinks are recorded in the database, and any request path not
7// matching a shortlink is assumed to be a go-get request. This is by
8// design, but can result in specious redirects. Additionally,
9// subdirectory paths are not allowed.
10//
11// Shortlinks generate an HTTP 301 response. Go-get requests generate
12// an HTTP 200 response. If configured, requests to the base path
13// (i.e., "/") generate an HTTP 302 response.
14//
15// The database file is human-readable. See [Shrtfile] for the full
16// specification.
17package shrt
18
19import (
20 "fmt"
21 "html/template"
22 "io/fs"
23 "log"
24 "net/http"
25 "strings"
26)
27
28var robotstxt = `# Welcome to Shrt
29User-Agent: *
30Disallow:
31`
32var shrtrsp = `<!DOCTYPE html>
33<html>
34<head>
35<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>"
36<meta name="go-import" content="{{ .SrvName }}/{{ .Repo }} {{ .ScmType }} {{ .URL }}">{{ if ne .GoSourceDir "" }}
37<meta name="go-source" content="{{ .SrvName }}/{{ .Repo }} {{ .URL }} {{ .URL }}/{{ .GoSourceDir }} {{ .URL }}/{{ .GoSourceFile }}">{{ end }}
38<meta http-equiv="refresh" content="0; url=https://godoc.org/{{ .SrvName }}/{{ .DocPath }}">
39</head>
40<body>
41Redirecting to docs at <a href="https://godoc.org/{{ .SrvName }}/{{ .DocPath }}">godoc.org/{{ .SrvName }}/{{ .DocPath }}</a>...
42</body>
43</html>
44`
45
46type shrtRequest struct {
47 SrvName string
48 Repo string
49 ScmType string
50 URL string
51 DocPath string
52 GoSourceDir string
53 GoSourceFile string
54}
55
56// Config contains all of the global configuration for Shrt. All
57// values except BareRdr and DbPath are used in the go-import meta tag
58// values for go-get requests.
59type Config struct {
60 // Server name of the Shrt host
61 SrvName string
62 // SCM (or VCS) type
63 ScmType string
64 // SCM repository suffix, if required by repository host
65 Suffix string
66 // The server name of the repository host
67 RdrName string
68 // Where requests with an empty path should redirect
69 BareRdr string
70 // The path to the [ShrtFile]-formatted database file.
71 DbPath string
72 // The string to append to the URL for go-get redirects to
73 // form the directory entry in the go-source meta tag. This
74 // key is experimental and may be removed in a future release.
75 GoSourceDir string
76 // The string to append to the URL for go-get redirects to
77 // form the file entry in the go-source meta tag. This
78 // key is experimental and may be removed in a future release.
79 GoSourceFile string
80}
81
82// ShrtHandler is the core [http.Handler] for go-shrt.
83type ShrtHandler struct {
84 ShrtFile *ShrtFile
85 Config Config
86 FS fs.FS
87}
88
89// Handle implements the http.Handler interface.
90func (s *ShrtHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
91 if req.Method != http.MethodGet {
92 w.WriteHeader(http.StatusMethodNotAllowed)
93 fmt.Fprintf(w, "Method not allowed")
94 return
95 }
96
97 p := req.URL.Path
98 p = strings.TrimPrefix(p, "/")
99
100 if p == "robots.txt" {
101 log.Println("incoming robot")
102 fmt.Fprint(w, robotstxt)
103 return
104 }
105
106 if p == "" && s.Config.BareRdr != "" {
107 log.Println("shortlink request for /")
108 w.Header().Add("Location", s.Config.BareRdr)
109 w.WriteHeader(http.StatusFound)
110 fmt.Fprintln(w, "Redirecting")
111 return
112 }
113
114 key := strings.SplitN(p, "/", 2)[0]
115
116 val, err := s.ShrtFile.Get(key)
117 if err != nil {
118 log.Println("not found:", key)
119 http.Error(w, "Not found", http.StatusNotFound)
120 return
121 }
122
123 switch val.Type {
124 case ShortLink:
125 if key != p {
126 log.Println("path elements following shortlink:", p)
127 http.Error(w, "Not found", http.StatusNotFound)
128 return
129 }
130 log.Println("shortlink request for", key)
131 w.Header().Add("Location", val.URL)
132 w.WriteHeader(http.StatusMovedPermanently)
133 fmt.Fprintln(w, "Redirecting")
134 case GoGet:
135 log.Println("go-get request for", key)
136 t := template.Must(template.New("shrt").Parse(shrtrsp))
137 sReq := shrtRequest{
138 SrvName: s.Config.SrvName,
139 Repo: key,
140 ScmType: s.Config.ScmType,
141 URL: val.URL,
142 DocPath: p,
143 GoSourceDir: s.Config.GoSourceDir,
144 GoSourceFile: s.Config.GoSourceFile,
145 }
146 if err := t.Execute(w, sReq); err != nil {
147 log.Println("error executing template:", err)
148 }
149 }
150}