summaryrefslogtreecommitdiff
path: root/gemini/client.go
blob: 7c78b7124a1655bb6151b8b782cfab70358c147c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package gemini

import (
	"bytes"
	"crypto/tls"
	"errors"
	"io"
	"net"

	sr "tildegit.org/tjp/sliderule"
)

// Client is used for sending gemini requests and parsing gemini responses.
//
// It carries no state and is usable and reusable simultaneously by multiple goroutines.
// The only reason you might create more than one Client is to support separate TLS-cert
// driven identities.
//
// The zero value is a usable Client with no client TLS certificate.
type Client struct {
	tlsConf *tls.Config
}

// Create a gemini Client with the given TLS configuration.
func NewClient(tlsConf *tls.Config) Client {
	return Client{tlsConf: tlsConf}
}

// RoundTrip sends a single gemini request to the correct server and returns its response.
//
// It also populates the TLSState and RemoteAddr fields on the request - the only field
// it needs populated beforehand is the URL.
//
// This method will not automatically follow redirects or cache permanent failures or
// redirects.
func (client Client) RoundTrip(request *sr.Request) (*sr.Response, error) {
	if request.Scheme != "gemini" && request.Scheme != "" {
		return nil, errors.New("non-gemini protocols not supported")
	}

	host := request.Host
	if _, port, _ := net.SplitHostPort(host); port == "" {
		host = net.JoinHostPort(host, "1965")
	}

	tlsConf := tls.Config{InsecureSkipVerify: true}
	if (client.tlsConf != nil) {
		tlsConf = *client.tlsConf
	}

	conn, err := tls.Dial("tcp", host, &tlsConf)
	if err != nil {
		return nil, err
	}
	defer conn.Close()

	request.RemoteAddr = conn.RemoteAddr()
	st := conn.ConnectionState()
	request.TLSState = &st

	if _, err := conn.Write([]byte(request.URL.String() + "\r\n")); err != nil {
		return nil, err
	}

	response, err := ParseResponse(conn)
	if err != nil {
		return nil, err
	}

	// read and store the request body in full or we may miss doing so before
	// closing the connection
	bodybuf, err := io.ReadAll(response.Body)
	if err != nil {
		return nil, err
	}
	response.Body = bytes.NewBuffer(bodybuf)

	return response, nil
}