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