1 /****************************************************************
2 * Licensed to the Apache Software Foundation (ASF) under one *
3 * or more contributor license agreements. See the NOTICE file *
4 * distributed with this work for additional information *
5 * regarding copyright ownership. The ASF licenses this file *
6 * to you under the Apache License, Version 2.0 (the *
7 * "License"); you may not use this file except in compliance *
8 * with the License. You may obtain a copy of the License at *
9 * *
10 * http://www.apache.org/licenses/LICENSE-2.0 *
11 * *
12 * Unless required by applicable law or agreed to in writing, *
13 * software distributed under the License is distributed on an *
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
15 * KIND, either express or implied. See the License for the *
16 * specific language governing permissions and limitations *
17 * under the License. *
18 ****************************************************************/
19
20
21
22 package org.apache.james.smtpserver.core;
23
24 import org.apache.james.dsn.DSNStatus;
25 import org.apache.james.smtpserver.CommandHandler;
26 import org.apache.james.smtpserver.SMTPSession;
27 import org.apache.james.util.codec.Base64;
28 import org.apache.avalon.framework.logger.AbstractLogEnabled;
29
30 import java.util.ArrayList;
31 import java.util.Collection;
32 import java.util.Locale;
33 import java.util.StringTokenizer;
34 import java.io.IOException;
35
36
37 /**
38 * handles AUTH command
39 */
40 public class AuthCmdHandler
41 extends AbstractLogEnabled
42 implements CommandHandler {
43
44 /**
45 * The text string for the SMTP AUTH type PLAIN.
46 */
47 private final static String AUTH_TYPE_PLAIN = "PLAIN";
48
49 /**
50 * The text string for the SMTP AUTH type LOGIN.
51 */
52 private final static String AUTH_TYPE_LOGIN = "LOGIN";
53
54 /**
55 * handles AUTH command
56 *
57 * @see org.apache.james.smtpserver.CommandHandler#onCommand(SMTPSession)
58 */
59 public void onCommand(SMTPSession session) {
60 //deviation from the Main code
61 //Instead of throwing exception just end the session
62 try{
63 doAUTH(session, session.getCommandArgument());
64 } catch (Exception ex) {
65 getLogger().error("Exception occured:" + ex.getMessage());
66 session.endSession();
67 }
68 }
69
70
71
72 /**
73 * Handler method called upon receipt of a AUTH command.
74 * Handles client authentication to the SMTP server.
75 *
76 * @param session SMTP session
77 * @param argument the argument passed in with the command by the SMTP client
78 */
79 private void doAUTH(SMTPSession session, String argument)
80 throws Exception {
81 String responseString = null;
82 if (session.getUser() != null) {
83 responseString = "503 "+DSNStatus.getStatus(DSNStatus.PERMANENT,DSNStatus.DELIVERY_OTHER)+" User has previously authenticated. "
84 + " Further authentication is not required!";
85 session.writeResponse(responseString);
86 } else if (argument == null) {
87 responseString = "501 "+DSNStatus.getStatus(DSNStatus.PERMANENT,DSNStatus.DELIVERY_INVALID_ARG)+" Usage: AUTH (authentication type) <challenge>";
88 session.writeResponse(responseString);
89 } else {
90 String initialResponse = null;
91 if ((argument != null) && (argument.indexOf(" ") > 0)) {
92 initialResponse = argument.substring(argument.indexOf(" ") + 1);
93 argument = argument.substring(0,argument.indexOf(" "));
94 }
95 String authType = argument.toUpperCase(Locale.US);
96 if (authType.equals(AUTH_TYPE_PLAIN)) {
97 doPlainAuth(session, initialResponse);
98 return;
99 } else if (authType.equals(AUTH_TYPE_LOGIN)) {
100 doLoginAuth(session, initialResponse);
101 return;
102 } else {
103 doUnknownAuth(session, authType, initialResponse);
104 return;
105 }
106 }
107 }
108
109 /**
110 * Carries out the Plain AUTH SASL exchange.
111 *
112 * According to RFC 2595 the client must send: [authorize-id] \0 authenticate-id \0 password.
113 *
114 * >>> AUTH PLAIN dGVzdAB0ZXN0QHdpei5leGFtcGxlLmNvbQB0RXN0NDI=
115 * Decoded: test\000test@wiz.example.com\000tEst42
116 *
117 * >>> AUTH PLAIN dGVzdAB0ZXN0AHRFc3Q0Mg==
118 * Decoded: test\000test\000tEst42
119 *
120 * @param session SMTP session object
121 * @param initialResponse the initial response line passed in with the AUTH command
122 */
123 private void doPlainAuth(SMTPSession session, String initialResponse)
124 throws IOException {
125 String userpass = null, user = null, pass = null, responseString = null;
126 if (initialResponse == null) {
127 responseString = "334 OK. Continue authentication";
128 session.writeResponse(responseString);
129 userpass = session.readCommandLine();
130 } else {
131 userpass = initialResponse.trim();
132 }
133 try {
134 if (userpass != null) {
135 userpass = Base64.decodeAsString(userpass);
136 }
137 if (userpass != null) {
138 /* See: RFC 2595, Section 6
139 The mechanism consists of a single message from the client to the
140 server. The client sends the authorization identity (identity to
141 login as), followed by a US-ASCII NUL character, followed by the
142 authentication identity (identity whose password will be used),
143 followed by a US-ASCII NUL character, followed by the clear-text
144 password. The client may leave the authorization identity empty to
145 indicate that it is the same as the authentication identity.
146
147 The server will verify the authentication identity and password with
148 the system authentication database and verify that the authentication
149 credentials permit the client to login as the authorization identity.
150 If both steps succeed, the user is logged in.
151 */
152 StringTokenizer authTokenizer = new StringTokenizer(userpass, "\0");
153 String authorize_id = authTokenizer.nextToken(); // Authorization Identity
154 user = authTokenizer.nextToken(); // Authentication Identity
155 try {
156 pass = authTokenizer.nextToken(); // Password
157 }
158 catch (java.util.NoSuchElementException _) {
159 // If we got here, this is what happened. RFC 2595
160 // says that "the client may leave the authorization
161 // identity empty to indicate that it is the same as
162 // the authentication identity." As noted above,
163 // that would be represented as a decoded string of
164 // the form: "\0authenticate-id\0password". The
165 // first call to nextToken will skip the empty
166 // authorize-id, and give us the authenticate-id,
167 // which we would store as the authorize-id. The
168 // second call will give us the password, which we
169 // think is the authenticate-id (user). Then when
170 // we ask for the password, there are no more
171 // elements, leading to the exception we just
172 // caught. So we need to move the user to the
173 // password, and the authorize_id to the user.
174 pass = user;
175 user = authorize_id;
176 }
177
178 authTokenizer = null;
179 }
180 }
181 catch (Exception e) {
182 // Ignored - this exception in parsing will be dealt
183 // with in the if clause below
184 }
185 // Authenticate user
186 if ((user == null) || (pass == null)) {
187 responseString = "501 Could not decode parameters for AUTH PLAIN";
188 session.writeResponse(responseString);
189 } else if (session.getConfigurationData().getUsersRepository().test(user, pass)) {
190 session.setUser(user);
191 responseString = "235 Authentication Successful";
192 session.writeResponse(responseString);
193 getLogger().info("AUTH method PLAIN succeeded");
194 } else {
195 responseString = "535 Authentication Failed";
196 session.writeResponse(responseString);
197 getLogger().error("AUTH method PLAIN failed");
198 }
199 return;
200 }
201
202 /**
203 * Carries out the Login AUTH SASL exchange.
204 *
205 * @param session SMTP session object
206 * @param initialResponse the initial response line passed in with the AUTH command
207 */
208 private void doLoginAuth(SMTPSession session, String initialResponse)
209 throws IOException {
210 String user = null, pass = null, responseString = null;
211 if (initialResponse == null) {
212 responseString = "334 VXNlcm5hbWU6"; // base64 encoded "Username:"
213 session.writeResponse(responseString);
214 user = session.readCommandLine();
215 } else {
216 user = initialResponse.trim();
217 }
218 if (user != null) {
219 try {
220 user = Base64.decodeAsString(user);
221 } catch (Exception e) {
222 // Ignored - this parse error will be
223 // addressed in the if clause below
224 user = null;
225 }
226 }
227 responseString = "334 UGFzc3dvcmQ6"; // base64 encoded "Password:"
228 session.writeResponse(responseString);
229 pass = session.readCommandLine();
230 if (pass != null) {
231 try {
232 pass = Base64.decodeAsString(pass);
233 } catch (Exception e) {
234 // Ignored - this parse error will be
235 // addressed in the if clause below
236 pass = null;
237 }
238 }
239 // Authenticate user
240 if ((user == null) || (pass == null)) {
241 responseString = "501 Could not decode parameters for AUTH LOGIN";
242 } else if (session.getConfigurationData().getUsersRepository().test(user, pass)) {
243 session.setUser(user);
244 responseString = "235 Authentication Successful";
245 if (getLogger().isDebugEnabled()) {
246 // TODO: Make this string a more useful debug message
247 getLogger().debug("AUTH method LOGIN succeeded");
248 }
249 } else {
250 responseString = "535 Authentication Failed";
251 // TODO: Make this string a more useful error message
252 getLogger().error("AUTH method LOGIN failed");
253 }
254 session.writeResponse(responseString);
255 return;
256 }
257
258 /**
259 * Handles the case of an unrecognized auth type.
260 *
261 * @param session SMTP session object
262 * @param authType the unknown auth type
263 * @param initialResponse the initial response line passed in with the AUTH command
264 */
265 private void doUnknownAuth(SMTPSession session, String authType, String initialResponse) {
266 String responseString = "504 Unrecognized Authentication Type";
267 session.writeResponse(responseString);
268 if (getLogger().isErrorEnabled()) {
269 StringBuffer errorBuffer =
270 new StringBuffer(128)
271 .append("AUTH method ")
272 .append(authType)
273 .append(" is an unrecognized authentication type");
274 getLogger().error(errorBuffer.toString());
275 }
276 return;
277 }
278
279
280
281 /**
282 * @see org.apache.james.smtpserver.CommandHandler#getImplCommands()
283 */
284 public Collection getImplCommands() {
285 Collection implCommands = new ArrayList();
286 implCommands.add("AUTH");
287
288 return implCommands;
289 }
290 }