View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.vfs2.provider;
18  
19  import org.apache.commons.vfs2.FileName;
20  import org.apache.commons.vfs2.FileSystemException;
21  import org.apache.commons.vfs2.FileSystemManager;
22  import org.apache.commons.vfs2.FileType;
23  import org.apache.commons.vfs2.VFS;
24  import org.apache.commons.vfs2.util.Cryptor;
25  import org.apache.commons.vfs2.util.CryptorFactory;
26  
27  /**
28   * Implementation for any URL based file system.
29   * <p>
30   * Parses the URL into user/password/host/port/path. Does not handle a query string (after ?).
31   * </p>
32   *
33   * @see URLFileNameParser URLFileNameParser for the implementation which also handles the query string too.
34   */
35  public class HostFileNameParser extends AbstractFileNameParser {
36  
37      /**
38       * Parsed authority info (scheme, hostname, username/password, port).
39       */
40      protected static class Authority {
41  
42          private String hostName;
43          private String password;
44          private int port;
45          private String scheme;
46          private String userName;
47  
48          /**
49           * Constructs a new instance.
50           */
51          public Authority() {
52              // empty
53          }
54  
55          /**
56           * Gets the host name.
57           *
58           * @return the host name.
59           * @since 2.0
60           */
61          public String getHostName() {
62              return hostName;
63          }
64  
65          /**
66           * Gets the user password.
67           *
68           * @return the password or null.
69           * @since 2.0
70           */
71          public String getPassword() {
72              return password;
73          }
74  
75          /**
76           * Gets the port.
77           *
78           * @return the port or -1.
79           * @since 2.0
80           */
81          public int getPort() {
82              return port;
83          }
84  
85          /**
86           * Gets the connection schema.
87           *
88           * @return the connection scheme.
89           * @since 2.0
90           */
91          public String getScheme() {
92              return scheme;
93          }
94  
95          /**
96           * Gets the user name.
97           *
98           * @return the user name or null.
99           * @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 }