blob: 34cc36b6393f30b20d4c761f32a995c68fcbd894 [file] [log] [blame]
/*
* Copyright 1999-2004 The Apache Software Foundation
*
* Licensed 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.jk.common;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.CharConversionException;
import java.net.InetAddress;
import java.util.Properties;
import javax.management.ObjectName;
import org.apache.commons.modeler.Registry;
import org.apache.coyote.Request;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
import org.apache.coyote.Response;
import org.apache.coyote.Constants;
import org.apache.jk.core.JkHandler;
import org.apache.jk.core.Msg;
import org.apache.jk.core.MsgContext;
import org.apache.jk.core.WorkerEnv;
import org.apache.jk.core.JkChannel;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.CharChunk;
import org.apache.tomcat.util.buf.HexUtils;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.http.MimeHeaders;
import org.apache.tomcat.util.net.SSLSupport;
import org.apache.tomcat.util.threads.ThreadWithAttributes;
/**
* Handle messages related with basic request information.
*
* This object can handle the following incoming messages:
* - "FORWARD_REQUEST" input message ( sent when a request is passed from the
* web server )
* - "RECEIVE_BODY_CHUNK" input ( sent by container to pass more body, in
* response to GET_BODY_CHUNK )
*
* It can handle the following outgoing messages:
* - SEND_HEADERS. Pass the status code and headers.
* - SEND_BODY_CHUNK. Send a chunk of body
* - GET_BODY_CHUNK. Request a chunk of body data
* - END_RESPONSE. Notify the end of a request processing.
*
* @author Henri Gomez [hgomez@apache.org]
* @author Dan Milstein [danmil@shore.net]
* @author Keith Wannamaker [Keith@Wannamaker.org]
* @author Costin Manolache
*/
public class HandlerRequest extends JkHandler
{
private static org.apache.commons.logging.Log log=
org.apache.commons.logging.LogFactory.getLog( HandlerRequest.class );
// XXX Will move to a registry system.
// Prefix codes for message types from server to container
public static final byte JK_AJP13_FORWARD_REQUEST = 2;
public static final byte JK_AJP13_SHUTDOWN = 7;
public static final byte JK_AJP13_PING_REQUEST = 8;
public static final byte JK_AJP13_CPING_REQUEST = 10;
// Prefix codes for message types from container to server
public static final byte JK_AJP13_SEND_BODY_CHUNK = 3;
public static final byte JK_AJP13_SEND_HEADERS = 4;
public static final byte JK_AJP13_END_RESPONSE = 5;
public static final byte JK_AJP13_GET_BODY_CHUNK = 6;
public static final byte JK_AJP13_CPONG_REPLY = 9;
// Integer codes for common response header strings
public static final int SC_RESP_CONTENT_TYPE = 0xA001;
public static final int SC_RESP_CONTENT_LANGUAGE = 0xA002;
public static final int SC_RESP_CONTENT_LENGTH = 0xA003;
public static final int SC_RESP_DATE = 0xA004;
public static final int SC_RESP_LAST_MODIFIED = 0xA005;
public static final int SC_RESP_LOCATION = 0xA006;
public static final int SC_RESP_SET_COOKIE = 0xA007;
public static final int SC_RESP_SET_COOKIE2 = 0xA008;
public static final int SC_RESP_SERVLET_ENGINE = 0xA009;
public static final int SC_RESP_STATUS = 0xA00A;
public static final int SC_RESP_WWW_AUTHENTICATE = 0xA00B;
// Integer codes for common (optional) request attribute names
public static final byte SC_A_CONTEXT = 1; // XXX Unused
public static final byte SC_A_SERVLET_PATH = 2; // XXX Unused
public static final byte SC_A_REMOTE_USER = 3;
public static final byte SC_A_AUTH_TYPE = 4;
public static final byte SC_A_QUERY_STRING = 5;
public static final byte SC_A_JVM_ROUTE = 6;
public static final byte SC_A_SSL_CERT = 7;
public static final byte SC_A_SSL_CIPHER = 8;
public static final byte SC_A_SSL_SESSION = 9;
public static final byte SC_A_SSL_KEYSIZE = 11;
public static final byte SC_A_SECRET = 12;
public static final byte SC_A_STORED_METHOD = 13;
// Used for attributes which are not in the list above
public static final byte SC_A_REQ_ATTRIBUTE = 10;
// Terminates list of attributes
public static final byte SC_A_ARE_DONE = (byte)0xFF;
// Translates integer codes to names of HTTP methods
public static final String []methodTransArray = {
"OPTIONS",
"GET",
"HEAD",
"POST",
"PUT",
"DELETE",
"TRACE",
"PROPFIND",
"PROPPATCH",
"MKCOL",
"COPY",
"MOVE",
"LOCK",
"UNLOCK",
"ACL",
"REPORT",
"VERSION-CONTROL",
"CHECKIN",
"CHECKOUT",
"UNCHECKOUT",
"SEARCH",
"MKWORKSPACE",
"UPDATE",
"LABEL",
"MERGE",
"BASELINE-CONTROL",
"MKACTIVITY"
};
public static final int SC_M_JK_STORED = (byte) 0xFF;
// id's for common request headers
public static final int SC_REQ_ACCEPT = 1;
public static final int SC_REQ_ACCEPT_CHARSET = 2;
public static final int SC_REQ_ACCEPT_ENCODING = 3;
public static final int SC_REQ_ACCEPT_LANGUAGE = 4;
public static final int SC_REQ_AUTHORIZATION = 5;
public static final int SC_REQ_CONNECTION = 6;
public static final int SC_REQ_CONTENT_TYPE = 7;
public static final int SC_REQ_CONTENT_LENGTH = 8;
public static final int SC_REQ_COOKIE = 9;
public static final int SC_REQ_COOKIE2 = 10;
public static final int SC_REQ_HOST = 11;
public static final int SC_REQ_PRAGMA = 12;
public static final int SC_REQ_REFERER = 13;
public static final int SC_REQ_USER_AGENT = 14;
// AJP14 new header
public static final byte SC_A_SSL_KEY_SIZE = 11; // XXX ???
// Translates integer codes to request header names
public static final String []headerTransArray = {
"accept",
"accept-charset",
"accept-encoding",
"accept-language",
"authorization",
"connection",
"content-type",
"content-length",
"cookie",
"cookie2",
"host",
"pragma",
"referer",
"user-agent"
};
/*
* Note for Host parsing.
*/
public static final int HOSTBUFFER = 10;
HandlerDispatch dispatch;
String ajpidDir="conf";
public HandlerRequest()
{
}
public void init() {
dispatch=(HandlerDispatch)wEnv.getHandler( "dispatch" );
if( dispatch != null ) {
// register incoming message handlers
dispatch.registerMessageType( JK_AJP13_FORWARD_REQUEST,
"JK_AJP13_FORWARD_REQUEST",
this, null); // 2
dispatch.registerMessageType( JK_AJP13_SHUTDOWN,
"JK_AJP13_SHUTDOWN",
this, null); // 7
dispatch.registerMessageType( JK_AJP13_CPING_REQUEST,
"JK_AJP13_CPING_REQUEST",
this, null); // 10
dispatch.registerMessageType( HANDLE_THREAD_END,
"HANDLE_THREAD_END",
this, null);
// register outgoing messages handler
dispatch.registerMessageType( JK_AJP13_SEND_BODY_CHUNK, // 3
"JK_AJP13_SEND_BODY_CHUNK",
this,null );
}
bodyNote=wEnv.getNoteId( WorkerEnv.ENDPOINT_NOTE, "jkInputStream" );
tmpBufNote=wEnv.getNoteId( WorkerEnv.ENDPOINT_NOTE, "tmpBuf" );
secretNote=wEnv.getNoteId( WorkerEnv.ENDPOINT_NOTE, "secret" );
if( next==null )
next=wEnv.getHandler( "container" );
if( log.isDebugEnabled() )
log.debug( "Container handler " + next + " " + next.getName() +
" " + next.getClass().getName());
// should happen on start()
generateAjp13Id();
}
public void setSecret( String s ) {
requiredSecret=s;
}
public void setUseSecret( boolean b ) {
requiredSecret=Double.toString(Math.random());
}
public void setDecodedUri( boolean b ) {
decoded=b;
}
public boolean isTomcatAuthentication() {
return tomcatAuthentication;
}
public void setTomcatAuthentication(boolean newTomcatAuthentication) {
tomcatAuthentication = newTomcatAuthentication;
}
public void setAjpidDir( String path ) {
if( "".equals( path ) ) path=null;
ajpidDir=path;
}
/**
* Set the flag to tell if we JMX register requests.
*/
public void setRegisterRequests(boolean srr) {
registerRequests = srr;
}
/**
* Get the flag to tell if we JMX register requests.
*/
public boolean getRegisterRequests() {
return registerRequests;
}
// -------------------- Ajp13.id --------------------
private void generateAjp13Id() {
int portInt=8009; // tcpCon.getPort();
InetAddress address=null; // tcpCon.getAddress();
if( requiredSecret == null )
return;
File f1=new File( wEnv.getJkHome() );
File f2=new File( f1, "conf" );
if( ! f2.exists() ) {
log.error( "No conf dir for ajp13.id " + f2 );
return;
}
File sf=new File( f2, "ajp13.id");
if( log.isDebugEnabled())
log.debug( "Using stop file: "+sf);
try {
Properties props=new Properties();
props.put( "port", Integer.toString( portInt ));
if( address!=null ) {
props.put( "address", address.getHostAddress() );
}
if( requiredSecret !=null ) {
props.put( "secret", requiredSecret );
}
FileOutputStream stopF=new FileOutputStream( sf );
props.save( stopF, "Automatically generated, don't edit" );
} catch( IOException ex ) {
log.debug( "Can't create stop file: "+sf );
ex.printStackTrace();
}
}
// -------------------- Incoming message --------------------
String requiredSecret=null;
int bodyNote;
int tmpBufNote;
int secretNote;
boolean decoded=true;
boolean tomcatAuthentication=true;
boolean registerRequests=true;
public int invoke(Msg msg, MsgContext ep )
throws IOException
{
int type=msg.getByte();
ThreadWithAttributes twa = null;
if (Thread.currentThread() instanceof ThreadWithAttributes) {
twa = (ThreadWithAttributes) Thread.currentThread();
}
Object control=ep.getControl();
MessageBytes tmpMB=(MessageBytes)ep.getNote( tmpBufNote );
if( tmpMB==null ) {
tmpMB=new MessageBytes();
ep.setNote( tmpBufNote, tmpMB);
}
if( log.isDebugEnabled() )
log.debug( "Handling " + type );
switch( type ) {
case JK_AJP13_FORWARD_REQUEST:
try {
if (twa != null) {
twa.setCurrentStage(control, "JkDecode");
}
decodeRequest( msg, ep, tmpMB );
if (twa != null) {
twa.setCurrentStage(control, "JkService");
twa.setParam(control,
((Request)ep.getRequest()).unparsedURI());
}
} catch( Exception ex ) {
log.error( "Error decoding request ", ex );
msg.dump( "Incomming message");
return ERROR;
}
if( requiredSecret != null ) {
String epSecret=(String)ep.getNote( secretNote );
if( epSecret==null || ! requiredSecret.equals( epSecret ) )
return ERROR;
}
/* XXX it should be computed from request, by workerEnv */
if(log.isDebugEnabled() )
log.debug("Calling next " + next.getName() + " " +
next.getClass().getName());
int err= next.invoke( msg, ep );
if (twa != null) {
twa.setCurrentStage(control, "JkDone");
}
if( log.isDebugEnabled() )
log.debug( "Invoke returned " + err );
return err;
case JK_AJP13_SHUTDOWN:
String epSecret=null;
if( msg.getLen() > 3 ) {
// we have a secret
msg.getBytes( tmpMB );
epSecret=tmpMB.toString();
}
if( requiredSecret != null &&
requiredSecret.equals( epSecret ) ) {
if( log.isDebugEnabled() )
log.debug("Received wrong secret, no shutdown ");
return ERROR;
}
// XXX add isSameAddress check
JkChannel ch=ep.getSource();
if( !ch.isSameAddress(ep) ) {
log.error("Shutdown request not from 'same address' ");
return ERROR;
}
// forward to the default handler - it'll do the shutdown
next.invoke( msg, ep );
log.info("Exiting");
System.exit(0);
return OK;
// We got a PING REQUEST, quickly respond with a PONG
case JK_AJP13_CPING_REQUEST:
msg.reset();
msg.appendByte(JK_AJP13_CPONG_REPLY);
ep.setType( JkHandler.HANDLE_SEND_PACKET );
ep.getSource().send( msg, ep );
return OK;
case HANDLE_THREAD_END:
return OK;
default:
log.info("Unknown message " + type);
}
return OK;
}
static int count = 0;
private int decodeRequest( Msg msg, MsgContext ep, MessageBytes tmpMB )
throws IOException
{
// FORWARD_REQUEST handler
Request req=(Request)ep.getRequest();
if( req==null ) {
req=new Request();
Response res=new Response();
req.setResponse(res);
ep.setRequest( req );
if( registerRequests ) {
ep.getSource().registerRequest(req, ep, count++);
}
}
RequestInfo rp = req.getRequestProcessor();
rp.setStage(Constants.STAGE_PARSE);
MessageBytes tmpMB2 = (MessageBytes)req.getNote(WorkerEnv.SSL_CERT_NOTE);
if(tmpMB2 != null) {
tmpMB2.recycle();
}
req.setStartTime(System.currentTimeMillis());
JkInputStream jkBody=(JkInputStream)ep.getNote( bodyNote );
if( jkBody==null ) {
jkBody=new JkInputStream();
jkBody.setMsgContext( ep );
ep.setNote( bodyNote, jkBody );
}
jkBody.recycle();
// Translate the HTTP method code to a String.
byte methodCode = msg.getByte();
if (methodCode != SC_M_JK_STORED) {
String mName=methodTransArray[(int)methodCode - 1];
req.method().setString(mName);
}
msg.getBytes(req.protocol());
msg.getBytes(req.requestURI());
msg.getBytes(req.remoteAddr());
msg.getBytes(req.remoteHost());
msg.getBytes(req.localName());
req.setLocalPort(msg.getInt());
boolean isSSL = msg.getByte() != 0;
if( isSSL ) {
// XXX req.setSecure( true );
req.scheme().setString("https");
}
decodeHeaders( ep, msg, req, tmpMB );
decodeAttributes( ep, msg, req, tmpMB );
rp.setStage(Constants.STAGE_PREPARE);
MessageBytes valueMB = req.getMimeHeaders().getValue("host");
parseHost(valueMB, req);
// set cookies on request now that we have all headers
req.getCookies().setHeaders(req.getMimeHeaders());
// Check to see if there should be a body packet coming along
// immediately after
int cl=req.getContentLength();
if(cl > 0) {
jkBody.setContentLength( cl );
jkBody.receive();
}
if (log.isTraceEnabled()) {
log.trace(req.toString());
}
return OK;
}
private int decodeAttributes( MsgContext ep, Msg msg, Request req,
MessageBytes tmpMB) {
boolean moreAttr=true;
while( moreAttr ) {
byte attributeCode=msg.getByte();
if( attributeCode == SC_A_ARE_DONE )
return 200;
/* Special case ( XXX in future API make it separate type !)
*/
if( attributeCode == SC_A_SSL_KEY_SIZE ) {
// Bug 1326: it's an Integer.
req.setAttribute(SSLSupport.KEY_SIZE_KEY,
new Integer( msg.getInt()));
//Integer.toString(msg.getInt()));
}
if( attributeCode == SC_A_REQ_ATTRIBUTE ) {
// 2 strings ???...
msg.getBytes( tmpMB );
String n=tmpMB.toString();
msg.getBytes( tmpMB );
String v=tmpMB.toString();
req.setAttribute(n, v );
}
// 1 string attributes
switch(attributeCode) {
case SC_A_CONTEXT :
msg.getBytes( tmpMB );
// nothing
break;
case SC_A_SERVLET_PATH :
msg.getBytes( tmpMB );
// nothing
break;
case SC_A_REMOTE_USER :
if( tomcatAuthentication ) {
// ignore server
msg.getBytes( tmpMB );
} else {
msg.getBytes(req.getRemoteUser());
}
break;
case SC_A_AUTH_TYPE :
if( tomcatAuthentication ) {
// ignore server
msg.getBytes( tmpMB );
} else {
msg.getBytes(req.getAuthType());
}
break;
case SC_A_QUERY_STRING :
msg.getBytes(req.queryString());
break;
case SC_A_JVM_ROUTE :
msg.getBytes(req.instanceId());
break;
case SC_A_SSL_CERT :
req.scheme().setString( "https" );
// Transform the string into certificate.
MessageBytes tmpMB2 = (MessageBytes)req.getNote(WorkerEnv.SSL_CERT_NOTE);
if(tmpMB2 == null) {
tmpMB2 = new MessageBytes();
req.setNote(WorkerEnv.SSL_CERT_NOTE, tmpMB2);
}
// SSL certificate extraction is costy, moved to JkCoyoteHandler
msg.getBytes(tmpMB2);
break;
case SC_A_SSL_CIPHER :
req.scheme().setString( "https" );
msg.getBytes(tmpMB);
req.setAttribute(SSLSupport.CIPHER_SUITE_KEY,
tmpMB.toString());
break;
case SC_A_SSL_SESSION :
req.scheme().setString( "https" );
msg.getBytes(tmpMB);
req.setAttribute(SSLSupport.SESSION_ID_KEY,
tmpMB.toString());
break;
case SC_A_SECRET :
msg.getBytes(tmpMB);
String secret=tmpMB.toString();
log.info("Secret: " + secret );
// endpoint note
ep.setNote( secretNote, secret );
break;
case SC_A_STORED_METHOD:
msg.getBytes(req.method());
break;
default:
break; // ignore, we don't know about it - backward compat
}
}
return 200;
}
private void decodeHeaders( MsgContext ep, Msg msg, Request req,
MessageBytes tmpMB ) {
// Decode headers
MimeHeaders headers = req.getMimeHeaders();
int hCount = msg.getInt();
for(int i = 0 ; i < hCount ; i++) {
String hName = null;
// Header names are encoded as either an integer code starting
// with 0xA0, or as a normal string (in which case the first
// two bytes are the length).
int isc = msg.peekInt();
int hId = isc & 0xFF;
MessageBytes vMB=null;
isc &= 0xFF00;
if(0xA000 == isc) {
msg.getInt(); // To advance the read position
hName = headerTransArray[hId - 1];
vMB=headers.addValue( hName );
} else {
// reset hId -- if the header currently being read
// happens to be 7 or 8 bytes long, the code below
// will think it's the content-type header or the
// content-length header - SC_REQ_CONTENT_TYPE=7,
// SC_REQ_CONTENT_LENGTH=8 - leading to unexpected
// behaviour. see bug 5861 for more information.
hId = -1;
msg.getBytes( tmpMB );
ByteChunk bc=tmpMB.getByteChunk();
//hName=tmpMB.toString();
// vMB=headers.addValue( hName );
vMB=headers.addValue( bc.getBuffer(),
bc.getStart(), bc.getLength() );
}
msg.getBytes(vMB);
if (hId == SC_REQ_CONTENT_LENGTH ||
tmpMB.equalsIgnoreCase("Content-Length")) {
// just read the content-length header, so set it
int contentLength = (vMB == null) ? -1 : vMB.getInt();
req.setContentLength(contentLength);
} else if (hId == SC_REQ_CONTENT_TYPE ||
tmpMB.equalsIgnoreCase("Content-Type")) {
// just read the content-type header, so set it
ByteChunk bchunk = vMB.getByteChunk();
req.contentType().setBytes(bchunk.getBytes(),
bchunk.getOffset(),
bchunk.getLength());
}
}
}
/**
* Parse host.
*/
private void parseHost(MessageBytes valueMB, Request request)
throws IOException {
if (valueMB == null || valueMB.isNull()) {
// HTTP/1.0
// Default is what the socket tells us. Overriden if a host is
// found/parsed
request.setServerPort(request.getLocalPort());
request.serverName().duplicate(request.localName());
return;
}
ByteChunk valueBC = valueMB.getByteChunk();
byte[] valueB = valueBC.getBytes();
int valueL = valueBC.getLength();
int valueS = valueBC.getStart();
int colonPos = -1;
CharChunk hostNameC = (CharChunk)request.getNote(HOSTBUFFER);
if(hostNameC == null) {
hostNameC = new CharChunk(valueL);
request.setNote(HOSTBUFFER, hostNameC);
}
hostNameC.recycle();
boolean ipv6 = (valueB[valueS] == '[');
boolean bracketClosed = false;
for (int i = 0; i < valueL; i++) {
char b = (char) valueB[i + valueS];
hostNameC.append(b);
if (b == ']') {
bracketClosed = true;
} else if (b == ':') {
if (!ipv6 || bracketClosed) {
colonPos = i;
break;
}
}
}
if (colonPos < 0) {
if (request.scheme().equalsIgnoreCase("https")) {
// 80 - Default HTTTP port
request.setServerPort(443);
} else {
// 443 - Default HTTPS port
request.setServerPort(80);
}
request.serverName().setChars(hostNameC.getChars(),
hostNameC.getStart(),
hostNameC.getLength());
} else {
request.serverName().setChars(hostNameC.getChars(),
hostNameC.getStart(), colonPos);
int port = 0;
int mult = 1;
for (int i = valueL - 1; i > colonPos; i--) {
int charValue = HexUtils.DEC[(int) valueB[i + valueS]];
if (charValue == -1) {
// Invalid character
throw new CharConversionException("Invalid char in port: " + valueB[i + valueS]);
}
port = port + (charValue * mult);
mult = 10 * mult;
}
request.setServerPort(port);
}
}
}