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