config file refactor
This commit is contained in:
parent
b4f45f7c65
commit
6e1c25af36
|
@ -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
|
||||
}
|
102
config.go
102
config.go
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
6
go.mod
6
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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
109
main.go
109
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 len(os.Args) < 2 {
|
||||
fmt.Fprintf(os.Stderr, "%s <configfile>\n", os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
config, err := Parse(configfile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, _, info, warn, errlog := serverContext()
|
||||
_, _, _, errlog := Loggers(config)
|
||||
|
||||
gopherSrv, err := buildGopher(ctx, conf, info, errlog)
|
||||
servers, err := buildServers(config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
_ = errlog.Log("msg", "error building servers", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fingerSrv, err := buildFinger(ctx, conf, info, errlog)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
dropped, err := dropPrivileges()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if !dropped {
|
||||
_ = warn.Log("msg", "dropping privileges to 'nobody' failed")
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
157
routes.go
157
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)
|
||||
|
||||
func routes(server Server) *sr.Router {
|
||||
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))
|
||||
for _, route := range server.Routes {
|
||||
addRoute(server, router, route)
|
||||
}
|
||||
|
||||
h := router.Handler()
|
||||
if conf.geminiAutoAtom {
|
||||
h = atomconv.Auto(h)
|
||||
}
|
||||
|
||||
return h
|
||||
return router
|
||||
}
|
||||
|
||||
func gopherRouter(conf config) sr.Handler {
|
||||
settings := gophermap.FileSystemSettings{
|
||||
ParseExtended: true,
|
||||
Exec: true,
|
||||
ListUsers: false,
|
||||
DirMaps: []string{"gophermap"},
|
||||
DirTag: "gophertag",
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
return router.Handler()
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue