Skip to content

Commit

Permalink
Merge pull request #33 from ninedraft/redirect
Browse files Browse the repository at this point in the history
  • Loading branch information
ninedraft committed Aug 2, 2022
2 parents fa3b974 + fb57b7b commit 81edfaa
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 5 deletions.
81 changes: 76 additions & 5 deletions gemax/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gemax
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
Expand All @@ -20,19 +21,89 @@ import (
type Client struct {
MaxResponseSize int64
Dial func(ctx context.Context, host string, cfg *tls.Config) (net.Conn, 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 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, via []RedirectedRequest) error {
if client.CheckRedirect != nil {
return client.CheckRedirect(ctx, req, via)
}
return defaultRedirect(ctx, req, via)
}

func defaultRedirect(_ context.Context, _ *urlpkg.URL, via []RedirectedRequest) error {
const max = 10
if len(via) < max {
return nil
}
return ErrTooManyRedirects
}

// RedirectedRequest contains executed gemini request data
// and corresponding response with closed body.
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)
//nolint:prealloc // unable to preallocate, we don't know number of redirects
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"
Expand All @@ -51,7 +122,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)
}
Expand Down
48 changes: 48 additions & 0 deletions gemax/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/*
Expand Down Expand Up @@ -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")
}
}
3 changes: 3 additions & 0 deletions gemax/testdata/client/pages/redirect1.com
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
30 gemini://success.com

# redirect
3 changes: 3 additions & 0 deletions gemax/testdata/client/pages/redirect2.com
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
30 gemini://redirect2.com

# infinite redirect

0 comments on commit 81edfaa

Please sign in to comment.