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