// Copyright (c) 2014 Tom Zhou<iwebpp@gmail.com> package com.iwebpp.node.http; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Hashtable; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Pattern; import com.iwebpp.node.EventEmitter2; import com.iwebpp.node.NodeContext; import com.iwebpp.node.Util; import com.iwebpp.node.NodeContext.nextTickListener; import com.iwebpp.node.net.AbstractSocket; import com.iwebpp.node.stream.Writable; public abstract class OutgoingMessage extends EventEmitter2 implements Writable { ///extends Writable2 { private final static String TAG = "OutgoingMessage"; protected static final String connectionExpression = "Connection"; protected static final String transferEncodingExpression = "Transfer-Encoding"; protected static final String closeExpression = "close"; protected static final String contentLengthExpression = "Content-Length"; protected static final String dateExpression = "Date"; protected static final String expectExpression = "Expect"; protected static final List<String> automaticHeaders; static { automaticHeaders = new ArrayList<String>(); automaticHeaders.add("connection"); automaticHeaders.add("content-length"); automaticHeaders.add("transfer-encoding"); automaticHeaders.add("date"); } protected List<Object> output; protected List<String> outputEncodings; protected List<WriteCB> outputCallbacks; protected boolean _last; protected boolean shouldKeepAlive; /** * @return the shouldKeepAlive */ public boolean isShouldKeepAlive() { return shouldKeepAlive; } /** * @param shouldKeepAlive the shouldKeepAlive to set */ public void setShouldKeepAlive(boolean shouldKeepAlive) { this.shouldKeepAlive = shouldKeepAlive; } protected boolean chunkedEncoding; protected boolean useChunkedEncodingByDefault; protected boolean sendDate; protected boolean _hasBody; /** * @return the _hasBody */ public boolean is_hasBody() { return _hasBody; } /** * @param _hasBody the _hasBody to set */ public void set_hasBody(boolean _hasBody) { this._hasBody = _hasBody; } protected String _trailer; protected boolean finished; protected boolean _hangupClose; protected AbstractSocket socket; protected AbstractSocket connection; protected Map<String, Boolean> _removedHeader; protected boolean _headerSent; protected String _header; protected NodeContext context; protected Agent agent; protected Map<String, List<String>> _headers; protected Map<String, String> _headerNames; protected int statusCode; protected OutgoingMessage(NodeContext ctx) { ///super(ctx, new Options(-1, false, "utf8", false)); super(); this.context = ctx; this.output = new LinkedList<Object>(); this.outputEncodings = new LinkedList<String>(); this.outputCallbacks = new LinkedList<WriteCB>(); // TBD... change default to false ///this.writable = true; this.writable(true); this._last = false; this.chunkedEncoding = false; this.shouldKeepAlive = true; this.useChunkedEncodingByDefault = true; this.sendDate = false; this._removedHeader = new Hashtable<String, Boolean>(); this._hasBody = true; this._trailer = ""; this.finished = false; this._hangupClose = false; this.socket = null; this.connection = null; } @SuppressWarnings("unused") private OutgoingMessage(){} /* * OutgoingMessage.prototype.setTimeout = function(msecs, callback) { if (callback) this.on('timeout', callback); if (!this.socket) { this.once('socket', function(socket) { socket.setTimeout(msecs); }); } else this.socket.setTimeout(msecs); }; * */ // It's possible that the socket will be destroyed, and removed from // any messages, before ever calling this. In that case, just skip // it, since something else is destroying this connection anyway. ///OutgoingMessage.prototype.destroy = function(error) { public void destroy(final String error) throws Exception { if (this.socket != null) this.socket.destroy(error); else this.once("socket", new Listener() { @Override public void onEvent(Object data) throws Exception { AbstractSocket socket = (AbstractSocket)data; socket.destroy(error); } }); } // This abstract either writing directly to the socket or buffering it. ///OutgoingMessage.prototype._send = function(data, encoding, callback) { public boolean _send(Object data, String encoding, WriteCB callback) throws Exception { // This is a shameful hack to get the headers and first body chunk onto // the same packet. Future versions of Node are going to take care of // this at a lower level and in a more general way. if (!this._headerSent) { if (Util.isString(data) && encoding != "hex" && encoding != "base64") { data = this._header + data; } else { ///this.output.unshift(this._header); ///this.outputEncodings.unshift("binary"); ///this.outputCallbacks.unshift(null); this.output.add(0, this._header); this.outputEncodings.add(0, "utf-8");///"binary");// TBD... this.outputCallbacks.add(0, null); } this._headerSent = true; } return this._writeRaw(data, encoding, callback); } ///OutgoingMessage.prototype._writeRaw = function(data, encoding, callback) { public boolean _writeRaw(Object data, String encoding, final WriteCB callback) throws Exception { ///if (data.length === 0) { if (data == null || Util.chunkLength(data)==0) { ///if (util.isFunction(callback)) if (callback != null) ///process.nextTick(callback); context.nextTick(new nextTickListener(){ @Override public void onNextTick() throws Exception { callback.writeDone(null); } }); return true; } debug(TAG, "this.connection: "+this.connection); // TBD... if (this.connection!=null && this.connection.get_httpMessage() == this && this.connection.writable() && !this.connection.isDestroyed()) { // There might be pending data in the this.output buffer. ///while (this.output.length) { while (this.output.size() > 0) { if (!this.connection.writable()) { this._buffer(data, encoding, callback); return false; } ///var c = this.output.shift(); ///var e = this.outputEncodings.shift(); ///var cb = this.outputCallbacks.shift(); Object c = this.output.remove(0); String e = this.outputEncodings.remove(0); WriteCB cb = this.outputCallbacks.remove(0); this.connection.write(c, e, cb); } debug(TAG, "..... 13"); // Directly write to socket. return this.connection.write(data, encoding, callback); } else if (this.connection!=null && this.connection.isDestroyed()) { debug(TAG, "..... 15"); // The socket was destroyed. If we're still trying to write to it, // then we haven't gotten the 'close' event yet. return false; } else { debug(TAG, "..... 16"); // buffer, as long as we're not destroyed. this._buffer(data, encoding, callback); return false; } } protected boolean _buffer(Object data, String encoding, WriteCB callback) { this.output.add(data); this.outputEncodings.add(encoding); this.outputCallbacks.add(callback); return false; } // POJO beans protected class _State { boolean sentConnectionHeader; boolean sentContentLengthHeader; boolean sentTransferEncodingHeader; boolean sentDateHeader; boolean sentExpect; String messageHeader; public _State( boolean sentConnectionHeader, boolean sentContentLengthHeader, boolean sentTransferEncodingHeader, boolean sentDateHeader, boolean sentExpect, String message) { this.sentConnectionHeader = sentConnectionHeader; this.sentContentLengthHeader = sentContentLengthHeader; this.sentTransferEncodingHeader = sentTransferEncodingHeader; this.sentDateHeader = sentDateHeader; this.sentExpect = sentExpect; this.messageHeader = message; } @SuppressWarnings("unused") private _State(){} } protected void _storeHeader(String firstLine, Map<String, List<String>> headers) throws Exception { // firstLine in the case of request is: 'GET /index.html HTTP/1.1\r\n' // in the case of response it is: 'HTTP/1.1 200 OK\r\n' /*var state = { sentConnectionHeader: false, sentContentLengthHeader: false, sentTransferEncodingHeader: false, sentDateHeader: false, sentExpect: false, messageHeader: firstLine };*/ _State state = new _State(false, false, false, false, false, firstLine); if (headers != null && !headers.isEmpty()) { for (Entry<String, List<String>> entry : headers.entrySet()) { String key = entry.getKey(); for (String value : entry.getValue()) storeHeader(state, key, value); } /* var keys = Object.keys(headers); var isArray = util.isArray(headers); var field, value; for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i]; if (isArray) { field = headers[key][0]; value = headers[key][1]; } else { field = key; value = headers[key]; } if (util.isArray(value)) { for (var j = 0; j < value.length; j++) { storeHeader(this, state, field, value[j]); } } else { storeHeader(this, state, field, value); } }*/ } debug(TAG, "..... -5"); // Date header if (this.sendDate == true && state.sentDateHeader == false) { state.messageHeader += "Date: " + context.utcDate() + http.CRLF; } debug(TAG, "..... -6"); // Force the connection to close when the response is a 204 No Content or // a 304 Not Modified and the user has set a "Transfer-Encoding: chunked" // header. // // RFC 2616 mandates that 204 and 304 responses MUST NOT have a body but // node.js used to send out a zero chunk anyway to accommodate clients // that don't have special handling for those responses. // // It was pointed out that this might confuse reverse proxies to the point // of creating security liabilities, so suppress the zero chunk and force // the connection to close. int statusCode = this.statusCode; if ((statusCode == 204 || statusCode == 304) && this.chunkedEncoding == true) { debug(TAG, ""+statusCode + " response should not use chunked encoding," + " closing connection."); this.chunkedEncoding = false; this.shouldKeepAlive = false; } // keep-alive logic ///if (this._removedHeader.connection) { if (this._removedHeader.containsKey("connection") && this._removedHeader.get("connection")) { this._last = true; this.shouldKeepAlive = false; } else if (state.sentConnectionHeader == false) { boolean shouldSendKeepAlive = this.shouldKeepAlive && (state.sentContentLengthHeader || this.useChunkedEncodingByDefault || this.agent!=null); if (shouldSendKeepAlive) { state.messageHeader += "Connection: keep-alive\r\n"; } else { this._last = true; state.messageHeader += "Connection: close\r\n"; } } debug(TAG, "..... -7"); if (state.sentContentLengthHeader == false && state.sentTransferEncodingHeader == false) { if (this._hasBody && !(this._removedHeader.containsKey("transfer-encoding") && this._removedHeader.get("transfer-encoding"))) { if (this.useChunkedEncodingByDefault) { state.messageHeader += "Transfer-Encoding: chunked\r\n"; this.chunkedEncoding = true; } else { this._last = true; } } else { // Make sure we don't end the 0\r\n\r\n at the end of the message. this.chunkedEncoding = false; } } this._header = state.messageHeader + http.CRLF; this._headerSent = false; debug(TAG, "..... -8, "+this._header); // wait until the first body chunk, or close(), is sent to flush, // UNLESS we're sending Expect: 100-continue. if (state.sentExpect) this._send("", "utf-8", null); } protected void storeHeader(_State state, String field, String value) { OutgoingMessage self = this; // Protect against response splitting. The if statement is there to // minimize the performance impact in the common case. /// TBD... ///if (/[\r\n]/.test(value)) ///if (value!=null && Pattern.matches("[\r\n]", value)) value = value.replaceAll("[\r\n]+[ \t]*", ""); state.messageHeader += field + ": " + value + http.CRLF; ///if (connectionExpression == field) { if (Pattern.matches(connectionExpression, field)) { state.sentConnectionHeader = true; if (Pattern.matches(closeExpression, value)) { self._last = true; } else { self.shouldKeepAlive = true; } } else if (Pattern.matches(transferEncodingExpression, field)) { state.sentTransferEncodingHeader = true; if (Pattern.matches(http.chunkExpression, value)) self.chunkedEncoding = true; } else if (Pattern.matches(contentLengthExpression, field)) { state.sentContentLengthHeader = true; } else if (Pattern.matches(dateExpression, field)) { state.sentDateHeader = true; } else if (Pattern.matches(expectExpression, field)) { state.sentExpect = true; } } public void setHeader(String name, List<String> value) throws Exception { if (!Util.zeroString(this._header)) { ///throw new Error('Can\'t set headers after they are sent.'); throw new Exception("Can\'t set headers after they are sent."); } String key = name.toLowerCase(); this._headers = this._headers!=null ? this._headers : new Hashtable<String, List<String>>(); this._headerNames = this._headerNames!=null ? this._headerNames : new Hashtable<String, String>(); this._headers.put(key, value); this._headerNames.put(key, name); if (automaticHeaders.contains(key)) { this._removedHeader.put(key, false); } } public void setHeader(String name, String value) throws Exception { List<String> v = new ArrayList<String>(); v.add(value); setHeader(name, v); } public List<String> getHeaders(String name) { if (null==this._headers) return null; String key = name.toLowerCase(); return this._headers.containsKey(key) && !this._headers.get(key).isEmpty() ? this._headers.get(key) : null; } public String getHeader(String name) { if (null==this._headers) return null; String key = name.toLowerCase(); return this._headers.containsKey(key) && !this._headers.get(key).isEmpty() ? this._headers.get(key).get(0) : null; } public void removeHeader(String name) throws Exception { if (!Util.zeroString(this._header)) { ///throw new Error('Can\'t remove headers after they are sent.'); throw new Exception("Can\'t remove headers after they are sent."); } String key = name.toLowerCase(); if (key == "date") this.sendDate = false; else if (automaticHeaders.contains(key)) this._removedHeader.put(key, true); if (this._headers != null) { ///delete this._headers[key]; ///delete this._headerNames[key]; this._headers.remove(key); this._headerNames.remove(key); } } protected Map<String, List<String>> _renderHeaders() throws Exception { if (!Util.zeroString(this._header)) { ///throw new Error('Can\'t render headers after they are sent to the client.'); throw new Exception("Can\'t render headers after they are sent to the client."); } if (null==this._headers || this._headers.isEmpty()) return null; Map<String, List<String>> headers = new Hashtable<String, List<String>>(); for (Entry<String, List<String>> entry : this._headers.entrySet()) headers.put(this._headerNames.get(entry.getKey()), entry.getValue()); return headers; } public boolean headersSent() { return !Util.zeroString(this._header); } protected abstract void _implicitHeader() throws Exception; public boolean write(Object chunk, String encoding, WriteCB callback) throws Exception { if (Util.zeroString(this._header)) { this._implicitHeader(); } debug(TAG, ".......... 0"); if (!this._hasBody) { debug(TAG, "This type of response MUST NOT have a body. " + "Ignoring write() calls."); return true; } if (!Util.isString(chunk) && !Util.isBuffer(chunk)) { ///throw new TypeError('first argument must be a string or Buffer'); throw new Exception("first argument must be a string or Buffer"); } debug(TAG, ".......... -1"); // If we get an empty string or buffer, then just do nothing, and // signal the user to keep writing. if (Util.chunkLength(chunk) == 0) return true; int len; boolean ret; if (this.chunkedEncoding) { debug(TAG, ".......... 1"); if (Util.isString(chunk) && encoding != "hex" && encoding != "base64" && encoding != "binary") { debug(TAG, ".......... 2"); ///len = Buffer.byteLength(chunk, encoding); len = Util.stringByteLength((String) chunk, encoding); ///chunk = len.toString(16) + CRLF + chunk + CRLF; chunk = Integer.toString(len, 16) + http.CRLF + chunk + http.CRLF; debug(TAG, "write _send: "+chunk.toString()); ret = this._send(chunk, encoding, callback); } else { // buffer, or a non-toString-friendly encoding if (Util.isString(chunk)) ///len = Buffer.byteLength(chunk, encoding); len = Util.stringByteLength((String) chunk, encoding); else ///len = chunk.length; len = Util.chunkLength(chunk); if (this.connection!=null && this.connection.corked()==0 ) { this.connection.cork(); final AbstractSocket conn = this.connection; ///process.nextTick(function connectionCork() { context.nextTick(new nextTickListener(){ @Override public void onNextTick() throws Exception { if (conn != null) conn.uncork(); } }); } ///ByteBuffer crlf_buf = ByteBuffer.wrap("\r\n".getBytes("utf-8")); this._send(Integer.toString(len, 16), "utf-8"/*TBD..."binary"*/, null); this._send(ByteBuffer.wrap("\r\n".getBytes("utf-8")), null, null); this._send(chunk, encoding, null); ret = this._send(ByteBuffer.wrap("\r\n".getBytes("utf-8")), null, callback); } } else { debug(TAG, ".......... 3"); ret = this._send(chunk, encoding, callback); } debug(TAG, "write ret = " + ret); return ret; } public boolean write(Object chunk, String encoding) throws Exception { return write(chunk, encoding, null); } public boolean write(Object chunk) throws Exception { return write(chunk, null, null); } public boolean write() throws Exception { return write(null, null, null); } public void addTrailers(Map<String, String> headers) { this._trailer = ""; for (Entry<String, String> entry : headers.entrySet()) this._trailer += entry.getKey() + ": " + entry.getValue() + http.CRLF; } public boolean end(Object data, String encoding, final WriteCB callback) throws Exception { /*if (util.isFunction(data)) { callback = data; data = null; } else if (util.isFunction(encoding)) { callback = encoding; encoding = null; }*/ if (data!=null && !Util.isString(data) && !Util.isBuffer(data)) { ///throw new TypeError('first argument must be a string or Buffer'); throw new Exception("first argument must be a string or Buffer"); } if (this.finished) { return false; } final OutgoingMessage self = this; WriteCB finish = new WriteCB() { @Override public void writeDone(String error) throws Exception { self.emit("finish"); } }; ///if (util.isFunction(callback)) if (null!=callback) this.once("finish", new Listener(){ public void onEvent(Object data) throws Exception { callback.writeDone(null); } }); if (Util.zeroString(this._header)) { this._implicitHeader(); } if (data!=null && !this._hasBody) { debug(TAG, "This type of response MUST NOT have a body. " + "Ignoring data passed to end()."); data = null; } if (this.connection!=null && data!=null) this.connection.cork(); boolean ret; if (data!=null) { // Normal body write. ret = this.write(data, encoding, null); } if (this.chunkedEncoding) { ret = this._send("0\r\n" + this._trailer + "\r\n", "utf-8"/*TBD..."binary"*/, finish); } else { // Force a flush, HACK. ret = this._send("", "utf-8"/*TBD..."binary"*/, finish); } if (this.connection!=null && data!=null) this.connection.uncork(); this.finished = true; // There is the first message on the outgoing queue, and we've sent // everything to the socket. debug(TAG, "outgoing message end."); if (this.output.size() == 0 && this.connection.get_httpMessage() == this) { this._finish(); } return ret; } public boolean end(Object data, String encoding) throws Exception { return end(data, encoding, null); } public boolean end(Object data) throws Exception { return end(data, null, null); } public boolean end() throws Exception { return end(null, null, null); } @Override public boolean writable() { if (this.connection != null) return this.connection.writable(); else return false; } @Override public void writable(boolean writable) { if (this.connection != null) this.connection.writable(writable); } protected void _finish() throws Exception { assert(this.connection!=null); this.emit("prefinish"); } // This logic is probably a bit confusing. Let me explain a bit: // // In both http servers and clients it is possible to queue up several // outgoing messages. This is easiest to imagine in the case of a client. // Take the following situation: // // req1 = client.request('GET', '/'); // req2 = client.request('POST', '/'); // // When the user does // // req2.write('hello world\n'); // // it's possible that the first request has not been completely flushed to // the socket yet. Thus the outgoing messages need to be prepared to queue // up data internally before sending it on further to the socket's queue. // // This function, outgoingFlush(), is called by both the AbstractServer and Client // to attempt to flush any pending messages out to the socket. protected void _flush() throws Exception { if (this.socket!=null && this.socket.writable()) { boolean ret = false; while (this.output.size() > 0) { ///var data = this.output.shift(); ///var encoding = this.outputEncodings.shift(); ///var cb = this.outputCallbacks.shift(); Object data = this.output.remove(0); String encoding = this.outputEncodings.remove(0); WriteCB cb = this.outputCallbacks.remove(0); ret = this.socket.write(data, encoding, cb); debug(TAG, "_flush "+data+"@"+encoding+",ret="+ret); } if (this.finished) { // This is a queue to the server or client to bring in the next this. this._finish(); } else if (ret) { // This is necessary to prevent https from breaking this.emit("drain"); } } } public void flush() throws Exception { if (Util.zeroString(this._header)) { // Force-flush the headers. this._implicitHeader(); this._send("", "utf-8", null); } } /** * @return the _last */ public boolean is_last() { return _last; } /** * @param _last the _last to set */ public void set_last(boolean _last) { this._last = _last; } }