Thursday, June 23, 2011

Foray into Photon - Part 16 - Gearing up for NHibernate

So today we are going to dive into NHibernate. NHibernate is the .NET implementation of Hibernate which we used in our SmartFoxServer implementation. Java had annotations which made it easier to work with for entities. We don't have the same luxury in .NET, but we do have an add-on called FluentNHibernate which extends NHibernate to allow us to easily define mappings without making .hbn.xml files for ever class. So first you will want to go to NHibernate Forge and pull down NH3.1.0. Then you will want to go to Fluent NHibernate and download version 1.2 for NHibernate 3.1. You will also want to go to MySQL and download the .NET driver for MySQL.

Once you have these downloaded, i created a folder called AegisBornLibs which contains any libraries we are going to use. Extract the 2 zip files to this folder. Then you will want to run the installer for the MySQL driver. Next we want to open the AegisBornPhoton project and add the .dlls to the AegisBorn project. The files we want are FluentNHibernate.dll, NHibernate.ByteCode.Castle.dll, and Required_Bins\NHibernate.dll

Once these references are added to our project we are going to create our first entity classes - SfGuardUser, AegisBornCharacter, and AegisBornUserProfile. Again these are just like the SmartFoxServer model classes. We will be creating these classes in a folder called Models\Base. Before I create them, I go ahead and set up a server connection in the server explorer. Once my connection is established I create the Models\Base and Models\Map directories in my solution. From there I create the above 3 classes and my first map file:


    public class SfGuardUser

    {
        public virtual int Id { get; set; }
        public virtual string Username { get; set; }
        public virtual string Password { get; set; }
        public virtual string Salt { get; set; }
    }



    public class AegisBornUserProfile
    {
        public virtual int Id { get; set; }
        public virtual SfGuardUser UserId { get; set; }
        public virtual int CharacterSlots { get; set; }
    }



    public class AegisBornCharacter
    {
        public virtual int Id { get; set; }
        public virtual SfGuardUser UserId { get; set; }
        public virtual string Name { get; set; }
        public virtual string Sex { get; set; }
        public virtual string Class { get; set; }
        public virtual int Level { get; set; }
        public virtual int PositionX { get; set; }
        public virtual int PositionY { get; set; }
    }


    public class SfGuardUserMap : ClassMap
    {
        public SfGuardUserMap()
        {
            Id(x => x.Id).Column("id");
            Map(x => x.Username).Column("username");
            Map(x => x.Password).Column("password");
            Map(x => x.Salt).Column("salt");
            Table("sf_guard_user");
        }
    }

I stopped after creating SfGuardUserMap because at this point I wanted to test my code and ensure that it all works. To do this I built my function in AegisBornPeer that handles the login call. It looks like this:


        public ISessionFactory SessionFactory { get; set; }


        [Operation(OperationCode = (byte)OperationCode.Login)]
        public OperationResponse OperationLogin(Peer peer, OperationRequest request)
        {
            var operation = new LoginSecurely(request);
            if(!operation.IsValid)
            {
                return new OperationResponse(request, (int)ErrorCode.InvalidOperationParameter, operation.GetErrorMessage());
            }


            // Attempt to get user from db and check password.
            try
            {
                using (var session = SessionFactory.OpenSession())
                {
                    using (var transaction = session.BeginTransaction())
                    {
                        var user = session.CreateCriteria(typeof(SfGuardUser), "sf").Add(Restrictions.Eq("sf.Username", operation.UserName)).UniqueResult();


                        var sha1 = SHA1CryptoServiceProvider.Create();


                        var hash = BitConverter.ToString(sha1.ComputeHash(Encoding.UTF8.GetBytes(user.Salt + operation.Password))).Replace("-", "");


                        transaction.Commit();


                        if (String.Equals(hash.Trim(), user.Password.Trim(), StringComparison.OrdinalIgnoreCase))
                        {
                            return operation.GetOperationResponse(0, "OK");
                        }
                    }
                }
            }

            catch (Exception)
            {
                // Do nothing because we are about to throw them out anyway.
            }


            peer.PublishOperationResponse(new OperationResponse(request, (int)ErrorCode.InvalidUserPass, "The Username or Password is incorrect"));
            peer.DisconnectByOtherPeer(this, request);
            return null;
        }



        private static ISessionFactory CreateSessionFactory()
        {
            return Fluently.Configure()
              .Database(
                MySQLConfiguration.Standard
                .ConnectionString(cs => cs.Server("localhost")
                .Database("cjrgam5_ab")
                .Username("cjrgam5_ab")
                .Password("user_ab1!")))
              .Mappings(m =>
                m.FluentMappings.AddFromAssemblyOf())
              .BuildSessionFactory();
        }

The CreateSessionFactory function is only in this file while I test this code. Next post we will be extracting it into a helper class that will Lazy create the connection the first time we try to access the database. For more information on the configuration jump on over to http://wiki.fluentnhibernate.org/Database_configuration where you can see what each piece does. As for the Operation handler, we first make sure the operation is valid, meaning it has UserName and Password fields. After that we are following NHibernate's process for accessing the database, we create a session, then a transaction, then our query. Our query looks up the SfGuardUser by username and then rebuilds the password using the salt and our provided password and running it through an SHA1 digest. If the password isn't correct or there is no user, we throw errors. 

The last changes we are going to make are to create a new error code called InvalidUserPass:

        ///
        /// The username or password isn't correct, don't let them log in.
        ///
        InvalidUserPass,

Now we have the catch and final returns to pass an InvalidUserPass error so the user doesn't know if they exist or not which will help with hacking attempts. We also kick them off the server, a handy feature.

So thats all this time. Next time we will make a helper class for NHibernate's session factory. Until then, enjoy!

No comments: