diff options
| author | tjpcc <tjp@ctrl-c.club> | 2023-09-28 08:08:48 -0600 |
|---|---|---|
| committer | tjpcc <tjp@ctrl-c.club> | 2023-10-09 08:47:37 -0600 |
| commit | 6e1c25af361dde4c063eccbf769e966df4b65f23 (patch) | |
| tree | d28044cf2db246555deda8db395f2f0a7e786590 /parse.go | |
| parent | b4f45f7c654e87bda6d5e7effb6ac5b5feb29ce0 (diff) | |
config file refactor
Diffstat (limited to 'parse.go')
| -rw-r--r-- | parse.go | 492 |
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 +} |
