package gemini import ( "bytes" "crypto/tls" "errors" "io" "net" "tildegit.org/tjp/gus" ) // 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 *gus.Request) (*gus.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") } conn, err := tls.Dial("tcp", host, client.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 }