f225a02ef4a559911fa5d054f0e3b09604e83833
[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 # Data transfer with URL syntax
18 #
19 # Download or upload over HTTP with `CurlHTTPRequest` and send emails with `CurlMail`.
20 module curl
21
22 import native_curl
23
24 redef class Sys
25 # Shared Curl library handle
26 #
27 # Usually, you do not have to use this attribute, it instancied by `CurlHTTPRequest` and `CurlMail`.
28 # But in some cases you may want to finalize it to free some small resources.
29 # However, if Curl services are needed once again, this attribute must be manually set.
30 var curl: Curl = new Curl is lazy, writable
31 end
32
33 # Curl library handle, it is initialized and released with this class
34 class Curl
35 super FinalizableOnce
36
37 private var native = new NativeCurl.easy_init
38
39 # Check for correct initialization
40 fun is_ok: Bool do return self.native.is_init
41
42 redef fun finalize_once do if is_ok then native.easy_clean
43 end
44
45 # CURL Request
46 class CurlRequest
47
48 private var curl: Curl = sys.curl
49
50 # Shall this request be verbose?
51 var verbose: Bool = false is writable
52
53 # Intern perform method, lowest level of request launching
54 private fun perform: nullable CurlResponseFailed
55 do
56 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
57
58 var err
59
60 err = self.curl.native.easy_setopt(new CURLOption.verbose, self.verbose)
61 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
62
63 err = self.curl.native.easy_perform
64 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
65
66 return null
67 end
68
69 # Intern method with return a failed answer with given code and message
70 private fun answer_failure(error_code: Int, error_msg: String): CurlResponseFailed
71 do
72 return new CurlResponseFailed(error_code, error_msg)
73 end
74 end
75
76 # CURL HTTP Request
77 class CurlHTTPRequest
78 super CurlRequest
79 super NativeCurlCallbacks
80
81 var url: String
82 var datas: nullable HeaderMap is writable
83 var headers: nullable HeaderMap is writable
84 var delegate: nullable CurlCallbacks is writable
85
86 # Set the user agent for all following HTTP requests
87 var user_agent: nullable String is writable
88
89 # Execute HTTP request with settings configured through attribute
90 fun execute: CurlResponse
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.native.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.native.easy_setopt(new CURLOption.url, url)
104 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
105
106 var user_agent = user_agent
107 if user_agent != null then
108 err = curl.native.easy_setopt(new CURLOption.user_agent, user_agent)
109 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
110 end
111
112 # Callbacks
113 err = self.curl.native.register_callback_header(callback_receiver)
114 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
115
116 err = self.curl.native.register_callback_body(callback_receiver)
117 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
118
119 # HTTP Header
120 var headers = self.headers
121 if headers != null then
122 var headers_joined = headers.join_pairs(": ")
123 err = self.curl.native.easy_setopt(new CURLOption.httpheader, headers_joined.to_curlslist)
124 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
125 end
126
127 # Datas
128 var datas = self.datas
129 if datas != null then
130 var postdatas = datas.to_url_encoded(self.curl)
131 err = self.curl.native.easy_setopt(new CURLOption.postfields, postdatas)
132 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
133 end
134
135 var err_resp = perform
136 if err_resp != null then return err_resp
137
138 var st_code = self.curl.native.easy_getinfo_long(new CURLInfoLong.response_code)
139 if not st_code == null then success_response.status_code = st_code
140
141 return success_response
142 end
143
144 # Download to file given resource
145 fun download_to_file(output_file_name: nullable String): CurlResponse
146 do
147 var success_response = new CurlFileResponseSuccess
148
149 var callback_receiver: CurlCallbacks = success_response
150 if self.delegate != null then callback_receiver = self.delegate.as(not null)
151
152 var err
153
154 err = self.curl.native.easy_setopt(new CURLOption.follow_location, 1)
155 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
156
157 err = self.curl.native.easy_setopt(new CURLOption.url, url)
158 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
159
160 err = self.curl.native.register_callback_header(callback_receiver)
161 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
162
163 err = self.curl.native.register_callback_stream(callback_receiver)
164 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
165
166 var opt_name
167 if not output_file_name == null then
168 opt_name = output_file_name
169 else if not self.url.substring(self.url.length-1, self.url.length) == "/" then
170 opt_name = self.url.basename
171 else
172 return answer_failure(0, "Unable to extract file name, please specify one")
173 end
174
175 success_response.file = new FileWriter.open(opt_name)
176 if not success_response.file.is_writable then
177 return answer_failure(0, "Unable to create associated file")
178 end
179
180 var err_resp = perform
181 if err_resp != null then return err_resp
182
183 var st_code = self.curl.native.easy_getinfo_long(new CURLInfoLong.response_code)
184 if not st_code == null then success_response.status_code = st_code
185
186 var speed = self.curl.native.easy_getinfo_double(new CURLInfoDouble.speed_download)
187 if not speed == null then success_response.speed_download = speed
188
189 var size = self.curl.native.easy_getinfo_double(new CURLInfoDouble.size_download)
190 if not size == null then success_response.size_download = size
191
192 var time = self.curl.native.easy_getinfo_double(new CURLInfoDouble.total_time)
193 if not time == null then success_response.total_time = time
194
195 success_response.file.close
196
197 return success_response
198 end
199 end
200
201 # CURL Mail Request
202 #
203 # ~~~
204 # # Craft mail
205 # var mail = new CurlMail("sender@example.org",
206 # to=["to@example.org"], cc=["bob@example.org"])
207 #
208 # mail.headers_body["Content-Type:"] = """text/html; charset="UTF-8""""
209 # mail.headers_body["Content-Transfer-Encoding:"] = "quoted-printable"
210 #
211 # mail.body = "<h1>Here you can write HTML stuff.</h1>"
212 # mail.subject = "Hello From My Nit Program"
213 #
214 # # Set mail server
215 # var error = mail.set_outgoing_server("smtps://smtp.example.org:465",
216 # "user@example.org", "mypassword")
217 # if error != null then
218 # print "Mail Server Error: {error}"
219 # exit 0
220 # end
221 #
222 # # Send
223 # error = mail.execute
224 # if error != null then
225 # print "Transfer Error: {error}"
226 # exit 0
227 # end
228 # ~~~
229 class CurlMail
230 super CurlRequest
231 super NativeCurlCallbacks
232
233 # Address of the sender
234 var from: nullable String is writable
235
236 # Main recipients
237 var to: nullable Array[String] is writable
238
239 # Subject line
240 var subject: nullable String is writable
241
242 # Text content
243 var body: nullable String is writable
244
245 # CC recipients
246 var cc: nullable Array[String] is writable
247
248 # BCC recipients (hidden from other recipients)
249 var bcc: nullable Array[String] is writable
250
251 # HTTP header
252 var headers = new HeaderMap is lazy, writable
253
254 # Content header
255 var headers_body = new HeaderMap is lazy, writable
256
257 # Protocols supported to send mail to a server
258 #
259 # Default value at `["smtp", "smtps"]`
260 var supported_outgoing_protocol: Array[String] = ["smtp", "smtps"]
261
262 # Helper method to add pair values to mail content while building it (ex: "To:", "address@mail.com")
263 private fun add_pair_to_content(str: String, att: String, val: nullable String): String
264 do
265 if val != null then return "{str}{att}{val}\n"
266 return "{str}{att}\n"
267 end
268
269 # Helper method to add entire list of pairs to mail content
270 private fun add_pairs_to_content(content: String, pairs: HeaderMap): String
271 do
272 for h_key, h_val in pairs do content = add_pair_to_content(content, h_key, h_val)
273 return content
274 end
275
276 # Check for host and protocol availability
277 private fun is_supported_outgoing_protocol(host: String): CURLCode
278 do
279 var host_reach = host.split_with("://")
280 if host_reach.length > 1 and supported_outgoing_protocol.has(host_reach[0]) then return once new CURLCode.ok
281 return once new CURLCode.unsupported_protocol
282 end
283
284 # Configure server host and user credentials if needed.
285 fun set_outgoing_server(host: String, user: nullable String, pwd: nullable String): nullable CurlResponseFailed
286 do
287 # Check Curl initialisation
288 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
289
290 var err
291
292 # Host & Protocol
293 err = is_supported_outgoing_protocol(host)
294 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
295
296 err = self.curl.native.easy_setopt(new CURLOption.url, host)
297 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
298
299 # Credentials
300 if not user == null and not pwd == null then
301 err = self.curl.native.easy_setopt(new CURLOption.username, user)
302 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
303
304 err = self.curl.native.easy_setopt(new CURLOption.password, pwd)
305 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
306 end
307
308 return null
309 end
310
311 # Execute Mail request with settings configured through attribute
312 fun execute: nullable CurlResponseFailed
313 do
314 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
315
316 var lines = new Array[String]
317
318 # Headers
319 var headers = self.headers
320 if not headers.is_empty then
321 for k, v in headers do lines.add "{k}{v}"
322 end
323
324 # Recipients
325 var all_recipients = new Array[String]
326 var to = self.to
327 if to != null and to.length > 0 then
328 lines.add "To:{to.join(",")}"
329 all_recipients.append to
330 end
331
332 var cc = self.cc
333 if cc != null and cc.length > 0 then
334 lines.add "Cc:{cc.join(",")}"
335 all_recipients.append cc
336 end
337
338 var bcc = self.bcc
339 if bcc != null and bcc.length > 0 then all_recipients.append bcc
340
341 if all_recipients.is_empty then return answer_failure(0, "There must be at lease one recipient")
342
343 var err = self.curl.native.easy_setopt(new CURLOption.follow_location, 1)
344 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
345
346 err = self.curl.native.easy_setopt(new CURLOption.mail_rcpt, all_recipients.to_curlslist)
347 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
348
349 # From
350 var from = self.from
351 if not from == null then
352 lines.add "From:{from}"
353
354 err = self.curl.native.easy_setopt(new CURLOption.mail_from, from)
355 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
356 end
357
358 # Subject
359 var subject = self.subject
360 if subject == null then subject = "" # Default
361 lines.add "Subject: {subject}"
362
363 # Headers body
364 var headers_body = self.headers_body
365 if not headers_body.is_empty then
366 for k, v in headers_body do lines.add "{k}{v}"
367 end
368
369 # Body
370 var body = self.body
371 if body == null then body = "" # Default
372
373 lines.add ""
374 lines.add body
375 lines.add ""
376
377 err = self.curl.native.register_callback_read(self)
378 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
379
380 var content = lines.join("\n")
381 err = self.curl.native.register_read_datas_callback(self, content)
382 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
383
384 var err_resp = perform
385 if err_resp != null then return err_resp
386
387 return null
388 end
389 end
390
391 # Callbacks Interface, allow you to manage in your way the different streams
392 interface CurlCallbacks
393 super NativeCurlCallbacks
394 end
395
396 # Abstract Curl request response
397 abstract class CurlResponse
398 end
399
400 # Failed Response Class returned when errors during configuration are raised
401 class CurlResponseFailed
402 super CurlResponse
403
404 var error_code: Int
405 var error_msg: String
406
407 redef fun to_s do return "{error_msg} ({error_code})"
408 end
409
410 # Success Abstract Response Success Class
411 abstract class CurlResponseSuccessIntern
412 super CurlCallbacks
413 super CurlResponse
414
415 var headers = new HashMap[String, String]
416
417 # Receive headers from request due to headers callback registering
418 redef fun header_callback(line)
419 do
420 var splitted = line.split_with(':')
421 if splitted.length > 1 then
422 var key = splitted.shift
423 self.headers[key] = splitted.to_s
424 end
425 end
426 end
427
428 # Success Response Class of a basic response
429 class CurlResponseSuccess
430 super CurlResponseSuccessIntern
431
432 var body_str = ""
433 var status_code = 0
434
435 # Receive body from request due to body callback registering
436 redef fun body_callback(line) do
437 self.body_str = "{self.body_str}{line}"
438 end
439 end
440
441 # Success Response Class of a downloaded File
442 class CurlFileResponseSuccess
443 super CurlResponseSuccessIntern
444
445 var status_code = 0
446 var speed_download = 0.0
447 var size_download = 0.0
448 var total_time = 0.0
449 private var file: nullable FileWriter = null
450
451 # Receive bytes stream from request due to stream callback registering
452 redef fun stream_callback(buffer)
453 do
454 file.write buffer
455 end
456 end
457
458 # Pseudo map associating `String` to `String` for HTTP exchanges
459 #
460 # This structure differs from `Map` as each key can have multiple associations
461 # and the order of insertion is important to some services.
462 class HeaderMap
463 private var array = new Array[Couple[String, String]]
464
465 # Add a `value` associated to `key`
466 fun []=(key, value: String)
467 do
468 array.add new Couple[String, String](key, value)
469 end
470
471 # Get a list of the keys associated to `key`
472 fun [](k: String): Array[String]
473 do
474 var res = new Array[String]
475 for c in array do if c.first == k then res.add c.second
476 return res
477 end
478
479 # Iterate over all the associations in `self`
480 fun iterator: MapIterator[String, String] do return new HeaderMapIterator(self)
481
482 # Get `self` as a single string for HTTP POST
483 #
484 # Require: `curl.is_ok`
485 fun to_url_encoded(curl: Curl): String
486 do
487 assert curl.is_ok
488
489 var lines = new Array[String]
490 for k, v in self do
491 if k.length == 0 then continue
492
493 k = curl.native.escape(k)
494 v = curl.native.escape(v)
495 lines.add "{k}={v}"
496 end
497 return lines.join("&")
498 end
499
500 # Concatenate couple of 'key value' separated by 'sep' in Array
501 fun join_pairs(sep: String): Array[String]
502 do
503 var col = new Array[String]
504 for k, v in self do col.add("{k}{sep}{v}")
505 return col
506 end
507
508 # Number of values in `self`
509 fun length: Int do return array.length
510
511 # Is this map empty?
512 fun is_empty: Bool do return array.is_empty
513 end
514
515 private class HeaderMapIterator
516 super MapIterator[String, String]
517
518 var map: HeaderMap
519 var iterator: Iterator[Couple[String, String]] = map.array.iterator is lazy
520
521 redef fun is_ok do return self.iterator.is_ok
522 redef fun next do self.iterator.next
523 redef fun item do return self.iterator.item.second
524 redef fun key do return self.iterator.item.first
525 end