diff options
author | Dmitry Volyntsev <xeioex@nginx.com> | 2023-05-22 17:59:47 -0700 |
---|---|---|
committer | Dmitry Volyntsev <xeioex@nginx.com> | 2023-05-22 17:59:47 -0700 |
commit | 7c39d2c23a2152f99f1602a0cb81903c6d15e980 (patch) | |
tree | 68292d76ac4b64a458ac6927909355b60b5bb4ee /nginx/t | |
parent | 3cf640f5a041bada0d39d32a27494ffe024767ef (diff) | |
download | njs-7c39d2c23a2152f99f1602a0cb81903c6d15e980.tar.gz njs-7c39d2c23a2152f99f1602a0cb81903c6d15e980.zip |
Tests: imported nginx modules tests from nginx-tests.
Diffstat (limited to 'nginx/t')
47 files changed, 9023 insertions, 0 deletions
diff --git a/nginx/t/README b/nginx/t/README new file mode 100644 index 00000000..ed1e6c59 --- /dev/null +++ b/nginx/t/README @@ -0,0 +1,12 @@ +Test suite for nginx JavaScript module. + +This test suite relies on nginx-tests repository for the test library. + +Use prove to run tests as one usually do for perl tests. Individual tests +may be run as well. + +Usage: + + $ TEST_NGINX_BINARY=/path/to/nginx prove -r -I /path/to/nginx-tests/lib/ nginx/t + +Refer to nginx-tests documentation for more details. diff --git a/nginx/t/js.t b/nginx/t/js.t new file mode 100644 index 00000000..5f2dcda8 --- /dev/null +++ b/nginx/t/js.t @@ -0,0 +1,391 @@ +#!/usr/bin/perl + +# (C) Roman Arutyunyan +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +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 rewrite/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_set $test_method test.method; + js_set $test_version test.version; + js_set $test_addr test.addr; + js_set $test_uri test.uri; + js_set $test_var test.variable; + js_set $test_type test.type; + js_set $test_global test.global_obj; + js_set $test_log test.log; + js_set $test_internal test.sub_internal; + js_set $test_except test.except; + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /njs { + js_content test.njs; + } + + location /method { + return 200 $test_method; + } + + location /version { + return 200 $test_version; + } + + location /addr { + return 200 $test_addr; + } + + location /uri { + return 200 $test_uri; + } + + location /var { + return 200 $test_var; + } + + location /global { + return 200 $test_global; + } + + location /body { + js_content test.request_body; + } + + location /in_file { + client_body_in_file_only on; + js_content test.request_body; + } + + location /status { + js_content test.status; + } + + location /request_body { + js_content test.request_body; + } + + location /request_body_cache { + js_content test.request_body_cache; + } + + location /send { + js_content test.send; + } + + location /return_method { + js_content test.return_method; + } + + location /type { + js_content test.type; + } + + location /log { + return 200 $test_log; + } + + location /internal { + js_content test.internal; + } + + location /sub_internal { + internal; + return 200 $test_internal; + } + + location /except { + return 200 $test_except; + } + + location /content_except { + js_content test.content_except; + } + + location /content_empty { + js_content test.content_empty; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + var global = ['n', 'j', 's'].join(""); + + function test_njs(r) { + r.return(200, njs.version); + } + + function method(r) { + return 'method=' + r.method; + } + + function version(r) { + return 'version=' + r.httpVersion; + } + + function addr(r) { + return 'addr=' + r.remoteAddress; + } + + function uri(r) { + return 'uri=' + r.uri; + } + + function variable(r) { + return 'variable=' + r.variables.remote_addr; + } + + function global_obj(r) { + return 'global=' + global; + } + + function status(r) { + r.status = 204; + r.sendHeader(); + r.finish(); + } + + function request_body(r) { + try { + var body = r.requestText; + r.return(200, body); + + } catch (e) { + r.return(500, e.message); + } + } + + function request_body_cache(r) { + function t(v) {return Buffer.isBuffer(v) ? 'buffer' : (typeof v);} + r.return(200, + `requestText:\${t(r.requestText)} requestBuffer:\${t(r.requestBuffer)}`); + } + + function send(r) { + var a, s; + r.status = 200; + r.sendHeader(); + for (a in r.args) { + if (a.substr(0, 3) == 'foo') { + s = r.args[a]; + r.send('n=' + a + ', v=' + s.substr(0, 2) + ' '); + } + } + r.finish(); + } + + function return_method(r) { + r.return(Number(r.args.c), r.args.t); + } + + function type(r) { + var p = r.args.path.split('.').reduce((a, v) => a[v], r); + + var typ = Buffer.isBuffer(p) ? 'buffer' : (typeof p); + r.return(200, `type: \${typ}`); + } + + function log(r) { + r.log('SEE-LOG'); + } + + async function internal(r) { + let reply = await r.subrequest('/sub_internal'); + + r.return(200, `parent: \${r.internal} sub: \${reply.responseText}`); + } + + function sub_internal(r) { + return r.internal; + } + + function except(r) { + var fs = require('fs'); + fs.readFileSync(); + } + + + function content_except(r) { + JSON.parse({}.a.a); + } + + function content_empty(r) { + } + + export default {njs:test_njs, method, version, addr, uri, + variable, global_obj, status, request_body, internal, + request_body_cache, send, return_method, sub_internal, + type, log, except, content_except, content_empty}; + +EOF + +$t->try_run('no njs available')->plan(27); + +############################################################################### + +like(http_get('/method'), qr/method=GET/, 'r.method'); +like(http_get('/version'), qr/version=1.0/, 'r.httpVersion'); +like(http_get('/addr'), qr/addr=127.0.0.1/, 'r.remoteAddress'); +like(http_get('/uri'), qr/uri=\/uri/, 'r.uri'); + +like(http_get('/status'), qr/204 No Content/, 'r.status'); + +like(http_post('/body'), qr/REQ-BODY/, 'request body'); +like(http_post('/in_file'), qr/request body is in a file/, + 'request body in file'); +like(http_post_big('/body'), qr/200.*^(1234567890){1024}$/ms, + 'request body big'); + +like(http_get('/send?foo=12345&n=11&foo-2=bar&ndd=&foo-3=z'), + qr/n=foo, v=12 n=foo-2, v=ba n=foo-3, v=z/, 'r.send'); + +like(http_get('/return_method?c=200'), qr/200 OK.*\x0d\x0a?\x0d\x0a?$/s, + 'return code'); +like(http_get('/return_method?c=200&t=SEE-THIS'), qr/200 OK.*^SEE-THIS$/ms, + 'return text'); +like(http_get('/return_method?c=301&t=path'), qr/ 301 .*Location: path/s, + 'return redirect'); +like(http_get('/return_method?c=404'), qr/404 Not.*html/s, 'return error page'); +like(http_get('/return_method?c=inv'), qr/ 500 /, 'return invalid'); + +like(http_get('/type?path=variables.host'), qr/200 OK.*type: string$/s, + 'variables type'); +like(http_get('/type?path=rawVariables.host'), qr/200 OK.*type: buffer$/s, + 'rawVariables type'); + +like(http_post('/type?path=requestText'), qr/200 OK.*type: string$/s, + 'requestText type'); +like(http_post('/type?path=requestBuffer'), qr/200 OK.*type: buffer$/s, + 'requestBuffer type'); +like(http_post('/request_body_cache'), + qr/requestText:string requestBuffer:buffer$/s, 'request body cache'); + +like(http_get('/var'), qr/variable=127.0.0.1/, 'r.variables'); +like(http_get('/global'), qr/global=njs/, 'global code'); +like(http_get('/log'), qr/200 OK/, 'r.log'); + +TODO: { +local $TODO = 'not yet' unless has_version('0.7.7'); + +like(http_get('/internal'), qr/parent: false sub: true/, 'r.internal'); + +} + +http_get('/except'); +http_get('/content_except'); + +like(http_get('/content_empty'), qr/500 Internal Server Error/, + 'empty handler'); + +$t->stop(); + +ok(index($t->read_file('error.log'), 'SEE-LOG') > 0, 'log js'); +ok(index($t->read_file('error.log'), 'at fs.readFileSync') > 0, + 'js_set backtrace'); +ok(index($t->read_file('error.log'), 'at JSON.parse') > 0, + 'js_content backtrace'); + +############################################################################### + +sub has_version { + my $need = shift; + + http_get('/njs') =~ /^([.0-9]+)$/m; + + my @v = split(/\./, $1); + my ($n, $v); + + for $n (split(/\./, $need)) { + $v = shift @v || 0; + return 0 if $n > $v; + return 1 if $v > $n; + } + + return 1; +} + +############################################################################### + +sub http_get_hdr { + my ($url, %extra) = @_; + return http(<<EOF, %extra); +GET $url HTTP/1.0 +FoO: 12345 + +EOF +} + +sub http_get_ihdr { + my ($url, %extra) = @_; + return http(<<EOF, %extra); +GET $url HTTP/1.0 +foo: 12345 +Host: localhost +foo2: bar +X-xxx: more +foo-3: z + +EOF +} + +sub http_post { + my ($url, %extra) = @_; + + my $p = "POST $url HTTP/1.0" . CRLF . + "Host: localhost" . CRLF . + "Content-Length: 8" . CRLF . + CRLF . + "REQ-BODY"; + + return http($p, %extra); +} + +sub http_post_big { + my ($url, %extra) = @_; + + my $p = "POST $url HTTP/1.0" . CRLF . + "Host: localhost" . CRLF . + "Content-Length: 10240" . CRLF . + CRLF . + ("1234567890" x 1024); + + return http($p, %extra); +} + +############################################################################### diff --git a/nginx/t/js_args.t b/nginx/t/js_args.t new file mode 100644 index 00000000..bb5cf4ff --- /dev/null +++ b/nginx/t/js_args.t @@ -0,0 +1,167 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, arguments tests. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +eval { require JSON::PP; }; +plan(skip_all => "JSON::PP not installed") if $@; + +my $t = Test::Nginx->new()->has(qw/http/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + js_set $test_iter test.iter; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /njs { + js_content test.njs; + } + + location /iter { + return 200 $test_iter; + } + + location /keys { + js_content test.keys; + } + + location /object { + js_content test.object; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function iter(r) { + var s = '', a; + for (a in r.args) { + if (a.substr(0, 3) == 'foo') { + s += r.args[a]; + } + } + + return s; + } + + function keys(r) { + r.return(200, Object.keys(r.args).sort()); + } + + function object(r) { + r.return(200, JSON.stringify(r.args)); + } + + export default {njs: test_njs, iter, keys, object}; + +EOF + +$t->try_run('no njs')->plan(15); + +############################################################################### + +sub recode { + my $json; + eval { $json = JSON::PP::decode_json(shift) }; + + if ($@) { + return "<failed to parse JSON>"; + } + + JSON::PP->new()->canonical()->encode($json); +} + +sub get_json { + http_get(shift) =~ /\x0d\x0a?\x0d\x0a?(.*)/ms; + recode($1); +} + +############################################################################### + +local $TODO = 'not yet' unless has_version('0.7.6'); + +like(http_get('/iter?foo=12345&foo2=bar&nn=22&foo-3=z'), qr/12345barz/, + 'r.args iteration'); +like(http_get('/iter?foo=123&foo2=&foo3&foo4=456'), qr/123456/, + 'r.args iteration 2'); +like(http_get('/iter?foo=123&foo2=&foo3'), qr/123/, 'r.args iteration 3'); +like(http_get('/iter?foo=123&foo2='), qr/123/, 'r.args iteration 4'); +like(http_get('/iter?foo=1&foo=2'), qr/1,2/m, 'r.args iteration 5'); + +like(http_get('/keys?b=1&c=2&a=5'), qr/a,b,c/m, 'r.args sorted keys'); +like(http_get('/keys?b=1&b=2'), qr/b/m, 'r.args duplicate keys'); +like(http_get('/keys?b=1&a&c='), qr/a,b,c/m, 'r.args empty value'); + +is(get_json('/object'), '{}', 'empty object'); +is(get_json('/object?a=1&b=2&c=3'), '{"a":"1","b":"2","c":"3"}', + 'ordinary object'); +is(get_json('/object?a=1&A=2'), '{"A":"2","a":"1"}', + 'case sensitive object'); +is(get_json('/object?a=1&A=2&a=3'), '{"A":"2","a":["1","3"]}', + 'duplicate keys object'); +is(get_json('/object?%61=1&a=2'), '{"a":["1","2"]}', + 'keys percent-encoded object'); +is(get_json('/object?a=%62%63&b=%63%64'), '{"a":"bc","b":"cd"}', + 'values percent-encoded object'); +is(get_json('/object?a=%6&b=%&c=%%&d=%zz'), + '{"a":"%6","b":"%","c":"%%","d":"%zz"}', + 'values percent-encoded broken object'); + +############################################################################### + +sub has_version { + my $need = shift; + + http_get('/njs') =~ /^([.0-9]+)$/m; + + my @v = split(/\./, $1); + my ($n, $v); + + for $n (split(/\./, $need)) { + $v = shift @v || 0; + return 0 if $n > $v; + return 1 if $v > $n; + } + + return 1; +} + +############################################################################### diff --git a/nginx/t/js_async.t b/nginx/t/js_async.t new file mode 100644 index 00000000..73e71851 --- /dev/null +++ b/nginx/t/js_async.t @@ -0,0 +1,225 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Async tests for http njs module. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 rewrite/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_set $test_async test.set_timeout; + js_set $context_var test.context_var; + js_set $test_set_rv_var test.set_rv_var; + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /njs { + js_content test.njs; + } + + location /async_var { + return 200 $test_async; + } + + location /shared_ctx { + add_header H $context_var; + js_content test.shared_ctx; + } + + location /set_timeout { + js_content test.set_timeout; + } + + location /set_timeout_many { + js_content test.set_timeout_many; + } + + location /set_timeout_data { + postpone_output 0; + js_content test.set_timeout_data; + } + + location /limit_rate { + postpone_output 0; + sendfile_max_chunk 5; + js_content test.limit_rate; + } + + location /async_content { + js_content test.async_content; + } + + location /set_rv_var { + return 200 $test_set_rv_var; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function set_timeout(r) { + var timerId = setTimeout(timeout_cb_r, 5, r, 0); + clearTimeout(timerId); + setTimeout(timeout_cb_r, 5, r, 0) + } + + function set_timeout_data(r) { + setTimeout(timeout_cb_data, 5, r, 0); + } + + function set_timeout_many(r) { + for (var i = 0; i < 5; i++) { + setTimeout(timeout_cb_empty, 5, r, i); + } + + setTimeout(timeout_cb_reply, 10, r); + } + + function timeout_cb_r(r, cnt) { + if (cnt == 10) { + r.status = 200; + r.headersOut['Content-Type'] = 'foo'; + r.sendHeader(); + r.finish(); + + } else { + setTimeout(timeout_cb_r, 5, r, ++cnt); + } + } + + function timeout_cb_empty(r, arg) { + r.log("timeout_cb_empty" + arg); + } + + function timeout_cb_reply(r) { + r.status = 200; + r.headersOut['Content-Type'] = 'reply'; + r.sendHeader(); + r.finish(); + } + + function timeout_cb_data(r, counter) { + if (counter == 0) { + r.log("timeout_cb_data: init"); + r.status = 200; + r.sendHeader(); + setTimeout(timeout_cb_data, 5, r, ++counter); + + } else if (counter == 10) { + r.log("timeout_cb_data: finish"); + r.finish(); + + } else { + r.send("" + counter); + setTimeout(timeout_cb_data, 5, r, ++counter); + } + } + + var js_; + function context_var() { + return js_; + } + + function shared_ctx(r) { + js_ = r.variables.arg_a; + + r.status = 200; + r.sendHeader(); + r.finish(); + } + + function limit_rate_cb(r) { + r.finish(); + } + + function limit_rate(r) { + r.status = 200; + r.sendHeader(); + r.send("AAAAA".repeat(10)) + setTimeout(limit_rate_cb, 1000, r); + } + + function pr(x) { + return new Promise(resolve => {resolve(x)}).then(v => v).then(v => v); + } + + async function async_content(r) { + const a1 = await pr('A'); + const a2 = await pr('B'); + + r.return(200, `retval: \${a1 + a2}`); + } + + async function set_rv_var(r) { + const a1 = await pr(10); + const a2 = await pr(20); + + r.setReturnValue(`retval: \${a1 + a2}`); + } + + export default {njs:test_njs, set_timeout, set_timeout_data, + set_timeout_many, context_var, shared_ctx, limit_rate, + async_content, set_rv_var}; + +EOF + +$t->try_run('no njs available')->plan(9); + +############################################################################### + +like(http_get('/set_timeout'), qr/Content-Type: foo/, 'setTimeout'); +like(http_get('/set_timeout_many'), qr/Content-Type: reply/, 'setTimeout many'); +like(http_get('/set_timeout_data'), qr/123456789/, 'setTimeout data'); +like(http_get('/shared_ctx?a=xxx'), qr/H: xxx/, 'shared context'); +like(http_get('/limit_rate'), qr/A{50}/, 'limit_rate'); + +like(http_get('/async_content'), qr/retval: AB/, 'async content'); +like(http_get('/set_rv_var'), qr/retval: 30/, 'set return value variable'); + +http_get('/async_var'); + +$t->stop(); + +ok(index($t->read_file('error.log'), 'pending events') > 0, + 'pending js events'); +ok(index($t->read_file('error.log'), 'async operation inside') > 0, + 'async op in var handler'); + +############################################################################### diff --git a/nginx/t/js_body_filter.t b/nginx/t/js_body_filter.t new file mode 100644 index 00000000..b90a2502 --- /dev/null +++ b/nginx/t/js_body_filter.t @@ -0,0 +1,168 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, body filter. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 proxy/) + ->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 /njs { + js_content test.njs; + } + + location /append { + js_body_filter test.append; + proxy_pass http://127.0.0.1:8081/source; + } + + location /buffer_type { + js_body_filter test.buffer_type buffer_type=buffer; + proxy_pass http://127.0.0.1:8081/source; + } + + location /forward { + js_body_filter test.forward buffer_type=string; + proxy_pass http://127.0.0.1:8081/source; + } + + location /filter { + proxy_buffering off; + js_body_filter test.filter; + proxy_pass http://127.0.0.1:8081/source; + } + + location /prepend { + js_body_filter test.prepend; + proxy_pass http://127.0.0.1:8081/source; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + location /source { + postpone_output 1; + js_content test.source; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function append(r, data, flags) { + r.sendBuffer(data, {last:false}); + + if (flags.last) { + r.sendBuffer("XXX", flags); + } + } + + var collect = Buffer.from([]); + function buffer_type(r, data, flags) { + collect = Buffer.concat([collect, data]); + + if (flags.last) { + r.sendBuffer(collect, flags); + } + } + + function chain(chunks, i) { + if (i < chunks.length) { + chunks.r.send(chunks[i++]); + setTimeout(chunks.chain, chunks.delay, chunks, i); + + } else { + chunks.r.finish(); + } + } + + function source(r) { + var chunks = ['AAA', 'BB', 'C', 'DDDD']; + chunks.delay = 5; + chunks.r = r; + chunks.chain = chain; + + r.status = 200; + r.sendHeader(); + chain(chunks, 0); + } + + function filter(r, data, flags) { + if (flags.last || data.length >= Number(r.args.len)) { + r.sendBuffer(`\${data}|`, flags); + + if (r.args.dup && !flags.last) { + r.sendBuffer(data, flags); + } + } + } + + function forward(r, data, flags) { + r.sendBuffer(data, flags); + } + + function prepend(r, data, flags) { + r.sendBuffer("XXX"); + r.sendBuffer(data, flags); + r.done(); + } + + export default {njs: test_njs, append, buffer_type, filter, forward, + prepend, source}; + +EOF + +$t->try_run('no njs body filter')->plan(6); + +############################################################################### + +like(http_get('/append'), qr/AAABBCDDDDXXX/, 'append'); +like(http_get('/buffer_type'), qr/AAABBCDDDD/, 'buffer type'); +like(http_get('/forward'), qr/AAABBCDDDD/, 'forward'); +like(http_get('/filter?len=3'), qr/AAA|DDDD|/, 'filter 3'); +like(http_get('/filter?len=2&dup=1'), qr/AAA|AAABB|BBDDDD|DDDD/, + 'filter 2 dup'); +like(http_get('/prepend'), qr/XXXAAABBCDDDD/, 'prepend'); + +############################################################################### diff --git a/nginx/t/js_body_filter_if.t b/nginx/t/js_body_filter_if.t new file mode 100644 index 00000000..af0aa865 --- /dev/null +++ b/nginx/t/js_body_filter_if.t @@ -0,0 +1,127 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, body filter, if context. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 proxy rewrite/) + ->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 /njs { + js_content test.njs; + } + + location /filter { + if ($arg_name ~ "prepend") { + js_body_filter test.prepend; + } + + if ($arg_name ~ "append") { + js_body_filter test.append; + } + + js_body_filter test.should_not_be_called; + + proxy_pass http://127.0.0.1:8081/source; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + location /source { + postpone_output 1; + js_content test.source; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function append(r, data, flags) { + r.sendBuffer(data, {last:false}); + + if (flags.last) { + r.sendBuffer("XXX", flags); + } + } + + function chain(chunks, i) { + if (i < chunks.length) { + chunks.r.send(chunks[i++]); + setTimeout(chunks.chain, chunks.delay, chunks, i); + + } else { + chunks.r.finish(); + } + } + + function source(r) { + var chunks = ['AAA', 'BB', 'C', 'DDDD']; + chunks.delay = 5; + chunks.r = r; + chunks.chain = chain; + + r.status = 200; + r.sendHeader(); + chain(chunks, 0); + } + + function prepend(r, data, flags) { + r.sendBuffer("XXX"); + r.sendBuffer(data, flags); + r.done(); + } + + export default {njs: test_njs, append, prepend, source}; + +EOF + +$t->try_run('no njs body filter')->plan(2); + +############################################################################### + +like(http_get('/filter?name=append'), qr/AAABBCDDDDXXX/, 'append'); +like(http_get('/filter?name=prepend'), qr/XXXAAABBCDDDD/, 'prepend'); + +############################################################################### diff --git a/nginx/t/js_buffer.t b/nginx/t/js_buffer.t new file mode 100644 index 00000000..ce4c15d0 --- /dev/null +++ b/nginx/t/js_buffer.t @@ -0,0 +1,184 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, buffer properties. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +eval { require JSON::PP; }; +plan(skip_all => "JSON::PP not installed") if $@; + +my $t = Test::Nginx->new()->has(qw/http rewrite proxy/) + ->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 /njs { + js_content test.njs; + } + + location /return { + js_content test.return; + } + + location /req_body { + js_content test.req_body; + } + + location /res_body { + js_content test.res_body; + } + + location /res_text { + js_content test.res_text; + } + + location /binary_var { + js_content test.binary_var; + } + + location /p/ { + proxy_pass http://127.0.0.1:8081/; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + location /sub1 { + return 200 '{"a": {"b": 1}}'; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function test_return(r) { + var body = Buffer.from("body: "); + body = Buffer.concat([body, Buffer.from(r.args.text)]); + r.return(200, body); + } + + function req_body(r) { + var body = r.requestBuffer; + var view = new DataView(body.buffer); + view.setInt8(2, 'c'.charCodeAt(0)); + r.return(200, JSON.parse(body).c.b); + } + + function type(v) {return Buffer.isBuffer(v) ? 'buffer' : (typeof v);} + + function res_body(r) { + r.subrequest('/p/sub1') + .then(reply => { + var body = reply.responseBuffer; + var view = new DataView(body.buffer); + view.setInt8(2, 'c'.charCodeAt(0)); + body = JSON.parse(body); + body.type = type(reply.responseBuffer); + r.return(200, JSON.stringify(body)); + }) + } + + function res_text(r) { + r.subrequest('/p/sub1') + .then(reply => { + var body = JSON.parse(reply.responseText); + body.type = type(reply.responseText); + r.return(200, JSON.stringify(body)); + }) + } + + function binary_var(r) { + var test = r.rawVariables.binary_remote_addr + .equals(Buffer.from([127,0,0,1])); + r.return(200, test); + } + + export default {njs: test_njs, return: test_return, req_body, res_body, + res_text, binary_var}; + +EOF + +$t->try_run('no njs buffer')->plan(5); + +############################################################################### + +like(http_get('/return?text=FOO'), qr/200 OK.*body: FOO$/s, + 'return buffer'); +like(http_post('/req_body'), qr/200 OK.*BAR$/s, 'request buffer'); +is(get_json('/res_body'), '{"c":{"b":1},"type":"buffer"}', 'response buffer'); +is(get_json('/res_text'), '{"a":{"b":1},"type":"string"}', 'response text'); +like(http_get('/binary_var'), qr/200 OK.*true$/s, + 'binary var'); + +############################################################################### + +sub recode { + my $json; + eval { $json = JSON::PP::decode_json(shift) }; + + if ($@) { + return "<failed to parse JSON>"; + } + + JSON::PP->new()->canonical()->encode($json); +} + +sub get_json { + http_get(shift) =~ /\x0d\x0a?\x0d\x0a?(.*)/ms; + recode($1); +} + +sub http_post { + my ($url, %extra) = @_; + + my $p = "POST $url HTTP/1.0" . CRLF . + "Host: localhost" . CRLF . + "Content-Length: 17" . CRLF . + CRLF . + "{\"a\":{\"b\":\"BAR\"}}"; + + return http($p, %extra); +} + +############################################################################### diff --git a/nginx/t/js_dump.t b/nginx/t/js_dump.t new file mode 100644 index 00000000..c00a53a2 --- /dev/null +++ b/nginx/t/js_dump.t @@ -0,0 +1,110 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, request object dump. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +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 rewrite/) + ->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 /dump { + js_content test.dump; + } + + location /stringify { + js_content test.stringify; + } + + location /stringify_subrequest { + js_content test.stringify_subrequest; + } + + location /js_sub { + return 201 '{$request_method}'; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function dump(r) { + r.headersOut.baz = 'bar'; + r.return(200, njs.dump(r)); + } + + function stringify(r) { + r.headersOut.baz = 'bar'; + var obj = JSON.parse(JSON.stringify(r)); + r.return(200, JSON.stringify(obj)); + } + + function stringify_subrequest(r) { + r.subrequest('/js_sub', reply => { + r.return(200, JSON.stringify(reply)) + }); + } + + export default {dump, stringify, stringify_subrequest}; + +EOF + +$t->try_run('no njs dump')->plan(3); + +############################################################################### + +like(http( + 'GET /dump?v=1&t=x HTTP/1.0' . CRLF + . 'Foo: bar' . CRLF + . 'Foo2: bar2' . CRLF + . 'Host: localhost' . CRLF . CRLF +), qr/method:'GET'/, 'njs.dump(r)'); + +like(http( + 'GET /stringify?v=1&t=x HTTP/1.0' . CRLF + . 'Foo: bar' . CRLF + . 'Foo2: bar2' . CRLF + . 'Host: localhost' . CRLF . CRLF +), qr/headersOut":\{"baz":"bar"}/, 'JSON.stringify(r)'); + +like(http( + 'GET /stringify_subrequest HTTP/1.0' . CRLF + . 'Host: localhost' . CRLF . CRLF +), qr/"status":201/, 'JSON.stringify(reply)'); + +############################################################################### diff --git a/nginx/t/js_fetch.t b/nginx/t/js_fetch.t new file mode 100644 index 00000000..428be90f --- /dev/null +++ b/nginx/t/js_fetch.t @@ -0,0 +1,710 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, fetch method. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +eval { require JSON::PP; }; +plan(skip_all => "JSON::PP not installed") if $@; + +my $t = Test::Nginx->new()->has(qw/http/) + ->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 /njs { + js_content test.njs; + } + + location /broken { + js_content test.broken; + } + + location /broken_response { + js_content test.broken_response; + } + + location /body { + js_content test.body; + } + + location /body_special { + js_content test.body_special; + } + + location /chain { + js_content test.chain; + } + + location /chunked_ok { + js_content test.chunked_ok; + } + + location /chunked_fail { + js_content test.chunked_fail; + } + + location /header { + js_content test.header; + } + + location /host_header { + js_content test.host_header; + } + + location /header_iter { + js_content test.header_iter; + } + + location /multi { + js_content test.multi; + } + + location /property { + js_content test.property; + } + + location /loc { + js_content test.loc; + } + + location /json { } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + location /loc { + js_content test.loc; + } + + location /host { + return 200 $http_host; + } + } +} + +EOF + +my $p0 = port(8080); +my $p1 = port(8081); +my $p2 = port(8082); + +$t->write_file('json', '{"a":[1,2], "b":{"c":"FIELD"}}'); + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function body(r) { + var loc = r.args.loc; + var getter = r.args.getter; + + function query(obj) { + var path = r.args.path; + var retval = (getter == 'arrayBuffer') ? Buffer.from(obj).toString() + : obj; + + if (path) { + retval = path.split('.').reduce((a, v) => a[v], obj); + } + + return JSON.stringify(retval); + } + + ngx.fetch(`http://127.0.0.1:$p0/\${loc}`) + .then(reply => reply[getter]()) + .then(data => r.return(200, query(data))) + .catch(e => r.return(501, e.message)) + } + + function property(r) { + var opts = {headers:{}}; + + if (r.args.code) { + opts.headers.code = r.args.code; + } + + var p = ngx.fetch('http://127.0.0.1:$p0/loc', opts) + + if (r.args.readBody) { + p = p.then(rep => + rep.text().then(body => {rep.text = body; return rep;})) + } + + p.then(reply => r.return(200, reply[r.args.pr])) + .catch(e => r.return(501, e.message)) + } + + function process_errors(r, tests) { + var results = []; + + tests.forEach(args => { + ngx.fetch.apply(r, args) + .then(reply => { + r.return(400, '["unexpected then"]'); + }) + .catch(e => { + results.push(e.message); + + if (results.length == tests.length) { + results.sort(); + r.return(200, JSON.stringify(results)); + } + }) + }) + } + + function broken(r) { + var tests = [ + ['http://127.0.0.1:1/loc'], + ['http://127.0.0.1:80800/loc'], + [Symbol.toStringTag], + ]; + + return process_errors(r, tests); + } + + function broken_response(r) { + var tests = [ + ['http://127.0.0.1:$p2/status_line'], + ['http://127.0.0.1:$p2/length'], + ['http://127.0.0.1:$p2/header'], + ['http://127.0.0.1:$p2/headers'], + ['http://127.0.0.1:$p2/content_length'], + ]; + + return process_errors(r, tests); + } + + function chain(r) { + var results = []; + var reqs = [ + ['http://127.0.0.1:$p0/loc'], + ['http://127.0.0.1:$p1/loc'], + ]; + + function next(reply) { + if (reqs.length == 0) { + r.return(200, "SUCCESS"); + return; + } + + ngx.fetch.apply(r, reqs.pop()) + .then(next) + .catch(e => r.return(400, e.message)) + } + + next(); + } + + function chunked_ok(r) { + var results = []; + var tests = [ + ['http://127.0.0.1:$p2/big/ok', {max_response_body_size:128000}], + ['http://127.0.0.1:$p2/chunked/ok'], + ['http://127.0.0.1:$p2/chunked/big'], + ]; + + function collect(v) { + results.push(v); + + if (results.length == tests.length) { + r.return(200); + } + } + + tests.forEach(args => { + ngx.fetch.apply(r, args) + .then(reply => reply.text()) + .then(body => collect(body.length)) + }) + } + + function chunked_fail(r) { + var results = []; + var tests = [ + ['http://127.0.0.1:$p2/big', {max_response_body_size:128000}], + ['http://127.0.0.1:$p2/chunked'], + ['http://127.0.0.1:$p2/chunked/big', {max_response_body_size:128}], + ]; + + function collect(v) { + results.push(v); + + if (results.length == tests.length) { + r.return(200); + } + } + + tests.forEach(args => { + ngx.fetch.apply(r, args) + .then(reply => reply.text()) + .catch(e => collect(e.message)) + }) + } + + function header(r) { + var url = `http://127.0.0.1:$p2/\${r.args.loc}`; + var method = r.args.method ? r.args.method : 'get'; + + var p = ngx.fetch(url) + + if (r.args.readBody) { + p = p.then(rep => + rep.text().then(body => {rep.text = body; return rep;})) + } + + p.then(reply => { + var h = reply.headers[method](r.args.h); + r.return(200, njs.dump(h)); + }) + .catch(e => r.return(501, e.message)) + } + + async function host_header(r) { + const reply = await ngx.fetch(`http://127.0.0.1:$p1/host`, + {headers: {Host: r.args.host}}); + const body = await reply.text(); + r.return(200, body); + } + + async function body_special(r) { + let opts = {}; + + if (r.args.method) { + opts.method = r.args.method; + } + + let reply = await ngx.fetch(`http://127.0.0.1:$p2/\${r.args.loc}`, + opts); + let body = await reply.text(); + + r.return(200, body != '' ? body : '<empty>'); + } + + async function header_iter(r) { + let url = `http://127.0.0.1:$p2/\${r.args.loc}`; + + let response = await ngx.fetch(url); + + let headers = response.headers; + let out = []; + for (let key in response.headers) { + if (key != 'Connection') { + out.push(`\${key}:\${headers.get(key)}`); + } + } + + r.return(200, njs.dump(out)); + } + + function multi(r) { + var results = []; + var tests = [ + [ + 'http://127.0.0.1:$p0/loc', + { headers: {Code: 201}}, + ], + [ + 'http://127.0.0.1:$p0/loc', + { method:'POST', headers: {Code: 401}, body: 'OK'}, + ], + [ + 'http://127.0.0.1:$p1/loc', + { method:'PATCH', + headers: {bar:'xxx'}}, + ], + ]; + + function cmp(a,b) { + if (a.b > b.b) {return 1;} + if (a.b < b.b) {return -1;} + return 0 + } + + tests.forEach(args => { + ngx.fetch.apply(r, args) + .then(rep => + rep.text().then(body => {rep.text = body; return rep;})) + .then(rep => { + results.push({b:rep.text, + c:rep.status, + u:rep.url}); + + if (results.length == tests.length) { + results.sort(cmp); + r.return(200, JSON.stringify(results)); + } + }) + .catch(e => { + r.return(400, `["\${e.message}"]`); + throw e; + }) + }) + + if (r.args.throw) { + throw 'Oops'; + } + } + + function str(v) { return v ? v : ''}; + + function loc(r) { + var v = r.variables; + var body = str(r.requestText); + var bar = str(r.headersIn.bar); + var c = r.headersIn.code ? Number(r.headersIn.code) : 200; + r.return(c, `\${v.request_method}:\${bar}:\${body}`); + } + + export default {njs: test_njs, body, broken, broken_response, body_special, + chain, chunked_ok, chunked_fail, header, header_iter, + host_header, multi, loc, property}; +EOF + +$t->try_run('no njs.fetch')->plan(35); + +$t->run_daemon(\&http_daemon, port(8082)); +$t->waitforsocket('127.0.0.1:' . port(8082)); + +############################################################################### + +like(http_get('/body?getter=arrayBuffer&loc=loc'), qr/200 OK.*"GET::"$/s, + 'fetch body arrayBuffer'); +like(http_get('/body?getter=text&loc=loc'), qr/200 OK.*"GET::"$/s, + 'fetch body text'); +like(http_get('/body?getter=json&loc=json&path=b.c'), + qr/200 OK.*"FIELD"$/s, 'fetch body json'); +like(http_get('/body?getter=json&loc=loc'), qr/501/s, + 'fetch body json invalid'); +like(http_get('/body_special?loc=parted'), qr/200 OK.*X{32000}$/s, + 'fetch body parted'); +like(http_get('/property?pr=bodyUsed'), qr/false$/s, + 'fetch bodyUsed false'); +like(http_get('/property?pr=bodyUsed&readBody=1'), qr/true$/s, + 'fetch bodyUsed true'); +like(http_get('/property?pr=ok'), qr/200 OK.*true$/s, + 'fetch ok true'); +like(http_get('/property?pr=ok&code=401'), qr/200 OK.*false$/s, + 'fetch ok false'); +like(http_get('/property?pr=redirected'), qr/200 OK.*false$/s, + 'fetch redirected false'); +like(http_get('/property?pr=statusText'), qr/200 OK.*OK$/s, + 'fetch statusText OK'); +like(http_get('/property?pr=statusText&code=403'), qr/200 OK.*Forbidden$/s, + 'fetch statusText Forbidden'); +like(http_get('/property?pr=type'), qr/200 OK.*basic$/s, + 'fetch type'); +like(http_get('/header?loc=duplicate_header&h=BAR'), qr/200 OK.*c$/s, + 'fetch header'); +like(http_get('/header?loc=duplicate_header&h=BARR'), qr/200 OK.*null$/s, + 'fetch no header'); +like(http_get('/header?loc=duplicate_header&h=foo'), qr/200 OK.*a, ?b$/s, + 'fetch header duplicate'); +like(http_get('/header?loc=duplicate_header&h=BAR&method=getAll'), + qr/200 OK.*\['c']$/s, 'fetch getAll header'); +like(http_get('/header?loc=duplicate_header&h=BARR&method=getAll'), + qr/200 OK.*\[]$/s, 'fetch getAll no header'); +like(http_get('/header?loc=duplicate_header&h=FOO&method=getAll'), + qr/200 OK.*\['a','b']$/s, 'fetch getAll duplicate'); +like(http_get('/header?loc=duplicate_header&h=bar&method=has'), + qr/200 OK.*true$/s, 'fetch header has'); +like(http_get('/header?loc=duplicate_header&h=buz&method=has'), + qr/200 OK.*false$/s, 'fetch header does not have'); +like(http_get('/header?loc=chunked/big&h=BAR&readBody=1'), qr/200 OK.*xxx$/s, + 'fetch chunked header'); +is(get_json('/multi'), + '[{"b":"GET::","c":201,"u":"http://127.0.0.1:'.$p0.'/loc"},' . + '{"b":"PATCH:xxx:","c":200,"u":"http://127.0.0.1:'.$p1.'/loc"},' . + '{"b":"POST::OK","c":401,"u":"http://127.0.0.1:'.$p0.'/loc"}]', + 'fetch multi'); +like(http_get('/multi?throw=1'), qr/500/s, 'fetch destructor'); +like(http_get('/broken'), qr/200/s, 'fetch broken'); +like(http_get('/broken_response'), qr/200/s, 'fetch broken response'); +like(http_get('/chunked_ok'), qr/200/s, 'fetch chunked ok'); +like(http_get('/chunked_fail'), qr/200/s, 'fetch chunked fail'); +like(http_get('/chain'), qr/200 OK.*SUCCESS$/s, 'fetch chain'); + +like(http_get('/header_iter?loc=duplicate_header_large'), + qr/\['A:a','B:a','C:a','D:a','E:a','F:a','G:a','H:a','Moo:a, ?b']$/s, + 'fetch header duplicate large'); + +TODO: { +local $TODO = 'not yet' unless has_version('0.7.7'); + +like(http_get('/body_special?loc=no_content_length'), + qr/200 OK.*CONTENT-BODY$/s, 'fetch body without content-length'); +like(http_get('/body_special?loc=no_content_length/parted'), + qr/200 OK.*X{32000}$/s, 'fetch body without content-length parted'); + +} + +TODO: { +local $TODO = 'not yet' unless has_version('0.7.8'); + +like(http_get('/body_special?loc=head&method=HEAD'), + qr/200 OK.*<empty>$/s, 'fetch head method'); +like(http_get('/body_special?loc=length&method=head'), + qr/200 OK.*<empty>$/s, 'fetch head method lower case'); + +} + +TODO: { +local $TODO = 'not yet' unless has_version('0.8.0'); + +like(http_get('/host_header?host=FOOBAR'), qr/200 OK.*FOOBAR$/s, + 'fetch host header'); +} + +############################################################################### + +sub has_version { + my $need = shift; + + http_get('/njs') =~ /^([.0-9]+)$/m; + + my @v = split(/\./, $1); + my ($n, $v); + + for $n (split(/\./, $need)) { + $v = shift @v || 0; + return 0 if $n > $v; + return 1 if $v > $n; + } + + return 1; +} + +############################################################################### + +sub recode { + my $json; + eval { $json = JSON::PP::decode_json(shift) }; + + if ($@) { + return "<failed to parse JSON>"; + } + + JSON::PP->new()->canonical()->encode($json); +} + +sub get_json { + http_get(shift) =~ /\x0d\x0a?\x0d\x0a?(.*)/ms; + recode($1); +} + +############################################################################### + +sub http_daemon { + my $port = shift; + + 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 = ''; + + while (<$client>) { + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + + $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; + + if ($uri eq '/status_line') { + print $client + "HTTP/1.1 2A"; + + } elsif ($uri eq '/content_length') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Content-Length: " . CRLF . + "Connection: close" . CRLF . + CRLF; + + } elsif ($uri eq '/header') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "@#" . CRLF . + "Connection: close" . CRLF . + CRLF; + + } elsif ($uri eq '/duplicate_header') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Foo: a" . CRLF . + "bar: c" . CRLF . + "Foo: b" . CRLF . + "Connection: close" . CRLF . + CRLF; + + } elsif ($uri eq '/duplicate_header_large') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "A: a" . CRLF . + "B: a" . CRLF . + "C: a" . CRLF . + "D: a" . CRLF . + "E: a" . CRLF . + "F: a" . CRLF . + "G: a" . CRLF . + "H: a" . CRLF . + "Moo: a" . CRLF . + "Moo: b" . CRLF . + "Connection: close" . CRLF . + CRLF; + + } elsif ($uri eq '/headers') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Connection: close" . CRLF; + + } elsif ($uri eq '/length') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Content-Length: 100" . CRLF . + "Connection: close" . CRLF . + CRLF . + "unfinished" . CRLF; + + } elsif ($uri eq '/head') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Content-Length: 100" . CRLF . + "Connection: close" . CRLF . + CRLF; + + } elsif ($uri eq '/parted') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Content-Length: 32000" . CRLF . + "Connection: close" . CRLF . + CRLF; + + for (1 .. 4) { + select undef, undef, undef, 0.01; + print $client "X" x 8000; + } + + } elsif ($uri eq '/no_content_length') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Connection: close" . CRLF . + CRLF . + "CONTENT-BODY"; + + } elsif ($uri eq '/no_content_length/parted') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Connection: close" . CRLF . + CRLF; + + for (1 .. 4) { + select undef, undef, undef, 0.01; + print $client "X" x 8000; + } + + } elsif ($uri eq '/big') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Content-Length: 100100" . CRLF . + "Connection: close" . CRLF . + CRLF; + for (1 .. 1000) { + print $client ("X" x 98) . CRLF; + } + print $client "unfinished" . CRLF; + + } elsif ($uri eq '/big/ok') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Content-Length: 100010" . CRLF . + "Connection: close" . CRLF . + CRLF; + for (1 .. 1000) { + print $client ("X" x 98) . CRLF; + } + print $client "finished" . CRLF; + + } elsif ($uri eq '/chunked') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Transfer-Encoding: chunked" . CRLF . + "Connection: close" . CRLF . + CRLF . + "ff" . CRLF . + "unfinished" . CRLF; + + } elsif ($uri eq '/chunked/ok') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Transfer-Encoding: chunked" . CRLF . + "Connection: close" . CRLF . + CRLF . + "a" . CRLF . + "finished" . CRLF . + CRLF . "0" . CRLF . CRLF; + } elsif ($uri eq '/chunked/big') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Transfer-Encoding: chunked" . CRLF . + "Bar: xxx" . CRLF . + "Connection: close" . CRLF . + CRLF; + + for (1 .. 100) { + print $client "ff" . CRLF . ("X" x 255) . CRLF; + } + + print $client "0" . CRLF . CRLF; + } + } +} + +############################################################################### diff --git a/nginx/t/js_fetch_https.t b/nginx/t/js_fetch_https.t new file mode 100644 index 00000000..154bbbcc --- /dev/null +++ b/nginx/t/js_fetch_https.t @@ -0,0 +1,283 @@ +#!/usr/bin/perl + +# (C) Antoine Bonavita +# (C) Nginx, Inc. + +# Tests for http njs module, fetch method, https support. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 rewrite/) + ->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; + + resolver 127.0.0.1:%%PORT_8981_UDP%%; + resolver_timeout 1s; + + location /njs { + js_content test.njs; + } + + location /https { + js_content test.https; + } + + location /https.myca { + js_content test.https; + + js_fetch_ciphers HIGH:!aNull:!MD5; + js_fetch_protocols TLSv1.1 TLSv1.2; + js_fetch_trusted_certificate myca.crt; + } + + location /https.myca.short { + js_content test.https; + + js_fetch_verify_depth 0; + js_fetch_trusted_certificate myca.crt; + } + } + + server { + listen 127.0.0.1:8081 ssl default; + server_name default.example.com; + + ssl_certificate default.example.com.chained.crt; + ssl_certificate_key default.example.com.key; + + location /loc { + return 200 "You are at default.example.com."; + } + } + + server { + listen 127.0.0.1:8081 ssl; + server_name 1.example.com; + + ssl_certificate 1.example.com.chained.crt; + ssl_certificate_key 1.example.com.key; + + location /loc { + return 200 "You are at 1.example.com."; + } + } +} + +EOF + +my $p1 = port(8081); + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function https(r) { + var url = `https://\${r.args.domain}:$p1/loc`; + var opt = {}; + + if (r.args.verify != null && r.args.verify == "false") { + opt.verify = false; + } + + ngx.fetch(url, opt) + .then(reply => reply.text()) + .then(body => r.return(200, body)) + .catch(e => r.return(501, e.message)) + } + + export default {njs: test_njs, https}; +EOF + +my $d = $t->testdir(); + +$t->write_file('openssl.conf', <<EOF); +[ req ] +default_bits = 2048 +encrypt_key = no +distinguished_name = req_distinguished_name +[ req_distinguished_name ] +EOF + +$t->write_file('myca.conf', <<EOF); +[ ca ] +default_ca = myca + +[ myca ] +new_certs_dir = $d +database = $d/certindex +default_md = sha256 +policy = myca_policy +serial = $d/certserial +default_days = 1 +x509_extensions = myca_extensions + +[ myca_policy ] +commonName = supplied + +[ myca_extensions ] +basicConstraints = critical,CA:TRUE +EOF + +system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=myca/ " + . "-out $d/myca.crt -keyout $d/myca.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create self-signed certificate for CA: $!\n"; + +foreach my $name ('intermediate', 'default.example.com', '1.example.com') { + system("openssl req -new " + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.csr -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate signing req for $name: $!\n"; +} + +$t->write_file('certserial', '1000'); +$t->write_file('certindex', ''); + +system("openssl ca -batch -config $d/myca.conf " + . "-keyfile $d/myca.key -cert $d/myca.crt " + . "-subj /CN=intermediate/ -in $d/intermediate.csr " + . "-out $d/intermediate.crt " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't sign certificate for intermediate: $!\n"; + +foreach my $name ('default.example.com', '1.example.com') { + system("openssl ca -batch -config $d/myca.conf " + . "-keyfile $d/intermediate.key -cert $d/intermediate.crt " + . "-subj /CN=$name/ -in $d/$name.csr -out $d/$name.crt " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't sign certificate for $name $!\n"; + $t->write_file("$name.chained.crt", $t->read_file("$name.crt") + . $t->read_file('intermediate.crt')); +} + +$t->try_run('no njs.fetch')->plan(7); + +$t->run_daemon(\&dns_daemon, port(8981), $t); +$t->waitforfile($t->testdir . '/' . port(8981)); + +############################################################################### + +like(http_get('/https?domain=default.example.com&verify=false'), + qr/You are at default.example.com.$/s, 'fetch https'); +like(http_get('/https?domain=127.0.0.1&verify=false'), + qr/You are at default.example.com.$/s, 'fetch https by IP'); +like(http_get('/https?domain=1.example.com&verify=false'), + qr/You are at 1.example.com.$/s, 'fetch tls extension'); +like(http_get('/https.myca?domain=default.example.com'), + qr/You are at default.example.com.$/s, 'fetch https trusted certificate'); +like(http_get('/https.myca?domain=localhost'), + qr/connect failed/s, 'fetch https wrong CN certificate'); +like(http_get('/https?domain=default.example.com'), + qr/connect failed/s, 'fetch https non trusted CA'); +like(http_get('/https.myca.short?domain=default.example.com'), + qr/connect failed/s, 'fetch https CA too far'); + +############################################################################### + +sub reply_handler { + my ($recv_data, $port, %extra) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant A => 1; + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 3600); + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + + my $name = join('.', @name); + + if ($type == A) { + push @rdata, rd_addr($ttl, '127.0.0.1'); + } + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + return pack 'n3N', 0xc00c, A, IN, $ttl if $addr eq ''; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub dns_daemon { + my ($port, $t) = @_; + + my ($data, $recv_data); + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => $port, + Proto => 'udp', + ) + or die "Can't create listening socket: $!\n"; + + local $SIG{PIPE} = 'IGNORE'; + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . $port; + close $fh; + + while (1) { + $socket->recv($recv_data, 65536); + $data = reply_handler($recv_data, $port); + $socket->send($data); + } +} + +############################################################################### diff --git a/nginx/t/js_fetch_objects.t b/nginx/t/js_fetch_objects.t new file mode 100644 index 00000000..9f23599c --- /dev/null +++ b/nginx/t/js_fetch_objects.t @@ -0,0 +1,500 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, fetch objects. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 rewrite/) + ->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 /njs { + js_content test.njs; + } + + location /headers { + js_content test.headers; + } + + location /request { + js_content test.request; + } + + location /response { + js_content test.response; + } + + location /fetch { + js_content test.fetch; + } + + location /method { + return 200 $request_method; + } + + location /header { + return 200 $http_a; + } + + location /body { + js_content test.body; + } + } +} + +EOF + +my $p0 = port(8080); + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function header(r) { + r.return(200, r.headersIn.a); + } + + function body(r) { + r.return(201, r.requestText); + } + + async function run(r, tests) { + var fails = []; + for (var i = 0; i < tests.length; i++) { + var v, t = tests[i]; + + try { + v = await t[1](); + + } catch (e) { + v = e.message; + } + + if (v != t[2]) { + fails.push(`\${t[0]}: got "\${v}" expected: "\${t[2]}"\n`); + } + } + + r.return(fails.length ? 400 : 200, fails); + } + + async function headers(r) { + const tests = [ + ['empty', () => { + var h = new Headers(); + return h.get('a'); + }, null], + ['normal', () => { + var h = new Headers({a: 'X', b: 'Z'}); + return `\${h.get('a')} \${h.get('B')}`; + }, 'X Z'], + ['trim value', () => { + var h = new Headers({a: ' X '}); + return h.get('a'); + }, 'X'], + ['invalid header name', () => { + const valid = "!#\$\%&'*+-.^_`|~0123456789"; + + for (var i = 0; i < 128; i++) { + var c = String.fromCodePoint(i); + + if (valid.indexOf(c) != -1 || /[a-zA-Z]+/.test(c)) { + continue; + } + + try { + new Headers([[c, 'a']]); + throw new Error( + `header with "\${c}" (\${i}) should throw`); + + } catch (e) { + if (e.message != 'invalid header name') { + throw e; + } + } + } + + return 'OK'; + + }, 'OK'], + ['invalid header value', () => { + var h = new Headers({A: 'aa\x00a'}); + }, 'invalid header value'], + ['combine', () => { + var h = new Headers({a: 'X', A: 'Z'}); + return h.get('a'); + }, 'X, Z'], + ['combine2', () => { + var h = new Headers([['A', 'x'], ['a', 'z']]); + return h.get('a'); + }, 'x, z'], + ['combine3', () => { + var h = new Headers(); + h.append('a', 'A'); + h.append('a', 'B'); + h.append('a', 'C'); + h.append('a', 'D'); + h.append('a', 'E'); + h.append('a', 'F'); + return h.get('a'); + }, 'A, B, C, D, E, F'], + ['getAll', () => { + var h = new Headers({a: 'X', A: 'Z'}); + return njs.dump(h.getAll('a')); + }, "['X','Z']"], + ['inherit', () => { + var h = new Headers({a: 'X', b: 'Y'}); + var h2 = new Headers(h); + h2.append('c', 'Z'); + return h2.has('a') && h2.has('B') && h2.has('c'); + }, true], + ['delete', () => { + var h = new Headers({a: 'X', b: 'Z'}); + h.delete('b'); + return h.get('a') && !h.get('b'); + }, true], + ['forEach', () => { + var r = []; + var h = new Headers({a: '0', b: '1', c: '2'}); + h.delete('b'); + h.append('z', '3'); + h.append('a', '4'); + h.append('q', '5'); + h.forEach((v, k) => { r.push(`\${v}:\${k}`)}) + return r.join('|'); + }, 'a:0, 4|c:2|q:5|z:3'], + ['set', () => { + var h = new Headers([['A', 'x'], ['a', 'y'], ['a', 'z']]); + h.set('a', '#'); + return h.get('a'); + }, '#'], + ]; + + run(r, tests); + } + + async function request(r) { + const tests = [ + ['empty', () => { + try { + new Request(); + throw new Error(`Request() should throw`); + + } catch (e) { + if (e.message != '1st argument is required') { + throw e; + } + } + + return 'OK'; + + }, 'OK'], + ['normal', () => { + var r = new Request("http://nginx.org", + {headers: {a: 'X', b: 'Y'}}); + return `\${r.url}: \${r.method} \${r.headers.a}`; + }, 'http://nginx.org: GET X'], + ['url trim', () => { + var r = new Request("\\x00\\x01\\x02\\x03\\x05\\x06\\x07\\x08" + + "\\x09\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f" + + "\\x10\\x11\\x12\\x13\\x14\\x15\\x16" + + "\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d" + + "\\x1e\\x1f\\x20http://nginx.org\\x00" + + "\\x01\\x02\\x03\\x05\\x06\\x07\\x08" + + "\\x09\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f" + + "\\x10\\x11\\x12\\x13\\x14\\x15\\x16" + + "\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d" + + "\\x1e\\x1f\\x20"); + return r.url; + }, 'http://nginx.org'], + ['read only', () => { + var r = new Request("http://nginx.org"); + + const props = ['bodyUsed', 'cache', 'credentials', 'headers', + 'method', 'mode', 'url']; + try { + props.forEach(prop => { + r[prop] = 1; + throw new Error( + `setting read-only \${prop} should throw`); + }) + + } catch (e) { + if (!e.message.startsWith('Cannot assign to read-only p')) { + throw e; + } + } + + return 'OK'; + + }, 'OK'], + ['cache', () => { + const props = ['default', 'no-cache', 'no-store', 'reload', + 'force-cache', 'only-if-cached', '#']; + try { + props.forEach(cv => { + var r = new Request("http://nginx.org", {cache: cv}); + if (r.cache != cv) { + throw new Error(`r.cache != \${cv}`); + } + }) + + } catch (e) { + if (!e.message.startsWith('unknown cache type: #')) { + throw e; + } + } + + return 'OK'; + + }, 'OK'], + ['credentials', () => { + const props = ['omit', 'include', 'same-origin', '#']; + try { + props.forEach(cr => { + var r = new Request("http://nginx.org", + {credentials: cr}); + if (r.credentials != cr) { + throw new Error(`r.credentials != \${cr}`); + } + }) + + } catch (e) { + if (!e.message.startsWith('unknown credentials type: #')) { + throw e; + } + } + + return 'OK'; + + }, 'OK'], + ['method', () => { + const methods = ['get', 'hEad', 'Post', 'OPTIONS', 'PUT', + 'DELETE', 'CONNECT']; + try { + methods.forEach(m => { + var r = new Request("http://nginx.org", {method: m}); + if (r.method != m.toUpperCase()) { + throw new Error(`r.method != \${m}`); + } + }) + + } catch (e) { + if (!e.message.startsWith('forbidden method: CONNECT')) { + throw e; + } + } + + return 'OK'; + + }, 'OK'], + ['mode', () => { + const props = ['same-origin', 'cors', 'no-cors', 'navigate', + 'websocket', '#']; + try { + props.forEach(m => { + var r = new Request("http://nginx.org", {mode: m}); + if (r.mode != m) { + throw new Error(`r.mode != \${m}`); + } + }) + + } catch (e) { + if (!e.message.startsWith('unknown mode type: #')) { + throw e; + } + } + + return 'OK'; + + }, 'OK'], + ['inherit', () => { + var r = new Request("http://nginx.org", + {headers: {a: 'X', b: 'Y'}}); + var r2 = new Request(r); + r2.headers.append('a', 'Z') + return `\${r2.url}: \${r2.headers.get('a')}`; + }, 'http://nginx.org: X, Z'], + ['inherit2', () => { + var r = new Request("http://nginx.org", + {headers: {a: 'X', b: 'Y'}}); + var r2 = new Request(r); + r2.headers.append('a', 'Z') + return `\${r.url}: \${r.headers.get('a')}`; + }, 'http://nginx.org: X'], + ['inherit3', () => { + var h = new Headers(); + h.append('a', 'X'); + h.append('a', 'Z'); + var r = new Request("http://nginx.org", {headers: h}); + return `\${r.url}: \${r.headers.get('a')}`; + }, 'http://nginx.org: X, Z'], + ['content type', async () => { + var r = new Request("http://nginx.org", + {body: 'ABC', method: 'POST'}); + var body = await r.text(); + return `\${body}: \${r.headers.get('Content-Type')}`; + }, 'ABC: text/plain;charset=UTF-8'], + ['GET body', () => { + try { + var r = new Request("http://nginx.org", {body: 'ABC'}); + + } catch (e) { + if (!e.message.startsWith('Request body incompatible w')) { + throw e; + } + } + + return 'OK'; + + }, 'OK'], + ]; + + run(r, tests); + } + + async function response(r) { + const tests = [ + ['empty', async () => { + var r = new Response(); + var body = await r.text(); + return `\${r.url}: \${r.status} \${body} \${r.headers.get('a')}`; + }, ': 200 null'], + ['normal', async () => { + var r = new Response("ABC", {headers: {a: 'X', b: 'Y'}}); + var body = await r.text(); + return `\${r.url}: \${r.status} \${body} \${r.headers.get('a')}`; + }, ': 200 ABC X'], + ['headers', async () => { + var r = new Response(null, + {headers: new Headers({a: 'X', b: 'Y'})}); + var body = await r.text(); + return `\${r.url}: \${body} \${r.headers.get('b')}`; + }, ': Y'], + ['json', async () => { + var r = new Response('{"a": {"b": 42}}'); + var json = await r.json(); + return json.a.b; + }, 42], + ['statusText', () => { + const statuses = ['status text', 'aa\\u0000a']; + try { + statuses.forEach(s => { + var r = new Response(null, {statusText: s}); + if (r.statusText != s) { + throw new Error(`r.statusText != \${s}`); + } + }) + + } catch (e) { + if (!e.message.startsWith('invalid Response statusText')) { + throw e; + } + } + + return 'OK'; + + }, 'OK'], + ]; + + run(r, tests); + } + + async function fetch(r) { + const tests = [ + ['method', async () => { + var req = new Request("http://127.0.0.1:$p0/method", + {method: 'PUT'}); + var r = await ngx.fetch(req); + var body = await r.text(); + return `\${r.url}: \${r.status} \${body} \${r.headers.get('a')}`; + }, 'http://127.0.0.1:$p0/method: 200 PUT null'], + ['request body', async () => { + var req = new Request("http://127.0.0.1:$p0/body", + {body: 'foo'}); + var r = await ngx.fetch(req); + var body = await r.text(); + return `\${r.url}: \${r.status} \${body}`; + }, 'http://127.0.0.1:$p0/body: 201 foo'], + ['request body', async () => { + var h = new Headers({a: 'X'}); + h.append('a', 'Z'); + var req = new Request("http://127.0.0.1:$p0/header", + {headers: h}); + var r = await ngx.fetch(req); + var body = await r.text(); + return `\${r.url}: \${r.status} \${body}`; + }, 'http://127.0.0.1:$p0/header: 200 X, Z'], + ]; + + run(r, tests); + } + + export default {njs: test_njs, body, headers, request, response, fetch}; +EOF + +$t->try_run('no njs')->plan(4); + +############################################################################### + +local $TODO = 'not yet' unless has_version('0.7.10'); + +like(http_get('/headers'), qr/200 OK/s, 'headers tests'); +like(http_get('/request'), qr/200 OK/s, 'request tests'); +like(http_get('/response'), qr/200 OK/s, 'response tests'); +like(http_get('/fetch'), qr/200 OK/s, 'fetch tests'); + +############################################################################### + +sub has_version { + my $need = shift; + + http_get('/njs') =~ /^([.0-9]+)$/m; + + my @v = split(/\./, $1); + my ($n, $v); + + for $n (split(/\./, $need)) { + $v = shift @v || 0; + return 0 if $n > $v; + return 1 if $v > $n; + } + + return 1; +} + +############################################################################### diff --git a/nginx/t/js_fetch_resolver.t b/nginx/t/js_fetch_resolver.t new file mode 100644 index 00000000..8b0dc450 --- /dev/null +++ b/nginx/t/js_fetch_resolver.t @@ -0,0 +1,231 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, fetch method, dns support. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use IO::Select; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +plan(skip_all => '127.0.0.2 local address required') + unless defined IO::Socket::INET->new( LocalAddr => '127.0.0.2' ); + +my $t = Test::Nginx->new()->has(qw/http/) + ->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 /njs { + js_content test.njs; + } + + location /dns { + js_content test.dns; + + resolver 127.0.0.1:%%PORT_8981_UDP%%; + resolver_timeout 1s; + } + } + + server { + listen 127.0.0.1:8080; + server_name aaa; + + location /loc { + js_content test.loc; + } + } + + server { + listen 127.0.0.1:8080; + server_name many; + + location /loc { + js_content test.loc; + } + } +} + +EOF + +my $p0 = port(8080); + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function dns(r) { + var url = `http://\${r.args.domain}:$p0/loc`; + + ngx.fetch(url) + .then(reply => reply.text()) + .then(body => r.return(200, body)) + .catch(e => r.return(501, e.message)) + } + + function str(v) { return v ? v : ''}; + + function loc(r) { + var v = r.variables; + var body = str(r.requestText); + var foo = str(r.headersIn.foo); + var bar = str(r.headersIn.bar); + var c = r.headersIn.code ? Number(r.headersIn.code) : 200; + r.return(c, `\${v.host}:\${v.request_method}:\${foo}:\${bar}:\${body}`); + } + + export default {njs: test_njs, dns, loc}; +EOF + +$t->try_run('no njs.fetch')->plan(3); + +$t->run_daemon(\&dns_daemon, port(8981), $t); +$t->waitforfile($t->testdir . '/' . port(8981)); + +############################################################################### + +like(http_get('/dns?domain=aaa'), qr/aaa:GET:::$/s, 'fetch dns aaa'); +like(http_get('/dns?domain=many'), qr/many:GET:::$/s, 'fetch dns many'); +like(http_get('/dns?domain=unknown'), qr/"unknown" could not be resolved/s, + 'fetch dns unknown'); + +############################################################################### + +sub reply_handler { + my ($recv_data, $port, %extra) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant FORMERR => 1; + use constant SERVFAIL => 2; + use constant NXDOMAIN => 3; + + use constant A => 1; + + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 3600); + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + + my $name = join('.', @name); + + if ($name eq 'aaa' && $type == A) { + push @rdata, rd_addr($ttl, '127.0.0.1'); + + } elsif ($name eq 'many' && $type == A) { + push @rdata, rd_addr($ttl, '127.0.0.2'); + push @rdata, rd_addr($ttl, '127.0.0.1'); + } + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + return pack 'n3N', 0xc00c, A, IN, $ttl if $addr eq ''; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub dns_daemon { + my ($port, $t, %extra) = @_; + + my ($data, $recv_data); + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => $port, + Proto => 'udp', + ) + or die "Can't create listening socket: $!\n"; + + my $sel = IO::Select->new($socket); + + local $SIG{PIPE} = 'IGNORE'; + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . $port; + close $fh; + + while (my @ready = $sel->can_read) { + foreach my $fh (@ready) { + if ($socket == $fh) { + $fh->recv($recv_data, 65536); + $data = reply_handler($recv_data, $port); + $fh->send($data); + + } else { + $fh->recv($recv_data, 65536); + unless (length $recv_data) { + $sel->remove($fh); + $fh->close; + next; + } + +again: + my $len = unpack("n", $recv_data); + $data = substr $recv_data, 2, $len; + $data = reply_handler($data, $port, tcp => 1); + $data = pack("n", length $data) . $data; + $fh->send($data); + $recv_data = substr $recv_data, 2 + $len; + goto again if length $recv_data; + } + } + } +} + +############################################################################### diff --git a/nginx/t/js_fetch_timeout.t b/nginx/t/js_fetch_timeout.t new file mode 100644 index 00000000..486656d6 --- /dev/null +++ b/nginx/t/js_fetch_timeout.t @@ -0,0 +1,119 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, fetch method timeout. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ CRLF /; + +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/) + ->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 /njs { + js_content test.njs; + } + + location /normal_timeout { + js_content test.timeout_test; + } + + location /short_timeout { + js_fetch_timeout 200ms; + js_content test.timeout_test; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + location /normal_reply { + js_content test.normal_reply; + } + + location /delayed_reply { + js_content test.delayed_reply; + } + } +} + +EOF + +my $p1 = port(8081); + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + async function timeout_test(r) { + let rs = await Promise.allSettled([ + 'http://127.0.0.1:$p1/normal_reply', + 'http://127.0.0.1:$p1/delayed_reply', + ].map(v => ngx.fetch(v))); + + let bs = rs.map(v => ({s: v.status, v: v.value ? v.value.headers.X + : v.reason})); + + r.return(200, njs.dump(bs)); + } + + function normal_reply(r) { + r.headersOut.X = 'N'; + r.return(200); + } + + function delayed_reply(r) { + r.headersOut.X = 'D'; + setTimeout((r) => { r.return(200); }, 250, r, 0); + } + + export default {njs: test_njs, timeout_test, normal_reply, delayed_reply}; +EOF + +$t->try_run('no js_fetch_timeout')->plan(2); + +############################################################################### + +like(http_get('/normal_timeout'), + qr/\[\{s:'fulfilled',v:'N'},\{s:'fulfilled',v:'D'}]$/s, + 'normal timeout'); +like(http_get('/short_timeout'), + qr/\[\{s:'fulfilled',v:'N'},\{s:'rejected',v:Error: read timed out}]$/s, + 'short timeout'); + +############################################################################### diff --git a/nginx/t/js_fetch_verify.t b/nginx/t/js_fetch_verify.t new file mode 100644 index 00000000..d6bb1d9e --- /dev/null +++ b/nginx/t/js_fetch_verify.t @@ -0,0 +1,192 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Tests for http njs module, fetch method, backend certificate verification. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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; + + resolver 127.0.0.1:%%PORT_8981_UDP%%; + resolver_timeout 1s; + + location /njs { + js_content test.njs; + } + + location /https { + js_content test.https; + } + + location /https.verify_off { + js_content test.https; + js_fetch_verify off; + } + } + + server { + listen 127.0.0.1:8081 ssl; + server_name localhost; + + ssl_certificate localhost.crt; + ssl_certificate_key localhost.key; + } +} + +EOF + +my $p1 = port(8081); + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function https(r) { + ngx.fetch(`https://example.com:$p1/loc`) + .then(reply => reply.text()) + .then(body => r.return(200, body)) + .catch(e => r.return(501, e.message)); + } + + export default {njs: test_njs, https}; +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(); + +foreach my $name ('localhost') { + system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.crt -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate for $name: $!\n"; +} + +$t->try_run('no js_fetch_verify')->plan(2); + +$t->run_daemon(\&dns_daemon, port(8981), $t); +$t->waitforfile($t->testdir . '/' . port(8981)); + +############################################################################### + +like(http_get('/https'), qr/connect failed/, 'fetch verify error'); +like(http_get('/https.verify_off'), qr/200 OK/, 'fetch verify off'); + +############################################################################### + +sub reply_handler { + my ($recv_data, $port, %extra) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant A => 1; + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 3600); + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + + my $name = join('.', @name); + + if ($type == A) { + push @rdata, rd_addr($ttl, '127.0.0.1'); + } + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + return pack 'n3N', 0xc00c, A, IN, $ttl if $addr eq ''; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub dns_daemon { + my ($port, $t) = @_; + + my ($data, $recv_data); + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => $port, + Proto => 'udp', + ) + or die "Can't create listening socket: $!\n"; + + local $SIG{PIPE} = 'IGNORE'; + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . $port; + close $fh; + + while (1) { + $socket->recv($recv_data, 65536); + $data = reply_handler($recv_data, $port); + $socket->send($data); + } +} + +############################################################################### diff --git a/nginx/t/js_header_filter.t b/nginx/t/js_header_filter.t new file mode 100644 index 00000000..aecac34d --- /dev/null +++ b/nginx/t/js_header_filter.t @@ -0,0 +1,93 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, header filter. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 proxy rewrite/) + ->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 /njs { + js_content test.njs; + } + + location /filter/ { + js_header_filter test.filter; + proxy_pass http://127.0.0.1:8081/; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + location / { + add_header Set-Cookie "BB"; + add_header Set-Cookie "CCCC"; + + return 200; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function filter(r) { + var cookies = r.headersOut['Set-Cookie']; + var len = r.args.len ? Number(r.args.len) : 0; + r.headersOut['Set-Cookie'] = cookies.filter(v=>v.length > len); + } + + export default {njs: test_njs, filter}; + +EOF + +$t->try_run('no njs header filter')->plan(2); + +############################################################################### + +like(http_get('/filter/?len=1'), qr/Set-Cookie: BB.*Set-Cookie: CCCC.*/ms, + 'all');; +unlike(http_get('/filter/?len=3'), qr/Set-Cookie: BB/, + 'filter'); + +############################################################################### diff --git a/nginx/t/js_header_filter_if.t b/nginx/t/js_header_filter_if.t new file mode 100644 index 00000000..a6ab3c42 --- /dev/null +++ b/nginx/t/js_header_filter_if.t @@ -0,0 +1,95 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, header filter, if context. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 proxy rewrite/) + ->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 /njs { + js_content test.njs; + } + + location / { + if ($arg_name ~ "add") { + js_header_filter test.add; + } + + js_header_filter test.add2; + + proxy_pass http://127.0.0.1:8081/; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + location / { + return 200; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function add(r) { + r.headersOut['Foo'] = 'bar'; + } + + function add2(r) { + r.headersOut['Bar'] = 'xxx'; + } + + export default {njs: test_njs, add, add2}; + +EOF + +$t->try_run('no njs header filter')->plan(2); + +############################################################################### + +like(http_get('/?name=add'), qr/Foo: bar/, 'header filter if'); +like(http_get('/'), qr/Bar: xxx/, 'header filter'); + +############################################################################### diff --git a/nginx/t/js_headers.t b/nginx/t/js_headers.t new file mode 100644 index 00000000..6f08dcff --- /dev/null +++ b/nginx/t/js_headers.t @@ -0,0 +1,568 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, working with headers. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ CRLF /; + +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 charset/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_set $test_foo_in test.foo_in; + js_set $test_ifoo_in test.ifoo_in; + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /njs { + js_content test.njs; + } + + location /content_length { + js_content test.content_length; + } + + location /content_length_arr { + js_content test.content_length_arr; + } + + location /content_length_keys { + js_content test.content_length_keys; + } + + location /content_type { + charset windows-1251; + + default_type text/plain; + js_content test.content_type; + } + + location /content_type_arr { + charset windows-1251; + + default_type text/plain; + js_content test.content_type_arr; + } + + location /content_encoding { + js_content test.content_encoding; + } + + location /content_encoding_arr { + js_content test.content_encoding_arr; + } + + location /headers_list { + js_content test.headers_list; + } + + location /foo_in { + return 200 $test_foo_in; + } + + location /ifoo_in { + return 200 $test_ifoo_in; + } + + location /hdr_in { + js_content test.hdr_in; + } + + location /raw_hdr_in { + js_content test.raw_hdr_in; + } + + location /hdr_out { + js_content test.hdr_out; + } + + location /raw_hdr_out { + js_content test.raw_hdr_out; + } + + location /hdr_out_array { + js_content test.hdr_out_array; + } + + location /hdr_out_set_cookie { + js_content test.hdr_out_set_cookie; + } + + location /hdr_out_single { + js_content test.hdr_out_single; + } + + location /ihdr_out { + js_content test.ihdr_out; + } + + location /hdr_sorted_keys { + js_content test.hdr_sorted_keys; + } + + location /hdr_out_special_set { + js_content test.hdr_out_special_set; + } + + location /copy_subrequest_hdrs { + js_content test.copy_subrequest_hdrs; + } + + location = /subrequest { + internal; + + js_content test.subrequest; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function content_length(r) { + if (njs.version_number >= 0x000705) { + var clength = r.headersOut['Content-Length']; + if (clength !== undefined) { + r.return(500, `Content-Length "\${clength}" is not empty`); + return; + } + } + + delete r.headersOut['Content-Length']; + r.headersOut['Content-Length'] = ''; + r.headersOut['Content-Length'] = 3; + delete r.headersOut['Content-Length']; + r.headersOut['Content-Length'] = 3; + r.sendHeader(); + r.send('XXX'); + r.finish(); + } + + function content_length_arr(r) { + r.headersOut['Content-Length'] = [5]; + r.headersOut['Content-Length'] = []; + r.headersOut['Content-Length'] = [4,3]; + r.sendHeader(); + r.send('XXX'); + r.finish(); + } + + function content_length_keys(r) { + r.headersOut['Content-Length'] = 3; + var in_keys = Object.keys(r.headersOut).some(v=>v=='Content-Length'); + r.return(200, `B:\${in_keys}`); + } + + function content_type(r) { + if (njs.version_number >= 0x000705) { + var ctype = r.headersOut['Content-Type']; + if (ctype !== undefined) { + r.return(500, `Content-Type "\${ctype}" is not empty`); + return; + } + } + + delete r.headersOut['Content-Type']; + r.headersOut['Content-Type'] = 'text/xml'; + r.headersOut['Content-Type'] = ''; + r.headersOut['Content-Type'] = 'text/xml; charset='; + delete r.headersOut['Content-Type']; + r.headersOut['Content-Type'] = 'text/xml; charset=utf-8'; + r.headersOut['Content-Type'] = 'text/xml; charset="utf-8"'; + var in_keys = Object.keys(r.headersOut).some(v=>v=='Content-Type'); + r.return(200, `B:\${in_keys}`); + } + + function content_type_arr(r) { + r.headersOut['Content-Type'] = ['text/html']; + r.headersOut['Content-Type'] = []; + r.headersOut['Content-Type'] = [ 'text/xml', 'text/html']; + r.return(200); + } + + function content_encoding(r) { + if (njs.version_number >= 0x000705) { + var ce = r.headersOut['Content-Encoding']; + if (ce !== undefined) { + r.return(500, `Content-Encoding "\${ce}" is not empty`); + return; + } + } + + delete r.headersOut['Content-Encoding']; + r.headersOut['Content-Encoding'] = ''; + r.headersOut['Content-Encoding'] = 'test'; + delete r.headersOut['Content-Encoding']; + r.headersOut['Content-Encoding'] = 'gzip'; + r.return(200); + } + + function content_encoding_arr(r) { + r.headersOut['Content-Encoding'] = 'test'; + r.headersOut['Content-Encoding'] = []; + r.headersOut['Content-Encoding'] = ['test', 'gzip']; + r.return(200); + } + + function headers_list(r) { + for (var h in {a:1, b:2, c:3}) { + r.headersOut[h] = h; + } + + delete r.headersOut.b; + r.headersOut.d = 'd'; + + var out = ""; + for (var h in r.headersOut) { + out += h + ":"; + } + + r.return(200, out); + } + + function hdr_in(r) { + var s = '', h; + for (h in r.headersIn) { + s += `\${h.toLowerCase()}: \${r.headersIn[h]}\n`; + } + + r.return(200, s); + } + + function raw_hdr_in(r) { + var filtered = r.rawHeadersIn + .filter(v=>v[0].toLowerCase() == r.args.filter); + r.return(200, 'raw:' + filtered.map(v=>v[1]).join('|')); + } + + function hdr_sorted_keys(r) { + var s = ''; + var hdr = r.args.in ? r.headersIn : r.headersOut; + + if (!r.args.in) { + r.headersOut.b = 'b'; + r.headersOut.c = 'c'; + r.headersOut.a = 'a'; + } + + r.return(200, Object.keys(hdr).sort()); + } + + function foo_in(r) { + return 'hdr=' + r.headersIn.foo; + } + + function ifoo_in(r) { + var s = '', h; + for (h in r.headersIn) { + if (h.substr(0, 3) == 'foo') { + s += r.headersIn[h]; + } + } + return s; + } + + function hdr_out(r) { + r.status = 200; + r.headersOut['Foo'] = r.args.foo; + + if (r.args.bar) { + r.headersOut['Bar'] = + r.headersOut[(r.args.bar == 'empty' ? 'Baz' :'Foo')] + } + + r.sendHeader(); + r.finish(); + } + + function raw_hdr_out(r) { + r.headersOut.a = ['foo', 'bar']; + r.headersOut.b = 'b'; + + var filtered = r.rawHeadersOut + .filter(v=>v[0].toLowerCase() == r.args.filter); + r.return(200, 'raw:' + filtered.map(v=>v[1]).join('|')); + } + + function hdr_out_array(r) { + if (!r.args.hidden) { + r.headersOut['Foo'] = [r.args.foo]; + r.headersOut['Foo'] = []; + r.headersOut['Foo'] = ['bar', r.args.foo]; + } + + if (r.args.scalar_set) { + r.headersOut['Foo'] = 'xxx'; + } + + r.return(200, `B:\${njs.dump(r.headersOut.foo)}`); + } + + function hdr_out_single(r) { + r.headersOut.ETag = ['a', 'b']; + r.return(200, `B:\${njs.dump(r.headersOut.etag)}`); + } + + function hdr_out_set_cookie(r) { + r.headersOut['Set-Cookie'] = []; + r.headersOut['Set-Cookie'] = ['a', 'b']; + delete r.headersOut['Set-Cookie']; + r.headersOut['Set-Cookie'] = 'e'; + r.headersOut['Set-Cookie'] = ['c', '', null, 'd', 'f']; + + r.return(200, `B:\${njs.dump(r.headersOut['Set-Cookie'])}`); + } + + function ihdr_out(r) { + r.status = 200; + r.headersOut['a'] = r.args.a; + r.headersOut['b'] = r.args.b; + + var s = '', h; + for (h in r.headersOut) { + s += r.headersOut[h]; + } + + r.sendHeader(); + r.send(s); + r.finish(); + } + + function hdr_out_special_set(r) { + r.headersOut['Foo'] = "xxx"; + r.headersOut['Content-Encoding'] = 'abc'; + + let ce = r.headersOut['Content-Encoding']; + r.return(200, `CE: \${ce}`); + } + + async function copy_subrequest_hdrs(r) { + let resp = await r.subrequest("/subrequest"); + + for (const h in resp.headersOut) { + r.headersOut[h] = resp.headersOut[h]; + } + + r.return(200, resp.responseText); + } + + function subrequest(r) { + r.headersOut['A'] = 'a'; + r.headersOut['Content-Encoding'] = 'ce'; + r.headersOut['B'] = 'b'; + r.headersOut['C'] = 'c'; + r.headersOut['D'] = 'd'; + r.headersOut['Set-Cookie'] = ['A', 'BB']; + r.headersOut['Content-Length'] = 3; + r.headersOut['Content-Type'] = 'ct'; + r.sendHeader(); + r.send('XXX'); + r.finish(); + } + + export default {njs:test_njs, content_length, content_length_arr, + content_length_keys, content_type, content_type_arr, + content_encoding, content_encoding_arr, headers_list, + hdr_in, raw_hdr_in, hdr_sorted_keys, foo_in, ifoo_in, + hdr_out, raw_hdr_out, hdr_out_array, hdr_out_single, + hdr_out_set_cookie, ihdr_out, hdr_out_special_set, + copy_subrequest_hdrs, subrequest}; + + +EOF + +$t->try_run('no njs')->plan(42); + +############################################################################### + +like(http_get('/content_length'), qr/Content-Length: 3/, + 'set Content-Length'); +like(http_get('/content_type'), qr/Content-Type: text\/xml; charset="utf-8"\r/, + 'set Content-Type'); +unlike(http_get('/content_type'), qr/Content-Type: text\/plain/, + 'set Content-Type 2'); +like(http_get('/content_encoding'), qr/Content-Encoding: gzip/, + 'set Content-Encoding'); +like(http_get('/headers_list'), qr/a:c:d/, 'headers list'); + +like(http_get('/ihdr_out?a=12&b=34'), qr/^1234$/m, 'r.headersOut iteration'); +like(http_get('/ihdr_out'), qr/\x0d\x0a?\x0d\x0a?$/m, 'r.send zero'); +like(http_get('/hdr_out?foo=12345'), qr/Foo: 12345/, 'r.headersOut'); +like(http_get('/hdr_out?foo=123&bar=copy'), qr/Bar: 123/, 'r.headersOut get'); +unlike(http_get('/hdr_out?bar=empty'), qr/Bar:/, 'r.headersOut empty'); +unlike(http_get('/hdr_out?foo='), qr/Foo:/, 'r.headersOut no value'); +unlike(http_get('/hdr_out?foo'), qr/Foo:/, 'r.headersOut no value 2'); + +like(http_get('/content_length_keys'), qr/B:true/, 'Content-Length in keys'); +like(http_get('/content_length_arr'), qr/Content-Length: 3/, + 'set Content-Length arr'); + +like(http_get('/content_type'), qr/B:true/, 'Content-Type in keys'); +like(http_get('/content_type_arr'), qr/Content-Type: text\/html/, + 'set Content-Type arr'); +like(http_get('/content_encoding_arr'), qr/Content-Encoding: gzip/, + 'set Content-Encoding arr'); + +like(http_get('/hdr_out_array?foo=12345'), qr/Foo: bar\r\nFoo: 12345/, + 'r.headersOut arr'); +like(http_get('/hdr_out_array'), qr/Foo: bar/, + 'r.headersOut arr last is empty'); +like(http_get('/hdr_out_array?foo=abc'), qr/B:bar,\s?abc/, + 'r.headersOut get'); +like(http_get('/hdr_out_array'), qr/B:bar/, 'r.headersOut get2'); +like(http_get('/hdr_out_array?hidden=1'), qr/B:undefined/, + 'r.headersOut get3'); +like(http_get('/hdr_out_array?scalar_set=1'), qr/B:xxx/, + 'r.headersOut scalar set'); +like(http_get('/hdr_out_single'), qr/ETag: a\r\nETag: b/, + 'r.headersOut single'); +like(http_get('/hdr_out_single'), qr/B:a/, + 'r.headersOut single get'); +like(http_get('/hdr_out_set_cookie'), qr/Set-Cookie: c\r\nSet-Cookie: d/, + 'set_cookie'); +like(http_get('/hdr_out_set_cookie'), qr/B:\['c','d','f']/, + 'set_cookie2'); +unlike(http_get('/hdr_out_set_cookie'), qr/Set-Cookie: [abe]/, + 'set_cookie3'); + +like(http( + 'GET /hdr_in HTTP/1.0' . CRLF + . 'Cookie: foo' . CRLF + . 'Host: localhost' . CRLF . CRLF +), qr/cookie: foo/, 'r.headersIn cookie'); + +like(http( + 'GET /hdr_in HTTP/1.0' . CRLF + . 'X-Forwarded-For: foo' . CRLF + . 'Host: localhost' . CRLF . CRLF +), qr/x-forwarded-for: foo/, 'r.headersIn xff'); + +like(http( + 'GET /hdr_in HTTP/1.0' . CRLF + . 'Cookie: foo1' . CRLF + . 'Cookie: foo2' . CRLF + . 'Host: localhost' . CRLF . CRLF +), qr/cookie: foo1;\s?foo2/, 'r.headersIn cookie2'); + +like(http( + 'GET /hdr_in HTTP/1.0' . CRLF + . 'X-Forwarded-For: foo1' . CRLF + . 'X-Forwarded-For: foo2' . CRLF + . 'Host: localhost' . CRLF . CRLF +), qr/x-forwarded-for: foo1,\s?foo2/, 'r.headersIn xff2'); + +like(http( + 'GET /hdr_in HTTP/1.0' . CRLF + . 'ETag: bar1' . CRLF + . 'ETag: bar2' . CRLF + . 'Host: localhost' . CRLF . CRLF +), qr/etag: bar1(?!,\s?bar2)/, 'r.headersIn duplicate single'); + +like(http( + 'GET /hdr_in HTTP/1.0' . CRLF + . 'Content-Type: bar1' . CRLF + . 'Content-Type: bar2' . CRLF + . 'Host: localhost' . CRLF . CRLF +), qr/content-type: bar1(?!,\s?bar2)/, 'r.headersIn duplicate single 2'); + +like(http( + 'GET /hdr_in HTTP/1.0' . CRLF + . 'Foo: bar1' . CRLF + . 'Foo: bar2' . CRLF + . 'Host: localhost' . CRLF . CRLF +), qr/foo: bar1,\s?bar2/, 'r.headersIn duplicate generic'); + +like(http( + 'GET /raw_hdr_in?filter=foo HTTP/1.0' . CRLF + . 'foo: bar1' . CRLF + . 'Foo: bar2' . CRLF + . 'Host: localhost' . CRLF . CRLF +), qr/raw: bar1|bar2/, 'r.rawHeadersIn'); + +like(http_get('/raw_hdr_out?filter=a'), qr/raw: foo|bar/, 'r.rawHeadersOut'); + +like(http( + 'GET /hdr_sorted_keys?in=1 HTTP/1.0' . CRLF + . 'Cookie: foo1' . CRLF + . 'Accept: */*' . CRLF + . 'Cookie: foo2' . CRLF + . 'Host: localhost' . CRLF . CRLF +), qr/Accept,Cookie,Host/, 'r.headersIn sorted keys'); + +like(http( + 'GET /hdr_sorted_keys HTTP/1.0' . CRLF + . 'Host: localhost' . CRLF . CRLF +), qr/a,b,c/, 'r.headersOut sorted keys'); + +TODO: { +local $TODO = 'not yet' unless has_version('0.7.6'); + +like(http_get('/hdr_out_special_set'), qr/CE: abc/, + 'r.headerOut special set'); + +like(http_get('/copy_subrequest_hdrs'), + qr/A: a.*B: b.*C: c.*D: d.*Set-Cookie: A.*Set-Cookie: BB/s, + 'subrequest copy'); + +like(http_get('/copy_subrequest_hdrs'), + qr/Content-Type: ct.*Content-Encoding: ce.*Content-Length: 3/s, + 'subrequest copy special'); + +} + +############################################################################### + +sub has_version { + my $need = shift; + + http_get('/njs') =~ /^([.0-9]+)$/m; + + my @v = split(/\./, $1); + my ($n, $v); + + for $n (split(/\./, $need)) { + $v = shift @v || 0; + return 0 if $n > $v; + return 1 if $v > $n; + } + + return 1; +} + +############################################################################### diff --git a/nginx/t/js_import.t b/nginx/t/js_import.t new file mode 100644 index 00000000..52ee7954 --- /dev/null +++ b/nginx/t/js_import.t @@ -0,0 +1,108 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (c) Nginx, Inc. + +# Tests for http njs module, js_import directive. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_set $test foo.bar.p; + + js_import lib.js; + js_import fun.js; + js_import foo from ./main.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /njs { + js_content foo.version; + } + + location /test_foo { + js_content foo.test; + } + + location /test_lib { + js_content lib.test; + } + + location /test_fun { + js_content fun; + } + + location /test_var { + return 200 $test; + } + } +} + +EOF + +$t->write_file('lib.js', <<EOF); + function test(r) { + r.return(200, "LIB-TEST"); + } + + export default {test}; + +EOF + +$t->write_file('fun.js', <<EOF); + export default function (r) {r.return(200, "FUN-TEST")}; + +EOF + +$t->write_file('main.js', <<EOF); + function version(r) { + r.return(200, njs.version); + } + + function test(r) { + r.return(200, "MAIN-TEST"); + } + + export default {version, test, bar: {p(r) {return "P-TEST"}}}; + +EOF + +$t->try_run('no njs available')->plan(4); + +############################################################################### + +like(http_get('/test_foo'), qr/MAIN-TEST/s, 'foo.test'); +like(http_get('/test_lib'), qr/LIB-TEST/s, 'lib.test'); +like(http_get('/test_fun'), qr/FUN-TEST/s, 'fun'); +like(http_get('/test_var'), qr/P-TEST/s, 'foo.bar.p'); + +############################################################################### diff --git a/nginx/t/js_import2.t b/nginx/t/js_import2.t new file mode 100644 index 00000000..cd29d2dc --- /dev/null +++ b/nginx/t/js_import2.t @@ -0,0 +1,127 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (c) Nginx, Inc. + +# Tests for http njs module, js_import directive in server | location contexts. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 proxy rewrite/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8080; + server_name localhost; + + js_set $test foo.bar.p; + + js_import foo from main.js; + + location /njs { + js_content foo.version; + } + + location /test_foo { + js_content foo.test; + } + + location /test_lib { + js_import lib.js; + js_content lib.test; + } + + location /test_fun { + js_import fun.js; + js_content fun; + } + + location /test_var { + return 200 $test; + } + + location /proxy { + proxy_pass http://127.0.0.1:8081/; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + location /test_fun { + js_import fun.js; + js_content fun; + } + } +} + +EOF + +$t->write_file('lib.js', <<EOF); + function test(r) { + r.return(200, "LIB-TEST"); + } + + function p(r) { + return "LIB-P"; + } + + export default {test, p}; + +EOF + +$t->write_file('fun.js', <<EOF); + export default function (r) {r.return(200, "FUN-TEST")}; + +EOF + +$t->write_file('main.js', <<EOF); + function version(r) { + r.return(200, njs.version); + } + + function test(r) { + r.return(200, "MAIN-TEST"); + } + + export default {version, test, bar: {p(r) {return "P-TEST"}}}; + +EOF + +$t->try_run('no njs available')->plan(5); + +############################################################################### + +like(http_get('/test_foo'), qr/MAIN-TEST/s, 'foo.test'); +like(http_get('/test_lib'), qr/LIB-TEST/s, 'lib.test'); +like(http_get('/test_fun'), qr/FUN-TEST/s, 'fun'); +like(http_get('/proxy/test_fun'), qr/FUN-TEST/s, 'proxy fun'); +like(http_get('/test_var'), qr/P-TEST/s, 'foo.bar.p'); + +############################################################################### diff --git a/nginx/t/js_internal_redirect.t b/nginx/t/js_internal_redirect.t new file mode 100644 index 00000000..ec6be4e1 --- /dev/null +++ b/nginx/t/js_internal_redirect.t @@ -0,0 +1,107 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, internalRedirect method. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 rewrite/) + ->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 /njs { + js_content test.njs; + } + + location /test { + js_content test.redirect; + } + + location /redirect { + internal; + return 200 redirect$arg_b; + } + + location @named { + return 200 named; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function redirect(r) { + if (r.variables.arg_dest == 'named') { + r.internalRedirect('\@named'); + + } else if (r.variables.arg_unsafe) { + r.internalRedirect('/red\0rect'); + + } else if (r.variables.arg_quoted) { + r.internalRedirect('/red%69rect'); + + } else { + if (r.variables.arg_a) { + r.internalRedirect('/redirect?b=' + r.variables.arg_a); + + } else { + r.internalRedirect('/redirect'); + } + } + } + + export default {njs:test_njs, redirect}; + +EOF + +$t->try_run('no njs available')->plan(5); + +############################################################################### + +like(http_get('/test'), qr/redirect/s, 'redirect'); +like(http_get('/test?a=A'), qr/redirectA/s, 'redirect with args'); +like(http_get('/test?dest=named'), qr/named/s, 'redirect to named location'); + +like(http_get('/test?unsafe=1'), qr/500 Internal Server/s, + 'unsafe redirect'); +like(http_get('/test?quoted=1'), qr/200 .*redirect/s, + 'quoted redirect'); + +############################################################################### diff --git a/nginx/t/js_modules.t b/nginx/t/js_modules.t new file mode 100644 index 00000000..21581b4a --- /dev/null +++ b/nginx/t/js_modules.t @@ -0,0 +1,84 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, ES6 import, export. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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/) + ->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 /test { + js_content test.test; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + import m from 'module.js'; + + function test(r) { + r.return(200, m[r.args.fun](r.args.a, r.args.b)); + } + + export default {test}; + +EOF + +$t->write_file('module.js', <<EOF); + function sum(a, b) { + return Number(a) + Number(b); + } + + function prod(a, b) { + return Number(a) * Number(b); + } + + export default {sum, prod}; + +EOF + + +$t->try_run('no njs modules')->plan(2); + +############################################################################### + +like(http_get('/test?fun=sum&a=3&b=4'), qr/7/s, 'test sum'); +like(http_get('/test?fun=prod&a=3&b=4'), qr/12/s, 'test prod'); + +############################################################################### diff --git a/nginx/t/js_ngx.t b/nginx/t/js_ngx.t new file mode 100644 index 00000000..c6271f08 --- /dev/null +++ b/nginx/t/js_ngx.t @@ -0,0 +1,94 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, ngx object. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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/) + ->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 /njs { + js_content test.njs; + } + + location /log { + js_content test.log; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function level(r) { + switch (r.args.level) { + case 'INFO': return ngx.INFO; + case 'WARN': return ngx.WARN; + case 'ERR': return ngx.ERR; + default: + throw Error(`Unknown log level:"\${r.args.level}"`); + } + } + + function log(r) { + ngx.log(level(r), `ngx.log:\${r.args.text}`); + r.return(200); + } + + export default {njs: test_njs, log}; + +EOF + +$t->try_run('no njs ngx')->plan(3); + +############################################################################### + +http_get('/log?level=INFO&text=FOO'); +http_get('/log?level=WARN&text=BAR'); +http_get('/log?level=ERR&text=BAZ'); + +$t->stop(); + +like($t->read_file('error.log'), qr/\[info\].*ngx.log:FOO/, 'ngx.log info'); +like($t->read_file('error.log'), qr/\[warn\].*ngx.log:BAR/, 'ngx.log warn'); +like($t->read_file('error.log'), qr/\[error\].*ngx.log:BAZ/, 'ngx.log err'); + +############################################################################### diff --git a/nginx/t/js_object.t b/nginx/t/js_object.t new file mode 100644 index 00000000..97e778a2 --- /dev/null +++ b/nginx/t/js_object.t @@ -0,0 +1,137 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, request object. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +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/) + ->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 /to_string { + js_content test.to_string; + } + + location /define_prop { + js_content test.define_prop; + } + + location /in_operator { + js_content test.in_operator; + } + + location /redefine_bind { + js_content test.redefine_bind; + } + + location /redefine_proxy { + js_content test.redefine_proxy; + } + + location /redefine_proto { + js_content test.redefine_proto; + } + + location /get_own_prop_descs { + js_content test.get_own_prop_descs; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function to_string(r) { + r.return(200, r.toString()); + } + + function define_prop(r) { + Object.defineProperty(r.headersOut, 'Foo', {value:'bar'}); + r.return(200); + } + + function in_operator(r) { + r.return(200, ['Foo', 'Bar'].map(v=>v in r.headersIn) + .toString() === 'true,false'); + } + + function redefine_bind(r) { + r.return = r.return.bind(r, 200); + r.return('redefine_bind'); + } + + function redefine_proxy(r) { + r.return_orig = r.return; + r.return = function (body) { this.return_orig(200, body);} + r.return('redefine_proxy'); + } + + function redefine_proto(r) { + r[0] = 'a'; + r[1] = 'b'; + r.length = 2; + Object.setPrototypeOf(r, Array.prototype); + r.return(200, r.join('|')); + } + + function get_own_prop_descs(r) { + r.return(200, + Object.getOwnPropertyDescriptors(r)['log'].value === r.log); + } + + export default {to_string, define_prop, in_operator, redefine_bind, + redefine_proxy, redefine_proto, get_own_prop_descs}; + +EOF + +$t->try_run('no njs request object')->plan(7); + +############################################################################### + +like(http_get('/to_string'), qr/\[object Request\]/, 'toString'); +like(http_get('/define_prop'), qr/Foo: bar/, 'define_prop'); +like(http( + 'GET /in_operator HTTP/1.0' . CRLF + . 'Foo: foo' . CRLF + . 'Host: localhost' . CRLF . CRLF +), qr/true/, 'in_operator'); +like(http_get('/redefine_bind'), qr/redefine_bind/, 'redefine_bind'); +like(http_get('/redefine_proxy'), qr/redefine_proxy/, 'redefine_proxy'); +like(http_get('/redefine_proto'), qr/a|b/, 'redefine_proto'); +like(http_get('/get_own_prop_descs'), qr/true/, 'get_own_prop_descs'); + +############################################################################### diff --git a/nginx/t/js_paths.t b/nginx/t/js_paths.t new file mode 100644 index 00000000..98d87512 --- /dev/null +++ b/nginx/t/js_paths.t @@ -0,0 +1,110 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, js_path directive. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_path "%%TESTDIR%%/lib1"; + js_path "lib2"; + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /test { + js_content test.test; + } + + location /test2 { + js_content test.test2; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + import m1 from 'module1.js'; + import m2 from 'module2.js'; + import m3 from 'lib1/module1.js'; + + function test(r) { + r.return(200, m1[r.args.fun](r.args.a, r.args.b)); + } + + function test2(r) { + r.return(200, m2.sum(r.args.a, r.args.b)); + } + + function test3(r) { + r.return(200, m3.sum(r.args.a, r.args.b)); + } + + export default {test, test2}; + +EOF + +my $d = $t->testdir(); + +mkdir("$d/lib1"); +mkdir("$d/lib2"); + +$t->write_file('lib1/module1.js', <<EOF); + function sum(a, b) { return Number(a) + Number(b); } + function prod(a, b) { return Number(a) * Number(b); } + + export default {sum, prod}; + +EOF + +$t->write_file('lib2/module2.js', <<EOF); + function sum(a, b) { return a + b; } + + export default {sum}; + +EOF + + +$t->try_run('no njs available')->plan(4); + +############################################################################### + +like(http_get('/test?fun=sum&a=3&b=4'), qr/7/s, 'test sum'); +like(http_get('/test?fun=prod&a=3&b=4'), qr/12/s, 'test prod'); +like(http_get('/test2?a=3&b=4'), qr/34/s, 'test2'); +like(http_get('/test2?a=A&b=B'), qr/AB/s, 'test2 relative'); + +############################################################################### diff --git a/nginx/t/js_preload_object.t b/nginx/t/js_preload_object.t new file mode 100644 index 00000000..407e97fe --- /dev/null +++ b/nginx/t/js_preload_object.t @@ -0,0 +1,181 @@ +#!/usr/bin/perl + +# (C) Vadim Zhestikov +# (C) Nginx, Inc. + +# Tests for http njs module, js_preload_object directive. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 rewrite/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_preload_object g1 from g.json; + js_preload_object ga from ga.json; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + js_import lib.js; + js_preload_object lx from l.json; + + location /test { + js_content lib.test; + } + + location /test_query { + js_import lib1.js; + js_content lib1.query; + } + + location /test_query_preloaded { + js_import lib1.js; + js_preload_object l.json; + js_content lib1.query; + } + + location /test_var { + js_set $test_var lib.test_var; + return 200 $test_var; + } + + location /test_mutate { + js_content lib.mutate; + } + + location /test_no_suffix { + js_preload_object gg from no_suffix; + js_content lib.suffix; + } + } +} + +EOF + +$t->write_file('lib.js', <<EOF); + function test(r) { + r.return(200, ga + ' ' + g1.c.prop[0].a + ' ' + lx); + } + + function test_var(r) { + return g1.b[2]; + } + + function mutate(r) { + var res = "OK"; + + try { + switch (r.args.method) { + case 'set_obj': + g1.c.prop[0].a = 5; + break; + case 'set_arr': + g1.c.prop[0] = 5; + break; + case 'add_obj': + g1.c.prop[0].xxx = 5; + break; + case 'add_arr': + g1.c.prop[10] = 5; + break; + case 'del_obj': + delete g1.c.prop[0].a; + break; + case 'del_arr': + delete g1.c.prop[0]; + break; + } + + } catch (e) { + res = e.message; + } + + r.return(200, res); + } + + function suffix(r) { + r.return(200, gg); + } + + export default {test, test_var, mutate, suffix}; + +EOF + +$t->write_file('lib1.js', <<EOF); + function query(r) { + var res = 'ok'; + + try { + res = r.args.path.split('.').reduce((a, v) => a[v], globalThis); + + } catch (e) { + res = e.message; + } + + r.return(200, njs.dump(res)); + } + + export default {query}; + +EOF + +$t->write_file('g.json', + '{"a":1, "b":[1,2,"element",4,5], "c":{"prop":[{"a":2}]}}'); +$t->write_file('ga.json', '"ga loaded"'); +$t->write_file('l.json', '"l loaded"'); +$t->write_file('no_suffix', '"no_suffix loaded"'); + +$t->try_run('no js_preload_object available')->plan(12); + +############################################################################### + +like(http_get('/test'), qr/ga loaded 2 l loaded/s, 'direct query'); +like(http_get('/test_query?path=l'), qr/undefined/s, 'unreferenced'); +like(http_get('/test_query_preloaded?path=l'), qr/l loaded/s, + 'reference preload'); +like(http_get('/test_query?path=g1.b.1'), qr/2/s, 'complex query'); +like(http_get('/test_var'), qr/element/s, 'var reference'); + +like(http_get('/test_mutate?method=set_obj'), qr/Cannot assign to read-only/s, + 'preload_object props are const (object)'); +like(http_get('/test_mutate?method=set_arr'), qr/Cannot assign to read-only/s, + 'preload_object props are const (array)'); +like(http_get('/test_mutate?method=add_obj'), qr/Cannot add property "xxx"/s, + 'preload_object props are not extensible (object)'); +like(http_get('/test_mutate?method=add_arr'), qr/Cannot add property "10"/s, + 'preload_object props are not extensible (array)'); +like(http_get('/test_mutate?method=del_obj'), qr/Cannot delete property "a"/s, + 'preload_object props are not deletable (object)'); +like(http_get('/test_mutate?method=del_arr'), qr/Cannot delete property "0"/s, + 'preload_object props are not deletable (array)'); + +like(http_get('/test_no_suffix'), qr/no_suffix loaded/s, + 'load without suffix'); + +############################################################################### diff --git a/nginx/t/js_promise.t b/nginx/t/js_promise.t new file mode 100644 index 00000000..f6084e99 --- /dev/null +++ b/nginx/t/js_promise.t @@ -0,0 +1,201 @@ +#!/usr/bin/perl + +# (C) Nginx, Inc. + +# Promise tests for http njs module. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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/) + ->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 /njs { + js_content test.njs; + } + + location /promise { + js_content test.promise; + } + + location /promise_throw { + js_content test.promise_throw; + } + + location /promise_pure { + js_content test.promise_pure; + } + + location /timeout { + js_content test.timeout; + } + + location /sub_token { + js_content test.sub_token; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + var global_token = ''; + + function test_njs(r) { + r.return(200, njs.version); + } + + function promise(r) { + promisified_subrequest(r, '/sub_token', 'code=200&token=a') + .then(reply => { + var data = JSON.parse(reply.responseText); + + if (data['token'] !== "a") { + throw new Error('token is not "a"'); + } + + return data['token']; + }) + .then(token => { + promisified_subrequest(r, '/sub_token', 'code=200&token=b') + .then(reply => { + var data = JSON.parse(reply.responseText); + + r.return(200, '{"token": "' + data['token'] + '"}'); + }) + .catch(() => { + throw new Error("failed promise() test"); + }); + }) + .catch(() => { + r.return(500); + }); + } + + function promise_throw(r) { + promisified_subrequest(r, '/sub_token', 'code=200&token=x') + .then(reply => { + var data = JSON.parse(reply.responseText); + + if (data['token'] !== "a") { + throw data['token']; + } + + return data['token']; + }) + .then(() => { + r.return(500); + }) + .catch(token => { + r.return(200, '{"token": "' + token + '"}'); + }); + } + + function promise_pure(r) { + var count = 0; + + Promise.resolve(true) + .then(() => count++) + .then(() => not_exist_ref) + .finally(() => count++) + .catch(() => { + r.return((count != 2) ? 500 : 200); + }); + } + + function timeout(r) { + promisified_subrequest(r, '/sub_token', 'code=200&token=R') + .then(reply => JSON.parse(reply.responseText)) + .then(data => { + setTimeout(timeout_cb, 50, r, '/sub_token', 'code=200&token=T'); + return data; + }) + .then(data => { + setTimeout(timeout_cb, 1, r, '/sub_token', 'code=200&token=' + + data['token']); + }) + .catch(() => { + r.return(500); + }); + } + + function timeout_cb(r, url, args) { + promisified_subrequest(r, url, args) + .then(reply => { + if (global_token == '') { + var data = JSON.parse(reply.responseText); + + global_token = data['token']; + + r.return(200, '{"token": "' + data['token'] + '"}'); + } + }) + .catch(() => { + r.return(500); + }); + } + + function promisified_subrequest(r, uri, args) { + return new Promise((resolve, reject) => { + r.subrequest(uri, args, (reply) => { + if (reply.status < 400) { + resolve(reply); + } else { + reject(reply); + } + }); + }) + } + + function sub_token(r) { + var code = r.variables.arg_code; + var token = r.variables.arg_token; + + r.return(parseInt(code), '{"token": "'+ token +'"}'); + } + + export default {njs:test_njs, promise, promise_throw, promise_pure, + timeout, sub_token}; + +EOF + +$t->try_run('no njs available')->plan(4); + +############################################################################### + +like(http_get('/promise'), qr/{"token": "b"}/, "Promise"); +like(http_get('/promise_throw'), qr/{"token": "x"}/, "Promise throw and catch"); +like(http_get('/timeout'), qr/{"token": "R"}/, "Promise with timeout"); +like(http_get('/promise_pure'), qr/200 OK/, "events handling"); + +############################################################################### diff --git a/nginx/t/js_request_body.t b/nginx/t/js_request_body.t new file mode 100644 index 00000000..350e7fb8 --- /dev/null +++ b/nginx/t/js_request_body.t @@ -0,0 +1,110 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, r.requestText method. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ CRLF /; + +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/) + ->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 /body { + js_content test.body; + } + + location /in_file { + client_body_in_file_only on; + js_content test.body; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function body(r) { + try { + var body = r.requestText; + r.return(200, body); + + } catch (e) { + r.return(500, e.message); + } + } + + export default {body}; + +EOF + +$t->try_run('no njs request body')->plan(3); + +############################################################################### + +like(http_post('/body'), qr/REQ-BODY/, 'request body'); +like(http_post('/in_file'), qr/request body is in a file/, + 'request body in file'); +like(http_post_big('/body'), qr/200.*^(1234567890){1024}$/ms, + 'request body big'); + +############################################################################### + +sub http_post { + my ($url, %extra) = @_; + + my $p = "POST $url HTTP/1.0" . CRLF . + "Host: localhost" . CRLF . + "Content-Length: 8" . CRLF . + CRLF . + "REQ-BODY"; + + return http($p, %extra); +} + +sub http_post_big { + my ($url, %extra) = @_; + + my $p = "POST $url HTTP/1.0" . CRLF . + "Host: localhost" . CRLF . + "Content-Length: 10240" . CRLF . + CRLF . + ("1234567890" x 1024); + + return http($p, %extra); +} + +############################################################################### diff --git a/nginx/t/js_return.t b/nginx/t/js_return.t new file mode 100644 index 00000000..2cc32f74 --- /dev/null +++ b/nginx/t/js_return.t @@ -0,0 +1,73 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Tests for http njs module, return method. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Config; + +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/) + ->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 / { + js_content test.returnf; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function returnf(r) { + r.return(Number(r.args.c), r.args.t); + } + + export default {returnf}; + +EOF + +$t->try_run('no njs return')->plan(5); + +############################################################################### + +like(http_get('/?c=200'), qr/200 OK.*\x0d\x0a?\x0d\x0a?$/s, 'return code'); +like(http_get('/?c=200&t=SEE-THIS'), qr/200 OK.*^SEE-THIS$/ms, 'return text'); +like(http_get('/?c=301&t=path'), qr/ 301 .*Location: path/s, 'return redirect'); +like(http_get('/?c=404'), qr/404 Not.*html/s, 'return error page'); +like(http_get('/?c=inv'), qr/ 500 /, 'return invalid'); + +############################################################################### diff --git a/nginx/t/js_subrequests.t b/nginx/t/js_subrequests.t new file mode 100644 index 00000000..a2007fa5 --- /dev/null +++ b/nginx/t/js_subrequests.t @@ -0,0 +1,636 @@ +#!/usr/bin/perl +# +# (C) Dmitry Volyntsev. +# (C) Nginx, Inc. + +# Tests for subrequests in http njs module. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +eval { require JSON::PP; }; +plan(skip_all => "JSON::PP not installed") if $@; + +my $t = Test::Nginx->new()->has(qw/http rewrite proxy cache/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + proxy_cache_path %%TESTDIR%%/cache1 + keys_zone=ON:1m use_temp_path=on; + + js_import test.js; + + js_set $async_var test.async_var; + js_set $subrequest_var test.subrequest_var; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /njs { + js_content test.njs; + } + + location /sr { + js_content test.sr; + } + + location /sr_pr { + js_content test.sr_pr; + } + + location /sr_args { + js_content test.sr_args; + } + + location /sr_options_args { + js_content test.sr_options_args; + } + + location /sr_options_args_pr { + js_content test.sr_options_args_pr; + } + + location /sr_options_method { + js_content test.sr_options_method; + } + + location /sr_options_method_pr { + js_content test.sr_options_method_pr; + } + + location /sr_options_body { + js_content test.sr_options_body; + } + + location /sr_options_method_head { + js_content test.sr_options_method_head; + } + + location /sr_body { + js_content test.sr_body; + } + + location /sr_body_pr { + js_content test.sr_body_pr; + } + + location /sr_body_special { + js_content test.sr_body_special; + } + + location /sr_in_variable_handler { + set $_ $async_var; + js_content test.sr_in_variable_handler; + } + + location /sr_detached_in_variable_handler { + return 200 $subrequest_var; + } + + location /sr_async_var { + set $_ $async_var; + error_page 404 /return; + return 404; + } + + location /sr_error_page { + js_content test.sr_error_page; + } + + location /sr_js_in_subrequest { + js_content test.sr_js_in_subrequest; + } + + location /sr_js_in_subrequest_pr { + js_content test.sr_js_in_subrequest_pr; + } + + location /sr_file { + js_content test.sr_file; + } + + location /sr_cache { + js_content test.sr_cache; + } + + + location /sr_unavail { + js_content test.sr_unavail; + } + + location /sr_unavail_pr { + js_content test.sr_unavail_pr; + } + + location /sr_broken { + js_content test.sr_broken; + } + + location /sr_too_large { + js_content test.sr_too_large; + } + + location /sr_out_of_order { + js_content test.sr_out_of_order; + } + + location /sr_except_not_a_func { + js_content test.sr_except_not_a_func; + } + + location /sr_except_failed_to_convert_options_arg { + js_content test.sr_except_failed_to_convert_options_arg; + } + + location /sr_except_invalid_options_header_only { + js_content test.sr_except_invalid_options_header_only; + } + + location /sr_in_sr_callback { + js_content test.sr_in_sr_callback; + } + + location /sr_uri_except { + js_content test.sr_uri_except; + } + + + location /file/ { + alias %%TESTDIR%%/; + } + + location /p/ { + proxy_cache $arg_c; + proxy_pass http://127.0.0.1:8081/; + } + + location /daemon/ { + proxy_pass http://127.0.0.1:8082/; + } + + location /too_large/ { + subrequest_output_buffer_size 3; + proxy_pass http://127.0.0.1:8081/; + } + + location /sr_in_sr { + js_content test.sr_in_sr; + } + + location /unavail { + proxy_pass http://127.0.0.1:8084/; + } + + location /sr_parent { + js_content test.sr_parent; + } + + location /js_sub { + js_content test.js_sub; + } + + location /return { + return 200 '["$request_method"]'; + } + + location /error_page_404 { + return 404; + + error_page 404 /404.html; + } + } + + server { + listen 127.0.0.1:8081; + server_name localhost; + + location /sub1 { + add_header H $arg_h; + return 206 '{"a": {"b": 1}}'; + } + + location /sub2 { + return 404 '{"e": "msg"}'; + } + + location /method { + return 200 '["$request_method"]'; + } + + location /body { + js_content test.body; + } + + location /detached { + js_content test.detached; + } + + location /delayed { + js_content test.delayed; + } + } + + server { + listen 127.0.0.1:8084; + server_name localhost; + + return 444; + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function sr(r) { + subrequest_fn(r, ['/p/sub2'], ['uri', 'status']) + } + + function sr_pr(r) { + r.subrequest('/p/sub1', 'h=xxx') + .then(reply => r.return(200, JSON.stringify({h:reply.headersOut.h}))) + } + + function sr_args(r) { + r.subrequest('/p/sub1', 'h=xxx', reply => { + r.return(200, JSON.stringify({h:reply.headersOut.h})); + }); + } + + function sr_options_args(r) { + r.subrequest('/p/sub1', {args:'h=xxx'}, reply => { + r.return(200, JSON.stringify({h:reply.headersOut.h})); + }); + } + + function sr_options_args_pr(r) { + r.subrequest('/p/sub1', {args:'h=xxx'}) + .then(reply => r.return(200, JSON.stringify({h:reply.headersOut.h}))) + } + + function sr_options_method(r) { + r.subrequest('/p/method', {method:r.args.m}, body_fwd_cb); + } + + function sr_options_method_pr(r) { + r.subrequest('/p/method', {method:r.args.m}) + .then(body_fwd_cb); + } + + function sr_options_body(r) { + r.subrequest('/p/body', {method:'POST', body:'["REQ-BODY"]'}, + body_fwd_cb); + } + + function sr_options_method_head(r) { + r.subrequest('/p/method', {method:'HEAD'}, reply => { + r.return(200, JSON.stringify({c:reply.status})); + }); + } + + function sr_body(r) { + r.subrequest('/p/sub1', body_fwd_cb); + } + + function sr_body_pr(r) { + r.subrequest('/p/sub1') + .then(body_fwd_cb); + } + + function sr_body_special(r) { + r.subrequest('/p/sub2', body_fwd_cb); + } + + function body(r) { + r.return(200, r.variables.request_body); + } + + function delayed(r) { + setTimeout(r => r.return(200), 100, r); + } + + function detached(r) { + var method = r.variables.request_method; + r.log(`DETACHED: \${method} args: \${r.variables.args}`); + + r.return(200); + } + + function sr_in_variable_handler(r) { + } + + function async_var(r) { + r.subrequest('/p/delayed', reply => { + r.return(200, JSON.stringify(["CB-VAR"])); + }); + + return ""; + } + + function sr_error_page(r) { + r.subrequest('/error_page_404') + .then(reply => {r.return(200, `reply.status:\${reply.status}`)}); + } + + function subrequest_var(r) { + r.subrequest('/p/detached', {detached:true}); + r.subrequest('/p/detached', {detached:true, args:'a=yyy', + method:'POST'}); + + return "subrequest_var"; + } + + function sr_file(r) { + r.subrequest('/file/t', body_fwd_cb); + } + + function sr_cache(r) { + r.subrequest('/p/t', body_fwd_cb); + } + + function sr_unavail(req) { + subrequest_fn(req, ['/unavail'], ['uri', 'status']); + } + + function sr_unavail_pr(req) { + subrequest_fn_pr(req, ['/unavail'], ['uri', 'status']); + } + + function sr_broken(r) { + r.subrequest('/daemon/unfinished', reply => { + r.return(200, JSON.stringify({code:reply.status})); + }); + } + + function sr_too_large(r) { + r.subrequest('/too_large/t', body_fwd_cb); + } + + function sr_in_sr(r) { + r.subrequest('/sr', body_fwd_cb); + } + + function sr_js_in_subrequest(r) { + r.subrequest('/js_sub', body_fwd_cb); + } + + function sr_js_in_subrequest_pr(r) { + r.subrequest('/js_sub') + .then(body_fwd_cb); + } + + function sr_in_sr_callback(r) { + r.subrequest('/return', function (reply) { + try { + reply.subrequest('/return'); + + } catch (err) { + r.return(200, JSON.stringify({e:err.message})); + return; + } + + r.return(200); + }); + } + + function sr_parent(r) { + try { + var parent = r.parent; + + } catch (err) { + r.return(200, JSON.stringify({e:err.message})); + return; + } + + r.return(200); + } + + function sr_out_of_order(r) { + subrequest_fn(r, ['/p/delayed', '/p/sub1', '/unknown'], + ['uri', 'status']); + } + + function collect(replies, props, total, reply) { + reply.log(`subrequest handler: \${reply.uri} status: \${reply.status}`) + + var rep = {}; + props.forEach(p => {rep[p] = reply[p]}); + + replies.push(rep); + + if (replies.length == total) { + reply.parent.return(200, JSON.stringify(replies)); + } + } + + function subrequest_fn(r, subs, props) { + var replies = []; + + subs.forEach(sr => + r.subrequest(sr, collect.bind(null, replies, + props, subs.length))); + } + + function subrequest_fn_pr(r, subs, props) { + var replies = []; + + subs.forEach(sr => r.subrequest(sr) + .then(collect.bind(null, replies, props, subs.length))); + } + + function sr_except_not_a_func(r) { + r.subrequest('/sub1', 'a=1', 'b'); + } + + let Failed = {get toConvert() { return {toString(){return {};}}}}; + + function sr_except_failed_to_convert_options_arg(r) { + r.subrequest('/sub1', {args:Failed.toConvert}, ()=>{}); + } + + function sr_uri_except(r) { + r.subrequest(Failed.toConvert, 'a=1', 'b'); + } + + function body_fwd_cb(r) { + r.parent.return(200, JSON.stringify(JSON.parse(r.responseText))); + } + + function js_sub(r) { + r.return(200, '["JS-SUB"]'); + } + + export default {njs:test_njs, sr, sr_pr, sr_args, sr_options_args, + sr_options_args_pr, sr_options_method, sr_options_method_pr, + sr_options_method_head, sr_options_body, sr_body, + sr_body_pr, sr_body_special, body, delayed, detached, + sr_in_variable_handler, async_var, sr_error_page, + subrequest_var, sr_file, sr_cache, sr_unavail, sr_parent, + sr_unavail_pr, sr_broken, sr_too_large, sr_in_sr, + sr_js_in_subrequest, sr_js_in_subrequest_pr, js_sub, + sr_in_sr_callback, sr_out_of_order, sr_except_not_a_func, + sr_uri_except, sr_except_failed_to_convert_options_arg}; + +EOF + +$t->write_file('t', '["SEE-THIS"]'); + +$t->try_run('no njs available')->plan(32); +$t->run_daemon(\&http_daemon); + +############################################################################### + +is(get_json('/sr'), '[{"status":404,"uri":"/p/sub2"}]', 'sr'); +is(get_json('/sr_args'), '{"h":"xxx"}', 'sr_args'); +is(get_json('/sr_options_args'), '{"h":"xxx"}', 'sr_options_args'); +is(get_json('/sr_options_method?m=POST'), '["POST"]', 'sr method POST'); +is(get_json('/sr_options_method?m=PURGE'), '["PURGE"]', 'sr method PURGE'); +is(get_json('/sr_options_body'), '["REQ-BODY"]', 'sr_options_body'); +is(get_json('/sr_options_method_head'), '{"c":200}', 'sr_options_method_head'); +is(get_json('/sr_body'), '{"a":{"b":1}}', 'sr_body'); +is(get_json('/sr_body_special'), '{"e":"msg"}', 'sr_body_special'); +is(get_json('/sr_in_variable_handler'), '["CB-VAR"]', 'sr_in_variable_handler'); +is(get_json('/sr_file'), '["SEE-THIS"]', 'sr_file'); +is(get_json('/sr_cache?c=1'), '["SEE-THIS"]', 'sr_cache'); +is(get_json('/sr_cache?c=1'), '["SEE-THIS"]', 'sr_cached'); +is(get_json('/sr_js_in_subrequest'), '["JS-SUB"]', 'sr_js_in_subrequest'); +is(get_json('/sr_unavail'), '[{"status":502,"uri":"/unavail"}]', + 'sr_unavail'); +is(get_json('/sr_out_of_order'), + '[{"status":404,"uri":"/unknown"},' . + '{"status":206,"uri":"/p/sub1"},' . + '{"status":200,"uri":"/p/delayed"}]', + 'sr_multi'); + +is(get_json('/sr_pr'), '{"h":"xxx"}', 'sr_promise'); +is(get_json('/sr_options_args_pr'), '{"h":"xxx"}', 'sr_options_args_pr'); +is(get_json('/sr_options_method_pr?m=PUT'), '["PUT"]', 'sr method PUT'); +is(get_json('/sr_body_pr'), '{"a":{"b":1}}', 'sr_body_pr'); +is(get_json('/sr_js_in_subrequest_pr'), '["JS-SUB"]', 'sr_js_in_subrequest_pr'); +is(get_json('/sr_unavail_pr'), '[{"status":502,"uri":"/unavail"}]', + 'sr_unavail_pr'); + +like(http_get('/sr_detached_in_variable_handler'), qr/subrequest_var/, + 'sr_detached_in_variable_handler'); + +like(http_get('/sr_error_page'), qr/reply\.status:404/, + 'sr_error_page'); + +http_get('/sr_broken'); +http_get('/sr_in_sr'); +http_get('/sr_in_variable_handler'); +http_get('/sr_async_var'); +http_get('/sr_too_large'); +http_get('/sr_except_not_a_func'); +http_get('/sr_except_failed_to_convert_options_arg'); +http_get('/sr_uri_except'); + +is(get_json('/sr_in_sr_callback'), + '{"e":"subrequest can only be created for the primary request"}', + 'subrequest for non-primary request'); + +$t->stop(); + +ok(index($t->read_file('error.log'), 'callback is not a function') > 0, + 'subrequest cb exception'); +ok(index($t->read_file('error.log'), 'failed to convert uri arg') > 0, + 'subrequest uri exception'); +ok(index($t->read_file('error.log'), 'failed to convert options.args') > 0, + 'subrequest invalid args exception'); +ok(index($t->read_file('error.log'), 'too big subrequest response') > 0, + 'subrequest too large body'); +ok(index($t->read_file('error.log'), 'subrequest creation failed') > 0, + 'subrequest creation failed'); +ok(index($t->read_file('error.log'), + 'js subrequest: failed to get the parent context') > 0, + 'zero parent ctx'); + +ok(index($t->read_file('error.log'), 'DETACHED') > 0, + 'detached subrequest'); + +############################################################################### + +sub recode { + my $json; + eval { $json = JSON::PP::decode_json(shift) }; + + if ($@) { + return "<failed to parse JSON>"; + } + + JSON::PP->new()->canonical()->encode($json); +} + +sub get_json { + http_get(shift) =~ /\x0d\x0a?\x0d\x0a?(.*)/ms; + recode($1); +} + +############################################################################### + +sub http_daemon { + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalAddr => '127.0.0.1:' . port(8082), + 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 = ''; + + while (<$client>) { + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + + $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; + + if ($uri eq '/unfinished') { + print $client + "HTTP/1.1 200 OK" . CRLF . + "Transfer-Encoding: chunked" . CRLF . + "Content-Length: 100" . CRLF . + CRLF . + "unfinished" . CRLF; + close($client); + } + } +} + +############################################################################### diff --git a/nginx/t/js_var.t b/nginx/t/js_var.t new file mode 100644 index 00000000..4de8236c --- /dev/null +++ b/nginx/t/js_var.t @@ -0,0 +1,90 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, js_var directive. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 rewrite/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + js_var $foo; + js_var $bar a:$arg_a; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /test { + js_content test.test; + } + + location /sub { + return 200 DONE; + } + + location /dest { + return 200 $bar; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test(r) { + if (r.args.sub) { + r.subrequest('/sub') + .then(reply => { + r.variables.bar = reply.responseText; + r.internalRedirect('/dest'); + }); + + return; + } + + r.return(200, `V:\${r.variables[r.args.var]}`); + } + + export default {test}; + +EOF + +$t->try_run('no njs js_var')->plan(3); + +############################################################################### + +like(http_get('/test?var=bar&a=qq'), qr/200 OK.*V:a:qq$/s, 'default value'); +like(http_get('/test?var=foo'), qr/200 OK.*V:$/s, 'default empty value'); +like(http_get('/test?sub=1&var=bar&a=qq'), qr/200 OK.*DONE$/s, 'value set'); + +############################################################################### diff --git a/nginx/t/js_var2.t b/nginx/t/js_var2.t new file mode 100644 index 00000000..5c59d9cc --- /dev/null +++ b/nginx/t/js_var2.t @@ -0,0 +1,90 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, js_var directive in server | location contexts. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 rewrite/) + ->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; + + js_var $foo; + + location /test { + js_content test.test; + } + + location /sub { + return 200 DONE; + } + + location /dest { + js_var $bar a:$arg_a; + return 200 $bar; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test(r) { + if (r.args.sub) { + r.subrequest('/sub') + .then(reply => { + r.variables.bar = reply.responseText; + r.internalRedirect('/dest'); + }); + + return; + } + + r.return(200, `V:\${r.variables[r.args.var]}`); + } + + export default {test}; + +EOF + +$t->try_run('no njs js_var')->plan(3); + +############################################################################### + +like(http_get('/test?var=bar&a=qq'), qr/200 OK.*V:a:qq$/s, 'default value'); +like(http_get('/test?var=foo'), qr/200 OK.*V:$/s, 'default empty value'); +like(http_get('/test?sub=1&var=bar&a=qq'), qr/200 OK.*DONE$/s, 'value set'); + +############################################################################### diff --git a/nginx/t/js_variables.t b/nginx/t/js_variables.t new file mode 100644 index 00000000..f2481e0b --- /dev/null +++ b/nginx/t/js_variables.t @@ -0,0 +1,95 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for http njs module, setting nginx variables. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 rewrite/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_set $test_var test.variable; + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + set $foo test.foo_orig; + + location /var_set { + return 200 $test_var$foo; + } + + location /content_set { + js_content test.content_set; + } + + location /not_found_set { + js_content test.not_found_set; + } + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function variable(r) { + r.variables.foo = r.variables.arg_a; + return 'test_var'; + } + + function content_set(r) { + r.variables.foo = r.variables.arg_a; + r.return(200, r.variables.foo); + } + + function not_found_set(r) { + try { + r.variables.unknown = 1; + } catch (e) { + r.return(500, e); + } + } + + export default {variable, content_set, not_found_set}; + +EOF + +$t->try_run('no njs')->plan(3); + +############################################################################### + +like(http_get('/var_set?a=bar'), qr/test_varbar/, 'var set'); +like(http_get('/content_set?a=bar'), qr/bar/, 'content set'); +like(http_get('/not_found_set'), qr/variable not found/, 'not found exception'); + +############################################################################### diff --git a/nginx/t/stream_js.t b/nginx/t/stream_js.t new file mode 100644 index 00000000..ff92aad2 --- /dev/null +++ b/nginx/t/stream_js.t @@ -0,0 +1,478 @@ +#!/usr/bin/perl + +# (C) Andrey Zelenkov +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for stream njs module. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ dgram stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http proxy rewrite stream stream_return udp/) + ->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:8079; + server_name localhost; + + location /njs { + js_content test.njs; + } + + location /p/ { + proxy_pass http://127.0.0.1:8095/; + + } + + location /return { + return 200 $http_foo; + } + } +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_set $js_addr test.addr; + js_set $js_var test.variable; + js_set $js_log test.log; + js_set $js_unk test.unk; + js_set $js_req_line test.req_line; + js_set $js_sess_unk test.sess_unk; + js_set $js_async test.asyncf; + + js_import test.js; + + log_format status $server_port:$status; + + server { + listen 127.0.0.1:8080; + return $js_addr; + } + + server { + listen 127.0.0.1:8081; + return $js_log; + } + + server { + listen 127.0.0.1:8082; + return $js_var; + } + + server { + listen 127.0.0.1:8083; + return $js_unk; + } + + server { + listen 127.0.0.1:8084; + return $js_sess_unk; + } + + server { + listen 127.0.0.1:%%PORT_8985_UDP%% udp; + return $js_addr; + } + + server { + listen 127.0.0.1:8086; + js_access test.access_step; + js_preread test.preread_step; + js_filter test.filter_step; + proxy_pass 127.0.0.1:8090; + } + + server { + listen 127.0.0.1:8087; + js_access test.access_undecided; + return OK; + access_log %%TESTDIR%%/status.log status; + } + + server { + listen 127.0.0.1:8088; + js_access test.access_allow; + return OK; + access_log %%TESTDIR%%/status.log status; + } + + server { + listen 127.0.0.1:8089; + js_access test.access_deny; + return OK; + access_log %%TESTDIR%%/status.log status; + } + + server { + listen 127.0.0.1:8091; + js_preread test.preread_async; + proxy_pass 127.0.0.1:8090; + } + + server { + listen 127.0.0.1:8092; + js_preread test.preread_data; + proxy_pass 127.0.0.1:8090; + } + + server { + listen 127.0.0.1:8093; + js_preread test.preread_req_line; + return $js_req_line; + } + + server { + listen 127.0.0.1:8094; + js_filter test.filter_empty; + proxy_pass 127.0.0.1:8090; + } + + server { + listen 127.0.0.1:8095; + js_filter test.filter_header_inject; + proxy_pass 127.0.0.1:8079; + } + + server { + listen 127.0.0.1:8096; + js_filter test.filter_search; + proxy_pass 127.0.0.1:8090; + } + + server { + listen 127.0.0.1:8097; + js_access test.access_except; + proxy_pass 127.0.0.1:8090; + } + + server { + listen 127.0.0.1:8098; + js_preread test.preread_except; + proxy_pass 127.0.0.1:8090; + } + + server { + listen 127.0.0.1:8099; + js_filter test.filter_except; + proxy_pass 127.0.0.1:8090; + } + + server { + listen 127.0.0.1:8100; + return $js_async; + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function addr(s) { + return 'addr=' + s.remoteAddress; + } + + function variable(s) { + return 'variable=' + s.variables.remote_addr; + } + + function sess_unk(s) { + return 'sess_unk=' + s.unk; + } + + function log(s) { + s.log("SEE-THIS"); + } + + var res = ''; + + function access_step(s) { + res += '1'; + + setTimeout(function() { + if (s.remoteAddress.match('127.0.0.1')) { + s.allow(); + } + }, 1); + } + + function preread_step(s) { + s.on('upload', function (data) { + res += '2'; + if (res.length >= 3) { + s.done(); + } + }); + } + + function filter_step(s) { + s.on('upload', function(data, flags) { + s.send(data); + res += '3'; + }); + + s.on('download', function(data, flags) { + + if (!flags.last) { + res += '4'; + s.send(data); + + } else { + res += '5'; + s.send(res, {last:1}); + s.off('download'); + } + }); + } + + function access_undecided(s) { + s.decline(); + } + + function access_allow(s) { + if (s.remoteAddress.match('127.0.0.1')) { + s.done(); + return; + } + + s.deny(); + } + + function access_deny(s) { + if (s.remoteAddress.match('127.0.0.1')) { + s.deny(); + return; + } + + s.allow(); + } + + + function preread_async(s) { + setTimeout(function() { + s.done(); + }, 1); + } + + function preread_data(s) { + s.on('upload', function (data, flags) { + if (data.indexOf('z') != -1) { + s.done(); + } + }); + } + + var line = ''; + + function preread_req_line(s) { + s.on('upload', function (data, flags) { + var n = data.indexOf('\\n'); + if (n != -1) { + line = data.substr(0, n); + s.done(); + } + }); + } + + function req_line(s) { + return line; + } + + function filter_empty(s) { + } + + function filter_header_inject(s) { + var req = ''; + + s.on('upload', function(data, flags) { + req += data; + + var n = req.search('\\n'); + if (n != -1) { + var rest = req.substr(n + 1); + req = req.substr(0, n + 1); + + s.send(req + 'Foo: foo' + '\\r\\n' + rest, flags); + + s.off('upload'); + } + }); + } + + function filter_search(s) { + s.on('download', function(data, flags) { + var n = data.search('y'); + if (n != -1) { + s.send('z'); + } + }); + + s.on('upload', function(data, flags) { + var n = data.search('x'); + if (n != -1) { + s.send('y'); + } + }); + } + + function access_except(s) { + function done() {return s.a.a}; + + setTimeout(done, 1); + setTimeout(done, 2); + } + + function preread_except(s) { + var fs = require('fs'); + fs.readFileSync(); + } + + function filter_except(s) { + s.on('unknown', function() {}); + } + + function pr(x) { + return new Promise(resolve => {resolve(x)}).then(v => v).then(v => v); + } + + async function asyncf(s) { + const a1 = await pr(10); + const a2 = await pr(20); + + s.setReturnValue(`retval: \${a1 + a2}`); + } + + export default {njs:test_njs, addr, variable, sess_unk, log, access_step, + preread_step, filter_step, access_undecided, access_allow, + access_deny, preread_async, preread_data, preread_req_line, + req_line, filter_empty, filter_header_inject, filter_search, + access_except, preread_except, filter_except, asyncf}; + +EOF + +$t->run_daemon(\&stream_daemon, port(8090)); +$t->try_run('no stream njs available')->plan(23); +$t->waitforsocket('127.0.0.1:' . port(8090)); + +############################################################################### + +is(stream('127.0.0.1:' . port(8080))->read(), 'addr=127.0.0.1', + 's.remoteAddress'); +is(dgram('127.0.0.1:' . port(8985))->io('.'), 'addr=127.0.0.1', + 's.remoteAddress udp'); +is(stream('127.0.0.1:' . port(8081))->read(), 'undefined', 's.log'); +is(stream('127.0.0.1:' . port(8082))->read(), 'variable=127.0.0.1', + 's.variables'); +is(stream('127.0.0.1:' . port(8083))->read(), '', 'stream js unknown function'); +is(stream('127.0.0.1:' . port(8084))->read(), 'sess_unk=undefined', 's.unk'); + +is(stream('127.0.0.1:' . port(8086))->io('0'), '0122345', + 'async handlers order'); +is(stream('127.0.0.1:' . port(8087))->io('#'), 'OK', 'access_undecided'); +is(stream('127.0.0.1:' . port(8088))->io('#'), 'OK', 'access_allow'); +is(stream('127.0.0.1:' . port(8089))->io('#'), '', 'access_deny'); + +is(stream('127.0.0.1:' . port(8091))->io('#'), '#', 'preread_async'); +is(stream('127.0.0.1:' . port(8092))->io('#z'), '#z', 'preread_async_data'); +is(stream('127.0.0.1:' . port(8093))->io("xy\na"), 'xy', 'preread_req_line'); + +is(stream('127.0.0.1:' . port(8094))->io('x'), 'x', 'filter_empty'); +like(get('/p/return'), qr/foo/, 'filter_injected_header'); +is(stream('127.0.0.1:' . port(8096))->io('x'), 'z', 'filter_search'); + +stream('127.0.0.1:' . port(8097))->io('x'); +stream('127.0.0.1:' . port(8098))->io('x'); +stream('127.0.0.1:' . port(8099))->io('x'); + +is(stream('127.0.0.1:' . port(8100))->read(), 'retval: 30', 'asyncf'); + +$t->stop(); + +ok(index($t->read_file('error.log'), 'SEE-THIS') > 0, 'stream js log'); +ok(index($t->read_file('error.log'), 'at fs.readFileSync') > 0, + 'stream js_preread backtrace'); +ok(index($t->read_file('error.log'), 'at filter_except') > 0, + 'stream js_filter backtrace'); + +my @p = (port(8087), port(8088), port(8089)); +like($t->read_file('status.log'), qr/$p[0]:200/, 'status undecided'); +like($t->read_file('status.log'), qr/$p[1]:200/, 'status allow'); +like($t->read_file('status.log'), qr/$p[2]:403/, 'status deny'); + +############################################################################### + +sub stream_daemon { + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalAddr => '127.0.0.1:' . port(8090), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + local $SIG{PIPE} = 'IGNORE'; + + while (my $client = $server->accept()) { + $client->autoflush(1); + + log2c("(new connection $client)"); + + $client->sysread(my $buffer, 65536) or next; + + log2i("$client $buffer"); + + log2o("$client $buffer"); + + $client->syswrite($buffer); + + close $client; + } +} + +sub log2i { Test::Nginx::log_core('|| <<', @_); } +sub log2o { Test::Nginx::log_core('|| >>', @_); } +sub log2c { Test::Nginx::log_core('||', @_); } + +sub get { + my ($url, %extra) = @_; + + my $s = IO::Socket::INET->new( + Proto => 'tcp', + PeerAddr => '127.0.0.1:' . port(8079) + ) or die "Can't connect to nginx: $!\n"; + + return http_get($url, socket => $s); +} + +############################################################################### diff --git a/nginx/t/stream_js_buffer.t b/nginx/t/stream_js_buffer.t new file mode 100644 index 00000000..cc960136 --- /dev/null +++ b/nginx/t/stream_js_buffer.t @@ -0,0 +1,177 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for stream njs module, buffer properties. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http proxy rewrite stream stream_return/) + ->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 /njs { + js_content test.njs; + } + + location /p/ { + proxy_pass http://127.0.0.1:8085/; + } + + location /return { + return 200 'RETURN:$http_foo'; + } + } +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_import test.js; + + js_set $type test.type; + js_set $binary_var test.binary_var; + + server { + listen 127.0.0.1:8081; + return $type; + } + + server { + listen 127.0.0.1:8082; + return $binary_var; + } + + server { + listen 127.0.0.1:8083; + js_preread test.cb_mismatch; + proxy_pass 127.0.0.1:8090; + } + + server { + listen 127.0.0.1:8084; + js_preread test.cb_mismatch2; + proxy_pass 127.0.0.1:8090; + } + + server { + listen 127.0.0.1:8085; + js_filter test.header_inject; + proxy_pass 127.0.0.1:8080; + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function type(s) { + var v = s.rawVariables.remote_addr; + var type = Buffer.isBuffer(v) ? 'buffer' : (typeof v); + return type; + } + + function binary_var(s) { + var test = s.rawVariables + .binary_remote_addr.equals(Buffer.from([127,0,0,1])); + return test; + } + + function cb_mismatch(s) { + try { + s.on('upload', () => {}); + s.on('downstream', () => {}); + } catch (e) { + throw new Error(`cb_mismatch:\${e.message}`) + } + } + + function cb_mismatch2(s) { + try { + s.on('upstream', () => {}); + s.on('download', () => {}); + } catch (e) { + throw new Error(`cb_mismatch2:\${e.message}`) + } + } + + function header_inject(s) { + var req = Buffer.from([]); + + s.on('upstream', function(data, flags) { + req = Buffer.concat([req, data]); + + var n = req.indexOf('\\n'); + if (n != -1) { + var rest = req.slice(n + 1); + req = req.slice(0, n + 1); + + s.send(req, flags); + s.send('Foo: foo\\r\\n', flags); + s.send(rest, flags); + + s.off('upstream'); + } + }); + } + + export default {njs: test_njs, type, binary_var, cb_mismatch, cb_mismatch2, + header_inject}; + +EOF + +$t->try_run('no njs ngx')->plan(5); + +############################################################################### + +is(stream('127.0.0.1:' . port(8081))->read(), 'buffer', 'var type'); +is(stream('127.0.0.1:' . port(8082))->read(), 'true', 'binary var'); + +stream('127.0.0.1:' . port(8083))->io('x'); +stream('127.0.0.1:' . port(8084))->io('x'); + +like(http_get('/p/return'), qr/RETURN:foo/, 'injected header'); + +$t->stop(); + +ok(index($t->read_file('error.log'), 'cb_mismatch:mixing string and buffer') + > 0, 'cb mismatch'); +ok(index($t->read_file('error.log'), 'cb_mismatch2:mixing string and buffer') + > 0, 'cb mismatch'); + +############################################################################### diff --git a/nginx/t/stream_js_exit.t b/nginx/t/stream_js_exit.t new file mode 100644 index 00000000..a8bc34ae --- /dev/null +++ b/nginx/t/stream_js_exit.t @@ -0,0 +1,152 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for stream njs module, exit hook. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http stream/) + ->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 /njs { + js_content test.njs; + } + } +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_import test.js; + + server { + listen 127.0.0.1:8081; + js_access test.access; + js_filter test.filter; + proxy_pass 127.0.0.1:8090; + } + + server { + listen 127.0.0.1:8082; + js_access test.access; + proxy_pass 127.0.0.1:1; + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function access(s) { + njs.on('exit', () => { + var v = s.variables; + var c = `\${v.bytes_received}/\${v.bytes_sent}`; + var u = `\${v.upstream_bytes_received}/\${v.upstream_bytes_sent}`; + s.error(`s:\${s.status} C: \${c} U: \${u}`); + }); + + s.allow(); + } + + function filter(s) { + s.on('upload', (data, flags) => { + s.send(`@\${data}`, flags); + }); + + s.on('download', (data, flags) => { + s.send(data.slice(2), flags); + }); + } + + export default {njs: test_njs, access, filter}; +EOF + +$t->try_run('no stream njs available')->plan(2); + +$t->run_daemon(\&stream_daemon, port(8090)); +$t->waitforsocket('127.0.0.1:' . port(8090)); + +############################################################################### + +stream('127.0.0.1:' . port(8081))->io('###'); +stream('127.0.0.1:' . port(8082))->io('###'); + +$t->stop(); + +ok(index($t->read_file('error.log'), 's:200 C: 3/6 U: 8/4') > 0, 'normal'); +ok(index($t->read_file('error.log'), 's:502 C: 0/0 U: 0/0') > 0, 'failed conn'); + +############################################################################### + +sub stream_daemon { + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalAddr => '127.0.0.1:' . port(8090), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + local $SIG{PIPE} = 'IGNORE'; + + while (my $client = $server->accept()) { + $client->autoflush(1); + + log2c("(new connection $client)"); + + $client->sysread(my $buffer, 65536) or next; + + log2i("$client $buffer"); + + $buffer = $buffer . $buffer; + + log2o("$client $buffer"); + + $client->syswrite($buffer); + + close $client; + } +} + +sub log2i { Test::Nginx::log_core('|| <<', @_); } +sub log2o { Test::Nginx::log_core('|| >>', @_); } +sub log2c { Test::Nginx::log_core('||', @_); } + +############################################################################### diff --git a/nginx/t/stream_js_fetch.t b/nginx/t/stream_js_fetch.t new file mode 100644 index 00000000..106702dc --- /dev/null +++ b/nginx/t/stream_js_fetch.t @@ -0,0 +1,277 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for stream njs module, fetch method. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http stream/) + ->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 /njs { + js_content test.njs; + } + + location /validate { + js_content test.validate; + } + + location /success { + return 200; + } + + location /fail { + return 403; + } + } +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_import test.js; + + server { + listen 127.0.0.1:8081; + js_preread test.preread_verify; + proxy_pass 127.0.0.1:8090; + } + + server { + listen 127.0.0.1:8082; + js_filter test.filter_verify; + proxy_pass 127.0.0.1:8091; + } + + server { + listen 127.0.0.1:8083; + js_access test.access_ok; + proxy_pass 127.0.0.1:8090; + } + + server { + listen 127.0.0.1:8084; + js_access test.access_nok; + proxy_pass 127.0.0.1:8090; + } +} + +EOF + +my $p = port(8080); + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function validate(r) { + r.return((r.requestText == 'QZ') ? 200 : 403); + } + + function preread_verify(s) { + var collect = Buffer.from([]); + + s.on('upstream', async function (data, flags) { + collect = Buffer.concat([collect, data]); + + if (collect.length >= 4 && collect.readUInt16BE(0) == 0xabcd) { + s.off('upstream'); + + let reply = await ngx.fetch('http://127.0.0.1:$p/validate', + {body: collect.slice(2,4)}); + + (reply.status == 200) ? s.done(): s.deny(); + + } else if (collect.length) { + s.deny(); + } + }); + } + + function filter_verify(s) { + var collect = Buffer.from([]); + + s.on('upstream', async function (data, flags) { + collect = Buffer.concat([collect, data]); + + if (collect.length >= 4 && collect.readUInt16BE(0) == 0xabcd) { + s.off('upstream'); + + let reply = await ngx.fetch('http://127.0.0.1:$p/validate', + {body: collect.slice(2,4)}); + + if (reply.status == 200) { + s.send(collect.slice(4), flags); + + } else { + s.send("__CLOSE__", flags); + } + } + }); + } + + async function access_ok(s) { + let reply = await ngx.fetch('http://127.0.0.1:$p/success'); + + (reply.status == 200) ? s.allow(): s.deny(); + } + + async function access_nok(s) { + let reply = await ngx.fetch('http://127.0.0.1:$p/fail'); + + (reply.status == 200) ? s.allow(): s.deny(); + } + + export default {njs: test_njs, validate, preread_verify, filter_verify, + access_ok, access_nok}; +EOF + +$t->try_run('no stream njs available')->plan(9); + +$t->run_daemon(\&stream_daemon, port(8090), port(8091)); +$t->waitforsocket('127.0.0.1:' . port(8090)); +$t->waitforsocket('127.0.0.1:' . port(8091)); + +############################################################################### + +is(stream('127.0.0.1:' . port(8081))->io('###'), '', 'preread not enough'); +is(stream('127.0.0.1:' . port(8081))->io("\xAB\xCDQZ##"), "\xAB\xCDQZ##", + 'preread validated'); +is(stream('127.0.0.1:' . port(8081))->io("\xAC\xCDQZ##"), '', + 'preread invalid magic'); +is(stream('127.0.0.1:' . port(8081))->io("\xAB\xCDQQ##"), '', + 'preread validation failed'); + +TODO: { +todo_skip 'leaves coredump', 3 unless $ENV{TEST_NGINX_UNSAFE} + or has_version('0.7.7'); + +my $s = stream('127.0.0.1:' . port(8082)); +is($s->io("\xAB\xCDQZ##", read => 1), '##', 'filter validated'); +is($s->io("@@", read => 1), '@@', 'filter off'); + +is(stream('127.0.0.1:' . port(8082))->io("\xAB\xCDQQ##"), '', + 'filter validation failed'); + +} + +is(stream('127.0.0.1:' . port(8083))->io('ABC'), 'ABC', 'access fetch ok'); +is(stream('127.0.0.1:' . port(8084))->io('ABC'), '', 'access fetch nok'); + +############################################################################### + +sub has_version { + my $need = shift; + + http_get('/njs') =~ /^([.0-9]+)$/m; + + my @v = split(/\./, $1); + my ($n, $v); + + for $n (split(/\./, $need)) { + $v = shift @v || 0; + return 0 if $n > $v; + return 1 if $v > $n; + } + + return 1; +} + +############################################################################### + +sub stream_daemon { + my (@ports) = @_; + my (@socks, @clients); + + for my $port (@ports) { + 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"; + push @socks, $server; + } + + my $sel = IO::Select->new(@socks); + + local $SIG{PIPE} = 'IGNORE'; + + while (my @ready = $sel->can_read) { + foreach my $fh (@ready) { + if (grep $_ == $fh, @socks) { + my $new = $fh->accept; + $new->autoflush(1); + $sel->add($new); + + } elsif (stream_handle_client($fh) + || $fh->sockport() == port(8090)) + { + $sel->remove($fh); + $fh->close; + } + } + } +} + +sub stream_handle_client { + my ($client) = @_; + + log2c("(new connection $client)"); + + $client->sysread(my $buffer, 65536) or return 1; + + log2i("$client $buffer"); + + if ($buffer eq "__CLOSE__") { + return 1; + } + + log2o("$client $buffer"); + + $client->syswrite($buffer); + + return 0; +} + +sub log2i { Test::Nginx::log_core('|| <<', @_); } +sub log2o { Test::Nginx::log_core('|| >>', @_); } +sub log2c { Test::Nginx::log_core('||', @_); } + +############################################################################### diff --git a/nginx/t/stream_js_fetch_https.t b/nginx/t/stream_js_fetch_https.t new file mode 100644 index 00000000..056352f4 --- /dev/null +++ b/nginx/t/stream_js_fetch_https.t @@ -0,0 +1,404 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for stream njs module, fetch method, https support. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new() + ->has(qw/http http_ssl rewrite stream stream_return socket_ssl/) + ->has_daemon('openssl') + ->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 /njs { + js_content test.njs; + } + } + + server { + listen 127.0.0.1:8081 ssl default; + server_name default.example.com; + + ssl_certificate default.example.com.chained.crt; + ssl_certificate_key default.example.com.key; + + location /loc { + return 200 "You are at default.example.com."; + } + + location /success { + return 200; + } + + location /fail { + return 403; + } + + location /backend { + return 200 "BACKEND OK"; + } + } + + server { + listen 127.0.0.1:8081 ssl; + server_name 1.example.com; + + ssl_certificate 1.example.com.chained.crt; + ssl_certificate_key 1.example.com.key; + + location /loc { + return 200 "You are at 1.example.com."; + } + } +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_import test.js; + js_var $message; + + resolver 127.0.0.1:%%PORT_8981_UDP%%; + resolver_timeout 1s; + + server { + listen 127.0.0.1:8082; + js_preread test.preread; + return "default CA $message"; + } + + server { + listen 127.0.0.1:8083; + js_preread test.preread; + return "my CA $message"; + + js_fetch_ciphers HIGH:!aNull:!MD5; + js_fetch_protocols TLSv1.1 TLSv1.2; + js_fetch_trusted_certificate myca.crt; + } + + server { + listen 127.0.0.1:8084; + js_preread test.preread; + return "my CA with verify_depth=0 $message"; + + js_fetch_verify_depth 0; + js_fetch_trusted_certificate myca.crt; + } + + server { + listen 127.0.0.1:8085; + + js_access test.access_ok; + ssl_preread on; + + js_fetch_ciphers HIGH:!aNull:!MD5; + js_fetch_protocols TLSv1.1 TLSv1.2; + js_fetch_trusted_certificate myca.crt; + + proxy_pass 127.0.0.1:8081; + } + + server { + listen 127.0.0.1:8086; + + js_access test.access_nok; + ssl_preread on; + + js_fetch_ciphers HIGH:!aNull:!MD5; + js_fetch_protocols TLSv1.1 TLSv1.2; + js_fetch_trusted_certificate myca.crt; + + proxy_pass 127.0.0.1:8081; + } +} + +EOF + +my $p1 = port(8081); +my $p2 = port(8082); +my $p3 = port(8083); +my $p4 = port(8084); + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function preread(s) { + s.on('upload', function (data, flags) { + if (data.startsWith('GO')) { + s.off('upload'); + ngx.fetch('https://' + data.substring(2) + ':$p1/loc') + .then(reply => { + s.variables.message = 'https OK - ' + reply.status; + s.done(); + }) + .catch(e => { + s.variables.message = 'https NOK - ' + e.message; + s.done(); + }) + + } else if (data.length) { + s.deny(); + } + }); + } + + async function access_ok(s) { + let r = await ngx.fetch('https://default.example.com:$p1/success', + {body: s.remoteAddress}); + + (r.status == 200) ? s.allow(): s.deny(); + } + + async function access_nok(s) { + let r = await ngx.fetch('https://default.example.com:$p1/fail', + {body: s.remoteAddress}); + + (r.status == 200) ? s.allow(): s.deny(); + } + + export default {njs: test_njs, preread, access_ok, access_nok}; +EOF + +my $d = $t->testdir(); + +$t->write_file('openssl.conf', <<EOF); +[ req ] +default_bits = 2048 +encrypt_key = no +distinguished_name = req_distinguished_name +[ req_distinguished_name ] +EOF + +$t->write_file('myca.conf', <<EOF); +[ ca ] +default_ca = myca + +[ myca ] +new_certs_dir = $d +database = $d/certindex +default_md = sha256 +policy = myca_policy +serial = $d/certserial +default_days = 1 +x509_extensions = myca_extensions + +[ myca_policy ] +commonName = supplied + +[ myca_extensions ] +basicConstraints = critical,CA:TRUE +EOF + +system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=myca/ " + . "-out $d/myca.crt -keyout $d/myca.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create self-signed certificate for CA: $!\n"; + +foreach my $name ('intermediate', 'default.example.com', '1.example.com') { + system("openssl req -new " + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.csr -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate signing req for $name: $!\n"; +} + +$t->write_file('certserial', '1000'); +$t->write_file('certindex', ''); + +system("openssl ca -batch -config $d/myca.conf " + . "-keyfile $d/myca.key -cert $d/myca.crt " + . "-subj /CN=intermediate/ -in $d/intermediate.csr " + . "-out $d/intermediate.crt " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't sign certificate for intermediate: $!\n"; + +foreach my $name ('default.example.com', '1.example.com') { + system("openssl ca -batch -config $d/myca.conf " + . "-keyfile $d/intermediate.key -cert $d/intermediate.crt " + . "-subj /CN=$name/ -in $d/$name.csr -out $d/$name.crt " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't sign certificate for $name $!\n"; + $t->write_file("$name.chained.crt", $t->read_file("$name.crt") + . $t->read_file('intermediate.crt')); +} + +$t->try_run('no njs.fetch')->plan(6); + +$t->run_daemon(\&dns_daemon, port(8981), $t); +$t->waitforfile($t->testdir . '/' . port(8981)); + +############################################################################### + +like(stream("127.0.0.1:$p2")->io('GOdefault.example.com'), + qr/connect failed/s, 'stream non trusted CA'); +like(stream("127.0.0.1:$p3")->io('GOdefault.example.com'), + qr/https OK/s, 'stream trusted CA'); +like(stream("127.0.0.1:$p3")->io('GOlocalhost'), + qr/connect failed/s, 'stream wrong CN'); +like(stream("127.0.0.1:$p4")->io('GOdefaul.example.com'), + qr/connect failed/s, 'stream verify_depth too small'); + +like(https_get('default.example.com', port(8085), '/backend'), + qr!BACKEND OK!, 'access https fetch'); +is(https_get('default.example.com', port(8086), '/backend'), '<conn failed>', + 'access https fetch not'); + +############################################################################### + +sub get_ssl_socket { + my ($host, $port) = @_; + my $s; + + eval { + local $SIG{ALRM} = sub { die "timeout\n" }; + local $SIG{PIPE} = sub { die "sigpipe\n" }; + alarm(8); + $s = IO::Socket::SSL->new( + Proto => 'tcp', + PeerAddr => '127.0.0.1:' . $port, + SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), + SSL_error_trap => sub { die $_[1] } + ); + + alarm(0); + }; + + alarm(0); + + if ($@) { + log_in("died: $@"); + return undef; + } + + return $s; +} + +sub https_get { + my ($host, $port, $url) = @_; + my $s = get_ssl_socket($host, $port); + + if (!$s) { + return '<conn failed>'; + } + + return http(<<EOF, socket => $s); +GET $url HTTP/1.0 +Host: $host + +EOF +} + +############################################################################### + +sub reply_handler { + my ($recv_data, $port, %extra) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant A => 1; + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 3600); + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + + my $name = join('.', @name); + + if ($type == A) { + push @rdata, rd_addr($ttl, '127.0.0.1'); + } + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + return pack 'n3N', 0xc00c, A, IN, $ttl if $addr eq ''; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub dns_daemon { + my ($port, $t) = @_; + + my ($data, $recv_data); + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => $port, + Proto => 'udp', + ) + or die "Can't create listening socket: $!\n"; + + local $SIG{PIPE} = 'IGNORE'; + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . $port; + close $fh; + + while (1) { + $socket->recv($recv_data, 65536); + $data = reply_handler($recv_data, $port); + $socket->send($data); + } +} + +############################################################################### diff --git a/nginx/t/stream_js_fetch_init.t b/nginx/t/stream_js_fetch_init.t new file mode 100644 index 00000000..6de487da --- /dev/null +++ b/nginx/t/stream_js_fetch_init.t @@ -0,0 +1,149 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for stream njs module, Response prototype reinitialization. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http rewrite stream/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_import test.js; + + server { + listen 127.0.0.1:8081; + js_access test.access_ok; + proxy_pass 127.0.0.1:8090; + } +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /njs { + js_content test.njs; + } + + location /success { + return 200; + } + } +} + +EOF + +my $p = port(8080); + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + async function access_ok(s) { + let reply = await ngx.fetch('http://127.0.0.1:$p/success'); + + (reply.status == 200) ? s.allow(): s.deny(); + } + + export default {njs: test_njs, access_ok}; +EOF + +$t->try_run('no stream njs available')->plan(1); + +$t->run_daemon(\&stream_daemon, port(8090)); +$t->waitforsocket('127.0.0.1:' . port(8090)); + +############################################################################### + +local $TODO = 'not yet' unless has_version('0.7.9'); + +is(stream('127.0.0.1:' . port(8081))->io('ABC'), 'ABC', 'access fetch ok'); + +############################################################################### + +sub has_version { + my $need = shift; + + http_get('/njs') =~ /^([.0-9]+)$/m; + + my @v = split(/\./, $1); + my ($n, $v); + + for $n (split(/\./, $need)) { + $v = shift @v || 0; + return 0 if $n > $v; + return 1 if $v > $n; + } + + return 1; +} + +############################################################################### + +sub stream_daemon { + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalAddr => '127.0.0.1:' . port(8090), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + local $SIG{PIPE} = 'IGNORE'; + + while (my $client = $server->accept()) { + $client->autoflush(1); + + log2c("(new connection $client)"); + + $client->sysread(my $buffer, 65536) or next; + + log2i("$client $buffer"); + + log2o("$client $buffer"); + + $client->syswrite($buffer); + + close $client; + } +} + +sub log2i { Test::Nginx::log_core('|| <<', @_); } +sub log2o { Test::Nginx::log_core('|| >>', @_); } +sub log2c { Test::Nginx::log_core('||', @_); } + +############################################################################### diff --git a/nginx/t/stream_js_import.t b/nginx/t/stream_js_import.t new file mode 100644 index 00000000..70000604 --- /dev/null +++ b/nginx/t/stream_js_import.t @@ -0,0 +1,117 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for stream njs module, js_import directive. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http stream stream_return/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_set $test foo.bar.p; + + js_import lib.js; + js_import foo from ./main.js; + + server { + listen 127.0.0.1:8081; + return $test; + } + + server { + listen 127.0.0.1:8082; + js_access lib.access; + js_preread lib.preread; + js_filter lib.filter; + proxy_pass 127.0.0.1:8083; + } + + server { + listen 127.0.0.1:8083; + return "x"; + } +} + +EOF + +$t->write_file('lib.js', <<EOF); + var res = ''; + + function access(s) { + res += '1'; + s.allow(); + } + + function preread(s) { + s.on('upload', function (data) { + res += '2'; + if (res.length >= 3) { + s.done(); + } + }); + } + + function filter(s) { + s.on('upload', function(data, flags) { + s.send(data); + res += '3'; + }); + + s.on('download', function(data, flags) { + if (!flags.last) { + res += '4'; + s.send(data); + + } else { + res += '5'; + s.send(res, {last:1}); + s.off('download'); + } + }); + } + + export default {access, preread, filter}; + +EOF + +$t->write_file('main.js', <<EOF); + export default {bar: {p(s) {return "P-TEST"}}}; + +EOF + +$t->try_run('no njs available')->plan(2); + +############################################################################### + +is(stream('127.0.0.1:' . port(8081))->read(), 'P-TEST', 'foo.bar.p'); +is(stream('127.0.0.1:' . port(8082))->io('0'), 'x122345', 'lib.access'); + +############################################################################### diff --git a/nginx/t/stream_js_import2.t b/nginx/t/stream_js_import2.t new file mode 100644 index 00000000..a057c258 --- /dev/null +++ b/nginx/t/stream_js_import2.t @@ -0,0 +1,117 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for stream njs module, js_import directive in server context. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/stream stream_return/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + server { + listen 127.0.0.1:8081; + js_import foo from ./main.js; + js_set $test foo.bar.p; + return $test; + } + + server { + listen 127.0.0.1:8082; + + js_import lib.js; + + js_access lib.access; + js_preread lib.preread; + js_filter lib.filter; + proxy_pass 127.0.0.1:8083; + } + + server { + listen 127.0.0.1:8083; + return "x"; + } +} + +EOF + +$t->write_file('lib.js', <<EOF); + var res = ''; + + function access(s) { + res += '1'; + s.allow(); + } + + function preread(s) { + s.on('upload', function (data) { + res += '2'; + if (res.length >= 3) { + s.done(); + } + }); + } + + function filter(s) { + s.on('upload', function(data, flags) { + s.send(data); + res += '3'; + }); + + s.on('download', function(data, flags) { + if (!flags.last) { + res += '4'; + s.send(data); + + } else { + res += '5'; + s.send(res, {last:1}); + s.off('download'); + } + }); + } + + export default {access, preread, filter}; + +EOF + +$t->write_file('main.js', <<EOF); + export default {bar: {p(s) {return "P-TEST"}}}; + +EOF + +$t->try_run('no njs available')->plan(2); + +############################################################################### + +is(stream('127.0.0.1:' . port(8081))->read(), 'P-TEST', 'foo.bar.p'); +is(stream('127.0.0.1:' . port(8082))->io('0'), 'x122345', 'lib.access'); + +############################################################################### diff --git a/nginx/t/stream_js_ngx.t b/nginx/t/stream_js_ngx.t new file mode 100644 index 00000000..de6dc58a --- /dev/null +++ b/nginx/t/stream_js_ngx.t @@ -0,0 +1,94 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for stream njs module, ngx object. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/stream stream_return/) + ->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 /njs { + js_content test.njs; + } + } +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_import test.js; + + js_set $log test.log; + + server { + listen 127.0.0.1:8081; + return $log; + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function log(s) { + ngx.log(ngx.INFO, `ngx.log:FOO`); + ngx.log(ngx.WARN, `ngx.log:BAR`); + ngx.log(ngx.ERR, `ngx.log:BAZ`); + return 'OK'; + } + + export default {njs: test_njs, log}; + +EOF + +$t->try_run('no njs ngx')->plan(4); + +############################################################################### + +is(stream('127.0.0.1:' . port(8081))->read(), 'OK', 'log var'); + +$t->stop(); + +like($t->read_file('error.log'), qr/\[info\].*ngx.log:FOO/, 'ngx.log info'); +like($t->read_file('error.log'), qr/\[warn\].*ngx.log:BAR/, 'ngx.log warn'); +like($t->read_file('error.log'), qr/\[error\].*ngx.log:BAZ/, 'ngx.log err'); + +############################################################################### diff --git a/nginx/t/stream_js_object.t b/nginx/t/stream_js_object.t new file mode 100644 index 00000000..504b9348 --- /dev/null +++ b/nginx/t/stream_js_object.t @@ -0,0 +1,98 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for stream njs module, stream session object. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/stream stream_return/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_set $test test.test; + + js_import test.js; + + server { + listen 127.0.0.1:8081; + return $test$status; + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function to_string(s) { + return s.toString() === '[object Stream Session]'; + } + + function define_prop(s) { + Object.defineProperty(s.variables, 'status', {value:400}); + return s.variables.status == 400; + } + + function in_operator(s) { + return ['status', 'unknown'] + .map(v=>v in s.variables) + .toString() === 'true,false'; + } + + function redefine_proto(s) { + s[0] = 'a'; + s[1] = 'b'; + s.length = 2; + Object.setPrototypeOf(s, Array.prototype); + return s.join('|') === 'a|b'; + } + + function get_own_prop_descs(s) { + return Object.getOwnPropertyDescriptors(s)['on'].value === s.on; + } + + function test(s) { + return [ to_string, + define_prop, + in_operator, + redefine_proto, + get_own_prop_descs, + ].every(v=>v(s)); + } + + export default {test}; + +EOF + +$t->try_run('no njs stream session object')->plan(1); + +############################################################################### + +is(stream('127.0.0.1:' . port(8081))->read(), 'true400', 'var set'); + +############################################################################### diff --git a/nginx/t/stream_js_preload_object.t b/nginx/t/stream_js_preload_object.t new file mode 100644 index 00000000..c28c401a --- /dev/null +++ b/nginx/t/stream_js_preload_object.t @@ -0,0 +1,122 @@ +#!/usr/bin/perl + +# (C) Vadim Zhestikov +# (C) Nginx, Inc. + +# Tests for stream njs module, js_preload_object directive. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/stream stream_return/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_preload_object g1 from g.json; + + js_set $test foo.bar.p; + + js_import lib.js; + js_import foo from ./main.js; + + server { + listen 127.0.0.1:8081; + return $test; + } + + server { + listen 127.0.0.1:8082; + js_access lib.access; + js_preread lib.preread; + js_filter lib.filter; + proxy_pass 127.0.0.1:8083; + } + + server { + listen 127.0.0.1:8083; + return "x"; + } +} + +EOF + +$t->write_file('lib.js', <<EOF); + var res = ''; + + function access(s) { + res += g1.a; + s.allow(); + } + + function preread(s) { + s.on('upload', function (data) { + res += g1.b[1]; + if (res.length >= 3) { + s.done(); + } + }); + } + + function filter(s) { + s.on('upload', function(data, flags) { + s.send(data); + res += g1.c.prop[0].a; + }); + + s.on('download', function(data, flags) { + if (!flags.last) { + res += g1.b[3]; + s.send(data); + + } else { + res += g1.b[4]; + s.send(res, {last:1}); + s.off('download'); + } + }); + } + + export default {access, preread, filter}; + +EOF + +$t->write_file('main.js', <<EOF); + export default {bar: {p(s) {return g1.b[2]}}}; + +EOF + +$t->write_file('g.json', + '{"a":1, "b":[1,2,"element",4,5], "c":{"prop":[{"a":3}]}}'); + +$t->try_run('no js_preload_object available')->plan(2); + +############################################################################### + +is(stream('127.0.0.1:' . port(8081))->read(), 'element', 'foo.bar.p'); +is(stream('127.0.0.1:' . port(8082))->io('0'), 'x122345', 'lib.access'); + +############################################################################### diff --git a/nginx/t/stream_js_send.t b/nginx/t/stream_js_send.t new file mode 100644 index 00000000..318be6a6 --- /dev/null +++ b/nginx/t/stream_js_send.t @@ -0,0 +1,186 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for s.send() in stream njs module. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http stream/) + ->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 /njs { + js_content test.njs; + } + } +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_import test.js; + + server { + listen 127.0.0.1:8081; + js_filter test.filter; + proxy_pass 127.0.0.1:8090; + } + + server { + listen 127.0.0.1:8082; + js_filter test.filter_direct; + proxy_pass 127.0.0.1:8090; + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function test_njs(r) { + r.return(200, njs.version); + } + + function filter(s) { + s.on("upload", async (data, flags) => { + s.send("__HANDSHAKE__", flags); + + const p = new Promise((resolve, reject) => { + s.on("download", (data, flags) => { + s.off("download"); + resolve(data); + }); + }); + + s.off("upload"); + + const handshakeResponse = await p; + if (handshakeResponse != '__HANDSHAKE_RESPONSE__') { + throw `Handshake failed: \${handshakeResponse}`; + } + + s.send(data, flags); + }); + } + + function filter_direct(s) { + s.on("upload", async (data, flags) => { + s.sendUpstream("__HANDSHAKE__", flags); + + const p = new Promise((resolve, reject) => { + s.on("download", (data, flags) => { + s.off("download"); + resolve(data); + }); + }); + + s.off("upload"); + + const handshakeResponse = await p; + if (handshakeResponse != '__HANDSHAKE_RESPONSE__') { + throw `Handshake failed: \${handshakeResponse}`; + } + + s.sendDownstream('xxx', flags); + s.sendUpstream(data, flags); + }); + } + + export default {njs:test_njs, filter, filter_direct}; + +EOF + +$t->run_daemon(\&stream_daemon, port(8090)); +$t->try_run('no stream njs available')->plan(2); +$t->waitforsocket('127.0.0.1:' . port(8090)); + +############################################################################### + +is(stream('127.0.0.1:' . port(8081))->io('abc'), 'ABC', + 'async filter');; +is(stream('127.0.0.1:' . port(8082))->io('abc'), 'xxxABC', + 'async filter direct'); + +$t->stop(); + +############################################################################### + +sub stream_daemon { + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalAddr => '127.0.0.1:' . port(8090), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + local $SIG{PIPE} = 'IGNORE'; + + while (my $client = $server->accept()) { + $client->autoflush(1); + + log2c("(new connection $client)"); + + $client->sysread(my $buffer, 65536) or next; + + log2i("$client $buffer"); + + if ($buffer ne "__HANDSHAKE__") { + $buffer = "__HANDSHAKE_INVALID__"; + log2o("$client $buffer"); + $client->syswrite($buffer); + + close $client; + } + + $buffer = "__HANDSHAKE_RESPONSE__"; + log2o("$client $buffer"); + $client->syswrite($buffer); + + $client->sysread($buffer, 65536) or next; + + $buffer = uc($buffer); + log2o("$client $buffer"); + $client->syswrite($buffer); + + close $client; + } +} + +sub log2i { Test::Nginx::log_core('|| <<', @_); } +sub log2o { Test::Nginx::log_core('|| >>', @_); } +sub log2c { Test::Nginx::log_core('||', @_); } + +############################################################################### diff --git a/nginx/t/stream_js_var.t b/nginx/t/stream_js_var.t new file mode 100644 index 00000000..cde2dc9f --- /dev/null +++ b/nginx/t/stream_js_var.t @@ -0,0 +1,75 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for stream njs module, js_var directive. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/stream stream_return/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_import test.js; + + js_var $foo; + js_var $bar a:$remote_addr; + js_set $var test.varr; + + server { + listen 127.0.0.1:8081; + return $bar$foo; + } + + server { + listen 127.0.0.1:8082; + return $var$foo; + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function varr(s) { + s.variables.foo = 'xxx'; + return ''; + } + + export default {varr}; +EOF + +$t->try_run('no stream js_var')->plan(2); + +############################################################################### + +is(stream('127.0.0.1:' . port(8081))->io('###'), 'a:127.0.0.1', + 'default value'); +is(stream('127.0.0.1:' . port(8082))->io('###'), 'xxx', 'value set'); + +############################################################################### diff --git a/nginx/t/stream_js_var2.t b/nginx/t/stream_js_var2.t new file mode 100644 index 00000000..71cee1e3 --- /dev/null +++ b/nginx/t/stream_js_var2.t @@ -0,0 +1,75 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for stream njs module, js_var directive in server context. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/stream stream_return/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_import test.js; + + js_var $foo; + + server { + listen 127.0.0.1:8081; + js_var $bar a:$remote_addr; + return $bar$foo; + } + + server { + listen 127.0.0.1:8082; + js_set $var test.varr; + return $var$foo; + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function varr(s) { + s.variables.foo = 'xxx'; + return ''; + } + + export default {varr}; +EOF + +$t->try_run('no stream js_var')->plan(2); + +############################################################################### + +is(stream('127.0.0.1:' . port(8081))->io('###'), 'a:127.0.0.1', + 'default value'); +is(stream('127.0.0.1:' . port(8082))->io('###'), 'xxx', 'value set'); + +############################################################################### diff --git a/nginx/t/stream_js_variables.t b/nginx/t/stream_js_variables.t new file mode 100644 index 00000000..29e6c33e --- /dev/null +++ b/nginx/t/stream_js_variables.t @@ -0,0 +1,84 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) Nginx, Inc. + +# Tests for stream njs module, setting nginx variables. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/stream stream_return/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + js_set $test_var test.variable; + js_set $test_not_found test.not_found; + + js_import test.js; + + server { + listen 127.0.0.1:8081; + return $test_var$status; + } + + server { + listen 127.0.0.1:8082; + return $test_not_found; + } +} + +EOF + +$t->write_file('test.js', <<EOF); + function variable(s) { + s.variables.status = 400; + return 'test_var'; + } + + function not_found(s) { + try { + s.variables.unknown = 1; + } catch (e) { + return 'not_found'; + } + } + + export default {variable, not_found}; + +EOF + +$t->try_run('no stream njs available')->plan(2); + +############################################################################### + +is(stream('127.0.0.1:' . port(8081))->read(), 'test_var400', 'var set'); +is(stream('127.0.0.1:' . port(8082))->read(), 'not_found', 'not found set'); + +$t->stop(); + +############################################################################### |