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}