Skip to content

Commit 302c50a

Browse files
Merge pull request #964 from lightpanda-io/proxy-header
http: refacto headerCallback and get proxy CONNECT request details
2 parents 7d51da1 + 7869159 commit 302c50a

File tree

4 files changed

+135
-62
lines changed

4 files changed

+135
-62
lines changed

.github/workflows/e2e-test.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,18 @@ jobs:
9696
go run runner/main.go --verbose
9797
kill `cat LPD.pid`
9898
99+
- name: build proxy
100+
run: |
101+
cd proxy
102+
go build
103+
104+
- name: run end to end tests through proxy
105+
run: |
106+
./proxy/proxy & echo $! > PROXY.id
107+
./lightpanda serve --http_proxy 'http://127.0.0.1:3000' & echo $! > LPD.pid
108+
go run runner/main.go --verbose
109+
kill `cat LPD.pid` `cat PROXY.id`
110+
99111
cdp-and-hyperfine-bench:
100112
name: cdp-and-hyperfine-bench
101113
needs: zig-build-release

src/browser/page.zig

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -756,7 +756,14 @@ pub const Page = struct {
756756
self.documentIsComplete();
757757
}
758758
},
759-
else => unreachable,
759+
.pre => {
760+
// we didn't get any data.
761+
self.documentIsComplete();
762+
},
763+
else => {
764+
log.err(.app, "unreachable mode", .{ .mode = self.mode });
765+
unreachable;
766+
},
760767
}
761768
}
762769

src/http/Client.zig

Lines changed: 115 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ fn requestFailed(self: *Client, transfer: *Transfer, err: anyerror) void {
278278
pub fn changeProxy(self: *Client, proxy: [:0]const u8) !void {
279279
try self.ensureNoActiveConnection();
280280

281-
for (self.handles.handles) |h| {
281+
for (self.handles.handles) |*h| {
282282
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy.ptr));
283283
}
284284
try errorCheck(c.curl_easy_setopt(self.blocking.conn.easy, c.CURLOPT_PROXY, proxy.ptr));
@@ -290,7 +290,7 @@ pub fn restoreOriginalProxy(self: *Client) !void {
290290
try self.ensureNoActiveConnection();
291291

292292
const proxy = if (self.http_proxy) |p| p.ptr else null;
293-
for (self.handles.handles) |h| {
293+
for (self.handles.handles) |*h| {
294294
try errorCheck(c.curl_easy_setopt(h.conn.easy, c.CURLOPT_PROXY, proxy));
295295
}
296296
try errorCheck(c.curl_easy_setopt(self.blocking.conn.easy, c.CURLOPT_PROXY, proxy));
@@ -359,7 +359,7 @@ fn perform(self: *Client, timeout_ms: c_int) !void {
359359
var messages_count: c_int = 0;
360360
while (c.curl_multi_info_read(multi, &messages_count)) |msg_| {
361361
const msg: *c.CURLMsg = @ptrCast(msg_);
362-
// This is the only possible mesage type from CURL for now.
362+
// This is the only possible message type from CURL for now.
363363
std.debug.assert(msg.msg == c.CURLMSG_DONE);
364364

365365
const easy = msg.easy_handle.?;
@@ -372,6 +372,15 @@ fn perform(self: *Client, timeout_ms: c_int) !void {
372372
defer transfer.deinit();
373373

374374
if (errorCheck(msg.data.result)) {
375+
// In case of request w/o data, we need to call the header done
376+
// callback now.
377+
if (!transfer._header_done_called) {
378+
transfer.headerDoneCallback(easy) catch |err| {
379+
log.err(.http, "header_done_callback", .{ .err = err });
380+
self.requestFailed(transfer, err);
381+
continue;
382+
};
383+
}
375384
transfer.req.done_callback(transfer.ctx) catch |err| {
376385
// transfer isn't valid at this point, don't use it.
377386
log.err(.http, "done_callback", .{ .err = err });
@@ -562,13 +571,18 @@ pub const Transfer = struct {
562571
bytes_received: usize = 0,
563572

564573
// We'll store the response header here
574+
proxy_response_header: ?ResponseHeader = null,
565575
response_header: ?ResponseHeader = null,
566576

577+
// track if the header callbacks done have been called.
578+
_header_done_called: bool = false,
579+
567580
_notified_fail: bool = false,
568581

569582
_handle: ?*Handle = null,
570583

571584
_redirecting: bool = false,
585+
_forbidden: bool = false,
572586

573587
fn deinit(self: *Transfer) void {
574588
self.req.headers.deinit();
@@ -579,17 +593,34 @@ pub const Transfer = struct {
579593
self.client.transfer_pool.destroy(self);
580594
}
581595

596+
fn buildResponseHeader(self: *Transfer, easy: *c.CURL) !void {
597+
std.debug.assert(self.response_header == null);
598+
599+
var url: [*c]u8 = undefined;
600+
try errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_EFFECTIVE_URL, &url));
601+
602+
var status: c_long = undefined;
603+
try errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_RESPONSE_CODE, &status));
604+
605+
self.response_header = .{
606+
.url = url,
607+
.status = @intCast(status),
608+
};
609+
610+
if (getResponseHeader(easy, "content-type", 0)) |ct| {
611+
var hdr = &self.response_header.?;
612+
const value = ct.value;
613+
const len = @min(value.len, ResponseHeader.MAX_CONTENT_TYPE_LEN);
614+
hdr._content_type_len = len;
615+
@memcpy(hdr._content_type[0..len], value[0..len]);
616+
}
617+
}
618+
582619
pub fn format(self: *const Transfer, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
583620
const req = self.req;
584621
return writer.print("{s} {s}", .{ @tagName(req.method), req.url });
585622
}
586623

587-
pub fn setBody(self: *Transfer, body: []const u8) !void {
588-
const easy = self.handle.easy;
589-
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDS, body.ptr));
590-
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDSIZE, @as(c_long, @intCast(body.len))));
591-
}
592-
593624
pub fn addHeader(self: *Transfer, value: [:0]const u8) !void {
594625
self._request_header_list = c.curl_slist_append(self._request_header_list, value);
595626
}
@@ -666,6 +697,48 @@ pub const Transfer = struct {
666697
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_COOKIE, @as([*c]const u8, @ptrCast(cookies.items.ptr))));
667698
}
668699

700+
// headerDoneCallback is called once the headers have been read.
701+
// It can be called either on dataCallback or once the request for those
702+
// w/o body.
703+
fn headerDoneCallback(transfer: *Transfer, easy: *c.CURL) !void {
704+
std.debug.assert(transfer._header_done_called == false);
705+
defer transfer._header_done_called = true;
706+
707+
try transfer.buildResponseHeader(easy);
708+
709+
if (getResponseHeader(easy, "content-type", 0)) |ct| {
710+
var hdr = &transfer.response_header.?;
711+
const value = ct.value;
712+
const len = @min(value.len, ResponseHeader.MAX_CONTENT_TYPE_LEN);
713+
hdr._content_type_len = len;
714+
@memcpy(hdr._content_type[0..len], value[0..len]);
715+
}
716+
717+
var i: usize = 0;
718+
while (true) {
719+
const ct = getResponseHeader(easy, "set-cookie", i);
720+
if (ct == null) break;
721+
transfer.req.cookie_jar.populateFromResponse(&transfer.uri, ct.?.value) catch |err| {
722+
log.err(.http, "set cookie", .{ .err = err, .req = transfer });
723+
return err;
724+
};
725+
i += 1;
726+
if (i >= ct.?.amount) break;
727+
}
728+
729+
transfer.req.header_callback(transfer) catch |err| {
730+
log.err(.http, "header_callback", .{ .err = err, .req = transfer });
731+
return err;
732+
};
733+
734+
if (transfer.client.notification) |notification| {
735+
notification.dispatch(.http_response_header_done, &.{
736+
.transfer = transfer,
737+
});
738+
}
739+
}
740+
741+
// headerCallback is called by curl on each request's header line read.
669742
fn headerCallback(buffer: [*]const u8, header_count: usize, buf_len: usize, data: *anyopaque) callconv(.c) usize {
670743
// libcurl should only ever emit 1 header at a time
671744
std.debug.assert(header_count == 1);
@@ -680,20 +753,13 @@ pub const Transfer = struct {
680753

681754
const header = buffer[0 .. buf_len - 2];
682755

683-
if (transfer.response_header == null) {
684-
if (transfer._redirecting and buf_len == 2) {
685-
// parse and set cookies for the redirection.
686-
redirectionCookies(transfer, easy) catch |err| {
687-
log.debug(.http, "redirection cookies", .{ .err = err });
688-
return 0;
689-
};
690-
return buf_len;
691-
}
692-
693-
if (buf_len < 13 or std.mem.startsWith(u8, header, "HTTP/") == false) {
694-
if (transfer._redirecting) {
695-
return buf_len;
696-
}
756+
// We need to parse the first line headers for each request b/c curl's
757+
// CURLINFO_RESPONSE_CODE returns the status code of the final request.
758+
// If a redirection or a proxy's CONNECT forbidden happens, we won't
759+
// get this intermediary status code.
760+
if (std.mem.startsWith(u8, header, "HTTP/")) {
761+
// Is it the first header line.
762+
if (buf_len < 13) {
697763
log.debug(.http, "invalid response line", .{ .line = header });
698764
return 0;
699765
}
@@ -715,53 +781,35 @@ pub const Transfer = struct {
715781
}
716782
transfer._redirecting = false;
717783

718-
var url: [*c]u8 = undefined;
719-
errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_EFFECTIVE_URL, &url)) catch |err| {
720-
log.err(.http, "failed to get URL", .{ .err = err });
721-
return 0;
722-
};
784+
if (status == 401 or status == 407) {
785+
transfer._forbidden = true;
786+
return buf_len;
787+
}
788+
transfer._forbidden = false;
723789

724-
transfer.response_header = .{
725-
.url = url,
726-
.status = status,
727-
};
728790
transfer.bytes_received = buf_len;
729791
return buf_len;
730792
}
731793

732-
transfer.bytes_received += buf_len;
733-
if (buf_len == 2) {
734-
if (getResponseHeader(easy, "content-type", 0)) |ct| {
735-
var hdr = &transfer.response_header.?;
736-
const value = ct.value;
737-
const len = @min(value.len, ResponseHeader.MAX_CONTENT_TYPE_LEN);
738-
hdr._content_type_len = len;
739-
@memcpy(hdr._content_type[0..len], value[0..len]);
740-
}
794+
if (transfer._redirecting == false and transfer._forbidden == false) {
795+
transfer.bytes_received += buf_len;
796+
}
741797

742-
var i: usize = 0;
743-
while (true) {
744-
const ct = getResponseHeader(easy, "set-cookie", i);
745-
if (ct == null) break;
746-
transfer.req.cookie_jar.populateFromResponse(&transfer.uri, ct.?.value) catch |err| {
747-
log.err(.http, "set cookie", .{ .err = err, .req = transfer });
748-
};
749-
i += 1;
750-
if (i >= ct.?.amount) break;
751-
}
798+
if (buf_len != 2) {
799+
return buf_len;
800+
}
801+
802+
// Starting here, we get the last header line.
752803

753-
transfer.req.header_callback(transfer) catch |err| {
754-
log.err(.http, "header_callback", .{ .err = err, .req = transfer });
755-
// returning < buf_len terminates the request
804+
if (transfer._redirecting) {
805+
// parse and set cookies for the redirection.
806+
redirectionCookies(transfer, easy) catch |err| {
807+
log.debug(.http, "redirection cookies", .{ .err = err });
756808
return 0;
757809
};
758-
759-
if (transfer.client.notification) |notification| {
760-
notification.dispatch(.http_response_header_done, &.{
761-
.transfer = transfer,
762-
});
763-
}
810+
return buf_len;
764811
}
812+
765813
return buf_len;
766814
}
767815

@@ -779,6 +827,13 @@ pub const Transfer = struct {
779827
return chunk_len;
780828
}
781829

830+
if (!transfer._header_done_called) {
831+
transfer.headerDoneCallback(easy) catch |err| {
832+
log.err(.http, "header_done_callback", .{ .err = err, .req = transfer });
833+
return c.CURL_WRITEFUNC_ERROR;
834+
};
835+
}
836+
782837
transfer.bytes_received += chunk_len;
783838
const chunk = buffer[0..chunk_len];
784839
transfer.req.data_callback(transfer, chunk) catch |err| {

src/http/Http.zig

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ pub const Connection = struct {
114114
// proxy
115115
if (opts.http_proxy) |proxy| {
116116
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY, proxy.ptr));
117-
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SUPPRESS_CONNECT_HEADERS, @as(c_long, 1)));
118117
}
119118

120119
// tls

0 commit comments

Comments
 (0)