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