A nimble http and echo server to test arbitrary http requests and REST API responses.
erised [options]
Parameters:
-cert string
path to a valid X.509 certificate file
-https
use HTTPS instead of HTTP. A valid X.509 certificate and private key are required
-idle int
maximum time in seconds to wait for the next request when keep-alive is enabled (default 120)
-json
use JSON log format
-key string
path to a valid private key file
-level string
one of debug/info/warn/error/off (default "info")
-path string
path to search recursively for X-Erised-Response-File
-port int
port to listen. Default is 8080 for HTTP and 8443 for HTTPS
-profile string
profile this session. A valid file name is required
-read int
maximum duration in seconds for reading the entire request (default 5)
-write int
maximum duration in seconds before timing out response writes (default 10)
For help type erised -h
When executing erised with no parameters, the server will listen on port 8080 for incoming http requests.
If you're using the -path option, please EXERCISE GREAT CAUTION when setting the path to search. See Known Issues for more information. For security reasons, path is restricted to the directory or subdirectories where the program was invoked.
The latest version is also available as a Docker image at edaddario/erised.
To start the server in a docker container, with defaults values, execute the following command:
docker run --rm -p 8080:8080 --name erised edaddario/erised
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.
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:
docker run --rm -p 8080:8080 --name erised -v /local_directory/response_files:/files edaddario/erised -path ./files
URL routes, HTTP methods (e.g. GET, POST, PATCH, etc.), query strings and body are ignored, except for:
Name | Method | Purpose |
---|---|---|
erised/headers | GET | Returns request headers |
erised/info | GET | Returns miscellaneous information |
erised/ip | GET | Returns the client IP |
erised/shutdown | POST | Shutdowns the server |
The erised/echoserver path will ignore any additional segments after /echoserver, including HTTP methods, query strings and body, and it will return a webpage displaying server information and the request's parameters.
Name | Method | Purpose |
---|---|---|
erised/echoserver/* | any | Returns a webpage displaying server information and the request's parameters |
Erised's response behaviour is controlled via custom headers in the http request:
Name | Purpose |
---|---|
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. |
X-Erised-Data | Returns the same value in the response body |
X-Erised-Headers | Returns the value(s) in the response header. Values must be in a JSON key/value list |
X-Erised-Location | Sets the response Location to the new (redirected) URL or path, when 300 ≤ X-Erised-Status-Code < 310 |
X-Erised-Response-Delay | Number of milliseconds to wait before sending response back to client |
X-Erised-Response-File | Returns the contents of file in the response body. If present, X-Erised-Data is ignored |
X-Erised-Status-Code | Sets the HTTP Status Code |
No validation is performed on X-Erised-Data or X-Erised-Location.
Valid X-Erised-Status-Code values are:
OK or 200 (default)
MultipleChoices or 300
MovedPermanently or 301
Found or 302
SeeOther or 303
UseProxy or 305
TemporaryRedirect or 307
PermanentRedirect or 308
BadRequest or 400
Unauthorized or 401
PaymentRequired or 402
Forbidden or 403
NotFound or 404
MethodNotAllowed or 405
RequestTimeout or 408
Conflict or 409
Gone or 410
Teapot or 418
TooManyRequests or 429
InternalServerError or 500
NotImplemented or 501
BadGateway or 502
ServiceUnavailable or 503
GatewayTimeout or 504
HTTPVersionNotSupported or 505
InsufficientStorage or 507
LoopDetected or 508
NotExtended or 510
NetworkAuthenticationRequired or 511
Any other value will resolve to 200 (OK)
- v0.11.2 - Add HTTPS capability, add test certificates, add program execution timing, add profiling option, refactor variable names for readability, and replace panics with more user-friendly fatal logs
- v0.9.7 - Refactor error handling
- v0.9.6 - Rename erised/webpage to erised/echoserver and add headers and server environment information
- v0.8.3 - Add erised/webpage path, add multi-architecture docker images, minor refactoring, and minor cosmetic changes
- v0.7.0 - Improve response file processing and security, change logging type, and minor source code readability changes
- v0.6.11 - Further server shutdown improvements, minor efficiency improvements, general code refactoring and bug fixes
- v0.6.7 - Improve server shutdown handling, and restrict allowed methods for erised/headers, erised/ip, erised/info and erised/shutdown routes
- v0.5.4 - Update dependencies
- v0.5.3 - Add file based responses
- v0.4.1 - Add route concurrency, update tests and dependencies
- v0.3.4 - Add gomega assertion library, refactor tests to use Ω assertions and minor bug fixes
- v0.3.0 - Add goblin framework and unit tests
- v0.2.5 - Switch to zerolog logging framework, add erised/shutdown path
- v0.2.2 - Add custom headers, add dockerfile
- v0.2.1 - Add gzip compression, improve erised/headers json handling
- v0.0.3 - Add erised/headers, erised/ip and erised/info routes. Add delayed responses
- v0.0.2 - Add HTTP redirection status codes (300's), startup configuration parameters and request's logging
- v0.0.1 - Initial release
In order to enable HTTPS support, erised requires a valid X.509 certificate signed by a trusted Certification Authority (CA) like IdenTrust, DigiCert or Let's Encrypt.
If you don't have one, or would prefer to use a local version, you'll find the necessary certificate (localCA.pem) to setup a Root CA in the /certs folder, which will allow your computer to verify the included test certificate and key. The process to install the CA certificate is beyond the objective of this README, but Google or your favourite AI can help. Once the certificate is installed, it will show as Erised Test CA. Please ensure it is marked as trusted.
You should now be able to run erised in HTTPS mode by executing erised -https -cert erised.crt -key erised.key
where erised.crt is the "site's" (your computer) X.509 certificate and erised.key is the private key.
As mentioned before, covering the intricacies of establishing cryptographically secure digital identities and documenting the process to generate the relevant keys and certificates is well beyond the scope of this README, but it is important to at least call out some of the risks incurred when trusting a digital certificate because, in addition to validate identity and secure the communication between parties, they are also used to "sign" code (programs and libraries) that can run with privileged permissions.
I hope the message is clear: when dealing with anything security related it always pays to be very careful and when in doubt, the best option is just not to.
The Secure Sockets Layer (SSL) certificates included with erised are linked to localhost and can only be used to enable TLS/SSL communication within your own computer.
For transparency, the (high level) steps to create them are:
- Create a private key (localCA.key) to sign the Root CA certificate
- Generate a Root CA certificate (localCA.pem) signed with the above key
- Create a "site" private key (erised.key)
- Generate an intermediate Certificate Signing Request certificate (erised.csr) signed with site's private key
- Create an X.509 V3 certificate extension config file (erised.ext) to link the final certificate to localhost
- Generate the site's final certificate (erised.crt) using the Root CA certificate, the CA private key, the intermediate CSR certificate, and the certificate extension config file
Please note that neither of the private keys are password protected. This is definitely not something that you would normally do, but decided to simplify the process in case you'd want to tinker with the provided certs.
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.
Of all of its deficiencies, the most notable is:
- Using the -path option could lead to significant security risks. When the X-Erised-Response-File header is set, it will search recursively for a matching filename in the current directory or all subdirectories underneath it, returning the contents of the first match. For security reasons, path is restricted to the directory or subdirectories where the program was invoked.
I may or may not address this or any other issues in a future release. Caveat Emptor
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.
erised was partially inspired by Kenneth Reitz's HTTP Request & Response Service httpbin.io and later on by Marchandise Rudy's Echo-Server.
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.
Imagine you're developing some client for api.chucknorris.io and want to test the /jokes/random path. You could certainly make live calls against the server:
curl -w '\n' -v -k https://api.chucknorris.io/jokes/random
(response edited for clarity)
* Trying 104.31.94.71...
* TCP_NODELAY set
* Connected to api.chucknorris.io (104.31.94.71) port 443 (#0)
> GET /jokes/random HTTP/2
> Host: api.chucknorris.io
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/2 200
< date: Wed, 30 Dec 2020 00:21:14 GMT
< content-type: application/json;charset=UTF-8
<
* Connection #0 to host api.chucknorris.io left intact
{"categories":[],"created_at":"2020-01-05 13:42:18.823766","icon_url":"https://assets.chucknorris.host/img/avatar/chuck-norris.png","id":"CfW0ccNFTpeq_v1r13IjTQ","updated_at":"2020-01-05 13:42:18.823766","url":"https://api.chucknorris.io/jokes/CfW0ccNFTpeq_v1r13IjTQ","value":"The lord giveth and Chuck Norris taketh away"}
* Closing connection 0
Or, even better yet, you could use erised like this:
curl -w '\n' -v \
-H "X-Erised-Status-Code:OK" \
-H "X-Erised-Content-Type:json" \
-H "X-Erised-Data:{\"categories\":[],\"created_at\":\"2020-01-05 13:42:26.766831\",\"icon_url\":\"https://assets.chucknorris.host/img/avatar/chuck-norris.png\",\"id\":\"CfW0ccNFTpeq_v1r13IjTQ\",\"updated_at\":\"2020-01-05 13:42:26.766831\",\"url\":\"https://api.chucknorris.io/jokes/CfW0ccNFTpeq_v1r13IjTQ\",\"value\":\"The lord giveth and Chuck Norris taketh away\"}" \
http://localhost:8080/jokes/random
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /jokes/random HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> X-Erised-Status-Code:OK
> X-Erised-Content-Type:json
> X-Erised-Data:{"categories":[],"created_at":"2020-01-05 13:42:26.766831","icon_url":"https://assets.chucknorris.host/img/avatar/chuck-norris.png","id":"CfW0ccNFTpeq_v1r13IjTQ","updated_at":"2020-01-05 13:42:26.766831","url":"https://api.chucknorris.io/jokes/CfW0ccNFTpeq_v1r13IjTQ","value":"The lord giveth and Chuck Norris taketh away"}
>
< HTTP/1.1 200 OK
< Content-Encoding: identity
< Content-Type: application/json
< Date: Wed, 30 Dec 2020 01:13:54 GMT
< Content-Length: 323
<
* Connection #0 to host localhost left intact
{"categories":[],"created_at":"2020-01-05 13:42:26.766831","icon_url":"https://assets.chucknorris.host/img/avatar/chuck-norris.png","id":"CfW0ccNFTpeq_v1r13IjTQ","updated_at":"2020-01-05 13:42:26.766831","url":"https://api.chucknorris.io/jokes/CfW0ccNFTpeq_v1r13IjTQ","value":"The lord giveth and Chuck Norris taketh away"}
* Closing connection 0
and even simulate common failures like,
curl -w '\n' -v \
-H "X-Erised-Status-Code:NotFound" \
-H "X-Erised-Content-Type:json" \
-H "X-Erised-Data:{\"timestamp\":\"2020-12-30T11:21:32.793Z\",\"status\":404,\"error\":\"Not Found\",\"message\":\"Chuck Norris knows everything there is to know - Except where this page is.\",\"path\":\"/jokes/random\"}" \
http://localhost:8080/jokes/random
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /jokes/random HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> X-Erised-Status-Code:NotFound
> X-Erised-Content-Type:json
> X-Erised-Data:{"timestamp":"2020-12-30T11:21:32.793Z","status":404,"error":"Not Found","message":"Chuck Norris knows everything there is to know - Except where this page is.","path":"/jokes/random"}
>
< HTTP/1.1 404 Not Found
< Content-Encoding: identity
< Content-Type: application/json
< Date: Wed, 30 Dec 2020 11:25:21 GMT
< Content-Length: 184
<
* Connection #0 to host localhost left intact
{"timestamp":"2020-12-30T11:21:32.793Z","status":404,"error":"Not Found","message":"Chuck Norris knows everything there is to know - Except where this page is.","path":"/jokes/random"}
* Closing connection 0
curl -w '\n' -v http://localhost:8080
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Encoding: identity
< Content-Type: text/plain
< Date: Tue, 29 Dec 2020 18:35:48 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact
* Closing connection 0
curl -w '\n' -I -H "X-Erised-Headers:{\"My-Header\":\"Hello World\",\"Another-Header\":\"Goodbye World\"}" http://localhost:8080
HTTP/1.1 200 OK
Another-Header: Goodbye World
Content-Encoding: identity
Content-Type: text/plain
My-Header: Hello World
Date: Sat, 13 Mar 2021 22:56:09 GMT
curl -w '\n' -v -H "X-Erised-Data:Hello World" http://localhost:8080
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> X-Erised-Data:Hello World
>
< HTTP/1.1 200 OK
< Content-Encoding: identity
< Content-Type: text/plain
< Date: Tue, 29 Dec 2020 18:38:10 GMT
< Content-Length: 11
<
* Connection #0 to host localhost left intact
Hello World
* Closing connection 0
curl -w '\n' -v -H "X-Erised-Content-Type:json" -H "X-Erised-Data:[{\"Hello\":\"World\"}]" http://localhost:8080
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> X-Erised-Content-Type:json
> X-Erised-Data:[{Hello:World}]
>
< HTTP/1.1 200 OK
< Content-Encoding: identity
< Content-Type: application/json
< Date: Tue, 29 Dec 2020 18:43:55 GMT
< Content-Length: 15
<
* Connection #0 to host localhost left intact
[{"Hello":"World"}]
* Closing connection 0
Request returning text in the response body and 418 I'm a teapot in the header's Status Code
curl -w '\n' -v -H "X-Erised-Status-Code:Teapot" -H "X-Erised-Data:Server refuses to brew coffee because it is, permanently, a teapot." http://localhost:8080
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> X-Erised-Status-Code:Teapot
> X-Erised-Data:Server refuses to brew coffee because it is, permanently, a teapot.
>
< HTTP/1.1 418 I'm a teapot
< Content-Encoding: identity
< Content-Type: text/plain
< Date: Tue, 29 Dec 2020 18:54:46 GMT
< Content-Length: 67
<
* Connection #0 to host localhost left intact
Server refuses to brew coffee because it is, permanently, a teapot.
* Closing connection 0