Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Titan protocol #11

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ API is subject to change until v1.0
* [Custom port](#custom-port)
* [Custom TLS config](#custom-tls-config)
* [Testing](#testing)
* [Titan protocol](#titan-protocol)
* [Who uses Gig](#who-uses-gig)
* [Benchmarks](#benchmarks)
* [Contribute](#contribute)
Expand Down Expand Up @@ -530,6 +531,56 @@ func TestCertificate(t *testing.T) {
}
```

### Titan Protocol

[Titan] protocol is supported as middleware. Example titan server can be found
in `examples/titan` directory.

To use handle titan requests you must set `AllowProxying` property of gig to true.

```go
g := gig.Default()
g.AllowProxying = true
```

It is recommenced to set request read timeout to non zero value:

```go
g.ReadTimeout = time.Second * 10
```

When creating middleware you must provide maximal request size in
bytes. Attempt to send bigger files than the limit will result in bad
request response.

```go
g.Use(gig.Titan(1024))
```

Titan middleware detects requests using `titan://` scheme and stores information about
it in request's context.

| key | type | description |
|-------|--------|-----------------------------------------|
| titan | bool | true if request is titan request |
| size | int | size of received file |
| mime | string | mime type as described in [Titan] spec |
| token | string | auth token as described in [Titan] spec |

The `titan` key in a context can be used to implement handlers that can
handle both titan and gemini requests.

Please note that if client sends less data than indicated by size parameter
then request should result in error response.

There are two utility function provided to make writing Titan servers easier:

- `TitanReadFull`: reads whole request into a buffer. Takes care of case
when client sends less data than expeted.
- `TitanRedirect`: redirects client to the gemini resource uploaded in the request.

[Titan]: https://communitywiki.org/wiki/Titan

## Who uses Gig

Gig is used by the following capsules:
Expand Down
15 changes: 14 additions & 1 deletion context.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ type (
// to a server. Usually the URL() or Path() should be used instead.
RequestURI() string

// Reader returns request connection reader that can be
// used to read data that client sent after Gemini request.
// This feature is necessary to implement Gemini extensions
// like Titan protocol. Spec compliant Gemini server should
// ignore all data after the request.
Reader() io.Reader

// Param returns path parameter by name.
Param(name string) string

Expand Down Expand Up @@ -98,6 +105,7 @@ type (
conn tlsconn
TLS *tls.ConnectionState
u *url.URL
reader io.Reader
response *Response
path string
requestURI string
Expand Down Expand Up @@ -168,6 +176,10 @@ func (c *context) QueryString() (string, error) {
return url.QueryUnescape(c.u.RawQuery)
}

func (c *context) Reader() io.Reader {
return c.reader
}

func (c *context) Get(key string) interface{} {
c.lock.RLock()
defer c.lock.RUnlock()
Expand Down Expand Up @@ -355,12 +367,13 @@ func (c *context) Handler() HandlerFunc {
return c.handler
}

func (c *context) reset(conn tlsconn, u *url.URL, requestURI string, tls *tls.ConnectionState) {
func (c *context) reset(conn tlsconn, u *url.URL, requestURI string, reader io.Reader, tls *tls.ConnectionState) {
c.conn = conn
c.TLS = tls
c.u = u
c.requestURI = requestURI
c.response.reset(conn)
c.reader = reader
c.handler = NotFoundHandler
c.store = nil
c.path = ""
Expand Down
4 changes: 2 additions & 2 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func TestContext(t *testing.T) {

// Reset
c.Set("foe", "ban")
c.(*context).reset(nil, nil, "", nil)
c.(*context).reset(nil, nil, "", nil, nil)
is.Equal(0, len(c.(*context).store))
is.Equal("", c.Path())
}
Expand Down Expand Up @@ -146,7 +146,7 @@ func TestContextGetParam(t *testing.T) {
is.Equal("", c.Param("bar"))

// shouldn't explode during Reset() afterwards!
c.(*context).reset(nil, nil, "", nil)
c.(*context).reset(nil, nil, "", nil, nil)
}

func TestContextFile(t *testing.T) {
Expand Down
7 changes: 7 additions & 0 deletions examples/titan/file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄ ▄▄▄ ▄▄
▀▀▀██▀▀▀ ▀▀██▀▀ ▀▀▀██▀▀▀ ████ ███ ██
██ ██ ██ ████ ██▀█ ██
██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██████ ██ █▄██
██ ▄▄██▄▄ ██ ▄██ ██▄ ██ ███
▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀ ▀▀ ▀▀▀
33 changes: 33 additions & 0 deletions examples/titan/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"os"

"github.com/pitr/gig"
)

func main() {
g := gig.Default()
g.ReadTimeout = time.Second * 10
g.AllowProxying = true
g.Use(gig.Titan(1024))

g.Handle("/file.txt*", func(c gig.Context) error {
if c.Get("titan").(bool) {
data, err := gig.TitanReadFull(c)
if err != nil {
return err
}

err = os.WriteFile("file.txt", data, 0o644)
if err != nil {
return err
}
gig.TitanRedirect(c)
return nil
}
return c.File("file.txt")
})

g.Run("../astro.crt", "../astro.key")
}
9 changes: 6 additions & 3 deletions gig.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ type (
// TLSConfig is passed to tls.NewListener and needs to be modified
// before Run is called.
TLSConfig *tls.Config

// AllowProxying disables error on using other schemas than gemini://
AllowProxying bool
}

// Route contains a handler and information for matching against requests.
Expand Down Expand Up @@ -374,7 +377,7 @@ func (g *Gig) ServeGemini(c Context) {
ctx := g.ctxpool.Get().(*context)
defer g.ctxpool.Put(ctx)

ctx.reset(orig.conn, orig.u, orig.requestURI, orig.TLS)
ctx.reset(orig.conn, orig.u, orig.requestURI, orig.reader, orig.TLS)

c = ctx
}
Expand Down Expand Up @@ -582,7 +585,7 @@ func (g *Gig) handleRequest(conn tlsconn) {
URL.Scheme = "gemini"
}

if URL.Scheme != "gemini" {
if !g.AllowProxying && URL.Scheme != "gemini" {
debugPrintf("gemini: non-gemini scheme: %s", header)

_, _ = conn.Write(responseBadSchema)
Expand All @@ -601,7 +604,7 @@ func (g *Gig) handleRequest(conn tlsconn) {

// Acquire context
c := g.ctxpool.Get().(*context)
c.reset(conn, URL, header, &tlsState)
c.reset(conn, URL, header, reader, &tlsState)

g.ServeGemini(c)

Expand Down
32 changes: 28 additions & 4 deletions gigtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gig
import (
"crypto/tls"
"errors"
"io"
"net"
"net/url"
"time"
Expand All @@ -16,6 +17,7 @@ type (
FakeConn struct {
FailAfter int
Written string
Reader io.Reader
}
)

Expand All @@ -25,8 +27,15 @@ func (a *FakeAddr) Network() string { return "tcp" }
// String returns dummy data.
func (a *FakeAddr) String() string { return "192.0.2.1:25" }

// Read always returns success.
func (c *FakeConn) Read(b []byte) (n int, err error) { return len(b), nil }
// Read always returns success if Reader is nil, otherwise Read is delegated
// to the FakeConn reader.
func (c *FakeConn) Read(b []byte) (n int, err error) {
if c.Reader != nil {
return c.Reader.Read(b)
}

return len(b), nil
}

// Write records bytes written and fails after FailAfter bytes.
func (c *FakeConn) Write(b []byte) (n int, err error) {
Expand Down Expand Up @@ -63,14 +72,29 @@ func (c *FakeConn) SetWriteDeadline(t time.Time) error { return nil }
// ConnectionState always returns nil.
func (c *FakeConn) ConnectionState() tls.ConnectionState { return tls.ConnectionState{} }

type FakeOpt func(Context, *FakeConn)

// WithFakeReader adds reader to the context and connection.
func WithFakeReader(r io.Reader) FakeOpt {
return func(c Context, f *FakeConn) {
f.Reader = r
c.(*context).reader = r
}
}

// NewFakeContext returns Context that writes to FakeConn.
func (g *Gig) NewFakeContext(uri string, tlsState *tls.ConnectionState) (Context, *FakeConn) {
func (g *Gig) NewFakeContext(uri string, tlsState *tls.ConnectionState, opts ...FakeOpt) (Context, *FakeConn) {
u, err := url.Parse(uri)
if err != nil {
panic(err)
}

conn := &FakeConn{}
ctx := g.newContext(conn, u, uri, tlsState)

for _, o := range opts {
o(ctx, conn)
}

return g.newContext(conn, u, uri, tlsState), conn
return ctx, conn
}
5 changes: 5 additions & 0 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ func (n *node) findChildByKind(t kind) *node {
// find lookup a handler registered for path. It also parses URL for path
// parameters and load them into context.
func (r *router) find(path string, c Context) {
i := strings.Index(path, ";")
if i != -1 {
path = path[0:i]
}

ctx := c.(*context)
ctx.path = path
cn := r.tree // Current node as root
Expand Down
8 changes: 8 additions & 0 deletions router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,14 @@ func TestRouterParam1466(t *testing.T) {
is.Equal("sharewithme", c.Param("username"))
is.Equal("self", c.Param("type"))

r.find("/users/sharewithme/uploads/self;mime=text/plain;size=24", c)
is.Equal("sharewithme", c.Param("username"))
is.Equal("self", c.Param("type"))

r.find("/users/sharewithme/uploads/self?foo=1", c)
is.Equal("sharewithme", c.Param("username"))
is.Equal("self?foo=1", c.Param("type"))

c = g.newContext(nil, nil, "", nil).(*context)
r.find("/users/ajitem/uploads/self", c)
is.Equal("ajitem", c.Param("username"))
Expand Down
1 change: 1 addition & 0 deletions serve_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:build !race
// +build !race

package gig
Expand Down
Loading