1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Copyright 2013 Matthieu Lucas <lucasmatthieu@gmail.com>
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 # Network functionnalities based on Curl_c module.
26 protected var native
= new NativeCurl.easy_init
28 # Check for correct initialization
29 fun is_ok
: Bool do return self.native
.is_init
31 redef fun finalize_once
do if is_ok
then native
.easy_clean
37 private var curl
: Curl
39 # Shall this request be verbose?
40 var verbose
: Bool = false is writable
42 # Launch request method
43 fun execute
: CurlResponse is abstract
45 # Intern perform method, lowest level of request launching
46 private fun perform
: nullable CurlResponseFailed
48 if not self.curl
.is_ok
then return answer_failure
(0, "Curl instance is not correctly initialized")
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
)
55 err
= self.curl
.native
.easy_perform
56 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
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
64 return new CurlResponseFailed(error_code
, error_msg
)
71 super NativeCurlCallbacks
74 var datas
: nullable HeaderMap = null is writable
75 var headers
: nullable HeaderMap = null is writable
76 var delegate
: nullable CurlCallbacks = null is writable
78 # Set the user agent for all following HTTP requests
79 fun user_agent
=(name
: String)
81 curl
.native
.easy_setopt
(new CURLOption.user_agent
, name
)
84 # Execute HTTP request with settings configured through attribute
87 if not self.curl
.is_ok
then return answer_failure
(0, "Curl instance is not correctly initialized")
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)
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
)
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
)
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
)
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
)
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
)
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
)
124 var err_resp
= perform
125 if err_resp
!= null then return err_resp
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
130 return success_response
133 # Download to file given resource
134 fun download_to_file
(output_file_name
: nullable String): CurlResponse
136 var success_response
= new CurlFileResponseSuccess
138 var callback_receiver
: CurlCallbacks = success_response
139 if self.delegate
!= null then callback_receiver
= self.delegate
.as(not null)
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
)
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
)
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
)
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
)
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
("")
161 return answer_failure
(0, "Unable to extract file name, please specify one")
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")
169 var err_resp
= perform
170 if err_resp
!= null then return err_resp
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
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
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
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
184 success_response
.file
.close
186 return success_response
193 super NativeCurlCallbacks
195 # Address of the sender
196 var from
: nullable String is writable
199 var to
: nullable Array[String] is writable
202 var subject
: nullable String is writable
205 var body
: nullable String is writable
208 var cc
: nullable Array[String] is writable
210 # BCC recipients (hidden from other recipients)
211 var bcc
: nullable Array[String] is writable
214 var headers
= new HeaderMap is lazy
, writable
217 var headers_body
= new HeaderMap is lazy
, writable
219 private var supported_outgoing_protocol
: Array[String] = ["smtp", "smtps"]
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
224 if val
!= null then return "{str}{att}{val}\n"
225 return "{str}{att}\n"
228 # Helper method to add entire list of pairs to mail content
229 private fun add_pairs_to_content
(content
: String, pairs
: HeaderMap): String
231 for h_key
, h_val
in pairs
do content
= add_pair_to_content
(content
, h_key
, h_val
)
235 # Check for host and protocol availability
236 private fun is_supported_outgoing_protocol
(host
: String): CURLCode
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
243 # Configure server host and user credentials if needed.
244 fun set_outgoing_server
(host
: String, user
: nullable String, pwd
: nullable String): nullable CurlResponseFailed
246 # Check Curl initialisation
247 if not self.curl
.is_ok
then return answer_failure
(0, "Curl instance is not correctly initialized")
252 err
= is_supported_outgoing_protocol
(host
)
253 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
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
)
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
)
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
)
270 # Execute Mail request with settings configured through attribute
273 if not self.curl
.is_ok
then return answer_failure
(0, "Curl instance is not correctly initialized")
275 var lines
= new Array[String]
278 var headers
= self.headers
279 if not headers
.is_empty
then
280 for k
, v
in headers
do lines
.add
"{k}{v}"
284 var all_recipients
= new Array[String]
286 if to
!= null and to
.length
> 0 then
287 lines
.add
"To:{to.join(",")}"
288 all_recipients
.append to
292 if cc
!= null and cc
.length
> 0 then
293 lines
.add
"Cc:{cc.join(",")}"
294 all_recipients
.append cc
298 if bcc
!= null and bcc
.length
> 0 then all_recipients
.append bcc
300 if all_recipients
.is_empty
then return answer_failure
(0, "There must be at lease one recipient")
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
)
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
)
310 if not from
== null then
311 lines
.add
"From:{from}"
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
)
318 var subject
= self.subject
319 if subject
== null then subject
= "" # Default
320 lines
.add
"Subject: {subject}"
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}"
330 if body
== null then body
= "" # Default
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
)
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
)
343 var err_resp
= perform
344 if err_resp
!= null then return err_resp
346 return new CurlMailResponseSuccess
350 # Callbacks Interface, allow you to manage in your way the different streams
351 interface CurlCallbacks
352 super NativeCurlCallbacks
355 # Abstract Curl request response
356 abstract class CurlResponse
359 # Failed Response Class returned when errors during configuration are raised
360 class CurlResponseFailed
364 var error_msg
: String
367 # Success Abstract Response Success Class
368 abstract class CurlResponseSuccessIntern
372 var headers
= new HashMap[String, String]
374 # Receive headers from request due to headers callback registering
375 redef fun header_callback
(line
)
377 var splitted
= line
.split_with
(':')
378 if splitted
.length
> 1 then
379 var key
= splitted
.shift
380 self.headers
[key
] = splitted
.to_s
385 # Success Response Class of a basic response
386 class CurlResponseSuccess
387 super CurlResponseSuccessIntern
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}"
398 # Success Response Class of mail request
399 class CurlMailResponseSuccess
400 super CurlResponseSuccessIntern
403 # Success Response Class of a downloaded File
404 class CurlFileResponseSuccess
405 super CurlResponseSuccessIntern
408 var speed_download
= 0
409 var size_download
= 0
411 private var file
: nullable FileWriter = null
413 # Receive bytes stream from request due to stream callback registering
414 redef fun stream_callback
(buffer
)
420 # Pseudo map associating `String` to `String` for HTTP exchanges
422 # This structure differs from `Map` as each key can have multiple associations
423 # and the order of insertion is important to some services.
425 private var array
= new Array[Couple[String, String]]
427 # Add a `value` associated to `key`
428 fun []=(key
, value
: String)
430 array
.add
new Couple[String, String](key
, value
)
433 # Get a list of the keys associated to `key`
434 fun [](k
: String): Array[String]
436 var res
= new Array[String]
437 for c
in array
do if c
.first
== k
then res
.add c
.second
441 # Iterate over all the associations in `self`
442 fun iterator
: MapIterator[String, String] do return new HeaderMapIterator(self)
444 # Get `self` as a single string for HTTP POST
446 # Require: `curl.is_ok`
447 fun to_url_encoded
(curl
: Curl): String
451 var lines
= new Array[String]
453 if k
.length
== 0 then continue
455 k
= curl
.native
.escape
(k
)
456 v
= curl
.native
.escape
(v
)
459 return lines
.join
("&")
462 # Concatenate couple of 'key value' separated by 'sep' in Array
463 fun join_pairs
(sep
: String): Array[String]
465 var col
= new Array[String]
466 for k
, v
in self do col
.add
("{k}{sep}{v}")
470 # Number of values in `self`
471 fun length
: Int do return array
.length
474 fun is_empty
: Bool do return array
.is_empty
477 private class HeaderMapIterator
478 super MapIterator[String, String]
481 var iterator
: Iterator[Couple[String, String]] = map
.array
.iterator
is lazy
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