summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortjpcc <tjp@ctrl-c.club>2023-09-28 08:08:48 -0600
committertjpcc <tjp@ctrl-c.club>2023-10-09 08:47:37 -0600
commit6e1c25af361dde4c063eccbf769e966df4b65f23 (patch)
treed28044cf2db246555deda8db395f2f0a7e786590
parentb4f45f7c654e87bda6d5e7effb6ac5b5feb29ce0 (diff)
config file refactor
-rw-r--r--auth.go115
-rw-r--r--config.go102
-rw-r--r--example.conf (renamed from config/example.sr71)61
-rw-r--r--finger.go85
-rw-r--r--gemini.go151
-rw-r--r--go.mod6
-rw-r--r--gopher.go119
-rw-r--r--logging.go18
-rw-r--r--main.go103
-rw-r--r--parse.go492
-rw-r--r--privdrop.go34
-rw-r--r--routes.go157
-rw-r--r--servers.go39
-rw-r--r--types.go66
-rw-r--r--userpath.go61
15 files changed, 1265 insertions, 344 deletions
diff --git a/auth.go b/auth.go
new file mode 100644
index 0000000..c482366
--- /dev/null
+++ b/auth.go
@@ -0,0 +1,115 @@
+package main
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "io"
+ "os"
+ "os/user"
+ "path/filepath"
+ "slices"
+ "strings"
+
+ "tildegit.org/tjp/sliderule"
+ "tildegit.org/tjp/sliderule/gemini"
+)
+
+func GeminiAuthMiddleware(auth *Auth) sliderule.Middleware {
+ if auth == nil {
+ return func(inner sliderule.Handler) sliderule.Handler { return inner }
+ }
+
+ return func(inner sliderule.Handler) sliderule.Handler {
+ return sliderule.HandlerFunc(func(ctx context.Context, request *sliderule.Request) *sliderule.Response {
+ if auth.Strategy.Approve(ctx, request) {
+ return inner.Handle(ctx, request)
+ }
+
+ if len(request.TLSState.PeerCertificates) == 0 {
+ return gemini.RequireCert("client certificate required")
+ }
+ return gemini.CertAuthFailure("client certificate rejected")
+ })
+ }
+}
+
+func ClientTLSFile(path string) (AuthStrategy, error) {
+ if strings.Contains(path, "~") {
+ return UserClientTLSAuth(path), nil
+ }
+
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = f.Close() }()
+
+ contents, err := io.ReadAll(f)
+ if err != nil {
+ return nil, err
+ }
+
+ fingerprints := []string{}
+ for _, line := range strings.Split(string(contents), "\n") {
+ line = strings.Trim(line, " \t\r")
+ if len(line) == sha256.Size*2 {
+ fingerprints = append(fingerprints, line)
+ }
+ }
+ return ClientTLSAuth(fingerprints), nil
+}
+
+func ClientTLS(raw string) AuthStrategy {
+ fingerprints := []string{}
+ for _, fp := range strings.Split(raw, ",") {
+ fp = strings.Trim(fp, " \t\r")
+ if len(fp) == sha256.Size*2 {
+ fingerprints = append(fingerprints, fp)
+ }
+ }
+ return ClientTLSAuth(fingerprints)
+}
+
+type UserClientTLSAuth string
+
+func (ca UserClientTLSAuth) Approve(ctx context.Context, request *sliderule.Request) bool {
+ u, err := user.Lookup(sliderule.RouteParams(ctx)["username"])
+ if err != nil {
+ return false
+ }
+ fpath := resolveTilde(string(ca), u)
+
+ strat, err := ClientTLSFile(fpath)
+ if err != nil {
+ return false
+ }
+ return strat.Approve(ctx, request)
+}
+
+func resolveTilde(path string, u *user.User) string {
+ if strings.HasPrefix(path, "~/") {
+ return filepath.Join(u.HomeDir, path[1:])
+ }
+ return strings.ReplaceAll(path, "~", u.Username)
+}
+
+type ClientTLSAuth []string
+
+func (ca ClientTLSAuth) Approve(_ context.Context, request *sliderule.Request) bool {
+ if request.TLSState == nil || len(request.TLSState.PeerCertificates) == 0 {
+ return false
+ }
+ return slices.Contains(ca, fingerprint(request.TLSState.PeerCertificates[0].Raw))
+}
+
+func fingerprint(raw []byte) string {
+ hash := sha256.Sum256(raw)
+ return hex.EncodeToString(hash[:])
+}
+
+type HasClientTLSAuth struct{}
+
+func (_ HasClientTLSAuth) Approve(_ context.Context, request *sliderule.Request) bool {
+ return request.TLSState != nil && len(request.TLSState.PeerCertificates) > 0
+}
diff --git a/config.go b/config.go
deleted file mode 100644
index 7160c19..0000000
--- a/config.go
+++ /dev/null
@@ -1,102 +0,0 @@
-package main
-
-import (
- "context"
- "os"
- "os/signal"
- "os/user"
- "strconv"
- "strings"
- "syscall"
-
- "tildegit.org/tjp/sliderule/logging"
-)
-
-type config struct {
- hostname string
-
- geminiRoot string
- gopherRoot string
-
- geminiRepos string
- gopherRepos string
-
- tlsKeyFile string
- tlsCertFile string
-
- privilegedUsers []string
-
- fingerResponses map[string]string
-
- geminiAutoAtom bool
-}
-
-func configure() config {
- privileged := strings.Split(os.Getenv("PRIVILEGED_FINGERPRINTS"), ",")
-
- fingers := map[string]string{}
- for _, pair := range os.Environ() {
- key, val, _ := strings.Cut(pair, "=")
- if !strings.HasPrefix(key, "FINGER_") {
- continue
- }
- fingers[strings.ToLower(key[7:])] = val
- }
-
- autoatom, err := strconv.ParseBool(os.Getenv("GEMINI_AUTOATOM"))
- if err != nil {
- autoatom = false
- }
-
- return config{
- hostname: os.Getenv("HOST_NAME"),
- geminiRoot: os.Getenv("GEMINI_ROOT"),
- gopherRoot: os.Getenv("GOPHER_ROOT"),
- geminiRepos: os.Getenv("GEMINI_REPOS"),
- gopherRepos: os.Getenv("GOPHER_REPOS"),
- tlsKeyFile: os.Getenv("TLS_KEY_FILE"),
- tlsCertFile: os.Getenv("TLS_CERT_FILE"),
-
- privilegedUsers: privileged,
-
- fingerResponses: fingers,
-
- geminiAutoAtom: autoatom,
- }
-}
-
-func dropPrivileges() (bool, error) {
- me, err := user.Current()
- if err != nil {
- return false, err
- }
-
- if me.Uid != "0" {
- return false, nil
- }
-
- nobody, err := user.Lookup("nobody")
- if err != nil {
- return false, err
- }
- uid, err := strconv.Atoi(nobody.Uid)
- if err != nil {
- return false, err
- }
-
- if err := syscall.Setuid(uid); err != nil {
- return false, err
- }
- return true, nil
-}
-
-func serverContext() (context.Context, logging.Logger, logging.Logger, logging.Logger, logging.Logger) {
- debug, info, warn, err := logging.DefaultLoggers()
- ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGHUP)
- ctx = context.WithValue(ctx, "debuglog", debug) //nolint:staticcheck
- ctx = context.WithValue(ctx, "infolog", info) //nolint:staticcheck
- ctx = context.WithValue(ctx, "warnlog", warn) //nolint:staticcheck
- ctx = context.WithValue(ctx, "errorlog", err) //nolint:staticcheck
-
- return ctx, debug, info, warn, err
-}
diff --git a/config/example.sr71 b/example.conf
index 6c1a977..6181fcf 100644
--- a/config/example.sr71
+++ b/example.conf
@@ -1,3 +1,26 @@
+# "auth" is a global directive that defines a named authentication strategy.
+# The "auth" keyword is followed by a name, and then the strategy.
+# "clienttlsfile" is a strategy which takes a path to a file which contains line-delimited SHA256 fingerprints of client certificates.
+# Tildes (~) are allowed in the file path, in which case the strategy is only usable in a ~user-scoped directive.
+auth private_gemini clienttlsfile ~/.private_gemini
+
+# The "clienttls" strategy takes comma-separated SHA256 fingerprints of client certificates.
+auth is_tony clienttls 0284bcb38d7c98548df4a67587163276373ea8f9a8cc931a89f475557bd9f3a3
+
+# The "hasclienttls" strategy requires only that the request be made with a client certificate.
+auth is_named hasclienttls
+
+# "systemuser" is a global directive which controls privilege dropping.
+# After performing some root-only actions (binding to gopher and finger ports, reading server key and certificate files),
+# sr-71 will attempt to change its effective user to the named user (which may be a numeric user id).
+# Alternatively, sr-71 can work when started as a non-root user but the "systemuser" directive shouldn't be used, and it won't be able to serve any protocol on privileged ports.
+systemuser nobody
+
+# "loglevel" defines the minimum log level that will be sent to stdout.
+# Allowed values are "debug", "info", "warn", "error".
+# Omitting the "loglevel" directive allows all logs through, equivalent to "loglevel debug".
+loglevel debug
+
# define a gopher server
# This IP/port is the default, both components are optional.
# To specify a port without changing the IP default, write it like ":70".
@@ -6,6 +29,9 @@ gopher 0.0.0.0:70 {
# It will be used for internal links, such as in directory listings.
host tjp.lol
+ # A gopher server may include a single "servertls" directive like gemini (example below).
+ # In that case the gopher server will host encrypted gopher with TLS.
+
# The "static" directive exposes a filesystem directory at a given path prefix.
# It will only serve files which are world-readable.
# "with" introduces comma-separated modifiers to a directive.
@@ -17,15 +43,15 @@ gopher 0.0.0.0:70 {
# The "cgi" directive exposes a filesystem directory at a path prefix as well but executes requested files.
# It will only execute world-executable files.
- # It supports the "extendedgophermap" and "dirdefault" modifiers.
+ # It supports only the "extendedgophermap" modifier.
# Executed files are assumed to produce gophermap, although the "extendedgophermap" modifier can make this more friendly.
cgi /var/gopher/cgi at /cgi-bin with extendedgophermap
# Directives which result in exposing a filesystem directory may include tilde (~) characters.
- # It must be present in both the path prefix and the filesystem path, or neither.
+ # It must be present in both the path prefix and the filesystem path, or neither (not one without the other).
# In the path prefix it will match a "~username" path segment and the user name will be captured.
- # If the filesystem path begins with the ~ character it represents the user's home directory.
- # Otherwise, it will be replaced by the user's name.
+ # If the filesystem path begins with "~/", it represents the user's home directory.
+ # Otherwise, the tilde will be replaced by the user's name.
# So on a system where users' home directories are at /home, "/home/~" and "~" are the same (though the latter is more general).
static ~/public_gopher at /~ with dirdefault gophermap, dirlist, exec, extendedgophermap
cgi ~/public_gopher/cgi-bin at /~/cgi-bin
@@ -62,27 +88,18 @@ gemini 0.0.0.0:1965 {
static /var/gemini/docs at / with dirdefault index.gmi, dirlist, exec
cgi /var/gemini/cgi at /cgi-bin
- static ~/public_gemini at /~ with dirdefault index.gmi, dirlist, exec
+ # The "autoatom" modifier is allowed on directives in a gemini server.
+ # It causes any text/gemini responses to be available as atom at <path>.atom.
+ # It uses the "Subscribing to Gemini pages" spec (gemini://geminiprotocol.net/docs/companion/subscription.gmi)
+ # to convert the text/gemini to Atom.
+ # The "titan" modifier allows uploads to world-writable directories.
+ # It can only be used on "static" directives in gemini servers.
+ # It takes a required auth name which will guard just titan requests.
+ static ~/public_gemini at /~ with dirdefault index.gmi, dirlist, exec, autoatom, titan private_gemini
cgi ~/public_gemini/cgi-bin at /~/cgi-bin
- # "titan" enables uploads into to (over-)write into world-writable directories.
- # It REQUIRES an "auth" clause that references an auth directive.
- titan ~/public_gemini at /~ auth private_gemini
-
- # "static" and "cgi" directives support an "auth <name>" clause which requires that an authentication pass.
+ # "static", "cgi", and "git" directives support an "auth <name>" clause which requires that an authentication to pass.
cgi ~/public_gemini/cgi-bin/private at /~/cgi-bin/private auth private_gemini
git ~/code at /~/code
}
-
-# "auth" is a global directive that defines a named authentication strategy.
-# The "auth" keyword is followed by a name, and then the strategy.
-# "clienttlsfile" is a strategy which takes a path to a file which contains line-delimited SHA256 fingerprints of client certificates.
-# Tildes (~) are allowed in the file path, in which case the strategy is only usable in a ~user-scoped directive.
-auth private_gemini clienttlsfile ~/.private_gemini
-
-# The "clienttls" strategy takes comma-separated SHA256 fingerprints of client certificates.
-auth is_tony clienttls 0284bcb38d7c98548df4a67587163276373ea8f9a8cc931a89f475557bd9f3a3
-
-# The "hasclienttls" strategy requires only that the request be made with a client certificate.
-auth is_named hasclienttls
diff --git a/finger.go b/finger.go
new file mode 100644
index 0000000..6ae82dd
--- /dev/null
+++ b/finger.go
@@ -0,0 +1,85 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/user"
+ "path/filepath"
+ "strings"
+
+ sr "tildegit.org/tjp/sliderule"
+ "tildegit.org/tjp/sliderule/contrib/cgi"
+ "tildegit.org/tjp/sliderule/finger"
+ "tildegit.org/tjp/sliderule/logging"
+)
+
+func buildFingerServer(server Server, config *Configuration) (sr.Server, error) {
+ addr := fmt.Sprintf("%s:%d", server.IP.String(), server.Port)
+
+ _, info, _, errlog := Loggers(config)
+ _ = info.Log("msg", "building finger server", "addr", addr)
+
+ if len(server.Routes) != 1 {
+ return nil, fmt.Errorf("finger server must have 1 route directive, found %d", len(server.Routes))
+ }
+
+ return finger.NewServer(
+ context.Background(),
+ "",
+ "tcp",
+ addr,
+ logging.LogRequests(info)(fingerHandler(server.Routes[0])),
+ errlog,
+ )
+}
+
+func fingerHandler(route RouteDirective) sr.Handler {
+ if route.Type != "static" && route.Type != "cgi" {
+ panic("invalid finger route type '" + route.Type + "'")
+ }
+
+ return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
+ u, err := user.Lookup(strings.TrimPrefix(request.Path, "/"))
+ if err != nil {
+ return nil
+ }
+
+ var fpath string
+ if strings.HasPrefix(route.FsPath, "~/") {
+ fpath = filepath.Join(u.HomeDir, route.FsPath[2:])
+ } else {
+ fpath = strings.Replace(route.FsPath, "~", u.Username, 1)
+ }
+
+ st, err := os.Stat(fpath)
+ if err != nil {
+ return finger.Error(err.Error())
+ }
+ if !st.Mode().IsRegular() {
+ return nil
+ }
+
+ if st.Mode()&5 == 5 && (route.Modifiers.Exec || route.Type == "cgi") {
+ buf, code, err := cgi.RunCGI(ctx, request, fpath, "/", nil)
+ if err != nil {
+ return finger.Error("execution error")
+ }
+ if code != 0 {
+ return finger.Error(fmt.Sprintf("execution error: code %d", code))
+ }
+
+ return finger.Success(buf)
+ }
+
+ if route.Type != "static" {
+ return nil
+ }
+
+ file, err := os.Open(fpath)
+ if err != nil {
+ return finger.Error(err.Error())
+ }
+ return finger.Success(file)
+ })
+}
diff --git a/gemini.go b/gemini.go
new file mode 100644
index 0000000..fd5dec5
--- /dev/null
+++ b/gemini.go
@@ -0,0 +1,151 @@
+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/gemini"
+ "tildegit.org/tjp/sliderule/gemini/gemtext/atomconv"
+ "tildegit.org/tjp/sliderule/logging"
+ "tildegit.org/tjp/syw"
+)
+
+func buildGeminiServers(servers []Server, config *Configuration) ([]sr.Server, error) {
+ _, info, _, errlog := Loggers(config)
+ _ = info.Log("msg", "building gemini servers", "count", len(servers))
+
+ 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", "building gemini 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
+ }
+ }
+
+ var catchall sr.Handler
+ if len(configs[0].Hostnames) > 0 {
+ catchall = mapping[configs[0].Hostnames[0]]
+ }
+
+ handler = sr.VirtualHosts(mapping, catchall)
+ }
+
+ var hostname string
+ for _, conf := range configs {
+ if len(conf.Hostnames) > 0 {
+ hostname = conf.Hostnames[0]
+ break
+ }
+ }
+
+ if configs[0].TLS == nil {
+ return nil, errors.New("gemini server must have a servertls directive")
+ }
+
+ gemsrv, err := gemini.NewServer(
+ context.Background(),
+ hostname,
+ "tcp",
+ addr,
+ logging.LogRequests(info)(handler),
+ errlog,
+ configs[0].TLS,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ result = append(result, gemsrv)
+ }
+
+ return result, nil
+}
+
+func addGeminiRoute(router *sr.Router, route RouteDirective) {
+ switch route.Type {
+ case "static":
+ addGeminiStaticRoute(router, route)
+ case "cgi":
+ buildAndAddRoute(router, route, func(route RouteDirective) sr.Handler {
+ handler := cgi.GeminiCGIDirectory(route.FsPath, route.URLPath)
+ if route.Modifiers.AutoAtom {
+ handler = atomconv.Auto(handler)
+ }
+ return GeminiAuthMiddleware(route.Auth)(handler)
+ })
+ case "git":
+ buildAndAddRoute(router, route, func(route RouteDirective) sr.Handler {
+ var handler sr.Handler = syw.GeminiRouter(route.FsPath, nil)
+ if route.Modifiers.AutoAtom {
+ handler = atomconv.Auto(handler)
+ }
+ return GeminiAuthMiddleware(route.Auth)(handler)
+ })
+ }
+}
+
+func addGeminiStaticRoute(router *sr.Router, route RouteDirective) {
+ buildAndAddRoute(router, route, func(route RouteDirective) sr.Handler {
+ handlers := []sr.Handler{}
+
+ if route.Modifiers.Exec {
+ handlers = append(handlers, cgi.GeminiCGIDirectory(route.FsPath, route.URLPath))
+ }
+
+ handlers = append(handlers, fs.GeminiFileHandler(route.FsPath, route.URLPath))
+
+ if route.Modifiers.DirDefault != "" {
+ handlers = append(
+ handlers,
+ fs.GeminiDirectoryDefault(route.FsPath, route.URLPath, route.Modifiers.DirDefault),
+ )
+ }
+
+ if route.Modifiers.DirList {
+ 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...)
+
+ if route.Modifiers.AutoAtom {
+ handler = atomconv.Auto(handler)
+ }
+
+ handler = GeminiAuthMiddleware(route.Auth)(handler)
+
+ if route.Modifiers.Titan != nil {
+ titan := fs.TitanUpload(route.FsPath, route.URLPath, route.Modifiers.Titan.Strategy.Approve)(handler)
+ handler = sr.Filter(func(ctx context.Context, request *sr.Request) bool {
+ return request.Scheme == "titan"
+ }, handler)(titan)
+ }
+
+ return handler
+ })
+}
diff --git a/go.mod b/go.mod
index 556f2ea..97eef39 100644
--- a/go.mod
+++ b/go.mod
@@ -3,11 +3,9 @@ module tildegit.org/tjp/sr-71
go 1.21.0
require (
+ github.com/go-kit/log v0.2.1
tildegit.org/tjp/sliderule v1.3.4-0.20230923191849-09c482d5016c
tildegit.org/tjp/syw v0.9.1-0.20230923192020-d5566a4ed9ad
)
-require (
- github.com/go-kit/log v0.2.1 // indirect
- github.com/go-logfmt/logfmt v0.6.0 // indirect
-)
+require github.com/go-logfmt/logfmt v0.6.0 // indirect
diff --git a/gopher.go b/gopher.go
new file mode 100644
index 0000000..5e36266
--- /dev/null
+++ b/gopher.go
@@ -0,0 +1,119 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ 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/gopher/gophermap"
+ "tildegit.org/tjp/sliderule/logging"
+ "tildegit.org/tjp/syw"
+)
+
+func buildGopherServer(server Server, config *Configuration) (sr.Server, error) {
+ addr := fmt.Sprintf("%s:%d", server.IP.String(), server.Port)
+
+ _, info, _, errlog := Loggers(config)
+ _ = info.Log("msg", "building gopher server", "addr", addr)
+
+ return gopher.NewServer(
+ context.Background(),
+ server.Hostnames[0],
+ "tcp",
+ addr,
+ logging.LogRequests(info)(routes(server)),
+ errlog,
+ )
+}
+
+func addGopherRoute(router *sr.Router, route RouteDirective) {
+ switch route.Type {
+ case "static":
+ addGopherStaticRoute(router, route)
+ case "cgi":
+ addGopherCGIRoute(router, route)
+ case "git":
+ addGopherGitRoute(router, route)
+ }
+}
+
+func addGopherStaticRoute(router *sr.Router, route RouteDirective) {
+ dirmaps := []string{}
+ if route.Modifiers.DirDefault != "" {
+ dirmaps = append(dirmaps, route.Modifiers.DirDefault)
+ }
+ settings := &gophermap.FileSystemSettings{
+ ParseExtended: route.Modifiers.ExtendedGophermap,
+ Exec: route.Modifiers.Exec,
+ ListUsers: false,
+ DirMaps: dirmaps,
+ }
+
+ buildAndAddRoute(router, route, func(route RouteDirective) sr.Handler {
+ handlers := []sr.Handler{}
+
+ if route.Modifiers.Exec {
+ handlers = append(handlers, cgi.ExecGopherMaps(route.FsPath, route.URLPath, settings))
+ }
+
+ handlers = append(handlers, fs.GopherFileHandler(route.FsPath, route.URLPath, settings))
+
+ if route.Modifiers.DirDefault != "" {
+ handlers = append(handlers, fs.GopherDirectoryDefault(route.FsPath, route.URLPath, settings))
+ }
+
+ if route.Modifiers.DirList {
+ handlers = append(handlers, fs.GopherDirectoryListing(route.FsPath, route.URLPath, settings))
+ }
+
+ if len(handlers) == 1 {
+ return handlers[0]
+ }
+ return sr.FallthroughHandler(handlers...)
+ })
+}
+
+func addGopherCGIRoute(router *sr.Router, route RouteDirective) {
+ dirmaps := []string{}
+ if route.Modifiers.DirDefault != "" {
+ dirmaps = append(dirmaps, route.Modifiers.DirDefault)
+ }
+ settings := &gophermap.FileSystemSettings{
+ ParseExtended: route.Modifiers.ExtendedGophermap,
+ Exec: true,
+ ListUsers: false,
+ DirMaps: dirmaps,
+ }
+
+ buildAndAddRoute(router, route, func(route RouteDirective) sr.Handler {
+ return cgi.GopherCGIDirectory(route.FsPath, route.URLPath, settings)
+ })
+}
+
+func addGopherGitRoute(router *sr.Router, route RouteDirective) {
+ buildAndAddRoute(router, route, func(route RouteDirective) sr.Handler {
+ return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
+ subrouter := syw.GopherRouter(route.FsPath, nil)
+
+ reqclone := cloneRequest(request)
+ reqclone.Path = strings.TrimPrefix(reqclone.Path, route.URLPath)
+
+ handler, params := subrouter.Match(reqclone)
+ if handler == nil {
+ return nil
+ }
+ return handler.Handle(context.WithValue(ctx, sr.RouteParamsKey, params), request)
+ })
+ })
+}
+
+func cloneRequest(request *sr.Request) *sr.Request {
+ r := *request
+ u := *request.URL
+ r.URL = &u
+ return &r
+}
diff --git a/logging.go b/logging.go
new file mode 100644
index 0000000..9221b24
--- /dev/null
+++ b/logging.go
@@ -0,0 +1,18 @@
+package main
+
+import (
+ "sync"
+
+ "github.com/go-kit/log/level"
+ "tildegit.org/tjp/sliderule/logging"
+)
+
+var (
+ once sync.Once
+ debug, info, warn, errlog logging.Logger
+)
+
+func Loggers(config *Configuration) (logging.Logger, logging.Logger, logging.Logger, logging.Logger) {
+ base := level.NewFilter(logging.Base(), level.Allow(config.LogLevel))
+ return level.Debug(base), level.Info(base), level.Warn(base), level.Error(base)
+}
diff --git a/main.go b/main.go
index 69ec021..b6bd5de 100644
--- a/main.go
+++ b/main.go
@@ -1,96 +1,57 @@
package main
import (
- "context"
- "crypto/tls"
+ "fmt"
+ "io"
"log"
+ "os"
"sync"
-
- sr "tildegit.org/tjp/sliderule"
- "tildegit.org/tjp/sliderule/finger"
- "tildegit.org/tjp/sliderule/gemini"
- "tildegit.org/tjp/sliderule/gopher"
- "tildegit.org/tjp/sliderule/logging"
)
func main() {
- conf := configure()
- gemTLS, err := gemini.FileTLS(conf.tlsCertFile, conf.tlsKeyFile)
- if err != nil {
- log.Fatal(err)
+ if len(os.Args) < 2 {
+ fmt.Fprintf(os.Stderr, "%s <configfile>\n", os.Args[0])
+ os.Exit(1)
}
- ctx, _, info, warn, errlog := serverContext()
-
- gopherSrv, err := buildGopher(ctx, conf, info, errlog)
- if err != nil {
- log.Fatal(err)
+ var configfile io.ReadCloser
+ if os.Args[1] == "-" {
+ configfile = os.Stdin
+ } else {
+ var err error
+ configfile, err = os.Open(os.Args[1])
+ if err != nil {
+ log.Fatal(err)
+ }
}
- fingerSrv, err := buildFinger(ctx, conf, info, errlog)
+ config, err := Parse(configfile)
if err != nil {
log.Fatal(err)
}
- dropped, err := dropPrivileges()
+ _, _, _, errlog := Loggers(config)
+
+ servers, err := buildServers(config)
if err != nil {
- log.Fatal(err)
- }
- if !dropped {
- _ = warn.Log("msg", "dropping privileges to 'nobody' failed")
+ _ = errlog.Log("msg", "error building servers", "err", err)
+ os.Exit(1)
}
- gemSrv, err := buildGemini(ctx, conf, gemTLS, info, errlog)
- if err != nil {
- log.Fatal(err)
+ if err := privdrop(config); err != nil {
+ _ = errlog.Log("msg", "failed to drop privileges", "err", err)
+ os.Exit(1)
}
wg := &sync.WaitGroup{}
-
-
- wg.Add(3)
- go runServer(gemSrv, wg)
- go runServer(gopherSrv, wg)
- go runServer(fingerSrv, wg)
+ wg.Add(len(servers))
+ for i := range servers {
+ server := servers[i]
+ go func() {
+ defer wg.Done()
+ _ = server.Serve()
+ }()
+ }
wg.Wait()
}
-
-func buildGemini(
- ctx context.Context,
- conf config,
- gemTLS *tls.Config,
- infolog logging.Logger,
- errlog logging.Logger,
-) (sr.Server, error) {
- handler := logging.LogRequests(infolog)(geminiRouter(conf))
- infolog.Log("msg", "starting gemini server", "gemini_root", conf.geminiRoot)
- return gemini.NewServer(ctx, conf.hostname, "tcp", "", handler, errlog, gemTLS)
-}
-
-func buildGopher(
- ctx context.Context,
- conf config,
- infolog logging.Logger,
- errlog logging.Logger,
-) (sr.Server, error) {
- handler := logging.LogRequests(infolog)(gopherRouter(conf))
- infolog.Log("msg", "starting gopher server", "gopher_root", conf.gopherRoot)
- return gopher.NewServer(ctx, conf.hostname, "tcp", "", handler, errlog)
-}
-
-func buildFinger(
- ctx context.Context,
- conf config,
- infolog logging.Logger,
- errlog logging.Logger,
-) (sr.Server, error) {
- handler := logging.LogRequests(infolog)(fingerHandler(conf))
- infolog.Log("msg", "starting finger server", "finger_users", len(conf.fingerResponses))
- return finger.NewServer(ctx, conf.hostname, "tcp", "", handler, errlog)
-}
-
-func runServer(server sr.Server, wg *sync.WaitGroup) error {
- defer wg.Done()
- return server.Serve()
-}
diff --git a/parse.go b/parse.go
new file mode 100644
index 0000000..0fe3f30
--- /dev/null
+++ b/parse.go
@@ -0,0 +1,492 @@
+package main
+
+import (
+ "bufio"
+ "crypto/tls"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "os/user"
+ "strconv"
+ "strings"
+
+ "github.com/go-kit/log/level"
+ "tildegit.org/tjp/sliderule/gemini"
+)
+
+func Parse(input io.ReadCloser) (*Configuration, error) {
+ defer func() { _ = input.Close() }()
+
+ var (
+ auths = map[string]*Auth{}
+ suser *user.User = nil
+ sawsuser = false
+ loglevel level.Value = level.DebugValue()
+ sawloglevel = false
+ servers = []Server{}
+ )
+
+ eof := false
+ buf := bufio.NewReader(input)
+ for !eof {
+ line, err := buf.ReadString('\n')
+ eof = errors.Is(err, io.EOF)
+ if err != nil && !eof {
+ return nil, err
+ }
+
+ line = strings.TrimRight(strings.TrimLeft(line, " \t"), "\n")
+
+ if line == "" || line[0] == '#' {
+ continue
+ }
+
+ spl := strings.Split(line, " ")
+ switch spl[0] {
+ case "auth":
+ auth, err := parseAuthDirective(spl[1:])
+ if err != nil {
+ return nil, err
+ }
+ if _, ok := auths[auth.Name]; ok {
+ return nil, fmt.Errorf("repeated 'auth %s' directive", auth.Name)
+ }
+ auths[auth.Name] = &auth
+ case "systemuser":
+ if sawsuser {
+ return nil, errors.New("repeated 'systemuser' directive")
+ }
+ sawsuser = true
+ if len(spl) != 2 {
+ return nil, errors.New("invalid 'systemuser' directive")
+ }
+ lookup := user.Lookup
+ _, err := strconv.Atoi(spl[1])
+ if err == nil {
+ lookup = user.LookupId
+ }
+ suser, err = lookup(spl[1])
+ if err != nil {
+ return nil, err
+ }
+ case "loglevel":
+ if sawloglevel {
+ return nil, errors.New("repeated 'loglevel' directive")
+ }
+ sawloglevel = true
+ if len(spl) != 2 {
+ return nil, errors.New("invalid 'loglevel' directive")
+ }
+ switch spl[1] {
+ case "debug":
+ case "info":
+ loglevel = level.InfoValue()
+ case "warn":
+ loglevel = level.WarnValue()
+ case "error":
+ loglevel = level.ErrorValue()
+ default:
+ return nil, errors.New("invalid 'loglevel' directive")
+ }
+ case "gopher":
+ s, err := parseGopherServer(line, buf)
+ if err != nil {
+ return nil, err
+ }
+ servers = append(servers, s)
+ case "finger":
+ s, err := parseFingerServer(line, buf)
+ if err != nil {
+ return nil, err
+ }
+ servers = append(servers, s)
+ case "gemini":
+ s, err := parseGeminiServer(line, buf)
+ if err != nil {
+ return nil, err
+ }
+ servers = append(servers, s)
+ }
+ }
+
+ for i := range servers {
+ for j := range servers[i].Routes {
+ if name := servers[i].Routes[j].authName; name != "" {
+ auth, ok := auths[name]
+ if !ok {
+ return nil, fmt.Errorf("auth '%s' not found", name)
+ }
+ servers[i].Routes[j].Auth = auth
+ }
+
+ if name := servers[i].Routes[j].Modifiers.titanName; name != "" {
+ auth, ok := auths[name]
+ if !ok {
+ return nil, fmt.Errorf("auth '%s' not found", name)
+ }
+ servers[i].Routes[j].Modifiers.Titan = auth
+ }
+ }
+ }
+
+ return &Configuration{
+ SystemUser: suser,
+ LogLevel: loglevel,
+ Servers: servers,
+ }, nil
+}
+
+var invalidAuth = errors.New("invalid 'auth' directive")
+
+func parseAuthDirective(info []string) (Auth, error) {
+ if len(info) < 2 {
+ return Auth{}, invalidAuth
+ }
+ auth := Auth{Name: info[0]}
+
+ switch info[1] {
+ case "clienttlsfile":
+ if len(info) < 3 {
+ return auth, invalidAuth
+ }
+ strat, err := ClientTLSFile(info[2])
+ if err != nil {
+ return auth, err
+ }
+ auth.Strategy = strat
+ case "clienttls":
+ if len(info) < 3 {
+ return auth, invalidAuth
+ }
+ auth.Strategy = ClientTLS(info[2])
+ case "hasclienttls":
+ auth.Strategy = HasClientTLSAuth{}
+ default:
+ return auth, invalidAuth
+ }
+
+ return auth, nil
+}
+
+func parseGopherServer(line string, buf *bufio.Reader) (Server, error) {
+ server := Server{Type: "gopher"}
+
+ if err := parseServerLine(&server, line); err != nil {
+ return server, err
+ }
+
+ if err := parseServerDirectives(&server, buf); err != nil {
+ return server, err
+ }
+
+ if len(server.Hostnames) != 1 {
+ return server, fmt.Errorf("gopher server expects 1 hostname, got %d", len(server.Hostnames))
+ }
+
+ return server, nil
+}
+
+func parseFingerServer(line string, buf *bufio.Reader) (Server, error) {
+ server := Server{Type: "finger"}
+
+ if err := parseServerLine(&server, line); err != nil {
+ return server, err
+ }
+
+ if err := parseServerDirectives(&server, buf); err != nil {
+ return server, err
+ }
+
+ if len(server.Hostnames) != 0 {
+ return server, errors.New("finger servers don't support 'host' directive")
+ }
+
+ return server, nil
+}
+
+func parseGeminiServer(line string, buf *bufio.Reader) (Server, error) {
+ server := Server{Type: "gemini"}
+
+ 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')
+ if err != nil {
+ return err //EOF is unexpected inside a server
+ }
+ line = strings.Trim(line, " \t\n")
+ if line == "" || line[0] == '#' {
+ continue
+ }
+
+ if strings.HasPrefix(line, "}") {
+ break
+ }
+
+ tag, rest, _ := strings.Cut(line, " ")
+ switch tag {
+ case "host":
+ server.Hostnames = append(server.Hostnames, parseHost(rest)...)
+ case "servertls":
+ server.TLS, err = parseServerTLS(rest)
+ if err != nil {
+ return err
+ }
+ case "static", "cgi", "git":
+ dir, err := parseRouteDirective(line)
+ if err != nil {
+ return err
+ }
+ if err := validateRoute(server.Type, &dir); err != nil {
+ return err
+ }
+ server.Routes = append(server.Routes, dir)
+ default:
+ return fmt.Errorf("'%s' directives not supported in %s servers", tag, server.Type)
+ }
+ }
+
+ return nil
+}
+
+func validateRoute(serverType string, dir *RouteDirective) error {
+ if dir.Type == "git" && !dir.Modifiers.Empty() {
+ return errors.New("git directives don't support 'with' modifiers")
+ }
+
+ if dir.Type == "cgi" && (dir.Modifiers.Exec || dir.Modifiers.DirList || dir.Modifiers.DirDefault != "") {
+ return errors.New("cgi directives only support the 'extendedgophermap' modifier")
+ }
+
+ if serverType == "finger" && (dir.Modifiers.DirDefault != "" || dir.Modifiers.DirList) {
+ return errors.New("finger servers don't support directory 'with' modifiers")
+ }
+ if serverType == "finger" && dir.Type != "static" && dir.Type != "cgi" {
+ return fmt.Errorf("finger servers don't support '%s' directives", dir.Type)
+ }
+ if serverType == "finger" && dir.authName != "" {
+ return errors.New("finger servers don't support 'auth' clauses")
+ }
+ if serverType != "finger" && dir.URLPath == "" {
+ return fmt.Errorf("url routes required in %s servers", serverType)
+ }
+
+ if serverType != "gopher" && dir.Modifiers.ExtendedGophermap {
+ return errors.New("'with extendedgophermap' outside gopher server")
+ }
+
+ if serverType != "gemini" && dir.Modifiers.AutoAtom {
+ return fmt.Errorf("%s servers don't support 'with autoatom'", serverType)
+ }
+ if dir.Modifiers.titanName != "" && (serverType != "gemini" || dir.Type != "static") {
+ return fmt.Errorf("titan modifier only allowed on gemini{static}")
+ }
+
+ return nil
+}
+
+func parseServerTLS(text string) (*tls.Config, error) {
+ spl := strings.Split(text, " ")
+ if len(spl) != 4 {
+ return nil, errors.New("invalid 'servertls' directive")
+ }
+
+ if spl[0] == "cert" {
+ spl[0], spl[1], spl[2], spl[3] = spl[2], spl[3], spl[0], spl[1]
+ }
+ if spl[0] != "key" || spl[2] != "cert" {
+ return nil, errors.New("invalid 'servertls' directive")
+ }
+
+ return gemini.FileTLS(spl[3], spl[1])
+}
+
+func parseHost(text string) []string {
+ hosts := []string{}
+ for _, segment := range strings.Split(text, ",") {
+ segment = strings.Trim(segment, " \t")
+ if segment == "" {
+ continue
+ }
+ hosts = append(hosts, segment)
+ }
+ return hosts
+}
+
+func parseRouteDirective(line string) (RouteDirective, error) {
+ dir := RouteDirective{}
+
+ tag, rest, found := strings.Cut(line, " ")
+ if !found {
+ return dir, fmt.Errorf("invalid '%s' directive", tag)
+ }
+ dir.Type = tag
+
+ fspath, rest, _ := strings.Cut(rest, " ")
+ dir.FsPath = fspath
+ if rest == "" {
+ return dir, nil
+ }
+
+ word, rest, found := strings.Cut(rest, " ")
+ if found && word == "at" {
+ var urlpath string
+ urlpath, rest, _ = strings.Cut(rest, " ")
+ dir.URLPath = urlpath
+ } else if found {
+ rest = word + " " + rest
+ }
+
+ for rest != "" {
+ word, rest, found = strings.Cut(rest, " ")
+ if !found {
+ return dir, fmt.Errorf("invalid '%s' directive", tag)
+ }
+
+ var err error
+ if word == "with" {
+ dir.Modifiers, rest, err = parseModifiers(rest)
+ } else if word == "auth" {
+ dir.authName, rest, err = parseAuth(rest)
+ }
+ if err != nil {
+ return dir, err
+ }
+ }
+
+ return dir, nil
+}
+
+func parseModifiers(text string) (Modifiers, string, error) {
+ text = strings.TrimPrefix(text, "with ")
+ mod := Modifiers{}
+
+ for {
+ idx := strings.IndexAny(text, " \t,")
+ if idx == 0 {
+ text = text[1:]
+ continue
+ }
+
+ var item, sep string
+ if idx > 0 {
+ item = text[:idx]
+ sep = string(text[idx])
+ text = text[idx+1:]
+ } else {
+ item = text
+ sep = ""
+ text = ""
+ }
+
+ switch item {
+ case "dirdefault":
+ if sep != " " {
+ return mod, "", errors.New("invalid 'dirdefault' clause")
+ }
+ text = strings.TrimLeft(text, " \t")
+ idx = strings.IndexAny(text, " \t,")
+ if idx == 0 {
+ return mod, "", errors.New("invalid 'dirdefault' clause")
+ } else if idx < 0 {
+ mod.DirDefault = text
+ text = ""
+ } else {
+ mod.DirDefault = text[0:idx]
+ text = text[idx+1:]
+ }
+ case "dirlist":
+ mod.DirList = true
+ case "exec":
+ mod.Exec = true
+ case "extendedgophermap":
+ mod.ExtendedGophermap = true
+ case "autoatom":
+ mod.AutoAtom = true
+ case "titan":
+ if sep != " " {
+ return mod, "", errors.New("invalid 'titan' clause")
+ }
+ text = strings.TrimLeft(text, " \t")
+ idx = strings.IndexAny(text, " \t,")
+ if idx == 0 {
+ return mod, "", errors.New("invalid 'titan' clause")
+ } else if idx < 0 {
+ mod.titanName = text
+ text = ""
+ } else {
+ mod.titanName = text[0:idx]
+ text = text[idx+1:]
+ }
+ default:
+ return mod, text, nil
+ }
+ }
+}
+
+func parseAuth(text string) (string, string, error) {
+ spl := strings.SplitN(text, " ", 2)
+ switch len(spl) {
+ case 1:
+ return spl[0], "", nil
+ case 2:
+ return spl[0], spl[1], nil
+ default:
+ return "", "", errors.New("invalid auth clause")
+ }
+}
+
+func parseServerLine(server *Server, line string) error {
+ if !strings.HasSuffix(line, "{") {
+ return errors.New("invalid server")
+ }
+
+ var ipstr, portstr string
+ defaultIP := "0.0.0.0"
+ var defaultPort string
+
+ line = strings.TrimRight(strings.TrimSuffix(line, "{"), " ")
+ tag, rest, found := strings.Cut(line, " ")
+ switch tag {
+ case "gopher":
+ defaultPort = "70"
+ case "finger":
+ defaultPort = "79"
+ case "gemini":
+ defaultPort = "1965"
+ default:
+ return errors.New("invalid server")
+ }
+ if !found {
+ ipstr = defaultIP
+ portstr = defaultPort
+ } else {
+ ipstr, portstr, found = strings.Cut(rest, ":")
+ if !found {
+ portstr = defaultPort
+ }
+ if ipstr == "" {
+ ipstr = "0.0.0.0"
+ }
+ }
+
+ port, err := strconv.ParseUint(portstr, 10, 16)
+ if err != nil {
+ return err
+ }
+
+ server.IP = net.ParseIP(ipstr)
+ server.Port = uint16(port)
+ return nil
+}
diff --git a/privdrop.go b/privdrop.go
new file mode 100644
index 0000000..c866430
--- /dev/null
+++ b/privdrop.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "os/user"
+ "strconv"
+ "syscall"
+)
+
+func privdrop(config *Configuration) error {
+ if config.SystemUser == nil {
+ return nil
+ }
+
+ current, err := user.Current()
+ if err != nil {
+ return fmt.Errorf("looking up current user: %w", err)
+ }
+ if current.Uid != "0" {
+ return errors.New("'systemuser' directive requires running as root user")
+ }
+
+ uid, err := strconv.Atoi(config.SystemUser.Uid)
+ if err != nil {
+ return errors.New("invalid 'systemuser' directive")
+ }
+
+ if err := syscall.Setuid(uid); err != nil {
+ return fmt.Errorf("setuid: %w", err)
+ }
+
+ return nil
+}
diff --git a/routes.go b/routes.go
index 83f2868..f1f57fd 100644
--- a/routes.go
+++ b/routes.go
@@ -1,157 +1,24 @@
package main
import (
- "context"
- "crypto/sha256"
- "crypto/x509"
- "encoding/hex"
- "os"
- "path/filepath"
- "sort"
- "strings"
-
sr "tildegit.org/tjp/sliderule"
- "tildegit.org/tjp/sliderule/contrib/cgi"
- "tildegit.org/tjp/sliderule/contrib/fs"
- "tildegit.org/tjp/sliderule/contrib/tlsauth"
- "tildegit.org/tjp/sliderule/finger"
- "tildegit.org/tjp/sliderule/gemini"
- "tildegit.org/tjp/sliderule/gemini/gemtext/atomconv"
- "tildegit.org/tjp/sliderule/gopher/gophermap"
- "tildegit.org/tjp/sliderule/logging"
- "tildegit.org/tjp/syw"
)
-func geminiRouter(conf config) sr.Handler {
- fsys := os.DirFS(conf.geminiRoot)
-
- privileged := tlsAuth(conf.privilegedUsers)
-
- router := &sr.Router{}
-
- router.Route(
- "/*",
- gemini.GeminiOnly(true)(sr.FallthroughHandler(
- fs.TitanUpload(privileged, conf.geminiRoot)(postUploadRedirect),
- fs.GeminiFileHandler(fsys),
- fs.GeminiDirectoryDefault(fsys, "index.gmi"),
- fs.GeminiDirectoryListing(fsys, nil),
- )),
- )
-
- router.Route(
- "/cgi-bin/*",
- gemini.GeminiOnly(false)(cgi.GeminiCGIDirectory(
- "/cgi-bin/",
- strings.Join([]string{".", strings.Trim(conf.geminiRoot, "/"), "cgi-bin"}, "/"),
- )),
- )
-
- router.Route(
- "/cgi-bin/private/*",
- gemini.GeminiOnly(false)(tlsauth.GeminiAuth(privileged)(
- cgi.GeminiCGIDirectory("/cgi-bin/private/", strings.Join([]string{
- ".",
- strings.Trim(conf.geminiRoot, "/"),
- "cgi-bin",
- "private",
- }, "/")),
- )),
- )
-
- if conf.geminiRepos != "" {
- router.Mount("/git", syw.GeminiRouter(conf.geminiRepos, nil))
- }
-
- h := router.Handler()
- if conf.geminiAutoAtom {
- h = atomconv.Auto(h)
- }
-
- return h
-}
-
-func gopherRouter(conf config) sr.Handler {
- settings := gophermap.FileSystemSettings{
- ParseExtended: true,
- Exec: true,
- ListUsers: false,
- DirMaps: []string{"gophermap"},
- DirTag: "gophertag",
- }
-
+func routes(server Server) *sr.Router {
router := &sr.Router{}
-
- router.Route(
- "/*",
- sr.FallthroughHandler(
- cgi.ExecGopherMaps("/", conf.gopherRoot, &settings),
- fs.GopherFileHandler(conf.gopherRoot, &settings),
- fs.GopherDirectoryDefault(conf.gopherRoot, &settings),
- fs.GopherDirectoryListing(conf.gopherRoot, &settings),
- ),
- )
-
- router.Route(
- "/cgi-bin/*",
- cgi.GopherCGIDirectory("/cgi-bin/", filepath.Join(conf.gopherRoot, "cgi-bin"), &settings),
- )
-
- if conf.gopherRepos != "" {
- router.Mount("/git", syw.GopherRouter(conf.gopherRepos, nil))
+ for _, route := range server.Routes {
+ addRoute(server, router, route)
}
-
- return router.Handler()
+ return router
}
-var postUploadRedirect = sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
- u := *request.URL
- u.Path = strings.SplitN(u.Path, ";", 2)[0]
- u.Scheme = "gemini"
- return gemini.Redirect(u.String())
-})
-
-func tlsAuth(uploaders []string) tlsauth.Approver {
- sort.Strings(uploaders)
-
- return func(cert *x509.Certificate) bool {
- raw := sha256.Sum256(cert.Raw)
- user := hex.EncodeToString(raw[:])
-
- _, found := sort.Find(len(uploaders), func(i int) int {
- switch {
- case uploaders[i] < user:
- return 1
- case uploaders[i] == user:
- return 0
- default:
- return -1
- }
- })
- return found
+func addRoute(server Server, router *sr.Router, route RouteDirective) {
+ switch server.Type {
+ case "gopher":
+ addGopherRoute(router, route)
+ case "gemini":
+ addGeminiRoute(router, route)
+ default:
+ panic("invalid server type '" + server.Type + "'")
}
}
-
-func fingerHandler(conf config) sr.Handler {
- return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
- name := strings.TrimPrefix(request.Path, "/")
- if name == "" {
- return finger.Error("listings not permitted")
- }
-
- path, ok := conf.fingerResponses[strings.ToLower(name)]
- if !ok {
- return finger.Error("user not found")
- }
-
- file, err := os.Open(path)
- if err != nil {
- ctx.Value("errorlog").(logging.Logger).Log(
- "msg", "finger response file open error",
- "error", err,
- )
- }
-
- return finger.Success(file)
- })
-}
diff --git a/servers.go b/servers.go
new file mode 100644
index 0000000..285b69c
--- /dev/null
+++ b/servers.go
@@ -0,0 +1,39 @@
+package main
+
+import (
+ sr "tildegit.org/tjp/sliderule"
+)
+
+func buildServers(config *Configuration) ([]sr.Server, error) {
+ result := []sr.Server{}
+
+ geminis := []Server{}
+ for _, server := range config.Servers {
+ switch server.Type {
+ case "gopher":
+ srv, err := buildGopherServer(server, config)
+ if err != nil {
+ return nil, err
+ }
+ result = append(result, srv)
+ case "finger":
+ srv, err := buildFingerServer(server, config)
+ if err != nil {
+ return nil, err
+ }
+ result = append(result, srv)
+ case "gemini":
+ geminis = append(geminis, server)
+ }
+ }
+
+ if len(geminis) > 0 {
+ srvs, err := buildGeminiServers(geminis, config)
+ if err != nil {
+ return nil, err
+ }
+ result = append(result, srvs...)
+ }
+
+ return result, nil
+}
diff --git a/types.go b/types.go
new file mode 100644
index 0000000..8d010b2
--- /dev/null
+++ b/types.go
@@ -0,0 +1,66 @@
+package main
+
+import (
+ "context"
+ "crypto/tls"
+ "net"
+ "os/user"
+
+ "github.com/go-kit/log/level"
+ "tildegit.org/tjp/sliderule"
+)
+
+type Modifiers struct {
+ DirDefault string
+ DirList bool
+ Exec bool
+ ExtendedGophermap bool
+ AutoAtom bool
+ Titan *Auth
+
+ titanName string
+}
+
+func (m Modifiers) Empty() bool {
+ return m.DirDefault == "" && !m.DirList && !m.Exec && !m.ExtendedGophermap
+}
+
+type RouteDirective struct {
+ // Allowed: "static", "cgi", "git", "titan"
+ Type string
+
+ // "<FsPath> at <URLPath>"
+ FsPath string
+ URLPath string
+
+ // "with ..."
+ Modifiers Modifiers
+
+ Auth *Auth
+ authName string
+}
+
+type Server struct {
+ Type string
+ IP net.IP
+ Port uint16
+ TLS *tls.Config
+ Hostnames []string
+ Routes []RouteDirective
+}
+
+type Auth struct {
+ Name string
+ Strategy AuthStrategy
+}
+
+type AuthStrategy interface {
+ Approve(context.Context, *sliderule.Request) bool
+}
+
+type Configuration struct {
+ SystemUser *user.User
+ LogLevel level.Value
+
+ Servers []Server
+}
diff --git a/userpath.go b/userpath.go
new file mode 100644
index 0000000..9c9793f
--- /dev/null
+++ b/userpath.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+ "context"
+ "os/user"
+ "path"
+ "path/filepath"
+ "strings"
+
+ sr "tildegit.org/tjp/sliderule"
+)
+
+func usernameFromRouter(ctx context.Context) (string, bool) {
+ username, ok := sr.RouteParams(ctx)["username"]
+ return username, ok
+}
+
+func userFsRoute(ctx context.Context, route RouteDirective) (RouteDirective, bool) {
+ username, ok := usernameFromRouter(ctx)
+ if !ok {
+ return route, false
+ }
+
+ u, err := user.Lookup(username)
+ if err != nil {
+ return route, false
+ }
+
+ route.URLPath = strings.ReplaceAll(route.URLPath, "~", "~"+u.Username)
+ if strings.HasPrefix(route.FsPath, "~/") {
+ route.FsPath = filepath.Join(u.HomeDir, route.FsPath[2:])
+ } else {
+ route.FsPath = strings.ReplaceAll(route.FsPath, "/~/", "/"+u.Username+"/")
+ }
+ return route, true
+}
+
+func buildAndAddRoute(router *sr.Router, route RouteDirective, handlerf func(RouteDirective) sr.Handler) {
+ var (
+ urlpath string
+ handler sr.Handler
+ )
+
+ if strings.IndexByte(route.FsPath, '~') < 0 {
+ urlpath = route.URLPath
+ handler = handlerf(route)
+ } else {
+ urlpath = strings.Replace(route.URLPath, "~", "~:username", 1)
+ handler = sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
+ route, ok := userFsRoute(ctx, route)
+ if !ok {
+ return nil
+ }
+
+ return handlerf(route).Handle(ctx, request)
+ })
+ }
+
+ router.Route(urlpath, handler)
+ router.Route(path.Join(urlpath, "*"), handler)
+}