This commit is contained in:
VadimZhestikov 2026-05-13 09:08:42 +08:00 committed by GitHub
commit e58800e60d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 769 additions and 20 deletions

View file

@ -10,9 +10,12 @@
#define NGX_SYSLOG_MAX_STR \
NGX_MAX_ERROR_STR + sizeof("<255>Jan 01 00:00:00 ") - 1 \
NGX_MAX_ERROR_STR + sizeof("<255>1 ") - 1 \
+ sizeof("1970-09-28T12:00:00.000+06:00") - 1 + 1 /* space */ \
+ (NGX_MAXHOSTNAMELEN - 1) + 1 /* space */ \
+ 32 /* tag */ + 2 /* colon, space */
+ 48 /* APP-NAME/TAG */ + 1 /* space */ \
+ NGX_INT64_LEN /* PROCID */ + 1 /* space */ \
+ 32 /* MSGID */ + sizeof(" - ") - 1
static char *ngx_syslog_parse_args(ngx_conf_t *cf, ngx_syslog_peer_t *peer);
@ -40,6 +43,8 @@ static ngx_event_t ngx_syslog_dummy_event;
char *
ngx_syslog_process_conf(ngx_conf_t *cf, ngx_syslog_peer_t *peer)
{
u_char ch;
ngx_uint_t j;
ngx_pool_cleanup_t *cln;
peer->facility = NGX_CONF_UNSET_UINT;
@ -49,6 +54,78 @@ ngx_syslog_process_conf(ngx_conf_t *cf, ngx_syslog_peer_t *peer)
return NGX_CONF_ERROR;
}
if (peer->tag.data != NULL) {
if (peer->rfc5424) {
/*
* RFC 5424: APP-NAME is at most 48 printable US-ASCII
* characters (0x21-0x7E; space and controls are excluded).
*/
for (j = 0; j < peer->tag.len; j++) {
ch = peer->tag.data[j];
if (ch < '!' || ch > '~') {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"syslog \"tag\" must contain "
"printable US-ASCII characters");
return NGX_CONF_ERROR;
}
}
} else {
/*
* RFC 3164: the TAG is a string of ABNF alphanumeric
* characters that MUST NOT exceed 32 characters.
*/
if (peer->tag.len > 32) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"syslog tag length exceeds 32");
return NGX_CONF_ERROR;
}
for (j = 0; j < peer->tag.len; j++) {
ch = ngx_tolower(peer->tag.data[j]);
if (ch < '0'
|| (ch > '9' && ch < 'a' && ch != '_')
|| ch > 'z')
{
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"syslog \"tag\" only allows "
"alphanumeric characters "
"and underscore");
return NGX_CONF_ERROR;
}
}
}
}
if (peer->msgid.data != NULL) {
if (!peer->rfc5424) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"syslog \"msgid\" requires rfc=rfc5424");
return NGX_CONF_ERROR;
}
/*
* RFC 5424 s6.2.7: MSGID consists of printable US-ASCII
* characters (0x21-0x7E; space and controls are excluded).
*/
for (j = 0; j < peer->msgid.len; j++) {
ch = peer->msgid.data[j];
if (ch < '!' || ch > '~') {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"syslog \"msgid\" must contain "
"printable US-ASCII characters");
return NGX_CONF_ERROR;
}
}
}
if (peer->server.sockaddr == NULL) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"no syslog server specified");
@ -67,6 +144,10 @@ ngx_syslog_process_conf(ngx_conf_t *cf, ngx_syslog_peer_t *peer)
ngx_str_set(&peer->tag, "nginx");
}
if (peer->msgid.data == NULL) {
ngx_str_set(&peer->msgid, "-");
}
peer->hostname = &cf->cycle->hostname;
peer->logp = &cf->cycle->new_log;
@ -92,7 +173,7 @@ ngx_syslog_process_conf(ngx_conf_t *cf, ngx_syslog_peer_t *peer)
static char *
ngx_syslog_parse_args(ngx_conf_t *cf, ngx_syslog_peer_t *peer)
{
u_char *p, *comma, c;
u_char *p, *comma;
size_t len;
ngx_str_t *value;
ngx_url_t u;
@ -188,30 +269,66 @@ ngx_syslog_parse_args(ngx_conf_t *cf, ngx_syslog_peer_t *peer)
}
/*
* RFC 3164: the TAG is a string of ABNF alphanumeric characters
* that MUST NOT exceed 32 characters.
* Character set and maximum length depend on the syslog
* protocol version (rfc= parameter) and are validated in
* ngx_syslog_process_conf() once all parameters are known.
* RFC 5424 APP-NAME allows up to 48 printable US-ASCII
* characters; RFC 3164 TAG allows up to 32 alphanumeric
* characters. Reject anything that exceeds the larger
* of the two limits here so that the later check can rely
* on the data fitting into the statically-sized buffers.
*/
if (len - 4 > 32) {
if (len - 4 > 48) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"syslog tag length exceeds 32");
"syslog tag length exceeds 48");
return NGX_CONF_ERROR;
}
for (i = 4; i < len; i++) {
c = ngx_tolower(p[i]);
if (c < '0' || (c > '9' && c < 'a' && c != '_') || c > 'z') {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"syslog \"tag\" only allows "
"alphanumeric characters "
"and underscore");
return NGX_CONF_ERROR;
}
}
peer->tag.data = p + 4;
peer->tag.len = len - 4;
} else if (ngx_strncmp(p, "rfc=", 4) == 0) {
if (peer->rfc_set) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"duplicate syslog \"rfc\"");
return NGX_CONF_ERROR;
}
peer->rfc_set = 1;
if (ngx_strcmp(p + 4, "rfc5424") == 0) {
peer->rfc5424 = 1;
} else if (ngx_strcmp(p + 4, "rfc3164") != 0) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"unknown syslog \"rfc\" value \"%s\"",
p + 4);
return NGX_CONF_ERROR;
}
} else if (ngx_strncmp(p, "msgid=", 6) == 0) {
if (peer->msgid.data != NULL) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"duplicate syslog \"msgid\"");
return NGX_CONF_ERROR;
}
/*
* RFC 5424 s6.2.7: MSGID is at most 32 printable US-ASCII
* characters. Character set and protocol-version constraints
* are validated in ngx_syslog_process_conf() once all
* parameters are known.
*/
if (len - 6 > 32) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"syslog msgid length exceeds 32");
return NGX_CONF_ERROR;
}
peer->msgid.data = p + 6;
peer->msgid.len = len - 6;
} else if (len == 10 && ngx_strncmp(p, "nohostname", 10) == 0) {
peer->nohostname = 1;
@ -237,10 +354,56 @@ ngx_syslog_parse_args(ngx_conf_t *cf, ngx_syslog_peer_t *peer)
u_char *
ngx_syslog_add_header(ngx_syslog_peer_t *peer, u_char *buf)
{
ngx_uint_t pri;
ngx_uint_t pri;
ngx_str_t datetime, tz;
ngx_time_t *tp;
pri = peer->facility * 8 + peer->severity;
if (peer->rfc5424) {
/*
* RFC 5424 HEADER: VERSION SP TIMESTAMP SP HOSTNAME SP
* APP-NAME SP PROCID SP MSGID SP STRUCTURED-DATA SP
*
* TIMESTAMP is formatted as an ISO 8601 date-time with
* millisecond precision and UTC offset, e.g.:
* 2003-10-11T22:14:15.003+05:30
*
* The date, time, and UTC offset are taken from the
* ngx_cached_http_log_iso8601 cache ("YYYY-MM-DDTHH:MM:SS+/-HH:MM",
* 25 bytes). The first 19 bytes are "YYYY-MM-DDTHH:MM:SS"
* and the trailing 6 bytes are "+/-HH:MM". The millisecond
* field is read live from ngx_timeofday() so that it reflects
* the current event-loop tick rather than the start of the
* current second.
*
* PROCID is the nginx process PID. MSGID defaults to the nil
* value "-" and can be overridden with the "msgid=" parameter.
* STRUCTURED-DATA is always the nil value "-".
*/
tp = ngx_timeofday();
datetime.data = ngx_cached_http_log_iso8601.data;
datetime.len = sizeof("1970-09-28T12:00:00") - 1;
tz.data = ngx_cached_http_log_iso8601.data
+ sizeof("1970-09-28T12:00:00") - 1;
tz.len = sizeof("+06:00") - 1;
if (peer->nohostname) {
return ngx_sprintf(buf, "<%ui>1 %V.%03ui%V - %V %P %V - ",
pri, &datetime, tp->msec, &tz,
&peer->tag, ngx_pid, &peer->msgid);
}
return ngx_sprintf(buf, "<%ui>1 %V.%03ui%V %V %V %P %V - ",
pri, &datetime, tp->msec, &tz,
peer->hostname, &peer->tag, ngx_pid,
&peer->msgid);
}
if (peer->nohostname) {
return ngx_sprintf(buf, "<%ui>%V %V: ", pri, &ngx_cached_syslog_time,
&peer->tag);

View file

@ -12,6 +12,7 @@ typedef struct {
ngx_uint_t facility;
ngx_uint_t severity;
ngx_str_t tag;
ngx_str_t msgid;
ngx_str_t *hostname;
@ -23,6 +24,8 @@ typedef struct {
unsigned busy:1;
unsigned nohostname:1;
unsigned rfc5424:1;
unsigned rfc_set:1;
} ngx_syslog_peer_t;

276
t/syslog_rfc5424.t Normal file
View file

@ -0,0 +1,276 @@
#!/usr/bin/perl
# (C) Nginx, Inc.
# Tests for RFC 5424 syslog message format.
###############################################################################
use warnings;
use strict;
use Test::More;
use IO::Select;
use IO::Socket::INET;
BEGIN { use FindBin; chdir($FindBin::Bin); }
use lib 'lib';
use Test::Nginx;
###############################################################################
select STDERR; $| = 1;
select STDOUT; $| = 1;
plan(skip_all => 'win32') if $^O eq 'MSWin32';
my $t = Test::Nginx->new()->has(qw/http/)->plan(23);
###############################################################################
$t->write_file_expand('nginx.conf', <<'EOF');
%%TEST_GLOBALS%%
error_log syslog:server=127.0.0.1:%%PORT_8981_UDP%%,rfc=rfc5424 info;
daemon off;
events {
}
http {
%%TEST_GLOBALS_HTTP%%
log_format logf "$uri:$status";
server {
listen 127.0.0.1:8080;
server_name localhost;
# RFC 5424 access log — basic format check
location /a5424 {
access_log syslog:server=127.0.0.1:%%PORT_8982_UDP%%,rfc=rfc5424
logf;
}
# RFC 5424 nohostname — HOSTNAME must be the nil value "-"
location /nohostname5424 {
access_log
syslog:server=127.0.0.1:%%PORT_8982_UDP%%,rfc=rfc5424,nohostname
logf;
}
# RFC 5424 error log with custom tag (hyphen) and facility
location /e5424 {
error_log
syslog:server=127.0.0.1:%%PORT_8982_UDP%%,rfc=rfc5424,tag=my-app,facility=user;
}
# RFC 5424 access log with explicit msgid=
location /a5424_msgid {
access_log syslog:server=127.0.0.1:%%PORT_8982_UDP%%,rfc=rfc5424,msgid=MYAPP
logf;
}
# RFC 3164 access log — verify backward compatibility
location /a3164 {
access_log syslog:server=127.0.0.1:%%PORT_8983_UDP%% logf;
}
}
}
EOF
# Port 8981: background daemon that writes global error_log messages to a file.
# Port 8982: the test binds this socket directly after run().
# Port 8983: the test binds this socket directly after run().
$t->run_daemon(\&syslog_daemon, port(8981), $t, 's_glob.log');
$t->waitforfile($t->testdir() . '/s_glob.log');
$t->run();
###############################################################################
my $s5424 = IO::Socket::INET->new(
Proto => 'udp',
LocalAddr => '127.0.0.1:' . port(8982)
) or die "Can't open syslog socket (8982): $!";
my $s3164 = IO::Socket::INET->new(
Proto => 'udp',
LocalAddr => '127.0.0.1:' . port(8983)
) or die "Can't open syslog socket (8983): $!";
###############################################################################
# RFC 5424 access log — full field-by-field check
parse_rfc5424_message('access_log', get_syslog($s5424, '/a5424'));
# RFC 5424 with nohostname — HOSTNAME field must be nil "-"
my $msg = get_syslog($s5424, '/nohostname5424');
like($msg,
qr/^<\d+>1\s # PRI + VERSION
\S+\s # TIMESTAMP
-\s # HOSTNAME = nil "-"
\S+\s # APP-NAME
\d+\s # PROCID
-\s-\s/x,
'rfc5424 nohostname: HOSTNAME is nil "-"');
# RFC 5424 error log — custom hyphenated tag (only valid in rfc5424)
$msg = get_syslog($s5424, '/e5424');
like($msg, qr/my-app/, 'rfc5424: hyphenated tag present in APP-NAME');
# RFC 5424 facility=user must be encoded as facility 1 in PRI
my ($pri) = $msg =~ /^<(\d+)>/;
my $fac = ($pri & 0x03f8) >> 3;
is($fac, 1, 'rfc5424: facility=user (1) encoded in PRI');
# RFC 5424 MSGID field: explicit msgid= value appears in position 6 of the header
$msg = get_syslog($s5424, '/a5424_msgid');
my ($msgid_field) = $msg =~ /^<\d+>1\s\S+\s\S+\s\S+\s\d+\s(\S+)\s/;
is($msgid_field, 'MYAPP', 'rfc5424: explicit msgid= appears in MSGID field');
# Without msgid=, the MSGID field defaults to nil "-"
$msg = get_syslog($s5424, '/a5424');
($msgid_field) = $msg =~ /^<\d+>1\s\S+\s\S+\s\S+\s\d+\s(\S+)\s/;
is($msgid_field, '-', 'rfc5424: default MSGID is nil "-"');
# Global error_log uses rfc5424 — check via background-daemon log file
http_get('/a5424');
my $glob = '';
for (1 .. 50) {
select undef, undef, undef, 0.1;
$glob = $t->read_file('s_glob.log');
last if $glob;
}
like($glob, qr/^<\d+>1\s/m, 'rfc5424 global error_log: VERSION "1" present');
# Millisecond field is live (ngx_timeofday()->msec, not once-per-second cache).
# Send several messages with small gaps and verify that the ms field is not
# permanently stuck at "000" as it would be with a second-boundary-only cache.
my @ms_vals;
for (1..10) {
select undef, undef, undef, 0.02; # 20 ms gap
my $m = get_syslog($s5424, '/a5424');
my ($ms_field) = $m =~ /T\d{2}:\d{2}:\d{2}\.(\d{3})/;
push @ms_vals, $ms_field if defined $ms_field;
}
ok((grep { $_ ne '000' } @ms_vals) > 0,
'rfc5424: millisecond field is not always zero (live ngx_timeofday)');
# RFC 3164 format is unchanged (backward compatibility)
$msg = get_syslog($s3164, '/a3164');
like($msg,
qr/^<\d+> # PRI (no VERSION field)
[A-Z][a-z]{2}\s # mon (BSD syslog timestamp)
[ \d]\d\s\d{2}:\d{2}:\d{2}\s # day HH:MM:SS
/x,
'rfc3164: BSD syslog timestamp unchanged');
unlike($msg, qr/^<\d+>1\s/, 'rfc3164: no VERSION field');
###############################################################################
sub get_syslog {
my ($sock, $uri) = @_;
my $data = '';
http_get($uri);
IO::Select->new($sock)->can_read(1);
while (IO::Select->new($sock)->can_read(0.1)) {
my $buf;
sysread($sock, $buf, 4096);
$data .= $buf;
}
return $data;
}
# Validate that $line looks like a valid RFC 5424 message and run
# 14 individual Test::More assertions, one per protocol field.
sub parse_rfc5424_message {
my ($desc, $line) = @_;
unless ($line) {
fail("$desc: no syslog message received");
return;
}
# RFC 5424 SYSLOG-MSG:
# <PRI>VERSION SP TIMESTAMP SP HOSTNAME SP APP-NAME SP
# PROCID SP MSGID SP STRUCTURED-DATA SP MSG
my ($pri, $ts, $host, $app, $pid, $msgid, $sd, $msg) =
$line =~ /^<(\d{1,3})> # PRI
1\s # VERSION (literal "1")
(\S+)\s # TIMESTAMP
(\S+)\s # HOSTNAME
(\S+)\s # APP-NAME
(\S+)\s # PROCID
(\S+)\s # MSGID
(\S+)\s # STRUCTURED-DATA
(.*)/x; # MSG
ok(defined($pri), "$desc: has PRI");
my $sev = $pri & 0x07;
my $fac = ($pri & 0x03f8) >> 3;
ok($sev >= 0 && $sev <= 7, "$desc: severity in PRI is 0-7");
ok($fac >= 0 && $fac < 24, "$desc: facility in PRI is 0-23");
ok(defined($ts), "$desc: has TIMESTAMP");
like($ts,
qr/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/,
"$desc: TIMESTAMP is ISO 8601 with ms and tz offset");
ok(defined($host), "$desc: has HOSTNAME");
ok(length($host) > 0 && $host ne '-', "$desc: HOSTNAME is non-nil");
ok(defined($app), "$desc: has APP-NAME");
like($app, qr/^[!-~]+$/, "$desc: APP-NAME is printable US-ASCII");
ok(defined($pid), "$desc: has PROCID");
like($pid, qr/^\d+$/, "$desc: PROCID is a decimal integer");
is($msgid, '-', "$desc: MSGID is nil");
is($sd, '-', "$desc: STRUCTURED-DATA is nil");
ok(defined($msg) && length($msg) > 0, "$desc: MSG is non-empty");
}
###############################################################################
sub syslog_daemon {
my ($port, $t, $file) = @_;
my $s = IO::Socket::INET->new(
Proto => 'udp',
LocalAddr => "127.0.0.1:$port"
);
open my $fh, '>', $t->testdir() . '/' . $file;
select $fh; $| = 1;
while (1) {
my $buffer;
$s->recv($buffer, 4096);
print $fh $buffer . "\n";
}
}
###############################################################################

139
t/syslog_rfc5424_config.t Normal file
View file

@ -0,0 +1,139 @@
#!/usr/bin/perl
# (C) Nginx, Inc.
# Tests for "rfc=" parameter of the syslog directive.
# Uses "nginx -t" config-check mode; no server is started.
###############################################################################
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;
plan(skip_all => 'win32') if $^O eq 'MSWin32';
my $t = Test::Nginx->new();
###############################################################################
# Write a minimal nginx.conf containing $syslog_param as a global error_log
# directive and run "nginx -t" against it. Returns combined stdout+stderr.
sub config_check {
my ($syslog_param) = @_;
$t->write_file_expand('nginx.conf', <<"EOF");
%%TEST_GLOBALS%%
error_log $syslog_param info;
daemon off;
events {
}
EOF
my $testdir = $t->testdir();
return qx{$Test::Nginx::NGINX -t -p $testdir/ -c nginx.conf 2>&1};
}
###############################################################################
my $out;
# rfc=rfc3164 is the default and must be accepted explicitly.
$out = config_check('syslog:server=127.0.0.1:5140,rfc=rfc3164');
like($out, qr/test is successful/i, 'rfc=rfc3164 accepted');
# rfc=rfc5424 must be accepted.
$out = config_check('syslog:server=127.0.0.1:5140,rfc=rfc5424');
like($out, qr/test is successful/i, 'rfc=rfc5424 accepted');
# An unknown rfc= value must be rejected with a descriptive error.
$out = config_check('syslog:server=127.0.0.1:5140,rfc=rfc9999');
like($out, qr/unknown syslog "rfc" value/, 'rfc=rfc9999 rejected');
# A hyphenated tag is printable ASCII and must be accepted with rfc5424.
$out = config_check('syslog:server=127.0.0.1:5140,rfc=rfc5424,tag=my-app');
like($out, qr/test is successful/i, 'rfc5424: hyphenated tag accepted');
# The same hyphenated tag must be rejected with rfc3164.
$out = config_check('syslog:server=127.0.0.1:5140,rfc=rfc3164,tag=my-app');
like($out, qr/only allows alphanumeric/, 'rfc3164: hyphenated tag rejected');
# A tag with a dot is printable ASCII and must be accepted with rfc5424.
$out = config_check('syslog:server=127.0.0.1:5140,rfc=rfc5424,tag=nginx.1');
like($out, qr/test is successful/i, 'rfc5424: dot in tag accepted');
# A 33-character tag is within the rfc5424 limit (48) and must be accepted.
my $tag33 = 'a' x 33;
$out = config_check("syslog:server=127.0.0.1:5140,rfc=rfc5424,tag=$tag33");
like($out, qr/test is successful/i, 'rfc5424: 33-char tag accepted');
# The same 33-character tag exceeds the rfc3164 limit (32) and must fail.
$out = config_check("syslog:server=127.0.0.1:5140,rfc=rfc3164,tag=$tag33");
like($out, qr/tag length exceeds 32/, 'rfc3164: 33-char tag rejected');
# A 49-character tag exceeds the maximum of both protocol versions (48).
my $tag49 = 'a' x 49;
$out = config_check("syslog:server=127.0.0.1:5140,rfc=rfc5424,tag=$tag49");
like($out, qr/tag length exceeds 48/, 'rfc5424: 49-char tag rejected');
$out = config_check("syslog:server=127.0.0.1:5140,rfc=rfc3164,tag=$tag49");
like($out, qr/tag length exceeds 48/, 'rfc3164: 49-char tag rejected');
# msgid= is accepted with rfc=rfc5424.
$out = config_check('syslog:server=127.0.0.1:5140,rfc=rfc5424,msgid=MYAPP');
like($out, qr/test is successful/i, 'rfc5424: msgid= accepted');
# msgid= without rfc=rfc5424 must be rejected.
$out = config_check('syslog:server=127.0.0.1:5140,msgid=MYAPP');
like($out, qr/requires rfc=rfc5424/, 'msgid= without rfc5424 rejected');
# msgid= with a non-ASCII byte (>0x7E) must be rejected.
# Space cannot be tested this way because nginx's config parser splits on
# whitespace before syslog parsing sees the value.
$out = config_check("syslog:server=127.0.0.1:5140,rfc=rfc5424,msgid=MY\xc3APP");
like($out, qr/printable US-ASCII/, 'msgid= with non-ASCII byte rejected');
# msgid= must not exceed 32 characters.
my $msgid33 = 'x' x 33;
$out = config_check("syslog:server=127.0.0.1:5140,rfc=rfc5424,msgid=$msgid33");
like($out, qr/msgid length exceeds 32/, 'msgid= 33-char rejected');
# A 32-character msgid is within the limit and must be accepted.
my $msgid32 = 'x' x 32;
$out = config_check("syslog:server=127.0.0.1:5140,rfc=rfc5424,msgid=$msgid32");
like($out, qr/test is successful/i, 'msgid= 32-char accepted');
done_testing;
###############################################################################

168
t/syslog_rfc5424_stream.t Normal file
View file

@ -0,0 +1,168 @@
#!/usr/bin/perl
# (C) Nginx, Inc.
# Tests for RFC 5424 syslog format in the stream access_log directive.
###############################################################################
use warnings;
use strict;
use Test::More;
use IO::Select;
use IO::Socket::INET;
BEGIN { use FindBin; chdir($FindBin::Bin); }
use lib 'lib';
use Test::Nginx;
use Test::Nginx::Stream qw/ stream /;
###############################################################################
select STDERR; $| = 1;
select STDOUT; $| = 1;
plan(skip_all => 'win32') if $^O eq 'MSWin32';
my $t = Test::Nginx->new()->has(qw/stream http/)->plan(15);
###############################################################################
$t->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;
location / {
return 200 ok;
}
}
}
stream {
%%TEST_GLOBALS_STREAM%%
log_format streamf "$remote_addr $protocol";
server {
listen 127.0.0.1:8081;
proxy_pass 127.0.0.1:8080;
access_log syslog:server=127.0.0.1:%%PORT_8982_UDP%%,rfc=rfc5424
streamf;
}
server {
listen 127.0.0.1:8082;
proxy_pass 127.0.0.1:8080;
access_log syslog:server=127.0.0.1:%%PORT_8982_UDP%%,rfc=rfc5424,nohostname
streamf;
}
}
EOF
$t->run();
###############################################################################
my $s5424 = IO::Socket::INET->new(
Proto => 'udp',
LocalAddr => '127.0.0.1:' . port(8982)
) or die "Can't open syslog socket: $!";
# RFC 5424 stream access log — full field-by-field check
stream('127.0.0.1:' . port(8081))->read();
parse_rfc5424_message('stream access_log', get_syslog_raw($s5424));
# RFC 5424 stream with nohostname
stream('127.0.0.1:' . port(8082))->read();
my $msg = get_syslog_raw($s5424);
like($msg,
qr/^<\d+>1\s # PRI + VERSION
\S+\s # TIMESTAMP
-\s # HOSTNAME = nil "-"
\S+\s # APP-NAME
\d+\s # PROCID
-\s-\s/x,
'stream rfc5424 nohostname: HOSTNAME is nil "-"');
###############################################################################
sub get_syslog_raw {
my ($sock) = @_;
my $data = '';
IO::Select->new($sock)->can_read(2);
while (IO::Select->new($sock)->can_read(0.1)) {
my $buf;
sysread($sock, $buf, 4096);
$data .= $buf;
}
return $data;
}
# Validate all RFC 5424 header fields; runs 14 assertions.
sub parse_rfc5424_message {
my ($desc, $line) = @_;
unless ($line) {
fail("$desc: no syslog message received");
return;
}
my ($pri, $ts, $host, $app, $pid, $msgid, $sd, $msg) =
$line =~ /^<(\d{1,3})> # PRI
1\s # VERSION
(\S+)\s # TIMESTAMP
(\S+)\s # HOSTNAME
(\S+)\s # APP-NAME
(\S+)\s # PROCID
(\S+)\s # MSGID
(\S+)\s # STRUCTURED-DATA
(.*)/x; # MSG
ok(defined($pri), "$desc: has PRI");
my $sev = $pri & 0x07;
my $fac = ($pri & 0x03f8) >> 3;
ok($sev >= 0 && $sev <= 7, "$desc: severity in PRI is 0-7");
ok($fac >= 0 && $fac < 24, "$desc: facility in PRI is 0-23");
ok(defined($ts), "$desc: has TIMESTAMP");
like($ts,
qr/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/,
"$desc: TIMESTAMP is ISO 8601 with ms and tz offset");
ok(defined($host), "$desc: has HOSTNAME");
ok(length($host) > 0 && $host ne '-', "$desc: HOSTNAME is non-nil");
ok(defined($app), "$desc: has APP-NAME");
like($app, qr/^[!-~]+$/, "$desc: APP-NAME is printable US-ASCII");
ok(defined($pid), "$desc: has PROCID");
like($pid, qr/^\d+$/, "$desc: PROCID is a decimal integer");
is($msgid, '-', "$desc: MSGID is nil");
is($sd, '-', "$desc: STRUCTURED-DATA is nil");
ok(defined($msg) && length($msg) > 0, "$desc: MSG is non-empty");
}
###############################################################################