diff --git a/src/core/ngx_syslog.c b/src/core/ngx_syslog.c index bad45bd16..0e90046f0 100644 --- a/src/core/ngx_syslog.c +++ b/src/core/ngx_syslog.c @@ -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); diff --git a/src/core/ngx_syslog.h b/src/core/ngx_syslog.h index e2d54acdb..80a46fa76 100644 --- a/src/core/ngx_syslog.h +++ b/src/core/ngx_syslog.h @@ -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; diff --git a/t/syslog_rfc5424.t b/t/syslog_rfc5424.t new file mode 100644 index 000000000..209f83961 --- /dev/null +++ b/t/syslog_rfc5424.t @@ -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: + # 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"; + } +} + +############################################################################### diff --git a/t/syslog_rfc5424_config.t b/t/syslog_rfc5424_config.t new file mode 100644 index 000000000..89dade6e7 --- /dev/null +++ b/t/syslog_rfc5424_config.t @@ -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; + +############################################################################### diff --git a/t/syslog_rfc5424_stream.t b/t/syslog_rfc5424_stream.t new file mode 100644 index 000000000..8b8a34bd5 --- /dev/null +++ b/t/syslog_rfc5424_stream.t @@ -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"); +} + +###############################################################################