Skip to content

Commit 4769998

Browse files
committed
feat(serverHandler): add https redirect and HSTS
- use `--hsts` to enable HSTS(HTTP Strict Transport Security) - use `--to-https` to indicate https port redirects to
1 parent 3db3573 commit 4769998

38 files changed

+622
-35
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,14 @@ server [options]
223223
-t|--template <file>
224224
Use a custom template file for rendering pages, instead of builtin template.
225225
226+
--hsts
227+
Enable HSTS(HTTP Strict Transport Security).
228+
Only available if current virtual host listens both plain HTTP and TLS on standard ports.
229+
--to-https [<target-port>]
230+
Redirect plain HTTP request to HTTPS TLS port.
231+
Target port must be exists in --listen-tls of current virtual host.
232+
If target port is omitted, the first item from --listen-tls will be used.
233+
226234
-S|--show <wildcard> ...
227235
-SD|--show-dir <wildcard> ...
228236
-SF|--show-file <wildcard> ...

README.zh-CN.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,14 @@ server [选项]
221221
-t|--template <模板文件>
222222
指定用于渲染页面的自定义模板,代替内建模板。
223223
224+
--hsts
225+
启用HSTS(HTTP Strict Transport Security)。
226+
仅当当前虚拟主机的纯HTTP和TLS模式都监听在标准端口上时才有效。
227+
--to-https [<目标端口>]
228+
将纯HTTP请求重定向到HTTPS端口。
229+
目标端口必须存在于当前虚拟主机--listen-tls中。
230+
如果省略目标端口,则使用--listen-tls中的第一项。
231+
224232
-S|--show <通配符> ...
225233
-SD|--show-dir <通配符> ...
226234
-SF|--show-file <通配符> ...

src/param/cli.go

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ func init() {
135135
err = options.AddFlagsValue("template", []string{"-t", "--template"}, "GHFS_TEMPLATE", "", "custom template file for page")
136136
serverErrHandler.CheckFatal(err)
137137

138+
err = options.AddFlag("globalhsts", "--hsts", "GHFS_HSTS", "enable HSTS(HTTP Strict Transport Security)")
139+
serverErrHandler.CheckFatal(err)
140+
141+
err = options.AddFlagValue("globalhttps", "--to-https", "GHFS_TO_HTTPS", "", "redirect http:// to https://, with optional target port")
142+
serverErrHandler.CheckFatal(err)
143+
138144
err = options.AddFlagsValues("shows", []string{"-S", "--show"}, "GHFS_SHOW", nil, "show directories or files match wildcard")
139145
serverErrHandler.CheckFatal(err)
140146
err = options.AddFlagsValues("showdirs", []string{"-SD", "--show-dir"}, "GHFS_SHOW_DIR", nil, "show directories match wildcard")
@@ -274,17 +280,6 @@ func doParseCli() []*Param {
274280
serverErrHandler.CheckFatal(errors.New("missing certificate key file"))
275281
}
276282

277-
// normalize listen
278-
listens, _ := result.GetStrings("listens")
279-
param.Listens = append(param.Listens, listens...)
280-
281-
listenRests := result.GetRests()
282-
param.Listens = append(param.Listens, listenRests...)
283-
284-
param.ListensPlain, _ = result.GetStrings("listensplain")
285-
286-
param.ListensTLS, _ = result.GetStrings("listenstls")
287-
288283
// normalize aliases
289284
arrAlias, _ := result.GetStrings("aliases")
290285
param.Aliases = normalizePathMaps(arrAlias)
@@ -356,6 +351,31 @@ func doParseCli() []*Param {
356351
serverErrHandler.CheckFatal(fmt.Errorf("duplicated usernames: %q", dupUserNames))
357352
}
358353

354+
// normalize listen
355+
listens, _ := result.GetStrings("listens")
356+
param.Listens = append(param.Listens, listens...)
357+
358+
listenRests := result.GetRests()
359+
param.Listens = append(param.Listens, listenRests...)
360+
361+
param.ListensPlain, _ = result.GetStrings("listensplain")
362+
363+
param.ListensTLS, _ = result.GetStrings("listenstls")
364+
365+
// hsts & https
366+
if len(param.ListensTLS) > 0 {
367+
param.GlobalHsts = result.HasKey("globalhsts")
368+
if param.GlobalHsts {
369+
param.GlobalHsts = validateHstsPort(param.ListensPlain, param.ListensTLS)
370+
}
371+
372+
param.GlobalHttps = result.HasKey("globalhttps")
373+
if param.GlobalHttps {
374+
httpsPort, _ := result.GetString("globalhttps")
375+
param.HttpsPort, param.GlobalHttps = normalizeHttpsPort(httpsPort, param.ListensTLS)
376+
}
377+
}
378+
359379
// shows
360380
shows, err := WildcardToRegexp(result.GetStrings("shows"))
361381
serverErrHandler.CheckFatal(err)

src/param/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ type Param struct {
5656
HostNames []string
5757
Template string
5858

59+
GlobalHsts bool
60+
GlobalHttps bool
61+
HttpsPort string
62+
5963
Shows *regexp.Regexp
6064
ShowDirs *regexp.Regexp
6165
ShowFiles *regexp.Regexp

src/param/strUtil.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,58 @@ func normalizeFilenames(inputs []string) []string {
9797

9898
return outputs
9999
}
100+
101+
func validateHstsPort(listensPlain, ListensTLS []string) bool {
102+
var fromOK, toOK bool
103+
104+
for _, listen := range listensPlain {
105+
port := util.ExtractListenPort(listen)
106+
if len(port) == 0 || port == "80" {
107+
fromOK = true
108+
break
109+
}
110+
}
111+
112+
for _, listen := range ListensTLS {
113+
port := util.ExtractListenPort(listen)
114+
if len(port) == 0 || port == "443" {
115+
toOK = true
116+
break
117+
}
118+
}
119+
120+
return fromOK && toOK
121+
}
122+
123+
func normalizeHttpsPort(httpsPort string, ListensTLS []string) (string, bool) {
124+
if len(httpsPort) > 0 {
125+
httpsPort = util.ExtractListenPort(httpsPort)
126+
if len(httpsPort) == 0 {
127+
return "", false
128+
}
129+
} else if len(ListensTLS) > 0 {
130+
httpsPort = util.ExtractListenPort(ListensTLS[0])
131+
}
132+
133+
lenHttpsPort := len(httpsPort)
134+
httpsColonPort := ":" + httpsPort
135+
for _, listen := range ListensTLS {
136+
if lenHttpsPort == 0 && len(listen) == 0 {
137+
return "", true
138+
}
139+
140+
port := util.ExtractListenPort(listen)
141+
if lenHttpsPort == 0 && len(port) == 0 {
142+
return "", true
143+
}
144+
if httpsPort == port {
145+
return httpsColonPort, true
146+
}
147+
148+
if httpsPort == "443" && port == "" {
149+
return "", true
150+
}
151+
}
152+
153+
return "", false
154+
}

src/param/strUtil_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,88 @@ func TestNormalizeFilenames(t *testing.T) {
5252
t.Fail()
5353
}
5454
}
55+
56+
func TestNormalizeHttpsPort(t *testing.T) {
57+
var httpsPort string
58+
var ok bool
59+
60+
httpsPort, ok = normalizeHttpsPort("123", []string{"123"})
61+
if !ok || httpsPort != ":123" {
62+
t.Error("1")
63+
}
64+
65+
httpsPort, ok = normalizeHttpsPort("234", []string{":234"})
66+
if !ok || httpsPort != ":234" {
67+
t.Error("2")
68+
}
69+
70+
httpsPort, ok = normalizeHttpsPort(":345", []string{"345"})
71+
if !ok || httpsPort != ":345" {
72+
t.Error("3")
73+
}
74+
75+
httpsPort, ok = normalizeHttpsPort(":456", []string{":456"})
76+
if !ok || httpsPort != ":456" {
77+
t.Error("4")
78+
}
79+
80+
httpsPort, ok = normalizeHttpsPort("", []string{""})
81+
if !ok || httpsPort != "" {
82+
t.Error("5")
83+
}
84+
85+
httpsPort, ok = normalizeHttpsPort("65536", []string{"65536"})
86+
if ok || httpsPort != "" {
87+
t.Error("6", httpsPort)
88+
}
89+
90+
httpsPort, ok = normalizeHttpsPort("", []string{"567"})
91+
if !ok || httpsPort != ":567" {
92+
t.Error("7")
93+
}
94+
95+
httpsPort, ok = normalizeHttpsPort("", []string{":678"})
96+
if !ok || httpsPort != ":678" {
97+
t.Error("8")
98+
}
99+
100+
httpsPort, ok = normalizeHttpsPort("789", []string{":890"})
101+
if ok || httpsPort != "" {
102+
t.Error("9")
103+
}
104+
105+
httpsPort, ok = normalizeHttpsPort("789", []string{"127.0.0.1:890"})
106+
if ok || httpsPort != "" {
107+
t.Error("10")
108+
}
109+
110+
httpsPort, ok = normalizeHttpsPort("789", []string{"[::1]:890"})
111+
if ok || httpsPort != "" {
112+
t.Error("11")
113+
}
114+
115+
httpsPort, ok = normalizeHttpsPort("", []string{":443"})
116+
if !ok || httpsPort != ":443" {
117+
t.Error("12")
118+
}
119+
120+
httpsPort, ok = normalizeHttpsPort("", []string{"127.0.0.1"})
121+
if !ok || httpsPort != "" {
122+
t.Error("13")
123+
}
124+
125+
httpsPort, ok = normalizeHttpsPort("", []string{"[::1]"})
126+
if !ok || httpsPort != "" {
127+
t.Error("14")
128+
}
129+
130+
httpsPort, ok = normalizeHttpsPort("443", []string{"127.0.0.1"})
131+
if !ok || httpsPort != "" {
132+
t.Error("15")
133+
}
134+
135+
httpsPort, ok = normalizeHttpsPort("443", []string{"[::1]"})
136+
if !ok || httpsPort != "" {
137+
t.Error("16")
138+
}
139+
}

src/serverHandler/hsts.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package serverHandler
2+
3+
import (
4+
"../util"
5+
"net/http"
6+
)
7+
8+
func (h *handler) hsts(w http.ResponseWriter, r *http.Request) (needRedirect bool) {
9+
_, port := util.ExtractHostnamePort(r.Host)
10+
11+
if len(port) > 0 {
12+
return
13+
}
14+
15+
header := w.Header()
16+
header.Set("Strict-Transport-Security", "max-age=31536000")
17+
18+
if r.TLS != nil {
19+
return
20+
}
21+
22+
location := "https://" + r.Host + r.RequestURI
23+
http.Redirect(w, r, location, http.StatusMovedPermanently)
24+
return true
25+
}
26+
27+
func (h *handler) https(w http.ResponseWriter, r *http.Request) (needRedirect bool) {
28+
if r.TLS != nil {
29+
return
30+
}
31+
32+
hostname, _ := util.ExtractHostnamePort(r.Host)
33+
34+
var targetPort string
35+
if len(h.httpsPort) > 0 && h.httpsPort != ":443" {
36+
targetPort = h.httpsPort
37+
}
38+
39+
targetHost := hostname + targetPort
40+
41+
location := "https://" + targetHost + r.RequestURI
42+
http.Redirect(w, r, location, http.StatusMovedPermanently)
43+
return true
44+
}

src/serverHandler/main.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import (
1414
type handler struct {
1515
root string
1616
emptyRoot bool
17+
globalHsts bool
18+
globalHttps bool
19+
httpsPort string // with prefix ":"
1720
defaultSort string
1821
urlPrefix string
1922

@@ -62,6 +65,16 @@ type handler struct {
6265
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
6366
go h.logRequest(r)
6467

68+
// hsts redirect
69+
if h.globalHsts && h.hsts(w, r) {
70+
return
71+
}
72+
73+
// https redirect
74+
if h.globalHttps && h.https(w, r) {
75+
return
76+
}
77+
6578
// asset
6679
const assetPrefix = "asset="
6780
if strings.HasPrefix(r.URL.RawQuery, assetPrefix) {
@@ -149,6 +162,9 @@ func NewHandler(
149162
h := &handler{
150163
root: root,
151164
emptyRoot: emptyRoot,
165+
globalHsts: p.GlobalHsts,
166+
globalHttps: p.GlobalHttps,
167+
httpsPort: p.HttpsPort,
152168
defaultSort: p.DefaultSort,
153169
urlPrefix: urlPrefix,
154170

src/util/digits.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package util
2+
3+
func IsDigits(input string) bool {
4+
for i, length := 0, len(input); i < length; i++ {
5+
b := input[i]
6+
if b < '0' || b > '9' {
7+
return false
8+
}
9+
}
10+
11+
return true
12+
}

src/util/digits_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package util
2+
3+
import "testing"
4+
5+
func TestIsDigits(t *testing.T) {
6+
var str string
7+
str = "12345"
8+
if !IsDigits(str) {
9+
t.Fail()
10+
}
11+
12+
str = "x12345"
13+
if IsDigits(str) {
14+
t.Fail()
15+
}
16+
}

0 commit comments

Comments
 (0)