├── .github
└── screenshot.png
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── bench
├── bench
└── main.lua
├── go.mod
├── internal
└── humanize
│ └── humanize.go
└── main.go
/.github/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshuarli/srv/fb87813ed0c9b5d243753496a311fa85d51bf6d1/.github/screenshot.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | srv
2 | srv-debug
3 | build/
4 | release/
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 |
9 | ## [0.6] - Unreleased
10 |
11 | Nothing planned at the moment.
12 |
13 |
14 | ## [0.5] - Feb 14 2021
15 | ### Fixed
16 | - Correctness fixes with serving more complicated filenames (links needed to be path escaped, and request uris path unescaped).
17 |
18 | ## [0.4] - 2020-10-09
19 | ### Changed
20 | - Mimetypes are now inferred firstly by file extension, and go's DetectContentType failing that.
21 |
22 | ### Fixed
23 | - As a result of the mimetype inference change mentioned above, CSS files for example will now be served with the appropriate text/css mimetype.
24 |
25 | ## [0.3] - 2020-08-16
26 | ### Added
27 | - Usage now shows Go's runtime version. Also builds with 0.15.
28 |
29 | ### Changed
30 | - User Agent strings are now logged.
31 |
32 | ### Fixed
33 | - Links to filenames with quotes are now html-escaped so they work.
34 |
35 | ## [0.2] - 2020-06-05
36 | ### Added
37 | - Custom bind address with `-b address`.
38 | - Optional TLS with `-c certfile -k keyfile`.
39 |
40 | ### Changed
41 | - Directory entries are now naturally/alphanumerically sorted.
42 | - Symlinks were made forbidden.
43 | - Sends `Cache-Control: no-store` for HTTP 1.1+ clients that obey it (pretty much all major browsers).
44 | - Rendering performance and size was improved.
45 | - Browsers should not request favicons anymore.
46 |
47 | ## 0.1 - 2019-09-03
48 | Initial release.
49 |
50 |
51 | [0.6]: https://github.com/joshuarli/srv/compare/0.6...HEAD
52 | [0.5]: https://github.com/joshuarli/srv/compare/0.4...0.5
53 | [0.4]: https://github.com/joshuarli/srv/compare/0.3...0.4
54 | [0.3]: https://github.com/joshuarli/srv/compare/0.2...0.3
55 | [0.2]: https://github.com/joshuarli/srv/compare/0.1...0.2
56 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019 JoshuaRLi (Joshua Li)
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4 |
5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
6 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | NAME := srv
2 | VERSION := $(shell printf git-%s-%s\\n "$$(git describe --tags --abbrev=0)" "$$(git log -1 --pretty=format:'%h')")
3 | GO_BUILDFLAGS := -trimpath
4 | GO_LDFLAGS := -ldflags "-s -w -X main.VERSION=$(VERSION)"
5 | GO_LDFLAGS_DEBUG := -ldflags "-X main.VERSION=$(VERSION)-DEBUG"
6 | GO_LDFLAGS_STATIC := -tags netgo -ldflags "-s -w -X main.VERSION=$(VERSION) -extldflags -static"
7 |
8 | .PHONY: build debug fmt lint clean release
9 |
10 | build: clean fmt lint $(NAME)
11 |
12 | $(NAME): main.go
13 | go build $(GO_BUILDFLAGS) -o $@ $(GO_LDFLAGS) .
14 |
15 | debug: $(NAME)-debug
16 | $(NAME)-debug: main.go
17 | go build $(GO_BUILDFLAGS) -o $@ -gcflags="all=-N -l" $(GO_LDFLAGS_DEBUG) .
18 |
19 | fmt:
20 | go fmt
21 |
22 | lint:
23 | golint
24 |
25 | clean:
26 | rm -f $(NAME) $(NAME)-debug
27 | rm -rf release
28 |
29 | # release static crossbuilds
30 | define buildrelease
31 | GOOS=$(1) GOARCH=$(2) go build $(GO_BUILDFLAGS) \
32 | -a \
33 | -o release/$(NAME)-$(1)-$(2) \
34 | $(GO_LDFLAGS_STATIC) . ;
35 | upx -9 release/$(NAME)-$(1)-$(2);
36 | sha512sum release/$(NAME)-$(1)-$(2) > release/$(NAME)-$(1)-$(2).sha512sum;
37 | endef
38 |
39 | GOOSARCHES = linux/arm linux/arm64 linux/amd64 darwin/amd64
40 |
41 | release: main.go
42 | $(foreach GOOSARCH,$(GOOSARCHES), $(call buildrelease,$(subst /,,$(dir $(GOOSARCH))),$(notdir $(GOOSARCH))))
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # srv
2 |
3 | minimalist http(s) server and file browser.
4 |
5 |
6 |
7 |
8 | ## download
9 |
10 | static executables for some platforms can be found [here](https://github.com/joshuarli/srv/releases).
11 |
12 |
13 | ## usage
14 |
15 | Simply `srv`. Defaults are `-p 8000 -b 127.0.0.1 -d .`
16 |
17 |
18 | ## usage: TLS
19 |
20 | TLS and HTTP/2 are enabled if you pass `-c certfile -k keyfile`.
21 |
22 | to make self-signed certs:
23 |
24 | openssl req -nodes -new -x509 -keyout key.pem -out cert.pem -subj "/"
25 |
26 | or better, locally trusted certs with [mkcert](https://github.com/FiloSottile/mkcert):
27 |
28 | mkcert -install
29 | mkcert -key-file key.pem -cert-file cert.pem -ecdsa 127.0.0.1
30 |
--------------------------------------------------------------------------------
/bench/bench:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4 |
5 | wrk -t8 -c8 -d5s -s "${HERE}/main.lua" http://127.0.0.1:8000
6 |
--------------------------------------------------------------------------------
/bench/main.lua:
--------------------------------------------------------------------------------
1 | local threads = {}
2 |
3 | function setup(thread)
4 | table.insert(threads, thread)
5 | thread:set("tid", table.getn(threads))
6 | end
7 |
8 | function init(args)
9 | responses = {}
10 | end
11 |
12 | wrk.method = "GET"
13 | wrk.path = "/"
14 |
15 | function response(status, headers, body)
16 | if responses[status] == nil then
17 | responses[status] = 1
18 | else
19 | responses[status] = responses[status] + 1
20 | end
21 | end
22 |
23 | function done(summary, latency, requests)
24 | print("wrk is done. response code counts:")
25 | local freqs = {}
26 | for _, thread in pairs(threads) do
27 | for code, freq in pairs(thread:get("responses")) do
28 | if freqs[code] == nil then
29 | freqs[code] = freq
30 | else
31 | freqs[code] = freqs[code] + freq
32 | end
33 | end
34 | end
35 | for code, freq in pairs(freqs) do
36 | print(code, freq)
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/joshuarli/srv
2 |
3 | go 1.15
4 |
--------------------------------------------------------------------------------
/internal/humanize/humanize.go:
--------------------------------------------------------------------------------
1 | package humanize
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | func FileSize(nbytes int64) string {
8 | if nbytes < 1024 {
9 | return fmt.Sprintf("%d", nbytes)
10 | }
11 | var exp int
12 | n := float64(nbytes)
13 | for exp = 0; exp < 4; exp++ {
14 | n /= 1024
15 | if n < 1024 {
16 | break
17 | }
18 | }
19 | return fmt.Sprintf("%.1f%c", float64(n), "KMGT"[exp])
20 | }
21 |
22 | func isdigit(b byte) bool { return '0' <= b && b <= '9' }
23 |
24 | // Vendored and slightly adapted from github.com/fvbommel/util/blob/master/sortorder/natsort.go
25 | // I wrote my own alphanumeric/natural sort, but it turned out to be inferior to this one.
26 | // Limitations (deliberate tradeoffs for speed and simplicity):
27 | // - only ASCII digits (0-9) are considered
28 | // - does not understand signs, scientific notation, or floating point representations
29 | func NaturalLess(str1, str2 string) bool {
30 | idx1, idx2 := 0, 0
31 | for idx1 < len(str1) && idx2 < len(str2) {
32 | c1, c2 := str1[idx1], str2[idx2]
33 | dig1, dig2 := isdigit(c1), isdigit(c2)
34 | switch {
35 | case dig1 != dig2: // Digits before other characters.
36 | return dig1 // True if LHS is a digit, false if the RHS is one.
37 | case !dig1: // && !dig2, because dig1 == dig2
38 | // UTF-8 compares bytewise-lexicographically, no need to decode
39 | // codepoints.
40 | if c1 != c2 {
41 | return c1 < c2
42 | }
43 | idx1++
44 | idx2++
45 | default: // Digits
46 | // Eat zeros.
47 | for ; idx1 < len(str1) && str1[idx1] == '0'; idx1++ {
48 | }
49 | for ; idx2 < len(str2) && str2[idx2] == '0'; idx2++ {
50 | }
51 | // Eat all digits.
52 | nonZero1, nonZero2 := idx1, idx2
53 | for ; idx1 < len(str1) && isdigit(str1[idx1]); idx1++ {
54 | }
55 | for ; idx2 < len(str2) && isdigit(str2[idx2]); idx2++ {
56 | }
57 | // If lengths of numbers with non-zero prefix differ, the shorter
58 | // one is less.
59 | if len1, len2 := idx1-nonZero1, idx2-nonZero2; len1 != len2 {
60 | return len1 < len2
61 | }
62 | // If they're equal, string comparison is correct.
63 | if nr1, nr2 := str1[nonZero1:idx1], str2[nonZero2:idx2]; nr1 != nr2 {
64 | return nr1 < nr2
65 | }
66 | // Otherwise, the one with less zeros is less.
67 | // Because everything up to the number is equal, comparing the index
68 | // after the zeros is sufficient.
69 | if nonZero1 != nonZero2 {
70 | return nonZero1 < nonZero2
71 | }
72 | }
73 | // They're identical so far, so continue comparing.
74 | }
75 | // So far they are identical. At least one is ended. If the other continues,
76 | // it sorts last.
77 | return len(str1) < len(str2)
78 | }
79 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io"
7 | "log"
8 | "net"
9 | "net/http"
10 | "net/url"
11 | "os"
12 | "path"
13 | "runtime"
14 | "sort"
15 | "strings"
16 | "time"
17 |
18 | "github.com/joshuarli/srv/internal/humanize"
19 | )
20 |
21 | type context struct {
22 | srvDir string
23 | }
24 |
25 | // We write the shortest browser-valid base64 data string,
26 | // so that the browser does not request the favicon.
27 | const listingPrelude = `
%s/ | |
%s | %s |
%s |