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 data over HTTP with `CurlHTTPRequest` and send emails
20 # with `CurlMail`. Scripts can use the easier (but limited) services on `Text`,
21 # `http_get` and `http_download`, provided by `curl::extra`.
30 var native
= new NativeCurl.easy_init
32 # Is this instance correctly initialized?
33 fun is_ok
: Bool do return self.native
.is_init
35 redef fun finalize_once
do if is_ok
then native
.easy_clean
41 private var curl
= new Curl
43 # Shall this request be verbose?
44 var verbose
: Bool = false is writable
46 # Intern perform method, lowest level of request launching
47 private fun perform
: nullable CurlResponseFailed
49 if not self.curl
.is_ok
then return answer_failure
(0, "Curl instance is not correctly initialized")
53 err
= self.curl
.native
.easy_setopt
(new CURLOption.verbose
, self.verbose
)
54 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
56 err
= self.curl
.native
.easy_perform
57 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
62 # Intern method with return a failed answer with given code and message
63 private fun answer_failure
(error_code
: Int, error_msg
: String): CurlResponseFailed
65 return new CurlResponseFailed(error_code
, error_msg
)
68 # Close low-level resources associated to this request
70 # Once closed, this request can't be used again.
72 # If this service isn't called explicitly, low-level resources
73 # may be freed automatically by the GC.
74 fun close
do curl
.finalize
77 # HTTP request builder
79 # The request itself is sent by either `execute` or `download_to_file`.
80 # The attributes of this class must be set before calling either of these two methods.
82 # ## Minimal usage example
85 # var request = new CurlHTTPRequest("http://example.org/")
86 # var response = request.execute
87 # if response isa CurlResponseSuccess then
88 # print "Response status code: {response.status_code}"
89 # print response.body_str
90 # else if response isa CurlResponseFailed then
91 # print_error response.error_msg
96 super NativeCurlCallbacks
98 # Address of the remote resource to request
101 # Data for the body of a POST request
102 var data
: nullable HeaderMap is writable
106 # Set this value to send raw data instead of the POST formatted `data`.
108 # If `data` is set, the body will not be sent.
109 var body
: nullable String is writable
111 # Header content of the request
112 var headers
: nullable HeaderMap is writable
114 # Delegates to customize the behavior when running `execute`
115 var delegate
: nullable CurlCallbacks is writable
117 # Set the user agent for all following HTTP requests
118 var user_agent
: nullable String is writable
120 # Set the Unix domain socket path to use
122 # When not null, enables using a Unix domain socket
123 # instead of a TCP connection and DNS hostname resolution.
124 var unix_socket_path
: nullable String is writable
126 # The HTTP method, GET by default
128 # Must be a capitalized string with request name complying with RFC7231
129 var method
: String = "GET" is optional
, writable
131 # Execute HTTP request
133 # By default, the response body is returned in an instance of `CurlResponse`.
134 # This behavior can be customized by setting a custom `delegate`.
135 fun execute
: CurlResponse
137 # Reset libcurl parameters as the lib is shared and options
138 # might affect requests from one another.
139 if not self.curl
.is_ok
then return answer_failure
(0, "Curl instance is not correctly initialized")
141 var success_response
= new CurlResponseSuccess
142 var callback_receiver
: CurlCallbacks = success_response
146 err
= prepare_request
(callback_receiver
)
147 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
150 var err_resp
= perform
151 if err_resp
!= null then return err_resp
153 var st_code
= self.curl
.native
.easy_getinfo_long
(new CURLInfoLong.response_code
)
154 if not st_code
== null then success_response
.status_code
= st_code
156 return success_response
159 # Internal function that sets cURL options and request' parameters
160 private fun prepare_request
(callback_receiver
: CurlCallbacks) : CURLCode
164 # cURL options and delegates
165 err
= set_curl_options
166 if not err
.is_ok
then return err
169 err
= set_curl_callback
(callback_receiver
)
170 if not err
.is_ok
then return err
173 err
= set_curl_http_header
174 if not err
.is_ok
then return err
176 # Set HTTP method and body
178 if not err
.is_ok
then return err
184 # Set cURL parameters according to assigned HTTP method set in method
185 # attribute and body if the method allows it according to RFC7231
186 private fun set_method
: CURLCode
190 if self.method
=="GET" then
191 err
=self.curl
.native
.easy_setopt
(new CURLOption.get
, 1)
193 else if self.method
=="POST" then
194 err
=self.curl
.native
.easy_setopt
(new CURLOption.post
, 1)
196 else if self.method
=="HEAD" then
197 err
=self.curl
.native
.easy_setopt
(new CURLOption.no_body
,1)
200 err
=self.curl
.native
.easy_setopt
(new CURLOption.custom_request
,self.method
)
206 private fun set_body
: CURLCode
213 var postdatas
= data
.to_url_encoded
(self.curl
)
214 err
= self.curl
.native
.easy_setopt
(new CURLOption.postfields
, postdatas
)
215 if not err
.is_ok
then return err
216 else if body
!= null then
217 err
= self.curl
.native
.easy_setopt
(new CURLOption.postfields
, body
)
218 if not err
.is_ok
then return err
220 return new CURLCode.ok
224 # such as delegate, follow location, URL, user agent and address family
225 private fun set_curl_options
: CURLCode
229 err
= self.curl
.native
.easy_setopt
(new CURLOption.follow_location
, 1)
230 if not err
.is_ok
then return err
232 err
= self.curl
.native
.easy_setopt
(new CURLOption.url
, url
)
233 if not err
.is_ok
then return err
235 var user_agent
= user_agent
236 if user_agent
!= null then
237 err
= curl
.native
.easy_setopt
(new CURLOption.user_agent
, user_agent
)
238 if not err
.is_ok
then return err
241 var unix_socket_path
= unix_socket_path
242 if unix_socket_path
!= null then
243 err
= self.curl
.native
.easy_setopt
(new CURLOption.unix_socket_path
, unix_socket_path
)
244 if not err
.is_ok
then return err
250 private fun set_curl_callback
(callback_receiver
: CurlCallbacks) : CURLCode
254 if self.delegate
!= null then callback_receiver
= self.delegate
.as(not null)
256 err
= self.curl
.native
.register_callback_header
(callback_receiver
)
257 if not err
.is_ok
then return err
259 err
= self.curl
.native
.register_callback_body
(callback_receiver
)
260 if not err
.is_ok
then return err
265 # Set cURL request header according to attribute headers
266 private fun set_curl_http_header
: CURLCode
268 var headers
= self.headers
269 if headers
!= null then
270 var headers_joined
= headers
.join_pairs
(": ")
271 var err
= self.curl
.native
.easy_setopt
(new CURLOption.httpheader
, headers_joined
.to_curlslist
)
272 if not err
.is_ok
then return err
274 return new CURLCode.ok
277 # Download to file given resource
278 fun download_to_file
(output_file_name
: nullable String): CurlResponse
280 if not self.curl
.is_ok
then return answer_failure
(0, "Curl instance is not correctly initialized")
282 var success_response
= new CurlFileResponseSuccess
284 var callback_receiver
: CurlCallbacks = success_response
285 if self.delegate
!= null then callback_receiver
= self.delegate
.as(not null)
289 err
= self.curl
.native
.easy_setopt
(new CURLOption.follow_location
, 1)
290 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
292 err
= self.curl
.native
.easy_setopt
(new CURLOption.url
, url
)
293 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
295 err
= self.curl
.native
.register_callback_header
(callback_receiver
)
296 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
298 err
= self.curl
.native
.register_callback_stream
(callback_receiver
)
299 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
302 if not output_file_name
== null then
303 opt_name
= output_file_name
304 else if not self.url
.substring
(self.url
.length-1
, self.url
.length
) == "/" then
305 opt_name
= self.url
.basename
307 return answer_failure
(0, "Unable to extract file name, please specify one")
310 success_response
.file
= new FileWriter.open
(opt_name
)
311 if not success_response
.file
.is_writable
then
312 return answer_failure
(0, "Unable to create associated file")
315 var err_resp
= perform
316 if err_resp
!= null then return err_resp
318 var st_code
= self.curl
.native
.easy_getinfo_long
(new CURLInfoLong.response_code
)
319 if not st_code
== null then success_response
.status_code
= st_code
321 var speed
= self.curl
.native
.easy_getinfo_double
(new CURLInfoDouble.speed_download
)
322 if not speed
== null then success_response
.speed_download
= speed
324 var size
= self.curl
.native
.easy_getinfo_double
(new CURLInfoDouble.size_download
)
325 if not size
== null then success_response
.size_download
= size
327 var time
= self.curl
.native
.easy_getinfo_double
(new CURLInfoDouble.total_time
)
328 if not time
== null then success_response
.total_time
= time
330 success_response
.file
.close
332 return success_response
341 # var mail = new CurlMail("sender@example.org",
342 # to=["to@example.org"], cc=["bob@example.org"])
344 # mail.headers_body["Content-Type:"] = """text/html; charset="UTF-8""""
345 # mail.headers_body["Content-Transfer-Encoding:"] = "quoted-printable"
347 # mail.body = "<h1>Here you can write HTML stuff.</h1>"
348 # mail.subject = "Hello From My Nit Program"
351 # var error = mail.set_outgoing_server("smtps://smtp.example.org:465",
352 # "user@example.org", "mypassword")
353 # if error != null then
354 # print "Mail Server Error: {error}"
359 # error = mail.execute
360 # if error != null then
361 # print "Transfer Error: {error}"
367 super NativeCurlCallbacks
369 # Address of the sender
370 var from
: nullable String is writable
373 var to
: nullable Array[String] is writable
376 var subject
: nullable String is writable
379 var body
: nullable String is writable
382 var cc
: nullable Array[String] is writable
384 # BCC recipients (hidden from other recipients)
385 var bcc
: nullable Array[String] is writable
388 var headers
= new HeaderMap is lazy
, writable
391 var headers_body
= new HeaderMap is lazy
, writable
393 # Protocols supported to send mail to a server
395 # Default value at `["smtp", "smtps"]`
396 var supported_outgoing_protocol
= ["smtp", "smtps"]
398 # Helper method to add pair values to mail content while building it (ex: "To:", "address@mail.com")
399 private fun add_pair_to_content
(str
: String, att
: String, val
: nullable String): String
401 if val
!= null then return "{str}{att}{val}\n"
402 return "{str}{att}\n"
405 # Helper method to add entire list of pairs to mail content
406 private fun add_pairs_to_content
(content
: String, pairs
: HeaderMap): String
408 for h_key
, h_val
in pairs
do content
= add_pair_to_content
(content
, h_key
, h_val
)
412 # Check for host and protocol availability
413 private fun is_supported_outgoing_protocol
(host
: String): CURLCode
415 var host_reach
= host
.split_with
("://")
416 if host_reach
.length
> 1 and supported_outgoing_protocol
.has
(host_reach
[0]) then return once
new CURLCode.ok
417 return once
new CURLCode.unsupported_protocol
420 # Configure server host and user credentials if needed.
421 fun set_outgoing_server
(host
: String, user
: nullable String, pwd
: nullable String): nullable CurlResponseFailed
423 # Check Curl initialisation
424 if not self.curl
.is_ok
then return answer_failure
(0, "Curl instance is not correctly initialized")
429 err
= is_supported_outgoing_protocol
(host
)
430 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
432 err
= self.curl
.native
.easy_setopt
(new CURLOption.url
, host
)
433 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
436 if not user
== null and not pwd
== null then
437 err
= self.curl
.native
.easy_setopt
(new CURLOption.username
, user
)
438 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
440 err
= self.curl
.native
.easy_setopt
(new CURLOption.password
, pwd
)
441 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
447 # Execute Mail request with settings configured through attribute
448 fun execute
: nullable CurlResponseFailed
450 if not self.curl
.is_ok
then return answer_failure
(0, "Curl instance is not correctly initialized")
452 var lines
= new Array[String]
455 var headers
= self.headers
456 if not headers
.is_empty
then
457 for k
, v
in headers
do lines
.add
"{k}{v}"
461 var all_recipients
= new Array[String]
463 if to
!= null and to
.length
> 0 then
464 lines
.add
"To:{to.join(",")}"
465 all_recipients
.append to
469 if cc
!= null and cc
.length
> 0 then
470 lines
.add
"Cc:{cc.join(",")}"
471 all_recipients
.append cc
475 if bcc
!= null and bcc
.length
> 0 then all_recipients
.append bcc
477 if all_recipients
.is_empty
then return answer_failure
(0, "There must be at lease one recipient")
479 var err
= self.curl
.native
.easy_setopt
(new CURLOption.follow_location
, 1)
480 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
482 err
= self.curl
.native
.easy_setopt
(new CURLOption.mail_rcpt
, all_recipients
.to_curlslist
)
483 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
487 if not from
== null then
488 lines
.add
"From:{from}"
490 err
= self.curl
.native
.easy_setopt
(new CURLOption.mail_from
, from
)
491 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
495 var subject
= self.subject
496 if subject
== null then subject
= "" # Default
497 lines
.add
"Subject: {subject}"
500 var headers_body
= self.headers_body
501 if not headers_body
.is_empty
then
502 for k
, v
in headers_body
do lines
.add
"{k}{v}"
507 if body
== null then body
= "" # Default
513 err
= self.curl
.native
.register_callback_read
(self)
514 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
516 var content
= lines
.join
("\n")
517 err
= self.curl
.native
.register_read_datas_callback
(self, content
)
518 if not err
.is_ok
then return answer_failure
(err
.to_i
, err
.to_s
)
520 var err_resp
= perform
521 if err_resp
!= null then return err_resp
527 # Callbacks Interface, allow you to manage in your way the different streams
528 interface CurlCallbacks
529 super NativeCurlCallbacks
532 # Abstract Curl request response
533 abstract class CurlResponse
536 # Failed Response Class returned when errors during configuration are raised
537 class CurlResponseFailed
544 var error_msg
: String
546 redef fun to_s
do return "{error_msg} ({error_code})"
549 # Success Abstract Response Success Class
550 abstract class CurlResponseSuccessIntern
554 var headers
= new HashMap[String, String]
556 # Receive headers from request due to headers callback registering
557 redef fun header_callback
(line
)
559 var splitted
= line
.split_with
(':')
560 if splitted
.length
> 1 then
561 var key
= splitted
.shift
562 self.headers
[key
] = splitted
.to_s
567 # Success Response Class of a basic response
568 class CurlResponseSuccess
569 super CurlResponseSuccessIntern
571 # Server HTTP response code
574 # Response body as a `String`
577 # Accept part of the response body
578 redef fun body_callback
(line
) do self.body_str
+= line
581 # Success Response Class of a downloaded File
582 class CurlFileResponseSuccess
583 super CurlResponseSuccessIntern
585 # Server HTTP response code
588 var speed_download
= 0.0
589 var size_download
= 0.0
592 private var file
: nullable FileWriter = null
594 # Receive bytes stream from request due to stream callback registering
595 redef fun stream_callback
(buffer
)
601 # Pseudo map associating `String` to `String` for HTTP exchanges
603 # This structure differs from `Map` as each key can have multiple associations
604 # and the order of insertion is important to some services.
606 private var array
= new Array[Couple[String, String]]
608 # Add a `value` associated to `key`
609 fun []=(key
, value
: String)
611 array
.add
new Couple[String, String](key
, value
)
614 # Get a list of the keys associated to `key`
615 fun [](k
: String): Array[String]
617 var res
= new Array[String]
618 for c
in array
do if c
.first
== k
then res
.add c
.second
622 # Iterate over all the associations in `self`
623 fun iterator
: MapIterator[String, String] do return new HeaderMapIterator(self)
625 # Get `self` as a single string for HTTP POST
627 # Require: `curl.is_ok`
628 private fun to_url_encoded
(curl
: Curl): String
632 var lines
= new Array[String]
634 if k
.length
== 0 then continue
636 k
= curl
.native
.escape
(k
)
637 v
= curl
.native
.escape
(v
)
640 return lines
.join
("&")
643 # Concatenate couple of 'key value' separated by 'sep' in Array
644 fun join_pairs
(sep
: String): Array[String]
646 var col
= new Array[String]
647 for k
, v
in self do col
.add
("{k}{sep}{v}")
651 # Number of values in `self`
652 fun length
: Int do return array
.length
655 fun is_empty
: Bool do return array
.is_empty
658 private class HeaderMapIterator
659 super MapIterator[String, String]
662 var iterator
: Iterator[Couple[String, String]] = map
.array
.iterator
is lazy
664 redef fun is_ok
do return self.iterator
.is_ok
665 redef fun next
do self.iterator
.next
666 redef fun item
do return self.iterator
.item
.second
667 redef fun key
do return self.iterator
.item
.first