Converted to generic credentials.
[guacamole.git] / src / main / java / net / sourceforge / guacamole / net / basic / BasicFileAuthenticationProvider.java
1
2 package net.sourceforge.guacamole.net.basic;
3
4 /*
5  *  Guacamole - Clientless Remote Desktop
6  *  Copyright (C) 2010  Michael Jumper
7  *
8  *  This program is free software: you can redistribute it and/or modify
9  *  it under the terms of the GNU Affero General Public License as published by
10  *  the Free Software Foundation, either version 3 of the License, or
11  *  (at your option) any later version.
12  *
13  *  This program is distributed in the hope that it will be useful,
14  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
15  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  *  GNU Affero General Public License for more details.
17  *
18  *  You should have received a copy of the GNU Affero General Public License
19  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
20  */
21
22 import net.sourceforge.guacamole.net.auth.AuthenticationProvider;
23 import java.io.File;
24 import java.io.IOException;
25 import java.security.MessageDigest;
26 import java.security.NoSuchAlgorithmException;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.Map;
30 import net.sourceforge.guacamole.GuacamoleException;
31 import net.sourceforge.guacamole.net.auth.UserConfiguration;
32 import net.sourceforge.guacamole.net.auth.UsernamePassword;
33 import net.sourceforge.guacamole.net.basic.properties.BasicGuacamoleProperties;
34 import net.sourceforge.guacamole.properties.GuacamoleProperties;
35 import net.sourceforge.guacamole.protocol.GuacamoleConfiguration;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38 import org.xml.sax.Attributes;
39 import org.xml.sax.SAXException;
40 import org.xml.sax.XMLReader;
41 import org.xml.sax.helpers.DefaultHandler;
42 import org.xml.sax.helpers.XMLReaderFactory;
43
44 public class BasicFileAuthenticationProvider implements AuthenticationProvider<UsernamePassword> {
45
46     private Logger logger = LoggerFactory.getLogger(BasicFileAuthenticationProvider.class);
47     
48     private long mappingTime;
49     private Map<String, AuthInfo> mapping;
50
51     private File getUserMappingFile() throws GuacamoleException {
52
53         // Get user mapping file
54         return GuacamoleProperties.getProperty(BasicGuacamoleProperties.BASIC_USER_MAPPING);
55
56     }
57
58     public synchronized void init() throws GuacamoleException {
59
60         // Get user mapping file
61         File mapFile = getUserMappingFile();
62         if (mapFile == null)
63             throw new GuacamoleException("Missing \"basic-user-mapping\" parameter required for basic login.");
64
65         logger.info("Reading user mapping file: {}", mapFile);
66         
67         // Parse document
68         try {
69
70             BasicUserMappingContentHandler contentHandler = new BasicUserMappingContentHandler();
71
72             XMLReader parser = XMLReaderFactory.createXMLReader();
73             parser.setContentHandler(contentHandler);
74             parser.parse(mapFile.getAbsolutePath());
75
76             mappingTime = mapFile.lastModified();
77             mapping = contentHandler.getUserMapping();
78
79         }
80         catch (IOException e) {
81             throw new GuacamoleException("Error reading basic user mapping file.", e);
82         }
83         catch (SAXException e) {
84             throw new GuacamoleException("Error parsing basic user mapping XML.", e);
85         }
86
87     }
88
89     @Override
90     public UserConfiguration getUserConfiguration(UsernamePassword credentials) throws GuacamoleException {
91
92         // Check mapping file mod time
93         File userMappingFile = getUserMappingFile();
94         if (userMappingFile.exists() && mappingTime < userMappingFile.lastModified()) {
95
96             // If modified recently, gain exclusive access and recheck
97             synchronized (this) {
98                 if (userMappingFile.exists() && mappingTime < userMappingFile.lastModified()) {
99                     logger.info("User mapping file {} has been modified.", userMappingFile);
100                     init(); // If still not up to date, re-init
101                 }
102             }
103
104         }
105
106         // If no mapping available, report as such
107         if (mapping == null)
108             throw new GuacamoleException("User mapping could not be read.");
109         
110         // Validate and return info for given user and pass
111         AuthInfo info = mapping.get(credentials.getUsername());
112         if (info != null && info.validate(credentials.getUsername(), credentials.getPassword()))
113             return info.getUserConfiguration();
114
115         return null;
116
117     }
118
119     public static class AuthInfo {
120
121         protected static final String CONFIG_ID = "DEFAULT";
122
123         public static enum Encoding {
124             PLAIN_TEXT,
125             MD5
126         }
127
128         private String auth_username;
129         private String auth_password;
130         private Encoding auth_encoding;
131
132         private BasicUserConfiguration userConfig;
133
134         public AuthInfo(String auth_username, String auth_password, Encoding auth_encoding) {
135             this.auth_username = auth_username;
136             this.auth_password = auth_password;
137             this.auth_encoding = auth_encoding;
138
139             userConfig = new BasicUserConfiguration();
140             userConfig.setConfiguration(CONFIG_ID, new GuacamoleConfiguration());
141
142         }
143
144         private static final char HEX_CHARS[] = {
145             '0', '1', '2', '3', '4', '5', '6', '7',
146             '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
147         };
148
149         public static String getHexString(byte[] bytes) {
150
151             if (bytes == null)
152                 return null;
153
154             StringBuilder hex = new StringBuilder(2 * bytes.length);
155             for (byte b : bytes) {
156                 hex.append(HEX_CHARS[(b & 0xF0) >> 4])
157                    .append(HEX_CHARS[(b & 0x0F)     ]);
158             }
159
160             return hex.toString();
161
162         }
163
164
165         public boolean validate(String username, String password) {
166
167             // If username matches
168             if (username != null && password != null && username.equals(auth_username)) {
169
170                 switch (auth_encoding) {
171
172                     case PLAIN_TEXT:
173
174                         // Compare plaintext
175                         return password.equals(auth_password);
176
177                     case MD5:
178
179                         // Compare hashed password
180                         try {
181                             MessageDigest digest = MessageDigest.getInstance("MD5");
182                             String hashedPassword = getHexString(digest.digest(password.getBytes()));
183                             return hashedPassword.equals(auth_password.toUpperCase());
184                         }
185                         catch (NoSuchAlgorithmException e) {
186                             throw new UnsupportedOperationException("Unexpected lack of MD5 support.", e);
187                         }
188
189                 }
190
191             }
192
193             return false;
194
195         }
196
197         public BasicUserConfiguration getUserConfiguration() {
198             return userConfig;
199         }
200
201     }
202
203
204     private static class BasicUserMappingContentHandler extends DefaultHandler {
205
206         private Map<String, AuthInfo> authMapping = new HashMap<String, AuthInfo>();
207
208         public Map<String, AuthInfo> getUserMapping() {
209             return Collections.unmodifiableMap(authMapping);
210         }
211
212         private enum State {
213             ROOT,
214             USER_MAPPING,
215             AUTH_INFO,
216             PROTOCOL,
217             PARAMETER,
218             END;
219         }
220
221         private State state = State.ROOT;
222         private AuthInfo current = null;
223         private String currentParameter = null;
224
225         @Override
226         public void endElement(String uri, String localName, String qName) throws SAXException {
227
228             switch (state)  {
229
230                 case USER_MAPPING:
231
232                     if (localName.equals("user-mapping")) {
233                         state = State.END;
234                         return;
235                     }
236
237                     break;
238
239                 case AUTH_INFO:
240
241                     if (localName.equals("authorize")) {
242
243                         // Finalize mapping for this user
244                         authMapping.put(
245                             current.auth_username,
246                             current
247                         );
248
249                         state = State.USER_MAPPING;
250                         return;
251                     }
252
253                     break;
254
255                 case PROTOCOL:
256
257                     if (localName.equals("protocol")) {
258                         state = State.AUTH_INFO;
259                         return;
260                     }
261
262                     break;
263
264                 case PARAMETER:
265
266                     if (localName.equals("param")) {
267                         state = State.AUTH_INFO;
268                         return;
269                     }
270
271                     break;
272
273             }
274
275             throw new SAXException("Tag not yet complete: " + localName);
276
277         }
278
279         @Override
280         public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
281
282             switch (state)  {
283
284                 // Document must be <user-mapping>
285                 case ROOT:
286
287                     if (localName.equals("user-mapping")) {
288                         state = State.USER_MAPPING;
289                         return;
290                     }
291
292                     break;
293
294                 // Only <authorize> tags allowed in main document
295                 case USER_MAPPING:
296
297                     if (localName.equals("authorize")) {
298
299                         AuthInfo.Encoding encoding;
300                         String encodingString = attributes.getValue("encoding");
301                         if (encodingString == null)
302                             encoding = AuthInfo.Encoding.PLAIN_TEXT;
303                         else if (encodingString.equals("plain"))
304                             encoding = AuthInfo.Encoding.PLAIN_TEXT;
305                         else if (encodingString.equals("md5"))
306                             encoding = AuthInfo.Encoding.MD5;
307                         else
308                             throw new SAXException("Invalid encoding type");
309
310
311                         current = new AuthInfo(
312                             attributes.getValue("username"),
313                             attributes.getValue("password"),
314                             encoding
315                         );
316
317                         // Next state
318                         state = State.AUTH_INFO;
319                         return;
320                     }
321
322                     break;
323
324                 case AUTH_INFO:
325
326                     if (localName.equals("protocol")) {
327                         // Next state
328                         state = State.PROTOCOL;
329                         return;
330                     }
331
332                     if (localName.equals("param")) {
333
334                         currentParameter = attributes.getValue("name");
335                         if (currentParameter == null)
336                             throw new SAXException("Attribute \"name\" required for param tag.");
337
338                         // Next state
339                         state = State.PARAMETER;
340                         return;
341                     }
342
343                     break;
344
345             }
346
347             throw new SAXException("Unexpected tag: " + localName);
348
349         }
350
351         @Override
352         public void characters(char[] ch, int start, int length) throws SAXException {
353
354             String str = new String(ch, start, length);
355             switch (state) {
356
357                 case PROTOCOL:
358                     current.getUserConfiguration().getConfiguration(AuthInfo.CONFIG_ID)
359                             .setProtocol(str);
360                     return;
361
362                 case PARAMETER:
363                     current.getUserConfiguration().getConfiguration(AuthInfo.CONFIG_ID)
364                             .setParameter(currentParameter, str);
365                     return;
366                 
367             }
368
369             if (str.trim().length() != 0)
370                 throw new SAXException("Unexpected character data.");
371
372         }
373
374
375     }
376
377
378 }