/* * JBoss, Home of Professional Open Source * Copyright 2008, JBoss Inc., and others contributors as indicated * by the @authors tag. All rights reserved. * See the copyright.txt in the distribution for a * full listing of individual contributors. * This copyrighted material is made available to anyone wishing to use, * modify, copy, or redistribute it subject to the terms and conditions * of the GNU Lesser General Public License, v. 2.1. * This program is distributed in the hope that it will be useful, but WITHOUT A * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. * You should have received a copy of the GNU Lesser General Public License, * v.2.1 along with this distribution; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * * (C) 2008, * @author Redhat Middleware LLC. */ package org.jboss.test.jbossts.crash; import com.arjuna.ats.jta.recovery.XAResourceRecovery; import javax.crypto.NoSuchPaddingException; import javax.crypto.BadPaddingException; import javax.crypto.IllegalBlockSizeException; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import javax.transaction.xa.XAResource; import javax.naming.InitialContext; import javax.sql.XADataSource; import javax.sql.ConnectionEventListener; import javax.sql.ConnectionEvent; import javax.sql.XAConnection; import javax.management.MBeanServerConnection; import javax.management.ObjectName; import javax.management.MBeanException; import javax.management.InstanceNotFoundException; import java.sql.SQLException; import java.sql.Connection; import java.util.Properties; import java.util.Iterator; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.io.InputStream; import java.io.ByteArrayInputStream; import java.lang.reflect.Method; import java.beans.PropertyEditor; import java.beans.PropertyEditorManager; import java.security.NoSuchAlgorithmException; import java.security.InvalidKeyException; import java.math.BigInteger; import org.jboss.security.SecurityAssociation; import org.jboss.security.SimplePrincipal; import org.jboss.logging.Logger; /** * Taken from https://svn.jboss.org/repos/labs/labs/jbosstm/branches/JBOSSTS_4_6_1_GA_CP/atsintegration/classes/com/arjuna/ats/internal/jbossatx/jta/AppServerJDBCXARecovery.java , * some changes applied. * Username/password credentials programmatically set to CrashHelper.DS_USER_NAME and CrashHelper.DS_PASSWORD. * * * This provides recovery for compliant JDBC drivers accessed via datasources deployed in JBossAS 5 * It is not meant to be db driver specific. * * This code is based on JDBCXARecovery, which expects JNDI to contain an XADataSource implementation. * In JBossAS, the object created in JNDI when a -ds.xml file containing <xa-datasource> element is used, * does not implement the XADataSource interface. We therefore use this modified code to pull the * datasource configuration information from the app server's JMX and instantiate an XADataSource from * which it can then create an XAConnection. * * To use this class, add an XAResourceRecovery entry in the jta section of jbossts-properties.xml * for each database for which you need recovery, ensuring the value ends with ;jndiname=<datasource-name> * i.e. the same value as is in the -ds.xml jndi-name element. * * Note for users with secured JMX invokers, use the extended format: * ;jndiname=MyExampleDbName,username=foo,password=bar * The username and password refer to the JMX invoker, NOT the datasource. * * It's not possible to override the jdbc username and password given in the -ds.xml file at present. * Since the recovery system sometimes needs greater db user privs than the app code, it may be preferable to * set up a -ds.xml file only for recovery usage. This approach works for databases which allow users to * recover one another's transactions, which is most of them. But consult * your db docs or friendly neighbourhood DBA to be sure, then test it anyhow. * * You also need the XARecoveryModule enabled and appropriate values for nodeIdentifier and xaRecoveryNode set. * See the JBossTS recovery guide if you are unclear on how the recovery system works. * * Note: This implementation expects to run inside the app server JVM. It probably won't work * if you configure the recovery manager to run as a separate process. (JMX can be accessed remotely, * but it would need code changes to the lookup function and some classpath additions). * * * <properties depends="arjuna" name="jta"> * ... * <property name="com.arjuna.ats.jta.recovery.XAResourceRecovery1" * value= "com.arjuna.ats.internal.jbossatx.jta.AppServerJDBCXARecovery;jndiname=MyExampleDbName[,username=foo,password=bar]"/> * <!-- xaRecoveryNode should match value in nodeIdentifier or be * --> * <property name="com.arjuna.ats.jta.xaRecoveryNode" value="1"/> * */ public class AppServerJDBCXARecovery implements XAResourceRecovery { // implementation based on com.arjuna.ats.internal.jdbc.recovery.JDBCXARecovery public AppServerJDBCXARecovery() throws SQLException { if (log.isDebugEnabled()) { log.debug("AppServerJDBCXARecovery<init>"); } _hasMoreResources = false; _connectionEventListener = new LocalConnectionEventListener(); } /** * The recovery module will have chopped off this class name already. The * parameter should specify a jndi name for the datasource. */ public boolean initialise(String parameter) throws SQLException { if (log.isDebugEnabled()) { log.debug("AppServerJDBCXARecovery.initialise(" + parameter + ")"); } if (parameter == null) return false; retrieveData(parameter, _DELIMITER); return true; } public synchronized XAResource getXAResource() throws SQLException { createConnection(); if (_connection == null) { throw new SQLException("The data source named [" + _dataSourceId + "] is not deployed."); } return _connection.getXAResource(); } public boolean hasMoreResources() { if (_dataSource == null) try { createDataSource(); } catch (SQLException sqlException) { return false; } if (_dataSource != null) { _hasMoreResources = ! _hasMoreResources; return _hasMoreResources; } else return false; } /** * Lookup the XADataSource in JNDI. */ private final void createDataSource() throws SQLException { // Note: the repeated use of SecurityAssociation.set[Principal|Credential] is a workaround for // JBAS-7171 / JBAS-6449 / JBPAPP-2479. Although now fixed in some AS/EAP releases, we keep the // workaround in place for now so we can work on older, non-fixed releases too. try { if (_dataSource == null) { // This is where we do JBossAS specific magic. Use the JMX to fetch the name of the XADataSource class // and its config params so that we don't have to duplicate the information in the -ds.xml file. // TODO: can we make this flexible enough to handle remote app server processes, so that // we can run the recovery manager out of process? The server addr would need to be on a per // jndiname basis, as we may be doing recovery for a whole cluster. We would need AS // jmx classes (and the db drivers naturally) on the recovery manager classpath. InitialContext context = new InitialContext(); MBeanServerConnection server = (MBeanServerConnection)context.lookup("jmx/invoker/RMIAdaptor"); ObjectName objectName = new ObjectName("jboss.jca:name="+_dataSourceId+",service=ManagedConnectionFactory"); if(_username !=null && _password !=null) { SecurityAssociation.setPrincipal(new SimplePrincipal(_username)); SecurityAssociation.setCredential(_password); } String className = (String)server.invoke(objectName, "getManagedConnectionFactoryAttribute", new Object[] {"XADataSourceClass"}, new String[] {"java.lang.String"}); log.debug("AppServerJDBCXARecovery datasource classname = "+className); if(_username !=null && _password !=null) { SecurityAssociation.setPrincipal(new SimplePrincipal(_username)); SecurityAssociation.setCredential(_password); } String properties = (String)server.invoke(objectName, "getManagedConnectionFactoryAttribute", new Object[] {"XADataSourceProperties"}, new String[] {"java.lang.String"}); // debug disabled due to security paranoia - it may log datasource password in cleartext. //log.debug("AppServerJDBCXARecovery.result="+properties); /* ObjectName txCmObjectName = new ObjectName("jboss.jca:name=" +_dataSourceId + ",service=XATxCM"); if(_username !=null && _password !=null) { SecurityAssociation.setPrincipal(new SimplePrincipal(_username)); SecurityAssociation.setCredential(_password); } String securityDomainName = (String) server.getAttribute(txCmObjectName, "SecurityDomainJndiName"); log.debug("Security domain name associated with JCA ConnectionManager jboss.jca:name=" +_dataSourceId + ",service=XATxCM"+" is:"+securityDomainName); if(securityDomainName != null && !securityDomainName.equals("")) { ObjectName _objectName = new ObjectName("jboss.security:service=XMLLoginConfig"); if(_username !=null && _password !=null) { SecurityAssociation.setPrincipal(new SimplePrincipal(_username)); SecurityAssociation.setCredential(_password); } String config = (String)server.invoke(_objectName, "displayAppConfig", new Object[] {securityDomainName}, new String[] {"java.lang.String"}); String loginModuleClass = getValueForLoginModuleClass(config); _dbUsername = getValueForKey(config, _USERNAME); String _encryptedPassword = getValueForKey(config, _PASSWORD); if("org.jboss.resource.security.JaasSecurityDomainIdentityLoginModule".equals(loginModuleClass)) { String jaasSecurityDomain = getValueForKey(config, "jaasSecurityDomain"); if(_username !=null && _password !=null) { SecurityAssociation.setPrincipal(new SimplePrincipal(_username)); SecurityAssociation.setCredential(_password); } _dbPassword = decodePBE(server, _encryptedPassword, jaasSecurityDomain); } else { _dbPassword = decode(_encryptedPassword); } _encrypted = true; } */ try { _dataSource = getXADataSource(className, properties); _supportsIsValidMethod = true; // assume it does; we'll lazily check the first time we try to connect } catch(Exception e) { _dataSource = null; log.error("AppServerJDBCXARecovery.createDataSource got exception during getXADataSource call: "+e.toString(), e); throw new SQLException(e.toString()); } } } catch (MBeanException mbe) { if (mbe.getTargetException() instanceof InstanceNotFoundException) { log.warn("AppServerJDBCXARecovery.createDataSource(name="+_dataSourceId+"): InstanceNotFound. Datasource not deployed, or wrong name?"); // this is an expected condition when the data source is not yet deployed // just ignore this for now, the next time around, we try again to see if its deployed yet return; } else { log.error("AppServerJDBCXARecovery.createDataSource(name="+_dataSourceId+") got exception " + mbe.toString(), mbe); } throw new SQLException(mbe.toString()); } catch (SQLException ex) { log.error("AppServerJDBCXARecovery.createDataSource got exception "+ex.toString(), ex); throw ex; } catch (Exception e) { log.error("AppServerJDBCXARecovery.createDataSource got exception "+e.toString(), e); throw new SQLException(e.toString()); } } /** * Create the XAConnection from the XADataSource. */ private final void createConnection() throws SQLException { try { if (_dataSource == null) { createDataSource(); // if we still don't have it, its because the data source isn't deployed yet if (_dataSource == null) { return; } } Boolean isConnectionValid; try { if (_connection != null && _supportsIsValidMethod) { Connection connection = _connection.getConnection(); Method method = connection.getClass().getMethod("isValid", new Class[] {Integer.class}); isConnectionValid = (Boolean) method.invoke(connection, new Object[] {new Integer(5)}); } else { isConnectionValid = Boolean.FALSE; } } catch (NoSuchMethodException nsme) { isConnectionValid = Boolean.FALSE; _supportsIsValidMethod = false; log.debug("XA datasource does not support isValid method - connection will always be recreated"); } catch (Throwable t) { isConnectionValid = Boolean.FALSE; log.debug("XA connection is invalid - will recreate a new one. Cause: " + t); } if (!isConnectionValid.booleanValue()) { if (_connection != null) { try { _connection.close(); // just attempt to clean up anything that we can } catch (Throwable t) { } finally { _connection = null; } } // Check if the password is encrypted, the criteria should be the existence of <security-domain>EncryptDBPassword</security-domain> // in the -ds.xml file. if(!_encrypted) { _connection = _dataSource.getXAConnection(); } else { _connection = _dataSource.getXAConnection(_dbUsername, _dbPassword); } _connection.addConnectionEventListener(_connectionEventListener); log.debug("Created new XAConnection"); } } catch (SQLException ex) { log.error("AppServerJDBCXARecovery.createConnection got exception "+ex.toString(), ex); throw ex; } catch (Exception e) { log.error("AppServerJDBCXARecovery.createConnection got exception "+e.toString(), e); throw new SQLException(e.toString()); } } private class LocalConnectionEventListener implements ConnectionEventListener { public void connectionErrorOccurred(ConnectionEvent connectionEvent) { _connection.removeConnectionEventListener(_connectionEventListener); _connection = null; } public void connectionClosed(ConnectionEvent connectionEvent) { _connection.removeConnectionEventListener(_connectionEventListener); _connection = null; } } // borrowed from org.jboss.resource.adapter.jdbc.xa.XAManagedConnectionFactory // in which the equivalent functionality is protected not public :-( private XADataSource getXADataSource(String xaDataSourceClassname, String propertiesString) throws Exception { // Map any \ to \\ propertiesString = propertiesString.replaceAll("\\\\", "\\\\\\\\"); Properties properties = new Properties(); InputStream is = new ByteArrayInputStream(propertiesString.getBytes()); properties.load(is); // HACK for crash recovery tests // check whether we have User and Password in XA properties if (! properties.containsKey("User")) { properties.setProperty("User", CrashHelper.DS_USER_NAME); properties.setProperty("Password", CrashHelper.DS_PASSWORD); } // end of hack Class clazz = Thread.currentThread().getContextClassLoader().loadClass(xaDataSourceClassname); XADataSource xads = (XADataSource) clazz.newInstance(); Class[] NOCLASSES = new Class[] {}; for (Iterator i = properties.keySet().iterator(); i.hasNext();) { String name = (String) i.next(); String value = properties.getProperty(name); //This is a bad solution. On the other hand the only known example // of a setter with no getter is for Oracle with password. //Anyway, each xadatasource implementation should get its //own subclass of this that explicitly sets the //properties individually. Class type = null; try { Method getter = clazz.getMethod("get" + name, NOCLASSES); type = getter.getReturnType(); } catch (NoSuchMethodException e) { type = String.class; try { //HACK for now until we can rethink the XADataSourceProperties variable and pass type information Method getter = clazz.getMethod("is" + name, NOCLASSES); type = getter.getReturnType(); }catch(NoSuchMethodException nsme) { type = String.class; } } Method setter = clazz.getMethod("set" + name, new Class[] { type }); PropertyEditor editor = PropertyEditorManager.findEditor(type); editor.setAsText(value); setter.invoke(xads, new Object[] { editor.getValue() }); } return xads; } public void retrieveData(String parameter,String delimiter) { StringTokenizer st = new StringTokenizer(parameter,delimiter); while (st.hasMoreTokens()) { String data = st.nextToken(); if(data.length()>9) { if(_USERNAME.equalsIgnoreCase(data.substring(0,8))) { _username =data.substring(9); } if(_PASSWORD.equalsIgnoreCase(data.substring(0,8))) { _password =data.substring(9); } if(_JNDINAME.equalsIgnoreCase(data.substring(0,8))) { _dataSourceId=data.substring(9); } } } if(_dataSourceId == null && parameter != null && parameter.indexOf('=') == -1) { // try to fallback to old parameter format where only the dataSourceId is given, without jndiname= prefix _dataSourceId = parameter; } } private String getValueForKey(String config, String key) { Pattern usernamePattern = Pattern.compile("(name=" + key + ", value=)(.*)(</li>)"); Matcher m = usernamePattern.matcher(config); if(m.find()) { return m.group(2); } return ""; } private String getValueForLoginModuleClass(String config) { Pattern usernamePattern = Pattern.compile("(" + _MODULE + ":)(.*)"); Matcher m = usernamePattern.matcher(config); if(m.find()) { return m.group(2).trim(); } return ""; } private static String decode(String secret) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { byte[] kbytes = "jaas is the way".getBytes(); SecretKeySpec key = new SecretKeySpec(kbytes, "Blowfish"); BigInteger n = new BigInteger(secret, 16); byte[] encoding = n.toByteArray(); Cipher cipher = Cipher.getInstance("Blowfish"); cipher.init(Cipher.DECRYPT_MODE, key); byte[] decode = cipher.doFinal(encoding); return new String(decode); } private static String decodePBE(MBeanServerConnection server, String password, String jaasSecurityDomain) throws Exception { byte[] secret = (byte[]) server.invoke(new ObjectName(jaasSecurityDomain), "decode64", new Object[] {password}, new String[] {"java.lang.String"}); return new String(secret, "UTF-8"); } private boolean _supportsIsValidMethod; private XAConnection _connection; private XADataSource _dataSource; private LocalConnectionEventListener _connectionEventListener; private boolean _hasMoreResources; private boolean _encrypted; private String _dataSourceId; private String _username; private String _password; private String _dbUsername; private String _dbPassword; private final String _JNDINAME = "jndiname"; private final String _USERNAME = "username"; private final String _PASSWORD = "password"; private final String _MODULE = "LoginModule Class"; private final String _DELIMITER = ","; private Logger log = org.jboss.logging.Logger.getLogger(AppServerJDBCXARecovery.class); }