gemini-browser

A text-based gemini browser
git clone git://git.laack.co/gemini-browser.git
Log | Files | Refs | README

client.go (13215B)


      1 package gemini
      2 
      3 import (
      4 	"bytes"
      5 	"crypto/tls"
      6 	"crypto/x509"
      7 	"fmt"
      8 	"io"
      9 	"net"
     10 	"net/url"
     11 	"os"
     12 	"strconv"
     13 	"strings"
     14 	"time"
     15 
     16 	"golang.org/x/net/idna"
     17 )
     18 
     19 func punycodeHost(host string) (string, error) {
     20 	hostname, port, err := net.SplitHostPort(host)
     21 	if err != nil {
     22 		// Likely means no port
     23 		hostname = host
     24 		port = ""
     25 	}
     26 
     27 	if net.ParseIP(hostname) != nil {
     28 		// Hostname is IP address, not domain
     29 		return host, nil
     30 	}
     31 	pc, err := idna.ToASCII(hostname)
     32 	if err != nil {
     33 		return host, err
     34 	}
     35 	if port == "" {
     36 		return pc, nil
     37 	}
     38 	return net.JoinHostPort(pc, port), nil
     39 }
     40 
     41 func punycodeHostFromURL(u string) (string, error) {
     42 	parsed, err := url.Parse(u)
     43 	if err != nil {
     44 		return "", err
     45 	}
     46 	return punycodeHost(parsed.Host)
     47 }
     48 
     49 // GetPunycodeURL takes a full URL that potentially has Unicode in the
     50 // domain name, and returns a URL with the domain punycoded.
     51 func GetPunycodeURL(u string) (string, error) {
     52 	parsed, err := url.Parse(u)
     53 	if err != nil {
     54 		return "", nil
     55 	}
     56 	host, err := punycodeHostFromURL(u)
     57 	if err != nil {
     58 		return "", err
     59 	}
     60 	parsed.Host = host
     61 	return parsed.String(), nil
     62 }
     63 
     64 // Response represents the response from a Gemini server.
     65 type Response struct {
     66 	Status int
     67 	Meta   string
     68 	Body   io.ReadCloser
     69 	Cert   *x509.Certificate
     70 	conn   net.Conn
     71 }
     72 
     73 type header struct {
     74 	status int
     75 	meta   string
     76 }
     77 
     78 // ProxyFunc. See Client documentation
     79 type ProxyFunc func(dialer *net.Dialer, address string) (net.Conn, error)
     80 
     81 type Client struct {
     82 	// NoTimeCheck allows connections with expired or future certs if set to true.
     83 	NoTimeCheck bool
     84 
     85 	// NoHostnameCheck allows connections when the cert doesn't match the
     86 	// requested hostname or IP.
     87 	NoHostnameCheck bool
     88 
     89 	// Insecure disables all TLS-based checks, use with caution.
     90 	// It overrides all the variables above.
     91 	Insecure bool
     92 
     93 	// AllowOutOfRangeStatuses means the client won't raise an error if a status
     94 	// that is out of range is returned.
     95 	// Use CleanStatus() to handle statuses that are in range but not specified in
     96 	// the spec.
     97 	AllowOutOfRangeStatuses bool
     98 
     99 	// ConnectTimeout is equivalent to the Timeout field in net.Dialer.
    100 	// It's the max amount of time allowed for the initial connection/handshake.
    101 	// The timeout of the DefaultClient is 15 seconds.
    102 	//
    103 	// If ReadTimeout is not set, then this value is also used to time out on getting
    104 	// the header after the connection is made.
    105 	ConnectTimeout time.Duration
    106 
    107 	// ReadTimeout is the max amount of time reading to a server can take.
    108 	// This should not be set if you want to support streams.
    109 	// It is equivalent to net.Conn.SetDeadline, see that func for more documentation.
    110 	//
    111 	// For example, if this is set to 30 seconds, then no more reading from the connection
    112 	// can happen 30 seconds after the initial handshake.
    113 	ReadTimeout time.Duration
    114 
    115 	// Proxy is a function that returns an existing connection. The TLS client
    116 	// will use this as the underlying transport, instead of making a direct TCP
    117 	// connection.
    118 	//
    119 	// go-gemini requires setting a dialer on the underlying connection, to impose
    120 	// a timeout on making the initial connection. This dialer is provided as an
    121 	// argument to the proxy function.
    122 	//
    123 	// The other argument provided is the address being connected to. For example
    124 	// "example.com:1965".
    125 	//
    126 	// Any errors returned will prevent a connection from occurring.
    127 	//
    128 	// This is not "gemini proxying", aka the proxying functionality built in to
    129 	// the Gemini protocol. This is for proxying requests over TOR, or SOCKS5, etc.
    130 	//
    131 	//     func(dialer *net.Dialer, address string) (net.Conn, error)
    132 	//
    133 	Proxy ProxyFunc
    134 }
    135 
    136 var DefaultClient = &Client{ConnectTimeout: 15 * time.Second}
    137 
    138 // getHost returns a full host for the given URL, always including a port.
    139 // It also punycodes the host, in case it contains Unicode.
    140 func getHost(parsedURL *url.URL) string {
    141 	host, _ := punycodeHostFromURL(parsedURL.String())
    142 	if parsedURL.Port() == "" {
    143 		host = net.JoinHostPort(parsedURL.Hostname(), "1965")
    144 	}
    145 	return host
    146 }
    147 
    148 // SetReadTimeout changes the read timeout after the connection has been made.
    149 // You can set it to 0 or less to disable the timeout. Otherwise, the duration
    150 // is relative to the time the function was called.
    151 func (r *Response) SetReadTimeout(d time.Duration) error {
    152 	if d <= 0 {
    153 		return r.conn.SetDeadline(time.Time{})
    154 	}
    155 	return r.conn.SetDeadline(time.Now().Add(d))
    156 }
    157 
    158 // TODO: apply punycoding to hosts
    159 
    160 // Fetch a resource from a Gemini server with the given URL.
    161 // It assumes port 1965 if no port is specified.
    162 func (c *Client) Fetch(rawURL string) (*Response, error) {
    163 	parsedURL, err := url.Parse(rawURL)
    164 	if err != nil {
    165 		return nil, fmt.Errorf("failed to parse URL: %w", err)
    166 	}
    167 	return c.FetchWithHost(getHost(parsedURL), rawURL)
    168 }
    169 
    170 // FetchWithHost fetches a resource from a Gemini server at the given host, with the given URL.
    171 // This can be used for Gemini proxying, where the URL host and actual server don't match.
    172 // It assumes the host is using port 1965 if no port number is provided.
    173 func (c *Client) FetchWithHost(host, rawURL string) (*Response, error) {
    174 	// Call with empty PEM bytes to skip using a cert
    175 	return c.FetchWithHostAndCert(host, rawURL, []byte{}, []byte{})
    176 }
    177 
    178 // FetchWithCert fetches a resource from a Gemini server with the given URL.
    179 // It allows you to provide the bytes of a PEM encoded block for a client
    180 // certificate and its key. This allows you to make requests using client
    181 // certs.
    182 //
    183 // It assumes port 1965 if no port is specified.
    184 func (c *Client) FetchWithCert(rawURL string, certPEM, keyPEM []byte) (*Response, error) {
    185 	parsedURL, err := url.Parse(rawURL)
    186 	if err != nil {
    187 		return nil, fmt.Errorf("failed to parse URL: %w", err)
    188 	}
    189 	// Call with empty PEM bytes to skip using a cert
    190 	return c.FetchWithHostAndCert(getHost(parsedURL), rawURL, certPEM, keyPEM)
    191 }
    192 
    193 // FetchWithHostAndCert combines FetchWithHost and FetchWithCert.
    194 func (c *Client) FetchWithHostAndCert(host, rawURL string, certPEM, keyPEM []byte) (*Response, error) {
    195 	u, err := GetPunycodeURL(rawURL)
    196 	if err != nil {
    197 		return nil, fmt.Errorf("error when punycoding URL: %w", err)
    198 	}
    199 	parsedURL, _ := url.Parse(u)
    200 
    201 	if len(u) > URLMaxLength {
    202 		// Out of spec
    203 		return nil, fmt.Errorf("url is too long")
    204 	}
    205 
    206 	// Add port to host if needed
    207 	_, _, err = net.SplitHostPort(host)
    208 	if err != nil {
    209 		// Error likely means there's no port in the host
    210 		host = net.JoinHostPort(host, "1965")
    211 	}
    212 	ogHost := host
    213 	host, err = punycodeHost(host)
    214 	if err != nil {
    215 		return nil, fmt.Errorf("failed to punycode host %s: %w", ogHost, err)
    216 	}
    217 
    218 	// Build tls.Certificate
    219 	var cert tls.Certificate
    220 	if len(certPEM) == 0 && len(keyPEM) == 0 {
    221 		// Cert bytes were intentionally left empty
    222 		cert = tls.Certificate{}
    223 	} else {
    224 		cert, err = tls.X509KeyPair(certPEM, keyPEM)
    225 		if err != nil {
    226 			return nil, fmt.Errorf("failed to parse cert/key PEM: %w", err)
    227 		}
    228 	}
    229 
    230 	res := Response{}
    231 
    232 	// Connect
    233 
    234 	start := time.Now()
    235 	conn, err := c.connect(&res, host, parsedURL, cert)
    236 	if err != nil {
    237 		return nil, fmt.Errorf("failed to connect to the server: %w", err)
    238 	}
    239 
    240 	// Send request
    241 
    242 	if c.ReadTimeout == 0 && c.ConnectTimeout != 0 {
    243 		// No r/w timeout, so a timeout for sending the request must be set
    244 		conn.SetDeadline(start.Add(c.ConnectTimeout))
    245 	}
    246 	err = sendRequest(conn, u)
    247 	if err != nil {
    248 		conn.Close()
    249 		return nil, err
    250 	}
    251 	if c.ReadTimeout == 0 && c.ConnectTimeout != 0 {
    252 		// Undo deadline
    253 		conn.SetDeadline(time.Time{})
    254 	}
    255 
    256 	// Get header
    257 
    258 	if c.ReadTimeout == 0 && c.ConnectTimeout != 0 {
    259 		// No r/w timeout, so a timeout for getting the header
    260 		conn.SetDeadline(start.Add(c.ConnectTimeout))
    261 	}
    262 	err = getResponse(&res, conn)
    263 	if err != nil {
    264 		conn.Close()
    265 		return nil, err
    266 	}
    267 	if c.ReadTimeout == 0 && c.ConnectTimeout != 0 {
    268 		// Undo deadline
    269 		conn.SetDeadline(time.Time{})
    270 	}
    271 
    272 	// Check status code
    273 	if !c.AllowOutOfRangeStatuses && !StatusInRange(res.Status) {
    274 		conn.Close()
    275 		return nil, fmt.Errorf("invalid status code: %v", res.Status)
    276 	}
    277 
    278 	return &res, nil
    279 }
    280 
    281 // Fetch a resource from a Gemini server with the given URL.
    282 // It assumes port 1965 if no port is specified.
    283 func Fetch(url string) (*Response, error) {
    284 	return DefaultClient.Fetch(url)
    285 }
    286 
    287 // FetchWithCert fetches a resource from a Gemini server with the given URL.
    288 // It allows you to provide the bytes of a PEM encoded block for a client
    289 // certificate and its key. This allows you to make requests using client
    290 // certs.
    291 //
    292 // It assumes port 1965 if no port is specified.
    293 func FetchWithCert(url string, certPEM, keyPEM []byte) (*Response, error) {
    294 	return DefaultClient.FetchWithCert(url, certPEM, keyPEM)
    295 }
    296 
    297 // FetchWithHost fetches a resource from a Gemini server at the given host, with the given URL.
    298 // This can be used for proxying, where the URL host and actual server don't match.
    299 // It assumes the host is using port 1965 if no port number is provided.
    300 func FetchWithHost(host, url string) (*Response, error) {
    301 	return DefaultClient.FetchWithHost(host, url)
    302 }
    303 
    304 // FetchWithHostAndCert combines FetchWithHost and FetchWithCert.
    305 func FetchWithHostAndCert(host, url string, certPEM, keyPEM []byte) (*Response, error) {
    306 	return DefaultClient.FetchWithHostAndCert(host, url, certPEM, keyPEM)
    307 }
    308 
    309 func (c *Client) connect(res *Response, host string, parsedURL *url.URL, clientCert tls.Certificate) (net.Conn, error) {
    310 	conf := &tls.Config{
    311 		MinVersion:         tls.VersionTLS12,
    312 		InsecureSkipVerify: true, // This must be set to allow self-signed certs
    313 	}
    314 	if clientCert.Certificate != nil {
    315 		// There is data, not an empty struct
    316 		conf.Certificates = []tls.Certificate{clientCert}
    317 	}
    318 
    319 	// Support logging TLS keys for debugging - See PR #5
    320 	keylogfile := os.Getenv("SSLKEYLOGFILE")
    321 	if keylogfile != "" {
    322 		w, err := os.OpenFile(keylogfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
    323 		if err == nil {
    324 			conf.KeyLogWriter = w
    325 			defer w.Close()
    326 		}
    327 	}
    328 
    329 	var conn *tls.Conn
    330 	var err error
    331 	if c.Proxy == nil {
    332 		// Dialer timeout for handshake
    333 		conn, err = tls.DialWithDialer(&net.Dialer{Timeout: c.ConnectTimeout}, "tcp", host, conf)
    334 		res.conn = conn
    335 		if err != nil {
    336 			return conn, err
    337 		}
    338 	} else {
    339 		// Use proxy
    340 		proxyConn, err := c.Proxy(&net.Dialer{Timeout: c.ConnectTimeout}, host)
    341 		if err != nil {
    342 			return nil, err
    343 		}
    344 		conn = tls.Client(proxyConn, conf)
    345 		// Make handshake manually to start connection, so later call to
    346 		// conn.ConnectionState() works
    347 		if err := conn.Handshake(); err != nil {
    348 			return nil, err
    349 		}
    350 	}
    351 
    352 	if c.ReadTimeout != 0 {
    353 		conn.SetDeadline(time.Now().Add(c.ReadTimeout))
    354 	}
    355 
    356 	cert := conn.ConnectionState().PeerCertificates[0]
    357 	res.Cert = cert
    358 
    359 	if c.Insecure {
    360 		return conn, nil
    361 	}
    362 
    363 	// Verify hostname
    364 	if !c.NoHostnameCheck {
    365 		// Cert hostname has to match connection host, not request host
    366 		hostname, _, _ := net.SplitHostPort(host)
    367 
    368 		if err := verifyHostname(cert, hostname); err != nil {
    369 			// Try with Unicode version
    370 			uniHost, uniErr := idna.ToUnicode(hostname)
    371 			err2 := verifyHostname(cert, uniHost)
    372 			if uniErr != nil {
    373 				return nil, fmt.Errorf("punycoded hostname does not verify and could not be converted to Unicode: %w", err)
    374 			}
    375 			if err2 != nil {
    376 				return nil, fmt.Errorf("hostname does not verify: %w", err2)
    377 			}
    378 			return nil, fmt.Errorf("hostname does not verify: %w", err)
    379 		}
    380 	}
    381 	// Verify expiry
    382 	if !c.NoTimeCheck {
    383 		if cert.NotBefore.After(time.Now()) {
    384 			return nil, fmt.Errorf("server cert is for the future")
    385 		} else if cert.NotAfter.Before(time.Now()) {
    386 			return nil, fmt.Errorf("server cert is expired")
    387 		}
    388 	}
    389 
    390 	return conn, nil
    391 }
    392 
    393 func sendRequest(conn io.Writer, requestURL string) error {
    394 	_, err := fmt.Fprintf(conn, "%s\r\n", requestURL)
    395 	if err != nil {
    396 		return fmt.Errorf("could not send request to the server: %w", err)
    397 	}
    398 	return nil
    399 }
    400 
    401 func getResponse(res *Response, conn io.ReadCloser) error {
    402 	header, err := getHeader(conn)
    403 	if err != nil {
    404 		conn.Close()
    405 		return fmt.Errorf("failed to get header: %w", err)
    406 	}
    407 
    408 	res.Status = header.status
    409 	res.Meta = header.meta
    410 	res.Body = conn
    411 	return nil
    412 }
    413 
    414 func getHeader(conn io.Reader) (header, error) {
    415 	line, err := readHeader(conn)
    416 	if err != nil {
    417 		return header{}, fmt.Errorf("failed to read header: %w", err)
    418 	}
    419 
    420 	fields := strings.Fields(string(line))
    421 	if len(fields) < 2 && line[len(line)-1] != ' ' {
    422 		return header{}, fmt.Errorf("header not formatted correctly")
    423 	}
    424 
    425 	status, err := strconv.Atoi(fields[0])
    426 	if err != nil {
    427 		return header{}, fmt.Errorf("unexpected status value %v: %w", fields[0], err)
    428 	}
    429 
    430 	var meta string
    431 	if len(line) <= 3 {
    432 		meta = ""
    433 	} else {
    434 		meta = string(line)[len(fields[0])+1:]
    435 	}
    436 	if len(meta) > MetaMaxLength {
    437 		return header{}, fmt.Errorf("meta string is too long")
    438 	}
    439 
    440 	return header{status, meta}, nil
    441 }
    442 
    443 func readHeader(conn io.Reader) ([]byte, error) {
    444 	var line []byte
    445 	delim := []byte("\r\n")
    446 	// A small buffer is inefficient but the maximum length of the header is small so it's okay
    447 	buf := make([]byte, 1)
    448 
    449 	for {
    450 		n, err := conn.Read(buf)
    451 		if err == io.EOF && n <= 0 {
    452 			return []byte{}, err
    453 		} else if err != nil && err != io.EOF {
    454 			return []byte{}, err
    455 		}
    456 
    457 		line = append(line, buf...)
    458 		if bytes.HasSuffix(line, delim) {
    459 			return line[:len(line)-len(delim)], nil
    460 		}
    461 	}
    462 }