package main import ( _ "embed" "fmt" "net/http" "os" "os/exec" "path/filepath" "regexp" "strings" ) //go:embed index.html var index string //go:embed theme.css var css string //go:embed scripts.js var scripts string //go:embed favicon.ico var favicon []byte var CFG struct { Hostname string Port string Mandoc string Addr string } func Fatal(v ...interface{}) { fmt.Fprintln(os.Stderr, "manhttpd exited due to an error it could not recover from.") fmt.Fprintf(os.Stderr, "Error: %s\n", fmt.Sprint(v...)) os.Exit(1) } func GetCFG() { var e error var b []byte var s string if s = os.Getenv("HOSTNAME"); s != "" { CFG.Hostname = s } else if s, e = os.Hostname(); e == nil { CFG.Hostname = s } else if b, e = os.ReadFile("/etc/hostname"); e == nil { CFG.Hostname = strings.TrimSpace(string(b)) } else { } index = strings.ReplaceAll(index, "{{ hostname }}", CFG.Hostname) if b, e = exec.Command("which", func() string { if s = os.Getenv("MANDOCPATH"); s != "" { return s } return "mandoc" }()).Output(); e != nil || len(b) == 0 { Fatal("dependency `mandoc` not found in $PATH, is it installed?\n") } else { CFG.Mandoc = strings.TrimSpace(string(b)) } CFG.Port = os.Getenv("ListenPort") if CFG.Port == "" { CFG.Port = "8082" } CFG.Addr = os.Getenv("ListenAddr") if CFG.Addr == "" { CFG.Addr = "0.0.0.0" } } func init() { index = strings.ReplaceAll(index, "{{ jsContent }}", scripts) index = strings.ReplaceAll(index, "{{ cssContent }}", css) } func main() { GetCFG() http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/x-icon") w.Header().Set("Content-Length", fmt.Sprint(len(favicon))) w.WriteHeader(http.StatusOK) w.Write(favicon) }) server := http.Server{ Addr: CFG.Addr + ":" + CFG.Port, Handler: http.HandlerFunc(indexHandler), } _ = server.ListenAndServe() } func WriteHtml(w http.ResponseWriter, r *http.Request, title, html string, q string) { out := strings.ReplaceAll(index, "{{ host }}", r.Host) out = strings.ReplaceAll(out, "{{ title }}", title) out = strings.ReplaceAll(out, "{{ query }}", q) out = strings.ReplaceAll(out, "{{ content }}", html) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprint(w, out) } var LinkRemover = regexp.MustCompile(`(]*>)|()`).ReplaceAllString var HTMLManName = regexp.MustCompile(`(?:)?([a-zA-Z0-9_.:\-]+)(?:)?\(([0-9][0-9a-z]*)\)`) type ManPage struct { Name string Section string Desc string Path string } func (m *ManPage) Where() error { var arg = []string{"-w", m.Name} if m.Section != "" { arg = []string{"-w", "-s" + m.Section, m.Name} } b, err := exec.Command("man", arg...).Output() m.Path = strings.TrimSpace(string(b)) return err } func (m *ManPage) Html() (string, NetErr) { if m.Where() != nil { return "", e404 } b, err := exec.Command(CFG.Mandoc, "-Thtml", "-O", "fragment", m.Path).Output() if err != nil { return "", e500 } html := LinkRemover(string(b), "") html = HTMLManName.ReplaceAllStringFunc(html, func(s string) string { m := HTMLManName.FindStringSubmatch(s) return fmt.Sprintf(`%s(%s)`, m[1], m[2], m[1], m[2]) }) return html, nil } var ManDotName = regexp.MustCompile(`^([a-zA-Z0-9_\-]+)(?:\.([0-9a-z]+))?$`) func NewManPage(s string) (m ManPage) { name := ManDotName.FindStringSubmatch(s) if len(name) >= 2 { m.Name = name[1] } if len(name) >= 3 { m.Section = name[2] } return } var RxWords = regexp.MustCompile(`("[^"]+")|([^ ]+)`).FindAllString var RxWhatIs = regexp.MustCompile(`([a-zA-Z0-9_\-]+) [(]([0-9a-z]+)[)][\- ]+(.*)`).FindAllStringSubmatch func searchHandler(w http.ResponseWriter, r *http.Request) { q := r.Form.Get("q") if q == "" { http.Redirect(w, r, r.URL.Path, http.StatusFound) } if func() bool { m := NewManPage(q) return m.Where() == nil }() { http.Redirect(w, r, "?"+q, http.StatusFound) return } var args = RxWords("-lw "+q, -1) for i := range args { args[i] = strings.TrimSpace(args[i]) args[i] = strings.TrimPrefix(args[i], `"`) args[i] = strings.TrimSuffix(args[i], `"`) } cmd := exec.Command("whatis", args...) b, e := cmd.Output() if len(b) < 1 || e != nil { e404.Write(w, r) return } var output string for _, line := range RxWhatIs(string(b), -1) { // strings.Split(string(b), "\n") { if len(line) == 4 { output += fmt.Sprintf(`

%s (%s) - %s

%c`, line[1], line[2], line[1], line[2], line[3], 10) } } WriteHtml(w, r, "Search", output, q) } var ( e400 = HTCode(400, "Bad Request", "Your request cannot be understood by the server.", "Check that you are using a release version of manhttpd.", "Please check spelling and try again.", "Otherwise browser extensions or proxies may be hijacking your requests.", ) e404 = HTCode(404, "Not Found", "The requested does match any known page names. Please check your spelling and try again.", `If you cannot find the page using your system's man command, then you may need to update your manDB or apt-get <package>-doc.`, "If you can open a page using the cli but not in manhttpd, your service is misconfigured. For best results set user and group to your login user:", `You can edit </etc/systemd/system/manhttpd.service> and set "User=<your-user>" and "Group=<your-group>".`, `Usually root user will work just fine, however root does not index user pages. If manuals are installed without superuser, they are saved to <$HOME/.local/share/man/>.`, `If you want user pages you have to run manhttpd as your login user. If you really want to run the service as root with user directories, at your own risk: adding users' homes into the global path </etc/manpath.config> is usually safe but may cause catastrophic failure on some systems.`, ) e500 = HTCode(500, "Internal Server Error", "The server encountered an error and could not complete your request.", "Make sure you are using a release version of manhttpd.", ) ) func HTCode(code int, name string, desc ...string) HTErr { return HTErr{code, name, desc} } func (h HTErr) Write(w http.ResponseWriter, r *http.Request) { WriteHtml(w, r, h.Title(), h.Content(), r.URL.RawQuery) } func (h HTErr) Is(err error, w http.ResponseWriter, r *http.Request) bool { if err == nil { return false } h.Write(w, r) return true } type HTErr struct { Code int Name string Desc []string } type NetErr interface { Error() HTErr Write(w http.ResponseWriter, r *http.Request) } func (e HTErr) Error() HTErr { return e } func (e HTErr) Title() string { return fmt.Sprintf("%d %s", e.Code, e.Name) } func (e HTErr) Content() string { s := fmt.Sprintf("

%3d

%s


\n", e.Code, e.Name) for d := range e.Desc { s += fmt.Sprintf("

%s


\n", e.Desc[d]) } s += ` ` return s } func indexHandler(w http.ResponseWriter, r *http.Request) { path := filepath.Base(r.URL.Path) path = strings.TrimSuffix(path, "/") err := r.ParseForm() if e400.Is(err, w, r) { return } if r.Method == "POST" { searchHandler(w, r) return } name := r.URL.RawQuery if name != "" { man := NewManPage(name) html, nerr := man.Html() if nerr != nil { nerr.Write(w, r) return } WriteHtml(w, r, man.Name, html, name) return } WriteHtml(w, r, "Index", "", name) return }