218 lines
4.7 KiB
Go
218 lines
4.7 KiB
Go
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()
|
|
|
|
}
|