001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.vfs2.provider; 018 019import org.apache.commons.vfs2.FileName; 020import org.apache.commons.vfs2.FileSystemException; 021import org.apache.commons.vfs2.FileSystemManager; 022import org.apache.commons.vfs2.FileType; 023import org.apache.commons.vfs2.VFS; 024import org.apache.commons.vfs2.util.Cryptor; 025import org.apache.commons.vfs2.util.CryptorFactory; 026 027/** 028 * Implementation for any URL based file system. 029 * <p> 030 * Parses the URL into user/password/host/port/path. Does not handle a query string (after ?). 031 * </p> 032 * 033 * @see URLFileNameParser URLFileNameParser for the implementation which also handles the query string too. 034 */ 035public class HostFileNameParser extends AbstractFileNameParser { 036 037 /** 038 * Parsed authority info (scheme, hostname, username/password, port). 039 */ 040 protected static class Authority { 041 042 private String hostName; 043 private String password; 044 private int port; 045 private String scheme; 046 private String userName; 047 048 /** 049 * Constructs a new instance. 050 */ 051 public Authority() { 052 // empty 053 } 054 055 /** 056 * Gets the host name. 057 * 058 * @return the host name. 059 * @since 2.0 060 */ 061 public String getHostName() { 062 return hostName; 063 } 064 065 /** 066 * Gets the user password. 067 * 068 * @return the password or null. 069 * @since 2.0 070 */ 071 public String getPassword() { 072 return password; 073 } 074 075 /** 076 * Gets the port. 077 * 078 * @return the port or -1. 079 * @since 2.0 080 */ 081 public int getPort() { 082 return port; 083 } 084 085 /** 086 * Gets the connection schema. 087 * 088 * @return the connection scheme. 089 * @since 2.0 090 */ 091 public String getScheme() { 092 return scheme; 093 } 094 095 /** 096 * Gets the user name. 097 * 098 * @return the user name or null. 099 * @since 2.0 100 */ 101 public String getUserName() { 102 return userName; 103 } 104 105 /** 106 * Sets the host name. 107 * 108 * @param hostName the host name. 109 * @since 2.0 110 */ 111 public void setHostName(final String hostName) { 112 this.hostName = hostName; 113 } 114 115 /** 116 * Sets the user password. 117 * 118 * @param password the user password. 119 * @since 2.0 120 */ 121 public void setPassword(final String password) { 122 this.password = password; 123 } 124 125 /** 126 * Sets the connection port. 127 * 128 * @param port the port number or -1. 129 * @since 2.0 130 */ 131 public void setPort(final int port) { 132 this.port = port; 133 } 134 135 /** 136 * Sets the connection schema. 137 * 138 * @param scheme the connection scheme. 139 * @since 2.0 140 */ 141 public void setScheme(final String scheme) { 142 this.scheme = scheme; 143 } 144 145 /** 146 * Sets the user name. 147 * 148 * @param userName the user name. 149 * @since 2.0 150 */ 151 public void setUserName(final String userName) { 152 this.userName = userName; 153 } 154 } 155 156 private static boolean isHostNameTerminatingChar(final char ch, final boolean isIPv6Host) { 157 if (isIPv6Host) { 158 return ch == ']'; 159 } 160 161 return ch == '/' || ch == ';' || ch == '?' || ch == ':' || ch == '@' || ch == '&' || ch == '=' || ch == '+' 162 || ch == '$' || ch == ','; 163 } 164 165 private static boolean isIPv6Host(final String name) { 166 return name.length() > 0 && isIPv6HostHeadingChar(name.charAt(0)); 167 } 168 169 private static boolean isIPv6HostHeadingChar(final char ch) { 170 return ch == '['; 171 } 172 173 private final int defaultPort; 174 175 /** 176 * Constructs a new instance. 177 * 178 * @param defaultPort The default port. 179 */ 180 public HostFileNameParser(final int defaultPort) { 181 this.defaultPort = defaultPort; 182 } 183 184 /** 185 * Extracts the hostname from a URI. 186 * 187 * @param name string buffer with the "scheme://[userinfo@]" part has been removed already. Will be modified. 188 * @return the host name or null. 189 */ 190 protected String extractHostName(final StringBuilder name) { 191 final int maxlen = name.length(); 192 final boolean isIPv6Host = isIPv6Host(name.toString()); 193 int pos = 0; 194 for (; pos < maxlen; pos++) { 195 final char ch = name.charAt(pos); 196 if (isHostNameTerminatingChar(ch, isIPv6Host)) { 197 break; 198 } 199 } 200 if (pos == 0) { 201 return null; 202 } 203 204 if (isIPv6Host && pos < maxlen) { 205 if (pos == 1) { 206 return null; // Returning empty host 207 } 208 209 pos++; // Including terminating ']' into the extracted host string for IPv6 hosts 210 } 211 212 final String hostname = name.substring(0, pos); 213 name.delete(0, pos); 214 return hostname; 215 } 216 217 /** 218 * Extracts the port from a URI. 219 * 220 * @param name string buffer with the "scheme://[userinfo@]hostname" part has been removed already. Will be 221 * modified. 222 * @param uri full URI for error reporting. 223 * @return The port, or -1 if the URI does not contain a port. 224 * @throws FileSystemException if URI is malformed. 225 * @throws NumberFormatException if port number cannot be parsed. 226 */ 227 protected int extractPort(final StringBuilder name, final String uri) throws FileSystemException { 228 if (name.length() < 1 || name.charAt(0) != ':') { 229 return -1; 230 } 231 232 final int maxlen = name.length(); 233 int pos = 1; 234 for (; pos < maxlen; pos++) { 235 final char ch = name.charAt(pos); 236 if (ch < '0' || ch > '9') { 237 break; 238 } 239 } 240 241 final String port = name.substring(1, pos); 242 name.delete(0, pos); 243 if (port.isEmpty()) { 244 throw new FileSystemException("vfs.provider/missing-port.error", uri); 245 } 246 247 return Integer.parseInt(port); 248 } 249 250 /** 251 * Extracts the scheme, userinfo, hostname and port components of a generic URI. 252 * 253 * @param uri The absolute URI to parse. 254 * @param name Used to return the remainder of the URI. 255 * @return Authority extracted host authority, never null. 256 * @throws FileSystemException if authority cannot be extracted. 257 * @deprecated Use {@link #extractToPath(VfsComponentContext, String, StringBuilder)}. 258 */ 259 @Deprecated 260 protected Authority extractToPath(final String uri, final StringBuilder name) throws FileSystemException { 261 return extractToPath(null, uri, name); 262 } 263 264 /** 265 * Extracts the scheme, userinfo, hostname and port components of a generic URI. 266 * 267 * @param context component context. 268 * @param uri The absolute URI to parse. 269 * @param name Used to return the remainder of the URI. 270 * @return Authority extracted host authority, never null. 271 * @throws FileSystemException if authority cannot be extracted. 272 */ 273 protected Authority extractToPath(final VfsComponentContext context, final String uri, final StringBuilder name) throws FileSystemException { 274 final Authority auth = new Authority(); 275 276 final FileSystemManager fsm; 277 if (context != null) { 278 fsm = context.getFileSystemManager(); 279 } else { 280 fsm = VFS.getManager(); 281 } 282 283 // Extract the scheme 284 auth.scheme = UriParser.extractScheme(fsm.getSchemes(), uri, name); 285 286 // Expecting "//" 287 if (name.length() < 2 || name.charAt(0) != '/' || name.charAt(1) != '/') { 288 throw new FileSystemException("vfs.provider/missing-double-slashes.error", uri); 289 } 290 name.delete(0, 2); 291 292 // Extract userinfo, and split into username and password 293 final String userInfo = extractUserInfo(name); 294 final String userName; 295 final String password; 296 if (userInfo != null) { 297 final int idx = userInfo.indexOf(':'); 298 if (idx == -1) { 299 userName = userInfo; 300 password = null; 301 } else { 302 userName = userInfo.substring(0, idx); 303 password = userInfo.substring(idx + 1); 304 } 305 } else { 306 userName = null; 307 password = null; 308 } 309 auth.userName = UriParser.decode(userName); 310 auth.password = UriParser.decode(password); 311 312 if (auth.password != null && auth.password.startsWith("{") && auth.password.endsWith("}")) { 313 try { 314 final Cryptor cryptor = CryptorFactory.getCryptor(); 315 auth.password = cryptor.decrypt(auth.password.substring(1, auth.password.length() - 1)); 316 } catch (final Exception ex) { 317 throw new FileSystemException("Unable to decrypt password", ex); 318 } 319 } 320 321 // Extract hostname, and normalize (lowercase) 322 final String hostName = extractHostName(name); 323 if (hostName == null) { 324 throw new FileSystemException("vfs.provider/missing-hostname.error", uri); 325 } 326 if (isIPv6Host(hostName) && !isHostNameTerminatingChar(hostName.charAt(hostName.length() - 1), true)) { 327 throw new FileSystemException("vfs.provider/unterminated-ipv6-hostname.error", uri); 328 } 329 330 auth.hostName = hostName.toLowerCase(); 331 332 // Extract port 333 auth.port = extractPort(name, uri); 334 335 // Expecting '/' or empty name 336 if (name.length() > 0 && name.charAt(0) != '/') { 337 throw new FileSystemException("vfs.provider/missing-hostname-path-sep.error", uri); 338 } 339 340 return auth; 341 } 342 343 /** 344 * Extracts the user info from a URI. 345 * 346 * @param name string buffer with the "scheme://" part has been removed already. Will be modified. 347 * @return the user information up to the '@' or null. 348 */ 349 protected String extractUserInfo(final StringBuilder name) { 350 final int maxlen = name.length(); 351 for (int pos = 0; pos < maxlen; pos++) { 352 final char ch = name.charAt(pos); 353 if (ch == '@') { 354 // Found the end of the user info 355 final String userInfo = name.substring(0, pos); 356 name.delete(0, pos + 1); 357 return userInfo; 358 } 359 if (ch == '/' || ch == '?') { 360 // Not allowed in user info 361 break; 362 } 363 } 364 365 // Not found 366 return null; 367 } 368 369 /** 370 * Gets the default port. 371 * 372 * @return the default port. 373 */ 374 public int getDefaultPort() { 375 return defaultPort; 376 } 377 378 @Override 379 public FileName parseUri(final VfsComponentContext context, final FileName base, final String fileName) 380 throws FileSystemException { 381 // FTP URI are generic URI (as per RFC 2396) 382 final StringBuilder name = new StringBuilder(); 383 384 // Extract the scheme and authority parts 385 final Authority auth = extractToPath(context, fileName, name); 386 387 // Decode and normalize the file name 388 UriParser.canonicalizePath(name, 0, name.length(), this); 389 UriParser.fixSeparators(name); 390 final FileType fileType = UriParser.normalisePath(name); 391 final String path = name.toString(); 392 393 return new GenericFileName(auth.scheme, auth.hostName, auth.port, defaultPort, auth.userName, auth.password, 394 path, fileType); 395 } 396}