0b5b6488fd69ac60dffdd21c138b71f6bdaaf878
[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.UsernamePassword;
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 exactly one configuration.
50  * This list is stored in an XML file which is reread if modified.
51  * 
52  * @author Michael Jumper
53  */
54 public class BasicFileAuthenticationProvider implements AuthenticationProvider<UsernamePassword> {
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(UsernamePassword 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         Map<String, GuacamoleConfiguration> configs = new HashMap<String, GuacamoleConfiguration>();
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             configs.put("DEFAULT", info.getConfiguration());
142
143         return configs;
144
145     }
146
147     public static class AuthInfo {
148
149         public static enum Encoding {
150             PLAIN_TEXT,
151             MD5
152         }
153
154         private String auth_username;
155         private String auth_password;
156         private Encoding auth_encoding;
157
158         private GuacamoleConfiguration config;
159
160         public AuthInfo(String auth_username, String auth_password, Encoding auth_encoding) {
161             this.auth_username = auth_username;
162             this.auth_password = auth_password;
163             this.auth_encoding = auth_encoding;
164
165             config = new GuacamoleConfiguration();
166         }
167
168         private static final char HEX_CHARS[] = {
169             '0', '1', '2', '3', '4', '5', '6', '7',
170             '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
171         };
172
173         public static String getHexString(byte[] bytes) {
174
175             if (bytes == null)
176                 return null;
177
178             StringBuilder hex = new StringBuilder(2 * bytes.length);
179             for (byte b : bytes) {
180                 hex.append(HEX_CHARS[(b & 0xF0) >> 4])
181                    .append(HEX_CHARS[(b & 0x0F)     ]);
182             }
183
184             return hex.toString();
185
186         }
187
188
189         public boolean validate(String username, String password) {
190
191             // If username matches
192             if (username != null && password != null && username.equals(auth_username)) {
193
194                 switch (auth_encoding) {
195
196                     case PLAIN_TEXT:
197
198                         // Compare plaintext
199                         return password.equals(auth_password);
200
201                     case MD5:
202
203                         // Compare hashed password
204                         try {
205                             MessageDigest digest = MessageDigest.getInstance("MD5");
206                             String hashedPassword = getHexString(digest.digest(password.getBytes()));
207                             return hashedPassword.equals(auth_password.toUpperCase());
208                         }
209                         catch (NoSuchAlgorithmException e) {
210                             throw new UnsupportedOperationException("Unexpected lack of MD5 support.", e);
211                         }
212
213                 }
214
215             }
216
217             return false;
218
219         }
220
221         public GuacamoleConfiguration getConfiguration() {
222             return config;
223         }
224
225     }
226
227
228     private static class BasicUserMappingContentHandler extends DefaultHandler {
229
230         private Map<String, AuthInfo> authMapping = new HashMap<String, AuthInfo>();
231
232         public Map<String, AuthInfo> getUserMapping() {
233             return Collections.unmodifiableMap(authMapping);
234         }
235
236         private enum State {
237             ROOT,
238             USER_MAPPING,
239             AUTH_INFO,
240             PROTOCOL,
241             PARAMETER,
242             END;
243         }
244
245         private State state = State.ROOT;
246         private AuthInfo current = null;
247         private String currentParameter = null;
248
249         @Override
250         public void endElement(String uri, String localName, String qName) throws SAXException {
251
252             switch (state)  {
253
254                 case USER_MAPPING:
255
256                     if (localName.equals("user-mapping")) {
257                         state = State.END;
258                         return;
259                     }
260
261                     break;
262
263                 case AUTH_INFO:
264
265                     if (localName.equals("authorize")) {
266
267                         // Finalize mapping for this user
268                         authMapping.put(
269                             current.auth_username,
270                             current
271                         );
272
273                         state = State.USER_MAPPING;
274                         return;
275                     }
276
277                     break;
278
279                 case PROTOCOL:
280
281                     if (localName.equals("protocol")) {
282                         state = State.AUTH_INFO;
283                         return;
284                     }
285
286                     break;
287
288                 case PARAMETER:
289
290                     if (localName.equals("param")) {
291                         state = State.AUTH_INFO;
292                         return;
293                     }
294
295                     break;
296
297             }
298
299             throw new SAXException("Tag not yet complete: " + localName);
300
301         }
302
303         @Override
304         public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
305
306             switch (state)  {
307
308                 // Document must be <user-mapping>
309                 case ROOT:
310
311                     if (localName.equals("user-mapping")) {
312                         state = State.USER_MAPPING;
313                         return;
314                     }
315
316                     break;
317
318                 // Only <authorize> tags allowed in main document
319                 case USER_MAPPING:
320
321                     if (localName.equals("authorize")) {
322
323                         AuthInfo.Encoding encoding;
324                         String encodingString = attributes.getValue("encoding");
325                         if (encodingString == null)
326                             encoding = AuthInfo.Encoding.PLAIN_TEXT;
327                         else if (encodingString.equals("plain"))
328                             encoding = AuthInfo.Encoding.PLAIN_TEXT;
329                         else if (encodingString.equals("md5"))
330                             encoding = AuthInfo.Encoding.MD5;
331                         else
332                             throw new SAXException("Invalid encoding type");
333
334
335                         current = new AuthInfo(
336                             attributes.getValue("username"),
337                             attributes.getValue("password"),
338                             encoding
339                         );
340
341                         // Next state
342                         state = State.AUTH_INFO;
343                         return;
344                     }
345
346                     break;
347
348                 case AUTH_INFO:
349
350                     if (localName.equals("protocol")) {
351                         // Next state
352                         state = State.PROTOCOL;
353                         return;
354                     }
355
356                     if (localName.equals("param")) {
357
358                         currentParameter = attributes.getValue("name");
359                         if (currentParameter == null)
360                             throw new SAXException("Attribute \"name\" required for param tag.");
361
362                         // Next state
363                         state = State.PARAMETER;
364                         return;
365                     }
366
367                     break;
368
369             }
370
371             throw new SAXException("Unexpected tag: " + localName);
372
373         }
374
375         @Override
376         public void characters(char[] ch, int start, int length) throws SAXException {
377
378             String str = new String(ch, start, length);
379             switch (state) {
380
381                 case PROTOCOL:
382                     current.getConfiguration()
383                             .setProtocol(str);
384                     return;
385
386                 case PARAMETER:
387                     current.getConfiguration()
388                             .setParameter(currentParameter, str);
389                     return;
390                 
391             }
392
393             if (str.trim().length() != 0)
394                 throw new SAXException("Unexpected character data.");
395
396         }
397
398
399     }
400
401
402 }