Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cURL: make possible to send file from buffer string #6456

Closed
wants to merge 1 commit into from
Closed

cURL: make possible to send file from buffer string #6456

wants to merge 1 commit into from

Conversation

Irker
Copy link
Contributor

@Irker Irker commented Nov 26, 2020

In my practice, very often I needed to send a file that was created in the current script and stored in memory. Most often these were small generated images, PDF documents, XML files, and etc.

At the current moment in PHP we can send a file from a string variable only through a temporary file on a file system(CURLFile) or with unnecessary base64 converting:

$file_stream = 'data://application/octet-stream;base64,' . base64_encode($file);
$curlFile = new \CURLFile($path);
$curlFile->setPostFilename($file_name);

However, libcurl itself has an easy way to send a file from a string.

Since sending a file in this way always requires passing the file name, it will not work to embed this functionality into an existing CurlFile class without losing backward compatibility. Therefore, I decided to create a new class CURLStringFile.

Usage:

$curlFile = new CURLStringFile($file_content, $postname, $mime);

ext/curl/curl.stub.php Outdated Show resolved Hide resolved
ext/curl/curl_string_file.stub.php Outdated Show resolved Hide resolved
Copy link
Member

@nikic nikic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay! This looks like a nice addition to me. I have one safety concern regarding the CURLFORM_BUFFERPTR use. And a more general design concern on whether we need all those methods on CURLStringFile at all, and can't just use typed properties.

Maybe @cmb69 or @kocsismate have thoughts on this addition as well?

ext/curl/curl.stub.php Outdated Show resolved Hide resolved
ext/curl/interface.c Outdated Show resolved Hide resolved
CURLFORM_BUFFER, filename,
CURLFORM_CONTENTTYPE, type ? type : "application/octet-stream",
CURLFORM_BUFFERPTR, ZSTR_VAL(postval),
CURLFORM_BUFFERLENGTH, ZSTR_LEN(postval),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is safe. According to curl docs:

is used in combination with CURLFORM_BUFFER. The parameter is a pointer to the buffer to be uploaded. This buffer must not be freed until after curl_easy_cleanup is called. You must also use CURLFORM_BUFFERLENGTH to set the number of bytes in the buffer.

This means that curl is not making a copy of the buffer. If the CURLStringFile object is destroyed before the request is sent, this will probably result in use-after-free.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @nikic! Great thanks for the code review!
This issue scares me because looks like I need to copy the data somewhere inside CurlHandle. And also copy them when cloning, and destroy them in the curl destructor. Most likely I will not have enough knowledge in the C language.
I can suggest adding CurlStringFile only for curl >= 7.56.0. Or someone will help me with this issue.
Right now, I have code changes based on all your comments, except this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind. I tried to do it myself. =)

ext/curl/interface.c Outdated Show resolved Hide resolved
--FILE--
<?php

function testcurl($ch, $postname, $data, $mime = '')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should $mime = null and pass it unconditionally rather than checking empty

ext/opcache/Optimizer/zend_func_info.c Outdated Show resolved Hide resolved
public function getData() {}

/** @return string */
public function getPostFilename() {}
Copy link
Member

@nikic nikic Jan 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CURLFile also has a setPostFilename method.

Though, taking a step back here, seeing as the class has public properties, these getters and setters are really redundant. I think we'd be better off declaring them as typed properties and just omitting the methods entirely.

It depends on whether we need to have the same interface as CURLFile. It doesn't seem necessary to me, as I would expect the common usage to just construct CURLStringFile and directly pass it to curl.

string(%d) "foo.txt"
string(%d) "62942c05ed0d1b501c4afe6dc1c4db1b"
string(%d) "foo.txt|text/plain|62942c05ed0d1b501c4afe6dc1c4db1b"
string(%d) "foo.txt|text/plain|62942c05ed0d1b501c4afe6dc1c4db1b"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: missing newline

@Irker
Copy link
Contributor Author

Irker commented Jan 30, 2021

@nikic @cmb69
Thanks again for the code review.
I updated the patch in which I tried to accommodate all your suggestions.
However, I'm not sure about some of my solutions:

  1. I made a mime parameter with null as default like in CurlFile. Null is then replaced with "application/octet-stream". But we can make this value default right in the class instead of null.
  2. I'm really not sure if I did everything right with ch-> to_free-> buffers
  3. I accidentally discovered a bug that CurlFile has. If an object property zval is converted to reference ($postname =& $file->postname), then this file property will not pass validation.
    I added checks like this:
if (Z_TYPE_P (prop) == IS_REFERENCE) {
    prop = Z_REFVAL_P (prop);
}

Not sure if this is correct.

php-pulls pushed a commit that referenced this pull request Feb 2, 2021
Z_PARAM_STR_OR_NULL(mime)
ZEND_PARSE_PARAMETERS_END();

zend_update_property_stringl(curl_CURLStringFile_class, Z_OBJ_P(object), "data", sizeof("data")-1, ZSTR_VAL(data), ZSTR_LEN(data));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will perform a copy of the data. You can use zend_update_property_str to avoid the copy.

ZEND_PARSE_PARAMETERS_END();

zend_update_property_stringl(curl_CURLStringFile_class, Z_OBJ_P(object), "data", sizeof("data")-1, ZSTR_VAL(data), ZSTR_LEN(data));
zend_update_property_string(curl_CURLStringFile_class, Z_OBJ_P(object), "postname", sizeof("postname")-1, ZSTR_VAL(postname));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here -- unless this is intentionally written this way to truncate the filename at a null byte?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it for mime and postname the same way as for CURLFile. I think it required to be truncated.
But what the best way? Truncate or, for example, throw an exception if we found null-byte, or do something else?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's okay to stick with the CURLFile behavior here.

class CURLStringFile
{
public string $data = "";
public string $postname = "";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can leave off the default values on these properties (and on $mime as well if you assign it unconditionally).


if (Z_TYPE_P(prop) == IS_REFERENCE) {
prop = Z_REFVAL_P(prop);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ZVAL_DEREF(prop) is simpler :)

@nikic
Copy link
Member

nikic commented Feb 2, 2021

@nikic @cmb69
Thanks again for the code review.
I updated the patch in which I tried to accommodate all your suggestions.
However, I'm not sure about some of my solutions:

1. I made a mime parameter with null as default like in CurlFile. Null is then replaced with "application/octet-stream". But we can make this value default right in the class instead of null.

Directly using "application/octect-stream" as the default makes sense to me.

2. I'm really not sure if I did everything right with `ch-> to_free-> buffers`

I think you did everything right!

3. I accidentally discovered a bug that CurlFile has. If an object property zval is converted to reference (`$postname =& $file->postname`), then this file property will not pass validation.
   I added checks like this:
if (Z_TYPE_P (prop) == IS_REFERENCE) {
    prop = Z_REFVAL_P (prop);
}

Not sure if this is correct.

Nice catch! I've backported this fix to the 7.4 branch in 54fa0a6, to make sure older versions get fixed.

public string $postname = "";
public ?string $mime = null;

public function __construct(string $data, string $postname, ?string $mime = null) {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not directly related: @kocsismate I just noticed that

public function __construct(string $filename, ?string $mime_type = null, ?string $posted_filename = null) {}
uses different names in the constructor from the property names. We actually managed to have three different names between $postname, $posted_filename and getPostFilename()...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhh 🤦

@Irker
Copy link
Contributor Author

Irker commented Feb 3, 2021

@nikic Thanks for the review!
I tried to accommodate all the notes and updated the patch again.
There is only a question with null-byte truncation in postname and mime

Copy link
Member

@nikic nikic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks fine to me. Anyone else have feedback?

Personally I'd probably merge curl_string_file.c into curl_file.c, as the final implementation ended up being rather small.

ZEND_PARSE_PARAMETERS_END();

zend_update_property_stringl(curl_CURLStringFile_class, Z_OBJ_P(object), "data", sizeof("data")-1, ZSTR_VAL(data), ZSTR_LEN(data));
zend_update_property_string(curl_CURLStringFile_class, Z_OBJ_P(object), "postname", sizeof("postname")-1, ZSTR_VAL(postname));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's okay to stick with the CURLFile behavior here.

@Irker
Copy link
Contributor Author

Irker commented Feb 11, 2021

This looks fine to me. Anyone else have feedback?

Personally I'd probably merge curl_string_file.c into curl_file.c, as the final implementation ended up being rather small.

Thank you! I merged files.

<?php

/**
* @generate-function-entries
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can now get rid of @generate-function-entries

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thank you.


prop = zend_read_property(curl_CURLStringFile_class, Z_OBJ_P(current), "postname", sizeof("postname")-1, 0, &rv);
ZVAL_DEREF(prop);
if (Z_TYPE_P(prop) != IS_STRING) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As CURLStringFile::$postname is a property of type string, is this condition true only if the property is uninitialized, right? If this is the case, shouldn't we throw an exception that we do in other cases? Or am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, no need to call php_error_docref here. The exception is already being thrown inside zend_read_property. But I'm do not know how to process it correctly. Is it correct to do just this? :

prop = zend_read_property(curl_CURLStringFile_class, Z_OBJ_P(current), "postname", sizeof("postname")-1, 0, &rv);
ZVAL_DEREF(prop);
if (Z_TYPE_P(prop) != IS_STRING) {
	zend_string_release_ex(string_key, 0);
	return FAILURE;
}

I cannot find a good example in other code =/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be fine, or possibly something like this?

prop = ...;
if (EG(exception)) {
    zend_string_release_ex(string_key, 0);
    return FAILURE;
}
ZVAL_DEREF(prop);
ZEND_ASSERT(Z_TYPE_P(prop) == IS_STRING);

ZEND_PARSE_PARAMETERS_END();

zend_update_property_str(curl_CURLStringFile_class, Z_OBJ_P(object), "data", sizeof("data") - 1, data);
zend_update_property_string(curl_CURLStringFile_class, Z_OBJ_P(object), "postname", sizeof("postname")-1, ZSTR_VAL(postname));
Copy link
Member

@kocsismate kocsismate Feb 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any specific reason you use both zend_update_property_string() and zend_update_property_str(), even though the latter would be sufficient (and wouldn't require a ZSTR_VAL() call)?

Copy link
Contributor Author

@Irker Irker Feb 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned before we need to truncate mime and postname to null-terminated strings. But looks like it will be truncated during reading their values. So we can use zend_update_property_str for all parameters.
But I do not know how to provide a default value for mime property for method zend_update_property_str. I can use "zend_string_init" method, but at least I do not know which value for its third option I must use.
Right now I know only this way:

	if (mime) {
		zend_update_property_str(curl_CURLStringFile_class, Z_OBJ_P(object), "mime", sizeof("mime")-1, mime);
	} else {
		zend_update_property_string(curl_CURLStringFile_class, Z_OBJ_P(object), "mime", sizeof("mime")-1, "application/octet-stream");
	}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You suggested code looks reasonable to me.

For zend_string_init the last parameter should be 0, but in this case it's probably a bit nicer to write it as you did.

@Irker
Copy link
Contributor Author

Irker commented Feb 15, 2021

@kocsismate @nikic, Thanks a lot for the review! I updated the PR again =)

@php-pulls php-pulls closed this in e727919 Feb 16, 2021
@Irker Irker deleted the curl_string_file branch February 16, 2021 09:51
jrfnl added a commit to PHPCompatibility/PHPCompatibility that referenced this pull request Mar 9, 2022
> Added `CURLStringFile`, which can be used to post a file from a string rather
> than a file:
> ```php
> $file = new CURLStringFile($data, 'filename.txt', 'text/plain');
> curl_setopt($curl, CURLOPT_POSTFIELDS, ['file' => $file]);
> ```

Includes unit test.

Refs:
* https://github.com/php/php-src/blob/f67986a9218f4889d9352a87c29337a5b6eaa4bd/UPGRADING#L231-L235
* php/php-src#6456
* php/php-src@e727919
jrfnl added a commit to jrfnl/doc-en that referenced this pull request Mar 11, 2022
> Added `CURLStringFile`, which can be used to post a file from a string rather than a file

Refs:
* https://github.com/php/php-src/blob/f67986a9218f4889d9352a87c29337a5b6eaa4bd/UPGRADING#L231-L235
* php/php-src#6456
* php/php-src@e727919

As part of the Fibers RFC/PR, the Reflection extension also received a new class.

* https://wiki.php.net/rfc/fibers
* php/php-src#6875
* php/php-src@c276c16

Note: I've not listed the `Fiber` and `FiberError` classes as those could be considered covered via the mention of Fibers on the "New Features" page, though happy to update the commit if so desired.
cmb69 pushed a commit to php/doc-en that referenced this pull request Mar 11, 2022
> Added `CURLStringFile`, which can be used to post a file from a string rather than a file

Refs:
* https://github.com/php/php-src/blob/f67986a9218f4889d9352a87c29337a5b6eaa4bd/UPGRADING#L231-L235
* php/php-src#6456
* php/php-src@e727919

As part of the Fibers RFC/PR, the Reflection extension also received a new class.

* https://wiki.php.net/rfc/fibers
* php/php-src#6875
* php/php-src@c276c16

Note: I've not listed the `Fiber` and `FiberError` classes as those could be considered covered via the mention of Fibers on the "New Features" page, though happy to update the commit if so desired.

Co-authored-by: jrfnl <jrfnl@users.noreply.github.com>

Closes GH-1447.
tiffany-taylor pushed a commit to tiffany-taylor/doc-en that referenced this pull request Jan 16, 2023
> Added `CURLStringFile`, which can be used to post a file from a string rather than a file

Refs:
* https://github.com/php/php-src/blob/f67986a9218f4889d9352a87c29337a5b6eaa4bd/UPGRADING#L231-L235
* php/php-src#6456
* php/php-src@e727919

As part of the Fibers RFC/PR, the Reflection extension also received a new class.

* https://wiki.php.net/rfc/fibers
* php/php-src#6875
* php/php-src@c276c16

Note: I've not listed the `Fiber` and `FiberError` classes as those could be considered covered via the mention of Fibers on the "New Features" page, though happy to update the commit if so desired.

Co-authored-by: jrfnl <jrfnl@users.noreply.github.com>

Closes phpGH-1447.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants