From 330b6e4828d59a6d5fae3cc7a30e6fda0856577b Mon Sep 17 00:00:00 2001 From: tjpcc Date: Sat, 12 Aug 2023 08:48:09 -0600 Subject: add Fetch method to clients which follows redirects There are currently only gopher, gemini, and spartan clients. The finger client will have to implement this when it is written. The Fetch method takes the url as a string for convenience, and parses it into a URL for RoundTrip. Fixes #3 --- gemini/client.go | 40 ++++++++++++++++++++++++++++++++++- gopher/client.go | 10 +++++++++ spartan/client.go | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/gemini/client.go b/gemini/client.go index 7c78b71..338271c 100644 --- a/gemini/client.go +++ b/gemini/client.go @@ -6,6 +6,7 @@ import ( "errors" "io" "net" + neturl "net/url" sr "tildegit.org/tjp/sliderule" ) @@ -18,14 +19,22 @@ import ( // // The zero value is a usable Client with no client TLS certificate. type Client struct { + MaxRedirects int + tlsConf *tls.Config } // Create a gemini Client with the given TLS configuration. func NewClient(tlsConf *tls.Config) Client { - return Client{tlsConf: tlsConf} + return Client{tlsConf: tlsConf, MaxRedirects: DefaultMaxRedirects} } +// DefaultMaxRedirects is the number of chained redirects a Client will perform for a +// single request by default. This can be changed by altering the MaxRedirects field. +const DefaultMaxRedirects int = 2 + +var ExceededMaxRedirects = errors.New("gemini.Client: exceeded MaxRedirects") + // 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 @@ -77,3 +86,32 @@ func (client Client) RoundTrip(request *sr.Request) (*sr.Response, error) { return response, nil } + +// Fetch parses a URL string and fetches the gemini resource. +// +// It will resolve any redirects along the way, up to client.MaxRedirects. +func (c Client) Fetch(url string) (*sr.Response, error) { + u, err := neturl.Parse(url) + if err != nil { + return nil, err + } + + for i := 0; i <= c.MaxRedirects; i += 1 { + response, err := c.RoundTrip(&sr.Request{URL: u}) + if err != nil { + return nil, err + } + if ResponseCategoryForStatus(response.Status) != ResponseCategoryRedirect { + return response, nil + } + + prev := u + u, err = neturl.Parse(url) + if err != nil { + return nil, err + } + u = prev.ResolveReference(u) + } + + return nil, ExceededMaxRedirects +} diff --git a/gopher/client.go b/gopher/client.go index fad9413..163d0cd 100644 --- a/gopher/client.go +++ b/gopher/client.go @@ -5,6 +5,7 @@ import ( "errors" "io" "net" + neturl "net/url" sr "tildegit.org/tjp/sliderule" ) @@ -53,3 +54,12 @@ func (c Client) RoundTrip(request *sr.Request) (*sr.Response, error) { return &sr.Response{Body: bytes.NewBuffer(response)}, nil } + +// Fetch parses a URL string and fetches the gopher resource. +func (c Client) Fetch(url string) (*sr.Response, error) { + u, err := neturl.Parse(url) + if err != nil { + return nil, err + } + return c.RoundTrip(&sr.Request{URL: u}) +} diff --git a/spartan/client.go b/spartan/client.go index 40c9dd6..a98949d 100644 --- a/spartan/client.go +++ b/spartan/client.go @@ -5,6 +5,7 @@ import ( "errors" "io" "net" + neturl "net/url" "strconv" sr "tildegit.org/tjp/sliderule" @@ -15,7 +16,19 @@ import ( // It carries no state and is reusable simultaneously by multiple goroutines. // // The zero value is immediately usabble. -type Client struct{} +type Client struct{ + MaxRedirects int +} + +func NewClient() Client { + return Client{MaxRedirects: DefaultMaxRedirects} +} + +// DefaultMaxRedirects is the number of chained redirects a Client will perform for a +// single request by default. This can be changed by altering the MaxRedirects field. +const DefaultMaxRedirects int = 2 + +var ExceededMaxRedirects = errors.New("spartan.Client: exceeded MaxRedirects") // RoundTrip sends a single spartan request and returns its response. func (c Client) RoundTrip(request *sr.Request) (*sr.Response, error) { @@ -38,9 +51,15 @@ func (c Client) RoundTrip(request *sr.Request) (*sr.Response, error) { request.RemoteAddr = conn.RemoteAddr() - rdr, ok := request.Meta.(*io.LimitedReader) - if !ok { - return nil, errors.New("request body must be an *io.LimitedReader") + var rdr *io.LimitedReader + if request.Meta == nil { + rdr = &io.LimitedReader{R: devnull{}, N: 0} + } else { + var ok bool + rdr, ok = request.Meta.(*io.LimitedReader) + if !ok { + return nil, errors.New("request body must be nil or an *io.LimitedReader") + } } requestLine := host + " " + request.EscapedPath() + " " + strconv.Itoa(int(rdr.N)) + "\r\n" @@ -65,3 +84,38 @@ func (c Client) RoundTrip(request *sr.Request) (*sr.Response, error) { return response, nil } + +// Fetch parses a URL string and fetches the spartan resource. +// +// It will resolve any redirects along the way, up to client.MaxRedirects. +func (c Client) Fetch(url string) (*sr.Response, error) { + u, err := neturl.Parse(url) + if err != nil { + return nil, err + } + + for i := 0; i <= c.MaxRedirects; i += 1 { + response, err := c.RoundTrip(&sr.Request{URL: u}) + if err != nil { + return nil, err + } + if response.Status != StatusRedirect { + return response, nil + } + + prev := u + u, err = neturl.Parse(url) + if err != nil { + return nil, err + } + u = prev.ResolveReference(u) + } + + return nil, ExceededMaxRedirects +} + +type devnull struct{} + +func (_ devnull) Read(p []byte) (int, error) { + return 0, nil +} -- cgit v1.2.3