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