lib/curl: implement `CurlResponseFailed::to_s`
[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 class CurlMail
198 super CurlRequest
199 super NativeCurlCallbacks
200
201 # Address of the sender
202 var from: nullable String is writable
203
204 # Main recipients
205 var to: nullable Array[String] is writable
206
207 # Subject line
208 var subject: nullable String is writable
209
210 # Text content
211 var body: nullable String is writable
212
213 # CC recipients
214 var cc: nullable Array[String] is writable
215
216 # BCC recipients (hidden from other recipients)
217 var bcc: nullable Array[String] is writable
218
219 # HTTP header
220 var headers = new HeaderMap is lazy, writable
221
222 # Content header
223 var headers_body = new HeaderMap is lazy, writable
224
225 private var supported_outgoing_protocol: Array[String] = ["smtp", "smtps"]
226
227 # Helper method to add pair values to mail content while building it (ex: "To:", "address@mail.com")
228 private fun add_pair_to_content(str: String, att: String, val: nullable String): String
229 do
230 if val != null then return "{str}{att}{val}\n"
231 return "{str}{att}\n"
232 end
233
234 # Helper method to add entire list of pairs to mail content
235 private fun add_pairs_to_content(content: String, pairs: HeaderMap): String
236 do
237 for h_key, h_val in pairs do content = add_pair_to_content(content, h_key, h_val)
238 return content
239 end
240
241 # Check for host and protocol availability
242 private fun is_supported_outgoing_protocol(host: String): CURLCode
243 do
244 var host_reach = host.split_with("://")
245 if host_reach.length > 1 and supported_outgoing_protocol.has(host_reach[0]) then return once new CURLCode.ok
246 return once new CURLCode.unsupported_protocol
247 end
248
249 # Configure server host and user credentials if needed.
250 fun set_outgoing_server(host: String, user: nullable String, pwd: nullable String): nullable CurlResponseFailed
251 do
252 # Check Curl initialisation
253 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
254
255 var err
256
257 # Host & Protocol
258 err = is_supported_outgoing_protocol(host)
259 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
260
261 err = self.curl.native.easy_setopt(new CURLOption.url, host)
262 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
263
264 # Credentials
265 if not user == null and not pwd == null then
266 err = self.curl.native.easy_setopt(new CURLOption.username, user)
267 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
268
269 err = self.curl.native.easy_setopt(new CURLOption.password, pwd)
270 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
271 end
272
273 return null
274 end
275
276 # Execute Mail request with settings configured through attribute
277 fun execute: nullable CurlResponseFailed
278 do
279 if not self.curl.is_ok then return answer_failure(0, "Curl instance is not correctly initialized")
280
281 var lines = new Array[String]
282
283 # Headers
284 var headers = self.headers
285 if not headers.is_empty then
286 for k, v in headers do lines.add "{k}{v}"
287 end
288
289 # Recipients
290 var all_recipients = new Array[String]
291 var to = self.to
292 if to != null and to.length > 0 then
293 lines.add "To:{to.join(",")}"
294 all_recipients.append to
295 end
296
297 var cc = self.cc
298 if cc != null and cc.length > 0 then
299 lines.add "Cc:{cc.join(",")}"
300 all_recipients.append cc
301 end
302
303 var bcc = self.bcc
304 if bcc != null and bcc.length > 0 then all_recipients.append bcc
305
306 if all_recipients.is_empty then return answer_failure(0, "There must be at lease one recipient")
307
308 var err = self.curl.native.easy_setopt(new CURLOption.follow_location, 1)
309 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
310
311 err = self.curl.native.easy_setopt(new CURLOption.mail_rcpt, all_recipients.to_curlslist)
312 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
313
314 # From
315 var from = self.from
316 if not from == null then
317 lines.add "From:{from}"
318
319 err = self.curl.native.easy_setopt(new CURLOption.mail_from, from)
320 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
321 end
322
323 # Subject
324 var subject = self.subject
325 if subject == null then subject = "" # Default
326 lines.add "Subject: {subject}"
327
328 # Headers body
329 var headers_body = self.headers_body
330 if not headers_body.is_empty then
331 for k, v in headers_body do lines.add "{k}{v}"
332 end
333
334 # Body
335 var body = self.body
336 if body == null then body = "" # Default
337
338 lines.add ""
339 lines.add body
340 lines.add ""
341
342 err = self.curl.native.register_callback_read(self)
343 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
344
345 var content = lines.join("\n")
346 err = self.curl.native.register_read_datas_callback(self, content)
347 if not err.is_ok then return answer_failure(err.to_i, err.to_s)
348
349 var err_resp = perform
350 if err_resp != null then return err_resp
351
352 return null
353 end
354 end
355
356 # Callbacks Interface, allow you to manage in your way the different streams
357 interface CurlCallbacks
358 super NativeCurlCallbacks
359 end
360
361 # Abstract Curl request response
362 abstract class CurlResponse
363 end
364
365 # Failed Response Class returned when errors during configuration are raised
366 class CurlResponseFailed
367 super CurlResponse
368
369 var error_code: Int
370 var error_msg: String
371
372 redef fun to_s do return "{error_msg} ({error_code})"
373 end
374
375 # Success Abstract Response Success Class
376 abstract class CurlResponseSuccessIntern
377 super CurlCallbacks
378 super CurlResponse
379
380 var headers = new HashMap[String, String]
381
382 # Receive headers from request due to headers callback registering
383 redef fun header_callback(line)
384 do
385 var splitted = line.split_with(':')
386 if splitted.length > 1 then
387 var key = splitted.shift
388 self.headers[key] = splitted.to_s
389 end
390 end
391 end
392
393 # Success Response Class of a basic response
394 class CurlResponseSuccess
395 super CurlResponseSuccessIntern
396
397 var body_str = ""
398 var status_code = 0
399
400 # Receive body from request due to body callback registering
401 redef fun body_callback(line) do
402 self.body_str = "{self.body_str}{line}"
403 end
404 end
405
406 # Success Response Class of a downloaded File
407 class CurlFileResponseSuccess
408 super CurlResponseSuccessIntern
409
410 var status_code = 0
411 var speed_download = 0
412 var size_download = 0
413 var total_time = 0
414 private var file: nullable FileWriter = null
415
416 # Receive bytes stream from request due to stream callback registering
417 redef fun stream_callback(buffer)
418 do
419 file.write buffer
420 end
421 end
422
423 # Pseudo map associating `String` to `String` for HTTP exchanges
424 #
425 # This structure differs from `Map` as each key can have multiple associations
426 # and the order of insertion is important to some services.
427 class HeaderMap
428 private var array = new Array[Couple[String, String]]
429
430 # Add a `value` associated to `key`
431 fun []=(key, value: String)
432 do
433 array.add new Couple[String, String](key, value)
434 end
435
436 # Get a list of the keys associated to `key`
437 fun [](k: String): Array[String]
438 do
439 var res = new Array[String]
440 for c in array do if c.first == k then res.add c.second
441 return res
442 end
443
444 # Iterate over all the associations in `self`
445 fun iterator: MapIterator[String, String] do return new HeaderMapIterator(self)
446
447 # Get `self` as a single string for HTTP POST
448 #
449 # Require: `curl.is_ok`
450 fun to_url_encoded(curl: Curl): String
451 do
452 assert curl.is_ok
453
454 var lines = new Array[String]
455 for k, v in self do
456 if k.length == 0 then continue
457
458 k = curl.native.escape(k)
459 v = curl.native.escape(v)
460 lines.add "{k}={v}"
461 end
462 return lines.join("&")
463 end
464
465 # Concatenate couple of 'key value' separated by 'sep' in Array
466 fun join_pairs(sep: String): Array[String]
467 do
468 var col = new Array[String]
469 for k, v in self do col.add("{k}{sep}{v}")
470 return col
471 end
472
473 # Number of values in `self`
474 fun length: Int do return array.length
475
476 # Is this map empty?
477 fun is_empty: Bool do return array.is_empty
478 end
479
480 private class HeaderMapIterator
481 super MapIterator[String, String]
482
483 var map: HeaderMap
484 var iterator: Iterator[Couple[String, String]] = map.array.iterator is lazy
485
486 redef fun is_ok do return self.iterator.is_ok
487 redef fun next do self.iterator.next
488 redef fun item do return self.iterator.item.second
489 redef fun key do return self.iterator.item.first
490 end