Added support for passing authentication tokens over ssl.

JSONRPC2 request (other than the authentication message) now require a valid token to be provided.
This commit is contained in:
dev
2011-07-05 10:48:48 +00:00
parent 65a8435141
commit 27d3df3403
11 changed files with 178 additions and 138 deletions

View File

@ -20,6 +20,7 @@ import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.util.Calendar;
import java.util.logging.LogManager;
import net.i2p.I2PAppContext;
import net.i2p.i2pcontrol.security.KeyStoreInitializer;
@ -59,24 +60,14 @@ public class I2PControlController{
stop();
else
throw new IllegalArgumentException("Usage: PluginController -d $PLUGIN [start|stop]");
}
public static String getTestString(){
Calendar cal = Calendar.getInstance();
int hour = cal.get(Calendar.HOUR_OF_DAY);
int minute = cal.get(Calendar.MINUTE);
int second = cal.get(Calendar.SECOND);
int ms = cal.get(Calendar.MILLISECOND);
return hour+":"+minute+":"+second+":"+ms;
}
}
private static void start(String args[]) {
//File pluginDir = new File(args[1]);
//if (!pluginDir.exists())
// throw new IllegalArgumentException("Plugin directory " + pluginDir.getAbsolutePath() + " does not exist");
I2PAppContext.getGlobalContext().logManager().setDefaultLimit(Log.STR_DEBUG);
_server = new Server();
try {
@ -96,7 +87,7 @@ public class I2PControlController{
ServletHttpContext context = (ServletHttpContext) _server.getContext("/");
context.addServlet("/", "net.i2p.i2pcontrol.servlets.SettingsServlet");
context.addServlet("/jsonrpc", "net.i2p.i2pcontrol.servlets.JSONRPC2Servlet");
context.addServlet("/history", "net.i2p.i2pcontrol.servlets.HistoryServlet");
context.addServlet("/logs", "net.i2p.i2pcontrol.servlets.LogServlet");
_server.start();
} catch (IOException e) {
_log.error("Unable to add listener " + Settings.getListenIP()+":"+Settings.getListenPort() + " - " + e.getMessage());
@ -109,8 +100,6 @@ public class I2PControlController{
} catch (Exception e) {
_log.error("Unable to start jetty server: " + e.getMessage());
}
}

View File

@ -1,64 +0,0 @@
package net.i2p.i2pcontrol;
/*
* Copyright 2010 hottuna (dev@robertfoss.se)
*
* 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.
*
*/
import java.lang.reflect.*;
/**
* There can be only one - ie. even if the class is loaded in several different classloaders,
* there will be only one instance of the object.
*/
public class I2PControlManager{
private static StringBuilder _history;
public static I2PControlManager instance = null;
public synchronized static I2PControlManager getInstance() {
if (instance == null) {
instance = new I2PControlManager();
}
return instance;
}
private I2PControlManager() {
_history = new StringBuilder();
}
/* (non-Javadoc)
* @see net.i2p.i2pcontrol.SingletonInterface#prependHistory(java.lang.String)
*/
public void prependHistory(String str){
_history.insert(0,str + "<br>\n");
}
/* (non-Javadoc)
* @see net.i2p.i2pcontrol.SingletonInterface#appendHistory(java.lang.String)
*/
public void appendHistory(String str){
_history.append("<br>\n" + str);
}
/* (non-Javadoc)
* @see net.i2p.i2pcontrol.SingletonInterface#getHistory()
*/
public String getHistory(){
return _history.toString();
}
}

View File

@ -1,8 +1,11 @@
package net.i2p.i2pcontrol.security;
import java.text.SimpleDateFormat;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.Date;
public class AuthToken {
private static final int VALIDITY_TIME = 1; // Measured in days
private String id;
@ -25,7 +28,13 @@ public class AuthToken {
* @return True if AuthToken hasn't expired. False in any other case.
*/
public boolean isValid(){
return Calendar.getInstance().before(expiry);
return Calendar.getInstance().getTime().before(expiry);
}
public String getExpiryTime(){
SimpleDateFormat sdf = new SimpleDateFormat();
sdf.applyPattern("yyyy-MM-dd HH:mm:ss");
return sdf.format(expiry);
}
@Override

View File

@ -4,7 +4,15 @@ public class ExpiredAuthTokenException extends Exception{
private static final long serialVersionUID = 2279019346592900289L;
public ExpiredAuthTokenException(String str){
private String expiryTime;
public ExpiredAuthTokenException(String str, String expiryTime){
super(str);
this.expiryTime = expiryTime;
}
public String getExpirytime(){
return expiryTime;
}
}

View File

@ -19,8 +19,11 @@ package net.i2p.i2pcontrol.security;
import java.security.KeyStore;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocket;
@ -45,7 +48,8 @@ public class SecurityManager {
private final static String SSL_PROVIDER = "SunJSSE";
private final static String DEFAULT_AUTH_BCRYPT_SALT = "$2a$11$5aOLx2x/8i4fNaitoCSSWu";
private final static String DEFAULT_AUTH_PASSWORD = "$2a$11$5aOLx2x/8i4fNaitoCSSWuut2wEl3Hupuca8DCT.NXzvH9fq1pBU.";
private static HashMap<String,AuthToken> authTokens;
private final static HashMap<String,AuthToken> authTokens;
private final static Timer timer;
private static String[] SSL_CIPHER_SUITES;
private static KeyStore _ks;
private static Log _log;
@ -53,12 +57,20 @@ public class SecurityManager {
static {
_log = I2PAppContext.getGlobalContext().logManager().getLog(SecurityManager.class);
authTokens = new HashMap<String,AuthToken>();
timer = new Timer();
// Start running periodic task after 20 minutes, run periodically every 10th minute.
timer.scheduleAtFixedRate(new Sweeper(), 1000*60*20, 1000*60*10);
// Get supported SSL copher suites.
SocketFactory SSLF = SSLSocketFactory.getDefault();
try{
SSL_CIPHER_SUITES = ((SSLSocket)SSLF.createSocket()).getSupportedCipherSuites();
SSL_CIPHER_SUITES = ((SSLSocket)SSLF.createSocket()).getSupportedCipherSuites();
} catch (Exception e){
_log.log(Log.CRIT, "Unable to create SSLSocket used for fetching supported ssl cipher suites.", e);
}
// Initialize keystore (if needed)
_ks = KeyStoreInitializer.getKeyStore();
}
@ -82,7 +94,10 @@ public class SecurityManager {
return KeyStoreFactory.DEFAULT_KEYSTORE_TYPE;
}
/**
* Return the X509Certificate of the server as a Base64 encoded string.
* @return base64 encode of X509Certificate
*/
public static String getBase64Cert(){
X509Certificate caCert = KeyStoreFactory.readCert(_ks,
CERT_ALIAS,
@ -90,6 +105,11 @@ public class SecurityManager {
return getBase64FromCert(caCert);
}
/**
* Return the X509Certificate as a base64 encoded string.
* @param cert
* @return base64 encode of X509Certificate
*/
private static String getBase64FromCert(X509Certificate cert){
BASE64Encoder encoder = new BASE64Encoder();
try {
@ -103,8 +123,9 @@ public class SecurityManager {
/**
* Hash input HASH_ITERATIONS times
* @return input hashed HASH_ITERATIONS times
* Hash pwd with using BCrypt with the default salt.
* @param pwd
* @return BCrypt hash of salt and input string
*/
public static String getPasswdHash(String pwd){
return BCrypt.hashpw(pwd, ConfigurationManager.getInstance().getConf("auth.salt", DEFAULT_AUTH_BCRYPT_SALT));
@ -151,11 +172,36 @@ public class SecurityManager {
if (token == null){
throw new InvalidAuthTokenException("AuthToken with ID: " + tokenID + " couldn't be found.");
} else if (!token.isValid()){
System.out.println("token.isValid: " + token.isValid()); // Delete me
authTokens.remove(token.getId());
throw new ExpiredAuthTokenException("AuthToken with ID: " + tokenID + " has expired.");
throw new ExpiredAuthTokenException("AuthToken with ID: " + tokenID + " expired " + token.getExpiryTime(), token.getExpiryTime());
} else {
return; // Everything is fine. :)
}
}
}
/**
* Clean up old authorization tokens to keep the token store slim and fit.
* @author hottuna
*
*/
private static class Sweeper extends TimerTask{
@Override
public void run(){
_log.debug("Starting cleanup job..");
ArrayList<String> arr = new ArrayList<String>();
for (Map.Entry<String,AuthToken> e : authTokens.entrySet()){
AuthToken token = e.getValue();
if (!token.isValid()){
arr.add(e.getKey());
}
}
for (String s : arr){
authTokens.remove(s);
}
_log.debug("Cleanup job done.");
}
}
}

View File

@ -22,27 +22,16 @@ import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import net.i2p.I2PAppContext;
import net.i2p.i2pcontrol.I2PControlManager;
import net.i2p.stat.RateStat;
import net.i2p.util.Log;
import net.i2p.i2pcontrol.security.AuthToken;
import net.i2p.i2pcontrol.security.SecurityManager;
import net.i2p.i2pcontrol.servlets.jsonrpc2handlers.AuthHandler;
import net.i2p.i2pcontrol.servlets.jsonrpc2handlers.EchoHandler;
import net.i2p.i2pcontrol.servlets.jsonrpc2handlers.JSONRPC2ExtendedError;
import net.i2p.i2pcontrol.servlets.jsonrpc2handlers.StatHandler;
import com.thetransactioncompany.jsonrpc2.*;
@ -58,15 +47,14 @@ public class JSONRPC2Servlet extends HttpServlet{
private static final int BUFFER_LENGTH = 2048;
private static Dispatcher disp;
private static char[] readBuffer;
private static I2PControlManager _manager;
private static Log _log;
@Override
public void init(){
_log = I2PAppContext.getGlobalContext().logManager().getLog(JSONRPC2Servlet.class);
_log.setMinimumPriority(Log.INFO);
readBuffer = new char[BUFFER_LENGTH];
_manager = I2PControlManager.getInstance();
disp = new Dispatcher();
disp.register(new EchoHandler());
@ -94,12 +82,12 @@ public class JSONRPC2Servlet extends HttpServlet{
if (msg instanceof JSONRPC2Request) {
jsonResp = disp.dispatch((JSONRPC2Request)msg, null);
_manager.prependHistory("Request: " + msg);
_manager.prependHistory("Response: " + jsonResp);
_log.debug("Request: " + msg);
_log.debug("Response: " + jsonResp);
}
else if (msg instanceof JSONRPC2Notification) {
disp.dispatch((JSONRPC2Notification)msg, null);
_manager.prependHistory("Notification: " + msg);
_log.debug("Notification: " + msg);
}
out.println(jsonResp);
@ -119,11 +107,4 @@ public class JSONRPC2Servlet extends HttpServlet{
}
return writer.toString();
}
}

View File

@ -2,6 +2,7 @@ package net.i2p.i2pcontrol.servlets;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
@ -9,23 +10,20 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.i2p.I2PAppContext;
import net.i2p.i2pcontrol.I2PControlManager;
import net.i2p.util.Log;
public class HistoryServlet extends HttpServlet {
public class LogServlet extends HttpServlet {
/**
*
*/
private static final long serialVersionUID = -4018705582081424641L;
private static I2PControlManager _manager;
private static Log _log;
@Override
public void init(){
_log = I2PAppContext.getGlobalContext().logManager().getLog(HistoryServlet.class);
_manager = I2PControlManager.getInstance();
_log = I2PAppContext.getGlobalContext().logManager().getLog(LogServlet.class);
}
protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException
@ -33,7 +31,17 @@ public class HistoryServlet extends HttpServlet {
httpServletResponse.setContentType("text/html");
PrintWriter out = httpServletResponse.getWriter();
out.println("<html>\n<body>");
out.println(_manager.getHistory());
out.println("<h2>Logs</h2>");
out.println("<h3>Most recent: </h3>");
List<String> strs = I2PAppContext.getGlobalContext().logManager().getBuffer().getMostRecentMessages();
for (String s : strs){
out.println(s + "<br>");
}
out.println("<br>\r\n<h3>Recent critical: </h3>");
strs = I2PAppContext.getGlobalContext().logManager().getBuffer().getMostRecentCriticalMessages();
for (String s : strs){
out.println(s + "<br>");
}
out.println("</body>\n</html>");
out.close();
}

View File

@ -9,7 +9,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.i2p.I2PAppContext;
import net.i2p.i2pcontrol.I2PControlManager;
import net.i2p.util.Log;
public class SettingsServlet extends HttpServlet {
@ -18,14 +17,12 @@ public class SettingsServlet extends HttpServlet {
*
*/
private static final long serialVersionUID = -4018705582081424641L;
private static I2PControlManager _manager;
private static Log _log;
@Override
public void init(){
_log = I2PAppContext.getGlobalContext().logManager().getLog(SettingsServlet.class);
_manager = I2PControlManager.getInstance();
}
protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException

View File

@ -24,7 +24,7 @@ public class AuthHandler implements RequestHandler {
// Processes the requests
public JSONRPC2Response process(JSONRPC2Request req, MessageContext ctx) {
if (req.getMethod().equals("authenticate")) {
JSONRPC2Error err = JSONRPC2Helper.validateParams(requiredArgs, req);
JSONRPC2Error err = JSONRPC2Helper.validateParams(requiredArgs, req, JSONRPC2Helper.USE_NO_AUTH);
if (err != null)
return new JSONRPC2Response(err, req.getID());
@ -39,7 +39,7 @@ public class AuthHandler implements RequestHandler {
}
Map outParams = new HashMap();
outParams.put("tokenID", token.getId());
outParams.put("token", token.getId());
return new JSONRPC2Response(outParams, req.getID());
} else {
// Method name not supported

View File

@ -61,16 +61,23 @@ public class JSONRPC2ExtendedError extends JSONRPC2Error {
private static final long serialVersionUID = -6574632977222371077L;
/** Invalid JSON-RPC 2.0, implementation defined error (-32099 .. -32000) */
public static final JSONRPC2Error INVALID_PASSWORD = new JSONRPC2ExtendedError(-32001, "Invalid password provided");
public static final int CODE_INVALID_PASSWORD = -32001;
public static final JSONRPC2Error INVALID_PASSWORD = new JSONRPC2ExtendedError(CODE_INVALID_PASSWORD, "Invalid password provided.");
/** Invalid JSON-RPC 2.0, implementation defined error (-32099 .. -32000) */
public static final JSONRPC2Error INVALID_TOKEN = new JSONRPC2ExtendedError(-32002, "Token doesn't exist");
public static final int CODE_NO_TOKEN = -32002;
public static final JSONRPC2Error NO_TOKEN = new JSONRPC2ExtendedError(CODE_NO_TOKEN, "No authentication token presented.");
/** Invalid JSON-RPC 2.0, implementation defined error (-32099 .. -32000) */
public static final JSONRPC2Error TOKEN_EXPIRED = new JSONRPC2ExtendedError(-32003, "Provided token was expired, will be removed.");
public static final int CODE_INVALID_TOKEN = -32003;
public static final JSONRPC2Error INVALID_TOKEN = new JSONRPC2ExtendedError(CODE_INVALID_TOKEN, "Authentication token doesn't exist.");
/** Invalid JSON-RPC 2.0, implementation defined error (-32099 .. -32000) */
public static final int CODE_TOKEN_EXPIRED = -32004;
public static final JSONRPC2Error TOKEN_EXPIRED = new JSONRPC2ExtendedError(CODE_TOKEN_EXPIRED, "Provided authentication token was expired, will be removed.");
/** Code used for invalid JSON-RPC 2.0, implementation defined error. Error describes missing parameter/parameters */
public static final int CODE_MISSING_PARAMETER = -32004;
public static final int CODE_MISSING_PARAMETER = -32005;
/**
* Creates a new JSON-RPC 2.0 error with the specified code and

View File

@ -1,29 +1,88 @@
package net.i2p.i2pcontrol.servlets.jsonrpc2handlers;
import net.i2p.i2pcontrol.security.*;
import net.i2p.i2pcontrol.security.SecurityManager;
import java.util.HashMap;
import com.thetransactioncompany.jsonrpc2.JSONRPC2Error;
import com.thetransactioncompany.jsonrpc2.JSONRPC2ParamsType;
import com.thetransactioncompany.jsonrpc2.JSONRPC2Request;
import com.thetransactioncompany.jsonrpc2.JSONRPC2Response;
public class JSONRPC2Helper {
public static JSONRPC2Error validateParams(String[] requiredArgs, JSONRPC2Request req){
public final static Boolean USE_NO_AUTH = false;
public final static Boolean USE_AUTH = true;
/**
* Check incoming request for required arguments, to make sure they are valid.
* @param requiredArgs - Array of names of required arguments. If null don't check for any parameters.
* @param req - Incoming JSONRPC2 request
* @param useAuth - If true, will validate authentication token.
* @return - null if no errors were found. Corresponding JSONRPC2Error if error is found.
*/
public static JSONRPC2Error validateParams(String[] requiredArgs, JSONRPC2Request req, Boolean useAuth){
// Error on unnamed parameters
if (req.getParamsType() != JSONRPC2ParamsType.OBJECT){
return JSONRPC2Error.INVALID_PARAMS;
}
HashMap params = (HashMap) req.getParams();
String missingArgs = "";
for (int i = 0; i < requiredArgs.length; i++){
if (!params.containsKey(requiredArgs[i])){
missingArgs = missingArgs.concat(requiredArgs[i] + ",");
// Validate authentication token.
if (useAuth){
JSONRPC2Error err = validateToken(params);
if (err != null){
return err;
}
}
if (missingArgs.length() > 0){
missingArgs = missingArgs.substring(0, missingArgs.length()-1);
return new JSONRPC2ExtendedError(JSONRPC2ExtendedError.CODE_MISSING_PARAMETER, "Missing parameter(s): " + missingArgs);
// If there exist any required arguments
if (requiredArgs != null && requiredArgs.length > 0){
String missingArgs = "";
for (int i = 0; i < requiredArgs.length; i++){
if (!params.containsKey(requiredArgs[i])){
missingArgs = missingArgs.concat(requiredArgs[i] + ",");
}
}
if (missingArgs.length() > 0){
missingArgs = missingArgs.substring(0, missingArgs.length()-1);
return new JSONRPC2ExtendedError(JSONRPC2ExtendedError.CODE_MISSING_PARAMETER, "Missing parameter(s): " + missingArgs);
}
}
return null;
}
/**
* Check incoming request for required arguments, to make sure they are valid. Will authenticate req.
* @param requiredArgs - Array of names of required arguments. If null don't check for any parameters.
* @param req - Incoming JSONRPC2 request
* @return - null if no errors were found. Corresponding JSONRPC2Error if error is found.
*/
public static JSONRPC2Error validateParams(String[] requiredArgs, JSONRPC2Request req){
return validateParams(requiredArgs, req, JSONRPC2Helper.USE_AUTH);
}
/**
* Will check incoming parameters to make sure they contain a valid token.
* @param req - Parameters of incoming request
* @return null if everything is fine, JSONRPC2Error for any corresponding error.
*/
private static JSONRPC2Error validateToken(HashMap params){
String tokenID = (String) params.get("token");
if (tokenID == null){
return JSONRPC2ExtendedError.NO_TOKEN;
}
try {
SecurityManager.verifyToken(tokenID);
} catch (InvalidAuthTokenException e){
return JSONRPC2ExtendedError.INVALID_TOKEN;
} catch (ExpiredAuthTokenException e){
JSONRPC2Error err = new JSONRPC2ExtendedError(JSONRPC2ExtendedError.CODE_TOKEN_EXPIRED,
"Provided authentication token expired "+e.getExpirytime()+", will be removed.");
return err;
}
return null;
}