apijc
fetches and compares the status codes and json responses of
user-defined paths on two domains and reports any differences.
- Diff comparison of response bodies from both domains
- Sequential request chains. See sequentialTargets below
- Check for expected status codes
- Path expansion of
- Lists (example:
/foo/{1,2,3}/bar
) - Numerical ranges (example:
/foo/{1-100}/bar
) - Mixed list and ranges (example:
/foo/{1,3-5,99,200-400}
) - See Path expansion below
- Lists (example:
- Rate limiting
- Load headers from file
- Specify header key-value pairs globally or per domain
- Custom headers per url target
- Write errors/mismatches to stdout or file
- Download the binary for your architecture from the Releases page.
- Put it into a directory in your
$PATH
go install github.com/phux/apijc@latest
apijc \
--baseDomain "<http://first.domain>" \
--newDomain "<http://second.domain>" \
--urlFile <path/to/a/url.json> \
--headerFile <path/to/a/header.json> \ # optional
--rateLimit 100 \ # optional
--outputFile <path/to/an/output.json> # optional
- Install - see Installation
- Create a urlFile JSON - see urlFile - and setup up at least one target
Minimal urlFile
example:
{
"targets": [
{
"relativePath": "/some/relative/path",
"httpMethod": "GET",
"expectedStatusCode": 200
}
]
}
- execute
apijc
apijc \
--baseDomain "<http://your-base.domain>" \
--newDomain "<http://your-other.domain>" \
--urlFile path/to/your/urlFile.json
Note: if --rateLimit
is not passed to apijc
, the default rate limit is 1 request per second.
$ apijc --baseDomain http://localhost:8080 \
--newDomain http://localhost:8081 \
--urlFile .testdata/urlfile_example.json \
--rateLimit 1000
Starting with rate limit: 1000.000000/second
2023/12/06 22:09:35 Checking GET /v1/example
2023/12/06 22:09:35 Success: GET /v1/example (checked 1 of 1 paths)
2023/12/06 22:09:35 Checking GET /v1/{1-100}
2023/12/06 22:09:36 Success: GET /v1/{1-100} (checked 100 of 100 paths)
2023/12/06 22:09:36 Checking GET /v1/@1-3@
2023/12/06 22:09:36 Success: GET /v1/@1-3@ (checked 3 of 3 paths)
2023/12/06 22:09:36 Checking GET /v1/expected_jsonmissmatch
2023/12/06 22:09:36 ERROR: GET /v1/expected_jsonmissmatch (checked 1 of 1 paths)
2023/12/06 22:09:36 Checking POST /v1/example
2023/12/06 22:09:36 Success: POST /v1/example (checked 1 of 1 paths)
2023/12/06 22:09:36 Checking sequential group: First POST, then GET
2023/12/06 22:09:36 Success: POST /v1/sequential_post (checked 1 of 1 paths)
2023/12/06 22:09:36 Success: GET /v1/sequential_get (checked 1 of 1 paths)
2023/12/06 22:09:36 Done. Checked 108 of 108 paths
2023/12/06 22:09:36 Findings:
2023/12/06 22:09:36 /v1/expected_jsonmissmatch
Error: JSON mismatch
Diff: @ ["foo"]
- "baz"
+ "bar"
2023/12/06 22:09:36 Finished - 1 findings
exit status 1
Flag | Required | Description | Default |
---|---|---|---|
baseDomain | yes | The first domain to make all requests to | - |
newDomain | yes | The second domain to make all requests to | - |
urlFile | yes | Path to JSON file containing target URL paths, HTTP method, ... See urlFile |
- |
headerFile | no | Path to JSON file containing global and/or per-domain header key-value pairs that will be set on each request. See headerFile | - |
rateLimit | no | Requests per second (float). See rateLimit |
1 |
outputFile | no | Path to store findings in JSON format. See outputFile | - |
The urlFile
defines the relative paths that will be requested and compared on
both domains. It contains targets
and/or sequentialTargets
.
Standalone requests are defined in the targets
key of the urlFile
. Each
target will be requested on both, baseDomain
and newDomain
.
In the sequentialTargets
key chains of consecutive calls can be defined.
Example: first make a POST request to create an entity, then
make a GET request to fetch the created entity.
The steps for a sequential target group are:
- make request to target 1 on
baseDomain
- compare actual status code vs
expectedStatusCode
- make request to target 1 on
newDomain
- compare actual status code vs
expectedStatusCode
- compare response bodies
- make request to target 2 on
baseDomain
- compare actual status code vs
expectedStatusCode
- make request to target 2 on
newDomain
- compare actual status code vs
expectedStatusCode
- compare response bodies
{
"targets": [
{
"relativePath": "<required string, /a/relative/path/to/check/on/both/domains>",
"httpMethod": "<GET|POST|...>",
"expectedStatusCode": <required int; checked on both domains>,
"requestBody": "<optional string, body to send to relativePath>",
"requestBodyFile": "<optional string, path to a file containing a JSON request body>",
"requestHeaders": { // optional
"<string, header key>": "<string, header value>"
},
"patternPrefix": "<optional string, a character to start expansion; default {>",
"patternSuffix": "<optional string, a character to stop expansion; default }>"
}
],
"sequentialTargets": {
"Some name for the sequence, example: Create Order, then fetch Order": [
{
"relativePath": "/first/path",
"httpMethod": "<GET|POST|...>",
"expectedStatusCode": 201
},
{
"relativePath": "/second/path",
"httpMethod": "<GET|POST|...>",
"expectedStatusCode": 200
}
]
}
}
relativePath
can contain lists and/or ranges to quickly define multiple
targets at once.
Expansions are triggered for everything between a configurable patternPrefix
(default: {
) and a patternSuffix
(default: }
) on each target.
List items are separated by ,
(comma).
Numerical ranges can be defined by -
(dash).
Example:
"relativePath": "/foo/{bar,3-5}"
This will translate to requesting and checking 4 paths:
/foo/bar
<-- from list itembar
/foo/3
<-- from range3-5
/foo/4
<-- from range3-5
/foo/5
<-- from range3-5
Note: a path can also define multiple expansions, like /foo/{1-2}/bar/{a,b}
.
This path will result in 4 paths in total:
/foo/1/bar/a
/foo/1/bar/b
/foo/2/bar/a
/foo/2/bar/b
The urlFile
can contain two different exclusive keys to specify the request body to a target: requestBody
and requestBodyFile
.
requestBody
contains an escaped JSON string to be sent as the body.
Example:
"requestBody": "{\"a\":\"b\"}",
requestBodyFile
contains a path to a JSON file containing the body to be sent
for the target. This is helpful if the request body to be sent is bigger and avoids escaping hell.
Example:
"requestBodyFile": ".testdata/request_body.json"
{
"targets": [
{
"relativePath": "/v1/example",
"httpMethod": "GET",
"expectedStatusCode": 200
},
{
"relativePath": "/v1/{1-100}",
"httpMethod": "GET",
"expectedStatusCode": 200
},
{
"relativePath": "/v1/example",
"httpMethod": "POST",
"expectedStatusCode": 201,
"requestBody": "{\"a\":\"b\"}",
"requestHeaders": {
"Content-Type": "application/json"
},
"patternPrefix": "{",
"patternSuffix": "}"
},
{
"relativePath": "/v1/post_with_body_file",
"httpMethod": "POST",
"expectedStatusCode": 201,
"requestBodyFile": ".testdata/request_body.json"
}
],
"sequentialTargets": {
"First POST, then GET": [
{
"relativePath": "/v1/sequential_post",
"httpMethod": "POST",
"expectedStatusCode": 201,
"requestBody": "{\"a\":\"b\"}"
},
{
"relativePath": "/v1/sequential_get",
"httpMethod": "GET",
"expectedStatusCode": 200
}
]
}
}
Sometimes it's necessary to limit the rate with which the tool makes requests to the configured domains.
The flag --rateLimit
allows to configure the rate.
Must be int
or float.
The number defines the allowed requests per seconds.
Examples:
--rateLimit=1
: 1 request per second (default)--rateLimit=0.5
: 1 request per 2 seconds--rateLimit=10
: 10 requests per second
The headerFile
allows to define key-value pairs in the global
key that will be set on each
request, in addition to the static requestHeaders
defined on each target in
the urlFile
.
Additionally, it is possible to set per-domain header key-value pairs that will
be set on each request to the particular domain (baseDomain|newDomain
).
This is helpful for example if you need to set different Authorization
headers per domain.
Note: The global
, baseDomain
and newDomain
keys are all optional.
# header.json
{
"global": {
"SomeHeaderName": "Value applied to all requests to both domains"
},
"baseDomain": {
"SomeHeaderName": "Value applied to all requests to BaseDomain"
},
"newDomain": {
"SomeHeaderName": "Value applied to all requests to NewDomain"
}
}
If the headerFile
and a target's requestHeaders
contain duplicate header keys,
the target's requestHeaders
value takes precedence.
headerFile.global < headerFile.<new|base>Domain < target.requestHeaders
If the flag --outputFile
is not passed, the findings are written to
stdout, see Example output
Via --outputFile
a path to a file can be passed. The findings will be written
to this file instead of stdout.
# findings.json
[
{
"url": "/v1/expected_jsonmissmatch",
"error": "JSON mismatch",
"diff": "@ [\"foo\"]\n- \"baz\"\n+ \"bar\"\n"
}
]
On successful execution apijc
exits with code 0
.
On any issue the exit code will be > 0
- read
requestBody
JSON from files - allow to skip the diff comparison for JSON response body fields on a target (e.g.
id
)