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 }