master
go 669 lines 24.8 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package weblog
4
5 import (
6 "errors"
7 "fmt"
8 "testing"
9
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12 )
13
14 const (
15 emptyStr = ""
16 )
17
18 var emptyLogLine = *newEmptyLogLine()
19
20 func TestLogLine_Assign(t *testing.T) {
21 type subTest struct {
22 input string
23 wantLine logLine
24 wantErr error
25 }
26 type test struct {
27 name string
28 fields []string
29 cases []subTest
30 }
31 tests := []test{
32 {
33 name: "Vhost",
34 fields: []string{
35 "host",
36 "http_host",
37 "v",
38 },
39 cases: []subTest{
40 {input: "1.1.1.1", wantLine: logLine{web: web{vhost: "1.1.1.1"}}},
41 {input: "::1", wantLine: logLine{web: web{vhost: "::1"}}},
42 {input: "[::1]", wantLine: logLine{web: web{vhost: "::1"}}},
43 {input: "1ce:1ce::babe", wantLine: logLine{web: web{vhost: "1ce:1ce::babe"}}},
44 {input: "[1ce:1ce::babe]", wantLine: logLine{web: web{vhost: "1ce:1ce::babe"}}},
45 {input: "localhost", wantLine: logLine{web: web{vhost: "localhost"}}},
46 {input: "debian10.debian", wantLine: logLine{web: web{vhost: "debian10.debian"}}},
47 {input: "my_vhost", wantLine: logLine{web: web{vhost: "my_vhost"}}},
48 {input: emptyStr, wantLine: emptyLogLine},
49 {input: hyphen, wantLine: emptyLogLine},
50 },
51 },
52 {
53 name: "Server Port",
54 fields: []string{
55 "server_port",
56 "p",
57 },
58 cases: []subTest{
59 {input: "80", wantLine: logLine{web: web{port: "80"}}},
60 {input: "8081", wantLine: logLine{web: web{port: "8081"}}},
61 {input: "30000", wantLine: logLine{web: web{port: "30000"}}},
62 {input: emptyStr, wantLine: emptyLogLine},
63 {input: hyphen, wantLine: emptyLogLine},
64 {input: "-1", wantLine: emptyLogLine, wantErr: errBadPort},
65 {input: "0", wantLine: emptyLogLine, wantErr: errBadPort},
66 {input: "50000", wantLine: emptyLogLine, wantErr: errBadPort},
67 },
68 },
69 {
70 name: "Vhost With Port",
71 fields: []string{
72 "host:$server_port",
73 "v:%p",
74 },
75 cases: []subTest{
76 {input: "1.1.1.1:80", wantLine: logLine{web: web{vhost: "1.1.1.1", port: "80"}}},
77 {input: "::1:80", wantLine: logLine{web: web{vhost: "::1", port: "80"}}},
78 {input: "[::1]:80", wantLine: logLine{web: web{vhost: "::1", port: "80"}}},
79 {input: "1ce:1ce::babe:80", wantLine: logLine{web: web{vhost: "1ce:1ce::babe", port: "80"}}},
80 {input: "debian10.debian:81", wantLine: logLine{web: web{vhost: "debian10.debian", port: "81"}}},
81 {input: emptyStr, wantLine: emptyLogLine},
82 {input: hyphen, wantLine: emptyLogLine},
83 {input: "1.1.1.1", wantLine: emptyLogLine, wantErr: errBadVhostPort},
84 {input: "1.1.1.1:", wantLine: emptyLogLine, wantErr: errBadVhostPort},
85 {input: "1.1.1.1 80", wantLine: emptyLogLine, wantErr: errBadVhostPort},
86 {input: "1.1.1.1:20", wantLine: emptyLogLine, wantErr: errBadVhostPort},
87 {input: "1.1.1.1:50000", wantLine: emptyLogLine, wantErr: errBadVhostPort},
88 },
89 },
90 {
91 name: "Scheme",
92 fields: []string{
93 "scheme",
94 },
95 cases: []subTest{
96 {input: "http", wantLine: logLine{web: web{reqScheme: "http"}}},
97 {input: "https", wantLine: logLine{web: web{reqScheme: "https"}}},
98 {input: emptyStr, wantLine: emptyLogLine},
99 {input: hyphen, wantLine: emptyLogLine},
100 {input: "HTTP", wantLine: emptyLogLine, wantErr: errBadReqScheme},
101 {input: "HTTPS", wantLine: emptyLogLine, wantErr: errBadReqScheme},
102 },
103 },
104 {
105 name: "Client",
106 fields: []string{
107 "remote_addr",
108 "a",
109 "h",
110 },
111 cases: []subTest{
112 {input: "1.1.1.1", wantLine: logLine{web: web{reqClient: "1.1.1.1"}}},
113 {input: "debian10", wantLine: logLine{web: web{reqClient: "debian10"}}},
114 {input: emptyStr, wantLine: emptyLogLine},
115 {input: hyphen, wantLine: emptyLogLine},
116 },
117 },
118 {
119 name: "Request",
120 fields: []string{
121 "request",
122 "r",
123 },
124 cases: []subTest{
125 {input: "GET / HTTP/1.0", wantLine: logLine{web: web{reqMethod: "GET", reqURL: "/", reqProto: "1.0"}}},
126 {input: "HEAD /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "HEAD", reqURL: "/ihs.gif", reqProto: "1.0"}}},
127 {input: "POST /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "POST", reqURL: "/ihs.gif", reqProto: "1.0"}}},
128 {input: "PUT /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "PUT", reqURL: "/ihs.gif", reqProto: "1.0"}}},
129 {input: "PATCH /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "PATCH", reqURL: "/ihs.gif", reqProto: "1.0"}}},
130 {input: "DELETE /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "DELETE", reqURL: "/ihs.gif", reqProto: "1.0"}}},
131 {input: "OPTIONS /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "OPTIONS", reqURL: "/ihs.gif", reqProto: "1.0"}}},
132 {input: "TRACE /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "TRACE", reqURL: "/ihs.gif", reqProto: "1.0"}}},
133 {input: "CONNECT ip.cn:443 HTTP/1.1", wantLine: logLine{web: web{reqMethod: "CONNECT", reqURL: "ip.cn:443", reqProto: "1.1"}}},
134 {input: "MKCOL ip.cn:443 HTTP/1.1", wantLine: logLine{web: web{reqMethod: "MKCOL", reqURL: "ip.cn:443", reqProto: "1.1"}}},
135 {input: "PROPFIND ip.cn:443 HTTP/1.1", wantLine: logLine{web: web{reqMethod: "PROPFIND", reqURL: "ip.cn:443", reqProto: "1.1"}}},
136 {input: "MOVE ip.cn:443 HTTP/1.1", wantLine: logLine{web: web{reqMethod: "MOVE", reqURL: "ip.cn:443", reqProto: "1.1"}}},
137 {input: "SEARCH ip.cn:443 HTTP/1.1", wantLine: logLine{web: web{reqMethod: "SEARCH", reqURL: "ip.cn:443", reqProto: "1.1"}}},
138 {input: "GET / HTTP/1.1", wantLine: logLine{web: web{reqMethod: "GET", reqURL: "/", reqProto: "1.1"}}},
139 {input: "GET / HTTP/2", wantLine: logLine{web: web{reqMethod: "GET", reqURL: "/", reqProto: "2"}}},
140 {input: "GET / HTTP/2.0", wantLine: logLine{web: web{reqMethod: "GET", reqURL: "/", reqProto: "2.0"}}},
141 {input: "GET /invalid_version http/1.1", wantLine: logLine{web: web{reqMethod: "GET", reqURL: "/invalid_version", reqProto: emptyString}}, wantErr: errBadReqProto},
142 {input: emptyStr, wantLine: emptyLogLine},
143 {input: hyphen, wantLine: emptyLogLine},
144 {input: "GET no_version", wantLine: emptyLogLine, wantErr: errBadRequest},
145 {input: "GOT / HTTP/2", wantLine: emptyLogLine, wantErr: errBadReqMethod},
146 {input: "get / HTTP/2", wantLine: emptyLogLine, wantErr: errBadReqMethod},
147 {input: "x04\x01\x00P$3\xFE\xEA\x00", wantLine: emptyLogLine, wantErr: errBadRequest},
148 },
149 },
150 {
151 name: "Request HTTP Method",
152 fields: []string{
153 "request_method",
154 "m",
155 },
156 cases: []subTest{
157 {input: "GET", wantLine: logLine{web: web{reqMethod: "GET"}}},
158 {input: "HEAD", wantLine: logLine{web: web{reqMethod: "HEAD"}}},
159 {input: "POST", wantLine: logLine{web: web{reqMethod: "POST"}}},
160 {input: "PUT", wantLine: logLine{web: web{reqMethod: "PUT"}}},
161 {input: "PATCH", wantLine: logLine{web: web{reqMethod: "PATCH"}}},
162 {input: "DELETE", wantLine: logLine{web: web{reqMethod: "DELETE"}}},
163 {input: "OPTIONS", wantLine: logLine{web: web{reqMethod: "OPTIONS"}}},
164 {input: "TRACE", wantLine: logLine{web: web{reqMethod: "TRACE"}}},
165 {input: "CONNECT", wantLine: logLine{web: web{reqMethod: "CONNECT"}}},
166 {input: "MKCOL", wantLine: logLine{web: web{reqMethod: "MKCOL"}}},
167 {input: "PROPFIND", wantLine: logLine{web: web{reqMethod: "PROPFIND"}}},
168 {input: "MOVE", wantLine: logLine{web: web{reqMethod: "MOVE"}}},
169 {input: "SEARCH", wantLine: logLine{web: web{reqMethod: "SEARCH"}}},
170 {input: "PURGE", wantLine: logLine{web: web{reqMethod: "PURGE"}}},
171 {input: emptyStr, wantLine: emptyLogLine},
172 {input: hyphen, wantLine: emptyLogLine},
173 {input: "GET no_version", wantLine: emptyLogLine, wantErr: errBadReqMethod},
174 {input: "GOT / HTTP/2", wantLine: emptyLogLine, wantErr: errBadReqMethod},
175 {input: "get / HTTP/2", wantLine: emptyLogLine, wantErr: errBadReqMethod},
176 },
177 },
178 {
179 name: "Request URL",
180 fields: []string{
181 "request_uri",
182 "U",
183 },
184 cases: []subTest{
185 {input: "/server-status?auto", wantLine: logLine{web: web{reqURL: "/server-status?auto"}}},
186 {input: "/default.html", wantLine: logLine{web: web{reqURL: "/default.html"}}},
187 {input: "10.0.0.1:3128", wantLine: logLine{web: web{reqURL: "10.0.0.1:3128"}}},
188 {input: emptyStr, wantLine: emptyLogLine},
189 {input: hyphen, wantLine: emptyLogLine},
190 },
191 },
192 {
193 name: "Request HTTP Protocol",
194 fields: []string{
195 "server_protocol",
196 "H",
197 },
198 cases: []subTest{
199 {input: "HTTP/1.0", wantLine: logLine{web: web{reqProto: "1.0"}}},
200 {input: "HTTP/1.1", wantLine: logLine{web: web{reqProto: "1.1"}}},
201 {input: "HTTP/2", wantLine: logLine{web: web{reqProto: "2"}}},
202 {input: "HTTP/2.0", wantLine: logLine{web: web{reqProto: "2.0"}}},
203 {input: "HTTP/3", wantLine: logLine{web: web{reqProto: "3"}}},
204 {input: "HTTP/3.0", wantLine: logLine{web: web{reqProto: "3.0"}}},
205 {input: emptyStr, wantLine: emptyLogLine},
206 {input: hyphen, wantLine: emptyLogLine},
207 {input: "1.1", wantLine: emptyLogLine, wantErr: errBadReqProto},
208 {input: "http/1.1", wantLine: emptyLogLine, wantErr: errBadReqProto},
209 },
210 },
211 {
212 name: "Response Status Code",
213 fields: []string{
214 "status",
215 "s",
216 ">s",
217 },
218 cases: []subTest{
219 {input: "100", wantLine: logLine{web: web{respCode: 100}}},
220 {input: "200", wantLine: logLine{web: web{respCode: 200}}},
221 {input: "300", wantLine: logLine{web: web{respCode: 300}}},
222 {input: "400", wantLine: logLine{web: web{respCode: 400}}},
223 {input: "500", wantLine: logLine{web: web{respCode: 500}}},
224 {input: "600", wantLine: logLine{web: web{respCode: 600}}},
225 {input: emptyStr, wantLine: emptyLogLine},
226 {input: hyphen, wantLine: emptyLogLine},
227 {input: "99", wantLine: emptyLogLine, wantErr: errBadRespCode},
228 {input: "601", wantLine: emptyLogLine, wantErr: errBadRespCode},
229 {input: "200 ", wantLine: emptyLogLine, wantErr: errBadRespCode},
230 {input: "0.222", wantLine: emptyLogLine, wantErr: errBadRespCode},
231 {input: "localhost", wantLine: emptyLogLine, wantErr: errBadRespCode},
232 },
233 },
234 {
235 name: "Request Size",
236 fields: []string{
237 "request_length",
238 "I",
239 },
240 cases: []subTest{
241 {input: "15", wantLine: logLine{web: web{reqSize: 15}}},
242 {input: "1000000", wantLine: logLine{web: web{reqSize: 1000000}}},
243 {input: emptyStr, wantLine: emptyLogLine},
244 {input: hyphen, wantLine: logLine{web: web{reqSize: 0}}},
245 {input: "-1", wantLine: emptyLogLine, wantErr: errBadReqSize},
246 {input: "100.222", wantLine: emptyLogLine, wantErr: errBadReqSize},
247 {input: "invalid", wantLine: emptyLogLine, wantErr: errBadReqSize},
248 },
249 },
250 {
251 name: "Response Size",
252 fields: []string{
253 "bytes_sent",
254 "body_bytes_sent",
255 "O",
256 "B",
257 "b",
258 },
259 cases: []subTest{
260 {input: "15", wantLine: logLine{web: web{respSize: 15}}},
261 {input: "1000000", wantLine: logLine{web: web{respSize: 1000000}}},
262 {input: emptyStr, wantLine: emptyLogLine},
263 {input: hyphen, wantLine: logLine{web: web{respSize: 0}}},
264 {input: "-1", wantLine: emptyLogLine, wantErr: errBadRespSize},
265 {input: "100.222", wantLine: emptyLogLine, wantErr: errBadRespSize},
266 {input: "invalid", wantLine: emptyLogLine, wantErr: errBadRespSize},
267 },
268 },
269 {
270 name: "Request Processing Time",
271 fields: []string{
272 "request_time",
273 "D",
274 },
275 cases: []subTest{
276 {input: "100222", wantLine: logLine{web: web{reqProcTime: 100222}}},
277 {input: "100.222", wantLine: logLine{web: web{reqProcTime: 100222000}}},
278 {input: emptyStr, wantLine: emptyLogLine},
279 {input: hyphen, wantLine: emptyLogLine},
280 {input: "-1", wantLine: emptyLogLine, wantErr: errBadReqProcTime},
281 {input: "0.333,0.444,0.555", wantLine: emptyLogLine, wantErr: errBadReqProcTime},
282 {input: "number", wantLine: emptyLogLine, wantErr: errBadReqProcTime},
283 },
284 },
285 {
286 name: "Upstream Response Time",
287 fields: []string{
288 "upstream_response_time",
289 },
290 cases: []subTest{
291 {input: "100222", wantLine: logLine{web: web{upsRespTime: 100222}}},
292 {input: "100.222", wantLine: logLine{web: web{upsRespTime: 100222000}}},
293 {input: "0.100 , 0.400 : 0.200 ", wantLine: logLine{web: web{upsRespTime: 700000}}},
294 {input: emptyStr, wantLine: emptyLogLine},
295 {input: hyphen, wantLine: emptyLogLine},
296 {input: "-1", wantLine: emptyLogLine, wantErr: errBadUpsRespTime},
297 {input: "number", wantLine: emptyLogLine, wantErr: errBadUpsRespTime},
298 },
299 },
300 {
301 name: "SSL Protocol",
302 fields: []string{
303 "ssl_protocol",
304 },
305 cases: []subTest{
306 {input: "SSLv3", wantLine: logLine{web: web{sslProto: "SSLv3"}}},
307 {input: "SSLv2", wantLine: logLine{web: web{sslProto: "SSLv2"}}},
308 {input: "TLSv1", wantLine: logLine{web: web{sslProto: "TLSv1"}}},
309 {input: "TLSv1.1", wantLine: logLine{web: web{sslProto: "TLSv1.1"}}},
310 {input: "TLSv1.2", wantLine: logLine{web: web{sslProto: "TLSv1.2"}}},
311 {input: "TLSv1.3", wantLine: logLine{web: web{sslProto: "TLSv1.3"}}},
312 {input: emptyStr, wantLine: emptyLogLine},
313 {input: hyphen, wantLine: emptyLogLine},
314 {input: "-1", wantLine: emptyLogLine, wantErr: errBadSSLProto},
315 {input: "invalid", wantLine: emptyLogLine, wantErr: errBadSSLProto},
316 },
317 },
318 {
319 name: "SSL Cipher Suite",
320 fields: []string{
321 "ssl_cipher",
322 },
323 cases: []subTest{
324 {input: "ECDHE-RSA-AES256-SHA", wantLine: logLine{web: web{sslCipherSuite: "ECDHE-RSA-AES256-SHA"}}},
325 {input: "DHE-RSA-AES256-SHA", wantLine: logLine{web: web{sslCipherSuite: "DHE-RSA-AES256-SHA"}}},
326 {input: "AES256-SHA", wantLine: logLine{web: web{sslCipherSuite: "AES256-SHA"}}},
327 {input: "PSK-RC4-SHA", wantLine: logLine{web: web{sslCipherSuite: "PSK-RC4-SHA"}}},
328 {input: "TLS_AES_256_GCM_SHA384", wantLine: logLine{web: web{sslCipherSuite: "TLS_AES_256_GCM_SHA384"}}},
329 {input: emptyStr, wantLine: emptyLogLine},
330 {input: hyphen, wantLine: emptyLogLine},
331 {input: "-1", wantLine: emptyLogLine, wantErr: errBadSSLCipherSuite},
332 {input: "invalid", wantLine: emptyLogLine, wantErr: errBadSSLCipherSuite},
333 },
334 },
335 {
336 name: "Custom Fields",
337 fields: []string{
338 "custom",
339 },
340 cases: []subTest{
341 {input: "POST", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "POST"}}}}},
342 {input: "/example.com", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "/example.com"}}}}},
343 {input: "HTTP/1.1", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "HTTP/1.1"}}}}},
344 {input: "0.333,0.444,0.555", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "0.333,0.444,0.555"}}}}},
345 {input: "-1", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "-1"}}}}},
346 {input: "invalid", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "invalid"}}}}},
347 {input: emptyStr, wantLine: emptyLogLine},
348 {input: hyphen, wantLine: emptyLogLine},
349 },
350 },
351 {
352 name: "Custom Fields Not Exist",
353 fields: []string{
354 "custom_field_not_exist",
355 },
356 cases: []subTest{
357 {input: "POST", wantLine: emptyLogLine},
358 {input: "/example.com", wantLine: emptyLogLine},
359 {input: "HTTP/1.1", wantLine: emptyLogLine},
360 {input: "0.333,0.444,0.555", wantLine: emptyLogLine},
361 {input: "-1", wantLine: emptyLogLine},
362 {input: "invalid", wantLine: emptyLogLine},
363 {input: emptyStr, wantLine: emptyLogLine},
364 {input: hyphen, wantLine: emptyLogLine},
365 },
366 },
367 }
368
369 for _, tt := range tests {
370 for _, field := range tt.fields {
371 for i, tc := range tt.cases {
372 name := fmt.Sprintf("[%s:%d]field='%s'|line='%s'", tt.name, i+1, field, tc.input)
373 t.Run(name, func(t *testing.T) {
374
375 line := newEmptyLogLineWithFields()
376 err := line.Assign(field, tc.input)
377
378 if tc.wantErr != nil {
379 require.Error(t, err)
380 assert.Truef(t, errors.Is(err, tc.wantErr), "expected '%v' error, got '%v'", tc.wantErr, err)
381 } else {
382 require.NoError(t, err)
383 }
384
385 expected := prepareLogLine(field, tc.wantLine)
386 assert.Equal(t, expected, *line)
387 })
388 }
389 }
390 }
391 }
392
393 func TestLogLine_verify(t *testing.T) {
394 type subTest struct {
395 line logLine
396 wantErr error
397 }
398 tests := []struct {
399 name string
400 field string
401 cases []subTest
402 }{
403 {
404 name: "Vhost",
405 field: "host",
406 cases: []subTest{
407 {line: logLine{web: web{vhost: "192.168.0.1"}}},
408 {line: logLine{web: web{vhost: "debian10.debian"}}},
409 {line: logLine{web: web{vhost: "1ce:1ce::babe"}}},
410 {line: logLine{web: web{vhost: "localhost"}}},
411 {line: logLine{web: web{vhost: "invalid_vhost"}}, wantErr: errBadVhost},
412 {line: logLine{web: web{vhost: "http://192.168.0.1/"}}, wantErr: errBadVhost},
413 },
414 },
415 {
416 name: "Server Port",
417 field: "server_port",
418 cases: []subTest{
419 {line: logLine{web: web{port: "80"}}},
420 {line: logLine{web: web{port: "8081"}}},
421 {line: logLine{web: web{port: "79"}}, wantErr: errBadPort},
422 {line: logLine{web: web{port: "50000"}}, wantErr: errBadPort},
423 {line: logLine{web: web{port: "0.0.0.0"}}, wantErr: errBadPort},
424 },
425 },
426 {
427 name: "Scheme",
428 field: "scheme",
429 cases: []subTest{
430 {line: logLine{web: web{reqScheme: "http"}}},
431 {line: logLine{web: web{reqScheme: "https"}}},
432 {line: logLine{web: web{reqScheme: "not_https"}}, wantErr: errBadReqScheme},
433 {line: logLine{web: web{reqScheme: "HTTP"}}, wantErr: errBadReqScheme},
434 {line: logLine{web: web{reqScheme: "HTTPS"}}, wantErr: errBadReqScheme},
435 {line: logLine{web: web{reqScheme: "10"}}, wantErr: errBadReqScheme},
436 },
437 },
438 {
439 name: "Client",
440 field: "remote_addr",
441 cases: []subTest{
442 {line: logLine{web: web{reqClient: "1.1.1.1"}}},
443 {line: logLine{web: web{reqClient: "::1"}}},
444 {line: logLine{web: web{reqClient: "1ce:1ce::babe"}}},
445 {line: logLine{web: web{reqClient: "localhost"}}},
446 {line: logLine{web: web{reqClient: "debian10.debian"}}, wantErr: errBadReqClient},
447 {line: logLine{web: web{reqClient: "invalid"}}, wantErr: errBadReqClient},
448 },
449 },
450 {
451 name: "Request HTTP Method",
452 field: "request_method",
453 cases: []subTest{
454 {line: logLine{web: web{reqMethod: "GET"}}},
455 {line: logLine{web: web{reqMethod: "POST"}}},
456 {line: logLine{web: web{reqMethod: "TRACE"}}},
457 {line: logLine{web: web{reqMethod: "OPTIONS"}}},
458 {line: logLine{web: web{reqMethod: "CONNECT"}}},
459 {line: logLine{web: web{reqMethod: "DELETE"}}},
460 {line: logLine{web: web{reqMethod: "PUT"}}},
461 {line: logLine{web: web{reqMethod: "PATCH"}}},
462 {line: logLine{web: web{reqMethod: "HEAD"}}},
463 {line: logLine{web: web{reqMethod: "MKCOL"}}},
464 {line: logLine{web: web{reqMethod: "PROPFIND"}}},
465 {line: logLine{web: web{reqMethod: "MOVE"}}},
466 {line: logLine{web: web{reqMethod: "SEARCH"}}},
467 {line: logLine{web: web{reqMethod: "Get"}}, wantErr: errBadReqMethod},
468 {line: logLine{web: web{reqMethod: "get"}}, wantErr: errBadReqMethod},
469 },
470 },
471 {
472 name: "Request URL",
473 field: "request_uri",
474 cases: []subTest{
475 {line: logLine{web: web{reqURL: "/"}}},
476 {line: logLine{web: web{reqURL: "/status?full&json"}}},
477 {line: logLine{web: web{reqURL: "/icons/openlogo-75.png"}}},
478 {line: logLine{web: web{reqURL: "status?full&json"}}},
479 {line: logLine{web: web{reqURL: "\"req_url=/ \""}}},
480 {line: logLine{web: web{reqURL: "http://192.168.0.1/"}}},
481 {line: logLine{web: web{reqURL: ""}}},
482 },
483 },
484 {
485 name: "Request HTTP Protocol",
486 field: "server_protocol",
487 cases: []subTest{
488 {line: logLine{web: web{reqProto: "1"}}},
489 {line: logLine{web: web{reqProto: "1.0"}}},
490 {line: logLine{web: web{reqProto: "1.1"}}},
491 {line: logLine{web: web{reqProto: "2.0"}}},
492 {line: logLine{web: web{reqProto: "2"}}},
493 {line: logLine{web: web{reqProto: "0.9"}}, wantErr: errBadReqProto},
494 {line: logLine{web: web{reqProto: "1.1.1"}}, wantErr: errBadReqProto},
495 {line: logLine{web: web{reqProto: "2.2"}}, wantErr: errBadReqProto},
496 {line: logLine{web: web{reqProto: "localhost"}}, wantErr: errBadReqProto},
497 },
498 },
499 {
500 name: "Response Status Code",
501 field: "status",
502 cases: []subTest{
503 {line: logLine{web: web{respCode: 100}}},
504 {line: logLine{web: web{respCode: 200}}},
505 {line: logLine{web: web{respCode: 300}}},
506 {line: logLine{web: web{respCode: 400}}},
507 {line: logLine{web: web{respCode: 500}}},
508 {line: logLine{web: web{respCode: 600}}},
509 {line: logLine{web: web{respCode: -1}}, wantErr: errBadRespCode},
510 {line: logLine{web: web{respCode: 99}}, wantErr: errBadRespCode},
511 {line: logLine{web: web{respCode: 601}}, wantErr: errBadRespCode},
512 },
513 },
514 {
515 name: "Request size",
516 field: "request_length",
517 cases: []subTest{
518 {line: logLine{web: web{reqSize: 0}}},
519 {line: logLine{web: web{reqSize: 100}}},
520 {line: logLine{web: web{reqSize: 1000000}}},
521 {line: logLine{web: web{reqSize: -1}}, wantErr: errBadReqSize},
522 },
523 },
524 {
525 name: "Response size",
526 field: "bytes_sent",
527 cases: []subTest{
528 {line: logLine{web: web{respSize: 0}}},
529 {line: logLine{web: web{respSize: 100}}},
530 {line: logLine{web: web{respSize: 1000000}}},
531 {line: logLine{web: web{respSize: -1}}, wantErr: errBadRespSize},
532 },
533 },
534 {
535 name: "Request Processing Time",
536 field: "request_time",
537 cases: []subTest{
538 {line: logLine{web: web{reqProcTime: 0}}},
539 {line: logLine{web: web{reqProcTime: 100}}},
540 {line: logLine{web: web{reqProcTime: 1000.123}}},
541 {line: logLine{web: web{reqProcTime: -1}}, wantErr: errBadReqProcTime},
542 },
543 },
544 {
545 name: "Upstream Response Time",
546 field: "upstream_response_time",
547 cases: []subTest{
548 {line: logLine{web: web{upsRespTime: 0}}},
549 {line: logLine{web: web{upsRespTime: 100}}},
550 {line: logLine{web: web{upsRespTime: 1000.123}}},
551 {line: logLine{web: web{upsRespTime: -1}}, wantErr: errBadUpsRespTime},
552 },
553 },
554 {
555 name: "SSL Protocol",
556 field: "ssl_protocol",
557 cases: []subTest{
558 {line: logLine{web: web{sslProto: "SSLv3"}}},
559 {line: logLine{web: web{sslProto: "SSLv2"}}},
560 {line: logLine{web: web{sslProto: "TLSv1"}}},
561 {line: logLine{web: web{sslProto: "TLSv1.1"}}},
562 {line: logLine{web: web{sslProto: "TLSv1.2"}}},
563 {line: logLine{web: web{sslProto: "TLSv1.3"}}},
564 {line: logLine{web: web{sslProto: "invalid"}}, wantErr: errBadSSLProto},
565 },
566 },
567 {
568 name: "SSL Cipher Suite",
569 field: "ssl_cipher",
570 cases: []subTest{
571 {line: logLine{web: web{sslCipherSuite: "ECDHE-RSA-AES256-SHA"}}},
572 {line: logLine{web: web{sslCipherSuite: "DHE-RSA-AES256-SHA"}}},
573 {line: logLine{web: web{sslCipherSuite: "AES256-SHA"}}},
574 {line: logLine{web: web{sslCipherSuite: "TLS_AES_256_GCM_SHA384"}}},
575 {line: logLine{web: web{sslCipherSuite: "invalid"}}, wantErr: errBadSSLCipherSuite},
576 },
577 },
578 {
579 name: "Custom Fields",
580 field: "custom",
581 cases: []subTest{
582 {line: logLine{custom: custom{values: []customValue{{name: "custom", value: "POST"}}}}},
583 {line: logLine{custom: custom{values: []customValue{{name: "custom", value: "/example.com"}}}}},
584 {line: logLine{custom: custom{values: []customValue{{name: "custom", value: "0.333,0.444,0.555"}}}}},
585 },
586 },
587 {
588 name: "Empty Line",
589 cases: []subTest{
590 {line: emptyLogLine, wantErr: errEmptyLine},
591 },
592 },
593 }
594
595 for _, tt := range tests {
596 for i, tc := range tt.cases {
597 name := fmt.Sprintf("[%s:%d]field='%s'", tt.name, i+1, tt.field)
598
599 t.Run(name, func(t *testing.T) {
600 line := prepareLogLine(tt.field, tc.line)
601
602 err := line.verify()
603
604 if tc.wantErr != nil {
605 require.Error(t, err)
606 assert.Truef(t, errors.Is(err, tc.wantErr), "expected '%v' error, got '%v'", tc.wantErr, err)
607 } else {
608 assert.NoError(t, err)
609 }
610 })
611 }
612 }
613 }
614
615 func prepareLogLine(field string, template logLine) logLine {
616 if template.empty() {
617 return *newEmptyLogLineWithFields()
618 }
619
620 line := newEmptyLogLineWithFields()
621 line.reset()
622
623 switch field {
624 case "host", "http_host", "v":
625 line.vhost = template.vhost
626 case "server_port", "p":
627 line.port = template.port
628 case "host:$server_port", "v:%p":
629 line.vhost = template.vhost
630 line.port = template.port
631 case "scheme":
632 line.reqScheme = template.reqScheme
633 case "remote_addr", "a", "h":
634 line.reqClient = template.reqClient
635 case "request", "r":
636 line.reqMethod = template.reqMethod
637 line.reqURL = template.reqURL
638 line.reqProto = template.reqProto
639 case "request_method", "m":
640 line.reqMethod = template.reqMethod
641 case "request_uri", "U":
642 line.reqURL = template.reqURL
643 case "server_protocol", "H":
644 line.reqProto = template.reqProto
645 case "status", "s", ">s":
646 line.respCode = template.respCode
647 case "request_length", "I":
648 line.reqSize = template.reqSize
649 case "bytes_sent", "body_bytes_sent", "b", "O", "B":
650 line.respSize = template.respSize
651 case "request_time", "D":
652 line.reqProcTime = template.reqProcTime
653 case "upstream_response_time":
654 line.upsRespTime = template.upsRespTime
655 case "ssl_protocol":
656 line.sslProto = template.sslProto
657 case "ssl_cipher":
658 line.sslCipherSuite = template.sslCipherSuite
659 default:
660 line.custom.values = template.custom.values
661 }
662 return *line
663 }
664
665 func newEmptyLogLineWithFields() *logLine {
666 l := newEmptyLogLine()
667 l.custom.fields = map[string]struct{}{"custom": {}}
668 return l
669 }