Add support for spartan protocol

spartan server definitions are very similar to gemini.

The major changes are:
  * no "servertls" directives
  * no "autoatom" modifier (to be fixed)
  * no "git" directives (to be fixed)

Fixes #15
This commit is contained in:
tjpcc 2023-10-30 13:02:06 -06:00
parent deb9bd7551
commit 0b57acaa2d
6 changed files with 178 additions and 19 deletions

View File

@ -28,7 +28,7 @@
The last airplane designed without the use of electronic calculators, the SR-71 was developed from concept to first flight in just over 2 years. It flew higher and faster than any jet of its time (holding the speed record to this day), and pioneered low-observability (stealth) characteristics.
This sr-71 project is a small web server, able to host your pages on the gemini, gopher, and finger protocols.
This sr-71 project is a small web server, able to host your pages on the gemini, gopher, spartan, and finger protocols.
# Getting it
@ -70,6 +70,12 @@ gopher {
static /var/gopher at / with dirdefault gophermap, dirlist, extendedgophermap
}
spartan {
host myhostname.com
static /var/spartan at / with dirdefault index.gmi, dirlist
}
finger {
static ~/.finger
}
@ -82,6 +88,8 @@ With the above configuration, sr-71 will:
* serve gopher by reading out of /var/gopher
* for gopher directory requests, it will look for a file called "gophermap", then fall back to listing the contents as gopher menu
* for any gopher files, it will apply the "extended gophermap" parsing documented below
* serve spartan by reading files from /var/spartan (perhaps imagine this being a symlink to /var/gemini)
* serve spartan directory requests by looking for index.gmi and falling back to a directory listing
* serve finger requests by reading a ".finger" file in the requested user's home directory
# Configuration Options In Detail
@ -135,6 +143,12 @@ Because of the TLS requirement, a gemini server must contain a "servertls" direc
sr-71 also supports virtualhosts in gemini, where multiple domains can be hosted on the same IP/port. This is done by having multiple "gemini {...}" blocks with the same IP and port (potentially defaults), but with "host" directives differentiating them. More on this in the section below on virtualhosting.
### spartan server
Spartan's default port is 300.
It also supports virtualhosting.
## Directives
Directives really come in two flavors: global directives and server directives. In either case, a directive is always contained on a single line, which begins (after any leading whitespace) with the type of the directive. What follows the directive type depends on that type.
@ -180,13 +194,13 @@ The "host" directive tells a server what hostname(s) it is serving. It is follow
A gopher server *must* contain a single host directive with just one hostname - this is to enable it to generate local links on generated gopher menus.
In gemini servers, host directives control virtual hosting behavior.
In gemini and spartan servers, host directives control virtual hosting behavior.
### [server] servertls
"servertls" provides servers with the paths to their TLS server credentials. It is followed by two clauses: "key <path to key file>", and "cert <path to cert file>". Both clauses are required (if a single file contains both then use that path for both clauses).
Gemini servers must always host with TLS and so require a "servertls" directive. In gopher and finger, the presence of a "servertls" directive will cause them to host their content tls-encrypted.
Gemini servers must always host with TLS and so require a "servertls" directive. In gopher and finger, the presence of a "servertls" directive will cause them to host their content tls-encrypted. It is not allowed in spartan servers.
### [server] static
@ -241,7 +255,7 @@ gemini {
}
```
"git" is not supported in finger servers, but otherwise it builds appropriate views according to the protocol of the server it is under (gemtext on gemini, gopher menu on gopher).
"git" is not supported in finger or spartan servers, but otherwise it builds appropriate views according to the protocol of the server it is under (gemtext on gemini, gopher menu on gopher).
The only supported modifier is "templates" - find more details on that in the section on "Git Viewing Templates".
@ -253,7 +267,7 @@ The routing directives "static", "cgi", and "git" support modifiers in the "with
"dirdefault" is followed by a file name, and it customizes the behavior of requests for directory paths. If the path exists and "dirdefault" is given, it will look for the provided file name within the requested directory and serve that *as the directory itself*. Think "index.html" in web servers.
Allowed contexts: static directive (neither cgi nor git), gemini or gopher servers (no finger).
Allowed contexts: static directive (neither cgi nor git), gemini, spartan, and gopher servers (no finger).
### dirlist
@ -261,7 +275,7 @@ Allowed contexts: static directive (neither cgi nor git), gemini or gopher serve
If both "dirdefault" and "dirlist" are in use then "dirdefault" will take precedence and the listing will only be built if the dirdefault filename doesn't exist.
Allowed contexts: static directive (neither cgi nor git), gemini or gopher servers (no finger).
Allowed contexts: static directive (neither cgi nor git), gemini, spartan, and gopher servers (no finger).
### exec
@ -269,7 +283,7 @@ Allowed contexts: static directive (neither cgi nor git), gemini or gopher serve
There is more detail in the section "Running CGIs" below.
Allowed contexts: static directive (neither cgi nor git), gemini/gopher/finger servers.
Allowed contexts: static directive (neither cgi nor git), gemini/gopher/spartan/finger servers.
### cmd <file path>
@ -282,13 +296,13 @@ Importantly, in all other ways it will still run as the located file:
So it can, for instance, be a good opportunity for a system administrator to impose boundaries on user CGIs in a shared hosting environment. The "cmd" script can set a nice level, increment a semaphore potentially waiting for a slot, set system resource limitations, chroot, and finally "exec ./$(basename $SCRIPT_NAME)".
Allowed contexts: "static...with exec", cgi (no git), gemini/gopher/finger servers.
Allowed contexts: "static...with exec", cgi (no git), gemini/gopher/spartan/finger servers.
### extendedgophermap
"extendedgophermap" enables lots of additional flexibility in writing the gopher menu format. The ideas are mostly borrowed from gophernicus, and sr-71's implementation is documented in more detail below in "Extended Gophermap Parsing".
Allowed contexts: static, cgi directives (no git), gopher servers (neither gemini nor finger).
Allowed contexts: static, cgi directives (no git), gopher servers (no gemini, spartan, or finger).
### autoatom
@ -296,13 +310,13 @@ The "autoatom" modifier customizes routing to recognize "<any other valid path>.
=> gemini://geminiprotocol.net/docs/companion/subscription.gmi "Subscribing to Gemini pages" gemini companion specification
Allowed contexts: static, cgi directives (no git), gemini servers (neither gopher nor finger).
Allowed contexts: static, cgi directives (no git), gemini servers (no gopher, spartan, or finger).
### titan <auth name>
The "titan" modifier takes an auth name (defined in a global "auth" directive) and enables the titan file upload protocol in a static route. Titan requests specifically will have to pass the named auth mechanism.
Allowed contexts: static (neither cgi nor git), gemini servers (neither gopher nor finger).
Allowed contexts: static (neither cgi nor git), gemini servers (no gopher, spartan, or finger).
### templates <dir path>
@ -310,7 +324,7 @@ Allowed contexts: static (neither cgi nor git), gemini servers (neither gopher n
The supported template names and their execution contexts are documented below in the section on "Git Viewing Templates".
Allowed contexts: git (neither static nor cgi), gemini or gopher servers (no finger).
Allowed contexts: git (neither static nor cgi), gemini or gopher servers (no spartan or finger).
# Git Viewing Templates
@ -449,7 +463,7 @@ When it runs a CGI process, sr-71 sets up a standard CGI environment for it, try
* TLS_CLIENT_SUBJECT is the subject field of the client TLS cert, if there is one
* TLS_CLIENT_SUBJECT_CN is the subject's common name of the client TLS cert, if there is one
Standard in of the CGI process is set to the request body, if there is one (this is only the case for titan requests). Standard out of the CGI process is used as the response body, and the default format for the protocol is assumed (gemtext for gemini, gopher menu for gopher, text/no format for finger). "extendedgophermap" is usable in conjunection with "cgi" or "static...with exec" in gopher servers, in which case the standard output will be processed with the gophermap extensions.
Standard in of the CGI process is set to the request body, if there is one (this is only the case for titan requests). Standard out of the CGI process is used as the response body, and the default format for the protocol is assumed (gemtext for gemini and spartan, gopher menu for gopher, text/no format for finger). "extendedgophermap" is usable in conjunection with "cgi" or "static...with exec" in gopher servers, in which case the standard output will be processed with the gophermap extensions.
The "cmd" modifier can be used on cgi and "static...with exec" directives, in which case the given world-executable file will be used in place of the resolved CGI program, however the environment will still be set up entirely as if the executable pointed at by the request is being run. This means the working directory is that of the located program (not the cmd override), and SCRIPT_NAME and PATH_INFO are set as if that program was being run. This means that a no-op cmd override could just "exec ./$(basename $SCRIPT_NAME)".
@ -486,7 +500,7 @@ gopher {
Gopher and Finger have no guarantee that a domain name will appear in a request. Therefore virtualhosting by domain doesn't make sense in these protocols. Multiple gopher or finger servers may still be defined in a configuration file, but they will have to appear on separate IPs and/or ports.
With Gemini, however, there can also be multiple servers defined on the same IP and port (perhaps defaults), which can be differentiated at request-time based on the requested domain and "host" directives in each server.
With Gemini and Spartan, however, there can also be multiple servers defined on the same IP and port (perhaps defaults), which can be differentiated at request-time based on the requested domain and "host" directives in each server.
```example of gemini virtualhosting
gemini {

View File

@ -133,11 +133,7 @@ func addGeminiStaticRoute(router *sr.Router, route RouteDirective) {
handlers = append(handlers, fs.GeminiDirectoryListing(route.FsPath, route.URLPath, nil))
}
var handler sr.Handler
if len(handlers) == 1 {
handler = handlers[0]
}
handler = sr.FallthroughHandler(handlers...)
handler := sr.FallthroughHandler(handlers...)
if route.Modifiers.AutoAtom {
handler = atomconv.Auto(handler)

View File

@ -109,6 +109,12 @@ func Parse(input io.ReadCloser) (*Configuration, error) {
return nil, err
}
servers = append(servers, s)
case "spartan":
s, err := parseSpartanServer(line, buf)
if err != nil {
return nil, err
}
servers = append(servers, s)
}
}
@ -221,6 +227,20 @@ func parseGeminiServer(line string, buf *bufio.Reader) (Server, error) {
return server, nil
}
func parseSpartanServer(line string, buf *bufio.Reader) (Server, error) {
server := Server{Type: "spartan"}
if err := parseServerLine(&server, line); err != nil {
return server, err
}
if err := parseServerDirectives(&server, buf); err != nil {
return server, err
}
return server, nil
}
func parseServerDirectives(server *Server, buf *bufio.Reader) error {
for {
line, err := buf.ReadString('\n')
@ -241,6 +261,9 @@ func parseServerDirectives(server *Server, buf *bufio.Reader) error {
case "host":
server.Hostnames = append(server.Hostnames, parseHost(rest)...)
case "servertls":
if server.Type == "spartan" {
return errors.New("servertls directive not allowed in spartan server")
}
if server.TLS != nil {
return fmt.Errorf("duplicate servertls directives in %s server", server.Type)
}
@ -523,6 +546,8 @@ func parseServerLine(server *Server, line string) error {
defaultPort = "79"
case "gemini":
defaultPort = "1965"
case "spartan":
defaultPort = "300"
default:
return errors.New("invalid server")
}

View File

@ -18,6 +18,8 @@ func addRoute(server Server, router *sr.Router, route RouteDirective) {
addGopherRoute(router, route)
case "gemini":
addGeminiRoute(router, route)
case "spartan":
addSpartanRoute(router, route)
default:
panic("invalid server type '" + server.Type + "'")
}

View File

@ -8,6 +8,7 @@ func buildServers(config *Configuration) ([]sr.Server, error) {
result := []sr.Server{}
geminis := []Server{}
spartans := []Server{}
for _, server := range config.Servers {
switch server.Type {
case "gopher":
@ -24,6 +25,8 @@ func buildServers(config *Configuration) ([]sr.Server, error) {
result = append(result, srv)
case "gemini":
geminis = append(geminis, server)
case "spartan":
spartans = append(spartans, server)
}
}
@ -35,5 +38,13 @@ func buildServers(config *Configuration) ([]sr.Server, error) {
result = append(result, srvs...)
}
if len(spartans) > 0 {
srvs, err := buildSpartanServers(spartans, config)
if err != nil {
return nil, err
}
result = append(result, srvs...)
}
return result, nil
}

111
spartan.go Normal file
View File

@ -0,0 +1,111 @@
package main
import (
"context"
"errors"
"fmt"
sr "tildegit.org/tjp/sliderule"
"tildegit.org/tjp/sliderule/contrib/cgi"
"tildegit.org/tjp/sliderule/contrib/fs"
"tildegit.org/tjp/sliderule/logging"
"tildegit.org/tjp/sliderule/spartan"
)
func buildSpartanServers(servers []Server, config *Configuration) ([]sr.Server, error) {
_, info, _, errlog := Loggers(config)
groups := map[string][]*Server{}
for i := range servers {
addr := fmt.Sprintf("%s:%d", servers[i].IP.String(), servers[i].Port)
grp, ok := groups[addr]
if !ok {
groups[addr] = []*Server{&servers[i]}
} else {
groups[addr] = append(grp, &servers[i])
}
}
result := []sr.Server{}
for addr, configs := range groups {
_ = info.Log("msg", "starting spartan server", "addr", addr)
var handler sr.Handler
if len(configs) == 1 {
handler = routes(*configs[0])
} else {
mapping := map[string]sr.Handler{}
for _, config := range configs {
router := routes(*config)
for _, hostname := range config.Hostnames {
mapping[hostname] = router
}
}
handler = sr.VirtualHosts(mapping, sr.HandlerFunc(func(_ context.Context, _ *sr.Request) *sr.Response {
return spartan.ClientError(errors.New("Proxy request refused"))
}))
}
var hostname string
for _, conf := range configs {
if len(conf.Hostnames) > 0 {
hostname = conf.Hostnames[0]
break
}
}
sptnsrv, err := spartan.NewServer(
context.Background(),
hostname,
"tcp",
addr,
logging.LogRequests(info)(handler),
errlog,
)
if err != nil {
return nil, err
}
result = append(result, sptnsrv)
}
return result, nil
}
func addSpartanRoute(router *sr.Router, route RouteDirective) {
switch route.Type {
case "static":
addSpartanStaticRoute(router, route)
case "cgi":
buildAndAddRoute(router, route, func(route RouteDirective) sr.Handler {
return cgi.SpartanCGIDirectory(route.FsPath, route.URLPath, route.Modifiers.ExecCmd)
})
case "git":
//TODO
}
}
func addSpartanStaticRoute(router *sr.Router, route RouteDirective) {
buildAndAddRoute(router, route, func(route RouteDirective) sr.Handler {
handlers := []sr.Handler{}
if route.Modifiers.Exec {
handlers = append(handlers, cgi.SpartanCGIDirectory(route.FsPath, route.URLPath, route.Modifiers.ExecCmd))
}
handlers = append(handlers, fs.SpartanFileHandler(route.FsPath, route.URLPath))
if route.Modifiers.DirDefault != "" {
handlers = append(
handlers,
fs.SpartanDirectoryDefault(route.FsPath, route.URLPath, route.Modifiers.DirDefault),
)
}
if route.Modifiers.DirList {
handlers = append(handlers, fs.SpartanDirectoryListing(route.FsPath, route.URLPath, nil))
}
return sr.FallthroughHandler(handlers...)
})
}