diff options
Diffstat (limited to 'server.go')
| -rw-r--r-- | server.go | 208 |
1 files changed, 208 insertions, 0 deletions
diff --git a/server.go b/server.go new file mode 100644 index 0000000..6296574 --- /dev/null +++ b/server.go @@ -0,0 +1,208 @@ +/* +Copyright 2022 Sam Anthony + +This file is part of samanthony.xyz. + +samanthony.xyz is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. + +samanthony.xyz is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +samanthony.xyz. If not, see <https://www.gnu.org/licenses/>. +*/ + +package main + +import ( + "flag" + "fmt" + "golang.org/x/sys/unix" + "html/template" + "io/fs" + "log" + "net" + "net/http" + "os" + "path" + fp "path/filepath" + "strings" +) + +// Flags +var ( + host = "localhost" + port = "80" + chroot = "/var/www/" + user = "www" + group = "www" + root = "/htdocs/samanthony.xyz/" +) + +func init() { + flag.StringVar(&host, "host", host, "") + flag.StringVar(&port, "port", port, "") + flag.StringVar(&chroot, "chroot", chroot, "") + flag.StringVar(&user, "user", user, "") + flag.StringVar(&group, "group", group, "") + flag.StringVar(&root, "root", root, "") + + flag.Parse() +} + +// Must lookup the hostname before entering the chroot. +var addr = "" + +func init() { + // host is an ip address + if ip := net.ParseIP(host); ip != nil { + addr = ip.String() + } else { // host is a domain name + addrs, err := net.LookupHost(host) + if err != nil { + log.Fatal(err) + } + for _, a := range addrs { + if ip := net.ParseIP(a); ip != nil { + if v4 := ip.To4(); v4 != nil { + addr = v4.String() + } + } + } + if addr == "" { + log.Fatalf("No ipv4 address bound to %s", host) + } + } +} + +var ( + uid int + gid int +) + +func init() { + var err error + uid, err = uidOf(user) + if err != nil { + log.Fatal(err) + } + gid, err = gidOf(group) + if err != nil { + log.Fatal(err) + } +} + +// Enter chroot +func init() { + if err := unix.Chroot(chroot); err != nil { + log.Fatalf("chroot: %s: %v", chroot, err) + } +} + +// Build templates +var tmpl = make(map[string]*template.Template) + +func init() { + err := fp.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if fp.Clean(path) == fp.Clean(root) || + fp.Ext(path) != ".html" || + path == fp.Join(root, "base.html") { + return nil + } + label := path[len(fp.Clean(root)):] + tmpl[label] = template.Must(template.ParseFiles( + fp.Join(root, "base.html"), path)) + return nil + }) + if err != nil { + log.Fatal(err) + } +} + +// Template data +type Page struct { + Nav Nav +} + +type Nav struct { + ThisSection string + Links []NavLink +} + +type NavLink struct { + Href string + Label string +} + +var nav = Nav{ + Links: []NavLink{ + {"/", "samanthony.xyz"}, + {"/software/", "software"}, + }, +} + +func rootHandler(w http.ResponseWriter, r *http.Request) { + if err := dropPerms(uid, gid); err != nil { + log.Println(err) + code := http.StatusInternalServerError + http.Error(w, http.StatusText(code), code) + return + } + + reqPath := r.URL.Path + + // If request directory, serve index.html. + // ie. /software -> /software/index.html + if info, err := os.Stat(fp.Join(root, reqPath)); err == nil { + if info.IsDir() { + reqPath = path.Join(reqPath, "index.html") + } + } else if os.IsNotExist(err) { + http.NotFound(w, r) + return + } else { + log.Println(err) + code := http.StatusInternalServerError + http.Error(w, http.StatusText(code), code) + return + } + + if t, ok := tmpl[reqPath]; ok { + thisSection := "" + for _, link := range nav.Links { + if strings.HasPrefix(reqPath, link.Href) { + thisSection = link.Href + } + } + nav := nav + nav.ThisSection = thisSection + page := Page{nav} + + err := t.Execute(w, page) + if err != nil { + log.Println(err) + code := http.StatusInternalServerError + http.Error(w, http.StatusText(code), code) + return + } + } else { + http.ServeFile(w, r, fp.Join(root, reqPath)) + } +} + +func main() { + http.HandleFunc("/", rootHandler) + http.Handle("/.well-known/acme-challenge/", + http.StripPrefix( + "/.well-known/acme-challenge/", + http.FileServer(http.Dir("/acme/")), + ), + ) + + log.Printf("Listening on %s:%s\n", addr, port) + log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%s", addr, port), nil)) +} |