package main import ( "bufio" "bytes" "crypto/x509" "errors" "fmt" "git.sr.ht/~adnano/go-gemini" "git.sr.ht/~adnano/go-gemini/tofu" "github.com/golang/freetype/truetype" "github.com/shermp/go-fbink-v2/gofbink" "github.com/shermp/go-kobo-input/koboin" "golang.org/x/image/font" "io/ioutil" "log" "net/url" "os" "time" ) var ( hosts tofu.KnownHosts hostsfile *tofu.HostWriter scanner *bufio.Scanner uiFace font.Face fbinkOpts gofbink.FBInkConfig fb *gofbink.FBInk f *truetype.Font t *koboin.TouchDevice ) // Visual options const ( width int = 1080 height int = 1440 // TODO get from device instead of hardcoding linewidth float64 = 5 oskpadding float64 = 20 keyspacing float64 = 5 // spacing between keys titleBarHeight int = height / 6 ) func trustCertificate(hostname string, cert *x509.Certificate) error { host := tofu.NewHost(hostname, cert.Raw, cert.NotAfter) knownHost, ok := hosts.Lookup(hostname) if ok && time.Now().Before(knownHost.Expires) { // Check fingerprint if bytes.Equal(knownHost.Fingerprint, host.Fingerprint) { return nil } return errors.New("error: fingerprint does not match!") } else { // Expired cert or new host: TOFU hosts.Add(host) hostsfile.WriteHost(host) return nil } return errors.New("error: fingerprint does not match!") } func do(req *gemini.Request, via []*gemini.Request) (*gemini.Response, error) { client := gemini.Client{ TrustCertificate: trustCertificate, } resp, err := client.Do(req) if err != nil { return resp, err } switch resp.Status.Class() { case gemini.StatusClassInput: // TODO sensitive input input, err := GetInput(&uiFace, resp.Meta+":") if err != nil { break } req.URL.ForceQuery = true req.URL.RawQuery = gemini.QueryEscape(input) return do(req, via) case gemini.StatusClassRedirect: via = append(via, req) if len(via) > 5 { return resp, errors.New("too many redirects") } target, err := url.Parse(resp.Meta) if err != nil { return resp, err } target = req.URL.ResolveReference(target) redirect := *req redirect.URL = target return do(&redirect, via) } return resp, err } func getPage(u *url.URL) (string, error) { req, err := gemini.NewRequest(u.String()) if err != nil { return "", nil } resp, err := do(req, nil) if err != nil { return "", nil } defer resp.Body.Close() // Handle response if resp.Status.Class() == gemini.StatusClassSuccess { body, err := ioutil.ReadAll(resp.Body) if err != nil { return "", err } return string(body), nil } else { return "", errors.New(fmt.Sprintf("%s %s", string(resp.Status), string(resp.Meta))) } } func main() { // Framebuffer setup fbinkOpts = gofbink.FBInkConfig{} rOpts := gofbink.RestrictedConfig{} fb = gofbink.New(&fbinkOpts, &rOpts) fb.Open() defer fb.Close() fb.Init(&fbinkOpts) fb.ClearScreen(&fbinkOpts) fb.Refresh(0, 0, 0, 0, gofbink.DitherPassthrough, &fbinkOpts) // Touchscreen setup touchPath := "/dev/input/event1" t = koboin.New(touchPath, width, height) defer t.Close() // Logging setup var logFile, err = os.Create("/mnt/onboard/gemini.log") if err != nil { drawErrorBox(err, &uiFace) } var logger *log.Logger = log.New(logFile, "gemini", log.LstdFlags) defer logFile.Close() path := "/mnt/onboard/.adds/gemini/known-hosts" // TODO don't hardcode // Reload or create known hosts file if _, err := os.Stat(path); os.IsNotExist(err) { os.Create(path) } err = hosts.Load(path) if err != nil { drawErrorBox(err, &uiFace) logger.Fatal(err) } hostsfile, err = tofu.NewHostsFile(path) if err != nil { fb.Println(err) logger.Fatal(err) } // Load Fonts, Create font face b, err := ioutil.ReadFile("/mnt/onboard/.adds/gemini/NotoSerif.ttf") if err != nil { logger.Println(err) return } f, err := truetype.Parse(b) if err != nil { logger.Println(err) return } uiFace = truetype.NewFace(f, &truetype.Options{Size: 32}) titleBarButtons, err := DrawTitleBar(&uiFace) if err != nil { drawErrorBox(err, &uiFace) logger.Fatal(err) } for { x, y, _ := t.GetInput() for _, b := range titleBarButtons { if touchingButton(x, y, b) { if b.Label == "Exit" { return } else if b.Label == "Go" { str, err := GetInput(&uiFace, "Enter URL:") if err != nil { drawErrorBox(err, &uiFace) logger.Println(err) } u, err := url.Parse(str) if err != nil { drawErrorBox(err, &uiFace) logger.Println(err) } if u.Scheme == "" { u.Scheme = "gemini" } content, err := getPage(u) if err != nil { drawErrorBox(err, &uiFace) logger.Println(err) } else { fb.Println(content) // TODO rendering, not this! Also links } } } } } hostsfile.Close() }