package net.sourceforge.kitteh.impl;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.URLDecoder;
import java.security.SecureRandom;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.StringTokenizer;
import net.sourceforge.kitteh.DocumentProducer;
import net.sourceforge.kitteh.Method;
import net.sourceforge.kitteh.Redirection;
import net.sourceforge.kitteh.WebSocketListener;
import net.sourceforge.kitteh.impl.ws.WebSocketImpl;
public class ConnectionImpl implements Runnable
{
private static final String COOKIE_NAME = "session-cookie";
private Socket socket;
private Random rand = new SecureRandom();
private DocumentProducer pageProducer;
RequestImpl req = new RequestImpl();
ResponseImpl resp = new ResponseImpl();
private WebSocketListener wsHandler;
public ConnectionImpl(Socket socket, DocumentProducer pageProducer, WebSocketListener wsHandler)
{
this.socket = socket;
this.pageProducer = pageProducer;
this.wsHandler = wsHandler;
}
private static Map<String, String> stringToParams(String p)
{
HashMap<String, String> res = new HashMap<String, String>();
StringTokenizer t = new StringTokenizer(p, "&");
while (t.hasMoreTokens())
{
String couple = t.nextToken();
int pos = couple.indexOf("=");
if (pos == -1)
{
res.put(couple, "");
}
else
{
String key = couple.substring(0, pos);
String val = couple.substring(pos + 1, couple.length());
try
{
val = URLDecoder.decode(val, "UTF-8");
}
catch (UnsupportedEncodingException e)
{
throw new RuntimeException("error", e);
}
res.put(key, val);
}
}
return res;
}
private static void sendHeader(OutputStream out, int code,
String contentType, long contentLength, boolean cacheable,
String location, Map<String, String> headers) throws IOException
{
StringBuffer h = new StringBuffer();
h.append("HTTP/1.0 ");
h.append(code);
h.append(" OK\r\n");
h.append("Date: ");
h.append(new Date().toString());
h.append("\r\n");
h.append("Server: WebServer/1.0\r\n");
h.append("Connection: close\r\n");
h.append("Content-Type: ");
h.append(contentType);
h.append("\r\n");
if (cacheable)
{
headers.put("Expires", "Thu, 01 Jan 2100 00:00:00 GMT");
}
else
{
headers.put("Cache-Control","no-cache");
headers.put("Pragma","no-cache");
headers.put("Expires", "Thu, 01 Jan 1970 00:00:00 GMT");
/*h.append("Last-modified: ");
h.append(new Date().toString());
h.append("\r\n");*/
}
if (contentLength != -1)
{
h.append("Content-Length: ");
h.append(contentLength);
h.append("\r\n");
}
if (location != null)
{
h.append("Location: " + location + "\r\n");
}
for (String header : headers.keySet())
{
h.append(header+": "+headers.get(header));
h.append("\r\n");
}
h.append("\r\n");
out.write(h.toString().getBytes());
}
private static void error(OutputStream out, Exception e) throws IOException
{
StringWriter s = new StringWriter();
PrintWriter pw = new PrintWriter(s);
pw.println("Kitteh Server error");
pw.println("-------------------");
e.printStackTrace(pw);
pw.flush();
byte[] buf = s.getBuffer().toString().getBytes();
sendHeader(out, 500, "text/plain", buf.length, false, null, new HashMap<String, String>(0));
out.write(buf);
}
private Map<String, String> loadReqData(BufferedReader in) throws IOException
{
HashMap<String, String> x = new HashMap<String, String>();
String s = in.readLine();
while (!"".equals(s))
{
String[] couple = s.split(":", 2);
if (couple.length == 2)
{
x.put(couple[0].trim().toLowerCase(), couple[1].trim());
}
s = in.readLine();
}
return x;
}
private String getCookie(Map<String, String> headers)
{
String c = (String)headers.get("cookie");
if (c != null)
{
String[] x = c.split("=");
if (x[0].equals(COOKIE_NAME))
{
return x[1];
}
}
return null;
}
private void handleRequest(RequestImpl request, BufferedReader in) throws IOException
{
// read the first line. HelloWorld "GET /index.html HTTP/1.1"
String firstLine = in.readLine();
if (firstLine == null)
{
throw new IOException("Null request");
}
// separate the three token
String[] req = firstLine.split(" ", 3);
if (req.length != 3)
{
throw new IOException("Non standard request");
}
// method and document name.. forget the protocol :P
String method = req[0].toLowerCase();
String docName = req[1];
if (docName.startsWith("/"))
{
docName = docName.substring(1, docName.length());
}
// load request data
request.setHeaders( loadReqData(in) );
if (method.equals("get"))
{
request.setMethod(Method.GET);
// if it is GET, separate the parameters from the doc name
int pos = docName.indexOf("?");
if (pos == -1)
{
// no parameters passed
request.setDocumentName( docName );
}
else
{
// parameters!
request.setDocumentName( docName.substring(0, pos) );
String paramStr = docName.substring(pos + 1, docName.length());
request.setParameters( stringToParams(paramStr) );
}
}
else if (method.equals("post"))
{
request.setMethod(Method.POST);
// if it is post, read the following data..
request.setDocumentName( docName );
try
{
int size = Integer.parseInt((String) request.getHeaders().get("content-length"));
char[] b = new char[size];
if (in.read(b) != size)
{
throw new Exception("can't read all");
}
request.setParameters( stringToParams(new String(b)) );
}
catch (Exception e)
{
e.printStackTrace();
// ensure we do not pass anything if something goes wrong
request.setParameters( new HashMap<String, String>() );
}
}
else
{
throw new IOException("Unknown method");
}
}
private String bakeNewCookie()
{
StringBuffer s = new StringBuffer();
while (s.length()<30)
{
s.append( Long.toString( rand.nextLong() & Long.MAX_VALUE , 36 ).toUpperCase() );
}
return s.substring(0, 30);
}
private static int copyStream(InputStream in, OutputStream out) throws IOException
{
int bufferSize = 1024;
int streamLength = 0;
byte[] buffer = new byte[bufferSize];
for (int len=in.read(buffer); len>0; len=in.read(buffer) )
{
out.write(buffer, 0, len);
out.flush();
streamLength += len;
}
return streamLength;
}
private void writeResponse() throws IOException
{
OutputStream out = socket.getOutputStream();
try
{
pageProducer.produceDocument(req, resp);
sendHeader(out, resp.getHtmlReturnCode(), resp.getContentType(), resp.getContentLength(), resp.isCacheable(), null, resp.getHeaders());
InputStream cont = resp.getContent();
try
{
copyStream(cont, out);
}
finally
{
cont.close();
}
}
catch (Redirection e)
{
// handle user redirections
String d = "Document moved";
sendHeader(out, 302, "text/plain", d.length(), false, e.getUrl(), new HashMap<String, String>(0));
out.write(d.getBytes());
}
catch (Exception e)
{
// send generic unexpected exception
error(out, e);
}
out.flush();
}
public void readRequest()
{
try
{
socket.setSoTimeout(600000);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
handleRequest(req, in);
req.setRemoteAddr( socket.getInetAddress().toString() );
handleSession(req, resp);
}
catch (Exception e)
{
try
{
socket.close();
}
catch (IOException e1)
{
// nothing
}
e.printStackTrace();
}
}
private void handleSession(RequestImpl req, ResponseImpl resp)
{
String cookie = getCookie(req.getHeaders());
if ((cookie == null) || (!SessionContainer.hasSession(cookie)))
{
cookie = bakeNewCookie();
resp.getHeaders().put("Set-Cookie", COOKIE_NAME+"="+cookie);
}
req.setSession(SessionContainer.getSession(cookie));
req.setSessionId(cookie);
}
public void run()
{
try
{
if(isWebSocketRequest())
{
WebSocketImpl ws = new WebSocketImpl(socket, req, wsHandler);
ws.start();
}
else
{
try
{
writeResponse();
}
finally
{
try
{
socket.close();
}
catch (IOException e1)
{
// nothing
}
}
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
private boolean isWebSocketRequest() {
String con = req.getHeaders().get("connection");
String upg = req.getHeaders().get("upgrade");
return "Upgrade".equals(con) && "websocket".equals(upg);
}
}