001    /****************************************************************
002     * Licensed to the Apache Software Foundation (ASF) under one   *
003     * or more contributor license agreements.  See the NOTICE file *
004     * distributed with this work for additional information        *
005     * regarding copyright ownership.  The ASF licenses this file   *
006     * to you under the Apache License, Version 2.0 (the            *
007     * "License"); you may not use this file except in compliance   *
008     * with the License.  You may obtain a copy of the License at   *
009     *                                                              *
010     *   http://www.apache.org/licenses/LICENSE-2.0                 *
011     *                                                              *
012     * Unless required by applicable law or agreed to in writing,   *
013     * software distributed under the License is distributed on an  *
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015     * KIND, either express or implied.  See the License for the    *
016     * specific language governing permissions and limitations      *
017     * under the License.                                           *
018     ****************************************************************/
019    
020    
021    
022    package org.apache.james.protocols.smtp.core.esmtp;
023    
024    import java.nio.charset.Charset;
025    import java.util.ArrayList;
026    import java.util.Collection;
027    import java.util.LinkedList;
028    import java.util.List;
029    import java.util.Locale;
030    import java.util.StringTokenizer;
031    
032    import org.apache.commons.codec.binary.Base64;
033    import org.apache.james.protocols.api.Request;
034    import org.apache.james.protocols.api.Response;
035    import org.apache.james.protocols.api.handler.CommandHandler;
036    import org.apache.james.protocols.api.handler.ExtensibleHandler;
037    import org.apache.james.protocols.api.handler.LineHandler;
038    import org.apache.james.protocols.api.handler.WiringException;
039    import org.apache.james.protocols.smtp.SMTPResponse;
040    import org.apache.james.protocols.smtp.SMTPRetCode;
041    import org.apache.james.protocols.smtp.SMTPSession;
042    import org.apache.james.protocols.smtp.dsn.DSNStatus;
043    import org.apache.james.protocols.smtp.hook.AuthHook;
044    import org.apache.james.protocols.smtp.hook.HookResult;
045    import org.apache.james.protocols.smtp.hook.HookResultHook;
046    import org.apache.james.protocols.smtp.hook.HookReturnCode;
047    import org.apache.james.protocols.smtp.hook.MailParametersHook;
048    
049    
050    /**
051     * handles AUTH command
052     * 
053     * Note: we could extend this to use java5 sasl standard libraries and provide client
054     * support against a server implemented via non-james specific hooks.
055     * This would allow us to reuse hooks between imap4/pop3/smtp and eventually different
056     * system (simple pluggabilty against external authentication services).
057     */
058    public class AuthCmdHandler
059        implements CommandHandler<SMTPSession>, EhloExtension, ExtensibleHandler, MailParametersHook {
060    
061        private final static Charset CHARSET = Charset.forName("US-ASCII");
062        private abstract class AbstractSMTPLineHandler implements LineHandler<SMTPSession> {
063    
064            public Response onLine(SMTPSession session, byte[] l) {
065                return handleCommand(session, new String(l, CHARSET));
066               
067            }
068    
069            private SMTPResponse handleCommand(SMTPSession session, String line) {
070                // See JAMES-939
071                
072                // According to RFC2554:
073                // "If the client wishes to cancel an authentication exchange, it issues a line with a single "*".
074                // If the server receives such an answer, it MUST reject the AUTH
075                // command by sending a 501 reply."
076                if (line.equals("*\r\n")) {
077                    session.popLineHandler();
078                    return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.SECURITY_AUTH) + " Authentication aborted");
079                }
080                return onCommand(session, line);
081            }
082    
083            protected abstract SMTPResponse onCommand(SMTPSession session, String l);
084        }
085    
086    
087    
088        /**
089         * The text string for the SMTP AUTH type PLAIN.
090         */
091        private final static String AUTH_TYPE_PLAIN = "PLAIN";
092    
093        /**
094         * The text string for the SMTP AUTH type LOGIN.
095         */
096        private final static String AUTH_TYPE_LOGIN = "LOGIN";
097    
098        /**
099         * The AuthHooks
100         */
101        private List<AuthHook> hooks;
102        
103        private List rHooks;
104        
105        /**
106         * handles AUTH command
107         *
108         */
109        public Response onCommand(SMTPSession session, Request request) {
110            return doAUTH(session, request.getArgument());
111        }
112    
113    
114    
115        /**
116         * Handler method called upon receipt of a AUTH command.
117         * Handles client authentication to the SMTP server.
118         *
119         * @param session SMTP session
120         * @param argument the argument passed in with the command by the SMTP client
121         */
122        private SMTPResponse doAUTH(SMTPSession session, String argument) {
123            if (session.getUser() != null) {
124                return new SMTPResponse(SMTPRetCode.BAD_SEQUENCE, DSNStatus.getStatus(DSNStatus.PERMANENT,DSNStatus.DELIVERY_OTHER)+" User has previously authenticated. "
125                        + " Further authentication is not required!");
126            } else if (argument == null) {
127                return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, DSNStatus.getStatus(DSNStatus.PERMANENT,DSNStatus.DELIVERY_INVALID_ARG)+" Usage: AUTH (authentication type) <challenge>");
128            } else {
129                String initialResponse = null;
130                if ((argument != null) && (argument.indexOf(" ") > 0)) {
131                    initialResponse = argument.substring(argument.indexOf(" ") + 1);
132                    argument = argument.substring(0,argument.indexOf(" "));
133                }
134                String authType = argument.toUpperCase(Locale.US);
135                if (authType.equals(AUTH_TYPE_PLAIN)) {
136                    String userpass;
137                    if (initialResponse == null) {
138                        session.pushLineHandler(new AbstractSMTPLineHandler() {
139                            protected SMTPResponse onCommand(SMTPSession session, String l) {
140                                return doPlainAuthPass(session, l);
141                            }
142                        });
143                        return new SMTPResponse(SMTPRetCode.AUTH_READY, "OK. Continue authentication");
144                    } else {
145                        userpass = initialResponse.trim();
146                        return doPlainAuthPass(session, userpass);
147                    }
148                } else if (authType.equals(AUTH_TYPE_LOGIN)) {
149                    
150                    if (initialResponse == null) {
151                        session.pushLineHandler(new AbstractSMTPLineHandler() {
152                            protected SMTPResponse onCommand(SMTPSession session, String l) {
153                                return doLoginAuthPass(session, l);
154                            }
155                        });
156                        return new SMTPResponse(SMTPRetCode.AUTH_READY, "VXNlcm5hbWU6"); // base64 encoded "Username:"
157                    } else {
158                        String user = initialResponse.trim();
159                        return doLoginAuthPass(session, user);
160                    }
161                } else {
162                    return doUnknownAuth(session, authType, initialResponse);
163                }
164            }
165        }
166    
167        /**
168         * Carries out the Plain AUTH SASL exchange.
169         *
170         * According to RFC 2595 the client must send: [authorize-id] \0 authenticate-id \0 password.
171         *
172         * >>> AUTH PLAIN dGVzdAB0ZXN0QHdpei5leGFtcGxlLmNvbQB0RXN0NDI=
173         * Decoded: test\000test@wiz.example.com\000tEst42
174         *
175         * >>> AUTH PLAIN dGVzdAB0ZXN0AHRFc3Q0Mg==
176         * Decoded: test\000test\000tEst42
177         *
178         * @param session SMTP session object
179         * @param initialResponse the initial response line passed in with the AUTH command
180         */
181        private SMTPResponse doPlainAuthPass(SMTPSession session, String userpass) {
182            String user = null, pass = null;
183            try {
184                if (userpass != null) {
185                    userpass = new String(Base64.decodeBase64(userpass));
186                }
187                if (userpass != null) {
188                    /*  See: RFC 2595, Section 6
189                        The mechanism consists of a single message from the client to the
190                        server.  The client sends the authorization identity (identity to
191                        login as), followed by a US-ASCII NUL character, followed by the
192                        authentication identity (identity whose password will be used),
193                        followed by a US-ASCII NUL character, followed by the clear-text
194                        password.  The client may leave the authorization identity empty to
195                        indicate that it is the same as the authentication identity.
196    
197                        The server will verify the authentication identity and password with
198                        the system authentication database and verify that the authentication
199                        credentials permit the client to login as the authorization identity.
200                        If both steps succeed, the user is logged in.
201                    */
202                    StringTokenizer authTokenizer = new StringTokenizer(userpass, "\0");
203                    String authorize_id = authTokenizer.nextToken();  // Authorization Identity
204                    user = authTokenizer.nextToken();                 // Authentication Identity
205                    try {
206                        pass = authTokenizer.nextToken();             // Password
207                    }
208                    catch (java.util.NoSuchElementException _) {
209                        // If we got here, this is what happened.  RFC 2595
210                        // says that "the client may leave the authorization
211                        // identity empty to indicate that it is the same as
212                        // the authentication identity."  As noted above,
213                        // that would be represented as a decoded string of
214                        // the form: "\0authenticate-id\0password".  The
215                        // first call to nextToken will skip the empty
216                        // authorize-id, and give us the authenticate-id,
217                        // which we would store as the authorize-id.  The
218                        // second call will give us the password, which we
219                        // think is the authenticate-id (user).  Then when
220                        // we ask for the password, there are no more
221                        // elements, leading to the exception we just
222                        // caught.  So we need to move the user to the
223                        // password, and the authorize_id to the user.
224                        pass = user;
225                        user = authorize_id;
226                    }
227    
228                    authTokenizer = null;
229                }
230            }
231            catch (Exception e) {
232                // Ignored - this exception in parsing will be dealt
233                // with in the if clause below
234            }
235            // Authenticate user
236            SMTPResponse response = doAuthTest(session, user, pass, "PLAIN");
237            
238            session.popLineHandler();
239    
240            return response;
241        }
242    
243        /**
244         * Carries out the Login AUTH SASL exchange.
245         *
246         * @param session SMTP session object
247         * @param initialResponse the initial response line passed in with the AUTH command
248         */
249        private SMTPResponse doLoginAuthPass(SMTPSession session, String user) {
250            if (user != null) {
251                try {
252                    user = new String(Base64.decodeBase64(user));
253                } catch (Exception e) {
254                    // Ignored - this parse error will be
255                    // addressed in the if clause below
256                    user = null;
257                }
258            }
259            
260            session.popLineHandler();
261            
262            session.pushLineHandler(new AbstractSMTPLineHandler() {
263    
264                private String user;
265    
266                public LineHandler<SMTPSession> setUser(String user) {
267                    this.user = user;
268                    return this;
269                }
270    
271                protected SMTPResponse onCommand(SMTPSession session, String l) {
272                    return doLoginAuthPassCheck(session, user, l);
273                }
274                
275            }.setUser(user));
276            return new SMTPResponse(SMTPRetCode.AUTH_READY, "UGFzc3dvcmQ6"); // base64 encoded "Password:"
277        }
278        
279        private SMTPResponse doLoginAuthPassCheck(SMTPSession session, String user, String pass) {
280            if (pass != null) {
281                try {
282                    pass = new String(Base64.decodeBase64(pass));
283                } catch (Exception e) {
284                    // Ignored - this parse error will be
285                    // addressed in the if clause below
286                    pass = null;
287                }
288            }
289           
290            session.popLineHandler();
291    
292            
293            // Authenticate user
294            SMTPResponse response = doAuthTest(session, user, pass, "LOGIN");
295           
296            return response;
297        }
298    
299    
300    
301        /**
302         * @param session
303         * @param user
304         * @param pass
305         * @param authType
306         * @return
307         */
308        private SMTPResponse doAuthTest(SMTPSession session, String user, String pass, String authType) {
309            if ((user == null) || (pass == null)) {
310                return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS,"Could not decode parameters for AUTH "+authType);
311            }
312    
313            SMTPResponse res = null;
314            
315            List<AuthHook> hooks = getHooks();
316            
317            if (hooks != null) {
318                int count = hooks.size();
319                for (int i = 0; i < count; i++) {
320                    AuthHook rawHook = hooks.get(i);
321                    session.getLogger().debug("executing  hook " + rawHook);
322                    
323    
324                    long start = System.currentTimeMillis();
325                    HookResult hRes = rawHook.doAuth(session, user, pass);
326                    long executionTime = System.currentTimeMillis() - start;
327    
328                    if (rHooks != null) {
329                        for (int i2 = 0; i2 < rHooks.size(); i2++) {
330                            Object rHook = rHooks.get(i2);
331                            session.getLogger().debug("executing  hook " + rHook);
332                        
333                            hRes = ((HookResultHook) rHook).onHookResult(session, hRes, executionTime, rawHook);
334                        }
335                    }
336                    
337                    res = calcDefaultSMTPResponse(hRes);
338                    
339                    if (res != null) {
340                        if (SMTPRetCode.AUTH_FAILED.equals(res.getRetCode())) {
341                            session.getLogger().error("AUTH method "+authType+" failed");
342                        } else if (SMTPRetCode.AUTH_OK.equals(res.getRetCode())) {
343                            if (session.getLogger().isDebugEnabled()) {
344                                // TODO: Make this string a more useful debug message
345                                session.getLogger().debug("AUTH method "+authType+" succeeded");
346                            }
347                        }
348                        return res;
349                    }
350                }
351            }
352    
353            res = new SMTPResponse(SMTPRetCode.AUTH_FAILED, "Authentication Failed");
354            session.getLogger().error("AUTH method "+authType+" failed from " + user + "@" + session.getRemoteIPAddress()); 
355            return res;
356        }
357    
358    
359        /**
360         * Calculate the SMTPResponse for the given result
361         * 
362         * @param result the HookResult which should converted to SMTPResponse
363         * @return the calculated SMTPResponse for the given HookReslut
364         */
365        protected SMTPResponse calcDefaultSMTPResponse(HookResult result) {
366            if (result != null) {
367                int rCode = result.getResult();
368                String smtpRetCode = result.getSmtpRetCode();
369                String smtpDesc = result.getSmtpDescription();
370        
371                if ((rCode & HookReturnCode.DENY) == HookReturnCode.DENY) {
372                    if (smtpRetCode == null)
373                        smtpRetCode = SMTPRetCode.AUTH_FAILED;
374                    if (smtpDesc == null)
375                        smtpDesc = "Authentication Failed";
376        
377                    SMTPResponse response =  new SMTPResponse(smtpRetCode, smtpDesc);
378    
379                    if ((rCode & HookReturnCode.DISCONNECT) == HookReturnCode.DISCONNECT) {
380                        response.setEndSession(true);
381                    }
382                    return response;
383                } else if ((rCode & HookReturnCode.DENYSOFT) == HookReturnCode.DENYSOFT) {
384                    if (smtpRetCode == null)
385                        smtpRetCode = SMTPRetCode.LOCAL_ERROR;
386                    if (smtpDesc == null)
387                        smtpDesc = "Temporary problem. Please try again later";
388        
389                    SMTPResponse response =  new SMTPResponse(smtpRetCode, smtpDesc);
390    
391                    if ((rCode & HookReturnCode.DISCONNECT) == HookReturnCode.DISCONNECT) {
392                        response.setEndSession(true);
393                    }
394                    return response;
395                } else if ((rCode & HookReturnCode.OK) == HookReturnCode.OK) {
396                    if (smtpRetCode == null)
397                        smtpRetCode = SMTPRetCode.AUTH_OK;
398                    if (smtpDesc == null)
399                        smtpDesc = "Authentication Succesfull";
400                    
401                    SMTPResponse response =  new SMTPResponse(smtpRetCode, smtpDesc);
402    
403                    if ((rCode & HookReturnCode.DISCONNECT) == HookReturnCode.DISCONNECT) {
404                        response.setEndSession(true);
405                    }
406                    return response;
407                } else if ((rCode & HookReturnCode.DISCONNECT) == HookReturnCode.DISCONNECT) {
408                    SMTPResponse response =  new SMTPResponse("");
409                    response.setEndSession(true);
410                
411                    return response;
412                } else {
413                    // Return null as default
414                    return null;
415                }
416            } else {
417                return null;
418            }
419        }
420    
421        /**
422         * Handles the case of an unrecognized auth type.
423         *
424         * @param session SMTP session object
425         * @param authType the unknown auth type
426         * @param initialResponse the initial response line passed in with the AUTH command
427         */
428        private SMTPResponse doUnknownAuth(SMTPSession session, String authType, String initialResponse) {
429            if (session.getLogger().isInfoEnabled()) {
430                StringBuilder errorBuffer =
431                    new StringBuilder(128)
432                        .append("AUTH method ")
433                            .append(authType)
434                            .append(" is an unrecognized authentication type");
435                session.getLogger().info(errorBuffer.toString());
436            }
437            return new SMTPResponse(SMTPRetCode.PARAMETER_NOT_IMPLEMENTED, "Unrecognized Authentication Type");
438        }
439    
440    
441    
442        /**
443         * @see org.apache.james.protocols.api.handler.CommandHandler#getImplCommands()
444         */
445        public Collection<String> getImplCommands() {
446            Collection<String> implCommands = new ArrayList<String>();
447            implCommands.add("AUTH");
448            
449            return implCommands;
450        }
451    
452        /**
453         * @see org.apache.james.protocols.smtp.core.esmtp.EhloExtension#getImplementedEsmtpFeatures(org.apache.james.protocols.smtp.SMTPSession)
454         */
455        public List<String> getImplementedEsmtpFeatures(SMTPSession session) {
456            if (session.isAuthSupported()) {
457                List<String> resp = new LinkedList<String>();
458                resp.add("AUTH LOGIN PLAIN");
459                resp.add("AUTH=LOGIN PLAIN");
460                return resp;
461            } else {
462                return null;
463            }
464        }
465    
466        /**
467         * @see org.apache.james.protocols.api.handler.ExtensibleHandler#getMarkerInterfaces()
468         */
469        public List<Class<?>> getMarkerInterfaces() {
470            List<Class<?>> classes = new ArrayList<Class<?>>(1);
471            classes.add(AuthHook.class);
472            return classes;
473        }
474    
475    
476        /**
477         * @see org.apache.james.protocols.api.handler.ExtensibleHandler#wireExtensions(java.lang.Class, java.util.List)
478         */
479        public void wireExtensions(Class interfaceName, List extension) throws WiringException {
480            if (AuthHook.class.equals(interfaceName)) {
481                this.hooks = extension;
482                // If no AuthHook is configured then we revert to the default LocalUsersRespository check
483                if (hooks == null || hooks.size() == 0) {
484                    throw new WiringException("AuthCmdHandler used without AuthHooks");
485                }
486            } else if (HookResultHook.class.equals(interfaceName)) {
487                this.rHooks = extension;
488            }
489        }
490        
491    
492        /**
493         * Return a list which holds all hooks for the cmdHandler
494         * 
495         * @return list containing all hooks for the cmd handler
496         */
497        protected List<AuthHook> getHooks() {
498            return hooks;
499        }
500    
501        /**
502         * @see org.apache.james.protocols.smtp.hook.MailParametersHook#doMailParameter(org.apache.james.protocols.smtp.SMTPSession, java.lang.String, java.lang.String)
503         */
504        public HookResult doMailParameter(SMTPSession session, String paramName, String paramValue) {
505            // Ignore the AUTH command.
506            // TODO we should at least check for correct syntax and put the result in session
507            return new HookResult(HookReturnCode.DECLINED);
508        }
509    
510        /**
511         * @see org.apache.james.protocols.smtp.hook.MailParametersHook#getMailParamNames()
512         */
513        public String[] getMailParamNames() {
514            return new String[] { "AUTH" };
515        }
516    
517    }