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