├── .gitignore ├── LICENSE ├── README.markdown ├── build.sh ├── config.go ├── go.mod ├── go.sum └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | /zsrv 2 | /www 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © Martin Tournoij 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to 7 | deal in the Software without restriction, including without limitation the 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | sell copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | The software is provided "as is", without warranty of any kind, express or 16 | implied, including but not limited to the warranties of merchantability, 17 | fitness for a particular purpose and noninfringement. In no event shall the 18 | authors or copyright holders be liable for any claim, damages or other 19 | liability, whether in an action of contract, tort or otherwise, arising 20 | from, out of or in connection with the software or the use or other dealings 21 | in the software. 22 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Simple static file server in Go. 2 | 3 | Features: 4 | 5 | - Directory-based vhosts (convention over configuration). 6 | - Compile all static resources in to the binary. 7 | - Redirects for pages (/old-url → new-url) and domains (foo.com → www.foo.com). 8 | 9 | Usage 10 | ----- 11 | Sites are in `www//files`: 12 | 13 | % mkdir -p www/example.com 14 | % echo '

Hello, world!

' > www/example.com/index.html 15 | 16 | If you want to use something outside of `www` you need to set up a bind mount: 17 | 18 | % mkdir -p www/mysite.com 19 | % doas mount -obind,ro ~/arp242.net www/www.arp242.net 20 | 21 | You can use wildcards if you want (`STAR.example.com`, or `STAR` for a 22 | catch-all). This needs to be in all-caps (we can't use `*` as Go embed doesn't 23 | like it). 24 | 25 | Now just build and start it: 26 | 27 | $ go build 28 | $ ./zsrv 29 | 30 | There is a little `build.sh` script which ensures there's no cgo dependencies. 31 | 32 | The file size is about 7.7M (stripped) or 3M (compressed with upx), plus the 33 | website data you have. 34 | 35 | This is not intended for very large websites. I use it to host my own website 36 | (https://www.arp242.net). 37 | 38 | Configuration 39 | ------------- 40 | Edit `config.go` and recompile. 41 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -euC 4 | 5 | # Binary name 6 | name="zsrv" 7 | 8 | CGO_ENABLED=0 \ 9 | go build -trimpath -ldflags "-X main.version=$(git log -n1 --format='%h_%cI')" -o "$name" 10 | 11 | which strip >/dev/null 2>&1 \ 12 | && strip "$name" \ 13 | || echo >&2 "strip not found; skipping removal of debug symbols" 14 | 15 | which upx >/dev/null 2>&1 \ 16 | && upx -qqq "$name" \ 17 | || echo >&2 "upx not found; skipping compression" 18 | 19 | exit 0 20 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var listen = "127.0.0.1:8000" 4 | 5 | var certdir = "/home/martin" 6 | 7 | var domainRedirects = map[string]string{ 8 | "arp242.net": "www.arp242.net", 9 | } 10 | 11 | // Redirect pages on a domain. 12 | // 13 | // Redirects are always 301. 14 | // 15 | // Files will *overide* the redirects; they're only checked on a 404. 16 | // 17 | // Wildcards (*) can only be at the end, and {} is replaced with the contents. 18 | var redirects = map[string]map[string]string{ 19 | "www.arp242.net": map[string]string{ 20 | // Redirect old /weblog/ paths. 21 | "/weblog*": "/{}", 22 | 23 | // # Code pages, just redirect to GitHub 24 | "/code": "/#code", 25 | "/code/index.html": "/#code", 26 | "/code/*": "https://github.com/arp242/{}", 27 | 28 | // Old weblog titles; use matching for some because Jekyll generated 29 | // weird filenames over the years :-/ 30 | "/weblog/run_multiple_services_*": "/pf-switch.html", 31 | "/weblog/minimal_apache*": "/apache-svn.html", 32 | "/weblog/manage_unreal*": "/ut-cache.html", 33 | "/weblog/online_unreal*()": "/ut-browser.html", 34 | "/weblog/tunnelling_ssh*-l": "/ssh-tunnel.html", 35 | "/weblog/creating_temporary*": "/php-mktemp.html", 36 | "/weblog/digging_for_hosts*": "/freebsd-dig.html", 37 | "/weblog/making_flag_shih*.html": "/flagshihtzu-formtastic.html", 38 | "/weblog/intercept_outgoing*": "/rails-devmail.html", 39 | "/weblog/uninstalling_emacs*": "/apt-get.html", 40 | "/weblog/generate_passwords*": "/cli-passwords.html", 41 | "/weblog/security-of-python*": "/pickle-marshal-security.html", 42 | "/weblog/i-dont-like-git*": "/git-hg.html", 43 | "/weblog/some-thoughts-on-cdns.html": "/cdn.html", 44 | "/weblog/a-primer*": "/python-str-bytes.html", 45 | "/weblog/yaml*": "/yaml-config.html", 46 | "/weblog/json_as_configuration*": "/json-config.html", 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module zgo.at/zsrv 2 | 3 | go 1.17 4 | 5 | require ( 6 | zgo.at/zhttp v0.0.0-20211008141613-bca048c56d70 7 | zgo.at/zstd v0.0.0-20211017205211-017273d7d29c 8 | ) 9 | 10 | require ( 11 | github.com/monoculum/formam v0.0.0-20210131081218-41b48e2a724b // indirect 12 | zgo.at/guru v1.1.0 // indirect 13 | zgo.at/json v0.0.0-20200627042140-d5025253667f // indirect 14 | zgo.at/zlog v0.0.0-20211008102840-46c1167bf2a9 // indirect 15 | zgo.at/ztpl v0.0.0-20210521121045-35d7df223188 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/monoculum/formam v0.0.0-20210131081218-41b48e2a724b h1:Iwvedi9ZDiJXu125WNj0Xm1fm3VKwbged5BavcLyz0Y= 2 | github.com/monoculum/formam v0.0.0-20210131081218-41b48e2a724b/go.mod h1:JKa2av1XVkGjhxdLS59nDoXa2JpmIHpnURWNbzCtXtc= 3 | zgo.at/guru v1.1.0 h1:hvMLOzPnv7XApwJZgxvFxBA8wb6RGPUw9lz8kmq450g= 4 | zgo.at/guru v1.1.0/go.mod h1:eltnfk6QwmM7ic3OLzmGMjEChP2iLuYXERyliREiA00= 5 | zgo.at/json v0.0.0-20200627042140-d5025253667f h1:o4XL0oDtAzxN+mkVCti8MYz5nLNfbrl17I78rqLWQrk= 6 | zgo.at/json v0.0.0-20200627042140-d5025253667f/go.mod h1:DUuNIRpNC7/cup+Gy0qhwQEjjlLFXXRZ04VnVv9bf3E= 7 | zgo.at/zhttp v0.0.0-20211008141613-bca048c56d70 h1:BxyV7jnrSFZv1O/lCUW8JAabhQZqiqQ+Vw8THcgvuJA= 8 | zgo.at/zhttp v0.0.0-20211008141613-bca048c56d70/go.mod h1:F2vshFSnJKQMOHvQEEA4L/9D0P6qkejg4nNR8rrY5l8= 9 | zgo.at/zlog v0.0.0-20201213081304-1dc74ce06e5f/go.mod h1:YkLAQZjLsp1RqWHn8SJokHzXyYU+6FZjM4GNY64IKME= 10 | zgo.at/zlog v0.0.0-20211008102840-46c1167bf2a9 h1:4Ot1ETCZQjEjISFiE+SjhwykNvUTLJfyK/BCoJJidnk= 11 | zgo.at/zlog v0.0.0-20211008102840-46c1167bf2a9/go.mod h1:2Pa5cqKA/6S9FrMFeNowtHPd7JEzqwooLXcxU6Wk7Hc= 12 | zgo.at/zstd v0.0.0-20210512041107-8951517febd3/go.mod h1:sQqrTxBwKW0nlwcOg9RxXB8ikY+NBciJnJRPOq/gEuY= 13 | zgo.at/zstd v0.0.0-20211017205211-017273d7d29c h1:Tn/vQ0YyXVbf6RWMuUQdHLNo0WWxbfHjIXlQb7x2xwQ= 14 | zgo.at/zstd v0.0.0-20211017205211-017273d7d29c/go.mod h1:sQqrTxBwKW0nlwcOg9RxXB8ikY+NBciJnJRPOq/gEuY= 15 | zgo.at/ztpl v0.0.0-20210521121045-35d7df223188 h1:AZUPCrQDqDF2tdODEhrLqc7/RdT/VhhF9XvOE0M7QCI= 16 | zgo.at/ztpl v0.0.0-20210521121045-35d7df223188/go.mod h1:acpuolnKwBftb7ISLZGKWm10YFqJ9a9bP3hClIoDglc= 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "io/fs" 7 | "net/http" 8 | "os" 9 | "strings" 10 | 11 | "zgo.at/zhttp" 12 | "zgo.at/zstd/znet" 13 | ) 14 | 15 | var version = "" 16 | 17 | //go:embed www/* 18 | var webroot embed.FS 19 | 20 | func main() { 21 | zhttp.Static404 = func(w http.ResponseWriter, r *http.Request) { 22 | r.RequestURI = strings.ToLower(r.RequestURI) 23 | 24 | // Check redirect. 25 | if redirs := redirects[znet.RemovePort(strings.ToLower(r.Host))]; redirs != nil { 26 | // Exact match. 27 | if r, ok := redirs[r.RequestURI]; ok { 28 | w.Header().Add("Location", r) 29 | w.WriteHeader(301) 30 | return 31 | } 32 | 33 | // TODO: can optimize by doing this on startup. 34 | for k, v := range redirs { 35 | k = strings.ToLower(k) 36 | if strings.HasSuffix(k, "*") && strings.HasPrefix(r.RequestURI, k[:len(k)-1]) { 37 | w.Header().Add("Location", strings.Replace(v, "{}", r.RequestURI[len(k):], -1)) 38 | w.WriteHeader(301) 39 | return 40 | } 41 | } 42 | } 43 | 44 | w.Header().Add("Content-Type", "text/html") 45 | w.WriteHeader(404) 46 | fmt.Fprintf(w, `Page not found. Back home`) 47 | } 48 | 49 | var ( 50 | domains []string 51 | routers = make(map[string]http.Handler) 52 | ) 53 | dirs, err := fs.ReadDir(webroot, "www") 54 | if err != nil { 55 | fmt.Fprintf(os.Stderr, "zsrv: reading www: %s", err) 56 | os.Exit(1) 57 | } 58 | for _, d := range dirs { 59 | domains = append(domains, d.Name()) 60 | routers[strings.ReplaceAll(d.Name(), "STAR", "*")] = static(d.Name()) 61 | } 62 | 63 | for k, v := range domainRedirects { 64 | routers[k] = zhttp.RedirectHost("//" + v) 65 | } 66 | 67 | fmt.Printf("zsrv %s listening on %s\n", version, listen) 68 | fmt.Printf(" serving domains: %s\n", domains) 69 | w, err := zhttp.Serve(0, nil, &http.Server{ 70 | Addr: listen, 71 | Handler: zhttp.HostRoute(routers), 72 | }) 73 | if err != nil { 74 | panic(err) 75 | } 76 | <-w 77 | <-w 78 | } 79 | 80 | // TODO: be smarter about cache (per-file and per-filetype) 81 | func static(dir string) http.Handler { 82 | fsys, err := fs.Sub(webroot, "www/"+dir) 83 | if err != nil { 84 | panic(err) 85 | } 86 | 87 | stat := zhttp.NewStatic("", fsys, map[string]int{"": 86400}) 88 | 89 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 90 | if r.URL.Path == "" || r.URL.Path == "/" { 91 | r.URL.Path = "/index.html" 92 | } 93 | stat.ServeHTTP(w, r) 94 | }) 95 | } 96 | --------------------------------------------------------------------------------