diff options
21 files changed, 1186 insertions, 716 deletions
@@ -88,7 +88,7 @@ <dependency> <groupId>org.openslx.bwlp</groupId> <artifactId>master-sync-shared</artifactId> - <version>1.0-SNAPSHOT</version> + <version>1.1-SNAPSHOT</version> </dependency> <dependency> <groupId>mysql</groupId> @@ -97,12 +97,6 @@ <scope>compile</scope> </dependency> <dependency> - <groupId>fi.evident.dalesbred</groupId> - <artifactId>dalesbred</artifactId> - <version>0.6.0</version> - <scope>compile</scope> - </dependency> - <dependency> <groupId>org.apache.directory.api</groupId> <artifactId>api-all</artifactId> <version>1.0.0-M22</version> @@ -120,5 +114,11 @@ <artifactId>commons-net</artifactId> <version>2.0</version> </dependency> + <dependency> + <groupId>com.google.code.gson</groupId> + <artifactId>gson</artifactId> + <version>2.2.4</version> + <scope>compile</scope> + </dependency> </dependencies> </project> diff --git a/src/main/java/org/openslx/imagemaster/db/Database.java b/src/main/java/org/openslx/imagemaster/db/Database.java new file mode 100644 index 0000000..76c44fc --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/db/Database.java @@ -0,0 +1,174 @@ +package org.openslx.imagemaster.db;
+
+import java.io.BufferedInputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Collections;
+import java.util.Properties;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import org.apache.log4j.Logger;
+import org.openslx.imagemaster.util.Util;
+
+public class Database
+{
+
+ private static final Logger LOGGER = Logger.getLogger( Database.class );
+ /**
+ * Pool of available connections.
+ */
+ private static final Queue<MysqlConnection> pool = new ConcurrentLinkedQueue<>();
+
+ /**
+ * Set of connections currently handed out.
+ */
+ private static final Set<MysqlConnection> busyConnections = Collections.newSetFromMap( new ConcurrentHashMap<MysqlConnection, Boolean>() );
+
+ private static final String host;
+ private static final String dbname;
+ private static final String user;
+ private static final String password;
+
+ /**
+ * Static initializer for setting up the database connection.
+ * This gets called implicitly as soon as the class loader loads
+ * the class. In most cases that happens when the class is being
+ * accessed for the first time during run time.
+ */
+ static
+ {
+ // Load connection info from class (TODO: Make pretty)
+ Properties properties = new Properties();
+ try {
+ final BufferedInputStream stream = new BufferedInputStream(
+ new FileInputStream(
+ "config/mysql.properties" ) );
+ properties.load( stream );
+ stream.close();
+ } catch ( FileNotFoundException e ) {
+ LOGGER.fatal( "config/mysql.properties not found!" );
+ System.exit( 1 );
+ } catch ( IOException e ) {
+ LOGGER.fatal( "Error reading from config/mysql.properties: " + e.getMessage() );
+ System.exit( 1 );
+ } catch ( Exception e ) {
+ LOGGER.fatal( "Generic error loading mysql properties file." );
+ e.printStackTrace();
+ System.exit( 1 );
+ }
+ host = properties.getProperty( "host" );
+ dbname = properties.getProperty( "db" );
+ user = properties.getProperty( "user" );
+ password = properties.getProperty( "password" );
+
+ Util.notNullFatal( host, "host not set in mysql properties" );
+ Util.notNullFatal( dbname, "db not set in mysql properties" );
+ Util.notNullFatal( user, "user not set in mysql properties" );
+ Util.notNullFatal( password, "password not set in mysql properties" );
+
+ try {
+ Class.forName( "com.mysql.jdbc.Driver" ).newInstance();
+ } catch ( InstantiationException | IllegalAccessException | ClassNotFoundException e ) {
+ LOGGER.fatal( "Cannot get mysql JDBC driver!", e );
+ System.exit( 1 );
+ }
+ }
+
+ /**
+ * Get a connection to the database. If there is a valid connection in the
+ * pool, it will be returned. Otherwise, a new connection is created. If
+ * there are more than 20 busy connections, <code>null</code> is returned.
+ *
+ * @return connection to database, or <code>null</code>
+ */
+ public static MysqlConnection getConnection()
+ {
+ MysqlConnection con;
+ for ( ;; ) {
+ con = pool.poll();
+ if ( con == null )
+ break;
+ if ( !con.isValid() ) {
+ con.release();
+ continue;
+ }
+ if ( !busyConnections.add( con ) )
+ throw new RuntimeException( "Tried to hand out a busy connection!" );
+ return con;
+ }
+ // No pooled connection
+ if ( busyConnections.size() > 20 ) {
+ LOGGER.warn( "Too many open MySQL connections. Possible connection leak!" );
+ return null;
+ }
+ try {
+ // Create fresh connection
+ String uri = "jdbc:mysql://" + host + "/" + dbname
+ + "?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8"
+ + "&characterSetResults=utf8&connectionCollation=utf8mb4_unicode_ci";
+ Connection rawConnection = DriverManager.getConnection( uri,
+ user, password );
+ // By convention in our program we don't want auto commit
+ rawConnection.setAutoCommit( false );
+ // Wrap into our proxy
+ con = new MysqlConnection( rawConnection );
+ // Keep track of busy mysql connection
+ if ( !busyConnections.add( con ) )
+ throw new RuntimeException( "Tried to hand out a busy connection!" );
+ return con;
+ } catch ( SQLException e ) {
+ LOGGER.info( "Failed to connect to local mysql server", e );
+ }
+ return null;
+ }
+
+ /**
+ * Called by a {@link MysqlConnection} when its <code>close()</code>-method
+ * is called, so the connection will be added to the pool of available
+ * connections again.
+ *
+ * @param connection
+ */
+ static void returnConnection( MysqlConnection connection )
+ {
+ if ( !busyConnections.remove( connection ) )
+ throw new RuntimeException( "Tried to return a mysql connection to the pool that was not taken!" );
+ pool.add( connection );
+ }
+
+ public static void printCharsetInformation()
+ {
+ LOGGER.info( "MySQL charset related variables:" );
+ try ( MysqlConnection connection = Database.getConnection() ) {
+ MysqlStatement stmt = connection.prepareStatement( "SHOW VARIABLES LIKE :what" );
+ stmt.setString( "what", "char%" );
+ ResultSet rs = stmt.executeQuery();
+ while ( rs.next() ) {
+ LOGGER.info( rs.getString( "Variable_name" ) + ": " + rs.getString( "Value" ) );
+ }
+ stmt.setString( "what", "collat%" );
+ rs = stmt.executeQuery();
+ while ( rs.next() ) {
+ LOGGER.info( rs.getString( "Variable_name" ) + ": " + rs.getString( "Value" ) );
+ }
+ } catch ( SQLException e ) {
+ LOGGER.error( "Query failed in Database.printCharsetInformation()", e );
+ }
+ LOGGER.info( "End of variables" );
+ }
+
+ public static void printDebug()
+ {
+ LOGGER.info( "Available: " + pool.size() );
+ LOGGER.info( "Busy: " + busyConnections.size() );
+ }
+
+}// end class
diff --git a/src/main/java/org/openslx/imagemaster/db/DbSatellite.java b/src/main/java/org/openslx/imagemaster/db/DbSatellite.java deleted file mode 100644 index 4f5a0c8..0000000 --- a/src/main/java/org/openslx/imagemaster/db/DbSatellite.java +++ /dev/null @@ -1,156 +0,0 @@ -package org.openslx.imagemaster.db; - -import java.math.BigInteger; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.spec.InvalidKeySpecException; -import java.util.ArrayList; -import java.util.List; - -import org.apache.log4j.Logger; -import org.openslx.encryption.AsymKeyHolder; -import org.openslx.imagemaster.thrift.iface.OrganizationData; - -/** - * Represents a satellite in the database. - * Is used to authenticate the satellite. - */ -public class DbSatellite -{ - private static final Logger LOG = Logger.getLogger( DbSatellite.class ); - - private String organizationId, address, name, publickeyString, authMethod; - private PublicKey publickey = null; - private List<String> suffixList = null; - - public DbSatellite( String organizationId, String address, String name, String authMethod, String publickeyString ) - { - this.organizationId = organizationId; - this.address = address; - this.name = name; - this.publickeyString = publickeyString; - this.authMethod = authMethod; - } - - public static DbSatellite fromOrganizationId( String organizationId ) - { - return MySQL - .findUniqueOrNull( - DbSatellite.class, - "SELECT satellite.organizationid, satellite.address, satellite.name, satellite.authmethod, satellite.publickey FROM satellite WHERE satellite.organizationid = ? LIMIT 1", - organizationId ); - } - - public static DbSatellite fromSuffix( String suffix ) - { - return MySQL - .findUniqueOrNull( - DbSatellite.class, - "SELECT satellite.organizationid, satellite.address, satellite.name, satellite.authmethod, satellite.publickey FROM satellite" - + " INNER JOIN satellite_suffix USING (organizationid)" - + " WHERE satellite_suffix.suffix = ? LIMIT 1", - suffix ); - } - - /** - * Return all known satellites/organizations as List of {@link OrganizationData}, which can be - * used directly by the thrift API. - * - * @return list of all known organizations/satellites - */ - public static List<OrganizationData> asOrganizationDataList() - { - List<DbSatellite> sats = MySQL.findAll( - DbSatellite.class, - "SELECT satellite.organizationid, satellite.address, satellite.name, satellite.authmethod, satellite.publickey FROM satellite" ); - List<OrganizationData> orgData = new ArrayList<>(); - for ( DbSatellite sat : sats ) { - orgData.add( new OrganizationData( sat.getOrganizationId(), sat.getName(), sat.getAuthenticationMethod(), sat.getSuffixList() ) ); - } - return orgData; - } - - public static DbSatellite fromPrefix( String prefix ) - { - return null; - } - - /* - * Member methods - */ - - public String getAddress() - { - return address; - } - - public String getName() - { - return name; - } - - public String getOrganizationId() - { - return organizationId; - } - - public String getAuthenticationMethod() - { - return authMethod; - } - - public List<String> getSuffixList() - { - if ( this.suffixList == null ) - this.suffixList = DbSatelliteSuffix.forSatellite( this ); - return this.suffixList; - } - - /** - * Get the public key of this organization, if known and valid. - * - * @return Public key, null on error or not known - */ - public PublicKey getPubkey() - { - if ( publickey == null && publickeyString != null ) { - String parts[] = publickeyString.split( " " ); - BigInteger mod = null; - BigInteger exp = null; - for ( int i = 0; i < parts.length; ++i ) { - if ( parts[i].startsWith( "mod:" ) ) { - // modulus found. - mod = new BigInteger( parts[i].substring( 4 ) ); - } - if ( parts[i].startsWith( "exp:" ) ) { - // exponent found. - exp = new BigInteger( parts[i].substring( 4 ) ); - } - } - if ( mod == null ) { - LOG.error( "No modulus for building public key was found." ); - return null; - } - if ( exp == null ) { - LOG.error( "No public exponent for building public key was found." ); - return null; - } - try { - publickey = new AsymKeyHolder( null, exp, mod ).getPublicKey(); - } catch ( InvalidKeySpecException | NoSuchAlgorithmException e ) { - LOG.info( "PubKey of " + this.name + " is not valid.", e ); - } catch ( NumberFormatException e ) { - LOG.info( "PubKey of " + this.name + " is corrupted in database!", e ); - } - } - return publickey; - } - - public void updateAddress( String address, String organization ) - { - this.address = address; - this.organizationId = organization; - MySQL.update( "UPDATE satellite SET address = ? WHERE organizationid = ?", address, organization ); - } - -} diff --git a/src/main/java/org/openslx/imagemaster/db/DbSatelliteSuffix.java b/src/main/java/org/openslx/imagemaster/db/DbSatelliteSuffix.java deleted file mode 100644 index a22d694..0000000 --- a/src/main/java/org/openslx/imagemaster/db/DbSatelliteSuffix.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.openslx.imagemaster.db; - -import java.util.List; - -public class DbSatelliteSuffix -{ - - public static List<String> forOrganization( String organizationId ) - { - return MySQL.findAll( String.class, "SELECT satellite_suffix.suffix FROM satellite_suffix WHERE organizationid = ?", organizationId ); - } - - public static List<String> forSatellite( DbSatellite satellite ) - { - return forOrganization( satellite.getOrganizationId() ); - } - -} diff --git a/src/main/java/org/openslx/imagemaster/db/DbUser.java b/src/main/java/org/openslx/imagemaster/db/DbUser.java deleted file mode 100644 index 87febe1..0000000 --- a/src/main/java/org/openslx/imagemaster/db/DbUser.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.openslx.imagemaster.db; - -import java.util.List; - -import org.apache.log4j.Logger; -import org.openslx.imagemaster.session.User; -import org.openslx.imagemaster.thrift.iface.UserInfo; -import org.openslx.imagemaster.util.Sha512Crypt; - -/** - * Represents a user that can login against the masterserver. - */ -public class DbUser extends User -{ - - private static Logger log = Logger.getLogger( DbUser.class ); - - public DbUser( int userId, String login, String password, String organizationId, - String firstName, String lastName, String eMail, - String satelliteAddress ) - { - super( userId, login, password, organizationId, firstName, lastName, eMail, - satelliteAddress ); - } - - /** - * Query database for user with given login - * - * @param login - * (user@organizationSuffix) - * @return instance of DbUser for matching entry from DB, or null if not - * found - */ - public static DbUser forLogin( final String login ) - { - return MySQL - .findUniqueOrNull( - DbUser.class, - "SELECT user.userid, user.login, user.password, user.organizationid, user.firstname, user.lastname, user.email, satellite.address FROM user" - + " LEFT JOIN satellite USING (organizationid)" - + " WHERE user.login = ? LIMIT 1", - login ); - } - - /** - * Query database for user with given userId - * - * @param userid - * @return instance of DbUser for matching entry from DB, or null if not - * found - */ - public static DbUser forLogin( final int userid ) - { - return MySQL - .findUniqueOrNull( - DbUser.class, - "SELECT user.userid, user.login, user.password, user.organizationid, user.firstname, user.lastname, user.email, satellite.address FROM user" - + " LEFT JOIN satellite USING (organizationid)" - + " WHERE user.userid = ? LIMIT 1", - userid ); - } - - public static boolean exists( final String login ) - { - return forLogin( login ) != null; - } - - public static DbUser forLogin( String login, String password ) - { - DbUser user = forLogin( login ); - if ( user == null || !Sha512Crypt.verifyPassword( password, user.password ) ) - return null; - return user; - } - - public static boolean insertOrUpdate( User user ) - { - log.debug( "Inserted user '" + user.login + "' into db." ); - MySQL.update( - "INSERT INTO user (login, password, organizationid, firstname, lastname, email) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE password=VALUES(password), organizationid=VALUES(organizationid), firstname=VALUES(firstname), lastname=VALUES(lastname), email=VALUES(email)", - user.login, user.password, user.organizationId, user.firstName, user.lastName, user.eMail ); - return false; - } - - public static boolean insertOrUpdate(UserInfo userInfo) { - log.debug( "Inserted user '" + userInfo.userId + "' into db." ); - int ret = MySQL.update( - "INSERT INTO user (login, password, organizationid, firstname, lastname, email) VALUES (?, '', ?, ?, ?, ?) ON DUPLICATE KEY UPDATE organizationid=VALUES(organizationid), firstname=VALUES(firstname), lastname=VALUES(lastname), email=VALUES(email)", - userInfo.userId, userInfo.organizationId, userInfo.firstName, userInfo.lastName, userInfo.eMail ); - return (ret != 0); - } - - public static List<UserInfo> findUser( String organizationId, String searchTerm ) - { - final String str = "%" + searchTerm + "%"; // TODO: Better handling, escape LIKE chars, or even make this use REGEXP - if ( organizationId == null ) - return MySQL.findAll( UserInfo.class, "SELECT login, firstname, lastname, email, organizationid" - + " FROM user" - + " WHERE login LIKE ? OR firstname LIKE ? OR lastname LIKE ? OR email LIKE ?" - + " LIMIT 100", str, str, str ); - return MySQL.findAll( UserInfo.class, "SELECT login, firstname, lastname, email, organizationid" - + " FROM user" - + " WHERE organizationid = ? AND (login LIKE ? OR firstname LIKE ? OR lastname LIKE ? OR email LIKE ?)" - + " LIMIT 100", organizationId, str, str, str ); - } - -} diff --git a/src/main/java/org/openslx/imagemaster/db/LdapUser.java b/src/main/java/org/openslx/imagemaster/db/LdapUser.java deleted file mode 100644 index 988f43a..0000000 --- a/src/main/java/org/openslx/imagemaster/db/LdapUser.java +++ /dev/null @@ -1,168 +0,0 @@ -package org.openslx.imagemaster.db; - -import java.io.File; -import java.io.IOException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.cert.CertificateException; - -import javax.net.ssl.TrustManagerFactory; - -import org.apache.directory.api.ldap.model.cursor.CursorException; -import org.apache.directory.api.ldap.model.cursor.EntryCursor; -import org.apache.directory.api.ldap.model.entry.Entry; -import org.apache.directory.api.ldap.model.exception.LdapException; -import org.apache.directory.api.ldap.model.message.SearchScope; -import org.apache.directory.ldap.client.api.LdapConnection; -import org.apache.directory.ldap.client.api.LdapConnectionConfig; -import org.apache.directory.ldap.client.api.LdapNetworkConnection; -import org.apache.log4j.Logger; -import org.apache.mina.filter.ssl.KeyStoreFactory; -import org.openslx.imagemaster.Globals; -import org.openslx.imagemaster.session.User; -import org.openslx.imagemaster.thrift.iface.AuthenticationError; -import org.openslx.imagemaster.thrift.iface.AuthenticationException; -import org.openslx.imagemaster.util.Sha512Crypt; - -/** - * Represents a user instance that was queries (primarily) from LDAP. - * Additional information that is not provided by the LDAP server might - * be fetched from other sources, like the local database (DbUser.java). - */ -public class LdapUser extends User -{ - - private static final Logger log = Logger.getLogger( LdapUser.class ); - - protected LdapUser(int userId, String username, String password, String organization, String firstName, String lastName, String eMail, String satelliteAddress) - { - super( userId, username, password, organization, firstName, lastName, eMail, - satelliteAddress ); - } - - /** - * Query LDAP for user with given login - * - * @param login Login of user in the form "prefix_username" - * @return instance of LDAPUser for matching entry from LDAP or null if sth went wrong - */ - public static LdapUser forLogin( final String login, final String password ) throws AuthenticationException - { - String username, organization, firstName, lastName, eMail, satelliteAddress = ""; - - final String[] split = login.split( "_" ); - if (split.length != 2) - throw new AuthenticationException(AuthenticationError.GENERIC_ERROR, "Login must be in form: prefix_username"); - - LdapConnection connection = null; - try { - LdapConnectionConfig config = new LdapConnectionConfig(); - - String ldapHost = Globals.getLdapHost(); - log.debug( "Setting host... " + ldapHost ); - config.setLdapHost( ldapHost ); - - boolean useSsl = Globals.getLdapSsl(); - log.debug( "Setting use ssl... " + useSsl); - config.setUseSsl( useSsl ); - - int ldapPort = Globals.getLdapPort(); - log.debug( "Setting port... " + ldapPort ); - config.setLdapPort( ldapPort ); - - // load keystore ... - KeyStoreFactory ksf = new KeyStoreFactory(); - ksf.setDataFile( new File(Globals.getLdapKeystorePath()) ); - ksf.setPassword( Globals.getLdapKeystorePassword()); - ksf.setType( "jks" ); - - // ... and set TrustManager - TrustManagerFactory tmf = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm() ); - tmf.init( ksf.newInstance() ); - - config.setTrustManagers( tmf.getTrustManagers() ); - - connection = new LdapNetworkConnection( config ); - - log.debug( "Trying to bind..." ); - String bind = Globals.getLdapBindQuery().replace( "%", login ); - connection.bind( bind, password ); - //connection.bind(); - log.debug( "Bind successful" ); - - - // make search query - EntryCursor cursor = connection.search( Globals.getLdapSearchBaseDn(), - Globals.getLdapSearchFilter().replace( "%", login ), SearchScope.SUBTREE ); - // only use the first result - cursor.next(); - Entry entry = cursor.get(); - username = entry.get( "cn" ).toString().split( " " )[1].split( "@" )[0]; - organization = entry.get( "cn" ).toString().split( "@" )[1]; - firstName = entry.get( "givenName" ).getString(); - lastName = entry.get( "sn" ).getString(); - eMail = entry.get( "mail" ).getString(); - - // get the satellite address from db - DbSatellite dbSatellite = DbSatellite.fromSuffix( organization ); - if ( dbSatellite != null ) { - satelliteAddress = dbSatellite.getAddress(); - } else { - // Organization is not known. This should not happen because the login would have failed then. - throw new AuthenticationException( AuthenticationError.GENERIC_ERROR, "Your Organization is not known by the server. Please contact your administrator." ); - } - // everything went fine - return new LdapUser( 0, username, Sha512Crypt.Sha512_crypt( password, null, 0 ), organization, firstName, lastName, eMail, satelliteAddress ); - } catch ( LdapException e) { - if ( e.getMessage().contains( "Cannot connect on the server" ) ) { - DbSatellite dbSatellite = DbSatellite.fromPrefix(split[0]); - if (dbSatellite == null) throw new AuthenticationException(AuthenticationError.INVALID_CREDENTIALS, "Credentials invalid."); - String lo = split[1] + "@" + dbSatellite.getOrganizationId(); - log.info( "LDAP server could not be reached. Trying to connect locally with: " + lo ); - return LdapUser.localLogin(lo, password); - } - e.printStackTrace(); - throw new AuthenticationException( AuthenticationError.GENERIC_ERROR, "Something went wrong." ); - } catch ( CursorException e ) { - e.printStackTrace(); - throw new AuthenticationException( AuthenticationError.INVALID_CREDENTIALS, "Could not find user entry." ); - } catch ( IOException e ) { - // could not load keyfile - e.printStackTrace(); - } catch ( NoSuchAlgorithmException e ) { - // could not load algorithm - e.printStackTrace(); - } catch ( KeyStoreException | NoSuchProviderException | CertificateException e ) { - // some problem with the key - e.printStackTrace(); - } finally { - // close connection - try { - connection.unBind(); - connection.close(); - } catch ( IOException | LdapException e ) { - // was not connected so don't do anything... - } - } - return null; - } - - /** - * Login user locally if external Ldap server is not available - * @param login Must be in form "username@organization" - * @param password The user's password - */ - private static LdapUser localLogin( String login, String password ) - { - DbUser user = DbUser.forLogin( login ); - if (user == null) return null; // no user found - - // check users password - if (!Sha512Crypt.verifyPassword( password, user.password )) return null; - - // return ldapuser if valid - return new LdapUser( user.userId, user.login, Sha512Crypt.Sha512_crypt( password, null, 0 ), user.organizationId, user.firstName, - user.lastName, user.eMail, user.satelliteAddress ); - } -} diff --git a/src/main/java/org/openslx/imagemaster/db/MySQL.java b/src/main/java/org/openslx/imagemaster/db/MySQL.java deleted file mode 100644 index 9443879..0000000 --- a/src/main/java/org/openslx/imagemaster/db/MySQL.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.openslx.imagemaster.db; - -import java.io.FileInputStream; -import java.io.BufferedInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.List; -import java.util.Properties; - -import com.mysql.jdbc.jdbc2.optional.MysqlDataSource; - -import org.apache.log4j.Logger; -import org.openslx.imagemaster.util.Util; - -import fi.evident.dalesbred.Database; - -/** - * Class for talking to the DB via the dalesbred jdbc wrapper. Package private, - * so only the Db* classes can actually communicate with the database. - * - */ -class MySQL -{ - - private static final Logger log = Logger.getLogger( MySQL.class ); - private static Database db = null; - - /** - * Static initializer for setting up the database connection. - * This gets called implicitly as soon as the class loader loads - * the class. In most cases that happens when the class is being - * accessed for the first time during run time. - */ - static - { - // Load connection info from class (TODO: Make pretty) - Properties properties = new Properties(); - try { - final BufferedInputStream stream = new BufferedInputStream( new FileInputStream( "config/mysql.properties" ) ); - properties.load( stream ); - stream.close(); - } catch ( FileNotFoundException e ) { - log.fatal( "config/mysql.properties not found!" ); - System.exit( 1 ); - } catch ( IOException e ) { - log.fatal( "Error reading from config/mysql.properties: " + e.getMessage() ); - System.exit( 1 ); - } catch ( Exception e ) { - log.fatal( "Generic error loading mysql properties file." ); - e.printStackTrace(); - System.exit( 1 ); - } - final String host = properties.getProperty( "host" ); - final String dbname = properties.getProperty( "db" ); - final String user = properties.getProperty( "user" ); - final String password = properties.getProperty( "password" ); - - Util.notNullFatal( host, "host not set in mysql properties" ); - Util.notNullFatal( dbname, "db not set in mysql properties" ); - Util.notNullFatal( user, "user not set in mysql properties" ); - Util.notNullFatal( password, "password not set in mysql properties" ); - - // Setup db connection - try { - MysqlDataSource ds = new MysqlDataSource(); - ds.setServerName( host ); - ds.setDatabaseName( dbname ); - ds.setUser( user ); - ds.setPassword( password ); - ds.setEncoding( "UTF-8" ); - db = Database.forDataSource( ds ); - } catch ( Exception e ) { - log.fatal( "Error initializing mysql data source!" ); - e.printStackTrace(); - System.exit( 1 ); - } - } - - /** - * Get a list of objects of the given class from the database. - * The class needs a matching constructor for the query you pass in, i.e. number of - * arguments has to be equal to number of columns returned by query. - * - * @param clazz The class to instanciate for the result(s) - * @param sql The sql query to run - * @param args Any number of arguments to the query (using the '?' placeholder) - * @return A list containing the rows returned by the query, represented by the given class - */ - protected static <T> List<T> findAll( final Class<T> clazz, final String sql, final Object... args ) - { - return db.findAll( clazz, sql, args ); - } - - /** - * Run a query on the database that will return at most one result. - * If the query returns a row, it will be used to instanciate the given class. If - * it doesn't return a row, null will be returned. - * - * @param clazz The class to instanciate for the result (if any) - * @param sql The sql query to run - * @param args Any number of arguments to the query (using the '?' placeholder) - * @return Instance of clazz or null - */ - protected static <T> T findUniqueOrNull( final Class<T> clazz, final String sql, final Object... args ) - { - return db.findUniqueOrNull( clazz, sql, args ); - } - - /** - * Run an update on the database, return number of rows affected. - * - * @param sql The update/insert query to run - * @param args Any number of arguments to the query (using the '?' placeholder) - * @return Number of rows affected by query - */ - protected static int update( String sql, Object... args ) - { - return db.update( sql, args ); - } -} diff --git a/src/main/java/org/openslx/imagemaster/db/MysqlConnection.java b/src/main/java/org/openslx/imagemaster/db/MysqlConnection.java new file mode 100644 index 0000000..dcfa713 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/db/MysqlConnection.java @@ -0,0 +1,78 @@ +package org.openslx.imagemaster.db; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.log4j.Logger; + +public class MysqlConnection implements AutoCloseable { + + private static final Logger LOGGER = Logger.getLogger(MysqlConnection.class); + + private static final int CONNECTION_TIMEOUT_MS = 5 * 60 * 1000; + + private final long deadline = System.currentTimeMillis() + CONNECTION_TIMEOUT_MS; + + private final Connection rawConnection; + + private boolean hasPendingQueries = false; + + private List<MysqlStatement> openStatements = new ArrayList<>(); + + MysqlConnection(Connection rawConnection) { + this.rawConnection = rawConnection; + } + + public MysqlStatement prepareStatement(String sql) throws SQLException { + if (!sql.startsWith("SELECT")) + hasPendingQueries = true; + MysqlStatement statement = new MysqlStatement(rawConnection, sql); + openStatements.add(statement); + return statement; + } + + public void commit() throws SQLException { + rawConnection.commit(); + hasPendingQueries = false; + } + + public void rollback() throws SQLException { + rawConnection.rollback(); + hasPendingQueries = false; + } + + boolean isValid() { + return System.currentTimeMillis() < deadline; + } + + @Override + public void close() { + if (hasPendingQueries) { + LOGGER.warn("Mysql connection had uncommited queries on .close()"); + hasPendingQueries = false; + } + try { + rawConnection.rollback(); + } catch (SQLException e) { + LOGGER.warn("Rolling back uncommited queries failed!", e); + } + if (!openStatements.isEmpty()) { + for (MysqlStatement statement : openStatements) { + statement.close(); + } + openStatements.clear(); + } + Database.returnConnection(this); + } + + void release() { + try { + rawConnection.close(); + } catch (SQLException e) { + // Nothing meaningful to do + } + } + +} diff --git a/src/main/java/org/openslx/imagemaster/db/MysqlStatement.java b/src/main/java/org/openslx/imagemaster/db/MysqlStatement.java new file mode 100644 index 0000000..391aed0 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/db/MysqlStatement.java @@ -0,0 +1,321 @@ +package org.openslx.imagemaster.db; + +import java.io.Closeable; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Class for creating {@link PreparedStatement}s with named parameters. Based on + * <a href= + * "http://www.javaworld.com/article/2077706/core-java/named-parameters-for-preparedstatement.html?page=2" + * >Named Parameters for PreparedStatement</a> + */ +public class MysqlStatement implements Closeable { + + private static final QueryCache cache = new QueryCache(); + + private final PreparsedQuery query; + + private final PreparedStatement statement; + + private final List<ResultSet> openResultSets = new ArrayList<>(); + + MysqlStatement(Connection con, String sql) throws SQLException { + PreparsedQuery query; + synchronized (cache) { + query = cache.get(sql); + } + if (query == null) { + query = parse(sql); + synchronized (cache) { + cache.put(sql, query); + } + } + this.query = query; + this.statement = con.prepareStatement(query.sql); + } + + /** + * Returns the indexes for a parameter. + * + * @param name parameter name + * @return parameter indexes + * @throws IllegalArgumentException if the parameter does not exist + */ + private List<Integer> getIndexes(String name) { + List<Integer> indexes = query.indexMap.get(name); + if (indexes == null) { + throw new IllegalArgumentException("Parameter not found: " + name); + } + return indexes; + } + + /** + * Sets a parameter. + * + * @param name parameter name + * @param value parameter value + * @throws SQLException if an error occurred + * @throws IllegalArgumentException if the parameter does not exist + * @see PreparedStatement#setObject(int, java.lang.Object) + */ + public void setObject(String name, Object value) throws SQLException { + List<Integer> indexes = getIndexes(name); + for (Integer index : indexes) { + statement.setObject(index, value); + } + } + + /** + * Sets a parameter. + * + * @param name parameter name + * @param value parameter value + * @throws SQLException if an error occurred + * @throws IllegalArgumentException if the parameter does not exist + * @see PreparedStatement#setString(int, java.lang.String) + */ + public void setString(String name, String value) throws SQLException { + List<Integer> indexes = getIndexes(name); + for (Integer index : indexes) { + statement.setString(index, value); + } + } + + /** + * Sets a parameter. + * + * @param name parameter name + * @param value parameter value + * @throws SQLException if an error occurred + * @throws IllegalArgumentException if the parameter does not exist + * @see PreparedStatement#setInt(int, int) + */ + public void setInt(String name, int value) throws SQLException { + List<Integer> indexes = getIndexes(name); + for (Integer index : indexes) { + statement.setInt(index, value); + } + } + + /** + * Sets a parameter. + * + * @param name parameter name + * @param value parameter value + * @throws SQLException if an error occurred + * @throws IllegalArgumentException if the parameter does not exist + * @see PreparedStatement#setLong(int, long) + */ + public void setLong(String name, long value) throws SQLException { + List<Integer> indexes = getIndexes(name); + for (Integer index : indexes) { + statement.setLong(index, value); + } + } + + /** + * Sets a parameter. + * + * @param name parameter name + * @param value parameter value + * @throws SQLException if an error occurred + * @throws IllegalArgumentException if the parameter does not exist + * @see PreparedStatement#setBoolean(int, boolean) + */ + public void setBoolean(String name, boolean value) throws SQLException { + List<Integer> indexes = getIndexes(name); + for (Integer index : indexes) { + statement.setBoolean(index, value); + } + } + + /** + * Sets a parameter. + * + * @param name parameter name + * @param value parameter value + * @throws SQLException if an error occurred + * @throws IllegalArgumentException if the parameter does not exist + * @see PreparedStatement#setBoolean(int, boolean) + */ + public void setBinary(String name, byte[] value) throws SQLException { + List<Integer> indexes = getIndexes(name); + for (Integer index : indexes) { + statement.setBytes(index, value); + } + } + + /** + * Executes the statement. + * + * @return true if the first result is a {@link ResultSet} + * @throws SQLException if an error occurred + * @see PreparedStatement#execute() + */ + public boolean execute() throws SQLException { + return statement.execute(); + } + + /** + * Executes the statement, which must be a query. + * + * @return the query results + * @throws SQLException if an error occurred + * @see PreparedStatement#executeQuery() + */ + public ResultSet executeQuery() throws SQLException { + ResultSet rs = statement.executeQuery(); + openResultSets.add(rs); + return rs; + } + + /** + * Executes the statement, which must be an SQL INSERT, UPDATE or DELETE + * statement; or an SQL statement that returns nothing, such as a DDL + * statement. + * + * @return number of rows affected + * @throws SQLException if an error occurred + * @see PreparedStatement#executeUpdate() + */ + public int executeUpdate() throws SQLException { + return statement.executeUpdate(); + } + + /** + * Closes the statement. + * + * @see Statement#close() + */ + @Override + public void close() { + for (ResultSet rs : openResultSets) { + try { + rs.close(); + } catch (SQLException e) { + // + } + } + try { + statement.close(); + } catch (SQLException e) { + // Nothing to do + } + } + + /** + * Adds the current set of parameters as a batch entry. + * + * @throws SQLException if something went wrong + */ + public void addBatch() throws SQLException { + statement.addBatch(); + } + + /** + * Executes all of the batched statements. + * + * See {@link Statement#executeBatch()} for details. + * + * @return update counts for each statement + * @throws SQLException if something went wrong + */ + public int[] executeBatch() throws SQLException { + return statement.executeBatch(); + } + + // static methods + + private static PreparsedQuery parse(String query) { + int length = query.length(); + StringBuffer parsedQuery = new StringBuffer(length); + Map<String, List<Integer>> paramMap = new HashMap<>(); + boolean inSingleQuote = false; + boolean inDoubleQuote = false; + boolean hasBackslash = false; + int index = 1; + + for (int i = 0; i < length; i++) { + char c = query.charAt(i); + if (hasBackslash) { + // Last char was a backslash, so we ignore the current char + hasBackslash = false; + } else if (c == '\\') { + // This is a backslash, next char will be escaped + hasBackslash = true; + } else if (inSingleQuote) { + // End of quoted string + if (c == '\'') { + inSingleQuote = false; + } + } else if (inDoubleQuote) { + // End of quoted string + if (c == '"') { + inDoubleQuote = false; + } + } else { + // Not in string, look for named params + if (c == '\'') { + inSingleQuote = true; + } else if (c == '"') { + inDoubleQuote = true; + } else if (c == ':' && i + 1 < length && Character.isJavaIdentifierStart(query.charAt(i + 1))) { + int j = i + 2; + while (j < length && Character.isJavaIdentifierPart(query.charAt(j))) { + j++; + } + String name = query.substring(i + 1, j); + c = '?'; // replace the parameter with a question mark + i += name.length(); // skip past the end of the parameter + + List<Integer> indexList = paramMap.get(name); + if (indexList == null) { + indexList = new ArrayList<>(); + paramMap.put(name, indexList); + } + indexList.add(new Integer(index)); + + index++; + } + } + parsedQuery.append(c); + } + + return new PreparsedQuery(parsedQuery.toString(), paramMap); + } + + // private helper classes + + private static class PreparsedQuery { + private final Map<String, List<Integer>> indexMap; + private final String sql; + + public PreparsedQuery(String sql, Map<String, List<Integer>> indexMap) { + this.sql = sql; + this.indexMap = indexMap; + } + } + + private static class QueryCache extends LinkedHashMap<String, PreparsedQuery> { + private static final long serialVersionUID = 1L; + + public QueryCache() { + super(30, (float) 0.75, true); + } + + @Override + protected boolean removeEldestEntry(Map.Entry<String, PreparsedQuery> eldest) { + return size() > 40; + } + } + +} diff --git a/src/main/java/org/openslx/imagemaster/db/Paginator.java b/src/main/java/org/openslx/imagemaster/db/Paginator.java new file mode 100644 index 0000000..267a1a3 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/db/Paginator.java @@ -0,0 +1,11 @@ +package org.openslx.imagemaster.db; + +public class Paginator { + + public static final int PER_PAGE = 200; + + public static String limitStatement(int page) { + return " LIMIT " + (page * PER_PAGE) + ", " + PER_PAGE; + } + +} diff --git a/src/main/java/org/openslx/imagemaster/db/DbImage.java b/src/main/java/org/openslx/imagemaster/db/mappers/DbImage.java index bc4fa25..027ddf6 100644 --- a/src/main/java/org/openslx/imagemaster/db/DbImage.java +++ b/src/main/java/org/openslx/imagemaster/db/mappers/DbImage.java @@ -1,10 +1,10 @@ -package org.openslx.imagemaster.db; +package org.openslx.imagemaster.db.mappers; import java.util.List; +import org.openslx.bwlp.thrift.iface.ImagePublishData; import org.openslx.imagemaster.Globals; import org.openslx.imagemaster.serverconnection.UploadingImage; -import org.openslx.imagemaster.thrift.iface.ImageData; import org.openslx.imagemaster.util.Util; /** @@ -95,11 +95,13 @@ public class DbImage /** * Insert a new image into database * - * @param imageData The metadata of the image - * @param filepath Local storage path of image + * @param imageData + * The metadata of the image + * @param filepath + * Local storage path of image * @return Affected rows */ - public static int insert( ImageData imageData, String filepath ) + public static int insert( ImagePublishData imageData, String filepath ) { int numBlocks = Util.getNumberOfBlocks( imageData.fileSize, Globals.blockSize ); String missingBlocksList = ""; @@ -213,7 +215,8 @@ public class DbImage DbUser user = DbUser.forLogin( this.ownerId ); if ( user != null ) owner = user.getLogin(); - return new ImageData( this.uuid, this.revision, this.title, this.createTime, + return new ImageData( + this.uuid, this.revision, this.title, this.createTime, this.updateTime, owner, this.operatingSystem, this.isValid, this.isDeleted, this.longDescription, this.fileSize ); } diff --git a/src/main/java/org/openslx/imagemaster/db/mappers/DbOperatingSystem.java b/src/main/java/org/openslx/imagemaster/db/mappers/DbOperatingSystem.java new file mode 100644 index 0000000..1504e50 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/db/mappers/DbOperatingSystem.java @@ -0,0 +1,36 @@ +package org.openslx.imagemaster.db.mappers; + +import java.util.List; + +import org.openslx.bwlp.thrift.iface.OperatingSystem; +import org.openslx.imagemaster.db.MySQL; +import org.openslx.util.TimeoutReference; + +public class DbOperatingSystem +{ + + private static TimeoutReference<List<OperatingSystem>> cached = new TimeoutReference<List<OperatingSystem>>( + 30000, null ); + + private DbOperatingSystem() + { + } + + public static List<OperatingSystem> getAll() + { + List<OperatingSystem> list = cached.get(); + if ( list != null ) + return list; + list = MySQL.findAll( + OperatingSystem.class, + "SELECT osid, displayname, NULL, architecture" + + " FROM operatingsystem" ); + for ( OperatingSystem os : list ) { + os.virtualizerOsId = MySQL.findMap( String.class, String.class, + "SELECT virtid, virtoskeyword FROM os_x_virt WHERE osid = ?", os.osId ); + } + cached.set( list ); + return list; + } + +} diff --git a/src/main/java/org/openslx/imagemaster/db/mappers/DbOrganization.java b/src/main/java/org/openslx/imagemaster/db/mappers/DbOrganization.java new file mode 100644 index 0000000..cf1a20a --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/db/mappers/DbOrganization.java @@ -0,0 +1,137 @@ +package org.openslx.imagemaster.db.mappers; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.log4j.Logger; +import org.openslx.bwlp.thrift.iface.Organization; +import org.openslx.imagemaster.db.Database; +import org.openslx.imagemaster.db.MysqlConnection; +import org.openslx.imagemaster.db.MysqlStatement; + +/** + * Represents an organization in the database. + * Is used to authenticate the organization. + */ +public class DbOrganization +{ + private static final Logger LOGGER = Logger.getLogger( DbOrganization.class ); + + private static final String organizationBaseSql = "SELECT" + + " o.organizationid, o.name, o.authmethod, o.publickey" + + " FROM organization o"; + + private static final String suffixListFromOrgSql = "SELECT suffix FROM organization_suffix" + + " WHERE organizationid = :organizationid"; + + private static Organization fromResultSet( MysqlConnection connection, ResultSet rs ) throws SQLException + { + String organizationId = rs.getString( "organizationid" ); + String ecpUrl = rs.getString( "authmethod" ); + if ( ecpUrl != null && !ecpUrl.startsWith( "http" ) ) { + ecpUrl = null; + } + return new Organization( organizationId, rs.getString( "name" ), ecpUrl, getSuffixList( connection, + organizationId ) ); + } + + /** + * Get organization by id. Returns null if not found. + * + * @param organizationId + * @return + * @throws SQLException + */ + public static Organization fromOrganizationId( String organizationId ) throws SQLException + { + try ( MysqlConnection connection = Database.getConnection() ) { + MysqlStatement stmt = connection.prepareStatement( organizationBaseSql + " WHERE o.organizationid = :organizationid" ); + stmt.setString( "organizationid", organizationId ); + ResultSet rs = stmt.executeQuery(); + if ( !rs.next() ) + return null; + return fromResultSet( connection, rs ); + } catch ( SQLException e ) { + LOGGER.error( "Query failed in DbOrganization.fromOrganizationId()", e ); + throw e; + } + } + + public static Organization fromSuffix( String suffix ) throws SQLException + { + try ( MysqlConnection connection = Database.getConnection() ) { + MysqlStatement stmt = connection.prepareStatement( organizationBaseSql + + " INNER JOIN organization_suffix s USING (organizationid)" + + " WHERE s.suffix = :suffix" ); + stmt.setString( "suffix", suffix ); + ResultSet rs = stmt.executeQuery(); + if ( !rs.next() ) + return null; + return fromResultSet( connection, rs ); + } catch ( SQLException e ) { + LOGGER.error( "Query failed in DbOrganization.fromSuffix()", e ); + throw e; + } + } + + private static List<String> suffixForOrg( MysqlStatement stmt, String organizationId ) throws SQLException + { + stmt.setString( "organizationid", organizationId ); + ResultSet rs = stmt.executeQuery(); + List<String> list = new ArrayList<>(); + while ( rs.next() ) { + list.add( rs.getString( "suffix" ) ); + } + return list; + } + + /** + * Return all known satellites/organizations as List of {@link OrganizationData}, which can be + * used directly by the thrift API. + * + * @return list of all known organizations/satellites + * @throws SQLException + */ + public static List<Organization> getAll() throws SQLException + { + try ( MysqlConnection connection = Database.getConnection() ) { + MysqlStatement stmt = connection.prepareStatement( organizationBaseSql ); + ResultSet rsOrg = stmt.executeQuery(); + MysqlStatement stmtSuffix = connection.prepareStatement( suffixListFromOrgSql ); + List<Organization> list = new ArrayList<>(); + while ( rsOrg.next() ) { + String organizationId = rsOrg.getString( "organizationid" ); + String ecpUrl = rsOrg.getString( "authmethod" ); + if ( ecpUrl != null && !ecpUrl.startsWith( "http" ) ) { + ecpUrl = null; + } + List<String> suffixList = suffixForOrg( stmtSuffix, organizationId ); + list.add( new Organization( organizationId, rsOrg.getString( "name" ), ecpUrl, suffixList ) ); + } + return list; + } catch ( SQLException e ) { + LOGGER.error( "Query failed in DbOrganization.getAll()", e ); + throw e; + } + } + + public static DbOrganization fromPrefix( String prefix ) + { + return null; + } + + public static List<String> getSuffixList( MysqlConnection connection, String organizationId ) throws SQLException + { + List<String> list = new ArrayList<>(); + MysqlStatement stmt = connection.prepareStatement( suffixListFromOrgSql ); + stmt.setString( "organizationid", organizationId ); + ResultSet rs = stmt.executeQuery(); + while ( rs.next() ) { + list.add( rs.getString( "suffix" ) ); + } + return list; + } + +} diff --git a/src/main/java/org/openslx/imagemaster/db/DbPendingSatellite.java b/src/main/java/org/openslx/imagemaster/db/mappers/DbPendingSatellite.java index 30ec5a2..15cdbb9 100644 --- a/src/main/java/org/openslx/imagemaster/db/DbPendingSatellite.java +++ b/src/main/java/org/openslx/imagemaster/db/mappers/DbPendingSatellite.java @@ -1,6 +1,7 @@ -package org.openslx.imagemaster.db; +package org.openslx.imagemaster.db.mappers; import org.apache.log4j.Logger; +import org.openslx.imagemaster.db.MySQL; public class DbPendingSatellite { diff --git a/src/main/java/org/openslx/imagemaster/db/mappers/DbUser.java b/src/main/java/org/openslx/imagemaster/db/mappers/DbUser.java new file mode 100644 index 0000000..ed55d8a --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/db/mappers/DbUser.java @@ -0,0 +1,108 @@ +package org.openslx.imagemaster.db.mappers; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.log4j.Logger; +import org.openslx.bwlp.thrift.iface.Role; +import org.openslx.bwlp.thrift.iface.TNotFoundException; +import org.openslx.bwlp.thrift.iface.UserInfo; +import org.openslx.imagemaster.db.Database; +import org.openslx.imagemaster.db.MysqlConnection; +import org.openslx.imagemaster.db.MysqlStatement; +import org.openslx.imagemaster.db.models.LocalUser; +import org.openslx.imagemaster.util.Sha512Crypt; + +/** + * Represents a user that can login against the masterserver. + */ +public class DbUser +{ + + private static final Logger LOGGER = Logger.getLogger( DbUser.class ); + + private static final String localUserSql = "SELECT" + + " user.userid, user.login, user.password, user.organizationid, user.firstname, user.lastname, user.email," + + " organization.address FROM user" + + " LEFT JOIN organization USING (organizationid)"; + + private static LocalUser localFromRs( ResultSet rs ) throws SQLException + { + return new LocalUser( rs.getInt( "userid" ), rs.getString( "login" ), rs.getString( "password" ), + rs.getString( "organizationid" ), rs.getString( "firstname" ), rs.getString( "lastname" ), rs.getString( "email" ), + Role.TUTOR ); + } + + /** + * Query database for user with given login + * + * @param login (global user-id, login@org for test-accounts) + * @return instance of DbUser for matching entry from DB, or null if not + * found + * @throws SQLException if the query fails + */ + public static LocalUser forLogin( final String login ) throws SQLException + { + try ( MysqlConnection connection = Database.getConnection() ) { + MysqlStatement stmt = connection.prepareStatement( localUserSql + + " WHERE user.login = :login" ); + stmt.setString( "login", login ); + ResultSet rs = stmt.executeQuery(); + if ( !rs.next() ) + return null; + return localFromRs( rs ); + } catch ( SQLException e ) { + LOGGER.error( "Query failed in DbUser.forLogin()", e ); + throw e; + } + } + + public static UserInfo getUserInfo( final String login ) throws SQLException, TNotFoundException + { + LocalUser user = forLogin( login ); + if ( user == null ) + throw new TNotFoundException(); + return user.toUserInfo(); + } + + /** + * Query database for user with given userId + * + * @param userid + * @return instance of DbUser for matching entry from DB, or null if not + * found + * @throws SQLException + */ + public static LocalUser forLogin( final int userId ) throws SQLException + { + try ( MysqlConnection connection = Database.getConnection() ) { + MysqlStatement stmt = connection.prepareStatement( localUserSql + + " WHERE user.userid = :userid" ); + stmt.setInt( "userid", userId ); + ResultSet rs = stmt.executeQuery(); + if ( !rs.next() ) + return null; + return localFromRs( rs ); + } catch ( SQLException e ) { + LOGGER.error( "Query failed in DbUser.forLogin()", e ); + throw e; + } + } + + public static LocalUser forLogin( String login, String password ) throws SQLException + { + LocalUser user = forLogin( login ); + if ( user == null || !Sha512Crypt.verifyPassword( password, user.password ) ) + return null; + return user; + } + + public static List<UserInfo> findUser( String organizationId, String searchTerm ) + { + // TODO Implement + return new ArrayList<>( 0 ); + } + +} diff --git a/src/main/java/org/openslx/imagemaster/db/models/LocalOrganization.java b/src/main/java/org/openslx/imagemaster/db/models/LocalOrganization.java new file mode 100644 index 0000000..4b5f076 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/db/models/LocalOrganization.java @@ -0,0 +1,62 @@ +package org.openslx.imagemaster.db.models; + +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; + +import org.apache.log4j.Logger; +import org.openslx.encryption.AsymKeyHolder; + +public class LocalOrganization +{ + + private static final Logger LOGGER = Logger.getLogger( LocalOrganization.class ); + + private PublicKey publickey; + + private String publickeyString; + + private String name; + + /** + * Get the public key of this organization, if known and valid. + * + * @return Public key, null on error or not known + */ + public PublicKey getPubkey() + { + if ( publickey == null && publickeyString != null ) { + String parts[] = publickeyString.split( " " ); + BigInteger mod = null; + BigInteger exp = null; + for ( int i = 0; i < parts.length; ++i ) { + if ( parts[i].startsWith( "mod:" ) ) { + // modulus found. + mod = new BigInteger( parts[i].substring( 4 ) ); + } + if ( parts[i].startsWith( "exp:" ) ) { + // exponent found. + exp = new BigInteger( parts[i].substring( 4 ) ); + } + } + if ( mod == null ) { + LOGGER.error( "No modulus for building public key was found." ); + return null; + } + if ( exp == null ) { + LOGGER.error( "No public exponent for building public key was found." ); + return null; + } + try { + publickey = new AsymKeyHolder( null, exp, mod ).getPublicKey(); + } catch ( InvalidKeySpecException | NoSuchAlgorithmException e ) { + LOGGER.info( "PubKey of " + this.name + " is not valid.", e ); + } catch ( NumberFormatException e ) { + LOGGER.info( "PubKey of " + this.name + " is corrupted in database!", e ); + } + } + return publickey; + } + +} diff --git a/src/main/java/org/openslx/imagemaster/session/User.java b/src/main/java/org/openslx/imagemaster/db/models/LocalUser.java index a64ab5b..4e0d4fb 100644 --- a/src/main/java/org/openslx/imagemaster/session/User.java +++ b/src/main/java/org/openslx/imagemaster/db/models/LocalUser.java @@ -1,30 +1,33 @@ -package org.openslx.imagemaster.session; +package org.openslx.imagemaster.db.models; + +import org.openslx.bwlp.thrift.iface.Role; +import org.openslx.bwlp.thrift.iface.UserInfo; /** * Represents a user. Should be extended and given an according static method to * instantiate by loading data from some backend. * */ -public abstract class User +public class LocalUser { public final String login, organizationId; public final String password; public final String firstName, lastName; public final String eMail; - public final String satelliteAddress; + public final Role role; public final int userId; - protected User( int userId, String login, String password, String organization, String firstName, String lastName, String eMail, - String satelliteAddress ) + public LocalUser( int userId, String login, String password, String organizationId, String firstName, String lastName, String eMail, + Role tutor ) { this.userId = userId; this.login = login; - this.organizationId = organization; + this.organizationId = organizationId; this.password = password; this.firstName = firstName; this.lastName = lastName; this.eMail = eMail; - this.satelliteAddress = satelliteAddress; + this.role = tutor; } @Override @@ -39,13 +42,16 @@ public abstract class User sb.append( this.lastName ); sb.append( ' ' ); sb.append( this.eMail ); - if ( this.satelliteAddress != null ) { - sb.append( ' ' ); - sb.append( this.satelliteAddress ); - } return sb.toString(); } + public UserInfo toUserInfo() + { + UserInfo ui = new UserInfo( login, firstName, lastName, eMail, organizationId ); + ui.role = role; + return ui; + } + public String getLogin() { return login; diff --git a/src/main/java/org/openslx/imagemaster/localrpc/JsonUser.java b/src/main/java/org/openslx/imagemaster/localrpc/JsonUser.java new file mode 100644 index 0000000..8f13084 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/localrpc/JsonUser.java @@ -0,0 +1,23 @@ +package org.openslx.imagemaster.localrpc; + +import org.openslx.imagemaster.session.User; + +public class JsonUser +{ + + private String login = null; + private String organizationid = null; + private String firstName = null; + private String lastName = null; + private String mail = null; + private String role = null; + private int userId = -1; + + public User toUser() + { + if ( userId <= 0 || firstName == null || lastName == null || firstName.isEmpty() || lastName.isEmpty() ) + return null; + return new ShibUser( userId, login, organizationid, firstName, lastName, mail, role ); + } + +} diff --git a/src/main/java/org/openslx/imagemaster/localrpc/NetworkHandler.java b/src/main/java/org/openslx/imagemaster/localrpc/NetworkHandler.java new file mode 100644 index 0000000..96b1212 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/localrpc/NetworkHandler.java @@ -0,0 +1,188 @@ +package org.openslx.imagemaster.localrpc; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketAddress; +import java.net.SocketException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import org.apache.log4j.Logger; +import org.openslx.bwlp.thrift.iface.ClientSessionData; +import org.openslx.imagemaster.session.Session; +import org.openslx.imagemaster.session.SessionManager; +import org.openslx.imagemaster.session.User; + +import com.google.gson.Gson; + +/** + * The network listener that will receive incoming UDP packets, try to process + * them, and then send a reply. + */ +public class NetworkHandler implements Runnable +{ + + private static final Logger log = Logger.getLogger( NetworkHandler.class ); + + private Thread sendThread = null; + /** + * Sender instance (Runnable handling outgoing packets) + */ + private final Sender sender; + /** + * UDP socket for sending and receiving. + */ + private final DatagramSocket socket; + /** + * Gson class + */ + private final Gson gson = new Gson(); + + /** + * Initialize the NetworkHandler by starting threads and opening the socket. + */ + public NetworkHandler( int port, InetAddress listenAddress ) throws SocketException + { + socket = new DatagramSocket( port, listenAddress ); + sendThread = new Thread( sender = new Sender() ); + } + + public void shutdown() + { + socket.close(); + } + + /** + * Prepare and enqueue reply for client request. + * Only ever to be called from the receiving thread. The reply message is crafted + * and then handed over to the sending thread. + * + * @param destination SocketAddress of the client + * @param messageId The same ID the client used in it's request. + * It's echoed back to the client to enable request bursts, and has no meaning for the + * server. + * @param status A TaskStatus instance to be serialized to json and sent to the client. + */ + private void send( SocketAddress destination, byte[] buffer ) + { + final DatagramPacket packet; + try { + packet = new DatagramPacket( buffer, buffer.length, destination ); + } catch ( SocketException e ) { + log.warn( "Could not construct datagram packet for target " + destination.toString() ); + e.printStackTrace(); + return; + } + sender.send( packet ); + } + + /** + * Main loop of receiving thread - wait until a packet arrives, then try to handle/decode + */ + @Override + public void run() + { + byte readBuffer[] = new byte[ 66000 ]; + try { + sendThread.start(); + while ( !Thread.interrupted() ) { + DatagramPacket packet = new DatagramPacket( readBuffer, readBuffer.length ); + try { + socket.receive( packet ); + } catch ( IOException e ) { + log.info( "IOException on UDP socket when reading: " + e.getMessage() ); + Thread.sleep( 100 ); + continue; + } + if ( packet.getLength() < 2 ) { + log.debug( "Message too short" ); + continue; + } + String payload = new String( readBuffer, 0, packet.getLength(), StandardCharsets.UTF_8 ); + try { + String reply = handle( payload ); + if ( reply != null ) + send( packet.getSocketAddress(), reply.getBytes( StandardCharsets.UTF_8 ) ); + } catch ( Throwable t ) { + log.error( "Exception in RequestParser: " + t.toString() ); + log.error( "Payload was: " + payload ); + t.printStackTrace(); + } + } + } catch ( InterruptedException e ) { + Thread.currentThread().interrupt(); + } finally { + sendThread.interrupt(); + log.info( "UDP receiver finished." ); + } + } + + private String handle( String payload ) + { + try { + JsonUser ju = gson.fromJson( payload, JsonUser.class ); + User u = ju.toUser(); + if ( u == null ) { + log.warn( "Invalid or inomplete RPC data (" + payload + ")" ); + return "Invalid or incomplete RPC data"; + } + ClientSessionData sd = SessionManager.addSession( new Session( u ) ); + return "TOKEN:" + sd.authToken + " SESSIONID:" + sd.sessionId; + } catch ( Throwable t ) { + log.error( "Exception on json decode", t ); + } + return "Json error"; + } + + /** + * Private sending thread. + * Use blocking queue, wait for packet to be added to it, then try to send. + */ + private class Sender implements Runnable + { + + /** + * Queue to stuff outgoing packets into. + */ + private final BlockingQueue<DatagramPacket> queue = new LinkedBlockingQueue<>( 128 ); + + /** + * Wait until something is put into the queue, then send it. + */ + @Override + public void run() + { + try { + while ( !Thread.interrupted() ) { + final DatagramPacket packet; + packet = queue.take(); + try { + socket.send( packet ); + } catch ( IOException e ) { + log.debug( "Could not send UDP packet to " + packet.getAddress().getHostAddress().toString() ); + } + } + } catch ( InterruptedException e ) { + Thread.currentThread().interrupt(); + } finally { + log.info( "UDP sender finished." ); + } + } + + /** + * Add something to the outgoing packet queue. + * Called from the receiving thread. + */ + public void send( DatagramPacket packet ) + { + if ( queue.offer( packet ) ) + return; + log.warn( "Could not add packet to queue: Full" ); + } + + } + +} diff --git a/src/main/java/org/openslx/imagemaster/localrpc/ShibUser.java b/src/main/java/org/openslx/imagemaster/localrpc/ShibUser.java new file mode 100644 index 0000000..4cba3a4 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/localrpc/ShibUser.java @@ -0,0 +1,13 @@ +package org.openslx.imagemaster.localrpc; + +import org.openslx.imagemaster.session.User; + +public class ShibUser extends User +{ + + protected ShibUser( int userId, String login, String organization, String firstName, String lastName, String eMail, String role ) + { + super( userId, login, "", organization, firstName, lastName, eMail, "", role ); + } + +} diff --git a/src/main/java/org/openslx/imagemaster/thrift/server/TBinaryProtocolSafe.java b/src/main/java/org/openslx/imagemaster/thrift/server/TBinaryProtocolSafe.java deleted file mode 100644 index 08654d6..0000000 --- a/src/main/java/org/openslx/imagemaster/thrift/server/TBinaryProtocolSafe.java +++ /dev/null @@ -1,122 +0,0 @@ -package org.openslx.imagemaster.thrift.server; - -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; - -import org.apache.thrift.TException; -import org.apache.thrift.protocol.TBinaryProtocol; -import org.apache.thrift.protocol.TMessage; -import org.apache.thrift.protocol.TProtocol; -import org.apache.thrift.protocol.TProtocolException; -import org.apache.thrift.protocol.TProtocolFactory; -import org.apache.thrift.transport.TTransport; - -/** - * Binary protocol implementation for thrift. - * Will not read messages bigger than 12MiB. - * - */ -public class TBinaryProtocolSafe extends TBinaryProtocol -{ - /** - * Factory - */ - @SuppressWarnings( "serial" ) - public static class Factory implements TProtocolFactory - { - - protected boolean strictRead_ = false; - protected boolean strictWrite_ = true; - - public Factory() - { - this( false, true ); - } - - public Factory(boolean strictRead, boolean strictWrite) - { - strictRead_ = strictRead; - strictWrite_ = strictWrite; - } - - public TProtocol getProtocol( TTransport trans ) - { - return new TBinaryProtocolSafe( trans, strictRead_, strictWrite_ ); - } - } - - private static final int maxLen = 12 * 1024 * 1024; // 12 MiB - - /** - * Constructor - */ - public TBinaryProtocolSafe(TTransport trans) - { - this( trans, false, true ); - } - - public TBinaryProtocolSafe(TTransport trans, boolean strictRead, boolean strictWrite) - { - super( trans ); - strictRead_ = strictRead; - strictWrite_ = strictWrite; - } - - /** - * Reading methods. - */ - - public TMessage readMessageBegin() throws TException - { - int size = readI32(); - if ( size > maxLen ) - throw new TProtocolException( TProtocolException.SIZE_LIMIT, "Payload too big." ); - if ( size < 0 ) { - int version = size & VERSION_MASK; - if ( version != VERSION_1 ) { - throw new TProtocolException( TProtocolException.BAD_VERSION, "Bad version in readMessageBegin" ); - } - return new TMessage( readString(), (byte) ( size & 0x000000ff ), readI32() ); - } else { - if ( strictRead_ ) { - throw new TProtocolException( TProtocolException.BAD_VERSION, "Missing version in readMessageBegin, old client?" ); - } - return new TMessage( readStringBody( size ), readByte(), readI32() ); - } - } - - public String readString() throws TException - { - int size = readI32(); - if ( size > maxLen ) - throw new TProtocolException( TProtocolException.SIZE_LIMIT, "Payload too big." ); - if ( trans_.getBytesRemainingInBuffer() >= size ) { - try { - String s = new String( trans_.getBuffer(), trans_.getBufferPosition(), size, "UTF-8" ); - trans_.consumeBuffer( size ); - return s; - } catch ( UnsupportedEncodingException e ) { - throw new TException( "JVM DOES NOT SUPPORT UTF-8" ); - } - } - - return readStringBody( size ); - } - - public ByteBuffer readBinary() throws TException - { - int size = readI32(); - if ( size > maxLen ) - throw new TProtocolException( TProtocolException.SIZE_LIMIT, "Payload too big." ); - if ( trans_.getBytesRemainingInBuffer() >= size ) { - ByteBuffer bb = ByteBuffer.wrap( trans_.getBuffer(), trans_.getBufferPosition(), size ); - trans_.consumeBuffer( size ); - return bb; - } - - byte[] buf = new byte[ size ]; - trans_.readAll( buf, 0, size ); - return ByteBuffer.wrap( buf ); - } - -} |