--- /dev/null
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.coyote.ajp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Socket;
+
+import javax.net.SocketFactory;
+
+/**
+ * AJP client that is not (yet) a full AJP client implementation as it just
+ * provides the functionality required for the unit tests. The client uses
+ * blocking IO throughout.
+ */
+public class SimpleAjpClient {
+
+ private static final int AJP_PACKET_SIZE = 8192;
+ private static final byte[] AJP_CPING;
+
+ static {
+ TesterAjpMessage ajpCping = new TesterAjpMessage(16);
+ ajpCping.reset();
+ ajpCping.appendByte(Constants.JK_AJP13_CPING_REQUEST);
+ ajpCping.end();
+ AJP_CPING = new byte[ajpCping.getLen()];
+ System.arraycopy(ajpCping.getBuffer(), 0, AJP_CPING, 0,
+ ajpCping.getLen());
+ }
+
+ private String host = "localhost";
+ private int port = -1;
+ private Socket socket = null;
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void connect() throws IOException {
+ socket = SocketFactory.getDefault().createSocket(host, port);
+ }
+
+ public void disconnect() throws IOException {
+ socket.close();
+ socket = null;
+ }
+
+ /**
+ * Create a message to request the given URL.
+ */
+ public TesterAjpMessage createForwardMessage(String url) {
+ TesterAjpMessage message = new TesterAjpMessage(AJP_PACKET_SIZE);
+ message.reset();
+
+ // Set the header bytes
+ message.getBuffer()[0] = 0x12;
+ message.getBuffer()[1] = 0x34;
+
+ // Code 2 for forward request
+ message.appendByte(Constants.JK_AJP13_FORWARD_REQUEST);
+
+ // HTTP method, GET = 2
+ message.appendByte(0x02);
+
+ // Protocol
+ message.appendString("http");
+
+ // Request URI
+ message.appendString(url);
+
+ // Remote address
+ message.appendString("10.0.0.1");
+
+ // Remote host
+ message.appendString("client.dev.local");
+
+ // Server name
+ message.appendString(host);
+
+ // Port
+ message.appendInt(port);
+
+ // Is ssl
+ message.appendByte(0x00);
+
+ // No other headers or attributes
+ message.appendInt(0);
+
+ // Terminator
+ message.appendByte(0xFF);
+
+ // End the message and set the length
+ message.end();
+
+ return message;
+ }
+
+ /**
+ * Sends an TesterAjpMessage to the server and returns the response message.
+ */
+ public TesterAjpMessage sendMessage(TesterAjpMessage message)
+ throws IOException {
+ // Send the message
+ socket.getOutputStream().write(
+ message.getBuffer(), 0, message.getLen());
+ // Read the response
+ return readMessage();
+ }
+
+ /**
+ * Tests the connection to the server and returns the CPONG response.
+ */
+ public TesterAjpMessage cping() throws IOException {
+ // Send the ping message
+ socket.getOutputStream().write(AJP_CPING);
+ // Read the response
+ return readMessage();
+ }
+
+ /**
+ * Reads a message from the server.
+ */
+ public TesterAjpMessage readMessage() throws IOException {
+
+ InputStream is = socket.getInputStream();
+
+ TesterAjpMessage message = new TesterAjpMessage(AJP_PACKET_SIZE);
+
+ byte[] buf = message.getBuffer();
+ int headerLength = message.getHeaderLength();
+
+ read(is, buf, 0, headerLength);
+
+ int messageLength = message.processHeader();
+ if (messageLength < 0) {
+ throw new IOException("Invalid AJP message length");
+ } else if (messageLength == 0) {
+ return message;
+ } else {
+ if (messageLength > buf.length) {
+ throw new IllegalArgumentException("Message too long [" +
+ Integer.valueOf(messageLength) +
+ "] for buffer length [" +
+ Integer.valueOf(buf.length) + "]");
+ }
+ read(is, buf, headerLength, messageLength);
+ return message;
+ }
+ }
+
+ protected boolean read(InputStream is, byte[] buf, int pos, int n)
+ throws IOException {
+
+ int read = 0;
+ int res = 0;
+ while (read < n) {
+ res = is.read(buf, read + pos, n - read);
+ if (res > 0) {
+ read += res;
+ } else {
+ throw new IOException("Read failed");
+ }
+ }
+ return true;
+ }
+}
--- /dev/null
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.coyote.ajp;
+
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
+
+public class TestAbstractAjpProcessor extends TomcatBaseTest {
+
+ @Override
+ protected String getProtocol() {
+ /*
+ * The tests are all setup for HTTP so need to convert the protocol
+ * values to AJP.
+ */
+ // Has a protocol been specified
+ String protocol = System.getProperty("tomcat.test.protocol");
+
+ // Use BIO by default
+ if (protocol == null) {
+ protocol = "org.apache.coyote.ajp.AjpProtocol";
+ } else if (protocol.contains("Nio")) {
+ protocol = "org.apache.coyote.ajp.AjpNioProtocol";
+ } else if (protocol.contains("Apr")) {
+ protocol = "org.apache.coyote.ajp.AjpAprProtocol";
+ } else {
+ protocol = "org.apache.coyote.ajp.AjpProtocol";
+ }
+
+ return protocol;
+ }
+
+ public void testKeepAlive() throws Exception {
+ Tomcat tomcat = getTomcatInstance();
+ tomcat.start();
+
+ // Must have a real docBase - just use temp
+ org.apache.catalina.Context ctx =
+ tomcat.addContext("", System.getProperty("java.io.tmpdir"));
+ Tomcat.addServlet(ctx, "helloWorld", new HelloWorldServlet());
+ ctx.addServletMapping("/", "helloWorld");
+
+ SimpleAjpClient ajpClient = new SimpleAjpClient();
+
+ ajpClient.setPort(getPort());
+
+ ajpClient.connect();
+
+ validateCpong(ajpClient.cping());
+
+ TesterAjpMessage forwardMessage = ajpClient.createForwardMessage("/");
+
+ // Two requests
+ for (int i = 0; i < 2; i++) {
+ TesterAjpMessage responseHeaders = ajpClient.sendMessage(forwardMessage);
+ // Expect 3 packets: headers, body, end
+ validateResponseHeaders(responseHeaders, 200);
+ TesterAjpMessage responseBody = ajpClient.readMessage();
+ validateResponseBody(responseBody, HelloWorldServlet.RESPONSE_TEXT);
+ validateResponseEnd(ajpClient.readMessage(), true);
+
+ // Double check the connection is still open
+ validateCpong(ajpClient.cping());
+ }
+
+ ajpClient.disconnect();
+ }
+
+ /**
+ * Process response header packet and checks the status. Any other data is
+ * ignored.
+ */
+ private void validateResponseHeaders(TesterAjpMessage message,
+ int expectedStatus) throws Exception {
+ // First two bytes should always be AB
+ assertEquals((byte) 'A', message.buf[0]);
+ assertEquals((byte) 'B', message.buf[1]);
+
+ // Set the start position and read the length
+ message.processHeader();
+
+ // Check the length
+ assertTrue(message.len > 0);
+
+ // Should be a header message
+ assertEquals(0x04, message.readByte());
+
+ // Check status
+ assertEquals(expectedStatus, message.readInt());
+
+ // Read the status message
+ message.readString();
+
+ // Get the number of headers
+ int headerCount = message.readInt();
+
+ for (int i = 0; i < headerCount; i++) {
+ // Read the header name
+ message.readHeaderName();
+ // Read the header value
+ message.readString();
+ }
+ }
+
+ /**
+ * Validates that the response message is valid and contains the expected
+ * content.
+ */
+ private void validateResponseBody(TesterAjpMessage message,
+ String expectedBody) throws Exception {
+ assertEquals((byte) 'A', message.buf[0]);
+ assertEquals((byte) 'B', message.buf[1]);
+
+ // Set the start position and read the length
+ message.processHeader();
+
+ // Should be a body chunk message
+ assertEquals(0x03, message.readByte());
+
+ int len = message.readInt();
+ assertTrue(len > 0);
+ String body = message.readString(len);
+
+ assertEquals(expectedBody, body);
+ }
+
+ private void validateResponseEnd(TesterAjpMessage message,
+ boolean expectedReuse) {
+ assertEquals((byte) 'A', message.buf[0]);
+ assertEquals((byte) 'B', message.buf[1]);
+
+ message.processHeader();
+
+ // Should be an end body message
+ assertEquals(0x05, message.readByte());
+
+ // Check the length
+ assertEquals(2, message.getLen());
+
+ boolean reuse = false;
+ if (message.readByte() > 0) {
+ reuse = true;
+ }
+
+ assertEquals(expectedReuse, reuse);
+ }
+
+ private void validateCpong(TesterAjpMessage message) throws Exception {
+ // First two bytes should always be AB
+ assertEquals((byte) 'A', message.buf[0]);
+ assertEquals((byte) 'B', message.buf[1]);
+ // CPONG should have a message length of 1
+ // This effectively checks the next two bytes
+ assertEquals(1, message.getLen());
+ // Data should be the value 9
+ assertEquals(9, message.buf[4]);
+ }
+}
--- /dev/null
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.coyote.ajp;
+
+/**
+ * Extends {@link AjpMessage} to provide additional methods for writing to the
+ * message.
+ * TODO: See if it makes sense for any/all of these methods to be transferred to
+ * AjpMessage
+ */
+public class TesterAjpMessage extends AjpMessage {
+
+ public TesterAjpMessage(int packetSize) {
+ super(packetSize);
+ }
+
+ public byte readByte() {
+ return buf[pos++];
+ }
+
+ public int readInt() {
+ int val = (buf[pos++] & 0xFF ) << 8;
+ val += buf[pos++] & 0xFF;
+ return val;
+ }
+
+ public String readString() {
+ int len = readInt();
+ return readString(len);
+ }
+
+ public String readString(int len) {
+ StringBuilder buffer = new StringBuilder(len);
+
+ for (int i = 0; i < len; i++) {
+ char c = (char) buf[pos++];
+ buffer.append(c);
+ }
+ // Read end of string marker
+ readByte();
+
+ return buffer.toString();
+ }
+
+ public String readHeaderName() {
+ byte b = readByte();
+ if ((b & 0xFF) == 0xA0) {
+ // Coded header
+ return Constants.getResponseHeaderForCode(readByte());
+ } else {
+ int len = (b & 0xFF) << 8;
+ len += getByte() & 0xFF;
+ return readString(len);
+ }
+ }
+}