2 * Copyright (C) 1996-2023 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"
12 #include "base/RegexPattern.h"
14 #include "ConfigParser.h"
15 #include "debug/Stream.h"
18 #include "neighbors.h"
19 #include "sbuf/Stream.h"
21 bool ConfigParser::RecognizeQuotedValues
= true;
22 bool ConfigParser::StrictMode
= true;
23 std::stack
<ConfigParser::CfgFile
*> ConfigParser::CfgFiles
;
24 ConfigParser::TokenType
ConfigParser::LastTokenType
= ConfigParser::SimpleToken
;
25 const char *ConfigParser::CfgLine
= nullptr;
26 const char *ConfigParser::CfgPos
= nullptr;
27 std::queue
<char *> ConfigParser::CfgLineTokens_
;
28 bool ConfigParser::AllowMacros_
= false;
29 bool ConfigParser::ParseQuotedOrToEol_
= false;
30 bool ConfigParser::ParseKvPair_
= false;
31 ConfigParser::ParsingStates
ConfigParser::KvPairState_
= ConfigParser::atParseKey
;
32 bool ConfigParser::RecognizeQuotedPair_
= false;
33 bool ConfigParser::PreviewMode_
= false;
35 static const char *SQUID_ERROR_TOKEN
= "[invalid token]";
38 ConfigParser::destruct()
41 if (!CfgFiles
.empty()) {
42 std::ostringstream message
;
43 CfgFile
*f
= CfgFiles
.top();
44 message
<< "Bungled " << f
->filePath
<< " line " << f
->lineNo
<<
45 ": " << f
->currentLine
<< std::endl
;
48 while (!CfgFiles
.empty()) {
50 message
<< " included from " << f
->filePath
<< " line " <<
51 f
->lineNo
<< ": " << f
->currentLine
<< std::endl
;
55 message
<< " included from " << cfg_filename
<< " line " <<
56 config_lineno
<< ": " << config_input_line
<< std::endl
;
57 std::string msg
= message
.str();
58 fatalf("%s", msg
.c_str());
60 fatalf("Bungled %s line %d: %s",
61 cfg_filename
, config_lineno
, config_input_line
);
65 ConfigParser::strtokFile()
67 if (RecognizeQuotedValues
)
68 return ConfigParser::NextToken();
70 static int fromFile
= 0;
71 static FILE *wordFile
= nullptr;
74 static char buf
[CONFIG_LINE_LIMIT
];
79 ConfigParser::TokenType tokenType
;
80 t
= ConfigParser::NextElement(tokenType
);
83 } else if (*t
== '\"' || *t
== '\'') {
84 /* quote found, start reading from file */
85 debugs(3, 8,"Quoted token found : " << t
);
88 while (*t
&& *t
!= '\"' && *t
!= '\'')
93 if ((wordFile
= fopen(fn
, "r")) == nullptr) {
94 debugs(3, DBG_CRITICAL
, "ERROR: Can not open file " << fn
<< " for reading");
99 setmode(fileno(wordFile
), O_TEXT
);
109 if (fgets(buf
, sizeof(buf
), wordFile
) == nullptr) {
110 /* stop reading from file */
118 /* skip leading and trailing white space */
119 t
+= strspn(buf
, w_space
);
120 t2
= t
+ strcspn(t
, w_space
);
121 t3
= t2
+ strspn(t2
, w_space
);
123 while (*t3
&& *t3
!= '#') {
124 t2
= t3
+ strcspn(t3
, w_space
);
125 t3
= t2
+ strspn(t2
, w_space
);
132 /* skip blank lines */
133 } while ( *t
== '#' || !*t
);
139 ConfigParser::UnQuote(const char *token
, const char **next
)
141 const char *errorStr
= nullptr;
142 const char *errorPos
= nullptr;
143 char quoteChar
= *token
;
144 assert(quoteChar
== '"' || quoteChar
== '\'');
145 LOCAL_ARRAY(char, UnQuoted
, CONFIG_LINE_LIMIT
);
146 const char *s
= token
+ 1;
148 /* scan until the end of the quoted string, handling escape sequences*/
149 while (*s
&& *s
!= quoteChar
&& !errorStr
&& (size_t)(d
- UnQuoted
) < sizeof(UnQuoted
)) {
164 errorStr
= "Unsupported escape sequence";
176 if (*s
!= quoteChar
&& !errorStr
) {
177 errorStr
= "missing quote char at the end of quoted string";
183 // We are expecting a separator after quoted string, space or one of "()#"
184 if (*(s
+ 1) != '\0' && !strchr(w_space
"()#", *(s
+ 1)) && !errorStr
) {
185 errorStr
= "Expecting space after the end of quoted token";
191 xstrncpy(UnQuoted
, SQUID_ERROR_TOKEN
, sizeof(UnQuoted
));
193 debugs(3, DBG_CRITICAL
, "FATAL: " << errorStr
<< ": " << errorPos
);
204 ConfigParser::SetCfgLine(char *line
)
208 while (!CfgLineTokens_
.empty()) {
209 char *token
= CfgLineTokens_
.front();
210 CfgLineTokens_
.pop();
216 ConfigParser::CurrentLocation()
218 return ToSBuf(SourceLocation(cfg_directive
, cfg_filename
, config_lineno
));
222 ConfigParser::TokenParse(const char * &nextToken
, ConfigParser::TokenType
&type
)
224 if (!nextToken
|| *nextToken
== '\0')
226 type
= ConfigParser::SimpleToken
;
227 nextToken
+= strspn(nextToken
, w_space
);
229 if (*nextToken
== '#')
232 if (ConfigParser::RecognizeQuotedValues
&& (*nextToken
== '"' || *nextToken
== '\'')) {
233 type
= ConfigParser::QuotedToken
;
234 char *token
= xstrdup(UnQuote(nextToken
, &nextToken
));
235 CfgLineTokens_
.push(token
);
239 const char *tokenStart
= nextToken
;
241 if (ConfigParser::ParseKvPair_
) {
242 if (ConfigParser::KvPairState_
== ConfigParser::atParseKey
)
246 } else if (ConfigParser::ParseQuotedOrToEol_
)
248 else if (ConfigParser::RecognizeQuotedPair_
)
250 else if (!ConfigParser::RecognizeQuotedValues
|| *nextToken
== '(')
254 nextToken
+= strcspn(nextToken
, sep
);
256 while (ConfigParser::RecognizeQuotedPair_
&& *nextToken
== '\\') {
257 // NP: do not permit \0 terminator to be escaped.
258 if (*(nextToken
+1) && *(nextToken
+1) != '\r' && *(nextToken
+1) != '\n') {
259 nextToken
+= 2; // skip the quoted-pair (\-escaped) character
260 nextToken
+= strcspn(nextToken
, sep
);
262 debugs(3, DBG_CRITICAL
, "FATAL: Unescaped '\' character in regex pattern: " << tokenStart
);
267 if (ConfigParser::RecognizeQuotedValues
&& *nextToken
== '(') {
268 if (strncmp(tokenStart
, "parameters", nextToken
- tokenStart
) == 0)
269 type
= ConfigParser::FunctionParameters
;
272 char *err
= xstrdup(SQUID_ERROR_TOKEN
);
273 CfgLineTokens_
.push(err
);
276 debugs(3, DBG_CRITICAL
, "FATAL: Unknown cfg function: " << tokenStart
);
281 type
= ConfigParser::SimpleToken
;
283 char *token
= nullptr;
284 if (nextToken
- tokenStart
) {
285 if (ConfigParser::StrictMode
&& type
== ConfigParser::SimpleToken
) {
286 bool tokenIsNumber
= true;
287 for (const char *s
= tokenStart
; s
!= nextToken
; ++s
) {
288 const bool isValidChar
= isalnum(*s
) || strchr(".,()-=_/:+", *s
) ||
289 (tokenIsNumber
&& *s
== '%' && (s
+ 1 == nextToken
));
292 tokenIsNumber
= false;
296 char *err
= xstrdup(SQUID_ERROR_TOKEN
);
297 CfgLineTokens_
.push(err
);
300 debugs(3, DBG_CRITICAL
, "FATAL: Not alphanumeric character '"<< *s
<< "' in unquoted token " << tokenStart
);
306 token
= xstrndup(tokenStart
, nextToken
- tokenStart
+ 1);
307 CfgLineTokens_
.push(token
);
310 if (*nextToken
!= '\0' && *nextToken
!= '#') {
318 ConfigParser::NextElement(ConfigParser::TokenType
&type
)
320 const char *pos
= CfgPos
;
321 char *token
= TokenParse(pos
, type
);
322 // If not in preview mode the next call of this method should start
323 // parsing after the end of current token.
324 // For function "parameters(...)" we need always to update current parsing
325 // position to allow parser read the arguments of "parameters(..)"
326 if (!PreviewMode_
|| type
== FunctionParameters
)
328 // else next call will read the same token
333 ConfigParser::NextToken()
335 char *token
= nullptr;
338 while (token
== nullptr && !CfgFiles
.empty()) {
339 ConfigParser::CfgFile
*wordfile
= CfgFiles
.top();
340 token
= wordfile
->parse(LastTokenType
);
342 assert(!wordfile
->isOpen());
344 debugs(3, 4, "CfgFiles.pop " << wordfile
->filePath
);
350 token
= NextElement(LastTokenType
);
352 if (token
&& LastTokenType
== ConfigParser::FunctionParameters
) {
353 //Disable temporary preview mode, we need to parse function parameters
354 const bool savePreview
= ConfigParser::PreviewMode_
;
355 ConfigParser::PreviewMode_
= false;
357 char *path
= NextToken();
358 if (LastTokenType
!= ConfigParser::QuotedToken
) {
359 debugs(3, DBG_CRITICAL
, "FATAL: Quoted filename missing: " << token
);
364 // The next token in current cfg file line must be a ")"
365 char *end
= NextToken();
366 ConfigParser::PreviewMode_
= savePreview
;
367 if (LastTokenType
!= ConfigParser::SimpleToken
|| strcmp(end
, ")") != 0) {
368 debugs(3, DBG_CRITICAL
, "FATAL: missing ')' after " << token
<< "(\"" << path
<< "\"");
373 if (CfgFiles
.size() > 16) {
374 debugs(3, DBG_CRITICAL
, "FATAL: can't open %s for reading parameters: includes are nested too deeply (>16)!\n" << path
);
379 ConfigParser::CfgFile
*wordfile
= new ConfigParser::CfgFile();
380 if (!path
|| !wordfile
->startParse(path
)) {
381 debugs(3, DBG_CRITICAL
, "FATAL: Error opening config file: " << token
);
386 CfgFiles
.push(wordfile
);
389 } while (token
== nullptr && !CfgFiles
.empty());
395 ConfigParser::PeekAtToken()
398 char *token
= NextToken();
399 PreviewMode_
= false;
404 ConfigParser::NextQuotedOrToEol()
406 ParseQuotedOrToEol_
= true;
407 char *token
= NextToken();
408 ParseQuotedOrToEol_
= false;
410 // Assume end of current config line
411 // Close all open configuration files for this config line
412 while (!CfgFiles
.empty()) {
413 ConfigParser::CfgFile
*wordfile
= CfgFiles
.top();
422 ConfigParser::optionalKvPair(char * &key
, char * &value
)
427 if (const char *currentToken
= PeekAtToken()) {
428 // NextKvPair() accepts "a = b" and skips "=" or "a=". To avoid
429 // misinterpreting the admin intent, we use strict checks.
430 if (const auto middle
= strchr(currentToken
, '=')) {
431 if (middle
== currentToken
)
432 throw TextException(ToSBuf("missing key in a key=value option: ", currentToken
), Here());
433 if (middle
+ 1 == currentToken
+ strlen(currentToken
))
434 throw TextException(ToSBuf("missing value in a key=value option: ", currentToken
), Here());
436 return false; // not a key=value token
438 if (!NextKvPair(key
, value
)) // may still fail (e.g., bad value quoting)
439 throw TextException(ToSBuf("invalid key=value option: ", currentToken
), Here());
444 return false; // end of directive or input
448 ConfigParser::NextKvPair(char * &key
, char * &value
)
450 key
= value
= nullptr;
452 KvPairState_
= ConfigParser::atParseKey
;
453 if ((key
= NextToken()) != nullptr) {
454 KvPairState_
= ConfigParser::atParseValue
;
455 value
= NextQuotedToken();
457 ParseKvPair_
= false;
462 debugs(3, DBG_CRITICAL
, "ERROR: Failure while parsing key=value token. Value missing after: " << key
);
470 ConfigParser::RegexStrtokFile()
472 if (ConfigParser::RecognizeQuotedValues
) {
473 debugs(3, DBG_CRITICAL
, "FATAL: Can not read regex expression while configuration_includes_quoted_values is enabled");
476 ConfigParser::RecognizeQuotedPair_
= true;
477 char * token
= strtokFile();
478 ConfigParser::RecognizeQuotedPair_
= false;
482 std::unique_ptr
<RegexPattern
>
483 ConfigParser::regex(const char *expectedRegexDescription
)
485 if (RecognizeQuotedValues
)
486 throw TextException("Cannot read regex expression while configuration_includes_quoted_values is enabled", Here());
489 int flags
= REG_EXTENDED
| REG_NOSUB
;
491 ConfigParser::RecognizeQuotedPair_
= true;
492 const auto flagOrPattern
= token(expectedRegexDescription
);
493 if (flagOrPattern
.cmp("-i") == 0) {
495 pattern
= token(expectedRegexDescription
);
496 } else if (flagOrPattern
.cmp("+i") == 0) {
498 pattern
= token(expectedRegexDescription
);
500 pattern
= flagOrPattern
;
502 ConfigParser::RecognizeQuotedPair_
= false;
504 return std::unique_ptr
<RegexPattern
>(new RegexPattern(pattern
, flags
));
508 ConfigParser::cachePeer(const char *peerNameTokenDescription
)
510 if (const auto name
= NextToken()) {
511 debugs(3, 5, CurrentLocation() << ' ' << peerNameTokenDescription
<< ": " << name
);
513 if (const auto p
= findCachePeerByName(name
))
516 throw TextException(ToSBuf("Cannot find a previously declared cache_peer referred to by ",
517 peerNameTokenDescription
, " as ", name
), Here());
520 throw TextException(ToSBuf("Missing ", peerNameTokenDescription
), Here());
524 ConfigParser::NextQuotedToken()
526 const bool saveRecognizeQuotedValues
= ConfigParser::RecognizeQuotedValues
;
527 ConfigParser::RecognizeQuotedValues
= true;
528 char *token
= NextToken();
529 ConfigParser::RecognizeQuotedValues
= saveRecognizeQuotedValues
;
534 ConfigParser::QuoteString(const String
&var
)
536 static String quotedStr
;
537 const char *s
= var
.termedBuf();
538 bool needQuote
= false;
540 for (const char *l
= s
; !needQuote
&& *l
!= '\0'; ++l
)
541 needQuote
= !isalnum(*l
);
547 quotedStr
.append('"');
548 for (; *s
!= '\0'; ++s
) {
549 if (*s
== '"' || *s
== '\\')
550 quotedStr
.append('\\');
551 quotedStr
.append(*s
);
553 quotedStr
.append('"');
554 return quotedStr
.termedBuf();
558 ConfigParser::rejectDuplicateDirective()
560 assert(cfg_directive
);
561 throw TextException("duplicate configuration directive", Here());
565 ConfigParser::closeDirective()
567 assert(cfg_directive
);
568 if (const auto garbage
= PeekAtToken())
569 throw TextException(ToSBuf("trailing garbage at the end of a configuration directive: ", garbage
), Here());
570 // TODO: cfg_directive = nullptr; // currently in generated code
574 ConfigParser::token(const char *expectedTokenDescription
)
576 if (const auto extractedToken
= NextToken()) {
577 debugs(3, 5, CurrentLocation() << ' ' << expectedTokenDescription
<< ": " << extractedToken
);
578 return SBuf(extractedToken
);
580 throw TextException(ToSBuf("missing ", expectedTokenDescription
), Here());
584 ConfigParser::skipOptional(const char *keyword
)
587 if (const auto nextToken
= PeekAtToken()) {
588 if (strcmp(nextToken
, keyword
) == 0) {
592 return false; // the next token on the line is not the optional keyword
594 return false; // no more tokens (i.e. we are at the end of the line)
598 ConfigParser::optionalAclList()
600 if (!skipOptional("if"))
601 return nullptr; // OK: the directive has no ACLs
603 Acl::Tree
*acls
= nullptr;
604 const auto aclCount
= aclParseAclList(*this, &acls
, cfg_directive
);
607 throw TextException("missing ACL name(s) after 'if' keyword", Here());
612 ConfigParser::CfgFile::startParse(char *path
)
614 assert(wordFile
== nullptr);
615 debugs(3, 3, "Parsing from " << path
);
616 if ((wordFile
= fopen(path
, "r")) == nullptr) {
617 debugs(3, DBG_CRITICAL
, "WARNING: file :" << path
<< " not found");
622 setmode(fileno(wordFile
), O_TEXT
);
626 return getFileLine();
630 ConfigParser::CfgFile::getFileLine()
632 // Else get the next line
633 if (fgets(parseBuffer
, CONFIG_LINE_LIMIT
, wordFile
) == nullptr) {
634 /* stop reading from file */
637 parseBuffer
[0] = '\0';
640 parsePos
= parseBuffer
;
641 currentLine
= parseBuffer
;
647 ConfigParser::CfgFile::parse(ConfigParser::TokenType
&type
)
656 while (!(token
= nextElement(type
))) {
664 ConfigParser::CfgFile::nextElement(ConfigParser::TokenType
&type
)
666 const char *pos
= parsePos
;
667 char *token
= TokenParse(pos
, type
);
668 if (!PreviewMode_
|| type
== FunctionParameters
)
670 // else next call will read the same token;
674 ConfigParser::CfgFile::~CfgFile()