2 package net.sourceforge.guacamole.net.basic;
5 * Guacamole - Clientless Remote Desktop
6 * Copyright (C) 2010 Michael Jumper
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.
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.
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/>.
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;
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;
38 public class BasicFileAuthenticationProvider implements BasicLogin.AuthenticationProvider {
40 private long mappingTime;
41 private Map<String, AuthInfo> mapping;
43 private File getUserMappingFile() throws GuacamoleException {
45 // Get user mapping filename
46 String filename = GuacamoleProperties.getProperty("basic-user-mapping");
50 return new File(filename);
54 public synchronized void init() throws GuacamoleException {
56 // Get user mapping file
57 File mapFile = getUserMappingFile();
59 throw new GuacamoleException("Missing \"basic-user-mapping\" parameter required for basic login.");
64 BasicUserMappingContentHandler contentHandler = new BasicUserMappingContentHandler();
66 XMLReader parser = XMLReaderFactory.createXMLReader();
67 parser.setContentHandler(contentHandler);
68 parser.parse(mapFile.getAbsolutePath());
70 mappingTime = mapFile.lastModified();
71 mapping = contentHandler.getUserMapping();
74 catch (IOException e) {
75 throw new GuacamoleException("Error reading basic user mapping file.", e);
77 catch (SAXException e) {
78 throw new GuacamoleException("Error parsing basic user mapping XML.", e);
84 public Configuration getAuthorizedConfiguration(String username, String password) throws GuacamoleException {
86 // Check mapping file mod time
87 File userMappingFile = getUserMappingFile();
88 if (userMappingFile.exists() && mappingTime < userMappingFile.lastModified()) {
90 // If modified recently, gain exclusive access and recheck
92 if (userMappingFile.exists() && mappingTime < userMappingFile.lastModified())
93 init(); // If still not up to date, re-init
98 AuthInfo info = mapping.get(username);
99 if (info != null && info.validate(username, password))
100 return info.getConfiguration();
106 public static class AuthInfo {
108 public static enum Encoding {
113 private String auth_username;
114 private String auth_password;
115 private Encoding auth_encoding;
117 private Configuration config;
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;
124 config = new Configuration();
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'
132 public static String getHexString(byte[] bytes) {
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) ]);
143 return hex.toString();
148 public boolean validate(String username, String password) {
150 // If username matches
151 if (username != null && password != null && username.equals(auth_username)) {
153 switch (auth_encoding) {
158 return password.equals(auth_password);
162 // Compare hashed password
164 MessageDigest digest = MessageDigest.getInstance("MD5");
165 String hashedPassword = getHexString(digest.digest(password.getBytes()));
166 return hashedPassword.equals(auth_password.toUpperCase());
168 catch (NoSuchAlgorithmException e) {
169 throw new UnsupportedOperationException("Unexpected lack of MD5 support.", e);
180 public Configuration getConfiguration() {
187 private static class BasicUserMappingContentHandler extends DefaultHandler {
189 private Map<String, AuthInfo> authMapping = new HashMap<String, AuthInfo>();
191 public Map<String, AuthInfo> getUserMapping() {
192 return Collections.unmodifiableMap(authMapping);
204 private State state = State.ROOT;
205 private AuthInfo current = null;
206 private String currentParameter = null;
209 public void endElement(String uri, String localName, String qName) throws SAXException {
215 if (localName.equals("user-mapping")) {
224 if (localName.equals("authorize")) {
226 // Finalize mapping for this user
228 current.auth_username,
232 state = State.USER_MAPPING;
240 if (localName.equals("protocol")) {
241 state = State.AUTH_INFO;
249 if (localName.equals("param")) {
250 state = State.AUTH_INFO;
258 throw new SAXException("Tag not yet complete: " + localName);
263 public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
267 // Document must be <user-mapping>
270 if (localName.equals("user-mapping")) {
271 state = State.USER_MAPPING;
277 // Only <authorize> tags allowed in main document
280 if (localName.equals("authorize")) {
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;
291 throw new SAXException("Invalid encoding type");
294 current = new AuthInfo(
295 attributes.getValue("username"),
296 attributes.getValue("password"),
301 state = State.AUTH_INFO;
309 if (localName.equals("protocol")) {
311 state = State.PROTOCOL;
315 if (localName.equals("param")) {
317 currentParameter = attributes.getValue("name");
318 if (currentParameter == null)
319 throw new SAXException("Attribute \"name\" required for param tag.");
322 state = State.PARAMETER;
330 throw new SAXException("Unexpected tag: " + localName);
335 public void characters(char[] ch, int start, int length) throws SAXException {
337 String str = new String(ch, start, length);
341 current.getConfiguration().setProtocol(str);
345 current.getConfiguration().setParameter(currentParameter, str);
350 if (str.trim().length() != 0)
351 throw new SAXException("Unexpected character data.");