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