curl_impersonate 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +84 -0
- data/ext/curl_impersonate/curl_impersonate.c +240 -0
- data/ext/curl_impersonate/extconf.rb +139 -0
- data/ext/curl_impersonate/include/curl/curl.h +3336 -0
- data/ext/curl_impersonate/include/curl/curlver.h +79 -0
- data/ext/curl_impersonate/include/curl/easy.h +125 -0
- data/ext/curl_impersonate/include/curl/header.h +74 -0
- data/ext/curl_impersonate/include/curl/mprintf.h +85 -0
- data/ext/curl_impersonate/include/curl/multi.h +481 -0
- data/ext/curl_impersonate/include/curl/options.h +70 -0
- data/ext/curl_impersonate/include/curl/stdcheaders.h +35 -0
- data/ext/curl_impersonate/include/curl/system.h +402 -0
- data/ext/curl_impersonate/include/curl/typecheck-gcc.h +867 -0
- data/ext/curl_impersonate/include/curl/urlapi.h +155 -0
- data/ext/curl_impersonate/include/curl/websockets.h +85 -0
- data/lib/curl_impersonate/cookies.rb +22 -0
- data/lib/curl_impersonate/response.rb +7 -0
- data/lib/curl_impersonate/version.rb +3 -0
- data/lib/curl_impersonate.rb +61 -0
- metadata +124 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 94cdd8bf828ebec2bc00821f780c5d63d189b64d0cbc0512a8658290504d9223
|
|
4
|
+
data.tar.gz: 0bf9e8780a0483145d8b92f3ae1d42cd03d48ce5ea7daa20da623e186ab595cb
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9530d6d3c3dbdaa79f4be3b2659b9a7da0820006796e08c95802f74cf46f24347afe5b9bef2341633b8434a8c7e3ba8bdf3f3accbda61a18f62695a003323e3e
|
|
7
|
+
data.tar.gz: 9cbf63f95038c728012f54ab3830c5740a702122b6601737eb147a0284b73d7e1bcde2df4636d272b49fe70aeb3ed7219a2fb20e379e588e96f4eb25311a0264
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TeamMilestone
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# curl_impersonate
|
|
2
|
+
|
|
3
|
+
Ruby bindings for [libcurl-impersonate](https://github.com/lexiforest/curl-impersonate) — generate real-browser TLS / JA3 / JA4 fingerprints from Ruby.
|
|
4
|
+
|
|
5
|
+
Unlike pure-Ruby HTTP libraries (`net/http`, `httpx`, `faraday`, …) whose TLS handshakes are unmistakably "not a browser," this gem links the same BoringSSL-backed `libcurl-impersonate` build that the `curl-impersonate` project ships, so the TLS ClientHello, HTTP/2 SETTINGS frame, and header ordering are byte-identical to Chrome, Firefox, or Safari.
|
|
6
|
+
|
|
7
|
+
This is a Ruby port of [`go-curl-impersonate`](https://github.com/TeamMilestone/go-curl-impersonate). The public API matches it 1:1 within Ruby idioms.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Gemfile
|
|
13
|
+
gem "curl_impersonate"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
On platforms with a precompiled gem (`arm64-darwin`, `x86_64-linux-gnu`), `gem install` is a single step with no external dependencies — the BoringSSL build is statically linked into the gem's native extension.
|
|
17
|
+
|
|
18
|
+
On other platforms you will need to build from source; see [Building from source](#building-from-source).
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require "curl_impersonate"
|
|
24
|
+
|
|
25
|
+
resp = CurlImpersonate.do_request(
|
|
26
|
+
url: "https://example.com",
|
|
27
|
+
impersonate: "chrome131",
|
|
28
|
+
headers: { "Accept-Language" => "en-US" },
|
|
29
|
+
timeout_sec: 15,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
resp.status_code # => 200
|
|
33
|
+
resp.success? # => true
|
|
34
|
+
resp.body # => "<!doctype html>..."
|
|
35
|
+
resp.headers # => "HTTP/2 200\r\ncontent-type: text/html\r\n..."
|
|
36
|
+
|
|
37
|
+
CurlImpersonate.extract_cookies(resp.headers) # => { "name" => "value", ... }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## API
|
|
41
|
+
|
|
42
|
+
### `CurlImpersonate.do_request(...)` → `Response`
|
|
43
|
+
|
|
44
|
+
| keyword | type | default | notes |
|
|
45
|
+
|---------|------|---------|-------|
|
|
46
|
+
| `url:` | `String` | — | Required. |
|
|
47
|
+
| `impersonate:` | `String` | `"chrome131"` | Any target supported by libcurl-impersonate. Examples: `chrome131`, `firefox133`, `safari180`. See the [upstream targets](https://github.com/lexiforest/curl-impersonate#supported-browsers). |
|
|
48
|
+
| `headers:` | `Hash<String, String>` | `{}` | Custom headers merged on top of the browser default set. |
|
|
49
|
+
| `post_data:` | `String` | `""` | Non-empty switches the request to POST and is sent as the body verbatim. |
|
|
50
|
+
| `follow_redirects:` | `Boolean` | `true` | Maps to `CURLOPT_FOLLOWLOCATION`. |
|
|
51
|
+
| `timeout_sec:` | `Integer` | `15` | Total request timeout. |
|
|
52
|
+
| `proxy:` | `String` | `""` | `"scheme://user:pass@host:port"`. Credentials are split out and passed via `CURLOPT_PROXYUSERPWD`. |
|
|
53
|
+
|
|
54
|
+
Returns a `CurlImpersonate::Response` Struct with `status_code` (Integer), `body` (String), and `headers` (String — raw `\r\n`-separated lines). On any libcurl error (resolution, timeout, TLS handshake, …) raises `CurlImpersonate::Error`.
|
|
55
|
+
|
|
56
|
+
`curl_easy_perform` runs without the GVL, so other Ruby threads continue executing during the network wait.
|
|
57
|
+
|
|
58
|
+
### `CurlImpersonate.extract_cookies(headers_str)` → `Hash`
|
|
59
|
+
|
|
60
|
+
Parses `Set-Cookie:` lines from a raw header string and returns a `Hash<String, String>` of cookie names → values. Attributes (`Path`, `Domain`, `HttpOnly`, `Expires`, `Secure`, …) are discarded. Later cookies with the same name overwrite earlier ones.
|
|
61
|
+
|
|
62
|
+
## Building from source
|
|
63
|
+
|
|
64
|
+
You'll need:
|
|
65
|
+
|
|
66
|
+
- A C compiler (`clang` / `gcc`) and `make`
|
|
67
|
+
- `libcurl-impersonate` 1.5.x installed via one of:
|
|
68
|
+
- **macOS**: `brew install teammilestone/tap/libcurl-impersonate`
|
|
69
|
+
- **Linux**: download the matching tarball from [lexiforest/curl-impersonate releases](https://github.com/lexiforest/curl-impersonate/releases) and copy `libcurl-impersonate.a` plus headers into a system path discoverable by `pkg-config`
|
|
70
|
+
- Or set `CURL_IMPERSONATE_DIR=/path/to/install` (containing `lib/libcurl-impersonate.a` and `include/`) before `gem install`
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
gem install curl_impersonate --platform=ruby # forces source compilation
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Caveats
|
|
77
|
+
|
|
78
|
+
- **SSL certificate verification is disabled** (`CURLOPT_SSL_VERIFYPEER = 0`, `CURLOPT_SSL_VERIFYHOST = 0`). This matches the Go reference implementation. The premise of TLS impersonation is that you are mimicking a browser TLS stack regardless of trust chain validation; if you need real verification, this gem is the wrong tool.
|
|
79
|
+
- **JA3 hash varies between requests.** Chrome injects random GREASE values into the cipher and extension lists on every TLS handshake, so the JA3 string and its hash change each time. This is the correct Chrome behavior — match on **JA4** (which sorts before hashing) if you need a stable fingerprint to assert against.
|
|
80
|
+
- **Status of this gem**: under active development; API is not stable until 1.0.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#include <ruby.h>
|
|
2
|
+
#include <ruby/thread.h>
|
|
3
|
+
#include <curl/curl.h>
|
|
4
|
+
#include <stdlib.h>
|
|
5
|
+
#include <string.h>
|
|
6
|
+
|
|
7
|
+
#ifdef HAVE_CURL_IMPERSONATE_CURL_IMPERSONATE_H
|
|
8
|
+
# include <curl-impersonate/curl_impersonate.h>
|
|
9
|
+
#else
|
|
10
|
+
extern CURLcode curl_easy_impersonate(CURL *curl, const char *target, int default_headers);
|
|
11
|
+
#endif
|
|
12
|
+
|
|
13
|
+
static VALUE mCurlImpersonate;
|
|
14
|
+
static VALUE cResponse;
|
|
15
|
+
static VALUE eError;
|
|
16
|
+
|
|
17
|
+
struct buffer {
|
|
18
|
+
char *data;
|
|
19
|
+
size_t len;
|
|
20
|
+
size_t cap;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
static void buffer_init(struct buffer *b) { b->data = NULL; b->len = 0; b->cap = 0; }
|
|
24
|
+
static void buffer_free(struct buffer *b) { free(b->data); b->data = NULL; b->len = 0; b->cap = 0; }
|
|
25
|
+
|
|
26
|
+
static int buffer_append(struct buffer *b, const char *src, size_t n) {
|
|
27
|
+
size_t needed = b->len + n;
|
|
28
|
+
if (needed > b->cap) {
|
|
29
|
+
size_t new_cap = b->cap == 0 ? 4096 : b->cap;
|
|
30
|
+
while (new_cap < needed) new_cap *= 2;
|
|
31
|
+
char *p = realloc(b->data, new_cap);
|
|
32
|
+
if (!p) return -1;
|
|
33
|
+
b->data = p;
|
|
34
|
+
b->cap = new_cap;
|
|
35
|
+
}
|
|
36
|
+
memcpy(b->data + b->len, src, n);
|
|
37
|
+
b->len += n;
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static size_t write_body_cb(char *ptr, size_t size, size_t nmemb, void *userdata) {
|
|
42
|
+
size_t total = size * nmemb;
|
|
43
|
+
return buffer_append((struct buffer *)userdata, ptr, total) == 0 ? total : 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static size_t write_header_cb(char *ptr, size_t size, size_t nmemb, void *userdata) {
|
|
47
|
+
size_t total = size * nmemb;
|
|
48
|
+
return buffer_append((struct buffer *)userdata, ptr, total) == 0 ? total : 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
struct perform_args {
|
|
52
|
+
CURL *handle;
|
|
53
|
+
CURLcode result;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
static void *perform_without_gvl(void *data) {
|
|
57
|
+
struct perform_args *args = (struct perform_args *)data;
|
|
58
|
+
args->result = curl_easy_perform(args->handle);
|
|
59
|
+
return NULL;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Iterator state passed to rb_hash_foreach to build a curl_slist of "Key: Value"
|
|
63
|
+
* strings from a Ruby Hash. */
|
|
64
|
+
struct slist_build {
|
|
65
|
+
struct curl_slist *list;
|
|
66
|
+
int error; /* non-zero on first conversion failure */
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
static int build_header_slist(VALUE key, VALUE val, VALUE arg) {
|
|
70
|
+
struct slist_build *s = (struct slist_build *)arg;
|
|
71
|
+
if (s->error) return ST_STOP;
|
|
72
|
+
|
|
73
|
+
VALUE k = rb_check_string_type(key);
|
|
74
|
+
VALUE v = rb_check_string_type(val);
|
|
75
|
+
if (NIL_P(k) || NIL_P(v)) { s->error = 1; return ST_STOP; }
|
|
76
|
+
|
|
77
|
+
/* "<key>: <value>\0" — RFC 7230 header line */
|
|
78
|
+
long klen = RSTRING_LEN(k);
|
|
79
|
+
long vlen = RSTRING_LEN(v);
|
|
80
|
+
char *line = malloc((size_t)klen + 2 + (size_t)vlen + 1);
|
|
81
|
+
if (!line) { s->error = 1; return ST_STOP; }
|
|
82
|
+
|
|
83
|
+
memcpy(line, RSTRING_PTR(k), klen);
|
|
84
|
+
line[klen] = ':';
|
|
85
|
+
line[klen + 1] = ' ';
|
|
86
|
+
memcpy(line + klen + 2, RSTRING_PTR(v), vlen);
|
|
87
|
+
line[klen + 2 + vlen] = '\0';
|
|
88
|
+
|
|
89
|
+
struct curl_slist *next = curl_slist_append(s->list, line);
|
|
90
|
+
free(line);
|
|
91
|
+
if (!next) { s->error = 1; return ST_STOP; }
|
|
92
|
+
s->list = next;
|
|
93
|
+
return ST_CONTINUE;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static VALUE rb_cci_native_version(VALUE self) {
|
|
97
|
+
(void)self;
|
|
98
|
+
return rb_str_new_cstr(curl_version());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Signature (stage 7):
|
|
102
|
+
* _do_request_native(url, impersonate, headers, post_data,
|
|
103
|
+
* follow_redirects, timeout_sec,
|
|
104
|
+
* proxy_url, proxy_userpwd) -> Response
|
|
105
|
+
*
|
|
106
|
+
* url : String
|
|
107
|
+
* impersonate : String (e.g. "chrome131")
|
|
108
|
+
* headers : Hash<String, String>
|
|
109
|
+
* post_data : String — empty string means GET, non-empty means POST
|
|
110
|
+
* follow_redirects : true / false
|
|
111
|
+
* timeout_sec : Integer
|
|
112
|
+
* proxy_url : String — empty string disables; otherwise host (no auth)
|
|
113
|
+
* proxy_userpwd : String — "user:pass" or empty
|
|
114
|
+
*/
|
|
115
|
+
static VALUE rb_cci_do_request(int argc, VALUE *argv, VALUE self) {
|
|
116
|
+
(void)self;
|
|
117
|
+
if (argc != 8) {
|
|
118
|
+
rb_raise(rb_eArgError, "wrong number of arguments (given %d, expected 8)", argc);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
VALUE rb_url = argv[0];
|
|
122
|
+
VALUE rb_impersonate = argv[1];
|
|
123
|
+
VALUE rb_headers = argv[2];
|
|
124
|
+
VALUE rb_post_data = argv[3];
|
|
125
|
+
VALUE rb_follow = argv[4];
|
|
126
|
+
VALUE rb_timeout = argv[5];
|
|
127
|
+
VALUE rb_proxy_url = argv[6];
|
|
128
|
+
VALUE rb_proxy_userpwd = argv[7];
|
|
129
|
+
|
|
130
|
+
Check_Type(rb_url, T_STRING);
|
|
131
|
+
Check_Type(rb_impersonate, T_STRING);
|
|
132
|
+
Check_Type(rb_headers, T_HASH);
|
|
133
|
+
Check_Type(rb_post_data, T_STRING);
|
|
134
|
+
Check_Type(rb_proxy_url, T_STRING);
|
|
135
|
+
Check_Type(rb_proxy_userpwd, T_STRING);
|
|
136
|
+
|
|
137
|
+
const char *url = StringValueCStr(rb_url);
|
|
138
|
+
const char *impersonate = StringValueCStr(rb_impersonate);
|
|
139
|
+
long timeout_sec = NUM2LONG(rb_timeout);
|
|
140
|
+
long follow = RTEST(rb_follow) ? 1L : 0L;
|
|
141
|
+
|
|
142
|
+
CURL *handle = curl_easy_init();
|
|
143
|
+
if (!handle) {
|
|
144
|
+
rb_raise(eError, "curl_easy_init failed");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
CURLcode rc = curl_easy_impersonate(handle, impersonate, 1);
|
|
148
|
+
if (rc != CURLE_OK) {
|
|
149
|
+
curl_easy_cleanup(handle);
|
|
150
|
+
rb_raise(eError, "curl_easy_impersonate(%s) failed: %s",
|
|
151
|
+
impersonate, curl_easy_strerror(rc));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
struct buffer body, headers_buf;
|
|
155
|
+
buffer_init(&body);
|
|
156
|
+
buffer_init(&headers_buf);
|
|
157
|
+
|
|
158
|
+
curl_easy_setopt(handle, CURLOPT_URL, url);
|
|
159
|
+
curl_easy_setopt(handle, CURLOPT_TIMEOUT, timeout_sec);
|
|
160
|
+
curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, follow);
|
|
161
|
+
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 0L);
|
|
162
|
+
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 0L);
|
|
163
|
+
/* Empty string enables all built-in encodings (gzip, br, zstd if available) —
|
|
164
|
+
* essential for matching browser Accept-Encoding fingerprint. */
|
|
165
|
+
curl_easy_setopt(handle, CURLOPT_ACCEPT_ENCODING, "");
|
|
166
|
+
curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, write_body_cb);
|
|
167
|
+
curl_easy_setopt(handle, CURLOPT_WRITEDATA, &body);
|
|
168
|
+
curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, write_header_cb);
|
|
169
|
+
curl_easy_setopt(handle, CURLOPT_HEADERDATA, &headers_buf);
|
|
170
|
+
|
|
171
|
+
/* POST body. We use COPYPOSTFIELDS so libcurl owns the copy and we can
|
|
172
|
+
* release the Ruby String after setopt returns. POSTFIELDSIZE is set
|
|
173
|
+
* explicitly so binary bodies with embedded NULs work. */
|
|
174
|
+
long post_len = RSTRING_LEN(rb_post_data);
|
|
175
|
+
if (post_len > 0) {
|
|
176
|
+
curl_easy_setopt(handle, CURLOPT_POSTFIELDSIZE, post_len);
|
|
177
|
+
curl_easy_setopt(handle, CURLOPT_COPYPOSTFIELDS, RSTRING_PTR(rb_post_data));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (RSTRING_LEN(rb_proxy_url) > 0) {
|
|
181
|
+
curl_easy_setopt(handle, CURLOPT_PROXY, StringValueCStr(rb_proxy_url));
|
|
182
|
+
}
|
|
183
|
+
if (RSTRING_LEN(rb_proxy_userpwd) > 0) {
|
|
184
|
+
curl_easy_setopt(handle, CURLOPT_PROXYUSERPWD, StringValueCStr(rb_proxy_userpwd));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Custom headers. Build a curl_slist from the Ruby Hash. */
|
|
188
|
+
struct slist_build sb = { .list = NULL, .error = 0 };
|
|
189
|
+
if (RHASH_SIZE(rb_headers) > 0) {
|
|
190
|
+
rb_hash_foreach(rb_headers, build_header_slist, (VALUE)&sb);
|
|
191
|
+
if (sb.error) {
|
|
192
|
+
curl_slist_free_all(sb.list);
|
|
193
|
+
buffer_free(&body);
|
|
194
|
+
buffer_free(&headers_buf);
|
|
195
|
+
curl_easy_cleanup(handle);
|
|
196
|
+
rb_raise(eError, "failed to build header list (non-string key/value or OOM)");
|
|
197
|
+
}
|
|
198
|
+
curl_easy_setopt(handle, CURLOPT_HTTPHEADER, sb.list);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
struct perform_args args = { .handle = handle, .result = CURLE_OK };
|
|
202
|
+
rb_thread_call_without_gvl(perform_without_gvl, &args, RUBY_UBF_IO, NULL);
|
|
203
|
+
|
|
204
|
+
if (args.result != CURLE_OK) {
|
|
205
|
+
char errbuf[256];
|
|
206
|
+
strncpy(errbuf, curl_easy_strerror(args.result), sizeof(errbuf) - 1);
|
|
207
|
+
errbuf[sizeof(errbuf) - 1] = '\0';
|
|
208
|
+
curl_slist_free_all(sb.list);
|
|
209
|
+
buffer_free(&body);
|
|
210
|
+
buffer_free(&headers_buf);
|
|
211
|
+
curl_easy_cleanup(handle);
|
|
212
|
+
rb_raise(eError, "curl_easy_perform failed: %s", errbuf);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
long status_code = 0;
|
|
216
|
+
curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &status_code);
|
|
217
|
+
|
|
218
|
+
VALUE rb_body = rb_str_new(body.data ? body.data : "", body.len);
|
|
219
|
+
VALUE rb_hdr_str = rb_str_new(headers_buf.data ? headers_buf.data : "", headers_buf.len);
|
|
220
|
+
|
|
221
|
+
curl_slist_free_all(sb.list);
|
|
222
|
+
buffer_free(&body);
|
|
223
|
+
buffer_free(&headers_buf);
|
|
224
|
+
curl_easy_cleanup(handle);
|
|
225
|
+
|
|
226
|
+
return rb_struct_new(cResponse, LONG2NUM(status_code), rb_body, rb_hdr_str);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
void Init_curl_impersonate(void) {
|
|
230
|
+
curl_global_init(CURL_GLOBAL_DEFAULT);
|
|
231
|
+
|
|
232
|
+
mCurlImpersonate = rb_define_module("CurlImpersonate");
|
|
233
|
+
eError = rb_const_get(mCurlImpersonate, rb_intern("Error"));
|
|
234
|
+
cResponse = rb_const_get(mCurlImpersonate, rb_intern("Response"));
|
|
235
|
+
|
|
236
|
+
rb_define_singleton_method(mCurlImpersonate, "_native_curl_version",
|
|
237
|
+
rb_cci_native_version, 0);
|
|
238
|
+
rb_define_singleton_method(mCurlImpersonate, "_do_request_native",
|
|
239
|
+
rb_cci_do_request, -1);
|
|
240
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
require "rbconfig"
|
|
2
|
+
require "shellwords"
|
|
3
|
+
|
|
4
|
+
# Avoid baking an absolute libruby.dylib install_name into the resulting
|
|
5
|
+
# .bundle on macOS: clear LIBRUBYARG_SHARED *before* mkmf is loaded, so its
|
|
6
|
+
# Makefile template renders LIBS without `-lruby.X.Y`. The dynamic loader will
|
|
7
|
+
# satisfy Ruby symbols from the host process at require time — this is the
|
|
8
|
+
# documented portable pattern for macOS Ruby extensions and is also how
|
|
9
|
+
# nokogiri/sqlite3 ship their precompiled darwin gems.
|
|
10
|
+
if RUBY_PLATFORM =~ /darwin/
|
|
11
|
+
# mkmf uses MAKEFILE_CONFIG (NOT CONFIG) for $(LIBRUBYARG_SHARED) expansion
|
|
12
|
+
# inside the generated Makefile. Clear both for safety.
|
|
13
|
+
RbConfig::CONFIG["LIBRUBYARG_SHARED"] = ""
|
|
14
|
+
RbConfig::CONFIG["LIBRUBYARG"] = ""
|
|
15
|
+
RbConfig::MAKEFILE_CONFIG["LIBRUBYARG_SHARED"] = ""
|
|
16
|
+
RbConfig::MAKEFILE_CONFIG["LIBRUBYARG"] = ""
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require "mkmf"
|
|
20
|
+
|
|
21
|
+
# Resolve libcurl-impersonate location.
|
|
22
|
+
#
|
|
23
|
+
# Priority:
|
|
24
|
+
# 1. ENV["CURL_IMPERSONATE_DIR"] — explicit override (CI)
|
|
25
|
+
# 2. ext/curl_impersonate/vendor/<arch>/ — bundled in precompiled gem
|
|
26
|
+
# 3. pkg-config --libs --cflags libcurl-impersonate — system install (brew/apt)
|
|
27
|
+
#
|
|
28
|
+
# In all cases we end up statically linking libcurl-impersonate.a so that the
|
|
29
|
+
# resulting .bundle / .so has BoringSSL baked in and the end user does not need
|
|
30
|
+
# the library installed at runtime.
|
|
31
|
+
|
|
32
|
+
def configure_from_dir(dir)
|
|
33
|
+
raise "CURL_IMPERSONATE_DIR=#{dir} does not exist" unless File.directory?(dir)
|
|
34
|
+
|
|
35
|
+
lib_dir = File.join(dir, "lib")
|
|
36
|
+
include_dir = File.join(dir, "include")
|
|
37
|
+
|
|
38
|
+
$LIBPATH.unshift(lib_dir) if File.directory?(lib_dir)
|
|
39
|
+
$CFLAGS << " -I#{include_dir.shellescape}" if File.directory?(include_dir)
|
|
40
|
+
$CFLAGS << " -I#{File.join(include_dir, "curl-impersonate").shellescape}" if File.directory?(File.join(include_dir, "curl-impersonate"))
|
|
41
|
+
|
|
42
|
+
static_archive = File.join(lib_dir, "libcurl-impersonate.a")
|
|
43
|
+
unless File.exist?(static_archive)
|
|
44
|
+
raise "libcurl-impersonate.a not found under #{lib_dir}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Link the static archive directly so its symbols (including curl_easy_impersonate)
|
|
48
|
+
# end up in our .bundle / .so.
|
|
49
|
+
$LDFLAGS << " #{static_archive.shellescape}"
|
|
50
|
+
|
|
51
|
+
# libcurl-impersonate.a bundles BoringSSL, nghttp{2,3}, ngtcp2, zstd, and
|
|
52
|
+
# brotli statically — but its remaining external dependencies have to be
|
|
53
|
+
# linked separately. The list differs by target platform; we use the
|
|
54
|
+
# vendor directory name as a hint when one is available, otherwise fall
|
|
55
|
+
# back to RUBY_PLATFORM.
|
|
56
|
+
target = File.basename(dir)
|
|
57
|
+
target = RUBY_PLATFORM if target.empty? || target == "vendor"
|
|
58
|
+
|
|
59
|
+
case target
|
|
60
|
+
when /darwin/
|
|
61
|
+
# Source: pkg-config --libs libcurl-impersonate on brew installation.
|
|
62
|
+
$LDFLAGS << " -framework CoreFoundation -framework SystemConfiguration"
|
|
63
|
+
$LDFLAGS << " -framework Security -framework LDAP"
|
|
64
|
+
# ruby/setup-ruby on macos-14 builds a Ruby whose libruby has an absolute
|
|
65
|
+
# install_name (e.g. /Users/runner/hostedtoolcache/Ruby/3.3.11/arm64/lib/
|
|
66
|
+
# libruby.3.3.dylib). Any extension we statically-link against that Ruby
|
|
67
|
+
# inherits the absolute reference, breaking the precompiled gem on every
|
|
68
|
+
# other machine. Tell the linker to leave Ruby symbols unresolved and let
|
|
69
|
+
# the dynamic loader fill them in at require time — this is the standard
|
|
70
|
+
# macOS pattern for portable Ruby extensions.
|
|
71
|
+
$DLDFLAGS << " -Wl,-undefined,dynamic_lookup"
|
|
72
|
+
$libs = "#{$libs} -lresolv -liconv -lz -lc++"
|
|
73
|
+
when /linux/
|
|
74
|
+
# The lexiforest release tarball is a fully-static archive — zstd, brotli,
|
|
75
|
+
# idn2, nghttp{2,3}, ngtcp2, BoringSSL, and a copy of curl itself are all
|
|
76
|
+
# statically linked into libcurl-impersonate.a. The only external symbols
|
|
77
|
+
# are the system C/C++ runtime, zlib (often satisfied by libz.so.1 which
|
|
78
|
+
# ships on every distro), and a handful of pthread/dl/m intrinsics.
|
|
79
|
+
$libs = "#{$libs} -lz -lpthread -ldl -lm -lstdc++"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def configure_from_pkg_config
|
|
84
|
+
unless pkg_config("libcurl-impersonate")
|
|
85
|
+
raise "pkg-config could not locate libcurl-impersonate"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# pkg_config sets $LIBS to "-lcurl-impersonate -lz ...". We want the static .a
|
|
89
|
+
# baked into the .bundle, so prepend the absolute path to the archive and strip
|
|
90
|
+
# the dynamic -lcurl-impersonate flag.
|
|
91
|
+
libdir = `pkg-config --variable=libdir libcurl-impersonate`.strip
|
|
92
|
+
static_archive = File.join(libdir, "libcurl-impersonate.a")
|
|
93
|
+
unless File.exist?(static_archive)
|
|
94
|
+
raise "libcurl-impersonate.a not found at #{static_archive}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
$libs = $libs.to_s.gsub(/-lcurl-impersonate\b/, "").strip
|
|
98
|
+
$LDFLAGS << " #{static_archive.shellescape}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Look for vendored libcurl-impersonate under any of these directory names.
|
|
102
|
+
# RUBY_PLATFORM on Darwin includes the OS major version ("arm64-darwin25") but
|
|
103
|
+
# the rake-compiler / rubygems platform triple does not ("arm64-darwin"), so
|
|
104
|
+
# we accept either. The order matters — most-specific wins.
|
|
105
|
+
vendor_candidates = [
|
|
106
|
+
RUBY_PLATFORM, # arm64-darwin25, x86_64-linux, ...
|
|
107
|
+
RUBY_PLATFORM.sub(/\d+\z/, ""), # arm64-darwin
|
|
108
|
+
RUBY_PLATFORM.sub(/-gnu\z/, ""), # x86_64-linux (from x86_64-linux-gnu)
|
|
109
|
+
].uniq.map { |t| File.join(__dir__, "vendor", t) }
|
|
110
|
+
|
|
111
|
+
vendor_arch_dir = vendor_candidates.find { |d| File.directory?(d) }
|
|
112
|
+
|
|
113
|
+
# Always make our vendored upstream curl 8.15.0 headers visible. brew installs
|
|
114
|
+
# only curl_impersonate.h on macOS (no full curl/ tree) and many Linux runners
|
|
115
|
+
# do not ship libcurl-dev. The .a we link against is the matching curl 8.15.0
|
|
116
|
+
# build, so using these headers is the correct pairing regardless of what is
|
|
117
|
+
# (or is not) in /usr/include/curl/.
|
|
118
|
+
bundled_headers = File.join(__dir__, "include")
|
|
119
|
+
$CFLAGS << " -I#{bundled_headers.shellescape}" if File.directory?(bundled_headers)
|
|
120
|
+
|
|
121
|
+
if (override = ENV["CURL_IMPERSONATE_DIR"]) && !override.empty?
|
|
122
|
+
warn "[curl_impersonate] using CURL_IMPERSONATE_DIR=#{override}"
|
|
123
|
+
configure_from_dir(override)
|
|
124
|
+
elsif vendor_arch_dir
|
|
125
|
+
warn "[curl_impersonate] using vendored libcurl-impersonate at #{vendor_arch_dir}"
|
|
126
|
+
configure_from_dir(vendor_arch_dir)
|
|
127
|
+
else
|
|
128
|
+
warn "[curl_impersonate] using pkg-config libcurl-impersonate"
|
|
129
|
+
configure_from_pkg_config
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Sanity check: ensure we can find the impersonate header. brew installs it
|
|
133
|
+
# under curl-impersonate/curl_impersonate.h; some custom layouts may place it
|
|
134
|
+
# elsewhere. If missing, fall back to declaring the prototype inline (handled
|
|
135
|
+
# in curl_impersonate.c).
|
|
136
|
+
have_header("curl-impersonate/curl_impersonate.h")
|
|
137
|
+
have_header("curl/curl.h") or abort("curl/curl.h not found — install curl development headers")
|
|
138
|
+
|
|
139
|
+
create_makefile("curl_impersonate/curl_impersonate")
|