Introduce scheme name escaping in SSLUtils.

Change-Id: I73f19e7d40d0b19dfd41cfaf7db0879ef2e3a3ea
This commit is contained in:
Ben Komalo 2011-06-08 11:38:36 -07:00
parent 313586c8eb
commit 724c3a81cd
4 changed files with 122 additions and 9 deletions

View File

@ -18,6 +18,7 @@
package com.android.emailcommon.provider;
import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
import com.android.emailcommon.utility.SSLUtils;
import com.android.emailcommon.utility.Utility;
import android.content.ContentValues;
@ -133,13 +134,10 @@ public final class HostAuth extends EmailContent implements HostAuthColumns, Par
throw new IllegalArgumentException(
"Can't specify a certificate alias for a non-secure connection");
}
// TODO: investigate what the certificate aliases look like from the framework
// and ensure they're safe scheme names.
String safeScheme = clientAlias;
if (!security.endsWith("+")) {
security += "+";
}
security += safeScheme;
security += SSLUtils.escapeForSchemeName(clientAlias);
}
return protocol + security;

View File

@ -42,4 +42,32 @@ public class SSLUtils {
return sSecureFactory;
}
}
/**
* Escapes the contents a string to be used as a safe scheme name in the URI according to
* http://tools.ietf.org/html/rfc3986#section-3.1
*
* This does not ensure that the first character is a letter (which is required by the RFC).
*/
public static String escapeForSchemeName(String s) {
// According to the RFC, scheme names are case-insensitive.
s = s.toLowerCase();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (Character.isLetter(c) || Character.isDigit(c)
|| ('-' == c) || ('.' == c)) {
// Safe - use as is.
sb.append(c);
} else if ('+' == c) {
// + is used as our escape character, so double it up.
sb.append("++");
} else {
// Unsafe - escape.
sb.append('+').append((int) c);
}
}
return sb.toString();
}
}

View File

@ -450,15 +450,15 @@ public class HostAuthTests extends AndroidTestCase {
scheme = HostAuth.getSchemeString("foo", HostAuth.FLAG_TLS, "custom-client-cert-alias");
assertEquals("foo+tls+custom-client-cert-alias", scheme);
scheme = HostAuth.getSchemeString(
"foo", HostAuth.FLAG_SSL | HostAuth.FLAG_TRUST_ALL, "custom_client_cert_alias");
assertEquals("foo+ssl+trustallcerts+custom_client_cert_alias", scheme);
"foo", HostAuth.FLAG_SSL | HostAuth.FLAG_TRUST_ALL, "custom-client-cert-alias");
assertEquals("foo+ssl+trustallcerts+custom-client-cert-alias", scheme);
scheme = HostAuth.getSchemeString(
"foo", HostAuth.FLAG_TLS | HostAuth.FLAG_TRUST_ALL, "custom_client_cert_alias");
assertEquals("foo+tls+trustallcerts+custom_client_cert_alias", scheme);
"foo", HostAuth.FLAG_TLS | HostAuth.FLAG_TRUST_ALL, "custom-client-cert-alias");
assertEquals("foo+tls+trustallcerts+custom-client-cert-alias", scheme);
try {
scheme = HostAuth.getSchemeString(
"foo", 0 /* no security flags */, "custom_client_cert_alias");
"foo", 0 /* no security flags */, "custom-client-cert-alias");
fail("Should not be able to set a custom client cert on an insecure connection");
} catch (IllegalArgumentException expected) {
}

View File

@ -0,0 +1,87 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.emailcommon.utility;
import android.test.AndroidTestCase;
import android.test.MoreAsserts;
import android.test.suitebuilder.annotation.SmallTest;
import java.util.Random;
import java.util.regex.Pattern;
/**
* Unit tests for SSLUtils.
*/
@SmallTest
public class SSLUtilsTest extends AndroidTestCase {
String SAFE_SCHEME_PATTERN = "[a-z][a-z0-9+\\-]*";
private void assertSchemeNameValid(String s) {
assertTrue(Pattern.matches(SAFE_SCHEME_PATTERN, s));
}
public void testSchemeNameEscapeAlreadySafe() {
// Safe names are unmodified.
assertEquals("http", SSLUtils.escapeForSchemeName("http"));
assertEquals("https", SSLUtils.escapeForSchemeName("https"));
assertEquals("ftp", SSLUtils.escapeForSchemeName("ftp"));
assertEquals("z39.50r", SSLUtils.escapeForSchemeName("z39.50r"));
assertEquals("fake-protocol.yes", SSLUtils.escapeForSchemeName("fake-protocol.yes"));
}
public void testSchemeNameEscapeIsSafe() {
// Invalid characters are escaped properly
assertSchemeNameValid(SSLUtils.escapeForSchemeName("name with spaces"));
assertSchemeNameValid(SSLUtils.escapeForSchemeName("odd * & characters"));
assertSchemeNameValid(SSLUtils.escapeForSchemeName("f3v!l;891023-47 +"));
}
private static final char[] RANDOM_DICT = new char[] {
'x', '.', '^', '4', ';', ' ', 'j', '#', '~', '+'
};
private String randomString(Random r) {
// 5 to 15 characters
int length = (r.nextInt() % 5) + 10;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(RANDOM_DICT[Math.abs(r.nextInt()) % RANDOM_DICT.length]);
}
return sb.toString();
}
public void testSchemeNamesAreMoreOrLessUnique() {
assertEquals(
SSLUtils.escapeForSchemeName("name with spaces"),
SSLUtils.escapeForSchemeName("name with spaces"));
// As expected, all escaping is case insensitive.
assertEquals(
SSLUtils.escapeForSchemeName("NAME with spaces"),
SSLUtils.escapeForSchemeName("name with spaces"));
Random random = new Random(314159 /* seed */);
for (int i = 0; i < 100; i++) {
// Other strings should more or less be unique.
String s1 = randomString(random);
String s2 = randomString(random);
MoreAsserts.assertNotEqual(
SSLUtils.escapeForSchemeName(s1),
SSLUtils.escapeForSchemeName(s2));
}
}
}