git.sophuwu.com > manhttpd
made it work again without config file
sophuwu sophie@sophuwu.com
Wed, 16 Jul 2025 16:42:36 +0200
commit

5e03728a4a65a3792f7211e4264121f5d6644c25

parent

4ad228bfd35082fea75a43064beb2a42338508c5

6 files changed, 218 insertions(+), 108 deletions(-)

jump to
M CFG/CFG.goCFG/CFG.go

@@ -1,19 +1,35 @@

package CFG import ( + "context" + "errors" "fmt" + "golang.org/x/sys/unix" + "log" + "net/http" "os" "os/exec" + "os/signal" "sophuwu.site/manhttpd/neterr" "strings" ) var ( - Mandoc string = "mandoc" - DbCmd string = "apropos" // or "whatis" - ManCmd string = "man" - - Hostname string + Mandoc string = "mandoc" + DbCmd string = "apropos" // or "whatis" + ManCmd string = "man" + Hostname string = "" + Port string = "8082" + Addr string = "0.0.0.0" + RequireAuth bool = false + PasswdFile string = "/var/lib/manhttpd/authuwu" + TldrPages bool = false + TldrDir string = "/var/lib/manhttpd/tldr" + EnableStats bool = false + StatisticDB string = "/var/lib/manhttpd/manhttpd.db" + UseTLS bool = false + TLSCertFile string = "" + TLSKeyFile string = "" ) func which(s string) (string, error) {

@@ -22,21 +38,7 @@ b, e := c.CombinedOutput()

return strings.TrimSpace(string(b)), e } -func init() { - var e error - var b []byte - var s string - if s = os.Getenv("HOSTNAME"); s != "" { - Hostname = s - } else if s, e = os.Hostname(); e == nil { - Hostname = s - } else if b, e = os.ReadFile("/etc/hostname"); e == nil { - Hostname = strings.TrimSpace(string(b)) - } - if Hostname == "" { - Hostname = "Unresolved" - } - +func checkCmds() { var err error Mandoc, err = which("mandoc") fmt.Println(Mandoc, err)

@@ -54,12 +56,91 @@ if err != nil || len(DbCmd) == 0 {

neterr.Fatal("dependency `apropos` or `whatis` not found in $PATH, is it installed?\n") } } +} + +func getHostname() { + var e error + var b []byte + var s string + if s = os.Getenv("HOSTNAME"); s != "" { + Hostname = s + } else if s, e = os.Hostname(); e == nil { + Hostname = s + } else if b, e = os.ReadFile("/etc/hostname"); e == nil { + Hostname = strings.TrimSpace(string(b)) + } +} + +func getEnvs() { + var s string + s = os.Getenv("ListenPort") + if s != "" { + Port = s + } + s = os.Getenv("ListenAddr") + if s != "" { + Addr = s + } +} + +func ParseConfig() { + checkCmds() if len(os.Args) > 1 && strings.HasSuffix(os.Args[1], ".conf") { ConfFile = os.Args[1] } - DefaultConf, err = ParseEtcConf() + + err := parse() if err != nil { - neterr.Fatal("Failed to parse configuration file:", err) + if !errors.Is(err, NoConfError) { + neterr.Fatal("Failed to parse configuration file:", err) + } + getEnvs() + } + + if Hostname == "" { + getHostname() + } +} + +func HttpHostname(r *http.Request) string { + if Hostname != "" { + return Hostname + } + if r.Host != "" { + return r.Host + } + if r.URL != nil && r.URL.Host != "" { + return r.URL.Host + } + if r.TLS != nil && r.TLS.ServerName != "" { + return r.TLS.ServerName + } + return "" +} + +func ListenAndServe(h http.Handler) { + server := http.Server{ + Addr: Addr + ":" + Port, + Handler: h, + } + var err error + go func() { + if UseTLS && TLSCertFile != "" && TLSKeyFile != "" { + err = server.ListenAndServeTLS(TLSCertFile, TLSKeyFile) + } else { + err = server.ListenAndServe() + } + if err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("Error starting server: %v", err) + } + }() + sigchan := make(chan os.Signal) + signal.Notify(sigchan, unix.SIGINT, unix.SIGTERM, unix.SIGQUIT, unix.SIGKILL, unix.SIGSTOP) + s := <-sigchan + println("stopping: got signal", s.String()) + err = server.Shutdown(context.Background()) + if err != nil { + log.Println("Error stopping server: %v", err) } }
M CFG/etc.conf.goCFG/etc.conf.go

@@ -1,99 +1,127 @@

package CFG import ( + "errors" "fmt" - "net/http" "os" "strings" ) var ConfFile = "/etc/manhttpd/manhttpd.conf" -type EtcConf struct { - Hostname string - Port string - Addr string - RequireAuth bool - PasswdFile string - TldrPages bool - EnableStats bool - StatisticDB string +var NoConfError = errors.New("no configuration file found") + +func setV(a any) func(j string) error { + switch v := a.(type) { + case *string: + return func(j string) error { + *v = j + return nil + } + case *bool: + return func(j string) error { + j = strings.ToLower(j) + *v = "yes" == j + if (*v) || "no" == j { + return nil + } + return errors.New("invalid boolean value: " + j) + } + } + return nil } -var DefaultConf = EtcConf{ - Hostname: "", - Port: "8082", - Addr: "0.0.0.0", - RequireAuth: false, - PasswdFile: "/var/lib/manhttpd/authuwu", - TldrPages: false, - EnableStats: false, - StatisticDB: "/var/lib/manhttpd/stats.db", +func rmComment(s *string) bool { + i := strings.Index(*s, "#") + if i >= 0 { + *s = (*s)[:i] + } + *s = strings.TrimSpace(*s) + return len(*s) == 0 } -func (c *EtcConf) Parse() error { - var mp = map[string]any{ - "hostname": &(c.Hostname), - "port": &(c.Port), - "addr": &(c.Addr), - "require_auth": &(c.RequireAuth), - "passwd_file": &(c.PasswdFile), - "tldr_pages": &(c.TldrPages), - "enable_stats": &(c.EnableStats), - "statistic_db": &(c.StatisticDB), +func getKV(line, k, v *string) bool { + i := strings.Index(*line, "=") + if i < 0 { + return false } + *k = strings.TrimSpace((*line)[:i]) + *v = strings.TrimSpace((*line)[i+1:]) + return len(*k) == 0 || len(*v) == 0 +} + +var mp = map[string]any{ + "hostname": &Hostname, + "port": &Port, + "addr": &Addr, + "require_auth": &RequireAuth, + "passwd_file": &PasswdFile, + "tldr_pages": &TldrPages, + "tldr_dir": &TldrDir, + "enable_stats": &EnableStats, + "statistic_db": &StatisticDB, + "use_tls": &UseTLS, + "tls_cert_file": &TLSCertFile, + "tls_key_file": &TLSKeyFile, +} + +func parse() error { b, err := os.ReadFile(ConfFile) if err != nil { - return err + return NoConfError } - var j, k string - var kv []string - for _, v := range strings.Split(string(b), "\n") { - if len(v) == 0 || v[0] == '#' { - continue // skip empty lines and comments + var line, k, v string + var i int + var a any + var fn func(j string) error + var ok bool + errFmt := "invalid %s in " + ConfFile + " at line %d: %s" + ErrPrint := func(e, s string) { + fmt.Fprintf(os.Stderr, errFmt, e, i+1, s) + } + for i, line = range strings.Split(string(b), "\n") { + if len(line) == 0 { + continue } - kv = strings.SplitN(v, " ", 2) - if len(kv) != 2 { - return fmt.Errorf("invalid line in %s: %s", ConfFile, v) + if rmComment(&line) { + continue + } + if getKV(&line, &k, &v) { + ErrPrint("format", line) + continue } - k = strings.TrimSpace(kv[0]) - j = strings.TrimSpace(kv[1]) - if val, ok := mp[k]; ok { - switch v := val.(type) { - case *string: - *v = j - case *bool: - *v = j == "yes" - default: - return fmt.Errorf("unsupported type for key %s in %s", k, ConfFile) - } - } else { - return fmt.Errorf("unknown key %s in %s", k, ConfFile) + if a, ok = mp[k]; !ok { + ErrPrint("key", k) + continue + } + fn = setV(a) + if fn == nil { + continue + } + err = fn(v) + if err != nil { + ErrPrint(fmt.Sprintf("value for %s", k), v) + continue } } return nil } -func ParseEtcConf() (EtcConf, error) { - c := DefaultConf - if err := c.Parse(); err != nil { - return c, fmt.Errorf("failed to parse %s: %w", ConfFile, err) +func MakeConfig() string { + var s, i string + for k, a := range mp { + switch v := a.(type) { + case *string: + i = *v + case *bool: + i = map[bool]string{true: "yes", false: "no"}[*v] + default: + continue + } + if len(i) == 0 { + continue + } + s += fmt.Sprintf("%s = %s\n", k, i) } - if c.Port == "" { - c.Port = "8082" - } - if c.Addr == "" { - c.Addr = "0.0.0.0" - } - if c.Hostname != "" { - Hostname = c.Hostname - } - return c, nil -} - -func (c *EtcConf) Server(h http.Handler) *http.Server { - return &http.Server{ - Addr: c.Addr + ":" + c.Port, - Handler: h, - } + return s }
M embeds/embeds.goembeds/embeds.go

@@ -75,7 +75,7 @@ Content template.HTML

Query string } -func init() { +func OpenAndParse() { openStatic() var e error t, e = template.New("index.html").Parse(index)

@@ -92,7 +92,7 @@

func WriteError(w http.ResponseWriter, r *http.Request, err neterr.NetErr) { p := Page{ Title: err.Error().Title(), - Hostname: CFG.Hostname, + Hostname: CFG.HttpHostname(r), Content: template.HTML(err.Error().Content()), Query: r.URL.RawQuery, }

@@ -102,7 +102,7 @@

func WriteHtml(w http.ResponseWriter, r *http.Request, title, html string, q string) { p := Page{ Title: title, - Hostname: CFG.Hostname, + Hostname: CFG.HttpHostname(r), Content: template.HTML(html), Query: q, }
M go.modgo.mod

@@ -1,5 +1,7 @@

module sophuwu.site/manhttpd -go 1.21.5 +go 1.23.0 -require gopkg.in/yaml.v3 v3.0.1 +toolchain go1.24.4 + +require golang.org/x/sys v0.34.0
M go.sumgo.sum

@@ -1,4 +1,2 @@

-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
M main.gomain.go

@@ -12,12 +12,13 @@ "sophuwu.site/manhttpd/neterr"

"strings" ) +func init() { + CFG.ParseConfig() + embeds.OpenAndParse() +} + func main() { - s := CFG.DefaultConf.Server(ManHandler{}) - err := s.ListenAndServe() - if err != nil { - fmt.Println("Error starting server:", err) - } + CFG.ListenAndServe(ManHandler{}) } var RxWords = regexp.MustCompile(`("[^"]+")|([^ ]+)`).FindAllString