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