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