Output curl command for each request at runtime

This commit is contained in:
Fabrice Reix 2021-05-14 19:31:18 +02:00
parent 8e1fdafca6
commit f9f3f55baf
79 changed files with 943 additions and 85 deletions

View File

@ -74,6 +74,7 @@ jobs:
export PATH="$PWD/target/debug:$PATH"
cd integration
./integration.py
./test_curl_commands.sh tests/*.curl
- name: Archive production artifacts
uses: actions/upload-artifact@v2
if: ${{ always() }}

1
Cargo.lock generated
View File

@ -342,6 +342,7 @@ dependencies = [
"hurl_core",
"libflate",
"libxml",
"percent-encoding",
"regex",
"serde",
"serde_json",

View File

@ -0,0 +1,14 @@
#!/bin/bash
set -eu
for f in $*; do
echo "** $f"
cat "$f" | grep -v '^$' | grep -v '^#' | while read -r line; do
echo "$line"
cmd="$line --no-progress-meter --output /dev/null --fail"
echo "$cmd" | bash || (echo ">>> Error <<<<" && exit 1)
done
echo
done
echo "all curl commands have been run with success!"

View File

@ -33,9 +33,13 @@ def get_os():
def test(hurl_file):
options_file = hurl_file.replace('.hurl','.options')
curl_file = hurl_file.replace('.hurl','.curl')
options = []
if os.path.exists(options_file):
options = open(options_file).read().strip().split(' ')
if os.path.exists(curl_file):
options.append('--verbose')
cmd = ['hurl', hurl_file] + options
print(' '.join(cmd))
@ -78,6 +82,31 @@ def test(hurl_file):
print(f'actual: <{actual}>\nexpected: <{expected}>')
sys.exit(1)
# curl output
if os.path.exists(curl_file):
expected_commands = []
for line in open(curl_file, 'r').readlines():
line = line.strip()
if line == "" or line.startswith("#"):
continue
expected_commands.append(line)
actual = decode_string(result.stderr).strip()
actual_commands= [line[2:] for line in actual.split('\n') if line.startswith('* curl')]
if len(actual_commands) != len(expected_commands):
print('Assert error at %s' % (f))
print('expected: %d commands' % len(expected_commands))
print('actual: %d commands' % len(actual_commands))
sys.exit(1)
for i in range(len(expected_commands)):
if actual_commands[i] != expected_commands[i]:
print('Assert error at %s:%i' % (curl_file, i+1))
print('expected: %s' % expected_commands[i])
print('actual: %s' % actual_commands[i])
sys.exit(1)
def main():
for hurl_file in sys.argv[1:]:
test(hurl_file)

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/assert-base64'

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/assert-header'

View File

@ -0,0 +1,4 @@
curl 'http://localhost:8000/assert-json'
curl 'http://localhost:8000/assert-json/index'
curl 'http://localhost:8000/assert-json'
curl 'http://localhost:8000/assert-json/list'

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/assert-match'

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/assert-regex'

View File

@ -0,0 +1,3 @@
curl 'http://localhost:8000/assert-status-code'
curl 'http://localhost:8000/assert-status-code'
curl 'http://localhost:8000/assert-status-code'

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/assert-xpath'

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/basic-authentication' --user 'bob:secret'

View File

@ -0,0 +1,2 @@
curl 'http://localhost:8000/utf8_bom'

View File

@ -0,0 +1,2 @@
curl 'http://localhost:8000/bytes'

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/capture-and-assert'

View File

@ -0,0 +1,3 @@
curl 'http://localhost:8000/captures'
curl 'http://localhost:8000/captures-check?param1=value1&param2=Bob'
curl 'http://localhost:8000/captures-json'

View File

View File

@ -1 +1,5 @@
* fail fast: true
* insecure: false
* follow redirect: false
* max redirect: 50
warning: no entry have been executed for file tests/color.hurl

View File

@ -0,0 +1,7 @@
curl 'http://localhost:8000/compressed/none' --compressed
curl 'http://localhost:8000/compressed/gzip' --compressed
curl 'http://localhost:8000/compressed/zlib' --compressed
# curl needs to be built with brotli support
curl 'http://localhost:8000/compressed/brotli' --compressed
curl 'http://localhost:8000/compressed/brotli_identity' --compressed

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/cookie_file' --cookie tests/cookie_file.cookies

View File

@ -1 +1 @@
<div class="hurl-file"><div class="hurl-entry"><div class="request"><span class="line"><span class="comment"># curl --cookie tests/cookie_file.cookies http://localhost:8000/cookie_file</span></span><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/cookie_file</span></span></div><div class="response"><span class="line"><span class="version">HTTP/*</span> <span class="status">200</span></span></div></div></div>
<div class="hurl-file"><div class="hurl-entry"><div class="request"><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/cookie_file</span></span></div><div class="response"><span class="line"><span class="version">HTTP/*</span> <span class="status">200</span></span></div></div></div>

View File

@ -1,3 +1,2 @@
# curl --cookie tests/cookie_file.cookies http://localhost:8000/cookie_file
GET http://localhost:8000/cookie_file
HTTP/* 200

View File

@ -0,0 +1,4 @@
curl 'http://localhost:8000/cookie-storage/assert-that-cookie1-is-valueA' --cookie 'cookie1=valueA'
curl 'http://localhost:8000/cookie-storage/assert-that-cookie1-is-not-in-session'

View File

@ -0,0 +1,9 @@
curl 'http://localhost:8000/cookies/set-request-cookie1-valueA' --cookie 'cookie1=valueA'
curl 'http://localhost:8000/cookies/assert-that-cookie1-is-not-in-session'
curl 'http://localhost:8000/cookies/set-multiple-request-cookies' --cookie 'user1=Bob; user2=Bill'
curl 'http://localhost:8000/cookies/set-session-cookie2-valueA'
curl 'http://localhost:8000/cookies/assert-that-cookie2-is-valueA' --cookie 'cookie2=valueA'
curl 'http://localhost:8000/cookies/assert-that-cookie2-is-valueA-and-valueB' --cookie 'cookie2=valueB; cookie2=valueA'
curl 'http://localhost:8000/cookies/delete-cookie2' --cookie 'cookie2=valueA'
curl 'http://localhost:8000/cookies/assert-that-cookie2-is-not-in-session'
curl 'http://localhost:8000/cookies/set'

File diff suppressed because one or more lines are too long

View File

@ -28,7 +28,7 @@ HTTP/1.0 200
GET http://localhost:8000/cookies/assert-that-cookie2-is-valueA-and-valueB
[Cookies]
cookie2: ValueB
cookie2: valueB
HTTP/1.0 200

View File

@ -1 +1 @@
{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/cookies/set-request-cookie1-valueA","cookies":[{"name":"cookie1","value":"valueA"}]},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/cookies/assert-that-cookie1-is-not-in-session"},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/cookies/set-multiple-request-cookies","cookies":[{"name":"user1","value":"Bob"},{"name":"user2","value":"Bill"}]},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/cookies/set-session-cookie2-valueA"},"response":{"version":"HTTP/1.0","status":200,"asserts":[{"query":{"type":"cookie","expr":"cookie2"},"predicate":{"type":"equal","value":"valueA"}}]}},{"request":{"method":"GET","url":"http://localhost:8000/cookies/assert-that-cookie2-is-valueA"},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/cookies/assert-that-cookie2-is-valueA-and-valueB","cookies":[{"name":"cookie2","value":"ValueB"}]},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/cookies/delete-cookie2"},"response":{"version":"HTTP/1.0","status":200,"asserts":[{"query":{"type":"cookie","expr":"cookie2"},"predicate":{"type":"equal","value":""}},{"query":{"type":"cookie","expr":"cookie2[Max-Age]"},"predicate":{"type":"equal","value":0}}]}},{"request":{"method":"GET","url":"http://localhost:8000/cookies/assert-that-cookie2-is-not-in-session"},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/cookies/set"},"response":{"version":"HTTP/1.0","status":200,"headers":[{"name":"Set-Cookie","value":"LSID=DQAAAKEaem_vYg; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly; Path=/accounts"},{"name":"Set-Cookie","value":"HSID=AYQEVnDKrdst; Domain=.localhost; Expires=Wed, 13 Jan 2021 22:23:01 GMT; HttpOnly; Path=/"},{"name":"Set-Cookie","value":"SSID=Ap4PGTEq; Domain=.localhost; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly; Path=/"}],"asserts":[{"query":{"type":"header","name":"Set-Cookie"},"predicate":{"type":"count","value":3}},{"query":{"type":"cookie","expr":"LSID"},"predicate":{"type":"equal","value":"DQAAAKEaem_vYg"}},{"query":{"type":"cookie","expr":"LSID[Value]"},"predicate":{"type":"equal","value":"DQAAAKEaem_vYg"}},{"query":{"type":"cookie","expr":"LSID[Expires]"},"predicate":{"type":"exist"}},{"query":{"type":"cookie","expr":"LSID[Expires]"},"predicate":{"type":"equal","value":"Wed, 13 Jan 2021 22:23:01 GMT"}},{"query":{"type":"cookie","expr":"LSID[Max-Age]"},"predicate":{"not":true,"type":"exist"}},{"query":{"type":"cookie","expr":"LSID[Domain]"},"predicate":{"not":true,"type":"exist"}},{"query":{"type":"cookie","expr":"LSID[Path]"},"predicate":{"type":"equal","value":"/accounts"}},{"query":{"type":"cookie","expr":"LSID[Secure]"},"predicate":{"type":"exist"}},{"query":{"type":"cookie","expr":"LSID[HttpOnly]"},"predicate":{"type":"exist"}},{"query":{"type":"cookie","expr":"LSID[SameSite]"},"predicate":{"not":true,"type":"exist"}}]}}]}
{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/cookies/set-request-cookie1-valueA","cookies":[{"name":"cookie1","value":"valueA"}]},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/cookies/assert-that-cookie1-is-not-in-session"},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/cookies/set-multiple-request-cookies","cookies":[{"name":"user1","value":"Bob"},{"name":"user2","value":"Bill"}]},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/cookies/set-session-cookie2-valueA"},"response":{"version":"HTTP/1.0","status":200,"asserts":[{"query":{"type":"cookie","expr":"cookie2"},"predicate":{"type":"equal","value":"valueA"}}]}},{"request":{"method":"GET","url":"http://localhost:8000/cookies/assert-that-cookie2-is-valueA"},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/cookies/assert-that-cookie2-is-valueA-and-valueB","cookies":[{"name":"cookie2","value":"valueB"}]},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/cookies/delete-cookie2"},"response":{"version":"HTTP/1.0","status":200,"asserts":[{"query":{"type":"cookie","expr":"cookie2"},"predicate":{"type":"equal","value":""}},{"query":{"type":"cookie","expr":"cookie2[Max-Age]"},"predicate":{"type":"equal","value":0}}]}},{"request":{"method":"GET","url":"http://localhost:8000/cookies/assert-that-cookie2-is-not-in-session"},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/cookies/set"},"response":{"version":"HTTP/1.0","status":200,"headers":[{"name":"Set-Cookie","value":"LSID=DQAAAKEaem_vYg; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly; Path=/accounts"},{"name":"Set-Cookie","value":"HSID=AYQEVnDKrdst; Domain=.localhost; Expires=Wed, 13 Jan 2021 22:23:01 GMT; HttpOnly; Path=/"},{"name":"Set-Cookie","value":"SSID=Ap4PGTEq; Domain=.localhost; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly; Path=/"}],"asserts":[{"query":{"type":"header","name":"Set-Cookie"},"predicate":{"type":"count","value":3}},{"query":{"type":"cookie","expr":"LSID"},"predicate":{"type":"equal","value":"DQAAAKEaem_vYg"}},{"query":{"type":"cookie","expr":"LSID[Value]"},"predicate":{"type":"equal","value":"DQAAAKEaem_vYg"}},{"query":{"type":"cookie","expr":"LSID[Expires]"},"predicate":{"type":"exist"}},{"query":{"type":"cookie","expr":"LSID[Expires]"},"predicate":{"type":"equal","value":"Wed, 13 Jan 2021 22:23:01 GMT"}},{"query":{"type":"cookie","expr":"LSID[Max-Age]"},"predicate":{"not":true,"type":"exist"}},{"query":{"type":"cookie","expr":"LSID[Domain]"},"predicate":{"not":true,"type":"exist"}},{"query":{"type":"cookie","expr":"LSID[Path]"},"predicate":{"type":"equal","value":"/accounts"}},{"query":{"type":"cookie","expr":"LSID[Secure]"},"predicate":{"type":"exist"}},{"query":{"type":"cookie","expr":"LSID[HttpOnly]"},"predicate":{"type":"exist"}},{"query":{"type":"cookie","expr":"LSID[SameSite]"},"predicate":{"not":true,"type":"exist"}}]}}]}

View File

@ -74,7 +74,8 @@ def assert_that_cookie2_is_valueb():
@app.route("/cookies/assert-that-cookie2-is-valueA-and-valueB")
def assert_that_cookie2_is_valuea_and_valueb():
assert request.headers['Cookie'] == 'cookie2=valueA; cookie2=ValueB'
assert 'cookie2=valueA' in request.headers['Cookie']
assert 'cookie2=valueB' in request.headers['Cookie']
return ''
@app.route("/cookies/set-session-cookie2-valueA-subdomain")

View File

@ -0,0 +1,2 @@
curl 'http://localhost:8000/delete' -X DELETE

View File

View File

@ -0,0 +1,2 @@
curl 'http://localhost:8000/encoding/utf8'
curl 'http://localhost:8000/encoding/latin1'

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/expect' -H 'Expect: 100-continue' -H 'Content-Type:' --data 'data'

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/follow-redirect' -L

View File

@ -0,0 +1,2 @@
curl 'http://localhost:8000/form-params' --data 'param1=value1' --data 'param2=' --data 'param3=a%3Db' --data 'param4=a%253db'
curl 'http://localhost:8000/form-params' -H 'Content-Type: application/x-www-form-urlencoded' --data 'param1=value1&param2=&param3=a%3db&param4=a%253db'

View File

@ -0,0 +1,7 @@
curl 'http://localhost:8000/default-headers'
curl 'http://localhost:8000/default-headers' -H 'User-Agent: hurl/1.0' -H 'Host: localhost:8000'
curl 'http://localhost:8000/default-headers' -H 'User-Agent: hurl/1.0' -H 'Host: localhost:8000'
curl 'http://localhost:8000/custom-headers' -H 'Fruit: Raspberry' -H 'Fruit: Apple' -H 'Fruit: Banana' -H 'Fruit: Grape' -H 'Color: Green'
curl 'http://localhost:8000/custom-headers-utf8' -H 'Beverage: café'
curl 'http://localhost:8000/custom-headers-quote' -H $'Header1: \''
curl 'http://localhost:8000/response-headers'

View File

@ -1 +1 @@
<div class="hurl-file"><div class="hurl-entry"><div class="request"><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/default-headers</span></span></div><div class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span></div></div><div class="hurl-entry"><div class="request"><span class="line"></span><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/default-headers</span></span></div><span class="line"><span class="string">User-Agent</span><span>:</span> <span class="string">hurl/1.0</span></span><span class="line"><span class="string">Host</span><span>:</span> <span class="string">localhost:8000</span> <span class="comment"># comment</span></span><div class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span></div></div><div class="hurl-entry"><div class="request"><span class="line"></span><span class="line"></span><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/default-headers</span></span></div><span class="line"><span class="string">User-Agent</span><span>:</span> <span class="string">hurl/1.0</span></span><span class="line"><span class="string">Host</span><span>:</span> <span class="string">localhost:8000</span> <span class="comment"># comment</span></span><div class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span></div></div><div class="hurl-entry"><div class="request"><span class="line"></span><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/custom-headers</span></span></div><span class="line"><span class="string">Fruit</span><span>:</span> <span class="string">Raspberry</span></span><span class="line"><span class="string">Fruit</span><span>:</span> <span class="string">Apple</span></span><span class="line"><span class="string">Fruit</span><span>:</span> <span class="string">Banana</span></span><span class="line"><span class="string">Fruit</span><span>:</span> <span class="string">Grape</span></span><span class="line"><span class="string">Color</span><span>:</span> <span class="string">Green</span></span><div class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span></div></div><div class="hurl-entry"><div class="request"><span class="line"></span><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/custom-headers-utf8</span></span></div><span class="line"><span class="string">Beverage</span><span>:</span> <span class="string">café</span> <span class="comment"># send the utf8 string - expected to be decoded as ascii in the server side</span></span><div class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span></div></div><div class="hurl-entry"><div class="request"><span class="line"></span><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/response-headers</span></span></div><div class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span><span class="line"><span class="string">Beverage</span><span>:</span> <span class="string">cafe</span> <span class="comment"># TBC send utf8</span></span></div></div></div>
<div class="hurl-file"><div class="hurl-entry"><div class="request"><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/default-headers</span></span></div><div class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span></div></div><div class="hurl-entry"><div class="request"><span class="line"></span><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/default-headers</span></span></div><span class="line"><span class="string">User-Agent</span><span>:</span> <span class="string">hurl/1.0</span></span><span class="line"><span class="string">Host</span><span>:</span> <span class="string">localhost:8000</span> <span class="comment"># comment</span></span><div class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span></div></div><div class="hurl-entry"><div class="request"><span class="line"></span><span class="line"></span><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/default-headers</span></span></div><span class="line"><span class="string">User-Agent</span><span>:</span> <span class="string">hurl/1.0</span></span><span class="line"><span class="string">Host</span><span>:</span> <span class="string">localhost:8000</span> <span class="comment"># comment</span></span><div class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span></div></div><div class="hurl-entry"><div class="request"><span class="line"></span><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/custom-headers</span></span></div><span class="line"><span class="string">Fruit</span><span>:</span> <span class="string">Raspberry</span></span><span class="line"><span class="string">Fruit</span><span>:</span> <span class="string">Apple</span></span><span class="line"><span class="string">Fruit</span><span>:</span> <span class="string">Banana</span></span><span class="line"><span class="string">Fruit</span><span>:</span> <span class="string">Grape</span></span><span class="line"><span class="string">Color</span><span>:</span> <span class="string">Green</span></span><div class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span></div></div><div class="hurl-entry"><div class="request"><span class="line"></span><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/custom-headers-utf8</span></span></div><span class="line"><span class="string">Beverage</span><span>:</span> <span class="string">café</span> <span class="comment"># send the utf8 string - expected to be decoded as ascii in the server side</span></span><div class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span></div></div><div class="hurl-entry"><div class="request"><span class="line"></span><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/custom-headers-quote</span></span></div><span class="line"><span class="string">Header1</span><span>:</span> <span class="string">'</span></span><div class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span></div></div><div class="hurl-entry"><div class="request"><span class="line"></span><span class="line"><span class="method">GET</span> <span class="url">http://localhost:8000/response-headers</span></span></div><div class="response"><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span><span class="line"><span class="string">Beverage</span><span>:</span> <span class="string">cafe</span> <span class="comment"># TBC send utf8</span></span></div></div></div>

View File

@ -24,6 +24,10 @@ GET http://localhost:8000/custom-headers-utf8
Beverage: café # send the utf8 string - expected to be decoded as ascii in the server side
HTTP/1.0 200
GET http://localhost:8000/custom-headers-quote
Header1: '
HTTP/1.0 200
GET http://localhost:8000/response-headers
HTTP/1.0 200
Beverage: cafe # TBC send utf8
Beverage: cafe # TBC send utf8

View File

@ -1 +1 @@
{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/default-headers"},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/default-headers","headers":[{"name":"User-Agent","value":"hurl/1.0"},{"name":"Host","value":"localhost:8000"}]},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/default-headers","headers":[{"name":"User-Agent","value":"hurl/1.0"},{"name":"Host","value":"localhost:8000"}]},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/custom-headers","headers":[{"name":"Fruit","value":"Raspberry"},{"name":"Fruit","value":"Apple"},{"name":"Fruit","value":"Banana"},{"name":"Fruit","value":"Grape"},{"name":"Color","value":"Green"}]},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/custom-headers-utf8","headers":[{"name":"Beverage","value":"café"}]},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/response-headers"},"response":{"version":"HTTP/1.0","status":200,"headers":[{"name":"Beverage","value":"cafe"}]}}]}
{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/default-headers"},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/default-headers","headers":[{"name":"User-Agent","value":"hurl/1.0"},{"name":"Host","value":"localhost:8000"}]},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/default-headers","headers":[{"name":"User-Agent","value":"hurl/1.0"},{"name":"Host","value":"localhost:8000"}]},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/custom-headers","headers":[{"name":"Fruit","value":"Raspberry"},{"name":"Fruit","value":"Apple"},{"name":"Fruit","value":"Banana"},{"name":"Fruit","value":"Grape"},{"name":"Color","value":"Green"}]},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/custom-headers-utf8","headers":[{"name":"Beverage","value":"café"}]},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/custom-headers-quote","headers":[{"name":"Header1","value":"'"}]},"response":{"version":"HTTP/1.0","status":200}},{"request":{"method":"GET","url":"http://localhost:8000/response-headers"},"response":{"version":"HTTP/1.0","status":200,"headers":[{"name":"Beverage","value":"cafe"}]}}]}

View File

@ -4,7 +4,7 @@ from tests import app
@app.route("/default-headers")
def default_headers():
assert 'hurl' in request.headers['User-Agent']
assert 'hurl' in request.headers['User-Agent'] or 'curl' in request.headers['User-Agent']
assert request.headers['Host'] == 'localhost:8000'
assert 'Content-Length' not in request.headers
return ''
@ -25,6 +25,12 @@ def custom_headers_utf8():
return ''
@app.route("/custom-headers-quote")
def custom_headers_quotes():
assert request.headers['Header1'] == "'"
return ''
@app.route("/response-headers")
def response_headers():
resp = make_response()

View File

@ -0,0 +1,2 @@
curl 'http://localhost:8000/hello'
curl 'http://localhost:8000/hello'

View File

@ -0,0 +1,2 @@
curl 'http://localhost:8000/include'

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/large'

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/multipart-form-data' -F 'key1=value1' -F 'upload1=@tests/data.txt;type=text/plain' -F 'upload2=@tests/data.html;type=text/html' -F 'upload3=@tests/data.txt;type=text/html'

View File

@ -1 +1 @@
<div class="hurl-file"><div class="hurl-entry"><div class="request"><span class="line"><span class="comment"># curl -v -F key1=value1 -F upload1=@tests/data.txt -Fupload2=@tests/data.html -Fupload3="@tests/data.txt;type=text/html" http://localhost:8000/multipart-form-data</span></span><span class="line"><span class="method">POST</span> <span class="url">http://localhost:8000/multipart-form-data</span></span></div><span class="line section-header">[MultipartFormData]</span></span><span class="line"><span class="string">key1</span><span>:</span> <span class="string">value1</span></span><span class="line"><span class="string"><span class="string">upload1</span></span>: file,<span class="string">data.txt</span>;</span><span class="line"><span class="string"><span class="string">upload2</span></span>: file,<span class="string">data.html</span>;</span><span class="line"><span class="string"><span class="string">upload3</span></span>: file,<span class="string">data.txt</span>; <span class="string">text/html</span></span><div class="response"><span class="line"></span><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span></div></div><span class="line"></span><span class="line"></span><span class="line"></span></div>
<div class="hurl-file"><div class="hurl-entry"><div class="request"><span class="line"><span class="method">POST</span> <span class="url">http://localhost:8000/multipart-form-data</span></span></div><span class="line section-header">[MultipartFormData]</span></span><span class="line"><span class="string">key1</span><span>:</span> <span class="string">value1</span></span><span class="line"><span class="string"><span class="string">upload1</span></span>: file,<span class="string">data.txt</span>;</span><span class="line"><span class="string"><span class="string">upload2</span></span>: file,<span class="string">data.html</span>;</span><span class="line"><span class="string"><span class="string">upload3</span></span>: file,<span class="string">data.txt</span>; <span class="string">text/html</span></span><div class="response"><span class="line"></span><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span></div></div><span class="line"></span><span class="line"></span><span class="line"></span></div>

View File

@ -1,4 +1,3 @@
# curl -v -F key1=value1 -F upload1=@tests/data.txt -Fupload2=@tests/data.html -Fupload3="@tests/data.txt;type=text/html" http://localhost:8000/multipart-form-data
POST http://localhost:8000/multipart-form-data
[MultipartFormData]
key1: value1

View File

@ -0,0 +1,2 @@
# all the entries
# have been commented

View File

@ -1 +1 @@
<div class="hurl-file"><span class="line"><span class="comment"># all the entries</span></span><span class="line"><span class="comment"># have been commented</span><br></span></div>
<div class="hurl-file"><span class="line"><span class="comment"># all the entries</span></span><span class="line"><span class="comment"># have been commented</span></span></div>

View File

@ -1,2 +1,2 @@
# all the entries
# have been commented
# have been commented

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/patch/file.txt' -X PATCH -H 'Host: www.example.com' -H 'Content-Type: application/example' -H 'If-Match: "e0023aa4e"'

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/post-base64' -H 'Content-Type: application/octet-stream' --data $'\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x21'

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/post-bytes' -H 'Content-Type: application/octet-stream' --data $'\x01\x02\x03'

View File

@ -0,0 +1 @@
0

View File

@ -0,0 +1 @@
<div class="hurl-file"><div class="hurl-entry"><div class="request"><span class="line"><span class="method">POST</span> <span class="url">http://localhost:8000/post-bytes</span></span></div><span class="line"><span class="string">Content-Type</span><span>:</span> <span class="string">application/octet-stream</span></span><span class="line">base64, AQID;</span><div class="response"><span class="line"></span><span class="line"><span class="version">HTTP/1.0</span> <span class="status">200</span></span></div></div></div>

View File

@ -0,0 +1,5 @@
POST http://localhost:8000/post-bytes
Content-Type: application/octet-stream
base64, AQID; # echo -e -n '\x01\x02\x03' | base64
HTTP/1.0 200

View File

@ -0,0 +1 @@
{"entries":[{"request":{"method":"POST","url":"http://localhost:8000/post-bytes","headers":[{"name":"Content-Type","value":"application/octet-stream"}],"body":{"type":"base64","value":"AQID"}},"response":{"version":"HTTP/1.0","status":200}}]}

View File

@ -0,0 +1,8 @@
from flask import request
from tests import app
@app.route('/post-bytes', methods=['POST'])
def post_bytes():
assert request.data == b'\x01\x02\x03'
return ''

View File

@ -0,0 +1,2 @@
curl 'http://localhost:8000/post-file' -H 'Content-Type:' --data '@tests/data.bin'

View File

@ -0,0 +1,8 @@
curl 'http://localhost:8000/post-json' -H 'Content-Type: application/json' --data $'{\n "name": "Bob",\n "password": "secret",\n "age": 30,\n "strict": true\n}'
curl 'http://localhost:8000/post-json-array' -H 'Content-Type: application/json' --data '[1,2,3]'
curl 'http://localhost:8000/post-json-string' -H 'Content-Type: application/json' --data '"Hello"'
curl 'http://localhost:8000/post-json-number' -H 'Content-Type: application/json' --data '100'
curl 'http://localhost:8000/post-json-numbers' -H 'Content-Type: application/json' --data $'{\n "natural": 100,\n "negative": -1,\n "float": "3.333333333333333",\n "exponent": 100e100\n}'
curl 'http://localhost:8000/post-json-boolean' -H 'Content-Type: application/json' --data 'true'
curl 'http://localhost:8000/get-name'
curl 'http://localhost:8000/post-json' -H 'Content-Type: application/json' --data $'{\n "name": "Bob",\n "password": "secret",\n "age": 30,\n "strict": true\n}'

View File

@ -0,0 +1,3 @@
curl 'http://localhost:8000/post-multilines' -H 'Content-Type:' --data $'name,age\nbob,10\nbill,22\n'
curl 'http://localhost:8000/get-bob-age'
curl 'http://localhost:8000/post-multilines' -H 'Content-Type:' --data $'name,age\nbob,10\nbill,22\n'

View File

@ -0,0 +1,2 @@
curl 'http://localhost:8000/post-xml' -H 'Content-Type: application/xml' --data $'<?xml version="1.0"?>\n<drink>café</drink>'
curl 'http://localhost:8000/post-xml-no-prolog' -H 'Content-Type: application/xml' --data '<drink>café</drink>'

View File

@ -0,0 +1,2 @@
curl 'http://localhost:8000/predicates-string'
curl 'http://localhost:8000/predicates-string-empty'

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/proxy' --proxy 'localhost:8888'

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/put' -X PUT

View File

@ -0,0 +1,4 @@
curl 'http://localhost:8000/querystring-params?param1=value1&param2=&param3=a%3Db&param4=1%2C2%2C3'
curl 'http://localhost:8000/querystring-params?param1=value1&param2=&param3=a%3db&param4=1,2,3'
curl 'http://localhost:8000/querystring-params?param1=value1&param2=&param3=a%3Db&param4=1%2C2%2C3'
curl 'http://localhost:8000/querystring-params-encoded?value1=/&value2=%2F&value3=%2F'

View File

@ -0,0 +1,2 @@
curl 'http://localhost:8000/redirect'
curl 'http://localhost:8000/redirected'

View File

@ -0,0 +1 @@
curl 'http://bob:secret@localhost:8000/basic-authentication'

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/utf8'

View File

@ -0,0 +1 @@
curl 'http://localhost:8000/variables' -H 'Content-Type: application/json' --data $'{\n "name": "Jennifer",\n "age": 30,\n "height": 1.700000000000000000,\n "female": true,\n "id": "123",\n "a_null": null\n}'

View File

@ -29,6 +29,7 @@ float-cmp = "0.6.0"
hurl_core = { version = "1.1.0", path = "../hurl_core" }
libflate = "1.0.2"
libxml = "0.2.12"
percent-encoding = "2.1.0"
regex = "1.1.0"
serde = "1.0.104"
serde_json = "1.0.40"

View File

@ -21,12 +21,13 @@ use std::str;
use curl::easy;
use encoding::all::ISO_8859_1;
use encoding::{DecoderTrap, Encoding};
use std::time::Duration;
use std::time::Instant;
use super::core::*;
use super::options::ClientOptions;
use super::request::*;
use super::response::*;
use url::Url;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HttpError {
@ -52,21 +53,6 @@ pub struct Client {
// hurl needs the return the headers only for the second (last) response)
}
#[derive(Debug, Clone)]
pub struct ClientOptions {
pub follow_location: bool,
pub max_redirect: Option<usize>,
pub cookie_input_file: Option<String>,
pub proxy: Option<String>,
pub no_proxy: Option<String>,
pub verbose: bool,
pub insecure: bool,
pub timeout: Duration,
pub connect_timeout: Duration,
pub user: Option<String>,
pub compressed: bool,
}
impl Client {
///
/// Init HTTP hurl client
@ -75,7 +61,7 @@ impl Client {
let mut h = easy::Easy::new();
// Set handle attributes
// that are not affected by rest
// that are not affected by reset
// Activate cookie storage
// with or without persistence (empty string)
@ -510,6 +496,67 @@ impl Client {
}
self.handle.cookie_list("ALL").unwrap();
}
///
/// return curl command-line for the http request run by the client
///
pub fn curl_command_line(&mut self, http_request: &Request) -> String {
let mut arguments = vec!["curl".to_string()];
arguments.append(&mut http_request.curl_args(self.options.context_dir.clone()));
let cookies = all_cookies(self.get_cookie_storage(), http_request);
if !cookies.is_empty() {
arguments.push("--cookie".to_string());
arguments.push(format!(
"'{}'",
cookies
.iter()
.map(|c| c.to_string())
.collect::<Vec<String>>()
.join("; ")
));
}
arguments.append(&mut self.options.curl_args());
arguments.join(" ")
}
}
///
/// return cookies from both cookies from the cookie storage and the request
///
pub fn all_cookies(cookie_storage: Vec<Cookie>, request: &Request) -> Vec<RequestCookie> {
let mut cookies = request.cookies.clone();
cookies.append(
&mut cookie_storage
.iter()
.filter(|c| c.expires != "1") // cookie expired when libcurl set value to 1?
.filter(|c| match_cookie(c, request.url.as_str()))
.map(|c| RequestCookie {
name: (*c).name.clone(),
value: c.value.clone(),
})
.collect(),
);
cookies
}
///
/// Match cookie for a given url
///
pub fn match_cookie(cookie: &Cookie, url: &str) -> bool {
// is it possible to do it with libcurl?
let url = Url::parse(url).expect("valid url");
if let Some(domain) = url.domain() {
if cookie.include_subdomain == "FALSE" {
if cookie.domain != domain {
return false;
}
} else if !domain.ends_with(cookie.domain.as_str()) {
return false;
}
}
url.path().starts_with(cookie.path.as_str())
}
impl Header {
@ -600,4 +647,33 @@ mod tests {
assert_eq!(lines.get(1).unwrap().as_str(), "Host: localhost:8000");
assert_eq!(lines.get(2).unwrap().as_str(), "");
}
#[test]
fn test_match_cookie() {
let cookie = Cookie {
domain: "example.com".to_string(),
include_subdomain: "FALSE".to_string(),
path: "/".to_string(),
https: "".to_string(),
expires: "".to_string(),
name: "".to_string(),
value: "".to_string(),
};
assert_eq!(match_cookie(&cookie, "http://example.com/toto"), true);
assert_eq!(match_cookie(&cookie, "http://sub.example.com/tata"), false);
assert_eq!(match_cookie(&cookie, "http://toto/tata"), false);
let cookie = Cookie {
domain: "example.com".to_string(),
include_subdomain: "TRUE".to_string(),
path: "/toto".to_string(),
https: "".to_string(),
expires: "".to_string(),
name: "".to_string(),
value: "".to_string(),
};
assert_eq!(match_cookie(&cookie, "http://example.com/toto"), true);
assert_eq!(match_cookie(&cookie, "http://sub.example.com/toto"), true);
assert_eq!(match_cookie(&cookie, "http://example.com/tata"), false);
}
}

View File

@ -16,8 +16,9 @@
*
*/
pub use self::client::{Client, ClientOptions, HttpError};
pub use self::client::{Client, HttpError};
pub use self::core::{Cookie, Header};
pub use self::options::ClientOptions;
#[cfg(test)]
pub use self::request::tests::*;
pub use self::request::{Body, FileParam, Method, MultipartParam, Param, Request, RequestCookie};
@ -27,5 +28,6 @@ pub use self::response::{Response, Version};
mod client;
mod core;
mod options;
mod request;
mod response;

View File

@ -0,0 +1,146 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct ClientOptions {
pub follow_location: bool,
pub max_redirect: Option<usize>,
pub cookie_input_file: Option<String>,
pub proxy: Option<String>,
pub no_proxy: Option<String>,
pub verbose: bool,
pub insecure: bool,
pub timeout: Duration,
pub connect_timeout: Duration,
pub user: Option<String>,
pub compressed: bool,
pub context_dir: String,
}
impl Default for ClientOptions {
fn default() -> Self {
ClientOptions {
follow_location: false,
max_redirect: Some(50),
cookie_input_file: None,
proxy: None,
no_proxy: None,
verbose: false,
insecure: false,
timeout: Duration::new(300, 0),
connect_timeout: Duration::new(300, 0),
user: None,
compressed: false,
context_dir: ".".to_string(),
}
}
}
impl ClientOptions {
pub fn curl_args(&self) -> Vec<String> {
let mut arguments = vec![];
if self.compressed {
arguments.push("--compressed".to_string());
}
if self.connect_timeout != ClientOptions::default().connect_timeout {
arguments.push("--connect-timeout".to_string());
arguments.push(self.connect_timeout.as_secs().to_string());
}
if let Some(cookie_file) = self.cookie_input_file.clone() {
arguments.push("--cookie".to_string());
arguments.push(cookie_file);
}
if self.insecure {
arguments.push("--insecure".to_string());
}
if self.follow_location {
arguments.push("-L".to_string());
}
if self.max_redirect != ClientOptions::default().max_redirect {
let max_redirect = match self.max_redirect {
None => -1,
Some(n) => n as i32,
};
arguments.push("--max-redirs".to_string());
arguments.push(max_redirect.to_string());
}
if let Some(proxy) = self.proxy.clone() {
arguments.push("--proxy".to_string());
arguments.push(format!("'{}'", proxy));
}
if self.timeout != ClientOptions::default().timeout {
arguments.push("--timeout".to_string());
arguments.push(self.timeout.as_secs().to_string());
}
if let Some(user) = self.user.clone() {
arguments.push("--user".to_string());
arguments.push(format!("'{}'", user));
}
arguments
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_curl_args() {
assert!(ClientOptions::default().curl_args().is_empty());
assert_eq!(
ClientOptions {
follow_location: true,
max_redirect: Some(10),
cookie_input_file: Some("cookie_file".to_string()),
proxy: Some("localhost:3128".to_string()),
no_proxy: None,
verbose: true,
insecure: true,
timeout: Duration::new(10, 0),
connect_timeout: Duration::new(20, 0),
user: Some("user:password".to_string()),
compressed: true,
context_dir: "".to_string()
}
.curl_args(),
[
"--compressed".to_string(),
"--connect-timeout".to_string(),
"20".to_string(),
"--cookie".to_string(),
"cookie_file".to_string(),
"--insecure".to_string(),
"-L".to_string(),
"--max-redirs".to_string(),
"10".to_string(),
"--proxy".to_string(),
"'localhost:3128'".to_string(),
"--timeout".to_string(),
"10".to_string(),
"--user".to_string(),
"'user:password'".to_string()
]
);
}
}

View File

@ -137,6 +137,213 @@ impl fmt::Display for RequestCookie {
}
}
impl Request {
///
/// return request as curl arguments
/// It does not contain the requests cookies (they will be accessed from the client)
///
pub fn curl_args(&self, context_dir: String) -> Vec<String> {
let querystring = if self.querystring.is_empty() {
"".to_string()
} else {
let params = self
.querystring
.iter()
.map(|p| p.curl_arg_escape())
.collect::<Vec<String>>();
params.join("&")
};
let url = if querystring.as_str() == "" {
self.url.to_string()
} else if self.url.to_string().contains('?') {
format!("{}&{}", self.url.to_string(), querystring)
} else {
format!("{}?{}", self.url.to_string(), querystring)
};
let mut arguments = vec![format!("'{}'", url)];
let data =
!self.multipart.is_empty() || !self.form.is_empty() || !self.body.bytes().is_empty();
arguments.append(&mut self.method.curl_args(data));
for header in self.headers.clone() {
arguments.append(&mut header.curl_args());
}
let has_explicit_content_type = self
.headers
.iter()
.map(|h| h.name.clone())
.any(|n| n.as_str() == "Content-Type");
if !has_explicit_content_type {
if let Some(content_type) = self.content_type.clone() {
if content_type.as_str() != "application/x-www-form-urlencoded"
&& content_type.as_str() != "multipart/form-data"
{
arguments.push("-H".to_string());
arguments.push(format!("'Content-Type: {}'", content_type));
}
} else if !self.body.bytes().is_empty() {
match self.body.clone() {
Body::Text(_) => {
arguments.push("-H".to_string());
arguments.push("'Content-Type:'".to_string())
}
Body::Binary(_) => {
arguments.push("-H".to_string());
arguments.push("'Content-Type: application/octet-stream'".to_string())
}
Body::File(_, _) => {
arguments.push("-H".to_string());
arguments.push("'Content-Type:'".to_string())
}
}
}
}
for param in self.form.clone() {
arguments.push("--data".to_string());
arguments.push(format!("'{}'", param.curl_arg_escape()));
}
for param in self.multipart.clone() {
arguments.push("-F".to_string());
arguments.push(format!("'{}'", param.curl_arg(context_dir.clone())));
}
if !self.body.bytes().is_empty() {
arguments.push("--data".to_string());
match self.body.clone() {
Body::Text(s) => {
let prefix = if s.contains('\n') { "$" } else { "" };
arguments.push(format!("{}'{}'", prefix, s.replace("\n", "\\n")))
}
Body::Binary(bytes) => arguments.push(format!("$'{}'", encode_bytes(bytes))),
Body::File(_, filename) => {
let prefix = if context_dir.as_str() == "." {
"".to_string()
} else {
format!("{}/", context_dir)
};
arguments.push(format!("'@{}{}'", prefix, filename))
}
}
}
arguments
}
}
fn encode_byte(b: u8) -> String {
format!("\\x{:02x}", b)
}
fn encode_bytes(b: Vec<u8>) -> String {
b.iter().map(|b| encode_byte(*b)).collect()
}
impl Method {
pub fn curl_args(&self, data: bool) -> Vec<String> {
match self {
Method::Get => {
if data {
vec!["-X".to_string(), "GET".to_string()]
} else {
vec![]
}
}
Method::Head => vec!["-X".to_string(), "HEAD".to_string()],
Method::Post => {
if data {
vec![]
} else {
vec!["-X".to_string(), "POST".to_string()]
}
}
Method::Put => vec!["-X".to_string(), "PUT".to_string()],
Method::Delete => vec!["-X".to_string(), "DELETE".to_string()],
Method::Connect => vec!["-X".to_string(), "CONNECT".to_string()],
Method::Options => vec!["-X".to_string(), "OPTIONS".to_string()],
Method::Trace => vec!["-X".to_string(), "TRACE".to_string()],
Method::Patch => vec!["-X".to_string(), "PATCH".to_string()],
}
}
}
impl Header {
pub fn curl_args(&self) -> Vec<String> {
let name = self.name.clone();
let value = self.value.clone();
vec![
"-H".to_string(),
encode_value(format!("{}: {}", name, value)),
]
}
}
impl Param {
pub fn curl_arg_escape(&self) -> String {
let name = self.name.clone();
let value = escape_url(self.value.clone());
format!("{}={}", name, value)
}
pub fn curl_arg(&self) -> String {
let name = self.name.clone();
let value = self.value.clone();
format!("{}={}", name, value)
}
}
impl MultipartParam {
pub fn curl_arg(&self, context_dir: String) -> String {
match self {
MultipartParam::Param(param) => param.curl_arg(),
MultipartParam::FileParam(FileParam {
name,
filename,
content_type,
..
}) => {
let prefix = if context_dir.as_str() == "." {
"".to_string()
} else {
format!("{}/", context_dir)
};
let value = format!("@{}{};type={}", prefix, filename, content_type);
format!("{}={}", name, value)
}
}
}
}
fn escape_single_quote(s: String) -> String {
s.chars()
.map(|c| {
if c == '\'' {
"\\'".to_string()
} else {
c.to_string()
}
})
.collect::<Vec<String>>()
.join("")
}
fn escape_url(s: String) -> String {
percent_encoding::percent_encode(s.as_bytes(), percent_encoding::NON_ALPHANUMERIC).to_string()
}
// special encoding for the shell
// $'...'
fn encode_value(s: String) -> String {
if s.contains('\'') {
let s = escape_single_quote(s);
format!("$'{}'", s)
} else {
format!("'{}'", s)
}
}
#[cfg(test)]
pub mod tests {
use super::*;
@ -220,10 +427,153 @@ pub mod tests {
value: String::from("application/x-www-form-urlencoded"),
}],
cookies: vec![],
body: Body::Text("param1=value1&param2=&param3=a%3db&param4=a%253db".to_string()),
body: Body::Binary(vec![]),
multipart: vec![],
form: vec![],
form: vec![
Param {
name: String::from("param1"),
value: String::from("value1"),
},
Param {
name: String::from("param2"),
value: String::from("a b"),
},
],
content_type: Some("multipart/form-data".to_string()),
}
}
#[test]
fn test_encode_byte() {
assert_eq!(encode_byte(1), "\\x01".to_string());
assert_eq!(encode_byte(32), "\\x20".to_string());
}
#[test]
fn method_curl_args() {
assert!(Method::Get.curl_args(false).is_empty());
assert_eq!(
Method::Get.curl_args(true),
vec!["-X".to_string(), "GET".to_string()]
);
assert_eq!(
Method::Post.curl_args(false),
vec!["-X".to_string(), "POST".to_string()]
);
assert!(Method::Post.curl_args(true).is_empty());
assert_eq!(
Method::Put.curl_args(false),
vec!["-X".to_string(), "PUT".to_string()]
);
assert_eq!(
Method::Put.curl_args(true),
vec!["-X".to_string(), "PUT".to_string()]
);
}
#[test]
fn header_curl_args() {
assert_eq!(
Header {
name: "Host".to_string(),
value: "example.com".to_string()
}
.curl_args(),
vec!["-H".to_string(), "'Host: example.com'".to_string()]
);
assert_eq!(
Header {
name: "If-Match".to_string(),
value: "\"e0023aa4e\"".to_string()
}
.curl_args(),
vec!["-H".to_string(), "'If-Match: \"e0023aa4e\"'".to_string()]
);
}
#[test]
fn param_curl_args() {
assert_eq!(
Param {
name: "param1".to_string(),
value: "value1".to_string()
}
.curl_arg(),
"param1=value1".to_string()
);
assert_eq!(
Param {
name: "param2".to_string(),
value: "".to_string()
}
.curl_arg(),
"param2=".to_string()
);
assert_eq!(
Param {
name: "param3".to_string(),
value: "a=b".to_string()
}
.curl_arg_escape(),
"param3=a%3Db".to_string()
);
assert_eq!(
Param {
name: "param4".to_string(),
value: "1,2,3".to_string()
}
.curl_arg_escape(),
"param4=1%2C2%2C3".to_string()
);
}
#[test]
fn requests_curl_args() {
assert_eq!(
hello_http_request().curl_args(".".to_string()),
vec!["'http://localhost:8000/hello'".to_string()]
);
assert_eq!(
custom_http_request().curl_args(".".to_string()),
vec![
"'http://localhost/custom'".to_string(),
"-H".to_string(),
"'User-Agent: iPhone'".to_string(),
"-H".to_string(),
"'Foo: Bar'".to_string(),
]
);
assert_eq!(
query_http_request().curl_args(".".to_string()),
vec![
"'http://localhost:8000/querystring-params?param1=value1&param2=a%20b'".to_string()
]
);
assert_eq!(
form_http_request().curl_args(".".to_string()),
vec![
"'http://localhost/form-params'".to_string(),
"-H".to_string(),
"'Content-Type: application/x-www-form-urlencoded'".to_string(),
"--data".to_string(),
"'param1=value1'".to_string(),
"--data".to_string(),
"'param2=a%20b'".to_string(),
]
);
}
#[test]
fn test_encode_value() {
assert_eq!(
encode_value("Header1: x".to_string()),
"'Header1: x'".to_string()
);
assert_eq!(
encode_value("Header1: '".to_string()),
"$'Header1: \\''".to_string()
);
}
}

View File

@ -32,6 +32,7 @@ use hurl::cli::interactive;
use hurl::cli::CliError;
use hurl::html;
use hurl::http;
use hurl::http::ClientOptions;
use hurl::runner;
use hurl::runner::{HurlResult, RunnerOptions, Value};
use hurl_core::ast::{Pos, SourceInfo};
@ -145,21 +146,6 @@ fn execute(
let connect_timeout = cli_options.connect_timeout;
let user = cli_options.user;
let compressed = cli_options.compressed;
let options = http::ClientOptions {
follow_location,
max_redirect,
cookie_input_file,
proxy,
no_proxy,
verbose,
insecure,
timeout,
connect_timeout,
user,
compressed,
};
let mut client = http::Client::init(options);
let context_dir = match file_root {
None => {
if filename == "-" {
@ -172,6 +158,23 @@ fn execute(
}
Some(filename) => filename,
};
let options = http::ClientOptions {
follow_location,
max_redirect,
cookie_input_file,
proxy,
no_proxy,
verbose,
insecure,
timeout,
connect_timeout,
user,
compressed,
context_dir: context_dir.clone(),
};
let mut client = http::Client::init(options);
let pre_entry = if cli_options.interactive {
interactive::pre_entry
} else {
@ -546,7 +549,7 @@ fn parse_options(matches: ArgMatches) -> Result<CliOptions, CliError> {
};
let timeout = match matches.value_of("max_time") {
None => Duration::from_secs(0),
None => ClientOptions::default().timeout,
Some(s) => match s.parse::<u64>() {
Ok(n) => Duration::from_secs(n),
Err(_) => {
@ -558,7 +561,7 @@ fn parse_options(matches: ArgMatches) -> Result<CliOptions, CliError> {
};
let connect_timeout = match matches.value_of("connect_timeout") {
None => Duration::from_secs(300),
None => ClientOptions::default().connect_timeout,
Some(s) => match s.parse::<u64>() {
Ok(n) => Duration::from_secs(n),
Err(_) => {

View File

@ -98,6 +98,13 @@ pub fn run(
}
log_verbose("");
log_request(log_verbose, &http_request);
log_verbose(
format!(
"request can be run with the following curl command:\n* {}\n*",
http_client.curl_command_line(&http_request)
)
.as_str(),
);
let http_response = match http_client.execute(&http_request, 0) {
Ok(response) => response,

View File

@ -58,7 +58,8 @@ use super::entry;
/// timeout: Default::default(),
/// connect_timeout: Default::default(),
/// user: None,
/// compressed: false
/// compressed: false,
/// context_dir: "".to_string(),
/// };
/// let mut client = http::Client::init(options);
///

View File

@ -99,6 +99,12 @@ pub fn eval_request(
}) = request.body
{
Some("application/json".to_string())
} else if let Some(Body {
value: Bytes::Xml { .. },
..
}) = request.body
{
Some("application/xml".to_string())
} else {
None
};

View File

@ -10,19 +10,7 @@ pub fn new_header(name: &str, value: &str) -> Header {
}
fn default_client() -> Client {
let options = ClientOptions {
follow_location: false,
max_redirect: None,
cookie_input_file: None,
proxy: None,
no_proxy: None,
verbose: true,
insecure: false,
timeout: Default::default(),
connect_timeout: Duration::from_secs(300),
user: None,
compressed: false,
};
let options = ClientOptions::default();
Client::init(options)
}
@ -46,6 +34,11 @@ fn default_get_request(url: String) -> Request {
fn test_hello() {
let mut client = default_client();
let request = default_get_request("http://localhost:8000/hello".to_string());
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/hello'".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.version, Version::Http10);
assert_eq!(response.status, 200);
@ -81,6 +74,11 @@ fn test_put() {
body: Body::Binary(vec![]),
content_type: None,
};
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/put' -X PUT".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
@ -113,6 +111,11 @@ fn test_patch() {
body: Body::Binary(vec![]),
content_type: None,
};
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/patch/file.txt' -X PATCH -H 'Host: www.example.com' -H 'Content-Type: application/example' -H 'If-Match: \"e0023aa4e\"'".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 204);
assert!(response.body.is_empty());
@ -142,6 +145,12 @@ fn test_custom_headers() {
body: Body::Binary(vec![]),
content_type: None,
};
assert!(client.options.curl_args().is_empty());
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/custom-headers' -H 'Fruit: Raspberry' -H 'Fruit: Apple' -H 'Fruit: Banana' -H 'Fruit: Grape' -H 'Color: Green'".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
@ -182,6 +191,10 @@ fn test_querystring_params() {
body: Body::Binary(vec![]),
content_type: None,
};
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/querystring-params?param1=value1&param2=&param3=a%3Db&param4=1%2C2%2C3'".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
@ -222,6 +235,11 @@ fn test_form_params() {
body: Body::Binary(vec![]),
content_type: Some("application/x-www-form-urlencoded".to_string()),
};
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/form-params' --data 'param1=value1' --data 'param2=' --data 'param3=a%3Db' --data 'param4=a%253db'".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
@ -255,18 +273,25 @@ fn test_follow_location() {
let options = ClientOptions {
follow_location: true,
max_redirect: None,
max_redirect: Some(50),
cookie_input_file: None,
proxy: None,
no_proxy: None,
verbose: false,
insecure: false,
timeout: Default::default(),
connect_timeout: Default::default(),
timeout: Duration::new(300, 0),
connect_timeout: Duration::new(300, 0),
user: None,
compressed: false,
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
assert_eq!(client.options.curl_args(), vec!["-L".to_string(),]);
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/redirect' -L".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert_eq!(
@ -296,13 +321,19 @@ fn test_max_redirect() {
no_proxy: None,
verbose: false,
insecure: false,
timeout: Default::default(),
connect_timeout: Default::default(),
timeout: Duration::new(300, 0),
connect_timeout: Duration::new(300, 0),
user: None,
compressed: false,
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/redirect".to_string());
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/redirect' -L --max-redirs 10".to_string()
);
let response = client.execute(&request, 5).unwrap();
assert_eq!(response.status, 200);
assert_eq!(client.redirect_count, 6);
@ -352,6 +383,11 @@ fn test_multipart_form_data() {
body: Body::Binary(vec![]),
content_type: Some("multipart/form-data".to_string()),
};
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/multipart-form-data' -F 'key1=value1' -F 'upload1=@data.txt;type=text/plain' -F 'upload2=@data.html;type=text/html' -F 'upload3=@data.txt;type=text/html'".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
@ -381,6 +417,10 @@ fn test_post_bytes() {
body: Body::Binary(b"Hello World!".to_vec()),
content_type: None,
};
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/post-base64' -H 'Content-Type: application/octet-stream' --data $'\\x48\\x65\\x6c\\x6c\\x6f\\x20\\x57\\x6f\\x72\\x6c\\x64\\x21'".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
@ -405,6 +445,11 @@ fn test_expect() {
body: Body::Text("data".to_string()),
content_type: None,
};
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/expect' -H 'Expect: 100-continue' -H 'Content-Type:' --data 'data'".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert_eq!(response.version, Version::Http10);
@ -415,16 +460,17 @@ fn test_expect() {
fn test_basic_authentication() {
let options = ClientOptions {
follow_location: false,
max_redirect: None,
max_redirect: Some(50),
cookie_input_file: None,
proxy: None,
no_proxy: None,
verbose: true,
verbose: false,
insecure: false,
timeout: Default::default(),
timeout: Duration::from_secs(300),
connect_timeout: Duration::from_secs(300),
user: Some("bob:secret".to_string()),
compressed: false,
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = Request {
@ -438,6 +484,10 @@ fn test_basic_authentication() {
body: Body::Binary(vec![]),
content_type: None,
};
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/basic-authentication' --user 'bob:secret'".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert_eq!(response.version, Version::Http10);
@ -455,6 +505,10 @@ fn test_basic_authentication() {
body: Body::Binary(vec![]),
content_type: None,
};
assert_eq!(
request.curl_args(".".to_string()),
vec!["'http://bob:secret@localhost:8000/basic-authentication'".to_string()]
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert_eq!(response.version, Version::Http10);
@ -491,6 +545,7 @@ fn test_error_fail_to_connect() {
connect_timeout: Default::default(),
user: None,
compressed: false,
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/hello".to_string());
@ -512,6 +567,7 @@ fn test_error_could_not_resolve_proxy_name() {
connect_timeout: Default::default(),
user: None,
compressed: false,
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/hello".to_string());
@ -533,6 +589,7 @@ fn test_error_ssl() {
connect_timeout: Default::default(),
user: None,
compressed: false,
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = default_get_request("https://localhost:8001/hello".to_string());
@ -559,6 +616,7 @@ fn test_timeout() {
connect_timeout: Default::default(),
user: None,
compressed: false,
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/timeout".to_string());
@ -577,11 +635,13 @@ fn test_accept_encoding() {
verbose: true,
insecure: false,
timeout: Default::default(),
connect_timeout: Duration::from_secs(300),
connect_timeout: Default::default(),
user: None,
compressed: true,
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = Request {
method: Method::Get,
url: "http://localhost:8000/compressed/gzip".to_string(),
@ -605,19 +665,24 @@ fn test_accept_encoding() {
fn test_connect_timeout() {
let options = ClientOptions {
follow_location: false,
max_redirect: None,
max_redirect: Some(50),
cookie_input_file: None,
proxy: None,
no_proxy: None,
verbose: false,
insecure: false,
timeout: Default::default(),
timeout: Duration::from_secs(300),
connect_timeout: Duration::from_secs(1),
user: None,
compressed: false,
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = default_get_request("http://10.0.0.0".to_string());
assert_eq!(
client.curl_command_line(&request),
"curl 'http://10.0.0.0' --connect-timeout 1".to_string()
);
let error = client.execute(&request, 0).err().unwrap();
if cfg!(target_os = "macos") {
assert_eq!(error, HttpError::FailToConnect);
@ -646,6 +711,13 @@ fn test_cookie() {
body: Body::Binary(vec![]),
content_type: None,
};
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/cookies/set-request-cookie1-valueA' --cookie 'cookie1=valueA'"
.to_string()
);
//assert_eq!(request.cookies(), vec!["cookie1=valueA".to_string(),]);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
@ -689,6 +761,11 @@ fn test_multiple_request_cookies() {
body: Body::Binary(vec![]),
content_type: None,
};
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/cookies/set-multiple-request-cookies' --cookie 'user1=Bob; user2=Bill'".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
@ -729,21 +806,27 @@ fn test_cookie_storage() {
fn test_cookie_file() {
let options = ClientOptions {
follow_location: false,
max_redirect: None,
max_redirect: Some(50),
cookie_input_file: Some("tests/cookies.txt".to_string()),
proxy: None,
no_proxy: None,
verbose: false,
insecure: false,
timeout: Default::default(),
connect_timeout: Default::default(),
timeout: Duration::new(300, 0),
connect_timeout: Duration::new(300, 0),
user: None,
compressed: false,
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = default_get_request(
"http://localhost:8000/cookies/assert-that-cookie2-is-valueA".to_string(),
);
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/cookies/assert-that-cookie2-is-valueA' --cookie tests/cookies.txt".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
@ -758,19 +841,24 @@ fn test_proxy() {
// mitmproxy listening on port 8888
let options = ClientOptions {
follow_location: false,
max_redirect: None,
max_redirect: Some(50),
cookie_input_file: None,
proxy: Some("localhost:8888".to_string()),
no_proxy: None,
verbose: false,
insecure: false,
timeout: Default::default(),
connect_timeout: Default::default(),
timeout: Duration::new(300, 0),
connect_timeout: Duration::new(300, 0),
user: None,
compressed: false,
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/proxy".to_string());
assert_eq!(
client.curl_command_line(&request),
"curl 'http://localhost:8000/proxy' --proxy 'localhost:8888'".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
}
@ -781,19 +869,25 @@ fn test_proxy() {
fn test_insecure() {
let options = ClientOptions {
follow_location: false,
max_redirect: None,
max_redirect: Some(50),
cookie_input_file: None,
proxy: None,
no_proxy: None,
verbose: false,
insecure: true,
timeout: Default::default(),
connect_timeout: Default::default(),
timeout: Duration::new(300, 0),
connect_timeout: Duration::new(300, 0),
user: None,
compressed: false,
context_dir: ".".to_string(),
};
let mut client = Client::init(options);
assert_eq!(client.options.curl_args(), vec!["--insecure".to_string()]);
let request = default_get_request("https://localhost:8001/hello".to_string());
assert_eq!(
client.curl_command_line(&request),
"curl 'https://localhost:8001/hello' --insecure".to_string()
);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);

View File

@ -54,6 +54,7 @@ fn test_hurl_file() {
connect_timeout: Default::default(),
user: None,
compressed: false,
context_dir: ".".to_string(),
};
let mut client = http::Client::init(options);
let mut lines: Vec<&str> = regex::Regex::new(r"\n|\r\n")
@ -164,6 +165,7 @@ fn test_hello() {
connect_timeout: Default::default(),
user: None,
compressed: false,
context_dir: ".".to_string(),
};
let mut client = http::Client::init(options);
let source_info = SourceInfo {