Skip to content

Commit e983cdd

Browse files
committed
Improve server shutdown handling, and restrict allowed methods for erised/headers, erised/ip, erised/info and erised/shutdown routes
1 parent 7c97263 commit e983cdd

File tree

4 files changed

+134
-41
lines changed

4 files changed

+134
-41
lines changed

README.md

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,48 +23,48 @@ Parameters:
2323

2424
For help type **erised -h**
2525

26-
Upon executing **erised** with no parameters, the server will listen on port **8080** for incoming http requests.
26+
When executing **erised** with no parameters, the server will listen on port **8080** for incoming http requests.
2727

28-
When using the _-path_ option, please **EXERCISE GREAT CAUTION** choosing the path to search. See **Known Issues** for more information.
28+
If you're using the _-path_ option, please **EXERCISE GREAT CAUTION** when setting the path to search. See **Known Issues** for more information.
2929

3030
The latest version is also available as a Docker image at [edaddario/erised](https://hub.docker.com/r/edaddario/erised).
3131

32-
To start the server in a docker container, with defaults values, exceute the following command:
32+
To start the server in a docker container, with defaults values, execute the following command:
3333

3434
```sh
35-
docker run --rm -p 8080:8080 edaddario/erised
35+
docker run --rm -p 8080:8080 --name erised edaddario/erised
3636
```
3737

3838
If you would like to return file based responses (_X-Erised-Response-File_ set) when using the docker image, you'll need to map the directory containing your local files and set the _-path_ option accordingly.
3939

4040
The following example maps the **/local_directory/response_files** directory in your local machine to **/files** in the docker image, and then sets the **-path** option:
4141

4242
```sh
43-
docker run --rm -p 8080:8080 -v /local_directory/response_files:/files edaddario/erised -path ./files
43+
docker run --rm -p 8080:8080 --name erised -v /local_directory/response_files:/files edaddario/erised -path ./files
4444
```
4545

46-
HTTP methods (e.g. GET, POST, PATCH, etc.), query strings and body are **ignored**. URL routes are also ignored, except for:
46+
URL routes, HTTP methods (e.g. GET, POST, PATCH, etc.), query strings and body are **ignored**, except for:
4747

48-
|Name|Purpose|
49-
|--|--|
50-
|erised/headers|Returns request headers|
51-
|erised/info|Returns miscellaneous information|
52-
|erised/ip|Returns the client IP|
53-
|erised/shutdown|Shutdowns the server|
48+
| Name | Method | Purpose |
49+
|-----------------|--------|-----------------------------------|
50+
| erised/headers | GET | Returns request headers |
51+
| erised/info | GET | Returns miscellaneous information |
52+
| erised/ip | GET | Returns the client IP |
53+
| erised/shutdown | POST | Shutdowns the server |
5454

5555
Response behaviour is controlled via custom headers in the http request:
5656

57-
|Name|Purpose|
58-
|--|--|
59-
|X-Erised-Content-Type|Sets the response _Content-Type_. Valid values are **text** (default) for _text/plain_, **json** for _application/json_, **xml** for _application/xml_ and **gzip** for _application/octet-stream_. When using **gzip**, _Content-Encoding_ is also set to **gzip** and the response body is compressed accordingly.|
60-
|X-Erised-Data|Returns the **same** value in the response body|
61-
|X-Erised-Headers|Returns the value(s) in the response header. Values **must** be in a JSON key/value list|
62-
|X-Erised-Location|Sets the response _Location_ to the new (redirected) URL or path, when 300 ≤ _X-Erised-Status-Code_ < 310|
63-
|X-Erised-Response-Delay|Number of **milliseconds** to wait before sending response back to client|
64-
|X-Erised-Response-File|Returns the contents of **file** in the response body. If present, _X-Erised-Data_ is ignored|
65-
|X-Erised-Status-Code|Sets the HTTP Status Code|
57+
| Name | Purpose |
58+
|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
59+
| X-Erised-Content-Type | Sets the response _Content-Type_. Valid values are **text** (default) for _text/plain_, **json** for _application/json_, **xml** for _application/xml_ and **gzip** for _application/octet-stream_. When using **gzip**, _Content-Encoding_ is also set to **gzip** and the response body is compressed accordingly. |
60+
| X-Erised-Data | Returns the **same** value in the response body |
61+
| X-Erised-Headers | Returns the value(s) in the response header. Values **must** be in a JSON key/value list |
62+
| X-Erised-Location | Sets the response _Location_ to the new (redirected) URL or path, when 300 ≤ _X-Erised-Status-Code_ < 310 |
63+
| X-Erised-Response-Delay | Number of **milliseconds** to wait before sending response back to client |
64+
| X-Erised-Response-File | Returns the contents of **file** in the response body. If present, _X-Erised-Data_ is ignored |
65+
| X-Erised-Status-Code | Sets the HTTP Status Code |
6666

67-
By design, no validation is performed on _X-Erised-Data_ or _X-Erised-Location_.
67+
No validation is performed on _X-Erised-Data_ or _X-Erised-Location_.
6868

6969
Valid _X-Erised-Status-Code_ values are:
7070
```text
@@ -104,6 +104,7 @@ NetworkAuthenticationRequired or 511
104104
Any other value will resolve to 200 (OK)
105105

106106
# Release History
107+
* v0.6.7 - Improve server shutdown handling, and restrict allowed methods for _erised/headers_, _erised/ip_, _erised/info_ and _erised/shutdown_ routes
107108
* v0.5.4 - Update dependencies
108109
* v0.5.3 - Add file based responses
109110
* v0.4.1 - Add route concurrency, update tests and dependencies
@@ -117,20 +118,20 @@ Any other value will resolve to 200 (OK)
117118
* v0.0.1 - Initial release
118119

119120
# Known Issues
120-
**erised** is full of bugs and "_...men have wasted away before it, not knowing if what they have seen is real, or even possible..._" so use it with caution for it gives no knowledge or truth.
121+
**erised** may be full of bugs. Poeple "_... have wasted away before it, not knowing if what they have seen is real, or even possible..._" so, use it with caution for it gives no knowledge or truth.
121122

122123
Of all of its deficiencies, the most notable is:
123124
* Using the _-path_ option could lead to significant security risks. By default, **erised** sets this option to point to the same directory in which is running and, when the _X-Erised-Response-File_ header is set, it will search recursively for a matching filename in the current directory and **all** subdirectories underneath, returning the contents of the first match. For example, if you set this value to your root directory (_-path=/_) **erised** will scan the entire volume for a match
124125
* https protocol is not yet supported
125126

126-
I may or may not address this in a future release. Caveat Emptor
127+
I may or may not address these issues in a future release. Caveat Emptor
127128

128129
# Motivation
129-
When developing and testing REST based API clients, sooner or later I'd come across situations where I needed a quick and easy way to dynamically test endpoint's responses under different scenarios. Although there are many excellent frameworks and mock servers available, the time and effort required to configure them is sometimes not justified, specially if the application under test provides 10's or 100's of routes, so after some brief and unsuccessful googling I decided to create my own.
130+
When developing and testing REST API clients, sooner or later I'd come across situations where I needed a quick and easy way to dynamically test endpoint's responses under different scenarios. Although there are many excellent frameworks and mock servers available, the time and effort required to configure them is sometimes not justified, specially if the application under test exposes many routes, so after some brief and unsuccessful googling I decided to create my own.
130131

131-
**erised** was inspired somewhat by [Kenneth Reitz's](https://kennethreitz.org/) HTTP Request & Response Service [httpbin.io](https://httpbin.org/) and it may offer similar functionality in future releases.
132+
**erised** was inspired by [Kenneth Reitz's](https://kennethreitz.org/) HTTP Request & Response Service [httpbin.io](https://httpbin.org/) and it may offer similar functionality in future releases.
132133

133-
The typical use case is to get a response to an arbitrary http request where the content of the body has a predetermined value and your ability to control the server's behaviour is limited or non-existent.
134+
The typical use case is to get a response to an arbitrary http request when your ability to control the server's behaviour is limited or non-existent.
134135

135136
Imagine you're developing some client for [api.chucknorris.io](https://api.chucknorris.io/) and want to test the **/jokes/random** path. You could certainly make live calls against the server:
136137
```sh
@@ -155,7 +156,7 @@ curl -w '\n' -v -k https://api.chucknorris.io/jokes/random
155156
* Closing connection 0
156157
```
157158
158-
**Or**, better yet, you could use **erised** like this:
159+
**Or**, even better yet, you could use **erised** like this:
159160
```sh
160161
curl -w '\n' -v \
161162
-H "X-Erised-Status-Code:OK" \
@@ -186,7 +187,7 @@ http://localhost:8080/jokes/random
186187
* Closing connection 0
187188
```
188189
189-
**and** also to test some common failures like,
190+
**and** even simulate common failures like,
190191
```sh
191192
curl -w '\n' -v \
192193
-H "X-Erised-Status-Code:NotFound" \

cmd/erised/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
"github.com/rs/zerolog/log"
1717
)
1818

19-
const version = "v0.5.4"
19+
const version = "v0.6.7"
2020

2121
type server struct {
2222
mux *http.ServeMux

cmd/erised/serverRoutes.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ package main
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
7-
"io/ioutil"
88
"net/http"
99
"os"
1010
"path/filepath"
@@ -80,7 +80,7 @@ func (s *server) handleLanding() http.HandlerFunc {
8080
}
8181

8282
if !info.IsDir() && filepath.Base(path) == fn {
83-
if ct, err := ioutil.ReadFile(path); err != nil {
83+
if ct, err := os.ReadFile(path); err != nil {
8484
log.Error().Msg("Unable to open the file: " + path)
8585
} else {
8686
data = string(ct)
@@ -115,6 +115,12 @@ func (s *server) handleHeaders() http.HandlerFunc {
115115
Str("uri", req.RequestURI).
116116
Msg("handleHeaders")
117117

118+
if req.Method != http.MethodGet {
119+
log.Error().Msg("Method " + req.Method + " not allowed for /erised/headers")
120+
http.Error(res, "Method Not Allowed", http.StatusMethodNotAllowed)
121+
return
122+
}
123+
118124
res.Header().Set("Content-Type", "application/json")
119125
data := "{"
120126

@@ -150,6 +156,12 @@ func (s *server) handleInfo() http.HandlerFunc {
150156
Str("uri", req.RequestURI).
151157
Msg("handleInfo")
152158

159+
if req.Method != http.MethodGet {
160+
log.Error().Msg("Method " + req.Method + " not allowed for /erised/info")
161+
http.Error(res, "Method Not Allowed", http.StatusMethodNotAllowed)
162+
return
163+
}
164+
153165
res.Header().Set("Content-Type", "application/json")
154166

155167
data := "{"
@@ -177,6 +189,12 @@ func (s *server) handleIP() http.HandlerFunc {
177189
Str("uri", req.RequestURI).
178190
Msg("handleIP")
179191

192+
if req.Method != http.MethodGet {
193+
log.Error().Msg("Method " + req.Method + " not allowed for /erised/ip")
194+
http.Error(res, "Method Not Allowed", http.StatusMethodNotAllowed)
195+
return
196+
}
197+
180198
res.Header().Set("Content-Type", "application/json")
181199

182200
data := "{"
@@ -201,6 +219,12 @@ func (s *server) handleShutdown() http.HandlerFunc {
201219
Str("uri", req.RequestURI).
202220
Msg("handleShutdown")
203221

222+
if req.Method != http.MethodPost {
223+
log.Error().Msg("Method " + req.Method + " not allowed for /erised/shutdown")
224+
http.Error(res, "Method Not Allowed", http.StatusMethodNotAllowed)
225+
return
226+
}
227+
204228
res.Header().Set("Content-Type", "application/json")
205229

206230
s.respond(res, encodingJSON, 0, "{\"shutdown\":\"ok\"}")

cmd/erised/serverRoutes_test.go

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,37 @@ func TestErisedInfoRoute(t *testing.T) {
1717
RegisterFailHandler(func(m string, _ ...int) { g.Fail(m) })
1818
exp := `{"Host":"localhost:8080","Method":"GET","Protocol":"HTTP/1.1","Request URI":"http://localhost:8080/erised/info"}`
1919
svr := server{}
20-
req := httptest.NewRequest(http.MethodGet, "http://localhost:8080/erised/info", nil)
21-
res := httptest.NewRecorder()
22-
svr.handleInfo().ServeHTTP(res, req)
2320

2421
g.Describe("Test erised/info", func() {
2522
g.It("Should return StatusOK", func() {
23+
res := httptest.NewRecorder()
24+
req := httptest.NewRequest(http.MethodGet, "http://localhost:8080/erised/info", nil)
25+
svr.handleInfo().ServeHTTP(res, req)
26+
2627
Ω(res.Code).Should(Equal(http.StatusOK))
2728
})
2829

30+
g.It("Should return MethodNotAllowed", func() {
31+
res := httptest.NewRecorder()
32+
req := httptest.NewRequest(http.MethodPost, "http://localhost:8080/erised/info", nil)
33+
svr.handleInfo().ServeHTTP(res, req)
34+
35+
Ω(res.Code).Should(Equal(http.StatusMethodNotAllowed))
36+
})
37+
2938
g.It("Should match expected body", func() {
39+
res := httptest.NewRecorder()
40+
req := httptest.NewRequest(http.MethodGet, "http://localhost:8080/erised/info", nil)
41+
svr.handleInfo().ServeHTTP(res, req)
42+
3043
Ω(res.Body.String()).Should(Equal(exp))
3144
})
3245

3346
g.It("Should match Content-Type header", func() {
47+
res := httptest.NewRecorder()
48+
req := httptest.NewRequest(http.MethodGet, "http://localhost:8080/erised/info", nil)
49+
svr.handleInfo().ServeHTTP(res, req)
50+
3451
Ω(res.Header().Get("Content-Type")).Should(Equal("application/json"))
3552
})
3653
})
@@ -42,20 +59,37 @@ func TestErisedIPRoute(t *testing.T) {
4259
RegisterFailHandler(func(m string, _ ...int) { g.Fail(m) })
4360
exp := `{"Client IP":"192.0.2.1:1234"}`
4461
svr := server{}
45-
req := httptest.NewRequest(http.MethodGet, "http://localhost:8080/erised/ip", nil)
46-
res := httptest.NewRecorder()
47-
svr.handleIP().ServeHTTP(res, req)
4862

4963
g.Describe("Test erised/ip", func() {
5064
g.It("Should return StatusOK", func() {
65+
res := httptest.NewRecorder()
66+
req := httptest.NewRequest(http.MethodGet, "http://localhost:8080/erised/ip", nil)
67+
svr.handleIP().ServeHTTP(res, req)
68+
5169
Ω(res.Code).Should(Equal(http.StatusOK))
5270
})
5371

72+
g.It("Should return MethodNotAllowed", func() {
73+
res := httptest.NewRecorder()
74+
req := httptest.NewRequest(http.MethodPost, "http://localhost:8080/erised/ip", nil)
75+
svr.handleIP().ServeHTTP(res, req)
76+
77+
Ω(res.Code).Should(Equal(http.StatusMethodNotAllowed))
78+
})
79+
5480
g.It("Should match expected body", func() {
81+
res := httptest.NewRecorder()
82+
req := httptest.NewRequest(http.MethodGet, "http://localhost:8080/erised/ip", nil)
83+
svr.handleIP().ServeHTTP(res, req)
84+
5585
Ω(res.Body.String()).Should(Equal(exp))
5686
})
5787

5888
g.It("Should match Content-Type header", func() {
89+
res := httptest.NewRecorder()
90+
req := httptest.NewRequest(http.MethodGet, "http://localhost:8080/erised/ip", nil)
91+
svr.handleIP().ServeHTTP(res, req)
92+
5993
Ω(res.Header().Get("Content-Type")).Should(Equal("application/json"))
6094
})
6195
})
@@ -67,25 +101,59 @@ func TestErisedHeadersRoute(t *testing.T) {
67101
RegisterFailHandler(func(m string, _ ...int) { g.Fail(m) })
68102
exp := `{"Host":"localhost:8080"}`
69103
svr := server{}
70-
req := httptest.NewRequest(http.MethodGet, "http://localhost:8080/erised/headers", nil)
71-
res := httptest.NewRecorder()
72-
svr.handleHeaders().ServeHTTP(res, req)
73104

74105
g.Describe("Test erised/headers", func() {
75106
g.It("Should return StatusOK", func() {
107+
res := httptest.NewRecorder()
108+
req := httptest.NewRequest(http.MethodGet, "http://localhost:8080/erised/headers", nil)
109+
svr.handleHeaders().ServeHTTP(res, req)
110+
76111
Ω(res.Code).Should(Equal(http.StatusOK))
77112
})
78113

114+
g.It("Should return MethodNotAllowed", func() {
115+
res := httptest.NewRecorder()
116+
req := httptest.NewRequest(http.MethodPost, "http://localhost:8080/erised/headers", nil)
117+
svr.handleHeaders().ServeHTTP(res, req)
118+
119+
Ω(res.Code).Should(Equal(http.StatusMethodNotAllowed))
120+
})
121+
79122
g.It("Should match expected body", func() {
123+
res := httptest.NewRecorder()
124+
req := httptest.NewRequest(http.MethodGet, "http://localhost:8080/erised/headers", nil)
125+
svr.handleHeaders().ServeHTTP(res, req)
126+
80127
Ω(res.Body.String()).Should(Equal(exp))
81128
})
82129

83130
g.It("Should match Content-Type header", func() {
131+
res := httptest.NewRecorder()
132+
req := httptest.NewRequest(http.MethodGet, "http://localhost:8080/erised/headers", nil)
133+
svr.handleHeaders().ServeHTTP(res, req)
134+
84135
Ω(res.Header().Get("Content-Type")).Should(Equal("application/json"))
85136
})
86137
})
87138
}
88139

140+
func TestErisedShutdownRoute(t *testing.T) {
141+
zerolog.SetGlobalLevel(zerolog.Disabled)
142+
g := goblin.Goblin(t)
143+
RegisterFailHandler(func(m string, _ ...int) { g.Fail(m) })
144+
svr := server{}
145+
146+
g.Describe("Test erised/shutdown", func() {
147+
g.It("Should return MethodNotAllowed", func() {
148+
res := httptest.NewRecorder()
149+
req := httptest.NewRequest(http.MethodPost, "http://localhost:8080/erised/shutdown", nil)
150+
svr.handleHeaders().ServeHTTP(res, req)
151+
152+
Ω(res.Code).Should(Equal(http.StatusMethodNotAllowed))
153+
})
154+
})
155+
}
156+
89157
func TestErisedLandingRoute(t *testing.T) {
90158
zerolog.SetGlobalLevel(zerolog.Disabled)
91159
g := goblin.Goblin(t)

0 commit comments

Comments
 (0)