From 25d8d63473fe69f81706ce2554e53b5da6436c7c Mon Sep 17 00:00:00 2001 From: merlin Date: Wed, 27 Jul 2022 17:04:23 +0300 Subject: [PATCH 1/4] addd redirect support for client --- gemax/client.go | 57 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/gemax/client.go b/gemax/client.go index a1361f3..72a5fdb 100644 --- a/gemax/client.go +++ b/gemax/client.go @@ -3,6 +3,7 @@ package gemax import ( "context" "crypto/tls" + "errors" "fmt" "io" "net" @@ -20,19 +21,67 @@ import ( type Client struct { MaxResponseSize int64 Dial func(ctx context.Context, host string, cfg *tls.Config) (net.Conn, error) + Redirect func(ctx context.Context, req *urlpkg.URL, prev []RedirectedRequest) error once sync.Once } +var ErrTooManyRedirects = errors.New("too many redirects") + +func (client *Client) checkRedirect(ctx context.Context, req *urlpkg.URL, prev []RedirectedRequest) error { + if client.Redirect != nil { + return client.Redirect(ctx, req, prev) + } + return defaultRedirect(ctx, req, prev) +} + +func defaultRedirect(_ context.Context, _ *urlpkg.URL, prev []RedirectedRequest) error { + const max = 10 + if len(prev) < max { + return nil + } + return ErrTooManyRedirects +} + +type RedirectedRequest struct { + Req *urlpkg.URL + Response *Response +} + const readerBufSize = 16 << 10 // Fetch gemini resource. func (client *Client) Fetch(ctx context.Context, url string) (*Response, error) { client.init() - var u, errParseURL = urlpkg.Parse(url) - if errParseURL != nil { - return nil, fmt.Errorf("parsing URL: %w", errParseURL) + var redirects []RedirectedRequest + for { + var u, errParseURL = urlpkg.Parse(url) + if errParseURL != nil { + return nil, fmt.Errorf("parsing URL: %w", errParseURL) + } + if err := client.checkRedirect(ctx, u, redirects); err != nil { + return nil, fmt.Errorf("redirect: %w", err) + } + resp, errFetch := client.fetch(ctx, url, u) + if errFetch != nil { + return resp, errFetch + } + if !isRedirect(resp.Status) { + return resp, nil + } + resp.Close() + redirects = append(redirects, RedirectedRequest{ + Req: u, + Response: resp, + }) + url = resp.Meta } +} + +func isRedirect(code status.Code) bool { + return code == status.Redirect || code == status.RedirectPermanent +} +func (client *Client) fetch(ctx context.Context, origURL string, u *urlpkg.URL) (*Response, error) { var host = u.Host if strings.LastIndexByte(host, ':') < 0 { host += ":1965" @@ -51,7 +100,7 @@ func (client *Client) Fetch(ctx context.Context, url string) (*Response, error) } ctxConnDeadline(ctx, conn) - var _, errWrite = io.WriteString(conn, url+"\r\n") + var _, errWrite = io.WriteString(conn, origURL+"\r\n") if errWrite != nil { return nil, fmt.Errorf("sending request: %w", errWrite) } From f92144067c062a76812d4618c103bc4faeb992ab Mon Sep 17 00:00:00 2001 From: merlin Date: Wed, 27 Jul 2022 17:28:39 +0300 Subject: [PATCH 2/4] redirect docs --- gemax/client.go | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/gemax/client.go b/gemax/client.go index 72a5fdb..f621e8e 100644 --- a/gemax/client.go +++ b/gemax/client.go @@ -21,22 +21,41 @@ import ( type Client struct { MaxResponseSize int64 Dial func(ctx context.Context, host string, cfg *tls.Config) (net.Conn, error) - Redirect func(ctx context.Context, req *urlpkg.URL, prev []RedirectedRequest) error - once sync.Once + // CheckRedirect specifies the policy for handling redirects. + // If CheckRedirect is not nil, the client calls it before + // following an Gemini redirect. The arguments req and via are + // the upcoming request and the requests made already, oldest + // first. If CheckRedirect returns an error, the Client's Fetch + // method returns both the previous Response (with its Body + // closed) and CheckRedirect's error. + // instead of issuing the Request req. + // As a special case, if CheckRedirect returns ErrUseLastResponse, + // then the most recent response is returned with its body + // unclosed, along with a nil error. + // + // If CheckRedirect is nil, the Client uses its default policy, + // which is to stop after 10 consecutive requests. + CheckRedirect func(ctx context.Context, verification *urlpkg.URL, via []RedirectedRequest) error + once sync.Once } -var ErrTooManyRedirects = errors.New("too many redirects") +var ( + // ErrTooManyRedirects means that server tried through too many adresses. + // Default limit is 10. + // User implementations of CheckRedirect should use this error then limiting number of redirects. + ErrTooManyRedirects = errors.New("too many redirects") +) -func (client *Client) checkRedirect(ctx context.Context, req *urlpkg.URL, prev []RedirectedRequest) error { - if client.Redirect != nil { - return client.Redirect(ctx, req, prev) +func (client *Client) checkRedirect(ctx context.Context, req *urlpkg.URL, via []RedirectedRequest) error { + if client.CheckRedirect != nil { + return client.CheckRedirect(ctx, req, via) } - return defaultRedirect(ctx, req, prev) + return defaultRedirect(ctx, req, via) } -func defaultRedirect(_ context.Context, _ *urlpkg.URL, prev []RedirectedRequest) error { +func defaultRedirect(_ context.Context, _ *urlpkg.URL, via []RedirectedRequest) error { const max = 10 - if len(prev) < max { + if len(via) < max { return nil } return ErrTooManyRedirects From ca5b37800046c174a64780f76f395957b98a3cf0 Mon Sep 17 00:00:00 2001 From: merlin Date: Wed, 27 Jul 2022 17:57:57 +0300 Subject: [PATCH 3/4] add redirect tests --- gemax/client_test.go | 48 +++++++++++++++++++++++ gemax/testdata/client/pages/redirect1.com | 3 ++ gemax/testdata/client/pages/redirect2.com | 3 ++ 3 files changed, 54 insertions(+) create mode 100644 gemax/testdata/client/pages/redirect1.com create mode 100644 gemax/testdata/client/pages/redirect2.com diff --git a/gemax/client_test.go b/gemax/client_test.go index 64e0bdb..a081987 100644 --- a/gemax/client_test.go +++ b/gemax/client_test.go @@ -3,11 +3,13 @@ package gemax_test import ( "context" "embed" + "errors" "io" "testing" "github.com/ninedraft/gemax/gemax" "github.com/ninedraft/gemax/gemax/internal/tester" + "github.com/ninedraft/gemax/gemax/status" ) //go:embed testdata/client/pages/* @@ -35,3 +37,49 @@ func TestClient(test *testing.T) { } test.Logf("%s", data) } + +func TestClient_Redirect(test *testing.T) { + var dialer = tester.DialFS{ + Prefix: "testdata/client/pages/", + FS: testClientPages, + } + var client = &gemax.Client{ + Dial: dialer.Dial, + } + var ctx = context.Background() + var resp, errFetch = client.Fetch(ctx, "gemini://redirect1.com") + if errFetch != nil { + test.Errorf("unexpected fetch error: %v", errFetch) + return + } + if resp.Status != status.Success { + test.Fatalf("unexpected status code %v", resp.Status) + } + defer func() { _ = resp.Close() }() + var data, errRead = io.ReadAll(resp) + if errRead != nil { + test.Errorf("unexpected error while reading response body: %v", errRead) + return + } + test.Logf("%s", data) +} + +func TestClient_InfiniteRedirect(test *testing.T) { + var dialer = tester.DialFS{ + Prefix: "testdata/client/pages/", + FS: testClientPages, + } + var client = &gemax.Client{ + Dial: dialer.Dial, + } + var ctx = context.Background() + var _, errFetch = client.Fetch(ctx, "gemini://redirect2.com") + switch { + case errors.Is(errFetch, gemax.ErrTooManyRedirects): + // ok + case errFetch != nil: + test.Fatalf("unexpected error %q", errFetch) + default: + test.Fatalf("an error is expected, got nil") + } +} diff --git a/gemax/testdata/client/pages/redirect1.com b/gemax/testdata/client/pages/redirect1.com new file mode 100644 index 0000000..c78f9e5 --- /dev/null +++ b/gemax/testdata/client/pages/redirect1.com @@ -0,0 +1,3 @@ +30 gemini://success.com + +# redirect diff --git a/gemax/testdata/client/pages/redirect2.com b/gemax/testdata/client/pages/redirect2.com new file mode 100644 index 0000000..b0a758c --- /dev/null +++ b/gemax/testdata/client/pages/redirect2.com @@ -0,0 +1,3 @@ +30 gemini://redirect2.com + +# infinite redirect From fb57b7bb9fa7caa6434eb7e04cad0084a92552e3 Mon Sep 17 00:00:00 2001 From: merlin Date: Tue, 2 Aug 2022 14:20:00 +0300 Subject: [PATCH 4/4] fix linter issues --- gemax/client.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gemax/client.go b/gemax/client.go index f621e8e..16e3e8b 100644 --- a/gemax/client.go +++ b/gemax/client.go @@ -61,6 +61,8 @@ func defaultRedirect(_ context.Context, _ *urlpkg.URL, via []RedirectedRequest) return ErrTooManyRedirects } +// RedirectedRequest contains executed gemini request data +// and corresponding response with closed body. type RedirectedRequest struct { Req *urlpkg.URL Response *Response @@ -71,6 +73,7 @@ const readerBufSize = 16 << 10 // Fetch gemini resource. func (client *Client) Fetch(ctx context.Context, url string) (*Response, error) { client.init() + //nolint:prealloc // unable to preallocate, we don't know number of redirects var redirects []RedirectedRequest for { var u, errParseURL = urlpkg.Parse(url) @@ -87,7 +90,7 @@ func (client *Client) Fetch(ctx context.Context, url string) (*Response, error) if !isRedirect(resp.Status) { return resp, nil } - resp.Close() + _ = resp.Close() redirects = append(redirects, RedirectedRequest{ Req: u, Response: resp,