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 }