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