nginx_config/
grammar.rs

1use combine::{eof, many, many1, Parser};
2use combine::{choice, position};
3use combine::combinator::{opaque, no_partial, FnOpaque};
4use combine::error::StreamError;
5use combine::easy::Error;
6
7use ast::{self, Main, Directive, Item};
8use error::ParseError;
9use helpers::{semi, ident, text, string};
10use position::Pos;
11use tokenizer::{TokenStream, Token};
12use value::Value;
13
14use access;
15use core;
16use gzip;
17use headers;
18use proxy;
19use rewrite;
20use log;
21use real_ip;
22
23
24pub enum Code {
25    Redirect(u32),
26    Normal(u32),
27}
28
29pub fn bool<'a>() -> impl Parser<Output=bool, Input=TokenStream<'a>> {
30    choice((
31        ident("on").map(|_| true),
32        ident("off").map(|_| false),
33    ))
34}
35
36pub fn value<'a>() -> impl Parser<Output=Value, Input=TokenStream<'a>> {
37    (position(), string())
38    .and_then(|(p, v)| Value::parse(p, v))
39}
40
41pub fn worker_processes<'a>()
42    -> impl Parser<Output=Item, Input=TokenStream<'a>>
43{
44    use ast::WorkerProcesses;
45    ident("worker_processes")
46    .with(choice((
47        ident("auto").map(|_| WorkerProcesses::Auto),
48        string().and_then(|s| s.value.parse().map(WorkerProcesses::Exact)),
49    )))
50    .skip(semi())
51    .map(Item::WorkerProcesses)
52}
53
54pub fn server_name<'a>() -> impl Parser<Output=Item, Input=TokenStream<'a>> {
55    use ast::ServerName::*;
56    ident("server_name")
57    .with(many1(
58        string().map(|t| {
59            if t.value.starts_with("~") {
60                Regex(t.value[1..].to_string())
61            } else if t.value.starts_with("*.") {
62                StarSuffix(t.value[2..].to_string())
63            } else if t.value.ends_with(".*") {
64                StarPrefix(t.value[..t.value.len()-2].to_string())
65            } else if t.value.starts_with(".") {
66                Suffix(t.value[1..].to_string())
67            } else {
68                Exact(t.value.to_string())
69            }
70        })
71    ))
72    .skip(semi())
73    .map(Item::ServerName)
74}
75
76
77pub fn map<'a>() -> impl Parser<Output=Item, Input=TokenStream<'a>> {
78    use tokenizer::Kind::{BlockStart, BlockEnd};
79    use helpers::kind;
80    enum Tok {
81        Hostnames,
82        Volatile,
83        Pattern(String, Value),
84        Default(Value),
85        Include(String),
86    }
87    ident("map")
88    .with(value())
89    .and(string().and_then(|t| {
90        let ch1 = t.value.chars().nth(0).unwrap_or(' ');
91        let ch2 = t.value.chars().nth(1).unwrap_or(' ');
92        if ch1 == '$' && matches!(ch2, 'a'...'z' | 'A'...'Z' | '_') &&
93            t.value[2..].chars()
94            .all(|x| matches!(x, 'a'...'z' | 'A'...'Z' | '0'...'9' | '_'))
95        {
96            Ok(t.value[1..].to_string())
97        } else {
98            Err(Error::unexpected_message("invalid variable"))
99        }
100    }))
101    .skip(kind(BlockStart))
102    .and(many(choice((
103        ident("hostnames").map(|_| Tok::Hostnames),
104        ident("volatile").map(|_| Tok::Volatile),
105        ident("default").with(value()).map(|v| Tok::Default(v)),
106        ident("include").with(raw()).map(|v| Tok::Include(v)),
107        raw().and(value()).map(|(s, v)| Tok::Pattern(s, v)),
108    )).skip(semi())))
109    .skip(kind(BlockEnd))
110    .map(|((expression, variable), vec): ((_, _), Vec<Tok>)| {
111        let mut res = ::ast::Map {
112            variable, expression,
113            default: None,
114            hostnames: false,
115            volatile: false,
116            includes: Vec::new(),
117            patterns: Vec::new(),
118        };
119        for val in vec {
120            match val {
121                Tok::Hostnames => res.hostnames = true,
122                Tok::Volatile => res.volatile = true,
123                Tok::Default(v) => res.default = Some(v),
124                Tok::Include(path) => res.includes.push(path),
125                Tok::Pattern(x, targ) => {
126                    use ast::MapPattern::*;
127                    let mut s = &x[..];
128                    if s.starts_with('~') {
129                        res.patterns.push((Regex(s[1..].to_string()), targ));
130                        continue;
131                    } else if s.starts_with('\\') {
132                        s = &s[1..];
133                    }
134                    let pat = if res.hostnames {
135                        if s.starts_with("*.") {
136                            StarSuffix(s[2..].to_string())
137                        } else if s.ends_with(".*") {
138                            StarPrefix(s[..s.len()-2].to_string())
139                        } else if s.starts_with(".") {
140                            Suffix(s[1..].to_string())
141                        } else {
142                            Exact(s.to_string())
143                        }
144                    } else {
145                        Exact(s.to_string())
146                    };
147                    res.patterns.push((pat, targ));
148                }
149            }
150        }
151        Item::Map(res)
152    })
153}
154
155pub fn block<'a>()
156    -> FnOpaque<TokenStream<'a>, ((Pos, Pos), Vec<Directive>)>
157{
158    use tokenizer::Kind::{BlockStart, BlockEnd};
159    use helpers::kind;
160    opaque(|f| {
161        f(&mut no_partial((
162                position(),
163                kind(BlockStart)
164                    .with(many(directive()))
165                    .skip(kind(BlockEnd)),
166                position(),
167        ))
168        .map(|(s, dirs, e)| ((s, e), dirs)))
169    })
170}
171
172// A string that forbids variables
173pub fn raw<'a>() -> impl Parser<Output=String, Input=TokenStream<'a>> {
174    // TODO(tailhook) unquote single and double quotes
175    // error on variables?
176    string().and_then(|t| Ok::<_, Error<_, _>>(t.value.to_string()))
177}
178
179pub fn location<'a>() -> impl Parser<Output=Item, Input=TokenStream<'a>> {
180    use ast::LocationPattern::*;
181    ident("location").with(choice((
182        text("=").with(raw().map(Exact)),
183        text("^~").with(raw().map(FinalPrefix)),
184        text("~").with(raw().map(Regex)),
185        text("~*").with(raw().map(RegexInsensitive)),
186        raw()
187            .map(|v| if v.starts_with('*') {
188                Named(v)
189            } else {
190                Prefix(v)
191            }),
192    ))).and(block())
193    .map(|(pattern, (position, directives))| {
194        Item::Location(ast::Location { pattern, position, directives })
195    })
196}
197
198impl Code {
199    pub fn parse<'x, 'y>(code_str: &'x str)
200        -> Result<Code, Error<Token<'y>, Token<'y>>>
201    {
202        let code = code_str.parse::<u32>()?;
203        match code {
204            301 | 302 | 303 | 307 | 308 => Ok(Code::Redirect(code)),
205            200...599 => Ok(Code::Normal(code)),
206            _ => return Err(Error::unexpected_message(
207                format!("invalid response code {}", code))),
208        }
209    }
210    pub fn as_code(&self) -> u32 {
211        match *self {
212            Code::Redirect(code) => code,
213            Code::Normal(code) => code,
214        }
215    }
216}
217
218
219pub fn try_files<'a>() -> impl Parser<Output=Item, Input=TokenStream<'a>> {
220    use ast::TryFilesLastOption::*;
221    use ast::Item::TryFiles;
222    use value::Item::*;
223
224    ident("try_files")
225    .with(many1(value()))
226    .skip(semi())
227    .and_then(|mut v: Vec<_>| -> Result<_, Error<_, _>> {
228        let last = v.pop().unwrap();
229        let last = match &last.data[..] {
230            [Literal(x)] if x.starts_with("=") => {
231                Code(self::Code::parse(&x[1..])?.as_code())
232            }
233            [Literal(x)] if x.starts_with("@") => {
234                NamedLocation(x[1..].to_string())
235            }
236            _ => Uri(last.clone()),
237        };
238        Ok(TryFiles(::ast::TryFiles {
239            options: v,
240            last_option: last,
241        }))
242    })
243}
244
245
246pub fn openresty<'a>() -> impl Parser<Output=Item, Input=TokenStream<'a>> {
247    use ast::Item::*;
248    choice((
249        ident("rewrite_by_lua_file").with(value()).skip(semi())
250            .map(Item::RewriteByLuaFile),
251        ident("balancer_by_lua_file").with(value()).skip(semi())
252            .map(BalancerByLuaFile),
253        ident("access_by_lua_file").with(value()).skip(semi())
254            .map(AccessByLuaFile),
255        ident("header_filter_by_lua_file").with(value()).skip(semi())
256            .map(HeaderFilterByLuaFile),
257        ident("content_by_lua_file").with(value()).skip(semi())
258            .map(ContentByLuaFile),
259        ident("body_filter_by_lua_file").with(value()).skip(semi())
260            .map(BodyFilterByLuaFile),
261        ident("log_by_lua_file").with(value()).skip(semi())
262            .map(LogByLuaFile),
263        ident("lua_need_request_body").with(value()).skip(semi())
264            .map(LuaNeedRequestBody),
265        ident("ssl_certificate_by_lua_file").with(value()).skip(semi())
266            .map(SslCertificateByLuaFile),
267        ident("ssl_session_fetch_by_lua_file").with(value()).skip(semi())
268            .map(SslSessionFetchByLuaFile),
269        ident("ssl_session_store_by_lua_file").with(value()).skip(semi())
270            .map(SslSessionStoreByLuaFile),
271    ))
272}
273
274pub fn directive<'a>() -> impl Parser<Output=Directive, Input=TokenStream<'a>>
275{
276    position()
277    .and(choice((
278        ident("daemon").with(bool()).skip(semi())
279            .map(Item::Daemon),
280        ident("master_process").with(bool()).skip(semi())
281            .map(Item::MasterProcess),
282        worker_processes(),
283        ident("http").with(block())
284            .map(|(position, directives)| ast::Http { position, directives })
285            .map(Item::Http),
286        ident("server").with(block())
287            .map(|(position, directives)| ast::Server { position, directives })
288            .map(Item::Server),
289        rewrite::directives(),
290        try_files(),
291        ident("include").with(value()).skip(semi()).map(Item::Include),
292        ident("ssl_certificate").with(value()).skip(semi())
293            .map(Item::SslCertificate),
294        ident("ssl_certificate_key").with(value()).skip(semi())
295            .map(Item::SslCertificateKey),
296        location(),
297        headers::directives(),
298        server_name(),
299        map(),
300        ident("client_max_body_size").with(value()).skip(semi())
301            .map(Item::ClientMaxBodySize),
302        proxy::directives(),
303        gzip::directives(),
304        core::directives(),
305        access::directives(),
306        log::directives(),
307        real_ip::directives(),
308        openresty(),
309        // it's own module
310        ident("empty_gif").skip(semi()).map(|_| Item::EmptyGif),
311        ident("index").with(many(value())).skip(semi())
312            .map(Item::Index),
313    )))
314    .map(|(pos, dir)| Directive {
315        position: pos,
316        item: dir,
317    })
318}
319
320
321/// Parses a piece of config in "main" context (i.e. top-level)
322///
323/// Currently, this is the same as parse_directives (except wraps everyting
324/// to a `Main` struct), but we expect to
325/// add validation/context checks in this function.
326pub fn parse_main(s: &str) -> Result<Main, ParseError> {
327    parse_directives(s).map(|directives| Main { directives })
328}
329
330/// Parses a piece of config from arbitrary context
331///
332/// This implies no validation of what context directives belong to.
333pub fn parse_directives(s: &str) -> Result<Vec<Directive>, ParseError> {
334    let mut tokens = TokenStream::new(s);
335    let (doc, _) = many1(directive())
336        .skip(eof())
337        .parse_stream(&mut tokens)
338        .map_err(|e| e.into_inner().error)?;
339    Ok(doc)
340}