001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: package org.apache.jmeter.protocol.http.control;
020:
021: import java.io.BufferedInputStream;
022: import java.io.BufferedOutputStream;
023: import java.net.Socket;
024:
025: import org.apache.jmeter.util.JMeterUtils;
026: import org.apache.jorphan.logging.LoggingManager;
027: import org.apache.jorphan.util.JOrphanUtils;
028: import org.apache.log.Logger;
029: import org.apache.oro.text.regex.MatchResult;
030: import org.apache.oro.text.regex.Pattern;
031: import org.apache.oro.text.regex.PatternMatcherInput;
032: import org.apache.oro.text.regex.Perl5Compiler;
033: import org.apache.oro.text.regex.Perl5Matcher;
034:
035: /**
036: * Thread to handle one client request. Gets the request from the client and
037: * sends the response back to the client.
038: */
039: public class HttpMirrorThread extends Thread {
040: private static final Logger log = LoggingManager
041: .getLoggerForClass();
042:
043: private static final String ISO_8859_1 = "ISO-8859-1"; //$NON-NLS-1$
044: private static final byte[] CRLF = { 0x0d, 0x0a };
045:
046: /** Socket to client. */
047: private final Socket clientSocket;
048:
049: public HttpMirrorThread(Socket _clientSocket) {
050: this .clientSocket = _clientSocket;
051: }
052:
053: /**
054: * Main processing method for the HttpMirror object
055: */
056: public void run() {
057: log.info("Starting thread");
058: BufferedInputStream in = null;
059: BufferedOutputStream out = null;
060:
061: try {
062: in = new BufferedInputStream(clientSocket.getInputStream());
063: out = new BufferedOutputStream(clientSocket
064: .getOutputStream());
065: // The headers are written using ISO_8859_1 encoding
066: out.write("HTTP/1.0 200 OK".getBytes(ISO_8859_1)); //$NON-NLS-1$
067: out.write(CRLF);
068: out.write("Content-Type: text/plain".getBytes(ISO_8859_1)); //$NON-NLS-1$
069: out.write(CRLF);
070: out.write(CRLF);
071: out.flush();
072:
073: // Read the header part, we will be looking for a content-length
074: // header, so we know how much we should read.
075: // We assume headers are in ISO_8859_1
076: // If we do not find such a header, we will just have to read until
077: // we have to block to read more, until we support chunked transfer
078: int contentLength = -1;
079: boolean isChunked = false;
080: byte[] buffer = new byte[1024];
081: StringBuffer headers = new StringBuffer();
082: int length = 0;
083: int positionOfBody = 0;
084: while (positionOfBody <= 0
085: && ((length = in.read(buffer)) != -1)) {
086: out.write(buffer, 0, length); // echo back
087: headers
088: .append(new String(buffer, 0, length,
089: ISO_8859_1));
090: // Check if we have read all the headers
091: positionOfBody = getPositionOfBody(headers.toString());
092: }
093:
094: // Check if we have found a content-length header
095: String contentLengthHeaderValue = getRequestHeaderValue(
096: headers.toString(), "Content-Length"); //$NON-NLS-1$
097: if (contentLengthHeaderValue != null) {
098: contentLength = new Integer(contentLengthHeaderValue)
099: .intValue();
100: }
101: String transferEncodingHeaderValue = getRequestHeaderValue(
102: headers.toString(), "Transfer-Encoding"); //$NON-NLS-1$
103: if (transferEncodingHeaderValue != null) {
104: isChunked = transferEncodingHeaderValue
105: .equalsIgnoreCase("chunked"); //$NON-NLS-1$
106: // We only support chunked transfer encoding
107: if (!isChunked) {
108: log
109: .error("Transfer-Encoding header set, the value is not supported : "
110: + transferEncodingHeaderValue);
111: }
112: }
113:
114: // If we know the content lenght, we can allow the reading of
115: // the request to block until more data arrives.
116: // If it is chunked transfer, we cannot allow the reading to
117: // block, because we do not know when to stop reading, because
118: // the chunked transfer is not properly supported yet
119: length = 0;
120: if (contentLength > 0) {
121: // Check how much of the body we have already read as part of reading
122: // the headers
123: // We subtract two bytes for the crlf divider between header and body
124: int totalReadBytes = headers.toString().length()
125: - positionOfBody - 2;
126:
127: // We know when to stop reading, so we can allow the read method to block
128: while ((totalReadBytes < contentLength)
129: && ((length = in.read(buffer)) != -1)) {
130: out.write(buffer, 0, length);
131:
132: totalReadBytes += length;
133: }
134: } else if (isChunked) {
135: // It is chunked transfer encoding, which we do not really support yet.
136: // So we just read without blocking, because we do not know when to
137: // stop reading, so we cannot block
138: // TODO propery implement support for chunked transfer, i.e. to
139: // know when we have read the whole request, and therefore allow
140: // the reading to block
141: while (in.available() > 0
142: && ((length = in.read(buffer)) != -1)) {
143: out.write(buffer, 0, length);
144: }
145: } else {
146: // The reqest has no body, or it has a transfer encoding we do not support.
147: // In either case, we read any data available
148: while (in.available() > 0
149: && ((length = in.read(buffer)) != -1)) {
150: out.write(buffer, 0, length);
151: }
152: }
153: out.flush();
154: } catch (Exception e) {
155: log.error("", e);
156: } finally {
157: JOrphanUtils.closeQuietly(out);
158: JOrphanUtils.closeQuietly(in);
159: JOrphanUtils.closeQuietly(clientSocket);
160: }
161: log.info("End of Thread");
162: }
163:
164: private static String getRequestHeaderValue(String requestHeaders,
165: String headerName) {
166: Perl5Matcher localMatcher = JMeterUtils.getMatcher();
167: // We use multi-line mask so can prefix the line with ^
168: // also match \w+ to catch Transfer-Encoding: chunked
169: // TODO: may need to be extended to allow for other header values with non-word contents
170: String expression = "^" + headerName + ":\\s+(\\w+)"; // $NON-NLS-1$ $NON-NLS-2$
171: Pattern pattern = JMeterUtils.getPattern(expression,
172: Perl5Compiler.READ_ONLY_MASK
173: | Perl5Compiler.CASE_INSENSITIVE_MASK
174: | Perl5Compiler.MULTILINE_MASK);
175: if (localMatcher.contains(requestHeaders, pattern)) {
176: // The value is in the first group, group 0 is the whole match
177: return localMatcher.getMatch().group(1);
178: } else {
179: return null;
180: }
181: }
182:
183: private static int getPositionOfBody(String stringToCheck) {
184: Perl5Matcher localMatcher = JMeterUtils.getMatcher();
185: // The headers and body are divided by a blank line (the \r is to allow for the CR before LF)
186: String regularExpression = "^\\r$"; // $NON-NLS-1$
187: Pattern pattern = JMeterUtils.getPattern(regularExpression,
188: Perl5Compiler.READ_ONLY_MASK
189: | Perl5Compiler.CASE_INSENSITIVE_MASK
190: | Perl5Compiler.MULTILINE_MASK);
191:
192: PatternMatcherInput input = new PatternMatcherInput(
193: stringToCheck);
194: if (localMatcher.contains(input, pattern)) {
195: MatchResult match = localMatcher.getMatch();
196: return match.beginOffset(0);
197: }
198: // No divider was found
199: return -1;
200: }
201: }
|