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