Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ contract MyScript is Script {
}
```

#### Redirects

Redirects are disabled by default to avoid leaking sensitive headers to untrusted hosts. To follow redirects, opt in
explicitly and (optionally) set a max redirect count.

```solidity
HTTP.Response memory response = http.initialize().GET("https://example.com")
.withFollowRedirects(true)
.withMaxRedirects(3)
.request();
```

#### 3. Enable FFI

This library relies on Foundry's [FFI cheatcode](https://book.getfoundry.sh/cheatcodes/ffi.html) to call external processes. Enable it by:
Expand Down
4 changes: 4 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ out = "out"
libs = ["lib"]
ffi = true

[lint]
ignore = ["lib", "test", "script"]
lint_on_build = false

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
38 changes: 37 additions & 1 deletion src/HTTP.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ library HTTP {
using StringMap for StringMap.StringToStringMap;

Vm constant vm = Vm(address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))));
uint256 constant DEFAULT_MAX_REDIRECTS = 3;

error HTTPArrayLengthsMismatch(uint256 a, uint256 b);

Expand All @@ -25,6 +26,8 @@ library HTTP {
Method method;
StringMap.StringToStringMap headers;
StringMap.StringToStringMap query;
bool followRedirects;
uint256 maxRedirects;
}

struct Response {
Expand All @@ -38,7 +41,8 @@ library HTTP {

function initialize(HTTP.Client storage client) internal returns (HTTP.Request storage) {
client.requests.push();
return client.requests[client.requests.length - 1];
HTTP.Request storage req = client.requests[client.requests.length - 1];
return withMaxRedirects(req, DEFAULT_MAX_REDIRECTS);
}

function initialize(HTTP.Client storage client, string memory url) internal returns (HTTP.Request storage) {
Expand Down Expand Up @@ -146,6 +150,16 @@ library HTTP {
return req;
}

function withFollowRedirects(HTTP.Request storage req, bool enabled) internal returns (HTTP.Request storage) {
req.followRedirects = enabled;
return req;
}

function withMaxRedirects(HTTP.Request storage req, uint256 maxRedirects) internal returns (HTTP.Request storage) {
req.maxRedirects = maxRedirects == 0 ? DEFAULT_MAX_REDIRECTS : maxRedirects;
return req;
}

function request(Request storage req) internal returns (Response memory res) {
string memory scriptStart = 'response=$(curl -s -w "\\n%{http_code}" ';
string memory scriptEnd =
Expand All @@ -164,6 +178,14 @@ library HTTP {
curlParams = string.concat(curlParams, "-d '", req.body, "' ");
}

if (req.followRedirects) {
string memory maxRedirects = vm.toString(req.maxRedirects);
curlParams = string.concat(curlParams, "-L --max-redirs ", maxRedirects, " ");
if (_hasHttpsPrefix(req.url)) {
curlParams = string.concat(curlParams, "--proto =https ");
}
}

string memory quotedURL = string.concat('"', req.url, '"');

string[] memory inputs = new string[](3);
Expand Down Expand Up @@ -191,4 +213,18 @@ library HTTP {
revert();
}
}

function _hasHttpsPrefix(string memory value) private pure returns (bool) {
bytes memory valueBytes = bytes(value);
bytes memory prefixBytes = bytes("https://");
if (valueBytes.length < prefixBytes.length) {
return false;
}
for (uint256 i = 0; i < prefixBytes.length; i++) {
if (valueBytes[i] != prefixBytes[i]) {
return false;
}
}
return true;
}
}
22 changes: 18 additions & 4 deletions test/HTTP.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ contract HTTPTest is Test {
}

function test_HTTP_GET_options() public {
HTTP.Response memory res = http.initialize("https://httpbin.org/headers").GET().withHeader(
"accept", "application/json"
).withHeader("Authorization", "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==").request();
HTTP.Response memory res = http.initialize("https://httpbin.org/headers").GET()
.withHeader("accept", "application/json").withHeader("Authorization", "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==")
.request();

assertEq(res.status, 200);

Expand Down Expand Up @@ -55,7 +55,7 @@ contract HTTPTest is Test {
}

function test_HTTP_PUT_json() public {
HTTP.Response memory res = http.initialize("https://httpbin.org/put").PUT().withBody('{"foo": "bar"}')
HTTP.Response memory res = http.initialize("https://postman-echo.com/put").PUT().withBody('{"foo": "bar"}')
.withHeader("Content-Type", "application/json").request();

assertEq(res.status, 200);
Expand All @@ -77,4 +77,18 @@ contract HTTPTest is Test {
HTTP.Request storage req = http.initialize("https://jsonplaceholder.typicode.com/todos/1");
assertEq(req.url, "https://jsonplaceholder.typicode.com/todos/1");
}

function test_HTTP_redirects_disabled_by_default() public {
HTTP.Response memory res = http.initialize().GET("https://httpbin.org/relative-redirect/1").request();

assertEq(res.status, 302);
}

function test_HTTP_redirects_enabled() public {
HTTP.Response memory res = http.initialize().GET("https://httpbin.org/relative-redirect/1")
.withFollowRedirects(true).withMaxRedirects(3).request();

assertEq(res.status, 200);
assertTrue(res.data.toSlice().contains(("https://httpbin.org/get").toSlice()));
}
}