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 # Data transfer powered by the native curl library
19 # Download or upload over HTTP with `CurlHTTPRequest` and send emails with `CurlMail`.
25 # Shared Curl library handle
27 # Usually, you do not have to use this attribute, it instancied by `CurlHTTPRequest` and `CurlMail`.
28 # But in some cases you may want to finalize it to free some small resources.
29 # However, if Curl services are needed once again, this attribute must be manually set.
30 var curl
: Curl = new Curl is lazy
, writable
33 # Curl library handle, it is initialized and released with this class
37 private var native
= new NativeCurl.easy_init
39 # Check for correct initialization
40 fun is_ok
: Bool do return self.native
.is_init
42 redef fun finalize_once
do if is_ok
then native
.easy_clean
48 private var curl
: Curl = sys
.curl
50 # Shall this request be verbose?
51 var verbose
: Bool = false is writable
53 # Intern perform method, lowest level of request launching
54 private fun perform
: nullable CurlResponseFailed
56 if not self.curl
.is_ok
then return answer_failure
(0, "Curl instance is not correctly initialized")
60 err
= self.curl
.native
.easy_setopt
(new CURLOption.verbose
, self.verbose
)
61 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
63 err
= self.curl
.native
.easy_perform
64 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
69 # Intern method with return a failed answer with given code and message
70 private fun answer_failure
(error_code
: Int, error_msg
: String): CurlResponseFailed
72 return new CurlResponseFailed(error_code
, error_msg
)
76 # HTTP request builder
78 # The request itself is sent by either `execute` or `download_to_file`.
79 # The attributes of this class must be set before calling either of these two methods.
81 # ## Minimal usage example
84 # var request = new CurlHTTPRequest("http://example.org/")
85 # var response = request.execute
86 # if response isa CurlResponseSuccess then
87 # print "Response status code: {response.status_code}"
88 # print response.body_str
89 # else if response isa CurlResponseFailed then
90 # print_error response.error_msg
95 super NativeCurlCallbacks
97 # Address of the remote resource to request
100 # Data for the body of a POST request
101 var data
: nullable HeaderMap is writable
105 # Set this value to send raw data instead of the POST formatted `data`.
107 # If `data` is set, the body will not be sent.
108 var body
: nullable String is writable
110 # Header content of the request
111 var headers
: nullable HeaderMap is writable
113 # Delegates to customize the behavior when running `execute`
114 var delegate
: nullable CurlCallbacks is writable
116 # Set the user agent for all following HTTP requests
117 var user_agent
: nullable String is writable
119 # Set the Unix domain socket path to use
121 # When not null, enables using a Unix domain socket
122 # instead of a TCP connection and DNS hostname resolution.
123 var unix_socket_path
: nullable String is writable
125 # Execute HTTP request
127 # By default, the response body is returned in an instance of `CurlResponse`.
128 # This behavior can be customized by setting a custom `delegate`.
129 fun execute
: CurlResponse
131 if not self.curl
.is_ok
then return answer_failure
(0, "Curl instance is not correctly initialized")
133 var success_response
= new CurlResponseSuccess
134 var callback_receiver
: CurlCallbacks = success_response
135 if self.delegate
!= null then callback_receiver
= self.delegate
.as(not null)
139 err
= self.curl
.native
.easy_setopt
(new CURLOption.follow_location
, 1)
140 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
142 err
= self.curl
.native
.easy_setopt
(new CURLOption.url
, url
)
143 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
145 var user_agent
= user_agent
146 if user_agent
!= null then
147 err
= curl
.native
.easy_setopt
(new CURLOption.user_agent
, user_agent
)
148 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
151 var unix_socket_path
= unix_socket_path
152 if unix_socket_path
!= null then
153 err
= self.curl
.native
.easy_setopt
(new CURLOption.unix_socket_path
, unix_socket_path
)
154 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
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
)
161 err
= self.curl
.native
.register_callback_body
(callback_receiver
)
162 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
165 var headers
= self.headers
166 if headers
!= null then
167 var headers_joined
= headers
.join_pairs
(": ")
168 err
= self.curl
.native
.easy_setopt
(new CURLOption.httpheader
, headers_joined
.to_curlslist
)
169 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
175 var postdatas
= data
.to_url_encoded
(self.curl
)
176 err
= self.curl
.native
.easy_setopt
(new CURLOption.postfields
, postdatas
)
177 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
178 else if body
!= null then
179 err
= self.curl
.native
.easy_setopt
(new CURLOption.postfields
, body
.as(not null))
180 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
183 var err_resp
= perform
184 if err_resp
!= null then return err_resp
186 var st_code
= self.curl
.native
.easy_getinfo_long
(new CURLInfoLong.response_code
)
187 if not st_code
== null then success_response
.status_code
= st_code
189 return success_response
192 # Download to file given resource
193 fun download_to_file
(output_file_name
: nullable String): CurlResponse
195 var success_response
= new CurlFileResponseSuccess
197 var callback_receiver
: CurlCallbacks = success_response
198 if self.delegate
!= null then callback_receiver
= self.delegate
.as(not null)
202 err
= self.curl
.native
.easy_setopt
(new CURLOption.follow_location
, 1)
203 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
205 err
= self.curl
.native
.easy_setopt
(new CURLOption.url
, url
)
206 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
208 err
= self.curl
.native
.register_callback_header
(callback_receiver
)
209 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
211 err
= self.curl
.native
.register_callback_stream
(callback_receiver
)
212 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
215 if not output_file_name
== null then
216 opt_name
= output_file_name
217 else if not self.url
.substring
(self.url
.length-1
, self.url
.length
) == "/" then
218 opt_name
= self.url
.basename
220 return answer_failure
(0, "Unable to extract file name, please specify one")
223 success_response
.file
= new FileWriter.open
(opt_name
)
224 if not success_response
.file
.is_writable
then
225 return answer_failure
(0, "Unable to create associated file")
228 var err_resp
= perform
229 if err_resp
!= null then return err_resp
231 var st_code
= self.curl
.native
.easy_getinfo_long
(new CURLInfoLong.response_code
)
232 if not st_code
== null then success_response
.status_code
= st_code
234 var speed
= self.curl
.native
.easy_getinfo_double
(new CURLInfoDouble.speed_download
)
235 if not speed
== null then success_response
.speed_download
= speed
237 var size
= self.curl
.native
.easy_getinfo_double
(new CURLInfoDouble.size_download
)
238 if not size
== null then success_response
.size_download
= size
240 var time
= self.curl
.native
.easy_getinfo_double
(new CURLInfoDouble.total_time
)
241 if not time
== null then success_response
.total_time
= time
243 success_response
.file
.close
245 return success_response
253 # var mail = new CurlMail("sender@example.org",
254 # to=["to@example.org"], cc=["bob@example.org"])
256 # mail.headers_body["Content-Type:"] = """text/html; charset="UTF-8""""
257 # mail.headers_body["Content-Transfer-Encoding:"] = "quoted-printable"
259 # mail.body = "<h1>Here you can write HTML stuff.</h1>"
260 # mail.subject = "Hello From My Nit Program"
263 # var error = mail.set_outgoing_server("smtps://smtp.example.org:465",
264 # "user@example.org", "mypassword")
265 # if error != null then
266 # print "Mail Server Error: {error}"
271 # error = mail.execute
272 # if error != null then
273 # print "Transfer Error: {error}"
279 super NativeCurlCallbacks
281 # Address of the sender
282 var from
: nullable String is writable
285 var to
: nullable Array[String] is writable
288 var subject
: nullable String is writable
291 var body
: nullable String is writable
294 var cc
: nullable Array[String] is writable
296 # BCC recipients (hidden from other recipients)
297 var bcc
: nullable Array[String] is writable
300 var headers
= new HeaderMap is lazy
, writable
303 var headers_body
= new HeaderMap is lazy
, writable
305 # Protocols supported to send mail to a server
307 # Default value at `["smtp", "smtps"]`
308 var supported_outgoing_protocol
= ["smtp", "smtps"]
310 # Helper method to add pair values to mail content while building it (ex: "To:", "address@mail.com")
311 private fun add_pair_to_content
(str
: String, att
: String, val
: nullable String): String
313 if val
!= null then return "{str}{att}{val}\n"
314 return "{str}{att}\n"
317 # Helper method to add entire list of pairs to mail content
318 private fun add_pairs_to_content
(content
: String, pairs
: HeaderMap): String
320 for h_key
, h_val
in pairs
do content
= add_pair_to_content
(content
, h_key
, h_val
)
324 # Check for host and protocol availability
325 private fun is_supported_outgoing_protocol
(host
: String): CURLCode
327 var host_reach
= host
.split_with
("://")
328 if host_reach
.length
> 1 and supported_outgoing_protocol
.has
(host_reach
[0]) then return once
new CURLCode.ok
329 return once
new CURLCode.unsupported_protocol
332 # Configure server host and user credentials if needed.
333 fun set_outgoing_server
(host
: String, user
: nullable String, pwd
: nullable String): nullable CurlResponseFailed
335 # Check Curl initialisation
336 if not self.curl
.is_ok
then return answer_failure
(0, "Curl instance is not correctly initialized")
341 err
= is_supported_outgoing_protocol
(host
)
342 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
344 err
= self.curl
.native
.easy_setopt
(new CURLOption.url
, host
)
345 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
348 if not user
== null and not pwd
== null then
349 err
= self.curl
.native
.easy_setopt
(new CURLOption.username
, user
)
350 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
352 err
= self.curl
.native
.easy_setopt
(new CURLOption.password
, pwd
)
353 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
359 # Execute Mail request with settings configured through attribute
360 fun execute
: nullable CurlResponseFailed
362 if not self.curl
.is_ok
then return answer_failure
(0, "Curl instance is not correctly initialized")
364 var lines
= new Array[String]
367 var headers
= self.headers
368 if not headers
.is_empty
then
369 for k
, v
in headers
do lines
.add
"{k}{v}"
373 var all_recipients
= new Array[String]
375 if to
!= null and to
.length
> 0 then
376 lines
.add
"To:{to.join(",")}"
377 all_recipients
.append to
381 if cc
!= null and cc
.length
> 0 then
382 lines
.add
"Cc:{cc.join(",")}"
383 all_recipients
.append cc
387 if bcc
!= null and bcc
.length
> 0 then all_recipients
.append bcc
389 if all_recipients
.is_empty
then return answer_failure
(0, "There must be at lease one recipient")
391 var err
= self.curl
.native
.easy_setopt
(new CURLOption.follow_location
, 1)
392 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
394 err
= self.curl
.native
.easy_setopt
(new CURLOption.mail_rcpt
, all_recipients
.to_curlslist
)
395 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
399 if not from
== null then
400 lines
.add
"From:{from}"
402 err
= self.curl
.native
.easy_setopt
(new CURLOption.mail_from
, from
)
403 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
407 var subject
= self.subject
408 if subject
== null then subject
= "" # Default
409 lines
.add
"Subject: {subject}"
412 var headers_body
= self.headers_body
413 if not headers_body
.is_empty
then
414 for k
, v
in headers_body
do lines
.add
"{k}{v}"
419 if body
== null then body
= "" # Default
425 err
= self.curl
.native
.register_callback_read
(self)
426 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
428 var content
= lines
.join
("\n")
429 err
= self.curl
.native
.register_read_datas_callback
(self, content
)
430 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
432 var err_resp
= perform
433 if err_resp
!= null then return err_resp
439 # Callbacks Interface, allow you to manage in your way the different streams
440 interface CurlCallbacks
441 super NativeCurlCallbacks
444 # Abstract Curl request response
445 abstract class CurlResponse
448 # Failed Response Class returned when errors during configuration are raised
449 class CurlResponseFailed
453 var error_msg
: String
455 redef fun to_s
do return "{error_msg} ({error_code})"
458 # Success Abstract Response Success Class
459 abstract class CurlResponseSuccessIntern
463 var headers
= new HashMap[String, String]
465 # Receive headers from request due to headers callback registering
466 redef fun header_callback
(line
)
468 var splitted
= line
.split_with
(':')
469 if splitted
.length
> 1 then
470 var key
= splitted
.shift
471 self.headers
[key
] = splitted
.to_s
476 # Success Response Class of a basic response
477 class CurlResponseSuccess
478 super CurlResponseSuccessIntern
483 # Receive body from request due to body callback registering
484 redef fun body_callback
(line
) do
485 self.body_str
= "{self.body_str}{line}"
489 # Success Response Class of a downloaded File
490 class CurlFileResponseSuccess
491 super CurlResponseSuccessIntern
494 var speed_download
= 0.0
495 var size_download
= 0.0
497 private var file
: nullable FileWriter = null
499 # Receive bytes stream from request due to stream callback registering
500 redef fun stream_callback
(buffer
)
506 # Pseudo map associating `String` to `String` for HTTP exchanges
508 # This structure differs from `Map` as each key can have multiple associations
509 # and the order of insertion is important to some services.
511 private var array
= new Array[Couple[String, String]]
513 # Add a `value` associated to `key`
514 fun []=(key
, value
: String)
516 array
.add
new Couple[String, String](key
, value
)
519 # Get a list of the keys associated to `key`
520 fun [](k
: String): Array[String]
522 var res
= new Array[String]
523 for c
in array
do if c
.first
== k
then res
.add c
.second
527 # Iterate over all the associations in `self`
528 fun iterator
: MapIterator[String, String] do return new HeaderMapIterator(self)
530 # Get `self` as a single string for HTTP POST
532 # Require: `curl.is_ok`
533 fun to_url_encoded
(curl
: Curl): String
537 var lines
= new Array[String]
539 if k
.length
== 0 then continue
541 k
= curl
.native
.escape
(k
)
542 v
= curl
.native
.escape
(v
)
545 return lines
.join
("&")
548 # Concatenate couple of 'key value' separated by 'sep' in Array
549 fun join_pairs
(sep
: String): Array[String]
551 var col
= new Array[String]
552 for k
, v
in self do col
.add
("{k}{sep}{v}")
556 # Number of values in `self`
557 fun length
: Int do return array
.length
560 fun is_empty
: Bool do return array
.is_empty
563 private class HeaderMapIterator
564 super MapIterator[String, String]
567 var iterator
: Iterator[Couple[String, String]] = map
.array
.iterator
is lazy
569 redef fun is_ok
do return self.iterator
.is_ok
570 redef fun next
do self.iterator
.next
571 redef fun item
do return self.iterator
.item
.second
572 redef fun key
do return self.iterator
.item
.first