diff options
Diffstat (limited to 'gopherproxy.go')
| -rw-r--r-- | gopherproxy.go | 261 |
1 files changed, 221 insertions, 40 deletions
diff --git a/gopherproxy.go b/gopherproxy.go index 87a1ad0..9a60507 100644 --- a/gopherproxy.go +++ b/gopherproxy.go | |||
| @@ -1,6 +1,7 @@ | |||
| 1 | package gopherproxy | 1 | package gopherproxy |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "bufio" | ||
| 4 | "bytes" | 5 | "bytes" |
| 5 | "crypto/md5" | 6 | "crypto/md5" |
| 6 | "fmt" | 7 | "fmt" |
| @@ -25,6 +26,11 @@ import ( | |||
| 25 | "github.com/NYTimes/gziphandler" | 26 | "github.com/NYTimes/gziphandler" |
| 26 | ) | 27 | ) |
| 27 | 28 | ||
| 29 | const ( | ||
| 30 | ITEM_TYPE_GEMINI_LINE = "" | ||
| 31 | ITEM_TYPE_GEMINI_LINK = " =>" | ||
| 32 | ) | ||
| 33 | |||
| 28 | type Item struct { | 34 | type Item struct { |
| 29 | Link template.URL | 35 | Link template.URL |
| 30 | Type string | 36 | Type string |
| @@ -40,7 +46,7 @@ type AssetList struct { | |||
| 40 | PropFontW2 string | 46 | PropFontW2 string |
| 41 | } | 47 | } |
| 42 | 48 | ||
| 43 | func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d gopher.Directory) error { | 49 | func renderGopherDirectory(w http.ResponseWriter, tpl *template.Template, assetList AssetList, uri string, hostport string, d gopher.Directory) error { |
| 44 | var title string | 50 | var title string |
| 45 | 51 | ||
| 46 | out := make([]Item, len(d.Items)) | 52 | out := make([]Item, len(d.Items)) |
| @@ -76,7 +82,7 @@ func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList As | |||
| 76 | path = strings.Replace(path, "%2F", "/", -1) | 82 | path = strings.Replace(path, "%2F", "/", -1) |
| 77 | tr.Link = template.URL( | 83 | tr.Link = template.URL( |
| 78 | fmt.Sprintf( | 84 | fmt.Sprintf( |
| 79 | "/%s/%s%s", | 85 | "/gopher/%s/%s%s", |
| 80 | hostport, | 86 | hostport, |
| 81 | string(byte(x.Type)), | 87 | string(byte(x.Type)), |
| 82 | path, | 88 | path, |
| @@ -92,13 +98,81 @@ func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList As | |||
| 92 | } | 98 | } |
| 93 | 99 | ||
| 94 | return tpl.Execute(w, struct { | 100 | return tpl.Execute(w, struct { |
| 95 | Title string | 101 | Title string |
| 96 | URI string | 102 | URI string |
| 97 | Assets AssetList | 103 | Assets AssetList |
| 98 | Lines []Item | 104 | Lines []Item |
| 99 | RawText string | 105 | RawText string |
| 100 | Error bool | 106 | Error bool |
| 101 | }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, out, "", false}) | 107 | Protocol string |
| 108 | }{title, fmt.Sprintf("%s/%s", hostport, uri), assetList, out, "", false, "gopher"}) | ||
| 109 | } | ||
| 110 | |||
| 111 | func parseGeminiDocument(response *GeminiResponse, uri string, hostport string) (items []Item) { | ||
| 112 | scanner := bufio.NewScanner(response.Body) | ||
| 113 | scanner.Split(bufio.ScanLines) | ||
| 114 | |||
| 115 | baseUrl, err := url.Parse(fmt.Sprintf( | ||
| 116 | "gemini://%s/%s", | ||
| 117 | hostport, | ||
| 118 | uri, | ||
| 119 | )) | ||
| 120 | if err != nil { | ||
| 121 | return []Item{} | ||
| 122 | } | ||
| 123 | |||
| 124 | for scanner.Scan() { | ||
| 125 | line := strings.Trim(scanner.Text(), "\r\n") | ||
| 126 | |||
| 127 | item := Item{ | ||
| 128 | Type: ITEM_TYPE_GEMINI_LINE, | ||
| 129 | Text: line, | ||
| 130 | } | ||
| 131 | |||
| 132 | linkMatch := GeminiLinkPattern.FindStringSubmatch(line) | ||
| 133 | if len(linkMatch) != 0 && linkMatch[0] != "" { | ||
| 134 | link := linkMatch[1] | ||
| 135 | |||
| 136 | if strings.HasPrefix(link, "//") { | ||
| 137 | link = "/gemini/" + strings.TrimPrefix(link, "//") | ||
| 138 | } else if strings.HasPrefix(link, "gemini://") { | ||
| 139 | link = "/gemini/" + strings.TrimPrefix(link, "gemini://") | ||
| 140 | } else if strings.HasPrefix(link, "gopher://") { | ||
| 141 | link = "/gopher/" + strings.TrimPrefix(link, "gopher://") | ||
| 142 | } else { | ||
| 143 | linkUrl, err := url.Parse(link) | ||
| 144 | if err != nil { | ||
| 145 | continue | ||
| 146 | } | ||
| 147 | adjustedUrl := baseUrl.ResolveReference(linkUrl) | ||
| 148 | if adjustedUrl.Scheme == "gemini" { | ||
| 149 | link = "/gemini/" + adjustedUrl.Host + adjustedUrl.Path | ||
| 150 | } else if adjustedUrl.Scheme == "gopher" { | ||
| 151 | link = "/gopher/" + adjustedUrl.Host + adjustedUrl.Path | ||
| 152 | } else { | ||
| 153 | link = adjustedUrl.String() | ||
| 154 | } | ||
| 155 | } | ||
| 156 | |||
| 157 | item.Type = ITEM_TYPE_GEMINI_LINK | ||
| 158 | item.Link = template.URL(link) | ||
| 159 | if linkMatch[2] != "" { | ||
| 160 | item.Text = linkMatch[2] | ||
| 161 | } else { | ||
| 162 | item.Text = linkMatch[1] | ||
| 163 | } | ||
| 164 | } | ||
| 165 | |||
| 166 | items = append(items, item) | ||
| 167 | } | ||
| 168 | |||
| 169 | return | ||
| 170 | } | ||
| 171 | |||
| 172 | func DefaultHandler(tpl *template.Template, uri string) http.HandlerFunc { | ||
| 173 | return func(w http.ResponseWriter, req *http.Request) { | ||
| 174 | http.Redirect(w, req, "/"+uri, http.StatusFound) | ||
| 175 | } | ||
| 102 | } | 176 | } |
| 103 | 177 | ||
| 104 | // GopherHandler returns a Handler that proxies requests | 178 | // GopherHandler returns a Handler that proxies requests |
| @@ -109,7 +183,7 @@ func renderDirectory(w http.ResponseWriter, tpl *template.Template, assetList As | |||
| 109 | func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool, uri string) http.HandlerFunc { | 183 | func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool, uri string) http.HandlerFunc { |
| 110 | return func(w http.ResponseWriter, req *http.Request) { | 184 | return func(w http.ResponseWriter, req *http.Request) { |
| 111 | agent := req.UserAgent() | 185 | agent := req.UserAgent() |
| 112 | path := strings.TrimPrefix(req.URL.Path, "/") | 186 | path := strings.TrimPrefix(req.URL.Path, "/gopher/") |
| 113 | 187 | ||
| 114 | if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) { | 188 | if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) { |
| 115 | log.Printf("UserAgent %s ignored robots.txt", agent) | 189 | log.Printf("UserAgent %s ignored robots.txt", agent) |
| @@ -132,13 +206,14 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
| 132 | uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) | 206 | uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) |
| 133 | if err != nil { | 207 | if err != nil { |
| 134 | tpl.Execute(w, struct { | 208 | tpl.Execute(w, struct { |
| 135 | Title string | 209 | Title string |
| 136 | URI string | 210 | URI string |
| 137 | Assets AssetList | 211 | Assets AssetList |
| 138 | RawText string | 212 | RawText string |
| 139 | Lines []Item | 213 | Lines []Item |
| 140 | Error bool | 214 | Error bool |
| 141 | }{"", hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true}) | 215 | Protocol string |
| 216 | }{"", hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}) | ||
| 142 | return | 217 | return |
| 143 | } | 218 | } |
| 144 | 219 | ||
| @@ -153,13 +228,14 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
| 153 | 228 | ||
| 154 | if err != nil { | 229 | if err != nil { |
| 155 | tpl.Execute(w, struct { | 230 | tpl.Execute(w, struct { |
| 156 | Title string | 231 | Title string |
| 157 | URI string | 232 | URI string |
| 158 | Assets AssetList | 233 | Assets AssetList |
| 159 | RawText string | 234 | RawText string |
| 160 | Lines []Item | 235 | Lines []Item |
| 161 | Error bool | 236 | Error bool |
| 162 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true}) | 237 | Protocol string |
| 238 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}) | ||
| 163 | return | 239 | return |
| 164 | } | 240 | } |
| 165 | 241 | ||
| @@ -171,13 +247,14 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
| 171 | buf := new(bytes.Buffer) | 247 | buf := new(bytes.Buffer) |
| 172 | buf.ReadFrom(res.Body) | 248 | buf.ReadFrom(res.Body) |
| 173 | tpl.Execute(w, struct { | 249 | tpl.Execute(w, struct { |
| 174 | Title string | 250 | Title string |
| 175 | URI string | 251 | URI string |
| 176 | Assets AssetList | 252 | Assets AssetList |
| 177 | RawText string | 253 | RawText string |
| 178 | Lines []Item | 254 | Lines []Item |
| 179 | Error bool | 255 | Error bool |
| 180 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, buf.String(), nil, false}) | 256 | Protocol string |
| 257 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, buf.String(), nil, false, "gopher"}) | ||
| 181 | } else if strings.HasPrefix(parts[1], "T") { | 258 | } else if strings.HasPrefix(parts[1], "T") { |
| 182 | _, _, err = vips.NewTransform(). | 259 | _, _, err = vips.NewTransform(). |
| 183 | Load(res.Body). | 260 | Load(res.Body). |
| @@ -190,21 +267,123 @@ func GopherHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, ass | |||
| 190 | io.Copy(w, res.Body) | 267 | io.Copy(w, res.Body) |
| 191 | } | 268 | } |
| 192 | } else { | 269 | } else { |
| 193 | if err := renderDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil { | 270 | if err := renderGopherDirectory(w, tpl, assetList, uri, hostport, res.Dir); err != nil { |
| 194 | tpl.Execute(w, struct { | 271 | tpl.Execute(w, struct { |
| 195 | Title string | 272 | Title string |
| 196 | URI string | 273 | URI string |
| 197 | Assets AssetList | 274 | Assets AssetList |
| 198 | RawText string | 275 | RawText string |
| 199 | Lines []Item | 276 | Lines []Item |
| 200 | Error bool | 277 | Error bool |
| 201 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true}) | 278 | Protocol string |
| 279 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gopher"}) | ||
| 202 | return | 280 | return |
| 203 | } | 281 | } |
| 204 | } | 282 | } |
| 205 | } | 283 | } |
| 206 | } | 284 | } |
| 207 | 285 | ||
| 286 | func GeminiHandler(tpl *template.Template, robotsdata *robotstxt.RobotsData, assetList AssetList, robotsdebug bool, uri string) http.HandlerFunc { | ||
| 287 | return func(w http.ResponseWriter, req *http.Request) { | ||
| 288 | agent := req.UserAgent() | ||
| 289 | path := strings.TrimPrefix(req.URL.Path, "/gemini/") | ||
| 290 | |||
| 291 | if robotsdata != nil && robotsdebug && !robotsdata.TestAgent(path, agent) { | ||
| 292 | log.Printf("UserAgent %s ignored robots.txt", agent) | ||
| 293 | } | ||
| 294 | |||
| 295 | parts := strings.Split(path, "/") | ||
| 296 | hostport := parts[0] | ||
| 297 | |||
| 298 | if len(hostport) == 0 { | ||
| 299 | http.Redirect(w, req, "/"+uri, http.StatusFound) | ||
| 300 | return | ||
| 301 | } | ||
| 302 | |||
| 303 | var qs string | ||
| 304 | |||
| 305 | if req.URL.RawQuery != "" { | ||
| 306 | qs = fmt.Sprintf("?%s", url.QueryEscape(req.URL.RawQuery)) | ||
| 307 | } | ||
| 308 | |||
| 309 | uri, err := url.QueryUnescape(strings.Join(parts[1:], "/")) | ||
| 310 | if err != nil { | ||
| 311 | tpl.Execute(w, struct { | ||
| 312 | Title string | ||
| 313 | URI string | ||
| 314 | Assets AssetList | ||
| 315 | RawText string | ||
| 316 | Lines []Item | ||
| 317 | Error bool | ||
| 318 | Protocol string | ||
| 319 | }{"", hostport, assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"}) | ||
| 320 | return | ||
| 321 | } | ||
| 322 | |||
| 323 | res, err := GeminiGet( | ||
| 324 | fmt.Sprintf( | ||
| 325 | "gemini://%s/%s%s", | ||
| 326 | hostport, | ||
| 327 | uri, | ||
| 328 | qs, | ||
| 329 | ), | ||
| 330 | ) | ||
| 331 | |||
| 332 | if err != nil { | ||
| 333 | tpl.Execute(w, struct { | ||
| 334 | Title string | ||
| 335 | URI string | ||
| 336 | Assets AssetList | ||
| 337 | RawText string | ||
| 338 | Lines []Item | ||
| 339 | Error bool | ||
| 340 | Protocol string | ||
| 341 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error: %s", err), nil, true, "gemini"}) | ||
| 342 | return | ||
| 343 | } | ||
| 344 | |||
| 345 | if int(res.Header.Status/10) != 2 { | ||
| 346 | tpl.Execute(w, struct { | ||
| 347 | Title string | ||
| 348 | URI string | ||
| 349 | Assets AssetList | ||
| 350 | RawText string | ||
| 351 | Lines []Item | ||
| 352 | Error bool | ||
| 353 | Protocol string | ||
| 354 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, fmt.Sprintf("Error %d: %s", res.Header.Status, res.Header.Meta), nil, true, "gemini"}) | ||
| 355 | return | ||
| 356 | } | ||
| 357 | |||
| 358 | if strings.HasPrefix(res.Header.Meta, MIME_GEMINI) { | ||
| 359 | items := parseGeminiDocument(res, uri, hostport) | ||
| 360 | tpl.Execute(w, struct { | ||
| 361 | Title string | ||
| 362 | URI string | ||
| 363 | Assets AssetList | ||
| 364 | RawText string | ||
| 365 | Lines []Item | ||
| 366 | Error bool | ||
| 367 | Protocol string | ||
| 368 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, "", items, false, "gemini"}) | ||
| 369 | } else if strings.HasPrefix(res.Header.Meta, "text/") { | ||
| 370 | buf := new(bytes.Buffer) | ||
| 371 | buf.ReadFrom(res.Body) | ||
| 372 | tpl.Execute(w, struct { | ||
| 373 | Title string | ||
| 374 | URI string | ||
| 375 | Assets AssetList | ||
| 376 | RawText string | ||
| 377 | Lines []Item | ||
| 378 | Error bool | ||
| 379 | Protocol string | ||
| 380 | }{uri, fmt.Sprintf("%s/%s", hostport, uri), assetList, buf.String(), nil, false, "gemini"}) | ||
| 381 | } else { | ||
| 382 | io.Copy(w, res.Body) | ||
| 383 | } | ||
| 384 | } | ||
| 385 | } | ||
| 386 | |||
| 208 | // RobotsTxtHandler returns the contents of the robots.txt file | 387 | // RobotsTxtHandler returns the contents of the robots.txt file |
| 209 | // if configured and valid. | 388 | // if configured and valid. |
| 210 | func RobotsTxtHandler(robotstxtdata []byte) http.HandlerFunc { | 389 | func RobotsTxtHandler(robotstxtdata []byte) http.HandlerFunc { |
| @@ -386,7 +565,9 @@ func ListenAndServe(bind, robotsfile string, robotsdebug bool, vipsconcurrency i | |||
| 386 | ConcurrencyLevel: vipsconcurrency, | 565 | ConcurrencyLevel: vipsconcurrency, |
| 387 | }) | 566 | }) |
| 388 | 567 | ||
| 389 | http.Handle("/", gziphandler.GzipHandler(GopherHandler(tpl, robotsdata, AssetList{styleAsset, jsAsset, fontwAsset, fontw2Asset, propfontwAsset, propfontw2Asset}, robotsdebug, uri))) | 568 | http.Handle("/", gziphandler.GzipHandler(DefaultHandler(tpl, uri))) |
| 569 | http.Handle("/gopher/", gziphandler.GzipHandler(GopherHandler(tpl, robotsdata, AssetList{styleAsset, jsAsset, fontwAsset, fontw2Asset, propfontwAsset, propfontw2Asset}, robotsdebug, uri))) | ||
| 570 | http.Handle("/gemini/", gziphandler.GzipHandler(GeminiHandler(tpl, robotsdata, AssetList{styleAsset, jsAsset, fontwAsset, fontw2Asset, propfontwAsset, propfontw2Asset}, robotsdebug, uri))) | ||
| 390 | http.Handle("/robots.txt", gziphandler.GzipHandler(RobotsTxtHandler(robotstxtdata))) | 571 | http.Handle("/robots.txt", gziphandler.GzipHandler(RobotsTxtHandler(robotstxtdata))) |
| 391 | http.Handle("/favicon.ico", gziphandler.GzipHandler(FaviconHandler(favicondata))) | 572 | http.Handle("/favicon.ico", gziphandler.GzipHandler(FaviconHandler(favicondata))) |
| 392 | http.Handle(styleAsset, gziphandler.GzipHandler(StyleHandler(styledata))) | 573 | http.Handle(styleAsset, gziphandler.GzipHandler(StyleHandler(styledata))) |
