parent
8007f090f2
commit
e03e4037d2
@ -0,0 +1,140 @@ |
||||
package deadlinks |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
|
||||
"git.sr.ht/~adnano/go-gemini" |
||||
) |
||||
|
||||
// Client is a thread-safe type which fetches a resource at the given URL,
|
||||
// returning its MIME type and body. If the MIME type is not known then empty
|
||||
// string should be returned.
|
||||
type Client interface { |
||||
Get(context.Context, URL) (string, io.ReadCloser, error) |
||||
} |
||||
|
||||
// ClientOpts are optional fields which can be provided to NewClient. A nil
|
||||
// ClientOpts is equivalent to an empty one.
|
||||
type ClientOpts struct { |
||||
// GeminiClient will be used for retrieving resources via the gemini
|
||||
// protocol.
|
||||
//
|
||||
// Defaults to `new(gemini.Client)`.
|
||||
GeminiClient interface { |
||||
Do(context.Context, *gemini.Request) (*gemini.Response, error) |
||||
} |
||||
|
||||
// MaxRedirects indicates the maximum number of redirects which will be
|
||||
// allowed when resolving a resource. A negative value indicates no
|
||||
// redirects are allowed.
|
||||
//
|
||||
// Default: 10.
|
||||
MaxRedirects int |
||||
} |
||||
|
||||
func (o *ClientOpts) withDefaults() *ClientOpts { |
||||
if o == nil { |
||||
o = new(ClientOpts) |
||||
} |
||||
|
||||
if o.GeminiClient == nil { |
||||
o.GeminiClient = new(gemini.Client) |
||||
} |
||||
|
||||
if o.MaxRedirects == 0 { |
||||
o.MaxRedirects = 10 |
||||
} |
||||
|
||||
return o |
||||
} |
||||
|
||||
type client struct { |
||||
opts ClientOpts |
||||
} |
||||
|
||||
// NewClient initializes and returns a Client which supports commonly used
|
||||
// transport protocols. The returned Client will error when it encounters an
|
||||
// unfamiliar protocol.
|
||||
//
|
||||
// Supported URL schemas:
|
||||
// - gemini
|
||||
// - http/https (TODO)
|
||||
func NewClient(opts *ClientOpts) Client { |
||||
return &client{*opts.withDefaults()} |
||||
} |
||||
|
||||
func (c *client) getGemini( |
||||
ctx context.Context, url URL, redirectDepth int, |
||||
) ( |
||||
string, io.ReadCloser, error, |
||||
) { |
||||
req, err := gemini.NewRequest(string(url)) |
||||
if err != nil { |
||||
return "", nil, fmt.Errorf("building request: %w", err) |
||||
} |
||||
|
||||
// TODO allow specifying client cert
|
||||
|
||||
res, err := c.opts.GeminiClient.Do(ctx, req) |
||||
if err != nil { |
||||
return "", nil, fmt.Errorf("performing request: %w", err) |
||||
} |
||||
|
||||
// all status numbers are grouped by their first digit, and actions taken
|
||||
// can be entirely based on that.
|
||||
switch res.Status / 10 { |
||||
|
||||
case 1: // input required
|
||||
// Assume that input required is fine, even though we don't know the
|
||||
// MIME type.
|
||||
return "", res.Body, nil |
||||
|
||||
case 2: // success
|
||||
return res.Meta, res.Body, nil |
||||
|
||||
case 3: // redirect
|
||||
defer res.Body.Close() |
||||
if redirectDepth >= c.opts.MaxRedirects { |
||||
return "", nil, errors.New("too many redirects") |
||||
} |
||||
|
||||
newURL, err := url.ResolveReference(res.Meta) |
||||
if err != nil { |
||||
return "", nil, fmt.Errorf("resolving redirect URL %q: %w", res.Meta, err) |
||||
} |
||||
|
||||
return c.get(ctx, newURL, redirectDepth+1) |
||||
|
||||
default: |
||||
defer res.Body.Close() |
||||
return "", nil, fmt.Errorf( |
||||
"response code %d (%v): %q", res.Status, res.Status, res.Meta, |
||||
) |
||||
} |
||||
} |
||||
|
||||
func (c *client) get( |
||||
ctx context.Context, url URL, redirectDepth int, |
||||
) ( |
||||
string, io.ReadCloser, error, |
||||
) { |
||||
|
||||
scheme := url.toStd().Scheme |
||||
switch scheme { |
||||
case "gemini": |
||||
return c.getGemini(ctx, url, redirectDepth) |
||||
default: |
||||
return "", nil, fmt.Errorf("unsupported scheme %q", scheme) |
||||
} |
||||
} |
||||
|
||||
func (c *client) Get( |
||||
ctx context.Context, url URL, |
||||
) ( |
||||
string, io.ReadCloser, error, |
||||
) { |
||||
return c.get(ctx, url, 0) |
||||
} |
Loading…
Reference in new issue