Add tests for libcurl-impersonate

Test that libcurl-impersonate produces the desired TLS signature when
the CURL_IMPERSONATE env var is set. A small C program called "minicurl"
is linked to libcurl, and libcurl-impersonate is loaded at runtime with
LD_PRELOAD.
This commit is contained in:
lwthiker
2022-02-27 23:21:00 +02:00
parent 8714c4631c
commit 6dad23b4b8
3 changed files with 228 additions and 6 deletions
+6 -1
View File
@@ -3,7 +3,7 @@ FROM python:3.10.1-slim-buster
WORKDIR /tests
RUN apt-get update && \
apt-get install -y tcpdump libbrotli1 libnss3
apt-get install -y tcpdump libbrotli1 libnss3 gcc libcurl4-openssl-dev
COPY requirements.txt requirements.txt
@@ -18,4 +18,9 @@ COPY --from=curl-impersonate-chrome /build/out/* /tests/chrome/
COPY . .
# Compile 'minicurl' which is used for testing libcurl-impersonate.
# 'minicurl' is compiled against the "regular" libcurl.
# libcurl-impersonate will replace it at runtime via LD_PRELOAD.
RUN gcc -Wall -Werror -o minicurl minicurl.c `curl-config --libs`
ENTRYPOINT ["pytest"]
+194
View File
@@ -0,0 +1,194 @@
/*
* A simple program that uses libcurl to fetch a URL and output to stdout.
*
* It is intended to be linked against the "regular" libcurl, with
* "libcurl-impersonate" loaded via LD_PRELOAD. It does the bare minimum
* to support the Python tests.
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <getopt.h>
#include <curl/curl.h>
/* Command line options. */
struct opts {
char *outfile;
uint16_t local_port_start;
uint16_t local_port_end;
char *url;
};
int parse_ports_range(char *str, uint16_t *start, uint16_t *end)
{
char port[32];
char *sep;
unsigned long int tmp;
if (strlen(str) >= sizeof(port)) {
return 1;
}
strncpy(port, str, sizeof(port) - 1);
sep = strchr(port, '-');
if (!sep) {
return 1;
}
*sep = 0;
errno = 0;
tmp = strtoul(port, NULL, 10);
if (errno || tmp == 0 || tmp > 0xffff) {
return 1;
}
*start = (uint16_t)tmp;
tmp = strtoul(sep + 1, NULL, 10);
if (errno || tmp == 0 || tmp > 0xffff || tmp < *start) {
return 1;
}
*end = (uint16_t)tmp;
return 0;
}
int parse_opts(int argc, char **argv, struct opts *opts)
{
int c;
int r;
memset(opts, 0, sizeof(*opts));
while (1) {
int option_index = 0;
static struct option long_options[] = {
{"local-port", required_argument, NULL, 'l'}
};
c = getopt_long(argc, argv, "o:", long_options, &option_index);
if (c == -1) {
break;
}
switch (c) {
case 'l':
r = parse_ports_range(optarg,
&opts->local_port_start,
&opts->local_port_end);
if (r) {
return r;
}
break;
case 'o':
opts->outfile = optarg;
break;
}
}
if (optind < argc) {
opts->url = argv[optind++];
} else {
return 1;
}
if (optind < argc) {
/* Too many arguments. */
return 1;
}
return 0;
}
int main(int argc, char *argv[])
{
struct opts opts;
CURLcode c;
CURL *curl = NULL;
FILE *file;
if (parse_opts(argc, argv, &opts)) {
fprintf(stderr, "Invalid arguments\n");
exit(1);
}
if (opts.outfile) {
file = fopen(opts.outfile, "w");
if (!file) {
fprintf(stderr, "Failed opening %s for writing\n", opts.outfile);
exit(1);
}
} else {
file = stdout;
}
c = curl_global_init(CURL_GLOBAL_DEFAULT);
if (c) {
fprintf(stderr, "curl_global_init() failed\n");
goto out_close;
}
curl = curl_easy_init();
if (!curl) {
fprintf(stderr, "curl_easy_init() failed\n");
c = 1;
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");
goto out;
}
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");
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;
}
c = 0;
out:
if (curl) {
curl_easy_cleanup(curl);
}
curl_global_cleanup();
out_close:
if (file) {
fclose(file);
}
return c;
}
+28 -5
View File
@@ -1,3 +1,4 @@
import os
import io
import logging
import subprocess
@@ -176,16 +177,30 @@ class TestImpersonation:
return None
@pytest.mark.parametrize(
"curl_binary, expected_signature",
"curl_binary, env_vars, expected_signature",
[
("chrome/curl_chrome98", "chrome_98.0.4758.102_win10"),
("firefox/curl_ff91esr", "firefox_91.6.0esr_win10"),
("firefox/curl_ff95", "firefox_95.0.2_win10")
# Test wrapper scripts
("chrome/curl_chrome98", None, "chrome_98.0.4758.102_win10"),
("firefox/curl_ff91esr", None, "firefox_91.6.0esr_win10"),
("firefox/curl_ff95", None, "firefox_95.0.2_win10"),
# Test libcurl-impersonate by loading it with LD_PRELOAD to an app
# linked against the regular libcurl and setting the
# CURL_IMPERSONATE env var.
(
"./minicurl",
{
"LD_PRELOAD": "./chrome/libcurl-impersonate.so",
"CURL_IMPERSONATE": "chrome98"
},
"chrome_98.0.4758.102_win10"
)
]
)
def test_impersonation(self,
tcpdump,
curl_binary,
env_vars,
browser_signatures,
expected_signature):
"""
@@ -196,13 +211,21 @@ class TestImpersonation:
extract the Client Hello packet from the capture and compares its
signature with the expected one defined in the YAML database.
"""
env = os.environ.copy()
if env_vars:
env |= env_vars
logging.debug(f"Launching '{curl_binary}' to {self.TEST_URL}")
if env_vars:
logging.debug("Environment variables: {}".format(
" ".join([f"{k}={v}" for k, v in env_vars.items()])))
curl = subprocess.Popen([
curl_binary,
"-o", "/dev/null",
"--local-port", f"{self.LOCAL_PORTS[0]}-{self.LOCAL_PORTS[1]}",
self.TEST_URL
])
], env=env)
ret = curl.wait(timeout=10)
assert ret == 0