M README.md => README.md +4 -3
@@ 1,9 1,10 @@
# kindleto
-`kindleto` is a [Gemini](https://gemini.circumlunar.space/) and
+`kindleto` is a [Gemini](https://gemini.circumlunar.space/),
+[Gopher](https://en.wikipedia.org/wiki/Gopher_(protocol)) and
[Finger](https://en.wikipedia.org/wiki/Finger_%28protocol%29) protocol proxy
-for older Amazon Kindles. It allows accessing Gemini and Finger through the
-Kindle's web browser. It is based on the [Gneto
+for older Amazon Kindles. It allows accessing Gemini, Gopher and Finger through
+the Kindle's web browser. It is based on the [Gneto
proxy](https://github.com/pgorman/gneto).
## Supported devices
M gemini/gemtext.go => gemini/gemtext.go +1 -1
@@ 99,7 99,7 @@ func gemtextToHTML(w http.ResponseWriter, u *url.URL, rd *bufio.Reader, td templ
linkURL, _ := util.AbsoluteURL(u, linkURLString)
linkURLString = linkURL.String()
io.WriteString(w, `<a href="`)
- if linkURL.Scheme == "gemini" || linkURL.Scheme == "finger" {
+ if linkURL.Scheme == "gemini" || linkURL.Scheme == "gopher" || linkURL.Scheme == "finger" {
io.WriteString(w, `/?url=`+gemini.QueryEscape(linkURLString))
} else {
io.WriteString(w, linkURLString)
M go.mod => go.mod +4 -1
@@ 2,4 2,7 @@ module git.sr.ht/~sotirisp/kindleto
go 1.15
-require git.sr.ht/~adnano/go-gemini v0.2.2
+require (
+ git.mills.io/prologic/go-gopher v0.0.0-20220131134120-44dd1c17a0dd
+ git.sr.ht/~adnano/go-gemini v0.2.2
+)
M go.sum => go.sum +15 -0
@@ 1,5 1,16 @@
+git.mills.io/prologic/go-gopher v0.0.0-20220131134120-44dd1c17a0dd h1:XTWDxTxuxh7Wq5GrvpWxxEU16iPIa+xyYaVLEqKK9N0=
+git.mills.io/prologic/go-gopher v0.0.0-20220131134120-44dd1c17a0dd/go.mod h1:EMXlYOIbYJQhPTtIltgaaHtCYDawV/HL0dYf8ShzAck=
git.sr.ht/~adnano/go-gemini v0.2.2 h1:p2owKzrQ1wTgvPS5CZCPYArQyNUL8ZgYOHHrTjH9sdI=
git.sr.ht/~adnano/go-gemini v0.2.2/go.mod h1:hQ75Y0i5jSFL+FQ7AzWVAYr5LQsaFC7v3ZviNyj46dY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ 7,3 18,7 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
A gopher/gopher.go => gopher/gopher.go +115 -0
@@ 0,0 1,115 @@
+// SPDX-FileCopyrightText: 2022 Sotiris Papatheodorou
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package gopher
+
+import (
+ "bufio"
+ "fmt"
+ "git.mills.io/prologic/go-gopher"
+ "log"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "git.sr.ht/~sotirisp/kindleto/templates"
+ "git.sr.ht/~sotirisp/kindleto/util"
+)
+
+// ProxyGopher finds the Gopher content at u.
+func ProxyGopher(w http.ResponseWriter, r *http.Request, u *url.URL) error {
+ response, err := gopher.Get(u.String())
+ if err != nil {
+ return fmt.Errorf("ProxyGopher: gopher.Get error for %s: %v", u.String(), err)
+ }
+ if util.LogLevel > 1 {
+ log.Printf("ProxyGopher: %v response: %v", u, response.Type)
+ }
+
+ td := templates.TemplateData{
+ URL: u.String(),
+ ParentURL: util.ParentURL(u),
+ }
+
+ // Received a resource other than a Gopher directory.
+ if response.Body != nil {
+ rd := bufio.NewReader(response.Body)
+ switch response.Type {
+ case gopher.FILE, gopher.ERROR:
+ return util.TextToHTML(w, u, rd, td)
+ case gopher.BINHEX, gopher.DOSARCHIVE, gopher.UUENCODED,
+ gopher.BINARY, gopher.GIF, gopher.IMAGE, gopher.HTML,
+ gopher.AUDIO, gopher.PNG, gopher.DOC:
+ return util.ServeFile(w, r, u, rd)
+ }
+ }
+
+ // Received a Gopher directory.
+ if r.URL.Query().Get("source") == "" {
+ return gopherMenuToHTML(w, u, &response.Dir, td)
+ } else {
+ return gopherMenuSourceToHTML(w, u, &response.Dir, td)
+ }
+}
+
+// HandleGopher handles full text search requests.
+func HandleGopher(w http.ResponseWriter, r *http.Request) {
+ if util.LogLevel > 1 {
+ log.Printf("HandleGopher: method: %v url: %q query: %q",
+ r.Method, r.FormValue("url"), r.FormValue("query"))
+ }
+ switch r.Method {
+ case http.MethodGet:
+ if r.FormValue("url") == "" {
+ log.Printf(`HandleGopher: missing field "url"`)
+ http.Error(w, "Bad Request", 400)
+ return
+ }
+ u, err := url.Parse(r.FormValue("url"))
+ if err != nil {
+ log.Printf(`HandleGopher: failed to parse URL "%s": %v`,
+ r.FormValue("url"), err)
+ http.Error(w, "Internal Server Error", 500)
+ return
+ }
+ td := templates.TemplateData{
+ URL: u.String(),
+ ParentURL: util.ParentURL(u),
+ }
+ err = templates.Templates.ExecuteTemplate(w, "gopher-search.html.tmpl", td)
+ if err != nil {
+ log.Printf("HandleGopher: failed to execute input template: %v", err)
+ http.Error(w, "Internal Server Error", 500)
+ return
+ }
+ case http.MethodPost:
+ if r.FormValue("url") == "" {
+ log.Printf(`HandleGopher: missing field "url"`)
+ http.Error(w, "Bad Request", 400)
+ return
+ }
+ if r.FormValue("query") == "" {
+ log.Printf(`HandleGopher: missing field "query"`)
+ http.Error(w, "Bad Request", 400)
+ return
+ }
+ escaped_query := strings.ReplaceAll(url.PathEscape(r.FormValue("query")), "+", "%2B")
+ u, err := url.Parse(r.FormValue("url") + "?" + escaped_query)
+ if err != nil {
+ log.Printf(`HandleGopher: failed to parse URL "%s": %v`, r.FormValue("url"), err)
+ http.Error(w, "Internal Server Error", 500)
+ return
+ }
+ if util.LogLevel > 1 {
+ log.Printf(`HandleGopher: gopher search URL: "%s"`, u)
+ }
+ err = ProxyGopher(w, r, u)
+ if err != nil {
+ log.Printf("HandleGopher: gopher error: %v", err)
+ http.Error(w, "Internal Server Error", 500)
+ return
+ }
+ default:
+ http.Error(w, "Bad Request", 400)
+ }
+}
A => +114 -0
@@ 0,0 1,114 @@
// SPDX-FileCopyrightText: 2022 Sotiris Papatheodorou
// SPDX-License-Identifier: GPL-3.0-or-later
package gopher
import (
"fmt"
"git.mills.io/prologic/go-gopher"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"git.sr.ht/~sotirisp/kindleto/templates"
"git.sr.ht/~sotirisp/kindleto/util"
)
// gopherMenuSourceToHTML parses a Gopher directory from d, and writes the HTML
// equivalent of its original format to w. The source URL is stored in u.
// util.TextToHTML wasn't used because go-gopher doesn't present a reader to
// the raw directory data.
func gopherMenuSourceToHTML(w http.ResponseWriter, u *url.URL, d *gopher.Directory, td templates.TemplateData) error {
var err error
err = templates.Templates.ExecuteTemplate(w, "header-only.html.tmpl", td)
if err != nil {
log.Println("gopherMenuSourceToHTML:", err)
http.Error(w, "Internal Server Error", 500)
}
io.WriteString(w, `<pre class="text">`+"\n")
for _, i := range d.Items {
// MarshalText should only fail if there is a bug in
// go-gemini since the Item was generated by go-gemini.
t, _ := i.MarshalText()
io.WriteString(w, string(t))
}
io.WriteString(w, "</pre>\n")
err = templates.Templates.ExecuteTemplate(w, "footer-only.html.tmpl", td)
if err != nil {
log.Println("gopherMenuSourceToHTML:", err)
http.Error(w, "Internal Server Error", 500)
}
return err
}
// gopherMenuToHTML parses a Gopher directory from d, and writes its HTML
// equivalent to w. The source URL is stored in u.
func gopherMenuToHTML(w http.ResponseWriter, u *url.URL, d *gopher.Directory, td templates.TemplateData) error {
var err error
err = templates.Templates.ExecuteTemplate(w, "header-only.html.tmpl", td)
if err != nil {
log.Println("gopherMenuToHTML:", err)
http.Error(w, "Internal Server Error", 500)
}
io.WriteString(w, `<pre class="gopher-directory">`+"\n")
for _, i := range d.Items {
if util.LogLevel > 2 {
// MarshalText should only fail if there is a bug in
// go-gemini since the Item was generated by go-gemini.
t, _ := i.MarshalText()
fmt.Print(string(t))
}
i.Description = util.StripSGR(i.Description)
switch i.Type {
case gopher.ERROR, gopher.INFO:
io.WriteString(w, i.Description+"\n")
case gopher.FILE, gopher.DIRECTORY, gopher.BINHEX,
gopher.DOSARCHIVE, gopher.UUENCODED,
gopher.INDEXSEARCH, gopher.BINARY, gopher.REDUNDANT,
gopher.GIF, gopher.IMAGE, gopher.AUDIO, gopher.PNG,
gopher.DOC:
link := url.URL{
Scheme: "gopher",
Host: i.Host + ":" + strconv.Itoa(i.Port),
Path: string(i.Type) + i.Selector,
}
io.WriteString(w, `<a href="`)
if i.Type == gopher.INDEXSEARCH {
io.WriteString(w, `/gopher?url=`+url.QueryEscape(link.String()))
} else {
io.WriteString(w, `/?url=`+url.PathEscape(link.String()))
}
io.WriteString(w, `">`+i.Description+"</a> ")
io.WriteString(w, `<span class="scheme">`+
i.Type.String()+"</span>\n")
case gopher.HTML:
io.WriteString(w, `<a href="`+
strings.TrimPrefix(i.Selector, "URL:")+`">`+
i.Description+"</a> ")
io.WriteString(w, `<span class="scheme">`+
i.Type.String()+"</span>\n")
// TODO handle PHONEBOOK, REDUNDANT
default:
io.WriteString(w, i.Description)
io.WriteString(w, ` <span class="scheme">`+
i.Type.String()+" UNSUPPORTED ITEM TYPE</span>\n")
}
}
io.WriteString(w, "</pre>\n")
err = templates.Templates.ExecuteTemplate(w, "footer-only.html.tmpl", td)
if err != nil {
log.Println("gopherMenuToHTML:", err)
http.Error(w, "Internal Server Error", 500)
}
return err
}
M handlers.go => handlers.go +3 -0
@@ 18,6 18,7 @@ import (
"git.sr.ht/~sotirisp/kindleto/certificates"
"git.sr.ht/~sotirisp/kindleto/finger"
"git.sr.ht/~sotirisp/kindleto/gemini"
+ "git.sr.ht/~sotirisp/kindleto/gopher"
"git.sr.ht/~sotirisp/kindleto/templates"
"git.sr.ht/~sotirisp/kindleto/util"
)
@@ 243,6 244,8 @@ func proxy(w http.ResponseWriter, r *http.Request) {
}
break
}
+ } else if u.Scheme == "gopher" {
+ err = gopher.ProxyGopher(w, r, u)
} else if u.Scheme == "finger" {
err = finger.ProxyFinger(w, r, u)
} else {
M kindleto.go => kindleto.go +2 -0
@@ 16,6 16,7 @@ import (
"git.sr.ht/~sotirisp/kindleto/certificates"
"git.sr.ht/~sotirisp/kindleto/gemini"
+ "git.sr.ht/~sotirisp/kindleto/gopher"
"git.sr.ht/~sotirisp/kindleto/templates"
"git.sr.ht/~sotirisp/kindleto/tofu"
"git.sr.ht/~sotirisp/kindleto/util"
@@ 72,6 73,7 @@ func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", proxy)
+ mux.HandleFunc("/gopher", gopher.HandleGopher)
mux.HandleFunc("/certificate/", clientCertificateRequired)
mux.HandleFunc("/settings/certificates/", manageClientCertificates)
mux.HandleFunc("/kindleto.css", css)
M templates/template_strings.go => templates/template_strings.go +13 -0
@@ 100,6 100,19 @@ const GeminiSensitiveInput string = `{{template "header" .}}
{{end}}
{{template "footer"}}`
+const GopherSearch string = `{{template "header" .}}
+{{if .Error}}
+<div class="error">ERROR: {{.Error}}</div>
+{{end}}
+<h1>Gopher full-text search</h1>
+<p>Enter a search query to send to<br/>{{.URL}}</p>
+<form action="/gopher" method="POST">
+<input type="hidden" name="url" value="{{.URL}}">
+<input type="text" name="query">
+<input type="submit" value="Search">
+</form>
+{{template "footer"}}`
+
const HeaderOnly string = `{{template "header" .}}
{{if .Error}}<div class="error">ERROR: {{.Error}}</div>{{end}}
{{if .Warning}}<div class="warning">Warning: {{.Warning}}</div>{{end}}`
M templates/templates.go => templates/templates.go +1 -0
@@ 38,6 38,7 @@ func LoadTemplates(webRoot string) {
Templates = template.Must(Templates.New("gemini-certificate.html.tmpl").Parse(GeminiCertificate))
Templates = template.Must(Templates.New("gemini-input.html.tmpl").Parse(GeminiInput))
Templates = template.Must(Templates.New("gemini-sensitive-input.html.tmpl").Parse(GeminiSensitiveInput))
+ Templates = template.Must(Templates.New("gopher-search.html.tmpl").Parse(GopherSearch))
Templates = template.Must(Templates.New("header-only.html.tmpl").Parse(HeaderOnly))
Templates = template.Must(Templates.New("header.html.tmpl").Parse(Header))
Templates = template.Must(Templates.New("help.html.tmpl").Parse(Help))