diff --git a/LICENSE.txt b/LICENSE.txt index 6e2f8e1ed8..0c7ba87ade 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -207,6 +207,14 @@ Applications: See https://glassfish.dev.java.net/public/CDDL+GPL.html See licenses/LICENSE-GPLv2.txt + I2PControl + See licenses/LICENSE-Apache2.0.txt + Includes jBCrypt: + Copyright (c) 2006 Damien Miller + See licenses/LICENSE-jBCrypt.txt + Includes jsonrpc2 1.38.1 (base) 1.11 (server) + See licenses/LICENSE-Apache2.0.txt + I2PSnark (i2psnark.jar, i2psnark.war): Copyright (C) 2003 Mark J. Wielaard GPLv2 (or any later version) diff --git a/apps/i2pcontrol/build.xml b/apps/i2pcontrol/build.xml new file mode 100644 index 0000000000..e6cf773070 --- /dev/null +++ b/apps/i2pcontrol/build.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Error.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Error.java new file mode 100644 index 0000000000..02f92fca2c --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Error.java @@ -0,0 +1,274 @@ +package com.thetransactioncompany.jsonrpc2; + + +import org.json.simple.JSONObject; + + +/** + * Represents a JSON-RPC 2.0 error that occurred during the processing of a + * request. This class is immutable. + * + *

The protocol expects error objects to be structured like this: + * + *

+ * + *

Note that the "Error" word in the class name was put there solely to + * comply with the parlance of the JSON-RPC spec. This class doesn't inherit + * from {@code java.lang.Error}. It's a regular subclass of + * {@code java.lang.Exception} and, if thrown, it's to indicate a condition + * that a reasonable application might want to catch. + * + *

This class also includes convenient final static instances for all + * standard JSON-RPC 2.0 errors: + * + *

+ * + *

Note that the range -32099..-32000 is reserved for additional server + * errors. + * + *

The mapping between JSON and Java entities (as defined by the + * underlying JSON Smart library): + *

+ *     true|false  <--->  java.lang.Boolean
+ *     number      <--->  java.lang.Number
+ *     string      <--->  java.lang.String
+ *     array       <--->  java.util.List
+ *     object      <--->  java.util.Map
+ *     null        <--->  null
+ * 
+ * + * @author Vladimir Dzhuvinov + */ +public class JSONRPC2Error extends Exception { + + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 4682571044532698806L; + + + /** + * JSON parse error (-32700). + */ + public static final JSONRPC2Error PARSE_ERROR = new JSONRPC2Error(-32700, "JSON parse error"); + + + /** + * Invalid JSON-RPC 2.0 request error (-32600). + */ + public static final JSONRPC2Error INVALID_REQUEST = new JSONRPC2Error(-32600, "Invalid request"); + + + /** + * Method not found error (-32601). + */ + public static final JSONRPC2Error METHOD_NOT_FOUND = new JSONRPC2Error(-32601, "Method not found"); + + + /** + * Invalid parameters error (-32602). + */ + public static final JSONRPC2Error INVALID_PARAMS = new JSONRPC2Error(-32602, "Invalid parameters"); + + + /** + * Internal JSON-RPC 2.0 error (-32603). + */ + public static final JSONRPC2Error INTERNAL_ERROR = new JSONRPC2Error(-32603, "Internal error"); + + + /** + * The error code. + */ + private final int code; + + + /** + * The optional error data. + */ + private final Object data; + + + /** + * Appends the specified string to the message of a JSON-RPC 2.0 error. + * + * @param err The JSON-RPC 2.0 error. Must not be {@code null}. + * @param apx The string to append to the original error message. + * + * @return A new JSON-RPC 2.0 error with the appended message. + */ + @Deprecated + public static JSONRPC2Error appendMessage(final JSONRPC2Error err, final String apx) { + + return new JSONRPC2Error(err.getCode(), err.getMessage() + apx, err.getData()); + } + + + /** + * Sets the specified data to a JSON-RPC 2.0 error. + * + * @param err The JSON-RPC 2.0 error to have its data field set. Must + * not be {@code null}. + * @param data Optional error data, must map to a + * valid JSON type. + * + * @return A new JSON-RPC 2.0 error with the set data. + */ + @Deprecated + public static JSONRPC2Error setData(final JSONRPC2Error err, final Object data) { + + return new JSONRPC2Error(err.getCode(), err.getMessage(), data); + } + + + /** + * Creates a new JSON-RPC 2.0 error with the specified code and + * message. The optional data is omitted. + * + * @param code The error code (standard pre-defined or + * application-specific). + * @param message The error message. + */ + public JSONRPC2Error(int code, String message) { + + this(code, message, null); + } + + + /** + * Creates a new JSON-RPC 2.0 error with the specified code, + * message and data. + * + * @param code The error code (standard pre-defined or + * application-specific). + * @param message The error message. + * @param data Optional error data, must map + * to a valid JSON type. + */ + public JSONRPC2Error(int code, String message, Object data) { + + super(message); + this.code = code; + this.data = data; + } + + + /** + * Gets the JSON-RPC 2.0 error code. + * + * @return The error code. + */ + public int getCode() { + + return code; + } + + + /** + * Gets the JSON-RPC 2.0 error data. + * + * @return The error data, {@code null} if none was specified. + */ + public Object getData() { + + return data; + } + + + /** + * Sets the specified data to a JSON-RPC 2.0 error. + * + * @param data Optional error data, must map to a + * valid JSON type. + * + * @return A new JSON-RPC 2.0 error with the set data. + */ + public JSONRPC2Error setData(final Object data) { + + return new JSONRPC2Error(code, getMessage(), data); + } + + + /** + * Appends the specified string to the message of this JSON-RPC 2.0 + * error. + * + * @param apx The string to append to the original error message. + * + * @return A new JSON-RPC 2.0 error with the appended message. + */ + public JSONRPC2Error appendMessage(final String apx) { + + return new JSONRPC2Error(code, getMessage() + apx, data); + } + + + /** + * @see #toJSONObject + */ + @Deprecated + public JSONObject toJSON() { + + return toJSONObject(); + } + + + /** + * Returns a JSON object representation of this JSON-RPC 2.0 error. + * + * @return A JSON object representing this error object. + */ + public JSONObject toJSONObject() { + + JSONObject out = new JSONObject(); + + out.put("code", code); + out.put("message", super.getMessage()); + if (data != null) + out.put("data", data); + + return out; + } + + + /** + * Serialises the error object to a JSON string. + * + * @return A JSON-encoded string representing this error object. + */ + @Override + public String toString() { + + return toJSON().toString(); + } + + + /** + * Overrides {@code Object.equals()}. + * + * @param object The object to compare to. + * + * @return {@code true} if both objects are instances if this class and + * their error codes are identical, {@code false} if not. + */ + @Override + public boolean equals(Object object) { + + return object != null && + object instanceof JSONRPC2Error && + code == ((JSONRPC2Error)object).getCode(); + } +} diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Message.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Message.java new file mode 100644 index 0000000000..561f5865be --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Message.java @@ -0,0 +1,251 @@ +package com.thetransactioncompany.jsonrpc2; + + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.json.simple.JSONAware; +import org.json.simple.JSONObject; + + +/** + * The base abstract class for JSON-RPC 2.0 requests, notifications and + * responses. Provides common methods for parsing (from JSON string) and + * serialisation (to JSON string) of these three message types. + * + *

Example parsing and serialisation back to JSON: + * + *

+ * String jsonString = "{\"method\":\"progressNotify\",\"params\":[\"75%\"],\"jsonrpc\":\"2.0\"}";
+ *
+ * JSONRPC2Message message = null;
+ *
+ * // parse
+ * try {
+ *        message = JSONRPC2Message.parse(jsonString);
+ * } catch (JSONRPC2ParseException e) {
+ *        // handle parse exception
+ * }
+ *
+ * if (message instanceof JSONRPC2Request)
+ *        System.out.println("The message is a request");
+ * else if (message instanceof JSONRPC2Notification)
+ *        System.out.println("The message is a notification");
+ * else if (message instanceof JSONRPC2Response)
+ *        System.out.println("The message is a response");
+ *
+ * // serialise back to JSON string
+ * System.out.println(message);
+ *
+ * 
+ * + *

The mapping between JSON and Java entities (as defined by the + * underlying JSON Smart library): + * + *

+ *     true|false  <--->  java.lang.Boolean
+ *     number      <--->  java.lang.Number
+ *     string      <--->  java.lang.String
+ *     array       <--->  java.util.List
+ *     object      <--->  java.util.Map
+ *     null        <--->  null
+ * 
+ * + * @author Vladimir Dzhuvinov + */ +public abstract class JSONRPC2Message implements JSONAware { + + + /** + * Map of non-standard JSON-RPC 2.0 message attributes, {@code null} if + * none. + */ + private Map nonStdAttributes = null; + + + /** + * Provides common parsing of JSON-RPC 2.0 requests, notifications + * and responses. Use this method if you don't know which type of + * JSON-RPC message the input JSON string represents. + * + *

Batched requests / notifications are not supported. + * + *

This method is thread-safe. + * + *

If you are certain about the message type use the dedicated + * {@link JSONRPC2Request#parse}, {@link JSONRPC2Notification#parse} + * or {@link JSONRPC2Response#parse} methods. They are more efficient + * and provide a more detailed parse error reporting. + * + *

The member order of parsed JSON objects will not be preserved + * (for efficiency reasons) and the JSON-RPC 2.0 version field must be + * set to "2.0". To change this behaviour check the optional {@link + * #parse(String,boolean,boolean)} method. + * + * @param jsonString A JSON string representing a JSON-RPC 2.0 request, + * notification or response, UTF-8 encoded. Must not + * be {@code null}. + * + * @return An instance of {@link JSONRPC2Request}, + * {@link JSONRPC2Notification} or {@link JSONRPC2Response}. + * + * @throws JSONRPC2ParseException With detailed message if parsing + * failed. + */ + public static JSONRPC2Message parse(final String jsonString) + throws JSONRPC2ParseException { + + return parse(jsonString, false, false); + } + + + /** + * Provides common parsing of JSON-RPC 2.0 requests, notifications + * and responses. Use this method if you don't know which type of + * JSON-RPC message the input string represents. + * + *

Batched requests / notifications are not supported. + * + *

This method is thread-safe. + * + *

If you are certain about the message type use the dedicated + * {@link JSONRPC2Request#parse}, {@link JSONRPC2Notification#parse} + * or {@link JSONRPC2Response#parse} methods. They are more efficient + * and provide a more detailed parse error reporting. + * + * @param jsonString A JSON string representing a JSON-RPC 2.0 + * request, notification or response, UTF-8 + * encoded. Must not be {@code null}. + * @param preserveOrder If {@code true} the member order of JSON objects + * in parameters and results must be preserved. + * @param ignoreVersion If {@code true} the {@code "jsonrpc":"2.0"} + * version field in the JSON-RPC 2.0 message will + * not be checked. + * + * @return An instance of {@link JSONRPC2Request}, + * {@link JSONRPC2Notification} or {@link JSONRPC2Response}. + * + * @throws JSONRPC2ParseException With detailed message if parsing + * failed. + */ + public static JSONRPC2Message parse(final String jsonString, final boolean preserveOrder, final boolean ignoreVersion) + throws JSONRPC2ParseException { + + JSONRPC2Parser parser = new JSONRPC2Parser(preserveOrder, ignoreVersion); + + return parser.parseJSONRPC2Message(jsonString); + } + + + /** + * Appends a non-standard attribute to this JSON-RPC 2.0 message. This is + * done by adding a new member (key / value pair) to the top level JSON + * object representing the message. + * + *

You may use this method to add meta and debugging attributes, + * such as the request processing time, to a JSON-RPC 2.0 message. + * + * @param name The attribute name. Must not conflict with the existing + * "method", "id", "params", "result", "error" and "jsonrpc" + * attributes reserved by the JSON-RPC 2.0 protocol, else + * an {@code IllegalArgumentException} will be thrown. Must + * not be {@code null} either. + * @param value The attribute value. Must be of type String, boolean, + * number, List, Map or null, else an + * {@code IllegalArgumentException} will be thrown. + */ + public void appendNonStdAttribute(final String name, final Object value) { + + // Name check + if (name == null || + name.equals("method") || + name.equals("id") || + name.equals("params") || + name.equals("result") || + name.equals("error") || + name.equals("jsonrpc") ) + + throw new IllegalArgumentException("Non-standard attribute name violation"); + + // Value check + if ( value != null && + ! (value instanceof Boolean) && + ! (value instanceof Number) && + ! (value instanceof String) && + ! (value instanceof List) && + ! (value instanceof Map) ) + + throw new IllegalArgumentException("Illegal non-standard attribute value, must map to a valid JSON type"); + + + if (nonStdAttributes == null) + nonStdAttributes = new HashMap(); + + nonStdAttributes.put(name, value); + } + + + /** + * Retrieves a non-standard JSON-RPC 2.0 message attribute. + * + * @param name The name of the non-standard attribute to retrieve. Must + * not be {@code null}. + * + * @return The value of the non-standard attribute (may also be + * {@code null}, {@code null} if not found. + */ + public Object getNonStdAttribute(final String name) { + + if (nonStdAttributes == null) + return null; + + return nonStdAttributes.get(name); + } + + + /** + * Retrieves the non-standard JSON-RPC 2.0 message attributes. + * + * @return The non-standard attributes as a map, {@code null} if none. + */ + public Map getNonStdAttributes() { + + return nonStdAttributes; + } + + + /** + * Returns a JSON object representing this JSON-RPC 2.0 message. + * + * @return The JSON object. + */ + public abstract JSONObject toJSONObject(); + + + /** + * Returns a JSON string representation of this JSON-RPC 2.0 message. + * + * @see #toString + * + * @return The JSON object string representing this JSON-RPC 2.0 + * message. + */ + public String toJSONString() { + + return toString(); + } + + + /** + * Serialises this JSON-RPC 2.0 message to a JSON object string. + * + * @return The JSON object string representing this JSON-RPC 2.0 + * message. + */ + @Override + public String toString() { + + return toJSONObject().toString(); + } +} diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Notification.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Notification.java new file mode 100644 index 0000000000..12d471f275 --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Notification.java @@ -0,0 +1,448 @@ +package com.thetransactioncompany.jsonrpc2; + + +import java.util.List; +import java.util.Map; + +import org.json.simple.JSONObject; + + +/** + * Represents a JSON-RPC 2.0 notification. + * + *

Notifications provide a mean for calling a remote procedure without + * generating a response. Note that notifications are inherently unreliable + * as no confirmation is sent back to the caller. + * + *

Notifications have the same JSON structure as requests, except that they + * lack an identifier: + *

+ * + *

Here is a sample JSON-RPC 2.0 notification string: + * + *

+ * {  
+ *    "method"  : "progressNotify",
+ *    "params"  : ["75%"],
+ *    "jsonrpc" : "2.0"
+ * }
+ * 
+ * + *

This class provides two methods to obtain a request object: + *

+ * + *

Example 1: Parsing a notification string: + * + *

+ * String jsonString = "{\"method\":\"progressNotify\",\"params\":[\"75%\"],\"jsonrpc\":\"2.0\"}";
+ * 
+ * JSONRPC2Notification notification = null;
+ * 
+ * try {
+ *         notification = JSONRPC2Notification.parse(jsonString);
+ *
+ * } catch (JSONRPC2ParseException e) {
+ *         // handle exception
+ * }
+ * 
+ * + *

Example 2: Recreating the above request: + * + *

+ * String method = "progressNotify";
+ * List<Object> params = new Vector<Object>();
+ * params.add("75%");
+ *
+ * JSONRPC2Notification notification = new JSONRPC2Notification(method, params);
+ *
+ * System.out.println(notification);
+ * 
+ * + *

The mapping between JSON and Java entities (as defined by the + * underlying JSON Smart library): + * + *

+ *     true|false  <--->  java.lang.Boolean
+ *     number      <--->  java.lang.Number
+ *     string      <--->  java.lang.String
+ *     array       <--->  java.util.List
+ *     object      <--->  java.util.Map
+ *     null        <--->  null
+ * 
+ * + * @author Vladimir Dzhuvinov + */ +public class JSONRPC2Notification extends JSONRPC2Message { + + + /** + * The requested method name. + */ + private String method; + + + /** + * The positional parameters, {@code null} if none. + */ + private List positionalParams; + + + /** + * The named parameters, {@code null} if none. + */ + private Map namedParams; + + + /** + * Parses a JSON-RPC 2.0 notification string. This method is + * thread-safe. + * + * @param jsonString The JSON-RPC 2.0 notification string, UTF-8 + * encoded. Must not be {@code null}. + * + * @return The corresponding JSON-RPC 2.0 notification object. + * + * @throws JSONRPC2ParseException With detailed message if parsing + * failed. + */ + public static JSONRPC2Notification parse(final String jsonString) + throws JSONRPC2ParseException { + + return parse(jsonString, false, false, false); + } + + + /** + * Parses a JSON-RPC 2.0 notification string. This method is + * thread-safe. + * + * @param jsonString The JSON-RPC 2.0 notification string, UTF-8 + * encoded. Must not be {@code null}. + * @param preserveOrder {@code true} to preserve the order of JSON + * object members in parameters. + * + * @return The corresponding JSON-RPC 2.0 notification object. + * + * @throws JSONRPC2ParseException With detailed message if parsing + * failed. + */ + public static JSONRPC2Notification parse(final String jsonString, + final boolean preserveOrder) + throws JSONRPC2ParseException { + + return parse(jsonString, preserveOrder, false, false); + } + + + /** + * Parses a JSON-RPC 2.0 notification string. This method is + * thread-safe. + * + * @param jsonString The JSON-RPC 2.0 notification string, UTF-8 + * encoded. Must not be {@code null}. + * @param preserveOrder {@code true} to preserve the order of JSON + * object members in parameters. + * @param ignoreVersion {@code true} to skip a check of the + * {@code "jsonrpc":"2.0"} version attribute in the + * JSON-RPC 2.0 message. + * + * @return The corresponding JSON-RPC 2.0 notification object. + * + * @throws JSONRPC2ParseException With detailed message if parsing + * failed. + */ + public static JSONRPC2Notification parse(final String jsonString, + final boolean preserveOrder, + final boolean ignoreVersion) + throws JSONRPC2ParseException { + + return parse(jsonString, preserveOrder, ignoreVersion, false); + } + + + /** + * Parses a JSON-RPC 2.0 notification string. This method is + * thread-safe. + * + * @param jsonString The JSON-RPC 2.0 notification string, + * UTF-8 encoded. Must not be {@code null}. + * @param preserveOrder {@code true} to preserve the order of + * JSON object members in parameters. + * @param ignoreVersion {@code true} to skip a check of the + * {@code "jsonrpc":"2.0"} version + * attribute in the JSON-RPC 2.0 message. + * @param parseNonStdAttributes {@code true} to parse non-standard + * attributes found in the JSON-RPC 2.0 + * message. + * + * @return The corresponding JSON-RPC 2.0 notification object. + * + * @throws JSONRPC2ParseException With detailed message if parsing + * failed. + */ + public static JSONRPC2Notification parse(final String jsonString, + final boolean preserveOrder, + final boolean ignoreVersion, + final boolean parseNonStdAttributes) + throws JSONRPC2ParseException { + + JSONRPC2Parser parser = new JSONRPC2Parser(preserveOrder, ignoreVersion, parseNonStdAttributes); + + return parser.parseJSONRPC2Notification(jsonString); + } + + + /** + * Constructs a new JSON-RPC 2.0 notification with no parameters. + * + * @param method The name of the requested method. Must not be + * {@code null}. + */ + public JSONRPC2Notification(final String method) { + + setMethod(method); + setParams(null); + } + + + /** + * Constructs a new JSON-RPC 2.0 notification with positional (JSON + * array) parameters. + * + * @param method The name of the requested method. Must not + * be {@code null}. + * @param positionalParams The positional (JSON array) parameters, + * {@code null} if none. + */ + public JSONRPC2Notification(final String method, + final List positionalParams) { + + setMethod(method); + setPositionalParams(positionalParams); + } + + + /** + * Constructs a new JSON-RPC 2.0 notification with named (JSON object) + * parameters. + * + * @param method The name of the requested method. + * @param namedParams The named (JSON object) parameters, {@code null} + * if none. + */ + public JSONRPC2Notification(final String method, + final Map namedParams) { + + setMethod(method); + setNamedParams(namedParams); + } + + + /** + * Gets the name of the requested method. + * + * @return The method name. + */ + public String getMethod() { + + return method; + } + + + /** + * Sets the name of the requested method. + * + * @param method The method name. Must not be {@code null}. + */ + public void setMethod(final String method) { + + // The method name is mandatory + if (method == null) + throw new IllegalArgumentException("The method name must not be null"); + + this.method = method; + } + + + /** + * Gets the parameters type ({@link JSONRPC2ParamsType#ARRAY positional}, + * {@link JSONRPC2ParamsType#OBJECT named} or + * {@link JSONRPC2ParamsType#NO_PARAMS none}). + * + * @return The parameters type. + */ + public JSONRPC2ParamsType getParamsType() { + + if (positionalParams == null && namedParams == null) + return JSONRPC2ParamsType.NO_PARAMS; + + if (positionalParams != null) + return JSONRPC2ParamsType.ARRAY; + + if (namedParams != null) + return JSONRPC2ParamsType.OBJECT; + + else + return JSONRPC2ParamsType.NO_PARAMS; + } + + /** + * Gets the notification parameters. + * + *

This method was deprecated in version 1.30. Use + * {@link #getPositionalParams} or {@link #getNamedParams} instead. + * + * @return The parameters as {@code List<Object>} for positional + * (JSON array), {@code Map<String,Object>} for named + * (JSON object), or {@code null} if none. + */ + @Deprecated + public Object getParams() { + + switch (getParamsType()) { + + case ARRAY: + return positionalParams; + + case OBJECT: + return namedParams; + + default: + return null; + } + } + + + /** + * Gets the positional (JSON array) parameters. + * + * @since 1.30 + * + * @return The positional (JSON array) parameters, {@code null} if none + * or named. + */ + public List getPositionalParams() { + + return positionalParams; + } + + + /** + * Gets the named parameters. + * + * @since 1.30 + * + * @return The named (JSON object) parameters, {@code null} if none or + * positional. + */ + public Map getNamedParams() { + + return namedParams; + } + + + /** + * Sets the notification parameters. + * + *

This method was deprecated in version 1.30. Use + * {@link #setPositionalParams} or {@link #setNamedParams} instead. + * + * @param params The parameters. For positional (JSON array) pass a + * {@code List<Object>}. For named (JSON object) + * pass a {@code Map<String,Object>}. If there are + * no parameters pass {@code null}. + */ + @Deprecated + @SuppressWarnings("unchecked") + public void setParams(final Object params) { + + if (params == null) { + positionalParams = null; + namedParams = null; + } else if (params instanceof List) { + positionalParams = (List) params; + } else if (params instanceof Map) { + namedParams = (Map) params; + } else { + throw new IllegalArgumentException("The notification parameters must be of type List, Map or null"); + } + } + + + /** + * Sets the positional (JSON array) request parameters. + * + * @since 1.30 + * + * @param positionalParams The positional (JSON array) request + * parameters, {@code null} if none. + */ + public void setPositionalParams(final List positionalParams) { + + if (positionalParams == null) + return; + + this.positionalParams = positionalParams; + } + + + /** + * Sets the named (JSON object) request parameters. + * + * @since 1.30 + * + * @param namedParams The named (JSON object) request parameters, + * {@code null} if none. + */ + public void setNamedParams(final Map namedParams) { + + if (namedParams == null) + return; + + this.namedParams = namedParams; + } + + + @Override + public JSONObject toJSONObject() { + + JSONObject notf = new JSONObject(); + + notf.put("method", method); + + // The params can be omitted if none + switch (getParamsType()) { + + case ARRAY: + notf.put("params", positionalParams); + break; + + case OBJECT: + notf.put("params", namedParams); + break; + } + + notf.put("jsonrpc", "2.0"); + + + Map nonStdAttributes = getNonStdAttributes(); + + if (nonStdAttributes != null) { + + for (final Map.Entry attr: nonStdAttributes.entrySet()) + notf.put(attr.getKey(), attr.getValue()); + } + + return notf; + } +} diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2ParamsType.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2ParamsType.java new file mode 100644 index 0000000000..d949451f28 --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2ParamsType.java @@ -0,0 +1,37 @@ +package com.thetransactioncompany.jsonrpc2; + + +/** + * Enumeration of the three parameter types in JSON-RPC 2.0 requests and + * notifications. + * + *
    + *
  • {@link #NO_PARAMS} The method takes no parameters. + *
  • {@link #ARRAY} The method takes positional parameters, packed as a + * JSON array, e.g. {@code ["val1", "val2", ...]}. + *
  • {@link #OBJECT} The method takes named parameters, packed as a JSON + * object, e.g. {@code {"param1":"val1", "param2":"val2", ...}}. + *
+ * + * @author Vladimir Dzhuvinov + */ +public enum JSONRPC2ParamsType { + + + /** + * No parameters. + */ + NO_PARAMS, + + + /** + * Positional parameters, packed as a JSON array. + */ + ARRAY, + + + /** + * Named parameters, packed as a JSON object. + */ + OBJECT +} diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2ParseException.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2ParseException.java new file mode 100644 index 0000000000..1cbb03f23f --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2ParseException.java @@ -0,0 +1,113 @@ +package com.thetransactioncompany.jsonrpc2; + + +/** + * Thrown to indicate an exception during the parsing of a JSON-RPC 2.0 + * message string. + * + * @author Vladimir Dzhuvinov + */ +public class JSONRPC2ParseException extends Exception { + + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 3376608778436136410L; + + + /** + * Indicates a parse exception caused by a JSON message not conforming + * to the JSON-RPC 2.0 protocol. + */ + public static final int PROTOCOL = 0; + + + /** + * Indicates a parse exception caused by invalid JSON. + */ + public static final int JSON = 1; + + + /** + * The parse exception cause type. Default is {@link #PROTOCOL}. + */ + private int causeType = PROTOCOL; + + + + /** + * The string that could't be parsed. + */ + private String unparsableString = null; + + + /** + * Creates a new parse exception with the specified message. The cause + * type is set to {@link #PROTOCOL}. + * + * @param message The exception message. + */ + public JSONRPC2ParseException(final String message) { + + super(message); + } + + + /** + * Creates a new parse exception with the specified message and the + * original string that didn't parse. The cause type is set to + * {@link #PROTOCOL}. + * + * @param message The exception message. + * @param unparsableString The unparsable string. + */ + public JSONRPC2ParseException(final String message, final String unparsableString) { + + super(message); + this.unparsableString = unparsableString; + } + + + /** + * Creates a new parse exception with the specified message, cause type + * and the original string that didn't parse. + * + * @param message The exception message. + * @param causeType The exception cause type, either + * {@link #PROTOCOL} or {@link #JSON}. + * @param unparsableString The unparsable string. + */ + public JSONRPC2ParseException(final String message, final int causeType, final String unparsableString) { + + super(message); + + if (causeType != PROTOCOL && causeType != JSON) + throw new IllegalArgumentException("Cause type must be either PROTOCOL or JSON"); + + this.causeType = causeType; + this.unparsableString = unparsableString; + } + + + /** + * Gets the parse exception cause type. + * + * @return The cause type, either {@link #PROTOCOL} or {@link #JSON}. + */ + public int getCauseType() { + + return causeType; + } + + + /** + * Gets original string that caused the parse exception (if specified). + * + * @return The string that didn't parse, {@code null} if none. + */ + public String getUnparsableString() { + + return unparsableString; + } +} diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Parser.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Parser.java new file mode 100644 index 0000000000..06db0bc28f --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Parser.java @@ -0,0 +1,654 @@ +package com.thetransactioncompany.jsonrpc2; + + +import java.util.List; +import java.util.Map; + +import org.json.simple.parser.ContainerFactory; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + + +/** + * Parses JSON-RPC 2.0 request, notification and response messages. + * + *

Parsing of batched requests / notifications is not supported. + * + *

This class is not thread-safe. A parser instance should not be used by + * more than one thread unless properly synchronised. Alternatively, you may + * use the thread-safe {@link JSONRPC2Message#parse} and its sister methods. + * + *

Example: + * + *

+ * String jsonString = "{\"method\":\"makePayment\"," +
+ *                      "\"params\":{\"recipient\":\"Penny Adams\",\"amount\":175.05}," +
+ *                      "\"id\":\"0001\","+
+ *                      "\"jsonrpc\":\"2.0\"}";
+ *  
+ *  JSONRPC2Request req = null;
+ *
+ * JSONRPC2Parser parser = new JSONRPC2Parser();
+ *  
+ *  try {
+ *          req = parser.parseJSONRPC2Request(jsonString);
+ * 
+ *  } catch (JSONRPC2ParseException e) {
+ *          // handle exception
+ *  }
+ *
+ * 
+ * + *

The mapping between JSON and Java entities (as defined by the + * underlying JSON Smart library): + * + *

+ *     true|false  <--->  java.lang.Boolean
+ *     number      <--->  java.lang.Number
+ *     string      <--->  java.lang.String
+ *     array       <--->  java.util.List
+ *     object      <--->  java.util.Map
+ *     null        <--->  null
+ * 
+ * + * @author Vladimir Dzhuvinov + */ +public class JSONRPC2Parser { + + + /** + * Reusable JSON parser. Not thread-safe! + */ + private final JSONParser parser; + + + /** + * If {@code true} the order of the parsed JSON object members must be + * preserved. + */ + private boolean preserveOrder; + + + /** + * If {@code true} the {@code "jsonrpc":"2.0"} version attribute in the + * JSON-RPC 2.0 message must be ignored during parsing. + */ + private boolean ignoreVersion; + + + /** + * If {@code true} non-standard JSON-RPC 2.0 message attributes must be + * parsed too. + */ + private boolean parseNonStdAttributes; + + + /** + * Creates a new JSON-RPC 2.0 message parser. + * + *

The member order of parsed JSON objects in parameters and results + * will not be preserved; strict checking of the 2.0 JSON-RPC version + * attribute will be enforced; non-standard message attributes will be + * ignored. Check the other constructors if you want to specify + * different behaviour. + */ + public JSONRPC2Parser() { + + this(false, false, false); + } + + + /** + * Creates a new JSON-RPC 2.0 message parser. + * + *

Strict checking of the 2.0 JSON-RPC version attribute will be + * enforced; non-standard message attributes will be ignored. Check the + * other constructors if you want to specify different behaviour. + * + * @param preserveOrder If {@code true} the member order of JSON objects + * in parameters and results will be preserved. + */ + public JSONRPC2Parser(final boolean preserveOrder) { + + this(preserveOrder, false, false); + } + + + /** + * Creates a new JSON-RPC 2.0 message parser. + * + *

Non-standard message attributes will be ignored. Check the other + * constructors if you want to specify different behaviour. + * + * @param preserveOrder If {@code true} the member order of JSON objects + * in parameters and results will be preserved. + * @param ignoreVersion If {@code true} the {@code "jsonrpc":"2.0"} + * version attribute in the JSON-RPC 2.0 message + * will not be checked. + */ + public JSONRPC2Parser(final boolean preserveOrder, + final boolean ignoreVersion) { + + this(preserveOrder, ignoreVersion, false); + } + + + /** + * Creates a new JSON-RPC 2.0 message parser. + * + *

This constructor allows full specification of the available + * JSON-RPC message parsing properties. + * + * @param preserveOrder If {@code true} the member order of JSON + * objects in parameters and results will + * be preserved. + * @param ignoreVersion If {@code true} the + * {@code "jsonrpc":"2.0"} version + * attribute in the JSON-RPC 2.0 message + * will not be checked. + * @param parseNonStdAttributes If {@code true} non-standard attributes + * found in the JSON-RPC 2.0 messages will + * be parsed too. + */ + public JSONRPC2Parser(final boolean preserveOrder, + final boolean ignoreVersion, + final boolean parseNonStdAttributes) { + + // Numbers parsed as long/double, requires JSON Smart 1.0.9+ + parser = new JSONParser(); + + this.preserveOrder = preserveOrder; + this.ignoreVersion = ignoreVersion; + this.parseNonStdAttributes = parseNonStdAttributes; + } + + + /** + * Parses a JSON object string. Provides the initial parsing of + * JSON-RPC 2.0 messages. The member order of JSON objects will be + * preserved if {@link #preserveOrder} is set to {@code true}. + * + * @param jsonString The JSON string to parse. Must not be + * {@code null}. + * + * @return The parsed JSON object. + * + * @throws JSONRPC2ParseException With detailed message if parsing + * failed. + */ + @SuppressWarnings("unchecked") + private Map parseJSONObject(final String jsonString) + throws JSONRPC2ParseException { + + if (jsonString.trim().length()==0) + throw new JSONRPC2ParseException("Invalid JSON: Empty string", + JSONRPC2ParseException.JSON, + jsonString); + + Object json; + + // Parse the JSON string + try { + //if (preserveOrder) + // json = parser.parse(jsonString, ContainerFactory.FACTORY_ORDERED); + + //else + json = parser.parse(jsonString); + + } catch (ParseException e) { + + // Terse message, do not include full parse exception message + throw new JSONRPC2ParseException("Invalid JSON", + JSONRPC2ParseException.JSON, + jsonString); + } + + if (json instanceof List) + throw new JSONRPC2ParseException("JSON-RPC 2.0 batch requests/notifications not supported", jsonString); + + if (! (json instanceof Map)) + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 message: Message must be a JSON object", jsonString); + + return (Map)json; + } + + + /** + * Ensures the specified parameter is a {@code String} object set to + * "2.0". This method is intended to check the "jsonrpc" attribute + * during parsing of JSON-RPC messages. + * + * @param version The version parameter. Must not be {@code null}. + * @param jsonString The original JSON string. + * + * @throws JSONRPC2ParseException If the parameter is not a string that + * equals "2.0". + */ + private static void ensureVersion2(final Object version, final String jsonString) + throws JSONRPC2ParseException { + + if (version == null) + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0: Version string missing", jsonString); + + else if (! (version instanceof String)) + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0: Version not a JSON string", jsonString); + + else if (! version.equals("2.0")) + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0: Version must be \"2.0\"", jsonString); + } + + + /** + * Provides common parsing of JSON-RPC 2.0 requests, notifications + * and responses. Use this method if you don't know which type of + * JSON-RPC message the input string represents. + * + *

If a particular message type is expected use the dedicated + * {@link #parseJSONRPC2Request}, {@link #parseJSONRPC2Notification} + * and {@link #parseJSONRPC2Response} methods. They are more efficient + * and would provide you with more detailed parse error reporting. + * + * @param jsonString A JSON string representing a JSON-RPC 2.0 request, + * notification or response, UTF-8 encoded. Must not + * be {@code null}. + * + * @return An instance of {@link JSONRPC2Request}, + * {@link JSONRPC2Notification} or {@link JSONRPC2Response}. + * + * @throws JSONRPC2ParseException With detailed message if the parsing + * failed. + */ + public JSONRPC2Message parseJSONRPC2Message(final String jsonString) + throws JSONRPC2ParseException { + + // Try each of the parsers until one succeeds (or all fail) + + try { + return parseJSONRPC2Request(jsonString); + + } catch (JSONRPC2ParseException e) { + + // throw on JSON error, ignore on protocol error + if (e.getCauseType() == JSONRPC2ParseException.JSON) + throw e; + } + + try { + return parseJSONRPC2Notification(jsonString); + + } catch (JSONRPC2ParseException e) { + + // throw on JSON error, ignore on protocol error + if (e.getCauseType() == JSONRPC2ParseException.JSON) + throw e; + } + + try { + return parseJSONRPC2Response(jsonString); + + } catch (JSONRPC2ParseException e) { + + // throw on JSON error, ignore on protocol error + if (e.getCauseType() == JSONRPC2ParseException.JSON) + throw e; + } + + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 message", + JSONRPC2ParseException.PROTOCOL, + jsonString); + } + + + /** + * Parses a JSON-RPC 2.0 request string. + * + * @param jsonString The JSON-RPC 2.0 request string, UTF-8 encoded. + * Must not be {@code null}. + * + * @return The corresponding JSON-RPC 2.0 request object. + * + * @throws JSONRPC2ParseException With detailed message if parsing + * failed. + */ + @SuppressWarnings("unchecked") + public JSONRPC2Request parseJSONRPC2Request(final String jsonString) + throws JSONRPC2ParseException { + + // Initial JSON object parsing + Map jsonObject = parseJSONObject(jsonString); + + + // Check for JSON-RPC version "2.0" + Object version = jsonObject.remove("jsonrpc"); + + if (! ignoreVersion) + ensureVersion2(version, jsonString); + + + // Extract method name + Object method = jsonObject.remove("method"); + + if (method == null) + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Method name missing", jsonString); + + else if (! (method instanceof String)) + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Method name not a JSON string", jsonString); + + else if (((String)method).length() == 0) + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Method name is an empty string", jsonString); + + + // Extract ID + if (! jsonObject.containsKey("id")) + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Missing identifier", jsonString); + + Object id = jsonObject.remove("id"); + + if ( id != null && + !(id instanceof Number ) && + !(id instanceof Boolean) && + !(id instanceof String ) ) + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Identifier not a JSON scalar", jsonString); + + + // Extract params + Object params = jsonObject.remove("params"); + + + JSONRPC2Request request; + + if (params == null) + request = new JSONRPC2Request((String)method, id); + + else if (params instanceof List) + request = new JSONRPC2Request((String)method, (List)params, id); + + else if (params instanceof Map) + request = new JSONRPC2Request((String)method, (Map)params, id); + + else + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Method parameters have unexpected JSON type", jsonString); + + + // Extract remaining non-std params? + if (parseNonStdAttributes) { + + for (Map.Entry entry: jsonObject.entrySet()) { + + request.appendNonStdAttribute(entry.getKey(), entry.getValue()); + } + } + + return request; + } + + + /** + * Parses a JSON-RPC 2.0 notification string. + * + * @param jsonString The JSON-RPC 2.0 notification string, UTF-8 + * encoded. Must not be {@code null}. + * + * @return The corresponding JSON-RPC 2.0 notification object. + * + * @throws JSONRPC2ParseException With detailed message if parsing + * failed. + */ + @SuppressWarnings("unchecked") + public JSONRPC2Notification parseJSONRPC2Notification(final String jsonString) + throws JSONRPC2ParseException { + + // Initial JSON object parsing + Map jsonObject = parseJSONObject(jsonString); + + + // Check for JSON-RPC version "2.0" + Object version = jsonObject.remove("jsonrpc"); + + if (! ignoreVersion) + ensureVersion2(version, jsonString); + + + // Extract method name + Object method = jsonObject.remove("method"); + + if (method == null) + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 notification: Method name missing", jsonString); + + else if (! (method instanceof String)) + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 notification: Method name not a JSON string", jsonString); + + else if (((String)method).length() == 0) + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 notification: Method name is an empty string", jsonString); + + + // Extract params + Object params = jsonObject.get("params"); + + JSONRPC2Notification notification; + + if (params == null) + notification = new JSONRPC2Notification((String)method); + + else if (params instanceof List) + notification = new JSONRPC2Notification((String)method, (List)params); + + else if (params instanceof Map) + notification = new JSONRPC2Notification((String)method, (Map)params); + else + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 notification: Method parameters have unexpected JSON type", jsonString); + + // Extract remaining non-std params? + if (parseNonStdAttributes) { + + for (Map.Entry entry: jsonObject.entrySet()) { + + notification.appendNonStdAttribute(entry.getKey(), entry.getValue()); + } + } + + return notification; + } + + + /** + * Parses a JSON-RPC 2.0 response string. + * + * @param jsonString The JSON-RPC 2.0 response string, UTF-8 encoded. + * Must not be {@code null}. + * + * @return The corresponding JSON-RPC 2.0 response object. + * + * @throws JSONRPC2ParseException With detailed message if parsing + * failed. + */ + @SuppressWarnings("unchecked") + public JSONRPC2Response parseJSONRPC2Response(final String jsonString) + throws JSONRPC2ParseException { + + // Initial JSON object parsing + Map jsonObject = parseJSONObject(jsonString); + + // Check for JSON-RPC version "2.0" + Object version = jsonObject.remove("jsonrpc"); + + if (! ignoreVersion) + ensureVersion2(version, jsonString); + + + // Extract request ID + Object id = jsonObject.remove("id"); + + if ( id != null && + ! (id instanceof Boolean) && + ! (id instanceof Number ) && + ! (id instanceof String ) ) + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Identifier not a JSON scalar", jsonString); + + + // Extract result/error and create response object + // Note: result and error are mutually exclusive + + JSONRPC2Response response; + + if (jsonObject.containsKey("result") && ! jsonObject.containsKey("error")) { + + // Success + Object res = jsonObject.remove("result"); + + response = new JSONRPC2Response(res, id); + + } + else if (! jsonObject.containsKey("result") && jsonObject.containsKey("error")) { + + // Error JSON object + Object errorJSON = jsonObject.remove("error"); + + if (errorJSON == null) + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Missing error object", jsonString); + + + if (! (errorJSON instanceof Map)) + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Error object not a JSON object"); + + + Map error = (Map)errorJSON; + + + int errorCode; + + try { + errorCode = ((Number)error.get("code")).intValue(); + + } catch (Exception e) { + + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Error code missing or not an integer", jsonString); + } + + String errorMessage; + + try { + errorMessage = (String)error.get("message"); + + } catch (Exception e) { + + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Error message missing or not a string", jsonString); + } + + Object errorData = error.get("data"); + + response = new JSONRPC2Response(new JSONRPC2Error(errorCode, errorMessage, errorData), id); + + } + else if (jsonObject.containsKey("result") && jsonObject.containsKey("error")) { + + // Invalid response + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: You cannot have result and error at the same time", jsonString); + } + else if (! jsonObject.containsKey("result") && ! jsonObject.containsKey("error")){ + + // Invalid response + throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Neither result nor error specified", jsonString); + } + else { + throw new AssertionError(); + } + + + // Extract remaining non-std params? + if (parseNonStdAttributes) { + + for (Map.Entry entry: jsonObject.entrySet()) { + + response.appendNonStdAttribute(entry.getKey(), entry.getValue()); + } + } + + return response; + } + + + /** + * Controls the preservation of JSON object member order in parsed + * JSON-RPC 2.0 messages. + * + * @param preserveOrder {@code true} to preserve the order of JSON + * object members, else {@code false}. + */ + public void preserveOrder(final boolean preserveOrder) { + + this.preserveOrder = preserveOrder; + } + + + /** + * Returns {@code true} if the order of JSON object members in parsed + * JSON-RPC 2.0 messages is preserved, else {@code false}. + * + * @return {@code true} if order is preserved, else {@code false}. + */ + public boolean preservesOrder() { + + return preserveOrder; + } + + + /** + * Specifies whether to ignore the {@code "jsonrpc":"2.0"} version + * attribute during parsing of JSON-RPC 2.0 messages. + * + *

You may with to disable strict 2.0 version checking if the parsed + * JSON-RPC 2.0 messages don't include a version attribute or if you + * wish to achieve limited compatibility with older JSON-RPC protocol + * versions. + * + * @param ignore {@code true} to skip checks of the + * {@code "jsonrpc":"2.0"} version attribute in parsed + * JSON-RPC 2.0 messages, else {@code false}. + */ + public void ignoreVersion(final boolean ignore) { + + ignoreVersion = ignore; + } + + + /** + * Returns {@code true} if the {@code "jsonrpc":"2.0"} version + * attribute in parsed JSON-RPC 2.0 messages is ignored, else + * {@code false}. + * + * @return {@code true} if the {@code "jsonrpc":"2.0"} version + * attribute in parsed JSON-RPC 2.0 messages is ignored, else + * {@code false}. + */ + public boolean ignoresVersion() { + + return ignoreVersion; + } + + + /** + * Specifies whether to parse non-standard attributes found in JSON-RPC + * 2.0 messages. + * + * @param enable {@code true} to parse non-standard attributes, else + * {@code false}. + */ + public void parseNonStdAttributes(final boolean enable) { + + parseNonStdAttributes = enable; + } + + + /** + * Returns {@code true} if non-standard attributes in JSON-RPC 2.0 + * messages are parsed. + * + * @return {@code true} if non-standard attributes are parsed, else + * {@code false}. + */ + public boolean parsesNonStdAttributes() { + + return parseNonStdAttributes; + } +} diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Request.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Request.java new file mode 100644 index 0000000000..ddcb491402 --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Request.java @@ -0,0 +1,507 @@ +package com.thetransactioncompany.jsonrpc2; + + +import java.util.List; +import java.util.Map; + +import org.json.simple.JSONObject; + + +/** + * Represents a JSON-RPC 2.0 request. + * + *

A request carries four pieces of data: + *

    + *
  • {@code method} The name of the remote method to call. + *
  • {@code params} The required method parameters (if any), which can + * be packed into a JSON array or object. + *
  • {@code id} An identifier which is echoed back to the client with + * the response. + *
  • {@code jsonrpc} A string indicating the JSON-RPC protocol version + * set to "2.0". + *
+ * + *

Here is a sample JSON-RPC 2.0 request string: + * + *

+ * {  
+ *    "method"  : "makePayment",
+ *    "params"  : { "recipient" : "Penny Adams", "amount":175.05 },
+ *    "id"      : "0001",
+ *    "jsonrpc" : "2.0"
+ * }
+ * 
+ * + *

This class provides two methods to obtain a request object: + *

    + *
  • Pass a JSON-RPC 2.0 request string to the static + * {@link #parse} method, or + *
  • Invoke one of the constructors with the appropriate arguments. + *
+ * + *

Example 1: Parsing a request string: + * + *

+ * String jsonString = "{\"method\":\"makePayment\"," +
+ *                     "\"params\":{\"recipient\":\"Penny Adams\",\"amount\":175.05}," +
+ *                     "\"id\":\"0001\","+
+ *                     "\"jsonrpc\":\"2.0\"}";
+ * 
+ * JSONRPC2Request req = null;
+ * 
+ * try {
+ *         req = JSONRPC2Request.parse(jsonString);
+ *
+ * } catch (JSONRPC2ParseException e) {
+ *         // handle exception
+ * }
+ * 
+ * + *

Example 2: Recreating the above request: + * + *

+ * String method = "makePayment";
+ * Map<String,Object> params = new HashMap<String,Object>();
+ * params.put("recipient", "Penny Adams");
+ * params.put("amount", 175.05);
+ * String id = "0001";
+ *
+ * JSONRPC2Request req = new JSONRPC2Request(method, params, id);
+ *
+ * System.out.println(req);
+ * 
+ * + *

The mapping between JSON and Java entities (as defined by the + * underlying JSON Smart library): + * + *

+ *     true|false  <--->  java.lang.Boolean
+ *     number      <--->  java.lang.Number
+ *     string      <--->  java.lang.String
+ *     array       <--->  java.util.List
+ *     object      <--->  java.util.Map
+ *     null        <--->  null
+ * 
+ * + * @author Vladimir Dzhuvinov + */ +public class JSONRPC2Request extends JSONRPC2Message { + + + /** + * The method name. + */ + private String method; + + + /** + * The positional parameters, {@code null} if none. + */ + private List positionalParams; + + + /** + * The named parameters, {@code null} if none. + */ + private Map namedParams; + + + /** + * The request identifier. + */ + private Object id; + + + /** + * Parses a JSON-RPC 2.0 request string. This method is thread-safe. + * + * @param jsonString The JSON-RPC 2.0 request string, UTF-8 encoded. + * Must not be {@code null}. + * + * @return The corresponding JSON-RPC 2.0 request object. + * + * @throws JSONRPC2ParseException With detailed message if parsing + * failed. + */ + public static JSONRPC2Request parse(final String jsonString) + throws JSONRPC2ParseException { + + return parse(jsonString, false, false, false); + } + + + /** + * Parses a JSON-RPC 2.0 request string. This method is thread-safe. + * + * @param jsonString The JSON-RPC 2.0 request string, UTF-8 encoded. + * Must not be {@code null}. + * @param preserveOrder {@code true} to preserve the order of JSON + * object members in parameters. + * + * @return The corresponding JSON-RPC 2.0 request object. + * + * @throws JSONRPC2ParseException With detailed message if parsing + * failed. + */ + public static JSONRPC2Request parse(final String jsonString, + final boolean preserveOrder) + throws JSONRPC2ParseException { + + return parse(jsonString, preserveOrder, false, false); + } + + + /** + * Parses a JSON-RPC 2.0 request string. This method is thread-safe. + * + * @param jsonString The JSON-RPC 2.0 request string, UTF-8 encoded. + * Must not be {@code null}. + * @param preserveOrder {@code true} to preserve the order of JSON + * object members in parameters. + * @param ignoreVersion {@code true} to skip a check of the + * {@code "jsonrpc":"2.0"} version attribute in the + * JSON-RPC 2.0 message. + * + * @return The corresponding JSON-RPC 2.0 request object. + * + * @throws JSONRPC2ParseException With detailed message if parsing + * failed. + */ + public static JSONRPC2Request parse(final String jsonString, + final boolean preserveOrder, + final boolean ignoreVersion) + throws JSONRPC2ParseException { + + return parse(jsonString, preserveOrder, ignoreVersion, false); + } + + + /** + * Parses a JSON-RPC 2.0 request string. This method is thread-safe. + * + * @param jsonString The JSON-RPC 2.0 request string, UTF-8 + * encoded. Must not be {@code null}. + * @param preserveOrder {@code true} to preserve the order of + * JSON object members in parameters. + * @param ignoreVersion {@code true} to skip a check of the + * {@code "jsonrpc":"2.0"} version + * attribute in the JSON-RPC 2.0 message. + * @param parseNonStdAttributes {@code true} to parse non-standard + * attributes found in the JSON-RPC 2.0 + * message. + * + * @return The corresponding JSON-RPC 2.0 request object. + * + * @throws JSONRPC2ParseException With detailed message if parsing + * failed. + */ + public static JSONRPC2Request parse(final String jsonString, + final boolean preserveOrder, + final boolean ignoreVersion, + final boolean parseNonStdAttributes) + throws JSONRPC2ParseException { + + JSONRPC2Parser parser = new JSONRPC2Parser(preserveOrder, + ignoreVersion, + parseNonStdAttributes); + + return parser.parseJSONRPC2Request(jsonString); + } + + + /** + * Constructs a new JSON-RPC 2.0 request with no parameters. + * + * @param method The name of the requested method. Must not be + * {@code null}. + * @param id The request identifier echoed back to the caller. + * The value must map to a JSON + * scalar ({@code null} and fractions, however, should + * be avoided). + */ + public JSONRPC2Request(final String method, final Object id) { + + setMethod(method); + setID(id); + } + + + /** + * Constructs a new JSON-RPC 2.0 request with positional (JSON array) + * parameters. + * + * @param method The name of the requested method. Must not + * be {@code null}. + * @param positionalParams The positional (JSON array) parameters, + * {@code null} if none. + * @param id The request identifier echoed back to the + * caller. The value must map + * to a JSON scalar ({@code null} and + * fractions, however, should be avoided). + */ + public JSONRPC2Request(final String method, + final List positionalParams, + final Object id) { + + setMethod(method); + setPositionalParams(positionalParams); + setID(id); + } + + + /** + * Constructs a new JSON-RPC 2.0 request with named (JSON object) + * parameters. + * + * @param method The name of the requested method. + * @param namedParams The named (JSON object) parameters, {@code null} + * if none. + * @param id The request identifier echoed back to the caller. + * The value must map to a JSON + * scalar ({@code null} and fractions, however, + * should be avoided). + */ + public JSONRPC2Request(final String method, + final Map namedParams, + final Object id) { + + setMethod(method); + setNamedParams(namedParams); + setID(id); + } + + + /** + * Gets the name of the requested method. + * + * @return The method name. + */ + public String getMethod() { + + return method; + } + + + /** + * Sets the name of the requested method. + * + * @param method The method name. Must not be {@code null}. + */ + public void setMethod(final String method) { + + // The method name is mandatory + if (method == null) + throw new IllegalArgumentException("The method name must not be null"); + + this.method = method; + } + + + /** + * Gets the parameters type ({@link JSONRPC2ParamsType#ARRAY positional}, + * {@link JSONRPC2ParamsType#OBJECT named} or + * {@link JSONRPC2ParamsType#NO_PARAMS none}). + * + * @return The parameters type. + */ + public JSONRPC2ParamsType getParamsType() { + + if (positionalParams == null && namedParams == null) + return JSONRPC2ParamsType.NO_PARAMS; + + if (positionalParams != null) + return JSONRPC2ParamsType.ARRAY; + + if (namedParams != null) + return JSONRPC2ParamsType.OBJECT; + + else + return JSONRPC2ParamsType.NO_PARAMS; + } + + + /** + * Gets the request parameters. + * + *

This method was deprecated in version 1.30. Use + * {@link #getPositionalParams} or {@link #getNamedParams} instead. + * + * @return The parameters as {@code List<Object>} for positional + * (JSON array), {@code Map<String,Object>} for named + * (JSON object), or {@code null} if none. + */ + @Deprecated + public Object getParams() { + + switch (getParamsType()) { + + case ARRAY: + return positionalParams; + + case OBJECT: + return namedParams; + + default: + return null; + } + } + + + /** + * Gets the positional (JSON array) parameters. + * + * @since 1.30 + * + * @return The positional (JSON array) parameters, {@code null} if none + * or named. + */ + public List getPositionalParams() { + + return positionalParams; + } + + + /** + * Gets the named parameters. + * + * @since 1.30 + * + * @return The named (JSON object) parameters, {@code null} if none or + * positional. + */ + public Map getNamedParams() { + + return namedParams; + } + + + /** + * Sets the request parameters. + * + *

This method was deprecated in version 1.30. Use + * {@link #setPositionalParams} or {@link #setNamedParams} instead. + * + * @param params The parameters. For positional (JSON array) pass a + * {@code List<Object>}. For named (JSON object) + * pass a {@code Map<String,Object>}. If there are + * no parameters pass {@code null}. + */ + @Deprecated + @SuppressWarnings("unchecked") + public void setParams(final Object params) { + + if (params == null) { + positionalParams = null; + namedParams = null; + } else if (params instanceof List) { + positionalParams = (List) params; + } else if (params instanceof Map) { + namedParams = (Map) params; + } else { + throw new IllegalArgumentException("The request parameters must be of type List, Map or null"); + } + } + + + /** + * Sets the positional (JSON array) request parameters. + * + * @since 1.30 + * + * @param positionalParams The positional (JSON array) request + * parameters, {@code null} if none. + */ + public void setPositionalParams(final List positionalParams) { + + if (positionalParams == null) + return; + + this.positionalParams = positionalParams; + } + + + /** + * Sets the named (JSON object) request parameters. + * + * @since 1.30 + * + * @param namedParams The named (JSON object) request parameters, + * {@code null} if none. + */ + public void setNamedParams(final Map namedParams) { + + if (namedParams == null) + return; + + this.namedParams = namedParams; + } + + + /** + * Gets the request identifier. + * + * @return The request identifier ({@code Number}, {@code Boolean}, + * {@code String}) or {@code null}. + */ + public Object getID() { + + return id; + } + + + /** + * Sets the request identifier (ID). + * + * @param id The request identifier echoed back to the caller. + * The value must map to a JSON + * scalar ({@code null} and fractions, however, should + * be avoided). + */ + public void setID(final Object id) { + + if (id == null || + id instanceof Boolean || + id instanceof Number || + id instanceof String + ) { + this.id = id; + } else { + this.id = id.toString(); + } + } + + + @Override + public JSONObject toJSONObject() { + + JSONObject req = new JSONObject(); + + req.put("method", method); + + // The params can be omitted if none + switch (getParamsType()) { + + case ARRAY: + req.put("params", positionalParams); + break; + + case OBJECT: + req.put("params", namedParams); + break; + } + + req.put("id", id); + + req.put("jsonrpc", "2.0"); + + Map nonStdAttributes = getNonStdAttributes(); + + if (nonStdAttributes != null) { + + for (final Map.Entry attr: nonStdAttributes.entrySet()) + req.put(attr.getKey(), attr.getValue()); + } + + return req; + } +} diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Response.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Response.java new file mode 100644 index 0000000000..6ce2a31fa2 --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Response.java @@ -0,0 +1,414 @@ +package com.thetransactioncompany.jsonrpc2; + + +import java.util.Map; + +import org.json.simple.JSONObject; + + +/** + * Represents a JSON-RPC 2.0 response. + * + *

A response is returned to the caller after a JSON-RPC 2.0 request has + * been processed (notifications, however, don't produce a response). The + * response can take two different forms depending on the outcome: + * + *

    + *
  • The request was successful. The corresponding response returns + * a JSON object with the following information: + *
      + *
    • {@code result} The result, which can be of any JSON type + * - a number, a boolean value, a string, an array, an object + * or null. + *
    • {@code id} The request identifier which is echoed back back + * to the caller. + *
    • {@code jsonrpc} A string indicating the JSON-RPC protocol + * version set to "2.0". + *
    + *
  • The request failed. The returned JSON object contains: + *
      + *
    • {@code error} An object with: + *
        + *
      • {@code code} An integer indicating the error type. + *
      • {@code message} A brief error messsage. + *
      • {@code data} Optional error data. + *
      + *
    • {@code id} The request identifier. If it couldn't be + * determined, e.g. due to a request parse error, the ID is + * set to {@code null}. + *
    • {@code jsonrpc} A string indicating the JSON-RPC protocol + * version set to "2.0". + *
    + *
+ * + *

Here is an example JSON-RPC 2.0 response string where the request + * has succeeded: + * + *

+ * {  
+ *    "result"  : true,
+ *    "id"      : "req-002",
+ *    "jsonrpc" : "2.0"  
+ * }
+ * 
+ * + * + *

And here is an example JSON-RPC 2.0 response string indicating a failure: + * + *

+ * {  
+ *    "error"   : { "code" : -32601, "message" : "Method not found" },
+ *    "id"      : "req-003",
+ *    "jsonrpc" : "2.0"
+ * }
+ * 
+ * + *

A response object is obtained either by passing a valid JSON-RPC 2.0 + * response string to the static {@link #parse} method or by invoking the + * appropriate constructor. + * + *

Here is how parsing is done: + * + *

+ * String jsonString = "{\"result\":true,\"id\":\"req-002\",\"jsonrpc\":\"2.0\"}";
+ * 
+ * JSONRPC2Response response = null;
+ * 
+ * try {
+ *         response = JSONRPC2Response.parse(jsonString);
+ *
+ * } catch (JSONRPC2Exception e) {
+ *         // handle exception
+ * }
+ * 
+ * + *

And here is how you can replicate the above example response strings: + * + *

+ * // success example
+ * JSONRPC2Response resp = new JSONRPC2Response(true, "req-002");
+ * System.out.println(resp);
+ * 
+ * // failure example
+ * JSONRPC2Error err = new JSONRPC2Error(-32601, "Method not found");
+ * resp = new JSONRPC2Response(err, "req-003");
+ * System.out.println(resp);
+ * 
+ * 
+ * + *

The mapping between JSON and Java entities (as defined by the + * underlying JSON Smart library): + * + *

+ *     true|false  <--->  java.lang.Boolean
+ *     number      <--->  java.lang.Number
+ *     string      <--->  java.lang.String
+ *     array       <--->  java.util.List
+ *     object      <--->  java.util.Map
+ *     null        <--->  null
+ * 
+ * + * @author Vladimir Dzhuvinov + */ +public class JSONRPC2Response extends JSONRPC2Message { + + + /** + * The result. + */ + private Object result = null; + + + /** + * The error object. + */ + private JSONRPC2Error error = null; + + + /** + * The echoed request identifier. + */ + private Object id = null; + + + /** + * Parses a JSON-RPC 2.0 response string. This method is thread-safe. + * + * @param jsonString The JSON-RPC 2.0 response string, UTF-8 encoded. + * Must not be {@code null}. + * + * @return The corresponding JSON-RPC 2.0 response object. + * + * @throws JSONRPC2ParseException With detailed message if parsing + * failed. + */ + public static JSONRPC2Response parse(final String jsonString) + throws JSONRPC2ParseException { + + return parse(jsonString, false, false, false); + } + + + /** + * Parses a JSON-RPC 2.0 response string. This method is thread-safe. + * + * @param jsonString The JSON-RPC 2.0 response string, UTF-8 encoded. + * Must not be {@code null}. + * @param preserveOrder {@code true} to preserve the order of JSON + * object members in results. + * + * @return The corresponding JSON-RPC 2.0 response object. + * + * @throws JSONRPC2ParseException With detailed message if parsing + * failed. + */ + public static JSONRPC2Response parse(final String jsonString, + final boolean preserveOrder) + throws JSONRPC2ParseException { + + return parse(jsonString, preserveOrder, false, false); + } + + + /** + * Parses a JSON-RPC 2.0 response string. This method is thread-safe. + * + * @param jsonString The JSON-RPC 2.0 response string, UTF-8 encoded. + * Must not be {@code null}. + * @param preserveOrder {@code true} to preserve the order of JSON + * object members in results. + * @param ignoreVersion {@code true} to skip a check of the + * {@code "jsonrpc":"2.0"} version attribute in the + * JSON-RPC 2.0 message. + * + * @return The corresponding JSON-RPC 2.0 response object. + * + * @throws JSONRPC2ParseException With detailed message if the parsing + * failed. + */ + public static JSONRPC2Response parse(final String jsonString, + final boolean preserveOrder, + final boolean ignoreVersion) + throws JSONRPC2ParseException { + + return parse(jsonString, preserveOrder, ignoreVersion, false); + } + + + /** + * Parses a JSON-RPC 2.0 response string. This method is thread-safe. + * + * @param jsonString The JSON-RPC 2.0 response string, UTF-8 + * encoded. Must not be {@code null}. + * @param preserveOrder {@code true} to preserve the order of + * JSON object members in results. + * @param ignoreVersion {@code true} to skip a check of the + * {@code "jsonrpc":"2.0"} version + * attribute in the JSON-RPC 2.0 message. + * @param parseNonStdAttributes {@code true} to parse non-standard + * attributes found in the JSON-RPC 2.0 + * message. + * + * @return The corresponding JSON-RPC 2.0 response object. + * + * @throws JSONRPC2ParseException With detailed message if the parsing + * failed. + */ + public static JSONRPC2Response parse(final String jsonString, + final boolean preserveOrder, + final boolean ignoreVersion, + final boolean parseNonStdAttributes) + throws JSONRPC2ParseException { + + JSONRPC2Parser parser = new JSONRPC2Parser(preserveOrder, ignoreVersion, parseNonStdAttributes); + + return parser.parseJSONRPC2Response(jsonString); + } + + + /** + * Creates a new JSON-RPC 2.0 response to a successful request. + * + * @param result The result. The value can map + * to any JSON type. May be {@code null}. + * @param id The request identifier echoed back to the caller. May + * be {@code null} though not recommended. + */ + public JSONRPC2Response(final Object result, final Object id) { + + setResult(result); + setID(id); + } + + + /** + * Creates a new JSON-RPC 2.0 response to a successful request which + * result is {@code null}. + * + * @param id The request identifier echoed back to the caller. May be + * {@code null} though not recommended. + */ + public JSONRPC2Response(final Object id) { + + setResult(null); + setID(id); + } + + + /** + * Creates a new JSON-RPC 2.0 response to a failed request. + * + * @param error A JSON-RPC 2.0 error instance indicating the + * cause of the failure. Must not be {@code null}. + * @param id The request identifier echoed back to the caller. + * Pass a {@code null} if the request identifier couldn't + * be determined (e.g. due to a parse error). + */ + public JSONRPC2Response(final JSONRPC2Error error, final Object id) { + + setError(error); + setID(id); + } + + + /** + * Indicates a successful JSON-RPC 2.0 request and sets the result. + * Note that if the response was previously indicating failure this + * will turn it into a response indicating success. Any previously set + * error data will be invalidated. + * + * @param result The result. The value can map to + * any JSON type. May be {@code null}. + */ + public void setResult(final Object result) { + + // result and error are mutually exclusive + this.result = result; + this.error = null; + } + + + /** + * Gets the result of the request. The returned value has meaning + * only if the request was successful. Use the + * {@link #getError getError} method to check this. + * + * @return The result. + */ + public Object getResult() { + + return result; + } + + + /** + * Indicates a failed JSON-RPC 2.0 request and sets the error details. + * Note that if the response was previously indicating success this + * will turn it into a response indicating failure. Any previously set + * result data will be invalidated. + * + * @param error A JSON-RPC 2.0 error instance indicating the cause of + * the failure. Must not be {@code null}. + */ + public void setError(final JSONRPC2Error error) { + + if (error == null) + throw new IllegalArgumentException("The error object cannot be null"); + + // result and error are mutually exclusive + this.error = error; + this.result = null; + } + + + /** + * Gets the error object indicating the cause of the request failure. + * If a {@code null} is returned, the request succeeded and there was + * no error. + * + * @return A JSON-RPC 2.0 error object, {@code null} if the response + * indicates success. + */ + public JSONRPC2Error getError() { + + return error; + } + + + /** + * A convinience method to check if the response indicates success or + * failure of the request. Alternatively, you can use the + * {@code #getError} method for this purpose. + * + * @return {@code true} if the request succeeded, {@code false} if + * there was an error. + */ + public boolean indicatesSuccess() { + + return error == null; + } + + + /** + * Sets the request identifier echoed back to the caller. + * + * @param id The value must map to a JSON scalar. + * Pass a {@code null} if the request identifier couldn't + * be determined (e.g. due to a parse error). + */ + public void setID(final Object id) { + + if (id == null || + id instanceof Boolean || + id instanceof Number || + id instanceof String + ) { + this.id = id; + } else { + this.id = id.toString(); + } + } + + + /** + * Gets the request identifier that is echoed back to the caller. + * + * @return The request identifier. If there was an error during the + * the request retrieval (e.g. parse error) and the identifier + * couldn't be determined, the value will be {@code null}. + */ + public Object getID() { + + return id; + } + + + @Override + public JSONObject toJSONObject() { + + JSONObject out = new JSONObject(); + + // Result and error are mutually exclusive + if (error != null) { + out.put("error", error.toJSONObject()); + } + else { + out.put("result", result); + } + + out.put("id", id); + + out.put("jsonrpc", "2.0"); + + + Map nonStdAttributes = getNonStdAttributes(); + + if (nonStdAttributes != null) { + + for (final Map.Entry attr: nonStdAttributes.entrySet()) + out.put(attr.getKey(), attr.getValue()); + } + + return out; + } +} diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/package-info.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/package-info.java new file mode 100644 index 0000000000..700af9f333 --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/package-info.java @@ -0,0 +1,32 @@ +/** + * Classes to represent, parse and serialise JSON-RPC 2.0 requests, + * notifications and responses. + * + *

JSON-RPC is a protocol for + * remote + * procedure calls (RPC) using JSON + * - encoded requests and responses. It can be easily relayed over HTTP + * and is of JavaScript origin, making it ideal for use in dynamic web + * applications in the spirit of Ajax and Web 2.0. + * + *

This package implements version 2.0 of the protocol, with the + * exception of batching / multicall. This feature is deliberately left + * out as it tends to confuse users (judging by posts in the JSON-RPC forum). + * + *

See the JSON-RPC 2.0 + * specification for more information or write to the + * user group if + * you have questions. + * + *

Package dependencies: The classes in this package rely on the + * {@code org.json.simple} and {@code org.json.simple.parser} packages + * (version 1.1.1 and compabile) for JSON encoding and decoding. You can obtain + * them from the JSON-Smart + * website. + * + * @author Vladimir Dzhuvinov + */ +package com.thetransactioncompany.jsonrpc2; + + + diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/server/Dispatcher.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/server/Dispatcher.java new file mode 100644 index 0000000000..cec3500bb9 --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/server/Dispatcher.java @@ -0,0 +1,263 @@ +package com.thetransactioncompany.jsonrpc2.server; + + +import java.util.Hashtable; + +import com.thetransactioncompany.jsonrpc2.JSONRPC2Error; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Notification; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Request; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Response; + + +/** + * Dispatcher for JSON-RPC 2.0 requests and notifications. This class is + * tread-safe. + * + *

Use the {@code register()} methods to add a request or notification + * handler for an RPC method. + * + *

Use the {@code process()} methods to have an incoming request or + * notification processed by the matching handler. + * + *

The {@code reportProcTime()} method enables reporting of request + * processing time (in microseconds) by appending a non-standard "xProcTime" + * attribute to the resulting JSON-RPC 2.0 response message. + * + *

Example: + * + *

+ * { 
+ *   "result"    : "xyz",
+ *   "id"        : 1,
+ *   "jsonrpc"   : "2.0",
+ *   "xProcTime" : "189 us"
+ * }
+ * 
+ * + *

Note: The dispatch(...) methods were deprecated in version 1.7. Use + * process(...) instead. + * + * @author Vladimir Dzhuvinov + */ +public class Dispatcher implements RequestHandler, NotificationHandler { + + + /** + * Hashtable of request name / handler pairs. + */ + private final Hashtable requestHandlers; + + + /** + * Hashtable of notification name / handler pairs. + */ + private final Hashtable notificationHandlers; + + + /** + * Controls reporting of request processing time by appending a + * non-standard "xProcTime" attribute to the JSON-RPC 2.0 response. + */ + private boolean reportProcTime = false; + + + /** + * Creates a new dispatcher with no registered handlers. + */ + public Dispatcher() { + + requestHandlers = new Hashtable(); + notificationHandlers = new Hashtable(); + } + + + /** + * Registers a new JSON-RPC 2.0 request handler. + * + * @param handler The request handler to register. Must not be + * {@code null}. + * + * @throws IllegalArgumentException On attempting to register a handler + * that duplicates an existing request + * name. + */ + public void register(final RequestHandler handler) { + + for (String name: handler.handledRequests()) { + + if (requestHandlers.containsKey(name)) + throw new IllegalArgumentException("Cannot register a duplicate JSON-RPC 2.0 handler for request " + name); + + requestHandlers.put(name, handler); + } + } + + + /** + * Registers a new JSON-RPC 2.0 notification handler. + * + * @param handler The notification handler to register. Must not be + * {@code null}. + * + * @throws IllegalArgumentException On attempting to register a handler + * that duplicates an existing + * notification name. + */ + public void register(final NotificationHandler handler) { + + for (String name: handler.handledNotifications()) { + + if (notificationHandlers.containsKey(name)) + throw new IllegalArgumentException("Cannot register a duplicate JSON-RPC 2.0 handler for notification " + name); + + notificationHandlers.put(name, handler); + } + } + + + @Override + public String[] handledRequests() { + + java.util.Set var = requestHandlers.keySet(); + return var.toArray(new String[var.size()]); + } + + + @Override + public String[] handledNotifications() { + + java.util.Set var = notificationHandlers.keySet(); + return var.toArray(new String[var.size()]); + } + + + /** + * Gets the handler for the specified JSON-RPC 2.0 request name. + * + * @param requestName The request name to lookup. + * + * @return The corresponding request handler or {@code null} if none + * was found. + */ + public RequestHandler getRequestHandler(final String requestName) { + + return requestHandlers.get(requestName); + } + + + /** + * Gets the handler for the specified JSON-RPC 2.0 notification name. + * + * @param notificationName The notification name to lookup. + * + * @return The corresponding notification handler or {@code null} if + * none was found. + */ + public NotificationHandler getNotificationHandler(final String notificationName) { + + return notificationHandlers.get(notificationName); + } + + + /** + * @deprecated + */ + public JSONRPC2Response dispatch(final JSONRPC2Request request, final MessageContext requestCtx) { + + return process(request, requestCtx); + } + + + @Override + public JSONRPC2Response process(final JSONRPC2Request request, final MessageContext requestCtx) { + + long startNanosec = 0; + + // Measure request processing time? + if (reportProcTime) + startNanosec = System.nanoTime(); + + + final String method = request.getMethod(); + + RequestHandler handler = getRequestHandler(method); + + if (handler == null) { + + // We didn't find a handler for the requested RPC + + Object id = request.getID(); + + return new JSONRPC2Response(JSONRPC2Error.METHOD_NOT_FOUND, id); + } + + // Process the request + + JSONRPC2Response response = handler.process(request, requestCtx); + + if (reportProcTime) { + + final long procTimeNanosec = System.nanoTime() - startNanosec; + + response.appendNonStdAttribute("xProcTime", procTimeNanosec / 1000 + " us"); + } + + return response; + } + + + /** + * @deprecated + */ + public void dispatch(final JSONRPC2Notification notification, final MessageContext notificationCtx) { + + process(notification, notificationCtx); + } + + + @Override + public void process(final JSONRPC2Notification notification, final MessageContext notificationCtx) { + + final String method = notification.getMethod(); + + NotificationHandler handler = getNotificationHandler(method); + + if (handler == null) { + + // We didn't find a handler for the requested RPC + return; + } + + // Process the notification + + handler.process(notification, notificationCtx); + } + + + /** + * Controls reporting of request processing time by appending a + * non-standard "xProcTime" attribute to the JSON-RPC 2.0 response. + * Reporting is disabled by default. + * + * @param enable {@code true} to enable proccessing time reporting, + * {@code false} to disable it. + */ + public void reportProcTime(final boolean enable) { + + reportProcTime = enable; + } + + + /** + * Returns {@code true} if reporting of request processing time is + * enabled. See the {@link #reportProcTime} description for more + * information. + * + * @return {@code true} if reporting of request processing time is + * enabled, else {@code false}. + */ + public boolean reportsProcTime() { + + return reportProcTime; + } +} diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/server/MessageContext.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/server/MessageContext.java new file mode 100644 index 0000000000..1582888d26 --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/server/MessageContext.java @@ -0,0 +1,428 @@ +package com.thetransactioncompany.jsonrpc2.server; + + +import java.net.InetAddress; +import java.net.URLConnection; +import java.security.Principal; +import java.security.cert.X509Certificate; + +import javax.net.ssl.HttpsURLConnection; +import javax.servlet.http.HttpServletRequest; + + +/** + * Context information about JSON-RPC 2.0 request and notification messages. + * This class is immutable. + * + *

    + *
  • The client's host name. + *
  • The client's IP address. + *
  • Whether the request / notification was transmitted securely (e.g. + * via HTTPS). + *
  • The client principal(s) (user), if authenticated. + *
+ * + * @author Vladimir Dzhuvinov + */ +public class MessageContext { + + + /** + * The client hostname, {@code null} if none was specified. + */ + private String clientHostName = null; + + + /** + * The client IP address, {@code null} if none was specified. + */ + private String clientInetAddress = null; + + + /** + * Indicates whether the request was received over a secure channel + * (typically HTTPS). + */ + private boolean secure = false; + + + /** + * The authenticated client principals, {@code null} if none were + * specified. + */ + private Principal[] principals = null; + + + /** + * Minimal implementation of the {@link java.security.Principal} + * interface. + */ + public class BasicPrincipal implements Principal { + + /** + * The principal name. + */ + private String name; + + + /** + * Creates a new principal. + * + * @param name The principal name, must not be {@code null} or + * empty string. + * + * @throws IllegalArgumentException On a {@code null} or empty + * principal name. + */ + public BasicPrincipal(final String name) { + + if (name == null || name.trim().isEmpty()) + throw new IllegalArgumentException("The principal name must be defined"); + + this.name = name; + } + + + /** + * Checks for equality. + * + * @param another The object to compare to. + */ + public boolean equals(final Object another) { + + return another != null && + another instanceof Principal && + ((Principal)another).getName().equals(this.getName()); + } + + + /** + * Returns a hash code for this principal. + * + * @return The hash code. + */ + public int hashCode() { + + return getName().hashCode(); + } + + + /** + * Returns the principal name. + * + * @return The principal name. + */ + public String getName() { + + return name; + } + } + + + /** + * Creates a new JSON-RPC 2.0 request / notification context. + * + * @param clientHostName The client host name, {@code null} if + * unknown. + * @param clientInetAddress The client IP address, {@code null} if + * unknown. + * @param secure Specifies a request received over HTTPS. + * @param principalName Specifies the authenticated client principle + * name, {@code null} if unknown. The name must + * not be an empty or blank string. + */ + public MessageContext(final String clientHostName, + final String clientInetAddress, + final boolean secure, + final String principalName) { + + this.clientHostName = clientHostName; + this.clientInetAddress = clientInetAddress; + this.secure = secure; + + if (principalName != null) { + principals = new Principal[1]; + principals[0] = new BasicPrincipal(principalName); + } + } + + + /** + * Creates a new JSON-RPC 2.0 request / notification context. + * + * @param clientHostName The client host name, {@code null} if + * unknown. + * @param clientInetAddress The client IP address, {@code null} if + * unknown. + * @param secure Specifies a request received over HTTPS. + * @param principalNames Specifies the authenticated client principle + * names, {@code null} if unknown. The names + * must not be an empty or blank string. + */ + public MessageContext(final String clientHostName, + final String clientInetAddress, + final boolean secure, + final String[] principalNames) { + + this.clientHostName = clientHostName; + this.clientInetAddress = clientInetAddress; + this.secure = secure; + + if (principalNames != null) { + principals = new Principal[principalNames.length]; + + for (int i=0; i < principals.length; i++) + principals[0] = new BasicPrincipal(principalNames[i]); + } + } + + + /** + * Creates a new JSON-RPC 2.0 request / notification context. No + * authenticated client principal is specified. + * + * @param clientHostName The client host name, {@code null} if + * unknown. + * @param clientInetAddress The client IP address, {@code null} if + * unknown. + * @param secure Specifies a request received over HTTPS. + */ + public MessageContext(final String clientHostName, + final String clientInetAddress, + final boolean secure) { + + this.clientHostName = clientHostName; + this.clientInetAddress = clientInetAddress; + this.secure = secure; + } + + + /** + * Creates a new JSON-RPC 2.0 request / notification context. Indicates + * an insecure transport (plain HTTP) and no authenticated client + * principal. + * + * @param clientHostName The client host name, {@code null} if + * unknown. + * @param clientInetAddress The client IP address, {@code null} if + * unknown. + */ + public MessageContext(final String clientHostName, + final String clientInetAddress) { + + this.clientHostName = clientHostName; + this.clientInetAddress = clientInetAddress; + this.secure = false; + } + + + /** + * Creates a new JSON-RPC 2.0 request / notification context. Indicates + * an insecure transport (plain HTTP) and no authenticated client + * principal. Not client host name / IP is specified. + */ + public MessageContext() { + + this.secure = false; + } + + + /** + * Creates a new JSON-RPC 2.0 request / notification context from the + * specified HTTP request. + * + * @param httpRequest The HTTP request. + */ + public MessageContext(final HttpServletRequest httpRequest) { + + clientInetAddress = httpRequest.getRemoteAddr(); + + clientHostName = httpRequest.getRemoteHost(); + + if (clientHostName != null && clientHostName.equals(clientInetAddress)) + clientHostName = null; // not resolved actually + + secure = httpRequest.isSecure(); + + X509Certificate[] certs = (X509Certificate[])httpRequest.getAttribute("javax.servlet.request.X509Certificate"); + + if (certs != null && certs.length > 0) { + + principals = new Principal[certs.length]; + + for (int i=0; i < principals.length; i++) + principals[i] = certs[i].getSubjectX500Principal(); + } + } + + + /** + * Creates a new JSON-RPC 2.0 request / notification context from the + * specified URL connection. Use this constructor in cases when the + * HTTP server is the origin of the JSON-RPC 2.0 requests / + * notifications. If the IP address of the HTTP server cannot be + * resolved {@link #getClientInetAddress} will return {@code null}. + * + * @param connection The URL connection, must be established and not + * {@code null}. + */ + public MessageContext(final URLConnection connection) { + + clientHostName = connection.getURL().getHost(); + + InetAddress ip = null; + + if (clientHostName != null) { + + try { + ip = InetAddress.getByName(clientHostName); + + } catch (Exception e) { + + // UnknownHostException, SecurityException + // ignore + } + } + + if (ip != null) + clientInetAddress = ip.getHostAddress(); + + + if (connection instanceof HttpsURLConnection) { + + secure = true; + + HttpsURLConnection httpsConnection = (HttpsURLConnection)connection; + + Principal prn = null; + + try { + prn = httpsConnection.getPeerPrincipal(); + + } catch (Exception e) { + + // SSLPeerUnverifiedException, IllegalStateException + // ignore + } + + if (prn != null) { + + principals = new Principal[1]; + principals[0] = prn; + } + } + } + + + /** + * Gets the host name of the client that sent the request / + * notification. + * + * @return The client host name, {@code null} if unknown. + */ + public String getClientHostName() { + + return clientHostName; + } + + + /** + * Gets the IP address of the client that sent the request / + * notification. + * + * @return The client IP address, {@code null} if unknown. + */ + public String getClientInetAddress() { + + return clientInetAddress; + } + + + /** + * Indicates whether the request / notification was received over a + * secure HTTPS connection. + * + * @return {@code true} If the request was received over HTTPS, + * {@code false} if it was received over plain HTTP. + */ + public boolean isSecure() { + + return secure; + } + + + /** + * Returns the first authenticated client principal, {@code null} if + * none. + * + * @return The first client principal, {@code null} if none. + */ + public Principal getPrincipal() { + + if (principals != null) + return principals[0]; + else + return null; + } + + + /** + * Returns the authenticated client principals, {@code null} if + * none. + * + * @return The client principals, {@code null} if none. + */ + public Principal[] getPrincipals() { + + return principals; + } + + + /** + * Returns the first authenticated client principal name, {@code null} + * if none. + * + * @return The first client principal name, {@code null} if none. + */ + public String getPrincipalName() { + + if (principals != null) + return principals[0].getName(); + else + return null; + } + + + /** + * Returns the authenticated client principal names, {@code null} + * if none. + * + * @return The client principal names, {@code null} if none. + */ + public String[] getPrincipalNames() { + + String[] names = new String[principals.length]; + + for (int i=0; i < names.length; i++) + names[i] = principals[i].getName(); + + return names; + } + + + @Override + public String toString() { + + String s = "[host=" + clientHostName + " hostIP=" + clientInetAddress + " secure=" + secure; + + if (principals != null) { + + int i = 0; + + for (Principal p: principals) + s += " principal[" + (i++) + "]=" + p; + } + + return s + "]"; + } +} diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/server/NotificationHandler.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/server/NotificationHandler.java new file mode 100644 index 0000000000..bc6bb942a6 --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/server/NotificationHandler.java @@ -0,0 +1,35 @@ +package com.thetransactioncompany.jsonrpc2.server; + + +import com.thetransactioncompany.jsonrpc2.JSONRPC2Notification; + + +/** + * Interface for handling JSON-RPC 2.0 notifications. + * + * @author Vladimir Dzhuvinov + */ +public interface NotificationHandler { + + + /** + * Gets the names of the handled JSON-RPC 2.0 notification methods. + * + * @return The names of the handled JSON-RPC 2.0 notification methods. + */ + public String[] handledNotifications(); + + + /** + * Processes a JSON-RPC 2.0 notification. + * + *

Note that JSON-RPC 2.0 notifications don't produce a response! + * + * @param notification A valid JSON-RPC 2.0 notification instance. + * Must not be {@code null}. + * @param notificationCtx Context information about the notification + * message, may be {@code null} if undefined. + */ + public void process(final JSONRPC2Notification notification, final MessageContext notificationCtx); + +} diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/server/RequestHandler.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/server/RequestHandler.java new file mode 100644 index 0000000000..5215282304 --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/server/RequestHandler.java @@ -0,0 +1,36 @@ +package com.thetransactioncompany.jsonrpc2.server; + + +import com.thetransactioncompany.jsonrpc2.JSONRPC2Request; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Response; + + +/** + * Interface for handling JSON-RPC 2.0 requests. + * + * @author Vladimir Dzhuvinov + */ +public interface RequestHandler { + + + /** + * Gets the names of the handled JSON-RPC 2.0 request methods. + * + * @return The names of the handled JSON-RPC 2.0 request methods. + */ + public String[] handledRequests(); + + + /** + * Processes a JSON-RPC 2.0 request. + * + * @param request A valid JSON-RPC 2.0 request instance. Must not be + * {@code null}. + * @param requestCtx Context information about the request message, may + * be {@code null} if undefined. + * + * @return The resulting JSON-RPC 2.0 response. It indicates success + * or an error, such as METHOD_NOT_FOUND. + */ + public JSONRPC2Response process(final JSONRPC2Request request, final MessageContext requestCtx); +} diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/server/package-info.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/server/package-info.java new file mode 100644 index 0000000000..3f127d454e --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/server/package-info.java @@ -0,0 +1,34 @@ +/** + * Simple server framework for processing JSON-RPC 2.0 requests and + * notifications. + * + *

Usage: + * + *

    + *
  1. Implement {@link com.thetransactioncompany.jsonrpc2.server.RequestHandler request} + * and / or {@link com.thetransactioncompany.jsonrpc2.server.NotificationHandler notification} + * handlers for the various expected JSON-RPC 2.0 messages. A handler + * may process one or more request/notification methods (identified by + * method name). + *
  2. Create a new {@link com.thetransactioncompany.jsonrpc2.server.Dispatcher} + * and register the handlers with it. + *
  3. Pass the received JSON-RPC 2.0 requests and notifications to the + * appropriate {@code Dispatcher.dispatch(...)} method, then, if the + * message is a request, pass the resulting JSON-RPC 2.0 response back + * to the client. + *
+ * + *

Direct package dependencies: + * + *

    + *
  • JSON-RPC 2.0 Base + * [com.thetransactioncompany.jsonrpc2] to construct and represent + * JSON-RPC 2.0 messages. + *
  • Java Servlet API [javax.servlet.http] for constructing + * {@link com.thetransactioncompany.jsonrpc2.server.MessageContext} + * objects from HTTP servlet requests. + *
+ * + * @author Vladimir Dzhuvinov + */ +package com.thetransactioncompany.jsonrpc2.server; diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/util/NamedParamsRetriever.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/util/NamedParamsRetriever.java new file mode 100644 index 0000000000..4eb4e83f80 --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/util/NamedParamsRetriever.java @@ -0,0 +1,1524 @@ +package com.thetransactioncompany.jsonrpc2.util; + + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.thetransactioncompany.jsonrpc2.JSONRPC2Error; + + +/** + * Utility class for retrieving JSON-RPC 2.0 named parameters (key-value pairs + * packed into a JSON Object). + * + *

Provides a set of getter methods according to the expected parameter type + * (number, string, etc.) and whether the parameter is mandatory or optional: + * + *

    + *
  • {@code getXXX(param_name)} for mandatory parameters, where + * {@code XXX} is the expected parameter type. + *
  • {@code getOptXXX(param_name, default_value)} for optional + * parameters, specifying a default value. + *
+ * + *

There are also generic getter methods that let you do the type conversion + * yourself. + * + *

If a parameter cannot be retrieved, e.g. due to a missing mandatory + * parameter or bad type, a + * {@link com.thetransactioncompany.jsonrpc2.JSONRPC2Error#INVALID_PARAMS} + * exception is thrown. + * + *

Example: suppose you have a method with 3 named parameters "name", "age" + * and "sex", where the last is optional and defaults to "female": + * + *

+ * // Parse received request string
+ * JSONRPC2Request request = null;
+ *
+ * try {
+ *         request = JSONRPC2Request.parse(jsonString);
+ * } catch (JSONRPC2ParseException e) {
+ *         // handle exception...
+ * }
+ *
+ * // Create a new retriever for named parameters
+ * Map params = (Map)request.getParams();
+ * NamedParamsRetriever r = new NamedParamsRetriever(params);
+ *
+ * try {
+ *         // Extract "name" string parameter
+ *         String name = r.getString("name");
+ *
+ *         // Extract "age" integer parameter
+ *         int age = r.getInt("age");
+ *
+ *         // Extract optional "sex" string parameter which defaults to "female"
+ *         String sex = r.getOptString("sex", "female");
+ *
+ * } catch (JSONRPC2Error e) {
+ *         // A JSONRPC2Error.INVALID_PARAMS will be thrown to indicate
+ *         // an unexpected parameter type or a missing mandatory parameter.
+ *         // You can use it straight away to create the appropriate
+ *         // JSON-RPC 2.0 error response.
+ *         JSONRPC2Response response = new JSONRPC2Response(e, null);
+ * }
+ * 
+ * 
+ * + * @author Vladimir Dzhuvinov + */ +public class NamedParamsRetriever + extends ParamsRetriever { + + + /** + * The named parameters interface. + */ + private Map params = null; + + + /** + * Throws a JSON-RPC 2.0 error indicating one or more missing named + * parameters. + * + * @param names The parameter names. Must not be {@code null}. + * + * @throws JSONRPC2Error Formatted JSON-RPC 2.0 error. + */ + private static void throwMissingParameterException(final String... names) + throws JSONRPC2Error { + + if (names.length == 1) + throw JSONRPC2Error.INVALID_PARAMS. + appendMessage(": Missing \"" + names[0] + "\" parameter"); + + // Compose list of missing parameters + StringBuilder list = new StringBuilder(); + + for (String name : names) { + + if (list.length() > 0) + list.append(','); + + list.append('"'); + list.append(name); + list.append('"'); + } + + throw JSONRPC2Error.INVALID_PARAMS. + appendMessage(": Missing " + list.toString() + " parameters"); + } + + + /** + * Throws a JSON-RPC 2.0 error indicating a named parameter with an + * unexpected {@code null} value. + * + * @param name The parameter name. Must not be {@code null}. + * + * @throws JSONRPC2Error Formatted JSON-RPC 2.0 error. + */ + private static void throwNullParameterException(final String name) + throws JSONRPC2Error { + + throw JSONRPC2Error.INVALID_PARAMS. + appendMessage(": Parameter \"" + name + "\" must not be null"); + } + + + /** + * Throws a JSON-RPC 2.0 error indicating a named parameter with an + * unexpected enumerated value. + * + * @param name The parameter name. + * @param enumStrings The acceptable string values. Must not be + * {@code null}. + * + * @throws JSONRPC2Error Formatted JSON-RPC 2.0 error. + */ + private static void throwEnumParameterException(final String name, + final String[] enumStrings) + throws JSONRPC2Error { + + StringBuilder msg = new StringBuilder(": Enumerated parameter \"" + name + "\" must have values "); + + for (int i=0; i < enumStrings.length; i++) { + + if (i > 0 && i == enumStrings.length - 1) + msg.append(" or "); + + else if (i > 0) + msg.append(", "); + + msg.append('"'); + msg.append(enumStrings[i]); + msg.append('"'); + } + + throw JSONRPC2Error.INVALID_PARAMS.appendMessage(msg.toString()); + } + + + /** + * Throws a JSON-RPC 2.0 error indicating a named parameter with an + * unexpected enumerated value. + * + * @param name The parameter name. + * @param enumClass The enumeration class specifying the acceptable + * string values. Must not be {@code null}. + * + * @throws JSONRPC2Error Formatted JSON-RPC 2.0 error. + */ + private static > void throwEnumParameterException(final String name, + final Class enumClass) + throws JSONRPC2Error { + + StringBuilder msg = new StringBuilder(": Enumerated parameter \"" + name + "\" must have values "); + + T[] constants = enumClass.getEnumConstants(); + + for (int i = 0; i < constants.length; i++) { + + if (i > 0 && i == constants.length - 1) + msg.append(" or "); + + else if (i > 0) + msg.append(", "); + + msg.append('"'); + msg.append(constants[i].toString()); + msg.append('"'); + } + + throw JSONRPC2Error.INVALID_PARAMS.appendMessage(msg.toString()); + } + + + /** + * Creates a JSON-RPC 2.0 error indicating a named parameter with an + * unexpected JSON type. + * + * @param name The parameter name. Must not be {@code null}. + * + * @return Formatted JSON-RPC 2.0 error. + */ + private static JSONRPC2Error newUnexpectedParameterTypeException(final String name) { + + return JSONRPC2Error.INVALID_PARAMS. + appendMessage(": Parameter \"" + name + "\" has an unexpected JSON type"); + } + + + /** + * Creates a JSON-RPC 2.0 error indicating an array exception. + * + * @param name The parameter name. Must not be {@code null}. + * + * @return Formatted JSON-RPC 2.0 error. + */ + private static JSONRPC2Error newArrayException(final String name) { + + return JSONRPC2Error.INVALID_PARAMS. + appendMessage(": Parameter \"" + name + "\" caused an array exception"); + } + + + /** + * Creates a new named parameters retriever from the specified + * key-value map. + * + * @param params The named parameters map. Must not be {@code null}. + */ + public NamedParamsRetriever(final Map params) { + + if (params == null) + throw new IllegalArgumentException("The parameters map must not be null"); + + this.params = params; + } + + + /** + * Gets the named parameters for this retriever. + * + * @return The named parameters. + */ + public Map getParams() { + + return params; + } + + + @Override + public int size() { + + return params.size(); + } + + + /** + * Returns {@code true} if a parameter by the specified name exists, + * else {@code false}. + * + * @param name The parameter name. + * + * @return {@code true} if the parameter exists, else {@code false}. + */ + public boolean hasParam(final String name) { + + return params.containsKey(name); + } + + + /** + * @see #hasParam + */ + @Deprecated + public boolean hasParameter(final String name) { + + return hasParam(name); + } + + + /** + * Returns {@code true} if the parameters by the specified names exist, + * else {@code false}. + * + * @param names The parameter names. Must not be {@code null}. + * + * @return {@code true} if the parameters exist, else {@code false}. + */ + public boolean hasParams(final String[] names) { + + return hasParams(names, null); + } + + + /** + * @see #hasParams(String[]) + */ + @Deprecated + public boolean hasParameters(final String[] names) { + + return hasParams(names); + } + + + /** + * Returns {@code true} if the parameters by the specified mandatory + * names exist, {@code false} if any mandatory name is missing or a + * name outside the mandatory and optional is present. + * + * @param mandatoryNames The expected mandatory parameter names. Must + * not be {@code null}. + * @param optionalNames The expected optional parameter names, + * empty array or {@code null} if none. + * + * @return {@code true} if the specified mandatory names and only any + * of the optional are present, else {@code false}. + */ + public boolean hasParams(final String[] mandatoryNames, + final String[] optionalNames) { + + // Do shallow copy of params + Map paramsCopy = (Map)((HashMap)params).clone(); + + // Pop the mandatory names + for (String name: mandatoryNames) { + + if (paramsCopy.containsKey(name)) + paramsCopy.remove(name); + else + return false; + } + + // Pop the optional names (if any specified) + if (optionalNames != null) { + + for (String name: optionalNames) { + + if (paramsCopy.containsKey(name)) + paramsCopy.remove(name); + } + } + + // Any remaining keys that shouldn't be there? + int remainingKeys = paramsCopy.size(); + + return remainingKeys == 0; + } + + + /** + * @see #hasParams(String[], String[]) + */ + @Deprecated + public boolean hasParameters(final String[] mandatoryNames, + final String[] optionalNames) { + + return hasParams(mandatoryNames, optionalNames); + } + + + /** + * Returns the names of all available parameters. + * + * @return The parameter names. + */ + public String[] getNames() { + + Set keySet = params.keySet(); + + return keySet.toArray(new String[keySet.size()]); + } + + + /** + * Throws a {@code JSONRPC2Error.INVALID_PARAMS} if the specified + * names aren't present in the parameters, or names outside the + * specified are contained. + * + *

You may use this method to a fire a proper JSON-RPC 2.0 error + * on a missing or unexpected mandatory parameter name. + * + * @param mandatoryNames The expected parameter names. Must not be + * {@code null}. + * + * @throws JSONRPC2Error On a missing parameter name or names outside + * the specified + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public void ensureParams(final String[] mandatoryNames) + throws JSONRPC2Error { + + ensureParameters(mandatoryNames, null); + } + + + /** + * @see #ensureParams(String[]) + */ + @Deprecated + public void ensureParameters(final String[] mandatoryNames) + throws JSONRPC2Error { + + ensureParams(mandatoryNames); + } + + + /** + * Throws a {@code JSONRPC2Error.INVALID_PARAMS} if the specified + * mandatory names aren't contained in the parameters, or names outside + * the specified mandatory and optional are present. + * + *

You may use this method to a fire a proper JSON-RPC 2.0 error + * on a missing or unexpected mandatory parameter name. + * + * @param mandatoryNames The expected mandatory parameter names. Must + * not be {@code null}. + * @param optionalNames The expected optional parameter names, + * empty array or {@code null} if none. + * + * @throws JSONRPC2Error On a missing mandatory parameter name or names + * outside the specified mandatory and optional + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public void ensureParams(final String[] mandatoryNames, + final String[] optionalNames) + throws JSONRPC2Error { + + if (! hasParameters(mandatoryNames, optionalNames)) + throwMissingParameterException(mandatoryNames); + } + + + /** + * @see #ensureParams(String[], String[]) + */ + @Deprecated + public void ensureParameters(final String[] mandatoryNames, + final String[] optionalNames) + throws JSONRPC2Error { + + ensureParams(mandatoryNames, optionalNames); + } + + + /** + * Throws a {@code JSONRPC2Error.INVALID_PARAMS} exception if there is + * no parameter by the specified name. + * + *

You may use this method to fire the proper JSON-RPC 2.0 error + * on a missing mandatory parameter. + * + * @param name The parameter name. + * + * @throws JSONRPC2Error On a missing parameter + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public void ensureParam(final String name) + throws JSONRPC2Error { + + if (! hasParameter(name)) + throwMissingParameterException(name); + } + + + /** + * @see #ensureParam(String) + */ + @Deprecated + public void ensureParameter(final String name) + throws JSONRPC2Error { + + ensureParam(name); + } + + + /** + * Throws a {@code JSONRPC2Error.INVALID_PARAMS} exception if there is + * no parameter by the specified name, its value is {@code null}, or + * its type doesn't map to the specified. + * + *

You may use this method to fire the proper JSON-RPC 2.0 error + * on a missing or badly-typed mandatory parameter. + * + * @param name The parameter name. + * @param clazz The corresponding Java class that the parameter should + * map to (any one of the return types of the + * {@code getXXX()} getter methods. Set to + * {@code Object.class} to allow any type. Must not be + * {@code null}. + * + * @throws JSONRPC2Error On a missing parameter, {@code null} value or + * bad type ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public void ensureParam(final String name, final Class clazz) + throws JSONRPC2Error { + + ensureParameter(name, clazz, false); + } + + + /** + * @see #ensureParam(String, Class) + */ + @Deprecated + public void ensureParameter(final String name, final Class clazz) + throws JSONRPC2Error { + + ensureParam(name, clazz); + } + + + /** + * Throws a {@code JSONRPC2Error.INVALID_PARAMS} exception if there is + * no parameter by the specified name or its type doesn't map to the + * specified. + * + *

You may use this method to fire the proper JSON-RPC 2.0 error + * on a missing or badly-typed mandatory parameter. + * + * @param name The parameter name. + * @param clazz The corresponding Java class that the parameter + * should map to (any one of the return types of the + * {@code getXXX()} getter methods. Set to + * {@code Object.class} to allow any type. Must not be + * {@code null}. + * @param allowNull If {@code true} allows a {@code null} parameter + * value. + * + * @throws JSONRPC2Error On a missing parameter or bad type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public void ensureParam(final String name, + final Class clazz, + final boolean allowNull) + throws JSONRPC2Error { + + // First, check existence only + ensureParameter(name); + + // Now check type + Object value = params.get(name); + + if (value == null) { + + if (allowNull) + return; // ok + + else + throwNullParameterException(name); + } + + if (! clazz.isAssignableFrom(value.getClass())) + throw newUnexpectedParameterTypeException(name); + } + + + /** + * @see #ensureParam(String, Class, boolean) + */ + @Deprecated + public void ensureParameter(final String name, + final Class clazz, + final boolean allowNull) + throws JSONRPC2Error { + + ensureParam(name, clazz, allowNull); + } + + + /** + * Retrieves the specified parameter which can be of any type. Use this + * generic getter if you want to cast the value yourself. Otherwise + * look at the typed {@code get*} methods. + * + * @param name The parameter name. + * + * @return The parameter value. + * + * @throws JSONRPC2Error On a missing parameter + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public Object get(final String name) + throws JSONRPC2Error { + + ensureParameter(name); + + return params.get(name); + } + + + /** + * Retrieves the specified parameter which must map to the provided + * class (use the appropriate wrapper class for primitive types). + * + * @param name The parameter name. + * @param clazz The corresponding Java class that the parameter should + * map to (any one of the return types of the + * {@code getXXX()} getter methods. Set to + * {@code Object.class} to allow any type. Must not be + * {@code null}. + * + * @return The parameter value. + * + * @throws JSONRPC2Error On a missing parameter, {@code null} value or + * bad type ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public T get(final String name, final Class clazz) + throws JSONRPC2Error { + + return get(name, clazz, false); + } + + + /** + * Retrieves the specified parameter which must map to the provided + * class (use the appropriate wrapper class for primitive types). + * + * @param name The parameter name. + * @param clazz The corresponding Java class that the parameter + * should map to (any one of the return types of the + * {@code getXXX()} getter methods. Set to + * {@code Object.class} to allow any type. Must not be + * {@code null}. + * @param allowNull If {@code true} allows a {@code null} parameter + * value. + * + * @return The parameter value. + * + * @throws JSONRPC2Error On a missing parameter or bad type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + @SuppressWarnings("unchecked") + public T get(final String name, final Class clazz, final boolean allowNull) + throws JSONRPC2Error { + + ensureParameter(name, clazz, allowNull); + + try { + return (T)params.get(name); + + } catch (ClassCastException e) { + + throw newUnexpectedParameterTypeException(name); + } + } + + + /** + * Retrieves the specified optional parameter which must map to the + * provided class (use the appropriate wrapper class for primitive + * types). If the parameter doesn't exist the method returns the + * specified default value. + * + * @param name The parameter name. + * @param clazz The corresponding Java class that the parameter + * should map to (any one of the return types of + * the {@code getXXX()} getter methods. Set to + * {@code Object.class} to allow any type. Must not + * be {@code null}. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public T getOpt(final String name, final Class clazz, final T defaultValue) + throws JSONRPC2Error { + + return getOpt(name, clazz, false, defaultValue); + } + + + /** + * Retrieves the specified optional parameter which must map to the + * provided class (use the appropriate wrapper class for primitive + * types). If the parameter doesn't exist the method returns the + * specified default value. + * + * @param name The parameter name. + * @param clazz The corresponding Java class that the parameter + * should map to (any one of the return types of + * the {@code getXXX()} getter methods. Set to + * {@code Object.class} to allow any type. Must not + * be {@code null}. + * @param allowNull If {@code true} allows a {@code null} parameter + * value. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value. + * + * @throws JSONRPC2Error On a bad parameter type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + @SuppressWarnings("unchecked") + public T getOpt(final String name, + final Class clazz, + final boolean allowNull, + final T defaultValue) + throws JSONRPC2Error { + + if (! hasParameter(name)) + return defaultValue; + + ensureParameter(name, clazz, allowNull); + + try { + return (T)params.get(name); + + } catch (ClassCastException e) { + + throw newUnexpectedParameterTypeException(name); + } + } + + + /** + * Retrieves the specified string parameter. + * + * @param name The parameter name. + * + * @return The parameter value as a string. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String getString(final String name) + throws JSONRPC2Error { + + return getString(name, false); + } + + + /** + * Retrieves the specified string parameter. + * + * @param name The parameter name. + * @param allowNull If {@code true} allows a {@code null} value. + * + * @return The parameter value as a string. + * + * @throws JSONRPC2Error On a missing parameter or bad type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String getString(final String name, final boolean allowNull) + throws JSONRPC2Error { + + return get(name, String.class, allowNull); + } + + + /** + * Retrieves the specified optional string parameter. If it doesn't + * exist the method will return the specified default value. + * + * @param name The parameter name. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a string. + * + * @throws JSONRPC2Error On a bad type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String getOptString(final String name, final String defaultValue) + throws JSONRPC2Error { + + return getOptString(name, false, defaultValue); + } + + + /** + * Retrieves the specified optional string parameter. If it doesn't + * exist the method will return the specified default value. + * + * @param name The parameter name. + * @param allowNull If {@code true} allows a {@code null} value. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a string. + * + * @throws JSONRPC2Error On a bad type ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String getOptString(final String name, final boolean allowNull, final String defaultValue) + throws JSONRPC2Error { + + return getOpt(name, String.class, allowNull, defaultValue); + } + + + /** + * Retrieves the specified enumerated string parameter. + * + * @param name The parameter name. + * @param enumStrings The acceptable string values. Must not be + * {@code null}. + * + * @return The parameter value as a string. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * bad enumeration value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String getEnumString(final String name, final String[] enumStrings) + throws JSONRPC2Error { + + return getEnumString(name, enumStrings, false); + } + + + /** + * Retrieves the specified enumerated string parameter, allowing for a + * case insenstive match. + * + * @param name The parameter name. + * @param enumStrings The acceptable string values. Must not be + * {@code null}. + * @param ignoreCase {@code true} for a case insensitive match. + * + * @return The matching parameter value as a string. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * bad enumeration value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String getEnumString(final String name, + final String[] enumStrings, + final boolean ignoreCase) + throws JSONRPC2Error { + + String value = get(name, String.class); + + String match = getEnumStringMatch(value, enumStrings, ignoreCase); + + if (match == null) + throwEnumParameterException(name, enumStrings); + + return match; + } + + + /** + * Retrieves the specified optional enumerated string parameter. + * + * @param name The parameter name. + * @param enumStrings The acceptable string values. Must not be + * {@code null}. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a string. + * + * @throws JSONRPC2Error On a bad type or bad enumeration value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String getOptEnumString(final String name, + final String[] enumStrings, + final String defaultValue) + throws JSONRPC2Error { + + return getOptEnumString(name, enumStrings, defaultValue, false); + } + + + /** + * Retrieves the specified optional enumerated string parameter, + * allowing for a case insenstive match. If it doesn't exist the method + * will return the specified default value. + * + * @param name The parameter name. + * @param enumStrings The acceptable string values. Must not be + * {@code null}. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * @param ignoreCase {@code true} for a case insensitive match. + * + * @return The matching parameter value as a string. + * + * @throws JSONRPC2Error On a bad type or bad enumeration value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String getOptEnumString(final String name, + final String[] enumStrings, + final String defaultValue, + final boolean ignoreCase) + throws JSONRPC2Error { + + String value = getOpt(name, String.class, defaultValue); + + if (defaultValue == null && value == null) + return null; + + String match = getEnumStringMatch(value, enumStrings, ignoreCase); + + if (match == null) + throwEnumParameterException(name, enumStrings); + + return match; + } + + + /** + * Retrieves the specified enumerated parameter (from a JSON string + * that has a predefined set of possible values). + * + * @param name The parameter name. + * @param enumClass An enumeration type with constant names + * representing the acceptable string values. Must not + * be {@code null}. + * + * @return The matching enumeration constant. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * bad enumeration value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public > T getEnum(final String name, final Class enumClass) + throws JSONRPC2Error { + + return getEnum(name, enumClass, false); + } + + + /** + * Retrieves the specified enumerated parameter (from a JSON string + * that has a predefined set of possible values), allowing for a case + * insensitive match. + * + * @param name The parameter name. + * @param enumClass An enumeration type with constant names + * representing the acceptable string values. Must + * not be {@code null}. + * @param ignoreCase If {@code true} a case insensitive match against + * the acceptable constant names is performed. + * + * @return The matching enumeration constant. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * bad enumeration value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public > T getEnum(final String name, + final Class enumClass, + final boolean ignoreCase) + throws JSONRPC2Error { + + String value = get(name, String.class); + + T match = getEnumStringMatch(value, enumClass, ignoreCase); + + if (match == null) + throwEnumParameterException(name, enumClass); + + return match; + } + + + /** + * Retrieves the specified optional enumerated parameter (from a JSON + * string that has a predefined set of possible values). + * + * @param name The parameter name. + * @param enumClass An enumeration type with constant names + * representing the acceptable string values. Must + * not be {@code null}. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The matching enumeration constant. + * + * @throws JSONRPC2Error On a bad type or bad enumeration value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public > T getOptEnum(final String name, + final Class enumClass, + final T defaultValue) + throws JSONRPC2Error { + + return getOptEnum(name, enumClass, defaultValue, false); + } + + + /** + * Retrieves the specified optional enumerated parameter (from a JSON + * string that has a predefined set of possible values), allowing for + * a case insenstive match. If it doesn't exist the method will return + * the specified default value. + * + * @param name The parameter name. + * @param enumClass An enumeration type with constant names + * representing the acceptable string values. Must + * not be {@code null}. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * @param ignoreCase If {@code true} a case insensitive match against + * the acceptable constant names is performed. + * + * @return The matching enumeration constant. + * + * @throws JSONRPC2Error On a bad type or bad enumeration value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public > T getOptEnum(final String name, + final Class enumClass, + final T defaultValue, + final boolean ignoreCase) + throws JSONRPC2Error { + + String value; + + if (defaultValue != null) + value = getOpt(name, String.class, defaultValue.toString()); + + else + value = getOpt(name, String.class, null); + + if (defaultValue == null && value == null) + return null; + + T match = getEnumStringMatch(value, enumClass, ignoreCase); + + if (match == null) + throwEnumParameterException(name, enumClass); + + return match; + } + + + /** + * Retrieves the specified boolean (maps from JSON true/false) + * parameter. + * + * @param name The parameter name. + * + * @return The parameter value as a {@code boolean}. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public boolean getBoolean(final String name) + throws JSONRPC2Error { + + return get(name, Boolean.class); + } + + + /** + * Retrieves the specified optional boolean (maps from JSON true/false) + * parameter. If it doesn't exist the method will return the specified + * default value. + * + * @param name The parameter name. + * @param defaultValue The default return value if the parameter + * doesn't exist. + * + * @return The parameter value as a {@code boolean}. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public boolean getOptBoolean(final String name, final boolean defaultValue) + throws JSONRPC2Error { + + return getOpt(name, Boolean.class, defaultValue); + } + + + /** + * Retrieves the specified numeric parameter as an {@code int}. + * + * @param name The parameter name. + * + * @return The parameter value as an {@code int}. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public int getInt(final String name) + throws JSONRPC2Error { + + Number number = get(name, Number.class); + return number.intValue(); + } + + + /** + * Retrieves the specified optional numeric parameter as an + * {@code int}. If it doesn't exist the method will return the + * specified default value. + * + * @param name The parameter name. + * @param defaultValue The default return value if the parameter + * doesn't exist. + * + * @return The parameter value as an {@code int}. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public int getOptInt(final String name, final int defaultValue) + throws JSONRPC2Error { + + Number number = getOpt(name, Number.class, defaultValue); + return number.intValue(); + } + + + /** + * Retrieves the specified numeric parameter as a {@code long}. + * + * @param name The parameter name. + * + * @return The parameter value as a {@code long}. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public long getLong(final String name) + throws JSONRPC2Error { + + Number number = get(name, Number.class); + return number.longValue(); + } + + + /** + * Retrieves the specified optional numeric parameter as a + * {@code long}. If it doesn't exist the method will return the + * specified default value. + * + * @param name The parameter name. + * @param defaultValue The default return value if the parameter + * doesn't exist. + * + * @return The parameter value as a long. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public long getOptLong(final String name, final long defaultValue) + throws JSONRPC2Error { + + Number number = getOpt(name, Number.class, defaultValue); + return number.longValue(); + } + + + /** + * Retrieves the specified numeric parameter as a {@code float}. + * + * @param name The parameter name. + * + * @return The parameter value as a {@code float}. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public float getFloat(final String name) + throws JSONRPC2Error { + + Number number = get(name, Number.class); + return number.floatValue(); + } + + + /** + * Retrieves the specified optional numeric parameter as a + * {@code float}. If it doesn't exist the method will return the + * specified default value. + * + * @param name The parameter name. + * @param defaultValue The default return value if the parameter + * doesn't exist. + * + * @return The parameter value as a {@code float}. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public float getOptFloat(final String name, final float defaultValue) + throws JSONRPC2Error { + + Number number = getOpt(name, Number.class, defaultValue); + return number.floatValue(); + } + + + /** + * Retrieves the specified numeric parameter as a {@code double}. + * + * @param name The parameter name. + * + * @return The parameter value as a {@code double}. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public double getDouble(final String name) + throws JSONRPC2Error { + + Number number = get(name, Number.class); + return number.doubleValue(); + } + + + /** + * Retrieves the specified optional numeric parameter as a + * {@code double}. If it doesn't exist the method will return the + * specified default value. + * + * @param name The parameter name. + * @param defaultValue The default return value if the parameter + * doesn't exist. + * + * @return The parameter value as a {@code double}. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public double getOptDouble(final String name, final double defaultValue) + throws JSONRPC2Error { + + Number number = getOpt(name, Number.class, defaultValue); + return number.doubleValue(); + } + + + /** + * Retrieves the specified list (maps from JSON array) parameter. + * + * @param name The parameter name. + * + * @return The parameter value as a list. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public List getList(final String name) + throws JSONRPC2Error { + + final boolean allowNull = false; + + return getList(name, allowNull); + } + + + /** + * Retrieves the specified list (maps from JSON array) parameter. + * + * @param name The parameter name. + * @param allowNull If {@code true} allows a {@code null} value. + * + * @return The parameter value as a list. + * + * @throws JSONRPC2Error On a missing parameter or bad type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + @SuppressWarnings("unchecked") + public List getList(final String name, final boolean allowNull) + throws JSONRPC2Error { + + return (List)get(name, List.class, allowNull); + } + + + /** + * Retrieves the specified optional list (maps from JSON array) + * parameter. If it doesn't exist the method will return the specified + * default value. + * + * @param name The parameter name. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a list. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public List getOptList(final String name, final List defaultValue) + throws JSONRPC2Error { + + final boolean allowNull = false; + + return getOptList(name, allowNull, defaultValue); + } + + + /** + * Retrieves the specified optional list (maps from JSON array) + * parameter. If it doesn't exist the method will return the specified + * default value. + * + * @param name The parameter name. + * @param allowNull If {@code true} allows a {@code null} value. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a list. + * + * @throws JSONRPC2Error On a bad parameter type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + @SuppressWarnings("unchecked") + public List getOptList(final String name, + final boolean allowNull, + final List defaultValue) + throws JSONRPC2Error { + + return (List)getOpt(name, List.class, allowNull, defaultValue); + } + + + /** + * Retrieves the specified string array (maps from JSON array of + * strings) parameter. + * + * @param name The parameter name. + * + * @return The parameter value as a string array. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String[] getStringArray(final String name) + throws JSONRPC2Error { + + final boolean allowNull = false; + + return getStringArray(name, allowNull); + } + + + /** + * Retrieves the specified string array (maps from JSON array of + * strings) parameter. + * + * @param name The parameter name. + * @param allowNull If {@code true} allows a {@code null} value. + * + * @return The parameter value as a string array. + * + * @throws JSONRPC2Error On a missing parameter or bad type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + @SuppressWarnings("unchecked") + public String[] getStringArray(final String name, final boolean allowNull) + throws JSONRPC2Error { + + List list = getList(name, allowNull); + + if (list == null) + return null; + + try { + return list.toArray(new String[list.size()]); + + } catch (ArrayStoreException e) { + + throw newArrayException(name); + } + } + + + /** + * Retrieves the specified optional string array (maps from JSON array + * of strings) parameter. If it doesn't exist the method will return + * the specified default value. + * + * @param name The parameter name. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a string array. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String[] getOptStringArray(final String name, final String[] defaultValue) + throws JSONRPC2Error { + + final boolean allowNull = false; + + return getOptStringArray(name, allowNull, defaultValue); + } + + + /** + * Retrieves the specified optional string array (maps from JSON array + * of strings) parameter. If it doesn't exist the method will return + * the specified default value. + * + * @param name The parameter name. + * @param allowNull If {@code true} allows a {@code null} value. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a string array. + * + * @throws JSONRPC2Error On a bad parameter type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String[] getOptStringArray(final String name, + final boolean allowNull, + final String[] defaultValue) + throws JSONRPC2Error { + + if (! hasParameter(name)) + return defaultValue; + + return getStringArray(name, allowNull); + } + + + /** + * Retrieves the specified map (maps from JSON object) parameter. + * + * @param name The parameter name. + * + * @return The parameter value as a map. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public Map getMap(final String name) + throws JSONRPC2Error { + + return getMap(name, false); + } + + + /** + * Retrieves the specified map (maps from JSON object) parameter. + * + * @param name The parameter name. + * @param allowNull If {@code true} allows a {@code null} value. + * + * @return The parameter value as a map. + * + * @throws JSONRPC2Error On a missing parameter or bad type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + @SuppressWarnings("unchecked") + public Map getMap(final String name, final boolean allowNull) + throws JSONRPC2Error { + + try { + return (Map)get(name, Map.class, allowNull); + + } catch (ClassCastException e) { + + throw newUnexpectedParameterTypeException(name); + } + } + + + /** + * Retrieves the specified optional map (maps from JSON object) + * parameter. If it doesn't exist the method will return the specified + * default value. + * + * @param name The parameter name. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a map. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public Map getOptMap(final String name, final Map defaultValue) + throws JSONRPC2Error { + + return getOptMap(name, false, defaultValue); + } + + + /** + * Retrieves the specified optional map (maps from JSON object) + * parameter. If it doesn't exist the method will return the specified + * default value. + * + * @param name The parameter name. + * @param allowNull If {@code true} allows a {@code null} value. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a map. + * + * @throws JSONRPC2Error On a bad parameter type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + @SuppressWarnings("unchecked") + public Map getOptMap(final String name, + final boolean allowNull, + final Map defaultValue) + throws JSONRPC2Error { + + try { + return (Map)getOpt(name, Map.class, allowNull, defaultValue); + + } catch (ClassCastException e) { + + throw newUnexpectedParameterTypeException(name); + } + } +} diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/util/ParamsRetriever.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/util/ParamsRetriever.java new file mode 100644 index 0000000000..137297a79f --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/util/ParamsRetriever.java @@ -0,0 +1,80 @@ +package com.thetransactioncompany.jsonrpc2.util; + + +/** + * The base abstract class for the JSON-RPC 2.0 parameter retrievers. + * + * @author Vladimir Dzhuvinov + */ +public abstract class ParamsRetriever { + + + /** + * Returns the parameter count. + * + * @return The parameters count. + */ + public abstract int size(); + + + /** + * Matches a string against an array of acceptable values. + * + * @param input The string to match. + * @param enumStrings The acceptable string values. Must not be + * {@code null}. + * @param ignoreCase {@code true} for a case insensitive match. + * + * @return The matching string value, {@code null} if no match was + * found. + */ + protected static String getEnumStringMatch(final String input, + final String[] enumStrings, + final boolean ignoreCase) { + + for (final String en: enumStrings) { + + if (ignoreCase) { + if (en.equalsIgnoreCase(input)) + return en; + } + else { + if (en.equals(input)) + return en; + } + } + + return null; + } + + + /** + * Matches a string against an enumeration of acceptable values. + * + * @param input The string to match. + * @param enumClass The enumeration class specifying the acceptable + * string values. Must not be {@code null}. + * @param ignoreCase {@code true} for a case insensitive match. + * + * @return The matching enumeration constant, {@code null} if no match + * was found. + */ + protected static > T getEnumStringMatch(final String input, + final Class enumClass, + final boolean ignoreCase) { + + for (T en: enumClass.getEnumConstants()) { + + if (ignoreCase) { + if (en.toString().equalsIgnoreCase(input)) + return en; + } + else { + if (en.toString().equals(input)) + return en; + } + } + + return null; + } +} diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/util/PositionalParamsRetriever.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/util/PositionalParamsRetriever.java new file mode 100644 index 0000000000..700aa31acd --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/util/PositionalParamsRetriever.java @@ -0,0 +1,1340 @@ +package com.thetransactioncompany.jsonrpc2.util; + + +import java.util.List; +import java.util.Map; + +import com.thetransactioncompany.jsonrpc2.JSONRPC2Error; + + +/** + * Utility class for retrieving JSON-RPC 2.0 positional parameters (packed into + * a JSON Array). + * + *

Provides a set of getter methods according to the expected parameter type + * (number, string, etc.) and whether the parameter is mandatory or optional: + * + *

    + *
  • {@code getXXX(param_pos)} for mandatory parameters, where + * {@code XXX} is the expected parameter type. + *
  • {@code getOptXXX(param_pos, default_value)} for optional parameters, + * specifying a default value. + *
+ * + *

There are also generic getter methods that let you do the type conversion + * yourself. + * + *

If a parameter cannot be retrieved, e.g. due to a missing mandatory + * parameter or bad type, a + * {@link com.thetransactioncompany.jsonrpc2.JSONRPC2Error#INVALID_PARAMS} + * exception is thrown. + * + *

Example: suppose you have a method with 3 positional parameters where the + * first two are mandatory and the last is optional and has a default value of + * {@code true}. + * + *

+ * // Parse received request string
+ * JSONRPC2Request request = null;
+ *
+ * try {
+ *         request = JSONRPC2Request.parse(jsonString);
+ * } catch (JSONRPC2ParseException e) {
+ *         // handle exception...
+ * }
+ *
+ * // Create a new retriever for positional parameters
+ * List params = (List)request.getParams();
+ * PositionalParamsRetriever r = new PositionalParamsRetriever(params);
+ *
+ * try {
+ *         // Extract first mandatory string parameter
+ *         String param1 = r.getString(0);
+ *
+ *         // Extract second integer parameter
+ *         int param2 = r.getInt(1);
+ *
+ *         // Extract third optional boolean parameter which defaults to true
+ *         boolean param3 = r.getOptBoolean(2, true);
+ *
+ * } catch (JSONRPC2Error e) {
+ *         // A JSONRPC2Error.INVALID_PARAMS will be thrown to indicate
+ *         // an unexpected parameter type or a missing mandatory parameter.
+ *         // You can use it straight away to create the appropriate
+ *         // JSON-RPC 2.0 error response.
+ *         JSONRPC2Response response = new JSONRPC2Response(e, null);
+ * }
+ * 
+ * + * @author Vladimir Dzhuvinov + */ +public class PositionalParamsRetriever + extends ParamsRetriever { + + + /** + * The positional parameters interface. + */ + private List params = null; + + + /** + * Throws a JSON-RPC 2.0 error indicating a missing positional + * parameter. + * + * @param position The parameter position. Should be non-negative. + * + * @throws JSONRPC2Error Formatted JSON-RPC 2.0 error. + */ + private static void throwMissingParameterException(final int position) + throws JSONRPC2Error { + + throw JSONRPC2Error.INVALID_PARAMS. + appendMessage(": Missing parameter at position " + position); + } + + + /** + * Throws a JSON-RPC 2.0 error indicating a positional parameter with + * unexpected {@code null} value. + * + * @param position The parameter position. Should be non-negative. + * + * @throws JSONRPC2Error Formatted JSON-RPC 2.0 error. + */ + private static void throwNullParameterException(final int position) + throws JSONRPC2Error { + + throw JSONRPC2Error.INVALID_PARAMS. + appendMessage(": Parameter at position " + position + " must not be null"); + } + + + /** + * Throws a JSON-RPC 2.0 error indicating a positional parameter with + * an unexpected enumerated value. + * + * @param position The parameter position. Should be non-negative. + * @param enumStrings The acceptable string values. Must not be + * {@code null}. + * + * @throws JSONRPC2Error Formatted JSON-RPC 2.0 error. + */ + private static void throwEnumParameterException(final int position, + final String[] enumStrings) + throws JSONRPC2Error { + + StringBuilder msg = new StringBuilder(": Enumerated parameter at position " + + position + "\" must have values "); + + for (int i=0; i < enumStrings.length; i++) { + + if (i > 0 && i == enumStrings.length - 1) + msg.append(" or "); + + else if (i > 0) + msg.append(", "); + + msg.append('"'); + msg.append(enumStrings[i]); + msg.append('"'); + } + + throw JSONRPC2Error.INVALID_PARAMS.appendMessage(msg.toString()); + } + + + /** + * Throws a JSON-RPC 2.0 error indicating a positional parameter with + * an unexpected enumerated value. + * + * @param position The parameter position. Should be non-negative. + * @param enumClass The enumeration class specifying the acceptable + * string values. Must not be {@code null}. + * + * @throws JSONRPC2Error Formatted JSON-RPC 2.0 error. + */ + private static > void throwEnumParameterException(final int position, + final Class enumClass) + throws JSONRPC2Error { + + StringBuilder msg = new StringBuilder(": Enumerated parameter at position " + + position + " must have values "); + + T[] constants = enumClass.getEnumConstants(); + + for (int i = 0; i < constants.length; i++) { + + if (i > 0 && i == constants.length - 1) + msg.append(" or "); + + else if (i > 0) + msg.append(", "); + + msg.append('"'); + msg.append(constants[i].toString()); + msg.append('"'); + } + + throw JSONRPC2Error.INVALID_PARAMS.appendMessage(msg.toString()); + } + + + /** + * Creates a JSON-RPC 2.0 error indicating a positional parameter with + * an unexpected JSON type. + * + * @param position The parameter position. Should be non-negative. + * + * @return Formatted JSON-RPC 2.0 error. + */ + private static JSONRPC2Error newUnexpectedParameterTypeException(final int position) { + + return JSONRPC2Error.INVALID_PARAMS. + appendMessage(": Parameter at position " + position + " has an unexpected JSON type"); + } + + + /** + * Creates a JSON-RPC 2.0 error indicating an array exception. + * + * @param position The parameter position. Should be non-negative. + * + * @return Formatted JSON-RPC 2.0 error. + */ + private static JSONRPC2Error newArrayException(final int position) { + + return JSONRPC2Error.INVALID_PARAMS. + appendMessage(": Parameter at position " + position + " caused an array exception"); + } + + + /** + * Creates a new positional parameters retriever from the specified + * value list. + * + * @param params The positional parameters list. Must not be + * {@code null}. + */ + public PositionalParamsRetriever(final List params) { + + if (params == null) + throw new IllegalArgumentException("The parameters list must not be null"); + + this.params = params; + } + + + /** + * Gets the positional parameters for this retriever. + * + * @return The positional parameters. + */ + public List getParams() { + + return params; + } + + + @Override + public int size() { + + return params.size(); + } + + + /** + * Returns {@code true} a parameter at the specified position exists, + * else {@code false}. + * + * @param position The parameter position. + * + * @return {@code true} if the parameter exists, else {@code false}. + */ + public boolean hasParam(final int position) { + + return position < params.size(); + } + + + /** + * @see #hasParam + */ + @Deprecated + public boolean hasParameter(final int position) { + + return hasParam(position); + } + + + /** + * Throws a {@code JSONRPC2Error.INVALID_PARAMS} exception if there is + * no parameter at the specified position. + * + *

You may use this method to fire the proper JSON-RPC 2.0 error + * on a missing mandatory parameter. + * + * @param position The parameter position, starting with zero for the + * first. + * + * @throws JSONRPC2Error On a missing parameter + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public void ensureParam(final int position) + throws JSONRPC2Error { + + if (position >= params.size() ) + throwMissingParameterException(position); + } + + + /** + * @see #ensureParam + */ + @Deprecated + public void ensureParameter(final int position) + throws JSONRPC2Error { + + ensureParam(position); + } + + + /** + * Throws a {@code JSONRPC2Error.INVALID_PARAMS} exception if there is + * no parameter at the specified position, its value is {@code null}, + * or its type doesn't map to the specified. + * + *

You may use this method to fire the proper JSON-RPC 2.0 error + * on a missing or badly-typed mandatory parameter. + * + * @param position The parameter position. + * @param clazz The corresponding Java class that the parameter + * should map to (any one of the return types of the + * {@code getXXX()} getter methods. Set to + * {@code Object.class} to allow any type. Must not be + * {@code null}. + * + * @throws JSONRPC2Error On a missing parameter, {@code null} value or + * bad type ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public void ensureParam(final int position, final Class clazz) + throws JSONRPC2Error { + + ensureParameter(position, clazz, false); + } + + + /** + * @see #ensureParam(int, Class) + */ + @Deprecated + public void ensureParameter(final int position, final Class clazz) + throws JSONRPC2Error { + + ensureParam(position, clazz); + } + + + /** + * Throws a {@code JSONRPC2Error.INVALID_PARAMS} exception if there is + * no parameter at the specified position or its type doesn't map to + * the specified. + * + *

You may use this method to fire the proper JSON-RPC 2.0 error + * on a missing or badly-typed mandatory parameter. + * + * @param position The parameter position. + * @param clazz The corresponding Java class that the parameter + * should map to (any one of the return types of the + * {@code getXXX()} getter methods. Set to + * {@code Object.class} to allow any type. Must not be + * {@code null}. + * @param allowNull If {@code true} allows a {@code null} parameter + * value. + * + * @throws JSONRPC2Error On a missing parameter or bad type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public void ensureParam(final int position, + final Class clazz, + final boolean allowNull) + throws JSONRPC2Error { + + // First, check existence only + ensureParameter(position); + + // Now check type + Object value = params.get(position); + + if (value == null) { + + if (allowNull) + return; // ok + else + throwNullParameterException(position); + } + + if (! clazz.isAssignableFrom(value.getClass())) + throw newUnexpectedParameterTypeException(position); + } + + + /** + * @see #ensureParam(int, Class, boolean) + */ + @Deprecated + public void ensureParameter(final int position, + final Class clazz, + final boolean allowNull) + throws JSONRPC2Error { + + ensureParam(position, clazz, allowNull); + } + + + /** + * Retrieves the specified parameter which can be of any type. Use this + * generic getter if you want to cast the value yourself. Otherwise + * look at the typed {@code get*} methods. + * + * @param position The parameter position. + * + * @return The parameter value. + * + * @throws JSONRPC2Error On a missing parameter + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public Object get(final int position) + throws JSONRPC2Error { + + ensureParameter(position); + + return params.get(position); + } + + + /** + * Retrieves the specified parameter which must map to the provided + * class (use the appropriate wrapper class for primitive types). + * + * @param position The parameter position. + * @param clazz The corresponding Java class that the parameter + * should map to (any one of the return types of the + * {@code getXXX()} getter methods. Set to + * {@code Object.class} to allow any type. Must not be + * {@code null}. + * + * @return The parameter value. + * + * @throws JSONRPC2Error On a missing parameter, {@code null} value or + * bad type ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public T get(final int position, final Class clazz) + throws JSONRPC2Error { + + return get(position, clazz, false); + } + + + /** + * Retrieves the specified parameter which must map to the provided + * class (use the appropriate wrapper class for primitive types). + * + * @param position The parameter position. + * @param clazz The corresponding Java class that the parameter + * should map to (any one of the return types of the + * {@code getXXX()} getter methods. Set to + * {@code Object.class} to allow any type. Must not be + * {@code null}. + * @param allowNull If {@code true} allows a {@code null} parameter + * value. + * + * @return The parameter value. + * + * @throws JSONRPC2Error On a missing parameter or bad type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + @SuppressWarnings("unchecked") + public T get(final int position, final Class clazz, final boolean allowNull) + throws JSONRPC2Error { + + ensureParameter(position, clazz, allowNull); + + try { + return (T)params.get(position); + + } catch (ClassCastException e) { + + throw newUnexpectedParameterTypeException(position); + } + } + + + /** + * Retrieves the specified optional parameter which must map to the + * provided class (use the appropriate wrapper class for primitive + * types). If the parameter doesn't exist the method returns the + * specified default value. + * + * @param position The parameter position. + * @param clazz The corresponding Java class that the parameter + * should map to (any one of the return types of + * the {@code getXXX()} getter methods. Set to + * {@code Object.class} to allow any type. Must not + * be {@code null}. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public T getOpt(final int position, final Class clazz, final T defaultValue) + throws JSONRPC2Error { + + return getOpt(position, clazz, false, defaultValue); + } + + + /** + * Retrieves the specified optional parameter which must map to the + * provided class (use the appropriate wrapper class for primitive + * types). If the parameter doesn't exist the method returns the + * specified default value. + * + * @param position The parameter position. + * @param clazz The corresponding Java class that the parameter + * should map to (any one of the return types of + * the {@code getXXX()} getter methods. Set to + * {@code Object.class} to allow any type. Must not + * be {@code null}. + * @param allowNull If {@code true} allows a {@code null} parameter + * value. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value. + * + * @throws JSONRPC2Error On a bad parameter type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + @SuppressWarnings("unchecked") + public T getOpt(final int position, + final Class clazz, + final boolean allowNull, + final T defaultValue) + throws JSONRPC2Error { + + if (! hasParameter(position)) + return defaultValue; + + ensureParameter(position, clazz, allowNull); + + try { + return (T)params.get(position); + + } catch (ClassCastException e) { + + throw newUnexpectedParameterTypeException(position); + } + } + + + /** + * Retrieves the specified string parameter. + * + * @param position The parameter position. + * + * @return The parameter value as a string. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String getString(final int position) + throws JSONRPC2Error { + + return getString(position, false); + } + + + /** + * Retrieves the specified string parameter. + * + * @param position The parameter position. + * @param allowNull If {@code true} allows a {@code null} value. + * + * @return The parameter value as a string. + * + * @throws JSONRPC2Error On a missing parameter or bad type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String getString(final int position, final boolean allowNull) + throws JSONRPC2Error { + + return get(position, String.class, allowNull); + } + + + /** + * Retrieves the specified optional string parameter. If it doesn't + * exist the method will return the specified default value. + * + * @param position The parameter position. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a string. + * + * @throws JSONRPC2Error On a bad type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String getOptString(final int position, final String defaultValue) + throws JSONRPC2Error { + + return getOptString(position, false, defaultValue); + } + + + /** + * Retrieves the specified optional string parameter. If it doesn't + * exist the method will return the specified default value. + * + * @param position The parameter position. + * @param allowNull If {@code true} allows a {@code null} value. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a string. + * + * @throws JSONRPC2Error On a bad type ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String getOptString(final int position, final boolean allowNull, final String defaultValue) + throws JSONRPC2Error { + + return getOpt(position, String.class, allowNull, defaultValue); + } + + + /** + * Retrieves the specified enumerated string parameter. + * + * @param position The parameter position. + * @param enumStrings The acceptable string values. Must not be + * {@code null}. + * + * @return The parameter value as a string. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * bad enumeration value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String getEnumString(final int position, final String[] enumStrings) + throws JSONRPC2Error { + + return getEnumString(position, enumStrings, false); + } + + + /** + * Retrieves the specified enumerated string parameter, allowing for a + * case insenstive match. + * + * @param position The parameter position. + * @param enumStrings The acceptable string values. Must not be + * {@code null}. + * @param ignoreCase {@code true} for a case insensitive match. + * + * @return The matching parameter value as a string. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * bad enumeration value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String getEnumString(final int position, + final String[] enumStrings, + final boolean ignoreCase) + throws JSONRPC2Error { + + String value = get(position, String.class); + + String match = getEnumStringMatch(value, enumStrings, ignoreCase); + + if (match == null) + throwEnumParameterException(position, enumStrings); + + return match; + } + + + /** + * Retrieves the specified optional enumerated string parameter. If it + * doesn't exist the method will return the specified default value. + * + * @param position The parameter position. + * @param enumStrings The acceptable string values. Must not be + * {@code null}. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a string. + * + * @throws JSONRPC2Error On a bad type or bad enumeration value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String getOptEnumString(final int position, + final String[] enumStrings, + final String defaultValue) + throws JSONRPC2Error { + + return getOptEnumString(position, enumStrings, defaultValue, false); + } + + + /** + * Retrieves the specified optional enumerated string parameter, + * allowing for a case insenstive match. If it doesn't exist the method + * will return the specified default value. + * + * @param position The parameter position. + * @param enumStrings The acceptable string values. Must not be + * {@code null}. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * @param ignoreCase {@code true} for a case insensitive match. + * + * @return The parameter value as a string. + * + * @throws JSONRPC2Error On a bad type or bad enumeration value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String getOptEnumString(final int position, + final String[] enumStrings, + final String defaultValue, + final boolean ignoreCase) + throws JSONRPC2Error { + + String value = getOpt(position, String.class, defaultValue); + + if (defaultValue == null && value == null) + return null; + + String match = getEnumStringMatch(value, enumStrings, ignoreCase); + + if (match == null) + throwEnumParameterException(position, enumStrings); + + return match; + } + + + /** + * Retrieves the specified enumerated parameter (from a JSON string + * that has a predefined set of possible values). + * + * @param position The parameter position. + * @param enumClass An enumeration type with constant names + * representing the acceptable string values. Must not + * be {@code null}. + * + * @return The matching enumeration constant. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * bad enumeration value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public > T getEnum(final int position, final Class enumClass) + throws JSONRPC2Error { + + return getEnum(position, enumClass, false); + } + + + /** + * Retrieves the specified enumerated parameter (from a JSON string + * that has a predefined set of possible values), allowing for a case + * insensitive match. + * + * @param position The parameter position. + * @param enumClass An enumeration type with constant names + * representing the acceptable string values. Must + * not be {@code null}. + * @param ignoreCase If {@code true} a case insensitive match against + * the acceptable constant names is performed. + * + * @return The matching enumeration constant. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * bad enumeration value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public > T getEnum(final int position, + final Class enumClass, + final boolean ignoreCase) + throws JSONRPC2Error { + + String value = get(position, String.class); + + T match = getEnumStringMatch(value, enumClass, ignoreCase); + + if (match == null) + throwEnumParameterException(position, enumClass); + + return match; + } + + + /** + * Retrieves the specified optional enumerated parameter (from a JSON + * string that has a predefined set of possible values). If it doesn't + * exist the method will return the specified default value. + * + * @param position The parameter position. + * @param enumClass An enumeration type with constant names + * representing the acceptable string values. Must + * not be {@code null}. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The matching enumeration constant. + * + * @throws JSONRPC2Error On a bad type or bad enumeration value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public > T getOptEnum(final int position, + final Class enumClass, + final String defaultValue) + throws JSONRPC2Error { + + return getOptEnum(position, enumClass, defaultValue, false); + } + + + /** + * Retrieves the specified optional enumerated parameter (from a JSON + * string that has a predefined set of possible values), allowing for a + * case insenstive match. If it doesn't exist the method will return + * the specified default value. + * + * @param position The parameter position. + * @param enumClass An enumeration type with constant names + * representing the acceptable string values. Must + * not be {@code null}. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * @param ignoreCase If {@code true} a case insensitive match against + * the acceptable constant names is performed. + * + * @return The matching enumeration constant. + * + * @throws JSONRPC2Error On a bad type or bad enumeration value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public > T getOptEnum(final int position, + final Class enumClass, + final String defaultValue, + final boolean ignoreCase) + throws JSONRPC2Error { + + String value = getOpt(position, String.class, defaultValue); + + if (defaultValue == null && value == null) + return null; + + T match = getEnumStringMatch(value, enumClass, ignoreCase); + + if (match == null) + throwEnumParameterException(position, enumClass); + + return match; + } + + + /** + * Retrieves the specified boolean (maps from JSON true/false) + * parameter. + * + * @param position The parameter position. + * + * @return The parameter value as a {@code boolean}. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public boolean getBoolean(final int position) + throws JSONRPC2Error { + + return get(position, Boolean.class); + } + + + /** + * Retrieves the specified optional boolean (maps from JSON true/false) + * parameter. If it doesn't exist the method will return the specified + * default value. + * + * @param position The parameter position. + * @param defaultValue The default return value if the parameter + * doesn't exist. + * + * @return The parameter value as a {@code boolean}. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public boolean getOptBoolean(final int position, final boolean defaultValue) + throws JSONRPC2Error { + + return getOpt(position, Boolean.class, defaultValue); + } + + + /** + * Retrieves the specified numeric parameter as an {@code int}. + * + * @param position The parameter position. + * + * @return The parameter value as an {@code int}. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public int getInt(final int position) + throws JSONRPC2Error { + + Number number = get(position, Number.class); + return number.intValue(); + } + + + /** + * Retrieves the specified optional numeric parameter as an + * {@code int}. If it doesn't exist the method will return the + * specified default value. + * + * @param position The parameter position. + * @param defaultValue The default return value if the parameter + * doesn't exist. + * + * @return The parameter value as an {@code int}. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public int getOptInt(final int position, final int defaultValue) + throws JSONRPC2Error { + + Number number = getOpt(position, Number.class, defaultValue); + return number.intValue(); + } + + + /** + * Retrieves the specified numeric parameter as a {@code long}. + * + * @param position The parameter position. + * + * @return The parameter value as a {@code long}. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public long getLong(final int position) + throws JSONRPC2Error { + + Number number = get(position, Number.class); + return number.longValue(); + } + + + /** + * Retrieves the specified optional numeric parameter as a + * {@code long}. If it doesn't exist the method will return the + * specified default value. + * + * @param position The parameter position. + * @param defaultValue The default return value if the parameter + * doesn't exist. + * + * @return The parameter value as a {@code long}. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public long getOptLong(final int position, final long defaultValue) + throws JSONRPC2Error { + + Number number = getOpt(position, Number.class, defaultValue); + return number.longValue(); + } + + + /** + * Retrieves the specified numeric parameter as a {@code float}. + * + * @param position The parameter position. + * + * @return The parameter value as a {@code float}. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public float getFloat(final int position) + throws JSONRPC2Error { + + Number number = get(position, Number.class); + return number.floatValue(); + } + + + /** + * Retrieves the specified optional numeric parameter as a + * {@code float}. If it doesn't exist the method will return the + * specified default value. + * + * @param position The parameter position. + * @param defaultValue The default return value if the parameter + * doesn't exist. + * + * @return The parameter value as a {@code float}. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public float getOptFloat(final int position, final float defaultValue) + throws JSONRPC2Error { + + Number number = getOpt(position, Number.class, defaultValue); + return number.floatValue(); + } + + + /** + * Retrieves the specified numeric parameter as a {@code double}. + * + * @param position The parameter position. + * + * @return The parameter value as a {@code double}. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public double getDouble(final int position) + throws JSONRPC2Error { + + Number number = get(position, Number.class); + return number.doubleValue(); + } + + + /** + * Retrieves the specified optional numeric parameter as a + * {@code double}. If it doesn't exist the method will return the + * specified default value. + * + * @param position The parameter position. + * @param defaultValue The default return value if the parameter + * doesn't exist. + * + * @return The parameter value as a {@code double}. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public double getOptDouble(final int position, final double defaultValue) + throws JSONRPC2Error { + + Number number = getOpt(position, Number.class, defaultValue); + return number.doubleValue(); + } + + + /** + * Retrieves the specified list (maps from JSON array) parameter. + * + * @param position The parameter position. + * + * @return The parameter value as a list. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public List getList(final int position) + throws JSONRPC2Error { + + final boolean allowNull = false; + + return getList(position, allowNull); + } + + + /** + * Retrieves the specified list (maps from JSON array) parameter. + * + * @param position The parameter position. + * @param allowNull If {@code true} allows a {@code null} value. + * + * @return The parameter value as a list. + * + * @throws JSONRPC2Error On a missing parameter or bad type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + @SuppressWarnings("unchecked") + public List getList(final int position, final boolean allowNull) + throws JSONRPC2Error { + + return (List)get(position, List.class, allowNull); + } + + + /** + * Retrieves the specified optional list (maps from JSON array) + * parameter. If it doesn't exist the method will return the specified + * default value. + * + * @param position The parameter position. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a list. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public List getOptList(final int position, final List defaultValue) + throws JSONRPC2Error { + + final boolean allowNull = false; + + return getOptList(position, allowNull, defaultValue); + } + + + /** + * Retrieves the specified optional list (maps from JSON array) + * parameter. If it doesn't exist the method will return the specified + * default value. + * + * @param position The parameter position. + * @param allowNull If {@code true} allows a {@code null} value. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a list. + * + * @throws JSONRPC2Error On a bad parameter type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + @SuppressWarnings("unchecked") + public List getOptList(final int position, + final boolean allowNull, + final List defaultValue) + throws JSONRPC2Error { + + return (List)getOpt(position, List.class, allowNull, defaultValue); + } + + + /** + * Retrieves the specified string array (maps from JSON array of + * strings) parameter. + * + * @param position The parameter position. + * + * @return The parameter value as a string array. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String[] getStringArray(final int position) + throws JSONRPC2Error { + + final boolean allowNull = false; + + return getStringArray(position, allowNull); + } + + + /** + * Retrieves the specified string array (maps from JSON array of + * strings) parameter. + * + * @param position The parameter position. + * @param allowNull If {@code true} allows a {@code null} value. + * + * @return The parameter value as a string array. + * + * @throws JSONRPC2Error On a missing parameter or bad type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + @SuppressWarnings("unchecked") + public String[] getStringArray(final int position, final boolean allowNull) + throws JSONRPC2Error { + + List list = getList(position, allowNull); + + if (list == null) + return null; + + try { + return list.toArray(new String[list.size()]); + + } catch (ArrayStoreException e) { + + throw newArrayException(position); + } + } + + + /** + * Retrieves the specified optional string array (maps from JSON array + * of strings) parameter. If it doesn't exist the method will return + * the specified default value. + * + * @param position The parameter position. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a string array. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String[] getOptStringArray(final int position, final String[] defaultValue) + throws JSONRPC2Error { + + final boolean allowNull = false; + + return getOptStringArray(position, allowNull, defaultValue); + } + + + /** + * Retrieves the specified optional string array (maps from JSON array + * of strings) parameter. If it doesn't exist the method will return + * the specified default value. + * + * @param position The parameter position. + * @param allowNull If {@code true} allows a {@code null} value. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a string array. + * + * @throws JSONRPC2Error On a bad parameter type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public String[] getOptStringArray(final int position, + final boolean allowNull, + final String[] defaultValue) + throws JSONRPC2Error { + + if (! hasParameter(position)) + return defaultValue; + + return getStringArray(position, allowNull); + } + + + /** + * Retrieves the specified map (maps from JSON object) parameter. + * + * @param position The parameter position. + * + * @return The parameter value as a map. + * + * @throws JSONRPC2Error On a missing parameter, bad type or + * {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public Map getMap(final int position) + throws JSONRPC2Error { + + return getMap(position, false); + } + + + /** + * Retrieves the specified map (maps from JSON object) parameter. + * + * @param position The parameter position. + * @param allowNull If {@code true} allows a {@code null} value. + * + * @return The parameter value as a map. + * + * @throws JSONRPC2Error On a missing parameter or bad type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + @SuppressWarnings("unchecked") + public Map getMap(final int position, final boolean allowNull) + throws JSONRPC2Error { + + try { + return (Map)get(position, Map.class, allowNull); + + } catch (ClassCastException e) { + + throw newUnexpectedParameterTypeException(position); + } + } + + + /** + * Retrieves the specified optional map (maps from JSON object) + * parameter. If it doesn't exist the method will return the specified + * default value. + * + * @param position The parameter position. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a map. + * + * @throws JSONRPC2Error On a bad parameter type or {@code null} value + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + public Map getOptMap(final int position, final Map defaultValue) + throws JSONRPC2Error { + + return getOptMap(position, false, defaultValue); + } + + + /** + * Retrieves the specified optional map (maps from JSON object) + * parameter. If it doesn't exist the method will return the specified + * default value. + * + * @param position The parameter position. + * @param allowNull If {@code true} allows a {@code null} value. + * @param defaultValue The default return value if the parameter + * doesn't exist. May be {@code null}. + * + * @return The parameter value as a map. + * + * @throws JSONRPC2Error On a bad parameter type + * ({@link JSONRPC2Error#INVALID_PARAMS}). + */ + @SuppressWarnings("unchecked") + public Map getOptMap(final int position, + final boolean allowNull, + final Map defaultValue) + throws JSONRPC2Error { + + try { + return (Map)getOpt(position, Map.class, allowNull, defaultValue); + + } catch (ClassCastException e) { + + throw newUnexpectedParameterTypeException(position); + } + } +} diff --git a/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/util/package-info.java b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/util/package-info.java new file mode 100644 index 0000000000..70fa813c02 --- /dev/null +++ b/apps/i2pcontrol/java/com/thetransactioncompany/jsonrpc2/util/package-info.java @@ -0,0 +1,44 @@ +/** + * Utility classes for typed retrieval of JSON-RPC 2.0 request parameters on the + * server side. + * + *

The following parameter type conversion choices are available: + * + *

    + *
  • JSON true/false to Java {@code boolean} + *
  • JSON number to Java {@code int}, {@code long}, {@code float} or + * {@code double} + *
  • JSON string to {@code java.lang.String} + *
  • Predefined (enumerated) JSON string to a Java {@code enum} constant + * or {@code java.lang.String} + *
  • JSON array to Java {@code boolean[]}, {@code int[]}, {@code long[]}, + * {@code float[]}, {@code double[]} or {@code string[]} array, or + * to mixed type {@code java.util.List} + *
  • JSON object to {@code java.util.Map} + *
+ * + *

If a parameter cannot be retrieved, either because it's missing or + * is of the wrong type, a standard + * {@link com.thetransactioncompany.jsonrpc2.JSONRPC2Error#INVALID_PARAMS} + * exception is thrown. + * + *

There are two concrete classes: + * + *

    + *
  • The {@link com.thetransactioncompany.jsonrpc2.util.PositionalParamsRetriever} + * class is for extracting positional parameters (packed in a + * JSON array). + *
  • The {@link com.thetransactioncompany.jsonrpc2.util.NamedParamsRetriever} + * class is for extracting named parameters (packed in a JSON + * object). + *
+ * + * + *

Package dependencies: The classes in this package depend on the + * sister {@link com.thetransactioncompany.jsonrpc2} package. + * + * @author Vladimir Dzhuvinov + */ +package com.thetransactioncompany.jsonrpc2.util; + + diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/HostCheckHandler.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/HostCheckHandler.java new file mode 100644 index 0000000000..455547dd0d --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/HostCheckHandler.java @@ -0,0 +1,127 @@ +package net.i2p.i2pcontrol; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +import org.apache.http.conn.util.InetAddressUtils; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.HandlerWrapper; + +/** + * Block certain Host headers to prevent DNS rebinding attacks. + * + * This Handler wraps the ContextHandlerCollection, which handles + * all the webapps (not just routerconsole). + * Therefore, this protects all the webapps. + * + * @since 0.12 copied from routerconsole + */ +public class HostCheckHandler extends HandlerWrapper +{ + private final I2PAppContext _context; + private final Set _listenHosts; + + /** + * MUST call setListenHosts() afterwards. + */ + public HostCheckHandler(I2PAppContext ctx) { + super(); + _context = ctx; + _listenHosts = new HashSet(8); + } + + /** + * Set the legal hosts. + * Not synched. Call this BEFORE starting. + * If empty, all are allowed. + * + * @param hosts contains hostnames or IPs. But we allow all IPs anyway. + */ + public void setListenHosts(Set hosts) { + _listenHosts.clear(); + _listenHosts.addAll(hosts); + } + + /** + * Block by Host header, pass everything else to the delegate. + */ + public void handle(String pathInContext, + Request baseRequest, + HttpServletRequest httpRequest, + HttpServletResponse httpResponse) + throws IOException, ServletException + { + + String host = httpRequest.getHeader("Host"); + if (!allowHost(host)) { + Log log = _context.logManager().getLog(HostCheckHandler.class); + host = DataHelper.stripHTML(getHost(host)); + String s = "Console request denied.\n" + + " To allow access using the hostname \"" + host + "\", add the line \"" + + I2PControlController.PROP_ALLOWED_HOSTS + '=' + host + + "\" to I2PControl.conf and restart."; + log.logAlways(Log.WARN, s); + httpResponse.sendError(403, s); + return; + } + + super.handle(pathInContext, baseRequest, httpRequest, httpResponse); + } + + /** + * Should we allow a request with this Host header? + * + * ref: https://en.wikipedia.org/wiki/DNS_rebinding + * + * @param host the HTTP Host header, null ok + * @return true if OK + */ + private boolean allowHost(String host) { + if (host == null) + return true; + // common cases + if (host.equals("127.0.0.1:7650") || + host.equals("localhost:7650")) + return true; + // all allowed? + if (_listenHosts.isEmpty()) + return true; + host = getHost(host); + if (_listenHosts.contains(host)) + return true; + // allow all IP addresses + if (InetAddressUtils.isIPv4Address(host) || InetAddressUtils.isIPv6Address(host)) + return true; + //System.out.println(host + " not found in " + s); + return false; + } + + /** + * Strip [] and port from a host header + * + * @param host the HTTP Host header non-null + */ + private static String getHost(String host) { + if (host.startsWith("[")) { + host = host.substring(1); + int brack = host.indexOf(']'); + if (brack >= 0) + host = host.substring(0, brack); + } else { + int colon = host.indexOf(':'); + if (colon >= 0) + host = host.substring(0, colon); + } + return host; + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/I2PControlController.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/I2PControlController.java new file mode 100644 index 0000000000..a70f449e63 --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/I2PControlController.java @@ -0,0 +1,403 @@ +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 net.i2p.I2PAppContext; +import net.i2p.app.ClientAppManager; +import net.i2p.app.ClientAppState; +import static net.i2p.app.ClientAppState.*; +import net.i2p.router.RouterContext; +import net.i2p.router.app.RouterApp; +import net.i2p.util.I2PSSLSocketFactory; +import net.i2p.util.Log; +import net.i2p.util.PortMapper; + +import net.i2p.i2pcontrol.security.KeyStoreProvider; +import net.i2p.i2pcontrol.security.SecurityManager; +import net.i2p.i2pcontrol.servlets.JSONRPC2Servlet; +import net.i2p.i2pcontrol.servlets.configuration.ConfigurationManager; + +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import java.io.File; +import java.io.IOException; +import java.net.UnknownHostException; +import java.security.KeyStore; +import java.util.HashSet; +import java.util.Set; +import java.util.StringTokenizer; + + +/** + * This handles the starting and stopping of Jetty + * from a single static class so it can be called via clients.config. + * + * This makes installation of a new eepsite a turnkey operation. + * + * Usage: I2PControlController -d $PLUGIN [start|stop] + * + * @author hottuna + */ +public class I2PControlController implements RouterApp { + // non-null + private final I2PAppContext _appContext; + // warning, null in app context + private final RouterContext _context; + private final ClientAppManager _mgr; + private final Log _log; + private final String _pluginDir; + private final ConfigurationManager _conf; + private final KeyStoreProvider _ksp; + private final SecurityManager _secMan; + private final Server _server; + private ClientAppState _state = UNINITIALIZED; + // only for main() + private static I2PControlController _instance; + static final String PROP_ALLOWED_HOSTS = "i2pcontrol.allowedhosts"; + private static final String SVC_HTTPS_I2PCONTROL = "https_i2pcontrol"; + + /** + * RouterApp (new way) + */ + public I2PControlController(RouterContext ctx, ClientAppManager mgr, String args[]) { + _appContext = _context = ctx; + _mgr = mgr; + _log = _appContext.logManager().getLog(I2PControlController.class); + File pluginDir = new File(_context.getAppDir(), "plugins/I2PControl"); + _pluginDir = pluginDir.getAbsolutePath(); + _conf = new ConfigurationManager(_appContext, pluginDir, true); + _ksp = new KeyStoreProvider(_pluginDir); + _secMan = new SecurityManager(_appContext, _ksp, _conf); + _server = buildServer(); + _state = INITIALIZED; + } + + /** + * From main() (old way) + */ + public I2PControlController(File pluginDir) { + _appContext = I2PAppContext.getGlobalContext(); + if (_appContext instanceof RouterContext) + _context = (RouterContext) _appContext; + else + _context = null; + _mgr = null; + _log = _appContext.logManager().getLog(I2PControlController.class); + _pluginDir = pluginDir.getAbsolutePath(); + _conf = new ConfigurationManager(_appContext, pluginDir, true); + _ksp = new KeyStoreProvider(_pluginDir); + _secMan = new SecurityManager(_appContext, _ksp, _conf); + _server = buildServer(); + _state = INITIALIZED; + } + + /////// ClientApp methods + + public synchronized void startup() { + changeState(STARTING); + try { + start(null); + changeState(RUNNING); + } catch (Exception e) { + changeState(START_FAILED, "Failed to start", e); + _log.error("Unable to start jetty server", e); + stop(); + } + } + + public synchronized void shutdown(String[] args) { + if (_state == STOPPED) + return; + changeState(STOPPING); + stop(); + changeState(STOPPED); + } + + public synchronized ClientAppState getState() { + return _state; + } + + public String getName() { + return "I2PControl"; + } + + public String getDisplayName() { + return "I2PControl"; + } + + /////// end ClientApp methods + + private void changeState(ClientAppState state) { + changeState(state, null, null); + } + + private synchronized void changeState(ClientAppState state, String msg, Exception e) { + _state = state; + if (_mgr != null) + _mgr.notify(this, state, msg, e); + if (_context == null) { + if (msg != null) + System.out.println(state + ": " + msg); + if (e != null) + e.printStackTrace(); + } + } + + + /** + * Deprecated, use constructor + */ + public static void main(String args[]) { + if (args.length != 3 || (!"-d".equals(args[0]))) + throw new IllegalArgumentException("Usage: PluginController -d $PLUGINDIR [start|stop]"); + + if ("start".equals(args[2])) { + File pluginDir = new File(args[1]); + if (!pluginDir.exists()) + throw new IllegalArgumentException("Plugin directory " + pluginDir.getAbsolutePath() + " does not exist"); + synchronized(I2PControlController.class) { + if (_instance != null) + throw new IllegalStateException(); + I2PControlController i2pcc = new I2PControlController(pluginDir); + try { + i2pcc.startup(); + _instance = i2pcc; + } catch (Exception e) { + e.printStackTrace(); + } + } + } else if ("stop".equals(args[2])) { + synchronized(I2PControlController.class) { + if (_instance != null) { + _instance.shutdown(null); + _instance = null; + } + } + } else { + throw new IllegalArgumentException("Usage: PluginController -d $PLUGINDIR [start|stop]"); + } + } + + + private synchronized void start(String args[]) throws Exception { + _appContext.logManager().getLog(JSONRPC2Servlet.class).setMinimumPriority(Log.DEBUG); + _server.start(); + _context.portMapper().register(SVC_HTTPS_I2PCONTROL, + _conf.getConf("i2pcontrol.listen.address", "127.0.0.1"), + _conf.getConf("i2pcontrol.listen.port", 7650)); + } + + + + /** + * Builds a new server. Used for changing ports during operation and such. + * @return Server - A new server built from current configuration. + */ + private Connector buildDefaultListener(Server server) { + Connector ssl = buildSslListener(server, _conf.getConf("i2pcontrol.listen.address", "127.0.0.1"), + _conf.getConf("i2pcontrol.listen.port", 7650)); + return ssl; + } + + + /** + * Builds a new server. Used for changing ports during operation and such. + * + * Does NOT start the server. Must call start() on the returned server. + * + * @return Server - A new server built from current configuration. + */ + public Server buildServer() { + Server server = new Server(); + Connector ssl = buildDefaultListener(server); + server.addConnector(ssl); + + ServletHandler sh = new ServletHandler(); + sh.addServletWithMapping(new ServletHolder(new JSONRPC2Servlet(_context, _secMan)), "/"); + HostCheckHandler hch = new HostCheckHandler(_appContext); + Set listenHosts = new HashSet(8); + // fix up the allowed hosts set (see HostCheckHandler) + // empty set says all are valid + String address = _conf.getConf("i2pcontrol.listen.address", "127.0.0.1"); + if (!(address.equals("0.0.0.0") || + address.equals("::") || + address.equals("0:0:0:0:0:0:0:0"))) { + listenHosts.add("localhost"); + listenHosts.add("127.0.0.1"); + listenHosts.add("::1"); + listenHosts.add("0:0:0:0:0:0:0:1"); + String allowed = _conf.getConf(PROP_ALLOWED_HOSTS, ""); + if (!allowed.equals("")) { + StringTokenizer tok = new StringTokenizer(allowed, " ,"); + while (tok.hasMoreTokens()) { + listenHosts.add(tok.nextToken()); + } + } + } + hch.setListenHosts(listenHosts); + hch.setHandler(sh); + server.getServer().setHandler(hch); + + _conf.writeConfFile(); + return server; + } + + + /** + * Creates a SSLListener with all the default options. The listener will use all the default options. + * @param address - The address the listener will listen to. + * @param port - The port the listener will listen to. + * @return - Newly created listener + */ + private Connector buildSslListener(Server server, String address, int port) { + int listeners = 0; + if (server != null) { + listeners = server.getConnectors().length; + } + + // the keystore path and password + SslContextFactory sslFactory = new SslContextFactory(_ksp.getKeyStoreLocation()); + sslFactory.setKeyStorePassword(KeyStoreProvider.DEFAULT_KEYSTORE_PASSWORD); + // the X.509 cert password (if not present, verifyKeyStore() returned false) + sslFactory.setKeyManagerPassword(KeyStoreProvider.DEFAULT_CERTIFICATE_PASSWORD); + sslFactory.addExcludeProtocols(I2PSSLSocketFactory.EXCLUDE_PROTOCOLS.toArray( + new String[I2PSSLSocketFactory.EXCLUDE_PROTOCOLS.size()])); + sslFactory.addExcludeCipherSuites(I2PSSLSocketFactory.EXCLUDE_CIPHERS.toArray( + new String[I2PSSLSocketFactory.EXCLUDE_CIPHERS.size()])); + + HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setSecureScheme("https"); + httpConfig.setSecurePort(port); + httpConfig.addCustomizer(new SecureRequestCustomizer()); + // number of acceptors, (default) number of selectors + ServerConnector ssl = new ServerConnector(server, 1, 0, + new SslConnectionFactory(sslFactory, "http/1.1"), + new HttpConnectionFactory(httpConfig)); + ssl.setHost(address); + ssl.setPort(port); + ssl.setIdleTimeout(90*1000); // default 10 sec + // all with same name will use the same thread pool + ssl.setName("I2PControl"); + + ssl.setName("SSL Listener-" + ++listeners); + + return ssl; + } + + + /** + * Add a listener to the server + * If a listener listening to the same port as the provided listener + * uses already exists within the server, replace the one already used by + * the server with the provided listener. + * @param listener + * @throws Exception + */ +/**** + public synchronized void replaceListener(Connector listener) throws Exception { + if (_server != null) { + stopServer(); + } + _server = buildServer(listener); + } +****/ + + /** + * Get all listeners of the server. + * @return + */ +/**** + public synchronized Connector[] getListeners() { + if (_server != null) { + return _server.getConnectors(); + } + return new Connector[0]; + } +****/ + + /** + * Removes all listeners + */ +/**** + public synchronized void clearListeners() { + if (_server != null) { + for (Connector listen : getListeners()) { + _server.removeConnector(listen); + } + } + } +****/ + + /** + * Stop it + */ + private synchronized void stopServer() + { + try { + if (_server != null) { + _appContext.portMapper().unregister(SVC_HTTPS_I2PCONTROL); + _server.stop(); + for (Connector listener : _server.getConnectors()) { + listener.stop(); + } + _server.destroy(); + } + } catch (Exception e) { + _log.error("Stopping server", e); + } + } + + private synchronized void stop() { + _conf.writeConfFile(); + _secMan.stopTimedEvents(); + stopServer(); + +/**** + // Get and stop all running threads + ThreadGroup threadgroup = Thread.currentThread().getThreadGroup(); + Thread[] threads = new Thread[threadgroup.activeCount() + 3]; + threadgroup.enumerate(threads, true); + for (Thread thread : threads) { + if (thread != null) {//&& thread.isAlive()){ + thread.interrupt(); + } + } + + for (Thread thread : threads) { + if (thread != null) { + System.out.println("Active thread: " + thread.getName()); + } + } + threadgroup.interrupt(); + + //Thread.currentThread().getThreadGroup().destroy(); +****/ + } + + public String getPluginDir() { + return _pluginDir; + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/I2PControlVersion.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/I2PControlVersion.java new file mode 100644 index 0000000000..513362fc0f --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/I2PControlVersion.java @@ -0,0 +1,20 @@ +package net.i2p.i2pcontrol; + +import java.util.HashSet; +import java.util.Set; + +public class I2PControlVersion { + /** The current version of I2PControl */ + public final static String VERSION = "0.12.0"; + + /** The current version of the I2PControl API being primarily being implemented */ + public final static int API_VERSION = 1; + + /** The supported versions of the I2PControl API */ + public final static Set SUPPORTED_API_VERSIONS; + + static { + SUPPORTED_API_VERSIONS = new HashSet(); + SUPPORTED_API_VERSIONS.add(1); + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/security/AuthToken.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/security/AuthToken.java new file mode 100644 index 0000000000..083c940a95 --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/security/AuthToken.java @@ -0,0 +1,50 @@ +package net.i2p.i2pcontrol.security; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + + +public class AuthToken { + static final int VALIDITY_TIME = 1; // Measured in days + private final SecurityManager _secMan; + private final String id; + private final Date expiry; + + public AuthToken(SecurityManager secMan, String password) { + _secMan = secMan; + String hash = _secMan.getPasswdHash(password); + this.id = _secMan.getHash(hash + Calendar.getInstance().getTimeInMillis()); + Calendar expiry = Calendar.getInstance(); + expiry.add(Calendar.DAY_OF_YEAR, VALIDITY_TIME); + this.expiry = expiry.getTime(); + } + + public String getId() { + return id; + } + + /** + * Checks whether the AuthToken has expired. + * @return True if AuthToken hasn't expired. False in any other case. + */ + public boolean isValid() { + 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 + public String toString() { + return id; + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/security/ExpiredAuthTokenException.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/security/ExpiredAuthTokenException.java new file mode 100644 index 0000000000..02cd32b747 --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/security/ExpiredAuthTokenException.java @@ -0,0 +1,16 @@ +package net.i2p.i2pcontrol.security; + +public class ExpiredAuthTokenException extends Exception { + private static final long serialVersionUID = 2279019346592900289L; + + private String expiryTime; + + public ExpiredAuthTokenException(String str, String expiryTime) { + super(str); + this.expiryTime = expiryTime; + } + + public String getExpirytime() { + return expiryTime; + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/security/InvalidAuthTokenException.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/security/InvalidAuthTokenException.java new file mode 100644 index 0000000000..51319f5c56 --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/security/InvalidAuthTokenException.java @@ -0,0 +1,9 @@ +package net.i2p.i2pcontrol.security; + +public class InvalidAuthTokenException extends Exception { + private static final long serialVersionUID = 7605321329341235577L; + + public InvalidAuthTokenException(String str) { + super(str); + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/security/KeyStoreProvider.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/security/KeyStoreProvider.java new file mode 100644 index 0000000000..544f38b8fa --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/security/KeyStoreProvider.java @@ -0,0 +1,218 @@ +package net.i2p.i2pcontrol.security; + +import net.i2p.crypto.KeyStoreUtil; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +public class KeyStoreProvider { + public static final String DEFAULT_CERTIFICATE_ALGORITHM_STRING = "RSA"; + public static final int DEFAULT_CERTIFICATE_KEY_LENGTH = 4096; + public static final int DEFAULT_CERTIFICATE_VALIDITY = 365 * 10; + public final static String DEFAULT_CERTIFICATE_DOMAIN = "localhost"; + public final static String DEFAULT_CERTIFICATE_ALIAS = "I2PControl CA"; + public static final String DEFAULT_KEYSTORE_NAME = "i2pcontrol.ks"; + public static final String DEFAULT_KEYSTORE_PASSWORD = KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD; + public static final String DEFAULT_CERTIFICATE_PASSWORD = "nut'nfancy"; + private final String _pluginDir; + private KeyStore _keystore; + + public KeyStoreProvider(String pluginDir) { + _pluginDir = pluginDir; + } + + public void initialize() { + KeyStoreUtil.createKeys(new File(getKeyStoreLocation()), + DEFAULT_KEYSTORE_PASSWORD, + DEFAULT_CERTIFICATE_ALIAS, + DEFAULT_CERTIFICATE_DOMAIN, + "i2pcontrol", + DEFAULT_CERTIFICATE_VALIDITY, + DEFAULT_CERTIFICATE_ALGORITHM_STRING, + DEFAULT_CERTIFICATE_KEY_LENGTH, + DEFAULT_CERTIFICATE_PASSWORD); + } + + /** + * @param password unused + * @return null on failure + */ + public static X509Certificate readCert(KeyStore ks, String certAlias, String password) { + try { + X509Certificate cert = (X509Certificate) ks.getCertificate(certAlias); + + if (cert == null) { + throw new RuntimeException("Got null cert from keystore!"); + } + + try { + cert.verify(cert.getPublicKey()); + return cert; + } catch (Exception e) { + System.err.println("Failed to verify caCert certificate against caCert"); + e.printStackTrace(); + } + } catch (KeyStoreException e) { + e.printStackTrace(); + } + return null; + } + + /** + * @param password for the keystore + * @return null on failure + */ +/**** + public static X509Certificate readCert(File keyStoreFile, String certAlias, String password) { + try { + KeyStore ks = getDefaultKeyStore(); + ks.load(new FileInputStream(keyStoreFile), password.toCharArray()); + X509Certificate cert = (X509Certificate) ks.getCertificate(certAlias); + + if (cert == null) { + throw new RuntimeException("Got null cert from keystore!"); + } + + try { + cert.verify(cert.getPublicKey()); + return cert; + } catch (Exception e) { + System.err.println("Failed to verify caCert certificate against caCert"); + e.printStackTrace(); + } + } catch (IOException e) { + System.err.println("Couldn't read keystore from: " + keyStoreFile.toString()); + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (CertificateException e) { + e.printStackTrace(); + } catch (KeyStoreException e) { + System.err.println("No certificate with alias: " + certAlias + " found."); + e.printStackTrace(); + } + return null; + } +****/ + + /** + * @param password for the key + * @return null on failure, or throws RuntimeException... + */ +/**** + public static PrivateKey readPrivateKey(KeyStore ks, String alias, String password) { + try { + // load the key entry from the keystore + Key key = ks.getKey(alias, password.toCharArray()); + + if (key == null) { + throw new RuntimeException("Got null key from keystore!"); + } + + PrivateKey privKey = (PrivateKey) key; + return privKey; + } catch (UnrecoverableKeyException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (KeyStoreException e) { + e.printStackTrace(); + } + return null; + } +****/ + + /** + * @return null on failure + */ +/**** + public static PrivateKey readPrivateKey(String alias, File keyStoreFile, String keyStorePassword, String keyPassword) { + try { + KeyStore ks = getDefaultKeyStore(); + ks.load(new FileInputStream(keyStoreFile), keyStorePassword.toCharArray()); + return readPrivateKey(ks, alias, keyStorePassword); + } catch (CertificateException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + System.err.println("Couldn't read keystore from: " + keyStoreFile.toString()); + e.printStackTrace(); + } + return null; + } +****/ + + /** + * @return null on failure + */ +/**** + public static KeyStore writeCACertToKeyStore(KeyStore keyStore, String keyPassword, String alias, PrivateKey caPrivKey, X509Certificate caCert) { + try { + X509Certificate[] chain = new X509Certificate[1]; + chain[0] = caCert; + + keyStore.setKeyEntry(alias, caPrivKey, keyPassword.toCharArray(), chain); + File keyStoreFile = new File(I2PControlController.getPluginDir() + File.separator + DEFAULT_KEYSTORE_NAME); + keyStore.store(new FileOutputStream(keyStoreFile), DEFAULT_KEYSTORE_PASSWORD.toCharArray()); + return keyStore; + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (CertificateException e) { + e.printStackTrace(); + } catch (KeyStoreException e) { + e.printStackTrace(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } +****/ + + /** + * @return null on failure + */ + public synchronized KeyStore getDefaultKeyStore() { + if (_keystore == null) { + File keyStoreFile = new File(getKeyStoreLocation()); + + try { + _keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + if (keyStoreFile.exists()) { + InputStream is = new FileInputStream(keyStoreFile); + _keystore.load(is, DEFAULT_KEYSTORE_PASSWORD.toCharArray()); + return _keystore; + } + + initialize(); + if (keyStoreFile.exists()) { + InputStream is = new FileInputStream(keyStoreFile); + _keystore.load(is, DEFAULT_KEYSTORE_PASSWORD.toCharArray()); + return _keystore; + } else { + throw new IOException("KeyStore file " + keyStoreFile.getAbsolutePath() + " wasn't readable"); + } + } catch (Exception e) { + // Ignore. Not an issue. Let's just create a new keystore instead. + } + return null; + } else { + return _keystore; + } + } + + public String getKeyStoreLocation() { + File keyStoreFile = new File(_pluginDir, DEFAULT_KEYSTORE_NAME); + return keyStoreFile.getAbsolutePath(); + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/security/SecurityManager.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/security/SecurityManager.java new file mode 100644 index 0000000000..c47cfad2a5 --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/security/SecurityManager.java @@ -0,0 +1,252 @@ +package net.i2p.i2pcontrol.security; +/* + * Copyright 2011 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 net.i2p.I2PAppContext; +import net.i2p.crypto.SHA256Generator; +import net.i2p.data.Base64; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; +import net.i2p.util.SimpleTimer2; + +import org.mindrot.jbcrypt.BCrypt; + +import net.i2p.i2pcontrol.servlets.configuration.ConfigurationManager; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import java.security.KeyStore; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Iterator; + +/** + * Manage the password storing for I2PControl. + */ +public class SecurityManager { + public final static String DEFAULT_AUTH_PASSWORD = "itoopie"; + private final HashMap authTokens; + private final SimpleTimer2.TimedEvent timer; + private final KeyStore _ks; + private final Log _log; + private final ConfigurationManager _conf; + private final I2PAppContext _context; + + /** + * @param ksp may be null (if webapp) + */ + public SecurityManager(I2PAppContext ctx, KeyStoreProvider ksp, ConfigurationManager conf) { + _context = ctx; + _conf = conf; + _log = ctx.logManager().getLog(SecurityManager.class); + authTokens = new HashMap(); + + timer = new Sweeper(); + + _ks = ksp != null ? ksp.getDefaultKeyStore() : null; + } + + public void stopTimedEvents() { + timer.cancel(); + synchronized (authTokens) { + authTokens.clear(); + } + } + + /** + * Return the X509Certificate of the server as a Base64 encoded string. + * @return base64 encode of X509Certificate + */ +/**** unused and incorrectly uses I2P Base64. Switch to CertUtil.exportCert() if needed. + public String getBase64Cert() { + X509Certificate caCert = KeyStoreProvider.readCert(_ks, + KeyStoreProvider.DEFAULT_CERTIFICATE_ALIAS, + KeyStoreProvider.DEFAULT_KEYSTORE_PASSWORD); + return getBase64FromCert(caCert); + } +****/ + + /** + * Return the X509Certificate as a base64 encoded string. + * @param cert + * @return base64 encode of X509Certificate + */ +/**** unused and incorrectly uses I2P Base64. Switch to CertUtil.exportCert() if needed. + private static String getBase64FromCert(X509Certificate cert) { + try { + return Base64.encode(cert.getEncoded()); + } catch (CertificateEncodingException e) { + e.printStackTrace(); + } + return null; + } +****/ + + + + /** + * Hash pwd with using BCrypt with the default salt. + * @param pwd + * @return BCrypt hash of salt and input string + */ + public String getPasswdHash(String pwd) { + String salt; + synchronized(_conf) { + salt = _conf.getConf("auth.salt", ""); + if (salt.equals("")) { + salt = BCrypt.gensalt(10, _context.random()); + _conf.setConf("auth.salt", salt); + _conf.writeConfFile(); + } + } + return BCrypt.hashpw(pwd, salt); + } + + /** + * Get saved password hash. Stores if not previously set. + * @return BCrypt hash of salt and password + * @since 0.12 + */ + private String getSavedPasswdHash() { + String pw; + synchronized(_conf) { + pw = _conf.getConf("auth.password", ""); + if (pw.equals("")) { + pw = getPasswdHash(DEFAULT_AUTH_PASSWORD); + _conf.setConf("auth.password", pw); + _conf.writeConfFile(); + } + } + return pw; + } + + /** + * Hash input one time with SHA-256, return Base64 encdoded string. + * @param string + * @return Base64 encoded string + */ + public String getHash(String string) { + SHA256Generator hashGen = _context.sha(); + byte[] bytes = string.getBytes(); + bytes = hashGen.calculateHash(bytes).toByteArray(); + return Base64.encode(bytes); + } + + /** + * Is this password correct? + * @return true if password is valid. + * @since 0.12 + */ + public boolean isValid(String pwd) { + String storedPass = getSavedPasswdHash(); + byte[] p1 = DataHelper.getASCII(getPasswdHash(pwd)); + byte[] p2 = DataHelper.getASCII(storedPass); + return p1.length == p2.length && DataHelper.eqCT(p1, 0, p2, 0, p1.length); + } + + /** + * Is this password correct? + * @return true if password is valid. + * @since 0.12 + */ + public boolean isDefaultPasswordValid() { + return isValid(DEFAULT_AUTH_PASSWORD); + } + + /** + * Add a Authentication Token if the provided password is valid. + * The token will be valid for one day. + * @return AuthToken if password is valid. If password is invalid null will be returned. + */ + public AuthToken validatePasswd(String pwd) { + if (isValid(pwd)) { + AuthToken token = new AuthToken(this, pwd); + synchronized (authTokens) { + authTokens.put(token.getId(), token); + } + return token; + } else { + return null; + } + } + + /** + * Set new password. Old tokens will NOT remain valid, to encourage the new password being tested. + * @param newPasswd + * @return Returns true if a new password was set. + */ + public boolean setPasswd(String newPasswd) { + String newHash = getPasswdHash(newPasswd); + String oldHash = getSavedPasswdHash(); + + if (!newHash.equals(oldHash)) { + _conf.setConf("auth.password", newHash); + _conf.writeConfFile(); + synchronized (authTokens) { + authTokens.clear(); + } + return true; + } + return false; + } + + /** + * Checks whether the AuthToken with the given ID exists and if it does whether is has expired. + * @param tokenID - The token to validate + * @throws InvalidAuthTokenException + * @throws ExpiredAuthTokenException + */ + public void verifyToken(String tokenID) throws InvalidAuthTokenException, ExpiredAuthTokenException { + synchronized (authTokens) { + AuthToken token = authTokens.get(tokenID); + if (token == null) + throw new InvalidAuthTokenException("AuthToken with ID: " + tokenID + " couldn't be found."); + if (!token.isValid()) { + authTokens.remove(tokenID); + throw new ExpiredAuthTokenException("AuthToken with ID: " + tokenID + " expired " + token.getExpiryTime(), token.getExpiryTime()); + } + } + // Everything is fine. :) + } + + /** + * Clean up old authorization tokens to keep the token store slim and fit. + * @author hottuna + * + */ + private class Sweeper extends SimpleTimer2.TimedEvent { + // Start running periodic task after 1 day, run periodically every 30 minutes. + public Sweeper() { + super(_context.simpleTimer2(), AuthToken.VALIDITY_TIME * 24*60*60*1000L); + } + + public void timeReached() { + _log.debug("Starting cleanup job.."); + synchronized (authTokens) { + for (Iterator iter = authTokens.values().iterator(); iter.hasNext(); ) { + AuthToken token = iter.next(); + if (!token.isValid()) + iter.remove(); + } + } + _log.debug("Cleanup job done."); + schedule(30*60*1000L); + } + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/JSONRPC2Servlet.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/JSONRPC2Servlet.java new file mode 100644 index 0000000000..51e4976ac4 --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/JSONRPC2Servlet.java @@ -0,0 +1,245 @@ +package net.i2p.i2pcontrol.servlets; +/* + * Copyright 2011 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 com.thetransactioncompany.jsonrpc2.*; +import com.thetransactioncompany.jsonrpc2.server.Dispatcher; + +import net.i2p.I2PAppContext; +import net.i2p.router.RouterContext; +import net.i2p.util.Log; +import net.i2p.util.PortMapper; + +import net.i2p.i2pcontrol.I2PControlVersion; +import net.i2p.i2pcontrol.security.KeyStoreProvider; +import net.i2p.i2pcontrol.security.SecurityManager; +import net.i2p.i2pcontrol.servlets.jsonrpc2handlers.*; +import net.i2p.i2pcontrol.servlets.configuration.ConfigurationManager; + +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; + + +/** + * Provide an JSON-RPC 2.0 API for remote controlling of I2P + */ +public class JSONRPC2Servlet extends HttpServlet { + + private static final long serialVersionUID = -45075606818515212L; + private static final int BUFFER_LENGTH = 2048; + private static final String SVC_HTTP_I2PCONTROL = "http_i2pcontrol"; + private static final String SVC_HTTPS_I2PCONTROL = "https_i2pcontrol"; + private Dispatcher disp; + private Log _log; + private final SecurityManager _secMan; + private final ConfigurationManager _conf; + private final JSONRPC2Helper _helper; + private final RouterContext _context; + private final boolean _isWebapp; + private boolean _isHTTP, _isHTTPS; + + /** + * Webapp + */ + public JSONRPC2Servlet() { + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + if (!ctx.isRouterContext()) + throw new IllegalStateException(); + _context = (RouterContext) ctx; + File appDir = ctx.getAppDir(); + _conf = new ConfigurationManager(ctx, appDir, false); + // we don't really need a keystore + //File ksDir = new File(ctx.getConfigDir(), "keystore"); + //ksDir.mkDir(); + //KeyStoreProvider ksp = new KeyStoreProvider(ksDir.getAbsolutePath()); + //_secMan = new SecurityManager(ctx, ksp, _conf); + _secMan = new SecurityManager(ctx, null, _conf); + _helper = new JSONRPC2Helper(_secMan); + _log = ctx.logManager().getLog(JSONRPC2Servlet.class); + _conf.writeConfFile(); + _isWebapp = true; + } + + /** + * Plugin + */ + public JSONRPC2Servlet(RouterContext ctx, SecurityManager secMan) { + _context = ctx; + _secMan = secMan; + _helper = new JSONRPC2Helper(_secMan); + if (ctx != null) + _log = ctx.logManager().getLog(JSONRPC2Servlet.class); + else + _log = I2PAppContext.getGlobalContext().logManager().getLog(JSONRPC2Servlet.class); + _conf = null; + _isWebapp = false; + } + + @Override + public void init() throws ServletException { + super.init(); + disp = new Dispatcher(); + disp.register(new EchoHandler(_helper)); + disp.register(new GetRateHandler(_helper)); + disp.register(new AuthenticateHandler(_helper, _secMan)); + disp.register(new NetworkSettingHandler(_context, _helper)); + disp.register(new RouterInfoHandler(_context, _helper)); + disp.register(new RouterManagerHandler(_context, _helper)); + disp.register(new I2PControlHandler(_context, _helper, _secMan)); + disp.register(new AdvancedSettingsHandler(_context, _helper)); + if (_isWebapp) { + PortMapper pm = _context.portMapper(); + int port = pm.getPort(PortMapper.SVC_CONSOLE); + if (port > 0) { + String host = pm.getHost(PortMapper.SVC_CONSOLE, "127.0.0.1"); + pm.register(SVC_HTTP_I2PCONTROL, host, port); + _isHTTP = true; + } + port = pm.getPort(PortMapper.SVC_HTTPS_CONSOLE); + if (port > 0) { + String host = pm.getHost(PortMapper.SVC_HTTPS_CONSOLE, "127.0.0.1"); + pm.register(SVC_HTTPS_I2PCONTROL, host, port); + _isHTTPS = true; + } + } + } + + @Override + public void destroy() { + if (_isWebapp) { + PortMapper pm = _context.portMapper(); + if (_isHTTP) + pm.unregister(SVC_HTTP_I2PCONTROL); + if (_isHTTPS) + pm.unregister(SVC_HTTPS_I2PCONTROL); + _secMan.stopTimedEvents(); + _conf.writeConfFile(); + } + super.destroy(); + } + + @Override + protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException { + httpServletResponse.setContentType("text/html"); + PrintWriter out = httpServletResponse.getWriter(); + out.println("

I2PControl RPC Service version " + I2PControlVersion.VERSION + " : Running"); + if ("/password".equals(httpServletRequest.getServletPath())) { + out.println("

"); + if (_secMan.isDefaultPasswordValid()) { + out.println("

The current API password is the default, \"" + _secMan.DEFAULT_AUTH_PASSWORD + "\". You should change it."); + } else { + out.println("

Current API password:"); + } + out.println("

New API password (twice):" + + "" + + "" + + "

If you forget the API password, stop i2pcontrol, delete the file " + _conf.getConfFile() + + ", and restart i2pcontrol."); + } else { + out.println("

Change API Password"); + } + out.close(); + } + + /** @since 0.12 */ + private void doPasswordChange(HttpServletRequest req, HttpServletResponse httpServletResponse) throws ServletException, IOException { + httpServletResponse.setContentType("text/html"); + PrintWriter out = httpServletResponse.getWriter(); + String pw = req.getParameter("password"); + if (pw == null) + pw = _secMan.DEFAULT_AUTH_PASSWORD; + else + pw = pw.trim(); + String pw2 = req.getParameter("password2"); + String pw3 = req.getParameter("password3"); + if (pw2 == null || pw3 == null) { + out.println("

Enter new password twice!"); + } else { + pw2 = pw2.trim(); + pw3 = pw3.trim(); + if (!pw2.equals(pw3)) { + out.println("

New passwords don't match!"); + } else if (pw2.length() <= 0) { + out.println("

Enter new password twice!"); + } else if (_secMan.isValid(pw)) { + _secMan.setPasswd(pw2); + out.println("

API Password changed"); + } else { + out.println("

Incorrect old password, not changed"); + } + } + out.println("

Change API Password"); + } + + @Override + protected void doPost(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException { + if ("/password".equals(httpServletRequest.getServletPath())) { + doPasswordChange(httpServletRequest, httpServletResponse); + return; + } + String req = getRequest(httpServletRequest.getInputStream()); + httpServletResponse.setContentType("application/json"); + PrintWriter out = httpServletResponse.getWriter(); + JSONRPC2Message msg = null; + JSONRPC2Response jsonResp = null; + try { + msg = JSONRPC2Message.parse(req); + + if (msg instanceof JSONRPC2Request) { + jsonResp = disp.process((JSONRPC2Request)msg, null); + jsonResp.toJSONObject().put("API", I2PControlVersion.API_VERSION); + if (_log.shouldDebug()) { + _log.debug("Request: " + msg); + _log.debug("Response: " + jsonResp); + } + } + else if (msg instanceof JSONRPC2Notification) { + disp.process((JSONRPC2Notification)msg, null); + if (_log.shouldDebug()) + _log.debug("Notification: " + msg); + } + + out.println(jsonResp); + out.close(); + } catch (JSONRPC2ParseException e) { + _log.error("Unable to parse JSONRPC2Message: " + e.getMessage()); + } + } + + private String getRequest(ServletInputStream sis) throws IOException { + Writer writer = new StringWriter(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(sis, "UTF-8")); + char[] readBuffer = new char[BUFFER_LENGTH]; + int n; + while ((n = reader.read(readBuffer)) != -1) { + writer.write(readBuffer, 0, n); + } + return writer.toString(); + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/configuration/ConfigurationManager.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/configuration/ConfigurationManager.java new file mode 100644 index 0000000000..c2916de89a --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/configuration/ConfigurationManager.java @@ -0,0 +1,219 @@ +package net.i2p.i2pcontrol.servlets.configuration; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; +import net.i2p.util.OrderedProperties; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; + +/** + * Manage the configuration of I2PControl. + * @author mathias + * modified: hottuna + * + */ +public class ConfigurationManager { + private final String CONFIG_FILE = "I2PControl.conf"; + private final String WEBAPP_CONFIG_FILE = "i2pcontrol.config"; + private final File configLocation; + private final Log _log; + private boolean _changed; + + //Configurations with a String as value + private final Map stringConfigurations = new HashMap(); + //Configurations with a Boolean as value + private final Map booleanConfigurations = new HashMap(); + //Configurations with an Integer as value + private final Map integerConfigurations = new HashMap(); + + + + public ConfigurationManager(I2PAppContext ctx, File dir, boolean isPlugin) { + _log = ctx.logManager().getLog(ConfigurationManager.class); + if (isPlugin) { + configLocation = new File(dir, CONFIG_FILE); + } else { + configLocation = new File(dir, WEBAPP_CONFIG_FILE); + } + readConfFile(); + } + + /** @since 0.12 */ + public File getConfFile() { + return configLocation; + } + + /** + * Collects arguments of the form --word, --word=otherword and -blah + * to determine user parameters. + * @param settingNames Command line arguments to the application + */ +/**** + public void loadArguments(String[] settingNames) { + for (int i = 0; i < settingNames.length; i++) { + String settingName = settingNames[i]; + if (settingName.startsWith("--")) { + parseConfigStr(settingName.substring(2)); + } + } + } +****/ + + /** + * Reads configuration from file, every line is parsed as key=value. + */ + public synchronized void readConfFile() { + try { + Properties input = new Properties(); + // true: map to lower case + DataHelper.loadProps(input, configLocation, true); + parseConfigStr(input); + _changed = false; + } catch (FileNotFoundException e) { + if (_log.shouldInfo()) + _log.info("Unable to find config file, " + configLocation); + } catch (IOException e) { + _log.error("Unable to read from config file, " + configLocation, e); + } + } + + /** + * Write configuration into default config file. + * As of 0.12, doesn't actually write unless something changed. + */ + public synchronized void writeConfFile() { + if (!_changed) + return; + Properties tree = new OrderedProperties(); + tree.putAll(stringConfigurations); + for (Entry e : integerConfigurations.entrySet()) { + tree.put(e.getKey(), e.getValue().toString()); + } + for (Entry e : booleanConfigurations.entrySet()) { + tree.put(e.getKey(), e.getValue().toString()); + } + try { + DataHelper.storeProps(tree, configLocation); + _changed = false; + } catch (IOException e1) { + _log.error("Couldn't open file, " + configLocation + " for writing config."); + } + } + + /** + * Try to parse the input as 'key=value', + * where value will (in order) be parsed as integer/boolean/string. + * @param str + */ + private void parseConfigStr(Properties input) { + for (Entry entry : input.entrySet()) { + String key = (String) entry.getKey(); + String value = (String) entry.getValue(); + //Try parse as integer. + try { + int i = Integer.parseInt(value); + integerConfigurations.put(key, i); + continue; + } catch (NumberFormatException e) {} + //Check if value is a bool + if (value.toLowerCase().equals("true")) { + booleanConfigurations.put(key, Boolean.TRUE); + continue; + } else if (value.toLowerCase().equals("false")) { + booleanConfigurations.put(key, Boolean.FALSE); + continue; + } + stringConfigurations.put(key, value); + } + } + + + /** + * Check if a specific boolean configuration exists. + * @param settingName The key for the configuration. + * @param defaultValue If the configuration is not found, we use a default value. + * @return The value of a configuration: true if found, defaultValue if not found. + */ + public synchronized boolean getConf(String settingName, boolean defaultValue) { + Boolean value = booleanConfigurations.get(settingName); + if (value != null) { + return value; + } else { + booleanConfigurations.put(settingName, defaultValue); + _changed = true; + return defaultValue; + } + } + + + /** + * Check if a specific boolean configuration exists. + * @param settingName The key for the configuration. + * @param defaultValue If the configuration is not found, we use a default value. + * @return The value of a configuration: true if found, defaultValue if not found. + */ + public synchronized int getConf(String settingName, int defaultValue) { + Integer value = integerConfigurations.get(settingName); + if (value != null) { + return value; + } else { + integerConfigurations.put(settingName, defaultValue); + _changed = true; + return defaultValue; + } + } + + /** + * Get a specific String configuration. + * @param settingName The key for the configuration. + * @param defaultValue If the configuration is not found, we use a default value. + * @return The value of the configuration, or the defaultValue. + */ + public synchronized String getConf(String settingName, String defaultValue) { + String value = stringConfigurations.get(settingName); + if (value != null) { + return value; + } else { + stringConfigurations.put(settingName, defaultValue); + _changed = true; + return defaultValue; + } + } + + /** + * Set a specific int setting + * @param settingName + * @param nbr + */ + public synchronized void setConf(String settingName, int nbr) { + integerConfigurations.put(settingName, nbr); + _changed = true; + } + + /** + * Set a specific string setting + * @param settingName + * @param str + */ + public synchronized void setConf(String settingName, String str) { + stringConfigurations.put(settingName, str); + _changed = true; + } + + /** + * Set a specific boolean setting + * @param settingName + * @param bool + */ + public synchronized void setConf(String settingName, boolean bool) { + booleanConfigurations.put(settingName, bool); + _changed = true; + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/AdvancedSettingsHandler.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/AdvancedSettingsHandler.java new file mode 100644 index 0000000000..4fb10b379c --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/AdvancedSettingsHandler.java @@ -0,0 +1,200 @@ +package net.i2p.i2pcontrol.servlets.jsonrpc2handlers; + +import com.thetransactioncompany.jsonrpc2.JSONRPC2Error; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Request; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Response; +import com.thetransactioncompany.jsonrpc2.server.MessageContext; +import com.thetransactioncompany.jsonrpc2.server.RequestHandler; + +import net.i2p.I2PAppContext; +import net.i2p.router.RouterContext; +import net.i2p.util.Log; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class AdvancedSettingsHandler implements RequestHandler { + + private final RouterContext _context; + private final Log _log; + private final JSONRPC2Helper _helper; + private static final String[] requiredArgs = {}; + + public AdvancedSettingsHandler(RouterContext ctx, JSONRPC2Helper helper) { + _helper = helper; + _context = ctx; + if (ctx != null) + _log = ctx.logManager().getLog(AdvancedSettingsHandler.class); + else + _log = I2PAppContext.getGlobalContext().logManager().getLog(AdvancedSettingsHandler.class); + } + + // Reports the method names of the handled requests + public String[] handledRequests() { + return new String[] {"AdvancedSettings"}; + } + + // Processes the requests + @SuppressWarnings("unchecked") + public JSONRPC2Response process(JSONRPC2Request req, MessageContext ctx) { + if (req.getMethod().equals("AdvancedSettings")) { + JSONRPC2Error err = _helper.validateParams(requiredArgs, req); + if (err != null) { + return new JSONRPC2Response(err, req.getID()); + } + + if (_context == null) { + return new JSONRPC2Response(new JSONRPC2Error( + JSONRPC2Error.INTERNAL_ERROR.getCode(), + "RouterContext was not initialized. Query failed"), + req.getID()); + } + + @SuppressWarnings("rawtypes") + Map inParams = req.getNamedParams(); + Map outParams = new HashMap(); + + if (inParams.containsKey("setAll")) { + Object obj = inParams.get("setAll"); + if (!(obj instanceof Map)) { + JSONRPC2Error rpcErr = new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "Value of \"setAll\" is not a Map"); + return new JSONRPC2Response(rpcErr, req.getID()); + } + + @SuppressWarnings("rawtypes") + Map objMap = (Map) inParams.get("setAll"); + if (objMap.size() > 0) + { + if (!(objMap.keySet().toArray()[0] instanceof String) && + !(objMap.values().toArray()[0] instanceof String)) { + JSONRPC2Error rpcErr = new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "Map of settings does not contain String keys and values"); + return new JSONRPC2Response(rpcErr, req.getID()); + } + + if (!checkTypes(objMap)) { + JSONRPC2Error rpcErr = new JSONRPC2Error(JSONRPC2Error.INTERNAL_ERROR.getCode(), + "Some of the supplied values are not strings"); + return new JSONRPC2Response(rpcErr, req.getID()); + } + + Map allSettings = (Map) objMap; + boolean success = setAdvancedSettings(allSettings, true); + if (!success) { + JSONRPC2Error rpcErr = new JSONRPC2Error(JSONRPC2Error.INTERNAL_ERROR.getCode(), + "Failed to save new config"); + return new JSONRPC2Response(rpcErr, req.getID()); + } + + } else { + // Empty list of settings submitted + boolean success = setAdvancedSettings(null, true); + if (!success) { + JSONRPC2Error rpcErr = new JSONRPC2Error(JSONRPC2Error.INTERNAL_ERROR.getCode(), + "Failed to save new config"); + return new JSONRPC2Response(rpcErr, req.getID()); + } + } + } + + if (inParams.containsKey("getAll")) { + outParams.put("getAll", getAdvancedSettings()); + } + + if (inParams.containsKey("set")) { + Object obj = inParams.get("set"); + if (!(obj instanceof Map)) { + JSONRPC2Error rpcErr = new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "Value of \"set\" is not a Map"); + return new JSONRPC2Response(rpcErr, req.getID()); + } + + Map objMap = (Map) inParams.get("set"); + if (objMap.size() > 0) + { + if (!(objMap.keySet().toArray()[0] instanceof String) && + !(objMap.values().toArray()[0] instanceof String)) { + JSONRPC2Error rpcErr = new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "Map of settings does not contain String keys and values"); + return new JSONRPC2Response(rpcErr, req.getID()); + } + + if (!checkTypes(objMap)) { + JSONRPC2Error rpcErr = new JSONRPC2Error(JSONRPC2Error.INTERNAL_ERROR.getCode(), + "Some of the supplied values are not strings"); + return new JSONRPC2Response(rpcErr, req.getID()); + } + + Map allSettings = (Map) objMap; + boolean success = setAdvancedSettings(allSettings, false); + if (!success) { + JSONRPC2Error rpcErr = new JSONRPC2Error(JSONRPC2Error.INTERNAL_ERROR.getCode(), + "Failed to save new config"); + return new JSONRPC2Response(rpcErr, req.getID()); + } + + } else { + // Empty list of settings submitted + JSONRPC2Error rpcErr = new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "Map of settings does not contain any entries"); + return new JSONRPC2Response(rpcErr, req.getID()); + } + } + + if (inParams.containsKey("get")) { + Object obj = inParams.get("get"); + if (!(obj instanceof String)) { + JSONRPC2Error rpcErr = new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "Value of \"get\" is not a string"); + return new JSONRPC2Response(rpcErr, req.getID()); + } + String getStr = (String) obj; + String getVal = getAdvancedSetting(getStr); + Map outMap = new HashMap(); + outMap.put(getStr, getVal); + outParams.put("get", outMap); + } + + return new JSONRPC2Response(outParams, req.getID()); + } else { + // Method name not supported + return new JSONRPC2Response(JSONRPC2Error.METHOD_NOT_FOUND, req.getID()); + } + } + + private String getAdvancedSetting(String key) { + return _context.router().getConfigSetting(key); + } + + + private Map getAdvancedSettings() { + return _context.router().getConfigMap(); + } + + private boolean checkTypes(Map newSettings) { + for (String key : newSettings.keySet()) { + if (!(newSettings.get(key) instanceof String)) { + return false; + } + } + + return true; + } + + private boolean setAdvancedSettings(Map newSettings, boolean clearConfig) { + Set unsetKeys = null; + + if (clearConfig) { + unsetKeys = new HashSet(_context.router().getConfigSettings()); + + for (String key : newSettings.keySet()) { + unsetKeys.remove(key); + } + } + + return _context.router().saveConfig(newSettings, unsetKeys); + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/AuthenticateHandler.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/AuthenticateHandler.java new file mode 100644 index 0000000000..a7f2f60a6a --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/AuthenticateHandler.java @@ -0,0 +1,105 @@ +package net.i2p.i2pcontrol.servlets.jsonrpc2handlers; + +import com.thetransactioncompany.jsonrpc2.JSONRPC2Error; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Request; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Response; +import com.thetransactioncompany.jsonrpc2.server.MessageContext; +import com.thetransactioncompany.jsonrpc2.server.RequestHandler; +import net.i2p.i2pcontrol.I2PControlVersion; +import net.i2p.i2pcontrol.security.AuthToken; +import net.i2p.i2pcontrol.security.SecurityManager; + +import java.util.HashMap; +import java.util.Map; + +/* + * Copyright 2011 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. + * + */ + +public class AuthenticateHandler implements RequestHandler { + + private static final String[] requiredArgs = {"Password", "API"}; + private final JSONRPC2Helper _helper; + private final SecurityManager _secMan; + + public AuthenticateHandler(JSONRPC2Helper helper, SecurityManager secMan) { + _helper = helper; + _secMan = secMan; + } + + // Reports the method names of the handled requests + public String[] handledRequests() { + return new String[] {"Authenticate"}; + } + + // Processes the requests + public JSONRPC2Response process(JSONRPC2Request req, MessageContext ctx) { + if (req.getMethod().equals("Authenticate")) { + JSONRPC2Error err = _helper.validateParams(requiredArgs, req, JSONRPC2Helper.USE_NO_AUTH); + if (err != null) + return new JSONRPC2Response(err, req.getID()); + + Map inParams = req.getNamedParams(); + + String pwd = (String) inParams.get("Password"); + + // Try get an AuthToken + + AuthToken token = _secMan.validatePasswd(pwd); + if (token == null) { + return new JSONRPC2Response(JSONRPC2ExtendedError.INVALID_PASSWORD, req.getID()); + } + + Object api = inParams.get("API"); + err = validateAPIVersion(api); + if (err != null) + return new JSONRPC2Response(err, req.getID()); + + + Map outParams = new HashMap(4); + outParams.put("Token", token.getId()); + outParams.put("API", I2PControlVersion.API_VERSION); + return new JSONRPC2Response(outParams, req.getID()); + } else { + // Method name not supported + return new JSONRPC2Response(JSONRPC2Error.METHOD_NOT_FOUND, req.getID()); + } + } + + /** + * Validate the provided I2PControl API version against the ones supported by I2PControl. + */ + private static JSONRPC2Error validateAPIVersion(Object api) { + + Integer apiVersion; + try { + apiVersion = ((Long) api).intValue(); + } catch (ClassCastException e) { + e.printStackTrace(); + return JSONRPC2ExtendedError.UNSPECIFIED_API_VERSION; + } + + if (!I2PControlVersion.SUPPORTED_API_VERSIONS.contains(apiVersion)) { + String supportedAPIVersions = ""; + for (Integer i : I2PControlVersion.SUPPORTED_API_VERSIONS) { + supportedAPIVersions += ", " + i; + } + return new JSONRPC2Error(JSONRPC2ExtendedError.UNSUPPORTED_API_VERSION.getCode(), + "The provided API version \'" + apiVersion + "\' is not supported. The supported versions are" + supportedAPIVersions + "."); + } + return null; + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/EchoHandler.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/EchoHandler.java new file mode 100644 index 0000000000..5ea69e9e5f --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/EchoHandler.java @@ -0,0 +1,44 @@ +package net.i2p.i2pcontrol.servlets.jsonrpc2handlers; + +import com.thetransactioncompany.jsonrpc2.JSONRPC2Error; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Request; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Response; +import com.thetransactioncompany.jsonrpc2.server.MessageContext; +import com.thetransactioncompany.jsonrpc2.server.RequestHandler; + +import java.util.HashMap; +import java.util.Map; + +public class EchoHandler implements RequestHandler { + + private static final String[] requiredArgs = {"Echo"}; + private final JSONRPC2Helper _helper; + + public EchoHandler(JSONRPC2Helper helper) { + _helper = helper; + } + + // Reports the method names of the handled requests + public String[] handledRequests() { + return new String[] {"Echo"}; + } + + // Processes the requests + public JSONRPC2Response process(JSONRPC2Request req, MessageContext ctx) { + if (req.getMethod().equals("Echo")) { + JSONRPC2Error err = _helper.validateParams(requiredArgs, req); + if (err != null) + return new JSONRPC2Response(err, req.getID()); + + Map inParams = req.getNamedParams(); + String echo = (String) inParams.get("Echo"); + Map outParams = new HashMap(4); + outParams.put("Result", echo); + return new JSONRPC2Response(outParams, req.getID()); + } + else { + // Method name not supported + return new JSONRPC2Response(JSONRPC2Error.METHOD_NOT_FOUND, req.getID()); + } + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/GetRateHandler.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/GetRateHandler.java new file mode 100644 index 0000000000..a484ce38d2 --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/GetRateHandler.java @@ -0,0 +1,85 @@ +package net.i2p.i2pcontrol.servlets.jsonrpc2handlers; + +import com.thetransactioncompany.jsonrpc2.JSONRPC2Error; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Request; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Response; +import com.thetransactioncompany.jsonrpc2.server.MessageContext; +import com.thetransactioncompany.jsonrpc2.server.RequestHandler; +import net.i2p.I2PAppContext; +import net.i2p.stat.Rate; +import net.i2p.stat.RateStat; + +import java.util.HashMap; +import java.util.Map; + +/* + * Copyright 2011 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. + * + */ + +public class GetRateHandler implements RequestHandler { + + private static final String[] requiredArgs = {"Stat", "Period"}; + private final JSONRPC2Helper _helper; + + public GetRateHandler(JSONRPC2Helper helper) { + _helper = helper; + } + + // Reports the method names of the handled requests + public String[] handledRequests() { + return new String[] {"GetRate"}; + } + + // Processes the requests + public JSONRPC2Response process(JSONRPC2Request req, MessageContext ctx) { + if (req.getMethod().equals("GetRate")) { + JSONRPC2Error err = _helper.validateParams(requiredArgs, req); + if (err != null) + return new JSONRPC2Response(err, req.getID()); + + Map inParams = req.getNamedParams(); + + String input = (String) inParams.get("Stat"); + if (input == null) { + return new JSONRPC2Response(JSONRPC2Error.INVALID_PARAMS, req.getID()); + } + long period; + try { + period = (Long) inParams.get("Period"); + } catch (NumberFormatException e) { + return new JSONRPC2Response(JSONRPC2Error.INVALID_PARAMS, req.getID()); + } + + RateStat rateStat = I2PAppContext.getGlobalContext().statManager().getRate(input); + + // If RateStat or the requested period doesn't already exist, create them. + if (rateStat == null || rateStat.getRate(period) == null) { + long[] tempArr = new long[1]; + tempArr[0] = period; + I2PAppContext.getGlobalContext().statManager().createRequiredRateStat(input, "I2PControl", "I2PControl", tempArr); + rateStat = I2PAppContext.getGlobalContext().statManager().getRate(input); + } + if (rateStat.getRate(period) == null) + return new JSONRPC2Response(JSONRPC2Error.INTERNAL_ERROR, req.getID()); + Map outParams = new HashMap(4); + Rate rate = rateStat.getRate(period); + rate.coalesce(); + outParams.put("Result", rate.getAverageValue()); + return new JSONRPC2Response(outParams, req.getID()); + } + return new JSONRPC2Response(JSONRPC2Error.METHOD_NOT_FOUND, req.getID()); + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/I2PControlHandler.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/I2PControlHandler.java new file mode 100644 index 0000000000..8c69021a8e --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/I2PControlHandler.java @@ -0,0 +1,205 @@ +package net.i2p.i2pcontrol.servlets.jsonrpc2handlers; + +import com.thetransactioncompany.jsonrpc2.JSONRPC2Error; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Request; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Response; +import com.thetransactioncompany.jsonrpc2.server.MessageContext; +import com.thetransactioncompany.jsonrpc2.server.RequestHandler; + +import net.i2p.I2PAppContext; +import net.i2p.i2pcontrol.I2PControlController; +import net.i2p.i2pcontrol.security.SecurityManager; +import net.i2p.i2pcontrol.servlets.configuration.ConfigurationManager; +import net.i2p.router.RouterContext; +import net.i2p.util.Log; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; + +/* + * Copyright 2011 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. + * + */ + +public class I2PControlHandler implements RequestHandler { + + private static final int BW_BURST_PCT = 110; + private static final int BW_BURST_TIME = 20; + private final RouterContext _context; + private final Log _log; + //private final ConfigurationManager _conf; + private final SecurityManager _secMan; + private final JSONRPC2Helper _helper; + + public I2PControlHandler(RouterContext ctx, JSONRPC2Helper helper, SecurityManager secMan) { + _helper = helper; + _secMan = secMan; + _context = ctx; + if (ctx != null) + _log = ctx.logManager().getLog(I2PControlHandler.class); + else + _log = I2PAppContext.getGlobalContext().logManager().getLog(I2PControlHandler.class); + } + + + // Reports the method names of the handled requests + public String[] handledRequests() { + return new String[] {"I2PControl"}; + } + + // Processes the requests + public JSONRPC2Response process(JSONRPC2Request req, MessageContext ctx) { + if (req.getMethod().equals("I2PControl")) { + return process(req); + } else { + // Method name not supported + return new JSONRPC2Response(JSONRPC2Error.METHOD_NOT_FOUND, req.getID()); + } + } + + + private JSONRPC2Response process(JSONRPC2Request req) { + JSONRPC2Error err = _helper.validateParams(null, req); + if (err != null) + return new JSONRPC2Response(err, req.getID()); + +/**** only if we enable host/port changes + if (_context == null) { + return new JSONRPC2Response( + new JSONRPC2Error(JSONRPC2Error.INTERNAL_ERROR.getCode(), + "RouterContext was not initialized. Query failed"), + req.getID()); + } +****/ + Map inParams = req.getNamedParams(); + Map outParams = new HashMap(4); + + boolean restartNeeded = false; + boolean settingsSaved = false; + String inParam; + +/**** + if (inParams.containsKey("i2pcontrol.port")) { + Integer oldPort = _conf.getConf("i2pcontrol.listen.port", 7650); + if ((inParam = (String) inParams.get("i2pcontrol.port")) != null) { + if (oldPort == null || !inParam.equals(oldPort.toString())) { + Integer newPort; + try { + newPort = Integer.valueOf(inParam); + if (newPort < 1 || newPort > 65535) { + throw new NumberFormatException(); + } + } catch (NumberFormatException e) { + return new JSONRPC2Response( + new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "\"i2pcontrol.port\" must be a string representing a number in the range 1-65535. " + inParam + " isn't valid."), + req.getID()); + } + try { + SslSocketConnector ssl = I2PControlController.buildSslListener(_conf.getConf("i2pcontrol.listen.address", "127.0.0.1"), newPort); + I2PControlController.clearListeners(); + I2PControlController.replaceListener(ssl); + + _conf.setConf("i2pcontrol.listen.port", newPort); + + + ConfigurationManager.writeConfFile(); + outParams.put("i2pcontrol.port", null); + settingsSaved = true; + } catch (Exception e) { + try { + _conf.setConf("i2pcontrol.listen.port", oldPort); + SslSocketConnector ssl = I2PControlController.buildSslListener(_conf.getConf("i2pcontrol.listen.address", "127.0.0.1"), oldPort); + I2PControlController.clearListeners(); + I2PControlController.replaceListener(ssl); + } catch (Exception e2) { + _log.log(Log.CRIT, "Unable to resume server on previous listening port."); + } + _log.error("Client tried to set listen port to, " + newPort + " which isn't valid.", e); + return new JSONRPC2Response( + new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "\"i2pcontrol.port\" has been set to a port that is already in use, reverting. " + + inParam + " is an already used port.\n" + + "Exception: " + e.toString()), + req.getID()); + } + } + } + outParams.put("RestartNeeded", restartNeeded); + } +****/ + + if (inParams.containsKey("i2pcontrol.password")) { + if ((inParam = (String) inParams.get("i2pcontrol.password")) != null) { + if (_secMan.setPasswd(inParam)) { + outParams.put("i2pcontrol.password", null); + settingsSaved = true; + } + } + } + +/**** + if (inParams.containsKey("i2pcontrol.address")) { + String oldAddress = _conf.getConf("i2pcontrol.listen.address", "127.0.0.1"); + if ((inParam = (String) inParams.get("i2pcontrol.address")) != null) { + if ((oldAddress == null || !inParam.equals(oldAddress.toString()) && + (inParam.equals("0.0.0.0") || inParam.equals("127.0.0.1")))) { + InetAddress[] newAddress; + + try { + newAddress = InetAddress.getAllByName(inParam); + } catch (UnknownHostException e) { + return new JSONRPC2Response( + new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "\"i2pcontrol.address\" must be a string representing a hostname or ipaddress. " + inParam + " isn't valid."), + req.getID()); + } + try { + SslSocketConnector ssl = I2PControlController.buildSslListener(inParam, _conf.getConf("i2pcontrol.listen.port", 7650)); + I2PControlController.clearListeners(); + I2PControlController.replaceListener(ssl); + _conf.setConf("i2pcontrol.listen.address", inParam); + + ConfigurationManager.writeConfFile(); + outParams.put("i2pcontrol.address", null); + settingsSaved = true; + } catch (Exception e) { + _conf.setConf("i2pcontrol.listen.address", oldAddress); + try { + SslSocketConnector ssl = I2PControlController.buildSslListener(inParam, _conf.getConf("i2pcontrol.listen.port", 7650)); + I2PControlController.clearListeners(); + I2PControlController.replaceListener(ssl); + } catch (Exception e2) { + _log.log(Log.CRIT, "Unable to resume server on previous listening ip."); + } + _log.error("Client tried to set listen address to, " + newAddress.toString() + " which isn't valid.", e); + return new JSONRPC2Response( + new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "\"i2pcontrol.address\" has been set to an invalid address, reverting. "), req.getID()); + } + } + } else { + outParams.put("i2pcontrol.address", oldAddress); + } + outParams.put("RestartNeeded", restartNeeded); + } +****/ + + outParams.put("SettingsSaved", settingsSaved); + return new JSONRPC2Response(outParams, req.getID()); + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/JSONRPC2ExtendedError.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/JSONRPC2ExtendedError.java new file mode 100644 index 0000000000..41e14b9490 --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/JSONRPC2ExtendedError.java @@ -0,0 +1,124 @@ +package net.i2p.i2pcontrol.servlets.jsonrpc2handlers; + +import com.thetransactioncompany.jsonrpc2.JSONRPC2Error; +import org.json.simple.JSONObject; + +/* + * Copyright 2011 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. + * + */ + +/** + * Represents a JSON-RPC 2.0 error that occured during the processing of a + * request. + * + *

The protocol expects error objects to be structured like this: + * + *

    + *
  • {@code code} An integer that indicates the error type. + *
  • {@code message} A string providing a short description of the + * error. The message should be limited to a concise single sentence. + *
  • {@code data} Additional information, which may be omitted. Its + * contents is entirely defined by the application. + *
+ * + *

Note that the "Error" word in the class name was put there solely to + * comply with the parlance of the JSON-RPC spec. This class doesn't inherit + * from {@code java.lang.Error}. It's a regular subclass of + * {@code java.lang.Exception} and, if thrown, it's to indicate a condition + * that a reasonable application might want to catch. + * + *

This class also includes convenient final static instances for all + * standard JSON-RPC 2.0 errors: + * + *

    + *
  • {@link #PARSE_ERROR} JSON parse error (-32700) + *
  • {@link #INVALID_REQUEST} Invalid JSON-RPC 2.0 Request (-32600) + *
  • {@link #METHOD_NOT_FOUND} Method not found (-32601) + *
  • {@link #INVALID_PARAMS} Invalid parameters (-32602) + *
  • {@link #INTERNAL_ERROR} Internal error (-32603) + *
+ * + *

Note that the range -32099..-32000 is reserved for additional server + * errors. + * + *

The mapping between JSON and Java entities (as defined by the + * underlying JSON.simple library): + *

+ *     true|false  <--->  java.lang.Boolean
+ *     number      <--->  java.lang.Number
+ *     string      <--->  java.lang.String
+ *     array       <--->  java.util.List
+ *     object      <--->  java.util.Map
+ *     null        <--->  null
+ * 
+ * + *

The JSON-RPC 2.0 specification and user group forum can be found + * here. + * + * @author Vladimir Dzhuvinov + * @version 1.16 (2010-10-04) + */ +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."); + + /** Invalid JSON-RPC 2.0, implementation defined error (-32099 .. -32000) */ + public static final JSONRPC2Error NO_TOKEN = new JSONRPC2ExtendedError(-32002, "No authentication token presented."); + + /** Invalid JSON-RPC 2.0, implementation defined error (-32099 .. -32000) */ + public static final JSONRPC2Error INVALID_TOKEN = new JSONRPC2ExtendedError(-32003, "Authentication token doesn't exist."); + + /** Invalid JSON-RPC 2.0, implementation defined error (-32099 .. -32000) */ + public static final JSONRPC2Error TOKEN_EXPIRED = new JSONRPC2ExtendedError(-32004, "Provided authentication token was expired and will be removed."); + + /** Invalid JSON-RPC 2.0, implementation defined error (-32099 .. -32000) */ + public static final JSONRPC2Error UNSPECIFIED_API_VERSION = new JSONRPC2ExtendedError(-32005, "The version of the I2PControl API wasn't specified, but is required to be specified."); + + /** Invalid JSON-RPC 2.0, implementation defined error (-32099 .. -32000) */ + public static final JSONRPC2Error UNSUPPORTED_API_VERSION = new JSONRPC2ExtendedError(-32006, "The version of the I2PControl API specified is not supported by I2PControl."); + + + + /** + * Creates a new JSON-RPC 2.0 error with the specified code and + * message. The optional data is omitted. + * + * @param code The error code (standard pre-defined or + * application-specific). + * @param message The error message. + */ + public JSONRPC2ExtendedError(int code, String message) { + super(code, message); + } + + + /** + * Creates a new JSON-RPC 2.0 error with the specified code, + * message and data. + * + * @param code The error code (standard pre-defined or + * application-specific). + * @param message The error message. + * @param data Optional error data, must map + * to a valid JSON type. + */ + public JSONRPC2ExtendedError(int code, String message, Object data) { + super(code, message, data); + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/JSONRPC2Helper.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/JSONRPC2Helper.java new file mode 100644 index 0000000000..0d8ed30931 --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/JSONRPC2Helper.java @@ -0,0 +1,111 @@ +package net.i2p.i2pcontrol.servlets.jsonrpc2handlers; + +import com.thetransactioncompany.jsonrpc2.JSONRPC2Error; +import com.thetransactioncompany.jsonrpc2.JSONRPC2ParamsType; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Request; +import net.i2p.i2pcontrol.security.ExpiredAuthTokenException; +import net.i2p.i2pcontrol.security.InvalidAuthTokenException; +import net.i2p.i2pcontrol.security.SecurityManager; + +import java.util.Map; + +/* + * Copyright 2011 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. + * + */ + +public class JSONRPC2Helper { + public final static Boolean USE_NO_AUTH = false; + public final static Boolean USE_AUTH = true; + + private final SecurityManager _secMan; + + public JSONRPC2Helper(SecurityManager secMan) { + _secMan = secMan; + } + + /** + * 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 JSONRPC2Error validateParams(String[] requiredArgs, JSONRPC2Request req, Boolean useAuth) { + + // Error on unnamed parameters + if (req.getParamsType() != JSONRPC2ParamsType.OBJECT) { + return JSONRPC2Error.INVALID_PARAMS; + } + Map params = req.getNamedParams(); + + // Validate authentication token. + if (useAuth) { + JSONRPC2Error err = validateToken(params); + if (err != null) { + return err; + } + } + + // 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 JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), "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 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 JSONRPC2Error validateToken(Map params) { + String tokenID = (String) params.get("Token"); + if (tokenID == null) { + return JSONRPC2ExtendedError.NO_TOKEN; + } + try { + _secMan.verifyToken(tokenID); + } catch (InvalidAuthTokenException e) { + return JSONRPC2ExtendedError.INVALID_TOKEN; + } catch (ExpiredAuthTokenException e) { + JSONRPC2Error err = new JSONRPC2ExtendedError(JSONRPC2ExtendedError.TOKEN_EXPIRED.getCode(), + "Provided authentication token expired " + e.getExpirytime() + ", will be removed."); + return err; + } + return null; + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/NetworkSettingHandler.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/NetworkSettingHandler.java new file mode 100644 index 0000000000..45f6d120d2 --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/NetworkSettingHandler.java @@ -0,0 +1,344 @@ +package net.i2p.i2pcontrol.servlets.jsonrpc2handlers; + +import com.thetransactioncompany.jsonrpc2.JSONRPC2Error; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Request; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Response; +import com.thetransactioncompany.jsonrpc2.server.MessageContext; +import com.thetransactioncompany.jsonrpc2.server.RequestHandler; + +import net.i2p.I2PAppContext; +import net.i2p.router.Router; +import net.i2p.router.RouterContext; +import net.i2p.router.transport.FIFOBandwidthRefiller; +import net.i2p.router.transport.TransportManager; +import net.i2p.router.transport.ntcp.NTCPTransport; +import net.i2p.router.transport.udp.UDPTransport; +import net.i2p.util.Log; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; + +/* + * Copyright 2011 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. + * + */ + +public class NetworkSettingHandler implements RequestHandler { + private static final int BW_BURST_PCT = 110; + private static final int BW_BURST_TIME = 20; + private final JSONRPC2Helper _helper; + private final RouterContext _context; + + public NetworkSettingHandler(RouterContext ctx, JSONRPC2Helper helper) { + _helper = helper; + _context = ctx; + } + + // Reports the method names of the handled requests + public String[] handledRequests() { + return new String[] {"NetworkSetting"}; + } + + // Processes the requests + public JSONRPC2Response process(JSONRPC2Request req, MessageContext ctx) { + if (req.getMethod().equals("NetworkSetting")) { + return process(req); + } else { + // Method name not supported + return new JSONRPC2Response(JSONRPC2Error.METHOD_NOT_FOUND, req.getID()); + } + } + + + private JSONRPC2Response process(JSONRPC2Request req) { + JSONRPC2Error err = _helper.validateParams(null, req); + if (err != null) + return new JSONRPC2Response(err, req.getID()); + + if (_context == null) { + return new JSONRPC2Response( + new JSONRPC2Error(JSONRPC2Error.INTERNAL_ERROR.getCode(), + "RouterContext was not initialized. Query failed"), + req.getID()); + } + Map inParams = req.getNamedParams(); + Map outParams = new HashMap(4); + + boolean restartNeeded = false; + boolean settingsSaved = false; + String inParam; + + if (inParams.containsKey("i2p.router.net.ntcp.port")) { + String oldNTCPPort = _context.getProperty(NTCPTransport.PROP_I2NP_NTCP_PORT); + if ((inParam = (String) inParams.get("i2p.router.net.ntcp.port")) != null) { + if (oldNTCPPort == null || !oldNTCPPort.equals(inParam.trim())) { + Integer newPort; + try { + newPort = Integer.valueOf(inParam); + if (newPort < 1 || newPort > 65535) { + throw new NumberFormatException(); + } + } catch (NumberFormatException e) { + return new JSONRPC2Response( + new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "\"i2p.router.net.ntcp.port\" must be a string representing a number in the range 1-65535. " + inParam + " isn't valid."), + req.getID()); + } + Map config = new HashMap(); + config.put(NTCPTransport.PROP_I2NP_NTCP_PORT, String.valueOf(newPort)); + config.put(NTCPTransport.PROP_I2NP_NTCP_AUTO_PORT, "false"); + _context.router().saveConfig(config, null); + restartNeeded = true; + } + settingsSaved = true; + } else { + String sAutoPort = _context.getProperty(NTCPTransport.PROP_I2NP_NTCP_AUTO_PORT, "true"); + boolean oldAutoPort = "true".equalsIgnoreCase(sAutoPort); + if (oldAutoPort) { + String oldSSUPort = "" + _context.getProperty(UDPTransport.PROP_INTERNAL_PORT, 8887); + outParams.put("i2p.router.net.ntcp.port", oldSSUPort); + } else { + outParams.put("i2p.router.net.ntcp.port", oldNTCPPort); + } + } + } + + if (inParams.containsKey("i2p.router.net.ntcp.hostname")) { + String oldNTCPHostname = _context.getProperty(NTCPTransport.PROP_I2NP_NTCP_HOSTNAME); + if ((inParam = (String) inParams.get("i2p.router.net.ntcp.hostname")) != null) { + if (oldNTCPHostname == null || !oldNTCPHostname.equals(inParam.trim())) { + _context.router().saveConfig(NTCPTransport.PROP_I2NP_NTCP_HOSTNAME, inParam); + restartNeeded = true; + } + settingsSaved = true; + } else { + outParams.put("i2p.router.net.ntcp.hostname", oldNTCPHostname); + } + } + + if (inParams.containsKey("i2p.router.net.ntcp.autoip")) { + String oldNTCPAutoIP = _context.getProperty(NTCPTransport.PROP_I2NP_NTCP_AUTO_IP); + if ((inParam = (String) inParams.get("i2p.router.net.ntcp.autoip")) != null) { + inParam = inParam.trim().toLowerCase(); + if (oldNTCPAutoIP == null || !oldNTCPAutoIP.equals(inParam)) { + if ("always".equals(inParam) || "true".equals(inParam) || "false".equals(inParam)) { + _context.router().saveConfig(NTCPTransport.PROP_I2NP_NTCP_AUTO_IP, inParam); + restartNeeded = true; + } else { + return new JSONRPC2Response( + new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "\"i2p.router.net.ntcp.autoip\" can only be always, true or false. " + inParam + " isn't valid."), + req.getID()); + } + } + settingsSaved = true; + } else { + outParams.put("i2p.router.net.ntcp.autoip", oldNTCPAutoIP); + } + } + + if (inParams.containsKey("i2p.router.net.ssu.port")) { + String oldSSUPort = "" + _context.getProperty(UDPTransport.PROP_INTERNAL_PORT, 8887); + if ((inParam = (String) inParams.get("i2p.router.net.ssu.port")) != null) { + if (oldSSUPort == null || !oldSSUPort.equals(inParam.trim())) { + Integer newPort; + try { + newPort = Integer.valueOf(inParam); + if (newPort < 1 || newPort > 65535) { + throw new NumberFormatException(); + } + } catch (NumberFormatException e) { + return new JSONRPC2Response( + new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "\"i2p.router.net.ssu.port\" must be a string representing a number in the range 1-65535. " + inParam + " isn't valid."), + req.getID()); + } + Map config = new HashMap(); + config.put(UDPTransport.PROP_EXTERNAL_PORT, String.valueOf(newPort)); + config.put(UDPTransport.PROP_INTERNAL_PORT, String.valueOf(newPort)); + _context.router().saveConfig(config, null); + restartNeeded = true; + } + settingsSaved = true; + } else { + outParams.put("i2p.router.net.ssu.port", oldSSUPort); + } + } + + if (inParams.containsKey("i2p.router.net.ssu.hostname")) { + String oldSSUHostname = _context.getProperty(UDPTransport.PROP_EXTERNAL_HOST); + if ((inParam = (String) inParams.get("i2p.router.net.ssu.hostname")) != null) { + if (oldSSUHostname == null || !oldSSUHostname.equals(inParam.trim())) { + _context.router().saveConfig(UDPTransport.PROP_EXTERNAL_HOST, inParam); + restartNeeded = true; + } + settingsSaved = true; + } else { + outParams.put("i2p.router.net.ssu.hostname", oldSSUHostname); + } + } + + if (inParams.containsKey("i2p.router.net.ssu.autoip")) { + String oldSSUAutoIP = _context.getProperty(UDPTransport.PROP_SOURCES); + if ((inParam = (String) inParams.get("i2p.router.net.ssu.autoip")) != null) { + inParam = inParam.trim().toLowerCase(); + if (oldSSUAutoIP == null || !oldSSUAutoIP.equals(inParam)) { + if (inParam.equals("ssu") || inParam.equals("local,ssu") || inParam.equals("upnp,ssu") || inParam.equals("local,upnp,ssu")) { + _context.router().saveConfig(UDPTransport.PROP_SOURCES, inParam); + restartNeeded = true; + } else { + return new JSONRPC2Response( + new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "\"i2p.router.net.ssu.autoip\" can only be ssu/local,upnp,ssu/local/ssu/upnp,ssu. " + inParam + " isn't valid."), + req.getID()); + } + } + settingsSaved = true; + } else { + outParams.put("i2p.router.net.ssu.autoip", oldSSUAutoIP); + } + } + + // Non-setable key. + if (inParams.containsKey("i2p.router.net.ssu.detectedip")) { + if ((inParam = (String) inParams.get("i2p.router.net.ssu.autoip")) == null) { + byte[] ipBytes = _context.router().getRouterInfo().getTargetAddress("SSU").getIP(); + try { + InetAddress i = InetAddress.getByAddress(ipBytes); + outParams.put("i2p.router.net.ssu.detectedip", i.getHostAddress()); + } catch (UnknownHostException e) { + outParams.put("i2p.router.net.ssu.detectedip", "Failed to parse ip address"); + } + } + } + + if (inParams.containsKey("i2p.router.net.upnp")) { + String oldUPNP = _context.getProperty(TransportManager.PROP_ENABLE_UPNP); + if ((inParam = (String) inParams.get("i2p.router.net.upnp")) != null) { + if (oldUPNP == null || !oldUPNP.equals(inParam.trim())) { + _context.router().saveConfig(TransportManager.PROP_ENABLE_UPNP, inParam); + restartNeeded = true; + } + settingsSaved = true; + } else { + outParams.put("i2p.router.net.upnp", oldUPNP); + } + } + + if (inParams.containsKey("i2p.router.net.bw.share")) { + String oldShare = _context.router().getConfigSetting(Router.PROP_BANDWIDTH_SHARE_PERCENTAGE); + if ((inParam = (String) inParams.get("i2p.router.net.bw.share")) != null) { + if (oldShare == null || !oldShare.equals(inParam.trim())) { + Integer percent; + try { + percent = Integer.parseInt(inParam); + if (percent < 0 || percent > 100 || inParam.length() == 0) { + throw new NumberFormatException(); + } + } catch (NumberFormatException e) { + return new JSONRPC2Response( + new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "\"i2p.router.net.bw.share\" A positive integer must supplied, \"" + inParam + "\" isn't valid"), + req.getID()); + } + _context.router().saveConfig(Router.PROP_BANDWIDTH_SHARE_PERCENTAGE, inParam); + } + settingsSaved = true; + } else { + outParams.put("i2p.router.net.bw.share", oldShare); + } + } + + if (inParams.containsKey("i2p.router.net.bw.in")) { + String oldBWIn = _context.getProperty(FIFOBandwidthRefiller.PROP_INBOUND_BANDWIDTH); + if ((inParam = (String) inParams.get("i2p.router.net.bw.in")) != null) { + Integer rate; + try { + rate = Integer.parseInt(inParam); + if (rate < 0 || inParam.length() == 0) { + throw new NumberFormatException(); + } + } catch (NumberFormatException e) { + return new JSONRPC2Response( + new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "\"i2p.router.net.bw.in\" A positive integer must supplied, " + inParam + " isn't valid"), + req.getID()); + } + Integer burstRate = (rate * BW_BURST_PCT) / 100; + Integer burstSize = (burstRate * BW_BURST_TIME); + if (oldBWIn == null || !oldBWIn.equals(rate.toString())) { + Map config = new HashMap(); + config.put(FIFOBandwidthRefiller.PROP_INBOUND_BANDWIDTH, rate.toString()); + config.put(FIFOBandwidthRefiller.PROP_INBOUND_BURST_BANDWIDTH, burstRate.toString()); + config.put(FIFOBandwidthRefiller.PROP_INBOUND_BANDWIDTH_PEAK, burstSize.toString()); + _context.router().saveConfig(config, null); + _context.bandwidthLimiter().reinitialize(); + } + settingsSaved = true; + } else { + outParams.put("i2p.router.net.bw.in", oldBWIn); + } + } + if (inParams.containsKey("i2p.router.net.bw.out")) { + String oldBWOut = _context.getProperty(FIFOBandwidthRefiller.PROP_OUTBOUND_BANDWIDTH); + if ((inParam = (String) inParams.get("i2p.router.net.bw.out")) != null) { + Integer rate; + try { + rate = Integer.parseInt(inParam); + if (rate < 0 || inParam.length() == 0) + throw new NumberFormatException(); + } catch (NumberFormatException e) { + return new JSONRPC2Response( + new JSONRPC2Error(JSONRPC2Error.INVALID_PARAMS.getCode(), + "\"i2p.router.net.bw.out\" A positive integer must supplied, " + inParam + " isn't valid"), + req.getID()); + } + Integer burstRate = (rate * BW_BURST_PCT) / 100; + Integer burstSize = (burstRate * BW_BURST_TIME); + if (oldBWOut == null || !oldBWOut.equals(rate.toString())) { + Map config = new HashMap(); + config.put(FIFOBandwidthRefiller.PROP_OUTBOUND_BANDWIDTH, rate.toString()); + config.put(FIFOBandwidthRefiller.PROP_OUTBOUND_BURST_BANDWIDTH, burstRate.toString()); + config.put(FIFOBandwidthRefiller.PROP_OUTBOUND_BANDWIDTH_PEAK, burstSize.toString()); + _context.router().saveConfig(config, null); + _context.bandwidthLimiter().reinitialize(); + } + settingsSaved = true; + } else { + outParams.put("i2p.router.net.bw.out", oldBWOut); + } + } + if (inParams.containsKey("i2p.router.net.laptopmode")) { + String oldLaptopMode = _context.getProperty(UDPTransport.PROP_LAPTOP_MODE); + if ((inParam = (String) inParams.get("i2p.router.net.laptopmode")) != null) { + if (oldLaptopMode == null || !oldLaptopMode.equals(inParam.trim())) { + _context.router().saveConfig(UDPTransport.PROP_LAPTOP_MODE, String.valueOf(inParam)); + } + settingsSaved = true; + } else { + outParams.put("i2p.router.net.laptopmode", oldLaptopMode); + } + } + + if (settingsSaved) + _context.router().saveConfig(); + + outParams.put("SettingsSaved", settingsSaved); + outParams.put("RestartNeeded", restartNeeded); + return new JSONRPC2Response(outParams, req.getID()); + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/RouterInfoHandler.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/RouterInfoHandler.java new file mode 100644 index 0000000000..8df58c81da --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/RouterInfoHandler.java @@ -0,0 +1,213 @@ +package net.i2p.i2pcontrol.servlets.jsonrpc2handlers; + +import com.thetransactioncompany.jsonrpc2.JSONRPC2Error; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Request; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Response; +import com.thetransactioncompany.jsonrpc2.server.MessageContext; +import com.thetransactioncompany.jsonrpc2.server.RequestHandler; + +import net.i2p.I2PAppContext; +import net.i2p.data.router.RouterAddress; +import net.i2p.router.CommSystemFacade; +import net.i2p.router.Router; +import net.i2p.router.RouterContext; +import net.i2p.router.RouterVersion; +import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade; +import net.i2p.router.transport.TransportUtil; +import net.i2p.router.transport.ntcp.NTCPTransport; + +import java.util.HashMap; +import java.util.Map; + +/* + * Copyright 2011 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. + * + */ + +public class RouterInfoHandler implements RequestHandler { + private final JSONRPC2Helper _helper; + private final RouterContext _context; + + public RouterInfoHandler(RouterContext ctx, JSONRPC2Helper helper) { + _helper = helper; + _context = ctx; + } + + + // Reports the method names of the handled requests + public String[] handledRequests() { + return new String[] { "RouterInfo" }; + } + + // Processes the requests + public JSONRPC2Response process(JSONRPC2Request req, MessageContext ctx) { + if (req.getMethod().equals("RouterInfo")) { + return process(req); + } else { + // Method name not supported + return new JSONRPC2Response(JSONRPC2Error.METHOD_NOT_FOUND, + req.getID()); + } + } + + @SuppressWarnings("unchecked") + private JSONRPC2Response process(JSONRPC2Request req) { + JSONRPC2Error err = _helper.validateParams(null, req); + if (err != null) + return new JSONRPC2Response(err, req.getID()); + + if (_context == null) { + return new JSONRPC2Response(new JSONRPC2Error( + JSONRPC2Error.INTERNAL_ERROR.getCode(), + "RouterContext was not initialized. Query failed"), + req.getID()); + } + Map inParams = req.getNamedParams(); + Map outParams = new HashMap(); + + if (inParams.containsKey("i2p.router.version")) { + try { + Class rvClass = Class.forName("net.i2p.router.RouterVersion"); + java.lang.reflect.Field field = rvClass.getDeclaredField("FULL_VERSION"); + String fullVersion = (String) field.get(new RouterVersion()); + outParams.put("i2p.router.version", fullVersion); + } catch (Exception e) {} // Ignore + } + + if (inParams.containsKey("i2p.router.uptime")) { + Router router = _context.router(); + if (router == null) { + outParams.put("i2p.router.uptime", 0); + } else { + outParams.put("i2p.router.uptime", router.getUptime()); + } + } + + if (inParams.containsKey("i2p.router.status")) { + outParams.put("i2p.router.status", _context.throttle().getTunnelStatus()); + } + + if (inParams.containsKey("i2p.router.net.status")) { + outParams.put("i2p.router.net.status", getNetworkStatus().ordinal()); + } + + if (inParams.containsKey("i2p.router.net.bw.inbound.1s")) { + outParams.put("i2p.router.net.bw.inbound.1s", _context.bandwidthLimiter().getReceiveBps()); + } + + if (inParams.containsKey("i2p.router.net.bw.outbound.1s")) { + outParams.put("i2p.router.net.bw.outbound.1s", _context.bandwidthLimiter().getSendBps()); + } + + if (inParams.containsKey("i2p.router.net.bw.inbound.15s")) { + outParams.put("i2p.router.net.bw.inbound.15s", _context.bandwidthLimiter().getReceiveBps15s()); + } + + if (inParams.containsKey("i2p.router.net.bw.outbound.15s")) { + outParams.put("i2p.router.net.bw.outbound.15s", _context.bandwidthLimiter().getSendBps15s()); + } + + if (inParams.containsKey("i2p.router.net.tunnels.participating")) { + outParams.put("i2p.router.net.tunnels.participating", _context.tunnelManager().getParticipatingCount()); + } + + if (inParams.containsKey("i2p.router.netdb.knownpeers")) { + // Why max(-1, 0) is used I don't know, it is the implementation used in the router console. + outParams.put("i2p.router.netdb.knownpeers", Math.max(_context.netDb().getKnownRouters() - 1, 0)); + } + + if (inParams.containsKey("i2p.router.netdb.activepeers")) { + outParams.put("i2p.router.netdb.activepeers", _context.commSystem().countActivePeers()); + } + + if (inParams.containsKey("i2p.router.netdb.fastpeers")) { + outParams.put("i2p.router.netdb.fastpeers", _context.profileOrganizer().countFastPeers()); + } + + if (inParams.containsKey("i2p.router.netdb.highcapacitypeers")) { + outParams.put("i2p.router.netdb.highcapacitypeers", _context.profileOrganizer().countHighCapacityPeers()); + } + + if (inParams.containsKey("i2p.router.netdb.isreseeding")) { + outParams.put("i2p.router.netdb.isreseeding", Boolean.valueOf(System.getProperty("net.i2p.router.web.ReseedHandler.reseedInProgress")).booleanValue()); + } + return new JSONRPC2Response(outParams, req.getID()); + } + + private static enum NETWORK_STATUS { + OK, + TESTING, + FIREWALLED, + HIDDEN, + WARN_FIREWALLED_AND_FAST, + WARN_FIREWALLED_AND_FLOODFILL, + WARN_FIREWALLED_WITH_INBOUND_TCP, + WARN_FIREWALLED_WITH_UDP_DISABLED, + ERROR_I2CP, + ERROR_CLOCK_SKEW, + ERROR_PRIVATE_TCP_ADDRESS, + ERROR_SYMMETRIC_NAT, + ERROR_UDP_PORT_IN_USE, + ERROR_NO_ACTIVE_PEERS_CHECK_CONNECTION_AND_FIREWALL, + ERROR_UDP_DISABLED_AND_TCP_UNSET, + }; + + // Ripped out of SummaryHelper.java + private NETWORK_STATUS getNetworkStatus() { + if (_context.router().getUptime() > 60 * 1000 + && (!_context.router().gracefulShutdownInProgress()) + && !_context.clientManager().isAlive()) + return (NETWORK_STATUS.ERROR_I2CP); + long skew = _context.commSystem().getFramedAveragePeerClockSkew(33); + // Display the actual skew, not the offset + if (Math.abs(skew) > 60 * 1000) + return NETWORK_STATUS.ERROR_CLOCK_SKEW; + if (_context.router().isHidden()) + return (NETWORK_STATUS.HIDDEN); + + int status = _context.commSystem().getStatus().getCode(); + switch (status) { + case CommSystemFacade.STATUS_OK: + RouterAddress ra = _context.router().getRouterInfo().getTargetAddress("NTCP"); + if (ra == null || TransportUtil.isPubliclyRoutable(ra.getIP(), true)) + return NETWORK_STATUS.OK; + return NETWORK_STATUS.ERROR_PRIVATE_TCP_ADDRESS; + case CommSystemFacade.STATUS_DIFFERENT: + return NETWORK_STATUS.ERROR_SYMMETRIC_NAT; + case CommSystemFacade.STATUS_REJECT_UNSOLICITED: + if (_context.router().getRouterInfo().getTargetAddress("NTCP") != null) + return NETWORK_STATUS.WARN_FIREWALLED_WITH_INBOUND_TCP; + if (((FloodfillNetworkDatabaseFacade) _context.netDb()).floodfillEnabled()) + return NETWORK_STATUS.WARN_FIREWALLED_AND_FLOODFILL; + if (_context.router().getRouterInfo().getCapabilities().indexOf('O') >= 0) + return NETWORK_STATUS.WARN_FIREWALLED_AND_FAST; + return NETWORK_STATUS.FIREWALLED; + case CommSystemFacade.STATUS_HOSED: + return NETWORK_STATUS.ERROR_UDP_PORT_IN_USE; + case CommSystemFacade.STATUS_UNKNOWN: // fallthrough + default: + ra = _context.router().getRouterInfo().getTargetAddress("SSU"); + if (ra == null && _context.router().getUptime() > 5 * 60 * 1000) { + if (_context.commSystem().countActivePeers() <= 0) + return NETWORK_STATUS.ERROR_NO_ACTIVE_PEERS_CHECK_CONNECTION_AND_FIREWALL; + else if (_context.getProperty(NTCPTransport.PROP_I2NP_NTCP_HOSTNAME) == null || _context.getProperty(NTCPTransport.PROP_I2NP_NTCP_PORT) == null) + return NETWORK_STATUS.ERROR_UDP_DISABLED_AND_TCP_UNSET; + else + return NETWORK_STATUS.WARN_FIREWALLED_WITH_UDP_DISABLED; + } + return NETWORK_STATUS.TESTING; + } + } +} diff --git a/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/RouterManagerHandler.java b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/RouterManagerHandler.java new file mode 100644 index 0000000000..f972b18cb4 --- /dev/null +++ b/apps/i2pcontrol/java/net/i2p/i2pcontrol/servlets/jsonrpc2handlers/RouterManagerHandler.java @@ -0,0 +1,231 @@ +package net.i2p.i2pcontrol.servlets.jsonrpc2handlers; + +import com.thetransactioncompany.jsonrpc2.JSONRPC2Error; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Request; +import com.thetransactioncompany.jsonrpc2.JSONRPC2Response; +import com.thetransactioncompany.jsonrpc2.server.MessageContext; +import com.thetransactioncompany.jsonrpc2.server.RequestHandler; + +import net.i2p.I2PAppContext; +import net.i2p.app.ClientAppManager; +import net.i2p.router.Router; +import net.i2p.router.RouterContext; +import net.i2p.router.networkdb.reseed.ReseedChecker; +import net.i2p.update.UpdateManager; +import net.i2p.update.UpdateType; +import net.i2p.util.Log; + +import org.tanukisoftware.wrapper.WrapperManager; + +import java.util.HashMap; +import java.util.Map; + +/* + * Copyright 2011 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. + * + */ + +public class RouterManagerHandler implements RequestHandler { + private final JSONRPC2Helper _helper; + private final RouterContext _context; + + private final static int SHUTDOWN_WAIT = 1500; + + + public RouterManagerHandler(RouterContext ctx, JSONRPC2Helper helper) { + _helper = helper; + _context = ctx; + } + + // Reports the method names of the handled requests + public String[] handledRequests() { + return new String[] { "RouterManager" }; + } + + // Processes the requests + public JSONRPC2Response process(JSONRPC2Request req, MessageContext ctx) { + if (req.getMethod().equals("RouterManager")) { + return process(req); + } else { + // Method name not supported + return new JSONRPC2Response(JSONRPC2Error.METHOD_NOT_FOUND, + req.getID()); + } + } + + private JSONRPC2Response process(JSONRPC2Request req) { + JSONRPC2Error err = _helper.validateParams(null, req); + if (err != null) + return new JSONRPC2Response(err, req.getID()); + + if (_context == null) { + return new JSONRPC2Response(new JSONRPC2Error( + JSONRPC2Error.INTERNAL_ERROR.getCode(), + "RouterContext was not initialized. Query failed"), + req.getID()); + } + Map inParams = req.getNamedParams(); + final Map outParams = new HashMap(4); + + if (inParams.containsKey("Shutdown")) { + outParams.put("Shutdown", null); + (new Thread() { + @Override + public void run() { + try { + Thread.sleep(SHUTDOWN_WAIT); + } catch (InterruptedException e) {} + _context.addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_HARD)); + _context.router().shutdown(Router.EXIT_HARD); + } + }).start(); + return new JSONRPC2Response(outParams, req.getID()); + } + + if (inParams.containsKey("Restart")) { + outParams.put("Restart", null); + (new Thread() { + @Override + public void run() { + try { + Thread.sleep(SHUTDOWN_WAIT); + } catch (InterruptedException e) {} + _context.addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_HARD_RESTART)); + _context.router().shutdown(Router.EXIT_HARD_RESTART); + } + }).start(); + return new JSONRPC2Response(outParams, req.getID()); + } + + if (inParams.containsKey("ShutdownGraceful")) { + outParams.put("ShutdownGraceful", null); + (new Thread() { + @Override + public void run() { + try { + Thread.sleep(SHUTDOWN_WAIT); + } catch (InterruptedException e) {} + _context.addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_GRACEFUL)); + _context.router().shutdownGracefully(); + } + }).start(); + return new JSONRPC2Response(outParams, req.getID()); + } + + if (inParams.containsKey("RestartGraceful")) { + outParams.put("RestartGraceful", null); + (new Thread() { + @Override + public void run() { + try { + Thread.sleep(SHUTDOWN_WAIT); + } catch (InterruptedException e) {} + _context.addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_GRACEFUL_RESTART)); + _context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART); + } + }).start(); + return new JSONRPC2Response(outParams, req.getID()); + } + + if (inParams.containsKey("Reseed")) { + outParams.put("Reseed", null); + (new Thread() { + @Override + public void run() { + ReseedChecker reseeder = new ReseedChecker(_context); + reseeder.requestReseed(); + } + }).start(); + return new JSONRPC2Response(outParams, req.getID()); + } + + if (inParams.containsKey("FindUpdates")) { + Thread t = new Thread() { + @Override + public void run() { + ClientAppManager clmgr = I2PAppContext.getCurrentContext().clientAppManager(); + if (clmgr == null) { + outParams.put("FindUpdates", "ClientAppManager is null"); + return; + } + UpdateManager upmgr = (UpdateManager) clmgr.getRegisteredApp(UpdateManager.APP_NAME); + if (upmgr == null) { + outParams.put("FindUpdates", "UpdateManager is null"); + return; + } + boolean updateIsAvailable = upmgr.checkAvailable(UpdateType.ROUTER_SIGNED) != null; + outParams.put("FindUpdates", updateIsAvailable); + } + }; + t.start(); + try { + t.join(); + } catch (InterruptedException e) {} + return new JSONRPC2Response(outParams, req.getID()); + } + + if (inParams.containsKey("Update")) { + Thread t = new Thread() { + @Override + public void run() { + ClientAppManager clmgr = I2PAppContext.getCurrentContext().clientAppManager(); + if (clmgr == null) { + outParams.put("Update", "ClientAppManager is null"); + return; + } + UpdateManager upmgr = (UpdateManager) clmgr.getRegisteredApp(UpdateManager.APP_NAME); + if (upmgr == null) { + outParams.put("Update", "UpdateManager is null"); + return; + } + boolean updateStarted = upmgr.update(UpdateType.ROUTER_SIGNED); + if (!updateStarted) { + outParams.put("Update", "Update not started"); + return; + } + boolean isUpdating = upmgr.isUpdateInProgress(UpdateType.ROUTER_SIGNED); + while (isUpdating) { + try { + Thread.sleep(100); + } catch (Exception e) {} + isUpdating = upmgr.isUpdateInProgress(UpdateType.ROUTER_SIGNED); + } + outParams.put("Update", upmgr.getStatus()); + } + }; + t.start(); + try { + t.join(); + } catch (InterruptedException e) {} + return new JSONRPC2Response(outParams, req.getID()); + } + + return new JSONRPC2Response(outParams, req.getID()); + } + + public static class UpdateWrapperManagerTask implements Runnable { + private int _exitCode; + public UpdateWrapperManagerTask(int exitCode) { + _exitCode = exitCode; + } + public void run() { + try { + WrapperManager.signalStopped(_exitCode); + } catch (Throwable t) { + t.printStackTrace(); + } + } + } +} diff --git a/apps/i2pcontrol/java/org/mindrot/jbcrypt/BCrypt.java b/apps/i2pcontrol/java/org/mindrot/jbcrypt/BCrypt.java new file mode 100644 index 0000000000..9e1effdf98 --- /dev/null +++ b/apps/i2pcontrol/java/org/mindrot/jbcrypt/BCrypt.java @@ -0,0 +1,777 @@ +// Copyright (c) 2006 Damien Miller +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package org.mindrot.jbcrypt; + +import java.io.UnsupportedEncodingException; +import java.security.SecureRandom; + +/** + * BCrypt implements OpenBSD-style Blowfish password hashing using + * the scheme described in "A Future-Adaptable Password Scheme" by + * Niels Provos and David Mazieres. + *

+ * This password hashing system tries to thwart off-line password + * cracking using a computationally-intensive hashing algorithm, + * based on Bruce Schneier's Blowfish cipher. The work factor of + * the algorithm is parameterised, so it can be increased as + * computers get faster. + *

+ * Usage is really simple. To hash a password for the first time, + * call the hashpw method with a random salt, like this: + *

+ * + * String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());
+ *
+ *

+ * To check whether a plaintext password matches one that has been + * hashed previously, use the checkpw method: + *

+ * + * if (BCrypt.checkpw(candidate_password, stored_hash))
+ *     System.out.println("It matches");
+ * else
+ *     System.out.println("It does not match");
+ *
+ *

+ * The gensalt() method takes an optional parameter (log_rounds) + * that determines the computational complexity of the hashing: + *

+ * + * String strong_salt = BCrypt.gensalt(10)
+ * String stronger_salt = BCrypt.gensalt(12)
+ *
+ *

+ * The amount of work increases exponentially (2**log_rounds), so + * each increment is twice as much work. The default log_rounds is + * 10, and the valid range is 4 to 30. + * + * @author Damien Miller + * @version 0.2 + */ +public class BCrypt { + // BCrypt parameters + private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10; + private static final int BCRYPT_SALT_LEN = 16; + + // Blowfish parameters + private static final int BLOWFISH_NUM_ROUNDS = 16; + + // Initial contents of key schedule + private static final int P_orig[] = { + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, + 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, + 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, + 0x9216d5d9, 0x8979fb1b + }; + private static final int S_orig[] = { + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, + 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, + 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, + 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, + 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, + 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, + 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, + 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, + 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, + 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, + 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, + 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, + 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, + 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, + 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, + 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, + 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, + 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, + 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, + 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, + 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, + 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, + 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, + 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, + 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, + 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, + 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, + 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, + 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, + 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, + 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, + 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, + 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, + 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, + 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, + 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, + 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, + 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, + 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, + 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, + 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a, + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, + 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, + 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, + 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, + 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, + 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, + 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, + 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, + 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, + 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, + 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, + 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, + 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, + 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, + 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, + 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, + 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, + 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, + 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, + 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, + 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, + 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, + 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, + 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, + 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, + 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, + 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, + 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, + 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, + 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, + 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, + 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, + 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, + 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, + 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, + 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, + 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, + 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, + 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, + 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, + 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, + 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, + 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, + 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, + 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, + 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, + 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, + 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, + 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, + 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, + 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, + 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, + 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, + 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, + 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, + 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, + 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, + 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, + 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, + 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, + 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, + 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, + 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, + 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, + 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, + 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, + 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, + 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, + 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, + 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, + 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, + 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, + 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, + 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, + 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, + 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, + 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, + 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, + 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, + 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, + 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, + 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, + 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, + 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, + 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, + 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, + 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, + 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, + 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, + 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, + 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, + 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, + 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, + 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, + 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, + 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, + 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, + 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, + 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, + 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, + 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, + 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, + 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, + 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, + 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, + 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, + 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, + 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, + 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, + 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, + 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, + 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, + 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, + 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, + 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, + 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, + 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, + 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, + 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, + 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, + 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, + 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, + 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 + }; + + // bcrypt IV: "OrpheanBeholderScryDoubt". The C implementation calls + // this "ciphertext", but it is really plaintext or an IV. We keep + // the name to make code comparison easier. + static private final int bf_crypt_ciphertext[] = { + 0x4f727068, 0x65616e42, 0x65686f6c, + 0x64657253, 0x63727944, 0x6f756274 + }; + + // Table for Base64 encoding + static private final char base64_code[] = { + '.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9' + }; + + // Table for Base64 decoding + static private final byte index_64[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 63, -1, -1, + -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, + -1, -1, -1, -1, -1, -1, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, -1, -1, -1, -1, -1 + }; + + // Expanded Blowfish key + private int P[]; + private int S[]; + + /** + * Encode a byte array using bcrypt's slightly-modified base64 + * encoding scheme. Note that this is *not* compatible with + * the standard MIME-base64 encoding. + * + * @param d the byte array to encode + * @param len the number of bytes to encode + * @return base64-encoded string + * @exception IllegalArgumentException if the length is invalid + */ + private static String encode_base64(byte d[], int len) + throws IllegalArgumentException { + int off = 0; + StringBuffer rs = new StringBuffer(); + int c1, c2; + + if (len <= 0 || len > d.length) + throw new IllegalArgumentException ("Invalid len"); + + while (off < len) { + c1 = d[off++] & 0xff; + rs.append(base64_code[(c1 >> 2) & 0x3f]); + c1 = (c1 & 0x03) << 4; + if (off >= len) { + rs.append(base64_code[c1 & 0x3f]); + break; + } + c2 = d[off++] & 0xff; + c1 |= (c2 >> 4) & 0x0f; + rs.append(base64_code[c1 & 0x3f]); + c1 = (c2 & 0x0f) << 2; + if (off >= len) { + rs.append(base64_code[c1 & 0x3f]); + break; + } + c2 = d[off++] & 0xff; + c1 |= (c2 >> 6) & 0x03; + rs.append(base64_code[c1 & 0x3f]); + rs.append(base64_code[c2 & 0x3f]); + } + return rs.toString(); + } + + /** + * Look up the 3 bits base64-encoded by the specified character, + * range-checking againt conversion table + * @param x the base64-encoded value + * @return the decoded value of x + */ + private static byte char64(char x) { + if ((int)x < 0 || (int)x > index_64.length) + return -1; + return index_64[(int)x]; + } + + /** + * Decode a string encoded using bcrypt's base64 scheme to a + * byte array. Note that this is *not* compatible with + * the standard MIME-base64 encoding. + * @param s the string to decode + * @param maxolen the maximum number of bytes to decode + * @return an array containing the decoded bytes + * @throws IllegalArgumentException if maxolen is invalid + */ + private static byte[] decode_base64(String s, int maxolen) + throws IllegalArgumentException { + StringBuffer rs = new StringBuffer(); + int off = 0, slen = s.length(), olen = 0; + byte ret[]; + byte c1, c2, c3, c4, o; + + if (maxolen <= 0) + throw new IllegalArgumentException ("Invalid maxolen"); + + while (off < slen - 1 && olen < maxolen) { + c1 = char64(s.charAt(off++)); + c2 = char64(s.charAt(off++)); + if (c1 == -1 || c2 == -1) + break; + o = (byte)(c1 << 2); + o |= (c2 & 0x30) >> 4; + rs.append((char)o); + if (++olen >= maxolen || off >= slen) + break; + c3 = char64(s.charAt(off++)); + if (c3 == -1) + break; + o = (byte)((c2 & 0x0f) << 4); + o |= (c3 & 0x3c) >> 2; + rs.append((char)o); + if (++olen >= maxolen || off >= slen) + break; + c4 = char64(s.charAt(off++)); + o = (byte)((c3 & 0x03) << 6); + o |= c4; + rs.append((char)o); + ++olen; + } + + ret = new byte[olen]; + for (off = 0; off < olen; off++) + ret[off] = (byte)rs.charAt(off); + return ret; + } + + /** + * Blowfish encipher a single 64-bit block encoded as + * two 32-bit halves + * @param lr an array containing the two 32-bit half blocks + * @param off the position in the array of the blocks + */ + private final void encipher(int lr[], int off) { + int i, n, l = lr[off], r = lr[off + 1]; + + l ^= P[0]; + for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2;) { + // Feistel substitution on left word + n = S[(l >> 24) & 0xff]; + n += S[0x100 | ((l >> 16) & 0xff)]; + n ^= S[0x200 | ((l >> 8) & 0xff)]; + n += S[0x300 | (l & 0xff)]; + r ^= n ^ P[++i]; + + // Feistel substitution on right word + n = S[(r >> 24) & 0xff]; + n += S[0x100 | ((r >> 16) & 0xff)]; + n ^= S[0x200 | ((r >> 8) & 0xff)]; + n += S[0x300 | (r & 0xff)]; + l ^= n ^ P[++i]; + } + lr[off] = r ^ P[BLOWFISH_NUM_ROUNDS + 1]; + lr[off + 1] = l; + } + + /** + * Cycically extract a word of key material + * @param data the string to extract the data from + * @param offp a "pointer" (as a one-entry array) to the + * current offset into data + * @return the next word of material from data + */ + private static int streamtoword(byte data[], int offp[]) { + int i; + int word = 0; + int off = offp[0]; + + for (i = 0; i < 4; i++) { + word = (word << 8) | (data[off] & 0xff); + off = (off + 1) % data.length; + } + + offp[0] = off; + return word; + } + + /** + * Initialise the Blowfish key schedule + */ + private void init_key() { + P = (int[])P_orig.clone(); + S = (int[])S_orig.clone(); + } + + /** + * Key the Blowfish cipher + * @param key an array containing the key + */ + private void key(byte key[]) { + int i; + int koffp[] = { 0 }; + int lr[] = { 0, 0 }; + int plen = P.length, slen = S.length; + + for (i = 0; i < plen; i++) + P[i] = P[i] ^ streamtoword(key, koffp); + + for (i = 0; i < plen; i += 2) { + encipher(lr, 0); + P[i] = lr[0]; + P[i + 1] = lr[1]; + } + + for (i = 0; i < slen; i += 2) { + encipher(lr, 0); + S[i] = lr[0]; + S[i + 1] = lr[1]; + } + } + + /** + * Perform the "enhanced key schedule" step described by + * Provos and Mazieres in "A Future-Adaptable Password Scheme" + * http://www.openbsd.org/papers/bcrypt-paper.ps + * @param data salt information + * @param key password information + */ + private void ekskey(byte data[], byte key[]) { + int i; + int koffp[] = { 0 }, doffp[] = { 0 }; + int lr[] = { 0, 0 }; + int plen = P.length, slen = S.length; + + for (i = 0; i < plen; i++) + P[i] = P[i] ^ streamtoword(key, koffp); + + for (i = 0; i < plen; i += 2) { + lr[0] ^= streamtoword(data, doffp); + lr[1] ^= streamtoword(data, doffp); + encipher(lr, 0); + P[i] = lr[0]; + P[i + 1] = lr[1]; + } + + for (i = 0; i < slen; i += 2) { + lr[0] ^= streamtoword(data, doffp); + lr[1] ^= streamtoword(data, doffp); + encipher(lr, 0); + S[i] = lr[0]; + S[i + 1] = lr[1]; + } + } + + /** + * Perform the central password hashing step in the + * bcrypt scheme + * @param password the password to hash + * @param salt the binary salt to hash with the password + * @param log_rounds the binary logarithm of the number + * of rounds of hashing to apply + * @param cdata the plaintext to encrypt + * @return an array containing the binary hashed password + */ + public byte[] crypt_raw(byte password[], byte salt[], int log_rounds, + int cdata[]) { + int rounds, i, j; + int clen = cdata.length; + byte ret[]; + + if (log_rounds < 4 || log_rounds > 30) + throw new IllegalArgumentException ("Bad number of rounds"); + rounds = 1 << log_rounds; + if (salt.length != BCRYPT_SALT_LEN) + throw new IllegalArgumentException ("Bad salt length"); + + init_key(); + ekskey(salt, password); + for (i = 0; i != rounds; i++) { + key(password); + key(salt); + } + + for (i = 0; i < 64; i++) { + for (j = 0; j < (clen >> 1); j++) + encipher(cdata, j << 1); + } + + ret = new byte[clen * 4]; + for (i = 0, j = 0; i < clen; i++) { + ret[j++] = (byte)((cdata[i] >> 24) & 0xff); + ret[j++] = (byte)((cdata[i] >> 16) & 0xff); + ret[j++] = (byte)((cdata[i] >> 8) & 0xff); + ret[j++] = (byte)(cdata[i] & 0xff); + } + return ret; + } + + /** + * Hash a password using the OpenBSD bcrypt scheme + * @param password the password to hash + * @param salt the salt to hash with (perhaps generated + * using BCrypt.gensalt) + * @return the hashed password + */ + public static String hashpw(String password, String salt) { + BCrypt B; + String real_salt; + byte passwordb[], saltb[], hashed[]; + char minor = (char)0; + int rounds, off = 0; + StringBuffer rs = new StringBuffer(); + + if (salt.charAt(0) != '$' || salt.charAt(1) != '2') + throw new IllegalArgumentException ("Invalid salt version"); + if (salt.charAt(2) == '$') + off = 3; + else { + minor = salt.charAt(2); + if (minor != 'a' || salt.charAt(3) != '$') + throw new IllegalArgumentException ("Invalid salt revision"); + off = 4; + } + + // Extract number of rounds + if (salt.charAt(off + 2) > '$') + throw new IllegalArgumentException ("Missing salt rounds"); + rounds = Integer.parseInt(salt.substring(off, off + 2)); + + real_salt = salt.substring(off + 3, off + 25); + try { + passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8"); + } catch (UnsupportedEncodingException uee) { + throw new AssertionError("UTF-8 is not supported"); + } + + saltb = decode_base64(real_salt, BCRYPT_SALT_LEN); + + B = new BCrypt(); + hashed = B.crypt_raw(passwordb, saltb, rounds, + (int[])bf_crypt_ciphertext.clone()); + + rs.append("$2"); + if (minor >= 'a') + rs.append(minor); + rs.append("$"); + if (rounds < 10) + rs.append("0"); + if (rounds > 30) { + throw new IllegalArgumentException( + "rounds exceeds maximum (30)"); + } + rs.append(Integer.toString(rounds)); + rs.append("$"); + rs.append(encode_base64(saltb, saltb.length)); + rs.append(encode_base64(hashed, + bf_crypt_ciphertext.length * 4 - 1)); + return rs.toString(); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method + * @param log_rounds the log2 of the number of rounds of + * hashing to apply - the work factor therefore increases as + * 2**log_rounds. + * @param random an instance of SecureRandom to use + * @return an encoded salt value + */ + public static String gensalt(int log_rounds, SecureRandom random) { + StringBuffer rs = new StringBuffer(); + byte rnd[] = new byte[BCRYPT_SALT_LEN]; + + random.nextBytes(rnd); + + rs.append("$2a$"); + if (log_rounds < 10) + rs.append("0"); + if (log_rounds > 30) { + throw new IllegalArgumentException( + "log_rounds exceeds maximum (30)"); + } + rs.append(Integer.toString(log_rounds)); + rs.append("$"); + rs.append(encode_base64(rnd, rnd.length)); + return rs.toString(); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method + * @param log_rounds the log2 of the number of rounds of + * hashing to apply - the work factor therefore increases as + * 2**log_rounds. + * @return an encoded salt value + */ + public static String gensalt(int log_rounds) { + return gensalt(log_rounds, new SecureRandom()); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method, + * selecting a reasonable default for the number of hashing + * rounds to apply + * @return an encoded salt value + */ + public static String gensalt() { + return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS); + } + + /** + * Check that a plaintext password matches a previously hashed + * one + * @param plaintext the plaintext password to verify + * @param hashed the previously-hashed password + * @return true if the passwords match, false otherwise + */ + public static boolean checkpw(String plaintext, String hashed) { + byte hashed_bytes[]; + byte try_bytes[]; + try { + String try_pw = hashpw(plaintext, hashed); + hashed_bytes = hashed.getBytes("UTF-8"); + try_bytes = try_pw.getBytes("UTF-8"); + } catch (UnsupportedEncodingException uee) { + return false; + } + if (hashed_bytes.length != try_bytes.length) + return false; + byte ret = 0; + for (int i = 0; i < try_bytes.length; i++) + ret |= hashed_bytes[i] ^ try_bytes[i]; + return ret == 0; + } +} diff --git a/apps/i2pcontrol/web.xml b/apps/i2pcontrol/web.xml new file mode 100644 index 0000000000..ab6913d000 --- /dev/null +++ b/apps/i2pcontrol/web.xml @@ -0,0 +1,15 @@ + + + + + net.i2p.i2pcontrol.servlets.JSONRPC2Servlet + net.i2p.i2pcontrol.servlets.JSONRPC2Servlet + 1 + + + + net.i2p.i2pcontrol.servlets.JSONRPC2Servlet + / + + + diff --git a/build.xml b/build.xml index cd90f3a52b..a00098713c 100644 --- a/build.xml +++ b/build.xml @@ -286,7 +286,7 @@ - + @@ -356,6 +356,11 @@ + + + + + @@ -834,6 +839,7 @@ + @@ -869,6 +875,7 @@ + @@ -1320,6 +1327,7 @@ + @@ -1648,6 +1656,7 @@ +