154 lines
3.7 KiB
Go
154 lines
3.7 KiB
Go
package sliderule
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
neturl "net/url"
|
|
|
|
"tildegit.org/tjp/sliderule/finger"
|
|
"tildegit.org/tjp/sliderule/gemini"
|
|
"tildegit.org/tjp/sliderule/gopher"
|
|
"tildegit.org/tjp/sliderule/internal/types"
|
|
"tildegit.org/tjp/sliderule/nex"
|
|
"tildegit.org/tjp/sliderule/spartan"
|
|
)
|
|
|
|
type protocolClient interface {
|
|
RoundTrip(context.Context, *Request) (*Response, error)
|
|
IsRedirect(*Response) bool
|
|
}
|
|
|
|
// Client is a multi-protocol client which handles all protocols known to sliderule.
|
|
type Client struct {
|
|
MaxRedirects int
|
|
|
|
protos map[string]protocolClient
|
|
}
|
|
|
|
const DefaultMaxRedirects int = 5
|
|
|
|
var ExceededMaxRedirects = errors.New("Client: exceeded MaxRedirects")
|
|
|
|
// NewClient builds a Client object.
|
|
//
|
|
// tlsConf may be nil, in which case gemini requests connections will not be made
|
|
// with any client certificate.
|
|
func NewClient(tlsConf *tls.Config) Client {
|
|
hc := httpClient{tp: http.DefaultTransport.(*http.Transport).Clone()}
|
|
if tlsConf != nil {
|
|
hc.tp.TLSClientConfig = tlsConf
|
|
}
|
|
gemcl := gemini.NewClient(tlsConf)
|
|
return Client{
|
|
protos: map[string]protocolClient{
|
|
"finger": finger.Client{},
|
|
"gopher": gopher.Client{},
|
|
"gemini": gemcl,
|
|
"titan": gemcl,
|
|
"spartan": spartan.NewClient(),
|
|
"http": hc,
|
|
"https": hc,
|
|
"nex": nex.Client{},
|
|
},
|
|
MaxRedirects: DefaultMaxRedirects,
|
|
}
|
|
}
|
|
|
|
// RoundTrip sends a single request and returns the repsonse.
|
|
//
|
|
// If the response is a redirect it will be returned, rather than fetched.
|
|
func (c Client) RoundTrip(ctx context.Context, request *Request) (*Response, error) {
|
|
pc, ok := c.protos[request.Scheme]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unrecognized protocol: %s", request.Scheme)
|
|
}
|
|
return pc.RoundTrip(ctx, request)
|
|
}
|
|
|
|
// Fetch collects a resource from a URL including following any redirects.
|
|
func (c Client) Fetch(ctx context.Context, url string) (*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(ctx, &types.Request{URL: u})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !c.protos[u.Scheme].IsRedirect(response) {
|
|
return response, nil
|
|
}
|
|
|
|
prev := u
|
|
u, err = neturl.Parse(getRedirectLocation(prev, u.Scheme, response.Meta))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if u.Scheme == "" {
|
|
u.Scheme = prev.Scheme
|
|
}
|
|
}
|
|
|
|
return nil, ExceededMaxRedirects
|
|
}
|
|
|
|
// Upload sends a request with a body and returns any redirect response.
|
|
func (c Client) Upload(ctx context.Context, url string, contents io.Reader) (*Response, error) {
|
|
u, err := neturl.Parse(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch u.Scheme {
|
|
case "titan", "spartan", "http", "https":
|
|
return c.RoundTrip(ctx, &types.Request{URL: u, Meta: contents})
|
|
default:
|
|
return nil, fmt.Errorf("upload not supported on %s", u.Scheme)
|
|
}
|
|
}
|
|
|
|
func getRedirectLocation(prev *neturl.URL, proto string, meta any) string {
|
|
switch proto {
|
|
case "gemini", "spartan":
|
|
u, _ := neturl.Parse(meta.(string))
|
|
return prev.ResolveReference(u).String()
|
|
case "http", "https":
|
|
return meta.(*http.Response).Header.Get("Location")
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type httpClient struct {
|
|
tp *http.Transport
|
|
}
|
|
|
|
func (hc httpClient) RoundTrip(ctx context.Context, request *Request) (*Response, error) {
|
|
body, _ := request.Meta.(io.Reader)
|
|
hreq, err := http.NewRequestWithContext(ctx, "GET", request.URL.String(), body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hresp, err := hc.tp.RoundTrip(hreq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Response{
|
|
Status: Status(hresp.StatusCode),
|
|
Meta: hresp,
|
|
Body: hresp.Body,
|
|
Request: request,
|
|
}, nil
|
|
}
|
|
|
|
func (hc httpClient) IsRedirect(response *Response) bool {
|
|
return response.Meta.(*http.Response).Header.Get("Location") != ""
|
|
}
|