summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortjpcc <tjp@ctrl-c.club>2023-08-12 10:47:51 -0600
committertjpcc <tjp@ctrl-c.club>2023-08-12 10:47:51 -0600
commit7d3cbefde656d5520067d56eeb44a8ba1f39d672 (patch)
treef8c12c0cc911cf24743798bbda6f3910f176db1a
parent23bc5f4fb7542e64c94eaa7fe2c7a6aa55010898 (diff)
multi-protocol client
Fixes #4
-rw-r--r--README.md1
-rw-r--r--client.go87
-rw-r--r--finger/client.go9
-rw-r--r--gemini/client.go10
-rw-r--r--gemini/response.go2
-rw-r--r--gopher/client.go2
-rw-r--r--spartan/client.go8
7 files changed, 112 insertions, 7 deletions
diff --git a/README.md b/README.md
index f07fe61..56fb71f 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,7 @@ sliderule is carefully structured as composable building blocks. The top-level p
* a "Server" interface type
* a "Handler" abstraction
* a "Middleware" abstraction
+* a "Client" which handles all the protocols known to sliderule
* some useful Handler wrappers: a router, request filtering, falling through a list of handlers
## Protocols
diff --git a/client.go b/client.go
new file mode 100644
index 0000000..3631bc5
--- /dev/null
+++ b/client.go
@@ -0,0 +1,87 @@
+package sliderule
+
+import (
+ "crypto/tls"
+ "errors"
+ "fmt"
+ 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/spartan"
+)
+
+type protocolClient interface {
+ RoundTrip(*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 = 2
+
+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 {
+ return Client{
+ protos: map[string]protocolClient{
+ "finger": finger.Client{},
+ "gopher": gopher.Client{},
+ "gemini": gemini.NewClient(tlsConf),
+ "spartan": spartan.NewClient(),
+ },
+ 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(request *Request) (*Response, error) {
+ pc, ok := c.protos[request.Scheme]
+ if !ok {
+ return nil, fmt.Errorf("unrecognized protocol: %s", request.Scheme)
+ }
+ return pc.RoundTrip(request)
+}
+
+// Fetch collects a resource from a URL including following any redirects.
+func (c Client) Fetch(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(&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(response.Meta.(string))
+ if err != nil {
+ return nil, err
+ }
+ if u.Scheme == "" {
+ u.Scheme = prev.Scheme
+ }
+ }
+
+ return nil, ExceededMaxRedirects
+}
diff --git a/finger/client.go b/finger/client.go
index 75a382f..bd1e3bf 100644
--- a/finger/client.go
+++ b/finger/client.go
@@ -37,7 +37,12 @@ func (c Client) RoundTrip(request *types.Request) (*types.Response, error) {
request.RemoteAddr = conn.RemoteAddr()
request.TLSState = nil
- if _, err := conn.Write([]byte(strings.TrimPrefix(request.Path, "/") + "\r\n")); err != nil {
+ username := request.User.String()
+ if username == "" {
+ username = strings.TrimPrefix(request.Path, "/")
+ }
+
+ if _, err := conn.Write([]byte(username + "\r\n")); err != nil {
return nil, err
}
@@ -57,3 +62,5 @@ func (c Client) Fetch(query string) (*types.Response, error) {
}
return c.RoundTrip(req)
}
+
+func (c Client) IsRedirect(_ *types.Response) bool { return false }
diff --git a/gemini/client.go b/gemini/client.go
index 34d5839..0a621dd 100644
--- a/gemini/client.go
+++ b/gemini/client.go
@@ -54,7 +54,7 @@ func (client Client) RoundTrip(request *types.Request) (*types.Response, error)
}
tlsConf := client.tlsConf
- if (tlsConf == nil) {
+ if tlsConf == nil {
tlsConf = &tls.Config{InsecureSkipVerify: true}
}
@@ -102,12 +102,12 @@ func (c Client) Fetch(url string) (*types.Response, error) {
if err != nil {
return nil, err
}
- if ResponseCategoryForStatus(response.Status) != ResponseCategoryRedirect {
+ if !c.IsRedirect(response) {
return response, nil
}
prev := u
- u, err = neturl.Parse(url)
+ u, err = neturl.Parse(response.Meta.(string))
if err != nil {
return nil, err
}
@@ -116,3 +116,7 @@ func (c Client) Fetch(url string) (*types.Response, error) {
return nil, ExceededMaxRedirects
}
+
+func (c Client) IsRedirect(response *types.Response) bool {
+ return ResponseCategoryForStatus(response.Status) == ResponseCategoryRedirect
+}
diff --git a/gemini/response.go b/gemini/response.go
index 13f493a..b3e53aa 100644
--- a/gemini/response.go
+++ b/gemini/response.go
@@ -44,7 +44,7 @@ const (
)
func ResponseCategoryForStatus(status types.Status) ResponseCategory {
- return ResponseCategory(status / 10)
+ return ResponseCategory((status / 10) * 10)
}
const (
diff --git a/gopher/client.go b/gopher/client.go
index 5ef54ff..a5c4228 100644
--- a/gopher/client.go
+++ b/gopher/client.go
@@ -63,3 +63,5 @@ func (c Client) Fetch(url string) (*types.Response, error) {
}
return c.RoundTrip(&types.Request{URL: u})
}
+
+func (c Client) IsRedirect(_ *types.Response) bool { return false }
diff --git a/spartan/client.go b/spartan/client.go
index e3025ee..affcf95 100644
--- a/spartan/client.go
+++ b/spartan/client.go
@@ -100,12 +100,12 @@ func (c Client) Fetch(url string) (*types.Response, error) {
if err != nil {
return nil, err
}
- if response.Status != StatusRedirect {
+ if !c.IsRedirect(response) {
return response, nil
}
prev := u
- u, err = neturl.Parse(url)
+ u, err = neturl.Parse(response.Meta.(string))
if err != nil {
return nil, err
}
@@ -115,6 +115,10 @@ func (c Client) Fetch(url string) (*types.Response, error) {
return nil, ExceededMaxRedirects
}
+func (c Client) IsRedirect(response *types.Response) bool {
+ return response.Status == StatusRedirect
+}
+
type devnull struct{}
func (_ devnull) Read(p []byte) (int, error) {