Skip to content

Commit dd0e4fd

Browse files
committed
feat: add redirect option
0 parents  commit dd0e4fd

File tree

14 files changed

+443
-0
lines changed

14 files changed

+443
-0
lines changed

.editorconfig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
root = true
2+
3+
[*]
4+
end_of_line = lf
5+
insert_final_newline = true
6+
max_line_length = 200
7+
indent_style = tab
8+
indent_size = 4
9+
charset = utf-8
10+

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.*/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2022 MJ PC Lab
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 deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
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 all
13+
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 FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Extra HTTP File Server
2+
3+
Extra HTTP File Server is based on Go HTTP File Server, with extra features.
4+
It provides frequently used features for a simple static website.
5+
6+
# Different to Go HTTP File Server
7+
8+
## Code base
9+
10+
Based on Go HTTP File Server's main branch, dropped support for legacy Go version.
11+
This means it is impossible to use legacy Go version to compile binaries for legacy systems, e.g. Windows XP.
12+
13+
## New options
14+
15+
```
16+
--redirect <separator><match><separator><replace>[<separator><status_code>]
17+
Perform an HTTP redirect when request URL (in the form of "/request/path?param=value")
18+
is matched by regular expression `match`.
19+
20+
The redirect target is specified by `replace`.
21+
Use `$0` to represent the whole match in `match`.
22+
use `$1` - `$9` to represent sub matches in `match`.
23+
24+
Optional `status_code` specifies HTTP redirect code. defaults to 301.
25+
```
26+
27+
## Examples
28+
29+
Perform redirect according to `redirect` param:
30+
31+
```sh
32+
# when requesting http://localhost:8080/redirect/www.example.com, redirect to https://www.example.com
33+
ehfs -l 8080 -r /path/to/share --redirect '#/redirect/(.*)#https://$1'
34+
```

README.zh-CN.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Extra HTTP File Server
2+
3+
Extra HTTP File Server基于Go HTTP File Server,附带额外功能。
4+
它为简单静态网站提供了常用的功能。
5+
6+
# 与Go HTTP File Server的区别
7+
8+
## 代码库
9+
10+
基于Go HTTP File Server主分支,放弃了对旧版Go的支持。
11+
这意味着不能使用旧的Go版本来编译较老系统的二进制文件,例如Windows XP。
12+
13+
## 新增选项
14+
15+
```
16+
--redirect <分隔符><match><separator><分隔符>[<separator><status_code>]
17+
当请求的URL(“/request/path?param=value”的形式)匹配正则表达式`match`时,
18+
执行HTTP重定向。
19+
20+
重定向目标由`replace`指定。
21+
使用`$0`表示`match`的完整匹配。
22+
使用`$1`-`$9`来表示`match`中的子匹配。
23+
24+
可选的`status_code`指定HTTP重定向代码。 默认为301。
25+
```
26+
27+
## 举例
28+
29+
根据`redirect`参数执行重定向:
30+
31+
```sh
32+
# 当请求 http://localhost:8080/redirect/www.example.com时,重定向到https://www.example.com
33+
ehfs -l 8080 -r /path/to/share --redirect '#/redirect/(.*)#https://$1'
34+
```

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module mjpclab.dev/ehfs
2+
3+
go 1.19
4+
5+
require mjpclab.dev/ghfs v1.15.2

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
mjpclab.dev/ghfs v1.15.2 h1:MucVnmAI62js3fY9TYdwcha+V36Mj1N+Cytk7r5W4FM=
2+
mjpclab.dev/ghfs v1.15.2/go.mod h1:mwJoteyRIJ9QXBxai58QmsHvTb29GFBbaumgZWpxxqk=

src/main.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"mjpclab.dev/ehfs/src/middleware"
6+
"mjpclab.dev/ehfs/src/param"
7+
"mjpclab.dev/ehfs/src/version"
8+
"mjpclab.dev/ghfs/src/app"
9+
"mjpclab.dev/ghfs/src/serverError"
10+
"mjpclab.dev/ghfs/src/setting"
11+
"os"
12+
"os/signal"
13+
"syscall"
14+
)
15+
16+
func cleanupOnEnd(appInst *app.App) {
17+
chSignal := make(chan os.Signal)
18+
signal.Notify(chSignal, syscall.SIGINT, syscall.SIGTERM)
19+
20+
go func() {
21+
<-chSignal
22+
appInst.Shutdown()
23+
os.Exit(0)
24+
}()
25+
}
26+
27+
func reopenLogOnHup(appInst *app.App) {
28+
chSignal := make(chan os.Signal)
29+
signal.Notify(chSignal, syscall.SIGHUP)
30+
31+
go func() {
32+
for range chSignal {
33+
errs := appInst.ReOpenLog()
34+
serverError.CheckFatal(errs...)
35+
}
36+
}()
37+
}
38+
39+
func main() {
40+
// params
41+
baseParams, params, printVersion, printHelp, errs := param.ParseFromCli()
42+
serverError.CheckFatal(errs...)
43+
if printVersion {
44+
version.PrintVersion()
45+
os.Exit(0)
46+
}
47+
if printHelp {
48+
param.PrintHelp()
49+
os.Exit(0)
50+
}
51+
52+
// apply middlewares
53+
errs = middleware.ApplyMiddlewares(baseParams, params)
54+
serverError.CheckFatal(errs...)
55+
56+
// setting
57+
setting := setting.ParseFromEnv()
58+
59+
// app
60+
appInst, errs := app.NewApp(baseParams, setting)
61+
serverError.CheckFatal(errs...)
62+
if appInst == nil {
63+
serverError.CheckFatal(errors.New("failed to create application instance"))
64+
}
65+
66+
cleanupOnEnd(appInst)
67+
reopenLogOnHup(appInst)
68+
errs = appInst.Open()
69+
serverError.CheckFatal(errs...)
70+
appInst.Shutdown()
71+
}

src/middleware/main.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package middleware
2+
3+
import (
4+
"errors"
5+
"mjpclab.dev/ehfs/src/param"
6+
"mjpclab.dev/ghfs/src/middleware"
7+
baseParam "mjpclab.dev/ghfs/src/param"
8+
"mjpclab.dev/ghfs/src/serverError"
9+
)
10+
11+
var errInvalidParamValue = errors.New("invalid param value")
12+
var errParamCountNotMatch = errors.New("base-param count is not equal to param count")
13+
14+
func ParamToMiddlewares(baseParam *baseParam.Param, param *param.Param) (preMids, postMids []middleware.Middleware, errs []error) {
15+
// redirects
16+
for i := range param.Redirects {
17+
mid, err := getRedirectMiddleware(param.Redirects[i])
18+
errs = serverError.AppendError(errs, err)
19+
if mid != nil {
20+
preMids = append(preMids, mid)
21+
}
22+
}
23+
24+
return
25+
}
26+
27+
func ApplyMiddlewares(baseParams []*baseParam.Param, params []*param.Param) (errs []error) {
28+
if len(baseParams) != len(params) {
29+
return []error{errParamCountNotMatch}
30+
}
31+
32+
for i := range baseParams {
33+
var es []error
34+
baseParams[i].PreMiddlewares, baseParams[i].PostMiddlewares, es = ParamToMiddlewares(baseParams[i], params[i])
35+
errs = append(errs, es...)
36+
}
37+
38+
return
39+
}

src/middleware/redirect.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package middleware
2+
3+
import (
4+
"mjpclab.dev/ghfs/src/middleware"
5+
"net/http"
6+
"net/url"
7+
"regexp"
8+
"strconv"
9+
"strings"
10+
)
11+
12+
func getRedirectMiddleware(arg [3]string) (middleware.Middleware, error) {
13+
var err error
14+
var reMatch *regexp.Regexp
15+
var replace string
16+
var code int
17+
18+
reMatch, err = regexp.Compile(arg[0])
19+
if err != nil {
20+
return nil, err
21+
}
22+
replace = arg[1]
23+
code, err = strconv.Atoi(arg[2])
24+
if err != nil {
25+
return nil, err
26+
}
27+
28+
return func(w http.ResponseWriter, r *http.Request, context *middleware.Context) (processed bool) {
29+
requestURI := r.URL.RequestURI() // request uri without prefix path
30+
if !reMatch.MatchString(requestURI) {
31+
return false
32+
}
33+
matches := reMatch.FindStringSubmatch(requestURI)
34+
if len(matches) > 10 {
35+
matches = matches[:10]
36+
}
37+
38+
target := replace
39+
for i := range matches {
40+
target = strings.ReplaceAll(target, "$"+strconv.Itoa(i), matches[i])
41+
}
42+
if len(target) == 0 {
43+
target = "/"
44+
}
45+
46+
u, err := url.Parse(target)
47+
if err != nil ||
48+
((len(u.Host) == 0 || u.Host == r.Host) && u.RequestURI() == r.RequestURI) {
49+
w.WriteHeader(http.StatusBadRequest)
50+
} else {
51+
http.Redirect(w, r, u.String(), code)
52+
}
53+
return true
54+
}, nil
55+
}

src/middleware/redirect_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package middleware
2+
3+
import (
4+
"mjpclab.dev/ghfs/src/middleware"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
)
9+
10+
func TestRedirect(t *testing.T) {
11+
mid, err := getRedirectMiddleware([3]string{`\?goto=(.*)`, "$1", "307"})
12+
if err != nil {
13+
t.FailNow()
14+
}
15+
16+
var w *httptest.ResponseRecorder
17+
var r *http.Request
18+
var result middleware.ProcessResult
19+
var location string
20+
21+
w = httptest.NewRecorder()
22+
r = httptest.NewRequest(http.MethodGet, "/abc", nil)
23+
result = mid(w, r, &middleware.Context{})
24+
if result != middleware.GoNext {
25+
t.Error(result)
26+
}
27+
if w.Code != 200 {
28+
t.Error(w.Code)
29+
}
30+
location = w.Header().Get("Location")
31+
if location != "" {
32+
t.Error(location)
33+
}
34+
35+
w = httptest.NewRecorder()
36+
r = httptest.NewRequest(http.MethodGet, "/abc?goto=/", nil)
37+
result = mid(w, r, &middleware.Context{})
38+
if result != middleware.Processed {
39+
t.Error(result)
40+
}
41+
if w.Code != 307 {
42+
t.Error(w.Code)
43+
}
44+
location = w.Header().Get("Location")
45+
if location != "/" {
46+
t.Error(location)
47+
}
48+
49+
w = httptest.NewRecorder()
50+
r = httptest.NewRequest(http.MethodGet, "/abc?goto=http://www.example.com/", nil)
51+
result = mid(w, r, &middleware.Context{})
52+
if result != middleware.Processed {
53+
t.Error(result)
54+
}
55+
if w.Code != 307 {
56+
t.Error(w.Code)
57+
}
58+
location = w.Header().Get("Location")
59+
if location != "http://www.example.com/" {
60+
t.Error(location)
61+
}
62+
}

0 commit comments

Comments
 (0)