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 java.io.BufferedReader;
23 import net.sourceforge.guacamole.net.auth.AuthenticationProvider;
25 import java.io.FileReader;
26 import java.io.IOException;
27 import java.io.Reader;
28 import java.security.MessageDigest;
29 import java.security.NoSuchAlgorithmException;
30 import java.util.Collections;
31 import java.util.HashMap;
33 import net.sourceforge.guacamole.GuacamoleException;
34 import net.sourceforge.guacamole.net.auth.Credentials;
35 import net.sourceforge.guacamole.properties.FileGuacamoleProperty;
36 import net.sourceforge.guacamole.properties.GuacamoleProperties;
37 import net.sourceforge.guacamole.protocol.GuacamoleConfiguration;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40 import org.xml.sax.Attributes;
41 import org.xml.sax.InputSource;
42 import org.xml.sax.SAXException;
43 import org.xml.sax.XMLReader;
44 import org.xml.sax.helpers.DefaultHandler;
45 import org.xml.sax.helpers.XMLReaderFactory;
48 * Authenticates users against a static list of username/password pairs.
49 * Each username/password may be associated with multiple configurations.
50 * This list is stored in an XML file which is reread if modified.
52 * @author Michael Jumper, Michal Kotas
54 public class BasicFileAuthenticationProvider implements AuthenticationProvider {
56 private Logger logger = LoggerFactory.getLogger(BasicFileAuthenticationProvider.class);
58 private long mappingTime;
59 private Map<String, AuthInfo> mapping;
62 * The filename of the XML file to read the user mapping from.
64 public static final FileGuacamoleProperty BASIC_USER_MAPPING = new FileGuacamoleProperty() {
67 public String getName() { return "basic-user-mapping"; }
71 private File getUserMappingFile() throws GuacamoleException {
73 // Get user mapping file
74 return GuacamoleProperties.getProperty(BASIC_USER_MAPPING);
78 public synchronized void init() throws GuacamoleException {
80 // Get user mapping file
81 File mapFile = getUserMappingFile();
83 throw new GuacamoleException("Missing \"basic-user-mapping\" parameter required for basic login.");
85 logger.info("Reading user mapping file: {}", mapFile);
91 BasicUserMappingContentHandler contentHandler = new BasicUserMappingContentHandler();
93 XMLReader parser = XMLReaderFactory.createXMLReader();
94 parser.setContentHandler(contentHandler);
96 // Read and parse file
97 Reader reader = new BufferedReader(new FileReader(mapFile));
98 parser.parse(new InputSource(reader));
101 // Init mapping and record mod time of file
102 mappingTime = mapFile.lastModified();
103 mapping = contentHandler.getUserMapping();
106 catch (IOException e) {
107 throw new GuacamoleException("Error reading basic user mapping file.", e);
109 catch (SAXException e) {
110 throw new GuacamoleException("Error parsing basic user mapping XML.", e);
116 public Map<String, GuacamoleConfiguration> getAuthorizedConfigurations(Credentials credentials) throws GuacamoleException {
118 // Check mapping file mod time
119 File userMappingFile = getUserMappingFile();
120 if (userMappingFile.exists() && mappingTime < userMappingFile.lastModified()) {
122 // If modified recently, gain exclusive access and recheck
123 synchronized (this) {
124 if (userMappingFile.exists() && mappingTime < userMappingFile.lastModified()) {
125 logger.info("User mapping file {} has been modified.", userMappingFile);
126 init(); // If still not up to date, re-init
132 // If no mapping available, report as such
134 throw new GuacamoleException("User mapping could not be read.");
136 // Validate and return info for given user and pass
137 AuthInfo info = mapping.get(credentials.getUsername());
138 if (info != null && info.validate(credentials.getUsername(), credentials.getPassword()))
139 return info.getConfigurations();
146 public static class AuthInfo {
148 public static enum Encoding {
153 private String auth_username;
154 private String auth_password;
155 private Encoding auth_encoding;
157 private Map<String, GuacamoleConfiguration> configs;
159 public AuthInfo(String auth_username, String auth_password, Encoding auth_encoding) {
160 this.auth_username = auth_username;
161 this.auth_password = auth_password;
162 this.auth_encoding = auth_encoding;
164 configs = new HashMap<String, GuacamoleConfiguration>();
167 private static final char HEX_CHARS[] = {
168 '0', '1', '2', '3', '4', '5', '6', '7',
169 '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
172 public static String getHexString(byte[] bytes) {
177 StringBuilder hex = new StringBuilder(2 * bytes.length);
178 for (byte b : bytes) {
179 hex.append(HEX_CHARS[(b & 0xF0) >> 4])
180 .append(HEX_CHARS[(b & 0x0F) ]);
183 return hex.toString();
188 public boolean validate(String username, String password) {
190 // If username matches
191 if (username != null && password != null && username.equals(auth_username)) {
193 switch (auth_encoding) {
198 return password.equals(auth_password);
202 // Compare hashed password
204 MessageDigest digest = MessageDigest.getInstance("MD5");
205 String hashedPassword = getHexString(digest.digest(password.getBytes()));
206 return hashedPassword.equals(auth_password.toUpperCase());
208 catch (NoSuchAlgorithmException e) {
209 throw new UnsupportedOperationException("Unexpected lack of MD5 support.", e);
220 public GuacamoleConfiguration getConfiguration(String name) {
222 // Create new configuration if not already in map
223 GuacamoleConfiguration config = configs.get(name);
224 if (config == null) {
225 config = new GuacamoleConfiguration();
226 configs.put(name, config);
233 public Map<String, GuacamoleConfiguration> getConfigurations() {
239 private static class BasicUserMappingContentHandler extends DefaultHandler {
241 private Map<String, AuthInfo> authMapping = new HashMap<String, AuthInfo>();
243 public Map<String, AuthInfo> getUserMapping() {
244 return Collections.unmodifiableMap(authMapping);
251 /* Username/password pair */
254 /* Connection configuration information */
259 /* Configuration information associated with default connection */
260 DEFAULT_CONNECTION_PROTOCOL,
261 DEFAULT_CONNECTION_PARAMETER,
266 private State state = State.ROOT;
267 private AuthInfo current = null;
268 private String currentParameter = null;
269 private String currentConnection = null;
272 public void endElement(String uri, String localName, String qName) throws SAXException {
278 if (localName.equals("user-mapping")) {
287 if (localName.equals("authorize")) {
289 // Finalize mapping for this user
291 current.auth_username,
295 state = State.USER_MAPPING;
303 if (localName.equals("connection")) {
304 state = State.AUTH_INFO;
312 if (localName.equals("protocol")) {
313 state = State.CONNECTION;
321 if (localName.equals("param")) {
322 state = State.CONNECTION;
328 case DEFAULT_CONNECTION_PROTOCOL:
330 if (localName.equals("protocol")) {
331 state = State.AUTH_INFO;
337 case DEFAULT_CONNECTION_PARAMETER:
339 if (localName.equals("param")) {
340 state = State.AUTH_INFO;
348 throw new SAXException("Tag not yet complete: " + localName);
353 public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
357 // Document must be <user-mapping>
360 if (localName.equals("user-mapping")) {
361 state = State.USER_MAPPING;
369 if (localName.equals("authorize")) {
371 AuthInfo.Encoding encoding;
372 String encodingString = attributes.getValue("encoding");
373 if (encodingString == null)
374 encoding = AuthInfo.Encoding.PLAIN_TEXT;
375 else if (encodingString.equals("plain"))
376 encoding = AuthInfo.Encoding.PLAIN_TEXT;
377 else if (encodingString.equals("md5"))
378 encoding = AuthInfo.Encoding.MD5;
380 throw new SAXException("Invalid encoding type");
383 current = new AuthInfo(
384 attributes.getValue("username"),
385 attributes.getValue("password"),
390 state = State.AUTH_INFO;
398 if (localName.equals("connection")) {
400 currentConnection = attributes.getValue("name");
401 if (currentConnection == null)
402 throw new SAXException("Attribute \"name\" required for connection tag.");
405 state = State.CONNECTION;
409 if (localName.equals("protocol")) {
411 // Associate protocol with default connection
412 currentConnection = "DEFAULT";
415 state = State.DEFAULT_CONNECTION_PROTOCOL;
419 if (localName.equals("param")) {
421 // Associate parameter with default connection
422 currentConnection = "DEFAULT";
424 currentParameter = attributes.getValue("name");
425 if (currentParameter == null)
426 throw new SAXException("Attribute \"name\" required for param tag.");
429 state = State.DEFAULT_CONNECTION_PARAMETER;
437 if (localName.equals("protocol")) {
439 state = State.PROTOCOL;
443 if (localName.equals("param")) {
445 currentParameter = attributes.getValue("name");
446 if (currentParameter == null)
447 throw new SAXException("Attribute \"name\" required for param tag.");
450 state = State.PARAMETER;
458 throw new SAXException("Unexpected tag: " + localName);
463 public void characters(char[] ch, int start, int length) throws SAXException {
465 String str = new String(ch, start, length);
470 current.getConfiguration(currentConnection)
475 current.getConfiguration(currentConnection)
476 .setParameter(currentParameter, str);
481 if (str.trim().length() != 0)
482 throw new SAXException("Unexpected character data.");