/*
* ChatServer.java
*
* Created on Apr 14, 2009, 2:37:17 PM
*
* Description: Handles HTTP requests specifically for chat dialog.
*
* Copyright (C) Apr 14, 2009 Stephen L. Reed.
*
* This program is free software; you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.texai.webserver;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.jcip.annotations.NotThreadSafe;
import org.apache.log4j.Logger;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.texai.network.netty.handler.TexaiHTTPRequestHandler;
import org.texai.network.netty.utils.NettyHTTPUtils;
import org.texai.network.netty.utils.NettyJSONUtils;
import org.texai.util.HTTPUtils;
import org.texai.util.OneWayEncryptionService;
import org.texai.util.StringUtils;
import org.texai.util.TexaiException;
/** Handles HTTP requests specifically for chat dialog.
*
* @author Stephen L. Reed
*/
@NotThreadSafe
public class ChatServer implements TexaiHTTPRequestHandler {
/** the username session key */
public static final String USERNAME_SESSION_KEY = "username";
/** the cookie session key */
public static final String COOKIE_SESSION_KEY = "cookie";
/** the log4j logger */
private static final Logger LOGGER = Logger.getLogger(ChatServer.class);
/** the fileInputStream cache, path --> fileInputStream bytes */
private final ConcurrentHashMap<String, byte[]> fileCache = new ConcurrentHashMap<>();
/** the root path */
private static final File ROOT_PATH = new File("html/");
/** the chat actions */
private WebChatActions chatSession;
/** the session dictionary dictionary, session cookie --> session dictionary (parameter --> value) */
private final Map<String, ConcurrentHashMap<String, Object>> sessionDictionaryDictionary = new ConcurrentHashMap<>();
/** the IP address / cookie dictionary, IP address --> session cookie */
private final Map<String, String> ipAddressCookieDictionary = new ConcurrentHashMap<>();
//TODO Because the ChatServer instance is shared among all HTTP requests, put a session cookie in the dialog HTML
// client and pass it on each HTTP request.
// The webcam page does not have its own cookie, but uses the cookie of the dialog page at that IP address.
/** Constructs a new ChatServer instance. */
public ChatServer() {
}
/** Handles the HTTP request.
*
* @param httpRequest the HTTP request
* @param channel the channel
* @return the indicator whether the HTTP request was handled
*/
@Override
public boolean httpRequestReceived(final HttpRequest httpRequest, final Channel channel) {
//Preconditions
assert httpRequest != null : "httpRequest must not be null";
assert channel != null : "channel must not be null";
assert chatSession != null : "chatSession must not be null";
if (LOGGER.isInfoEnabled()) {
LOGGER.info("httpRequest: " + httpRequest);
LOGGER.info("method: " + httpRequest.getMethod());
LOGGER.info("protocol version: " + httpRequest.getProtocolVersion());
LOGGER.info("method: " + httpRequest.getMethod());
LOGGER.info("uri: " + httpRequest.getUri());
for (final String headerName : httpRequest.getHeaderNames()) {
LOGGER.info("header: " + headerName + " " + httpRequest.getHeader(headerName));
}
}
final URI uri;
try {
uri = new URI(httpRequest.getUri());
} catch (URISyntaxException ex) {
throw new TexaiException(ex);
}
final String path = uri.getPath();
if (LOGGER.isInfoEnabled()) {
LOGGER.info("path: " + path);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(httpRequest.getMethod() + " " + path);
}
}
final Map<String, String> parameterDictionary = new HashMap<>();
if (uri.getRawQuery() != null && !uri.getRawQuery().isEmpty()) {
parameterDictionary.putAll(HTTPUtils.getQueryMap(uri.getRawQuery()));
}
final ConcurrentHashMap<String, Object> sessionDictionary = getSessionDictionary(
null, // sessionCookie
channel);
if (path.equals("/clear-file-cache")) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("clearing the file cache");
}
fileCache.clear();
NettyHTTPUtils.writeHTMLResponse(
httpRequest,
"<html><body><h2>File Cache Cleared</h2></body></html>",
channel,
null); // sessionCookie
return true;
}
if (httpRequest.getMethod().equals(HttpMethod.POST) && path.equals("/upload-picture")) {
NettyHTTPUtils.writeHTMLResponse(
httpRequest,
"<html><body>image uploaded OK</body></html>",
channel,
null); // sessionCookie
// uploaded webcam image
chatSession.receiveWebcamImage(
httpRequest,
channel,
sessionDictionary);
return true;
}
//TODO some of the below actions have not been tested since the migration to the single page interface
String nextTarget = null;
try {
if (path.equals("/availableUsername")) {
// validate new username
final String username = parameterDictionary.get("username");
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("availableUsername: " + username);
}
chatSession.determineUsernameAvailability(username, httpRequest, channel);
return true;
}
if (path.equals("/availableEmailAddress")) {
// validate new email address
final String emailAddress = parameterDictionary.get("emailAddress");
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("availableEmailAddress: " + emailAddress);
}
chatSession.determineUsernameAvailability(emailAddress, httpRequest, channel);
return true;
}
if (path.equals("/authenticate")) {
// authenticate the username and password
final String username = parameterDictionary.get("username");
final String plainText = username + parameterDictionary.get("password");
final String encryptedPassword = OneWayEncryptionService.getInstance().encrypt(plainText);
chatSession.authenticate(
username,
encryptedPassword,
sessionDictionary,
httpRequest,
channel);
return true;
}
if (path.equals("/validateEmailOrUsername")) {
// validate the email address or username, and if valid then send user their username and new password
final String emailOrUsername = parameterDictionary.get("emailOrUsername");
chatSession.validateEmailOrUsername(
emailOrUsername,
sessionDictionary,
httpRequest, channel);
return true;
}
if (httpRequest.getMethod().equals(HttpMethod.POST) && path.equals("/send-username-new-password")) {
// forgot username or password
nextTarget = "/forgot-username-password";
} else if (httpRequest.getMethod().equals(HttpMethod.POST) && path.equals("/login")) {
// login completion
nextTarget = "/chat";
} else if (httpRequest.getMethod().equals(HttpMethod.POST) && path.equals("/register")) {
// new user registration
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("register: " + httpRequest);
}
final String username = parameterDictionary.get("username");
final String plainText = username + parameterDictionary.get("password");
final String encryptedPassword = OneWayEncryptionService.getInstance().encrypt(plainText);
chatSession.registerUser(
parameterDictionary.get("firstName"),
parameterDictionary.get("lastName"),
parameterDictionary.get("email"),
username,
encryptedPassword,
httpRequest,
channel);
nextTarget = "/chat/email-confirmation";
} else if (path.startsWith("/registration-confirmation&token=")) {
// registration confirmation
final String confirmationToken = path.substring(33);
final boolean isError;
if (confirmationToken == null || confirmationToken.isEmpty()) {
isError = true;
} else {
isError = !chatSession.confirmEmailAddress(confirmationToken, sessionDictionary);
}
if (isError) {
NettyHTTPUtils.writeHTMLResponse(
httpRequest,
"<html><body><h2>A registration confirmation error occured.</h2></body></html>",
channel,
null); // sessionCookie
} else {
nextTarget = "/chat";
}
}
final String target;
if (nextTarget == null) {
target = path;
} else {
target = nextTarget;
}
final String filePath = target.replace('/', File.separatorChar);
LOGGER.info("filePath: " + ROOT_PATH + filePath);
if (new File(ROOT_PATH, filePath).isDirectory()) {
NettyHTTPUtils.writeHTMLResponse(
httpRequest,
"<html><body><h2>You do not have permission to view this directory</h2></body></html>",
channel,
null); // sessionCookie
return true;
}
// serve file
byte[] cachedFileContent = fileCache.get(filePath);
if (cachedFileContent == null) {
File file;
InputStream fileInputStream = null;
try {
file = new File(ROOT_PATH, filePath);
LOGGER.info("serving file: " + file);
fileInputStream = new FileInputStream(file);
} catch (final FileNotFoundException ex) {
if (fileInputStream != null) {
fileInputStream.close();
}
return false;
}
assert file != null;
assert fileInputStream != null;
final byte[] chunk = new byte[(int) file.length()];
int count;
int pos = 0;
while ((count = fileInputStream.read(chunk, pos, chunk.length - pos)) > 0) {
pos += count;
}
cachedFileContent = chunk;
fileCache.put(filePath, cachedFileContent);
} else {
LOGGER.info(" ...cached");
}
assert cachedFileContent != null;
if (path.endsWith(".html")) {
NettyHTTPUtils.writeHTMLResponse(
httpRequest,
new String(cachedFileContent, "US-ASCII"),
channel,
null); // sessionCookie
} else if (path.endsWith(".css")) {
NettyHTTPUtils.writeCSSResponse(httpRequest, new String(cachedFileContent, "US-ASCII"), channel);
} else {
NettyHTTPUtils.writeBinaryResponse(
httpRequest,
cachedFileContent,
channel,
null); // sessionCookie
}
return true;
} catch (final IOException ex) {
LOGGER.error("exception message: " + ex.getMessage());
LOGGER.error("exception: " + ex);
LOGGER.error(StringUtils.getStackTraceAsString(ex));
NettyHTTPUtils.writeHTMLResponse(
httpRequest,
"<html><body><h2>An error occured.</h2></body></html>",
channel,
null); // sessionCookie
return true;
}
}
/** Gets the session dictionary given the cookie, and if the cookie is absent then the IP address is used to get
* the associated dialog session's cookie.
*
* @param sessionCookie the session cookie
* @param channel the channel
* @return the session dictionary
*/
private ConcurrentHashMap<String, Object> getSessionDictionary(
final String sessionCookie,
final Channel channel) {
//Preconditions
assert channel != null : "channel must not be null";
ConcurrentHashMap<String, Object> sessionDictionary;
String sessionCookie1;
final String hostString = ((InetSocketAddress) channel.getRemoteAddress()).getHostString();
synchronized (ipAddressCookieDictionary) {
if (sessionCookie == null || sessionCookie.equals("undefined")) {
// no session cookie - the request might be from the webcam so try getting the cookie of the dialog at the same IP address
sessionCookie1 = ipAddressCookieDictionary.get(hostString);
if (LOGGER.isInfoEnabled()) {
LOGGER.info("cookie from associated IP address: " + sessionCookie1);
}
} else {
sessionCookie1 = sessionCookie;
}
if (sessionCookie1 == null) {
// no session cookie so generate one
sessionCookie1 = UUID.randomUUID().toString();
if (LOGGER.isInfoEnabled()) {
LOGGER.info("generated cookie: " + sessionCookie1);
}
}
// update the IP address / session cookie dictionary
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(hostString + " --> " + sessionCookie1);
}
ipAddressCookieDictionary.put(hostString, sessionCookie1);
}
// get the existing session dictionary or create a new empty one
sessionDictionary = sessionDictionaryDictionary.get(sessionCookie1);
if (sessionDictionary == null) {
sessionDictionary = new ConcurrentHashMap<>();
sessionDictionaryDictionary.put(sessionCookie1, sessionDictionary);
// set the session cookie of the session dictionary
sessionDictionary.put(COOKIE_SESSION_KEY, sessionCookie1);
}
//Postconditions
assert sessionDictionary != null : "sessionDictionary must not be null";
return sessionDictionary;
}
/** Gets the chat session.
*
* @return the chat session
*/
public WebChatActions getChatSession() {
return chatSession;
}
/** Sets the chat session.
*
* @param chatSession the chat session
*/
public void setChatSession(final WebChatActions chatSession) {
this.chatSession = chatSession;
}
/** Handles a received text web socket frame.
*
* @param channel the channel handler context
* @param textWebSocketFrame the text web socket frame
* @return the indicator whether the web socket request was handled
*/
@Override
public boolean textWebSocketFrameReceived(
final Channel channel,
final TextWebSocketFrame textWebSocketFrame) {
//Preconditions
assert channel != null : "channel must not be null";
assert textWebSocketFrame != null : "textWebSocketFrame must not be null";
final String webSocketText = textWebSocketFrame.getText();
LOGGER.info("web socket text received: " + webSocketText);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("web socket text received: " + webSocketText);
}
final ConcurrentHashMap<String, Object> sessionDictionary = getSessionDictionary(
NettyJSONUtils.getTexaiSessionCookie(textWebSocketFrame.getText()), // sessionCookie
channel);
chatSession.receiveWebSocketText(
webSocketText,
channel,
sessionDictionary);
return true;
}
/** Gets the IP address / cookie dictionary, IP address --> session cookie.
*
* @return the IP address / cookie dictionary
*/
public Map<String, String> getIpAddressCookieDictionary() {
return ipAddressCookieDictionary;
}
}