// Copyright (c) 2014 Tom Zhou<iwebpp@gmail.com> package com.iwebpp.node.http; import java.nio.ByteBuffer; import java.util.List; import java.util.Map; import com.iwebpp.node.NodeContext; import com.iwebpp.node.NodeContext.nextTickListener; import com.iwebpp.node.NodeError; import com.iwebpp.node.HttpParser.http_parser_type; import com.iwebpp.node.Util; import com.iwebpp.node.http.http.response_socket_head_b; import com.iwebpp.node.net.AbstractSocket; import com.iwebpp.node.net.TCP; import com.iwebpp.node.net.UDT; import com.iwebpp.node.others.TripleState; public final class ClientRequest extends OutgoingMessage { private final static String TAG = "ClientRequest"; private IncomingParser parser; private IncomingMessage res; private String method; private boolean upgradeOrConnect; // AbstractSocket event listeners private socketCloseListener socketCloseListener; private socketErrorListener socketErrorListener; private socketOnData socketOnData; private socketOnEnd socketOnEnd; private int maxHeadersCount = 4000; private long aborted = -1; private String path; /** * @return the method */ public String getMethod() { return method; } /** * @return the path */ public String getPath() { return path; } public ClientRequest(NodeContext context, ReqOptions options, responseListener cb) throws Exception { super(context); final ClientRequest self = this; /*if (util.isString(options)) { options = url.parse(options); } else { options = util._extend({}, options); }*/ // No global agent in node.android Agent agent = options.agent; ///var defaultAgent = options._defaultAgent || Agent.globalAgent; /*if (agent === false) { agent = new defaultAgent.constructor(); } else */if (Util.isNullOrUndefined(agent) && null==options.createConnection) { // TBD... agent = options.httpp ? new Agent.HttppAgent(context, options) : new Agent.HttpAgent(context, options);///defaultAgent; } self.agent = agent; ///self.agent = null; String protocol = !Util.zeroString(options.protocol) ? options.protocol : "http:";///defaultAgent.protocol; String expectedProtocol = "http:";///defaultAgent.protocol; if (self.agent!=null && self.agent.protocol()!=null) expectedProtocol = self.agent.protocol(); if (options.path!=null && options.path.contains(" ") /*/ /.test(options.path)*/) { // The actual regex is more like /[^A-Za-z0-9\-._~!$&'()*+,;=/:@]/ // with an additional rule for ignoring percentage-escaped characters // but that's a) hard to capture in a regular expression that performs // well, and b) possibly too restrictive for real-world usage. That's // why it only scans for spaces because those are guaranteed to create // an invalid request. throw new Exception("Request path contains unescaped characters."); } else if (!protocol.equalsIgnoreCase(expectedProtocol)) { throw new Error("Protocol " + protocol + " not supported. " + "Expected " + expectedProtocol + "."); } int defaultPort = options.defaultPort > 0 ? options.defaultPort : self.agent!=null ? self.agent.defaultPort() : 80; int port = options.port = options.port > 0 ? options.port : defaultPort;/// || 80; String host = options.host = options.hostname!=null ? options.hostname : options.host!=null ? options.host : "localhost"; ///if (util.isUndefined(options.setHost)) { boolean setHost = options.setHost; ///} String method = self.method = (options.method!=null? options.method : "GET").toUpperCase(); self.path = options.path!=null && !options.path.equals("") ? options.path : "/"; if (cb!=null) { ///self.once("response", cb); self.onceResponse(cb); } /* if (!util.isArray(options.headers)) { if (options.headers) { var keys = Object.keys(options.headers); for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i]; self.setHeader(key, options.headers[key]); } } if (host!=null && !this.getHeader("host") && setHost) { var hostHeader = host; if (port && +port !== defaultPort) { hostHeader += ':' + port; } this.setHeader("Host", hostHeader); } } */ { if (options.headers != null) for (Map.Entry<String, List<String>> entry : options.headers.entrySet()) self.setHeader(entry.getKey(), entry.getValue()); if (host!=null && null==this.getHeaders("host") && setHost) { String hostHeader = host; if (port>0 && port != defaultPort) { hostHeader += ':' + port; } this.setHeader("Host", hostHeader); } } if (options.auth!=null && null==this.getHeaders("Authorization")) { //basic auth this.setHeader("Authorization", "Basic " + ///new ByteBuffer(options.auth).toString("base64")); options.auth); } if (method == "GET" || method == "HEAD" || method == "DELETE" || method == "OPTIONS" || method == "CONNECT") { self.useChunkedEncodingByDefault = false; } else { self.useChunkedEncodingByDefault = true; } if (null!=options.headers/*util.isArray(options.headers)*/) { self._storeHeader(self.method + " " + self.path + " HTTP/1.1\r\n", options.headers); } else if (self.getHeaders("expect")!=null) { self._storeHeader(self.method + " " + self.path + " HTTP/1.1\r\n", self._renderHeaders()); } /*if (self.socketPath) { self._last = true; self.shouldKeepAlive = false; var conn = self.agent.createConnection({ path: self.socketPath }); self.onSocket(conn); } else*/ if (self.agent!=null) { // If there is an agent we should default to Connection:keep-alive, // but only if the Agent will actually reuse the connection! // If it's not a keepAlive agent, and the maxSockets==Infinity, then // there's never a case where this socket will actually be reused ///if (!self.agent.isKeepAlive() && !Number.isFinite(self.agent.getMaxSockets())) { if (!self.agent.keepAlive() && self.agent.maxSockets()==Agent.defaultMaxSockets) { self._last = true; self.shouldKeepAlive = false; } else { self._last = false; self.shouldKeepAlive = true; } self.agent.addRequest(self, options); } else { // No agent, default to Connection:close. self._last = true; self.shouldKeepAlive = false; AbstractSocket conn; if (options.createConnection != null) { conn = options. createConnection. createConnection( context, options.host, options.port, options.localAddress, options.localPort, null); } else { debug(TAG, "CLIENT use TCP.createConnection " + options); conn = options.httpp ? UDT.createConnection( context, options.host, options.port, options.localAddress, options.localPort, null) : TCP.createConnection( context, options.host, options.port, options.localAddress, options.localPort, null); } self.onSocket(conn); } self._deferToConnect(null, null, new Listener(){ @Override public void onEvent(Object data) throws Exception { self._flush(); ///self = null; debug(TAG, "_flush done"); } }); } protected void _finish() throws Exception { ///DTRACE_HTTP_CLIENT_REQUEST(this, this.connection); ///COUNTER_HTTP_CLIENT_REQUEST(); super._finish(); } public void abort() throws Exception { // Mark as aborting so we can avoid sending queued request data // This is used as a truthy flag elsewhere. The use of Date.now is for // debugging purposes only. this.aborted = System.currentTimeMillis(); // If we're aborting, we don't care about any more response data. if (this.res != null) this.res._dump(); else this.onceResponse(new responseListener(){ @Override public void onResponse(IncomingMessage res) throws Exception { res._dump(); } }); // In the event that we don't have a socket, we will pop out of // the request queue through handling in onSocket. if (this.socket != null) { // in-progress this.socket.destroy(null); } } private void _deferToConnect( final String method, final Object arguments_, final Listener cb) throws Exception { // This function is for calls that need to happen once the socket is // connected and writable. It's an important promisy thing for all the socket // calls that happen either now (when a socket is assigned) or // in the future (when a socket gets assigned out of the pool and is // eventually writable). final ClientRequest self = this; Listener onSocket = new Listener(){ @Override public void onEvent(Object data) throws Exception { ///final TCP.Socket socket = (TCP.Socket)data; debug(TAG, "onSocket: "+self.socket); debug(TAG, "this.connection: "+self.connection); if (self.socket.writable()) { debug(TAG, "_deferToConnect: "+self.socket.writable()); /* if (method) { self.socket[method].apply(self.socket, arguments_); } if (cb) { cb(); }*/ if (method!=null && method == "setNoDelay") { Boolean args = (Boolean)arguments_; self.socket.setNoDelay(args); } else if (method!=null && method == "setSocketKeepAlive") { enable_initialDelay_b args = (enable_initialDelay_b)arguments_; self.socket.setKeepAlive(args.enable, args.initialDelay); } if (cb != null) cb.onEvent(null); } else { /*self.socket.once("connect", function() { if (method) { self.socket[method].apply(self.socket, arguments_); } if (cb) { cb();} });*/ // TBD... self.socket.once("connect", new Listener(){ @Override public void onEvent(Object data) throws Exception { debug(TAG, "onSocket connected: "+socket); if (method!=null && method == "setNoDelay") { Boolean args = (Boolean)arguments_; self.socket.setNoDelay(args); } else if (method!=null && method == "setSocketKeepAlive") { enable_initialDelay_b args = (enable_initialDelay_b)arguments_; self.socket.setKeepAlive(args.enable, args.initialDelay); } if (cb != null) cb.onEvent(null); } }); } } }; if (null==self.socket) { self.once("socket", onSocket); } else { onSocket.onEvent(self.socket); } } ///request.setTimeout(timeout, [callback]) public void setNoDelay(boolean noDelay) throws Exception { this._deferToConnect("setNoDelay", new Boolean(noDelay), null); } public void setSocketKeepAlive(boolean enable, int initialDelay) throws Exception { this._deferToConnect("setKeepAlive", new enable_initialDelay_b(enable, initialDelay), null); } /*ClientRequest.prototype.clearTimeout = function(cb) { this.setTimeout(0, cb); };*/ // POJO beans private class enable_initialDelay_b { private boolean enable; private int initialDelay; private enable_initialDelay_b(boolean enable, int initialDelay) { this.enable = enable; this.initialDelay = initialDelay; } } // Event listeners public void onceResponse(final responseListener cb) throws Exception { this.once("response", new Listener(){ @Override public void onEvent(Object raw) throws Exception { IncomingMessage data = (IncomingMessage)raw; cb.onResponse(data); } }); } public interface responseListener { public void onResponse(IncomingMessage res) throws Exception; } public void onceSocket(final socketListener cb) throws Exception { this.once("socket", new Listener(){ @Override public void onEvent(Object raw) throws Exception { AbstractSocket data = (AbstractSocket)raw; cb.onSocket(data); } }); } public interface socketListener { public void onSocket(AbstractSocket socket) throws Exception; } public void onceConnect(final connectListener cb) throws Exception { this.once("connect", new Listener(){ @Override public void onEvent(Object raw) throws Exception { response_socket_head_b data = (response_socket_head_b)raw; cb.onConnect(data.getResponse(), data.getSocket(), data.getHead()); } }); } public interface connectListener { public void onConnect(IncomingMessage res, AbstractSocket socket, ByteBuffer head) throws Exception; } public void onceUpgrade(final upgradeListener cb) throws Exception { this.once("upgrade", new Listener(){ @Override public void onEvent(Object raw) throws Exception { response_socket_head_b data = (response_socket_head_b)raw; cb.onUpgrade(data.getResponse(), data.getSocket(), data.getHead()); } }); } public interface upgradeListener { public void onUpgrade(IncomingMessage res, AbstractSocket socket, ByteBuffer head) throws Exception; } public void onContinue(final continueListener cb) throws Exception { this.on("continue", new Listener(){ @Override public void onEvent(Object data) throws Exception { cb.onContinue(); } }); } public interface continueListener { public void onContinue() throws Exception; } // Parser on response private class parserOnIncomingClient extends IncomingParser { private static final String TAG = "parserOnIncoming"; private NodeContext context; public parserOnIncomingClient(NodeContext ctx, AbstractSocket socket) { super(ctx, http_parser_type.HTTP_RESPONSE, socket); this.context = ctx; } private parserOnIncomingClient(){super(null, null, null);} @Override protected boolean onIncoming(final IncomingMessage res, boolean shouldKeepAlive) throws Exception { AbstractSocket socket = this.socket; final ClientRequest req = (ClientRequest)socket.get_httpMessage(); // propogate "domain" setting... /*if (req.domain && !res.domain) { debug(TAG, "setting res.domain"); res.domain = req.domain; }*/ debug(TAG, "AGENT incoming response!"); if (req.res != null) { // We already have a response object, this means the server // sent a double response. socket.destroy(null); ///return; return false; } req.res = res; // Responses to CONNECT request is handled as Upgrade. if (req.method == "CONNECT") { res.setUpgrade(true); return true; // skip body } // Responses to HEAD requests are crazy. // HEAD responses aren't allowed to have an entity-body // but *can* have a content-length which actually corresponds // to the content-length of the entity-body had the request // been a GET. boolean isHeadResponse = req.method == "HEAD"; debug(TAG, "AGENT isHeadResponse "+isHeadResponse); if (res.statusCode() == 100) { // restart the parser, as this is a continue message. ///delete req.res; // Clear res so that we don't hit double-responses. req.res = null; req.emit("continue"); return true; } if (req.shouldKeepAlive && !shouldKeepAlive && !req.upgradeOrConnect) { // AbstractServer MUST respond with Connection:keep-alive for us to enable it. // If we've been upgraded (via WebSockets) we also shouldn't try to // keep the connection open. req.shouldKeepAlive = false; } ///DTRACE_HTTP_CLIENT_RESPONSE(socket, req); ///COUNTER_HTTP_CLIENT_RESPONSE(); req.res = res; res.setReq(req); // add our listener first, so that we guarantee socket cleanup Listener responseOnEnd = new Listener() { @Override public void onEvent(Object data) throws Exception { ///var res = this; ClientRequest req = res.getReq(); final AbstractSocket socket = req.socket; if (!req.shouldKeepAlive) { if (socket.writable()) { debug(TAG, "AGENT socket.destroySoon()"); socket.destroySoon(); } assert(!socket.writable()); } else { debug(TAG, "AGENT socket keep-alive"); // TBD... ///if (req.timeoutCb) { /// socket.setTimeout(0, req.timeoutCb); /// req.timeoutCb = null; ///} // TBD... socket.removeListener("close", socketCloseListener); socket.removeListener("error", socketErrorListener); // Mark this socket as available, AFTER user-added end // handlers have a chance to run. ///process.nextTick(function() { context.nextTick(new nextTickListener() { @Override public void onNextTick() throws Exception { socket.emit("free"); } }); } } }; res.on("end", responseOnEnd); boolean handled = req.emit("response", res); // If the user did not listen for the 'response' event, then they // can't possibly read the data, so we ._dump() it into the void // so that the socket doesn't hang there in a paused state. if (!handled) res._dump(); return isHeadResponse; } } private void tickOnSocket(ClientRequest req, AbstractSocket socket) throws Exception { ///var parser = parsers.alloc(); parserOnIncomingClient parser = new parserOnIncomingClient(context, socket); req.socket = socket; req.connection = socket; parser.Reinitialize(http_parser_type.HTTP_RESPONSE); parser.socket = socket; parser.incoming = null; req.setParser(parser); socket.setParser(parser); socket.set_httpMessage(req); debug(TAG, "req.connection: "+req.connection); // Setup "drain" propogation. http.httpSocketSetup(socket); // Propagate headers limit from request object to parser if (req.maxHeadersCount > 0/*util.isNumber(req.maxHeadersCount)*/) { parser.maxHeaderPairs = req.maxHeadersCount << 1; } else { // Set default value because parser may be reused from FreeList parser.maxHeaderPairs = 2000; } ///parser.onIncoming = parserOnIncomingClient; this.socketErrorListener = new socketErrorListener(context, socket); socket.on("error", socketErrorListener); // set flowing ??? TBD... socket.get_readableState().setFlowing(TripleState.TRUE); this.socketOnData = new socketOnData(context, socket); socket.on("data", socketOnData); this.socketOnEnd = new socketOnEnd(context, socket); socket.on("end", socketOnEnd); this.socketCloseListener = new socketCloseListener(context, socket); socket.on("close", socketCloseListener); req.emit("socket", socket); debug(TAG, "emit socket: "+socket); } public void onSocket(final AbstractSocket socket) { final ClientRequest req = this; debug(TAG, "onSocket"); context.nextTick(new NodeContext.nextTickListener() { @Override public void onNextTick() throws Exception { debug(TAG, "onNextTick "); if (req.aborted > 0) { // If we were aborted while waiting for a socket, skip the whole thing. socket.emit("free"); } else { debug(TAG, "tickOnSocket"); tickOnSocket(req, socket); } } }); } @Override protected void _implicitHeader() throws Exception { this._storeHeader(this.method + " " + this.path + " HTTP/1.1\r\n", this._renderHeaders()); } private static NodeError createHangUpError() { return new NodeError("ECONNRESET", "socket hang up"); } /** * @return the parser */ public IncomingParser getParser() { return parser; } /** * @param parser the parser to set */ public void setParser(IncomingParser parser) { this.parser = parser; } private class socketCloseListener implements Listener { private AbstractSocket socket; public socketCloseListener(NodeContext ctx, AbstractSocket socket) { this.socket = socket; } @SuppressWarnings("unused") private socketCloseListener(){} @Override public void onEvent(Object data) throws Exception { ///var socket = this; final ClientRequest req = (ClientRequest)socket.get_httpMessage(); debug(TAG, "http socket close"); // Pull through final chunk, if anything is buffered. // the ondata function will handle it properly, and this // is a no-op if no final chunk remains. ///socket.read(); socket.read(-1); // NOTE: Its important to get parser here, because it could be freed by // the `socketOnData`. parserOnIncomingClient parser = (parserOnIncomingClient) socket.getParser(); req.emit("close"); if (req.res!=null && req.res.readable()) { // AbstractSocket closed before we emitted 'end' below. req.res.emit("aborted"); final IncomingMessage res = req.res; res.on("end", new Listener(){ @Override public void onEvent(Object data) throws Exception { res.emit("close"); } }); res.push(null, null); } else if (req.res==null && !req.socket.is_hadError()) { // This socket error fired before we started to // receive a response. The error needs to // fire on the request. req.emit("error", createHangUpError()); req.socket.set_hadError(true); } // Too bad. That output wasn't getting written. // This is pretty terrible that it doesn't raise an error. // Fixed better in v0.10 if (req.output != null) ///req.output.length = 0; req.output.clear(); if (req.outputEncodings != null) ///req.outputEncodings.length = 0; req.outputEncodings.clear(); if (parser != null) { parser.Finish(); IncomingParser.freeParser(parser, req); } } } private class socketErrorListener implements Listener { private AbstractSocket socket; public socketErrorListener(NodeContext ctx, AbstractSocket socket) { this.socket = socket; } @SuppressWarnings("unused") private socketErrorListener(){} @Override public void onEvent(Object err) throws Exception { ///var socket = this; parserOnIncomingClient parser = (parserOnIncomingClient) socket.getParser(); ClientRequest req = (ClientRequest) socket.get_httpMessage(); debug(TAG, "SOCKET ERROR: " + err);/// err.message, err.stack); if (req != null) { req.emit("error", err); // For Safety. Some additional errors might fire later on // and we need to make sure we don't double-fire the error event. req.socket.set_hadError(true); } if (parser != null) { parser.Finish(); IncomingParser.freeParser(parser, req); } socket.destroy(null); } } private class socketOnEnd implements Listener { private AbstractSocket socket; public socketOnEnd(NodeContext ctx, AbstractSocket socket) { this.socket = socket; } @SuppressWarnings("unused") private socketOnEnd(){} @Override public void onEvent(Object data) throws Exception { ///var socket = this; ClientRequest req = (ClientRequest) socket.get_httpMessage(); parserOnIncomingClient parser = (parserOnIncomingClient) socket.getParser(); if (req.res==null && !req.socket.is_hadError()) { // If we don't have a response then we know that the socket // ended prematurely and we need to emit an error on the request. req.emit("error", createHangUpError()); req.socket.set_hadError(true); } if (parser != null) { parser.Finish(); IncomingParser.freeParser(parser, req); } socket.destroy(null); } } private class socketOnData implements Listener { private AbstractSocket socket; public socketOnData(NodeContext ctx, AbstractSocket socket) { this.socket = socket; } @SuppressWarnings("unused") private socketOnData(){} @Override public void onEvent(Object raw) throws Exception { ByteBuffer d = (ByteBuffer)raw; ///var socket = this; ClientRequest req = (ClientRequest) socket.get_httpMessage(); parserOnIncomingClient parser = (parserOnIncomingClient) socket.getParser(); assert(parser!=null && parser.socket == socket); int ret = parser.Execute(d); if (ret < 0/*ret instanceof Error*/) { debug(TAG, "parse error"); IncomingParser.freeParser(parser, req); socket.destroy(null); req.emit("error", "parse error"); req.socket.set_hadError(true); } else if (parser.incoming!=null && parser.incoming.isUpgrade()) { debug(TAG, "Upgrade or CONNECT"); // Upgrade or CONNECT int bytesParsed = ret; IncomingMessage res = parser.incoming; req.res = res; socket.removeListener("data", socketOnData); socket.removeListener("end", socketOnEnd); parser.Finish(); ByteBuffer bodyHead = (ByteBuffer)Util.chunkSlice(d, bytesParsed, d.capacity());// d.slice(bytesParsed, d.length); String eventName = req.method.equalsIgnoreCase("CONNECT") ? "connect" : "upgrade"; if (req.listenerCount(eventName) > 0) { req.upgradeOrConnect = true; // detach the socket socket.emit("agentRemove"); socket.removeListener("close", socketCloseListener); socket.removeListener("error", socketErrorListener); // TODO(isaacs): Need a way to reset a stream to fresh state // IE, not flowing, and not explicitly paused. socket.get_readableState().setFlowing(TripleState.MAYBE); req.emit(eventName, new http.response_socket_head_b(res, socket, bodyHead)); req.emit("close"); } else { debug(TAG, "Got Upgrade header or CONNECT method, but have no handler"); // Got Upgrade header or CONNECT method, but have no handler. socket.destroy(null); } IncomingParser.freeParser(parser, req); } else if (parser.incoming!=null && parser.incoming.isComplete() && // When the status code is 100 (Continue), the server will // send a final response after this client sends a request // body. So, we must not free the parser. parser.incoming.statusCode() != 100) { socket.removeListener("data", socketOnData); socket.removeListener("end", socketOnEnd); IncomingParser.freeParser(parser, req); } } } }