278
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(`(<a [^>]*>)|(</a>)`).ReplaceAllString
var HTMLManName = regexp.MustCompile(`(?:<b>)?([a-zA-Z0-9_.:\-]+)(?:</b>)?\(([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(`<a href="?%s.%s">%s(%s)</a>`, 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(`<p><a href="?%s.%s">%s (%s)</a> - %s</p>%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 <b><package>-doc</b>.`,
"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 <<b>/etc/systemd/system/manhttpd.service</b>> and set "<b>User</b>=<<b>your-user</b>>" and "<b>Group</b>=<<b>your-group</b>>".`,
`Usually root user will work just fine, however root does not index user pages. If manuals are installed without superuser, they are saved to <<b>$HOME/.local/share/man/</b>>.`,
`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 <<b>/etc/manpath.config</b>> 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("<h1>%3d</h1><h2>%s</h2><br>\n", e.Code, e.Name)
for d := range e.Desc {
s += fmt.Sprintf("<p>%s</p><br>\n", e.Desc[d])
}
s += `<script>SetRawQuery()</script>
`
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
}