027fd950b4027736160703e80cb22a248928b0ac
[nit.git] / lib / curl / curl.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2013 Matthieu Lucas <lucasmatthieu@gmail.com>
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 # Network functionnalities based on Curl_c module.
18 module curl
19
20 import curl_c
21
22 # Top level of Curl
23 class Curl
24 protected var prim_curl = new CCurl.easy_init
25
26 init
27 do
28 assert curlInstance:self.prim_curl.is_init else
29 print "Curl must be instancied to be used"
30 end
31 end
32
33 # Check for correct initialization
34 fun is_ok: Bool do return self.prim_curl.is_init
35
36 # Release Curl instance
37 fun destroy do self.prim_curl.easy_clean
38 end
39
40 # CURL Request
41 class CurlRequest
42
43 var verbose: Bool = false is writable
44 private var curl: nullable Curl = null
45
46 # Launch request method
47 fun execute: CurlResponse is abstract
48
49 # Intern perform method, lowest level of request launching
50 private fun perform: nullable CurlResponseFailed
51 do
52 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
53
54 var err
55
56 err = self.curl.prim_curl.easy_setopt(new CURLOption.verbose, self.verbose)
57 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
58
59 err = self.curl.prim_curl.easy_perform
60 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
61
62 return null
63 end
64
65 # Intern method with return a failed answer with given code and message
66 private fun answer_failure(error_code: Int, error_msg: String): CurlResponseFailed
67 do
68 return new CurlResponseFailed(error_code, error_msg)
69 end
70 end
71
72 # CURL HTTP Request
73 class CurlHTTPRequest
74 super CurlRequest
75 super CCurlCallbacks
76 super CurlCallbacksRegisterIntern
77
78 var url: String
79 var datas: nullable HeaderMap = null is writable
80 var headers: nullable HeaderMap = null is writable
81
82 # Set the user agent for all following HTTP requests
83 fun user_agent=(name: String)
84 do
85 curl.prim_curl.easy_setopt(new CURLOption.user_agent, name)
86 end
87
88 init (url: String, curl: nullable Curl) is old_style_init do
89 self.url = url
90 self.curl = curl
91 end
92
93 # Execute HTTP request with settings configured through attribute
94 redef fun execute
95 do
96 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
97
98 var success_response = new CurlResponseSuccess
99 var callback_receiver: CurlCallbacks = success_response
100 if self.delegate != null then callback_receiver = self.delegate.as(not null)
101
102 var err
103
104 err = self.curl.prim_curl.easy_setopt(new CURLOption.follow_location, 1)
105 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
106
107 err = self.curl.prim_curl.easy_setopt(new CURLOption.url, url)
108 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
109
110 # Callbacks
111 err = self.curl.prim_curl.register_callback_header(callback_receiver)
112 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
113
114 err = self.curl.prim_curl.register_callback_body(callback_receiver)
115 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
116
117 # HTTP Header
118 if self.headers != null then
119 var headers_joined = self.headers.join_pairs(": ")
120 err = self.curl.prim_curl.easy_setopt(new CURLOption.httpheader, headers_joined.to_curlslist)
121 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
122 end
123
124 # Datas
125 if self.datas != null then
126 var postdatas = self.datas.to_url_encoded(self.curl.prim_curl)
127 err = self.curl.prim_curl.easy_setopt(new CURLOption.postfields, postdatas)
128 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
129 end
130
131 var err_resp = perform
132 if err_resp != null then return err_resp
133
134 var st_code = self.curl.prim_curl.easy_getinfo_long(new CURLInfoLong.response_code)
135 if not st_code == null then success_response.status_code = st_code.response
136
137 return success_response
138 end
139
140 # Download to file given resource
141 fun download_to_file(output_file_name: nullable String): CurlResponse
142 do
143 var success_response = new CurlFileResponseSuccess
144
145 var callback_receiver: CurlCallbacks = success_response
146 if self.delegate != null then callback_receiver = self.delegate.as(not null)
147
148 var err
149
150 err = self.curl.prim_curl.easy_setopt(new CURLOption.follow_location, 1)
151 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
152
153 err = self.curl.prim_curl.easy_setopt(new CURLOption.url, url)
154 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
155
156 err = self.curl.prim_curl.register_callback_header(callback_receiver)
157 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
158
159 err = self.curl.prim_curl.register_callback_stream(callback_receiver)
160 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
161
162 var opt_name
163 if not output_file_name == null then
164 opt_name = output_file_name
165 else if not self.url.substring(self.url.length-1, self.url.length) == "/" then
166 opt_name = self.url.basename("")
167 else
168 return answer_failure(0, "Unable to extract file name, please specify one")
169 end
170
171 success_response.i_file = new OFile.open(opt_name.to_cstring)
172 if not success_response.i_file.is_valid then
173 success_response.i_file.close
174 return answer_failure(0, "Unable to create associated file")
175 end
176
177 var err_resp = perform
178 if err_resp != null then return err_resp
179
180 var st_code = self.curl.prim_curl.easy_getinfo_long(new CURLInfoLong.response_code)
181 if not st_code == null then success_response.status_code = st_code.response
182
183 var speed = self.curl.prim_curl.easy_getinfo_double(new CURLInfoDouble.speed_download)
184 if not speed == null then success_response.speed_download = speed.response
185
186 var size = self.curl.prim_curl.easy_getinfo_double(new CURLInfoDouble.size_download)
187 if not size == null then success_response.size_download = size.response
188
189 var time = self.curl.prim_curl.easy_getinfo_double(new CURLInfoDouble.total_time)
190 if not time == null then success_response.total_time = time.response
191
192 success_response.i_file.close
193
194 return success_response
195 end
196 end
197
198 # CURL Mail Request
199 class CurlMailRequest
200 super CurlRequest
201 super CCurlCallbacks
202
203 var headers: nullable HeaderMap = null is writable
204 var headers_body: nullable HeaderMap = null is writable
205 var from: nullable String = null is writable
206 var to: nullable Array[String] = null is writable
207 var cc: nullable Array[String] = null is writable
208 var bcc: nullable Array[String] = null is writable
209 var subject: nullable String = "" is writable
210 var body: nullable String = "" is writable
211 private var supported_outgoing_protocol: Array[String] = ["smtp", "smtps"]
212
213 init (curl: nullable Curl) is old_style_init do
214 self.curl = curl
215 end
216
217 # Helper method to add conventional space while building entire mail
218 private fun add_conventional_space(str: String):String do return "{str}\n" end
219
220 # Helper method to add pair values to mail content while building it (ex: "To:", "address@mail.com")
221 private fun add_pair_to_content(str: String, att: String, val: nullable String):String
222 do
223 if val != null then return "{str}{att}{val}\n"
224 return "{str}{att}\n"
225 end
226
227 # Helper method to add entire list of pairs to mail content
228 private fun add_pairs_to_content(content: String, pairs: HeaderMap):String
229 do
230 for h_key, h_val in pairs do content = add_pair_to_content(content, h_key, h_val)
231 return content
232 end
233
234 # Check for host and protocol availability
235 private fun is_supported_outgoing_protocol(host: String):CURLCode
236 do
237 var host_reach = host.split_with("://")
238 if host_reach.length > 1 and supported_outgoing_protocol.has(host_reach[0]) then return once new CURLCode.ok
239 return once new CURLCode.unsupported_protocol
240 end
241
242 # Configure server host and user credentials if needed.
243 fun set_outgoing_server(host: String, user: nullable String, pwd: nullable String): nullable CurlResponseFailed
244 do
245 # Check Curl initialisation
246 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
247
248 var err
249
250 # Host & Protocol
251 err = is_supported_outgoing_protocol(host)
252 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
253 err = self.curl.prim_curl.easy_setopt(new CURLOption.url, host)
254 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
255
256 # Credentials
257 if not user == null and not pwd == null then
258 err = self.curl.prim_curl.easy_setopt(new CURLOption.username, user)
259 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
260 err = self.curl.prim_curl.easy_setopt(new CURLOption.password, pwd)
261 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
262 end
263
264 return null
265 end
266
267 # Execute Mail request with settings configured through attribute
268 redef fun execute
269 do
270 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
271
272 var success_response = new CurlMailResponseSuccess
273 var content = ""
274 # Headers
275 if self.headers != null then
276 content = add_pairs_to_content(content, self.headers.as(not null))
277 end
278
279 # Recipients
280 var g_rec = new Array[String]
281 if self.to != null and self.to.length > 0 then
282 content = add_pair_to_content(content, "To:", self.to.join(","))
283 g_rec.append(self.to.as(not null))
284 end
285 if self.cc != null and self.cc.length > 0 then
286 content = add_pair_to_content(content, "Cc:", self.cc.join(","))
287 g_rec.append(self.cc.as(not null))
288 end
289 if self.bcc != null and self.bcc.length > 0 then g_rec.append(self.bcc.as(not null))
290
291 if g_rec.length < 1 then return answer_failure(0, "The mail recipients can not be empty")
292
293 var err
294
295 err = self.curl.prim_curl.easy_setopt(new CURLOption.follow_location, 1)
296 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
297
298 err = self.curl.prim_curl.easy_setopt(new CURLOption.mail_rcpt, g_rec.to_curlslist)
299 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
300
301 # From
302 if not self.from == null then
303 content = add_pair_to_content(content, "From:", self.from)
304 err = self.curl.prim_curl.easy_setopt(new CURLOption.mail_from, self.from.as(not null))
305 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
306 end
307
308 # Subject
309 content = add_pair_to_content(content, "Subject:", self.subject)
310
311 # Headers body
312 if self.headers_body != null then
313 content = add_pairs_to_content(content, self.headers_body.as(not null))
314 end
315
316 # Body
317 content = add_conventional_space(content)
318 content = add_pair_to_content(content, "", self.body)
319 content = add_conventional_space(content)
320 err = self.curl.prim_curl.register_callback_read(self)
321 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
322 err = self.curl.prim_curl.register_read_datas_callback(self, content)
323 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
324
325 var err_resp = perform
326 if err_resp != null then return err_resp
327
328 return success_response
329 end
330 end
331
332 # Callbacks Interface, allow you to manage in your way the different streams
333 interface CurlCallbacks
334 super CCurlCallbacks
335 end
336
337 # Callbacks attributes
338 abstract class CurlCallbacksRegisterIntern
339 var delegate: nullable CurlCallbacks = null is writable
340 end
341
342 # Abstract Curl request response
343 abstract class CurlResponse
344 end
345
346 # Failed Response Class returned when errors during configuration are raised
347 class CurlResponseFailed
348 super CurlResponse
349
350 var error_code: Int
351 var error_msg: String
352 end
353
354 # Success Abstract Response Success Class
355 abstract class CurlResponseSuccessIntern
356 super CurlCallbacks
357 super CurlResponse
358
359 var headers = new HashMap[String, String]
360
361 # Receive headers from request due to headers callback registering
362 redef fun header_callback(line)
363 do
364 var splitted = line.split_with(':')
365 if splitted.length > 1 then
366 var key = splitted.shift
367 self.headers[key] = splitted.to_s
368 end
369 end
370 end
371
372 # Success Response Class of a basic response
373 class CurlResponseSuccess
374 super CurlResponseSuccessIntern
375
376 var body_str = ""
377 var status_code = 0
378
379 # Receive body from request due to body callback registering
380 redef fun body_callback(line) do
381 self.body_str = "{self.body_str}{line}"
382 end
383 end
384
385 # Success Response Class of mail request
386 class CurlMailResponseSuccess
387 super CurlResponseSuccessIntern
388 end
389
390 # Success Response Class of a downloaded File
391 class CurlFileResponseSuccess
392 super CurlResponseSuccessIntern
393
394 var status_code = 0
395 var speed_download = 0
396 var size_download = 0
397 var total_time = 0
398 private var i_file: nullable OFile = null
399
400 # Receive bytes stream from request due to stream callback registering
401 redef fun stream_callback(buffer)
402 do
403 self.i_file.write(buffer, size, count)
404 end
405 end
406
407 # Pseudo map associating Strings to Strings,
408 # each key can have multiple associations
409 # and the order of insertion is important.
410 class HeaderMap
411 private var arr = new Array[Couple[String, String]]
412
413 fun []=(k, v: String) do arr.add(new Couple[String, String](k, v))
414
415 fun [](k: String): Array[String]
416 do
417 var res = new Array[String]
418 for c in arr do if c.first == k then res.add(c.second)
419 return res
420 end
421
422 fun iterator: MapIterator[String, String] do return new HeaderMapIterator(self)
423
424 # Convert Self to a single string used to post http fields
425 fun to_url_encoded(curl: CCurl): String
426 do
427 assert curlNotInitialized: curl.is_init else
428 print "to_url_encoded required a valid instance of CCurl Object."
429 end
430 var str = ""
431 var length = self.length
432 var i = 0
433 for k, v in self do
434 if k.length > 0 then
435 k = curl.escape(k)
436 v = curl.escape(v)
437 str = "{str}{k}={v}"
438 if i < length-1 then str = "{str}&"
439 end
440 i += 1
441 end
442 return str
443 end
444
445 # Concatenate couple of 'key value' separated by 'sep' in Array
446 fun join_pairs(sep: String): Array[String]
447 do
448 var col = new Array[String]
449 for k, v in self do col.add("{k}{sep}{v}")
450 return col
451 end
452
453 fun length: Int do return arr.length
454 fun is_empty: Bool do return arr.is_empty
455 end
456
457 class HeaderMapIterator
458 super MapIterator[String, String]
459
460 private var iterator: Iterator[Couple[String, String]]
461 init(map: HeaderMap) is old_style_init do self.iterator = map.arr.iterator
462
463 redef fun is_ok do return self.iterator.is_ok
464 redef fun next do self.iterator.next
465 redef fun item do return self.iterator.item.second
466 redef fun key do return self.iterator.item.first
467 end