2 * Copyright (C) 1996-2021 The Squid Software Foundation and contributors
4 * Squid software is distributed under GPLv2+ license and includes
5 * contributions from numerous individuals and organizations.
6 * Please see the COPYING and CONTRIBUTORS files for details.
10 #include "acl/Gadgets.h"
11 #include "base/Here.h"
13 #include "ConfigParser.h"
17 #include "sbuf/Stream.h"
19 bool ConfigParser::RecognizeQuotedValues
= true;
20 bool ConfigParser::StrictMode
= true;
21 std::stack
<ConfigParser::CfgFile
*> ConfigParser::CfgFiles
;
22 ConfigParser::TokenType
ConfigParser::LastTokenType
= ConfigParser::SimpleToken
;
23 const char *ConfigParser::CfgLine
= NULL
;
24 const char *ConfigParser::CfgPos
= NULL
;
25 std::queue
<char *> ConfigParser::CfgLineTokens_
;
26 std::queue
<std::string
> ConfigParser::Undo_
;
27 bool ConfigParser::AllowMacros_
= false;
28 bool ConfigParser::ParseQuotedOrToEol_
= false;
29 bool ConfigParser::ParseKvPair_
= false;
30 ConfigParser::ParsingStates
ConfigParser::KvPairState_
= ConfigParser::atParseKey
;
31 bool ConfigParser::RecognizeQuotedPair_
= false;
32 bool ConfigParser::PreviewMode_
= false;
34 static const char *SQUID_ERROR_TOKEN
= "[invalid token]";
37 ConfigParser::destruct()
40 if (!CfgFiles
.empty()) {
41 std::ostringstream message
;
42 CfgFile
*f
= CfgFiles
.top();
43 message
<< "Bungled " << f
->filePath
<< " line " << f
->lineNo
<<
44 ": " << f
->currentLine
<< std::endl
;
47 while (!CfgFiles
.empty()) {
49 message
<< " included from " << f
->filePath
<< " line " <<
50 f
->lineNo
<< ": " << f
->currentLine
<< std::endl
;
54 message
<< " included from " << cfg_filename
<< " line " <<
55 config_lineno
<< ": " << config_input_line
<< std::endl
;
56 std::string msg
= message
.str();
57 fatalf("%s", msg
.c_str());
59 fatalf("Bungled %s line %d: %s",
60 cfg_filename
, config_lineno
, config_input_line
);
64 ConfigParser::TokenPutBack(const char *tok
)
73 static char undoToken
[CONFIG_LINE_LIMIT
];
75 xstrncpy(undoToken
, Undo_
.front().c_str(), sizeof(undoToken
));
76 undoToken
[sizeof(undoToken
) - 1] = '\0';
85 ConfigParser::strtokFile()
87 if (RecognizeQuotedValues
)
88 return ConfigParser::NextToken();
90 static int fromFile
= 0;
91 static FILE *wordFile
= NULL
;
94 static char buf
[CONFIG_LINE_LIMIT
];
96 if ((t
= ConfigParser::Undo()))
102 ConfigParser::TokenType tokenType
;
103 t
= ConfigParser::NextElement(tokenType
);
106 } else if (*t
== '\"' || *t
== '\'') {
107 /* quote found, start reading from file */
108 debugs(3, 8,"Quoted token found : " << t
);
111 while (*t
&& *t
!= '\"' && *t
!= '\'')
116 if ((wordFile
= fopen(fn
, "r")) == NULL
) {
117 debugs(3, DBG_CRITICAL
, "ERROR: Can not open file " << fn
<< " for reading");
122 setmode(fileno(wordFile
), O_TEXT
);
132 if (fgets(buf
, sizeof(buf
), wordFile
) == NULL
) {
133 /* stop reading from file */
141 /* skip leading and trailing white space */
142 t
+= strspn(buf
, w_space
);
143 t2
= t
+ strcspn(t
, w_space
);
144 t3
= t2
+ strspn(t2
, w_space
);
146 while (*t3
&& *t3
!= '#') {
147 t2
= t3
+ strcspn(t3
, w_space
);
148 t3
= t2
+ strspn(t2
, w_space
);
155 /* skip blank lines */
156 } while ( *t
== '#' || !*t
);
162 ConfigParser::UnQuote(const char *token
, const char **next
)
164 const char *errorStr
= NULL
;
165 const char *errorPos
= NULL
;
166 char quoteChar
= *token
;
167 assert(quoteChar
== '"' || quoteChar
== '\'');
168 LOCAL_ARRAY(char, UnQuoted
, CONFIG_LINE_LIMIT
);
169 const char *s
= token
+ 1;
171 /* scan until the end of the quoted string, handling escape sequences*/
172 while (*s
&& *s
!= quoteChar
&& !errorStr
&& (size_t)(d
- UnQuoted
) < sizeof(UnQuoted
)) {
187 errorStr
= "Unsupported escape sequence";
199 if (*s
!= quoteChar
&& !errorStr
) {
200 errorStr
= "missing quote char at the end of quoted string";
206 // We are expecting a separator after quoted string, space or one of "()#"
207 if (*(s
+ 1) != '\0' && !strchr(w_space
"()#", *(s
+ 1)) && !errorStr
) {
208 errorStr
= "Expecting space after the end of quoted token";
214 xstrncpy(UnQuoted
, SQUID_ERROR_TOKEN
, sizeof(UnQuoted
));
216 debugs(3, DBG_CRITICAL
, "FATAL: " << errorStr
<< ": " << errorPos
);
227 ConfigParser::SetCfgLine(char *line
)
231 while (!CfgLineTokens_
.empty()) {
232 char *token
= CfgLineTokens_
.front();
233 CfgLineTokens_
.pop();
239 ConfigParser::CurrentLocation()
241 return ToSBuf(SourceLocation(cfg_directive
, cfg_filename
, config_lineno
));
245 ConfigParser::TokenParse(const char * &nextToken
, ConfigParser::TokenType
&type
)
247 if (!nextToken
|| *nextToken
== '\0')
249 type
= ConfigParser::SimpleToken
;
250 nextToken
+= strspn(nextToken
, w_space
);
252 if (*nextToken
== '#')
255 if (ConfigParser::RecognizeQuotedValues
&& (*nextToken
== '"' || *nextToken
== '\'')) {
256 type
= ConfigParser::QuotedToken
;
257 char *token
= xstrdup(UnQuote(nextToken
, &nextToken
));
258 CfgLineTokens_
.push(token
);
262 const char *tokenStart
= nextToken
;
264 if (ConfigParser::ParseKvPair_
) {
265 if (ConfigParser::KvPairState_
== ConfigParser::atParseKey
)
269 } else if (ConfigParser::ParseQuotedOrToEol_
)
271 else if (ConfigParser::RecognizeQuotedPair_
)
273 else if (!ConfigParser::RecognizeQuotedValues
|| *nextToken
== '(')
277 nextToken
+= strcspn(nextToken
, sep
);
279 while (ConfigParser::RecognizeQuotedPair_
&& *nextToken
== '\\') {
280 // NP: do not permit \0 terminator to be escaped.
281 if (*(nextToken
+1) && *(nextToken
+1) != '\r' && *(nextToken
+1) != '\n') {
282 nextToken
+= 2; // skip the quoted-pair (\-escaped) character
283 nextToken
+= strcspn(nextToken
, sep
);
285 debugs(3, DBG_CRITICAL
, "FATAL: Unescaped '\' character in regex pattern: " << tokenStart
);
290 if (ConfigParser::RecognizeQuotedValues
&& *nextToken
== '(') {
291 if (strncmp(tokenStart
, "parameters", nextToken
- tokenStart
) == 0)
292 type
= ConfigParser::FunctionParameters
;
295 char *err
= xstrdup(SQUID_ERROR_TOKEN
);
296 CfgLineTokens_
.push(err
);
299 debugs(3, DBG_CRITICAL
, "FATAL: Unknown cfg function: " << tokenStart
);
304 type
= ConfigParser::SimpleToken
;
307 if (nextToken
- tokenStart
) {
308 if (ConfigParser::StrictMode
&& type
== ConfigParser::SimpleToken
) {
309 bool tokenIsNumber
= true;
310 for (const char *s
= tokenStart
; s
!= nextToken
; ++s
) {
311 const bool isValidChar
= isalnum(*s
) || strchr(".,()-=_/:+", *s
) ||
312 (tokenIsNumber
&& *s
== '%' && (s
+ 1 == nextToken
));
315 tokenIsNumber
= false;
319 char *err
= xstrdup(SQUID_ERROR_TOKEN
);
320 CfgLineTokens_
.push(err
);
323 debugs(3, DBG_CRITICAL
, "FATAL: Not alphanumeric character '"<< *s
<< "' in unquoted token " << tokenStart
);
329 token
= xstrndup(tokenStart
, nextToken
- tokenStart
+ 1);
330 CfgLineTokens_
.push(token
);
333 if (*nextToken
!= '\0' && *nextToken
!= '#') {
341 ConfigParser::NextElement(ConfigParser::TokenType
&type
)
343 const char *pos
= CfgPos
;
344 char *token
= TokenParse(pos
, type
);
345 // If not in preview mode the next call of this method should start
346 // parsing after the end of current token.
347 // For function "parameters(...)" we need always to update current parsing
348 // position to allow parser read the arguments of "parameters(..)"
349 if (!PreviewMode_
|| type
== FunctionParameters
)
351 // else next call will read the same token
356 ConfigParser::NextToken()
359 if ((token
= ConfigParser::Undo())) {
360 debugs(3, 6, "TOKEN (undone): " << token
);
365 while (token
== NULL
&& !CfgFiles
.empty()) {
366 ConfigParser::CfgFile
*wordfile
= CfgFiles
.top();
367 token
= wordfile
->parse(LastTokenType
);
369 assert(!wordfile
->isOpen());
371 debugs(3, 4, "CfgFiles.pop " << wordfile
->filePath
);
377 token
= NextElement(LastTokenType
);
379 if (token
&& LastTokenType
== ConfigParser::FunctionParameters
) {
380 //Disable temporary preview mode, we need to parse function parameters
381 const bool savePreview
= ConfigParser::PreviewMode_
;
382 ConfigParser::PreviewMode_
= false;
384 char *path
= NextToken();
385 if (LastTokenType
!= ConfigParser::QuotedToken
) {
386 debugs(3, DBG_CRITICAL
, "FATAL: Quoted filename missing: " << token
);
391 // The next token in current cfg file line must be a ")"
392 char *end
= NextToken();
393 ConfigParser::PreviewMode_
= savePreview
;
394 if (LastTokenType
!= ConfigParser::SimpleToken
|| strcmp(end
, ")") != 0) {
395 debugs(3, DBG_CRITICAL
, "FATAL: missing ')' after " << token
<< "(\"" << path
<< "\"");
400 if (CfgFiles
.size() > 16) {
401 debugs(3, DBG_CRITICAL
, "FATAL: can't open %s for reading parameters: includes are nested too deeply (>16)!\n" << path
);
406 ConfigParser::CfgFile
*wordfile
= new ConfigParser::CfgFile();
407 if (!path
|| !wordfile
->startParse(path
)) {
408 debugs(3, DBG_CRITICAL
, "FATAL: Error opening config file: " << token
);
413 CfgFiles
.push(wordfile
);
416 } while (token
== NULL
&& !CfgFiles
.empty());
422 ConfigParser::PeekAtToken()
425 char *token
= NextToken();
426 PreviewMode_
= false;
431 ConfigParser::NextQuotedOrToEol()
433 ParseQuotedOrToEol_
= true;
434 char *token
= NextToken();
435 ParseQuotedOrToEol_
= false;
437 // Assume end of current config line
438 // Close all open configuration files for this config line
439 while (!CfgFiles
.empty()) {
440 ConfigParser::CfgFile
*wordfile
= CfgFiles
.top();
449 ConfigParser::optionalKvPair(char * &key
, char * &value
)
454 if (const char *currentToken
= PeekAtToken()) {
455 // NextKvPair() accepts "a = b" and skips "=" or "a=". To avoid
456 // misinterpreting the admin intent, we use strict checks.
457 if (const auto middle
= strchr(currentToken
, '=')) {
458 if (middle
== currentToken
)
459 throw TextException(ToSBuf("missing key in a key=value option: ", currentToken
), Here());
460 if (middle
+ 1 == currentToken
+ strlen(currentToken
))
461 throw TextException(ToSBuf("missing value in a key=value option: ", currentToken
), Here());
463 return false; // not a key=value token
465 if (!NextKvPair(key
, value
)) // may still fail (e.g., bad value quoting)
466 throw TextException(ToSBuf("invalid key=value option: ", currentToken
), Here());
471 return false; // end of directive or input
475 ConfigParser::NextKvPair(char * &key
, char * &value
)
479 KvPairState_
= ConfigParser::atParseKey
;
480 if ((key
= NextToken()) != NULL
) {
481 KvPairState_
= ConfigParser::atParseValue
;
482 value
= NextQuotedToken();
484 ParseKvPair_
= false;
489 debugs(3, DBG_CRITICAL
, "Error while parsing key=value token. Value missing after: " << key
);
497 ConfigParser::RegexStrtokFile()
499 if (ConfigParser::RecognizeQuotedValues
) {
500 debugs(3, DBG_CRITICAL
, "FATAL: Can not read regex expression while configuration_includes_quoted_values is enabled");
503 ConfigParser::RecognizeQuotedPair_
= true;
504 char * token
= strtokFile();
505 ConfigParser::RecognizeQuotedPair_
= false;
510 ConfigParser::RegexPattern()
512 if (ConfigParser::RecognizeQuotedValues
) {
513 debugs(3, DBG_CRITICAL
, "FATAL: Can not read regex expression while configuration_includes_quoted_values is enabled");
516 ConfigParser::RecognizeQuotedPair_
= true;
517 char * token
= NextToken();
518 ConfigParser::RecognizeQuotedPair_
= false;
523 ConfigParser::NextQuotedToken()
525 const bool saveRecognizeQuotedValues
= ConfigParser::RecognizeQuotedValues
;
526 ConfigParser::RecognizeQuotedValues
= true;
527 char *token
= NextToken();
528 ConfigParser::RecognizeQuotedValues
= saveRecognizeQuotedValues
;
533 ConfigParser::QuoteString(const String
&var
)
535 static String quotedStr
;
536 const char *s
= var
.termedBuf();
537 bool needQuote
= false;
539 for (const char *l
= s
; !needQuote
&& *l
!= '\0'; ++l
)
540 needQuote
= !isalnum(*l
);
546 quotedStr
.append('"');
547 for (; *s
!= '\0'; ++s
) {
548 if (*s
== '"' || *s
== '\\')
549 quotedStr
.append('\\');
550 quotedStr
.append(*s
);
552 quotedStr
.append('"');
553 return quotedStr
.termedBuf();
557 ConfigParser::rejectDuplicateDirective()
559 assert(cfg_directive
);
560 throw TextException("duplicate configuration directive", Here());
564 ConfigParser::closeDirective()
566 assert(cfg_directive
);
567 if (const auto garbage
= PeekAtToken())
568 throw TextException(ToSBuf("trailing garbage at the end of a configuration directive: ", garbage
), Here());
569 // TODO: cfg_directive = nullptr; // currently in generated code
573 ConfigParser::token(const char *expectedTokenDescription
)
575 if (const auto extractedToken
= NextToken()) {
576 debugs(3, 5, CurrentLocation() << ' ' << expectedTokenDescription
<< ": " << extractedToken
);
577 return SBuf(extractedToken
);
579 throw TextException(ToSBuf("missing ", expectedTokenDescription
), Here());
583 ConfigParser::skipOptional(const char *keyword
)
586 if (const auto nextToken
= PeekAtToken()) {
587 if (strcmp(nextToken
, keyword
) == 0) {
591 return false; // the next token on the line is not the optional keyword
593 return false; // no more tokens (i.e. we are at the end of the line)
597 ConfigParser::optionalAclList()
599 if (!skipOptional("if"))
600 return nullptr; // OK: the directive has no ACLs
602 Acl::Tree
*acls
= nullptr;
603 const auto aclCount
= aclParseAclList(*this, &acls
, cfg_directive
);
606 throw TextException("missing ACL name(s) after 'if' keyword", Here());
611 ConfigParser::CfgFile::startParse(char *path
)
613 assert(wordFile
== NULL
);
614 debugs(3, 3, "Parsing from " << path
);
615 if ((wordFile
= fopen(path
, "r")) == NULL
) {
616 debugs(3, DBG_CRITICAL
, "WARNING: file :" << path
<< " not found");
621 setmode(fileno(wordFile
), O_TEXT
);
625 return getFileLine();
629 ConfigParser::CfgFile::getFileLine()
631 // Else get the next line
632 if (fgets(parseBuffer
, CONFIG_LINE_LIMIT
, wordFile
) == NULL
) {
633 /* stop reading from file */
636 parseBuffer
[0] = '\0';
639 parsePos
= parseBuffer
;
640 currentLine
= parseBuffer
;
646 ConfigParser::CfgFile::parse(ConfigParser::TokenType
&type
)
655 while (!(token
= nextElement(type
))) {
663 ConfigParser::CfgFile::nextElement(ConfigParser::TokenType
&type
)
665 const char *pos
= parsePos
;
666 char *token
= TokenParse(pos
, type
);
667 if (!PreviewMode_
|| type
== FunctionParameters
)
669 // else next call will read the same token;
673 ConfigParser::CfgFile::~CfgFile()