package main import ( _ "embed" "fmt" "log" "net/http" "os" "os/exec" "path/filepath" "regexp" "strings" ) //go:embed index.html var index string //go:embed dark_theme.css var css []byte //go:embed favicon.ico var favicon []byte var CFG struct { Hostname string Port string Mandoc string Addr string } func GetCFG() { CFG.Hostname = os.Getenv("HOSTNAME") index = strings.ReplaceAll(index, "{{ hostname }}", CFG.Hostname) b, e := exec.Command("which", "mandoc").Output() if e != nil || len(b) == 0 { CFG.Mandoc = os.Getenv("MANDOCPATH") if CFG.Mandoc == "" { log.Fatal("Fatal: no mandoc `apt-get install mandoc`") } } 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 CssHandle(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/css; charset=utf-8") w.Header().Set("Content-Length", fmt.Sprint(len(css))) w.WriteHeader(http.StatusOK) w.Write(css) } func main() { GetCFG() http.HandleFunc("/style.css", CssHandle) 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) }) http.HandleFunc("/", indexHandler) http.ListenAndServe(CFG.Addr+":"+CFG.Port, nil) } func WriteHtml(w http.ResponseWriter, r *http.Request, title, html string) { out := strings.ReplaceAll(index, "{{ host }}", r.Host) out = strings.ReplaceAll(out, "{{ title }}", title) 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 { if m.Where() != nil { return fmt.Sprintf("

404: Unable to locate page %s

", m.Name) } b, err := exec.Command(CFG.Mandoc, "-Thtml", "-O", "fragment", m.Path).Output() if err != nil { return fmt.Sprintf("

500: server error loading %s

", m.Name) } 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 } 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) { _ = r.ParseForm() 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 { WriteHtml(w, r, "Search", fmt.Sprintf("

404: no resualts matching %s

", q)) 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) } func indexHandler(w http.ResponseWriter, r *http.Request) { path := filepath.Base(r.URL.Path) path = strings.TrimSuffix(path, "/") if path == "style.css" { CssHandle(w, r) return } if r.Method == "POST" { searchHandler(w, r) return } r.ParseForm() name := r.URL.RawQuery if name != "" { man := NewManPage(name) WriteHtml(w, r, man.Name, man.Html()) return } WriteHtml(w, r, "Index", "") return }