1
/*
2
 * Hurl (https://hurl.dev)
3
 * Copyright (C) 2024 Orange
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *          http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 */
18
use crate::ast::*;
19
use crate::parser::error::*;
20
use crate::parser::primitives::*;
21
use crate::parser::{expr, ParseResult};
22
use crate::reader::Reader;
23

            
24
13310
pub fn url(reader: &mut Reader) -> ParseResult<Template> {
25
13310
    // Must be neither JSON-encoded nor empty.
26
13310
    // But more restrictive: whitelist characters, not empty
27
13310
    let start = reader.cursor();
28
13310
    let mut elements = vec![];
29
13310
    let mut buffer = String::new();
30
13310

            
31
13310
    if !url_prefix_valid(reader) {
32
20
        return Err(ParseError::new(
33
20
            reader.cursor().pos,
34
20
            false,
35
20
            ParseErrorKind::UrlInvalidStart,
36
20
        ));
37
    }
38

            
39
    loop {
40
449280
        let save = reader.cursor();
41
449280
        match line_terminator(reader) {
42
            Ok(_) => {
43
13280
                reader.seek(save);
44
13280
                break;
45
            }
46
436000
            _ => reader.seek(save),
47
        }
48
436000

            
49
436000
        match expr::parse(reader) {
50
130
            Ok(value) => {
51
130
                if !buffer.is_empty() {
52
95
                    elements.push(TemplateElement::String {
53
95
                        value: buffer.clone(),
54
95
                        encoded: buffer.clone(),
55
95
                    });
56
95
                    buffer = String::new();
57
                }
58
130
                elements.push(TemplateElement::Expression(value));
59
            }
60
435870
            Err(e) => {
61
435870
                if !e.recoverable {
62
5
                    return Err(e);
63
                } else {
64
435865
                    reader.seek(save);
65
435865
                    match reader.read() {
66
                        None => break,
67
435865
                        Some(c) => {
68
435865
                            if c.is_alphanumeric()
69
435865
                                | [
70
435865
                                    ':', '/', '.', '-', '?', '=', '&', '_', '%', '*', ',', '@',
71
435865
                                    '~', '+', '!', '$', '\'', '(', ')', ';', '[', ']',
72
435865
                                ]
73
435865
                                .contains(&c)
74
435860
                            {
75
435860
                                buffer.push(c);
76
435860
                            } else {
77
5
                                reader.seek(save);
78
5
                                break;
79
                            }
80
                        }
81
                    }
82
                }
83
            }
84
        }
85
    }
86

            
87
13285
    if !buffer.is_empty() {
88
13155
        elements.push(TemplateElement::String {
89
13155
            value: buffer.clone(),
90
13155
            encoded: buffer,
91
13155
        });
92
    }
93

            
94
    // URLs should be followed by a line terminator
95
13285
    let save = reader.cursor();
96
13285
    if line_terminator(reader).is_err() {
97
5
        reader.seek(save);
98
5
        let c = reader.peek().unwrap();
99
5
        return Err(ParseError::new(
100
5
            reader.cursor().pos,
101
5
            false,
102
5
            ParseErrorKind::UrlIllegalCharacter(c),
103
5
        ));
104
    }
105
13280

            
106
13280
    reader.seek(save);
107
13280
    Ok(Template {
108
13280
        delimiter: None,
109
13280
        elements,
110
13280
        source_info: SourceInfo::new(start.pos, reader.cursor().pos),
111
13280
    })
112
}
113

            
114
/// Returns true if url starts with http://, https:// or {{
115
13310
fn url_prefix_valid(reader: &mut Reader) -> bool {
116
13310
    let prefixes = ["https://", "http://", "{{"];
117
26465
    for expected_p in prefixes.iter() {
118
26465
        let current_p = reader.peek_n(expected_p.len());
119
26465
        if &current_p == expected_p {
120
13290
            return true;
121
        }
122
    }
123
20
    false
124
}
125

            
126
#[cfg(test)]
127
mod tests {
128
    use super::*;
129
    use crate::reader::Pos;
130

            
131
    #[test]
132
    fn test_url() {
133
        let mut reader = Reader::new("http://google.fr # ");
134
        assert_eq!(
135
            url(&mut reader).unwrap(),
136
            Template {
137
                elements: vec![TemplateElement::String {
138
                    value: String::from("http://google.fr"),
139
                    encoded: String::from("http://google.fr"),
140
                }],
141
                delimiter: None,
142
                source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 17)),
143
            }
144
        );
145
        assert_eq!(reader.cursor().index, 16);
146
    }
147

            
148
    #[test]
149
    fn test_url2() {
150
        let mut reader = Reader::new("http://localhost:8000/cookies/set-session-cookie2-valueA");
151
        assert_eq!(
152
            url(&mut reader).unwrap(),
153
            Template {
154
                elements: vec![TemplateElement::String {
155
                    value: String::from("http://localhost:8000/cookies/set-session-cookie2-valueA"),
156
                    encoded: String::from(
157
                        "http://localhost:8000/cookies/set-session-cookie2-valueA"
158
                    ),
159
                }],
160
                delimiter: None,
161
                source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 57)),
162
            }
163
        );
164
        assert_eq!(reader.cursor().index, 56);
165
    }
166

            
167
    #[test]
168
    fn test_url_with_expression() {
169
        let mut reader = Reader::new("http://{{host}}.fr ");
170
        assert_eq!(
171
            url(&mut reader).unwrap(),
172
            Template {
173
                elements: vec![
174
                    TemplateElement::String {
175
                        value: String::from("http://"),
176
                        encoded: String::from("http://"),
177
                    },
178
                    TemplateElement::Expression(Expr {
179
                        space0: Whitespace {
180
                            value: String::new(),
181
                            source_info: SourceInfo::new(Pos::new(1, 10), Pos::new(1, 10)),
182
                        },
183
                        variable: Variable {
184
                            name: String::from("host"),
185
                            source_info: SourceInfo::new(Pos::new(1, 10), Pos::new(1, 14)),
186
                        },
187
                        space1: Whitespace {
188
                            value: String::new(),
189
                            source_info: SourceInfo::new(Pos::new(1, 14), Pos::new(1, 14)),
190
                        },
191
                    }),
192
                    TemplateElement::String {
193
                        value: String::from(".fr"),
194
                        encoded: String::from(".fr"),
195
                    },
196
                ],
197
                delimiter: None,
198
                source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 19)),
199
            }
200
        );
201
        assert_eq!(reader.cursor().index, 18);
202
    }
203

            
204
    #[test]
205
    fn test_url_error_variable() {
206
        let mut reader = Reader::new("http://{{host>}}.fr");
207
        let error = url(&mut reader).err().unwrap();
208
        assert_eq!(
209
            error.pos,
210
            Pos {
211
                line: 1,
212
                column: 14,
213
            }
214
        );
215
        assert_eq!(
216
            error.kind,
217
            ParseErrorKind::Expecting {
218
                value: String::from("}}")
219
            }
220
        );
221
        assert!(!error.recoverable);
222
        assert_eq!(reader.cursor().index, 14);
223
    }
224

            
225
    #[test]
226
    fn test_url_error_missing_delimiter() {
227
        let mut reader = Reader::new("http://{{host");
228
        let error = url(&mut reader).err().unwrap();
229
        assert_eq!(
230
            error.pos,
231
            Pos {
232
                line: 1,
233
                column: 14,
234
            }
235
        );
236
        assert_eq!(
237
            error.kind,
238
            ParseErrorKind::Expecting {
239
                value: String::from("}}")
240
            }
241
        );
242
        assert!(!error.recoverable);
243
    }
244

            
245
    #[test]
246
    fn test_url_error_empty() {
247
        let mut reader = Reader::new(" # eol");
248
        let error = url(&mut reader).err().unwrap();
249
        assert_eq!(error.pos, Pos { line: 1, column: 1 });
250
        assert_eq!(error.kind, ParseErrorKind::UrlInvalidStart);
251
    }
252

            
253
    #[test]
254
    fn test_valid_urls() {
255
        // from official url_test.go file
256
        let valid_urls = [
257
            "http://www.google.com",
258
            "http://www.google.com/",
259
            "http://www.google.com/file%20one%26two",
260
            "http://www.google.com/#file%20one%26two",
261
            "http://www.google.com/?",
262
            "http://www.google.com/?foo=bar?",
263
            "http://www.google.com/?q=go+language",
264
            "http://www.google.com/?q=go%20language",
265
            "http://www.google.com/a%20b?q=c+d",
266
            // The following URLs are supported in the Go test file
267
            // but are not considered as valid URLs by curl
268
            // "http:www.google.com/?q=go+language",
269
            // "http:www.google.com/?q=go+language",
270
            // "http:%2f%2fwww.google.com/?q=go+language",
271
            "http://user:password@google.com",
272
            "http://user:password@google.com",
273
            "http://j@ne:password@google.com",
274
            "http://j%40ne:password@google.com",
275
            "http://jane:p@ssword@google.com",
276
            "http://j@ne:password@google.com/p@th?q=@go",
277
            "http://j%40ne:password@google.com/p@th?q=@go",
278
            "http://www.google.com/?q=go+language#foo",
279
            "http://www.google.com/?q=go+language#foo&bar",
280
            "http://www.google.com/?q=go+language#foo&bar",
281
            "http://www.google.com/?q=go+language#foo%26bar",
282
            "http://www.google.com/?q=go+language#foo%26bar",
283
            "http://%3Fam:pa%3Fsword@google.com",
284
            "http://192.168.0.1/",
285
            "http://192.168.0.1:8080/",
286
            "http://[fe80::1]/",
287
            "http://[fe80::1]:8080/",
288
            "http://[fe80::1%25en0]/",
289
            "http://[fe80::1%25en0]:8080/",
290
            "http://[fe80::1%25%65%6e%301-._~]/",
291
            "http://[fe80::1%25en01-._~]/",
292
            "http://[fe80::1%25%65%6e%301-._~]:8080/",
293
            "http://rest.rsc.io/foo%2fbar/baz%2Fquux?alt=media",
294
            "http://host/!$&'()*+,;=:@[hello]",
295
            "http://example.com/oid/[order_id]",
296
            "http://192.168.0.2:8080/foo",
297
            "http://192.168.0.2:/foo",
298
            "http://2b01:e34:ef40:7730:8e70:5aff:fefe:edac:8080/foo",
299
            "http://2b01:e34:ef40:7730:8e70:5aff:fefe:edac:/foo",
300
            "http://[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:8080/foo",
301
            "http://[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:/foo",
302
            "http://hello.世界.com/foo",
303
            "http://hello.%E4%B8%96%E7%95%8C.com/foo",
304
            "http://hello.%e4%b8%96%e7%95%8c.com/foo",
305
            "http://hello.%E4%B8%96%E7%95%8C.com/foo",
306
            "http://hello.%E4%B8%96%E7%95%8C.com/foo",
307
            "http://example.com//foo",
308
        ];
309
        for s in valid_urls {
310
            //eprintln!("{}", s);
311
            let mut reader = Reader::new(s);
312
            assert!(url(&mut reader).is_ok());
313
        }
314
    }
315

            
316
    #[test]
317
    fn test_invalid_urls() {
318
        // from official url_test.go file
319
        let invalid_urls = [
320
            "foo.com",
321
            "httpfoo.com",
322
            "http:foo.com",
323
            "https:foo.com",
324
            "https:/foo.com",
325
            "{https://foo.com",
326
        ];
327

            
328
        for s in invalid_urls {
329
            let mut reader = Reader::new(s);
330
            assert!(url(&mut reader).is_err());
331
        }
332
    }
333
}