OLD | NEW |
1 // Copyright 2013 The Chromium Authors. All rights reserved. | 1 // Copyright 2013 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 package org.chromium.chrome.browser; | 5 package org.chromium.chrome.browser; |
6 | 6 |
7 import android.text.TextUtils; | 7 import android.text.TextUtils; |
8 | 8 |
9 import org.chromium.base.CollectionUtil; | 9 import org.chromium.base.CollectionUtil; |
| 10 import org.chromium.base.Log; |
10 | 11 |
| 12 import java.io.UnsupportedEncodingException; |
11 import java.net.URI; | 13 import java.net.URI; |
12 import java.net.URISyntaxException; | 14 import java.net.URISyntaxException; |
| 15 import java.net.URLDecoder; |
13 import java.util.HashSet; | 16 import java.util.HashSet; |
| 17 import java.util.regex.Matcher; |
| 18 import java.util.regex.Pattern; |
14 | 19 |
15 /** | 20 /** |
16 * Utilities for working with URIs (and URLs). These methods may be used in secu
rity-sensitive | 21 * Utilities for working with URIs (and URLs). These methods may be used in secu
rity-sensitive |
17 * contexts (after all, origins are the security boundary on the web), and so th
e correctness bar | 22 * contexts (after all, origins are the security boundary on the web), and so th
e correctness bar |
18 * must be high. | 23 * must be high. |
19 */ | 24 */ |
20 public class UrlUtilities { | 25 public class UrlUtilities { |
| 26 private static final String TAG = "UrlUtilities"; |
| 27 |
21 /** | 28 /** |
22 * URI schemes that ContentView can handle. | 29 * URI schemes that ContentView can handle. |
23 */ | 30 */ |
24 private static final HashSet<String> ACCEPTED_SCHEMES = CollectionUtil.newHa
shSet( | 31 private static final HashSet<String> ACCEPTED_SCHEMES = CollectionUtil.newHa
shSet( |
25 "about", "data", "file", "http", "https", "inline", "javascript"); | 32 "about", "data", "file", "http", "https", "inline", "javascript"); |
26 | 33 |
27 /** | 34 /** |
28 * URI schemes that Chrome can download. | 35 * URI schemes that Chrome can download. |
29 */ | 36 */ |
30 private static final HashSet<String> DOWNLOADABLE_SCHEMES = CollectionUtil.n
ewHashSet( | 37 private static final HashSet<String> DOWNLOADABLE_SCHEMES = CollectionUtil.n
ewHashSet( |
(...skipping 169 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
200 * no subdomains, from the given URI. Returns an empty string if the URI is
invalid, has no host | 207 * no subdomains, from the given URI. Returns an empty string if the URI is
invalid, has no host |
201 * (e.g. a file: URI), has multiple trailing dots, is an IP address, has onl
y one subcomponent | 208 * (e.g. a file: URI), has multiple trailing dots, is an IP address, has onl
y one subcomponent |
202 * (i.e. no dots other than leading/trailing ones), or is itself a recognize
d registry | 209 * (i.e. no dots other than leading/trailing ones), or is itself a recognize
d registry |
203 * identifier. | 210 * identifier. |
204 */ | 211 */ |
205 public static String getDomainAndRegistry(String uri, boolean includePrivate
Registries) { | 212 public static String getDomainAndRegistry(String uri, boolean includePrivate
Registries) { |
206 if (TextUtils.isEmpty(uri)) return uri; | 213 if (TextUtils.isEmpty(uri)) return uri; |
207 return nativeGetDomainAndRegistry(uri, includePrivateRegistries); | 214 return nativeGetDomainAndRegistry(uri, includePrivateRegistries); |
208 } | 215 } |
209 | 216 |
| 217 // Patterns used in validateIntentUrl. |
| 218 private static final Pattern DNS_HOSTNAME_PATTERN = |
| 219 Pattern.compile("^[\\w\\.-]*$"); |
| 220 private static final Pattern JAVA_PACKAGE_NAME_PATTERN = |
| 221 Pattern.compile("^[\\w\\.-]*$"); |
| 222 private static final Pattern ANDROID_COMPONENT_NAME_PATTERN = |
| 223 Pattern.compile("^[\\w\\./-]*$"); |
| 224 private static final Pattern URL_SCHEME_PATTERN = |
| 225 Pattern.compile("^[a-zA-Z]+$"); |
| 226 |
| 227 /** |
| 228 * @param url An Android intent:// URL to validate. |
| 229 * |
| 230 * @throws URISyntaxException if url is not a valid Android intent:// |
| 231 * URL, as specified at |
| 232 * https://developer.chrome.com/multidevice/android/intents#syntax. |
| 233 */ |
| 234 public static boolean validateIntentUrl(String url) { |
| 235 if (url == null) { |
| 236 Log.d(TAG, "url was null"); |
| 237 return false; |
| 238 } |
| 239 |
| 240 URI parsed = null; |
| 241 try { |
| 242 parsed = new URI(url); |
| 243 } catch (URISyntaxException e) { |
| 244 Log.d(TAG, e.toString()); |
| 245 return false; |
| 246 } |
| 247 |
| 248 if (!parsed.getScheme().equals("intent")) { |
| 249 Log.d(TAG, "scheme was not 'intent'"); |
| 250 return false; |
| 251 } |
| 252 |
| 253 String hostname = parsed.getHost(); |
| 254 if (hostname == null) { |
| 255 Log.d(TAG, "hostname was null for '" + url + "'"); |
| 256 return false; |
| 257 } |
| 258 Matcher m = DNS_HOSTNAME_PATTERN.matcher(hostname); |
| 259 if (!m.matches()) { |
| 260 Log.d(TAG, "hostname did not match DNS_HOSTNAME_PATTERN"); |
| 261 return false; |
| 262 } |
| 263 |
| 264 String path = parsed.getPath(); |
| 265 if (path == null || (!path.isEmpty() && !path.equals("/"))) { |
| 266 Log.d(TAG, "path was null or not '/'"); |
| 267 return false; |
| 268 } |
| 269 |
| 270 // We need to get the raw, unparsed, un-URL-decoded fragment. |
| 271 // parsed.getFragment() returns a URL-decoded fragment, which can |
| 272 // interfere with lexing and parsing Intent extras correctly. Therefore, |
| 273 // we handle the fragment "manually", but first assert that it |
| 274 // URL-decodes correctly. |
| 275 int fragmentStart = url.indexOf('#'); |
| 276 if (fragmentStart == -1 || fragmentStart == url.length() - 1) { |
| 277 Log.d(TAG, "Could not find '#'"); |
| 278 return false; |
| 279 } |
| 280 String fragment = url.substring(url.indexOf('#') + 1); |
| 281 try { |
| 282 String f = parsed.getFragment(); |
| 283 if (f == null) { |
| 284 Log.d(TAG, "Could not get fragment from parsed URL"); |
| 285 return false; |
| 286 } |
| 287 if (!URLDecoder.decode(fragment, "UTF-8").equals(f)) { |
| 288 Log.d(TAG, "Parsed fragment does not equal lexed fragment"); |
| 289 return false; |
| 290 } |
| 291 } catch (UnsupportedEncodingException e) { |
| 292 Log.d(TAG, e.toString()); |
| 293 return false; |
| 294 } |
| 295 |
| 296 // Now lex and parse the correctly-encoded fragment. |
| 297 String[] parts = fragment.split(";"); |
| 298 if (parts.length < 3 |
| 299 || !parts[0].equals("Intent") |
| 300 || !parts[parts.length - 1].equals("end")) { |
| 301 Log.d(TAG, "Invalid fragment (not enough parts, lacking Intent, " |
| 302 + "or lacking end"); |
| 303 return false; |
| 304 } |
| 305 |
| 306 boolean seenPackage = false; |
| 307 boolean seenAction = false; |
| 308 boolean seenCategory = false; |
| 309 boolean seenComponent = false; |
| 310 boolean seenScheme = false; |
| 311 |
| 312 for (int i = 1; i < parts.length - 1; ++i) { |
| 313 // This is OK *only* because no valid package, action, category, |
| 314 // component, or scheme contains (unencoded) "=". |
| 315 String[] pair = parts[i].split("="); |
| 316 if (2 != pair.length) { |
| 317 Log.d(TAG, "Invalid key=value pair '" + parts[i] + "'"); |
| 318 return false; |
| 319 } |
| 320 |
| 321 m = JAVA_PACKAGE_NAME_PATTERN.matcher(pair[1]); |
| 322 if (pair[0].equals("package")) { |
| 323 if (seenPackage || !m.matches()) { |
| 324 Log.d(TAG, "Invalid package '" + pair[1] + "'"); |
| 325 return false; |
| 326 } |
| 327 seenPackage = true; |
| 328 } else if (pair[0].equals("action")) { |
| 329 if (seenAction || !m.matches()) { |
| 330 Log.d(TAG, "Invalid action '" + pair[1] + "'"); |
| 331 return false; |
| 332 } |
| 333 seenAction = true; |
| 334 } else if (pair[0].equals("category")) { |
| 335 if (seenCategory || !m.matches()) { |
| 336 Log.d(TAG, "Invalid category '" + pair[1] + "'"); |
| 337 return false; |
| 338 } |
| 339 seenCategory = true; |
| 340 } else if (pair[0].equals("component")) { |
| 341 Matcher componentMatcher = ANDROID_COMPONENT_NAME_PATTERN.matche
r(pair[1]); |
| 342 if (seenComponent || !componentMatcher.matches()) { |
| 343 Log.d(TAG, "Invalid component '" + pair[1] + "'"); |
| 344 return false; |
| 345 } |
| 346 seenComponent = true; |
| 347 } else if (pair[0].equals("scheme")) { |
| 348 if (seenScheme) return false; |
| 349 Matcher schemeMatcher = URL_SCHEME_PATTERN.matcher(pair[1]); |
| 350 if (!schemeMatcher.matches()) { |
| 351 Log.d(TAG, "Invalid scheme '" + pair[1] + "'"); |
| 352 return false; |
| 353 } |
| 354 seenScheme = true; |
| 355 } else { |
| 356 // Assume we are seeing an Intent Extra. Up above, we ensured |
| 357 // that the #Intent... fragment was correctly URL-encoded; |
| 358 // beyond that, there is no further validation we can do. Extras |
| 359 // are blobs to us. |
| 360 continue; |
| 361 } |
| 362 } |
| 363 |
| 364 return true; |
| 365 } |
| 366 |
210 private static native boolean nativeSameDomainOrHost(String primaryUrl, Stri
ng secondaryUrl, | 367 private static native boolean nativeSameDomainOrHost(String primaryUrl, Stri
ng secondaryUrl, |
211 boolean includePrivateRegistries); | 368 boolean includePrivateRegistries); |
212 private static native String nativeGetDomainAndRegistry(String url, | 369 private static native String nativeGetDomainAndRegistry(String url, |
213 boolean includePrivateRegistries); | 370 boolean includePrivateRegistries); |
214 public static native boolean nativeIsGoogleSearchUrl(String url); | 371 public static native boolean nativeIsGoogleSearchUrl(String url); |
215 public static native boolean nativeIsGoogleHomePageUrl(String url); | 372 public static native boolean nativeIsGoogleHomePageUrl(String url); |
216 private static native String nativeFixupUrl(String url, String desiredTld); | 373 private static native String nativeFixupUrl(String url, String desiredTld); |
217 } | 374 } |
OLD | NEW |