Branch data Line data Source code
1 : : // $Id: Login.cc 6724 2009-06-07 09:23:03Z vern $
2 : : //
3 : : // See the file "COPYING" in the main distribution directory for copyright.
4 : :
5 : : #include "config.h"
6 : :
7 : : #include <ctype.h>
8 : : #include <stdlib.h>
9 : :
10 : : #include "NetVar.h"
11 : : #include "Login.h"
12 : : #include "RE.h"
13 : : #include "Event.h"
14 : :
15 : : static RE_Matcher* re_skip_authentication = 0;
16 : : static RE_Matcher* re_direct_login_prompts;
17 : : static RE_Matcher* re_login_prompts;
18 : : static RE_Matcher* re_login_non_failure_msgs;
19 : : static RE_Matcher* re_login_failure_msgs;
20 : : static RE_Matcher* re_login_success_msgs;
21 : : static RE_Matcher* re_login_timeouts;
22 : :
23 : : static RE_Matcher* init_RE(ListVal* l);
24 : :
25 : 0 : Login_Analyzer::Login_Analyzer(AnalyzerTag::Tag tag, Connection* conn)
26 : 0 : : TCP_ApplicationAnalyzer(tag, conn)
27 : : {
28 : 0 : state = LOGIN_STATE_AUTHENTICATE;
29 : 0 : num_user_lines_seen = lines_scanned = 0;
30 : : // Set last_failure_num_user_lines so we will always generate
31 : : // at least one failure message, even if the user doesn't
32 : : // type anything (but we see, e.g., a timeout).
33 : 0 : last_failure_num_user_lines = -1;
34 : 0 : login_prompt_line = failure_line = 0;
35 : 0 : user_text_first = 0;
36 : 0 : user_text_last = MAX_USER_TEXT - 1;
37 : 0 : num_user_text = 0;
38 : 0 : client_name = username = 0;
39 : 0 : saw_ploy = is_VMS = 0;
40 : :
41 [ # # # # ]: 0 : if ( ! re_skip_authentication )
42 : : {
43 : : #ifdef USE_PERFTOOLS
44 : : HeapLeakChecker::Disabler disabler;
45 : : #endif
46 : 0 : re_skip_authentication = init_RE(skip_authentication);
47 : 0 : re_direct_login_prompts = init_RE(direct_login_prompts);
48 : 0 : re_login_prompts = init_RE(login_prompts);
49 : 0 : re_login_non_failure_msgs = init_RE(login_non_failure_msgs);
50 : 0 : re_login_failure_msgs = init_RE(login_failure_msgs);
51 : 0 : re_login_success_msgs = init_RE(login_success_msgs);
52 : 0 : re_login_timeouts = init_RE(login_timeouts);
53 : : }
54 : 0 : }
55 : :
56 : 0 : Login_Analyzer::~Login_Analyzer()
57 : : {
58 [ # # ][ # # ]: 0 : while ( num_user_text > 0 )
[ # # ]
59 : : {
60 : 0 : char* s = PopUserText();
61 [ # # # # ]: 0 : delete [] s;
[ # # ]
62 : : }
63 : :
64 : 0 : Unref(username);
65 : 0 : Unref(client_name);
66 [ # # ][ # # ]: 0 : }
[ # # ]
67 : :
68 : 0 : void Login_Analyzer::DeliverStream(int length, const u_char* line, bool orig)
69 : : {
70 : 0 : TCP_ApplicationAnalyzer::DeliverStream(length, line, orig);
71 : :
72 : 0 : char* str = new char[length+1];
73 : :
74 : : // Eliminate NUL characters.
75 : : int i, j;
76 [ # # ]: 0 : for ( i = 0, j = 0; i < length; ++i )
77 [ # # ]: 0 : if ( line[i] != '\0' )
78 : 0 : str[j++] = line[i];
79 : : else
80 : : {
81 [ # # ]: 0 : if ( Conn()->FlagEvent(NUL_IN_LINE) )
82 : 0 : Weird("NUL_in_line");
83 : : }
84 : :
85 : 0 : str[j] = '\0';
86 : :
87 : 0 : NewLine(orig, str);
88 [ # # ]: 0 : delete [] str;
89 : 0 : }
90 : :
91 : 0 : void Login_Analyzer::NewLine(bool orig, char* line)
92 : : {
93 [ # # ]: 0 : if ( state == LOGIN_STATE_SKIP )
94 : 0 : return;
95 : :
96 [ # # ][ # # ]: 0 : if ( orig && login_input_line )
[ # # ]
97 : 0 : LineEvent(login_input_line, line);
98 : :
99 [ # # ][ # # ]: 0 : if ( ! orig && login_output_line )
[ # # ]
100 : 0 : LineEvent(login_output_line, line);
101 : :
102 [ # # ]: 0 : if ( state == LOGIN_STATE_LOGGED_IN )
103 : 0 : return;
104 : :
105 [ # # ]: 0 : if ( state == LOGIN_STATE_AUTHENTICATE )
106 : : {
107 [ # # ][ # # ]: 0 : if ( TCP()->OrigState() == TCP_ENDPOINT_PARTIAL ||
[ # # ]
108 : : TCP()->RespState() == TCP_ENDPOINT_PARTIAL )
109 : 0 : state = LOGIN_STATE_CONFUSED; // unknown login state
110 : : else
111 : : {
112 : 0 : AuthenticationDialog(orig, line);
113 : 0 : return;
114 : : }
115 : : }
116 : :
117 [ # # ]: 0 : if ( state != LOGIN_STATE_CONFUSED )
118 : 0 : internal_error("bad state in Login_Analyzer::NewLine");
119 : :
120 : : // When we're in "confused", we feed each user input line to
121 : : // login_confused_text, but also scan the text in the
122 : : // other direction for evidence of successful login.
123 [ # # ]: 0 : if ( orig )
124 : : {
125 : 0 : (void) IsPloy(line);
126 : 0 : ConfusionText(line);
127 : : }
128 : :
129 [ # # ][ # # ]: 0 : else if ( ! saw_ploy && IsSuccessMsg(line) )
[ # # ]
130 : : {
131 : 0 : LoginEvent(login_success, line, 1);
132 : 0 : state = LOGIN_STATE_LOGGED_IN;
133 : : }
134 : : }
135 : :
136 : 0 : void Login_Analyzer::AuthenticationDialog(bool orig, char* line)
137 : : {
138 [ # # ]: 0 : if ( orig )
139 : : {
140 [ # # ]: 0 : if ( is_VMS )
141 : : {
142 : : #define VMS_REPEAT_SEQ "\x1b[A"
143 : 0 : char* repeat_prev_line = strstr(line, VMS_REPEAT_SEQ);
144 [ # # ]: 0 : if ( repeat_prev_line )
145 : : {
146 [ # # ]: 0 : if ( repeat_prev_line[strlen(VMS_REPEAT_SEQ)] )
147 : : {
148 : 0 : Confused("extra_repeat_text", line);
149 : 0 : return;
150 : : }
151 : :
152 : : // VMS repeats the username, not the last line
153 : : // typed (which presumably is the password).
154 [ # # ]: 0 : if ( username )
155 : : {
156 : 0 : line = (char*) username->AsString()->Bytes();
157 [ # # ]: 0 : if ( strstr(line, VMS_REPEAT_SEQ) )
158 : 0 : Confused("username_with_embedded_repeat", line);
159 : : else
160 : 0 : NewLine(orig, line);
161 : : }
162 : :
163 : : else
164 : 0 : Confused("repeat_without_username", line);
165 : 0 : return;
166 : : }
167 : : }
168 : :
169 : 0 : ++num_user_lines_seen;
170 : :
171 [ # # ]: 0 : if ( ! IsPloy(line) )
172 : 0 : AddUserText(line);
173 : :
174 : 0 : return;
175 : : }
176 : :
177 : : // Ignore blank lines from the responder - some systems spew
178 : : // out a whole bunch of these.
179 [ # # ]: 0 : if ( IsEmpty(line) )
180 : 0 : return;
181 : :
182 [ # # ][ # # ]: 0 : if ( ++lines_scanned > MAX_AUTHENTICATE_LINES &&
[ # # ][ # # ]
183 : : login_prompt_line == 0 && failure_line == 0 )
184 : 0 : Confused("no_login_prompt", line);
185 : :
186 : 0 : const char* prompt = IsLoginPrompt(line);
187 : 0 : int is_timeout = IsTimeout(line);
188 [ # # # # ]: 0 : if ( prompt && ! IsSuccessMsg(line) && ! is_timeout )
[ # # ][ # # ]
189 : : {
190 : 0 : is_VMS = strstr(line, "Username:") != 0;
191 : :
192 : : // If we see multiple login prompts, presume that
193 : : // each is consuming one line of typeahead.
194 : : //
195 : : // We can also get multiple login prompts spread
196 : : // across adjacent lines, for example if the user
197 : : // enters a blank line or a line that wasn't accepted
198 : : // (e.g., "foo^C").
199 : :
200 : : int multi_line_prompt =
201 : : (login_prompt_line == lines_scanned - 1 &&
202 : : // if login_prompt_line is the same as failure_line,
203 : : // then we didn't actually see a login prompt
204 : : // there, we're just remembering that as the
205 : : // prompt line so we can count typeahead with
206 : : // respect to it (see below).
207 [ # # # # ]: 0 : login_prompt_line != failure_line);
208 : :
209 : 0 : const char* next_prompt = 0;
210 [ # # ][ # # ]: 0 : while ( (*prompt != '\0' &&
[ # # ][ # # ]
211 : : (next_prompt = IsLoginPrompt(prompt + 1))) ||
212 : : multi_line_prompt )
213 : : {
214 [ # # ]: 0 : if ( ! HaveTypeahead() )
215 : : {
216 : 0 : Confused("multiple_login_prompts", line);
217 : 0 : break;
218 : : }
219 : :
220 : 0 : char* pop_str = PopUserText();
221 : 0 : bool empty = IsEmpty(pop_str);
222 [ # # ]: 0 : delete [] pop_str;
223 : :
224 [ # # ]: 0 : if ( multi_line_prompt )
225 : 0 : break;
226 : :
227 [ # # ]: 0 : if ( ! empty )
228 : : {
229 : 0 : Confused("non_empty_multi_login", line);
230 : 0 : break;
231 : : }
232 : :
233 : 0 : prompt = next_prompt;
234 : : }
235 : :
236 [ # # ]: 0 : if ( state == LOGIN_STATE_CONFUSED )
237 : 0 : return;
238 : :
239 : 0 : const char* user = GetUsername(prompt);
240 [ # # # # ]: 0 : if ( user && ! IsEmpty(user) )
[ # # ]
241 : : {
242 [ # # ]: 0 : if ( ! HaveTypeahead() )
243 : 0 : AddUserText(user);
244 : : }
245 : :
246 : 0 : login_prompt_line = lines_scanned;
247 : :
248 [ # # ]: 0 : if ( IsDirectLoginPrompt(line) )
249 : : {
250 : 0 : LoginEvent(login_success, line, 1);
251 : 0 : state = LOGIN_STATE_LOGGED_IN;
252 : 0 : SetSkip(1);
253 : 0 : return;
254 : : }
255 : : }
256 : :
257 [ # # ][ # # ]: 0 : else if ( is_timeout || IsFailureMsg(line) )
[ # # ]
258 : : {
259 [ # # ]: 0 : if ( num_user_lines_seen > last_failure_num_user_lines )
260 : : {
261 : : // The user has typed something since we last
262 : : // generated a failure event, so it's worth
263 : : // recording another failure event.
264 : : //
265 : : // We can otherwise wind up generating multiple
266 : : // failure events for sequences like:
267 : : //
268 : : // Error reading command input
269 : : // Timeout period expired
270 [ # # ]: 0 : if ( is_timeout )
271 : 0 : AddUserText("<timeout>");
272 : 0 : LoginEvent(login_failure, line);
273 : : }
274 : :
275 : : // Set the login prompt line to be here, too, so
276 : : // that we require MAX_LOGIN_LOOKAHEAD beyond this
277 : : // point before deciding they've logged in.
278 : 0 : login_prompt_line = failure_line = lines_scanned;
279 : 0 : last_failure_num_user_lines = num_user_lines_seen;
280 : : }
281 : :
282 [ # # ]: 0 : else if ( IsSkipAuthentication(line) )
283 : : {
284 [ # # ]: 0 : if ( authentication_skipped )
285 : : {
286 : 0 : val_list* vl = new val_list;
287 : 0 : vl->append(BuildConnVal());
288 : 0 : ConnectionEvent(authentication_skipped, vl);
289 : : }
290 : :
291 : 0 : state = LOGIN_STATE_SKIP;
292 : 0 : SetSkip(1);
293 : : }
294 : :
295 [ # # ][ # # ]: 0 : else if ( IsSuccessMsg(line) ||
[ # # ][ # # ]
296 : : (login_prompt_line > 0 &&
297 : : lines_scanned >
298 : : login_prompt_line + MAX_LOGIN_LOOKAHEAD + num_user_text) )
299 : : {
300 : 0 : LoginEvent(login_success, line);
301 : 0 : state = LOGIN_STATE_LOGGED_IN;
302 : : }
303 : : }
304 : :
305 : 0 : void Login_Analyzer::SetEnv(bool orig, char* name, char* val)
306 : : {
307 [ # # ]: 0 : if ( ! orig )
308 : : // Why is the responder transmitting its environment??
309 : 0 : Confused("responder_environment", name);
310 : :
311 : : else
312 : : {
313 [ # # ]: 0 : if ( streq(name, "USER") )
314 : : {
315 [ # # ]: 0 : if ( username )
316 : : {
317 : 0 : const BroString* u = username->AsString();
318 : 0 : const byte_vec ub = u->Bytes();
319 : 0 : const char* us = (const char*) ub;
320 [ # # ]: 0 : if ( ! streq(val, us) )
321 : 0 : Confused("multiple_USERs", val);
322 : 0 : Unref(username);
323 : : }
324 : :
325 : : // "val" gets copied here.
326 : 0 : username = new StringVal(val);
327 : : }
328 : :
329 [ # # ][ # # ]: 0 : else if ( login_terminal && streq(name, "TERM") )
[ # # ]
330 : : {
331 : 0 : val_list* vl = new val_list;
332 : :
333 : 0 : vl->append(BuildConnVal());
334 : 0 : vl->append(new StringVal(val));
335 : :
336 : 0 : ConnectionEvent(login_terminal, vl);
337 : : }
338 : :
339 [ # # ][ # # ]: 0 : else if ( login_display && streq(name, "DISPLAY") )
[ # # ]
340 : : {
341 : 0 : val_list* vl = new val_list;
342 : :
343 : 0 : vl->append(BuildConnVal());
344 : 0 : vl->append(new StringVal(val));
345 : :
346 : 0 : ConnectionEvent(login_display, vl);
347 : : }
348 : :
349 [ # # ][ # # ]: 0 : else if ( login_prompt && streq(name, "TTYPROMPT") )
[ # # ]
350 : : {
351 : 0 : val_list* vl = new val_list;
352 : :
353 : 0 : vl->append(BuildConnVal());
354 : 0 : vl->append(new StringVal(val));
355 : :
356 : 0 : ConnectionEvent(login_prompt, vl);
357 : : }
358 : : }
359 : :
360 [ # # ]: 0 : delete [] name;
361 [ # # ]: 0 : delete [] val;
362 : 0 : }
363 : :
364 : 0 : void Login_Analyzer::EndpointEOF(bool orig)
365 : : {
366 : 0 : TCP_ApplicationAnalyzer::EndpointEOF(orig);
367 : :
368 [ # # # # ]: 0 : if ( state == LOGIN_STATE_AUTHENTICATE && HaveTypeahead() )
[ # # ]
369 : : {
370 : 0 : LoginEvent(login_success, "<EOF>", 1);
371 : 0 : state = LOGIN_STATE_LOGGED_IN;
372 : : }
373 : 0 : }
374 : :
375 : : void Login_Analyzer::LoginEvent(EventHandlerPtr f, const char* line,
376 : 0 : int no_user_okay)
377 : : {
378 [ # # ]: 0 : if ( ! f )
379 : 0 : return;
380 : :
381 [ # # ]: 0 : if ( login_prompt_line > failure_line )
382 : : {
383 : 0 : FlushEmptyTypeahead();
384 : :
385 : : // We should've seen a username.
386 [ # # ]: 0 : if ( ! HaveTypeahead() )
387 : : {
388 [ # # ]: 0 : if ( no_user_okay )
389 : : {
390 : 0 : Unref(username);
391 : 0 : username = new StringVal("<none>");
392 : : }
393 : :
394 : : else
395 : : {
396 : 0 : Confused("no_username", line);
397 : 0 : return;
398 : : }
399 : : }
400 : : else
401 : : {
402 : 0 : Unref(username);
403 : 0 : username = PopUserTextVal();
404 : : }
405 : : }
406 : :
407 : : else
408 : : {
409 : : // Evidently the system reprompted for a password upon an
410 : : // earlier failure. Use the previously-recorded username.
411 [ # # ]: 0 : if ( ! username )
412 : : {
413 [ # # ]: 0 : if ( no_user_okay )
414 : : {
415 : 0 : Unref(username);
416 : 0 : username = new StringVal("<none>");
417 : : }
418 : :
419 : : else
420 : : {
421 : 0 : Confused("no_username2", line);
422 : 0 : return;
423 : : }
424 : : }
425 : : }
426 : :
427 : : Val* password = HaveTypeahead() ?
428 [ # # ]: 0 : PopUserTextVal() : new StringVal("<none>");
429 : :
430 : 0 : val_list* vl = new val_list;
431 : :
432 : 0 : vl->append(BuildConnVal());
433 : 0 : vl->append(username->Ref());
434 [ # # ]: 0 : vl->append(client_name ? client_name->Ref() : new StringVal(""));
435 : 0 : vl->append(password);
436 : 0 : vl->append(new StringVal(line));
437 : :
438 : 0 : ConnectionEvent(f, vl);
439 : : }
440 : :
441 : 0 : const char* Login_Analyzer::GetUsername(const char* line) const
442 : : {
443 [ # # ]: 0 : while ( isspace(*line) )
444 : 0 : ++line;
445 : :
446 : 0 : return line;
447 : : }
448 : :
449 : 0 : void Login_Analyzer::LineEvent(EventHandlerPtr f, const char* line)
450 : : {
451 : 0 : val_list* vl = new val_list;
452 : :
453 : 0 : vl->append(BuildConnVal());
454 : 0 : vl->append(new StringVal(line));
455 : :
456 : 0 : ConnectionEvent(f, vl);
457 : 0 : }
458 : :
459 : :
460 : 0 : void Login_Analyzer::Confused(const char* msg, const char* line)
461 : : {
462 : 0 : state = LOGIN_STATE_CONFUSED; // to suppress further messages
463 : :
464 [ # # ]: 0 : if ( login_confused )
465 : : {
466 : 0 : val_list* vl = new val_list;
467 : 0 : vl->append(BuildConnVal());
468 : 0 : vl->append(new StringVal(msg));
469 : 0 : vl->append(new StringVal(line));
470 : :
471 : 0 : ConnectionEvent(login_confused, vl);
472 : : }
473 : :
474 [ # # ]: 0 : if ( login_confused_text )
475 : : {
476 : : // Send all of the typeahead, and the current line, as
477 : : // confusion text.
478 [ # # ]: 0 : while ( HaveTypeahead() )
479 : : {
480 : 0 : char* s = PopUserText();
481 : 0 : ConfusionText(s);
482 [ # # ]: 0 : delete [] s;
483 : : }
484 : :
485 : 0 : ConfusionText(line);
486 : : }
487 : 0 : }
488 : :
489 : 0 : void Login_Analyzer::ConfusionText(const char* line)
490 : : {
491 [ # # ]: 0 : if ( login_confused_text )
492 : : {
493 : 0 : val_list* vl = new val_list;
494 : 0 : vl->append(BuildConnVal());
495 : 0 : vl->append(new StringVal(line));
496 : 0 : ConnectionEvent(login_confused_text, vl);
497 : : }
498 : 0 : }
499 : :
500 : 0 : int Login_Analyzer::IsPloy(const char* line)
501 : : {
502 [ # # ][ # # ]: 0 : if ( IsLoginPrompt(line) || IsFailureMsg(line) || IsSuccessMsg(line) ||
[ # # ][ # # ]
[ # # ]
503 : : IsSkipAuthentication(line) )
504 : : {
505 : 0 : saw_ploy = 1;
506 : 0 : Confused("possible_login_ploy", line);
507 : 0 : return 1;
508 : : }
509 : : else
510 : 0 : return 0;
511 : : }
512 : :
513 : 0 : int Login_Analyzer::IsSkipAuthentication(const char* line) const
514 : : {
515 : 0 : return re_skip_authentication->MatchAnywhere(line);
516 : : }
517 : :
518 : 0 : const char* Login_Analyzer::IsLoginPrompt(const char* line) const
519 : : {
520 : 0 : int prompt_match = re_login_prompts->MatchAnywhere(line);
521 [ # # # # ]: 0 : if ( ! prompt_match || IsFailureMsg(line) )
[ # # ]
522 : : // IRIX can report "login: ERROR: Login incorrect"
523 : 0 : return 0;
524 : :
525 : 0 : return &line[prompt_match];
526 : : }
527 : :
528 : 0 : int Login_Analyzer::IsDirectLoginPrompt(const char* line) const
529 : : {
530 : 0 : return re_direct_login_prompts->MatchAnywhere(line);
531 : : }
532 : :
533 : 0 : int Login_Analyzer::IsFailureMsg(const char* line) const
534 : : {
535 : : return re_login_failure_msgs->MatchAnywhere(line) &&
536 [ # # ][ # # ]: 0 : ! re_login_non_failure_msgs->MatchAnywhere(line);
537 : : }
538 : :
539 : 0 : int Login_Analyzer::IsSuccessMsg(const char* line) const
540 : : {
541 : 0 : return re_login_success_msgs->MatchAnywhere(line);
542 : : }
543 : :
544 : 0 : int Login_Analyzer::IsTimeout(const char* line) const
545 : : {
546 : 0 : return re_login_timeouts->MatchAnywhere(line);
547 : : }
548 : :
549 : 0 : int Login_Analyzer::IsEmpty(const char* line) const
550 : : {
551 [ # # ][ # # ]: 0 : while ( *line && isspace(*line) )
552 : 0 : ++line;
553 : :
554 : 0 : return *line == '\0';
555 : : }
556 : :
557 : 0 : void Login_Analyzer::AddUserText(const char* line)
558 : : {
559 [ # # ]: 0 : if ( num_user_text >= MAX_USER_TEXT )
560 : 0 : Confused("excessive_typeahead", line);
561 : : else
562 : : {
563 [ # # ]: 0 : if ( ++user_text_last == MAX_USER_TEXT )
564 : 0 : user_text_last = 0;
565 : :
566 : 0 : user_text[user_text_last] = copy_string(line);
567 : :
568 : 0 : ++num_user_text;
569 : : }
570 : 0 : }
571 : :
572 : 0 : char* Login_Analyzer::PeekUserText() const
573 : : {
574 [ # # ]: 0 : if ( num_user_text <= 0 )
575 : 0 : internal_error("underflow in Login_Analyzer::PeekUserText()");
576 : :
577 : 0 : return user_text[user_text_first];
578 : : }
579 : :
580 : 0 : char* Login_Analyzer::PopUserText()
581 : : {
582 : 0 : char* s = PeekUserText();
583 : :
584 [ # # ]: 0 : if ( ++user_text_first == MAX_USER_TEXT )
585 : 0 : user_text_first = 0;
586 : :
587 : 0 : --num_user_text;
588 : :
589 : 0 : return s;
590 : : }
591 : :
592 : 0 : Val* Login_Analyzer::PopUserTextVal()
593 : : {
594 : 0 : char* s = PopUserText();
595 : 0 : BroString* bs = new BroString(1, byte_vec(s), strlen(s));
596 : 0 : return new StringVal(bs);
597 : : }
598 : :
599 : 0 : int Login_Analyzer::MatchesTypeahead(const char* line) const
600 : : {
601 [ # # ]: 0 : for ( int i = user_text_first, n = 0; n < num_user_text; ++i, ++n )
602 : : {
603 [ # # ]: 0 : if ( i == MAX_USER_TEXT )
604 : 0 : i = 0;
605 : :
606 [ # # ]: 0 : if ( streq(user_text[i], line) )
607 : 0 : return 1;
608 : : }
609 : :
610 : 0 : return 0;
611 : : }
612 : :
613 : 0 : void Login_Analyzer::FlushEmptyTypeahead()
614 : : {
615 [ # # ][ # # ]: 0 : while ( HaveTypeahead() && IsEmpty(PeekUserText()) )
[ # # ]
616 [ # # ]: 0 : delete [] PopUserText();
617 : 0 : }
618 : :
619 : 0 : RE_Matcher* init_RE(ListVal* l)
620 : : {
621 : 0 : RE_Matcher* re = l->BuildRE();
622 [ # # ]: 0 : if ( re )
623 : 0 : re->Compile();
624 : :
625 : 0 : return re;
626 [ + - ][ + - ]: 6 : }
|