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