furl - furl.go
1 // Package Furl provides a drop-in http.Handler that provides short url
2 // redirects for longer URLs.
3 package furl
4
5 import (
6 "encoding/base64"
7 "encoding/json"
8 "encoding/xml"
9 "fmt"
10 "io"
11 "math/rand"
12 "net/http"
13 "path"
14 "strings"
15 "time"
16 )
17
18 const (
19 defaultKeyLength = 6
20 defaultRetries = 100
21 maxURLLength = 2048
22 maxKeyLength = 2048
23
24 unrecognisedContentType = "unrecognised content-type"
25 failedReadRequest = "failed to read request"
26 invalidURL = "invalid url"
27 failedKeyGeneration = "failed to generate key"
28 invalidKey = "invalid key"
29 keyExists = "key exists"
30
31 optionsPost = "OPTIONS, POST"
32 optionsGetHead = "OPTIONS, GET, HEAD"
33 )
34
35 var xmlStart = xml.StartElement{
36 Name: xml.Name{
37 Local: "furl",
38 },
39 }
40
41 func allValid(_ string) bool {
42 return true
43 }
44
45 // The Furl type represents a keystore of URLs to either generated or supplied
46 // keys.
47 type Furl struct {
48 urlValidator, keyValidator func(string) bool
49 keyLength, retries uint
50 rand *rand.Rand
51 index func(http.ResponseWriter, *http.Request, int, string)
52 store Store
53 }
54
55 // The New function creates a new instance of Furl, with the following defaults
56 // that can be changed by adding Option params.
57 //
58 // urlValidator: By default all strings are treated as valid URLs, this can be
59 // changed by using the URLValidator Option.
60 //
61 // keyValidator: By default all strings are treated as valid Keys, this can be
62 // changed by using the KeyValidator Option.
63 //
64 // keyLength: The default length of generated keys (before base64 encoding) is
65 // 6 and can be changed by using the KeyLength Option.
66 //
67 // retries: The default number of retries the key generator will before
68 // increasing the key length is 100 and can be changed by using the
69 // CollisionRetries Option.
70 //
71 // store: The default store is an empty map that will not permanently record
72 // the data. This can be changed by using the SetStore Option.
73 //
74 // index: By default, Furl offers no HTML output. This can be changed by using
75 // the Index Option.
76 func New(opts ...Option) *Furl {
77 f := &Furl{
78 urlValidator: allValid,
79 keyValidator: allValid,
80 keyLength: defaultKeyLength,
81 retries: defaultRetries,
82 }
83 for _, o := range opts {
84 o(f)
85 }
86 if f.store == nil {
87 f.store = NewStore()
88 }
89 if f.rand == nil {
90 f.rand = rand.New(rand.NewSource(time.Now().UnixMicro()))
91 }
92 return f
93 }
94
95 // The ServeHTTP method satifies the http.Handler interface and provides the
96 // following endpoints:
97 // GET /[key] - Will redirect the call to the associated URL if it exists, or
98 // will return 404 Not Found if it doesn't exists and 422
99 // Unprocessable Entity if the key is invalid.
100 // POST / - The root can be used to add urls to the store with a generated
101 // key. The URL must be specified in the POST body as per the
102 // specification below.
103 // POST /[key] - Will attempt to create the specified path with the URL
104 // provided as below. If the key is invalid, will respond with
105 // 422 Unprocessable Entity. This method cannot be used on
106 // existing keys.
107 //
108 // The URL for the POST methods can be provided in a few content types:
109 // application/json: {"key": "KEY HERE", "url": "URL HERE"}
110 // text/xml: <furl><key>KEY HERE</key><url>URL HERE</url></furl>
111 // application/x-www-form-urlencoded: key=KEY+HERE&url=URL+HERE
112 // text/plain: URL HERE
113 //
114 // For the json, xml, and form content types, the key can be ommitted if it has
115 // been supplied in the path or if the key is to be generated.
116 //
117 // The response type will be determined by the POST content type:
118 // application/json: {"key": "KEY HERE", "url": "URL HERE"}
119 // text/xml: <furl><key>KEY HERE</key><url>URL HERE</url></furl>
120 // text/plain: KEY HERE
121 //
122 // For application/x-www-form-urlencoded, the content type of the return will
123 // be text/html and the response will match that of text/plain.
124 func (f *Furl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
125 switch r.Method {
126 case http.MethodGet, http.MethodHead:
127 f.get(w, r)
128 case http.MethodPost:
129 f.post(w, r)
130 case http.MethodOptions:
131 f.options(w, r)
132 }
133 }
134
135 func (f *Furl) get(w http.ResponseWriter, r *http.Request) {
136 key := path.Base(r.URL.Path)
137 if !f.keyValidator(key) {
138 if f.index != nil {
139 f.index(w, r, http.StatusUnprocessableEntity, invalidKey)
140 } else {
141 http.Error(w, invalidKey, http.StatusUnprocessableEntity)
142 }
143 return
144 }
145 url, ok := f.store.Get(key)
146 if ok {
147 http.Redirect(w, r, url, http.StatusMovedPermanently)
148 } else {
149 if f.index != nil {
150 f.index(w, r, http.StatusNotFound, "404 page not found")
151 } else {
152 http.NotFound(w, r)
153 }
154 }
155 }
156
157 func (f *Furl) writeResponse(w http.ResponseWriter, r *http.Request, status int, contentType, output string) {
158 var format string
159 switch contentType {
160 case "text/json", "application/json":
161 format = "{\"error\":%q}"
162 case "text/xml", "application/xml":
163 format = "<furl><error>%s</error></furl>"
164 case "text/html":
165 if f.index != nil {
166 f.index(w, r, status, output)
167 return
168 }
169 fallthrough
170 default:
171 format = "%s"
172 }
173 w.WriteHeader(status)
174 fmt.Fprintf(w, format, output)
175 }
176
177 type keyURL struct {
178 Key string `json:"key" xml:"key"`
179 URL string `json:"url" xml:"url"`
180 }
181
182 func (f *Furl) post(w http.ResponseWriter, r *http.Request) {
183 var (
184 data keyURL
185 err error
186 )
187 contentType := r.Header.Get("Content-Type")
188 switch contentType {
189 case "text/json", "application/json":
190 err = json.NewDecoder(r.Body).Decode(&data)
191 case "text/xml", "application/xml":
192 err = xml.NewDecoder(r.Body).Decode(&data)
193 case "application/x-www-form-urlencoded":
194 err = r.ParseForm()
195 data.Key = r.PostForm.Get("key")
196 data.URL = r.PostForm.Get("url")
197 contentType = "text/html"
198 case "text/plain":
199 var sb strings.Builder
200 _, err = io.Copy(&sb, r.Body)
201 data.URL = sb.String()
202 default:
203 http.Error(w, unrecognisedContentType, http.StatusUnsupportedMediaType)
204 return
205 }
206 w.Header().Set("Content-Type", contentType)
207 if err != nil {
208 f.writeResponse(w, r, http.StatusBadRequest, contentType, failedReadRequest)
209 return
210 }
211 if len(data.URL) > maxURLLength || data.URL == "" || !f.urlValidator(data.URL) {
212 f.writeResponse(w, r, http.StatusBadRequest, contentType, invalidURL)
213 return
214 }
215 if data.Key == "" {
216 data.Key = path.Base("/" + r.URL.Path) // see if suggested key in path
217 }
218 var (
219 errCode int
220 errString string
221 )
222 if data.Key == "" || data.Key == "/" || data.Key == "." || data.Key == ".." { // generate key
223 f.store.Tx(func(tx Tx) {
224 for idLength := f.keyLength; ; idLength++ {
225 keyBytes := make([]byte, idLength)
226 for i := uint(0); i < f.retries; i++ {
227 f.rand.Read(keyBytes) // NB: will never error
228 data.Key = base64.RawURLEncoding.EncodeToString(keyBytes)
229 if ok := tx.Has(data.Key); !ok && f.keyValidator(data.Key) {
230 tx.Set(data.Key, data.URL)
231 return
232 }
233 }
234 if idLength == maxKeyLength {
235 errCode = http.StatusInternalServerError
236 errString = failedKeyGeneration
237 return
238 }
239 }
240 })
241 } else if len(data.Key) > maxKeyLength || !f.keyValidator(data.Key) {
242 f.writeResponse(w, r, http.StatusUnprocessableEntity, contentType, invalidKey)
243 return
244 } else { // use suggested key
245 f.store.Tx(func(tx Tx) {
246 if ok := tx.Has(data.Key); ok {
247 errCode = http.StatusMethodNotAllowed
248 errString = keyExists
249 } else {
250 tx.Set(data.Key, data.URL)
251 }
252 })
253 }
254 if errCode != 0 {
255 f.writeResponse(w, r, errCode, contentType, errString)
256 return
257 }
258 switch contentType {
259 case "text/json", "application/json":
260 json.NewEncoder(w).Encode(data)
261 case "text/xml", "application/xml":
262 xml.NewEncoder(w).EncodeElement(data, xmlStart)
263 case "text/html":
264 if f.index != nil {
265 f.index(w, r, http.StatusOK, data.Key)
266 return
267 }
268 fallthrough
269 case "text/plain":
270 io.WriteString(w, data.Key)
271 }
272 }
273
274 func (f *Furl) options(w http.ResponseWriter, r *http.Request) {
275 key := path.Base(r.URL.Path)
276 if key == "" || key == "/" {
277 w.Header().Add("Allow", optionsPost)
278 } else if !f.keyValidator(key) {
279 http.Error(w, invalidKey, http.StatusUnprocessableEntity)
280 return
281 } else {
282 _, ok := f.store.Get(key)
283 if ok {
284 w.Header().Add("Allow", optionsGetHead)
285 } else {
286 w.Header().Add("Allow", optionsPost)
287 }
288 }
289 w.WriteHeader(http.StatusNoContent)
290 }
291