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