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