name change gus -> sliderule

This commit is contained in:
tjpcc 2023-05-01 07:56:25 -06:00
parent 21e2758145
commit 9a2da81b11
74 changed files with 483 additions and 467 deletions

View File

@ -1,12 +1,12 @@
# Gus: The small web toolkit for Go
# sliderule: The small web toolkit for Go
Gus is the toolkit for working with the small web in Go.
Sliderule is the toolkit for working with the small web in Go.
Think of it as a net/http for small web protocols. You still have to write your server, but you can focus on the logic you want to implement knowing the protocol is already dealt with. It's been said of gemini that you can write your server in a day. Now you can write it in well under an hour.
## The "gus" package
## The "sliderule" package
Gus is carefully structured as composable building blocks. The top-level package defines the framework in which servers and clients can be built.
Sliderule is carefully structured as composable building blocks. The top-level package defines the framework in which servers and clients can be built.
* a request type
* a response type
@ -17,7 +17,7 @@ Gus is carefully structured as composable building blocks. The top-level package
## Protocols
The packages gus/gemini, gus/gopher, and gus/finger provide concrete implementations of gus abstractions specific to those protocols.
The packages sliderule/gemini, sliderule/gopher, and sliderule/finger provide concrete implementations specific to those protocols.
* I/O (parsing, formatting) request and responses
* constructors for the various kinds of protocol responses
* helpers for building a protocol-suitable TLS config
@ -25,23 +25,23 @@ The packages gus/gemini, gus/gopher, and gus/finger provide concrete implementat
* Servers which can run your Handlers.
The primary text formats for those protocols have higher-level support provided in sub-packages:
* gus/gemini/gemtext supports parsing gemtext and getting direct programmatic access to its AST. Deeper sub-packages provide converters to other formats (markdown and HTML) with overridable templates.
* gus/gopher/gophermap similarly parses the gophermap format and provides access to its AST.
* sliderule/gemini/gemtext supports parsing gemtext and getting direct programmatic access to its AST. Deeper sub-packages provide converters to other formats (markdown and HTML) with overridable templates.
* sliderule/gopher/gophermap similarly parses the gophermap format and provides access to its AST.
## Logging
Gus borrows the logging interface from go-kit.
Sliderule borrows the logging interface from go-kit.
=> https://pkg.go.dev/github.com/go-kit/log#Logger The logger interface from go-kit/log.
The gus/logging package provides everything you need to get a good basic start to producing helpful logs.
The sliderule/logging package provides everything you need to get a good basic start to producing helpful logs.
* A request-logging middleware with common diagnostics (time, duration, url, status codes, response body lengths)
* A simple constructor of useful default loggers at various levels. They output colorful logfmt lines to stdout.
## Routing
The router in the gus package supports slash-delimited path pattern strings. In the segments of these patterns:
The router in the sliderule package supports slash-delimited path pattern strings. In the segments of these patterns:
* A "/:wildcard/" segment matches anything in that position, and captures the value as a route parameter. Or if the paramter name is omitted like "/:/", it matches anything in a single segment without capturing a paramter.
* A "/*remainder" segment is only allowed at the end and matches the rest of the path, capturing it into the paramter name. Or again, omitting a parameter name like "/*" simple matches any path suffix.
@ -49,7 +49,7 @@ The router in the gus package supports slash-delimited path pattern strings. In
Router also supports maintaining a list of middlewares at the router level, mounting sub-routers under a pattern, looking up the matching handler for any request, and of course acting as a Handler itself.
## gus/contrib/*
## sliderule/contrib/*
This is where useful building blocks themselves start to come in. Sub-packages of contrib include Handler and Middleware implementations which accomplish the things your servers actually need to do.
@ -62,29 +62,29 @@ The sub-packages include:
## Get it
### Using gus in your project
### Using sliderule in your project
To add it to your own go project:
```shell command to add a dependency on gus in a go module
$ go get tildegit.org/tjp/gus
```shell command to add a dependency on sliderule in a go module
$ go get tildegit.org/tjp/sliderule
```
### Straight to the code please
=> https://tildegit.org/tjp/gus The code is hosted here on tildegit.
=> https://pkg.go.dev/tildegit.org/tjp/gus The generated documentation is on the go package index.
=> https://tildegit.org/tjp/sliderule The code is hosted here on tildegit.
=> https://pkg.go.dev/tildegit.org/tjp/sliderule The generated documentation is on the go package index.
### Verify releases
Since v0.9.0, releases are signed with minisign. The signature file is included in the release downloads page, and the public key is RWSzQywJwHgjSMD0y0RXwXAGpapcMJplwbCVYQqabhAJ+NAnKAeh98Vb - this is also referenced on tjp's home page on gemini.
=> gemini://gemini.ctrl-c.club/~tjp TJP's home page, which also mentions the public key used for signing gus releases.
=> gemini://gemini.ctrl-c.club/~tjp TJP's home page, which also mentions the public key used for signing sliderule releases.
## Contribute
There's lots still to do, and contributions are very welcome!
=> https://tildegit.org/tjp/gus submit an issue or pull request on the tildegit repository,
=> https://tildegit.org/tjp/sliderule submit an issue or pull request on the tildegit repository,
=> mailto:tjp@ctrl-c.club send me an email directly,
or poke me on IRC: I'm @tjp on irc.tilde.chat where you'll find me in #gemini

View File

@ -1,13 +1,13 @@
# Gus: The small web toolkit for Go
# sliderule: The small web toolkit for Go
Gus is the toolkit for working with the small web in Go.
Sliderule is the toolkit for working with the small web in Go.
Think of it as a net/http for small web protocols. You still have to write your server, but you can focus on the logic you want to implement knowing the protocol is already dealt with. It's been said of gemini that you can write your server in a day. Now you can write it in well under an hour.
## The "gus" package
## The "sliderule" package
Gus is carefully structured as composable building blocks. The top-level package defines the framework in which servers and clients can be built.
Sliderule is carefully structured as composable building blocks. The top-level package defines the framework in which servers and clients can be built.
* a request type
* a response type
@ -18,7 +18,7 @@ Gus is carefully structured as composable building blocks. The top-level package
## Protocols
The packages gus/gemini, gus/gopher, and gus/finger provide concrete implementations of gus abstractions specific to those protocols.
The packages sliderule/gemini, sliderule/gopher, and sliderule/finger provide concrete implementations specific to those protocols.
* I/O (parsing, formatting) request and responses
* constructors for the various kinds of protocol responses
@ -28,23 +28,23 @@ The packages gus/gemini, gus/gopher, and gus/finger provide concrete implementat
The primary text formats for those protocols have higher-level support provided in sub-packages:
* gus/gemini/gemtext supports parsing gemtext and getting direct programmatic access to its AST. Deeper sub-packages provide converters to other formats (markdown and HTML) with overridable templates.
* gus/gopher/gophermap similarly parses the gophermap format and provides access to its AST.
* sliderule/gemini/gemtext supports parsing gemtext and getting direct programmatic access to its AST. Deeper sub-packages provide converters to other formats (markdown and HTML) with overridable templates.
* sliderule/gopher/gophermap similarly parses the gophermap format and provides access to its AST.
## Logging
Gus borrows the logging interface from go-kit.
Sliderule borrows the logging interface from go-kit.
=> [The logger interface from go-kit/log.](https://pkg.go.dev/github.com/go-kit/log#Logger)
The gus/logging package provides everything you need to get a good basic start to producing helpful logs.
The sliderule/logging package provides everything you need to get a good basic start to producing helpful logs.
* A request-logging middleware with common diagnostics (time, duration, url, status codes, response body lengths)
* A simple constructor of useful default loggers at various levels. They output colorful logfmt lines to stdout.
## Routing
The router in the gus package supports slash-delimited path pattern strings. In the segments of these patterns:
The router in the sliderule package supports slash-delimited path pattern strings. In the segments of these patterns:
* A "/:wildcard/" segment matches anything in that position, and captures the value as a route parameter. Or if the paramter name is omitted like "/:/", it matches anything in a single segment without capturing a paramter.
* A "/*remainder" segment is only allowed at the end and matches the rest of the path, capturing it into the paramter name. Or again, omitting a parameter name like "/*" simple matches any path suffix.
@ -52,7 +52,7 @@ The router in the gus package supports slash-delimited path pattern strings. In
Router also supports maintaining a list of middlewares at the router level, mounting sub-routers under a pattern, looking up the matching handler for any request, and of course acting as a Handler itself.
## gus/contrib/*
## sliderule/contrib/*
This is where useful building blocks themselves start to come in. Sub-packages of contrib include Handler and Middleware implementations which accomplish the things your servers actually need to do.
@ -66,31 +66,31 @@ The sub-packages include:
## Get it
### Using gus in your project
### Using sliderule in your project
To add it to your own go project:
```
$ go get tildegit.org/tjp/gus
$ go get tildegit.org/tjp/sliderule
```
### Straight to the code please
=> [The code is hosted here on tildegit.](https://tildegit.org/tjp/gus)
=> [The code is hosted here on tildegit.](https://tildegit.org/tjp/sliderule)
=> [The generated documentation is on the go package index.](https://pkg.go.dev/tildegit.org/tjp/gus)
=> [The generated documentation is on the go package index.](https://pkg.go.dev/tildegit.org/tjp/sliderule)
### Verify releases
Since v0.9.0, releases are signed with minisign. The signature file is included in the release downloads page, and the public key is RWSzQywJwHgjSMD0y0RXwXAGpapcMJplwbCVYQqabhAJ+NAnKAeh98Vb - this is also referenced on tjp's home page on gemini.
=> [TJP's home page, which also mentions the public key used for signing gus releases.](gemini://gemini.ctrl-c.club/~tjp)
=> [TJP's home page, which also mentions the public key used for signing sliderule releases.](gemini://gemini.ctrl-c.club/~tjp)
## Contribute
There's lots still to do, and contributions are very welcome!
=> [submit an issue or pull request on the tildegit repository,](https://tildegit.org/tjp/gus)
=> [submit an issue or pull request on the tildegit repository,](https://tildegit.org/tjp/sliderule)
=> [send me an email directly,](mailto:tjp@ctrl-c.club)

View File

@ -13,7 +13,7 @@ import (
"os/exec"
"strings"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
// ResolveCGI finds a CGI program corresponding to a request path.
@ -91,7 +91,7 @@ func isNotExistError(err error) bool {
// RunCGI runs a specific program as a CGI script.
func RunCGI(
ctx context.Context,
request *gus.Request,
request *sr.Request,
executable string,
pathInfo string,
) (io.Reader, int, error) {
@ -130,7 +130,7 @@ func RunCGI(
func prepareCGIEnv(
ctx context.Context,
request *gus.Request,
request *sr.Request,
scriptName string,
pathInfo string,
) []string {

View File

@ -12,9 +12,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/contrib/cgi"
"tildegit.org/tjp/gus/gemini"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/contrib/cgi"
"tildegit.org/tjp/sliderule/gemini"
)
func TestCGIDirectory(t *testing.T) {
@ -30,7 +30,7 @@ func TestCGIDirectory(t *testing.T) {
tests := []struct {
requestPath string
responseCode gus.Status
responseCode sr.Status
responseBody string
}{
{
@ -69,7 +69,7 @@ func TestCGIDirectory(t *testing.T) {
code, err := strconv.Atoi(string(response[:2]))
if assert.Nil(t, err) {
assert.Equal(t, test.responseCode, gus.Status(code))
assert.Equal(t, test.responseCode, sr.Status(code))
}
_, body, found := strings.Cut(string(response), "\r\n")

View File

@ -5,8 +5,8 @@ import (
"fmt"
"strings"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/gemini"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/gemini"
)
// GeminiCGIDirectory runs any executable files relative to a root directory on the file system.
@ -15,9 +15,9 @@ import (
// a request for /foo/bar/baz can also run an executable found at /foo or /foo/bar. In
// such a case the PATH_INFO environment variable will include the remaining portion of
// the URI path.
func GeminiCGIDirectory(pathRoot, fsRoot string) gus.Handler {
func GeminiCGIDirectory(pathRoot, fsRoot string) sr.Handler {
fsRoot = strings.TrimRight(fsRoot, "/")
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
if !strings.HasPrefix(request.Path, pathRoot) {
return nil
}

View File

@ -5,8 +5,8 @@ import (
"fmt"
"strings"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/gopher"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/gopher"
)
// GopherCGIDirectory runs any executable files relative to a root directory on the file system.
@ -15,9 +15,9 @@ import (
// a request for /foo/bar/baz can also run an executable found at /foo or /foo/bar. In
// such a case the PATH_INFO environment variable will include the remaining portion of
// the URI path.
func GopherCGIDirectory(pathRoot, fsRoot string) gus.Handler {
func GopherCGIDirectory(pathRoot, fsRoot string) sr.Handler {
fsRoot = strings.TrimRight(fsRoot, "/")
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
if !strings.HasPrefix(request.Path, pathRoot) {
return nil
}

View File

@ -5,8 +5,8 @@ import (
"fmt"
"strings"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/spartan"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/spartan"
)
// SpartanCGIDirectory runs executable files relative to a root directory in the file system.
@ -14,9 +14,9 @@ import (
// It will also find any run any executable _part way_ through the path, so for example a
// request for /foo/bar/baz can also run an executable found at /foo or /foo/bar. In such
// a case the PATH_INFO environment variable will include the remaining portion of the URI.
func SpartanCGIDirectory(pathRoot, fsRoot string) gus.Handler {
func SpartanCGIDirectory(pathRoot, fsRoot string) sr.Handler {
fsRoot = strings.TrimRight(fsRoot, "/")
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
if !strings.HasPrefix(request.Path, pathRoot) {
return nil
}

View File

@ -8,7 +8,7 @@ import (
"strings"
"text/template"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
// ResolveDirectory opens the directory corresponding to a request path.
@ -16,7 +16,7 @@ import (
// The string is the full path to the directory. If the returned ReadDirFile
// is not nil, it will be open and must be closed by the caller.
func ResolveDirectory(
request *gus.Request,
request *sr.Request,
fileSystem fs.FS,
) (string, fs.ReadDirFile, error) {
path := strings.Trim(request.Path, "/")
@ -102,7 +102,7 @@ func RenderDirectoryListing(
path string,
dir fs.ReadDirFile,
template *template.Template,
server gus.Server,
server sr.Server,
) (io.Reader, error) {
buf := &bytes.Buffer{}
@ -118,7 +118,7 @@ func RenderDirectoryListing(
return buf, nil
}
func dirlistNamespace(path string, dirFile fs.ReadDirFile, server gus.Server) (map[string]any, error) {
func dirlistNamespace(path string, dirFile fs.ReadDirFile, server sr.Server) (map[string]any, error) {
entries, err := dirFile.ReadDir(0)
if err != nil {
return nil, err

View File

@ -10,9 +10,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/contrib/fs"
"tildegit.org/tjp/gus/gemini"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/contrib/fs"
"tildegit.org/tjp/sliderule/gemini"
)
func TestDirectoryDefault(t *testing.T) {
@ -20,7 +20,7 @@ func TestDirectoryDefault(t *testing.T) {
tests := []struct {
url string
status gus.Status
status sr.Status
meta string
body string
}{
@ -46,7 +46,7 @@ func TestDirectoryDefault(t *testing.T) {
u, err := url.Parse(test.url)
require.Nil(t, err)
request := &gus.Request{URL: u}
request := &sr.Request{URL: u}
response := handler.Handle(context.Background(), request)
if response == nil {
@ -73,7 +73,7 @@ func TestDirectoryListing(t *testing.T) {
tests := []struct {
url string
status gus.Status
status sr.Status
meta string
body string
}{
@ -107,7 +107,7 @@ func TestDirectoryListing(t *testing.T) {
u, err := url.Parse(test.url)
require.Nil(t, err)
request := &gus.Request{URL: u}
request := &sr.Request{URL: u}
response := handler.Handle(context.Background(), request)
if response == nil {

View File

@ -5,7 +5,7 @@ import (
"mime"
"strings"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
// ResolveFile finds a file from a filesystem based on a request path.
@ -13,7 +13,7 @@ import (
// It only returns a non-nil file if a file is found - not a directory.
// If there is any other sort of filesystem access error, it will be
// returned.
func ResolveFile(request *gus.Request, fileSystem fs.FS) (string, fs.File, error) {
func ResolveFile(request *sr.Request, fileSystem fs.FS) (string, fs.File, error) {
filepath := strings.TrimPrefix(request.Path, "/")
file, err := fileSystem.Open(filepath)
if isNotFound(err) {

View File

@ -10,9 +10,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/contrib/fs"
"tildegit.org/tjp/gus/gemini"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/contrib/fs"
"tildegit.org/tjp/sliderule/gemini"
)
func TestFileHandler(t *testing.T) {
@ -20,7 +20,7 @@ func TestFileHandler(t *testing.T) {
tests := []struct {
url string
status gus.Status
status sr.Status
meta string
body string
}{
@ -57,7 +57,7 @@ func TestFileHandler(t *testing.T) {
u, err := url.Parse(test.url)
require.Nil(t, err)
request := &gus.Request{URL: u}
request := &sr.Request{URL: u}
response := handler.Handle(context.Background(), request)
if response == nil {

View File

@ -6,15 +6,15 @@ import (
"strings"
"text/template"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/gemini"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/gemini"
)
// GeminiFileHandler builds a handler which serves up files from a file system.
//
// It only serves responses for paths which do not correspond to directories on disk.
func GeminiFileHandler(fileSystem fs.FS) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
func GeminiFileHandler(fileSystem fs.FS) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
filepath, file, err := ResolveFile(request, fileSystem)
if err != nil {
return gemini.Failure(err)
@ -41,8 +41,8 @@ func GeminiFileHandler(fileSystem fs.FS) gus.Handler {
//
// It requires that files from the provided fs.FS implement fs.ReadDirFile. If they
// don't, it will produce nil responses for any directory paths.
func GeminiDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
func GeminiDirectoryDefault(fileSystem fs.FS, filenames ...string) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
dirpath, dir, response := handleDirGemini(request, fileSystem)
if response != nil {
return response
@ -77,8 +77,8 @@ func GeminiDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler {
//
// The template may be nil, in which case DefaultGeminiDirectoryList is used instead. The
// template is then processed with RenderDirectoryListing.
func GeminiDirectoryListing(fileSystem fs.FS, template *template.Template) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
func GeminiDirectoryListing(fileSystem fs.FS, template *template.Template) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
dirpath, dir, response := handleDirGemini(request, fileSystem)
if response != nil {
return response
@ -109,7 +109,7 @@ var DefaultGeminiDirectoryList = template.Must(template.New("gemini_dirlist").Pa
=> ../
`[1:]))
func handleDirGemini(request *gus.Request, fileSystem fs.FS) (string, fs.ReadDirFile, *gus.Response) {
func handleDirGemini(request *sr.Request, fileSystem fs.FS) (string, fs.ReadDirFile, *sr.Response) {
path, dir, err := ResolveDirectory(request, fileSystem)
if err != nil {
return "", nil, gemini.Failure(err)

View File

@ -8,15 +8,15 @@ import (
"strings"
"text/template"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/gopher"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/gopher"
)
// GopherFileHandler builds a handler which serves up files from a file system.
//
// It only serves responses for paths which correspond to files, not directories.
func GopherFileHandler(fileSystem fs.FS) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
func GopherFileHandler(fileSystem fs.FS) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
filepath, file, err := ResolveFile(request, fileSystem)
if err != nil {
return gopher.Error(err).Response()
@ -39,8 +39,8 @@ func GopherFileHandler(fileSystem fs.FS) gus.Handler {
//
// It requires that files from the provided fs.FS implement fs.ReadDirFile. If
// they don't, it will produce nil responses for all directory paths.
func GopherDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
func GopherDirectoryDefault(fileSystem fs.FS, filenames ...string) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
dirpath, dir, err := ResolveDirectory(request, fileSystem)
if err != nil {
return gopher.Error(err).Response()
@ -71,8 +71,8 @@ func GopherDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler {
//
// A template may be nil, in which case DefaultGopherDirectoryList is used instead. The
// template is then processed with RenderDirectoryListing.
func GopherDirectoryListing(fileSystem fs.FS, tpl *template.Template) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
func GopherDirectoryListing(fileSystem fs.FS, tpl *template.Template) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
dirpath, dir, err := ResolveDirectory(request, fileSystem)
if err != nil {
return gopher.Error(err).Response()
@ -130,7 +130,7 @@ i {{ $hostname }} {{ $port }}
)
// GuessGopherItemType attempts to find the best gopher item type for a file based on its name.
func GuessGopherItemType(filepath string) gus.Status {
func GuessGopherItemType(filepath string) sr.Status {
ext := path.Ext(filepath)
switch ext {
case "txt", "gmi":

View File

@ -6,15 +6,15 @@ import (
"strings"
"text/template"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/spartan"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/spartan"
)
// SpartanFileHandler builds a handler which serves up files from a filesystem.
//
// It only serves responses for paths which do not correspond to directories on disk.
func SpartanFileHandler(fileSystem fs.FS) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
func SpartanFileHandler(fileSystem fs.FS) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
filepath, file, err := ResolveFile(request, fileSystem)
if err != nil {
return spartan.ClientError(err)
@ -41,8 +41,8 @@ func SpartanFileHandler(fileSystem fs.FS) gus.Handler {
//
// It requires that files from the provided fs.FS implement fs.ReadDirFile. If they
// don't, it will produce nil responses for any directory paths.
func SpartanDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
func SpartanDirectoryDefault(fileSystem fs.FS, filenames ...string) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
dirpath, dir, response := handleDirSpartan(request, fileSystem)
if response != nil {
return response
@ -77,8 +77,8 @@ func SpartanDirectoryDefault(fileSystem fs.FS, filenames ...string) gus.Handler
//
// The tmeplate may be nil, in which cause DefaultSpartanDirectoryList is used instead. The
// template is then processed with RenderDirectoryListing.
func SpartanDirectoryListing(filesystem fs.FS, template *template.Template) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
func SpartanDirectoryListing(filesystem fs.FS, template *template.Template) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
dirpath, dir, response := handleDirSpartan(request, filesystem)
if response != nil {
return response
@ -103,7 +103,7 @@ func SpartanDirectoryListing(filesystem fs.FS, template *template.Template) gus.
// DefaultSpartanDirectoryList is a template which renders a reasonable gemtext dir listing.
var DefaultSpartanDirectoryList = DefaultGeminiDirectoryList
func handleDirSpartan(request *gus.Request, filesystem fs.FS) (string, fs.ReadDirFile, *gus.Response) {
func handleDirSpartan(request *sr.Request, filesystem fs.FS) (string, fs.ReadDirFile, *sr.Response) {
path, dir, err := ResolveDirectory(request, filesystem)
if err != nil {
return "", nil, spartan.ServerError(err)

View File

@ -5,7 +5,7 @@ import (
"crypto/tls"
"net/url"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
// ReplaceTilde builds a middleware which substitutes a leading '~' in the request path.
@ -17,9 +17,9 @@ import (
// Typically the replacement should end with a "/", so that the ~ ends up mapping to a
// particular directory on the filesystem. For instance with a replacement string of
// "users/", "domain.com/~jim/index.gmi" maps to "domain.com/users/jim/index.gmi".
func ReplaceTilde(replacement string) gus.Middleware {
return func(inner gus.Handler) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
func ReplaceTilde(replacement string) sr.Middleware {
return func(inner sr.Handler) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
if len(request.Path) > 1 && request.Path[0] == '/' && request.Path[1] == '~' {
request = cloneRequest(request)
request.Path = "/" + replacement + request.Path[2:]
@ -30,8 +30,8 @@ func ReplaceTilde(replacement string) gus.Middleware {
}
}
func cloneRequest(start *gus.Request) *gus.Request {
next := &gus.Request{}
func cloneRequest(start *sr.Request) *sr.Request {
next := &sr.Request{}
*next = *start
next.URL = &url.URL{}

View File

@ -7,8 +7,8 @@ import (
"github.com/stretchr/testify/assert"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/contrib/sharedhost"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/contrib/sharedhost"
)
func TestReplaceTilde(t *testing.T) {
@ -42,10 +42,10 @@ func TestReplaceTilde(t *testing.T) {
originalPath := u.Path
replacer := sharedhost.ReplaceTilde(test.replacement)
request := &gus.Request{URL: u}
handler := replacer(gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
request := &sr.Request{URL: u}
handler := replacer(sr.HandlerFunc(func(_ context.Context, request *sr.Request) *sr.Response {
assert.Equal(t, test.replacedPath, request.Path)
return &gus.Response{}
return &sr.Response{}
}))
handler.Handle(context.Background(), request)

View File

@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
"tildegit.org/tjp/gus/contrib/tlsauth"
"tildegit.org/tjp/sliderule/contrib/tlsauth"
)
func TestRequireSpecificIdentity(t *testing.T) {

View File

@ -4,11 +4,11 @@ import (
"context"
"crypto/x509"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
// Identity returns the client certificate for the request or nil if there is none.
func Identity(request *gus.Request) *x509.Certificate {
func Identity(request *sr.Request) *x509.Certificate {
if request.TLSState == nil || len(request.TLSState.PeerCertificates) == 0 {
return nil
}
@ -19,8 +19,8 @@ func Identity(request *gus.Request) *x509.Certificate {
//
// The check requires both that there is a client certificate associated with the
// request and that it passes the provided approver.
func RequiredAuth(approve Approver) func(context.Context, *gus.Request) bool {
return func(_ context.Context, request *gus.Request) bool {
func RequiredAuth(approve Approver) func(context.Context, *sr.Request) bool {
return func(_ context.Context, request *sr.Request) bool {
identity := Identity(request)
if identity == nil {
return false
@ -34,8 +34,8 @@ func RequiredAuth(approve Approver) func(context.Context, *gus.Request) bool {
//
// The check allows through any request with no client certificate, but if
// there is one present then it requires that it pass the provided approver.
func OptionalAuth(approve Approver) func(context.Context, *gus.Request) bool {
return func(_ context.Context, request *gus.Request) bool {
func OptionalAuth(approve Approver) func(context.Context, *sr.Request) bool {
return func(_ context.Context, request *sr.Request) bool {
identity := Identity(request)
if identity == nil {
return true

View File

@ -12,9 +12,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/contrib/tlsauth"
"tildegit.org/tjp/gus/gemini"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/contrib/tlsauth"
"tildegit.org/tjp/sliderule/gemini"
)
func TestIdentify(t *testing.T) {
@ -24,7 +24,7 @@ func TestIdentify(t *testing.T) {
server, client, clientCert := setup(t,
"testdata/server.crt", "testdata/server.key",
"testdata/client1.crt", "testdata/client1.key",
gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
sr.HandlerFunc(func(_ context.Context, request *sr.Request) *sr.Response {
invoked = true
ident := tlsauth.Identity(request)
@ -51,20 +51,20 @@ func TestRequiredAuth(t *testing.T) {
invoked1 := false
invoked2 := false
handler1 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
handler1 := sr.HandlerFunc(func(_ context.Context, request *sr.Request) *sr.Response {
invoked1 = true
return gemini.Success("", &bytes.Buffer{})
})
handler2 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
handler2 := sr.HandlerFunc(func(_ context.Context, request *sr.Request) *sr.Response {
invoked2 = true
return gemini.Success("", &bytes.Buffer{})
})
authMiddleware := gus.Filter(tlsauth.RequiredAuth(tlsauth.Allow), nil)
authMiddleware := sr.Filter(tlsauth.RequiredAuth(tlsauth.Allow), nil)
handler1 = gus.Filter(
func(_ context.Context, req *gus.Request) bool {
handler1 = sr.Filter(
func(_ context.Context, req *sr.Request) bool {
return strings.HasPrefix(req.Path, "/one")
},
nil,
@ -74,7 +74,7 @@ func TestRequiredAuth(t *testing.T) {
server, client, _ := setup(t,
"testdata/server.crt", "testdata/server.key",
"testdata/client1.crt", "testdata/client1.key",
gus.FallthroughHandler(handler1, handler2),
sr.FallthroughHandler(handler1, handler2),
)
go func() {
@ -94,7 +94,7 @@ func TestOptionalAuth(t *testing.T) {
invoked1 := false
invoked2 := false
handler1 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
handler1 := sr.HandlerFunc(func(_ context.Context, request *sr.Request) *sr.Response {
if !strings.HasPrefix(request.Path, "/one") {
return nil
}
@ -103,13 +103,13 @@ func TestOptionalAuth(t *testing.T) {
return gemini.Success("", &bytes.Buffer{})
})
handler2 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
handler2 := sr.HandlerFunc(func(_ context.Context, request *sr.Request) *sr.Response {
invoked2 = true
return gemini.Success("", &bytes.Buffer{})
})
mw := gus.Filter(tlsauth.OptionalAuth(tlsauth.Reject), nil)
handler := gus.FallthroughHandler(mw(handler1), mw(handler2))
mw := sr.Filter(tlsauth.OptionalAuth(tlsauth.Reject), nil)
handler := sr.FallthroughHandler(mw(handler1), mw(handler2))
server, client, _ := setup(t,
"testdata/server.crt", "testdata/server.key",
@ -136,8 +136,8 @@ func setup(
serverKeyPath string,
clientCertPath string,
clientKeyPath string,
handler gus.Handler,
) (gus.Server, gemini.Client, tls.Certificate) {
handler sr.Handler,
) (sr.Server, gemini.Client, tls.Certificate) {
serverTLS, err := gemini.FileTLS(serverCertPath, serverKeyPath)
require.Nil(t, err)
@ -159,7 +159,7 @@ func setup(
func clientFor(
t *testing.T,
server gus.Server,
server sr.Server,
certPath string,
keyPath string,
) (gemini.Client, tls.Certificate) {
@ -179,11 +179,11 @@ func clientFor(
}), clientCert
}
func requestPath(t *testing.T, client gemini.Client, server gus.Server, path string) *gus.Response {
func requestPath(t *testing.T, client gemini.Client, server sr.Server, path string) *sr.Response {
u, err := url.Parse("gemini://" + server.Address() + path)
require.Nil(t, err)
response, err := client.RoundTrip(&gus.Request{URL: u})
response, err := client.RoundTrip(&sr.Request{URL: u})
require.Nil(t, err)
return response

View File

@ -3,8 +3,8 @@ package tlsauth
import (
"context"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/gemini"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/gemini"
)
// GeminiAuth builds an authentication middleware from approval criteria.
@ -12,9 +12,9 @@ import (
// If a request does not contain a client certificate it will be rejected
// with a "60 certificate required" response. If the client identity does
// not pass the approver it will be rejected with "62 certificate invalid".
func GeminiAuth(approver Approver) gus.Middleware {
return func(inner gus.Handler) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
func GeminiAuth(approver Approver) sr.Middleware {
return func(inner sr.Handler) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
identity := Identity(request)
if identity == nil {
return geminiMissingCert(ctx, request)
@ -33,9 +33,9 @@ func GeminiAuth(approver Approver) gus.Middleware {
// If there is no client certificate the request will pass through the middleware.
// It will only be rejected with "62 certificate invalid" if there *is* a client
// certificate, but it fails the approval.
func GeminiOptionalAuth(approver Approver) gus.Middleware {
return func(inner gus.Handler) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
func GeminiOptionalAuth(approver Approver) sr.Middleware {
return func(inner sr.Handler) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
identity := Identity(request)
if identity != nil && !approver(identity) {
return geminiCertNotAuthorized(ctx, request)
@ -49,10 +49,10 @@ func GeminiOptionalAuth(approver Approver) gus.Middleware {
// GeminiRequireCertificate is a middleware that only requires a client certificate.
var GeminiRequireCertificate = GeminiAuth(Allow)
func geminiMissingCert(_ context.Context, _ *gus.Request) *gus.Response {
func geminiMissingCert(_ context.Context, _ *sr.Request) *sr.Response {
return gemini.RequireCert("A client certificate is required.")
}
func geminiCertNotAuthorized(_ context.Context, _ *gus.Request) *gus.Response {
func geminiCertNotAuthorized(_ context.Context, _ *sr.Request) *sr.Response {
return gemini.CertAuthFailure("Client certificate not authorized.")
}

View File

@ -8,38 +8,38 @@ import (
"github.com/stretchr/testify/assert"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/contrib/tlsauth"
"tildegit.org/tjp/gus/gemini"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/contrib/tlsauth"
"tildegit.org/tjp/sliderule/gemini"
)
func TestGeminiAuth(t *testing.T) {
handler1 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
handler1 := sr.HandlerFunc(func(_ context.Context, request *sr.Request) *sr.Response {
if !strings.HasPrefix(request.Path, "/one") {
return nil
}
return gemini.Success("", &bytes.Buffer{})
})
handler2 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
handler2 := sr.HandlerFunc(func(_ context.Context, request *sr.Request) *sr.Response {
if !strings.HasPrefix(request.Path, "/two") {
return nil
}
return gemini.Success("", &bytes.Buffer{})
})
handler3 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
handler3 := sr.HandlerFunc(func(_ context.Context, request *sr.Request) *sr.Response {
if !strings.HasPrefix(request.Path, "/three") {
return nil
}
return gemini.Success("", &bytes.Buffer{})
})
handler4 := gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
handler4 := sr.HandlerFunc(func(_ context.Context, request *sr.Request) *sr.Response {
return gemini.Success("", &bytes.Buffer{})
})
handler := gus.FallthroughHandler(
handler := sr.FallthroughHandler(
tlsauth.GeminiAuth(tlsauth.Allow)(handler1),
tlsauth.GeminiAuth(tlsauth.Allow)(handler2),
tlsauth.GeminiAuth(tlsauth.Reject)(handler3),
@ -73,8 +73,8 @@ func TestGeminiAuth(t *testing.T) {
}
func TestGeminiOptionalAuth(t *testing.T) {
pathHandler := func(path string) gus.Handler {
return gus.HandlerFunc(func(_ context.Context, request *gus.Request) *gus.Response {
pathHandler := func(path string) sr.Handler {
return sr.HandlerFunc(func(_ context.Context, request *sr.Request) *sr.Response {
if !strings.HasPrefix(request.Path, path) {
return nil
}
@ -82,7 +82,7 @@ func TestGeminiOptionalAuth(t *testing.T) {
})
}
handler := gus.FallthroughHandler(
handler := sr.FallthroughHandler(
tlsauth.GeminiOptionalAuth(tlsauth.Allow)(pathHandler("/one")),
tlsauth.GeminiOptionalAuth(tlsauth.Allow)(pathHandler("/two")),
tlsauth.GeminiOptionalAuth(tlsauth.Reject)(pathHandler("/three")),

View File

@ -7,9 +7,9 @@ import (
"os/signal"
"syscall"
"tildegit.org/tjp/gus/contrib/cgi"
"tildegit.org/tjp/gus/gemini"
"tildegit.org/tjp/gus/logging"
"tildegit.org/tjp/sliderule/contrib/cgi"
"tildegit.org/tjp/sliderule/gemini"
"tildegit.org/tjp/sliderule/logging"
)
func main() {

View File

@ -8,9 +8,9 @@ import (
"os"
"os/exec"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/gemini"
"tildegit.org/tjp/gus/logging"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/gemini"
"tildegit.org/tjp/sliderule/logging"
)
func main() {
@ -36,7 +36,7 @@ func main() {
server.Serve()
}
var cowsayHandler = gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response {
var cowsayHandler = sr.HandlerFunc(func(ctx context.Context, req *sr.Request) *sr.Response {
// prompt for a query if there is none already
if req.RawQuery == "" {
return gemini.Input("enter a phrase")

View File

@ -8,8 +8,8 @@ import (
"os"
"strings"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/gemini"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/gemini"
)
func main() {
@ -30,7 +30,7 @@ func main() {
}
// parse the URL and build the request
request := &gus.Request{URL: buildURL()}
request := &sr.Request{URL: buildURL()}
// fetch the response
response, err := client.RoundTrip(request)

View File

@ -5,10 +5,10 @@ import (
"log"
"os"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/contrib/fs"
"tildegit.org/tjp/gus/gemini"
"tildegit.org/tjp/gus/logging"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/contrib/fs"
"tildegit.org/tjp/sliderule/gemini"
"tildegit.org/tjp/sliderule/logging"
)
func main() {
@ -24,7 +24,7 @@ func main() {
// build the request handler
fileSystem := os.DirFS(".")
// Fallthrough tries each handler in succession until it gets something other than "51 Not Found"
handler := gus.FallthroughHandler(
handler := sr.FallthroughHandler(
// first see if they're fetching a directory and we have <dir>/index.gmi
fs.GeminiDirectoryDefault(fileSystem, "index.gmi"),
// next (still if they requested a directory) build a directory listing response

View File

@ -4,8 +4,8 @@ import (
"context"
"log"
"tildegit.org/tjp/gus/finger"
"tildegit.org/tjp/gus/logging"
"tildegit.org/tjp/sliderule/finger"
"tildegit.org/tjp/sliderule/logging"
)
func main() {

View File

@ -4,8 +4,8 @@ import (
"log"
"os"
"tildegit.org/tjp/gus/gemini/gemtext"
"tildegit.org/tjp/gus/gemini/gemtext/htmlconv"
"tildegit.org/tjp/sliderule/gemini/gemtext"
"tildegit.org/tjp/sliderule/gemini/gemtext/htmlconv"
)
func main() {

View File

@ -4,8 +4,8 @@ import (
"log"
"os"
"tildegit.org/tjp/gus/gemini/gemtext"
"tildegit.org/tjp/gus/gemini/gemtext/mdconv"
"tildegit.org/tjp/sliderule/gemini/gemtext"
"tildegit.org/tjp/sliderule/gemini/gemtext/mdconv"
)
func main() {

View File

@ -5,17 +5,17 @@ import (
"log"
"os"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/contrib/cgi"
"tildegit.org/tjp/gus/contrib/fs"
"tildegit.org/tjp/gus/gopher"
"tildegit.org/tjp/gus/logging"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/contrib/cgi"
"tildegit.org/tjp/sliderule/contrib/fs"
"tildegit.org/tjp/sliderule/gopher"
"tildegit.org/tjp/sliderule/logging"
)
func main() {
fileSystem := os.DirFS(".")
handler := gus.FallthroughHandler(
handler := sr.FallthroughHandler(
fs.GopherDirectoryDefault(fileSystem, "index.gophermap"),
fs.GopherDirectoryListing(fileSystem, nil),
cgi.GopherCGIDirectory("/cgi-bin", "./cgi-bin"),

View File

@ -12,9 +12,9 @@ import (
"os"
"strings"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/gemini"
"tildegit.org/tjp/gus/logging"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/gemini"
"tildegit.org/tjp/sliderule/logging"
)
func main() {
@ -54,7 +54,7 @@ func envConfig() (string, string) {
return certfile, keyfile
}
var inspectHandler = gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response {
var inspectHandler = sr.HandlerFunc(func(ctx context.Context, req *sr.Request) *sr.Response {
// build and return a ```-wrapped description of the connection TLS state
body := "```\n" + displayTLSState(req.TLSState) + "\n```"
return gemini.Success("text/gemini", bytes.NewBufferString(body))

View File

@ -7,7 +7,7 @@ import (
"net/url"
"strings"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
// ForwardingDenied is returned in response to requests for forwarding service.
@ -16,7 +16,7 @@ var ForwardingDenied = errors.New("Finger forwarding service denied.")
// InvalidFingerQuery is sent when a client doesn't properly format the query.
var InvalidFingerQuery = errors.New("Invalid finger query .")
// ParseRequest builds a gus.Request by reading a finger protocol request.
// ParseRequest builds a sliderule.Request by reading a finger protocol request.
//
// At the time of writing, there is no firm standard on how to represent finger
// queries as URLs (the finger protocol itself predates URLs entirely), but there
@ -26,11 +26,11 @@ var InvalidFingerQuery = errors.New("Invalid finger query .")
// - There is an IETF draft:
// https://datatracker.ietf.org/doc/html/draft-ietf-uri-url-finger
//
// As this function builds a *gus.Request (which is mostly a wrapper around a URL)
// As this function builds a *sliderule.Request (which is mostly a wrapper around a URL)
// from nothing but an io.Reader, it doesn't have the context of the hostname which
// the receiving server was hosting. So it only has the host component if it
// arrived in the body of the query in the form username@hostname. Bear in mind that
// in gus handlers, request objects will also carry a reference to the server so
// in sliderule handlers, request objects will also carry a reference to the server so
// that hostname is always available as request.Server.Hostname().
//
// The primary deviation from the IETF draft is that a query-specified host becomes
@ -47,7 +47,7 @@ var InvalidFingerQuery = errors.New("Invalid finger query .")
// In accordance with the recommendation of RFC 1288 section 3.2.1
// (https://datatracker.ietf.org/doc/html/rfc1288#section-3.2.1), any queries which
// include a jump-host (user@host1@host2) are rejected with the ForwardingDenied error.
func ParseRequest(rdr io.Reader) (*gus.Request, error) {
func ParseRequest(rdr io.Reader) (*sr.Request, error) {
line, err := bufio.NewReader(rdr).ReadString('\n')
if err != nil {
return nil, err
@ -66,7 +66,7 @@ func ParseRequest(rdr io.Reader) (*gus.Request, error) {
return nil, ForwardingDenied
}
return &gus.Request{URL: &url.URL{
return &sr.Request{URL: &url.URL{
Scheme: "finger",
Host: hostname,
Path: "/" + username,

View File

@ -8,7 +8,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tildegit.org/tjp/gus/finger"
"tildegit.org/tjp/sliderule/finger"
)
func TestParseRequest(t *testing.T) {

View File

@ -5,18 +5,18 @@ import (
"io"
"strings"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
// Error produces a finger Response containing the error message and Status 1.
func Error(msg string) *gus.Response {
func Error(msg string) *sr.Response {
if !strings.HasSuffix(msg, "\r\n") {
msg += "\r\n"
}
return &gus.Response{Body: bytes.NewBufferString(msg), Status: 1}
return &sr.Response{Body: bytes.NewBufferString(msg), Status: 1}
}
// Success produces a finger response with a Status of 0.
func Success(body io.Reader) *gus.Response {
return &gus.Response{Body: body}
func Success(body io.Reader) *sr.Response {
return &sr.Response{Body: body}
}

View File

@ -7,14 +7,14 @@ import (
"net"
"strings"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/internal"
"tildegit.org/tjp/gus/logging"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/internal"
"tildegit.org/tjp/sliderule/logging"
)
type fingerServer struct {
internal.Server
handler gus.Handler
handler sr.Handler
}
func (fs fingerServer) Protocol() string { return "FINGER" }
@ -25,9 +25,9 @@ func NewServer(
hostname string,
network string,
address string,
handler gus.Handler,
handler sr.Handler,
errLog logging.Logger,
) (gus.Server, error) {
) (sr.Server, error) {
fs := &fingerServer{handler: handler}
if strings.IndexByte(hostname, ':') < 0 {

View File

@ -6,15 +6,15 @@ import (
"errors"
"os/exec"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
// ListingDenied is returned to reject online user listing requests.
var ListingDenied = errors.New("Finger online user list denied.")
// SystemFinger handles finger requests by invoking the finger(1) command-line utility.
func SystemFinger(allowListings bool) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
func SystemFinger(allowListings bool) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
fingerPath, err := exec.LookPath("finger")
if err != nil {
_ = request.Server.LogError(

View File

@ -7,7 +7,7 @@ import (
"io"
"net"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
// Client is used for sending gemini requests and parsing gemini responses.
@ -33,7 +33,7 @@ func NewClient(tlsConf *tls.Config) Client {
//
// This method will not automatically follow redirects or cache permanent failures or
// redirects.
func (client Client) RoundTrip(request *gus.Request) (*gus.Response, error) {
func (client Client) RoundTrip(request *sr.Request) (*sr.Response, error) {
if request.Scheme != "gemini" && request.Scheme != "" {
return nil, errors.New("non-gemini protocols not supported")
}

View File

@ -4,7 +4,7 @@ import (
"bytes"
"testing"
"tildegit.org/tjp/gus/gemini/gemtext"
"tildegit.org/tjp/sliderule/gemini/gemtext"
)
func FuzzParse(f *testing.F) {

View File

@ -4,8 +4,8 @@ import (
"html/template"
"io"
"tildegit.org/tjp/gus/gemini/gemtext"
"tildegit.org/tjp/gus/gemini/gemtext/internal"
"tildegit.org/tjp/sliderule/gemini/gemtext"
"tildegit.org/tjp/sliderule/gemini/gemtext/internal"
)
// Convert writes markdown to a writer from the provided gemtext document.

View File

@ -7,8 +7,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tildegit.org/tjp/gus/gemini/gemtext"
"tildegit.org/tjp/gus/gemini/gemtext/htmlconv"
"tildegit.org/tjp/sliderule/gemini/gemtext"
"tildegit.org/tjp/sliderule/gemini/gemtext/htmlconv"
)
var gmiDoc = `

View File

@ -5,7 +5,7 @@ import (
"net/url"
"text/template"
"tildegit.org/tjp/gus/gemini/gemtext"
"tildegit.org/tjp/sliderule/gemini/gemtext"
)
var Renderers = map[gemtext.LineType]string{

View File

@ -4,8 +4,8 @@ import (
"io"
"text/template"
"tildegit.org/tjp/gus/gemini/gemtext"
"tildegit.org/tjp/gus/gemini/gemtext/internal"
"tildegit.org/tjp/sliderule/gemini/gemtext"
"tildegit.org/tjp/sliderule/gemini/gemtext/internal"
)
// Convert writes markdown to a writer from the provided gemtext document.

View File

@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tildegit.org/tjp/gus/gemini/gemtext"
"tildegit.org/tjp/gus/gemini/gemtext/mdconv"
"tildegit.org/tjp/sliderule/gemini/gemtext"
"tildegit.org/tjp/sliderule/gemini/gemtext/mdconv"
)
var gmiDoc = `

View File

@ -3,7 +3,7 @@ package gemtext_test
import (
"testing"
"tildegit.org/tjp/gus/gemini/gemtext"
"tildegit.org/tjp/sliderule/gemini/gemtext"
)
func TestParseLinkLine(t *testing.T) {

View File

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tildegit.org/tjp/gus/gemini/gemtext"
"tildegit.org/tjp/sliderule/gemini/gemtext"
)
func TestParse(t *testing.T) {

View File

@ -6,7 +6,7 @@ import (
"io"
"net/url"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
// InvalidRequestLineEnding indicates that a gemini request didn't end with "\r\n".
@ -15,7 +15,7 @@ var InvalidRequestLineEnding = errors.New("invalid request line ending")
// ParseRequest parses a single gemini request from a reader.
//
// If the reader argument is a *bufio.Reader, it will only read a single line from it.
func ParseRequest(rdr io.Reader) (*gus.Request, error) {
func ParseRequest(rdr io.Reader) (*sr.Request, error) {
bufrdr, ok := rdr.(*bufio.Reader)
if !ok {
bufrdr = bufio.NewReader(rdr)
@ -39,5 +39,19 @@ func ParseRequest(rdr io.Reader) (*gus.Request, error) {
u.Scheme = "gemini"
}
return &gus.Request{URL: u}, nil
return &sr.Request{URL: u}, nil
}
// GetTitanRequestBody fetches the request body from a titan request.
//
// It returns nil if the argument is not a titan request or it otherwise
// does not have a request body set.
func GetTitanRequestBody(request *sr.Request) io.Reader {
if request.Scheme != "titan" {
return nil
}
if rdr, ok := request.Meta.(io.Reader); ok {
return rdr
}
return nil
}

View File

@ -4,7 +4,7 @@ import (
"bytes"
"testing"
"tildegit.org/tjp/gus/gemini"
"tildegit.org/tjp/sliderule/gemini"
)
func TestParseRequest(t *testing.T) {

View File

@ -8,7 +8,7 @@ import (
"strconv"
"sync"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
// ResponseCategory represents the various types of gemini responses.
@ -43,32 +43,32 @@ const (
ResponseCategoryCertificateRequired
)
func ResponseCategoryForStatus(status gus.Status) ResponseCategory {
func ResponseCategoryForStatus(status sr.Status) ResponseCategory {
return ResponseCategory(status / 10)
}
const (
// StatusInput indicates a required query parameter at the requested URL.
StatusInput gus.Status = gus.Status(ResponseCategoryInput) + iota
StatusInput sr.Status = sr.Status(ResponseCategoryInput) + iota
// StatusSensitiveInput indicates a sensitive query parameter is required.
StatusSensitiveInput
)
const (
// StatusSuccess is a successful response.
StatusSuccess = gus.Status(ResponseCategorySuccess) + iota
StatusSuccess = sr.Status(ResponseCategorySuccess) + iota
)
const (
// StatusTemporaryRedirect indicates a temporary redirect to another URL.
StatusTemporaryRedirect = gus.Status(ResponseCategoryRedirect) + iota
StatusTemporaryRedirect = sr.Status(ResponseCategoryRedirect) + iota
// StatusPermanentRedirect indicates that the resource should always be requested at the new URL.
StatusPermanentRedirect
)
const (
// StatusTemporaryFailure indicates that the request failed and there is no response body.
StatusTemporaryFailure = gus.Status(ResponseCategoryTemporaryFailure) + iota
StatusTemporaryFailure = sr.Status(ResponseCategoryTemporaryFailure) + iota
// StatusServerUnavailable occurs when the server is unavailable due to overload or maintenance.
StatusServerUnavailable
// StatusCGIError is the result of a failure of a CGI script.
@ -84,7 +84,7 @@ const (
const (
// StatusPermanentFailure is a server failure which should be expected to continue indefinitely.
StatusPermanentFailure = gus.Status(ResponseCategoryPermanentFailure) + iota
StatusPermanentFailure = sr.Status(ResponseCategoryPermanentFailure) + iota
// StatusNotFound means the resource doesn't exist but it may in the future.
StatusNotFound
// StatusGone occurs when a resource will not be available any longer.
@ -92,12 +92,12 @@ const (
// StatusProxyRequestRefused means the server is unwilling to act as a proxy for the resource.
StatusProxyRequestRefused
// StatusBadRequest indicates that the request was malformed somehow.
StatusBadRequest = gus.Status(ResponseCategoryPermanentFailure) + 9
StatusBadRequest = sr.Status(ResponseCategoryPermanentFailure) + 9
)
const (
// StatusClientCertificateRequired is returned when a certificate was required but not provided.
StatusClientCertificateRequired = gus.Status(ResponseCategoryCertificateRequired) + iota
StatusClientCertificateRequired = sr.Status(ResponseCategoryCertificateRequired) + iota
// StatusCertificateNotAuthorized means the certificate doesn't grant access to the requested resource.
StatusCertificateNotAuthorized
// StatusCertificateNotValid means the provided client certificate is invalid.
@ -105,24 +105,24 @@ const (
)
// Input builds an input-prompting response.
func Input(prompt string) *gus.Response {
return &gus.Response{
func Input(prompt string) *sr.Response {
return &sr.Response{
Status: StatusInput,
Meta: prompt,
}
}
// SensitiveInput builds a password-prompting response.
func SensitiveInput(prompt string) *gus.Response {
return &gus.Response{
func SensitiveInput(prompt string) *sr.Response {
return &sr.Response{
Status: StatusSensitiveInput,
Meta: prompt,
}
}
// Success builds a success response with resource body.
func Success(mediatype string, body io.Reader) *gus.Response {
return &gus.Response{
func Success(mediatype string, body io.Reader) *sr.Response {
return &sr.Response{
Status: StatusSuccess,
Meta: mediatype,
Body: body,
@ -130,120 +130,120 @@ func Success(mediatype string, body io.Reader) *gus.Response {
}
// Redirect builds a redirect response.
func Redirect(url string) *gus.Response {
return &gus.Response{
func Redirect(url string) *sr.Response {
return &sr.Response{
Status: StatusTemporaryRedirect,
Meta: url,
}
}
// PermanentRedirect builds a response with a permanent redirect.
func PermanentRedirect(url string) *gus.Response {
return &gus.Response{
func PermanentRedirect(url string) *sr.Response {
return &sr.Response{
Status: StatusPermanentRedirect,
Meta: url,
}
}
// Failure builds a temporary failure response from an error.
func Failure(err error) *gus.Response {
return &gus.Response{
func Failure(err error) *sr.Response {
return &sr.Response{
Status: StatusTemporaryFailure,
Meta: err.Error(),
}
}
// Unavailable build a "server unavailable" response.
func Unavailable(msg string) *gus.Response {
return &gus.Response{
func Unavailable(msg string) *sr.Response {
return &sr.Response{
Status: StatusServerUnavailable,
Meta: msg,
}
}
// CGIError builds a "cgi error" response.
func CGIError(err string) *gus.Response {
return &gus.Response{
func CGIError(err string) *sr.Response {
return &sr.Response{
Status: StatusCGIError,
Meta: err,
}
}
// ProxyError builds a proxy error response.
func ProxyError(msg string) *gus.Response {
return &gus.Response{
func ProxyError(msg string) *sr.Response {
return &sr.Response{
Status: StatusProxyError,
Meta: msg,
}
}
// SlowDown builds a "slow down" response with the number of seconds until the resource is available.
func SlowDown(seconds int) *gus.Response {
return &gus.Response{
func SlowDown(seconds int) *sr.Response {
return &sr.Response{
Status: StatusSlowDown,
Meta: strconv.Itoa(seconds),
}
}
// PermanentFailure builds a "permanent failure" from an error.
func PermanentFailure(err error) *gus.Response {
return &gus.Response{
func PermanentFailure(err error) *sr.Response {
return &sr.Response{
Status: StatusPermanentFailure,
Meta: err.Error(),
}
}
// NotFound builds a "resource not found" response.
func NotFound(msg string) *gus.Response {
return &gus.Response{
func NotFound(msg string) *sr.Response {
return &sr.Response{
Status: StatusNotFound,
Meta: msg,
}
}
// Gone builds a "resource gone" response.
func Gone(msg string) *gus.Response {
return &gus.Response{
func Gone(msg string) *sr.Response {
return &sr.Response{
Status: StatusGone,
Meta: msg,
}
}
// RefuseProxy builds a "proxy request refused" response.
func RefuseProxy(msg string) *gus.Response {
return &gus.Response{
func RefuseProxy(msg string) *sr.Response {
return &sr.Response{
Status: StatusProxyRequestRefused,
Meta: msg,
}
}
// BadRequest builds a "bad request" response.
func BadRequest(msg string) *gus.Response {
return &gus.Response{
func BadRequest(msg string) *sr.Response {
return &sr.Response{
Status: StatusBadRequest,
Meta: msg,
}
}
// RequireCert builds a "client certificate required" response.
func RequireCert(msg string) *gus.Response {
return &gus.Response{
func RequireCert(msg string) *sr.Response {
return &sr.Response{
Status: StatusClientCertificateRequired,
Meta: msg,
}
}
// CertAuthFailure builds a "certificate not authorized" response.
func CertAuthFailure(msg string) *gus.Response {
return &gus.Response{
func CertAuthFailure(msg string) *sr.Response {
return &sr.Response{
Status: StatusCertificateNotAuthorized,
Meta: msg,
}
}
// CertInvalid builds a "client certificate not valid" response.
func CertInvalid(msg string) *gus.Response {
return &gus.Response{
func CertInvalid(msg string) *sr.Response {
return &sr.Response{
Status: StatusCertificateNotValid,
Meta: msg,
}
@ -258,7 +258,7 @@ var InvalidResponseHeaderLine = errors.New("Invalid response header line.")
// ParseResponse parses a complete gemini response from a reader.
//
// The reader must contain only one gemini response.
func ParseResponse(rdr io.Reader) (*gus.Response, error) {
func ParseResponse(rdr io.Reader) (*sr.Response, error) {
bufrdr := bufio.NewReader(rdr)
hdrLine, err := bufrdr.ReadBytes('\n')
@ -278,14 +278,14 @@ func ParseResponse(rdr io.Reader) (*gus.Response, error) {
return nil, InvalidResponseHeaderLine
}
return &gus.Response{
Status: gus.Status(status),
return &sr.Response{
Status: sr.Status(status),
Meta: string(hdrLine[3:]),
Body: bufrdr,
}, nil
}
func NewResponseReader(response *gus.Response) gus.ResponseReader {
func NewResponseReader(response *sr.Response) sr.ResponseReader {
return &responseReader{
Response: response,
once: &sync.Once{},
@ -293,7 +293,7 @@ func NewResponseReader(response *gus.Response) gus.ResponseReader {
}
type responseReader struct {
*gus.Response
*sr.Response
reader io.Reader
once *sync.Once
}

View File

@ -6,15 +6,15 @@ import (
"io"
"testing"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/gemini"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/gemini"
)
func TestBuildResponses(t *testing.T) {
table := []struct {
name string
response *gus.Response
status gus.Status
response *sr.Response
status sr.Status
meta string
body string
}{
@ -154,7 +154,7 @@ func TestBuildResponses(t *testing.T) {
func TestParseResponses(t *testing.T) {
table := []struct {
input string
status gus.Status
status sr.Status
meta string
body string
err error
@ -233,7 +233,7 @@ func TestParseResponses(t *testing.T) {
func TestResponseClose(t *testing.T) {
body := &rdCloser{Buffer: bytes.NewBufferString("the body here")}
resp := &gus.Response{
resp := &sr.Response{
Status: gemini.StatusSuccess,
Meta: "text/gemini",
Body: body,
@ -247,7 +247,7 @@ func TestResponseClose(t *testing.T) {
t.Error("response body was not closed by response.Close()")
}
resp = &gus.Response{
resp = &sr.Response{
Status: gemini.StatusInput,
Meta: "give me more",
}
@ -270,8 +270,8 @@ func (rc *rdCloser) Close() error {
func TestResponseWriteTo(t *testing.T) {
// invariant under test: WriteTo() sends the same bytes as Read()
clone := func(resp *gus.Response) *gus.Response {
other := &gus.Response{
clone := func(resp *sr.Response) *sr.Response {
other := &sr.Response{
Status: resp.Status,
Meta: resp.Meta,
}
@ -297,7 +297,7 @@ func TestResponseWriteTo(t *testing.T) {
table := []struct {
name string
response *gus.Response
response *sr.Response
}{
{
name: "simple success",

View File

@ -12,15 +12,15 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/gemini"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/gemini"
)
func TestRoundTrip(t *testing.T) {
tlsConf, err := gemini.FileTLS("./testdata/server.crt", "./testdata/server.key")
require.Nil(t, err)
handler := gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response {
handler := sr.HandlerFunc(func(ctx context.Context, req *sr.Request) *sr.Response {
return gemini.Success("text/gemini", bytes.NewBufferString("you've found my page"))
})
@ -36,7 +36,7 @@ func TestRoundTrip(t *testing.T) {
require.Nil(t, err)
cli := gemini.NewClient(testClientTLS())
response, err := cli.RoundTrip(&gus.Request{URL: u})
response, err := cli.RoundTrip(&sr.Request{URL: u})
require.Nil(t, err)
assert.Equal(t, gemini.StatusSuccess, response.Status)
@ -54,10 +54,10 @@ func TestTitanRequest(t *testing.T) {
require.Nil(t, err)
invoked := false
handler := gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
handler := sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
invoked = true
body := ctx.Value(gemini.TitanRequestBody)
body := gemini.GetTitanRequestBody(request)
if !assert.NotNil(t, body) {
return gemini.Success("", nil)
}

View File

@ -11,23 +11,17 @@ import (
"strconv"
"strings"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/internal"
"tildegit.org/tjp/gus/logging"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/internal"
"tildegit.org/tjp/sliderule/logging"
)
type titanRequestBodyKey struct{}
// TitanRequestBody is the key set in a handler's context for titan requests.
//
// When this key is present in the context (request.URL.Scheme will be "titan"), the
// corresponding value is a *bufio.Reader from which the request body can be read.
var TitanRequestBody = titanRequestBodyKey{}
type server struct {
internal.Server
handler gus.Handler
handler sr.Handler
}
func (s server) Protocol() string { return "GEMINI" }
@ -38,10 +32,10 @@ func NewServer(
hostname string,
network string,
address string,
handler gus.Handler,
handler sr.Handler,
errorLog logging.Logger,
tlsConfig *tls.Config,
) (gus.Server, error) {
) (sr.Server, error) {
s := &server{handler: handler}
if strings.IndexByte(hostname, ':') < 0 {
@ -62,7 +56,7 @@ func NewServer(
func (s *server) handleConn(conn net.Conn) {
buf := bufio.NewReader(conn)
var response *gus.Response
var response *sr.Response
request, err := ParseRequest(buf)
if err != nil {
response = BadRequest(err.Error())
@ -79,11 +73,7 @@ func (s *server) handleConn(conn net.Conn) {
if request.Scheme == "titan" {
len, err := sizeParam(request.Path)
if err == nil {
ctx = context.WithValue(
ctx,
TitanRequestBody,
io.LimitReader(buf, int64(len)),
)
request.Meta = io.LimitReader(buf, int64(len))
}
}
@ -125,9 +115,9 @@ func sizeParam(path string) (int, error) {
// Optionally, it will also allow through titan:// requests.
//
// Filtered requests will be turned away with a 53 response "proxy request refused".
func GeminiOnly(allowTitan bool) gus.Middleware {
return func(inner gus.Handler) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
func GeminiOnly(allowTitan bool) sr.Middleware {
return func(inner sr.Handler) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
if request.Scheme == "gemini" || (allowTitan && request.Scheme == "titan") {
return inner.Handle(ctx, request)
}

2
go.mod
View File

@ -1,4 +1,4 @@
module tildegit.org/tjp/gus
module tildegit.org/tjp/sliderule
go 1.19

View File

@ -6,7 +6,7 @@ import (
"io"
"net"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
// Client is used for sending gopher requests and producing the responses.
@ -17,7 +17,7 @@ import (
type Client struct{}
// RoundTrip sends a single gopher request and returns its response.
func (c Client) RoundTrip(request *gus.Request) (*gus.Response, error) {
func (c Client) RoundTrip(request *sr.Request) (*sr.Response, error) {
if request.Scheme != "gopher" && request.Scheme != "" {
return nil, errors.New("non-gopher protocols not supported")
}
@ -51,5 +51,5 @@ func (c Client) RoundTrip(request *gus.Request) (*gus.Response, error) {
return nil, err
}
return &gus.Response{Body: bytes.NewBuffer(response)}, nil
return &sr.Response{Body: bytes.NewBuffer(response)}, nil
}

View File

@ -7,8 +7,8 @@ import (
"fmt"
"io"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/gopher"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/gopher"
)
// Parse reads a gophermap document from a reader.
@ -30,7 +30,7 @@ func Parse(input io.Reader) (gopher.MapDocument, error) {
return nil, InvalidLine(num)
}
item := gopher.MapItem{Type: gus.Status(line[0])}
item := gopher.MapItem{Type: sr.Status(line[0])}
spl := bytes.Split(line[1:len(line)-2], []byte{'\t'})
if len(spl) != 4 {

View File

@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tildegit.org/tjp/gus/gopher"
"tildegit.org/tjp/gus/gopher/gophermap"
"tildegit.org/tjp/sliderule/gopher"
"tildegit.org/tjp/sliderule/gopher/gophermap"
)
func TestParse(t *testing.T) {

View File

@ -8,11 +8,11 @@ import (
"path"
"strings"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
// ParseRequest parses a gopher protocol request into a gus.Request object.
func ParseRequest(rdr io.Reader) (*gus.Request, error) {
// ParseRequest parses a gopher protocol request into a sliderule.Request object.
func ParseRequest(rdr io.Reader) (*sr.Request, error) {
selector, search, err := readFullRequest(rdr)
if err != nil {
return nil, err
@ -22,7 +22,7 @@ func ParseRequest(rdr io.Reader) (*gus.Request, error) {
selector = "/" + selector
}
return &gus.Request{
return &sr.Request{
URL: &url.URL{
Scheme: "gopher",
Path: path.Clean(strings.TrimSuffix(selector, "\r\n")),

View File

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tildegit.org/tjp/gus/gopher"
"tildegit.org/tjp/sliderule/gopher"
)
func TestParseRequest(t *testing.T) {

View File

@ -6,49 +6,49 @@ import (
"io"
"sync"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
// The Canonical gopher item types.
const (
TextFileType gus.Status = '0'
MenuType gus.Status = '1'
CSOPhoneBookType gus.Status = '2'
ErrorType gus.Status = '3'
MacBinHexType gus.Status = '4'
DosBinType gus.Status = '5'
UuencodedType gus.Status = '6'
SearchType gus.Status = '7'
TelnetSessionType gus.Status = '8'
BinaryFileType gus.Status = '9'
MirrorServerType gus.Status = '+'
GifFileType gus.Status = 'g'
ImageFileType gus.Status = 'I'
Telnet3270Type gus.Status = 'T'
TextFileType sr.Status = '0'
MenuType sr.Status = '1'
CSOPhoneBookType sr.Status = '2'
ErrorType sr.Status = '3'
MacBinHexType sr.Status = '4'
DosBinType sr.Status = '5'
UuencodedType sr.Status = '6'
SearchType sr.Status = '7'
TelnetSessionType sr.Status = '8'
BinaryFileType sr.Status = '9'
MirrorServerType sr.Status = '+'
GifFileType sr.Status = 'g'
ImageFileType sr.Status = 'I'
Telnet3270Type sr.Status = 'T'
)
// The gopher+ types.
const (
BitmapType gus.Status = ':'
MovieFileType gus.Status = ';'
SoundFileType gus.Status = '<'
BitmapType sr.Status = ':'
MovieFileType sr.Status = ';'
SoundFileType sr.Status = '<'
)
// The various non-canonical gopher types.
const (
DocumentType gus.Status = 'd'
HTMLType gus.Status = 'h'
InfoMessageType gus.Status = 'i'
PngImageFileType gus.Status = 'p'
RtfDocumentType gus.Status = 'r'
WavSoundFileType gus.Status = 's'
PdfDocumentType gus.Status = 'P'
XmlDocumentType gus.Status = 'X'
DocumentType sr.Status = 'd'
HTMLType sr.Status = 'h'
InfoMessageType sr.Status = 'i'
PngImageFileType sr.Status = 'p'
RtfDocumentType sr.Status = 'r'
WavSoundFileType sr.Status = 's'
PdfDocumentType sr.Status = 'P'
XmlDocumentType sr.Status = 'X'
)
// MapItem is a single item in a gophermap.
type MapItem struct {
Type gus.Status
Type sr.Status
Display string
Selector string
Hostname string
@ -70,8 +70,8 @@ func (mi MapItem) String() string {
// Response builds a response which contains just this single MapItem.
//
// Meta in the response will be a pointer to the MapItem.
func (mi *MapItem) Response() *gus.Response {
return &gus.Response{
func (mi *MapItem) Response() *sr.Response {
return &sr.Response{
Status: mi.Type,
Meta: &mi,
Body: bytes.NewBufferString(mi.String() + ".\r\n"),
@ -89,8 +89,8 @@ func (md MapDocument) String() string {
// Response builds a gopher response containing the gophermap.
//
// Meta will be the MapDocument itself.
func (md MapDocument) Response() *gus.Response {
return &gus.Response{
func (md MapDocument) Response() *sr.Response {
return &sr.Response{
Status: DocumentType,
Meta: md,
Body: md.serialize(),
@ -119,12 +119,12 @@ func Error(err error) *MapItem {
// File builds a minimal response delivering a file's contents.
//
// Meta is nil and Status is 0 in this response.
func File(status gus.Status, contents io.Reader) *gus.Response {
return &gus.Response{Status: status, Body: contents}
func File(status sr.Status, contents io.Reader) *sr.Response {
return &sr.Response{Status: status, Body: contents}
}
// NewResponseReader produces a reader which supports reading gopher protocol responses.
func NewResponseReader(response *gus.Response) gus.ResponseReader {
func NewResponseReader(response *sr.Response) sr.ResponseReader {
return &responseReader{
Response: response,
once: &sync.Once{},
@ -132,7 +132,7 @@ func NewResponseReader(response *gus.Response) gus.ResponseReader {
}
type responseReader struct {
*gus.Response
*sr.Response
reader io.Reader
once *sync.Once
}

View File

@ -8,14 +8,14 @@ import (
"net"
"strings"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/internal"
"tildegit.org/tjp/gus/logging"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/internal"
"tildegit.org/tjp/sliderule/logging"
)
type gopherServer struct {
internal.Server
handler gus.Handler
handler sr.Handler
}
func (gs gopherServer) Protocol() string { return "GOPHER" }
@ -26,9 +26,9 @@ func NewServer(
hostname string,
network string,
address string,
handler gus.Handler,
handler sr.Handler,
errLog logging.Logger,
) (gus.Server, error) {
) (sr.Server, error) {
gs := &gopherServer{handler: handler}
if strings.IndexByte(hostname, ':') < 0 {
@ -45,7 +45,7 @@ func NewServer(
}
func (gs *gopherServer) handleConn(conn net.Conn) {
var response *gus.Response
var response *sr.Response
request, err := ParseRequest(conn)
if err != nil {
response = Error(errors.New("Malformed request.")).Response()

View File

@ -1,4 +1,4 @@
package gus
package sliderule
import "context"

View File

@ -1,4 +1,4 @@
package gus_test
package sliderule_test
import (
"bytes"
@ -8,33 +8,33 @@ import (
"strings"
"testing"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/gemini"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/gemini"
)
func TestFallthrough(t *testing.T) {
h1 := gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response {
h1 := sr.HandlerFunc(func(ctx context.Context, req *sr.Request) *sr.Response {
if req.Path == "/one" {
return gemini.Success("text/gemini", bytes.NewBufferString("one"))
}
return nil
})
h2 := gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response {
h2 := sr.HandlerFunc(func(ctx context.Context, req *sr.Request) *sr.Response {
if req.Path == "/two" {
return gemini.Success("text/gemini", bytes.NewBufferString("two"))
}
return nil
})
fth := gus.FallthroughHandler(h1, h2)
fth := sr.FallthroughHandler(h1, h2)
u, err := url.Parse("gemini://test.local/one")
if err != nil {
t.Fatalf("url.Parse: %s", err.Error())
}
resp := fth.Handle(context.Background(), &gus.Request{URL: u})
resp := fth.Handle(context.Background(), &sr.Request{URL: u})
if resp.Status != gemini.StatusSuccess {
t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status)
@ -57,7 +57,7 @@ func TestFallthrough(t *testing.T) {
t.Fatalf("url.Parse: %s", err.Error())
}
resp = fth.Handle(context.Background(), &gus.Request{URL: u})
resp = fth.Handle(context.Background(), &sr.Request{URL: u})
if resp.Status != gemini.StatusSuccess {
t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status)
@ -80,7 +80,7 @@ func TestFallthrough(t *testing.T) {
t.Fatalf("url.Parse: %s", err.Error())
}
resp = fth.Handle(context.Background(), &gus.Request{URL: u})
resp = fth.Handle(context.Background(), &sr.Request{URL: u})
if resp != nil {
t.Errorf("expected nil, got %+v", resp)
@ -88,20 +88,20 @@ func TestFallthrough(t *testing.T) {
}
func TestFilter(t *testing.T) {
pred := func(ctx context.Context, req *gus.Request) bool {
pred := func(ctx context.Context, req *sr.Request) bool {
return strings.HasPrefix(req.Path, "/allow")
}
base := gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response {
base := sr.HandlerFunc(func(ctx context.Context, req *sr.Request) *sr.Response {
return gemini.Success("text/gemini", bytes.NewBufferString("allowed!"))
})
handler := gus.Filter(pred, nil)(base)
handler := sr.Filter(pred, nil)(base)
u, err := url.Parse("gemini://test.local/allow/please")
if err != nil {
t.Fatalf("url.Parse: %s", err.Error())
}
resp := handler.Handle(context.Background(), &gus.Request{URL: u})
resp := handler.Handle(context.Background(), &sr.Request{URL: u})
if resp.Status != gemini.StatusSuccess {
t.Errorf("expected status %d, got %d", gemini.StatusSuccess, resp.Status)
}
@ -111,7 +111,7 @@ func TestFilter(t *testing.T) {
t.Fatalf("url.Parse: %s", err.Error())
}
resp = handler.Handle(context.Background(), &gus.Request{URL: u})
resp = handler.Handle(context.Background(), &sr.Request{URL: u})
if resp != nil {
t.Errorf("expected nil, got %+v", resp)
}

View File

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tildegit.org/tjp/gus/internal"
"tildegit.org/tjp/sliderule/internal"
)
func TestPathTree(t *testing.T) {

View File

@ -6,12 +6,12 @@ import (
"io"
"time"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
func LogRequests(logger Logger) gus.Middleware {
return func(inner gus.Handler) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, request *gus.Request) *gus.Response {
func LogRequests(logger Logger) sr.Middleware {
return func(inner sr.Handler) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
response := inner.Handle(ctx, request)
if response != nil {
response.Body = loggingBody(logger, request, response)
@ -23,8 +23,8 @@ func LogRequests(logger Logger) gus.Middleware {
}
type loggedResponseBody struct {
request *gus.Request
response *gus.Response
request *sr.Request
response *sr.Response
body io.Reader
start time.Time
@ -81,7 +81,7 @@ func (lwtr loggedWriteToResponseBody) WriteTo(dst io.Writer) (int64, error) {
return n, err
}
func loggingBody(logger Logger, request *gus.Request, response *gus.Response) io.Reader {
func loggingBody(logger Logger, request *sr.Request, response *sr.Response) io.Reader {
body := &loggedResponseBody{
request: request,
response: response,

View File

@ -8,17 +8,17 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/logging"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/logging"
)
func TestLogRequests(t *testing.T) {
logger := logRecorder{}
handler := logging.LogRequests(&logger)(gus.HandlerFunc(func(_ context.Context, _ *gus.Request) *gus.Response {
return &gus.Response{}
handler := logging.LogRequests(&logger)(sr.HandlerFunc(func(_ context.Context, _ *sr.Request) *sr.Response {
return &sr.Response{}
}))
response := handler.Handle(context.Background(), &gus.Request{})
response := handler.Handle(context.Background(), &sr.Request{})
_, err := io.ReadAll(response.Body)
assert.Nil(t, err)

View File

@ -1,4 +1,4 @@
package gus
package sliderule
import (
"crypto/tls"
@ -20,6 +20,12 @@ type Request struct {
// It is unused on the client end.
Server Server
// Meta is a place for opaque data.
//
// Look for helper methods in protocol packages to use it appropriately
// for the protocol.
Meta any
// RemoteAddr is the address of the other side of the connection.
//
// This will be the server address for clients, or the connecting

View File

@ -1,10 +1,10 @@
package gus_test
package sliderule_test
import (
"net/url"
"testing"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
func TestUnescapedQuery(t *testing.T) {
@ -15,7 +15,7 @@ func TestUnescapedQuery(t *testing.T) {
for _, test := range table {
t.Run(test, func(t *testing.T) {
u, _ := url.Parse("gemini://domain.com/path?" + url.QueryEscape(test))
result := gus.Request{URL: u}.UnescapedQuery()
result := sr.Request{URL: u}.UnescapedQuery()
if result != test {
t.Errorf("expected %q, got %q", test, result)
}

View File

@ -1,4 +1,4 @@
package gus
package sliderule
import "io"

View File

@ -1,10 +1,10 @@
package gus
package sliderule
import (
"context"
"strings"
"tildegit.org/tjp/gus/internal"
"tildegit.org/tjp/sliderule/internal"
)
// Router stores a mapping of request path patterns to handlers.

View File

@ -1,4 +1,4 @@
package gus_test
package sliderule_test
import (
"bytes"
@ -9,24 +9,24 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/gemini"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/gemini"
)
var h1 = gus.HandlerFunc(func(_ context.Context, _ *gus.Request) *gus.Response {
var h1 = sr.HandlerFunc(func(_ context.Context, _ *sr.Request) *sr.Response {
return gemini.Success("", &bytes.Buffer{})
})
func mw1(h gus.Handler) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response {
func mw1(h sr.Handler) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, req *sr.Request) *sr.Response {
resp := h.Handle(ctx, req)
resp.Body = io.MultiReader(resp.Body, bytes.NewBufferString("\nmiddleware 1"))
return resp
})
}
func mw2(h gus.Handler) gus.Handler {
return gus.HandlerFunc(func(ctx context.Context, req *gus.Request) *gus.Response {
func mw2(h sr.Handler) sr.Handler {
return sr.HandlerFunc(func(ctx context.Context, req *sr.Request) *sr.Response {
resp := h.Handle(ctx, req)
resp.Body = io.MultiReader(resp.Body, bytes.NewBufferString("\nmiddleware 2"))
return resp
@ -34,7 +34,7 @@ func mw2(h gus.Handler) gus.Handler {
}
func TestRouterUse(t *testing.T) {
r := &gus.Router{}
r := &sr.Router{}
r.Use(mw1)
r.Use(mw2)
r.Route("/", h1)
@ -52,10 +52,10 @@ func TestRouterUse(t *testing.T) {
}
func TestRouterMount(t *testing.T) {
outer := &gus.Router{}
outer := &sr.Router{}
outer.Use(mw2)
inner := &gus.Router{}
inner := &sr.Router{}
inner.Use(mw1)
inner.Route("/bar", h1)

View File

@ -1,4 +1,4 @@
package gus
package sliderule
// Server is a type which can serve a protocol.
type Server interface {

View File

@ -7,7 +7,7 @@ import (
"net"
"strconv"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
// Client is used for sending spartan requests and receiving responses.
@ -18,7 +18,7 @@ import (
type Client struct{}
// RoundTrip sends a single spartan request and returns its response.
func (c Client) RoundTrip(request *gus.Request, body io.Reader) (*gus.Response, error) {
func (c Client) RoundTrip(request *sr.Request, body io.Reader) (*sr.Response, error) {
if request.Scheme != "spartan" && request.Scheme != "" {
return nil, errors.New("non-spartan protocols not supported")
}

View File

@ -8,7 +8,7 @@ import (
"strconv"
"strings"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
var (
@ -22,7 +22,7 @@ var (
// ParseRequest parses a single spartan request and the indicated content-length from a reader.
//
// If ther reader artument is a *bufio.Reader, it will only read a single line from it.
func ParseRequest(rdr io.Reader) (*gus.Request, int, error) {
func ParseRequest(rdr io.Reader) (*sr.Request, int, error) {
bufrdr, ok := rdr.(*bufio.Reader)
if !ok {
bufrdr = bufio.NewReader(rdr)
@ -51,7 +51,7 @@ func ParseRequest(rdr io.Reader) (*gus.Request, int, error) {
return nil, 0, err
}
return &gus.Request{
return &sr.Request{
URL: &url.URL{
Scheme: "spartan",
Host: host,
@ -60,3 +60,23 @@ func ParseRequest(rdr io.Reader) (*gus.Request, int, error) {
},
}, contentlen, nil
}
// GetRequestContentLength reads the remaining un-read number of bytes in a request body.
//
// It will immediately return 0 if there is no request body.
func GetRequestContentLength(request *sr.Request) int {
if lr, ok := request.Meta.(*io.LimitedReader); ok {
return int(lr.N)
}
return 0
}
// GetRequestBody returns a reader of the spartan request body.
//
// It will return nil if the request has no body.
func GetRequestBody(request *sr.Request) io.Reader {
if rdr, ok := request.Meta.(io.Reader); ok {
return rdr
}
return nil
}

View File

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tildegit.org/tjp/gus/spartan"
"tildegit.org/tjp/sliderule/spartan"
)
func TestParseRequest(t *testing.T) {

View File

@ -8,20 +8,20 @@ import (
"strconv"
"sync"
"tildegit.org/tjp/gus"
sr "tildegit.org/tjp/sliderule"
)
// The spartan response types.
const (
StatusSuccess gus.Status = 2
StatusRedirect gus.Status = 3
StatusClientError gus.Status = 4
StatusServerError gus.Status = 5
StatusSuccess sr.Status = 2
StatusRedirect sr.Status = 3
StatusClientError sr.Status = 4
StatusServerError sr.Status = 5
)
// Success builds a successful spartan response.
func Success(mediatype string, body io.Reader) *gus.Response {
return &gus.Response{
func Success(mediatype string, body io.Reader) *sr.Response {
return &sr.Response{
Status: StatusSuccess,
Meta: mediatype,
Body: body,
@ -29,24 +29,24 @@ func Success(mediatype string, body io.Reader) *gus.Response {
}
// Redirect builds a spartan redirect response.
func Redirect(url string) *gus.Response {
return &gus.Response{
func Redirect(url string) *sr.Response {
return &sr.Response{
Status: StatusRedirect,
Meta: url,
}
}
// ClientError builds a "client error" spartan response.
func ClientError(err error) *gus.Response {
return &gus.Response{
func ClientError(err error) *sr.Response {
return &sr.Response{
Status: StatusClientError,
Meta: err.Error(),
}
}
// ServerError builds a "server error" spartan response.
func ServerError(err error) *gus.Response {
return &gus.Response{
func ServerError(err error) *sr.Response {
return &sr.Response{
Status: StatusServerError,
Meta: err.Error(),
}
@ -58,7 +58,7 @@ var InvalidResponseHeaderLine = errors.New("Invalid response header line.")
// InvalidResponseLineEnding indicates that a spartan response header didn't end with "\r\n".
var InvalidResponseLineEnding = errors.New("Invalid response line ending.")
func ParseResponse(rdr io.Reader) (*gus.Response, error) {
func ParseResponse(rdr io.Reader) (*sr.Response, error) {
bufrdr := bufio.NewReader(rdr)
hdrLine, err := bufrdr.ReadString('\n')
@ -74,15 +74,15 @@ func ParseResponse(rdr io.Reader) (*gus.Response, error) {
return nil, InvalidResponseHeaderLine
}
return &gus.Response{
Status: gus.Status(status),
return &sr.Response{
Status: sr.Status(status),
Meta: hdrLine[2 : len(hdrLine)-2],
Body: bufrdr,
}, nil
}
// NewResponseReader builds a reader for a response.
func NewResponseReader(response *gus.Response) gus.ResponseReader {
func NewResponseReader(response *sr.Response) sr.ResponseReader {
return &responseReader{
Response: response,
once: &sync.Once{},
@ -90,7 +90,7 @@ func NewResponseReader(response *gus.Response) gus.ResponseReader {
}
type responseReader struct {
*gus.Response
*sr.Response
reader io.Reader
once *sync.Once
}

View File

@ -9,27 +9,14 @@ import (
"net"
"strings"
"tildegit.org/tjp/gus"
"tildegit.org/tjp/gus/internal"
"tildegit.org/tjp/gus/logging"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/internal"
"tildegit.org/tjp/sliderule/logging"
)
type spartanRequestBodyKey struct{}
type spartanRequestBodyLenKey struct{}
// SpartanRequestBody is the key set in a handler's context for spartan request bodies.
//
// The corresponding value is a *bufio.Reader from which the request body can be read.
var SpartanRequestBody = spartanRequestBodyKey{}
// SpartanRequestBodyLen is the key set in a handler's context for the content-length of the request.
//
// The corresponding value is an int.
var SpartanRequestBodyLen = spartanRequestBodyLenKey{}
type spartanServer struct {
internal.Server
handler gus.Handler
handler sr.Handler
}
func (ss spartanServer) Protocol() string { return "SPARTAN" }
@ -40,9 +27,9 @@ func NewServer(
hostname string,
network string,
address string,
handler gus.Handler,
handler sr.Handler,
errLog logging.Logger,
) (gus.Server, error) {
) (sr.Server, error) {
ss := &spartanServer{handler: handler}
if strings.IndexByte(hostname, ':') < 0 {
@ -61,7 +48,7 @@ func NewServer(
func (ss *spartanServer) handleConn(conn net.Conn) {
buf := bufio.NewReader(conn)
var response *gus.Response
var response *sr.Response
request, clen, err := ParseRequest(buf)
if err != nil {
response = ClientError(err)
@ -69,12 +56,11 @@ func (ss *spartanServer) handleConn(conn net.Conn) {
request.Server = ss
request.RemoteAddr = conn.RemoteAddr()
var body *bufio.Reader = nil
var body io.Reader = nil
if clen > 0 {
body = bufio.NewReader(io.LimitReader(buf, int64(clen)))
body = io.LimitReader(buf, int64(clen))
}
ctx := context.WithValue(ss.Ctx, SpartanRequestBody, body)
ctx = context.WithValue(ctx, SpartanRequestBodyLen, clen)
request.Meta = body
defer func() {
if r := recover(); r != nil {
@ -84,7 +70,7 @@ func (ss *spartanServer) handleConn(conn net.Conn) {
_, _ = io.Copy(conn, rdr)
}
}()
response = ss.handler.Handle(ctx, request)
response = ss.handler.Handle(ss.Ctx, request)
if response == nil {
response = ClientError(errors.New("Resource does not exist."))
}