summaryrefslogtreecommitdiff
path: root/parse.go
diff options
context:
space:
mode:
Diffstat (limited to 'parse.go')
-rw-r--r--parse.go492
1 files changed, 492 insertions, 0 deletions
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
+}