go-gemini/examples/client.go

169 lines
3.3 KiB
Go
Raw Normal View History

2020-10-12 20:34:52 +00:00
// +build ignore
2020-09-21 21:23:51 +00:00
2020-12-18 05:47:30 +00:00
// This example illustrates a Gemini client.
2020-09-21 21:23:51 +00:00
package main
import (
"bufio"
tofu: Refactor This commit changes underlying file handling and known hosts parsing. A known hosts file opened through Load() never closed the underlying file. During known hosts parsing most errors were unchecked, or just led to the line being skipped. I removed the KnownHosts type, which didn't really have a role after the refactor. The embedding of KnownHosts in KnownHosts file has been removed as it also leaked the map unprotected by the mutex. The Fingerprint type is now KnownHost and has taken over the responsibility of marshalling and unmarshalling. SetOutput now takes a WriteCloser so that we can close the underlying writer when it's replaced, or when it's explicitly closed through the new Close() function. KnownHostsFile.Add() now also writes the known host to the output if set. I think that makes sense expectation-wise for the type. Turned WriteAll() into WriteTo() to conform with the io.WriterTo interface. Load() is now Open() to better reflect the fact that a file is opened, and kept open. It can now also return errors from the parsing process. The parser does a lot more error checking, and this might be an area where I've changed a desired behaviour as invalid entries no longer are ignored, but aborts the parsing process. That could be changed to a warning, or some kind of parsing feedback. I added KnownHostsFile.TOFU() to fill the developer experience gap that was left after the client no longer knows about KnownHostsFile. It implements a basic non-interactive TOFU flow.
2021-01-13 21:33:48 +00:00
"bytes"
2021-02-20 20:39:47 +00:00
"context"
2020-11-01 02:50:42 +00:00
"crypto/x509"
2020-12-18 00:50:26 +00:00
"errors"
2020-09-21 21:23:51 +00:00
"fmt"
2021-02-16 23:57:24 +00:00
"io"
"log"
"net/url"
2020-09-21 21:23:51 +00:00
"os"
2020-11-09 17:04:53 +00:00
"path/filepath"
2020-09-28 03:49:41 +00:00
"time"
2020-10-28 02:12:10 +00:00
"git.sr.ht/~adnano/go-gemini"
2021-01-10 21:44:32 +00:00
"git.sr.ht/~adnano/go-gemini/tofu"
2020-09-21 21:23:51 +00:00
)
var (
2021-01-14 22:28:03 +00:00
hosts tofu.KnownHosts
hostsfile *tofu.HostWriter
scanner *bufio.Scanner
)
func xdgDataHome() string {
if s, ok := os.LookupEnv("XDG_DATA_HOME"); ok {
return s
}
return filepath.Join(os.Getenv("HOME"), ".local", "share")
}
func init() {
// Load known hosts file
path := filepath.Join(xdgDataHome(), "gemini", "known_hosts")
2021-01-14 22:28:03 +00:00
err := hosts.Load(path)
if err != nil {
2021-01-14 22:28:03 +00:00
log.Fatal(err)
}
hostsfile, err = tofu.OpenHostsFile(path)
2021-01-14 22:28:03 +00:00
if err != nil {
log.Fatal(err)
}
scanner = bufio.NewScanner(os.Stdin)
}
const trustPrompt = `The certificate offered by %s is of unknown trust. Its fingerprint is:
%s
If you knew the fingerprint to expect in advance, verify that this matches.
Otherwise, this should be safe to trust.
[t]rust always; trust [o]nce; [a]bort
=> `
func trustCertificate(hostname string, cert *x509.Certificate) error {
2021-01-14 22:28:03 +00:00
host := tofu.NewHost(hostname, cert.Raw, cert.NotAfter)
tofu: Refactor This commit changes underlying file handling and known hosts parsing. A known hosts file opened through Load() never closed the underlying file. During known hosts parsing most errors were unchecked, or just led to the line being skipped. I removed the KnownHosts type, which didn't really have a role after the refactor. The embedding of KnownHosts in KnownHosts file has been removed as it also leaked the map unprotected by the mutex. The Fingerprint type is now KnownHost and has taken over the responsibility of marshalling and unmarshalling. SetOutput now takes a WriteCloser so that we can close the underlying writer when it's replaced, or when it's explicitly closed through the new Close() function. KnownHostsFile.Add() now also writes the known host to the output if set. I think that makes sense expectation-wise for the type. Turned WriteAll() into WriteTo() to conform with the io.WriterTo interface. Load() is now Open() to better reflect the fact that a file is opened, and kept open. It can now also return errors from the parsing process. The parser does a lot more error checking, and this might be an area where I've changed a desired behaviour as invalid entries no longer are ignored, but aborts the parsing process. That could be changed to a warning, or some kind of parsing feedback. I added KnownHostsFile.TOFU() to fill the developer experience gap that was left after the client no longer knows about KnownHostsFile. It implements a basic non-interactive TOFU flow.
2021-01-13 21:33:48 +00:00
knownHost, ok := hosts.Lookup(hostname)
if ok && time.Now().Before(knownHost.Expires) {
2020-12-19 18:44:33 +00:00
// Check fingerprint
tofu: Refactor This commit changes underlying file handling and known hosts parsing. A known hosts file opened through Load() never closed the underlying file. During known hosts parsing most errors were unchecked, or just led to the line being skipped. I removed the KnownHosts type, which didn't really have a role after the refactor. The embedding of KnownHosts in KnownHosts file has been removed as it also leaked the map unprotected by the mutex. The Fingerprint type is now KnownHost and has taken over the responsibility of marshalling and unmarshalling. SetOutput now takes a WriteCloser so that we can close the underlying writer when it's replaced, or when it's explicitly closed through the new Close() function. KnownHostsFile.Add() now also writes the known host to the output if set. I think that makes sense expectation-wise for the type. Turned WriteAll() into WriteTo() to conform with the io.WriterTo interface. Load() is now Open() to better reflect the fact that a file is opened, and kept open. It can now also return errors from the parsing process. The parser does a lot more error checking, and this might be an area where I've changed a desired behaviour as invalid entries no longer are ignored, but aborts the parsing process. That could be changed to a warning, or some kind of parsing feedback. I added KnownHostsFile.TOFU() to fill the developer experience gap that was left after the client no longer knows about KnownHostsFile. It implements a basic non-interactive TOFU flow.
2021-01-13 21:33:48 +00:00
if bytes.Equal(knownHost.Fingerprint, host.Fingerprint) {
2020-12-19 18:44:33 +00:00
return nil
}
return errors.New("error: fingerprint does not match!")
2020-12-18 00:50:26 +00:00
}
tofu: Refactor This commit changes underlying file handling and known hosts parsing. A known hosts file opened through Load() never closed the underlying file. During known hosts parsing most errors were unchecked, or just led to the line being skipped. I removed the KnownHosts type, which didn't really have a role after the refactor. The embedding of KnownHosts in KnownHosts file has been removed as it also leaked the map unprotected by the mutex. The Fingerprint type is now KnownHost and has taken over the responsibility of marshalling and unmarshalling. SetOutput now takes a WriteCloser so that we can close the underlying writer when it's replaced, or when it's explicitly closed through the new Close() function. KnownHostsFile.Add() now also writes the known host to the output if set. I think that makes sense expectation-wise for the type. Turned WriteAll() into WriteTo() to conform with the io.WriterTo interface. Load() is now Open() to better reflect the fact that a file is opened, and kept open. It can now also return errors from the parsing process. The parser does a lot more error checking, and this might be an area where I've changed a desired behaviour as invalid entries no longer are ignored, but aborts the parsing process. That could be changed to a warning, or some kind of parsing feedback. I added KnownHostsFile.TOFU() to fill the developer experience gap that was left after the client no longer knows about KnownHostsFile. It implements a basic non-interactive TOFU flow.
2021-01-13 21:33:48 +00:00
fmt.Printf(trustPrompt, hostname, host.Fingerprint)
scanner.Scan()
switch scanner.Text() {
case "t":
tofu: Refactor This commit changes underlying file handling and known hosts parsing. A known hosts file opened through Load() never closed the underlying file. During known hosts parsing most errors were unchecked, or just led to the line being skipped. I removed the KnownHosts type, which didn't really have a role after the refactor. The embedding of KnownHosts in KnownHosts file has been removed as it also leaked the map unprotected by the mutex. The Fingerprint type is now KnownHost and has taken over the responsibility of marshalling and unmarshalling. SetOutput now takes a WriteCloser so that we can close the underlying writer when it's replaced, or when it's explicitly closed through the new Close() function. KnownHostsFile.Add() now also writes the known host to the output if set. I think that makes sense expectation-wise for the type. Turned WriteAll() into WriteTo() to conform with the io.WriterTo interface. Load() is now Open() to better reflect the fact that a file is opened, and kept open. It can now also return errors from the parsing process. The parser does a lot more error checking, and this might be an area where I've changed a desired behaviour as invalid entries no longer are ignored, but aborts the parsing process. That could be changed to a warning, or some kind of parsing feedback. I added KnownHostsFile.TOFU() to fill the developer experience gap that was left after the client no longer knows about KnownHostsFile. It implements a basic non-interactive TOFU flow.
2021-01-13 21:33:48 +00:00
hosts.Add(host)
2021-01-14 22:28:03 +00:00
hostsfile.WriteHost(host)
return nil
case "o":
2021-01-14 22:28:03 +00:00
hosts.Add(host)
return nil
default:
return errors.New("certificate not trusted")
2020-12-18 00:50:26 +00:00
}
}
2020-12-18 00:50:26 +00:00
func getInput(prompt string, sensitive bool) (input string, ok bool) {
fmt.Printf("%s ", prompt)
scanner.Scan()
return scanner.Text(), true
}
2020-12-18 00:50:26 +00:00
func do(req *gemini.Request, via []*gemini.Request) (*gemini.Response, error) {
client := gemini.Client{
TrustCertificate: trustCertificate,
}
2021-02-23 20:55:27 +00:00
ctx := context.Background()
resp, err := client.Do(ctx, req)
if err != nil {
return resp, err
}
2021-02-24 14:22:23 +00:00
switch resp.Status().Class() {
2021-02-14 23:59:33 +00:00
case gemini.StatusInput:
2021-02-24 14:22:23 +00:00
input, ok := getInput(resp.Meta(), resp.Status() == gemini.StatusSensitiveInput)
if !ok {
break
}
req.URL.ForceQuery = true
req.URL.RawQuery = gemini.QueryEscape(input)
return do(req, via)
2021-02-14 23:59:33 +00:00
case gemini.StatusRedirect:
via = append(via, req)
if len(via) > 5 {
return resp, errors.New("too many redirects")
2020-12-18 00:50:26 +00:00
}
2020-09-21 21:23:51 +00:00
2021-02-24 14:22:23 +00:00
target, err := url.Parse(resp.Meta())
if err != nil {
return resp, err
2020-11-01 02:50:42 +00:00
}
target = req.URL.ResolveReference(target)
redirect := *req
redirect.URL = target
return do(&redirect, via)
2020-11-01 02:50:42 +00:00
}
return resp, err
}
func main() {
if len(os.Args) < 2 {
fmt.Printf("usage: %s <url> [host]\n", os.Args[0])
os.Exit(1)
}
2020-09-27 23:45:48 +00:00
2020-12-18 00:50:26 +00:00
// Do the request
2020-09-27 23:45:48 +00:00
url := os.Args[1]
2020-10-28 02:12:10 +00:00
req, err := gemini.NewRequest(url)
2020-09-27 23:45:48 +00:00
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if len(os.Args) == 3 {
req.Host = os.Args[2]
}
resp, err := do(req, nil)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
2021-02-23 22:52:34 +00:00
defer resp.Close()
2020-12-18 00:50:26 +00:00
// Handle response
2021-02-24 14:22:23 +00:00
if resp.Status().Class() == gemini.StatusSuccess {
2021-02-23 22:52:34 +00:00
_, err := io.Copy(os.Stdout, resp)
if err != nil {
log.Fatal(err)
}
} else {
2021-01-10 05:10:57 +00:00
fmt.Printf("%d %s\n", resp.Status, resp.Meta)
2020-12-18 00:50:26 +00:00
os.Exit(1)
2020-09-21 21:23:51 +00:00
}
}