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.FileType;
022import org.apache.commons.vfs2.util.Cryptor;
023import org.apache.commons.vfs2.util.CryptorFactory;
024
025/**
026 * Implementation for any url based filesystem.
027 * <p>
028 * Parses the url into user/password/host/port/path.
029 * Does not handle a query string (after ?)
030 *
031 * @see URLFileNameParser URLFileNameParser for the implementation which also handles the query string too
032 */
033public class HostFileNameParser extends AbstractFileNameParser
034{
035    private final int defaultPort;
036
037    public HostFileNameParser(final int defaultPort)
038    {
039        this.defaultPort = defaultPort;
040    }
041
042    public int getDefaultPort()
043    {
044        return defaultPort;
045    }
046
047    @Override
048    public FileName parseUri(final VfsComponentContext context, final FileName base, final String filename)
049            throws FileSystemException
050    {
051        // FTP URI are generic URI (as per RFC 2396)
052        final StringBuilder name = new StringBuilder();
053
054        // Extract the scheme and authority parts
055        final Authority auth = extractToPath(filename, name);
056
057        // Decode and normalise the file name
058        UriParser.canonicalizePath(name, 0, name.length(), this);
059        UriParser.fixSeparators(name);
060        final FileType fileType = UriParser.normalisePath(name);
061        final String path = name.toString();
062
063        return new GenericFileName(
064            auth.scheme,
065            auth.hostName,
066            auth.port,
067            defaultPort,
068            auth.userName,
069            auth.password,
070            path,
071            fileType);
072    }
073
074    /**
075     * Extracts the scheme, userinfo, hostname and port components of a
076     * generic URI.
077     *
078     * @param uri  The absolute URI to parse.
079     * @param name Used to return the remainder of the URI.
080     * @return Authority extracted host authority, never null.
081     * @throws FileSystemException if authority cannot be extracted.
082     */
083    protected Authority extractToPath(final String uri,
084                                      final StringBuilder name)
085        throws FileSystemException
086    {
087        final Authority auth = new Authority();
088
089        // Extract the scheme
090        auth.scheme = UriParser.extractScheme(uri, name);
091
092        // Expecting "//"
093        if (name.length() < 2 || name.charAt(0) != '/' || name.charAt(1) != '/')
094        {
095            throw new FileSystemException("vfs.provider/missing-double-slashes.error", uri);
096        }
097        name.delete(0, 2);
098
099        // Extract userinfo, and split into username and password
100        final String userInfo = extractUserInfo(name);
101        final String userName;
102        final String password;
103        if (userInfo != null)
104        {
105            final int idx = userInfo.indexOf(':');
106            if (idx == -1)
107            {
108                userName = userInfo;
109                password = null;
110            }
111            else
112            {
113                userName = userInfo.substring(0, idx);
114                password = userInfo.substring(idx + 1);
115            }
116        }
117        else
118        {
119            userName = null;
120            password = null;
121        }
122        auth.userName = UriParser.decode(userName);
123        auth.password = UriParser.decode(password);
124
125        if (auth.password != null && auth.password.startsWith("{") && auth.password.endsWith("}"))
126        {
127            try
128            {
129                final Cryptor cryptor = CryptorFactory.getCryptor();
130                auth.password = cryptor.decrypt(auth.password.substring(1, auth.password.length() - 1));
131            }
132            catch (final Exception ex)
133            {
134                throw new FileSystemException("Unable to decrypt password", ex);
135            }
136        }
137
138        // Extract hostname, and normalise (lowercase)
139        final String hostName = extractHostName(name);
140        if (hostName == null)
141        {
142            throw new FileSystemException("vfs.provider/missing-hostname.error", uri);
143        }
144        auth.hostName = hostName.toLowerCase();
145
146        // Extract port
147        auth.port = extractPort(name, uri);
148
149        // Expecting '/' or empty name
150        if (name.length() > 0 && name.charAt(0) != '/')
151        {
152            throw new FileSystemException("vfs.provider/missing-hostname-path-sep.error", uri);
153        }
154
155        return auth;
156    }
157
158    /**
159     * Extracts the user info from a URI.
160     *
161     * @param name string buffer with the "scheme://" part has been removed already. Will be modified.
162     * @return the user information up to the '@' or null.
163     */
164    protected String extractUserInfo(final StringBuilder name)
165    {
166        final int maxlen = name.length();
167        for (int pos = 0; pos < maxlen; pos++)
168        {
169            final char ch = name.charAt(pos);
170            if (ch == '@')
171            {
172                // Found the end of the user info
173                final String userInfo = name.substring(0, pos);
174                name.delete(0, pos + 1);
175                return userInfo;
176            }
177            if (ch == '/' || ch == '?')
178            {
179                // Not allowed in user info
180                break;
181            }
182        }
183
184        // Not found
185        return null;
186    }
187
188    /**
189     * Extracts the hostname from a URI.
190     *
191     * @param name string buffer with the "scheme://[userinfo@]" part has been removed already. Will be modified.
192     * @return the host name  or null.
193     */
194    protected String extractHostName(final StringBuilder name)
195    {
196        final int maxlen = name.length();
197        int pos = 0;
198        for (; pos < maxlen; pos++)
199        {
200            final char ch = name.charAt(pos);
201            if (ch == '/' || ch == ';' || ch == '?' || ch == ':'
202                || ch == '@' || ch == '&' || ch == '=' || ch == '+'
203                || ch == '$' || ch == ',')
204            {
205                break;
206            }
207        }
208        if (pos == 0)
209        {
210            return null;
211        }
212
213        final String hostname = name.substring(0, pos);
214        name.delete(0, pos);
215        return hostname;
216    }
217
218    /**
219     * Extracts the port from a URI.
220     * @param name string buffer with the "scheme://[userinfo@]hostname" part has been removed already.
221     *     Will be 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    {
229        if (name.length() < 1 || name.charAt(0) != ':')
230        {
231            return -1;
232        }
233
234        final int maxlen = name.length();
235        int pos = 1;
236        for (; pos < maxlen; pos++)
237        {
238            final char ch = name.charAt(pos);
239            if (ch < '0' || ch > '9')
240            {
241                break;
242            }
243        }
244
245        final String port = name.substring(1, pos);
246        name.delete(0, pos);
247        if (port.length() == 0)
248        {
249            throw new FileSystemException("vfs.provider/missing-port.error", uri);
250        }
251
252        return Integer.parseInt(port);
253    }
254
255    /**
256     * Parsed authority info (scheme, hostname, username/password, port).
257     */
258    protected static class Authority
259    {
260        private String scheme;
261        private String hostName;
262        private String userName;
263        private String password;
264        private int port;
265
266        /**
267         * Get the connection schema.
268         * @return the connection scheme.
269         * @since 2.0
270         */
271        public String getScheme()
272        {
273            return scheme;
274        }
275
276        /**
277         * Set the connection schema.
278         * @param scheme the connection scheme.
279         * @since 2.0
280         */
281        public void setScheme(final String scheme)
282        {
283            this.scheme = scheme;
284        }
285
286        /**
287         * Get the host name.
288         * @return the host name.
289         * @since 2.0
290         */
291        public String getHostName()
292        {
293            return hostName;
294        }
295
296        /**
297         * Set the host name.
298         * @param hostName the host name.
299         * @since 2.0
300         */
301        public void setHostName(final String hostName)
302        {
303            this.hostName = hostName;
304        }
305
306        /**
307         * Get the user name.
308         * @return the user name or null.
309         * @since 2.0
310         */
311        public String getUserName()
312        {
313            return userName;
314        }
315
316        /**
317         * Set the user name.
318         * @param userName the user name.
319         * @since 2.0
320         */
321        public void setUserName(final String userName)
322        {
323            this.userName = userName;
324        }
325
326        /**
327         * Get the user password.
328         * @return the password or null.
329         * @since 2.0
330         */
331        public String getPassword()
332        {
333            return password;
334        }
335
336        /**
337         * Set the user password.
338         * @param password the user password.
339         * @since 2.0
340         */
341        public void setPassword(final String password)
342        {
343            this.password = password;
344        }
345
346        /**
347         * Get the port.
348         * @return the port or -1.
349         * @since 2.0
350         */
351        public int getPort()
352        {
353            return port;
354        }
355
356        /**
357         * Set the connection port.
358         * @param port the port number or -1.
359         * @since 2.0
360         */
361        public void setPort(final int port)
362        {
363            this.port = port;
364        }
365    }
366}