Skip to content

Commit 2139b79

Browse files
committed
Pass POST data to PHP request.
1 parent 16d7020 commit 2139b79

File tree

8 files changed

+373
-31
lines changed

8 files changed

+373
-31
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# php-embed x.x.x (not yet released)
2+
* Support passing cookies and POST data to PHP request.
23

34
# php-embed 0.5.0 (2015-10-28)
45
* Support server variables, headers, and query string

lib/index.js

Lines changed: 129 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,27 @@ exports.PhpObject = bindings.PhpObject;
3131
// the passed-in stream and do a little bit of bookkeeping to
3232
// ensure this always works right.
3333
// While we're at it, we'll do some hand-holding of the
34-
// HTTP header API as well.
34+
// HTTP header API as well, as well as support reading
35+
// POST data from an input stream.
3536
// NOTE that this is not actually a node.js "WritableStream" any more;
3637
// this is just an internal interface we can pass over to the PHP side.
37-
var StreamWrapper = function(stream) {
38-
this.stream = stream;
38+
var StreamWrapper = function(inStream, outStream) {
39+
this._initWrite(outStream);
40+
this._initHeader(outStream);
41+
this._initRead(inStream);
42+
43+
};
44+
45+
// WRITE interface
46+
StreamWrapper.prototype._initWrite = function(outStream) {
47+
this.stream = outStream;
3948
this.flushed = true;
4049
this.error = null;
4150
this.callbacks = [];
42-
this.supportsHeaders = (
43-
typeof (stream.getHeader) === 'function' &&
44-
typeof (stream.setHeader) === 'function'
45-
);
46-
stream.on('drain', this.onDrain.bind(this));
47-
stream.on('error', this.onError.bind(this));
48-
stream.on('close', this.onClose.bind(this));
49-
stream.on('finish', this.onFinish.bind(this));
51+
this.stream.on('drain', this._onDrain.bind(this));
52+
this.stream.on('error', this._onError.bind(this));
53+
this.stream.on('close', this._onClose.bind(this));
54+
this.stream.on('finish', this._onFinish.bind(this));
5055
};
5156
StreamWrapper.prototype.write = function(buffer, cb) {
5257
if (this.error) {
@@ -64,39 +69,50 @@ StreamWrapper.prototype.write = function(buffer, cb) {
6469
}
6570
return notBuffered;
6671
};
67-
StreamWrapper.prototype.onDrain = function() {
72+
StreamWrapper.prototype._onDrain = function() {
6873
this.flushed = true;
6974
this.callbacks.forEach(function(f) { setImmediate(f); });
7075
this.callbacks.length = 0;
7176
};
72-
StreamWrapper.prototype.onError = function(e) {
77+
StreamWrapper.prototype._onError = function(e) {
7378
this.error = e;
74-
this.onDrain();
79+
this._onDrain();
80+
};
81+
StreamWrapper.prototype._onClose = function() {
82+
this._onError(new Error('stream closed'));
7583
};
76-
StreamWrapper.prototype.onClose = function() {
77-
this.onError(new Error('stream closed'));
84+
StreamWrapper.prototype._onFinish = function() {
85+
this._onError(new Error('stream finished'));
7886
};
79-
StreamWrapper.prototype.onFinish = function() {
80-
this.onError(new Error('stream finished'));
87+
88+
// HEADER interface
89+
StreamWrapper.prototype._initHeader = function(outStream) {
90+
this.supportsHeaders = (
91+
typeof (this.stream.getHeader) === 'function' &&
92+
typeof (this.stream.setHeader) === 'function'
93+
);
8194
};
8295
StreamWrapper.prototype.sendHeader = function(headerBuf) {
8396
if (!this.supportsHeaders) { return; }
8497
if (headerBuf === null) { return; } // This indicates the "last header".
8598
var header;
8699
try {
87-
// Headers are sent from PHP to JS to avoid re-encoding, but
88-
// technically they are ISO-8859-1 encoded, with a strong
89-
// recommendation to only use ASCII.
100+
// Headers are sent as Buffer from PHP to JS to avoid re-encoding
101+
// in transit. But node.js wants strings, so we need to do
102+
// some decoding. Technically headers are ISO-8859-1 encoded,
103+
// with a strong recommendation to only use ASCII.
90104
// See RFC 2616, https://tools.ietf.org/html/rfc7230#section-3.2.4
91105
header = headerBuf.toString('ascii');
92106
} catch (e) {
93107
console.error('BAD HEADER ENCODING, SKIPPING:', headerBuf);
94108
return;
95109
}
96-
var m = /^HTTP\/(\d+\.\d+) (\d+) (.*)$/.exec(header);
110+
var m = /^HTTP\/(\d+\.\d+) (\d+)( (.*))?$/.exec(header);
97111
if (m) {
98112
this.stream.statusCode = parseInt(m[2], 10);
99-
this.stream.statusMessage = m[3];
113+
if (m[4]) {
114+
this.stream.statusMessage = m[4];
115+
}
100116
return;
101117
}
102118
m = /^([^: ]+): (.*)$/.exec(header);
@@ -116,13 +132,98 @@ StreamWrapper.prototype.sendHeader = function(headerBuf) {
116132
console.error('UNEXPECTED HEADER, SKIPPING:', header);
117133
};
118134

135+
// READ interface
136+
StreamWrapper.prototype._initRead = function(inStream) {
137+
this.inputStream = inStream;
138+
this.inputSize = 0;
139+
this.inputResult = null;
140+
this.inputComplete = null;
141+
this.inputLeftover = null;
142+
this.inputCallbacks = []; // Future read requests.
143+
this.inputError = null;
144+
this.inputEnd = false;
145+
146+
if (!this.inputStream) {
147+
this.inputEnd = true;
148+
} else {
149+
this.inputStream.on('data', this._onInputData.bind(this, false));
150+
this.inputStream.on('end', this._onInputEnd.bind(this));
151+
this.inputStream.on('error', this._onInputError.bind(this));
152+
this.inputStream.pause();
153+
}
154+
};
155+
StreamWrapper.prototype.read = function(size, cb) {
156+
var self = this;
157+
var error = this.inputError;
158+
if (this.inputResult !== null) {
159+
// Read already in progress, queue for later.
160+
this.inputCallbacks.push(function() { self.read(size, cb); });
161+
return;
162+
}
163+
if (this.inputEnd || error) {
164+
if (cb) { setImmediate(function() { cb(error, new Buffer(0)); }); }
165+
return;
166+
}
167+
this.inputResult = new Buffer(size);
168+
this.inputSize = 0;
169+
this.inputComplete = cb;
170+
if (this.inputLeftover || size === 0) {
171+
this._onInputData(false, this.inputLeftover || new Buffer(0));
172+
} else {
173+
// Enable data events.
174+
this.inputStream.resume();
175+
}
176+
};
177+
StreamWrapper.prototype._onInputData = function(isEnd, buffer) {
178+
this.inputStream.pause();
179+
var remaining = (this.inputResult.length - this.inputSize);
180+
var amt = Math.min(buffer.length, remaining);
181+
buffer.copy(this.inputResult, this.inputSize, 0, amt);
182+
this.inputSize += amt;
183+
// Are we done with this input request?
184+
if (this.inputSize === this.inputResult.length || isEnd) {
185+
var cb = this.inputComplete; // Capture this for callback
186+
var err = this.inputError; // Capture this for callback
187+
var result = this.inputResult.slice(0, this.inputSize);
188+
setImmediate(function() { cb(err, result); }); // Queue callback
189+
this.inputResult = this.inputComplete = null;
190+
this.inputEnd = isEnd;
191+
// Are we done with this buffer?
192+
this.inputLeftover = (amt < buffer.length) ? buffer.slice(amt) : null;
193+
// Were there any more reads waiting?
194+
if (this.inputCallbacks.length > 0) {
195+
setImmediate(this.inputCallbacks.shift());
196+
}
197+
} else {
198+
// Need more chunks!
199+
this.inputLeftover = null;
200+
this.inputStream.resume();
201+
}
202+
};
203+
StreamWrapper.prototype._onInputEnd = function() {
204+
if (this.inputResult) {
205+
this._onInputData(true, new Buffer(0));
206+
} else {
207+
this.inputEnd = true;
208+
}
209+
while (this.inputCallbacks.length > 0) {
210+
setImmediate(this.inputCallbacks.shift());
211+
}
212+
};
213+
StreamWrapper.prototype._onInputError = function(e) {
214+
this.inputError = e;
215+
this._onInputEnd();
216+
};
217+
218+
119219
exports.request = function(options, cb) {
120220
options = options || {};
121221
var source = options.source;
122222
if (options.file) {
123223
source = 'require ' + addslashes(options.file) + ';';
124224
}
125-
var stream = new StreamWrapper(options.stream || process.stdout);
225+
var stream = new StreamWrapper(options.request,
226+
options.stream || process.stdout);
126227
var buildServerVars = function() {
127228
var server = Object.create(null);
128229
server.CONTEXT = options.context;
@@ -134,6 +235,10 @@ exports.request = function(options, cb) {
134235
var headers = options.request.headers || {};
135236
Object.keys(headers).forEach(function(h) {
136237
var hh = 'HTTP_' + h.toUpperCase().replace(/[^A-Z]/g, '_');
238+
// The array case is very unusual here: it should basically
239+
// only occur for Set-Cookie, which isn't going to be sent
240+
// *to* PHP. But make sure we don't crash if it is.
241+
if (Array.isArray(headers[h])) { return; }
137242
server[hh] = headers[h];
138243
});
139244
server.PATH = process.env.PATH;

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"mocha": "~2.3.3",
5858
"readable-stream": "~2.0.2",
5959
"segfault-handler": "git+https://github.com/cscott/node-segfault-handler#any-signal",
60-
"should": "~7.1.0"
60+
"should": "~7.1.0",
61+
"should-http": "0.0.4"
6162
}
6263
}

src/node_php_embed.cc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ using node_php_embed::OwnershipType;
3131
using node_php_embed::PhpRequestWorker;
3232
using node_php_embed::Value;
3333
using node_php_embed::ZVal;
34+
using node_php_embed::node_php_jsbuffer;
35+
using node_php_embed::node_php_jsobject_call_method;
3436

3537
static void node_php_embed_ensure_init(void);
3638

@@ -139,6 +141,46 @@ static void node_php_embed_send_header(sapi_header_struct *sapi_header,
139141
TRACE("<");
140142
}
141143

144+
static int node_php_embed_read_post(char *buffer, uint count_bytes TSRMLS_DC) {
145+
// Invoke stream.read with a PHP "JsWait" callback, which causes PHP
146+
// to block until the callback is handled.
147+
TRACE(">");
148+
// Fetch the MapperChannel for this thread.
149+
PhpRequestWorker *worker = NODE_PHP_EMBED_G(worker);
150+
MapperChannel *channel = NODE_PHP_EMBED_G(channel);
151+
if (!worker) { return 0; /* we're in module shutdown, no request any more */ }
152+
ZVal stream{ZEND_FILE_LINE_C}, retval{ZEND_FILE_LINE_C};
153+
worker->GetStream().ToPhp(channel, stream TSRMLS_CC);
154+
// Use plain zval to avoid allocating copy of method name.
155+
zval method; ZVAL_STRINGL(&method, "read", 4, 0);
156+
zval size; ZVAL_LONG(&size, count_bytes);
157+
// Create the special JsWait object.
158+
zval wait; INIT_ZVAL(wait);
159+
node_php_embed::node_php_jswait_create(&wait TSRMLS_CC);
160+
zval *args[] = { &size, &wait };
161+
// We can't use call_user_function yet because the PHP function caches
162+
// are not properly set up. Use the backdoor.
163+
node_php_jsobject_call_method(stream.Ptr(), &method, 2, args,
164+
retval.Ptr(), retval.PtrPtr() TSRMLS_CC);
165+
if (EG(exception)) {
166+
NPE_ERROR("- exception caught (ignoring)");
167+
zend_clear_exception(TSRMLS_C);
168+
return 0;
169+
}
170+
zval_dtor(&wait);
171+
// Transfer the data from the retval to the buffer
172+
if (!(retval.IsObject() && Z_OBJCE_P(retval.Ptr()) == php_ce_jsbuffer)) {
173+
NPE_ERROR("Return value was not buffer :(");
174+
return 0;
175+
}
176+
node_php_jsbuffer *b = reinterpret_cast<node_php_jsbuffer *>
177+
(zend_object_store_get_object(retval.Ptr() TSRMLS_CC));
178+
assert(b->length <= count_bytes);
179+
memcpy(buffer, b->data, b->length);
180+
TRACEX("< (read %lu)", b->length);
181+
return static_cast<int>(b->length);
182+
}
183+
142184
static char * node_php_embed_read_cookies(TSRMLS_D) {
143185
// This is a hack to prevent the SAPI from overwriting the
144186
// cookie data we set up in the PhpRequestWorker constructor.
@@ -298,6 +340,7 @@ NAN_MODULE_INIT(ModuleInit) {
298340
php_embed_module.ub_write = node_php_embed_ub_write;
299341
php_embed_module.flush = node_php_embed_flush;
300342
php_embed_module.send_header = node_php_embed_send_header;
343+
php_embed_module.read_post = node_php_embed_read_post;
301344
php_embed_module.read_cookies = node_php_embed_read_cookies;
302345
php_embed_module.register_server_variables =
303346
node_php_embed_register_server_variables;

src/node_php_jsobject_class.cc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,21 @@ PHP_METHOD(JsObject, __invoke) {
471471
TRACE("<");
472472
}
473473

474+
/* Backdoor invocation, for use in node_php_embed before the request has
475+
* been started. */
476+
void node_php_embed::node_php_jsobject_call_method(
477+
zval *object, zval *member, ulong argc, zval **argv,
478+
zval *return_value, zval **return_value_ptr TSRMLS_DC) {
479+
assert(Z_TYPE_P(object) == IS_OBJECT && Z_OBJCE_P(object) == php_ce_jsobject);
480+
FETCH_OBJ("node_php_embed_call_method", object);
481+
JsInvokeMsg msg(obj->channel, nullptr, true, // Sync call
482+
obj->id, member, argc, argv TSRMLS_CC);
483+
obj->channel->SendToJs(&msg, MessageFlags::SYNC TSRMLS_CC);
484+
THROW_IF_EXCEPTION("JS exception thrown during node_php_embed_call_method"
485+
"of \"%*s\"", Z_STRLEN_P(member), Z_STRVAL_P(member));
486+
msg.retval().ToPhp(obj->channel, return_value, return_value_ptr TSRMLS_CC);
487+
}
488+
474489
ZEND_BEGIN_ARG_INFO_EX(node_php_jsobject_toString_args, 0, 0, 0)
475490
ZEND_END_ARG_INFO()
476491

src/node_php_jsobject_class.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ void node_php_jsobject_create(zval *res, MapperChannel *channel,
3232
* reference. */
3333
void node_php_jsobject_maybe_neuter(zval *o TSRMLS_DC);
3434

35+
/* Export a method call backdoor to work around the fact that we want
36+
* to call JS to get POST data before the request's function
37+
* caches are properly set up. */
38+
void node_php_jsobject_call_method(zval *object, zval *member,
39+
ulong argc, zval **argv,
40+
zval *return_value, zval **return_value_ptr
41+
TSRMLS_DC);
42+
3543
} // namespace node_php_embed
3644

3745
extern zend_class_entry *php_ce_jsobject;

src/phprequestworker.cc

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,21 +88,41 @@ void PhpRequestWorker::Execute(MapperChannel *channel TSRMLS_DC) {
8888
NPE_ERROR("OOPS! " #requestvar " is set!"); \
8989
SG(request_info).requestvar = nullptr; \
9090
}
91+
if (argc_ == 0) {
92+
SG(sapi_headers).http_response_code = 200;
93+
}
9194
SET_REQUEST_INFO("REQUEST_METHOD", request_method);
9295
SET_REQUEST_INFO("QUERY_STRING", query_string);
9396
SET_REQUEST_INFO("PATH_TRANSLATED", path_translated);
9497
SET_REQUEST_INFO("REQUEST_URI", request_uri);
9598
SET_REQUEST_INFO("HTTP_COOKIE", cookie_data);
96-
// xxx set proto_num ?
97-
// xxx set cookie_data ?
98-
server_vars_.clear(); // We don't need to keep this around any more.
99-
99+
SET_REQUEST_INFO("HTTP_CONTENT_TYPE", content_type);
100+
SG(request_info).content_length =
101+
server_vars_.count("HTTP_CONTENT_LENGTH") ?
102+
atol(server_vars_["HTTP_CONTENT_LENGTH"].c_str()) : 0;
103+
// Unlike the other settings, proto_num needs to be set *after* we
104+
// activate the new request. Go figure.
105+
int proto_num = 1000;
106+
if (server_vars_.count("SERVER_PROTOCOL")) {
107+
const char *sline = server_vars_["SERVER_PROTOCOL"].c_str();
108+
if (strlen(sline) > 7 && strncmp(sline, "HTTP/1.", 7) == 0) {
109+
proto_num = 1000 + (sline[7] - '0');
110+
}
111+
}
112+
server_vars_.clear(); // We don't need to keep these around any more.
113+
// SG(server_context) needs to be non-zero. Believe it or not,
114+
// this is what the php-cgi binary does:
115+
SG(server_context) = reinterpret_cast<void*>(1); // Sigh.
116+
// The read_post callback gets executed by php_request_startup, and it
117+
// will need access to the worker and channel, so set them up now.
118+
NODE_PHP_EMBED_G(worker) = this;
119+
NODE_PHP_EMBED_G(channel) = channel;
120+
// Ok, *now* we can startup the request.
100121
if (php_request_startup(TSRMLS_C) == FAILURE) {
101122
Nan::ThrowError("can't create request");
102123
return;
103124
}
104-
NODE_PHP_EMBED_G(worker) = this;
105-
NODE_PHP_EMBED_G(channel) = channel;
125+
SG(request_info).proto_num = proto_num;
106126
{
107127
ZVal source{ZEND_FILE_LINE_C}, result{ZEND_FILE_LINE_C};
108128
zend_first_try {
@@ -155,6 +175,7 @@ void PhpRequestWorker::AfterExecute(TSRMLS_D) {
155175
FREE_REQUEST_INFO(path_translated);
156176
FREE_REQUEST_INFO(request_uri);
157177
FREE_REQUEST_INFO(cookie_data);
178+
FREE_REQUEST_INFO(content_type);
158179
php_request_shutdown(nullptr);
159180
TRACE("< PhpRequestWorker");
160181
}
@@ -164,6 +185,7 @@ void PhpRequestWorker::CheckRequestInfo(TSRMLS_D) {
164185
CHECK_REQUEST_INFO(path_translated);
165186
CHECK_REQUEST_INFO(request_uri);
166187
CHECK_REQUEST_INFO(cookie_data);
188+
CHECK_REQUEST_INFO(content_type);
167189
}
168190

169191
// Executed when the async work is complete.

0 commit comments

Comments
 (0)