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