// A barebones gopher server written in Golang // This fork uses a config file instead of environment variables // Copyright: // (C) 2021 Shokara Kou // (C) 2023 Izuru Yakumo package main import ( "bufio" "flag" "io" "log" "net" "os" "os/user" "strconv" "strings" "syscall" "gopkg.in/ini.v1" "marisa.chaotic.ninja/tokiko" ) var conf struct { port string addr string hostname string rootdir string user string group string } // Configuration file parsing func parseconfig(file string) error { cfg, err := ini.Load(file) if err != nil { return err } conf.port = cfg.Section("tokiko").Key("port").String() conf.addr = cfg.Section("tokiko").Key("addr").String() conf.hostname = cfg.Section("tokiko").Key("hostname").String() conf.rootdir = cfg.Section("tokiko").Key("rootdir").String() conf.user = cfg.Section("tokiko").Key("user").String() conf.group = cfg.Section("tokiko").Key("group").String() return nil } // Line formatting func formatLine(line string) string { trimmed := strings.TrimRight(line, "\r\n") splitted := strings.Split(trimmed, "\t") if len(splitted) == 3 { return line } else if len(splitted) == 2 { line += "\t" + conf.addr + "\t" + conf.port } else if len(splitted) == 1 { line += "\tErr\t" + conf.hostname + "\t" + conf.port } return line + "\n" } func writeError(c net.Conn, msg string) { c.Write([]byte(formatLine("3" + msg))) } // Display gophermap to clients func printGophermap(c net.Conn, dir string) { file, err := os.Open(dir + "/gophermap") if err != nil { writeError(c, err.Error()) log.Println(err) } defer func() { if err = file.Close(); err != nil { log.Println(err) } }() scanner := bufio.NewScanner(file) for scanner.Scan() { c.Write([]byte(formatLine(scanner.Text()) /*+ "\n"*/)) } c.Write([]byte(".\r\n")) } func printFile(c net.Conn, path string) { file, err := os.Open(path) if err != nil { writeError(c, err.Error()) log.Println(err) } defer func() { if err = file.Close(); err != nil { log.Println(err) } }() const bufSz = 1024 b := make([]byte, bufSz) for { readSz, err := file.Read(b) if err != nil { if err != io.EOF { log.Println(err) } break } c.Write(b[:readSz]) } } func connHandle(c net.Conn) { data, err := bufio.NewReader(c).ReadString('\n') if err != nil { log.Println(err) return } selector := strings.TrimRight(data, "\r\n") if selector == "" { printGophermap(c, "./") } else if strings.Contains(selector, "..") { writeError(c, "Selector contains ..") } else if selector[0] == '/' { info, err := os.Stat(selector[1:]) if err != nil { writeError(c, err.Error()) log.Println(err) c.Close() return } if info.IsDir() { printGophermap(c, selector[1:]) } else { printFile(c, selector[1:]) } } else { writeError(c, "Selector doesn't start with a /") } c.Close() } // UID/GID lookup, needed for privilege dropping func usergroupids(username string, groupname string) (int, int, error) { u, err := user.Lookup(username) if err != nil { return -1, -1, err } uid, _ := strconv.Atoi(u.Uid) gid, _ := strconv.Atoi(u.Gid) if conf.group != "" { g, err := user.LookupGroup(groupname) if err != nil { return uid, -1, err } gid, _ = strconv.Atoi(g.Gid) } return uid, gid, nil } func main() { var configfile string flag.StringVar(&configfile, "f", "", "Configuration file") flag.Parse() conf.addr = "127.0.0.1" conf.port = "70" conf.hostname = "localhost" conf.rootdir = "/var/gopher" if configfile != "" { parseconfig(configfile) } LISTEN_ADDR := conf.addr + ":" + conf.port if conf.user != "" { log.Printf("Dropping privileges to %s", conf.user) uid, gid, err := usergroupids(conf.user, conf.group) if err != nil { log.Fatal(err) } syscall.Setuid(uid) syscall.Setgid(gid) } log.Printf("Starting tokiko version %v on %s\n", tokiko.FullVersion(), LISTEN_ADDR) os.Chdir(conf.rootdir) l, err := net.Listen("tcp", LISTEN_ADDR) if err != nil { log.Fatal(err) return } defer l.Close() for { c, err := l.Accept() if err != nil { log.Println(err) return } go connHandle(c) } }