Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2680388
HTTPSFuture.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
27 KB
Referenced Files
None
Subscribers
None
HTTPSFuture.php
View Options
<?php
/**
* Very basic HTTPS future.
*/
final
class
HTTPSFuture
extends
BaseHTTPFuture
{
private
static
$multi
;
private
static
$results
=
array
(
)
;
private
static
$pool
=
array
(
)
;
private
static
$globalCABundle
;
private
$handle
;
private
$profilerCallID
;
private
$cabundle
;
private
$followLocation
=
true
;
private
$responseBuffer
=
''
;
private
$responseBufferPos
;
private
$files
=
array
(
)
;
private
$temporaryFiles
=
array
(
)
;
private
$rawBody
;
private
$rawBodyPos
=
0
;
private
$fileHandle
;
private
$downloadPath
;
private
$downloadHandle
;
private
$parser
;
private
$progressSink
;
private
$curlOptions
=
array
(
)
;
/**
* Create a temp file containing an SSL cert, and use it for this session.
*
* This allows us to do host-specific SSL certificates in whatever client
* is using libphutil. e.g. in Arcanist, you could add an "ssl_cert" key
* to a specific host in ~/.arcrc and use that.
*
* cURL needs this to be a file, it doesn't seem to be able to handle a string
* which contains the cert. So we make a temporary file and store it there.
*
* @param string $certificate The multi-line, possibly lengthy, SSL
* certificate to use.
* @return $this
*/
public
function
setCABundleFromString
(
$certificate
)
{
$temp
=
new
TempFile
(
)
;
Filesystem
::
writeFile
(
$temp
,
$certificate
)
;
$this
->
cabundle
=
$temp
;
return
$this
;
}
/**
* Set the SSL certificate to use for this session, given a path.
*
* @param string $path The path to a valid SSL certificate for this session
* @return $this
*/
public
function
setCABundleFromPath
(
$path
)
{
$this
->
cabundle
=
$path
;
return
$this
;
}
/**
* Get the path to the SSL certificate for this session.
*
* @return string|null
*/
public
function
getCABundle
(
)
{
return
$this
->
cabundle
;
}
/**
* Set whether Location headers in the response will be respected.
* The default is true.
*
* @param boolean $follow true to follow any Location header present in the
* response, false to return the request directly
* @return $this
*/
public
function
setFollowLocation
(
$follow
)
{
$this
->
followLocation
=
$follow
;
return
$this
;
}
/**
* Get whether Location headers in the response will be respected.
*
* @return boolean
*/
public
function
getFollowLocation
(
)
{
return
$this
->
followLocation
;
}
/**
* Set the fallback CA certificate if one is not specified
* for the session, given a path.
*
* @param string $path The path to a valid SSL certificate
* @return void
*/
public
static
function
setGlobalCABundleFromPath
(
$path
)
{
self
::
$globalCABundle
=
$path
;
}
/**
* Set the fallback CA certificate if one is not specified
* for the session, given a string.
*
* @param string $certificate The certificate
* @return void
*/
public
static
function
setGlobalCABundleFromString
(
$certificate
)
{
$temp
=
new
TempFile
(
)
;
Filesystem
::
writeFile
(
$temp
,
$certificate
)
;
self
::
$globalCABundle
=
$temp
;
}
/**
* Get the fallback global CA certificate
*
* @return string
*/
public
static
function
getGlobalCABundle
(
)
{
return
self
::
$globalCABundle
;
}
/**
* Load contents of remote URI. Behaves pretty much like
* `@file_get_contents($uri)` but doesn't require `allow_url_fopen`.
*
* @param string $uri
* @param float $timeout (optional)
* @return string|false
*/
public
static
function
loadContent
(
$uri
,
$timeout
=
null
)
{
$future
=
new
self
(
$uri
)
;
if
(
$timeout
!==
null
)
{
$future
->
setTimeout
(
$timeout
)
;
}
try
{
list
(
$body
)
=
$future
->
resolvex
(
)
;
return
$body
;
}
catch
(
HTTPFutureResponseStatus
$ex
)
{
return
false
;
}
}
public
function
setDownloadPath
(
$download_path
)
{
$this
->
downloadPath
=
$download_path
;
if
(
Filesystem
::
pathExists
(
$download_path
)
)
{
throw
new
Exception
(
pht
(
'Specified download path "%s" already exists, refusing to '
.
'overwrite.'
,
$download_path
)
)
;
}
return
$this
;
}
public
function
setProgressSink
(
PhutilProgressSink
$progress_sink
)
{
$this
->
progressSink
=
$progress_sink
;
return
$this
;
}
public
function
getProgressSink
(
)
{
return
$this
->
progressSink
;
}
/**
* See T13533. This supports an install-specific Kerberos workflow.
*/
public
function
addCURLOption
(
$option_key
,
$option_value
)
{
if
(
!
is_scalar
(
$option_key
)
)
{
throw
new
Exception
(
pht
(
'Expected option key passed to "addCurlOption(<key>, ...)" to be '
.
'a scalar, got "%s".'
,
phutil_describe_type
(
$option_key
)
)
)
;
}
$this
->
curlOptions
[
]
=
array
(
$option_key
,
$option_value
)
;
return
$this
;
}
/**
* Attach a file to the request.
*
* @param string $key HTTP parameter name.
* @param string $data File content.
* @param string $name File name.
* @param string $mime_type File mime type.
* @return $this
*/
public
function
attachFileData
(
$key
,
$data
,
$name
,
$mime_type
)
{
if
(
isset
(
$this
->
files
[
$key
]
)
)
{
throw
new
Exception
(
pht
(
'%s currently supports only one file attachment for each '
.
'parameter name. You are trying to attach two different files with '
.
'the same parameter, "%s".'
,
__CLASS__
,
$key
)
)
;
}
$this
->
files
[
$key
]
=
array
(
'data'
=>
$data
,
'name'
=>
$name
,
'mime'
=>
$mime_type
,
)
;
return
$this
;
}
public
function
isReady
(
)
{
if
(
$this
->
hasResult
(
)
)
{
return
true
;
}
$uri
=
$this
->
getURI
(
)
;
$domain
=
id
(
new
PhutilURI
(
$uri
)
)
->
getDomain
(
)
;
$is_download
=
$this
->
isDownload
(
)
;
// See T13396. For now, use the streaming response parser only if we're
// downloading the response to disk.
$use_streaming_parser
=
(bool)
$is_download
;
if
(
!
$this
->
handle
)
{
$uri_object
=
new
PhutilURI
(
$uri
)
;
$proxy
=
PhutilHTTPEngineExtension
::
buildHTTPProxyURI
(
$uri_object
)
;
// TODO: Currently, the "proxy" is not passed to the ServiceProfiler
// because of changes to how ServiceProfiler is integrated. It would
// be nice to pass it again.
if
(
!
self
::
$multi
)
{
self
::
$multi
=
curl_multi_init
(
)
;
if
(
!
self
::
$multi
)
{
throw
new
Exception
(
pht
(
'%s failed!'
,
'curl_multi_init()'
)
)
;
}
}
if
(
!
empty
(
self
::
$pool
[
$domain
]
)
)
{
$curl
=
array_pop
(
self
::
$pool
[
$domain
]
)
;
}
else
{
$curl
=
curl_init
(
)
;
if
(
!
$curl
)
{
throw
new
Exception
(
pht
(
'%s failed!'
,
'curl_init()'
)
)
;
}
}
$this
->
handle
=
$curl
;
curl_multi_add_handle
(
self
::
$multi
,
$curl
)
;
curl_setopt
(
$curl
,
CURLOPT_URL
,
$uri
)
;
if
(
defined
(
'CURLOPT_PROTOCOLS'
)
)
{
// cURL supports a lot of protocols, and by default it will honor
// redirects across protocols (for instance, from HTTP to POP3). Beyond
// being very silly, this also has security implications:
//
// http://blog.volema.com/curl-rce.html
//
// Disable all protocols other than HTTP and HTTPS.
$allowed_protocols
=
CURLPROTO_HTTPS
|
CURLPROTO_HTTP
;
curl_setopt
(
$curl
,
CURLOPT_PROTOCOLS
,
$allowed_protocols
)
;
curl_setopt
(
$curl
,
CURLOPT_REDIR_PROTOCOLS
,
$allowed_protocols
)
;
}
if
(
$this
->
rawBody
!==
null
)
{
if
(
$this
->
getData
(
)
)
{
throw
new
Exception
(
pht
(
'You can not execute an HTTP future with both a raw request '
.
'body and structured request data.'
)
)
;
}
// We aren't actually going to use this file handle, since we are
// just pushing data through the callback, but cURL gets upset if
// we don't hand it a real file handle.
$tmp
=
new
TempFile
(
)
;
$this
->
fileHandle
=
fopen
(
$tmp
,
'r'
)
;
// NOTE: We must set CURLOPT_PUT here to make cURL use CURLOPT_INFILE.
// We'll possibly overwrite the method later on, unless this is really
// a PUT request.
curl_setopt
(
$curl
,
CURLOPT_PUT
,
true
)
;
curl_setopt
(
$curl
,
CURLOPT_INFILE
,
$this
->
fileHandle
)
;
curl_setopt
(
$curl
,
CURLOPT_INFILESIZE
,
strlen
(
$this
->
rawBody
)
)
;
curl_setopt
(
$curl
,
CURLOPT_READFUNCTION
,
array
(
$this
,
'willWriteBody'
)
)
;
}
else
{
$data
=
$this
->
formatRequestDataForCURL
(
)
;
curl_setopt
(
$curl
,
CURLOPT_POSTFIELDS
,
$data
)
;
}
$headers
=
$this
->
getHeaders
(
)
;
$saw_expect
=
false
;
$saw_accept
=
false
;
for
(
$ii
=
0
;
$ii
<
count
(
$headers
)
;
$ii
++
)
{
list
(
$name
,
$value
)
=
$headers
[
$ii
]
;
$headers
[
$ii
]
=
$name
.
': '
.
$value
;
if
(
!
strcasecmp
(
$name
,
'Expect'
)
)
{
$saw_expect
=
true
;
}
if
(
!
strcasecmp
(
$name
,
'Accept-Encoding'
)
)
{
$saw_accept
=
true
;
}
}
if
(
!
$saw_expect
)
{
// cURL sends an "Expect" header by default for certain requests. While
// there is some reasoning behind this, it causes a practical problem
// in that lighttpd servers reject these requests with a 417. Both sides
// are locked in an eternal struggle (lighttpd has introduced a
// 'server.reject-expect-100-with-417' option to deal with this case).
//
// The ostensibly correct way to suppress this behavior on the cURL side
// is to add an empty "Expect:" header. If we haven't seen some other
// explicit "Expect:" header, do so.
//
// See here, for example, although this issue is fairly widespread:
// http://curl.haxx.se/mail/archive-2009-07/0008.html
$headers
[
]
=
'Expect:'
;
}
if
(
!
$saw_accept
)
{
if
(
!
$use_streaming_parser
)
{
if
(
$this
->
canAcceptGzip
(
)
)
{
$headers
[
]
=
'Accept-Encoding: gzip'
;
}
}
}
curl_setopt
(
$curl
,
CURLOPT_HTTPHEADER
,
$headers
)
;
// Set the requested HTTP method, e.g. GET / POST / PUT.
curl_setopt
(
$curl
,
CURLOPT_CUSTOMREQUEST
,
$this
->
getMethod
(
)
)
;
// Make sure we get the headers and data back.
curl_setopt
(
$curl
,
CURLOPT_HEADER
,
true
)
;
curl_setopt
(
$curl
,
CURLOPT_WRITEFUNCTION
,
array
(
$this
,
'didReceiveDataCallback'
)
)
;
if
(
$this
->
followLocation
)
{
curl_setopt
(
$curl
,
CURLOPT_FOLLOWLOCATION
,
true
)
;
curl_setopt
(
$curl
,
CURLOPT_MAXREDIRS
,
20
)
;
}
if
(
defined
(
'CURLOPT_TIMEOUT_MS'
)
)
{
// If CURLOPT_TIMEOUT_MS is available, use the higher-precision timeout.
$timeout
=
max
(
1
,
ceil
(
1000
*
$this
->
getTimeout
(
)
)
)
;
curl_setopt
(
$curl
,
CURLOPT_TIMEOUT_MS
,
$timeout
)
;
}
else
{
// Otherwise, fall back to the lower-precision timeout.
$timeout
=
max
(
1
,
ceil
(
$this
->
getTimeout
(
)
)
)
;
curl_setopt
(
$curl
,
CURLOPT_TIMEOUT
,
$timeout
)
;
}
// We're going to try to set CAINFO below. This doesn't work at all on
// OSX around Yosemite (see T5913). On these systems, we'll use the
// system CA and then try to tell the user that their settings were
// ignored and how to fix things if we encounter a CA-related error.
// Assume we have custom CA settings to start with; we'll clear this
// flag if we read the default CA info below.
// Try some decent fallbacks here:
// - First, check if a bundle is set explicitly for this request, via
// `setCABundle()` or similar.
// - Then, check if a global bundle is set explicitly for all requests,
// via `setGlobalCABundle()` or similar.
// - Then, if a local custom.pem exists, use that, because it probably
// means that the user wants to override everything (also because the
// user might not have access to change the box's php.ini to add
// curl.cainfo).
// - Otherwise, try using curl.cainfo. If it's set explicitly, it's
// probably reasonable to try using it before we fall back to what
// libphutil ships with.
// - Lastly, try the default that libphutil ships with. If it doesn't
// work, give up and yell at the user.
if
(
!
$this
->
getCABundle
(
)
)
{
$caroot
=
dirname
(
phutil_get_library_root
(
'arcanist'
)
)
;
$caroot
=
$caroot
.
'/resources/ssl/'
;
$ini_val
=
ini_get
(
'curl.cainfo'
)
;
if
(
self
::
getGlobalCABundle
(
)
)
{
$this
->
setCABundleFromPath
(
self
::
getGlobalCABundle
(
)
)
;
}
else
if
(
Filesystem
::
pathExists
(
$caroot
.
'custom.pem'
)
)
{
$this
->
setCABundleFromPath
(
$caroot
.
'custom.pem'
)
;
}
else
if
(
$ini_val
)
{
// TODO: We can probably do a pathExists() here, even.
$this
->
setCABundleFromPath
(
$ini_val
)
;
}
else
{
$this
->
setCABundleFromPath
(
$caroot
.
'default.pem'
)
;
}
}
if
(
$this
->
canSetCAInfo
(
)
)
{
curl_setopt
(
$curl
,
CURLOPT_CAINFO
,
$this
->
getCABundle
(
)
)
;
}
$verify_peer
=
1
;
$verify_host
=
2
;
$extensions
=
PhutilHTTPEngineExtension
::
getAllExtensions
(
)
;
foreach
(
$extensions
as
$extension
)
{
if
(
$extension
->
shouldTrustAnySSLAuthorityForURI
(
$uri_object
)
)
{
$verify_peer
=
0
;
}
if
(
$extension
->
shouldTrustAnySSLHostnameForURI
(
$uri_object
)
)
{
$verify_host
=
0
;
}
}
curl_setopt
(
$curl
,
CURLOPT_SSL_VERIFYPEER
,
$verify_peer
)
;
curl_setopt
(
$curl
,
CURLOPT_SSL_VERIFYHOST
,
$verify_host
)
;
curl_setopt
(
$curl
,
CURLOPT_SSLVERSION
,
0
)
;
// See T13391. Recent versions of cURL default to "HTTP/2" on some
// connections, but do not support HTTP/2 proxies. Until HTTP/2
// stabilizes, force HTTP/1.1 explicitly.
curl_setopt
(
$curl
,
CURLOPT_HTTP_VERSION
,
CURL_HTTP_VERSION_1_1
)
;
if
(
$proxy
)
{
curl_setopt
(
$curl
,
CURLOPT_PROXY
,
(string)
$proxy
)
;
}
foreach
(
$this
->
curlOptions
as
$curl_option
)
{
list
(
$curl_key
,
$curl_value
)
=
$curl_option
;
try
{
$ok
=
curl_setopt
(
$curl
,
$curl_key
,
$curl_value
)
;
if
(
!
$ok
)
{
throw
new
Exception
(
pht
(
'Call to "curl_setopt(...)" returned "false".'
)
)
;
}
}
catch
(
Exception
$ex
)
{
throw
new
PhutilProxyException
(
pht
(
'Call to "curl_setopt(...) failed for option key "%s".'
,
$curl_key
)
,
$ex
)
;
}
}
if
(
$is_download
)
{
$this
->
downloadHandle
=
@
fopen
(
$this
->
downloadPath
,
'wb+'
)
;
if
(
!
$this
->
downloadHandle
)
{
throw
new
Exception
(
pht
(
'Failed to open filesystem path "%s" for writing.'
,
$this
->
downloadPath
)
)
;
}
}
if
(
$use_streaming_parser
)
{
$streaming_parser
=
id
(
new
PhutilHTTPResponseParser
(
)
)
->
setFollowLocationHeaders
(
$this
->
getFollowLocation
(
)
)
;
if
(
$this
->
downloadHandle
)
{
$streaming_parser
->
setWriteHandle
(
$this
->
downloadHandle
)
;
}
$progress_sink
=
$this
->
getProgressSink
(
)
;
if
(
$progress_sink
)
{
$streaming_parser
->
setProgressSink
(
$progress_sink
)
;
}
$this
->
parser
=
$streaming_parser
;
}
}
else
{
$curl
=
$this
->
handle
;
if
(
!
self
::
$results
)
{
// NOTE: In curl_multi_select(), PHP calls curl_multi_fdset() but does
// not check the return value of &maxfd for -1 until recent versions
// of PHP (5.4.8 and newer). cURL may return -1 as maxfd in some unusual
// situations; if it does, PHP enters select() with nfds=0, which blocks
// until the timeout is reached.
//
// We could try to guess whether this will happen or not by examining
// the version identifier, but we can also just sleep for only a short
// period of time.
curl_multi_select
(
self
::
$multi
,
0.01
)
;
}
}
do
{
$active
=
null
;
$result
=
curl_multi_exec
(
self
::
$multi
,
$active
)
;
}
while
(
$result
==
CURLM_CALL_MULTI_PERFORM
)
;
while
(
$info
=
curl_multi_info_read
(
self
::
$multi
)
)
{
if
(
$info
[
'msg'
]
==
CURLMSG_DONE
)
{
self
::
$results
[
(int)
$info
[
'handle'
]
]
=
$info
;
}
}
if
(
!
array_key_exists
(
(int)
$curl
,
self
::
$results
)
)
{
return
false
;
}
// The request is complete, so release any temporary files we wrote
// earlier.
$this
->
temporaryFiles
=
array
(
)
;
$info
=
self
::
$results
[
(int)
$curl
]
;
$result
=
$this
->
responseBuffer
;
$err_code
=
$info
[
'result'
]
;
if
(
$err_code
)
{
if
(
(
$err_code
==
CURLE_SSL_CACERT
)
&&
!
$this
->
canSetCAInfo
(
)
)
{
$status
=
new
HTTPFutureCertificateResponseStatus
(
HTTPFutureCertificateResponseStatus
::
ERROR_IMMUTABLE_CERTIFICATES
,
$uri
)
;
}
else
{
$status
=
new
HTTPFutureCURLResponseStatus
(
$err_code
,
$uri
)
;
}
$body
=
null
;
$headers
=
array
(
)
;
$this
->
setResult
(
array
(
$status
,
$body
,
$headers
)
)
;
}
else
if
(
$this
->
parser
)
{
$streaming_parser
=
$this
->
parser
;
try
{
$responses
=
$streaming_parser
->
getResponses
(
)
;
$final_response
=
last
(
$responses
)
;
$result
=
array
(
$final_response
->
getStatus
(
)
,
$final_response
->
getBody
(
)
,
$final_response
->
getHeaders
(
)
,
)
;
}
catch
(
HTTPFutureParseResponseStatus
$ex
)
{
$result
=
array
(
$ex
,
null
,
array
(
)
)
;
}
$this
->
setResult
(
$result
)
;
}
else
{
// cURL returns headers of all redirects, we strip all but the final one.
$redirects
=
curl_getinfo
(
$curl
,
CURLINFO_REDIRECT_COUNT
)
;
$result
=
preg_replace
(
'/^(.*\r\n\r\n){'
.
$redirects
.
'}/sU'
,
''
,
$result
)
;
$this
->
setResult
(
$this
->
parseRawHTTPResponse
(
$result
)
)
;
}
curl_multi_remove_handle
(
self
::
$multi
,
$curl
)
;
unset
(
self
::
$results
[
(int)
$curl
]
)
;
// NOTE: We want to use keepalive if possible. Return the handle to a
// pool for the domain; don't close it.
if
(
$this
->
shouldReuseHandles
(
)
)
{
self
::
$pool
[
$domain
]
[
]
=
$curl
;
}
if
(
$is_download
)
{
if
(
$this
->
downloadHandle
)
{
fflush
(
$this
->
downloadHandle
)
;
fclose
(
$this
->
downloadHandle
)
;
$this
->
downloadHandle
=
null
;
}
}
$sink
=
$this
->
getProgressSink
(
)
;
if
(
$sink
)
{
$status
=
head
(
$this
->
getResult
(
)
)
;
if
(
$status
->
isError
(
)
)
{
$sink
->
didFailWork
(
)
;
}
else
{
$sink
->
didCompleteWork
(
)
;
}
}
return
true
;
}
/**
* Callback invoked by cURL as it reads HTTP data from the response. We save
* the data to a buffer.
*/
public
function
didReceiveDataCallback
(
$handle
,
$data
)
{
if
(
$this
->
parser
)
{
$this
->
parser
->
readBytes
(
$data
)
;
}
else
{
$this
->
responseBuffer
.=
$data
;
}
return
strlen
(
$data
)
;
}
/**
* Read data from the response buffer.
*
* NOTE: Like @{class:ExecFuture}, this method advances a read cursor but
* does not discard the data. The data will still be buffered, and it will
* all be returned when the future resolves. To discard the data after
* reading it, call @{method:discardBuffers}.
*
* @return string Response data, if available.
*/
public
function
read
(
)
{
if
(
$this
->
isDownload
(
)
)
{
throw
new
Exception
(
pht
(
'You can not read the result buffer while streaming results '
.
'to disk: there is no in-memory buffer to read.'
)
)
;
}
if
(
$this
->
parser
)
{
throw
new
Exception
(
pht
(
'Streaming reads are not currently supported by the streaming '
.
'parser.'
)
)
;
}
$result
=
substr
(
$this
->
responseBuffer
,
$this
->
responseBufferPos
)
;
$this
->
responseBufferPos
=
strlen
(
$this
->
responseBuffer
)
;
return
$result
;
}
/**
* Discard any buffered data. Normally, you call this after reading the
* data with @{method:read}.
*
* @return $this
*/
public
function
discardBuffers
(
)
{
if
(
$this
->
isDownload
(
)
)
{
throw
new
Exception
(
pht
(
'You can not discard the result buffer while streaming results '
.
'to disk: there is no in-memory buffer to discard.'
)
)
;
}
if
(
$this
->
parser
)
{
throw
new
Exception
(
pht
(
'Buffer discards are not currently supported by the streaming '
.
'parser.'
)
)
;
}
$this
->
responseBuffer
=
''
;
$this
->
responseBufferPos
=
0
;
return
$this
;
}
/**
* Produces a value safe to pass to `CURLOPT_POSTFIELDS`.
*
* @return wild Some value, suitable for use in `CURLOPT_POSTFIELDS`.
*/
private
function
formatRequestDataForCURL
(
)
{
// We're generating a value to hand to cURL as CURLOPT_POSTFIELDS. The way
// cURL handles this value has some tricky caveats.
// First, we can return either an array or a query string. If we return
// an array, we get a "multipart/form-data" request. If we return a
// query string, we get an "application/x-www-form-urlencoded" request.
// Second, if we return an array we can't duplicate keys. The user might
// want to send the same parameter multiple times.
// Third, if we return an array and any of the values start with "@",
// cURL includes arbitrary files off disk and sends them to an untrusted
// remote server. For example, an array like:
//
// array('name' => '@/usr/local/secret')
//
// ...will attempt to read that file off disk and transmit its contents with
// the request. This behavior is pretty surprising, and it can easily
// become a relatively severe security vulnerability which allows an
// attacker to read any file the HTTP process has access to. Since this
// feature is very dangerous and not particularly useful, we prevent its
// use. Broadly, this means we must reject some requests because they
// contain an "@" in an inconvenient place.
// Generally, to avoid the "@" case and because most servers usually
// expect "application/x-www-form-urlencoded" data, we try to return a
// string unless there are files attached to this request.
$data
=
$this
->
getData
(
)
;
$files
=
$this
->
files
;
$any_data
=
(
$data
||
(
is_string
(
$data
)
&&
strlen
(
$data
)
)
)
;
$any_files
=
(bool)
$this
->
files
;
if
(
!
$any_data
&&
!
$any_files
)
{
// No files or data, so just bail.
return
null
;
}
if
(
!
$any_files
)
{
// If we don't have any files, just encode the data as a query string,
// make sure it's not including any files, and we're good to go.
if
(
is_array
(
$data
)
)
{
$data
=
phutil_build_http_querystring
(
$data
)
;
}
$this
->
checkForDangerousCURLMagic
(
$data
,
$is_query_string
=
true
)
;
return
$data
;
}
// If we've made it this far, we have some files, so we need to return
// an array. First, convert the other data into an array if it isn't one
// already.
if
(
is_string
(
$data
)
)
{
// NOTE: We explicitly don't want fancy array parsing here, so just
// do a basic parse and then convert it into a dictionary ourselves.
$parser
=
new
PhutilQueryStringParser
(
)
;
$pairs
=
$parser
->
parseQueryStringToPairList
(
$data
)
;
$map
=
array
(
)
;
foreach
(
$pairs
as
$pair
)
{
list
(
$key
,
$value
)
=
$pair
;
if
(
array_key_exists
(
$key
,
$map
)
)
{
throw
new
Exception
(
pht
(
'Request specifies two values for key "%s", but parameter '
.
'names must be unique if you are posting file data due to '
.
'limitations with cURL.'
,
$key
)
)
;
}
$map
[
$key
]
=
$value
;
}
$data
=
$map
;
}
foreach
(
$data
as
$key
=>
$value
)
{
$this
->
checkForDangerousCURLMagic
(
$value
,
$is_query_string
=
false
)
;
}
foreach
(
$this
->
files
as
$name
=>
$info
)
{
if
(
array_key_exists
(
$name
,
$data
)
)
{
throw
new
Exception
(
pht
(
'Request specifies a file with key "%s", but that key is also '
.
'defined by normal request data. Due to limitations with cURL, '
.
'requests that post file data must use unique keys.'
,
$name
)
)
;
}
$tmp
=
new
TempFile
(
$info
[
'name'
]
)
;
Filesystem
::
writeFile
(
$tmp
,
$info
[
'data'
]
)
;
$this
->
temporaryFiles
[
]
=
$tmp
;
// In 5.5.0 and later, we can use CURLFile. Prior to that, we have to
// use this "@" stuff.
if
(
class_exists
(
'CURLFile'
,
false
)
)
{
$file_value
=
new
CURLFile
(
(string)
$tmp
,
$info
[
'mime'
]
,
$info
[
'name'
]
)
;
}
else
{
$file_value
=
'@'
.
(string)
$tmp
;
}
$data
[
$name
]
=
$file_value
;
}
return
$data
;
}
/**
* Detect strings which will cause cURL to do horrible, insecure things.
*
* @param string $string Possibly dangerous string.
* @param bool $is_query_string True if this string is being used as part
* of a query string.
* @return void
*/
private
function
checkForDangerousCURLMagic
(
$string
,
$is_query_string
)
{
if
(
empty
(
$string
[
0
]
)
||
(
$string
[
0
]
!=
'@'
)
)
{
// This isn't an "@..." string, so it's fine.
return
;
}
if
(
$is_query_string
)
{
if
(
version_compare
(
phpversion
(
)
,
'5.2.0'
,
'<'
)
)
{
throw
new
Exception
(
pht
(
'Attempting to make an HTTP request, but query string data begins '
.
'with "%s". Prior to PHP 5.2.0 this reads files off disk, which '
.
'creates a wide attack window for security vulnerabilities. '
.
'Upgrade PHP or avoid making cURL requests which begin with "%s".'
,
'@'
,
'@'
)
)
;
}
// This is safe if we're on PHP 5.2.0 or newer.
return
;
}
throw
new
Exception
(
pht
(
'Attempting to make an HTTP request which includes file data, but the '
.
'value of a query parameter begins with "%s". PHP interprets these '
.
'values to mean that it should read arbitrary files off disk and '
.
'transmit them to remote servers. Declining to make this request.'
,
'@'
)
)
;
}
/**
* Determine whether CURLOPT_CAINFO is usable on this system.
*/
private
function
canSetCAInfo
(
)
{
// We cannot set CAInfo on OSX after Yosemite.
$osx_version
=
PhutilExecutionEnvironment
::
getOSXVersion
(
)
;
if
(
$osx_version
)
{
if
(
version_compare
(
$osx_version
,
14
,
'>='
)
)
{
return
false
;
}
}
return
true
;
}
/**
* Write a raw HTTP body into the request.
*
* You must write the entire body before starting the request.
*
* @param string $raw_body Raw body.
* @return $this
*/
public
function
write
(
$raw_body
)
{
$this
->
rawBody
=
$raw_body
;
return
$this
;
}
/**
* Callback to pass data to cURL.
*/
public
function
willWriteBody
(
$handle
,
$infile
,
$len
)
{
$bytes
=
substr
(
$this
->
rawBody
,
$this
->
rawBodyPos
,
$len
)
;
$this
->
rawBodyPos
+=
$len
;
return
$bytes
;
}
private
function
shouldReuseHandles
(
)
{
$curl_version
=
curl_version
(
)
;
$version
=
idx
(
$curl_version
,
'version'
)
;
// NOTE: cURL 7.43.0 has a bug where the POST body length is not recomputed
// properly when a handle is reused. For this version of cURL, disable
// handle reuse and accept a small performance penalty. See T8654.
if
(
$version
==
'7.43.0'
)
{
return
false
;
}
return
true
;
}
private
function
isDownload
(
)
{
return
(
$this
->
downloadPath
!==
null
)
;
}
protected
function
getServiceProfilerStartParameters
(
)
{
return
array
(
'type'
=>
'http'
,
'uri'
=>
phutil_string_cast
(
$this
->
getURI
(
)
)
,
)
;
}
private
function
canAcceptGzip
(
)
{
return
function_exists
(
'gzdecode'
)
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Thu, Dec 19, 17:00 (18 h, 57 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1014850
Default Alt Text
HTTPSFuture.php (27 KB)
Attached To
Mode
rARC Arcanist
Attached
Detach File
Event Timeline
Log In to Comment