@@ -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} ;
5156StreamWrapper . 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} ;
8295StreamWrapper . 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 = / ^ H T T P \/ ( \d + \. \d + ) ( \d + ) ( .* ) $ / . exec ( header ) ;
110+ var m = / ^ H T T P \/ ( \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+
119219exports . 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 ;
0 commit comments