package main import ( "bufio" "flag" "fmt" "html" "io" "log" "net" "net/http" "net/http/fcgi" "os" "path/filepath" "regexp" "sort" "strings" ) var ( dir = flag.String("d", ".", "root directory") port = flag.Int("p", 0, "http port (optional, for standalone or reverse-proxy)") fcgiSocket = flag.String("s", "", "fastcgi socket path (optional, e.g., /var/run/app.sock)") linkPattern = regexp.MustCompile(`^\s*=>\s*(.*)$`) ) func parseFile(path string) (title, content string, err error) { file, err := os.Open(path) if err != nil { return "", "", err } defer file.Close() scanner := bufio.NewScanner(file) if !scanner.Scan() { return "", "", fmt.Errorf("empty file") } firstLine := scanner.Text() if strings.HasPrefix(firstLine, "title ") { title = strings.TrimPrefix(firstLine, "title ") } else { content = firstLine + "\n" } var buf strings.Builder for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "#") { continue } buf.WriteString(line) buf.WriteString("\n") } if content == "" { content = buf.String() } else { content += buf.String() } return title, content, nil } func resolvePath(root, inputPath string) (title, content string, isDir bool, err error) { cleanPath := filepath.Clean("/" + inputPath) target := filepath.Join(root, cleanPath) info, err := os.Stat(target) if err != nil { if os.IsNotExist(err) { // Try as file first title, content, err2 := parseFile(target) if err2 == nil { return title, content, false, nil } // Try index file in directory indexFile := filepath.Join(target, "index") if _, err3 := os.Stat(indexFile); err3 == nil { title, content, _ := parseFile(indexFile) return title, content, true, nil } // Try directory listing listing, err := dirListing(target) if err == nil { return "", listing, true, nil } return "", "", false, fmt.Errorf("not found") } return "", "", false, err } if info.IsDir() { indexFile := filepath.Join(target, "index") if _, err := os.Stat(indexFile); err == nil { title, content, _ := parseFile(indexFile) return title, content, true, nil } listing, err := dirListing(target) if err != nil { return "", "", true, err } return "", listing, true, nil } title, content, err = parseFile(target) return title, content, false, err } func dirListing(dir string) (string, error) { entries, err := os.ReadDir(dir) if err != nil { return "", err } var items []string for _, e := range entries { if strings.HasPrefix(e.Name(), ".") { continue } suffix := "" if e.IsDir() { suffix = "/" } items = append(items, " => "+e.Name()+suffix) } sort.Strings(items) return strings.Join(items, "\n"), nil } func linkify(text string) string { lines := strings.Split(text, "\n") var result []string for _, line := range lines { matches := linkPattern.FindStringSubmatch(line) if matches != nil { rest := strings.TrimSpace(matches[1]) if rest == "" { result = append(result, html.EscapeString(line)) continue } parts := strings.Fields(rest) if len(parts) >= 1 { target := parts[0] label := target if len(parts) > 1 { label = strings.Join(parts[1:], " ") } href := target if !strings.HasPrefix(target, "http://") && !strings.HasPrefix(target, "https://") { href = "./" + strings.TrimPrefix(target, "/") } linkLine := ` => ` + html.EscapeString(label) + `` result = append(result, linkLine) continue } } result = append(result, html.EscapeString(line)) } return strings.Join(result, "\n") } func isCliUserAgent(userAgent string) bool { cliAgents := []string{"curl", "wget", "lynx", "links", "w3m", "elinks"} userAgentLower := strings.ToLower(userAgent) for _, agent := range cliAgents { if strings.Contains(userAgentLower, agent) { return true } } return false } func handleHTTP(w http.ResponseWriter, r *http.Request) { root := *dir path := strings.TrimPrefix(r.URL.Path, "/") title, content, isDir, err := resolvePath(root, path) if err != nil { http.NotFound(w, r) return } // Check if this is a CLI browser userAgent := r.Header.Get("User-Agent") if isCliUserAgent(userAgent) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Write([]byte(content)) return } // For regular browsers, always return HTML if !isDir { // Check if this is actually a binary file cleanPath := filepath.Clean("/" + path) target := filepath.Join(root, cleanPath) file, err := os.Open(target) if err == nil { defer file.Close() // Read first few bytes to check if it's a text file buf := make([]byte, 512) n, _ := file.Read(buf) if n > 0 && http.DetectContentType(buf[:n]) != "text/plain; charset=utf-8" { // Binary file, serve as-is file.Seek(0, 0) io.Copy(w, file) return } } } w.Header().Set("Content-Type", "text/html; charset=utf-8") content = linkify(content) htmlContent := `
` + content + `` w.Write([]byte(htmlContent)) } func handleNex(conn net.Conn, root string) { defer conn.Close() reader := bufio.NewReader(conn) line, err := reader.ReadString('\n') if err != nil { return } path := strings.TrimSpace(line) _, content, _, err := resolvePath(root, path) if err != nil { content = "Not found\n" } conn.Write([]byte(content)) } func startNexServer(root string) { listener, err := net.Listen("tcp", ":1900") if err != nil { log.Fatalf("Nex server error: %v", err) } defer listener.Close() log.Printf("Nex server on :1900, root=%s", root) for { conn, err := listener.Accept() if err != nil { continue } go handleNex(conn, root) } } func startFastCGI(socketPath string) { os.Remove(socketPath) listener, err := net.Listen("unix", socketPath) if err != nil { log.Fatalf("FastCGI socket error: %v", err) } defer listener.Close() os.Chmod(socketPath, 0666) log.Printf("FastCGI server on %s", socketPath) handler := http.HandlerFunc(handleHTTP) if err := fcgi.Serve(listener, handler); err != nil { log.Fatalf("FastCGI serve error: %v", err) } } func main() { flag.Parse() if flag.NArg() > 0 { log.Fatal("unexpected arguments; use flags only") } root, err := filepath.Abs(*dir) if err != nil { log.Fatal(err) } http.HandleFunc("/", handleHTTP) if *fcgiSocket != "" { go startNexServer(root) startFastCGI(*fcgiSocket) return } if *port > 0 { go startNexServer(root) addr := fmt.Sprintf(":%d", *port) log.Printf("HTTP server on %s, root=%s", addr, root) log.Fatal(http.ListenAndServe(addr, nil)) return } startNexServer(root) }