You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pivot.apache.org by sm...@apache.org on 2009/10/21 17:18:15 UTC

svn commit: r828043 - in /incubator/pivot/trunk/web: src/org/apache/pivot/web/ test/org/apache/pivot/web/test/

Author: smartini
Date: Wed Oct 21 15:18:15 2009
New Revision: 828043

URL: http://svn.apache.org/viewvc?rev=828043&view=rev
Log:
PIVOT-43 : Add support for digest authentication to web queries , (first version, working and tested with Apache HTTPD 2.0.x and 2.2.x)

Added:
    incubator/pivot/trunk/web/src/org/apache/pivot/web/DigestAuthentication.java
    incubator/pivot/trunk/web/src/org/apache/pivot/web/HexUtils.java
    incubator/pivot/trunk/web/src/org/apache/pivot/web/MD5.java
    incubator/pivot/trunk/web/test/org/apache/pivot/web/test/WebQueryTestClient.README
    incubator/pivot/trunk/web/test/org/apache/pivot/web/test/WebQueryTestClientBasic.java
    incubator/pivot/trunk/web/test/org/apache/pivot/web/test/WebQueryTestClientDigest.java

Added: incubator/pivot/trunk/web/src/org/apache/pivot/web/DigestAuthentication.java
URL: http://svn.apache.org/viewvc/incubator/pivot/trunk/web/src/org/apache/pivot/web/DigestAuthentication.java?rev=828043&view=auto
==============================================================================
--- incubator/pivot/trunk/web/src/org/apache/pivot/web/DigestAuthentication.java (added)
+++ incubator/pivot/trunk/web/src/org/apache/pivot/web/DigestAuthentication.java Wed Oct 21 15:18:15 2009
@@ -0,0 +1,733 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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.
+ */
+package org.apache.pivot.web;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.MessageDigest;
+
+import org.apache.pivot.collections.HashMap;
+import org.apache.pivot.collections.Map;
+import org.apache.pivot.web.HexUtils;
+import org.apache.pivot.web.MD5;
+import org.apache.pivot.util.concurrent.TaskExecutionException;
+
+/**
+ * Implementation of the {@link Authentication} interface supporting the
+ * HTTP <a href="http://tools.ietf.org/rfc/rfc2617.txt">Digest Authentication</a> scheme.
+ * <br/>
+ * Portions of code here are taken from Apache Tomcat, and from Apache Commons HTTP Client. 
+ *
+ * TODO:
+ *   
+ *   - important: 
+ *     -- make first query call from here ...
+ *     
+ *     -- verify if/how to cache the right authorization, one time done ...
+ *     -- verify if/how to reuse MessageDigest ...
+ *     -- see what FindBugs say ...
+ *
+ *   - verify at URL change if/how to recalculate values ...
+ *   
+ *   - verify how to reuse the authorization data (if already authenticated) ...
+ *   - verify if/how to handle the nonce count, for queries after the first ...
+ *   - verify if this works also with proxy ... and the same also for Basic Authentication
+ *   
+ *   - in the future:
+ *     -- verify if this works with redirects, proxy, etc ...
+ *     -- verify if implement also the algorithm "MD5-sess"
+ *     -- verify if implement also the algorithm for SHA1
+ *     -- verify in case of Single-Sign-On (SSO) if this is working ...
+ *     -- verify if make also an implementation (in other classes) 
+ *        of SSLAuthentication and JAASAuthentication ...
+ * 
+ * @see DigestAuthenticator and related classes from Tomcat 6 sources
+ * @see DigestScheme and related classes from HTTPCommons 3.1 sources
+ */
+public class DigestAuthentication implements Authentication {
+    private static final String HTTP_RESPONSE_AUTHENTICATE_HEADER_KEY = "WWW-Authenticate";
+    private static final String HTTP_REPLY_AUTHENTICATE_HEADER_KEY = "Authorization";
+    // private static final String HTTP_RESPONSE_AUTHENTICATED_HEADER_KEY = "Authentication-Info";
+    // private static final String HTTP_RESPONSE_MIME_TYPE_HEADER_KEY = "Content-Type";
+
+    private static final String HTTP_REPLY_FIELD_SEPARATOR = ":";
+
+    private static final String AUTH_FIELD_KEY_USERNAME = "username";
+    private static final String AUTH_FIELD_KEY_REALM = "realm";
+    private static final String AUTH_FIELD_KEY_RESPONSE = "response";
+    private static final String AUTH_FIELD_KEY_URI = "uri";
+    private static final String AUTH_FIELD_KEY_CNONCE = "cnonce";
+    private static final String AUTH_FIELD_KEY_NC = "nc";
+    private static final String AUTH_FIELD_KEY_NONCE = "nonce";
+    private static final String AUTH_FIELD_KEY_DOMAIN = "domain";
+    private static final String AUTH_FIELD_KEY_ALGORITHM = "algorithm";
+    private static final String AUTH_FIELD_KEY_QOP = "qop";
+
+    private static final String AUTH_FIELD_VALUE_QOP_AUTH = "auth";
+    private static final String AUTH_FIELD_VALUE_ALGORITHM_AUTH_MD5 = "MD5";
+    // private static final String AUTH_FIELD_VALUE_ALGORITHM_AUTH_MD5_SESSION = "MD5-sess";
+
+    private static final String AUTH_FIELD_VALUE_NC_FIRST = "00000001";
+
+    /** The Digest String constant, used in many places (from the standard). */
+    protected static final String DIGEST_KEY = "Digest ";
+
+    /**
+     * The default message digest algorithm to use if we cannot use the
+     * requested one.
+     */
+    protected static final String DEFAULT_ALGORITHM = "MD5";
+
+    /** The default private key. */
+    protected static final String PRIVATE_KEY = "Pivot";
+
+    private static final String EMPTY_STRING = "";
+    
+    /** The username */
+    private String username;
+
+    /** The password */
+    private String password;
+
+    /**
+     * The message digest algorithm to be used when generating session
+     * identifiers. This must be an algorithm supported by the
+     * <code>java.security.MessageDigest</code> class on your platform.
+     */
+    protected String algorithm = DEFAULT_ALGORITHM;
+
+    /** The (digest) encoding charset */
+    private String encoding;
+
+    /**
+     * Default constructor.
+     * 
+     * @deprecated Do not use. Null user name or Null Password are no longer
+     * allowed.
+     */
+    public DigestAuthentication() {
+        this(null, null, null, null);
+    }
+
+    /**
+     * The constructor with the username and password arguments.
+     * 
+     * @param username the user name
+     * @param password the password
+     */
+    public DigestAuthentication(String username, String password) {
+        this(username, password, DEFAULT_ALGORITHM, null);
+    }
+
+    /**
+     * The constructor with the username, password, and other invariant
+     * arguments.
+     * 
+     * @param username the user name
+     * @param password the password
+     * @param algorithm the algorithm to use (if null, the default will be used)
+     * @param encoding the encoding for Strings (for digesting strings)
+     */
+    public DigestAuthentication(String username, String password, String algorithm, String encoding) {
+        super();
+
+        if (username == null) {
+            throw new IllegalArgumentException("Username may not be null");
+        }
+        if (password == null) {
+            throw new IllegalArgumentException("Password may not be null");
+        }
+
+        this.username = username;
+        this.password = password;
+
+        this.algorithm = algorithm;
+
+        setDigestEncoding(encoding);
+    }
+
+    /**
+     * Return the User Name property.
+     * 
+     * @return the username
+     */
+    public String getUsername() {
+        return username;
+    }
+
+    /**
+     * Return the Password property.
+     * 
+     * @return the password
+     */
+    public String getPassword() {
+        return password;
+    }
+
+    /**
+     * Return the message digest algorithm for this Manager.
+     * 
+     * @return the algorithm
+     */
+    public String getAlgorithm() {
+        return algorithm;
+    }
+
+    /**
+     * Get this object string.
+     * 
+     * @return main data in a formed string
+     */
+    public String toString() {
+        StringBuffer result = new StringBuffer();
+
+        result.append(this.getClass().getName());
+        result.append(HTTP_REPLY_FIELD_SEPARATOR);
+        result.append(username);
+        result.append(HTTP_REPLY_FIELD_SEPARATOR);
+        result.append(password);
+        result.append(HTTP_REPLY_FIELD_SEPARATOR);
+        result.append(algorithm);
+        result.append(HTTP_REPLY_FIELD_SEPARATOR);
+        result.append(encoding);
+
+        return result.toString();
+    }
+
+    /**
+     * Returns the (digest) encoding charset.
+     * 
+     * @return The charset (may be null) for platform default
+     */
+    public String getDigestEncoding() {
+        return encoding;
+    }
+
+    /**
+     * Set the (digest) encoding to use.
+     * 
+     * @param encoding The encoding to use, or if null a default will be used
+     */
+    private void setDigestEncoding(String encoding) {
+        // this.encoding = encoding;
+
+        // String charset = getParameter("charset"); // future: read the charset from the response header
+        String charset = encoding;
+
+        // if a charset is not specified, default to ISO-8859-1
+        // if (charset == null)
+        // charset = "ISO-8859-1";
+
+        this.encoding = charset;
+        // log("charset = \"" + charset + "\"");
+    }
+
+    /**
+     * Returns the IP Address of the given Host.
+     * 
+     * @param hostName the host name, or if null the local host will be used
+     * @return the ID Address, as a String
+     */
+    protected static String getIPAddress(String hostName) throws UnknownHostException {
+        InetAddress inetAddr = null;
+
+        if (hostName == null || hostName.length() < 1) {
+            inetAddr = InetAddress.getByName(hostName);
+        }
+        else {
+            inetAddr = InetAddress.getLocalHost();
+        }
+
+        byte[] ipAddr = inetAddr.getAddress();
+
+        StringBuffer ipAddressAsString = new StringBuffer();
+        for (int i = 0; i < ipAddr.length; i++) {
+            if (i > 0) {
+                ipAddressAsString.append(".");
+            }
+
+            ipAddressAsString.append(ipAddr[i] & 0xFF);
+        }
+
+        return ipAddressAsString.toString();
+    }
+
+    /**
+     * Authenticate
+     * 
+     * @param query The Query
+     */
+    public void authenticate(Query<?> query) {
+
+        try {
+            String responseAuthenticateHeader = query.getResponseHeaders().get(
+                HTTP_RESPONSE_AUTHENTICATE_HEADER_KEY);
+// log("responseAuthenticateHeader " +
+// HTTP_RESPONSE_AUTHENTICATE_HEADER_KEY + " = \n" +
+// responseAuthenticateHeader + "\n");
+            if (responseAuthenticateHeader == null) {
+                // Digest authentication needs a first query to retrieve
+                // authentication hints from the server,
+                // then retry filling the missing values, so here i make the
+                // first query call here ...
+                try {
+                    query.execute();
+                } catch (TaskExecutionException tee) {
+                    // tee.printStackTrace(); // expected, so ignore it
+                }
+
+                responseAuthenticateHeader = query.getResponseHeaders().get(
+                    HTTP_RESPONSE_AUTHENTICATE_HEADER_KEY);
+// log("responseAuthenticateHeader " +
+// HTTP_RESPONSE_AUTHENTICATE_HEADER_KEY + " = \n" +
+// responseAuthenticateHeader + "\n");
+                if (responseAuthenticateHeader == null) {
+                    return;
+                }
+            } else if (!responseAuthenticateHeader.startsWith(DIGEST_KEY)) {
+                throw new RuntimeException(
+                    "authentication header not for this authentication method");
+            } else {
+                // TODO: (maybe for the future)
+                // authentication data already present:
+                // try to reuse it, reading hints from
+                // HTTP_RESPONSE_AUTHENTICATED_HEADER_KEY
+                // -- for example, the nonce_count value to use should be read from there ...
+                ;
+            }
+
+            String uri = query.getPath();
+            String method = query.getMethod().toString();
+
+            Map<String, String> responseAuthenticateHeaderMap = DigestAuthentication.splitAuthenticationHeader(responseAuthenticateHeader);
+
+            // nOnce: clients must only read this value
+            String nOnce = responseAuthenticateHeaderMap.get(AUTH_FIELD_KEY_NONCE);
+            if (nOnce == null) {
+                nOnce = "";
+            }
+
+            String realmName = responseAuthenticateHeaderMap.get(AUTH_FIELD_KEY_REALM);
+            if (realmName == null) {
+                realmName = "";
+            }
+
+            String algorithm = responseAuthenticateHeaderMap.get(AUTH_FIELD_KEY_ALGORITHM);
+            if (algorithm == null || algorithm.length() < 1) {
+                algorithm = AUTH_FIELD_VALUE_ALGORITHM_AUTH_MD5;
+            }
+
+            String qop = responseAuthenticateHeaderMap.get(AUTH_FIELD_KEY_QOP);
+            String cnonce = responseAuthenticateHeaderMap.get(AUTH_FIELD_KEY_CNONCE);
+            String nonce_count = responseAuthenticateHeaderMap.get(AUTH_FIELD_KEY_NC);
+            // rule from RFC
+            if (qop == null || qop.length() < 1) {
+                cnonce = ""; // safer empty value
+                nonce_count = ""; // = AUTH_FIELD_VALUE_NC_FIRST;
+            } else {
+                // String clientIP = DigestAuthentication.getIPAddress(null);
+                // cnonce = DigestAuthentication.generateUniqueToken(clientIP,
+                // key, digest);
+                cnonce = generateRandomValue();
+                nonce_count = AUTH_FIELD_VALUE_NC_FIRST;
+                // increment the (hex) value by 1
+                // or read from the response, and store for later queries ...
+                // -- see upper ...
+            }
+
+            String opaque = responseAuthenticateHeaderMap.get("opaque");
+            String response = calculateResponse(username, realmName, 
+                nOnce, nonce_count, cnonce, qop, method, uri);
+
+            StringBuffer authenticateHeader = new StringBuffer(512);
+            authenticateHeader.append(DIGEST_KEY);
+
+            authenticateHeader.append("username=\"");
+            authenticateHeader.append(username);
+            authenticateHeader.append("\"");
+            authenticateHeader.append(", realm=\"");
+            authenticateHeader.append(realmName);
+            authenticateHeader.append("\"");
+            authenticateHeader.append(", nonce=\"");
+            authenticateHeader.append(nOnce);
+            authenticateHeader.append("\"");
+            authenticateHeader.append(", uri=\"");
+            authenticateHeader.append(uri);
+            authenticateHeader.append("\"");
+            authenticateHeader.append(", response=\"");
+            authenticateHeader.append(response);
+            authenticateHeader.append("\"");
+
+            if (qop != null && qop.length() > 0) {
+                authenticateHeader.append(", qop=");
+                authenticateHeader.append(qop);
+                authenticateHeader.append(", nc=");
+                authenticateHeader.append(nonce_count);
+                authenticateHeader.append(", cnonce=\"");
+                authenticateHeader.append(cnonce);
+                authenticateHeader.append("\"");
+            }
+
+            authenticateHeader.append(", algorithm=");
+            authenticateHeader.append(algorithm);
+
+            if (opaque != null && opaque.length() > 0) {
+                authenticateHeader.append(", opaque=\"");
+                authenticateHeader.append(opaque);
+                authenticateHeader.append("\"");
+            }
+
+// log("authenticateHeader " + HTTP_REPLY_AUTHENTICATE_HEADER_KEY +
+// " = \n" + authenticateHeader.toString() + "\n");
+
+            query.getRequestHeaders().put(HTTP_REPLY_AUTHENTICATE_HEADER_KEY,
+                authenticateHeader.toString());
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+    }
+
+    /**
+     * Return the digested response (only this field) value to put in the reply
+     * authentication header, as described in RFC 2069; otherwise return
+     * <code>null</code>.
+     * 
+     * @param username Username of the Principal to look up
+     * @param realm Realm name
+     * @param nOnce Unique (or supposedly unique) token which has been used for
+     * this request
+     * @param nc Number of query (starting from 1) with this authentication info
+     * @param cnonce Value (usually random) chosen by the client
+     * @param qop Quality Of Protection flag
+     * @param method The HTTP method
+     * @param uri The URI of the query
+     * @return the digested value for the response field
+     */
+    private String calculateResponse(final String username, final String realm, final String nOnce,
+        final String nc, final String cnonce, final String qop, final String method,
+        final String uri) {
+        String response = null;
+
+        String md5a1 = getDigestUsernameAndRealm(getAlgorithm(), username, realm);
+        // log("md5a1 = \"" + md5a1 + "\"");
+        if (md5a1 == null) {
+            return null;
+        }
+
+        String md5a2 = getDigestMethodAndUri(qop, method, uri);
+        // log("md5a2 = \"" + md5a2 + "\"");
+        if (md5a2 == null) {
+            return null;
+        }
+
+        StringBuffer sbCompleteValue = new StringBuffer(512);
+        sbCompleteValue.append(md5a1);
+
+        sbCompleteValue.append(HTTP_REPLY_FIELD_SEPARATOR);
+        sbCompleteValue.append((nOnce != null) ? nOnce : "");
+
+        sbCompleteValue.append(HTTP_REPLY_FIELD_SEPARATOR);
+        sbCompleteValue.append((nc != null) ? nc : "");
+
+        sbCompleteValue.append(HTTP_REPLY_FIELD_SEPARATOR);
+        sbCompleteValue.append((cnonce != null) ? cnonce : "");
+
+        sbCompleteValue.append(HTTP_REPLY_FIELD_SEPARATOR);
+        sbCompleteValue.append((qop != null) ? qop : "");
+
+        sbCompleteValue.append(HTTP_REPLY_FIELD_SEPARATOR);
+        sbCompleteValue.append(md5a2);
+
+        String a3 = sbCompleteValue.toString();
+// log("a3 = \"" + a3 + "\"");
+        byte[] digestBytes = MD5.digest(a3, getDigestEncoding());
+        String md5a3 = MD5.encode(digestBytes);
+// log("md5a3 = \"" + md5a3 + "\"");
+
+        response = md5a3;
+        return response;
+    }
+
+    /**
+     * Removes the quotes on a string. RFC2617 states quotes are optional for
+     * all parameters except realm.
+     * 
+     * @param quotedString The string with enclosing quotes
+     * @param quotesRequired If quotes are required on the previous parameter
+     */
+    protected static String removeQuotes(final String quotedString, boolean quotesRequired) {
+        // support both quoted and non-quoted
+        if (quotedString.length() > 0 && quotedString.charAt(0) != '"' && !quotesRequired) {
+            return quotedString;
+        } else if (quotedString.length() > 2) {
+            return quotedString.substring(1, quotedString.length() - 1);
+        } else {
+            return DigestAuthentication.EMPTY_STRING;
+        }
+    }
+
+    /**
+     * Removes the quotes on a string.
+     * 
+     * @param quotedString The string with enclosing quotes
+     */
+    protected static String removeQuotes(String quotedString) {
+        return removeQuotes(quotedString, false);
+    }
+
+    /**
+     * Utility method that returns in a Map all the fields inside the given
+     * Authentication Header (returned from the Server after a first query
+     * without authentication or without successful authentication). Note that
+     * some of these info are required to construct other fields for the
+     * following reply.
+     * 
+     * @param authorizationHeader the authorization info returned from the
+     * Server.
+     * @return a Map of key / value, both Strings
+     */
+    protected static Map<String, String> splitAuthenticationHeader(String authorizationHeader) {
+// log("Authorization header: " + authorizationHeader);
+        Map<String, String> map = new HashMap<String, String>();
+
+        // Validate the authorization credentials format
+        if (authorizationHeader == null) {
+            return map;
+        }
+        else if (!authorizationHeader.startsWith(DIGEST_KEY)) {
+            return map;
+        }
+
+        authorizationHeader = authorizationHeader.substring(DIGEST_KEY.length()).trim();
+
+        String[] tokens = authorizationHeader.split(",(?=(?:[^\"]*\"[^\"]*\")+$)");
+
+        String userName = null;
+        String realmName = null;
+        String nOnce = null;
+        String uri = null;
+        String domain = null;
+        String response = null;
+        String nc = null;
+        String cNonce = null;
+        String qop = null;
+
+        for (int i = 0; i < tokens.length; i++) {
+            String currentToken = tokens[i];
+            if (currentToken.length() == 0) {
+                continue;
+            }
+
+            int equalSign = currentToken.indexOf('=');
+            if (equalSign < 0) {
+                return null;
+            }
+
+            String currentTokenName = currentToken.substring(0, equalSign).trim();
+            String currentTokenValue = currentToken.substring(equalSign + 1).trim();
+// log("tokens[" + i + "]: currentTokenName = \"" + currentTokenName
+// + "\", currentTokenValue = \"" + currentTokenValue + "\"");
+
+            if (AUTH_FIELD_KEY_USERNAME.equals(currentTokenName)) {
+                userName = removeQuotes(currentTokenValue);
+                map.put(AUTH_FIELD_KEY_USERNAME, userName);
+            } else if (AUTH_FIELD_KEY_REALM.equals(currentTokenName)) {
+                realmName = removeQuotes(currentTokenValue, true);
+                map.put(AUTH_FIELD_KEY_REALM, realmName);
+            } else if (AUTH_FIELD_KEY_NONCE.equals(currentTokenName)) {
+                nOnce = removeQuotes(currentTokenValue);
+                map.put(AUTH_FIELD_KEY_NONCE, nOnce);
+            } else if (AUTH_FIELD_KEY_NC.equals(currentTokenName)) {
+                nc = removeQuotes(currentTokenValue);
+                map.put(AUTH_FIELD_KEY_NC, nc);
+            } else if (AUTH_FIELD_KEY_CNONCE.equals(currentTokenName)) {
+                cNonce = removeQuotes(currentTokenValue);
+                map.put(AUTH_FIELD_KEY_CNONCE, cNonce);
+            } else if (AUTH_FIELD_KEY_QOP.equals(currentTokenName)) {
+                qop = removeQuotes(currentTokenValue);
+                map.put(AUTH_FIELD_KEY_QOP, qop);
+            } else if (AUTH_FIELD_KEY_URI.equals(currentTokenName)) {
+                uri = removeQuotes(currentTokenValue);
+                map.put(AUTH_FIELD_KEY_URI, uri);
+            } else if (AUTH_FIELD_KEY_DOMAIN.equals(currentTokenName)) {
+                domain = removeQuotes(currentTokenValue);
+                map.put(AUTH_FIELD_KEY_DOMAIN, domain);
+            } else if (AUTH_FIELD_KEY_ALGORITHM.equals(currentTokenName)) {
+                map.put(AUTH_FIELD_KEY_ALGORITHM, removeQuotes(currentTokenValue));
+            } else if (AUTH_FIELD_KEY_RESPONSE.equals(currentTokenName)) {
+                response = removeQuotes(currentTokenValue);
+                map.put(AUTH_FIELD_KEY_RESPONSE, response);
+            } else {
+// log("Unknown token name: \"" + currentTokenName + "\"");
+                map.put(currentTokenName, currentTokenValue);
+            }
+
+        }
+
+        if ((realmName == null) || (nOnce == null)) {
+// log("One of the mandatory fields is null , returning an empty map");
+            return new HashMap<String, String>();
+        } else {
+            return map;
+        }
+
+    }
+
+    /**
+     * Generate a unique token. The token is generated according to the
+     * following pattern: uniqueToken = Base64 ( MD5 ( client-IP ":" time-stamp
+     * ":" private-key ) ).
+     * 
+     * @param clientIP the IP address of the Client, as a String
+     * @param privateKey the private key, as a String
+     * @param digest the MessageDigest to use
+     */
+    protected static String generateUniqueToken(final String clientIP, final String privateKey,
+        final MessageDigest digest) {
+        if (clientIP == null || clientIP.length() < 7 || clientIP.length() > 15) {
+            throw new IllegalArgumentException(
+                "clientIP must be a valid IP address, or at least 127.0.0.1");
+        }
+        if (privateKey == null || privateKey.length() < 1) {
+            throw new IllegalArgumentException("privateKey must be a valid (not empty) String");
+        }
+
+        long currentTime = System.currentTimeMillis();
+
+        StringBuffer onceValue = new StringBuffer();
+        onceValue.append(clientIP);
+        onceValue.append(HTTP_REPLY_FIELD_SEPARATOR);
+        onceValue.append(currentTime);
+        onceValue.append(HTTP_REPLY_FIELD_SEPARATOR);
+        onceValue.append(privateKey);
+
+        String onceValueString = onceValue.toString();
+
+        byte[] buffer = null;
+        synchronized (digest) {
+            buffer = digest.digest(onceValueString.getBytes());
+        }
+        onceValueString = MD5.encode(buffer);
+
+        return onceValueString;
+    }
+
+    /**
+     * Creates a random value based on the current time.
+     * 
+     * @return The calculated value as aString, or null if an error occurs
+     */
+    public String generateRandomValue() {
+        String randomValue = Long.toString(System.currentTimeMillis());
+        byte[] digestBytes = MD5.digest(randomValue, getDigestEncoding());
+
+        return MD5.encode(digestBytes);
+    }
+
+    /**
+     * Return the digest associated with given user name and realm.
+     * 
+     * @param algorithm The algorithm to use
+     * @param username Username of the Principal to look up
+     * @param realm Realm name
+     */
+    protected String getDigestUsernameAndRealm(final String algorithm, final String username,
+        final String realmName) {
+        String a1 = null;
+        if (AUTH_FIELD_VALUE_ALGORITHM_AUTH_MD5.equals(algorithm)) {
+            StringBuffer bufferValue = new StringBuffer();
+            bufferValue.append(username);
+            bufferValue.append(HTTP_REPLY_FIELD_SEPARATOR);
+            bufferValue.append(realmName);
+            bufferValue.append(HTTP_REPLY_FIELD_SEPARATOR);
+            bufferValue.append(password);
+
+            a1 = bufferValue.toString();
+        } else {
+            a1 = "";
+        }
+// log("a1 = \"" + a1 + "\"");
+
+        byte[] digestBytes = MD5.digest(a1, getDigestEncoding());
+        return MD5.encode(digestBytes);
+    }
+
+    /**
+     * Return the digest associated with given method and uri.
+     * 
+     * @param method The HTTP method
+     * @param uri The URI of the query
+     */
+    protected String getDigestMethodAndUri(final String qop, final String method, final String uri) {
+        String a2 = null;
+        if (qop == null || AUTH_FIELD_VALUE_QOP_AUTH.equals(qop)) {
+            StringBuffer bufferValue = new StringBuffer();
+            bufferValue.append(method);
+            bufferValue.append(HTTP_REPLY_FIELD_SEPARATOR);
+            bufferValue.append(uri);
+
+            a2 = bufferValue.toString();
+        } else {
+            a2 = ""; // dummy, to avoid NPE ...
+            throw new RuntimeException("not supported qop value: \"" + qop + "\"");
+        }
+// log("a2 = \"" + a2 + "\"");
+
+        byte[] digestBytes = MD5.digest(a2, getDigestEncoding());
+        return MD5.encode(digestBytes);
+    }
+
+    /**
+     * Digest the given string using the algorithm specified and convert the
+     * result to a corresponding hex string. If exception, the plain credentials
+     * string is returned
+     * 
+     * @param value the (input) value to digest
+     * @param algorithm Algorithm used to do the digest
+     * @param encoding Character encoding of the string to digest
+     */
+    public final static String digestString(final String value, final String algorithm,
+        final String encoding) {
+
+        try {
+            // Obtain a new message digest with "digest" encryption
+            MessageDigest md = (MessageDigest) MessageDigest.getInstance(algorithm).clone();
+
+            // encode the credentials
+            // Should use the digestEncoding, but that's not a static field
+            if (encoding == null) {
+                md.update(value.getBytes());
+            } else {
+                md.update(value.getBytes(encoding));
+            }
+
+            // Digest the credentials and return as hexadecimal
+            return (HexUtils.convert(md.digest()));
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+    }
+
+// /** Utility method, mainly for debugging purposes */
+// protected void log(String msg)
+// {
+// System.out.println(msg);
+// }
+
+}

Added: incubator/pivot/trunk/web/src/org/apache/pivot/web/HexUtils.java
URL: http://svn.apache.org/viewvc/incubator/pivot/trunk/web/src/org/apache/pivot/web/HexUtils.java?rev=828043&view=auto
==============================================================================
--- incubator/pivot/trunk/web/src/org/apache/pivot/web/HexUtils.java (added)
+++ incubator/pivot/trunk/web/src/org/apache/pivot/web/HexUtils.java Wed Oct 21 15:18:15 2009
@@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.pivot.web;
+
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Library of utility methods useful in dealing with converting byte arrays
+ * to and from strings of hexadecimal digits.
+ * <br/>
+ * Portions of code here are taken from Apache Tomcat. 
+ *  
+ * @see org.apache.catalina.util.HexUtils;
+ */
+public final class HexUtils {
+
+    /** Hexadecimal standard chars. */
+    public static final char[] hexadecimal = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+        'a', 'b', 'c', 'd', 'e', 'f' };
+
+    /** Table for HEX to DEC byte translation */
+    public static final int[] DEC = { -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, -1, -1, 00, 01, 02, 03, 04, 05, 06, 07, 8, 9, -1, -1, -1,
+        -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, -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, 10, 11, 12, 13, 14, 15, -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, -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, -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, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, };
+
+    /** Constructor for private usage */
+    private HexUtils() {
+    }
+
+    /**
+     * Convert a String of hexadecimal digits into the corresponding byte array
+     * by encoding each two hexadecimal digits as a byte.
+     * 
+     * @param digits Hexadecimal digits representation
+     * @exception IllegalArgumentException if an invalid hexadecimal digit is
+     * found, or the input string contains an odd number of hexadecimal digits
+     */
+    public static byte[] convert(String digits) {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+        for (int i = 0; i < digits.length(); i += 2) {
+            char c1 = digits.charAt(i);
+            if ((i + 1) >= digits.length()) {
+                throw new IllegalArgumentException("bad hex input data");
+            }
+
+            char c2 = digits.charAt(i + 1);
+            byte b = 0;
+            if ((c1 >= '0') && (c1 <= '9')) {
+                b += ((c1 - '0') * 16);
+            }
+            else if ((c1 >= 'a') && (c1 <= 'f')) {
+                b += ((c1 - 'a' + 10) * 16);
+            }
+            else if ((c1 >= 'A') && (c1 <= 'F')) {
+                b += ((c1 - 'A' + 10) * 16);
+            }
+            else {
+                throw new IllegalArgumentException("bad hex input data");
+            }
+
+            if ((c2 >= '0') && (c2 <= '9')) {
+                b += (c2 - '0');
+            }
+            else if ((c2 >= 'a') && (c2 <= 'f')) {
+                b += (c2 - 'a' + 10);
+            }
+            else if ((c2 >= 'A') && (c2 <= 'F')) {
+                b += (c2 - 'A' + 10);
+            }
+            else {
+                throw new IllegalArgumentException("bad hex input data");
+            }
+
+            baos.write(b);
+        }
+
+        return (baos.toByteArray());
+    }
+
+    /**
+     * Convert a byte array into a printable format containing a String of
+     * hexadecimal digit characters (two per byte).
+     * 
+     * @param bytes Byte array representation
+     */
+    public static String convert(byte bytes[]) {
+        StringBuffer sb = new StringBuffer(bytes.length * 2);
+        for (int i = 0; i < bytes.length; i++) {
+            sb.append(convertDigit((int) (bytes[i] >> 4)));
+            sb.append(convertDigit((int) (bytes[i] & 0x0f)));
+        }
+
+        return (sb.toString());
+    }
+
+    /**
+     * Convert 4 hex digits to an int, and return the number of converted bytes.
+     * 
+     * @param hex Byte array containing exactly four hexadecimal digits
+     * @exception IllegalArgumentException if an invalid hexadecimal digit is
+     * included
+     */
+    public static int convert2Int(byte[] hex) {
+        // assert valid data
+        int len;
+        if (hex.length < 4) {
+            return 0;
+        }
+        if (DEC[hex[0]] < 0) {
+            throw new IllegalArgumentException("bad hex input data");
+        }
+
+        len = DEC[hex[0]];
+        len = len << 4;
+        if (DEC[hex[1]] < 0) {
+            throw new IllegalArgumentException("bad hex input data");
+        }
+        len += DEC[hex[1]];
+        len = len << 4;
+        if (DEC[hex[2]] < 0) {
+            throw new IllegalArgumentException("bad hex input data");
+        }
+        len += DEC[hex[2]];
+        len = len << 4;
+        if (DEC[hex[3]] < 0) {
+            throw new IllegalArgumentException("bad hex input data");
+        }
+
+        len += DEC[hex[3]];
+        return len;
+    }
+
+    /**
+     * [Private] Convert the specified value (0 .. 15) to the corresponding
+     * hexadecimal digit.
+     * 
+     * @param value Value to be converted
+     */
+    private static char convertDigit(int value) {
+        value &= 0x0f;
+
+        if (value >= 10) {
+            return ((char) (value - 10 + 'a'));
+        }
+        else {
+            return ((char) (value + '0'));
+        }
+
+    }
+
+    /**
+     * Given a String and the encoding, returns it as a byte array.
+     * 
+     * @param string the string
+     * @param encoding the encoding, or if null a default will be used
+     * @return the string transformed to byte array
+     * @throws UnsupportedEncodingException
+     */
+    public static final byte[] toByteArray(final String string, final String encoding)
+        throws UnsupportedEncodingException {
+        byte[] bytes = null;
+        if (encoding == null) {
+            bytes = string.getBytes();
+        }
+        else {
+            bytes = string.getBytes(encoding);
+        }
+
+        return bytes;
+    }
+
+}

Added: incubator/pivot/trunk/web/src/org/apache/pivot/web/MD5.java
URL: http://svn.apache.org/viewvc/incubator/pivot/trunk/web/src/org/apache/pivot/web/MD5.java?rev=828043&view=auto
==============================================================================
--- incubator/pivot/trunk/web/src/org/apache/pivot/web/MD5.java (added)
+++ incubator/pivot/trunk/web/src/org/apache/pivot/web/MD5.java Wed Oct 21 15:18:15 2009
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.pivot.web;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Encode / Decode an MD5 digest into / from a String.
+ * <p>
+ * The 128 bit MD5 hash is converted into a 32 character long String.
+ * Each character of the String is the hexadecimal representation of 4 bits
+ * of the digest.
+ * <br/>
+ * Portions of code here are taken from Apache Tomcat. 
+ *
+ * @see org.apache.catalina.util.MD5Encoder
+ */
+public final class MD5 {
+    public static final int MD5_DIGEST_LENTGH_IN_BYTES = 16;
+    public static final String MD5_ALGORITHM_NAME = "MD5";
+
+    /** The MD5 message digest generator. */
+    private static MessageDigest md5;
+
+    /** Constructor for private usage */
+    private MD5() {
+    }
+
+    /**
+     * Retrieves a byte sequence representing the MD5 digest of the specified
+     * byte sequence. Note that any Exception is handled inside.
+     * 
+     * @param data the data to digest.
+     * @return the MD5 digest as an array of 16 bytes.
+     */
+    public static final byte[] digest(byte[] data) {
+        if (data == null) {
+            throw new IllegalArgumentException("null data");
+        }
+
+        byte[] dataDigested = null;
+
+        synchronized (MD5.class) {
+            if (md5 == null) {
+                try {
+                    md5 = MessageDigest.getInstance(MD5_ALGORITHM_NAME);
+                } catch (NoSuchAlgorithmException e) {
+                    // e.printStackTrace();
+                    md5 = null;
+                }
+            }
+
+            if (md5 != null) {
+                dataDigested = md5.digest(data);
+            }
+
+        }
+
+        return dataDigested;
+    }
+
+    /**
+     * Transform the given string in a byte array, using the given encoding.
+     * Note that any Exception is handled inside.
+     * 
+     * @param string the string
+     * @param encoding the encoding, or if null a default will be used
+     * @return the string transformed to byte array
+     */
+    public static final byte[] digest(final String string, final String encoding) {
+        byte[] data = null;
+        try {
+            data = HexUtils.toByteArray(string, encoding);
+        } catch (UnsupportedEncodingException e) {
+            // e.printStackTrace();
+        }
+
+        byte[] dataDigested = digest(data);
+        return dataDigested;
+    }
+
+    public static boolean isEqual(byte[] digesta, byte[] digestb) {
+        // Two arrays are equal if they have the same length and each element
+        // is equal to the corresponding element in the other array;
+        // otherwise, they’re not.
+
+        if (digesta == null || digestb == null) {
+            return false;
+        }
+        else if (digesta.length == MD5_DIGEST_LENTGH_IN_BYTES
+            && digestb.length == MD5_DIGEST_LENTGH_IN_BYTES) {
+            for (int i = 0; i < MD5_DIGEST_LENTGH_IN_BYTES; i++) {
+                if (digesta[i] != digestb[i])
+                    return false;
+            }
+
+            return true;
+        } else {
+            return false;
+        }
+
+    }
+
+    /**
+     * Encodes the 128 bit (16 bytes) MD5 into a 32 character String.
+     * 
+     * @param binaryData The Array containing the digest
+     * @return Encoded MD5, or null if encoding failed
+     */
+    public static final String encode(byte[] binaryData) {
+        if (binaryData.length != MD5_DIGEST_LENTGH_IN_BYTES) {
+            throw new IllegalArgumentException("binaryData must be an array of 16 bytes");
+        }
+
+        char[] buffer = new char[MD5_DIGEST_LENTGH_IN_BYTES * 2];
+
+        for (int i = 0; i < MD5_DIGEST_LENTGH_IN_BYTES; i++) {
+            int low = (int) (binaryData[i] & 0x0f);
+            int high = (int) ((binaryData[i] & 0xf0) >> 4);
+
+            buffer[i * 2] = HexUtils.hexadecimal[high];
+            buffer[i * 2 + 1] = HexUtils.hexadecimal[low];
+        }
+
+        return new String(buffer);
+    }
+
+    /**
+     * Decodes the specified base64 string back into its raw data.
+     * 
+     * @param encodedData The MD5 encoded string.
+     */
+    public static final byte[] decode(String encodedData) {
+        if (encodedData.length() != 32) {
+            throw new IllegalArgumentException("encodedData must be a String of length 32 chars");
+        }
+
+        throw new UnsupportedOperationException("Decoding from an MD5 Hash is not supported");
+    }
+
+}

Added: incubator/pivot/trunk/web/test/org/apache/pivot/web/test/WebQueryTestClient.README
URL: http://svn.apache.org/viewvc/incubator/pivot/trunk/web/test/org/apache/pivot/web/test/WebQueryTestClient.README?rev=828043&view=auto
==============================================================================
--- incubator/pivot/trunk/web/test/org/apache/pivot/web/test/WebQueryTestClient.README (added)
+++ incubator/pivot/trunk/web/test/org/apache/pivot/web/test/WebQueryTestClient.README Wed Oct 21 15:18:15 2009
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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.
+ */
+
+// 
+// WebQueryTestClientDigest.README
+// 
+
+This README file explains the setup required for the right execution of WebQueryTestClientBasic and WebQueryTestClientDigest Integration Tests with JUnit 4.
+
+Requirements:
+- Apache 2.x Web Server installed and working on localhost
+- JUnit 4.x Test Runner, like that inside Eclipse
+
+
+Test Setup:
+- in the main Apache configuration file (APACHE_HOME/conf/httpd.conf) make some modify:
+
+enable the digest authentication module, removing the comment on it, for example:
+# LoadModule auth_digest_module modules/mod_auth_digest.so
+LoadModule auth_digest_module modules/mod_auth_digest.so
+
+Note that the module for the Basic Authentication (needed by the other class WebQueryTestClientBasic), by default is already enabled in Apache.
+
+
+Then, enable the usage of .htaccess files on the root folder (by default htdocs), for a simpler per-directory authentication and authorization setup:
+
+#     AllowOverride None
+    AllowOverride All
+or
+    AllowOverride AuthConfig    
+
+Note that this is only a convenience setting, targeted at these tests.
+
+
+- test that Apache works, starting/restarting it, to ensure the configuration is right
+
+- go into the published directory (htdocs, or public_html, depending on the environment) and create the following empty directories: 
+public (optional), 
+protected (for testing basic authentication), 
+protected_digest (for testing digest authentication), 
+dir (for testing digest authentication, using the same values from the RFC)
+
+Copy some files (for example one binary like test.jpg, and one of text like test.txt) inside any of the previously-created directories.
+In the dir directory, put a index.html file (required by the test, because all tests as explained in the RFC use it).
+
+
+- in the "dir" directory, create a (text) file named .htaccess , and put inside this:
+AuthType Digest
+
+# Note that with Digest Authentication, the value of AuthName must be the same as the realm
+AuthName "testrealm@host.com"
+
+AuthDigestDomain /dir/ 
+AuthDigestFile  "htdocs/dir/.htpasswd"
+Require valid-user
+
+# Hack for enabling pre IE 7 to work good with Digest Authentication
+BrowserMatch "MSIE" AuthDigestEnableQueryStringHack=On
+
+
+- in the "protected" dir (required by Basic Test), create a (text) file named .htaccess , and put inside this:
+AuthType Basic
+AuthName "Password Required"
+
+AuthUserFile  "htdocs/protected/.htpasswd"
+Require valid-user
+
+
+- in the "protected_digest" dir, create a (text) file named .htaccess , and put inside this:
+AuthType Digest
+
+# Note that with Digest Authentication, the value of AuthName must be the same as the realm
+AuthName "Password Required (Digest)"
+
+AuthDigestDomain /protected_digest/ 
+AuthDigestFile  "htdocs/protected_digest/.htpasswd"
+Require valid-user
+
+# Hack for enabling pre IE 7 to work good with Digest Authentication
+BrowserMatch "MSIE" AuthDigestEnableQueryStringHack=On
+
+
+- from the prompt, use some standard Apache commands to create the digested passwords.
+If Apache commands are available in PATH, go directly in the target directory, 
+otherwise for example you'll have to go to the main apache directory and launch them using for example bin\ht*.exe (for Windows), 
+and then move the generated file into the right directory.
+
+Optional, for the "protected" (Basic Authentication) for example use:
+htpasswd -c .htpasswd test
+htpasswd will ask you for the password (in this case "test0", but without quotes), and then ask you to type it again to confirm it
+
+For the "protected_digest" (Digest Authentication) for example use:
+htdigest -c .htpasswd "Password Required (Digest)" test
+htdigest will ask you for the password (in this case "test0", but without quotes), and then ask you to type it again to confirm it
+
+For the "dir" (Digest Authentication) for example use:
+htdigest -c .htpasswd "testrealm@host.com" Mufasa
+htdigest will ask you for the password (in this case "Circle Of Life", but without quotes), and then ask you to type it again to confirm it
+
+
+- manual test using a browser (to ensure the directories has been protected in the right way): 
+point to the URL http://localhost/ , and then add (by hand) the subdirectory names, and try to access.
+Note that all protected directories are not visible from here.
+Any time required, the browser will open an Authentication popup, where to put username/password data (always without quotes).
+
+"protected/" needs username "test" and password "test0"
+"protected_digest/" needs username "test" and password "test0"
+"dir/" needs username "Mufasa" and password "Circle Of Life"
+
+
+- execute the Test class, for example by running it from Eclipse (or by prompt), using JUnit 4, and see results.
+
+
+// 
+// End of Readme.
+// 

Added: incubator/pivot/trunk/web/test/org/apache/pivot/web/test/WebQueryTestClientBasic.java
URL: http://svn.apache.org/viewvc/incubator/pivot/trunk/web/test/org/apache/pivot/web/test/WebQueryTestClientBasic.java?rev=828043&view=auto
==============================================================================
--- incubator/pivot/trunk/web/test/org/apache/pivot/web/test/WebQueryTestClientBasic.java (added)
+++ incubator/pivot/trunk/web/test/org/apache/pivot/web/test/WebQueryTestClientBasic.java Wed Oct 21 15:18:15 2009
@@ -0,0 +1,275 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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.
+ */
+package org.apache.pivot.web.test;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.apache.pivot.serialization.ByteArraySerializer;
+import org.apache.pivot.web.Authentication;
+import org.apache.pivot.web.BasicAuthentication;
+import org.apache.pivot.web.GetQuery;
+import org.apache.pivot.web.QueryException;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Integration test of Client-side Authentication with the Basic method.
+ * <br/>
+ * This is a JUnit 4 Test, but should be excluded from usual (Unit) Test Suite.
+ * <br/>
+ * To Run these tests, a local instance of Apache must be started and
+ * configured with the required resources (dir /public , and dir /protected
+ * protected with basic authentication) and files. 
+ * Then, before to run these tests, ensure basic authentication has been 
+ * successfully setup asking the same URLs from a Web Browser. 
+ * 
+ * TODO: 
+ *   - test other HTTP methods ...
+ * 
+ */
+public class WebQueryTestClientBasic {
+    final static String HOSTNAME = "localhost";
+    final static String PATH = null;
+    final static int PORT = 80;
+    final static boolean SECURE = false;
+
+    final static String PATH_PUBLIC = "/public/";
+    final static String PATH_PROTECTED_BASIC = "/protected/";
+
+    final static String SAMPLE_FILE_BINARY = "test.jpg";
+    final static String SAMPLE_FILE_TEXT = "test.txt";
+
+    final static String USER_NAME = "test";
+    final static String USER_PASSWORD = "test0";
+
+    final static long TIMEOUT = 5000l; // default timeout for WebQuery tests
+                                       // here: 5 sec
+
+    String host = null;
+    int port = 0;
+    String path = null;
+
+    Authentication authentication = null;
+
+    Object result = null;
+
+    public void log(String msg) {
+        System.out.println(msg);
+    }
+
+    @BeforeClass
+    public static void runBeforeClass() {
+        // run for one time before all test cases
+    }
+
+    @AfterClass
+    public static void runAfterClass() {
+        // run for one time after all test cases
+    }
+
+    @Before
+    public void runBeforeEveryTest() {
+        // run before any single test case
+    }
+
+    @After
+    public void runAfterEveryTest() {
+        // run after any single test case
+        host = null;
+        port = 0;
+        path = null;
+
+        authentication = null;
+
+        result = null;
+    }
+
+    @Test(timeout = 10000, expected = QueryException.class)
+    public void publicOnApache_noauth_NotExistingHost() throws QueryException {
+        log("publicOnApache_noauth_NotExistingHost()");
+
+        host = "non_existing_host";
+        port = PORT;
+        path = PATH_PUBLIC;
+
+        GetQuery query = new GetQuery(host, port, path, SECURE);
+        query.setTimeout(TIMEOUT);
+        log("GET Query to " + query.getLocation());
+
+        result = query.execute();
+        assertNull(result);
+
+        log("Query result: \n" + result);
+    }
+
+    @Test(timeout = 10000, expected = QueryException.class)
+    public void publicOnApache_noauth_localhost_NotExistingResource() throws QueryException {
+        log("publicOnApache_noauth_localhost_NotExistingResource()");
+
+        host = HOSTNAME;
+        port = PORT;
+        path = PATH_PUBLIC + "non_existing_resource";
+
+        GetQuery query = new GetQuery(host, port, path, SECURE);
+        query.setTimeout(TIMEOUT);
+        log("GET Query to " + query.getLocation());
+
+        result = query.execute();
+        assertNull(result);
+
+        log("Query result: \n" + result);
+    }
+
+    @Test(timeout = 10000)
+    public void publicOnApache_noauth_localhost_testFile() throws QueryException {
+        log("publicOnApache_noauth_localhost_testFile()");
+
+        host = HOSTNAME;
+        port = PORT;
+        path = PATH_PUBLIC + SAMPLE_FILE_TEXT;
+
+        GetQuery query = new GetQuery(host, port, path, SECURE);
+
+        // attention, don't use BinarySerializer here, but instead use the
+        // generic ByteArraySerializer
+        query.setSerializer(new ByteArraySerializer());
+        query.setTimeout(TIMEOUT);
+        log("GET Query to " + query.getLocation());
+
+        result = query.execute();
+        assertNotNull(result);
+
+        // dump content, but useful only for text resources ...
+        String dump = // result.toString()
+        // Arrays.toString((byte []) result);
+        new String((byte[]) result);
+        log("Query result: " + (dump.getBytes().length) + " bytes \n" + dump);
+    }
+
+    @Test(timeout = 10000)
+    public void publicOnApache_basic_localhost_forceUnnecessaryAuthentication()
+        throws QueryException {
+        log("publicOnApache_basic_localhost_forceUnnecessaryAuthentication()");
+
+        host = HOSTNAME;
+        port = PORT;
+        path = PATH_PUBLIC + SAMPLE_FILE_TEXT;
+
+        GetQuery query = new GetQuery(host, port, path, SECURE);
+
+        // attention, don't use BinarySerializer here, but instead use the
+        // generic ByteArraySerializer
+        query.setSerializer(new ByteArraySerializer());
+        query.setTimeout(TIMEOUT);
+        log("GET Query to " + query.getLocation());
+
+        authentication = new BasicAuthentication(USER_NAME, USER_PASSWORD);
+        authentication.authenticate(query);
+
+        result = query.execute();
+        assertNotNull(result);
+
+        // dump content, but useful only for text resources ...
+        String dump = // result.toString()
+        // Arrays.toString((byte []) result);
+        new String((byte[]) result);
+        log("Query result: " + (dump.getBytes().length) + " bytes \n" + dump);
+    }
+
+    // @Test(timeout = 10000, expected = QueryException.class)
+    @Test(timeout = 1000000, expected = QueryException.class)
+    // for debugging the execution
+    public void protectedOnApache_basic_localhostWithoutAuthenticate() throws QueryException {
+        log("protectedOnApache_basic_localhostWithoutAuthenticate()");
+
+        host = HOSTNAME;
+        port = PORT;
+        path = PATH_PROTECTED_BASIC + SAMPLE_FILE_TEXT;
+
+        GetQuery query = new GetQuery(host, port, path, SECURE);
+        query.setSerializer(new ByteArraySerializer());
+        query.setTimeout(TIMEOUT);
+        log("GET Query to " + query.getLocation());
+
+        result = query.execute();
+        log("Query result: \n" + result);
+
+        assertNull(result);
+    }
+
+    @Test(timeout = 10000, expected = QueryException.class)
+    public void protectedOnApache_basic_localhostWithWrongCredentials() throws QueryException {
+        log("protectedOnApache_basic_localhostWithWrongCredentials()");
+
+        host = HOSTNAME;
+        port = PORT;
+        path = PATH_PROTECTED_BASIC + SAMPLE_FILE_TEXT;
+
+        GetQuery query = new GetQuery(host, port, path, SECURE);
+        query.setSerializer(new ByteArraySerializer());
+        query.setTimeout(TIMEOUT);
+        log("GET Query to " + query.getLocation());
+
+        authentication = new BasicAuthentication("wrongUsername", "wrongPassword");
+        authentication.authenticate(query);
+
+        result = query.execute();
+        log("Query result: \n" + result);
+
+        assertNull(result);
+    }
+
+    @Test(timeout = 10000)
+    public void protectedOnApache_basic_localhost() throws QueryException {
+        log("protectedOnApache_basic_localhost()");
+
+        host = HOSTNAME;
+        port = PORT;
+        // path = PATH_PROTECTED_BASIC + SAMPLE_FILE_TEXT;
+        // path = PATH_PROTECTED_BASIC + SAMPLE_FILE_BINARY;
+        path = PATH_PROTECTED_BASIC + SAMPLE_FILE_TEXT;
+
+        GetQuery query = new GetQuery(host, port, path, SECURE);
+
+        // attention, don't use BinarySerializer here, but instead use the
+        // generic ByteArraySerializer
+        query.setSerializer(new ByteArraySerializer());
+        query.setTimeout(TIMEOUT);
+        log("GET Query to " + query.getLocation());
+
+        authentication = new BasicAuthentication(USER_NAME, USER_PASSWORD);
+        authentication.authenticate(query);
+
+        result = query.execute();
+        assertNotNull(result);
+
+        // int status = query.getStatus(); // method missing at the moment ...
+        // log("Query: status = " + status + ", result: \n" + result);
+        // assertEquals(401, status);
+
+        // dump content, but useful only for text resources ...
+        String dump = // result.toString()
+        // Arrays.toString((byte []) result);
+        new String((byte[]) result);
+        log("Query result: " + (dump.getBytes().length) + " bytes \n" + dump);
+    }
+
+}

Added: incubator/pivot/trunk/web/test/org/apache/pivot/web/test/WebQueryTestClientDigest.java
URL: http://svn.apache.org/viewvc/incubator/pivot/trunk/web/test/org/apache/pivot/web/test/WebQueryTestClientDigest.java?rev=828043&view=auto
==============================================================================
--- incubator/pivot/trunk/web/test/org/apache/pivot/web/test/WebQueryTestClientDigest.java (added)
+++ incubator/pivot/trunk/web/test/org/apache/pivot/web/test/WebQueryTestClientDigest.java Wed Oct 21 15:18:15 2009
@@ -0,0 +1,328 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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.
+ */
+package org.apache.pivot.web.test;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.apache.pivot.serialization.ByteArraySerializer;
+import org.apache.pivot.web.Authentication;
+import org.apache.pivot.web.DigestAuthentication;
+import org.apache.pivot.web.GetQuery;
+import org.apache.pivot.web.QueryException;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Integration Test of Client-side Authentication with the Digest method.
+ * <br/>
+ * This is a JUnit 4 Test, but should be excluded from usual (Unit) Test Suite.
+ * <br/>
+ * To Run these tests, a local instance of Apache must be started and configured with
+ * the required resources ( dir /public , and dir /protected_digest protected with digest authentication) and files.
+ * Before to run these tests, ensure digest authentication has been successfully setup 
+ * asking the same URLs from a Web Browser.
+ * See the file WebQueryTestClientDigest.README for further info.
+ *
+ * TODO:
+ *   - test other HTTP methods ...
+ * 
+ *   - in the future, verify if make a new Demo for accessing (digest etc) protected data,
+ *     as a Pivot Application, giving all the parameters 
+ *     (username, password, url, auth method, http method, etc) from the GUI ...
+ *   
+ */
+public class WebQueryTestClientDigest {
+    final static String HOSTNAME = "localhost";
+    final static String PATH = null;
+    final static int PORT = 80;
+    final static boolean SECURE = false;
+
+    final static String PATH_PUBLIC = "/public/";
+    final static String PATH_PROTECTED_DIGEST = "/protected_digest/";
+
+    final static String SAMPLE_FILE_BINARY = "test.jpg";
+    final static String SAMPLE_FILE_TEXT = "test.txt";
+
+    final static String USER_NAME = "test";
+    final static String USER_PASSWORD = "test0";
+
+    final static String USER_NAME_BY_RFC = "Mufasa";
+    final static String USER_PASSWORD_BY_RFC = "Circle Of Life";
+    final static String PROTECTED_DIGEST_URI_BY_RFC = "/dir/index.html";
+
+    final static long TIMEOUT = 5000l; // default timeout for WebQuery tests
+                                       // here: 5 sec
+
+    final static int TOMCAT_PORT = 8080;
+    final static String TOMCAT_PIVOT_TEST_WEBAPP = "pivot-tests";
+
+    String host = null;
+    int port = 0;
+    String path = null;
+
+    Authentication authentication = null;
+
+    Object result = null;
+
+    public void log(String msg) {
+        System.out.println(msg);
+    }
+
+    @BeforeClass
+    public static void runBeforeClass() {
+        // run for one time before all test cases
+    }
+
+    @AfterClass
+    public static void runAfterClass() {
+        // run for one time after all test cases
+    }
+
+    @Before
+    public void runBeforeEveryTest() {
+        // run before any single test case
+    }
+
+    @After
+    public void runAfterEveryTest() {
+        // run after any single test case
+        host = null;
+        port = 0;
+        path = null;
+
+        authentication = null;
+
+        result = null;
+    }
+
+    // @Ignore
+    @Test(timeout = 10000, expected = QueryException.class)
+    public void publicOnApache_noauth_NotExistingHost() throws QueryException {
+        log("publicOnApache_noauth_NotExistingHost()");
+
+        host = "non_existing_host";
+        port = PORT;
+        path = PATH_PUBLIC;
+
+        GetQuery query = new GetQuery(host, port, path, SECURE);
+        query.setTimeout(TIMEOUT);
+        log("GET Query to " + query.getLocation());
+
+        result = query.execute();
+        assertNull(result);
+
+        log("Query result: \n" + result);
+    }
+
+    @Test(timeout = 10000, expected = QueryException.class)
+    public void publicOnApache_noauth_localhost_NotExistingResource() throws QueryException {
+        log("publicOnApache_noauth_localhost_NotExistingResource()");
+
+        host = HOSTNAME;
+        port = PORT;
+        path = PATH_PUBLIC + "non_existing_resource";
+
+        GetQuery query = new GetQuery(host, port, path, SECURE);
+        query.setTimeout(TIMEOUT);
+        log("GET Query to " + query.getLocation());
+
+        result = query.execute();
+        assertNull(result);
+
+        log("Query result: \n" + result);
+    }
+
+    @Test(timeout = 10000)
+    public void publicOnApache_noauth_localhost_testFile() throws QueryException {
+        log("publicOnApache_noauth_localhost_testFile()");
+
+        host = HOSTNAME;
+        port = PORT;
+        path = PATH_PUBLIC + SAMPLE_FILE_TEXT;
+
+        GetQuery query = new GetQuery(host, port, path, SECURE);
+
+        // attention, don't use BinarySerializer here, but instead use the
+        // generic ByteArraySerializer
+        query.setSerializer(new ByteArraySerializer());
+        query.setTimeout(TIMEOUT);
+        log("GET Query to " + query.getLocation());
+
+        result = query.execute();
+        assertNotNull(result);
+
+        // dump content, but useful only for text resources ...
+        String dump = // result.toString()
+        // Arrays.toString((byte []) result);
+        new String((byte[]) result);
+        log("Query result: " + (dump.getBytes().length) + " bytes \n" + dump);
+    }
+
+    @Test(timeout = 10000)
+    public void publicOnApache_digest_localhost_forceUnnecessaryAuthentication()
+        throws QueryException {
+        log("publicOnApache_digest_localhost_forceUnnecessaryAuthentication()");
+
+        host = HOSTNAME;
+        port = PORT;
+        path = PATH_PUBLIC + SAMPLE_FILE_TEXT;
+
+        GetQuery query = new GetQuery(host, port, path, SECURE);
+
+        // attention, don't use BinarySerializer here, but instead use the
+        // generic ByteArraySerializer
+        query.setSerializer(new ByteArraySerializer());
+        query.setTimeout(TIMEOUT);
+        log("GET Query to " + query.getLocation());
+
+        authentication = new DigestAuthentication(USER_NAME, USER_PASSWORD);
+        authentication.authenticate(query);
+
+        result = query.execute();
+        assertNotNull(result);
+
+        // dump content, but useful only for text resources ...
+        String dump = // result.toString()
+        // Arrays.toString((byte []) result);
+        new String((byte[]) result);
+        log("Query result: " + (dump.getBytes().length) + " bytes \n" + dump);
+    }
+
+    // @Test(timeout = 10000, expected = QueryException.class)
+    @Test(timeout = 1000000, expected = QueryException.class)
+    // for debugging the execution
+    public void protectedOnApache_digest_localhostWithoutAuthenticate() throws QueryException {
+        log("protectedOnApache_digest_localhostWithoutAuthenticate()");
+
+        host = HOSTNAME;
+        port = PORT;
+        path = PATH_PROTECTED_DIGEST + SAMPLE_FILE_TEXT;
+
+        GetQuery query = new GetQuery(host, port, path, SECURE);
+        query.setSerializer(new ByteArraySerializer());
+        query.setTimeout(TIMEOUT);
+        log("GET Query to " + query.getLocation());
+
+        result = query.execute();
+        log("Query result: \n" + result);
+
+        assertNull(result);
+    }
+
+    @Test(timeout = 10000, expected = QueryException.class)
+    public void protectedOnApache_digest_localhostWithWrongCredentials() throws QueryException {
+        log("protectedOnApache_digest_localhostWithWrongCredentials()");
+
+        host = HOSTNAME;
+        port = PORT;
+        path = PATH_PROTECTED_DIGEST + SAMPLE_FILE_TEXT;
+
+        GetQuery query = new GetQuery(host, port, path, SECURE);
+        query.setSerializer(new ByteArraySerializer());
+        query.setTimeout(TIMEOUT);
+        log("GET Query to " + query.getLocation());
+
+        authentication = new DigestAuthentication("wrongUsername", "wrongPassword");
+        authentication.authenticate(query);
+
+        result = query.execute();
+        log("Query result: \n" + result);
+
+        assertNull(result);
+    }
+
+    // @Test(timeout = 10000)
+    @Test(timeout = 1000000)
+    // for debugging the execution
+    public void protectedOnApache_digest_localhost() throws QueryException {
+        log("protectedOnApache_digest_localhost()");
+
+        host = HOSTNAME;
+        port = PORT;
+        // path = PATH_PROTECTED_DIGEST + SAMPLE_FILE_TEXT;
+        // path = PATH_PROTECTED_DIGEST + SAMPLE_FILE_BINARY;
+        path = PATH_PROTECTED_DIGEST + SAMPLE_FILE_TEXT;
+
+        GetQuery query = new GetQuery(host, port, path, SECURE);
+
+        // attention, don't use BinarySerializer here, but instead use the
+        // generic ByteArraySerializer
+        query.setSerializer(new ByteArraySerializer());
+        query.setTimeout(TIMEOUT);
+        log("GET Query to " + query.getLocation());
+
+        authentication = new DigestAuthentication(USER_NAME, USER_PASSWORD);
+        authentication.authenticate(query);
+
+        result = query.execute();
+        assertNotNull(result);
+
+        // int status = query.getStatus(); // method missing at the moment ...
+        // log("Query: status = " + status + ", result: \n" + result);
+        // assertEquals(401, status);
+
+        // dump content, but useful only for text resources ...
+        String dump = // result.toString()
+        // Arrays.toString((byte []) result);
+        new String((byte[]) result);
+        log("Query result: " + (dump.getBytes().length) + " bytes \n" + dump);
+    }
+
+    // @Test(timeout = 10000)
+    @Test(timeout = 1000000)
+    // for debugging the execution
+    public void protectedOnApache_digest_localhostByRFC() throws QueryException {
+        log("protectedOnApache_digest_localhostByRFC()");
+
+        host = HOSTNAME;
+        port = PORT;
+        path = PROTECTED_DIGEST_URI_BY_RFC;
+
+        GetQuery query = new GetQuery(host, port, path, SECURE);
+
+        // attention, don't use BinarySerializer here, but instead use the
+        // generic ByteArraySerializer
+        query.setSerializer(new ByteArraySerializer());
+        query.setTimeout(TIMEOUT);
+        log("GET Query to " + query.getLocation());
+
+        authentication = new DigestAuthentication(USER_NAME_BY_RFC, USER_PASSWORD_BY_RFC);
+        authentication.authenticate(query);
+
+        result = query.execute();
+        assertNotNull(result);
+
+        // dump content, but useful only for text resources ...
+        String dump = // result.toString()
+        // Arrays.toString((byte []) result);
+        new String((byte[]) result);
+        log("Query result: " + (dump.getBytes().length) + " bytes \n" + dump);
+    }
+
+    // TODO: test also many queries after the first, to see if/how to handle the
+    // nonce count ...
+    // TODO: test also with URL parameters ...
+    // TODO: test also with long query:
+    // - test the timeout (of query, and not of the test method)
+    // - test the cancel (of query) , like downloading an iso file
+    // TODO: test also with Tomcat ...
+
+}



Re: svn commit: r828043 - in /incubator/pivot/trunk/web: src/org/apache/pivot/web/ test/org/apache/pivot/web/test/

Posted by Greg Brown <gk...@mac.com>.
One quick comment - the contents of "WebQueryTestClient.README" should  
be moved into a (properly formatted) "package.html" file.


Re: svn commit: r828043

Posted by Greg Brown <gk...@mac.com>.
>> The Integer class provides methods for translating to and from hex - is there any reason we can't simply use those methods?
>Probably this class (the original version) was written when that
>feature wasn't existing or wasn't efficient ... I'll try to look at
>the code.

OK.


Re: svn commit: r828043

Posted by Sandro Martini <sa...@gmail.com>.
> The Integer class provides methods for translating to and from hex - is there any reason we can't simply use those methods?
Probably this class (the original version) was written when that
feature wasn't existing or wasn't efficient ... I'll try to look at
the code.

Ah, note that this class currently is optimized for byte [] , I have
to see using Integers (for doing the same things) if there are some
performances overhead ...

Bye

Re: svn commit: r828043

Posted by Greg Brown <gk...@mac.com>.
The Integer class provides methods for translating to and from hex - is there any reason we can't simply use those methods?
 
On Friday, October 23, 2009, at 11:52AM, "Sandro Martini" <sa...@gmail.com> wrote:
>Hi Greg,
>> Ah, I see. At a quick glance, it seemed pretty specialized. Is it really generic, or might it only be useful to MD5? If the latter, maybe it should be a private static inner class of MD5.
>At the moment is used only inside MD5, but has some generic logic to
>transform from/to Hex values, so I think it would be better (in terms
>or future use / reuse) to have it as a standard class ...
>
>Bye
>
>

Re: svn commit: r828043

Posted by Sandro Martini <sa...@gmail.com>.
Hi Greg,
> Ah, I see. At a quick glance, it seemed pretty specialized. Is it really generic, or might it only be useful to MD5? If the latter, maybe it should be a private static inner class of MD5.
At the moment is used only inside MD5, but has some generic logic to
transform from/to Hex values, so I think it would be better (in terms
or future use / reuse) to have it as a standard class ...

Bye

Re: svn commit: r828043

Posted by Greg Brown <gk...@mac.com>.
>HexUtils is a generic class to transform data from/to HEX format, so
>I'd put in the same package of MD5 ...

Ah, I see. At a quick glance, it seemed pretty specialized. Is it really generic, or might it only be useful to MD5? If the latter, maybe it should be a private static inner class of MD5.


Re: svn commit: r828043

Posted by Sandro Martini <sa...@gmail.com>.
>>> 1) I don't think we should deprecate methods that we've never released.
>>> IMO, deprecation is for backward compatibility for perhaps one release cycle
>>> to allow developers who've used a method to transition to the newer API.
>>> I'd say let's remove the methods instead of deprecating them.  If we need
>>> them later, they're in SVN and can be easily restored.
>>Right, but if there aren't objections I'd prefer to keep them at the
>>moment (so i can not add the deprecation on them), and maybe add the
>>new version (by mixing two methods, to return a String).
>
> I agree that we should not be tagging any new methods as deprecated. This is only likely to cause confusion.
Ok.


>>> 2) I think MD5 should go in core/util -- there's nothing specifically "web" about it.
>>My initial version of it (and the HexUtils) was in core, but to avoid
>>fill with strange code I moved them to web, but I agree with you, so
>>if there aren't objections I can move them (MD5 and HexUtils) in core,
>>under the org.apache.pivot.util package.
>
> I haven't looked at these files closely yet, but I am guessing that org.apache.pivot.util is a good place for MD5, whereas HexUtils may be better off as a package private class in org.apache.pivot.web or as an inner class of DigestAuthentication.
HexUtils is a generic class to transform data from/to HEX format, so
I'd put in the same package of MD5 ...

Bye

Re: svn commit: r828043

Posted by Greg Brown <gk...@mac.com>.
>> 1) I don't think we should deprecate methods that we've never released.
>> IMO, deprecation is for backward compatibility for perhaps one release cycle
>> to allow developers who've used a method to transition to the newer API.
>> I'd say let's remove the methods instead of deprecating them.  If we need
>> them later, they're in SVN and can be easily restored.
>Right, but if there aren't objections I'd prefer to keep them at the
>moment (so i can not add the deprecation on them), and maybe add the
>new version (by mixing two methods, to return a String).

I agree that we should not be tagging any new methods as deprecated. This is only likely to cause confusion.

>> 2) I think MD5 should go in core/util -- there's nothing specifically "web" about it.
>My initial version of it (and the HexUtils) was in core, but to avoid
>fill with strange code I moved them to web, but I agree with you, so
>if there aren't objections I can move them (MD5 and HexUtils) in core,
>under the org.apache.pivot.util package.

I haven't looked at these files closely yet, but I am guessing that org.apache.pivot.util is a good place for MD5, whereas HexUtils may be better off as a package private class in org.apache.pivot.web or as an inner class of DigestAuthentication.

G


Re: svn commit: r828043 - in /incubator/pivot/trunk/web: src/org/apache/pivot/web/ test/org/apache/pivot/web/test/

Posted by Sandro Martini <sa...@gmail.com>.
Hi to all,
a quick note:
my sample folders for the Apache test were working on Apache 2.0.x,
but no more on 2.2.x, I just discovered that a little change in
.htaccess (used to setup a simple per-directory security) files were
needed ... tell me if someone needs a zip containing this little test
setup.

And I've just committed some little changes discussed today on MD5.

Good week-end to all.

Bye,
Sandro

Re: svn commit: r828043 - in /incubator/pivot/trunk/web: src/org/apache/pivot/web/ test/org/apache/pivot/web/test/

Posted by Sandro Martini <sa...@gmail.com>.
Hi Todd,

> 1) I don't think we should deprecate methods that we've never released.
> IMO, deprecation is for backward compatibility for perhaps one release cycle
> to allow developers who've used a method to transition to the newer API.
> I'd say let's remove the methods instead of deprecating them.  If we need
> them later, they're in SVN and can be easily restored.
Right, but if there aren't objections I'd prefer to keep them at the
moment (so i can not add the deprecation on them), and maybe add the
new version (by mixing two methods, to return a String).

> 2) I think MD5 should go in core/util -- there's nothing specifically "web" about it.
My initial version of it (and the HexUtils) was in core, but to avoid
fill with strange code I moved them to web, but I agree with you, so
if there aren't objections I can move them (MD5 and HexUtils) in core,
under the org.apache.pivot.util package.

Bye

Re: svn commit: r828043 - in /incubator/pivot/trunk/web: src/org/apache/pivot/web/ test/org/apache/pivot/web/test/

Posted by Todd Volkert <tv...@gmail.com>.
Two new thoughts on this discussion:

1) I don't think we should deprecate methods that we've never released.
IMO, deprecation is for backward compatibility for perhaps one release cycle
to allow developers who've used a method to transition to the newer API.
I'd say let's remove the methods instead of deprecating them.  If we need
them later, they're in SVN and can be easily restored.

2) I think MD5 should go in core/util -- there's nothing specifically "web"
about it.

Thoughts?
-T

On Fri, Oct 23, 2009 at 5:48 AM, Sandro Martini <sa...@gmail.com>wrote:

> Hi Chris,
>
> first the simple answer, for Todd:
> > I was just looking at that class... I usually wrap the MessageDigest
> > stuff in a utility class myself, mainly because it's a few lines of
> > code that would otherwise get repeated quite a lot.
> right.
>
>
> > I see that our MD5 class also lets you convert to bytes.  It would be
> > easy enough to extract that from the above snippet - but does anyone
> > use the byte[] version of an MD5 hash?
> In this class, portions of code are taken from Apache Tomcat
> (org.apache.catalina.util.MD5Encoder), and this is only an "utility"
> class, to solve some things in the more complex DigestAuthentication,
> so when I wrote the code, this class was as closer as the original
> one, but your suggestions are good.
> We could add your method and deprecate others (they are working, and
> in some cases they could be useful ...).
> But in any case I'd keep the MD5 class, could be useful to reuse also
> from other things.
>
> > I would also question why our MD5 class is synchronising on itself -
> > as far as I was aware using MessageDigest was a thread-safe operation
> > - then again, I don't actually know and the API docs don't seem to
> > mention this one way or the other.
> In the original source (take a look here
>
> http://www.docjar.com/html/api/org/apache/catalina/authenticator/DigestAuthenticator.java.html
> ), they do the same, so to avoid problems I do it ... but probably you
> are right, it's unnecessary, I know little this (many years of Web
> Sites only) and I forgot this part.
>
> > Finally - why bother with the static decode method?   Hashes are
> > intended to be one way, right?  Even if not, this method seems totally
> redundant.
> Yes, this was an idea to have it as a marker method for other classes
> that needs to do something like MD5 (that could have the decode
> feature), but we can drop it.
>
>
> Ah, a little note:
> some of my things to verify were taken from features of the Digest as
> seen in the RFC, like using SHA instead of MD5, but I haven't found
> anyone that uses it, and the same for other things.
> Other things to verify were taken from the Tomcat implementation of
> this code but probably here are unnecessary because they were
> Tomcat-specific.
>
> Important tests still to do are to make it working also with Tomcat,
> and also try to call the same URL more times (the RFC says there is a
> counter in the answer that have to be read and do a +1 in the next
> request) but I wasn't able to make this test ... I hope to find time
> for this stuff next week.
>
> > Just some thoughts.
> No problem, comments/suggestions are always welcome :-) .
>
> If you want, be free to make changes to the code, but only remember to
> run the Unit Test to ensure that all works after changes ... only tell
> me, to avoid doing in two the same changes :-) .
>
> Byeee
>

Re: svn commit: r828043 - in /incubator/pivot/trunk/web: src/org/apache/pivot/web/ test/org/apache/pivot/web/test/

Posted by Sandro Martini <sa...@gmail.com>.
Hi Chris,

first the simple answer, for Todd:
> I was just looking at that class... I usually wrap the MessageDigest
> stuff in a utility class myself, mainly because it's a few lines of
> code that would otherwise get repeated quite a lot.
right.


> I see that our MD5 class also lets you convert to bytes.  It would be
> easy enough to extract that from the above snippet - but does anyone
> use the byte[] version of an MD5 hash?
In this class, portions of code are taken from Apache Tomcat
(org.apache.catalina.util.MD5Encoder), and this is only an "utility"
class, to solve some things in the more complex DigestAuthentication,
so when I wrote the code, this class was as closer as the original
one, but your suggestions are good.
We could add your method and deprecate others (they are working, and
in some cases they could be useful ...).
But in any case I'd keep the MD5 class, could be useful to reuse also
from other things.

> I would also question why our MD5 class is synchronising on itself -
> as far as I was aware using MessageDigest was a thread-safe operation
> - then again, I don't actually know and the API docs don't seem to
> mention this one way or the other.
In the original source (take a look here
http://www.docjar.com/html/api/org/apache/catalina/authenticator/DigestAuthenticator.java.html
), they do the same, so to avoid problems I do it ... but probably you
are right, it's unnecessary, I know little this (many years of Web
Sites only) and I forgot this part.

> Finally - why bother with the static decode method?   Hashes are
> intended to be one way, right?  Even if not, this method seems totally redundant.
Yes, this was an idea to have it as a marker method for other classes
that needs to do something like MD5 (that could have the decode
feature), but we can drop it.


Ah, a little note:
some of my things to verify were taken from features of the Digest as
seen in the RFC, like using SHA instead of MD5, but I haven't found
anyone that uses it, and the same for other things.
Other things to verify were taken from the Tomcat implementation of
this code but probably here are unnecessary because they were
Tomcat-specific.

Important tests still to do are to make it working also with Tomcat,
and also try to call the same URL more times (the RFC says there is a
counter in the answer that have to be read and do a +1 in the next
request) but I wasn't able to make this test ... I hope to find time
for this stuff next week.

> Just some thoughts.
No problem, comments/suggestions are always welcome :-) .

If you want, be free to make changes to the code, but only remember to
run the Unit Test to ensure that all works after changes ... only tell
me, to avoid doing in two the same changes :-) .

Byeee

Re: svn commit: r828043 - in /incubator/pivot/trunk/web: src/org/apache/pivot/web/ test/org/apache/pivot/web/test/

Posted by Christopher Brind <br...@brindy.org.uk>.
*de-lurks*

I was just looking at that class... I usually wrap the MessageDigest
stuff in a utility class myself, mainly because it's a few lines of
code that would otherwise get repeated quite a lot.

For hashing a string I usually use this:

public static String getMd5Digest(String input) {
            try {
                MessageDigest md = MessageDigest.getInstance("MD5");
                byte[] messageDigest = md.digest(input.getBytes());
                BigInteger number = new BigInteger(1,messageDigest);
                String hash = number.toString(16);
                if (hash.length() == 31) {
                    hash = "0" + hash;
                }

                return hash;
            } catch(NoSuchAlgorithmException e) {
                throw new RuntimeException(e);
            }
        }

I see that our MD5 class also lets you convert to bytes.  It would be
easy enough to extract that from the above snippet - but does anyone
use the byte[] version of an MD5 hash?

I would also question why our MD5 class is synchronising on itself -
as far as I was aware using MessageDigest was a thread-safe operation
- then again, I don't actually know and the API docs don't seem to
mention this one way or the other.

Finally - why bother with the static decode method?   Hashes are
intended to be one way, right?  Even if not, this method seems totally
redundant.

Just some thoughts.

Cheers,
Chris


2009/10/22 Sandro Martini <sa...@gmail.com>:
> Hi Todd,
>> Question: what does the MD5 class do that java.security.MessageDigest doesn't?
> Good question ... I have to look at the code (i made it many months
> ago), and tell you.
>
> Bye
>

Re: svn commit: r828043 - in /incubator/pivot/trunk/web: src/org/apache/pivot/web/ test/org/apache/pivot/web/test/

Posted by Sandro Martini <sa...@gmail.com>.
Hi Todd,
> Question: what does the MD5 class do that java.security.MessageDigest doesn't?
Good question ... I have to look at the code (i made it many months
ago), and tell you.

Bye

Re: svn commit: r828043 - in /incubator/pivot/trunk/web: src/org/apache/pivot/web/ test/org/apache/pivot/web/test/

Posted by Todd Volkert <tv...@gmail.com>.
Question: what does the MD5 class do that java.security.MessageDigest
doesn't?

Re: svn commit: r828043

Posted by Sandro Martini <sa...@gmail.com>.
Hi Greg,
as you know Digest Authentication is a "little" complex, so I keep
inside the source some TODO, mainly to remember that there are some
cases that are not tested at the moment ... but many of them was
handled in the same way (without TODOs) also in other libraries.

We can remove them, and the rest of commented code inside ... but
before doing this I'd like to wait some time, so if someone try it can
have more info. What do you think ?

Some things to note:
- under test I've added two JUnit Test classes:
WebQueryTestClientBasic and WebQueryTestClientDigest , but nothe that
these are "Integration Tests" so they need a working www server on
localhost (and setup in the right way), so if we'll enable automatic
test running at any Compile / Deploy via Ant, these should be excluded
... they could be moved inside another test directory, or excluded in
another way, but i think it's important to keep them in some place
here, under the test area
- to test this you can follow my instructions in the README, to setup
folders and permissions on an Apache 2.0.x or 2.2.x running on
localhost
- I'll make some test under Tomcat as soon as possible (this is my
first and maybe the only TODO) to finish this part ...
- for the README to migrate to package html file, have you got some
idea / hint / sample for me ? Thanks.


Ah, one quick note:
I've seen today that the latest Apache 2.2.x under Vista doesn't work
very well (in default htdocs doesn't take right permissions, the
service starts but in a wrong way, etc) ... but also here after many
manual settings, all works on my environment.
So, the real, important point to finish this part is:
this is a complex feature, and needs more testing, or better, not only
from me ...

In any case, be free to modify code if you want.
FindBugs has no warning on it :-) ...


Comments ?

Bye

Re: svn commit: r828043

Posted by Greg Brown <gk...@mac.com>.
I'll look at this in more detail when I get a minute, but I am  
concerned by the large number of TODOs in DigestAuthentication.java.  
What is your plan for resolving these items?