diff options
21 files changed, 1464 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6fcdeb --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/testing +/.settings +/.project +/.classpath +/config/mysql.properties +/target +/src/main/java/org/openslx/imagemaster/thrift/iface +/gen-java + @@ -0,0 +1,24 @@ +1. Import project in eclipse (requires m2e) +Import -> Maven -> Existing Maven Project +Eclipse will complain about missing source files/classes. +They need to be generated... + +2. Install the thrift compiler +Prequisites: +apt-get install libboost-dev libboost-test-dev libboost-program-options-dev libevent-dev automake libtool flex bison pkg-config g++ libssl-dev +Thrift 0.9.1 (current as of writing): +http://www.apache.org/dyn/closer.cgi?path=/thrift/0.9.1/thrift-0.9.1.tar.gz + +./configure, make, make install + +3. Run ./thrift-compile.sh, it will generate the missing +files mentioned before. Refresh the project in Eclipse. + +"Run as -> Maven install..." should work now and create +a nice *.jar in ./target/ + +4. Create config/mysql.properties + + +## TODO: Dump db schema + diff --git a/config/mysql.properties.example b/config/mysql.properties.example new file mode 100644 index 0000000..a434dff --- /dev/null +++ b/config/mysql.properties.example @@ -0,0 +1,6 @@ +# Fill in credentials and rename to mysql.properties +host=localhost +db=masterserver +user=masterserver +password=geheim + @@ -0,0 +1,90 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>org.openslx.imagemaster</groupId> + <artifactId>image-master</artifactId> + <version>1.0-SNAPSHOT</version> + <packaging>jar</packaging> + + <name>image-master</name> + <url>http://maven.apache.org</url> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.1</version> + <configuration> + <source>1.7</source> + <target>1.7</target> + </configuration> + </plugin> + <plugin> + <artifactId>maven-assembly-plugin</artifactId> + <executions> + <execution> + <phase>package</phase> + <goals> + <goal>single</goal> + </goals> + </execution> + </executions> + <configuration> + <archive> + <manifest> + <mainClass>org.openslx.imagemaster.App</mainClass> + </manifest> + </archive> + <descriptorRefs> + <descriptorRef>jar-with-dependencies</descriptorRef> + </descriptorRefs> + </configuration> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>3.8.1</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>log4j</groupId> + <artifactId>log4j</artifactId> + <version>1.2.17</version> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-log4j12</artifactId> + <version>1.7.5</version> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>org.apache.thrift</groupId> + <artifactId>libthrift</artifactId> + <version>0.9.1</version> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>mysql</groupId> + <artifactId>mysql-connector-java</artifactId> + <version>5.1.28</version> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>fi.evident.dalesbred</groupId> + <artifactId>dalesbred</artifactId> + <version>0.6.0</version> + <scope>compile</scope> + </dependency> + </dependencies> +</project> diff --git a/src/main/java/org/openslx/imagemaster/App.java b/src/main/java/org/openslx/imagemaster/App.java new file mode 100644 index 0000000..ef04e54 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/App.java @@ -0,0 +1,46 @@ +package org.openslx.imagemaster; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.log4j.BasicConfigurator; +import org.apache.log4j.Logger; +import org.openslx.imagemaster.thrift.server.BinaryListener; + +/** + * Hello world! + * + */ +public class App +{ + private static Logger log = Logger.getLogger( App.class ); + + private static List<Thread> servers = new ArrayList<>(); + + public static void main( String[] args ) + { + // Init logging + BasicConfigurator.configure(); + log.info( "Starting Application" ); + // Create binary listener + Thread t; + t = new Thread(new BinaryListener(), "BinaryListener"); + servers.add(t); + t.start(); + // Run more servers + // ... + // Wait for all servers to die + for (Thread wait : servers) { + boolean success = false; + while (!success) { + try { + wait.join(); + success = true; + } catch ( InterruptedException e ) { + // Do nothing... + } + } + } + log.info( "All Servers shut down, exiting..." ); + } +} diff --git a/src/main/java/org/openslx/imagemaster/db/DbUser.java b/src/main/java/org/openslx/imagemaster/db/DbUser.java new file mode 100644 index 0000000..f8400a9 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/db/DbUser.java @@ -0,0 +1,30 @@ +package org.openslx.imagemaster.db; + +import org.openslx.imagemaster.session.User; + + +public class DbUser extends User +{ + public DbUser(String username, String password, String organization, String firstName, String lastName, String eMail, + String satelliteAddress) + { + super( username, password, organization, firstName, lastName, eMail, satelliteAddress ); + } + + /** + * Query database for user with given login + * @param login (user@organization) + * @return instance of DbUser for matching entry from DB, or null if not found + */ + public static DbUser forLogin( final String login ) + { + final String[] parts = login.split( "@" ); + if ( parts.length != 2 ) + return null; + return MySQL.findUniqueOrNull( DbUser.class, + "SELECT user.username, user.password, user.organization, user.firstname, user.lastname, user.email, satellite.address FROM user" + + " LEFT JOIN satellite USING (organization)" + + " WHERE user.username = ? AND user.organization = ? LIMIT 1", parts[0], parts[1] ); + } + +} diff --git a/src/main/java/org/openslx/imagemaster/db/MySQL.java b/src/main/java/org/openslx/imagemaster/db/MySQL.java new file mode 100644 index 0000000..15bf5e2 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/db/MySQL.java @@ -0,0 +1,80 @@ +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 + { + // 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 ); + db = Database.forDataSource( ds ); + } catch ( Exception e ) { + log.fatal( "Error initializing mysql data source!" ); + e.printStackTrace(); + System.exit( 1 ); + } + } + + protected static <T> List<T> findAll( final Class<T> clazz, final String sql, final Object... args ) + { + return db.findAll( clazz, sql, args ); + } + + protected static <T> T findUniqueOrNull( final Class<T> clazz, final String sql, final Object... args ) + { + return db.findUniqueOrNull( clazz, sql, args ); + } + +} diff --git a/src/main/java/org/openslx/imagemaster/server/ApiServer.java b/src/main/java/org/openslx/imagemaster/server/ApiServer.java new file mode 100644 index 0000000..e62b61b --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/server/ApiServer.java @@ -0,0 +1,65 @@ +package org.openslx.imagemaster.server; + +import org.apache.log4j.Logger; +import org.openslx.imagemaster.session.Authenticator; +import org.openslx.imagemaster.session.Session; +import org.openslx.imagemaster.session.SessionManager; +import org.openslx.imagemaster.session.User; +import org.openslx.imagemaster.thrift.iface.AuthenticationError; +import org.openslx.imagemaster.thrift.iface.AuthenticationException; +import org.openslx.imagemaster.thrift.iface.InvalidTokenException; +import org.openslx.imagemaster.thrift.iface.SessionData; +import org.openslx.imagemaster.thrift.iface.UserInfo; + +/** + * API Server This is where all the requests from the outside arrive. We don't + * handle them directly in the Thrift handlers, as we might be adding other APIs + * later, like JSON/SOAP/REST/HTTP/XML or some other stuff. They'd all just + * interface with this static class here. Note that we use the exceptions from + * the thrift interface that you can simply catch in any other API handler and + * eg. transform into error codes, if the API doesn't support exceptions. + * + * This will be accessed from multiple threads, so use synchronization when + * needed (or in doubt) + */ +public class ApiServer +{ + @SuppressWarnings( "unused" ) + private static Logger log = Logger.getLogger( ApiServer.class ); + // + + /** + * Request for authentication + * @param login (username@organization) + * @param password + * @return SessionData struct with session id/token iff login successful + * @throws AuthenticationException if login not successful + */ + public static SessionData authenticate( String login, String password ) + throws AuthenticationException + { + if ( login == null || password == null ) { + throw new AuthenticationException( AuthenticationError.INVALID_CREDENTIALS, "Empty username or password!" ); + } + final User user = Authenticator.authenticate( login, password ); + + final Session session = new Session( user ); + return SessionManager.addSession( session ); + } + + /** + * Request information about user for given token + * @param token - a user's token + * @return UserInfo struct for given token's user + * @throws InvalidTokenException if no user matches the given token + */ + public static UserInfo getUserFromToken( String token ) + throws InvalidTokenException + { + final Session session = SessionManager.getSession( token ); + if ( session == null ) + throw new InvalidTokenException(); + return new UserInfo( session.getUserId(), session.getFirstName(), session.getLastName(), session.getEMail() ); + } + +} diff --git a/src/main/java/org/openslx/imagemaster/session/Authenticator.java b/src/main/java/org/openslx/imagemaster/session/Authenticator.java new file mode 100644 index 0000000..f730c72 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/session/Authenticator.java @@ -0,0 +1,31 @@ +package org.openslx.imagemaster.session; + +import org.apache.log4j.Logger; +import org.openslx.imagemaster.db.DbUser; +import org.openslx.imagemaster.thrift.iface.AuthenticationError; +import org.openslx.imagemaster.thrift.iface.AuthenticationException; +import org.openslx.imagemaster.util.Sha512Crypt; + +public class Authenticator +{ + private static Logger log = Logger.getLogger( Authenticator.class ); + + /** + * Authenticate the user against whatever backend... currently MySQL only + * @param username + * @param password + * @return + * @throws AuthenticationException + */ + public static User authenticate( String username, String password ) throws AuthenticationException + { + DbUser user = DbUser.forLogin( username ); + if ( user == null || !Sha512Crypt.verifyPassword( password, user.password ) ) { + log.debug( "Login failed: " + username ); + throw new AuthenticationException( AuthenticationError.INVALID_CREDENTIALS, "Invalid username or password!" ); + } + log.debug( "Login successful: " + username ); + return user; + } + // +} diff --git a/src/main/java/org/openslx/imagemaster/session/Session.java b/src/main/java/org/openslx/imagemaster/session/Session.java new file mode 100644 index 0000000..8dc7f2b --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/session/Session.java @@ -0,0 +1,58 @@ +package org.openslx.imagemaster.session; + +/** + * Simple representation of a user session. Contains user-related data and + * information on whether the session is still valid. + * + */ +public class Session +{ + private static final long TIMEOUT = 600L * 1000L; // TODO: config + + private long timeOut = 0; + private final User user; + + public Session(final User dbuser) + { + this.user = dbuser; + this.timeOut = System.currentTimeMillis() + TIMEOUT; + } + + public synchronized void refresh() + { + if ( timedOut() ) + return; // Don't allow refreshing timed out session + this.timeOut = System.currentTimeMillis() + TIMEOUT; + } + + public synchronized boolean timedOut() + { + return System.currentTimeMillis() > this.timeOut; + } + + public String getSatelliteAddress() + { + return user.satelliteAddress; + } + + public String getUserId() + { + return user.username + "@" + user.organization; + } + + public String getFirstName() + { + return user.firstName; + } + + public String getLastName() + { + return user.lastName; + } + + public String getEMail() + { + return user.eMail; + } + +} diff --git a/src/main/java/org/openslx/imagemaster/session/SessionManager.java b/src/main/java/org/openslx/imagemaster/session/SessionManager.java new file mode 100644 index 0000000..cc68d0b --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/session/SessionManager.java @@ -0,0 +1,73 @@ +package org.openslx.imagemaster.session; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +import org.apache.log4j.Logger; +import org.openslx.imagemaster.thrift.iface.SessionData; +import org.openslx.imagemaster.util.Hash; + +/** + * Class for managing active user sessions. This class and all its function are + * (supposed to be) thread-safe. + */ +public class SessionManager +{ + private static Logger log = Logger.getLogger( SessionManager.class ); + + // Map of currently known sessions + private static final Map<String, Session> sessions = new LinkedHashMap<>(); + private static final Thread gcThread; + + public static SessionData addSession( Session session ) + { + final String authToken = Hash.md5( UUID.randomUUID().toString() ); + final String sessionId = Hash.sha256( UUID.randomUUID().toString() ); + + synchronized ( sessions ) { + sessions.put( authToken, session ); + } + return new SessionData( sessionId, authToken, session.getSatelliteAddress() ); + } + + public static Session getSession( String token ) + { + final Session session; + synchronized ( sessions ) { + session = sessions.get( token ); + } + if ( session == null || session.timedOut() ) { + return null; + } + return session; + } + + static { + gcThread = new Thread( new Runnable() { + @Override + public void run() + { + for ( ;; ) { + try { + Thread.sleep( 1800L * 1000L ); + } catch ( InterruptedException e ) { + } + synchronized ( sessions ) { + Iterator<Session> it = sessions.values().iterator(); + while ( it.hasNext() ) { + final Session s = it.next(); + if ( s.timedOut() ) { + log.debug( "Removing old session of " + s.getUserId() ); + it.remove(); + } + } + } + } + } + } ); + gcThread.start(); + } + +} diff --git a/src/main/java/org/openslx/imagemaster/session/User.java b/src/main/java/org/openslx/imagemaster/session/User.java new file mode 100644 index 0000000..52c8c78 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/session/User.java @@ -0,0 +1,48 @@ +package org.openslx.imagemaster.session; + +/** + * 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 final String username, organization; + public final String password; + public final String firstName, lastName; + public final String eMail; + public final String satelliteAddress; + + protected User(String username, String password, String organization, String firstName, String lastName, String eMail, + String satelliteAddress) + { + this.username = username; + this.organization = organization; + this.password = password; + this.firstName = firstName; + this.lastName = lastName; + this.eMail = eMail; + this.satelliteAddress = satelliteAddress; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder( this.username ); + sb.append( "@" ); + sb.append( this.organization ); + sb.append( ": " ); + sb.append( this.firstName ); + sb.append( ' ' ); + sb.append( this.lastName ); + sb.append( ' ' ); + sb.append( this.eMail ); + if ( this.satelliteAddress != null ) { + sb.append( ' ' ); + sb.append( this.satelliteAddress ); + } + return sb.toString(); + } + +} diff --git a/src/main/java/org/openslx/imagemaster/thrift/server/BinaryListener.java b/src/main/java/org/openslx/imagemaster/thrift/server/BinaryListener.java new file mode 100644 index 0000000..8eeb7bc --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/thrift/server/BinaryListener.java @@ -0,0 +1,36 @@ +package org.openslx.imagemaster.thrift.server; + +import org.apache.log4j.Logger; +import org.apache.thrift.protocol.TProtocolFactory; +import org.apache.thrift.server.TServer; +import org.apache.thrift.server.TThreadPoolServer; +import org.apache.thrift.transport.TServerSocket; +import org.apache.thrift.transport.TServerTransport; +import org.apache.thrift.transport.TTransportException; +import org.openslx.imagemaster.thrift.iface.ImageServer; +import org.apache.thrift.server.TThreadPoolServer.Args; + +public class BinaryListener implements Runnable +{ + private static Logger log = Logger.getLogger( BinaryListener.class ); + + @Override + public void run() + { + final ImageServerHandler handler = new ImageServerHandler(); + final ImageServer.Processor<ImageServerHandler> processor = new ImageServer.Processor<ImageServerHandler>( handler ); + final TProtocolFactory protFactory = new TBinaryProtocolSafe.Factory( true, true ); + final TServerTransport transport; + try { + transport = new TServerSocket( 9090 ); + } catch ( TTransportException e ) { + log.fatal( "Could not listen on port 9090" ); + return; + } + TServer server = new TThreadPoolServer( new Args( transport ).protocolFactory( protFactory ).processor( processor ) + .minWorkerThreads( 4 ).maxWorkerThreads( 8 ) ); + log.info( "Starting Binary Thrift" ); + server.serve(); + } + +} diff --git a/src/main/java/org/openslx/imagemaster/thrift/server/ImageServerHandler.java b/src/main/java/org/openslx/imagemaster/thrift/server/ImageServerHandler.java new file mode 100644 index 0000000..6be5d40 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/thrift/server/ImageServerHandler.java @@ -0,0 +1,35 @@ +package org.openslx.imagemaster.thrift.server; + +import org.apache.thrift.TException; +import org.openslx.imagemaster.server.ApiServer; +import org.openslx.imagemaster.thrift.iface.AuthenticationException; +import org.openslx.imagemaster.thrift.iface.ImageServer; +import org.openslx.imagemaster.thrift.iface.InvalidTokenException; +import org.openslx.imagemaster.thrift.iface.SessionData; +import org.openslx.imagemaster.thrift.iface.UserInfo; + +public class ImageServerHandler implements ImageServer.Iface +{ + + @Override + public boolean ping() throws TException + { + // TODO: Return false if service unavailable but running + return true; + } + + @Override + public SessionData authenticate( String username, String password ) + throws AuthenticationException + { + return ApiServer.authenticate( username, password ); + } + + @Override + public UserInfo getUserFromToken( String token ) + throws InvalidTokenException + { + return ApiServer.getUserFromToken( token ); + } + +} diff --git a/src/main/java/org/openslx/imagemaster/thrift/server/TBinaryProtocolSafe.java b/src/main/java/org/openslx/imagemaster/thrift/server/TBinaryProtocolSafe.java new file mode 100644 index 0000000..614be22 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/thrift/server/TBinaryProtocolSafe.java @@ -0,0 +1,121 @@ +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 + */ + 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 ); + } + +} diff --git a/src/main/java/org/openslx/imagemaster/util/Hash.java b/src/main/java/org/openslx/imagemaster/util/Hash.java new file mode 100644 index 0000000..8ac0e5f --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/util/Hash.java @@ -0,0 +1,100 @@ +package org.openslx.imagemaster.util; + +import java.security.MessageDigest; +import java.nio.charset.Charset; +import java.security.NoSuchAlgorithmException; + +public class Hash +{ + // Cache of md5 digesters + private static final ThreadLocal<MessageDigest> md5hash = new ThreadLocal<MessageDigest>() { + @Override + public MessageDigest initialValue() + { + try { + return MessageDigest.getInstance( "MD5" ); + } catch ( NoSuchAlgorithmException e ) { + e.printStackTrace(); + System.exit(1); + return null; + } + } + }; + // Cache of sha256 digesters + private static final ThreadLocal<MessageDigest> sha256hash = new ThreadLocal<MessageDigest>() { + @Override + public MessageDigest initialValue() + { + try { + return MessageDigest.getInstance( "SHA-256" ); + } catch ( NoSuchAlgorithmException e ) { + e.printStackTrace(); + System.exit(1); + return null; + } + } + }; + // Cache of sha512 digesters + private static final ThreadLocal<MessageDigest> sha512hash = new ThreadLocal<MessageDigest>() { + @Override + public MessageDigest initialValue() + { + try { + return MessageDigest.getInstance( "SHA-512" ); + } catch ( NoSuchAlgorithmException e ) { + e.printStackTrace(); + System.exit(1); + return null; + } + } + }; + // For converting to hex string + private static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + // Constant + private static final Charset UTF8 = Charset.forName( "UTF-8" ); + + // MD5 + + public static String md5( final byte[] bytes ) + { + return toHexString( md5hash.get().digest( bytes ) ); + } + + public static String md5( final String text ) + { + return md5( text.getBytes( UTF8 )); + } + + // SHA-256 + + public static String sha256( final byte[] bytes ) + { + return toHexString( sha256hash.get().digest( bytes ) ); + } + + public static String sha256( final String text ) + { + return sha256( text.getBytes( UTF8 )); + } + + // SHA-512 + + public static MessageDigest getSha512Digest() + { + return sha512hash.get(); + } + + // Helper + + private static String toHexString( final byte[] bytes ) + { + final char[] hexChars = new char[bytes.length * 2]; + for ( int j = 0; j < bytes.length; ++j ) { + final int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_CHARS[v >>> 4]; + hexChars[j * 2 + 1] = HEX_CHARS[v & 0x0F]; + } + return new String( hexChars ); + } + +} diff --git a/src/main/java/org/openslx/imagemaster/util/Sha512Crypt.java b/src/main/java/org/openslx/imagemaster/util/Sha512Crypt.java new file mode 100644 index 0000000..472ead9 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/util/Sha512Crypt.java @@ -0,0 +1,492 @@ +/* + Sha512Crypt.java + + Created: 18 December 2007 + + Java Port By: James Ratcliff, falazar@arlut.utexas.edu + + This class implements the new generation, scalable, SHA512-based + Unix 'crypt' algorithm developed by a group of engineers from Red + Hat, Sun, IBM, and HP for common use in the Unix and Linux + /etc/shadow files. + + The Linux glibc library (starting at version 2.7) includes support + for validating passwords hashed using this algorithm. + + The algorithm itself was released into the Public Domain by Ulrich + Drepper <drepper@redhat.com>. A discussion of the rationale and + development of this algorithm is at + + http://people.redhat.com/drepper/sha-crypt.html + + and the specification and a sample C language implementation is at + + http://people.redhat.com/drepper/SHA-crypt.txt + + This Java Port is + + Copyright (c) 2008-2013 The University of Texas at Austin. + + All rights reserved. + + Redistribution and use in source and binary form are permitted + provided that distributions retain this entire copyright notice + and comment. Neither the name of the University nor the names of + its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE. + +*/ + +package org.openslx.imagemaster.util; + +import java.security.MessageDigest; + +/*------------------------------------------------------------------------------ + class + Sha512Crypt + +------------------------------------------------------------------------------*/ + +/** + * <p>This class defines a method, {@link + * Sha512Crypt#Sha512_crypt(java.lang.String, java.lang.String, int) + * Sha512_crypt()}, which takes a password and a salt string and + * generates a Sha512 encrypted password entry.</p> + * + * <p>This class implements the new generation, scalable, SHA512-based + * Unix 'crypt' algorithm developed by a group of engineers from Red + * Hat, Sun, IBM, and HP for common use in the Unix and Linux + * /etc/shadow files.</p> + * + * <p>The Linux glibc library (starting at version 2.7) includes + * support for validating passwords hashed using this algorithm.</p> + * + * <p>The algorithm itself was released into the Public Domain by + * Ulrich Drepper <drepper@redhat.com>. A discussion of the + * rationale and development of this algorithm is at</p> + * + * <p>http://people.redhat.com/drepper/sha-crypt.html</p> + * + * <p>and the specification and a sample C language implementation is + * at</p> + * + * <p>http://people.redhat.com/drepper/SHA-crypt.txt</p> + */ + +public final class Sha512Crypt +{ + static private final String sha512_salt_prefix = "$6$"; + static private final String sha512_rounds_prefix = "rounds="; + static private final int SALT_LEN_MAX = 16; + static private final int ROUNDS_DEFAULT = 5000; + static private final int ROUNDS_MIN = 1000; + static private final int ROUNDS_MAX = 999999999; + static private final String SALTCHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + static private final String itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + static private MessageDigest getSHA512() + { + try + { + return MessageDigest.getInstance("SHA-512"); + } + catch (java.security.NoSuchAlgorithmException ex) + { + throw new RuntimeException(ex); + } + } + + /** + * <p>This method actually generates an Sha512 crypted password hash + * from a plaintext password and a salt.</p> + * + * <p>The resulting string will be in the form + * '$6$<rounds=n>$<salt>$<hashed mess></p> + * + * @param keyStr Plaintext password + * + * @param saltStr An encoded salt/roundes which will be consulted to determine the salt + * and round count, if not null + * + * @param roundsCount If this value is not 0, this many rounds will + * used to generate the hash text. + * + * @return The Sha512 Unix Crypt hash text for the keyStr + */ + + public static final String Sha512_crypt(String keyStr, String saltStr, int roundsCount) + { + MessageDigest ctx = getSHA512(); + MessageDigest alt_ctx = getSHA512(); + + byte[] alt_result; + byte[] temp_result; + byte[] p_bytes = null; + byte[] s_bytes = null; + int cnt, cnt2; + int rounds = ROUNDS_DEFAULT; // Default number of rounds. + StringBuilder buffer; + boolean include_round_count = false; + + /* -- */ + + if (saltStr != null) + { + if (saltStr.startsWith(sha512_salt_prefix)) + { + saltStr = saltStr.substring(sha512_salt_prefix.length()); + } + + if (saltStr.startsWith(sha512_rounds_prefix)) + { + String num = saltStr.substring(sha512_rounds_prefix.length(), saltStr.indexOf('$')); + int srounds = Integer.valueOf(num).intValue(); + saltStr = saltStr.substring(saltStr.indexOf('$')+1); + rounds = Math.max(ROUNDS_MIN, Math.min(srounds, ROUNDS_MAX)); + include_round_count = true; + } + + if (saltStr.length() > SALT_LEN_MAX) + { + saltStr = saltStr.substring(0, SALT_LEN_MAX); + } + + // gnu libc's crypt(3) implementation allows the salt to end + // in $ which is then ignored. + + if (saltStr.endsWith("$")) + { + saltStr = saltStr.substring(0, saltStr.length() - 1); + } + else + { + if (saltStr.indexOf("$") != -1) + { + saltStr = saltStr.substring(0, saltStr.indexOf("$")); + } + } + } + else + { + java.util.Random randgen = new java.util.Random(); + StringBuilder saltBuf = new StringBuilder(); + + while (saltBuf.length() < 16) + { + int index = (int) (randgen.nextFloat() * SALTCHARS.length()); + saltBuf.append(SALTCHARS.substring(index, index+1)); + } + + saltStr = saltBuf.toString(); + } + + if (roundsCount != 0) + { + rounds = Math.max(ROUNDS_MIN, Math.min(roundsCount, ROUNDS_MAX)); + } + + byte[] key = keyStr.getBytes(); + byte[] salt = saltStr.getBytes(); + + ctx.reset(); + ctx.update(key, 0, key.length); + ctx.update(salt, 0, salt.length); + + alt_ctx.reset(); + alt_ctx.update(key, 0, key.length); + alt_ctx.update(salt, 0, salt.length); + alt_ctx.update(key, 0, key.length); + + alt_result = alt_ctx.digest(); + + for (cnt = key.length; cnt > 64; cnt -= 64) + { + ctx.update(alt_result, 0, 64); + } + + ctx.update(alt_result, 0, cnt); + + for (cnt = key.length; cnt > 0; cnt >>= 1) + { + if ((cnt & 1) != 0) + { + ctx.update(alt_result, 0, 64); + } + else + { + ctx.update(key, 0, key.length); + } + } + + alt_result = ctx.digest(); + + alt_ctx.reset(); + + for (cnt = 0; cnt < key.length; ++cnt) + { + alt_ctx.update(key, 0, key.length); + } + + temp_result = alt_ctx.digest(); + + p_bytes = new byte[key.length]; + + for (cnt2 = 0, cnt = p_bytes.length; cnt >= 64; cnt -= 64) + { + System.arraycopy(temp_result, 0, p_bytes, cnt2, 64); + cnt2 += 64; + } + + System.arraycopy(temp_result, 0, p_bytes, cnt2, cnt); + + alt_ctx.reset(); + + for (cnt = 0; cnt < 16 + (alt_result[0]&0xFF); ++cnt) + { + alt_ctx.update(salt, 0, salt.length); + } + + temp_result = alt_ctx.digest(); + + s_bytes = new byte[salt.length]; + + for (cnt2 = 0, cnt = s_bytes.length; cnt >= 64; cnt -= 64) + { + System.arraycopy(temp_result, 0, s_bytes, cnt2, 64); + cnt2 += 64; + } + + System.arraycopy(temp_result, 0, s_bytes, cnt2, cnt); + + /* Repeatedly run the collected hash value through SHA512 to burn + CPU cycles. */ + + for (cnt = 0; cnt < rounds; ++cnt) + { + ctx.reset(); + + if ((cnt & 1) != 0) + { + ctx.update(p_bytes, 0, key.length); + } + else + { + ctx.update (alt_result, 0, 64); + } + + if (cnt % 3 != 0) + { + ctx.update(s_bytes, 0, salt.length); + } + + if (cnt % 7 != 0) + { + ctx.update(p_bytes, 0, key.length); + } + + if ((cnt & 1) != 0) + { + ctx.update(alt_result, 0, 64); + } + else + { + ctx.update(p_bytes, 0, key.length); + } + + alt_result = ctx.digest(); + } + + buffer = new StringBuilder(sha512_salt_prefix); + + if (include_round_count || rounds != ROUNDS_DEFAULT) + { + buffer.append(sha512_rounds_prefix); + buffer.append(rounds); + buffer.append("$"); + } + + buffer.append(saltStr); + buffer.append("$"); + + buffer.append(b64_from_24bit (alt_result[0], alt_result[21], alt_result[42], 4)); + buffer.append(b64_from_24bit (alt_result[22], alt_result[43], alt_result[1], 4)); + buffer.append(b64_from_24bit (alt_result[44], alt_result[2], alt_result[23], 4)); + buffer.append(b64_from_24bit (alt_result[3], alt_result[24], alt_result[45], 4)); + buffer.append(b64_from_24bit (alt_result[25], alt_result[46], alt_result[4], 4)); + buffer.append(b64_from_24bit (alt_result[47], alt_result[5], alt_result[26], 4)); + buffer.append(b64_from_24bit (alt_result[6], alt_result[27], alt_result[48], 4)); + buffer.append(b64_from_24bit (alt_result[28], alt_result[49], alt_result[7], 4)); + buffer.append(b64_from_24bit (alt_result[50], alt_result[8], alt_result[29], 4)); + buffer.append(b64_from_24bit (alt_result[9], alt_result[30], alt_result[51], 4)); + buffer.append(b64_from_24bit (alt_result[31], alt_result[52], alt_result[10], 4)); + buffer.append(b64_from_24bit (alt_result[53], alt_result[11], alt_result[32], 4)); + buffer.append(b64_from_24bit (alt_result[12], alt_result[33], alt_result[54], 4)); + buffer.append(b64_from_24bit (alt_result[34], alt_result[55], alt_result[13], 4)); + buffer.append(b64_from_24bit (alt_result[56], alt_result[14], alt_result[35], 4)); + buffer.append(b64_from_24bit (alt_result[15], alt_result[36], alt_result[57], 4)); + buffer.append(b64_from_24bit (alt_result[37], alt_result[58], alt_result[16], 4)); + buffer.append(b64_from_24bit (alt_result[59], alt_result[17], alt_result[38], 4)); + buffer.append(b64_from_24bit (alt_result[18], alt_result[39], alt_result[60], 4)); + buffer.append(b64_from_24bit (alt_result[40], alt_result[61], alt_result[19], 4)); + buffer.append(b64_from_24bit (alt_result[62], alt_result[20], alt_result[41], 4)); + buffer.append(b64_from_24bit ((byte)0x00, (byte)0x00, alt_result[63], 2)); + + /* Clear the buffer for the intermediate result so that people + attaching to processes or reading core dumps cannot get any + information. */ + + ctx.reset(); + + return buffer.toString(); + } + + private static final String b64_from_24bit(byte B2, byte B1, byte B0, int size) + { + int v = ((((int) B2) & 0xFF) << 16) | ((((int) B1) & 0xFF) << 8) | ((int)B0 & 0xff); + + StringBuilder result = new StringBuilder(); + + while (--size >= 0) + { + result.append(itoa64.charAt((int) (v & 0x3f))); + v >>>= 6; + } + + return result.toString(); + } + + /** + * <p>This method tests a plaintext password against a SHA512 Unix + * Crypt'ed hash and returns true if the password matches the + * hash.</p> + * + * @param plaintextPass The plaintext password text to test. + * @param sha512CryptText The hash text we're testing against. + * We'll extract the salt and the round count from this String. + */ + + static public final boolean verifyPassword(String plaintextPass, String sha512CryptText) + { + if (sha512CryptText.startsWith("$6$")) + { + return sha512CryptText.equals(Sha512_crypt(plaintextPass, sha512CryptText, 0)); + } + else + { + throw new RuntimeException("Bad sha512CryptText"); + } + } + + /** + * <p>Returns true if sha512CryptText is a valid Sha512Crypt hashtext, + * false if not.</p> + */ + + public static final boolean verifyHashTextFormat(String sha512CryptText) + { + if (!sha512CryptText.startsWith(sha512_salt_prefix)) + { + return false; + } + + sha512CryptText = sha512CryptText.substring(sha512_salt_prefix.length()); + + if (sha512CryptText.startsWith(sha512_rounds_prefix)) + { + String num = sha512CryptText.substring(sha512_rounds_prefix.length(), sha512CryptText.indexOf('$')); + + try + { + int srounds = Integer.valueOf(num).intValue(); + } + catch (NumberFormatException ex) + { + return false; + } + + sha512CryptText = sha512CryptText.substring(sha512CryptText.indexOf('$')+1); + } + + if (sha512CryptText.indexOf('$') > (SALT_LEN_MAX + 1)) + { + return false; + } + + sha512CryptText = sha512CryptText.substring(sha512CryptText.indexOf('$') + 1); + + for (int i = 0; i < sha512CryptText.length(); i++) + { + if (itoa64.indexOf(sha512CryptText.charAt(i)) == -1) + { + return false; + } + } + + return true; + } + + /** + * <p>Validate our implementation using test data from Ulrich + * Drepper's C implementation.</p> + */ + + private static void selfTest() + { + String msgs[] = + { + "$6$saltstring", "Hello world!", "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1", + "$6$xxxxxxxx", "geheim", "$6$xxxxxxxx$wuSdyeOvQXjj/nNoWnjjo.6OxUWrQFRIj019kh1cDpun6l6cpr3ywSrBprYRYZXcm4Kv9lboCEFI3GzBkdNAz/", + "$6$rounds=10000$saltstringsaltstring", "Hello world!", "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sbHbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v.", + "$6$rounds=5000$toolongsaltstring", "This is just a test", "$6$rounds=5000$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQzQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0", + "$6$rounds=1400$anotherlongsaltstring", "a very much longer text to encrypt. This one even stretches over morethan one line.", "$6$rounds=1400$anotherlongsalts$POfYwTEok97VWcjxIiSOjiykti.o/pQs.wPvMxQ6Fm7I6IoYN3CmLs66x9t0oSwbtEW7o7UmJEiDwGqd8p4ur1", + "$6$rounds=77777$short", "we have a short salt string but not a short password", "$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0gge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0", + "$6$rounds=123456$asaltof16chars..", "a short string", "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwcelCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1", + "$6$rounds=10$roundstoolow", "the minimum number is still observed", "$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1xhLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX.", + }; + + System.out.println("Starting Sha512Crypt tests now..."); + + for (int t=0; t<(msgs.length/3); t++) + { + String saltPrefix = msgs[t*3]; + String plainText = msgs[t*3+1]; + String cryptText = msgs[t*3+2]; + + String result = Sha512_crypt(plainText, cryptText, 0); + + System.out.println("test " + t + " result is:" + result); + System.out.println("test " + t + " should be:" + cryptText); + + if (result.equals(cryptText)) + { + System.out.println("Passed Crypt well"); + } + else + { + System.out.println("Failed Crypt Badly"); + } + + if (verifyPassword(plainText, cryptText)) + { + System.out.println("Passed verifyPassword well"); + } + else + { + System.out.println("Failed verifyPassword Badly"); + } + } + } + + /** + * Test rig + */ + + public static void main(String arg[]) + { + selfTest(); + } +} diff --git a/src/main/java/org/openslx/imagemaster/util/Util.java b/src/main/java/org/openslx/imagemaster/util/Util.java new file mode 100644 index 0000000..ff0b8c1 --- /dev/null +++ b/src/main/java/org/openslx/imagemaster/util/Util.java @@ -0,0 +1,16 @@ +package org.openslx.imagemaster.util; + +public class Util +{ + + public static void notNullFatal( Object something, String message ) + { + if ( something == null ) { + if ( message != null ) + System.out.println( "[NOTNULL] " + message ); + System.out.println( Thread.currentThread().getStackTrace().toString() ); + System.exit( 2 ); + } + } + +} diff --git a/src/main/thrift/imagemaster.thrift b/src/main/thrift/imagemaster.thrift new file mode 100644 index 0000000..7c59079 --- /dev/null +++ b/src/main/thrift/imagemaster.thrift @@ -0,0 +1,59 @@ +/** + * Define some namespace/package name for our stuff + */ + +namespace java org.openslx.imagemaster.thrift.iface +namespace php testing + +typedef string ID +typedef string Token + +enum AuthorizationError { + GENERIC_ERROR, + NOT_AUTHENTICATED, + NO_PERMISSION +} + +enum AuthenticationError { + GENERIC_ERROR, + INVALID_CREDENTIALS, + ACCOUNT_SUSPENDED, + BANNED_NETWORK +} + +exception AuthorizationException { + 1: AuthorizationError number, + 2: string message +} + +exception AuthenticationException { + 1: AuthenticationError number, + 2: string message +} + +exception InvalidTokenException { +} + +struct UserInfo { + 1: string userId, + 2: string firstName, + 3: string lastName, + 4: string eMail +} + +struct SessionData { + 1: ID sessionId, + 2: Token authToken, + 3: string serverAddress +} + +service ImageServer { + + bool ping(), + + SessionData authenticate(1:string username, 2:string password) throws (1:AuthenticationException failure), + + UserInfo getUserFromToken(1:Token token) throws (1:InvalidTokenException failure) + +} + diff --git a/src/test/java/org/openslx/imagemaster/AppTest.java b/src/test/java/org/openslx/imagemaster/AppTest.java new file mode 100644 index 0000000..38cff57 --- /dev/null +++ b/src/test/java/org/openslx/imagemaster/AppTest.java @@ -0,0 +1,38 @@ +package org.openslx.imagemaster; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Unit test for simple App. + */ +public class AppTest + extends TestCase +{ + /** + * Create the test case + * + * @param testName name of the test case + */ + public AppTest( String testName ) + { + super( testName ); + } + + /** + * @return the suite of tests being tested + */ + public static Test suite() + { + return new TestSuite( AppTest.class ); + } + + /** + * Rigourous Test :-) + */ + public void testApp() + { + assertTrue( true ); + } +} diff --git a/thrift-compile.sh b/thrift-compile.sh new file mode 100755 index 0000000..0a1099b --- /dev/null +++ b/thrift-compile.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +rm -r gen-java +thrift --gen java src/main/thrift/imagemaster.thrift && \ + rm -r src/main/java/org/openslx/imagemaster/thrift/iface && \ + cp -r gen-java/org src/main/java/ + |