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