Repository: certbot/certbot

Issue:
Nginx parser apparently can't parse "map"
I was looking at issues on the nginxparser project and found this one:

https://github.com/fatiherikli/nginxparser/issues/19

It appears to me that we can't parse configurations containing this. I got something like

`ParseException: Expected stringEnd (at char 100), (line:5, col:1)`

when trying to `nginxparser.loads()` a file that included that map directive.

cc @pde 



Discussion:
By contrast, one of the sample configs says

```
map $scheme $https {
    default '';
    https on;
}
```

and the parser _is_ able to parse this correctly.

The `map` that can't be parsed is

```
map $http_user_agent $mobile {
    default       0;
    "~Opera Mini" 1;
}
```

I'm not sure what the relevant difference between the two will turn out to be.


Resolving patch:
diff --git a/certbot-nginx/certbot_nginx/nginxparser.py b/certbot-nginx/certbot_nginx/nginxparser.py
--- a/certbot-nginx/certbot_nginx/nginxparser.py
+++ b/certbot-nginx/certbot_nginx/nginxparser.py
@@ -23,10 +23,12 @@ class RawNginxParser(object):
     right_bracket = space.leaveWhitespace() + Literal("}").suppress()
     semicolon = Literal(";").suppress()
     key = Word(alphanums + "_/+-.")
-    dollar_var = Combine(Literal('$') + nonspace)
+    dollar_var = Combine(Literal('$') + Regex(r"[^\{\};,\s]+"))
     condition = Regex(r"\(.+\)")
     # Matches anything that is not a special character AND any chars in single
     # or double quotes
+    # All of these COULD be upgraded to something like
+    # https://stackoverflow.com/a/16130746
     value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+")
     location = CharsNotIn("{};," + string.whitespace)
     # modifier for location uri [ = | ~ | ~* | ^~ ]
@@ -38,19 +40,35 @@ class RawNginxParser(object):
     assignment = space + key + Optional(space + value, default=None) + semicolon
     location_statement = space + Optional(modifier) + Optional(space + location + space)
     if_statement = space + Literal("if") + space + condition + space
+
     map_statement = space + Literal("map") + space + nonspace + space + dollar_var + space
-    block = Forward()
+    # This is NOT an accurate way to parse nginx map entries; it's almost
+    # certianly too permissive and may be wrong in other ways, but it should
+    # preserve things correctly in mmmmost or all cases.
+    #
+    #    - I can neither prove nor disprove that it is corect wrt all escaped
+    #      semicolon situations
+    # Addresses https://github.com/fatiherikli/nginxparser/issues/19
+    map_pattern = Regex(r'".*"') | Regex(r"'.*'") | nonspace
+    map_entry = space + map_pattern + space + value + space + semicolon
+    map_block = Group(
+        # key could for instance be "server" or "http", or "location" (in which case
+        # location_statement needs to have a non-empty location)
+        Group(map_statement).leaveWhitespace() +
+        left_bracket +
+        Group(ZeroOrMore(Group(comment | map_entry)) + space).leaveWhitespace() +
+        right_bracket)
 
+    block = Forward()
     block << Group(
         # key could for instance be "server" or "http", or "location" (in which case
         # location_statement needs to have a non-empty location)
-        (Group(space + key + location_statement) ^ Group(if_statement) ^
-        Group(map_statement)).leaveWhitespace() +
+        (Group(space + key + location_statement) ^ Group(if_statement)).leaveWhitespace() +
         left_bracket +
-        Group(ZeroOrMore(Group(comment | assignment) | block) + space).leaveWhitespace() +
-        right_bracket)
+        Group(ZeroOrMore(Group(comment | assignment) | block | map_block) + space).leaveWhitespace()
+              + right_bracket)
 
-    script = OneOrMore(Group(comment | assignment) ^ block) + space + stringEnd
+    script = OneOrMore(Group(comment | assignment) ^ block ^ map_block) + space + stringEnd
     script.parseWithTabs()
 
     def __init__(self, source):


Test patch: