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/>.
22 import net.sourceforge.guacamole.net.auth.AuthenticationProvider;
24 import java.io.IOException;
25 import java.security.MessageDigest;
26 import java.security.NoSuchAlgorithmException;
27 import java.util.Collections;
28 import java.util.HashMap;
30 import net.sourceforge.guacamole.GuacamoleException;
31 import net.sourceforge.guacamole.net.auth.UserConfiguration;
32 import net.sourceforge.guacamole.net.auth.UsernamePassword;
33 import net.sourceforge.guacamole.net.basic.properties.BasicGuacamoleProperties;
34 import net.sourceforge.guacamole.properties.GuacamoleProperties;
35 import net.sourceforge.guacamole.protocol.GuacamoleConfiguration;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38 import org.xml.sax.Attributes;
39 import org.xml.sax.SAXException;
40 import org.xml.sax.XMLReader;
41 import org.xml.sax.helpers.DefaultHandler;
42 import org.xml.sax.helpers.XMLReaderFactory;
44 public class BasicFileAuthenticationProvider implements AuthenticationProvider<UsernamePassword> {
46 private Logger logger = LoggerFactory.getLogger(BasicFileAuthenticationProvider.class);
48 private long mappingTime;
49 private Map<String, AuthInfo> mapping;
51 private File getUserMappingFile() throws GuacamoleException {
53 // Get user mapping file
54 return GuacamoleProperties.getProperty(BasicGuacamoleProperties.BASIC_USER_MAPPING);
58 public synchronized void init() throws GuacamoleException {
60 // Get user mapping file
61 File mapFile = getUserMappingFile();
63 throw new GuacamoleException("Missing \"basic-user-mapping\" parameter required for basic login.");
65 logger.info("Reading user mapping file: {}", mapFile);
70 BasicUserMappingContentHandler contentHandler = new BasicUserMappingContentHandler();
72 XMLReader parser = XMLReaderFactory.createXMLReader();
73 parser.setContentHandler(contentHandler);
74 parser.parse(mapFile.getAbsolutePath());
76 mappingTime = mapFile.lastModified();
77 mapping = contentHandler.getUserMapping();
80 catch (IOException e) {
81 throw new GuacamoleException("Error reading basic user mapping file.", e);
83 catch (SAXException e) {
84 throw new GuacamoleException("Error parsing basic user mapping XML.", e);
90 public UserConfiguration getUserConfiguration(UsernamePassword credentials) throws GuacamoleException {
92 // Check mapping file mod time
93 File userMappingFile = getUserMappingFile();
94 if (userMappingFile.exists() && mappingTime < userMappingFile.lastModified()) {
96 // If modified recently, gain exclusive access and recheck
98 if (userMappingFile.exists() && mappingTime < userMappingFile.lastModified()) {
99 logger.info("User mapping file {} has been modified.", userMappingFile);
100 init(); // If still not up to date, re-init
106 // If no mapping available, report as such
108 throw new GuacamoleException("User mapping could not be read.");
110 // Validate and return info for given user and pass
111 AuthInfo info = mapping.get(credentials.getUsername());
112 if (info != null && info.validate(credentials.getUsername(), credentials.getPassword()))
113 return info.getUserConfiguration();
119 public static class AuthInfo {
121 protected static final String CONFIG_ID = "DEFAULT";
123 public static enum Encoding {
128 private String auth_username;
129 private String auth_password;
130 private Encoding auth_encoding;
132 private BasicUserConfiguration userConfig;
134 public AuthInfo(String auth_username, String auth_password, Encoding auth_encoding) {
135 this.auth_username = auth_username;
136 this.auth_password = auth_password;
137 this.auth_encoding = auth_encoding;
139 userConfig = new BasicUserConfiguration();
140 userConfig.setConfiguration(CONFIG_ID, new GuacamoleConfiguration());
144 private static final char HEX_CHARS[] = {
145 '0', '1', '2', '3', '4', '5', '6', '7',
146 '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
149 public static String getHexString(byte[] bytes) {
154 StringBuilder hex = new StringBuilder(2 * bytes.length);
155 for (byte b : bytes) {
156 hex.append(HEX_CHARS[(b & 0xF0) >> 4])
157 .append(HEX_CHARS[(b & 0x0F) ]);
160 return hex.toString();
165 public boolean validate(String username, String password) {
167 // If username matches
168 if (username != null && password != null && username.equals(auth_username)) {
170 switch (auth_encoding) {
175 return password.equals(auth_password);
179 // Compare hashed password
181 MessageDigest digest = MessageDigest.getInstance("MD5");
182 String hashedPassword = getHexString(digest.digest(password.getBytes()));
183 return hashedPassword.equals(auth_password.toUpperCase());
185 catch (NoSuchAlgorithmException e) {
186 throw new UnsupportedOperationException("Unexpected lack of MD5 support.", e);
197 public BasicUserConfiguration getUserConfiguration() {
204 private static class BasicUserMappingContentHandler extends DefaultHandler {
206 private Map<String, AuthInfo> authMapping = new HashMap<String, AuthInfo>();
208 public Map<String, AuthInfo> getUserMapping() {
209 return Collections.unmodifiableMap(authMapping);
221 private State state = State.ROOT;
222 private AuthInfo current = null;
223 private String currentParameter = null;
226 public void endElement(String uri, String localName, String qName) throws SAXException {
232 if (localName.equals("user-mapping")) {
241 if (localName.equals("authorize")) {
243 // Finalize mapping for this user
245 current.auth_username,
249 state = State.USER_MAPPING;
257 if (localName.equals("protocol")) {
258 state = State.AUTH_INFO;
266 if (localName.equals("param")) {
267 state = State.AUTH_INFO;
275 throw new SAXException("Tag not yet complete: " + localName);
280 public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
284 // Document must be <user-mapping>
287 if (localName.equals("user-mapping")) {
288 state = State.USER_MAPPING;
294 // Only <authorize> tags allowed in main document
297 if (localName.equals("authorize")) {
299 AuthInfo.Encoding encoding;
300 String encodingString = attributes.getValue("encoding");
301 if (encodingString == null)
302 encoding = AuthInfo.Encoding.PLAIN_TEXT;
303 else if (encodingString.equals("plain"))
304 encoding = AuthInfo.Encoding.PLAIN_TEXT;
305 else if (encodingString.equals("md5"))
306 encoding = AuthInfo.Encoding.MD5;
308 throw new SAXException("Invalid encoding type");
311 current = new AuthInfo(
312 attributes.getValue("username"),
313 attributes.getValue("password"),
318 state = State.AUTH_INFO;
326 if (localName.equals("protocol")) {
328 state = State.PROTOCOL;
332 if (localName.equals("param")) {
334 currentParameter = attributes.getValue("name");
335 if (currentParameter == null)
336 throw new SAXException("Attribute \"name\" required for param tag.");
339 state = State.PARAMETER;
347 throw new SAXException("Unexpected tag: " + localName);
352 public void characters(char[] ch, int start, int length) throws SAXException {
354 String str = new String(ch, start, length);
358 current.getUserConfiguration().getConfiguration(AuthInfo.CONFIG_ID)
363 current.getUserConfiguration().getConfiguration(AuthInfo.CONFIG_ID)
364 .setParameter(currentParameter, str);
369 if (str.trim().length() != 0)
370 throw new SAXException("Unexpected character data.");