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 */
017
018package org.apache.commons.net.imap;
019
020import java.io.BufferedWriter;
021import java.io.IOException;
022import java.io.InputStreamReader;
023import java.io.OutputStreamWriter;
024
025import javax.net.ssl.HostnameVerifier;
026import javax.net.ssl.KeyManager;
027import javax.net.ssl.SSLContext;
028import javax.net.ssl.SSLException;
029import javax.net.ssl.SSLHandshakeException;
030import javax.net.ssl.SSLSocket;
031import javax.net.ssl.SSLSocketFactory;
032import javax.net.ssl.TrustManager;
033
034import org.apache.commons.net.io.CRLFLineReader;
035import org.apache.commons.net.util.SSLContextUtils;
036import org.apache.commons.net.util.SSLSocketUtils;
037
038/**
039 * The IMAPSClient class provides SSL/TLS connection encryption to IMAPClient. Copied from
040 * <a href="https://commons.apache.org/proper/commons-net/apidocs/index.html?org/apache/commons/net/ftp/FTPSClient.html"> FTPSClient</a> and modified to suit
041 * IMAP. If implicit mode is selected (NOT the default), SSL/TLS negotiation starts right after the connection has been established. In explicit mode (the
042 * default), SSL/TLS negotiation starts when the user calls execTLS() and the server accepts the command.
043 *
044 * <pre>
045 * {@code
046 * //Implicit usage:
047 *
048 *               IMAPSClient c = new IMAPSClient(true);
049 *               c.connect("127.0.0.1", 993);
050 *
051 * //Explicit usage:
052 *
053 *               IMAPSClient c = new IMAPSClient();
054 *               c.connect("127.0.0.1", 143);
055 *               if (c.execTLS()) { /rest of the commands here/ }
056 * }
057 * </pre>
058 *
059 * <b>Warning</b>: the hostname is not verified against the certificate by default, use {@link #setHostnameVerifier(HostnameVerifier)} or
060 * {@link #setEndpointCheckingEnabled(boolean)} (on Java 1.7+) to enable verification.
061 */
062public class IMAPSClient extends IMAPClient {
063    /** The default IMAP over SSL port. */
064    public static final int DEFAULT_IMAPS_PORT = 993;
065
066    /** Default secure socket protocol name. */
067    public static final String DEFAULT_PROTOCOL = "TLS";
068
069    /** The security mode. True - Implicit Mode / False - Explicit Mode. */
070    private final boolean isImplicit;
071    /** The secure socket protocol to be used, like SSL/TLS. */
072    private final String protocol;
073    /** The context object. */
074    private SSLContext context;
075    /**
076     * The cipher suites. SSLSockets have a default set of these anyway, so no initialization required.
077     */
078    private String[] suites;
079    /** The protocol versions. */
080    private String[] protocols // null;
081    ; // {"SSLv2", "SSLv3", "TLSv1", "TLSv1.1", "SSLv2Hello"};
082
083    /** The IMAPS {@link TrustManager} implementation, default null. */
084    private TrustManager trustManager;
085
086    /** The {@link KeyManager}, default null. */
087    private KeyManager keyManager;
088
089    /** The {@link HostnameVerifier} to use post-TLS, default null (i.e. no verification). */
090    private HostnameVerifier hostnameVerifier;
091
092    /** Use Java 1.7+ HTTPS Endpoint Identification Algorithm. */
093    private boolean tlsEndpointChecking;
094
095    /**
096     * Constructor for IMAPSClient. Sets security mode to explicit (isImplicit = false).
097     */
098    public IMAPSClient() {
099        this(DEFAULT_PROTOCOL, false);
100    }
101
102    /**
103     * Constructor for IMAPSClient.
104     *
105     * @param implicit The security mode (Implicit/Explicit).
106     */
107    public IMAPSClient(final boolean implicit) {
108        this(DEFAULT_PROTOCOL, implicit);
109    }
110
111    /**
112     * Constructor for IMAPSClient.
113     *
114     * @param implicit The security mode(Implicit/Explicit).
115     * @param ctx      A pre-configured SSL Context.
116     */
117    public IMAPSClient(final boolean implicit, final SSLContext ctx) {
118        this(DEFAULT_PROTOCOL, implicit, ctx);
119    }
120
121    /**
122     * Constructor for IMAPSClient.
123     *
124     * @param context A pre-configured SSL Context.
125     */
126    public IMAPSClient(final SSLContext context) {
127        this(false, context);
128    }
129
130    /**
131     * Constructor for IMAPSClient.
132     *
133     * @param proto the protocol.
134     */
135    public IMAPSClient(final String proto) {
136        this(proto, false);
137    }
138
139    /**
140     * Constructor for IMAPSClient.
141     *
142     * @param proto    the protocol.
143     * @param implicit The security mode(Implicit/Explicit).
144     */
145    public IMAPSClient(final String proto, final boolean implicit) {
146        this(proto, implicit, null);
147    }
148
149    /**
150     * Constructor for IMAPSClient.
151     *
152     * @param proto    the protocol.
153     * @param implicit The security mode(Implicit/Explicit).
154     * @param ctx      the SSL context
155     */
156    public IMAPSClient(final String proto, final boolean implicit, final SSLContext ctx) {
157        setDefaultPort(DEFAULT_IMAPS_PORT);
158        protocol = proto;
159        isImplicit = implicit;
160        context = ctx;
161    }
162
163    /**
164     * Because there are so many connect() methods, the _connectAction_() method is provided as a means of performing some action immediately after establishing
165     * a connection, rather than reimplementing all the connect() methods.
166     *
167     * @throws IOException If it is thrown by _connectAction_().
168     * @see org.apache.commons.net.SocketClient#_connectAction_()
169     */
170    @Override
171    protected void _connectAction_() throws IOException {
172        // Implicit mode.
173        if (isImplicit) {
174            performSSLNegotiation();
175        }
176        super._connectAction_();
177        // Explicit mode - don't do anything. The user calls execTLS()
178    }
179
180    /**
181     * The TLS command execution.
182     *
183     * @throws SSLException If the server reply code is not positive.
184     * @throws IOException  If an I/O error occurs while sending the command or performing the negotiation.
185     * @return TRUE if the command and negotiation succeeded.
186     */
187    public boolean execTLS() throws SSLException, IOException {
188        if (sendCommand(IMAPCommand.getCommand(IMAPCommand.STARTTLS)) != IMAPReply.OK) {
189            return false;
190            // throw new SSLException(getReplyString());
191        }
192        performSSLNegotiation();
193        return true;
194    }
195
196    /**
197     * Returns the names of the cipher suites which could be enabled for use on this connection. When the underlying {@link java.net.Socket Socket} is not an
198     * {@link SSLSocket} instance, returns null.
199     *
200     * @return An array of cipher suite names, or {@code null}.
201     */
202    public String[] getEnabledCipherSuites() {
203        if (_socket_ instanceof SSLSocket) {
204            return ((SSLSocket) _socket_).getEnabledCipherSuites();
205        }
206        return null;
207    }
208
209    /**
210     * Returns the names of the protocol versions which are currently enabled for use on this connection. When the underlying {@link java.net.Socket Socket} is
211     * not an {@link SSLSocket} instance, returns null.
212     *
213     * @return An array of protocols, or {@code null}.
214     */
215    public String[] getEnabledProtocols() {
216        if (_socket_ instanceof SSLSocket) {
217            return ((SSLSocket) _socket_).getEnabledProtocols();
218        }
219        return null;
220    }
221
222    /**
223     * Gets the currently configured {@link HostnameVerifier}.
224     *
225     * @return A HostnameVerifier instance.
226     * @since 3.4
227     */
228    public HostnameVerifier getHostnameVerifier() {
229        return hostnameVerifier;
230    }
231
232    /**
233     * Gets the {@link KeyManager} instance.
234     *
235     * @return The current {@link KeyManager} instance.
236     */
237    private KeyManager getKeyManager() {
238        return keyManager;
239    }
240
241    /**
242     * Gets the currently configured {@link TrustManager}.
243     *
244     * @return A TrustManager instance.
245     */
246    public TrustManager getTrustManager() {
247        return trustManager;
248    }
249
250    /**
251     * Performs a lazy init of the SSL context.
252     *
253     * @throws IOException When could not initialize the SSL context.
254     */
255    private void initSSLContext() throws IOException {
256        if (context == null) {
257            context = SSLContextUtils.createSSLContext(protocol, getKeyManager(), getTrustManager());
258        }
259    }
260
261    /**
262     * Return whether or not endpoint identification using the HTTPS algorithm on Java 1.7+ is enabled. The default behavior is for this to be disabled.
263     *
264     * @return True if enabled, false if not.
265     * @since 3.4
266     */
267    public boolean isEndpointCheckingEnabled() {
268        return tlsEndpointChecking;
269    }
270
271    /**
272     * SSL/TLS negotiation. Acquires an SSL socket of a connection and carries out handshake processing.
273     *
274     * @throws IOException If server negotiation fails.
275     */
276    private void performSSLNegotiation() throws IOException {
277        initSSLContext();
278
279        final SSLSocketFactory ssf = context.getSocketFactory();
280        final String host = _hostname_ != null ? _hostname_ : getRemoteAddress().getHostAddress();
281        final int port = getRemotePort();
282        final SSLSocket socket = (SSLSocket) ssf.createSocket(_socket_, host, port, true);
283        socket.setEnableSessionCreation(true);
284        socket.setUseClientMode(true);
285
286        if (tlsEndpointChecking) {
287            SSLSocketUtils.enableEndpointNameVerification(socket);
288        }
289
290        if (protocols != null) {
291            socket.setEnabledProtocols(protocols);
292        }
293        if (suites != null) {
294            socket.setEnabledCipherSuites(suites);
295        }
296        socket.startHandshake();
297
298        // TODO the following setup appears to duplicate that in the super class methods
299        _socket_ = socket;
300        _input_ = socket.getInputStream();
301        _output_ = socket.getOutputStream();
302        _reader = new CRLFLineReader(new InputStreamReader(_input_, __DEFAULT_ENCODING));
303        __writer = new BufferedWriter(new OutputStreamWriter(_output_, __DEFAULT_ENCODING));
304
305        if (hostnameVerifier != null && !hostnameVerifier.verify(host, socket.getSession())) {
306            throw new SSLHandshakeException("Hostname doesn't match certificate");
307        }
308    }
309
310    /**
311     * Controls which particular cipher suites are enabled for use on this connection. Called before server negotiation.
312     *
313     * @param cipherSuites The cipher suites.
314     */
315    public void setEnabledCipherSuites(final String[] cipherSuites) {
316        suites = cipherSuites.clone();
317    }
318
319    /**
320     * Controls which particular protocol versions are enabled for use on this connection. I perform setting before a server negotiation.
321     *
322     * @param protocolVersions The protocol versions.
323     */
324    public void setEnabledProtocols(final String[] protocolVersions) {
325        protocols = protocolVersions.clone();
326    }
327
328    /**
329     * Automatic endpoint identification checking using the HTTPS algorithm is supported on Java 1.7+. The default behavior is for this to be disabled.
330     *
331     * @param enable Enable automatic endpoint identification checking using the HTTPS algorithm on Java 1.7+.
332     * @since 3.4
333     */
334    public void setEndpointCheckingEnabled(final boolean enable) {
335        tlsEndpointChecking = enable;
336    }
337
338    /**
339     * Override the default {@link HostnameVerifier} to use.
340     *
341     * @param newHostnameVerifier The HostnameVerifier implementation to set or {@code null} to disable.
342     * @since 3.4
343     */
344    public void setHostnameVerifier(final HostnameVerifier newHostnameVerifier) {
345        hostnameVerifier = newHostnameVerifier;
346    }
347
348    /**
349     * Sets a {@link KeyManager} to use.
350     *
351     * @param newKeyManager The KeyManager implementation to set.
352     * @see org.apache.commons.net.util.KeyManagerUtils
353     */
354    public void setKeyManager(final KeyManager newKeyManager) {
355        keyManager = newKeyManager;
356    }
357
358    /**
359     * Override the default {@link TrustManager} to use.
360     *
361     * @param newTrustManager The TrustManager implementation to set.
362     * @see org.apache.commons.net.util.TrustManagerUtils
363     */
364    public void setTrustManager(final TrustManager newTrustManager) {
365        trustManager = newTrustManager;
366    }
367}
368