' Copyright (c) 2007-2022 Bruce A Henderson
'
' Permission is hereby granted, free of charge, to any person obtaining a copy
' of this software and associated documentation files (the "Software"), to deal
' in the Software without restriction, including without limitation the rights
' to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
' copies of the Software, and to permit persons to whom the Software is
' furnished to do so, subject to the following conditions:
'
' The above copyright notice and this permission notice shall be included in
' all copies or substantial portions of the Software.
'
' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
' OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
' THE SOFTWARE.
SuperStrict
Import BRL.Map
Import BRL.Stream
Import BRL.LinkedList
Import "common.bmx"
Rem
bbdoc: Sets up the program environment that libcurl needs.
about: This will be run internally on the creation of the first #TCurlEasy with default values, but you can
call it yourself if you need to set anything specifically.
The flags option is a bit pattern that tells libcurl exactly what features to init, as described below.
Set the desired bits by ORing the values together. In normal operation, you must specify CURL_GLOBAL_ALL.
Don't use any other value unless you are familiar with and mean to control internal operations of libcurl.
Available options:
CURL_GLOBAL_ALL
CURL_GLOBAL_SSL
CURL_GLOBAL_WIN32
CURL_GLOBAL_NOTHING
End Rem
Function CurlGlobalInit:Int(flags:Int)
Return curl_global_init(flags)
End Function
Rem
bbdoc: Returns a string describing a libcurl error code.
End Rem
Function CurlError:String(errorCode:Int)
Return String.fromUTF8String(curl_easy_strerror(errorCode))
End Function
Type TCurlHasLists Abstract
Field sLists:TSList[]
Method freeLists()
If sLists Then
For Local i:Int = 0 Until sLists.length
curl_slist_free_all(sLists[i].slist)
Next
sLists = Null
End If
End Method
End Type
Rem
bbdoc: The libcurl easy interface.
about: When using libcurl's "easy" interface you create your new session and get a #TCurlEasy handle
(often referred to as an "easy handle").
You then set all the options you want in the upcoming transfer, the most important among them is the
URL itself (you can't transfer anything without a specified URL as you may have figured out yourself).
You might want to set some callbacks as well that will be called from the library when data is
available etc. There are various methods to use depending on the type of option you want to set. They are,
#setOptInt, #setOptLong, #setOptBytePtr, #setOptString, #setPrivate, #setProgressCallback,
#setDebugCallback, #setWriteStream, #setWriteCallback, #setWriteString, #setReadStream, #setReadCallback,
#setHeaderCallback, #httpPost, #httpHeader, #http200Aliases, #preQuote, #quote and #postQuote.
When all is setup, you tell libcurl to perform the transfer using #perform. It will then do the entire
operation and won't return until it is done (successfully or not).
After the transfer has been made, you can set new options and make another transfer, or if you're done,
cleanup the session by calling #cleanup. If you want persistent connections, you don't cleanup immediately,
but instead run ahead and perform other transfers using the same easy handle.
End Rem
Type TCurlEasy Extends TCurlHasLists
Field easyHandlePtr:Byte Ptr
' keeps strings alive for as long as we need them...
Field stringMap:TMap = New TMap
' the transfer data as a string...
Field data:String
Rem
bbdoc:
returns: A new instance of #TCurlEasy or Null if the libcurl handle could not be created.
End Rem
Function Create:TCurlEasy()
Local this:TCurlEasy = New TCurlEasy
this.easyHandlePtr = curl_easy_init()
If Not this.easyHandlePtr Then
Return Null
End If
Return this
End Function
Method setOpt(option:Int, parameter:Byte Ptr)
If easyHandlePtr Then
bmx_curl_easy_setopt_ptr(easyHandlePtr, option, parameter)
End If
End Method
Rem
bbdoc: Data that should be associated with this curl handle.
about: The data can subsequently be retrieved using #getInfo with the #privateData method.
libcurl itself does nothing with this data.
Use this instead of setopt #CURLOPT_PRIVATE
End Rem
Method setPrivate(data:Object)
If easyHandlePtr Then
bmx_curl_easy_setopt_obj(easyHandlePtr, CURLOPT_PRIVATE, data)
End If
End Method
Rem
bbdoc: Sets a particular curl @option with the int @parameter.
End Rem
Method setOptInt:Int(option:Int, parameter:Int)
If easyHandlePtr Then
Return bmx_curl_easy_setopt_int(easyHandlePtr, option, parameter)
End If
End Method
Rem
bbdoc: Sets a particular curl @option with the long @parameter.
End Rem
Method setOptLong:Int(option:Int, parameter:Long)
If easyHandlePtr Then
Return bmx_curl_easy_setopt_bbint64(easyHandlePtr, option, parameter)
End If
End Method
Rem
bbdoc: Sets a particular curl @option with the byte ptr @parameter.
End Rem
Method setOptBytePtr:Int(option:Int, parameter:Byte Ptr)
If easyHandlePtr Then
Return bmx_curl_easy_setopt_ptr(easyHandlePtr, option, parameter)
End If
End Method
Rem
bbdoc: Sets a particular curl @option with the String @parameter.
End Rem
Method setOptString:Int(option:Int, parameter:String)
If easyHandlePtr Then
' strings need to be alive for as long as libcurl needs them... so we cache them.
Local s:Byte Ptr
Local opt:TCurlInt = TCurlInt(stringMap.ValueForKey(String(option)))
If opt Then
' done with this one... free it up.
If opt.s Then
MemFree(opt.s)
End If
Else
opt = New TCurlInt
opt.opt = option
stringMap.insert(String(option), opt)
End If
If parameter Then
opt.s = parameter.toUTF8String()
Return bmx_curl_easy_setopt_str(easyHandlePtr, option, opt.s)
Else
opt.s = Null
Return bmx_curl_easy_setopt_ptr(easyHandlePtr, option, Null)
End If
End If
End Method
Rem
bbdoc: Perform a file transfer.
returns: 0 when successful, or non-zero when an error occured.
about: This method is called after all the @setOpt calls are made, and will perform the transfer as described in the options.
You can do any amount of calls to #perform while using the same handle. If you intend to transfer more than one
file, you are even encouraged to do so. libcurl will then attempt to re-use the same connection for the
following transfers, thus making the operations faster, less CPU intense and using less network resources.
Just note that you will have to use @setopt between the invokes to set options for the following #perform.
End Rem
Method perform:Int()
If easyHandlePtr Then
Return curl_easy_perform(easyHandlePtr)
End If
End Method
Rem
bbdoc: End a libcurl easy session.
about: This function must be the last function to call for an easy session.
This will effectively close all connections this handle has used and possibly has kept open until now.
Don't call this method if you intend to transfer more files.
BlitzMax will call this method when the instance of the #TCurlEasy object is garbage collected, but you should
probably call it yourself when you are done with the session. Once called, create yourself a new #TCurlEasy
object when you need a new connection.
End Rem
Method cleanup()
If easyHandlePtr Then
curl_easy_cleanup(easyHandlePtr)
easyHandlePtr = Null
End If
' free up the slists
freeLists()
' cleanup strings
For Local i:TCurlInt = EachIn stringMap.values()
MemFree(i.s)
Next
stringMap.clear()
' cleanup callbacks and related data
dbData = Null
dbFunction = Null
wrData = Null
wrFunction = Null
rdData = Null
rdFunction = Null
hrData = Null
hrFunction = Null
End Method
Rem
bbdoc: Reset all options of a libcurl session handle.
about: Re-initializes all options previously set on a specified CURL handle to the default values.
This puts back the handle to the same state as it was in when it was first created.
It does not change the following information kept in the handle: live connections, the Session ID cache,
the DNS cache, the cookies and shares.
End Rem
Method reset()
If easyHandlePtr Then
curl_easy_reset(easyHandlePtr)
End If
End Method
Rem
bbdoc: Get transfer progress information.
about: This function gets called by libcurl instead of its internal equivalent with a frequent interval.
While data is being transferred it will be called very frequently, and during slow periods like when
nothing is being transferred it can slow down to about one call per second.
The callback gets told how much data libcurl will transfer and has transferred, in number of bytes.
@dltotal is the total number of bytes libcurl expects to download in this transfer.
@dlnow is the number of bytes downloaded so far. @ultotal is the total number of bytes libcurl expects to
upload in this transfer. @ulnow is the number of bytes uploaded so far.
Unknown/unused argument values passed to the callback will be set to zero (like if you only download
data, the upload size will remain 0). Returning a non-zero value from this callback will cause libcurl
to abort the transfer and return #CURLE_ABORTED_BY_CALLBACK.
If you transfer data with the multi interface, this function will not be called during periods of idleness
unless you call the appropriate libcurl function that performs transfers.
Notes: This is a convenience method for using setopt with #CURLOPT_XFERINFOFUNCTION and #CURLOPT_XFERINFODATA.
End Rem
Method setProgressCallback(xferinfoFunction:Int(data:Object, dltotal:Long, dlnow:Long, ultotal:Long, ulnow:Long), data:Object = Null)
If easyHandlePtr Then
' enable progress callback
setOptInt(CURLOPT_NOPROGRESS, 0)
' set the callback
bmx_curl_easy_setopt_ptr(easyHandlePtr, CURLOPT_XFERINFOFUNCTION, xferinfoFunction)
' set user data. Need to set it to at least Null so the callback doesn't send us a NULL pointer instead of a Null Object.
bmx_curl_easy_setopt_obj(easyHandlePtr, CURLOPT_XFERINFODATA, data)
End If
End Method
Rem
bbdoc: Allows the replacement of the standard debug function used when #CURLOPT_VERBOSE is in effect.
about: This callback receives debug information, as specified with the msgType argument. The function must
return 0.
Possible msgTypes include, #CURLINFO_TEXT, #CURLINFO_HEADER_IN, #CURLINFO_HEADER_OUT, #CURLINFO_DATA_IN
and #CURLINFO_DATA_OUT.
Notes: This is a convenience method for using setopt with #CURLOPT_DEBUGFUNCTION and #CURLOPT_DEBUGDATA.
End Rem
Method setDebugCallback(debugFunction:Int(data:Object, msgType:Int, message:String), data:Object = Null)
If easyHandlePtr Then
dbFunction = debugFunction
dbData = data
bmx_curl_easy_setopt_ptr(easyHandlePtr, CURLOPT_DEBUGFUNCTION, dbCallback)
bmx_curl_easy_setopt_obj(easyHandlePtr, CURLOPT_DEBUGDATA, Self)
End If
End Method
' +++++
Field dbData:Object
Field dbFunction:Int(data:Object, msgType:Int, message:String)
Function dbCallback:Int(handle:Byte Ptr, infotype:Int, msg:Byte Ptr, size:Int, data:Object)
Return TCurlEasy(data).dbFunction(TCurlEasy(data).dbData, infotype, String.fromBytes(msg, size))
End Function
' +++++
Rem
bbdoc: Specify the stream to use for writing.
about: Setting this will override a previous call to #setWriteCallback or #setWriteString.
End Rem
Method setWriteStream(stream:TStream)
If easyHandlePtr Then
bmx_curl_easy_setopt_ptr(easyHandlePtr, CURLOPT_WRITEFUNCTION, writeStreamCallback)
bmx_curl_easy_setopt_obj(easyHandlePtr, CURLOPT_WRITEDATA, stream)
End If
End Method
Function writeStreamCallback:Int(buffer:Byte Ptr, size:Int, nmemb:Int, stream:Object)
Return TStream(stream).writeBytes(buffer, size * nmemb)
End Function
Rem
bbdoc: Sets up a callback for writing (incoming data).
about: This function gets called by libcurl as soon as there is data received that needs to be saved.
The size of the data pointed to by @buffer is @size, it will not be zero terminated. Return the
number of bytes actually taken care of. If that amount differs from the amount passed to your
function, it'll signal an error to the library and it will abort the transfer and return
#CURLE_WRITE_ERROR.
This function may be called with zero bytes data if the transfered file is empty.
Setting this will override a previous call to #setWriteStream or #setWriteString.
End Rem
Method setWriteCallback(writeFunction:Int(buffer:Byte Ptr, size:Int, data:Object), data:Object = Null)
If easyHandlePtr Then
wrFunction = writeFunction
wrData = data
bmx_curl_easy_setopt_ptr(easyHandlePtr, CURLOPT_WRITEFUNCTION, wrCallback)
bmx_curl_easy_setopt_obj(easyHandlePtr, CURLOPT_WRITEDATA, Self)
End If
End Method
' +++++
Field wrData:Object
Field wrFunction:Int(buffer:Byte Ptr, size:Int, data:Object)
Function wrCallback:Int(buffer:Byte Ptr, size:Int, nmemb:Int, data:Object)
Return TCurlEasy(data).wrFunction(buffer, size * nmemb, TCurlEasy(data).wrData)
End Function
' +++++
Rem
bbdoc: Data is retrieved into a String.
about: Use toString() to get the data once the transfer is complete.
Setting this will override a previous call to #setWriteStream or #setWriteCallback.
End Rem
Method setWriteString()
data = Null
setWriteCallback(writeStringFunction, Self)
End Method
Function writeStringFunction:Int(buffer:Byte Ptr, size:Int, curl:Object)
TCurlEasy(curl).data:+ String.FromBytes( buffer, size )
Return size
End Function
Rem
bbdoc: Specify the stream to use for reading.
about: Setting this will override a previous call to #setReadCallback.
End Rem
Method setReadStream(stream:TStream)
If easyHandlePtr Then
bmx_curl_easy_setopt_ptr(easyHandlePtr, CURLOPT_READFUNCTION, readStreamCallback)
bmx_curl_easy_setopt_obj(easyHandlePtr, CURLOPT_READDATA, stream)
End If
End Method
Function readStreamCallback:Int(buffer:Byte Ptr, size:Int, nmemb:Int, stream:Object)
Try
Return TStream(stream).read(buffer, size * nmemb)
Catch e:TStreamReadException
Return CURL_READFUNC_ABORT
End Try
End Function
Rem
bbdoc: Sets up a callback for reading (outgoing data).
about: This function gets called by libcurl as soon as it needs to read data in order to send it
to the peer. The data area pointed at by the buffer may be filled with at most @size bytes. Your
function must return the actual number of bytes that you stored in that memory area.
Returning 0 will signal end-of-file to the library and cause it to stop the current transfer.
If you stop the current transfer by returning 0 "pre-maturely" (i.e before the server expected it,
like when you've told you will upload N bytes and you upload less than N bytes), you may experience
that the server "hangs" waiting for the rest of the data that won't come.
The read callback may return #CURL_READFUNC_ABORT to stop the current operation immediately,
resulting in a #CURLE_ABORTED_BY_CALLBACK error code from the transfer.
Setting this will override a previous call to #setReadStream.
End Rem
Method setReadCallback(readFunction:Int(buffer:Byte Ptr, size:Int, data:Object), data:Object = Null)
If easyHandlePtr Then
rdFunction = readFunction
rdData = data
bmx_curl_easy_setopt_ptr(easyHandlePtr, CURLOPT_READFUNCTION, rdCallback)
bmx_curl_easy_setopt_obj(easyHandlePtr, CURLOPT_READDATA, Self)
End If
End Method
' +++++
Field rdData:Object
Field rdFunction:Int(buffer:Byte Ptr, size:Int, data:Object)
Function rdCallback:Int(buffer:Byte Ptr, size:Int, nmemb:Int, data:Object)
Return TCurlEasy(data).rdFunction(buffer, size * nmemb, TCurlEasy(data).rdData)
End Function
Rem
bbdoc: Sets up a callback for header data (incoming).
about: This function gets called by libcurl as soon as it has received header data. The header
callback will be called once for each header and only complete header lines are passed on to the
callback. Parsing headers should be easy enough using this. The size of the data pointed to by buffed
is @size bytes. Do not assume that the header line is zero terminated! The callback function must
return the number of bytes actually taken care of, or return -1 to signal error to the library
(it will cause it to abort the transfer with a #CURLE_WRITE_ERROR return code).
When a server sends a chunked encoded transfer, it may contain a trailer. That trailer is identical
to a HTTP header and if such a trailer is received it is passed to the application using this
callback as well. There are several ways to detect it being a trailer and not an ordinary header:
it comes after the response-body.
it comes after the final header line (CR LF)
a Trailer: header among the response-headers mention what header to expect in the trailer.
End Rem
Method setHeaderCallback(headerFunction:Int(buffer:Byte Ptr, size:Int, data:Object), data:Object = Null)
If easyHandlePtr Then
hrFunction = headerFunction
hrData = data
bmx_curl_easy_setopt_ptr(easyHandlePtr, CURLOPT_HEADERFUNCTION, hrCallback)
bmx_curl_easy_setopt_obj(easyHandlePtr, CURLOPT_WRITEHEADER, Self)
End If
End Method
' +++++
Field hrData:Object
Field hrFunction:Int(buffer:Byte Ptr, size:Int, data:Object)
Function hrCallback:Int(buffer:Byte Ptr, size:Int, nmemb:Int, data:Object)
Return TCurlEasy(data).hrFunction(buffer, size * nmemb, TCurlEasy(data).hrData)
End Function
' +++++
Rem
bbdoc: Tells libcurl you want a multipart/formdata HTTP POST to be made, with the specified data in @formData.
about: Using POST with HTTP 1.1 implies the use of a "Expect: 100-continue" header. You can disable
this header with #CURLOPT_HTTPHEADER as usual.
See #TCurlFormData for details of specific data. The #TCurlFormData object MUST be available until
this curl object is cleaned up / deleted.
Calling this method automatically sets #CURLOPT_NOBODY to 0.
Use this instead of setopt #CURLOPT_HTTPPOST.
End Rem
Method httpPost(formData:TCurlFormData)
If easyHandlePtr Then
bmx_curl_easy_setopt_ptr(easyHandlePtr, CURLOPT_HTTPPOST, formData.httpPost.post);
End If
End Method
Rem
bbdoc: A list of HTTP headers to pass to the server in your HTTP request.
about: If you add a header that is otherwise generated and used by libcurl internally, your added one will be
used instead. If you add a header with no contents as in 'Accept:' (no data on the right side of the colon),
the internally used header will get disabled. Thus, using this option you can add new headers, replace
internal headers and remove internal headers. To add a header with no contents, make the contents be
two quotes: "" (~q~q). The headers included in the list must not be CRLF-terminated, because curl
adds CRLF after each header item. Failure to comply with this will result in strange bugs because the server
will most likely ignore part of the headers you specified.
The first line in a request (containing the method, usually a GET or POST) is not a header and cannot be
replaced using this option. Only the lines following the request-line are headers. Adding this method
line in this list of headers will only cause your request to send an invalid header.
Pass a Null to this to reset back to no custom headers.
The most commonly replaced headers have "shortcuts" in the options #CURLOPT_COOKIE, #CURLOPT_USERAGENT and
#CURLOPT_REFERER.
Use this instead of setopt #CURLOPT_HTTPHEADER.
After the call to #perform, you should call #freeLists to free up the internal structs.
End Rem
Method httpHeader(headers:String[])
If easyHandlePtr Then
If headers Then
processArray(CURLOPT_HTTPHEADER, headers)
Else
bmx_curl_easy_setopt_ptr(easyHandlePtr, CURLOPT_HTTPHEADER, Null)
End If
End If
End Method
Rem
bbdoc: A list of aliases to be treated as valid HTTP 200 responses.
about: Some servers respond with a custom header response line. For example, IceCast servers respond with
"ICY 200 OK". By including this string in your list of aliases, the response will be treated as a
valid HTTP header line such as "HTTP/1.0 200 OK".
The alias itself is not parsed for any version strings. The protocol is assumed to match HTTP 1.0 when an
alias matched.
Use this instead of setopt #CURLOPT_HTTP200ALIASES.
After the call to #perform, you should call #freeLists to free up the internal structs.
End Rem
Method http200Aliases(aliases:String[])
If easyHandlePtr Then
If aliases Then
processArray(CURLOPT_HTTP200ALIASES, aliases)
Else
bmx_curl_easy_setopt_ptr(easyHandlePtr, CURLOPT_HTTP200ALIASES, Null)
End If
End If
End Method
Rem
bbdoc: A list of FTP commands to pass to the server after the transfer type is set.
about: Disable this operation again by passing Null to this method.
Use this instead of setopt #CURLOPT_PREQUOTE.
After the call to #perform, you should call #freeLists to free up the internal structs.
End Rem
Method preQuote(commands:String[])
If easyHandlePtr Then
If commands Then
processArray(CURLOPT_PREQUOTE, commands)
Else
bmx_curl_easy_setopt_ptr(easyHandlePtr, CURLOPT_PREQUOTE, Null)
End If
End If
End Method
Rem
bbdoc: A list of FTP or SFTP commands to pass to the server prior to your ftp request.
about: This will be done before any other commands are issued (even before the CWD command for FTP).
Disable this operation again by passing Null to this method. The valid SFTP commands are: chgrp, chmod, chown,
ln, mkdir, pwd, rename, rm, rmdir, symlink.
Use this instead of setopt #CURLOPT_QUOTE.
After the call to #perform, you should call #freeLists to free up the internal structs.
End Rem
Method quote(commands:String[])
If easyHandlePtr Then
If commands Then
processArray(CURLOPT_QUOTE, commands)
Else
bmx_curl_easy_setopt_ptr(easyHandlePtr, CURLOPT_QUOTE, Null)
End If
End If
End Method
Rem
bbdoc: A list of FTP or SFTP commands to pass to the server after your ftp transfer request.
about: Disable this operation again by passing Null to this method.
Use this instead of setopt #CURLOPT_POSTQUOTE.
After the call to #perform, you should call #freeLists to free up the internal structs.
End Rem
Method postQuote(commands:String[])
If easyHandlePtr Then
If commands Then
processArray(CURLOPT_POSTQUOTE, commands)
Else
bmx_curl_easy_setopt_ptr(easyHandlePtr, CURLOPT_POSTQUOTE, Null)
End If
End If
End Method
Rem
bbdoc: A list of Telnet variables to pass to the telnet negotiations.
about: The variables should be in the format