/**
* This file is part of the VNCProxy program.
* <p>
* VNCPRoxy Summary :
* In just one clic (no setup) this Java Applet based solution
* allows you to run VNC Server / VNC Viewer
* through an HTTP AES encrypted tunnel.
* As it is full HTTP, there is no proxy or firewall setup needed.
* <p>
* Copyright (C) 2009 - Remi Serrano - http://www.vncproxy.com
* <p>
* VNCProxy 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 3 of the License, or
* (at your option) any later version.
* <p>
* VNCProxy 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.
* <p>
* You should have received a copy of the GNU General Public License
* along with VNCProxy. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.vncproxy.hub;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.sql.Timestamp;
import java.util.Date;
import java.util.Enumeration;
import java.util.Random;
import java.util.TimeZone;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import com.vncproxy.applet.VNCPActionData;
import com.vncproxy.applet.VNCPSession;
/**
* This class implements the VNCPHubAction Servlet the VNCPHubAction will handle
* and execute all the ActionData sent from the VNCProxy Applet to the VNCProxy
* Hub
*
* @author Remi Serrano
*
*/
public class VNCPHubAction extends HttpServlet {
/**
* This constant variable is needed to implements java.io.Serializable
*/
private static final long serialVersionUID = 1L;
/**
* This constant variable represents the time out value in milliseconds
*/
private static final long TIMEOUT = 60 * 60 * 1000;
/**
* This constant variable represents the size of the AES key in bits
*/
private static final int AES_KEY_LEN = 128;
/**
* This constant variable represents the sid upper limit
*/
private static final int SID_UPPER_LIMIT = 999999;
/**
* This constant variable represents the sid lower limit
*/
private static final int SID_LOWER_LIMIT = 100000;
/**
* This method overrides the standard Servlet doPost method It will handle and
* execute all the ActionData sent from the VNCProxy Applet to the VNCProxy
* Hub
*
* @param request
* The HTTP request
* @param response
* The HTTP response
* @throws IOException
* Any IO Exception
* @throws ServletException
* Any Servlet Exception
*/
@Override
public final synchronized void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
ServletContext context = getServletConfig().getServletContext();
InputStream in = request.getInputStream();
OutputStream outstr = response.getOutputStream();
VNCPActionData objActionDataBack = null;
response.setContentType("application/x-java-serialized-object");
VNCPLog log = new VNCPLog();
try {
log.logInfo("------------------------------------");
log.logInfo("VNCP_HubInit");
// Make some cleaning
Enumeration<?> attributeEnum = context.getAttributeNames();
while (attributeEnum.hasMoreElements()) {
String key = (String) attributeEnum.nextElement();
if (key.startsWith("VNCProxySession_")) {
VNCPSession theSessionToTest = (VNCPSession) context.getAttribute(key);
long lm = theSessionToTest.getLastMove();
if (System.currentTimeMillis() - lm > TIMEOUT) {
log.logInfo("- Cleaning Oudated Session : [" + theSessionToTest.getSid() + "]");
closeSession(theSessionToTest, context);
logStats(theSessionToTest);
}
}
}
// READ Init From Applet
ObjectInputStream inputFromApplet = new ObjectInputStream(in);
VNCPActionData objActionData = (VNCPActionData) inputFromApplet.readObject();
// Validation is needed for any other action than SERVER or VIEWER
if (objActionData.getAction() > 1) {
log.logInfo("- Validation needed for action : " + objActionData.getAction());
VNCPSession theSession = (VNCPSession) context.getAttribute("VNCProxySession_" + objActionData.getSessionId());
if (theSession != null) {
if (objActionData.getValidationCode() != null) {
String validation = new String(decryptInBytesWithAES(objActionData.getValidationCode(), theSession.getKey()), "UTF-8");
if (!validation.startsWith("Validation OK")) {
log.logInfo("- Invalid validation code !");
// Error
objActionDataBack = new VNCPActionData("Invalid Validation Code");
// Send response
ObjectOutputStream oos = new ObjectOutputStream(outstr);
oos.writeObject(objActionDataBack);
oos.flush();
oos.close();
return;
}
} else {
log.logInfo("- Null validation code !");
// Error
objActionDataBack = new VNCPActionData("Null Validation Code");
// Send response
ObjectOutputStream oos = new ObjectOutputStream(outstr);
oos.writeObject(objActionDataBack);
oos.flush();
oos.close();
return;
}
} else {
log.logInfo("- The Session is null ! ");
// Error
objActionDataBack = new VNCPActionData("Session Closed");
// Send response
ObjectOutputStream oos = new ObjectOutputStream(outstr);
oos.writeObject(objActionDataBack);
oos.flush();
oos.close();
return;
}
log.logInfo("- Validation code OK");
}
// PERFORM Given Action
switch (objActionData.getAction()) {
case 0:
// SERVER
log.logInfo("SERVER :");
// VNC Server ask for Session creation
// Random session number
Random rand = new java.util.Random();
int sessionId = SID_LOWER_LIMIT + rand.nextInt(SID_UPPER_LIMIT - SID_LOWER_LIMIT);
// Check if session number if free
while (context.getAttribute("VNCProxySession_" + sessionId) != null) {
sessionId++;
}
log.logInfo("- SID generated : " + sessionId);
// Create VNCPSession Object
VNCPSession newSession = new VNCPSession(sessionId);
SecretKey sk = generateKeyAES128();
newSession.setKey(sk.getEncoded());
newSession.setStartDate(System.currentTimeMillis());
newSession.setServerIPWan(getIPWan(request));
// Put the new Session in context
log.logInfo("- Putting session : [" + "VNCProxySession_" + newSession.getSid() + "] in context");
context.setAttribute("VNCProxySession_" + newSession.getSid(), newSession);
// Send back dataInit with new Session info
objActionDataBack = new VNCPActionData("" + newSession.getSid(), newSession.getKey());
break;
case 1:
// VIEWER
log.logInfo("VIEWER :");
// Viewer asking for session
// Check sessionId sent by viewer
if (context.getAttribute("VNCProxySession_" + objActionData.getSessionId()) != null) {
VNCPSession theSession = (VNCPSession) context.getAttribute("VNCProxySession_" + objActionData.getSessionId());
log.logInfo("- The Session [" + theSession.getSid() + "] is in context");
if (!theSession.getActive()) {
log.logInfo("- The Session [" + theSession.getSid() + "] is not active yet");
objActionDataBack = new VNCPActionData("" + theSession.getSid(), theSession.getKey());
theSession.setActive(true);
theSession.setViewerIPWan(getIPWan(request));
context.setAttribute("VNCProxySession_" + objActionData.getSessionId(), theSession);
} else {
// pirate ?
log.logInfo("- The Session [" + theSession.getSid() + "] already active !");
objActionDataBack = new VNCPActionData("Session already active");
}
} else {
// pirate ?
log.logInfo("- Invalid sessionId [" + objActionData.getSessionId() + "] !");
objActionDataBack = new VNCPActionData("Invalid Session ID");
}
break;
case 2:
// CLOSE
log.logInfo("CLOSE :");
// Send Back Session
VNCPSession theSession = (VNCPSession) context.getAttribute("VNCProxySession_" + objActionData.getSessionId());
if (theSession != null) {
log.logInfo("- The Session [" + theSession.getSid() + "] is in context, closing...");
objActionDataBack = new VNCPActionData("Session closed OK");
// Close...
closeSession(theSession, context);
// Send the session for history
logStats(theSession);
} else {
log.logInfo("- The Session [" + objActionData.getSessionId() + "] is closed");
objActionDataBack = new VNCPActionData("Session closed");
}
break;
default:
// ERROR
log.logInfo("- Invalid Action !");
objActionDataBack = new VNCPActionData("Invalid Action");
break;
}
// Send response
ObjectOutputStream oos = new ObjectOutputStream(outstr);
oos.writeObject(objActionDataBack);
oos.flush();
oos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* This method overrides the Servlet doGet method. It is just there for
* testing purpose.
*
* @param request
* The HTTP request
* @param response
* The HTTP response
* @throws IOException
* Any IO Exception
* @throws ServletException
* Any Servlet Exception
*/
@Override
public final void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
DataOutputStream dos = new DataOutputStream(response.getOutputStream());
StringBuffer sb = new StringBuffer();
sb.append("I'm VNCPHubAction...");
dos.write(sb.toString().getBytes());
dos.flush();
}
/**
* This methods generate a 128 Bit AES Key
*
* @return The AES SecretKey
*/
public final SecretKey generateKeyAES128() throws Exception {
VNCPLog log = new VNCPLog();
KeyGenerator keyGen = null;
try {
keyGen = KeyGenerator.getInstance("AES");
keyGen.init(AES_KEY_LEN);
return keyGen.generateKey();
} catch (Exception e) {
log.logError("Error generateKeyAES128 : " + e.getMessage());
e.printStackTrace();
throw e;
}
}
/**
* This methods decrypt an AES encoded byte array using a given AES key
*
* @param ciphertext
* The byte array to decrypt
* @param bytesKey
* The AES key
* @return The decrypted byte array
* @throws Exception
* Any Exception
*/
private byte[] decryptInBytesWithAES(final byte[] ciphertext, final byte[] bytesKey) throws Exception {
byte[] decrypted = null;
Cipher cipher = Cipher.getInstance("AES"); //$NON-NLS-1$
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(bytesKey, "AES"));
decrypted = cipher.doFinal(ciphertext);
return decrypted;
}
/**
* This methods retrieves the IP Wan address of the VNCProxy Applet that sends
* the request
*
* @param request
* The request
* @return The WAN IP Address the request is coming from
* @throws Exception
* Any Exception
*/
private String getIPWan(final HttpServletRequest request) throws Exception {
return request.getHeader("X-Forwarded-For") + "/" + request.getRemoteAddr();
}
/**
* This methods close the given VNCPSession
*
* @param theSession
* The current VNCPSession
* @param context
* The current Servlet request context
*/
public final void closeSession(final VNCPSession theSession, final ServletContext context) {
VNCPLog log = new VNCPLog();
TimeZone tz = TimeZone.getTimeZone("GMT");
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("zzz yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(tz);
log.logInfo("------------------------------------");
log.logInfo("Removing Session : " + theSession.getSid());
log.logInfo(" - startDate : " + sdf.format(new Date(theSession.getStartDate())).toString());
log.logInfo(" - lastMove : " + sdf.format(new Date(theSession.getLastMove())).toString());
log.logInfo(" - nbRequests : " + theSession.getNbRequests());
log.logInfo(" - dataSize : " + theSession.getDataSize());
context.removeAttribute("VNCProxySession_" + theSession.getSid());
Enumeration<?> attributeEnum = context.getAttributeNames();
while (attributeEnum.hasMoreElements()) {
String key = (String) attributeEnum.nextElement();
if (key.startsWith("" + theSession.getSid())) {
log.logInfo("Removing Key : " + key);
context.removeAttribute(key);
}
}
}
/**
* This method logs some statistics about the VNCPSession when closing
*
* @param theSession
* The VNCPSession
* @throws Exception
* Any Exception
*/
public final void logStats(final VNCPSession theSession) throws Exception {
VNCPLog log = new VNCPLog();
StringBuffer stats = new StringBuffer();
stats.append("'" + new Timestamp(theSession.getStartDate()) + "',");
stats.append("'" + theSession.getDataSize() + "',");
stats.append("'" + theSession.getSid() + "',");
stats.append("'" + new Timestamp(theSession.getLastMove()) + "',");
stats.append("'" + theSession.getNbRequests() + "',");
stats.append("'" + theSession.getServerIPWan() + "',");
stats.append("'" + theSession.getViewerIPWan() + "',");
//
log.logInfo("------------------------------------");
log.logInfo("SESSION STATS : [" + stats.toString() + "]");
}
}