Author: remm
Date: Fri Apr 28 06:22:08 2006
New Revision: 397895

URL: http://svn.apache.org/viewcvs?rev=397895&view=rev
Log:
- Port (not tested yet) the APR AJP code to java.io. I don't think it will be 
used in Tomcat unless
  people configure it explicitly.

Added:
    tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpProcessor.java   (with 
props)
    tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpProtocol.java   (with 
props)
Modified:
    tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpAprProtocol.java

Modified: tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpAprProtocol.java
URL: 
http://svn.apache.org/viewcvs/tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpAprProtocol.java?rev=397895&r1=397894&r2=397895&view=diff
==============================================================================
--- tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpAprProtocol.java 
(original)
+++ tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpAprProtocol.java Fri Apr 
28 06:22:08 2006
@@ -20,6 +20,7 @@
 import java.net.URLEncoder;
 import java.util.Hashtable;
 import java.util.Iterator;
+import java.util.concurrent.Executor;
 
 import javax.management.MBeanRegistration;
 import javax.management.MBeanServer;
@@ -250,6 +251,16 @@
     }
 
 
+    // *
+    public Executor getExecutor() {
+        return ep.getExecutor();
+    }
+    
+    // *
+    public void setExecutor(Executor executor) {
+        ep.setExecutor(executor);
+    }
+    
     public int getMaxThreads() {
         return ep.getMaxThreads();
     }

Added: tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpProcessor.java
URL: 
http://svn.apache.org/viewcvs/tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpProcessor.java?rev=397895&view=auto
==============================================================================
--- tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpProcessor.java (added)
+++ tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpProcessor.java Fri Apr 
28 06:22:08 2006
@@ -0,0 +1,1177 @@
+/*
+ *  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.coyote.ajp;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+import org.apache.coyote.ActionCode;
+import org.apache.coyote.ActionHook;
+import org.apache.coyote.Adapter;
+import org.apache.coyote.InputBuffer;
+import org.apache.coyote.OutputBuffer;
+import org.apache.coyote.Request;
+import org.apache.coyote.RequestInfo;
+import org.apache.coyote.Response;
+import org.apache.tomcat.util.buf.ByteChunk;
+import org.apache.tomcat.util.buf.HexUtils;
+import org.apache.tomcat.util.buf.MessageBytes;
+import org.apache.tomcat.util.http.HttpMessages;
+import org.apache.tomcat.util.http.MimeHeaders;
+import org.apache.tomcat.util.net.JIoEndpoint;
+import org.apache.tomcat.util.res.StringManager;
+import org.apache.tomcat.util.threads.ThreadWithAttributes;
+
+
+/**
+ * Processes HTTP requests.
+ *
+ * @author Remy Maucherat
+ * @author Henri Gomez
+ * @author Dan Milstein
+ * @author Keith Wannamaker
+ * @author Kevin Seguin
+ * @author Costin Manolache
+ * @author Bill Barker
+ */
+public class AjpProcessor implements ActionHook {
+
+
+    /**
+     * Logger.
+     */
+    protected static org.apache.commons.logging.Log log
+        = org.apache.commons.logging.LogFactory.getLog(AjpProcessor.class);
+
+    /**
+     * The string manager for this package.
+     */
+    protected static StringManager sm =
+        StringManager.getManager(Constants.Package);
+
+
+    // ----------------------------------------------------------- Constructors
+
+
+    public AjpProcessor(JIoEndpoint endpoint) {
+
+        this.endpoint = endpoint;
+
+        request = new Request();
+        request.setInputBuffer(new SocketInputBuffer());
+
+        response = new Response();
+        response.setHook(this);
+        response.setOutputBuffer(new SocketOutputBuffer());
+        request.setResponse(response);
+
+        // Cause loading of HexUtils
+        int foo = HexUtils.DEC[0];
+
+        // Cause loading of HttpMessages
+        HttpMessages.getMessage(200);
+
+    }
+
+
+    // ----------------------------------------------------- Instance Variables
+
+
+    /**
+     * Associated adapter.
+     */
+    protected Adapter adapter = null;
+
+
+    /**
+     * Request object.
+     */
+    protected Request request = null;
+
+
+    /**
+     * Response object.
+     */
+    protected Response response = null;
+
+
+    /**
+     * Header message. Note that this header is merely the one used during the
+     * processing of the first message of a "request", so it might not be a 
request
+     * header. It will stay unchanged during the processing of the whole 
request.
+     */
+    protected AjpMessage requestHeaderMessage = new AjpMessage();
+
+
+    /**
+     * Message used for response header composition.
+     */
+    protected AjpMessage responseHeaderMessage = new AjpMessage();
+
+
+    /**
+     * Body message.
+     */
+    protected AjpMessage bodyMessage = new AjpMessage();
+
+
+    /**
+     * Body message.
+     */
+    protected MessageBytes bodyBytes = MessageBytes.newInstance();
+
+
+    /**
+     * State flag.
+     */
+    protected boolean started = false;
+
+
+    /**
+     * Error flag.
+     */
+    protected boolean error = false;
+
+
+    /**
+     * Socket associated with the current connection.
+     */
+    protected Socket socket;
+
+    
+    /**
+     * Input stream.
+     */
+    protected InputStream input;
+    
+    
+    /**
+     * Output stream.
+     */
+    protected OutputStream output;
+    
+
+    /**
+     * Host name (used to avoid useless B2C conversion on the host name).
+     */
+    protected char[] hostNameC = new char[0];
+
+
+    /**
+     * Associated endpoint.
+     */
+    protected JIoEndpoint endpoint;
+
+
+    /**
+     * The socket timeout used when reading the first block of the request
+     * header.
+     */
+    protected long readTimeout;
+
+
+    /**
+     * Temp message bytes used for processing.
+     */
+    protected MessageBytes tmpMB = MessageBytes.newInstance();
+
+
+    /**
+     * Byte chunk for certs.
+     */
+    protected MessageBytes certificates = MessageBytes.newInstance();
+
+
+    /**
+     * End of stream flag.
+     */
+    protected boolean endOfStream = false;
+
+
+    /**
+     * Body empty flag.
+     */
+    protected boolean empty = true;
+
+
+    /**
+     * First read.
+     */
+    protected boolean first = true;
+
+
+    /**
+     * Replay read.
+     */
+    protected boolean replay = false;
+
+
+    /**
+     * Finished response.
+     */
+    protected boolean finished = false;
+
+
+    /**
+     * Direct buffer used for sending right away a get body message.
+     */
+    protected static final byte[] getBodyMessageArray;
+
+
+    /**
+     * Direct buffer used for sending right away a pong message.
+     */
+    protected static final byte[] pongMessageArray;
+
+
+    /**
+     * End message array.
+     */
+    protected static final byte[] endMessageArray;
+
+
+    // ----------------------------------------------------- Static Initializer
+
+
+    static {
+
+        // Set the get body message buffer
+        AjpMessage getBodyMessage = new AjpMessage();
+        getBodyMessage.reset();
+        getBodyMessage.appendByte(Constants.JK_AJP13_GET_BODY_CHUNK);
+        getBodyMessage.appendInt(Constants.MAX_READ_SIZE);
+        getBodyMessage.end();
+        getBodyMessageArray = new byte[getBodyMessage.getLen()];
+        System.arraycopy(getBodyMessage.getBuffer(), 0, getBodyMessageArray, 
+                0, getBodyMessage.getLen());
+
+        // Set the read body message buffer
+        AjpMessage pongMessage = new AjpMessage();
+        pongMessage.reset();
+        pongMessage.appendByte(Constants.JK_AJP13_CPONG_REPLY);
+        pongMessage.end();
+        pongMessageArray = new byte[pongMessage.getLen()];
+        System.arraycopy(pongMessage.getBuffer(), 0, pongMessageArray, 
+                0, pongMessage.getLen());
+
+        // Allocate the end message array
+        AjpMessage endMessage = new AjpMessage();
+        endMessage.reset();
+        endMessage.appendByte(Constants.JK_AJP13_END_RESPONSE);
+        endMessage.appendByte(1);
+        endMessage.end();
+        endMessageArray = new byte[endMessage.getLen()];
+        System.arraycopy(endMessage.getBuffer(), 0, endMessageArray, 0,
+                endMessage.getLen());
+
+    }
+
+
+    // ------------------------------------------------------------- Properties
+
+
+    /**
+     * Use Tomcat authentication ?
+     */
+    protected boolean tomcatAuthentication = true;
+    public boolean getTomcatAuthentication() { return tomcatAuthentication; }
+    public void setTomcatAuthentication(boolean tomcatAuthentication) { 
this.tomcatAuthentication = tomcatAuthentication; }
+
+
+    /**
+     * Required secret.
+     */
+    protected String requiredSecret = null;
+    public void setRequiredSecret(String requiredSecret) { this.requiredSecret 
= requiredSecret; }
+
+
+    // --------------------------------------------------------- Public Methods
+
+
+    /** Get the request associated with this processor.
+     *
+     * @return The request
+     */
+    public Request getRequest() {
+        return request;
+    }
+
+
+    /**
+     * Process pipelined HTTP requests using the specified input and output
+     * streams.
+     *
+     * @throws IOException error during an I/O operation
+     */
+    public boolean process(Socket socket)
+        throws IOException {
+        ThreadWithAttributes thrA=
+                (ThreadWithAttributes)Thread.currentThread();
+        RequestInfo rp = request.getRequestProcessor();
+        thrA.setCurrentStage(endpoint, "parsing http request");
+        rp.setStage(org.apache.coyote.Constants.STAGE_PARSE);
+
+        // Setting up the socket
+        this.socket = socket;
+        input = socket.getInputStream();
+        output = socket.getOutputStream();
+
+        // Error flag
+        error = false;
+
+        long soTimeout = endpoint.getSoTimeout();
+
+        while (started && !error) {
+
+            // Parsing the request header
+            try {
+                // Get first message of the request
+                if (!readMessage(requestHeaderMessage)) {
+                    // This means a connection timeout
+                    rp.setStage(org.apache.coyote.Constants.STAGE_ENDED);
+                    break;
+                }
+                // Check message type, process right away and break if
+                // not regular request processing
+                int type = requestHeaderMessage.getByte();
+                if (type == Constants.JK_AJP13_CPING_REQUEST) {
+                    try {
+                        output.write(pongMessageArray);
+                    } catch (IOException e) {
+                        error = true;
+                    }
+                    continue;
+                } else if(type != Constants.JK_AJP13_FORWARD_REQUEST) {
+                    // Usually the servlet didn't read the previous request 
body
+                    if(log.isDebugEnabled()) {
+                        log.debug("Unexpected message: "+type);
+                    }
+                    continue;
+                }
+
+                request.setStartTime(System.currentTimeMillis());
+            } catch (IOException e) {
+                error = true;
+                break;
+            } catch (Throwable t) {
+                log.debug(sm.getString("ajpprocessor.header.error"), t);
+                // 400 - Bad Request
+                response.setStatus(400);
+                error = true;
+            }
+
+            // Setting up filters, and parse some request headers
+            thrA.setCurrentStage(endpoint, "prepareRequest");
+            rp.setStage(org.apache.coyote.Constants.STAGE_PREPARE);
+            try {
+                prepareRequest();
+                thrA.setParam(endpoint, request.requestURI());
+            } catch (Throwable t) {
+                log.debug(sm.getString("ajpprocessor.request.prepare"), t);
+                // 400 - Internal Server Error
+                response.setStatus(400);
+                error = true;
+            }
+
+            // Process the request in the adapter
+            if (!error) {
+                try {
+                    thrA.setCurrentStage(endpoint, "service");
+                    rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE);
+                    adapter.service(request, response);
+                } catch (InterruptedIOException e) {
+                    error = true;
+                } catch (Throwable t) {
+                    log.error(sm.getString("ajpprocessor.request.process"), t);
+                    // 500 - Internal Server Error
+                    response.setStatus(500);
+                    error = true;
+                }
+            }
+
+            // Finish the response if not done yet
+            if (!finished) {
+                try {
+                    finish();
+                } catch (Throwable t) {
+                    error = true;
+                }
+            }
+
+            // If there was an error, make sure the request is counted as
+            // and error, and update the statistics counter
+            if (error) {
+                response.setStatus(500);
+            }
+            request.updateCounters();
+
+            thrA.setCurrentStage(endpoint, "ended");
+            rp.setStage(org.apache.coyote.Constants.STAGE_KEEPALIVE);
+            recycle();
+
+        }
+
+        rp.setStage(org.apache.coyote.Constants.STAGE_ENDED);
+        recycle();
+
+        return true;
+
+    }
+
+
+    // ----------------------------------------------------- ActionHook Methods
+
+
+    /**
+     * Send an action to the connector.
+     *
+     * @param actionCode Type of the action
+     * @param param Action parameter
+     */
+    public void action(ActionCode actionCode, Object param) {
+
+        if (actionCode == ActionCode.ACTION_COMMIT) {
+
+            if (response.isCommitted())
+                return;
+
+            // Validate and write response headers
+            try {
+                prepareResponse();
+            } catch (IOException e) {
+                // Set error flag
+                error = true;
+            }
+
+        } else if (actionCode == ActionCode.ACTION_CLIENT_FLUSH) {
+
+            if (!response.isCommitted()) {
+                // Validate and write response headers
+                try {
+                    prepareResponse();
+                } catch (IOException e) {
+                    // Set error flag
+                    error = true;
+                    return;
+                }
+            }
+
+            try {
+                flush();
+            } catch (IOException e) {
+                // Set error flag
+                error = true;
+            }
+
+        } else if (actionCode == ActionCode.ACTION_CLOSE) {
+            // Close
+
+            // End the processing of the current request, and stop any further
+            // transactions with the client
+
+            try {
+                finish();
+            } catch (IOException e) {
+                // Set error flag
+                error = true;
+            }
+
+        } else if (actionCode == ActionCode.ACTION_START) {
+
+            started = true;
+
+        } else if (actionCode == ActionCode.ACTION_STOP) {
+
+            started = false;
+
+        } else if (actionCode == ActionCode.ACTION_REQ_SSL_ATTRIBUTE ) {
+
+            if (!certificates.isNull()) {
+                ByteChunk certData = certificates.getByteChunk();
+                X509Certificate jsseCerts[] = null;
+                ByteArrayInputStream bais =
+                    new ByteArrayInputStream(certData.getBytes(),
+                            certData.getStart(),
+                            certData.getLength());
+                // Fill the first element.
+                try {
+                    CertificateFactory cf =
+                        CertificateFactory.getInstance("X.509");
+                    X509Certificate cert = (X509Certificate)
+                    cf.generateCertificate(bais);
+                    jsseCerts = new X509Certificate[1];
+                    jsseCerts[0] = cert;
+                    request.setAttribute(JIoEndpoint.CERTIFICATE_KEY, 
jsseCerts);
+                } catch (java.security.cert.CertificateException e) {
+                    log.error(sm.getString("ajpprocessor.certs.fail"), e);
+                    return;
+                }
+            }
+
+        } else if (actionCode == ActionCode.ACTION_REQ_HOST_ATTRIBUTE) {
+
+            // Get remote host name using a DNS resolution
+            if (request.remoteHost().isNull()) {
+                try {
+                    request.remoteHost().setString(InetAddress.getByName
+                            (request.remoteAddr().toString()).getHostName());
+                } catch (IOException iex) {
+                    // Ignore
+                }
+            }
+
+        } else if (actionCode == ActionCode.ACTION_REQ_LOCAL_ADDR_ATTRIBUTE) {
+
+            // Copy from local name for now, which should simply be an address
+            request.localAddr().setString(request.localName().toString());
+
+        } else if (actionCode == ActionCode.ACTION_REQ_SET_BODY_REPLAY) {
+
+            // Set the given bytes as the content
+            ByteChunk bc = (ByteChunk) param;
+            bodyBytes.setBytes(bc.getBytes(), bc.getStart(), bc.getLength());
+            first = false;
+            empty = false;
+            replay = true;
+
+        }
+
+
+    }
+
+
+    // ------------------------------------------------------ Connector Methods
+
+
+    /**
+     * Set the associated adapter.
+     *
+     * @param adapter the new adapter
+     */
+    public void setAdapter(Adapter adapter) {
+        this.adapter = adapter;
+    }
+
+
+    /**
+     * Get the associated adapter.
+     *
+     * @return the associated adapter
+     */
+    public Adapter getAdapter() {
+        return adapter;
+    }
+
+
+    // ------------------------------------------------------ Protected Methods
+
+
+    /**
+     * After reading the request headers, we have to setup the request filters.
+     */
+    protected void prepareRequest() {
+
+        // Translate the HTTP method code to a String.
+        byte methodCode = requestHeaderMessage.getByte();
+        if (methodCode != Constants.SC_M_JK_STORED) {
+            String methodName = Constants.methodTransArray[(int)methodCode - 
1];
+            request.method().setString(methodName);
+        }
+
+        requestHeaderMessage.getBytes(request.protocol());
+        requestHeaderMessage.getBytes(request.requestURI());
+
+        requestHeaderMessage.getBytes(request.remoteAddr());
+        requestHeaderMessage.getBytes(request.remoteHost());
+        requestHeaderMessage.getBytes(request.localName());
+        request.setLocalPort(requestHeaderMessage.getInt());
+
+        boolean isSSL = requestHeaderMessage.getByte() != 0;
+        if (isSSL) {
+            request.scheme().setString("https");
+        }
+
+        // Decode headers
+        MimeHeaders headers = request.getMimeHeaders();
+
+        int hCount = requestHeaderMessage.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 = requestHeaderMessage.peekInt();
+            int hId = isc & 0xFF;
+
+            MessageBytes vMB = null;
+            isc &= 0xFF00;
+            if(0xA000 == isc) {
+                requestHeaderMessage.getInt(); // To advance the read position
+                hName = Constants.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;
+                requestHeaderMessage.getBytes(tmpMB);
+                ByteChunk bc = tmpMB.getByteChunk();
+                vMB = headers.addValue(bc.getBuffer(),
+                        bc.getStart(), bc.getLength());
+            }
+
+            requestHeaderMessage.getBytes(vMB);
+
+            if (hId == Constants.SC_REQ_CONTENT_LENGTH ||
+                    (hId == -1 && tmpMB.equalsIgnoreCase("Content-Length"))) {
+                // just read the content-length header, so set it
+                request.setContentLength( vMB.getInt() );
+            } else if (hId == Constants.SC_REQ_CONTENT_TYPE ||
+                    (hId == -1 && tmpMB.equalsIgnoreCase("Content-Type"))) {
+                // just read the content-type header, so set it
+                ByteChunk bchunk = vMB.getByteChunk();
+                request.contentType().setBytes(bchunk.getBytes(),
+                        bchunk.getOffset(),
+                        bchunk.getLength());
+            }
+        }
+
+        // Decode extra attributes
+        boolean secret = false;
+        byte attributeCode;
+        while ((attributeCode = requestHeaderMessage.getByte())
+                != Constants.SC_A_ARE_DONE) {
+
+            switch (attributeCode) {
+
+            case Constants.SC_A_REQ_ATTRIBUTE :
+                requestHeaderMessage.getBytes(tmpMB);
+                String n = tmpMB.toString();
+                requestHeaderMessage.getBytes(tmpMB);
+                String v = tmpMB.toString();
+                request.setAttribute(n, v);
+                break;
+
+            case Constants.SC_A_CONTEXT :
+                requestHeaderMessage.getBytes(tmpMB);
+                // nothing
+                break;
+
+            case Constants.SC_A_SERVLET_PATH :
+                requestHeaderMessage.getBytes(tmpMB);
+                // nothing
+                break;
+
+            case Constants.SC_A_REMOTE_USER :
+                if (tomcatAuthentication) {
+                    // ignore server
+                    requestHeaderMessage.getBytes(tmpMB);
+                } else {
+                    requestHeaderMessage.getBytes(request.getRemoteUser());
+                }
+                break;
+
+            case Constants.SC_A_AUTH_TYPE :
+                if (tomcatAuthentication) {
+                    // ignore server
+                    requestHeaderMessage.getBytes(tmpMB);
+                } else {
+                    requestHeaderMessage.getBytes(request.getAuthType());
+                }
+                break;
+
+            case Constants.SC_A_QUERY_STRING :
+                requestHeaderMessage.getBytes(request.queryString());
+                break;
+
+            case Constants.SC_A_JVM_ROUTE :
+                requestHeaderMessage.getBytes(request.instanceId());
+                break;
+
+            case Constants.SC_A_SSL_CERT :
+                request.scheme().setString("https");
+                // SSL certificate extraction is lazy, moved to JkCoyoteHandler
+                requestHeaderMessage.getBytes(certificates);
+                break;
+
+            case Constants.SC_A_SSL_CIPHER :
+                request.scheme().setString("https");
+                requestHeaderMessage.getBytes(tmpMB);
+                request.setAttribute(JIoEndpoint.CIPHER_SUITE_KEY,
+                                     tmpMB.toString());
+                break;
+
+            case Constants.SC_A_SSL_SESSION :
+                request.scheme().setString("https");
+                requestHeaderMessage.getBytes(tmpMB);
+                request.setAttribute(JIoEndpoint.SESSION_ID_KEY,
+                                     tmpMB.toString());
+                break;
+
+            case Constants.SC_A_SSL_KEY_SIZE :
+                request.setAttribute(JIoEndpoint.KEY_SIZE_KEY,
+                                     new 
Integer(requestHeaderMessage.getInt()));
+                break;
+
+            case Constants.SC_A_STORED_METHOD:
+                requestHeaderMessage.getBytes(request.method());
+                break;
+
+            case Constants.SC_A_SECRET:
+                requestHeaderMessage.getBytes(tmpMB);
+                if (requiredSecret != null) {
+                    secret = true;
+                    if (!tmpMB.equals(requiredSecret)) {
+                        response.setStatus(403);
+                        error = true;
+                    }
+                }
+                break;
+
+            default:
+                // Ignore unknown attribute for backward compatibility
+                break;
+
+            }
+
+        }
+
+        // Check if secret was submitted if required
+        if ((requiredSecret != null) && !secret) {
+            response.setStatus(403);
+            error = true;
+        }
+
+        // Check for a full URI (including protocol://host:port/)
+        ByteChunk uriBC = request.requestURI().getByteChunk();
+        if (uriBC.startsWithIgnoreCase("http", 0)) {
+
+            int pos = uriBC.indexOf("://", 0, 3, 4);
+            int uriBCStart = uriBC.getStart();
+            int slashPos = -1;
+            if (pos != -1) {
+                byte[] uriB = uriBC.getBytes();
+                slashPos = uriBC.indexOf('/', pos + 3);
+                if (slashPos == -1) {
+                    slashPos = uriBC.getLength();
+                    // Set URI as "/"
+                    request.requestURI().setBytes
+                        (uriB, uriBCStart + pos + 1, 1);
+                } else {
+                    request.requestURI().setBytes
+                        (uriB, uriBCStart + slashPos,
+                         uriBC.getLength() - slashPos);
+                }
+                MessageBytes hostMB = headers.setValue("host");
+                hostMB.setBytes(uriB, uriBCStart + pos + 3,
+                                slashPos - pos - 3);
+            }
+
+        }
+
+        MessageBytes valueMB = request.getMimeHeaders().getValue("host");
+        parseHost(valueMB);
+
+    }
+
+
+    /**
+     * Parse host.
+     */
+    public void parseHost(MessageBytes valueMB) {
+
+        if (valueMB == null || (valueMB != null && valueMB.isNull()) ) {
+            // HTTP/1.0
+            // Default is what the socket tells us. Overriden if a host is
+            // found/parsed
+            request.setServerPort(endpoint.getPort());
+            return;
+        }
+
+        ByteChunk valueBC = valueMB.getByteChunk();
+        byte[] valueB = valueBC.getBytes();
+        int valueL = valueBC.getLength();
+        int valueS = valueBC.getStart();
+        int colonPos = -1;
+        if (hostNameC.length < valueL) {
+            hostNameC = new char[valueL];
+        }
+
+        boolean ipv6 = (valueB[valueS] == '[');
+        boolean bracketClosed = false;
+        for (int i = 0; i < valueL; i++) {
+            char b = (char) valueB[i + valueS];
+            hostNameC[i] = b;
+            if (b == ']') {
+                bracketClosed = true;
+            } else if (b == ':') {
+                if (!ipv6 || bracketClosed) {
+                    colonPos = i;
+                    break;
+                }
+            }
+        }
+
+        if (colonPos < 0) {
+            if (request.scheme().equalsIgnoreCase("https")) {
+                // 443 - Default HTTPS port
+                request.setServerPort(443);
+            } else {
+                // 80 - Default HTTTP port
+                request.setServerPort(80);
+            }
+            request.serverName().setChars(hostNameC, 0, valueL);
+        } else {
+
+            request.serverName().setChars(hostNameC, 0, 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
+                    error = true;
+                    // 400 - Bad request
+                    response.setStatus(400);
+                    break;
+                }
+                port = port + (charValue * mult);
+                mult = 10 * mult;
+            }
+            request.setServerPort(port);
+
+        }
+
+    }
+
+
+    /**
+     * When committing the response, we have to validate the set of headers, as
+     * well as setup the response filters.
+     */
+    protected void prepareResponse()
+        throws IOException {
+
+        response.setCommitted(true);
+
+        responseHeaderMessage.reset();
+        responseHeaderMessage.appendByte(Constants.JK_AJP13_SEND_HEADERS);
+
+        // HTTP header contents
+        responseHeaderMessage.appendInt(response.getStatus());
+        String message = response.getMessage();
+        if (message == null){
+            message = HttpMessages.getMessage(response.getStatus());
+        } else {
+            message = message.replace('\n', ' ').replace('\r', ' ');
+        }
+        tmpMB.setString(message);
+        responseHeaderMessage.appendBytes(tmpMB);
+
+        // Special headers
+        MimeHeaders headers = response.getMimeHeaders();
+        String contentType = response.getContentType();
+        if (contentType != null) {
+            headers.setValue("Content-Type").setString(contentType);
+        }
+        String contentLanguage = response.getContentLanguage();
+        if (contentLanguage != null) {
+            headers.setValue("Content-Language").setString(contentLanguage);
+        }
+        int contentLength = response.getContentLength();
+        if (contentLength >= 0) {
+            headers.setValue("Content-Length").setInt(contentLength);
+        }
+
+        // Other headers
+        int numHeaders = headers.size();
+        responseHeaderMessage.appendInt(numHeaders);
+        for (int i = 0; i < numHeaders; i++) {
+            MessageBytes hN = headers.getName(i);
+            responseHeaderMessage.appendBytes(hN);
+            MessageBytes hV=headers.getValue(i);
+            responseHeaderMessage.appendBytes(hV);
+        }
+
+        // Write to buffer
+        responseHeaderMessage.end();
+        output.write(responseHeaderMessage.getBuffer(), 0, 
responseHeaderMessage.getLen());
+
+    }
+
+
+    /**
+     * Finish AJP response.
+     */
+    protected void finish()
+        throws IOException {
+
+        if (!response.isCommitted()) {
+            // Validate and write response headers
+            try {
+                prepareResponse();
+            } catch (IOException e) {
+                // Set error flag
+                error = true;
+            }
+        }
+
+        if (finished)
+            return;
+
+        finished = true;
+
+        // Add the end message
+        output.write(endMessageArray);
+
+    }
+
+
+    /**
+     * Read at least the specified amount of bytes, and place them
+     * in the input buffer.
+     */
+    protected boolean read(byte[] buf, int pos, int n)
+        throws IOException {
+
+        int read = 0;
+        int res = 0;
+        while (read < n) {
+            res = input.read(buf, read + pos, n - read);
+            if (res > 0) {
+                read += res;
+            } else {
+                throw new IOException(sm.getString("ajpprotocol.failedread"));
+            }
+        }
+        
+        return true;
+
+    }
+
+
+    /** Receive a chunk of data. Called to implement the
+     *  'special' packet in ajp13 and to receive the data
+     *  after we send a GET_BODY packet
+     */
+    public boolean receive() throws IOException {
+
+        first = false;
+        bodyMessage.reset();
+        readMessage(bodyMessage);
+
+        // No data received.
+        if (bodyMessage.getLen() == 0) {
+            // just the header
+            // Don't mark 'end of stream' for the first chunk.
+            return false;
+        }
+        int blen = bodyMessage.peekInt();
+        if (blen == 0) {
+            return false;
+        }
+
+        bodyMessage.getBytes(bodyBytes);
+        empty = false;
+        return true;
+    }
+
+    /**
+     * Get more request body data from the web server and store it in the
+     * internal buffer.
+     *
+     * @return true if there is more data, false if not.
+     */
+    private boolean refillReadBuffer() throws IOException {
+        // If the server returns an empty packet, assume that that end of
+        // the stream has been reached (yuck -- fix protocol??).
+        // FORM support
+        if (replay) {
+            endOfStream = true; // we've read everything there is
+        }
+        if (endOfStream) {
+            return false;
+        }
+
+        // Request more data immediately
+        output.write(getBodyMessageArray);
+
+        boolean moreData = receive();
+        if( !moreData ) {
+            endOfStream = true;
+        }
+        return moreData;
+    }
+
+
+    /**
+     * Read an AJP message.
+     *
+     * @return true if the message has been read, false if the short read
+     *         didn't return anything
+     * @throws IOException any other failure, including incomplete reads
+     */
+    protected boolean readMessage(AjpMessage message)
+        throws IOException {
+
+        byte[] buf = message.getBuffer();
+
+        read(buf, 0, message.getHeaderLength());
+
+        message.processHeader();
+        read(buf, message.getHeaderLength(), message.getLen());
+
+        return true;
+
+    }
+
+
+    /**
+     * Recycle the processor.
+     */
+    public void recycle() {
+
+        // Recycle Request object
+        first = true;
+        endOfStream = false;
+        empty = true;
+        replay = false;
+        finished = false;
+        request.recycle();
+        response.recycle();
+        certificates.recycle();
+
+        input = null;
+        output = null;
+        
+    }
+
+
+    /**
+     * Callback to write data from the buffer.
+     */
+    protected void flush()
+        throws IOException {
+        // Note: at the moment, there's no buffering at the socket level
+    }
+
+
+    // ------------------------------------- InputStreamInputBuffer Inner Class
+
+
+    /**
+     * This class is an input buffer which will read its data from an input
+     * stream.
+     */
+    protected class SocketInputBuffer
+        implements InputBuffer {
+
+
+        /**
+         * Read bytes into the specified chunk.
+         */
+        public int doRead(ByteChunk chunk, Request req )
+            throws IOException {
+
+            if (endOfStream) {
+                return -1;
+            }
+            if (first && req.getContentLength() > 0) {
+                // Handle special first-body-chunk
+                if (!receive()) {
+                    return 0;
+                }
+            } else if (empty) {
+                if (!refillReadBuffer()) {
+                    return -1;
+                }
+            }
+            ByteChunk bc = bodyBytes.getByteChunk();
+            chunk.setBytes(bc.getBuffer(), bc.getStart(), bc.getLength());
+            empty = true;
+            return chunk.getLength();
+
+        }
+
+    }
+
+
+    // ----------------------------------- OutputStreamOutputBuffer Inner Class
+
+
+    /**
+     * This class is an output buffer which will write data to an output
+     * stream.
+     */
+    protected class SocketOutputBuffer
+        implements OutputBuffer {
+
+
+        /**
+         * Write chunk.
+         */
+        public int doWrite(ByteChunk chunk, Response res)
+            throws IOException {
+
+            if (!response.isCommitted()) {
+                // Validate and write response headers
+                try {
+                    prepareResponse();
+                } catch (IOException e) {
+                    // Set error flag
+                    error = true;
+                }
+            }
+
+            int len = chunk.getLength();
+            // 4 - hardcoded, byte[] marshalling overhead
+            int chunkSize = Constants.MAX_SEND_SIZE;
+            int off = 0;
+            while (len > 0) {
+                int thisTime = len;
+                if (thisTime > chunkSize) {
+                    thisTime = chunkSize;
+                }
+                len -= thisTime;
+                responseHeaderMessage.reset();
+                
responseHeaderMessage.appendByte(Constants.JK_AJP13_SEND_BODY_CHUNK);
+                responseHeaderMessage.appendBytes(chunk.getBytes(), 
chunk.getOffset() + off, thisTime);
+                output.write(responseHeaderMessage.getBuffer(), 0, 
responseHeaderMessage.getLen());
+                off += thisTime;
+            }
+
+            return chunk.getLength();
+
+        }
+
+
+    }
+
+
+}

Propchange: tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpProcessor.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpProtocol.java
URL: 
http://svn.apache.org/viewcvs/tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpProtocol.java?rev=397895&view=auto
==============================================================================
--- tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpProtocol.java (added)
+++ tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpProtocol.java Fri Apr 28 
06:22:08 2006
@@ -0,0 +1,484 @@
+/*
+ *  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.coyote.ajp;
+
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.URLEncoder;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.concurrent.Executor;
+
+import javax.management.MBeanRegistration;
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.coyote.ActionCode;
+import org.apache.coyote.ActionHook;
+import org.apache.coyote.Adapter;
+import org.apache.coyote.ProtocolHandler;
+import org.apache.coyote.RequestGroupInfo;
+import org.apache.coyote.RequestInfo;
+import org.apache.tomcat.util.modeler.Registry;
+import org.apache.tomcat.util.net.JIoEndpoint;
+import org.apache.tomcat.util.net.JIoEndpoint.Handler;
+import org.apache.tomcat.util.res.StringManager;
+
+
+/**
+ * Abstract the protocol implementation, including threading, etc.
+ * Processor is single threaded and specific to stream-based protocols,
+ * will not fit Jk protocols like JNI.
+ *
+ * @author Remy Maucherat
+ * @author Costin Manolache
+ */
+public class AjpProtocol 
+    implements ProtocolHandler, MBeanRegistration {
+    
+    
+    protected static org.apache.commons.logging.Log log =
+        org.apache.commons.logging.LogFactory.getLog(AjpProtocol.class);
+
+    /**
+     * The string manager for this package.
+     */
+    protected static StringManager sm =
+        StringManager.getManager(Constants.Package);
+
+
+    // ------------------------------------------------------------ Constructor
+
+
+    public AjpProtocol() {
+        cHandler = new AjpConnectionHandler(this);
+        setSoLinger(Constants.DEFAULT_CONNECTION_LINGER);
+        setSoTimeout(Constants.DEFAULT_CONNECTION_TIMEOUT);
+        //setServerSoTimeout(Constants.DEFAULT_SERVER_SOCKET_TIMEOUT);
+        setTcpNoDelay(Constants.DEFAULT_TCP_NO_DELAY);
+    }
+
+    
+    // ----------------------------------------------------- Instance Variables
+
+
+    protected ObjectName tpOname;
+    
+    
+    protected ObjectName rgOname;
+
+
+    /**
+     * Associated java.io endpoint.
+     */
+    protected JIoEndpoint ep = new JIoEndpoint();
+
+
+    /**
+     * Configuration attributes.
+     */
+    protected Hashtable attributes = new Hashtable();
+
+
+    /**
+     * Should authentication be done in the native webserver layer, 
+     * or in the Servlet container ?
+     */
+    protected boolean tomcatAuthentication = true;
+
+
+    /**
+     * Required secret.
+     */
+    protected String requiredSecret = null;
+
+
+    /**
+     * Adapter which will process the requests recieved by this endpoint.
+     */
+    private Adapter adapter;
+    
+    
+    /**
+     * Connection handler for AJP.
+     */
+    private AjpConnectionHandler cHandler;
+
+
+    // --------------------------------------------------------- Public Methods
+
+
+    /** 
+     * Pass config info
+     */
+    public void setAttribute(String name, Object value) {
+        if (log.isTraceEnabled()) {
+            log.trace(sm.getString("ajpprotocol.setattribute", name, value));
+        }
+        attributes.put(name, value);
+    }
+
+    public Object getAttribute(String key) {
+        if (log.isTraceEnabled()) {
+            log.trace(sm.getString("ajpprotocol.getattribute", key));
+        }
+        return attributes.get(key);
+    }
+
+
+    public Iterator getAttributeNames() {
+        return attributes.keySet().iterator();
+    }
+
+
+    /**
+     * Set a property.
+     */
+    public void setProperty(String name, String value) {
+        setAttribute(name, value);
+    }
+
+
+    /**
+     * Get a property
+     */
+    public String getProperty(String name) {
+        return (String) getAttribute(name);
+    }
+
+
+    /**
+     * The adapter, used to call the connector
+     */
+    public void setAdapter(Adapter adapter) {
+        this.adapter = adapter;
+    }
+
+
+    public Adapter getAdapter() {
+        return adapter;
+    }
+
+
+    /** Start the protocol
+     */
+    public void init() throws Exception {
+        ep.setName(getName());
+        ep.setHandler(cHandler);
+
+        try {
+            ep.init();
+        } catch (Exception ex) {
+            log.error(sm.getString("ajpprotocol.endpoint.initerror"), ex);
+            throw ex;
+        }
+        if (log.isInfoEnabled()) {
+            log.info(sm.getString("ajpprotocol.init", getName()));
+        }
+    }
+
+
+    public void start() throws Exception {
+        if (this.domain != null ) {
+            try {
+                tpOname = new ObjectName
+                    (domain + ":" + "type=ThreadPool,name=" + getName());
+                Registry.getRegistry(null, null)
+                    .registerComponent(ep, tpOname, null );
+            } catch (Exception e) {
+                log.error("Can't register threadpool" );
+            }
+            rgOname = new ObjectName
+                (domain + ":type=GlobalRequestProcessor,name=" + getName());
+            Registry.getRegistry(null, null).registerComponent
+                (cHandler.global, rgOname, null);
+        }
+
+        try {
+            ep.start();
+        } catch (Exception ex) {
+            log.error(sm.getString("ajpprotocol.endpoint.starterror"), ex);
+            throw ex;
+        }
+        if (log.isInfoEnabled())
+            log.info(sm.getString("ajpprotocol.start", getName()));
+    }
+
+    public void pause() throws Exception {
+        try {
+            ep.pause();
+        } catch (Exception ex) {
+            log.error(sm.getString("ajpprotocol.endpoint.pauseerror"), ex);
+            throw ex;
+        }
+        if (log.isInfoEnabled())
+            log.info(sm.getString("ajpprotocol.pause", getName()));
+    }
+
+    public void resume() throws Exception {
+        try {
+            ep.resume();
+        } catch (Exception ex) {
+            log.error(sm.getString("ajpprotocol.endpoint.resumeerror"), ex);
+            throw ex;
+        }
+        if (log.isInfoEnabled())
+            log.info(sm.getString("ajpprotocol.resume", getName()));
+    }
+
+    public void destroy() throws Exception {
+        if (log.isInfoEnabled())
+            log.info(sm.getString("ajpprotocol.stop", getName()));
+        ep.destroy();
+        if (tpOname!=null)
+            Registry.getRegistry(null, null).unregisterComponent(tpOname);
+        if (rgOname != null)
+            Registry.getRegistry(null, null).unregisterComponent(rgOname);
+    }
+
+    // *
+    public Executor getExecutor() {
+        return ep.getExecutor();
+    }
+    
+    // *
+    public void setExecutor(Executor executor) {
+        ep.setExecutor(executor);
+    }
+    
+    public int getMaxThreads() {
+        return ep.getMaxThreads();
+    }
+
+    public void setMaxThreads(int maxThreads) {
+        ep.setMaxThreads(maxThreads);
+        setAttribute("maxThreads", "" + maxThreads);
+    }
+
+    public void setThreadPriority(int threadPriority) {
+        ep.setThreadPriority(threadPriority);
+        setAttribute("threadPriority", "" + threadPriority);
+    }
+    
+    public int getThreadPriority() {
+        return ep.getThreadPriority();
+    }
+    
+
+    public int getBacklog() {
+        return ep.getBacklog();
+    }
+
+
+    public void setBacklog( int i ) {
+        ep.setBacklog(i);
+        setAttribute("backlog", "" + i);
+    }
+
+
+    public int getPort() {
+        return ep.getPort();
+    }
+
+
+    public void setPort( int port ) {
+        ep.setPort(port);
+        setAttribute("port", "" + port);
+    }
+
+
+    public InetAddress getAddress() {
+        return ep.getAddress();
+    }
+
+
+    public void setAddress(InetAddress ia) {
+        ep.setAddress(ia);
+        setAttribute("address", "" + ia);
+    }
+
+
+    public String getName() {
+        String encodedAddr = "";
+        if (getAddress() != null) {
+            encodedAddr = "" + getAddress();
+            if (encodedAddr.startsWith("/"))
+                encodedAddr = encodedAddr.substring(1);
+            encodedAddr = URLEncoder.encode(encodedAddr) + "-";
+        }
+        return ("ajp-" + encodedAddr + ep.getPort());
+    }
+
+
+    public boolean getTcpNoDelay() {
+        return ep.getTcpNoDelay();
+    }
+
+
+    public void setTcpNoDelay(boolean b) {
+        ep.setTcpNoDelay(b);
+        setAttribute("tcpNoDelay", "" + b);
+    }
+
+
+    public boolean getTomcatAuthentication() {
+        return tomcatAuthentication;
+    }
+
+
+    public void setTomcatAuthentication(boolean tomcatAuthentication) {
+        this.tomcatAuthentication = tomcatAuthentication;
+    }
+
+
+    public int getSoLinger() {
+        return ep.getSoLinger();
+    }
+
+
+    public void setSoLinger(int i) {
+        ep.setSoLinger(i);
+        setAttribute("soLinger", "" + i);
+    }
+
+
+    public int getSoTimeout() {
+        return ep.getSoTimeout();
+    }
+
+
+    public void setSoTimeout( int i ) {
+        ep.setSoTimeout(i);
+        setAttribute("soTimeout", "" + i);
+    }
+
+    
+    public void setRequiredSecret(String requiredSecret) {
+        this.requiredSecret = requiredSecret;
+    }
+    
+    
+    // --------------------------------------  AjpConnectionHandler Inner Class
+
+
+    protected static class AjpConnectionHandler implements Handler {
+        protected AjpProtocol proto;
+        protected static int count = 0;
+        protected RequestGroupInfo global=new RequestGroupInfo();
+        protected ThreadLocal localProcessor = new ThreadLocal();
+
+        public AjpConnectionHandler(AjpProtocol proto) {
+            this.proto = proto;
+        }
+
+        public boolean process(Socket socket) {
+            AjpProcessor processor = null;
+            try {
+                processor = (AjpProcessor) localProcessor.get();
+                if (processor == null) {
+                    processor = new AjpProcessor(proto.ep);
+                    processor.setAdapter(proto.adapter);
+                    
processor.setTomcatAuthentication(proto.tomcatAuthentication);
+                    processor.setRequiredSecret(proto.requiredSecret);
+                    localProcessor.set(processor);
+                    if (proto.getDomain() != null) {
+                        synchronized (this) {
+                            try {
+                                RequestInfo rp = 
processor.getRequest().getRequestProcessor();
+                                rp.setGlobalProcessor(global);
+                                ObjectName rpName = new ObjectName
+                                    (proto.getDomain() + 
":type=RequestProcessor,worker="
+                                        + proto.getName() + ",name=AjpRequest" 
+ count++ );
+                                Registry.getRegistry(null, null)
+                                    .registerComponent(rp, rpName, null);
+                            } catch (Exception ex) {
+                                
log.warn(sm.getString("ajpprotocol.request.register"));
+                            }
+                        }
+                    }
+                }
+
+                if (processor instanceof ActionHook) {
+                    ((ActionHook) processor).action(ActionCode.ACTION_START, 
null);
+                }
+
+                return processor.process(socket);
+
+            } catch(java.net.SocketException e) {
+                // SocketExceptions are normal
+                AjpProtocol.log.debug
+                    (sm.getString
+                     ("ajpprotocol.proto.socketexception.debug"), e);
+            } catch (java.io.IOException e) {
+                // IOExceptions are normal
+                AjpProtocol.log.debug
+                    (sm.getString
+                     ("ajpprotocol.proto.ioexception.debug"), e);
+            }
+            // Future developers: if you discover any other
+            // rare-but-nonfatal exceptions, catch them here, and log as
+            // above.
+            catch (Throwable e) {
+                // any other exception or error is odd. Here we log it
+                // with "ERROR" level, so it will show up even on
+                // less-than-verbose logs.
+                AjpProtocol.log.error
+                    (sm.getString("ajpprotocol.proto.error"), e);
+            } finally {
+                if (processor instanceof ActionHook) {
+                    ((ActionHook) processor).action(ActionCode.ACTION_STOP, 
null);
+                }
+            }
+            return false;
+        }
+    }
+
+
+    // -------------------- Various implementation classes --------------------
+
+
+    protected String domain;
+    protected ObjectName oname;
+    protected MBeanServer mserver;
+
+    public ObjectName getObjectName() {
+        return oname;
+    }
+
+    public String getDomain() {
+        return domain;
+    }
+
+    public ObjectName preRegister(MBeanServer server,
+                                  ObjectName name) throws Exception {
+        oname=name;
+        mserver=server;
+        domain=name.getDomain();
+        return name;
+    }
+
+    public void postRegister(Boolean registrationDone) {
+    }
+
+    public void preDeregister() throws Exception {
+    }
+
+    public void postDeregister() {
+    }
+    
+ 
+}

Propchange: tomcat/tc6.0.x/trunk/java/org/apache/coyote/ajp/AjpProtocol.java
------------------------------------------------------------------------------
    svn:eol-style = native



---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]

Reply via email to