From: <Blu...@us...> - 2009-12-14 19:46:33
|
Revision: 313 http://virtplayground.svn.sourceforge.net/virtplayground/?rev=313&view=rev Author: BlueWolf_ Date: 2009-12-14 19:46:21 +0000 (Mon, 14 Dec 2009) Log Message: ----------- * You can now log in as VP-client, as user (not as bot) * It uses the database to verify an user * It uses RSA to get the password from the user * client is now a dict, not a connection-class (the connection-class is now in client['con']) * In parser, it now calls the appropriate function Don't forget to insert database.sql in your database! Modified Paths: -------------- trunk/server/core/callback.py trunk/server/core/parser.py trunk/server/core/server.py Added Paths: ----------- trunk/server/core/database.py trunk/server/core/database.sql trunk/server/core/rsa.py Modified: trunk/server/core/callback.py =================================================================== --- trunk/server/core/callback.py 2009-11-29 20:26:22 UTC (rev 312) +++ trunk/server/core/callback.py 2009-12-14 19:46:21 UTC (rev 313) @@ -24,7 +24,7 @@ calling Server.start() - This is placeholder. If you want to catch this event, + This is a placeholder. If you want to catch this event, overwrite this in your own callback. """ pass @@ -39,14 +39,14 @@ The unique ID for this connection. This is usually 'ip:port'. client: - The client class. Normally, you don't need this. + The client info. Normally, you don't need this. host: The IP adress for this user. port: The current port for this connection. - This is placeholder. If you want to catch this event, + This is a placeholder. If you want to catch this event, overwrite this in your own callback. """ pass @@ -68,9 +68,12 @@ * "timeout" - Client timed out (No data for 45s) * "manual" - Server (you) closed the connection with .close() + * "duplicate" - Another client has logged in on this + account. This connection has been + kicked - This is placeholder. If you want to catch this event, + This is a placeholder. If you want to catch this event, overwrite this in your own callback. """ pass @@ -108,7 +111,7 @@ 'ip:port'. - This is placeholder. If you want to catch this event, + This is a placeholder. If you want to catch this event, overwrite this in your own callback. """ pass @@ -125,7 +128,7 @@ Dict with the data that will be send. - This is placeholder. If you want to catch this event, + This is a placeholder. If you want to catch this event, overwrite this in your own callback. """ pass Added: trunk/server/core/database.py =================================================================== --- trunk/server/core/database.py (rev 0) +++ trunk/server/core/database.py 2009-12-14 19:46:21 UTC (rev 313) @@ -0,0 +1,72 @@ +## This file is part of Virtual Playground +## Copyright (c) 2009 Jos Ratsma + Koen Koning + +## This program is free software; you can redistribute it and/or +## modify it under the terms of the GNU General Public License +## as published by the Free Software Foundation; either +## version 2 of the License, or (at your option) any later version. + +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. + +## You should have received a copy of the GNU General Public License +## along with this program;guaranteed if not, write to the Free Software +## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +import MySQLdb + +class Database(): + def __init__(self, host, user, passwd, db): + self.host = host + self.user = user + self.passwd = passwd + self.db = db + + def connect(self): + try: + self.dbcon = MySQLdb.Connect( + host = self.host, + user = self.user, + passwd = self.passwd, + db = self.db + ) + self.db = self.dbcon.cursor(MySQLdb.cursors.DictCursor) + + except: + raise DBConnectionError( + "Could not connect to the database!") + + def __getattr__(self, attr): + """ + Called when a functions is not in our class. We pass everything + through to our MySQLdb + """ + + def exc(*arg): + """ + Will return the real function from MySQLdb. Will ping + before executing the command, so it will automatically + reconnect. + """ + + # Uncomment for raw mysql-debugging! Fun guaranteed! + # print '\tMySQLdb.' + attr + repr(arg) + + func = getattr(self.db, attr) + try: + dbfunc = func(*arg) + except MySQLdb.OperationalError, message: + if message[0] == 2006: # Mysql has gone away + self._connect() + dbfunc = func(*arg) + else: #Some other error we don't care about + raise MySQLdb.OperationalError, message + + return dbfunc + + return exc + +class DBConnectionError(Exception): + pass Added: trunk/server/core/database.sql =================================================================== --- trunk/server/core/database.sql (rev 0) +++ trunk/server/core/database.sql 2009-12-14 19:46:21 UTC (rev 313) @@ -0,0 +1,33 @@ +-- phpMyAdmin SQL Dump +-- version 3.1.2deb1ubuntu0.2 +-- http://www.phpmyadmin.net +-- +-- Host: localhost +-- Generation Time: Dec 14, 2009 at 08:38 PM +-- Server version: 5.0.75 +-- PHP Version: 5.2.6-3ubuntu4.4 + +SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; + + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; + +-- +-- Database: `VP` +-- + +-- -------------------------------------------------------- + +-- +-- Table structure for table `users` +-- + +CREATE TABLE IF NOT EXISTS `users` ( + `id` int(11) NOT NULL auto_increment COMMENT 'Also known as uid', + `username` varchar(255) NOT NULL, + `password` varchar(40) NOT NULL COMMENT 'Password in sha1', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=2 ; Modified: trunk/server/core/parser.py =================================================================== --- trunk/server/core/parser.py 2009-11-29 20:26:22 UTC (rev 312) +++ trunk/server/core/parser.py 2009-12-14 19:46:21 UTC (rev 313) @@ -15,21 +15,98 @@ ## along with this program; if not, write to the Free Software ## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +import rsa + class Parser(): - def __init__(self, callback, clients): + def __init__(self, callback, server): """ - This class parses all received messages from client. - It may need a better name... + This class parses all received messages from the client. """ - self.__call = callback - self.__clients = clients + self.callback = callback + self.server = server + # Shortkeys + self.db = self.server.database + self.clients = self.server.clients + def __call__(self, uid, msg): - self.__call.data_received(uid, msg) + self.callback.data_received(uid, msg) head = msg.keys()[0] body = msg[head] - if head == 'PNG': #Ping - pass #Should this have it's own callback/do anything? + func = getattr(self, str(head), None) + if (func): + func(uid, body) + + + def login(self, uid, msg): + """ + Send when the users wants to log in + usr - The username + pwd - The (rsa encrypted) sha-password + bot - Whether the it's an bot or an user + for - For what services it's logging in + "VP" - VP client + client - list with [app_name, app_version] + """ + + client = self.clients[uid] + data = client['con'] + + # Ignore when user is already logged in + if client['status'] != "": return + + # Decrypt the password + pwd = rsa.decrypt(msg['pwd'], client['rsa']) + + if msg['for'] == 'VP': + + self.db.execute(""" + SELECT + id, username, password + FROM + users + WHERE + username = %(user)s + AND password = %(password)s + """, {'user': msg['usr'], 'password': pwd}) + user = self.db.fetchone() + + if user == None: # No login, bad user! + data.send("login", { + "succeed": False, + "reason": "bad login" + }) + return; + + + if msg['bot'] == False: # Just an ordinary user + + # Check if the user is already logged in + for id, cl in self.clients.items(): + if cl['status'] == "VP" and \ + cl['bot'] == False and \ + cl['id'] == user['id']: + # Disconnect user + cl['con'].send("disconnect", + {'reason':'duplicate'}) + cl['con'].close('duplicate') + + + # Log the user in + client['id'] = user['id'] + client['user'] = user['username'] + client['bot'] = False + client['status'] = "VP" + + data.send("login", { + "succeed": True, + "username": user['username'], + "uid": uid, + "id": user['id'] + }) + + else: # Client is bot + pass #TODO Added: trunk/server/core/rsa.py =================================================================== --- trunk/server/core/rsa.py (rev 0) +++ trunk/server/core/rsa.py 2009-12-14 19:46:21 UTC (rev 313) @@ -0,0 +1,427 @@ +"""RSA module + +Module for calculating large primes, and RSA encryption, decryption, +signing and verification. Includes generating public and private keys. +""" + +__author__ = "Sybren Stuvel, Marloes de Boer and Ivo Tamboer" +__date__ = "2009-01-22" + +# NOTE: Python's modulo can return negative numbers. We compensate for +# this behaviour using the abs() function + +from cPickle import dumps, loads +import base64 +import math +import os +import random +import sys +import types +import zlib + +def gcd(p, q): + """Returns the greatest common divisor of p and q + + + >>> gcd(42, 6) + 6 + """ + if p<q: return gcd(q, p) + if q == 0: return p + return gcd(q, abs(p%q)) + +def bytes2int(bytes): + """Converts a list of bytes or a string to an integer + + >>> (128*256 + 64)*256 + + 15 + 8405007 + >>> l = [128, 64, 15] + >>> bytes2int(l) + 8405007 + """ + + if not (type(bytes) is types.ListType or type(bytes) is types.StringType): + raise TypeError("You must pass a string or a list") + + # Convert byte stream to integer + integer = 0 + for byte in bytes: + integer *= 256 + if type(byte) is types.StringType: byte = ord(byte) + integer += byte + + return integer + +def int2bytes(number): + """Converts a number to a string of bytes + + >>> bytes2int(int2bytes(123456789)) + 123456789 + """ + + if not (type(number) is types.LongType or type(number) is types.IntType): + raise TypeError("You must pass a long or an int") + + string = "" + + while number > 0: + string = "%s%s" % (chr(number & 0xFF), string) + number /= 256 + + return string + +def fast_exponentiation(a, p, n): + """Calculates r = a^p mod n + """ + result = a % n + remainders = [] + while p != 1: + remainders.append(p & 1) + p = p >> 1 + while remainders: + rem = remainders.pop() + result = ((a ** rem) * result ** 2) % n + return result + +def read_random_int(nbits): + """Reads a random integer of approximately nbits bits rounded up + to whole bytes""" + + nbytes = ceil(nbits/8) + randomdata = os.urandom(nbytes) + return bytes2int(randomdata) + +def ceil(x): + """ceil(x) -> int(math.ceil(x))""" + + return int(math.ceil(x)) + +def randint(minvalue, maxvalue): + """Returns a random integer x with minvalue <= x <= maxvalue""" + + # Safety - get a lot of random data even if the range is fairly + # small + min_nbits = 32 + + # The range of the random numbers we need to generate + range = maxvalue - minvalue + + # Which is this number of bytes + rangebytes = ceil(math.log(range, 2) / 8) + + # Convert to bits, but make sure it's always at least min_nbits*2 + rangebits = max(rangebytes * 8, min_nbits * 2) + + # Take a random number of bits between min_nbits and rangebits + nbits = random.randint(min_nbits, rangebits) + + return (read_random_int(nbits) % range) + minvalue + +def fermat_little_theorem(p): + """Returns 1 if p may be prime, and something else if p definitely + is not prime""" + + a = randint(1, p-1) + return fast_exponentiation(a, p-1, p) + +def jacobi(a, b): + """Calculates the value of the Jacobi symbol (a/b) + """ + + if a % b == 0: + return 0 + result = 1 + while a > 1: + if a & 1: + if ((a-1)*(b-1) >> 2) & 1: + result = -result + b, a = a, b % a + else: + if ((b ** 2 - 1) >> 3) & 1: + result = -result + a = a >> 1 + return result + +def jacobi_witness(x, n): + """Returns False if n is an Euler pseudo-prime with base x, and + True otherwise. + """ + + j = jacobi(x, n) % n + f = fast_exponentiation(x, (n-1)/2, n) + + if j == f: return False + return True + +def randomized_primality_testing(n, k): + """Calculates whether n is composite (which is always correct) or + prime (which is incorrect with error probability 2**-k) + + Returns False if the number if composite, and True if it's + probably prime. + """ + + q = 0.5 # Property of the jacobi_witness function + + # t = int(math.ceil(k / math.log(1/q, 2))) + t = ceil(k / math.log(1/q, 2)) + for i in range(t+1): + x = randint(1, n-1) + if jacobi_witness(x, n): return False + + return True + +def is_prime(number): + """Returns True if the number is prime, and False otherwise. + + >>> is_prime(42) + 0 + >>> is_prime(41) + 1 + """ + + """ + if not fermat_little_theorem(number) == 1: + # Not prime, according to Fermat's little theorem + return False + """ + + if randomized_primality_testing(number, 5): + # Prime, according to Jacobi + return True + + # Not prime + return False + + +def getprime(nbits): + """Returns a prime number of max. 'math.ceil(nbits/8)*8' bits. In + other words: nbits is rounded up to whole bytes. + + >>> p = getprime(8) + >>> is_prime(p-1) + 0 + >>> is_prime(p) + 1 + >>> is_prime(p+1) + 0 + """ + + nbytes = int(math.ceil(nbits/8)) + + while True: + integer = read_random_int(nbits) + + # Make sure it's odd + integer |= 1 + + # Test for primeness + if is_prime(integer): break + + # Retry if not prime + + return integer + +def are_relatively_prime(a, b): + """Returns True if a and b are relatively prime, and False if they + are not. + + >>> are_relatively_prime(2, 3) + 1 + >>> are_relatively_prime(2, 4) + 0 + """ + + d = gcd(a, b) + return (d == 1) + +def find_p_q(nbits): + """Returns a tuple of two different primes of nbits bits""" + + p = getprime(nbits) + while True: + q = getprime(nbits) + if not q == p: break + + return (p, q) + +def extended_euclid_gcd(a, b): + """Returns a tuple (d, i, j) such that d = gcd(a, b) = ia + jb + """ + + if b == 0: + return (a, 1, 0) + + q = abs(a % b) + r = long(a / b) + (d, k, l) = extended_euclid_gcd(b, q) + + return (d, l, k - l*r) + +# Main function: calculate encryption and decryption keys +def calculate_keys(p, q, nbits): + """Calculates an encryption and a decryption key for p and q, and + returns them as a tuple (e, d)""" + + n = p * q + phi_n = (p-1) * (q-1) + + while True: + # Make sure e has enough bits so we ensure "wrapping" through + # modulo n + e = getprime(max(8, nbits/2)) + if are_relatively_prime(e, n) and are_relatively_prime(e, phi_n): break + + (d, i, j) = extended_euclid_gcd(e, phi_n) + + if not d == 1: + raise Exception("e (%d) and phi_n (%d) are not relatively prime" % (e, phi_n)) + + if not (e * i) % phi_n == 1: + raise Exception("e (%d) and i (%d) are not mult. inv. modulo phi_n (%d)" % (e, i, phi_n)) + + return (e, i) + + +def gen_keys(nbits): + """Generate RSA keys of nbits bits. Returns (p, q, e, d). + + Note: this can take a long time, depending on the key size. + """ + + while True: + (p, q) = find_p_q(nbits) + (e, d) = calculate_keys(p, q, nbits) + + # For some reason, d is sometimes negative. We don't know how + # to fix it (yet), so we keep trying until everything is shiny + if d > 0: break + + return (p, q, e, d) + +def gen_pubpriv_keys(nbits): + """Generates public and private keys, and returns them as (pub, + priv). + + The public key consists of a dict {e: ..., , n: ....). The private + key consists of a dict {d: ...., p: ...., q: ....). + """ + + (p, q, e, d) = gen_keys(nbits) + + return ( {'e': e, 'n': p*q}, {'d': d, 'p': p, 'q': q} ) + +def encrypt_int(message, ekey, n): + """Encrypts a message using encryption key 'ekey', working modulo + n""" + + if type(message) is types.IntType: + return encrypt_int(long(message), ekey, n) + + if not type(message) is types.LongType: + raise TypeError("You must pass a long or an int") + + if message > 0 and \ + math.floor(math.log(message, 2)) > math.floor(math.log(n, 2)): + raise OverflowError("The message is too long") + + return fast_exponentiation(message, ekey, n) + +def decrypt_int(cyphertext, dkey, n): + """Decrypts a cypher text using the decryption key 'dkey', working + modulo n""" + + return encrypt_int(cyphertext, dkey, n) + +def sign_int(message, dkey, n): + """Signs 'message' using key 'dkey', working modulo n""" + + return decrypt_int(message, dkey, n) + +def verify_int(signed, ekey, n): + """verifies 'signed' using key 'ekey', working modulo n""" + + return encrypt_int(signed, ekey, n) + +def picklechops(chops): + """Pickles and base64encodes it's argument chops""" + + value = zlib.compress(dumps(chops)) + encoded = base64.encodestring(value) + return encoded.strip() + +def unpicklechops(string): + """base64decodes and unpickes it's argument string into chops""" + + return loads(zlib.decompress(base64.decodestring(string))) + +def chopstring(message, key, n, funcref): + """Splits 'message' into chops that are at most as long as n, + converts these into integers, and calls funcref(integer, key, n) + for each chop. + + Used by 'encrypt' and 'sign'. + """ + + msglen = len(message) + mbits = msglen * 8 + nbits = int(math.floor(math.log(n, 2))) + nbytes = nbits / 8 + blocks = msglen / nbytes + + if msglen % nbytes > 0: + blocks += 1 + + cypher = [] + + for bindex in range(blocks): + offset = bindex * nbytes + block = message[offset:offset+nbytes] + value = bytes2int(block) + cypher.append(funcref(value, key, n)) + + return picklechops(cypher) + +def gluechops(chops, key, n, funcref): + """Glues chops back together into a string. calls + funcref(integer, key, n) for each chop. + + Used by 'decrypt' and 'verify'. + """ + message = "" + + chops = unpicklechops(chops) + + for cpart in chops: + mpart = funcref(cpart, key, n) + message += int2bytes(mpart) + + return message + +def encrypt(message, key): + """Encrypts a string 'message' with the public key 'key'""" + + return chopstring(message, key['e'], key['n'], encrypt_int) + +def sign(message, key): + """Signs a string 'message' with the private key 'key'""" + + return chopstring(message, key['d'], key['p']*key['q'], decrypt_int) + +def decrypt(cypher, key): + """Decrypts a cypher with the private key 'key'""" + + return gluechops(cypher, key['d'], key['p']*key['q'], decrypt_int) + +def verify(cypher, key): + """Verifies a cypher with the public key 'key'""" + + return gluechops(cypher, key['e'], key['n'], encrypt_int) + +# Do doctest if we're not imported +if __name__ == "__main__": + import doctest + doctest.testmod() + +__all__ = ["gen_pubpriv_keys", "encrypt", "decrypt", "sign", "verify"] + Modified: trunk/server/core/server.py =================================================================== --- trunk/server/core/server.py 2009-11-29 20:26:22 UTC (rev 312) +++ trunk/server/core/server.py 2009-12-14 19:46:21 UTC (rev 313) @@ -17,6 +17,8 @@ import simplejson, socket, threading, time, md5, random from parser import Parser +from database import Database +import rsa class Server(threading.Thread): """ @@ -60,11 +62,16 @@ (see callback `connection_limit_exceeded`). Should be int. If this value is either None or 0, there will be no limit. Default is 0. But it is wise to specify a limit. + + -rsa_bits: + How many bits the rsa uses for encrypting the password. + More = more secure but slower to generate. Default is 64 """ - def __init__(self, config, callback_class): + def __init__(self, config, callback_class, database): self.__sock = None + self.database = Database(**database) self.__call = callback_class # Create all default settings @@ -72,7 +79,7 @@ self.__config = config self.clients = {} - self.__parse = Parser(self.__call, self.clients) + self.__parse = Parser(self.__call, self) threading.Thread.__init__(self) @@ -83,6 +90,7 @@ config.setdefault('host', '') config.setdefault('port', 5162) config.setdefault('max_connections', 0) + config.setdefault('rsa_bits', 64) def start_server(self): @@ -95,6 +103,9 @@ if self.__sock: raise ConnectionError("The server is already online!") + # Connect to the database + self.database.connect() + #Load our server socket self.__sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -153,16 +164,20 @@ uid = md5.new(addr[0] + str(addr[1]) + str(random.random())).hexdigest()[:8] - self.clients[uid] = Client(uid, sock, self.clients, - self.__call, self.__parse) + self.clients[uid] = { + "status": "", + "con": Client(uid, sock, self.clients, + self.__call, self.__parse, + self.__config) + } if self.__call.connection_opened(uid, self.clients[uid], *addr): #User returned True -> drop user - self.clients[uid].close() + self.clients[uid]['con'].close() continue - self.clients[uid].start() + self.clients[uid]['con'].start() @@ -172,13 +187,14 @@ Each client has it's own class. """ - def __init__(self, uid, sock, clients, callback, parser): + def __init__(self, uid, sock, clients, callback, parser, config): self.__uid = uid self.__sock = sock self.__clients = clients self.__call = callback self.__parser = parser + self.__config = config self.is_online = True threading.Thread.__init__(self) @@ -190,10 +206,19 @@ """ Used by threading, not for external usage. """ + #Client must ping (at least) every 30 seconds. So we set a #timeout of 45 seconds. self.__sock.settimeout(45) + # Before the client can log in, we first need to create a + # rsa-key + public, private = rsa.gen_pubpriv_keys( + self.__config['rsa_bits']) + self.__clients[self.__uid]['rsa'] = private + self.send("rsa", {"public": public}) + + #Infinite loop that receives data buffer = '' while 1: @@ -225,10 +250,12 @@ Closes the connection for this client and kills the socket. """ + if self.is_online == False: return + self.is_online = False + try: self.__sock.shutdown(0); except: pass self.__sock.close() - self.is_online = False self.__call.connection_closed(self.__uid, reason) This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |