Skip to content

Instantly share code, notes, and snippets.

@xeioex
Created October 9, 2025 00:32
Show Gist options
  • Save xeioex/b7c31c20b31d9aadc299926d43cace12 to your computer and use it in GitHub Desktop.
Save xeioex/b7c31c20b31d9aadc299926d43cace12 to your computer and use it in GitHub Desktop.
#!/usr/bin/perl
# (C) Dmitry Volyntsev
# (C) Nginx, Inc.
# Tests for http njs module, fetch method with HTTPS through forward proxy.
###############################################################################
use warnings;
use strict;
use Test::More;
use Socket qw/ CRLF SOCK_STREAM /;
use IO::Select;
BEGIN { use FindBin; chdir($FindBin::Bin); }
use lib 'lib';
use Test::Nginx;
###############################################################################
select STDERR; $| = 1;
select STDOUT; $| = 1;
my $t = Test::Nginx->new()->has(qw/http http_ssl/)
->write_file_expand('nginx.conf', <<'EOF');
%%TEST_GLOBALS%%
daemon off;
events {
}
http {
%%TEST_GLOBALS_HTTP%%
js_import test.js;
server {
listen 127.0.0.1:8080;
server_name localhost;
location /https_via_proxy {
js_fetch_proxy http://127.0.0.1:%%PORT_8082%%;
js_fetch_proxy_auth_basic testuser testpass;
js_content test.https_fetch;
}
location /https_no_proxy {
js_content test.https_fetch;
}
location /https_via_proxy_status {
js_fetch_proxy http://127.0.0.1:%%PORT_8082%%;
js_fetch_proxy_auth_basic testuser testpass;
js_content test.https_fetch_status;
}
}
server {
listen 127.0.0.1:%%PORT_8083%% ssl;
server_name localhost;
ssl_certificate localhost.crt;
ssl_certificate_key localhost.key;
location = /test {
return 200 "ORIGIN:HTTPS:response";
}
}
}
EOF
my $p2 = port(8082);
my $p3 = port(8083);
$t->write_file('test.js', <<EOF);
async function https_fetch(r) {
try {
let reply = await ngx.fetch('https://127.0.0.1:$p3/test',
{verify: false});
let body = await reply.text();
r.return(200, body);
} catch (e) {
r.return(500, e.message);
}
}
async function https_fetch_status(r) {
try {
let reply = await ngx.fetch('https://127.0.0.1:$p3/test',
{verify: false});
r.return(200, 'STATUS:' + reply.status);
} catch (e) {
r.return(500, e.message);
}
}
export default {https_fetch, https_fetch_status};
EOF
$t->write_file('openssl.conf', <<EOF);
[ req ]
default_bits = 2048
encrypt_key = no
distinguished_name = req_distinguished_name
[ req_distinguished_name ]
EOF
my $d = $t->testdir();
system('openssl req -x509 -new '
. "-config $d/openssl.conf -subj /CN=localhost/ "
. "-out $d/localhost.crt -keyout $d/localhost.key "
. ">>$d/openssl.out 2>&1") == 0
or die "Can't create certificate: $!\n";
$t->try_run('no njs.fetch')->plan(6);
$t->run_daemon(\&https_proxy_daemon, $p2, $p3);
$t->waitforsocket('127.0.0.1:' . $p2);
###############################################################################
my $resp = http_get('/https_via_proxy');
like($resp, qr/PROXY:method=CONNECT/, 'proxy received CONNECT method');
like($resp, qr/PROXY:uri=127\.0\.0\.1:$p3/,
'proxy received host:port in CONNECT');
like($resp, qr/ORIGIN:HTTPS:response/, 'origin HTTPS response through proxy');
$resp = http_get('/https_no_proxy');
like($resp, qr/ORIGIN:HTTPS:response/, 'origin HTTPS response without proxy');
$resp = http_get('/https_via_proxy_status');
like($resp, qr/STATUS:200/, 'HTTPS request status 200');
###############################################################################
sub https_proxy_daemon {
my ($port, $origin_port) = @_;
my $server = IO::Socket::INET->new(
Proto => 'tcp',
LocalAddr => "127.0.0.1:$port",
Listen => 5,
Reuse => 1
) or die "Can't create listening socket: $!\n";
local $SIG{PIPE} = 'IGNORE';
while (my $client = $server->accept()) {
$client->autoflush(1);
my $headers = '';
my $uri = '';
my $method = '';
my $proxy_auth = '';
my $first_line = 1;
while (<$client>) {
$headers .= $_;
last if (/^\x0d?\x0a?$/);
if ($first_line) {
$method = $1 if /^(\S+)\s+/;
$uri = $1 if /^\S+\s+(\S+)\s+HTTP/;
$first_line = 0;
}
$proxy_auth = $1 if /^Proxy-Authorization:\s*(.+?)\s*$/i;
}
if ($method eq 'CONNECT') {
if ($proxy_auth =~ /^Basic\s+dGVzdHVzZXI6dGVzdHBhc3M=$/i) {
print $client "HTTP/1.1 200 Connection established" . CRLF .
CRLF;
my $origin = IO::Socket::INET->new(
PeerAddr => "127.0.0.1:$origin_port",
Proto => 'tcp',
Type => SOCK_STREAM
) or do {
close $client;
next;
};
my $sel = IO::Select->new($client, $origin);
while (1) {
my @ready = $sel->can_read(3.0);
last unless @ready;
foreach my $sock (@ready) {
my $buf;
my $n = sysread($sock, $buf, 4096);
if (!defined($n) || $n == 0) {
$sel->remove($client, $origin);
close $origin;
close $client;
last;
}
if ($sock == $client) {
syswrite($origin, $buf);
} else {
syswrite($client, $buf);
}
}
}
} else {
print $client
"HTTP/1.1 407 Proxy Authentication Required" . CRLF .
"Proxy-Authenticate: Basic realm=\"proxy\"" . CRLF .
"Content-Length: 0" . CRLF .
"Connection: close" . CRLF . CRLF;
}
}
close $client;
}
}
###############################################################################
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment