lib: remove useless comparison on null that broke tests
[nit.git] / lib / nitcorn / http_request.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2013 Frederic Sevillano
4 # Copyright 2013 Jean-Philippe Caissy <jpcaissy@piji.ca>
5 # Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
6 #
7 # Licensed under the Apache License, Version 2.0 (the "License");
8 # you may not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
10 #
11 # http://www.apache.org/licenses/LICENSE-2.0
12 #
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS,
15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
18
19 # Provides the `HttpRequest` class and services to create it
20 module http_request
21
22 import standard
23
24 # A request received over HTTP, is build by `HttpRequestParser`
25 class HttpRequest
26 private init do end
27
28 # HTTP protocol version
29 var http_version: String
30
31 # Method of this request (GET or POST)
32 var method: String
33
34 # The host targetter by this request (usually the server)
35 var host: String
36
37 # The full URL requested by the client (including the `query_string`)
38 var url: String
39
40 # The resource requested by the client (only the page, not the `query_string`)
41 var uri: String
42
43 # The string following `?` in the requested URL
44 var query_string = ""
45
46 # The header of this request
47 var header = new HashMap[String, String]
48
49 # The content of the cookie of this request
50 var cookie = new HashMap[String, String]
51
52 # The arguments passed with the GET method,
53 var get_args = new HashMap[String, String]
54
55 # The arguments passed with the POST method
56 var post_args = new HashMap[String, String]
57
58 # The arguments passed with the POST or GET method (with a priority on POST)
59 var all_args = new HashMap[String, String]
60
61 # Returns argument `arg_name` in the request as a String
62 # or null if it was not found.
63 # Also cleans the String by trimming it.
64 # If the Strings happens to be empty after trimming,
65 # the method will return `null`
66 #
67 # NOTE: Prioritizes POST before GET
68 fun string_arg(arg_name: String): nullable String do
69 if not all_args.has_key(arg_name) then return null
70 var s = all_args[arg_name].trim
71 if s.is_empty then return null
72 return s
73 end
74
75 # Returns argument `arg_name` as an Int or `null` if not found or not a number.
76 #
77 # NOTE: Prioritizes POST before GET
78 fun int_arg(arg_name: String): nullable Int do
79 if not all_args.has_key(arg_name) then return null
80 var i = all_args[arg_name]
81 if not i.is_numeric then return null
82 return i.to_i
83 end
84
85 # Returns argument `arg_name` as a Bool or `null` if not found or not a boolean.
86 #
87 # NOTE: Prioritizes POST before GET
88 fun bool_arg(arg_name: String): nullable Bool do
89 if not all_args.has_key(arg_name) then return null
90 var i = all_args[arg_name]
91 if i == "true" then return true
92 if i == "false" then return false
93 return null
94 end
95 end
96
97 # Utility class to parse a request string and build a `HttpRequest`
98 #
99 # The main method is `parse_http_request`.
100 class HttpRequestParser
101 # The current `HttpRequest` under construction
102 private var http_request: HttpRequest is noinit
103
104 # Untreated body
105 private var body = ""
106
107 # Lines of the header
108 private var header_fields = new Array[String]
109
110 # Words of the first line
111 private var first_line = new Array[String]
112
113 init do end
114
115 fun parse_http_request(full_request: String): nullable HttpRequest
116 do
117 clear_data
118
119 var http_request = new HttpRequest
120 self.http_request = http_request
121
122 segment_http_request(full_request)
123
124 # Parse first line, looks like "GET dir/index.html?user=xymus HTTP/1.0"
125 if first_line.length < 3 then
126 print "HTTP error: request first line apprears invalid: {first_line}"
127 return null
128 end
129 http_request.method = first_line[0]
130 http_request.url = first_line[1]
131 http_request.http_version = first_line[2]
132
133 # GET args
134 if http_request.url.has('?') then
135 http_request.uri = first_line[1].substring(0, first_line[1].index_of('?'))
136 http_request.query_string = first_line[1].substring_from(first_line[1].index_of('?')+1)
137
138 var parse_url = parse_url
139 http_request.get_args = parse_url
140 http_request.all_args.recover_with parse_url
141 else
142 http_request.uri = first_line[1]
143 end
144
145 # POST args
146 if http_request.method == "POST" then
147 var lines = body.split_with('&')
148 for line in lines do if not line.trim.is_empty then
149 var parts = line.split_once_on('=')
150 if parts.length > 1 then
151 var decoded = parts[1].replace('+', " ").from_percent_encoding
152 http_request.post_args[parts[0]] = decoded
153 http_request.all_args[parts[0]] = decoded
154 else
155 print "POST Error: {line} format error on {line}"
156 end
157 end
158 end
159
160 # Headers
161 for i in header_fields do
162 var temp_field = i.split_with(": ")
163
164 if temp_field.length == 2 then
165 http_request.header[temp_field[0]] = temp_field[1]
166 end
167 end
168
169 # Cookies
170 if http_request.header.keys.has("Cookie") then
171 var cookie = http_request.header["Cookie"]
172 for couple in cookie.split_with(';') do
173 var words = couple.trim.split_with('=')
174 if words.length != 2 then continue
175 http_request.cookie[words[0]] = words[1]
176 end
177 end
178
179 return http_request
180 end
181
182 private fun clear_data
183 do
184 first_line.clear
185 header_fields.clear
186 end
187
188 private fun segment_http_request(http_request: String): Bool
189 do
190 var header_end = http_request.search("\r\n\r\n")
191
192 if header_end == null then
193 header_fields = http_request.split_with("\r\n")
194 else
195 header_fields = http_request.substring(0, header_end.from).split_with("\r\n")
196 body = http_request.substring(header_end.after, http_request.length-1)
197 end
198
199 # If a line of the http_request is long it may change line, it has " " at the
200 # end to indicate this. This section turns them into 1 line.
201 if header_fields.length > 1 and header_fields[0].has_suffix(" ") then
202 var temp_req = header_fields[0].substring(0, header_fields[0].length-1) + header_fields[1]
203
204 first_line = temp_req.split_with(' ')
205 header_fields.shift
206 header_fields.shift
207
208 if first_line.length != 3 then return false
209 else
210 first_line = header_fields[0].split_with(' ')
211 header_fields.shift
212
213 if first_line.length != 3 then return false
214 end
215
216 # Cut off the header in lines
217 var pos = 0
218 while pos < header_fields.length do
219 if pos < header_fields.length-1 and header_fields[pos].has_suffix(" ") then
220 header_fields[pos] = header_fields[pos].substring(0, header_fields[pos].length-1) + header_fields[pos+1]
221 header_fields.remove_at(pos+1)
222 pos = pos-1
223 end
224 pos = pos+1
225 end
226
227 return true
228 end
229
230 # Extract args from the URL
231 private fun parse_url: HashMap[String, String]
232 do
233 var query_strings = new HashMap[String, String]
234
235 if http_request.url.has('?') then
236 var get_args = http_request.query_string.split_with("&")
237 for param in get_args do
238 var key_value = param.split_with("=")
239 if key_value.length < 2 then continue
240 query_strings[key_value[0]] = key_value[1]
241 end
242 end
243
244 return query_strings
245 end
246 end