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