mirror of
https://github.com/lwthiker/curl-impersonate.git
synced 2026-09-03 15:40:29 +00:00
Handle curl_easy_reset() calls when impersonating (#44)
curl_easy_reset() may be used by an application to reset the options on a curl handle. If an app has the CURL_IMPERSONATE env var defined, then the impersonation options are automatically set in curl_easy_init() but will be cleared in a call to curl_easy_reset(). The desired behavior is for the impersonation options to be retained (as they are "transparent" to the user), which this commit takes care of. Note that this only has an effect when libcurl-impersonate is loaded and the CURL_IMPERSONATE env var is set. Otherwise the regular behavior of resetting all the handle options is retained. Test that the unique TLS signature of curl-impersonate is preserved after a call to curl_easy_reset() when libcurl-impersonate is loaded. For this purpose change the 'minicurl' testing util to support multiple URLs and launch it with 2 different URLs when testing the TLS signature.
This commit is contained in:
@@ -97,7 +97,11 @@ Note that if you call `curl_easy_setopt()` later with one of the above it will o
|
||||
```bash
|
||||
LD_PRELOAD=/path/to/libcurl-impersonate.so CURL_IMPERSONATE=chrome98 my_app
|
||||
```
|
||||
The `CURL_IMPERSONATE` env var will cause `curl_easy_impersonate()` to be called automatically for any new curl handle created by `curl_easy_init()`.
|
||||
The `CURL_IMPERSONATE` env var has two effects:
|
||||
* `curl_easy_impersonate()` is called automatically for any new curl handle created by `curl_easy_init()`.
|
||||
* `curl_easy_impersonate()` is called automatically after any `curl_easy_reset()` call.
|
||||
|
||||
This means that all the options needed for impersonation will be automatically set for any curl handle.
|
||||
|
||||
Note that the above will NOT WORK for `curl` itself because the curl tool overrides the TLS settings. Use the wrapper scripts instead.
|
||||
|
||||
|
||||
@@ -244,7 +244,7 @@ index 769363941..cd59ad4b2 100644
|
||||
|
||||
CHECKSRC = $(CS_$(V))
|
||||
diff --git a/lib/easy.c b/lib/easy.c
|
||||
index 20293a710..37cda0992 100644
|
||||
index 20293a710..8b6a0f4e1 100644
|
||||
--- a/lib/easy.c
|
||||
+++ b/lib/easy.c
|
||||
@@ -80,6 +80,7 @@
|
||||
@@ -755,6 +755,28 @@ index 20293a710..37cda0992 100644
|
||||
/* Reinitialize an SSL engine for the new handle
|
||||
* note: the engine name has already been copied by dupset */
|
||||
if(outcurl->set.str[STRING_SSL_ENGINE]) {
|
||||
@@ -967,6 +1440,8 @@ struct Curl_easy *curl_easy_duphandle(struct Curl_easy *data)
|
||||
*/
|
||||
void curl_easy_reset(struct Curl_easy *data)
|
||||
{
|
||||
+ char *target;
|
||||
+
|
||||
Curl_free_request_state(data);
|
||||
|
||||
/* zero out UserDefined data: */
|
||||
@@ -991,6 +1466,12 @@ void curl_easy_reset(struct Curl_easy *data)
|
||||
#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_CRYPTO_AUTH)
|
||||
Curl_http_auth_cleanup_digest(data);
|
||||
#endif
|
||||
+
|
||||
+ target = curl_getenv("CURL_IMPERSONATE");
|
||||
+ if(target) {
|
||||
+ curl_easy_impersonate(data, target);
|
||||
+ free(target);
|
||||
+ }
|
||||
}
|
||||
|
||||
/*
|
||||
diff --git a/lib/easyoptions.c b/lib/easyoptions.c
|
||||
index 04871ad1e..ce280eaa3 100644
|
||||
--- a/lib/easyoptions.c
|
||||
|
||||
@@ -216,7 +216,7 @@ index 769363941..6e2f1b829 100644
|
||||
|
||||
CHECKSRC = $(CS_$(V))
|
||||
diff --git a/lib/easy.c b/lib/easy.c
|
||||
index 20293a710..87acdb814 100644
|
||||
index 20293a710..66730b5fc 100644
|
||||
--- a/lib/easy.c
|
||||
+++ b/lib/easy.c
|
||||
@@ -80,6 +80,7 @@
|
||||
@@ -510,6 +510,28 @@ index 20293a710..87acdb814 100644
|
||||
/* Reinitialize an SSL engine for the new handle
|
||||
* note: the engine name has already been copied by dupset */
|
||||
if(outcurl->set.str[STRING_SSL_ENGINE]) {
|
||||
@@ -967,6 +1223,8 @@ struct Curl_easy *curl_easy_duphandle(struct Curl_easy *data)
|
||||
*/
|
||||
void curl_easy_reset(struct Curl_easy *data)
|
||||
{
|
||||
+ char *target;
|
||||
+
|
||||
Curl_free_request_state(data);
|
||||
|
||||
/* zero out UserDefined data: */
|
||||
@@ -991,6 +1249,12 @@ void curl_easy_reset(struct Curl_easy *data)
|
||||
#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_CRYPTO_AUTH)
|
||||
Curl_http_auth_cleanup_digest(data);
|
||||
#endif
|
||||
+
|
||||
+ target = curl_getenv("CURL_IMPERSONATE");
|
||||
+ if(target) {
|
||||
+ curl_easy_impersonate(data, target);
|
||||
+ free(target);
|
||||
+ }
|
||||
}
|
||||
|
||||
/*
|
||||
diff --git a/lib/easyoptions.c b/lib/easyoptions.c
|
||||
index 04871ad1e..cd5998146 100644
|
||||
--- a/lib/easyoptions.c
|
||||
|
||||
119
tests/minicurl.c
119
tests/minicurl.c
@@ -16,13 +16,15 @@
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
/* Support up to 16 URLs */
|
||||
#define MAX_URLS 16
|
||||
/* Command line options. */
|
||||
struct opts {
|
||||
char *outfile;
|
||||
uint16_t local_port_start;
|
||||
uint16_t local_port_end;
|
||||
bool insecure;
|
||||
char *url;
|
||||
char *urls[MAX_URLS];
|
||||
};
|
||||
|
||||
int parse_ports_range(char *str, uint16_t *start, uint16_t *end)
|
||||
@@ -60,6 +62,7 @@ int parse_opts(int argc, char **argv, struct opts *opts)
|
||||
{
|
||||
int c;
|
||||
int r;
|
||||
int i;
|
||||
|
||||
memset(opts, 0, sizeof(*opts));
|
||||
|
||||
@@ -94,17 +97,69 @@ int parse_opts(int argc, char **argv, struct opts *opts)
|
||||
}
|
||||
}
|
||||
|
||||
if (optind < argc) {
|
||||
opts->url = argv[optind++];
|
||||
} else {
|
||||
/* No URL supplied. */
|
||||
if (optind >= argc) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (optind < argc) {
|
||||
/* Too many arguments. */
|
||||
/* The rest of the options are URLs */
|
||||
i = 0;
|
||||
while (optind < argc) {
|
||||
opts->urls[i++] = argv[optind++];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Set all options except for the URL. */
|
||||
int set_opts(CURL *curl, struct opts *opts, FILE *file)
|
||||
{
|
||||
CURLcode c;
|
||||
|
||||
c = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite);
|
||||
if (c) {
|
||||
fprintf(stderr, "curl_easy_setopt(CURLOPT_WRITEFUNCTION) failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
c = curl_easy_setopt(curl, CURLOPT_WRITEDATA, file);
|
||||
if (c) {
|
||||
fprintf(stderr, "curl_easy_setopt(CURLOPT_WRITEDATA) failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (opts->local_port_start && opts->local_port_end) {
|
||||
c = curl_easy_setopt(curl,
|
||||
CURLOPT_LOCALPORT,
|
||||
opts->local_port_start);
|
||||
if (c) {
|
||||
fprintf(stderr, "curl_easy_setopt(CURLOPT_LOCALPORT) failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
c = curl_easy_setopt(curl,
|
||||
CURLOPT_LOCALPORTRANGE,
|
||||
opts->local_port_end - opts->local_port_start);
|
||||
if (c) {
|
||||
fprintf(stderr,
|
||||
"curl_easy_setopt(CURLOPT_LOCALPORTRANGE) failed\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (opts->insecure) {
|
||||
c = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
if (c) {
|
||||
fprintf(stderr, "curl_easy_setopt(CURLOPT_SSL_VERIFYPEER) failed\n");
|
||||
return 1;
|
||||
}
|
||||
c = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
if (c) {
|
||||
fprintf(stderr, "curl_easy_setopt(CURLOPT_SSL_VERIFYHOST) failed\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -114,6 +169,7 @@ int main(int argc, char *argv[])
|
||||
CURLcode c;
|
||||
CURL *curl = NULL;
|
||||
FILE *file;
|
||||
int i;
|
||||
|
||||
if (parse_opts(argc, argv, &opts)) {
|
||||
fprintf(stderr, "Invalid arguments\n");
|
||||
@@ -143,60 +199,25 @@ int main(int argc, char *argv[])
|
||||
goto out;
|
||||
}
|
||||
|
||||
c = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite);
|
||||
if (c) {
|
||||
fprintf(stderr, "curl_easy_setopt(CURLOPT_WRITEFUNCTION) failed\n");
|
||||
goto out;
|
||||
}
|
||||
|
||||
c = curl_easy_setopt(curl, CURLOPT_WRITEDATA, file);
|
||||
if (c) {
|
||||
fprintf(stderr, "curl_easy_setopt(CURLOPT_WRITEDATA) failed\n");
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (opts.local_port_start && opts.local_port_end) {
|
||||
c = curl_easy_setopt(curl,
|
||||
CURLOPT_LOCALPORT,
|
||||
opts.local_port_start);
|
||||
if (c) {
|
||||
fprintf(stderr, "curl_easy_setopt(CURLOPT_LOCALPORT) failed\n");
|
||||
for (i = 0; i <= MAX_URLS && opts.urls[i]; i++) {
|
||||
if (set_opts(curl, &opts, file)) {
|
||||
goto out;
|
||||
}
|
||||
|
||||
c = curl_easy_setopt(curl,
|
||||
CURLOPT_LOCALPORTRANGE,
|
||||
opts.local_port_end - opts.local_port_start);
|
||||
c = curl_easy_setopt(curl, CURLOPT_URL, opts.urls[i]);
|
||||
if (c) {
|
||||
fprintf(stderr,
|
||||
"curl_easy_setopt(CURLOPT_LOCALPORTRANGE) failed\n");
|
||||
fprintf(stderr, "curl_easy_setopt(CURLOPT_URL) failed\n");
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.insecure) {
|
||||
c = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
c = curl_easy_perform(curl);
|
||||
if (c) {
|
||||
fprintf(stderr, "curl_easy_setopt(CURLOPT_SSL_VERIFYPEER) failed\n");
|
||||
fprintf(stderr, "curl_easy_perform() failed\n");
|
||||
goto out;
|
||||
}
|
||||
c = curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
if (c) {
|
||||
fprintf(stderr, "curl_easy_setopt(CURLOPT_SSL_VERIFYHOST) failed\n");
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
c = curl_easy_setopt(curl, CURLOPT_URL, opts.url);
|
||||
if (c) {
|
||||
fprintf(stderr, "curl_easy_setopt(CURLOPT_URL) failed\n");
|
||||
goto out;
|
||||
}
|
||||
|
||||
c = curl_easy_perform(curl);
|
||||
if (c) {
|
||||
fprintf(stderr, "curl_easy_perform() failed\n");
|
||||
goto out;
|
||||
/* Re-use the curl handle. */
|
||||
curl_easy_reset(curl);
|
||||
}
|
||||
|
||||
c = 0;
|
||||
|
||||
@@ -6,6 +6,7 @@ import asyncio
|
||||
import logging
|
||||
import subprocess
|
||||
import tempfile
|
||||
from typing import List
|
||||
|
||||
import yaml
|
||||
import dpkt
|
||||
@@ -124,7 +125,10 @@ class TestImpersonation:
|
||||
# This ensures we will capture the correct traffic in tcpdump.
|
||||
LOCAL_PORTS = (50000, 50100)
|
||||
|
||||
TEST_URL = "https://www.wikipedia.org"
|
||||
TEST_URLS = [
|
||||
"https://www.wikimedia.org",
|
||||
"https://www.wikipedia.org"
|
||||
]
|
||||
|
||||
# List of binaries and their expected signatures
|
||||
CURL_BINARIES_AND_SIGNATURES = [
|
||||
@@ -330,13 +334,13 @@ class TestImpersonation:
|
||||
elif sys.platform.startswith("darwin"):
|
||||
env_vars["DYLD_INSERT_LIBRARIES"] = lib + ".dylib"
|
||||
|
||||
def _run_curl(self, curl_binary, env_vars, extra_args, url,
|
||||
def _run_curl(self, curl_binary, env_vars, extra_args, urls,
|
||||
output="/dev/null"):
|
||||
env = os.environ.copy()
|
||||
if env_vars:
|
||||
env.update(env_vars)
|
||||
|
||||
logging.debug(f"Launching '{curl_binary}' to {url}")
|
||||
logging.debug(f"Launching '{curl_binary}' to {urls}")
|
||||
if env_vars:
|
||||
logging.debug("Environment variables: {}".format(
|
||||
" ".join([f"{k}={v}" for k, v in env_vars.items()])))
|
||||
@@ -348,17 +352,18 @@ class TestImpersonation:
|
||||
]
|
||||
if extra_args:
|
||||
args += extra_args
|
||||
args.append(url)
|
||||
args.extend(urls)
|
||||
|
||||
curl = subprocess.Popen(args, env=env)
|
||||
return curl.wait(timeout=10)
|
||||
|
||||
def _extract_client_hello(self, pcap: bytes) -> bytes:
|
||||
def _extract_client_hello(self, pcap: bytes) -> List[bytes]:
|
||||
"""Find and return the Client Hello TLS record from a pcap.
|
||||
|
||||
If there are multiple, returns the first.
|
||||
If there are none, returns None.
|
||||
"""
|
||||
client_hellos = []
|
||||
for ts, buf in dpkt.pcap.Reader(io.BytesIO(pcap)):
|
||||
eth = dpkt.ethernet.Ethernet(buf)
|
||||
if not isinstance(eth.data, dpkt.ip.IP) and not isinstance(eth.data, dpkt.ip6.IP6):
|
||||
@@ -380,9 +385,9 @@ class TestImpersonation:
|
||||
if handshake.type != 0x01:
|
||||
continue
|
||||
# Return the whole TLS record
|
||||
return tcp.data
|
||||
client_hellos.append(tcp.data)
|
||||
|
||||
return None
|
||||
return client_hellos
|
||||
|
||||
def _parse_nghttpd2_output(self, output):
|
||||
"""Parse the output of nghttpd2.
|
||||
@@ -456,7 +461,7 @@ class TestImpersonation:
|
||||
ret = self._run_curl(curl_binary,
|
||||
env_vars=env_vars,
|
||||
extra_args=None,
|
||||
url=self.TEST_URL)
|
||||
urls=self.TEST_URLS)
|
||||
assert ret == 0
|
||||
|
||||
try:
|
||||
@@ -474,21 +479,23 @@ class TestImpersonation:
|
||||
assert len(pcap) > 0
|
||||
logging.debug(f"Captured pcap of length {len(pcap)} bytes")
|
||||
|
||||
client_hello = self._extract_client_hello(pcap)
|
||||
assert client_hello is not None
|
||||
client_hellos = self._extract_client_hello(pcap)
|
||||
# A client hello message for each URL
|
||||
assert len(client_hellos) == len(self.TEST_URLS)
|
||||
|
||||
logging.debug(f"Found Client Hello, "
|
||||
logging.debug(f"Found {len(client_hellos)} Client Hello messages, "
|
||||
f"comparing to signature '{expected_signature}'")
|
||||
|
||||
sig = TLSClientHelloSignature.from_bytes(client_hello)
|
||||
expected_sig = TLSClientHelloSignature.from_dict(
|
||||
browser_signatures[expected_signature] \
|
||||
["signature"] \
|
||||
["tls_client_hello"]
|
||||
)
|
||||
for client_hello in client_hellos:
|
||||
sig = TLSClientHelloSignature.from_bytes(client_hello)
|
||||
expected_sig = TLSClientHelloSignature.from_dict(
|
||||
browser_signatures[expected_signature] \
|
||||
["signature"] \
|
||||
["tls_client_hello"]
|
||||
)
|
||||
|
||||
equals, msg = sig.equals(expected_sig, reason=True)
|
||||
assert equals, msg
|
||||
equals, msg = sig.equals(expected_sig, reason=True)
|
||||
assert equals, msg
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
@@ -520,7 +527,7 @@ class TestImpersonation:
|
||||
ret = self._run_curl(curl_binary,
|
||||
env_vars=env_vars,
|
||||
extra_args=["-k"],
|
||||
url="https://localhost:8443")
|
||||
urls=["https://localhost:8443"])
|
||||
assert ret == 0
|
||||
|
||||
output = await self._read_proc_output(nghttpd, timeout=2)
|
||||
@@ -575,7 +582,7 @@ class TestImpersonation:
|
||||
ret = self._run_curl(curl_binary,
|
||||
env_vars=env_vars,
|
||||
extra_args=None,
|
||||
url=self.TEST_URL,
|
||||
urls=[self.TEST_URLS[0]],
|
||||
output=output)
|
||||
assert ret == 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user