From 08b91502c0ff54e10a54832c6eb98e8212b32430 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Wed, 28 Oct 2015 15:14:05 +0100
Subject: [PATCH] OO-1757: replace the simple parser used to tokenize the
 Authorization header by the one of Tomcat which is RFC compliant

---
 .../webdav/manager/DigestAuthentication.java  |  49 +--
 .../webdav/manager/WebDAVManagerImpl.java     |   9 +-
 .../olat/core/util/http/Authorization.java    | 141 ++++++
 .../org/olat/core/util/http/HttpParser.java   | 401 ++++++++++++++++++
 .../org/olat/core/util/http/SkipResult.java   |  23 +
 .../java/org/olat/core/util/http/package.html |   6 +
 .../manager/DigestAuthenticationTest.java     |  64 +++
 .../webdav/manager/WebDAVManagerTest.java     |  14 -
 .../java/org/olat/test/AllTestsJunit4.java    |   1 +
 9 files changed, 662 insertions(+), 46 deletions(-)
 create mode 100644 src/main/java/org/olat/core/util/http/Authorization.java
 create mode 100644 src/main/java/org/olat/core/util/http/HttpParser.java
 create mode 100644 src/main/java/org/olat/core/util/http/SkipResult.java
 create mode 100644 src/main/java/org/olat/core/util/http/package.html
 create mode 100644 src/test/java/org/olat/core/commons/services/webdav/manager/DigestAuthenticationTest.java

diff --git a/src/main/java/org/olat/core/commons/services/webdav/manager/DigestAuthentication.java b/src/main/java/org/olat/core/commons/services/webdav/manager/DigestAuthentication.java
index ac2fbe52b76..f4a7afdd302 100644
--- a/src/main/java/org/olat/core/commons/services/webdav/manager/DigestAuthentication.java
+++ b/src/main/java/org/olat/core/commons/services/webdav/manager/DigestAuthentication.java
@@ -19,7 +19,13 @@
  */
 package org.olat.core.commons.services.webdav.manager;
 
-import java.util.StringTokenizer;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Map;
+
+import org.olat.core.logging.OLog;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.http.Authorization;
 
 /**
  * 
@@ -31,6 +37,8 @@ import java.util.StringTokenizer;
  */
 public class DigestAuthentication {
 	
+	private static final OLog log = Tracing.createLoggerFor(DigestAuthentication.class);
+	
 	private final String username;
 	private final String realm;
 	private final String nonce;
@@ -83,7 +91,7 @@ public class DigestAuthentication {
 		return qop;
 	}
 
-	public static DigestAuthentication parse(String request) {
+	public static DigestAuthentication parse(String header) {
 		String username = null;
 		String realm = null;
 		String nonce = null;
@@ -92,30 +100,19 @@ public class DigestAuthentication {
 		String nc = null;
 		String response = null;
 		String qop = null;
-	
-		StringTokenizer tokenizer = new StringTokenizer(request, ",\n");
-		for(; tokenizer.hasMoreTokens(); ) {
-			String token=tokenizer.nextToken().trim();
-			int index = token.indexOf('=');
-			String key = token.substring(0, index);
-			String val = token.substring(index + 1, token.length()).replace("\"", "");
-			if("username".equals(key)) {
-				username = val;
-			} else if("realm".equals(key)) {
-				realm = val;
-			} else if("nonce".equals(key)) {
-				nonce = val;
-			} else if("uri".equals(key)) {
-				uri = val;
-			} else if("cnonce".equals(key)) {
-				cnonce = val;
-			} else if("nc".equals(key)) {
-				nc = val;
-			} else if("response".equals(key)) {
-				response = val;
-			} else if("qop".equals(key)) {
-				qop = val;
-			}
+		
+		try {
+			Map<String,String> parsedHeader = Authorization.parseAuthorizationDigest(new StringReader(header));
+			username = parsedHeader.get("username");
+			realm = parsedHeader.get("realm");
+			nonce = parsedHeader.get("nonce");
+			uri = parsedHeader.get("uri");
+			cnonce = parsedHeader.get("cnonce");
+			nc = parsedHeader.get("nc");
+			response = parsedHeader.get("response");
+			qop = parsedHeader.get("qop");
+		} catch (IOException | IllegalArgumentException e) {
+			log.error("", e);
 		}
 		return new DigestAuthentication(username, realm, nonce, uri, cnonce, nc, response, qop);
 	}
diff --git a/src/main/java/org/olat/core/commons/services/webdav/manager/WebDAVManagerImpl.java b/src/main/java/org/olat/core/commons/services/webdav/manager/WebDAVManagerImpl.java
index 187a107fa03..f320b8c71f4 100644
--- a/src/main/java/org/olat/core/commons/services/webdav/manager/WebDAVManagerImpl.java
+++ b/src/main/java/org/olat/core/commons/services/webdav/manager/WebDAVManagerImpl.java
@@ -202,13 +202,11 @@ public class WebDAVManagerImpl implements WebDAVManager, InitializingBean {
 						usess = handleBasicAuthentication(credentials, request);
 					}
 				} else if (basic.equalsIgnoreCase("Digest")) {
-					int digestIndex = authHeader.indexOf("Digest");
-					String digestInfos = authHeader.substring(digestIndex + 7);
-					DigestAuthentication digestAuth = DigestAuthentication.parse(digestInfos);
+					DigestAuthentication digestAuth = DigestAuthentication.parse(authHeader);
 					cacheKey = digestAuth.getUsername();
 					usess = timedSessionCache.get(new CacheKey(remoteAddr, digestAuth.getUsername()));
 					if (usess == null || !usess.isAuthenticated()) {
-						usess = handleDigestAuthentication(digestInfos, request);
+						usess = handleDigestAuthentication(digestAuth, request);
 					}
 				}
 			}
@@ -241,8 +239,7 @@ public class WebDAVManagerImpl implements WebDAVManager, InitializingBean {
 		return null;
 	}
 
-	protected UserSession handleDigestAuthentication(String credentials, HttpServletRequest request) {
-		DigestAuthentication digestAuth = DigestAuthentication.parse(credentials);
+	protected UserSession handleDigestAuthentication(DigestAuthentication digestAuth, HttpServletRequest request) {
 		Identity identity = webDAVAuthManager.digestAuthentication(request.getMethod(), digestAuth);
 		if(identity != null) {
 			return afterAuthorization(identity, request);
diff --git a/src/main/java/org/olat/core/util/http/Authorization.java b/src/main/java/org/olat/core/util/http/Authorization.java
new file mode 100644
index 00000000000..8916b76b931
--- /dev/null
+++ b/src/main/java/org/olat/core/util/http/Authorization.java
@@ -0,0 +1,141 @@
+/*
+ * 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.olat.core.util.http;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Parser for an "Authorization" header.
+ */
+public class Authorization {
+
+    @SuppressWarnings("unused")  // Unused due to buggy client implementations
+    private static final Integer FIELD_TYPE_TOKEN = Integer.valueOf(0);
+    private static final Integer FIELD_TYPE_QUOTED_STRING = Integer.valueOf(1);
+    private static final Integer FIELD_TYPE_TOKEN_OR_QUOTED_STRING = Integer.valueOf(2);
+    private static final Integer FIELD_TYPE_LHEX = Integer.valueOf(3);
+    private static final Integer FIELD_TYPE_QUOTED_TOKEN = Integer.valueOf(4);
+
+    private static final Map<String,Integer> fieldTypes = new HashMap<>();
+
+    static {
+        // Digest field types.
+        // Note: These are more relaxed than RFC2617. This adheres to the
+        //       recommendation of RFC2616 that servers are tolerant of buggy
+        //       clients when they can be so without ambiguity.
+        fieldTypes.put("username", FIELD_TYPE_QUOTED_STRING);
+        fieldTypes.put("realm", FIELD_TYPE_QUOTED_STRING);
+        fieldTypes.put("nonce", FIELD_TYPE_QUOTED_STRING);
+        fieldTypes.put("digest-uri", FIELD_TYPE_QUOTED_STRING);
+        // RFC2617 says response is <">32LHEX<">. 32LHEX will also be accepted
+        fieldTypes.put("response", FIELD_TYPE_LHEX);
+        // RFC2617 says algorithm is token. <">token<"> will also be accepted
+        fieldTypes.put("algorithm", FIELD_TYPE_QUOTED_TOKEN);
+        fieldTypes.put("cnonce", FIELD_TYPE_QUOTED_STRING);
+        fieldTypes.put("opaque", FIELD_TYPE_QUOTED_STRING);
+        // RFC2617 says qop is token. <">token<"> will also be accepted
+        fieldTypes.put("qop", FIELD_TYPE_QUOTED_TOKEN);
+        // RFC2617 says nc is 8LHEX. <">8LHEX<"> will also be accepted
+        fieldTypes.put("nc", FIELD_TYPE_LHEX);
+
+    }
+
+    /**
+     * Parses an HTTP Authorization header for DIGEST authentication as per RFC
+     * 2617 section 3.2.2.
+     *
+     * @param input The header value to parse
+     *
+     * @return  A map of directives and values as {@link String}s or
+     *          <code>null</code> if a parsing error occurs. Although the
+     *          values returned are {@link String}s they will have been
+     *          validated to ensure that they conform to RFC 2617.
+     *
+     * @throws IllegalArgumentException If the header does not conform to RFC
+     *                                  2617
+     * @throws java.io.IOException If an error occurs while reading the input
+     */
+    public static Map<String,String> parseAuthorizationDigest (StringReader input)
+            throws IllegalArgumentException, IOException {
+
+        Map<String,String> result = new HashMap<>();
+
+        if (HttpParser.skipConstant(input, "Digest") != SkipResult.FOUND) {
+            return null;
+        }
+        // All field names are valid tokens
+        String field = HttpParser.readToken(input);
+        if (field == null) {
+            return null;
+        }
+        while (!field.equals("")) {
+            if (HttpParser.skipConstant(input, "=") != SkipResult.FOUND) {
+                return null;
+            }
+            String value;
+            Integer type = fieldTypes.get(field.toLowerCase(Locale.ENGLISH));
+            if (type == null) {
+                // auth-param = token "=" ( token | quoted-string )
+                type = FIELD_TYPE_TOKEN_OR_QUOTED_STRING;
+            }
+            switch (type.intValue()) {
+                case 0:
+                    // FIELD_TYPE_TOKEN
+                    value = HttpParser.readToken(input);
+                    break;
+                case 1:
+                    // FIELD_TYPE_QUOTED_STRING
+                    value = HttpParser.readQuotedString(input, false);
+                    break;
+                case 2:
+                    // FIELD_TYPE_TOKEN_OR_QUOTED_STRING
+                    value = HttpParser.readTokenOrQuotedString(input, false);
+                    break;
+                case 3:
+                    // FIELD_TYPE_LHEX
+                    value = HttpParser.readLhex(input);
+                    break;
+                case 4:
+                    // FIELD_TYPE_QUOTED_TOKEN
+                    value = HttpParser.readQuotedToken(input);
+                    break;
+                default:
+                    // Error
+                    throw new IllegalArgumentException("TODO i18n: Unsupported type");
+            }
+
+            if (value == null) {
+                return null;
+            }
+            result.put(field, value);
+
+            if (HttpParser.skipConstant(input, ",") == SkipResult.NOT_FOUND) {
+                return null;
+            }
+            field = HttpParser.readToken(input);
+            if (field == null) {
+                return null;
+            }
+        }
+
+        return result;
+    }
+}
diff --git a/src/main/java/org/olat/core/util/http/HttpParser.java b/src/main/java/org/olat/core/util/http/HttpParser.java
new file mode 100644
index 00000000000..640b1a955ef
--- /dev/null
+++ b/src/main/java/org/olat/core/util/http/HttpParser.java
@@ -0,0 +1,401 @@
+/*
+ * 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.olat.core.util.http;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+/**
+ * HTTP header value parser implementation. Parsing HTTP headers as per RFC2616
+ * is not always as simple as it first appears. For headers that only use tokens
+ * the simple approach will normally be sufficient. However, for the other
+ * headers, while simple code meets 99.9% of cases, there are often some edge
+ * cases that make things far more complicated.
+ *
+ * The purpose of this parser is to let the parser worry about the edge cases.
+ * It provides tolerant (where safe to do so) parsing of HTTP header values
+ * assuming that wrapped header lines have already been unwrapped. (The Tomcat
+ * header processing code does the unwrapping.)
+ *
+ */
+public class HttpParser {
+
+    // Arrays used by isToken(), isHex()
+    private static final boolean isToken[] = new boolean[128];
+    private static final boolean isHex[] = new boolean[128];
+
+    static {
+        // Setup the flag arrays
+        for (int i = 0; i < 128; i++) {
+            if (i < 32) {
+                isToken[i] = false;
+            } else if (i == '(' || i == ')' || i == '<' || i == '>'  || i == '@'  ||
+                       i == ',' || i == ';' || i == ':' || i == '\\' || i == '\"' ||
+                       i == '/' || i == '[' || i == ']' || i == '?'  || i == '='  ||
+                       i == '{' || i == '}' || i == ' ' || i == '\t') {
+                isToken[i] = false;
+            } else {
+                isToken[i] = true;
+            }
+
+            if (i >= '0' && i <= '9' || i >= 'A' && i <= 'F' ||
+                    i >= 'a' && i <= 'f') {
+                isHex[i] = true;
+            } else {
+                isHex[i] = false;
+            }
+        }
+    }
+
+    public static String unquote(String input) {
+        if (input == null || input.length() < 2 || input.charAt(0) != '"') {
+            return input;
+        }
+
+        StringBuilder result = new StringBuilder();
+        for (int i = 1 ; i < (input.length() - 1); i++) {
+            char c = input.charAt(i);
+            if (input.charAt(i) == '\\') {
+                i++;
+                result.append(input.charAt(i));
+            } else {
+                result.append(c);
+            }
+        }
+        return result.toString();
+    }
+
+    static boolean isToken(int c) {
+        // Fast for correct values, slower for incorrect ones
+        try {
+            return isToken[c];
+        } catch (ArrayIndexOutOfBoundsException ex) {
+            return false;
+        }
+    }
+
+    static boolean isHex(int c) {
+        // Fast for correct values, slower for incorrect ones
+        try {
+            return isHex[c];
+        } catch (ArrayIndexOutOfBoundsException ex) {
+            return false;
+        }
+    }
+
+    // Skip any LWS and return the next char
+    static int skipLws(StringReader input, boolean withReset) throws IOException {
+
+        if (withReset) {
+            input.mark(1);
+        }
+        int c = input.read();
+
+        while (c == 32 || c == 9 || c == 10 || c == 13) {
+            if (withReset) {
+                input.mark(1);
+            }
+            c = input.read();
+        }
+
+        if (withReset) {
+            input.reset();
+        }
+        return c;
+    }
+
+    static SkipResult skipConstant(StringReader input, String constant) throws IOException {
+        int len = constant.length();
+
+        int c = skipLws(input, false);
+
+        for (int i = 0; i < len; i++) {
+            if (i == 0 && c == -1) {
+                return SkipResult.EOF;
+            }
+            if (c != constant.charAt(i)) {
+                input.skip(-(i + 1));
+                return SkipResult.NOT_FOUND;
+            }
+            if (i != (len - 1)) {
+                c = input.read();
+            }
+        }
+        return SkipResult.FOUND;
+    }
+
+    /**
+     * @return  the token if one was found, the empty string if no data was
+     *          available to read or <code>null</code> if data other than a
+     *          token was found
+     */
+    static String readToken(StringReader input) throws IOException {
+        StringBuilder result = new StringBuilder();
+
+        int c = skipLws(input, false);
+
+        while (c != -1 && isToken(c)) {
+            result.append((char) c);
+            c = input.read();
+        }
+        // Skip back so non-token character is available for next read
+        input.skip(-1);
+
+        if (c != -1 && result.length() == 0) {
+            return null;
+        } else {
+            return result.toString();
+        }
+    }
+
+    /**
+     * @return the quoted string if one was found, null if data other than a
+     *         quoted string was found or null if the end of data was reached
+     *         before the quoted string was terminated
+     */
+    static String readQuotedString(StringReader input, boolean returnQuoted) throws IOException {
+
+        int c = skipLws(input, false);
+
+        if (c != '"') {
+            return null;
+        }
+
+        StringBuilder result = new StringBuilder();
+        if (returnQuoted) {
+            result.append('\"');
+        }
+        c = input.read();
+
+        while (c != '"') {
+            if (c == -1) {
+                return null;
+            } else if (c == '\\') {
+                c = input.read();
+                if (returnQuoted) {
+                    result.append('\\');
+                }
+                result.append(c);
+            } else {
+                result.append((char) c);
+            }
+            c = input.read();
+        }
+        if (returnQuoted) {
+            result.append('\"');
+        }
+
+        return result.toString();
+    }
+
+    static String readTokenOrQuotedString(StringReader input, boolean returnQuoted)
+            throws IOException {
+
+        // Go back so first non-LWS character is available to be read again
+        int c = skipLws(input, true);
+
+        if (c == '"') {
+            return readQuotedString(input, returnQuoted);
+        } else {
+            return readToken(input);
+        }
+    }
+
+    /**
+     * Token can be read unambiguously with or without surrounding quotes so
+     * this parsing method for token permits optional surrounding double quotes.
+     * This is not defined in any RFC. It is a special case to handle data from
+     * buggy clients (known buggy clients for DIGEST auth include Microsoft IE 8
+     * &amp; 9, Apple Safari for OSX and iOS) that add quotes to values that
+     * should be tokens.
+     *
+     * @return the token if one was found, null if data other than a token or
+     *         quoted token was found or null if the end of data was reached
+     *         before a quoted token was terminated
+     */
+    static String readQuotedToken(StringReader input) throws IOException {
+
+        StringBuilder result = new StringBuilder();
+        boolean quoted = false;
+
+        int c = skipLws(input, false);
+
+        if (c == '"') {
+            quoted = true;
+        } else if (c == -1 || !isToken(c)) {
+            return null;
+        } else {
+            result.append((char) c);
+        }
+        c = input.read();
+
+        while (c != -1 && isToken(c)) {
+            result.append((char) c);
+            c = input.read();
+        }
+
+        if (quoted) {
+            if (c != '"') {
+                return null;
+            }
+        } else {
+            // Skip back so non-token character is available for next read
+            input.skip(-1);
+        }
+
+        if (c != -1 && result.length() == 0) {
+            return null;
+        } else {
+            return result.toString();
+        }
+    }
+
+    /**
+     * LHEX can be read unambiguously with or without surrounding quotes so this
+     * parsing method for LHEX permits optional surrounding double quotes. Some
+     * buggy clients (libwww-perl for DIGEST auth) are known to send quoted LHEX
+     * when the specification requires just LHEX.
+     *
+     * <p>
+     * LHEX are, literally, lower-case hexadecimal digits. This implementation
+     * allows for upper-case digits as well, converting the returned value to
+     * lower-case.
+     *
+     * @return  the sequence of LHEX (minus any surrounding quotes) if any was
+     *          found, or <code>null</code> if data other LHEX was found
+     */
+    static String readLhex(StringReader input) throws IOException {
+
+        StringBuilder result = new StringBuilder();
+        boolean quoted = false;
+
+        int c = skipLws(input, false);
+
+        if (c == '"') {
+            quoted = true;
+        } else if (c == -1 || !isHex(c)) {
+            return null;
+        } else {
+            if ('A' <= c && c <= 'F') {
+                c -= ('A' - 'a');
+            }
+            result.append((char) c);
+        }
+        c = input.read();
+
+        while (c != -1 && isHex(c)) {
+            if ('A' <= c && c <= 'F') {
+                c -= ('A' - 'a');
+            }
+            result.append((char) c);
+            c = input.read();
+        }
+
+        if (quoted) {
+            if (c != '"') {
+                return null;
+            }
+        } else {
+            // Skip back so non-hex character is available for next read
+            input.skip(-1);
+        }
+
+        if (c != -1 && result.length() == 0) {
+            return null;
+        } else {
+            return result.toString();
+        }
+    }
+
+    static double readWeight(StringReader input, char delimiter) throws IOException {
+        int c = skipLws(input, false);
+        if (c == -1 || c == delimiter) {
+            // No q value just whitespace
+            return 1;
+        } else if (c != 'q') {
+            // Malformed. Use quality of zero so it is dropped.
+            skipUntil(input, c, delimiter);
+            return 0;
+        }
+        // RFC 7231 does not allow whitespace here but be tolerant
+        c = skipLws(input, false);
+        if (c != '=') {
+            // Malformed. Use quality of zero so it is dropped.
+            skipUntil(input, c, delimiter);
+            return 0;
+        }
+
+        // RFC 7231 does not allow whitespace here but be tolerant
+        c = skipLws(input, false);
+
+        // Should be no more than 3 decimal places
+        StringBuilder value = new StringBuilder(5);
+        int decimalPlacesRead = 0;
+        if (c == '0' || c == '1') {
+            value.append((char) c);
+            c = input.read();
+            if (c == '.') {
+                value.append('.');
+            } else if (c < '0' || c > '9') {
+                decimalPlacesRead = 3;
+            }
+            while (true) {
+                c = input.read();
+                if (c >= '0' && c <= '9') {
+                    if (decimalPlacesRead < 3) {
+                        value.append((char) c);
+                        decimalPlacesRead++;
+                    }
+                } else if (c == delimiter || c == 9 || c == 32 || c == -1) {
+                    break;
+                } else {
+                    // Malformed. Use quality of zero so it is dropped and skip until
+                    // EOF or the next delimiter
+                    skipUntil(input, c, delimiter);
+                    return 0;
+                }
+            }
+        } else {
+            // Malformed. Use quality of zero so it is dropped and skip until
+            // EOF or the next delimiter
+            skipUntil(input, c, delimiter);
+            return 0;
+        }
+
+        double result = Double.parseDouble(value.toString());
+        if (result > 1) {
+            return 0;
+        }
+        return result;
+    }
+
+
+    /**
+     * Skips all characters until EOF or the specified target is found. Normally
+     * used to skip invalid input until the next separator.
+     */
+    static SkipResult skipUntil(StringReader input, int c, char target) throws IOException {
+        while (c != -1 && c != target) {
+            c = input.read();
+        }
+        if (c == -1) {
+            return SkipResult.EOF;
+        } else {
+            return SkipResult.FOUND;
+        }
+    }
+}
diff --git a/src/main/java/org/olat/core/util/http/SkipResult.java b/src/main/java/org/olat/core/util/http/SkipResult.java
new file mode 100644
index 00000000000..bc1a4d40d4c
--- /dev/null
+++ b/src/main/java/org/olat/core/util/http/SkipResult.java
@@ -0,0 +1,23 @@
+/*
+ * 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.olat.core.util.http;
+
+enum SkipResult {
+    FOUND,
+    NOT_FOUND,
+    EOF
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/core/util/http/package.html b/src/main/java/org/olat/core/util/http/package.html
new file mode 100644
index 00000000000..cf15556957b
--- /dev/null
+++ b/src/main/java/org/olat/core/util/http/package.html
@@ -0,0 +1,6 @@
+<html>
+<head></head>
+<body>
+This Authorization, HttpParser and SkipResult are copy from the Tomcat code.
+</body>
+</html>
\ No newline at end of file
diff --git a/src/test/java/org/olat/core/commons/services/webdav/manager/DigestAuthenticationTest.java b/src/test/java/org/olat/core/commons/services/webdav/manager/DigestAuthenticationTest.java
new file mode 100644
index 00000000000..51075c57159
--- /dev/null
+++ b/src/test/java/org/olat/core/commons/services/webdav/manager/DigestAuthenticationTest.java
@@ -0,0 +1,64 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.core.commons.services.webdav.manager;
+
+import java.io.IOException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * 
+ * Initial date: 28.10.2015<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class DigestAuthenticationTest {
+	
+	@Test
+	public void parseDigestAuthentication() {
+		String request = "Digest username=\"kanu\",realm=\"OLAT WebDAV Access\",nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\",uri=\"/webdav\",cnonce=\"195d0d15b31ee8f0a7f243b9bfcd881d\",nc=00000001,response=\"671742b00fae8d4c8ceb6a5bcf2b36fa\",qop=\"auth\"";
+		DigestAuthentication auth = DigestAuthentication.parse(request);
+		Assert.assertEquals("kanu", auth.getUsername());
+		Assert.assertEquals("OLAT WebDAV Access", auth.getRealm());
+		Assert.assertEquals("dcd98b7102dd2f0e8b11d0f600bfb0c093", auth.getNonce());
+		Assert.assertEquals("/webdav", auth.getUri());
+		Assert.assertEquals("195d0d15b31ee8f0a7f243b9bfcd881d", auth.getCnonce());
+		Assert.assertEquals("00000001", auth.getNc());
+		Assert.assertEquals("671742b00fae8d4c8ceb6a5bcf2b36fa", auth.getResponse());
+		Assert.assertEquals("auth", auth.getQop());
+	}
+	
+	@Test
+	public void parseDigestAuthentication_withSpecialCharacters() throws IOException {
+		String request = "Digest username=\"kanu\",realm=\"OLAT WebDAV Access\",nonce=\"f1794336f940449a91c0214d39a45538\",uri=\"/olat/webdav/groupfolders/Ajax%20Group/Test%201/Grobplanung,%20rollende%20Planung%20Formular.xls\",cnonce=\"5128af45c016b7590f136fcf1152ebaf\",nc=00000001,response=\"2c56db720b5bb34f1887d4b8f8a41f51\",qop=\"auth\"";
+		
+		DigestAuthentication auth = DigestAuthentication.parse(request);
+		Assert.assertEquals("kanu", auth.getUsername());
+		Assert.assertEquals("OLAT WebDAV Access", auth.getRealm());
+		Assert.assertEquals("f1794336f940449a91c0214d39a45538", auth.getNonce());
+		Assert.assertEquals("/olat/webdav/groupfolders/Ajax%20Group/Test%201/Grobplanung,%20rollende%20Planung%20Formular.xls", auth.getUri());
+		Assert.assertEquals("5128af45c016b7590f136fcf1152ebaf", auth.getCnonce());
+		Assert.assertEquals("00000001", auth.getNc());
+		Assert.assertEquals("2c56db720b5bb34f1887d4b8f8a41f51", auth.getResponse());
+		Assert.assertEquals("auth", auth.getQop());
+	}
+
+}
diff --git a/src/test/java/org/olat/core/commons/services/webdav/manager/WebDAVManagerTest.java b/src/test/java/org/olat/core/commons/services/webdav/manager/WebDAVManagerTest.java
index 97cb76cf780..3fec61b4cfe 100644
--- a/src/test/java/org/olat/core/commons/services/webdav/manager/WebDAVManagerTest.java
+++ b/src/test/java/org/olat/core/commons/services/webdav/manager/WebDAVManagerTest.java
@@ -59,20 +59,6 @@ public class WebDAVManagerTest extends OlatTestCase {
 	@Autowired
 	private WebDAVManagerImpl webDAVManager;
 	
-	@Test
-	public void parseDigestAuthentication() {
-		String request = "username=\"kanu\",realm=\"OLAT WebDAV Access\",nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\",uri=\"/webdav\",cnonce=\"195d0d15b31ee8f0a7f243b9bfcd881d\",nc=00000001,response=\"671742b00fae8d4c8ceb6a5bcf2b36fa\",qop=\"auth\"";
-		DigestAuthentication auth = DigestAuthentication.parse(request);
-		Assert.assertEquals("kanu", auth.getUsername());
-		Assert.assertEquals("OLAT WebDAV Access", auth.getRealm());
-		Assert.assertEquals("dcd98b7102dd2f0e8b11d0f600bfb0c093", auth.getNonce());
-		Assert.assertEquals("/webdav", auth.getUri());
-		Assert.assertEquals("195d0d15b31ee8f0a7f243b9bfcd881d", auth.getCnonce());
-		Assert.assertEquals("00000001", auth.getNc());
-		Assert.assertEquals("671742b00fae8d4c8ceb6a5bcf2b36fa", auth.getResponse());
-		Assert.assertEquals("auth", auth.getQop());
-	}
-	
 	@Test
 	public void handleBasicAuthentication() {
 		Identity id = JunitTestHelper.createAndPersistIdentityAsUser("dav-user-" + UUID.randomUUID().toString());
diff --git a/src/test/java/org/olat/test/AllTestsJunit4.java b/src/test/java/org/olat/test/AllTestsJunit4.java
index 5cf77c00e05..deb4aa9f32b 100644
--- a/src/test/java/org/olat/test/AllTestsJunit4.java
+++ b/src/test/java/org/olat/test/AllTestsJunit4.java
@@ -84,6 +84,7 @@ import org.junit.runners.Suite;
 	org.olat.commons.coordinate.CoordinatorTest.class,
 	org.olat.core.commons.services.help.spi.ConfluenceLinkSPITest.class,
 	org.olat.core.commons.services.webdav.WebDAVCommandsTest.class,
+	org.olat.core.commons.services.webdav.manager.DigestAuthenticationTest.class,
 	org.olat.core.commons.services.webdav.manager.WebDAVManagerTest.class,
 	org.olat.core.commons.services.webdav.servlets.RequestUtilsTest.class,
 	org.olat.core.commons.services.taskexecutor.PersistentTaskDAOTest.class,
-- 
GitLab