/**
* This file Copyright (c) 2005-2008 Aptana, Inc. This program is
* dual-licensed under both the Aptana Public License and the GNU General
* Public license. You may elect to use one or the other of these licenses.
*
* This program is distributed in the hope that it will be useful, but
* AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or
* NONINFRINGEMENT. Redistribution, except as permitted by whichever of
* the GPL or APL you select, is prohibited.
*
* 1. For the GPL license (GPL), you can redistribute and/or modify this
* program under the terms of the GNU General Public License,
* Version 3, as published by the Free Software Foundation. You should
* have received a copy of the GNU General Public License, Version 3 along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Aptana provides a special exception to allow redistribution of this file
* with certain other free and open source software ("FOSS") code and certain additional terms
* pursuant to Section 7 of the GPL. You may view the exception and these
* terms on the web at http://www.aptana.com/legal/gpl/.
*
* 2. For the Aptana Public License (APL), this program and the
* accompanying materials are made available under the terms of the APL
* v1.0 which accompanies this distribution, and is available at
* http://www.aptana.com/legal/apl/.
*
* You may view the GPL, Aptana's exception and additional terms, and the
* APL in the file titled license.html at the root of the corresponding
* plugin containing this source file.
*
* Any modifications to this file must keep this entire header intact.
*/
package com.aptana.ide.server.http;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Hashtable;
import java.util.Vector;
import org.eclipse.core.runtime.Path;
import com.aptana.ide.core.IdeLog;
import com.aptana.ide.core.StringUtils;
import com.aptana.ide.server.core.ServerCorePlugin;
import com.aptana.ide.server.logging.IHttpLog;
import com.aptana.ide.server.resolvers.IHttpResourceResolver;
import com.aptana.ide.server.resources.IHttpFolderResource;
import com.aptana.ide.server.resources.IHttpResource;
/**
* @author Kevin Lindsey
*/
public class ServerThreadRunnable implements Runnable
{
/*
* Fields
*/
private boolean _stopped;
private HttpServer _server;
private ServerSocket _socketServer;
private IHttpResourceResolver _resourceResolver;
private IHttpLog _logger;
/* Where worker threads stand idle */
Vector threads = new Vector();
/* max # worker threads */
int workers = 5;
/* timeout on client connections */
int timeout = 5000;
/*
* Constructors
*/
/**
* Create a new instance of ServerThreadRunnable
*
* @param server
*/
public ServerThreadRunnable(HttpServer server)
{
this(server, 5000);
}
/**
* Create a new instance of ServerThreadRunnable
*
* @param server
* @param timeout
*/
public ServerThreadRunnable(HttpServer server, int timeout)
{
this._server = server;
this._socketServer = this._server.getSocketServer();
this._resourceResolver = this._server.getResourceResolver();
this._logger = this._server.getLogger();
this.timeout = timeout;
}
/*
* Methods
*/
/**
* Runs the server
*/
public void run()
{
for (int i = 0; i < workers; ++i)
{
Worker w = new Worker();
(new Thread(w, "Aptana: HTTP Worker " + i)).start(); //$NON-NLS-1$
threads.addElement(w);
}
while (this._stopped == false && !this._socketServer.isClosed() )
{
try
{
Socket s = this._socketServer.accept();
Worker w = null;
synchronized (threads)
{
if (threads.isEmpty())
{
Worker ws = new Worker();
ws.setSocket(this, s, this._server, this._resourceResolver, this._logger);
(new Thread(ws, "Aptana: HTTP Worker (Additional)")).start(); //$NON-NLS-1$
}
else
{
w = (Worker) threads.elementAt(0);
threads.removeElementAt(0);
w.setSocket(this, s, this._server, this._resourceResolver, this._logger);
}
}
}
catch (Exception e)
{
if ( this._stopped == false || this._socketServer.isClosed() == false ) {
/* skip socket closed exception on stop() */
this._logger.logError(e.getMessage(), e);
}
}
}
}
/**
* Stops the server
*/
public synchronized void stop()
{
if (this._stopped == false)
{
this._stopped = true;
try
{
this._socketServer.close();
}
catch (Exception e)
{
this._logger.logError(e.getMessage(), e);
}
}
}
}
/**
* Worker
* @author Ingo Muschenetz
*
*/
class Worker implements Runnable
{
static final int BUF_SIZE = 2048;
static final byte[] EOL = { (byte) '\r', (byte) '\n' };
private Socket s;
private ServerThreadRunnable _serverThreadRunnable;
private HttpServer _server;
private IHttpResourceResolver _resourceResolver;
private IHttpLog _logger;
/* buffer to use for requests */
byte[] buf;
Worker()
{
buf = new byte[BUF_SIZE];
s = null;
}
synchronized void setSocket(ServerThreadRunnable serverThreadRunnable, Socket s, HttpServer server,
IHttpResourceResolver resourceResolver, IHttpLog logger)
{
this._serverThreadRunnable = serverThreadRunnable;
this.s = s;
this._server = server;
this._resourceResolver = resourceResolver;
this._logger = logger;
notify();
}
/**
* @see java.lang.Runnable#run()
*/
public synchronized void run()
{
while (true)
{
if (s == null)
{
/* nothing to do */
try
{
wait();
}
catch (InterruptedException e)
{
/* should not happen */
continue;
}
}
try
{
processRequest();
}
catch (Exception e)
{
// e.printStackTrace();
}
/*
* go back in wait queue if there's fewer than numHandler connections.
*/
s = null;
Vector pool = _serverThreadRunnable.threads;
synchronized (pool)
{
if (pool.size() >= _serverThreadRunnable.workers)
{
/* too many threads, exit this one */
return;
}
else
{
pool.addElement(this);
}
}
}
}
/**
* processRequest
*
* @param s
* @throws IOException
*/
private void processRequest() throws IOException
{
String uri = null;
try
{
InputStream is = new BufferedInputStream(s.getInputStream());
/*
* we will only block in read for this many milliseconds before we fail with java.io.InterruptedIOException,
* at which point we will abandon the connection.
*/
s.setSoTimeout(_serverThreadRunnable.timeout);
s.setTcpNoDelay(true);
/* zero out the buffer from last time */
for (int i = 0; i < BUF_SIZE; i++)
{
buf[i] = 0;
}
/*
* Read in the full header of the request
*/
int nread = 0, r = 0;
outerloop: while (nread < BUF_SIZE)
{
try
{
r = is.read(buf, nread, BUF_SIZE - nread);
}
catch(SocketException e)
{
return;
}
if (r == -1)
{
/* EOF */
return;
}
int i = nread;
nread += r;
for (; i < nread; i++)
{
if (buf[i] == (byte) '\n' || buf[i] == (byte) '\r')
{
/* read one line */
break outerloop;
}
}
}
String reqLine = new String(buf);
RequestLineParser reqLineParser = new RequestLineParser(reqLine);
uri = reqLineParser.getUri();
if (uri != null && uri.length() > 0)
{
try
{
IHttpResource resource = this._resourceResolver.getResource(reqLineParser);
if (resource != null)
{
if (resource instanceof IHttpFolderResource)
{
IHttpFolderResource folderResource = (IHttpFolderResource) resource;
if (!uri.endsWith("/")) //$NON-NLS-1$
{
// send a redirect to tell the browser to connect with a URL that includes
// the trailing slash (required for relative resources to be properly resolved)
this.sendRedirect(s, uri + "/"); //$NON-NLS-1$
}
else
{
String[] fileNames = folderResource.getFileNames();
String[] folderNames = folderResource.getFolderNames();
String folderHTML = HttpResponseUtils.createBrowseFolderHTML(new Path(uri), fileNames,
folderNames);
if (reqLineParser.getMethod().equals("GET")) //$NON-NLS-1$
{
this.sendContent(s, folderHTML, "text/html"); //$NON-NLS-1$
}
else if (reqLineParser.getMethod().equals("HEAD")) //$NON-NLS-1$
{
this.sendHeaders(s, "text/html"); //$NON-NLS-1$
}
}
}
else
{
InputStream contentInput = resource.getContentInputStream(this._server);
long length = resource.getContentLength();
String type = resource.getContentType();
try
{
if (reqLineParser.getMethod().equals("GET")) //$NON-NLS-1$
{
this.sendFile(s, contentInput, length, type);
}
else if (reqLineParser.getMethod().equals("HEAD")) //$NON-NLS-1$
{
this.sendHeaders(s, type);
}
}
finally
{
if ( contentInput != null ) {
contentInput.close();
}
}
}
}
else
{
throw new HttpServerException(404, "file not found", uri, "file not found: " + uri); //$NON-NLS-1$ //$NON-NLS-2$
}
}
catch (HttpServerException e)
{
this._logger.logTrace(e.getStatusMessage() + "-" + e.getRequestUri()); //$NON-NLS-1$
sendError(s, e);
}
}
else
{
this._logger.logTrace("bad request detected: " + reqLine != null ? reqLine : "request line was empty"); //$NON-NLS-1$ //$NON-NLS-2$
sendError(s, new HttpServerException(400, "bad request", uri, null)); //$NON-NLS-1$
}
}
catch (Exception e)
{
// this._logger.logError("error processing request: " + uri != null ? uri : "uri unavailable", e);
sendError(s, new HttpServerException(500, "Internal error", uri, null, e)); //$NON-NLS-1$
IdeLog.logError(ServerCorePlugin.getDefault(), "Error", e); //$NON-NLS-1$
}
finally
{
if(s != null && !s.isClosed())
{
s.close();
}
}
}
/**
* makeHeaders
*
* @return Hashtable
*/
private Hashtable<String, String> makeHeaders()
{
Hashtable<String, String> headers = new Hashtable<String, String>();
headers.put("Server", "Aptana v0.2.7"); //$NON-NLS-1$ //$NON-NLS-2$
// add IE no-cache headers per
// http://en.wikipedia.org/wiki/XMLHttpRequest#Microsoft_Internet_Explorer_cache_issues
headers.put("Expires", "Mon, 26 Jul 1997 05:00:00 GMT"); //$NON-NLS-1$ //$NON-NLS-2$
headers.put("Cache-Control", "no-store, no-cache, must-revalidate"); //$NON-NLS-1$ //$NON-NLS-2$
headers.put("Pragma", "no-cache"); //$NON-NLS-1$ //$NON-NLS-2$
SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zZ"); //$NON-NLS-1$
headers.put("Last-Modified", format.format(new Date())); //$NON-NLS-1$
return headers;
}
/**
* sendContent
*
* @param s
* @param content
* @param contentType
* @throws IOException
*/
private void sendContent(Socket s, String content, String contentType) throws IOException
{
OutputStream output = s.getOutputStream();
HttpResponse response = new HttpResponse(output);
response.sendResponseContent(content);
}
/**
* @param s
* @throws IOException
*/
private void sendHeaders(Socket s, String contentType) throws IOException
{
OutputStream output = s.getOutputStream();
HttpResponse response = new HttpResponse(output);
response.sendReponseHeader(200, "OK", 0, contentType, this.makeHeaders()); //$NON-NLS-1$
}
/**
* sendFile
*
* @param s
* @param fileInput
* @param contentLength
* @param contentType
* @throws IOException
*/
private void sendFile(Socket s, InputStream fileInput, long contentLength, String contentType) throws IOException
{
OutputStream output = s.getOutputStream();
HttpResponse response = new HttpResponse(output);
response.sendFileContent(fileInput, contentLength, contentType, this.makeHeaders());
}
/**
* sendError
*
* @param s
* @param e
* @throws IOException
*/
private void sendError(Socket s, HttpServerException e) throws IOException
{
OutputStream output = s.getOutputStream();
HttpResponse response = new HttpResponse(output);
response.sendError(e);
}
/**
* sendRedirect
*
* @param s
* @param newURL
* @throws IOException
*/
private void sendRedirect(Socket s, String newURL) throws IOException
{
OutputStream output = s.getOutputStream();
HttpResponse response = new HttpResponse(output);
Hashtable<String, String> headers = new Hashtable<String, String>();
headers.put("location", StringUtils.urlEncodeForSpaces(newURL.toCharArray())); //$NON-NLS-1$
response.sendReponseHeader(307, "redirect", 1, "text/html", headers); //$NON-NLS-1$ //$NON-NLS-2$
}
}