First release, remove not used lines from gitignore
This commit is contained in:
parent
c0bfb3b062
commit
d28e1cb4a6
85 changed files with 7205 additions and 50 deletions
466
src/lib/BitcoinECC/BitcoinECC.py
Normal file
466
src/lib/BitcoinECC/BitcoinECC.py
Normal file
|
@ -0,0 +1,466 @@
|
|||
# By: HurlSly
|
||||
# Source: https://github.com/HurlSly/Python/blob/master/BitcoinECC.py
|
||||
# Modified: random number generator in def GeneratePrivateKey(self):
|
||||
|
||||
import random
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
class GaussInt:
|
||||
#A class for the Gauss integers of the form a + b sqrt(n) where a,b are integers.
|
||||
#n can be positive or negative.
|
||||
def __init__(self,x,y,n,p=0):
|
||||
if p:
|
||||
self.x=x%p
|
||||
self.y=y%p
|
||||
self.n=n%p
|
||||
else:
|
||||
self.x=x
|
||||
self.y=y
|
||||
self.n=n
|
||||
|
||||
self.p=p
|
||||
|
||||
def __add__(self,b):
|
||||
return GaussInt(self.x+b.x,self.y+b.y,self.n,self.p)
|
||||
|
||||
def __sub__(self,b):
|
||||
return GaussInt(self.x-b.x,self.y-b.y,self.n,self.p)
|
||||
|
||||
def __mul__(self,b):
|
||||
return GaussInt(self.x*b.x+self.n*self.y*b.y,self.x*b.y+self.y*b.x,self.n,self.p)
|
||||
|
||||
def __div__(self,b):
|
||||
return GaussInt((self.x*b.x-self.n*self.y*b.y)/(b.x*b.x-self.n*b.y*b.y),(-self.x*b.y+self.y*b.x)/(b.x*b.x-self.n*b.y*b.y),self.n,self.p)
|
||||
|
||||
def __eq__(self,b):
|
||||
return self.x==b.x and self.y==b.y
|
||||
|
||||
def __repr__(self):
|
||||
if self.p:
|
||||
return "%s+%s (%d,%d)"%(self.x,self.y,self.n,self.p)
|
||||
else:
|
||||
return "%s+%s (%d)"%(self.x,self.y,self.n)
|
||||
|
||||
def __pow__(self,n):
|
||||
b=Base(n,2)
|
||||
t=GaussInt(1,0,self.n)
|
||||
while b:
|
||||
t=t*t
|
||||
if b.pop():
|
||||
t=self*t
|
||||
|
||||
return t
|
||||
|
||||
def Inv(self):
|
||||
return GaussInt(self.x/(self.x*self.x-self.n*self.y*self.y),-self.y/(self.x*self.x-self.n*self.y*self.y),self.n,self.p)
|
||||
|
||||
def Cipolla(a,p):
|
||||
#Find a square root of a modulo p using the algorithm of Cipolla
|
||||
b=0
|
||||
while pow((b*b-a)%p,(p-1)/2,p)==1:
|
||||
b+=1
|
||||
|
||||
return (GaussInt(b,1,b**2-a,p)**((p+1)/2)).x
|
||||
|
||||
def Base(n,b):
|
||||
#Decompose n in base b
|
||||
l=[]
|
||||
while n:
|
||||
l.append(n%b)
|
||||
n/=b
|
||||
|
||||
return l
|
||||
|
||||
def InvMod(a,n):
|
||||
#Find the inverse mod n of a.
|
||||
#Use the Extended Euclides Algorithm.
|
||||
m=[]
|
||||
|
||||
s=n
|
||||
while n:
|
||||
m.append(a/n)
|
||||
(a,n)=(n,a%n)
|
||||
|
||||
u=1
|
||||
v=0
|
||||
while m:
|
||||
(u,v)=(v,u-m.pop()*v)
|
||||
|
||||
return u%s
|
||||
|
||||
def b58encode(v):
|
||||
#Encode a byte string to the Base58
|
||||
digit="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||
base=len(digit)
|
||||
val=0
|
||||
for c in v:
|
||||
val*=256
|
||||
val+=ord(c)
|
||||
|
||||
result=""
|
||||
while val:
|
||||
(val,mod)=divmod(val,base)
|
||||
result=digit[mod]+result
|
||||
|
||||
pad=0
|
||||
for c in v:
|
||||
if c=="\0":
|
||||
pad+=1
|
||||
else:
|
||||
break
|
||||
|
||||
return (digit[0]*pad)+result
|
||||
|
||||
def b58decode(v):
|
||||
#Decode a Base58 string to byte string
|
||||
digit="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||
base=len(digit)
|
||||
val=0
|
||||
for c in v:
|
||||
val*=base
|
||||
val+=digit.find(c)
|
||||
|
||||
result=""
|
||||
while val:
|
||||
(val,mod)=divmod(val,256)
|
||||
result=chr(mod)+result
|
||||
|
||||
pad=0
|
||||
for c in v:
|
||||
if c==digit[0]:
|
||||
pad+=1
|
||||
else:
|
||||
break
|
||||
|
||||
result="\0"*pad+result
|
||||
|
||||
return result
|
||||
|
||||
def Byte2Hex(b):
|
||||
#Convert a byte string to hex number
|
||||
out=""
|
||||
for x in b:
|
||||
y=hex(ord(x))[2:]
|
||||
if len(y)==1:
|
||||
y="0"+y
|
||||
out+="%2s"%y
|
||||
|
||||
return out
|
||||
|
||||
def Int2Byte(n,b):
|
||||
#Convert a integer to a byte string of length b
|
||||
out=""
|
||||
|
||||
for i in range(b):
|
||||
(n,m)=divmod(n,256)
|
||||
out=chr(m)+out
|
||||
|
||||
return out
|
||||
|
||||
class EllipticCurvePoint:
|
||||
#Main class
|
||||
#It is an point on an Elliptic Curve
|
||||
|
||||
def __init__(self,x,a,b,p,n=0):
|
||||
#We store the coordinate in x and the elliptic curbe parameter.
|
||||
#x is of length 3. This is the 3 projective coordinates of the point.
|
||||
self.x=x[:]
|
||||
self.a=a
|
||||
self.b=b
|
||||
self.p=p
|
||||
self.n=n
|
||||
|
||||
def EqualProj(self,y):
|
||||
#Does y equals self ?
|
||||
#It computes self cross product with y and check if the result is 0.
|
||||
return self.x[0]*y.x[1]==self.x[1]*y.x[0] and self.x[1]*y.x[2]==self.x[2]*y.x[1] and self.x[2]*y.x[0]==self.x[0]*y.x[2]
|
||||
|
||||
def __add__(self,y):
|
||||
#The main function to add self and y
|
||||
#It uses the formulas I derived in projective coordinates.
|
||||
#Projectives coordinates are more performant than the usual (x,y) coordinates
|
||||
#because it we don't need to compute inverse mod p, which is faster.
|
||||
z=EllipticCurvePoint([0,0,0],self.a,self.b,self.p)
|
||||
|
||||
if self.EqualProj(y):
|
||||
d=(2*self.x[1]*self.x[2])%self.p
|
||||
d3=pow(d,3,self.p)
|
||||
n=(3*pow(self.x[0],2,self.p)+self.a*pow(self.x[2],2,self.p))%self.p
|
||||
|
||||
z.x[0]=(pow(n,2,self.p)*d*self.x[2]-2*d3*self.x[0])%self.p
|
||||
z.x[1]=(3*self.x[0]*n*pow(d,2,self.p)-pow(n,3,self.p)*self.x[2]-self.x[1]*d3)%self.p
|
||||
z.x[2]=(self.x[2]*d3)%self.p
|
||||
else:
|
||||
d=(y.x[0]*self.x[2]-y.x[2]*self.x[0])%self.p
|
||||
d3=pow(d,3,self.p)
|
||||
n=(y.x[1]*self.x[2]-self.x[1]*y.x[2])%self.p
|
||||
|
||||
z.x[0]=(y.x[2]*self.x[2]*pow(n,2,self.p)*d-d3*(y.x[2]*self.x[0]+y.x[0]*self.x[2]))%self.p
|
||||
z.x[1]=(pow(d,2,self.p)*n*(2*self.x[0]*y.x[2]+y.x[0]*self.x[2])-pow(n,3,self.p)*self.x[2]*y.x[2]-self.x[1]*d3*y.x[2])%self.p
|
||||
z.x[2]=(self.x[2]*d3*y.x[2])%self.p
|
||||
|
||||
return z
|
||||
|
||||
def __mul__(self,n):
|
||||
#The fast multiplication of point n times by itself.
|
||||
b=Base(n,2)
|
||||
t=EllipticCurvePoint(self.x,self.a,self.b,self.p)
|
||||
b.pop()
|
||||
while b:
|
||||
t+=t
|
||||
if b.pop():
|
||||
t+=self
|
||||
|
||||
return t
|
||||
|
||||
def __repr__(self):
|
||||
#print a point in (x,y) coordinate.
|
||||
return "x=%d\ny=%d\n"%((self.x[0]*InvMod(self.x[2],self.p))%self.p,(self.x[1]*InvMod(self.x[2],self.p))%self.p)
|
||||
|
||||
def __eq__(self,x):
|
||||
#Does self==x ?
|
||||
return self.x==x.x and self.a==x.a and self.b==x.b and self.p==x.p
|
||||
|
||||
def __ne__(self,x):
|
||||
#Does self!=x ?
|
||||
return self.x!=x.x or self.a!=x.a or self.b!=x.b or self.p!=x.p
|
||||
|
||||
def Check(self):
|
||||
#Is self on the curve ?
|
||||
return (self.x[0]**3+self.a*self.x[0]*self.x[2]**2+self.b*self.x[2]**3-self.x[1]**2*self.x[2])%self.p==0
|
||||
|
||||
def GeneratePrivateKey(self):
|
||||
#Generate a private key. It's just a random number between 1 and n-1.
|
||||
#Of course, this function isn't cryptographically secure.
|
||||
#Don't use it to generate your key. Use a cryptographically secure source of randomness instead.
|
||||
#self.d = random.randint(1,self.n-1)
|
||||
self.d = int(os.urandom(32).encode("hex"), 16) # Better random fix
|
||||
|
||||
def SignECDSA(self,m):
|
||||
#Sign a message. The private key is self.d .
|
||||
h=hashlib.new("SHA256")
|
||||
h.update(m)
|
||||
z=int(h.hexdigest(),16)
|
||||
|
||||
r=0
|
||||
s=0
|
||||
while not r or not s:
|
||||
k=random.randint(1,self.n-1)
|
||||
R=self*k
|
||||
R.Normalize()
|
||||
r=R.x[0]%self.n
|
||||
s=(InvMod(k,self.n)*(z+r*self.d))%self.n
|
||||
|
||||
return (r,s)
|
||||
|
||||
def CheckECDSA(self,sig,m):
|
||||
#Check a signature (r,s) of the message m using the public key self.Q
|
||||
# and the generator which is self.
|
||||
#This is not the one used by Bitcoin because the public key isn't known;
|
||||
# only a hash of the public key is known. See the next function.
|
||||
(r,s)=sig
|
||||
|
||||
h=hashlib.new("SHA256")
|
||||
h.update(m)
|
||||
z=int(h.hexdigest(),16)
|
||||
|
||||
if self.Q.x[2]==0:
|
||||
return False
|
||||
if not self.Q.Check():
|
||||
return False
|
||||
if (self.Q*self.n).x[2]!=0:
|
||||
return False
|
||||
if r<1 or r>self.n-1 or s<1 or s>self.n-1:
|
||||
return False
|
||||
|
||||
w=InvMod(s,self.n)
|
||||
u1=(z*w)%self.n
|
||||
u2=(r*w)%self.n
|
||||
R=self*u1+self.Q*u2
|
||||
R.Normalize()
|
||||
|
||||
return (R.x[0]-r)%self.n==0
|
||||
|
||||
def VerifyMessageFromBitcoinAddress(self,adresse,m,sig):
|
||||
#Check a signature (r,s) for the message m signed by the Bitcoin
|
||||
# address "addresse".
|
||||
h=hashlib.new("SHA256")
|
||||
h.update(m)
|
||||
z=int(h.hexdigest(),16)
|
||||
|
||||
(r,s)=sig
|
||||
x=r
|
||||
y2=(pow(x,3,self.p)+self.a*x+self.b)%self.p
|
||||
y=Cipolla(y2,self.p)
|
||||
|
||||
for i in range(2):
|
||||
kG=EllipticCurvePoint([x,y,1],self.a,self.b,self.p,self.n)
|
||||
mzG=self*((-z)%self.n)
|
||||
self.Q=(kG*s+mzG)*InvMod(r,self.n)
|
||||
|
||||
adr=self.BitcoinAddresFromPublicKey()
|
||||
if adr==adresse:
|
||||
break
|
||||
y=(-y)%self.p
|
||||
|
||||
if adr!=adresse:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def BitcoinAddressFromPrivate(self,pri=None):
|
||||
#Transform a private key in base58 encoding to a bitcoin address.
|
||||
#normal means "uncompressed".
|
||||
if not pri:
|
||||
print "Private Key :",
|
||||
pri=raw_input()
|
||||
|
||||
normal=(len(pri)==51)
|
||||
pri=b58decode(pri)
|
||||
|
||||
if normal:
|
||||
pri=pri[1:-4]
|
||||
else:
|
||||
pri=pri[1:-5]
|
||||
|
||||
self.d=int(Byte2Hex(pri),16)
|
||||
|
||||
return self.BitcoinAddress(normal)
|
||||
|
||||
def PrivateEncoding(self,normal=True):
|
||||
#Encode a private key self.d to base58 encoding.
|
||||
p=Int2Byte(self.d,32)
|
||||
p="\80"+p
|
||||
|
||||
if not normal:
|
||||
p+=chr(1)
|
||||
|
||||
h=hashlib.new("SHA256")
|
||||
h.update(p)
|
||||
s=h.digest()
|
||||
|
||||
h=hashlib.new("SHA256")
|
||||
h.update(s)
|
||||
s=h.digest()
|
||||
|
||||
cs=s[:4]
|
||||
|
||||
p+=cs
|
||||
p=b58encode(p)
|
||||
|
||||
return p
|
||||
|
||||
def BitcoinAddresFromPublicKey(self,normal=True):
|
||||
#Find the bitcoin address from the public key self.Q
|
||||
#We do normalization to go from the projective coordinates to the usual
|
||||
# (x,y) coordinates.
|
||||
self.Q.Normalize()
|
||||
if normal:
|
||||
pk=chr(4)+Int2Byte(self.Q.x[0],32)+Int2Byte((self.Q.x[1])%self.p,32)
|
||||
else:
|
||||
if self.Q.x[1]%2==0:
|
||||
pk=chr(2)+Int2Byte(self.Q.x[0],32)
|
||||
else:
|
||||
pk=chr(3)+Int2Byte(self.Q.x[0],32)
|
||||
|
||||
version=chr(0)
|
||||
|
||||
h=hashlib.new("SHA256")
|
||||
h.update(pk)
|
||||
s=h.digest()
|
||||
|
||||
h=hashlib.new("RIPEMD160")
|
||||
h.update(s)
|
||||
kh=version+h.digest()
|
||||
|
||||
h=hashlib.new("SHA256")
|
||||
h.update(kh)
|
||||
cs=h.digest()
|
||||
|
||||
h=hashlib.new("SHA256")
|
||||
h.update(cs)
|
||||
cs=h.digest()[:4]
|
||||
|
||||
adr=b58encode(kh+cs)
|
||||
|
||||
return adr
|
||||
|
||||
def BitcoinAddress(self,normal=True):
|
||||
#Computes a bitcoin address given the private key self.d.
|
||||
self.Q=self*self.d
|
||||
|
||||
return self.BitcoinAddresFromPublicKey(normal)
|
||||
|
||||
def BitcoinAddressGenerator(self,k,filename):
|
||||
#Generate Bitcoin address and write them in the filename in the multibit format.
|
||||
#Change the date as you like.
|
||||
f=open(filename,"w")
|
||||
for i in range(k):
|
||||
self.GeneratePrivateKey()
|
||||
adr=self.BitcoinAddress()
|
||||
p=self.PrivateEncoding()
|
||||
f.write("#%s\n%s 2014-01-30T12:00:00Z\n"%(adr,p))
|
||||
|
||||
#print hex(self.d)
|
||||
print adr,p
|
||||
|
||||
f.close()
|
||||
|
||||
def TestSign(self):
|
||||
#Test signature
|
||||
self.GeneratePrivateKey()
|
||||
self.Q=self*self.d
|
||||
m="Hello World"
|
||||
adresse=self.BitcoinAddresFromPublicKey()
|
||||
(r,s)=self.SignECDSA(m)
|
||||
|
||||
m="Hello World"
|
||||
print self.VerifyMessageFromBitcoinAddress(adresse,m,r,s)
|
||||
|
||||
def Normalize(self):
|
||||
#Transform projective coordinates of self to the usual (x,y) coordinates.
|
||||
if self.x[2]:
|
||||
self.x[0]=(self.x[0]*InvMod(self.x[2],self.p))%self.p
|
||||
self.x[1]=(self.x[1]*InvMod(self.x[2],self.p))%self.p
|
||||
self.x[2]=1
|
||||
elif self.x[1]:
|
||||
self.x[0]=(self.x[0]*InvMod(self.x[1],self.p))%self.p
|
||||
self.x[1]=1
|
||||
elif self.x[0]:
|
||||
self.x[0]=1
|
||||
else:
|
||||
raise Exception
|
||||
|
||||
def Bitcoin():
|
||||
#Create the Bitcoin elliptiv curve
|
||||
a=0
|
||||
b=7
|
||||
p=2**256-2**32-2**9-2**8-2**7-2**6-2**4-1
|
||||
|
||||
#Create the generator G of the Bitcoin elliptic curve, with is order n.
|
||||
Gx=int("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",16)
|
||||
Gy=int("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8",16)
|
||||
n =int("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141",16)
|
||||
|
||||
#Create the generator
|
||||
return EllipticCurvePoint([Gx,Gy,1],a,b,p,n)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
bitcoin=Bitcoin()
|
||||
|
||||
#Generate the public key from the private one
|
||||
print bitcoin.BitcoinAddressFromPrivate("23DKRBLkeDbcSaddsMYLAHXhanPmGwkWAhSPVGbspAkc72Hw9BdrDF")
|
||||
print bitcoin.BitcoinAddress()
|
||||
|
||||
#Print the bitcoin address of the public key generated at the previous line
|
||||
adr=bitcoin.BitcoinAddresFromPublicKey()
|
||||
print adr
|
||||
|
||||
#Sign a message with the current address
|
||||
m="Hello World"
|
||||
sig=bitcoin.SignECDSA("Hello World")
|
||||
#Verify the message using only the bitcoin adress, the signature and the message.
|
||||
#Not using the public key as it is not needed.
|
||||
print bitcoin.VerifyMessageFromBitcoinAddress(adr,m,sig)
|
0
src/lib/BitcoinECC/__init__.py
Normal file
0
src/lib/BitcoinECC/__init__.py
Normal file
0
src/lib/__init__.py
Normal file
0
src/lib/__init__.py
Normal file
0
src/lib/cssvendor/__init__.py
Normal file
0
src/lib/cssvendor/__init__.py
Normal file
29
src/lib/cssvendor/cssvendor.py
Normal file
29
src/lib/cssvendor/cssvendor.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
import re
|
||||
|
||||
def prefix(content):
|
||||
content = re.sub("@keyframes (.*? {.*?[^ ]})", "@keyframes \\1\n@-webkit-keyframes \\1\n@-moz-keyframes \\1\n", content, flags=re.DOTALL)
|
||||
content = re.sub('([^-\*])(border-radius|box-shadow|transition|animation|box-sizing|transform|filter|perspective|animation-[a-z-]+): (.*?)([;}])', '\\1-webkit-\\2: \\3; -moz-\\2: \\3; -o-\\2: \\3; -ms-\\2: \\3; \\2: \\3 \\4', content)
|
||||
content = re.sub('(?<=[^a-zA-Z0-9-])([a-zA-Z0-9-]+): {0,1}(linear-gradient)\((.*?)(\)[;\n])',
|
||||
'\\1: -webkit-\\2(\\3);'+
|
||||
'\\1: -moz-\\2(\\3);'+
|
||||
'\\1: -o-\\2(\\3);'+
|
||||
'\\1: -ms-\\2(\\3);'+
|
||||
'\\1: \\2(\\3);', content)
|
||||
return content
|
||||
|
||||
if __name__ == "__main__":
|
||||
print prefix("""
|
||||
.test {
|
||||
border-radius: 5px;
|
||||
background: linear-gradient(red, blue);
|
||||
}
|
||||
|
||||
|
||||
@keyframes flip {
|
||||
0% { transform: perspective(120px) rotateX(0deg) rotateY(0deg); }
|
||||
50% { transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg) }
|
||||
100% { transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); }
|
||||
}
|
||||
|
||||
|
||||
""")
|
21
src/lib/geventwebsocket/__init__.py
Normal file
21
src/lib/geventwebsocket/__init__.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
VERSION = (0, 9, 3, 'final', 0)
|
||||
|
||||
__all__ = [
|
||||
'WebSocketApplication',
|
||||
'Resource',
|
||||
'WebSocketServer',
|
||||
'WebSocketError',
|
||||
'get_version'
|
||||
]
|
||||
|
||||
|
||||
def get_version(*args, **kwargs):
|
||||
from .utils import get_version
|
||||
return get_version(*args, **kwargs)
|
||||
|
||||
try:
|
||||
from .resource import WebSocketApplication, Resource
|
||||
from .server import WebSocketServer
|
||||
from .exceptions import WebSocketError
|
||||
except ImportError:
|
||||
pass
|
19
src/lib/geventwebsocket/exceptions.py
Normal file
19
src/lib/geventwebsocket/exceptions.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from socket import error as socket_error
|
||||
|
||||
|
||||
class WebSocketError(socket_error):
|
||||
"""
|
||||
Base class for all websocket errors.
|
||||
"""
|
||||
|
||||
|
||||
class ProtocolError(WebSocketError):
|
||||
"""
|
||||
Raised if an error occurs when de/encoding the websocket protocol.
|
||||
"""
|
||||
|
||||
|
||||
class FrameTooLargeException(ProtocolError):
|
||||
"""
|
||||
Raised if a frame is received that is too large.
|
||||
"""
|
0
src/lib/geventwebsocket/gunicorn/__init__.py
Normal file
0
src/lib/geventwebsocket/gunicorn/__init__.py
Normal file
6
src/lib/geventwebsocket/gunicorn/workers.py
Normal file
6
src/lib/geventwebsocket/gunicorn/workers.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from geventwebsocket.handler import WebSocketHandler
|
||||
from gunicorn.workers.ggevent import GeventPyWSGIWorker
|
||||
|
||||
|
||||
class GeventWebSocketWorker(GeventPyWSGIWorker):
|
||||
wsgi_handler = WebSocketHandler
|
283
src/lib/geventwebsocket/handler.py
Normal file
283
src/lib/geventwebsocket/handler.py
Normal file
|
@ -0,0 +1,283 @@
|
|||
# Modified: Werkzeug Debugger workaround in run_websocket(self):
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import warnings
|
||||
|
||||
from gevent.pywsgi import WSGIHandler
|
||||
from .websocket import WebSocket, Stream
|
||||
from .logging import create_logger
|
||||
|
||||
|
||||
class Client(object):
|
||||
def __init__(self, address, ws):
|
||||
self.address = address
|
||||
self.ws = ws
|
||||
|
||||
|
||||
class WebSocketHandler(WSGIHandler):
|
||||
"""
|
||||
Automatically upgrades the connection to a websocket.
|
||||
|
||||
To prevent the WebSocketHandler to call the underlying WSGI application,
|
||||
but only setup the WebSocket negotiations, do:
|
||||
|
||||
mywebsockethandler.prevent_wsgi_call = True
|
||||
|
||||
before calling run_application(). This is useful if you want to do more
|
||||
things before calling the app, and want to off-load the WebSocket
|
||||
negotiations to this library. Socket.IO needs this for example, to send
|
||||
the 'ack' before yielding the control to your WSGI app.
|
||||
"""
|
||||
|
||||
SUPPORTED_VERSIONS = ('13', '8', '7')
|
||||
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
|
||||
def run_websocket(self):
|
||||
"""
|
||||
Called when a websocket has been created successfully.
|
||||
"""
|
||||
|
||||
if getattr(self, 'prevent_wsgi_call', False):
|
||||
return
|
||||
|
||||
# In case WebSocketServer is not used
|
||||
if not hasattr(self.server, 'clients'):
|
||||
self.server.clients = {}
|
||||
|
||||
# Since we're now a websocket connection, we don't care what the
|
||||
# application actually responds with for the http response
|
||||
|
||||
try:
|
||||
self.server.clients[self.client_address] = Client(
|
||||
self.client_address, self.websocket)
|
||||
if self.application.__class__.__name__ == "DebuggedApplication": # Modified: Werkzeug Debugger workaround (https://bitbucket.org/Jeffrey/gevent-websocket/issue/53/if-the-application-returns-a-generator-we)
|
||||
list(self.application(self.environ, lambda s, h: []))
|
||||
else:
|
||||
self.application(self.environ, lambda s, h: [])
|
||||
finally:
|
||||
del self.server.clients[self.client_address]
|
||||
if not self.websocket.closed:
|
||||
self.websocket.close()
|
||||
self.environ.update({
|
||||
'wsgi.websocket': None
|
||||
})
|
||||
self.websocket = None
|
||||
|
||||
def run_application(self):
|
||||
if (hasattr(self.server, 'pre_start_hook')
|
||||
and self.server.pre_start_hook):
|
||||
self.logger.debug("Calling pre-start hook")
|
||||
if self.server.pre_start_hook(self):
|
||||
return super(WebSocketHandler, self).run_application()
|
||||
|
||||
self.logger.debug("Initializing WebSocket")
|
||||
self.result = self.upgrade_websocket()
|
||||
|
||||
if hasattr(self, 'websocket'):
|
||||
if self.status and not self.headers_sent:
|
||||
self.write('')
|
||||
|
||||
self.run_websocket()
|
||||
else:
|
||||
if self.status:
|
||||
# A status was set, likely an error so just send the response
|
||||
if not self.result:
|
||||
self.result = []
|
||||
|
||||
self.process_result()
|
||||
return
|
||||
|
||||
# This handler did not handle the request, so defer it to the
|
||||
# underlying application object
|
||||
return super(WebSocketHandler, self).run_application()
|
||||
|
||||
def upgrade_websocket(self):
|
||||
"""
|
||||
Attempt to upgrade the current environ into a websocket enabled
|
||||
connection. If successful, the environ dict with be updated with two
|
||||
new entries, `wsgi.websocket` and `wsgi.websocket_version`.
|
||||
|
||||
:returns: Whether the upgrade was successful.
|
||||
"""
|
||||
|
||||
# Some basic sanity checks first
|
||||
|
||||
self.logger.debug("Validating WebSocket request")
|
||||
|
||||
if self.environ.get('REQUEST_METHOD', '') != 'GET':
|
||||
# This is not a websocket request, so we must not handle it
|
||||
self.logger.debug('Can only upgrade connection if using GET method.')
|
||||
return
|
||||
|
||||
upgrade = self.environ.get('HTTP_UPGRADE', '').lower()
|
||||
|
||||
if upgrade == 'websocket':
|
||||
connection = self.environ.get('HTTP_CONNECTION', '').lower()
|
||||
|
||||
if 'upgrade' not in connection:
|
||||
# This is not a websocket request, so we must not handle it
|
||||
self.logger.warning("Client didn't ask for a connection "
|
||||
"upgrade")
|
||||
return
|
||||
else:
|
||||
# This is not a websocket request, so we must not handle it
|
||||
return
|
||||
|
||||
if self.request_version != 'HTTP/1.1':
|
||||
self.start_response('402 Bad Request', [])
|
||||
self.logger.warning("Bad server protocol in headers")
|
||||
|
||||
return ['Bad protocol version']
|
||||
|
||||
if self.environ.get('HTTP_SEC_WEBSOCKET_VERSION'):
|
||||
return self.upgrade_connection()
|
||||
else:
|
||||
self.logger.warning("No protocol defined")
|
||||
self.start_response('426 Upgrade Required', [
|
||||
('Sec-WebSocket-Version', ', '.join(self.SUPPORTED_VERSIONS))])
|
||||
|
||||
return ['No Websocket protocol version defined']
|
||||
|
||||
def upgrade_connection(self):
|
||||
"""
|
||||
Validate and 'upgrade' the HTTP request to a WebSocket request.
|
||||
|
||||
If an upgrade succeeded then then handler will have `start_response`
|
||||
with a status of `101`, the environ will also be updated with
|
||||
`wsgi.websocket` and `wsgi.websocket_version` keys.
|
||||
|
||||
:param environ: The WSGI environ dict.
|
||||
:param start_response: The callable used to start the response.
|
||||
:param stream: File like object that will be read from/written to by
|
||||
the underlying WebSocket object, if created.
|
||||
:return: The WSGI response iterator is something went awry.
|
||||
"""
|
||||
|
||||
self.logger.debug("Attempting to upgrade connection")
|
||||
|
||||
version = self.environ.get("HTTP_SEC_WEBSOCKET_VERSION")
|
||||
|
||||
if version not in self.SUPPORTED_VERSIONS:
|
||||
msg = "Unsupported WebSocket Version: {0}".format(version)
|
||||
|
||||
self.logger.warning(msg)
|
||||
self.start_response('400 Bad Request', [
|
||||
('Sec-WebSocket-Version', ', '.join(self.SUPPORTED_VERSIONS))
|
||||
])
|
||||
|
||||
return [msg]
|
||||
|
||||
key = self.environ.get("HTTP_SEC_WEBSOCKET_KEY", '').strip()
|
||||
|
||||
if not key:
|
||||
# 5.2.1 (3)
|
||||
msg = "Sec-WebSocket-Key header is missing/empty"
|
||||
|
||||
self.logger.warning(msg)
|
||||
self.start_response('400 Bad Request', [])
|
||||
|
||||
return [msg]
|
||||
|
||||
try:
|
||||
key_len = len(base64.b64decode(key))
|
||||
except TypeError:
|
||||
msg = "Invalid key: {0}".format(key)
|
||||
|
||||
self.logger.warning(msg)
|
||||
self.start_response('400 Bad Request', [])
|
||||
|
||||
return [msg]
|
||||
|
||||
if key_len != 16:
|
||||
# 5.2.1 (3)
|
||||
msg = "Invalid key: {0}".format(key)
|
||||
|
||||
self.logger.warning(msg)
|
||||
self.start_response('400 Bad Request', [])
|
||||
|
||||
return [msg]
|
||||
|
||||
# Check for WebSocket Protocols
|
||||
requested_protocols = self.environ.get(
|
||||
'HTTP_SEC_WEBSOCKET_PROTOCOL', '')
|
||||
protocol = None
|
||||
|
||||
if hasattr(self.application, 'app_protocol'):
|
||||
allowed_protocol = self.application.app_protocol(
|
||||
self.environ['PATH_INFO'])
|
||||
|
||||
if allowed_protocol and allowed_protocol in requested_protocols:
|
||||
protocol = allowed_protocol
|
||||
self.logger.debug("Protocol allowed: {0}".format(protocol))
|
||||
|
||||
self.websocket = WebSocket(self.environ, Stream(self), self)
|
||||
self.environ.update({
|
||||
'wsgi.websocket_version': version,
|
||||
'wsgi.websocket': self.websocket
|
||||
})
|
||||
|
||||
headers = [
|
||||
("Upgrade", "websocket"),
|
||||
("Connection", "Upgrade"),
|
||||
("Sec-WebSocket-Accept", base64.b64encode(
|
||||
hashlib.sha1(key + self.GUID).digest())),
|
||||
]
|
||||
|
||||
if protocol:
|
||||
headers.append(("Sec-WebSocket-Protocol", protocol))
|
||||
|
||||
self.logger.debug("WebSocket request accepted, switching protocols")
|
||||
self.start_response("101 Switching Protocols", headers)
|
||||
|
||||
@property
|
||||
def logger(self):
|
||||
if not hasattr(self.server, 'logger'):
|
||||
self.server.logger = create_logger(__name__)
|
||||
|
||||
return self.server.logger
|
||||
|
||||
def log_request(self):
|
||||
if '101' not in self.status:
|
||||
self.logger.info(self.format_request())
|
||||
|
||||
@property
|
||||
def active_client(self):
|
||||
return self.server.clients[self.client_address]
|
||||
|
||||
def start_response(self, status, headers, exc_info=None):
|
||||
"""
|
||||
Called when the handler is ready to send a response back to the remote
|
||||
endpoint. A websocket connection may have not been created.
|
||||
"""
|
||||
writer = super(WebSocketHandler, self).start_response(
|
||||
status, headers, exc_info=exc_info)
|
||||
|
||||
self._prepare_response()
|
||||
|
||||
return writer
|
||||
|
||||
def _prepare_response(self):
|
||||
"""
|
||||
Sets up the ``pywsgi.Handler`` to work with a websocket response.
|
||||
|
||||
This is used by other projects that need to support WebSocket
|
||||
connections as part of a larger effort.
|
||||
"""
|
||||
assert not self.headers_sent
|
||||
|
||||
if not self.environ.get('wsgi.websocket'):
|
||||
# a WebSocket connection is not established, do nothing
|
||||
return
|
||||
|
||||
# So that `finalize_headers` doesn't write a Content-Length header
|
||||
self.provided_content_length = False
|
||||
|
||||
# The websocket is now controlling the response
|
||||
self.response_use_chunked = False
|
||||
|
||||
# Once the request is over, the connection must be closed
|
||||
self.close_connection = True
|
||||
|
||||
# Prevents the Date header from being written
|
||||
self.provided_date = True
|
31
src/lib/geventwebsocket/logging.py
Normal file
31
src/lib/geventwebsocket/logging.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
from logging import getLogger, StreamHandler, getLoggerClass, Formatter, DEBUG
|
||||
|
||||
|
||||
def create_logger(name, debug=False, format=None):
|
||||
Logger = getLoggerClass()
|
||||
|
||||
class DebugLogger(Logger):
|
||||
def getEffectiveLevel(x):
|
||||
if x.level == 0 and debug:
|
||||
return DEBUG
|
||||
else:
|
||||
return Logger.getEffectiveLevel(x)
|
||||
|
||||
class DebugHandler(StreamHandler):
|
||||
def emit(x, record):
|
||||
StreamHandler.emit(x, record) if debug else None
|
||||
|
||||
handler = DebugHandler()
|
||||
handler.setLevel(DEBUG)
|
||||
|
||||
if format:
|
||||
handler.setFormatter(Formatter(format))
|
||||
|
||||
logger = getLogger(name)
|
||||
del logger.handlers[:]
|
||||
logger.__class__ = DebugLogger
|
||||
logger.addHandler(handler)
|
||||
|
||||
return logger
|
0
src/lib/geventwebsocket/protocols/__init__.py
Normal file
0
src/lib/geventwebsocket/protocols/__init__.py
Normal file
35
src/lib/geventwebsocket/protocols/base.py
Normal file
35
src/lib/geventwebsocket/protocols/base.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
class BaseProtocol(object):
|
||||
PROTOCOL_NAME = ''
|
||||
|
||||
def __init__(self, app):
|
||||
self._app = app
|
||||
|
||||
def on_open(self):
|
||||
self.app.on_open()
|
||||
|
||||
def on_message(self, message):
|
||||
self.app.on_message(message)
|
||||
|
||||
def on_close(self, reason=None):
|
||||
self.app.on_close(reason)
|
||||
|
||||
@property
|
||||
def app(self):
|
||||
if self._app:
|
||||
return self._app
|
||||
else:
|
||||
raise Exception("No application coupled")
|
||||
|
||||
@property
|
||||
def server(self):
|
||||
if not hasattr(self.app, 'ws'):
|
||||
return None
|
||||
|
||||
return self.app.ws.handler.server
|
||||
|
||||
@property
|
||||
def handler(self):
|
||||
if not hasattr(self.app, 'ws'):
|
||||
return None
|
||||
|
||||
return self.app.ws.handler
|
234
src/lib/geventwebsocket/protocols/wamp.py
Normal file
234
src/lib/geventwebsocket/protocols/wamp.py
Normal file
|
@ -0,0 +1,234 @@
|
|||
import inspect
|
||||
import random
|
||||
import string
|
||||
import types
|
||||
|
||||
try:
|
||||
import ujson as json
|
||||
except ImportError:
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
from ..exceptions import WebSocketError
|
||||
from .base import BaseProtocol
|
||||
|
||||
|
||||
def export_rpc(arg=None):
|
||||
if isinstance(arg, types.FunctionType):
|
||||
arg._rpc = arg.__name__
|
||||
return arg
|
||||
|
||||
|
||||
def serialize(data):
|
||||
return json.dumps(data)
|
||||
|
||||
|
||||
class Prefixes(object):
|
||||
def __init__(self):
|
||||
self.prefixes = {}
|
||||
|
||||
def add(self, prefix, uri):
|
||||
self.prefixes[prefix] = uri
|
||||
|
||||
def resolve(self, curie_or_uri):
|
||||
if "http://" in curie_or_uri:
|
||||
return curie_or_uri
|
||||
elif ':' in curie_or_uri:
|
||||
prefix, proc = curie_or_uri.split(':', 1)
|
||||
return self.prefixes[prefix] + proc
|
||||
else:
|
||||
raise Exception(curie_or_uri)
|
||||
|
||||
|
||||
class RemoteProcedures(object):
|
||||
def __init__(self):
|
||||
self.calls = {}
|
||||
|
||||
def register_procedure(self, uri, proc):
|
||||
self.calls[uri] = proc
|
||||
|
||||
def register_object(self, uri, obj):
|
||||
for k in inspect.getmembers(obj, inspect.ismethod):
|
||||
if '_rpc' in k[1].__dict__:
|
||||
proc_uri = uri + k[1]._rpc
|
||||
self.calls[proc_uri] = (obj, k[1])
|
||||
|
||||
def call(self, uri, args):
|
||||
if uri in self.calls:
|
||||
proc = self.calls[uri]
|
||||
|
||||
# Do the correct call whether it's a function or instance method.
|
||||
if isinstance(proc, tuple):
|
||||
if proc[1].__self__ is None:
|
||||
# Create instance of object and call method
|
||||
return proc[1](proc[0](), *args)
|
||||
else:
|
||||
# Call bound method on instance
|
||||
return proc[1](*args)
|
||||
else:
|
||||
return self.calls[uri](*args)
|
||||
else:
|
||||
raise Exception("no such uri '{}'".format(uri))
|
||||
|
||||
|
||||
class Channels(object):
|
||||
def __init__(self):
|
||||
self.channels = {}
|
||||
|
||||
def create(self, uri, prefix_matching=False):
|
||||
if uri not in self.channels:
|
||||
self.channels[uri] = []
|
||||
|
||||
# TODO: implement prefix matching
|
||||
|
||||
def subscribe(self, uri, client):
|
||||
if uri in self.channels:
|
||||
self.channels[uri].append(client)
|
||||
|
||||
def unsubscribe(self, uri, client):
|
||||
if uri not in self.channels:
|
||||
return
|
||||
|
||||
client_index = self.channels[uri].index(client)
|
||||
self.channels[uri].pop(client_index)
|
||||
|
||||
if len(self.channels[uri]) == 0:
|
||||
del self.channels[uri]
|
||||
|
||||
def publish(self, uri, event, exclude=None, eligible=None):
|
||||
if uri not in self.channels:
|
||||
return
|
||||
|
||||
# TODO: exclude & eligible
|
||||
|
||||
msg = [WampProtocol.MSG_EVENT, uri, event]
|
||||
|
||||
for client in self.channels[uri]:
|
||||
try:
|
||||
client.ws.send(serialize(msg))
|
||||
except WebSocketError:
|
||||
# Seems someone didn't unsubscribe before disconnecting
|
||||
self.channels[uri].remove(client)
|
||||
|
||||
|
||||
class WampProtocol(BaseProtocol):
|
||||
MSG_WELCOME = 0
|
||||
MSG_PREFIX = 1
|
||||
MSG_CALL = 2
|
||||
MSG_CALL_RESULT = 3
|
||||
MSG_CALL_ERROR = 4
|
||||
MSG_SUBSCRIBE = 5
|
||||
MSG_UNSUBSCRIBE = 6
|
||||
MSG_PUBLISH = 7
|
||||
MSG_EVENT = 8
|
||||
|
||||
PROTOCOL_NAME = "wamp"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.procedures = RemoteProcedures()
|
||||
self.prefixes = Prefixes()
|
||||
self.session_id = ''.join(
|
||||
[random.choice(string.digits + string.letters)
|
||||
for i in xrange(16)])
|
||||
|
||||
super(WampProtocol, self).__init__(*args, **kwargs)
|
||||
|
||||
def register_procedure(self, *args, **kwargs):
|
||||
self.procedures.register_procedure(*args, **kwargs)
|
||||
|
||||
def register_object(self, *args, **kwargs):
|
||||
self.procedures.register_object(*args, **kwargs)
|
||||
|
||||
def register_pubsub(self, *args, **kwargs):
|
||||
if not hasattr(self.server, 'channels'):
|
||||
self.server.channels = Channels()
|
||||
|
||||
self.server.channels.create(*args, **kwargs)
|
||||
|
||||
def do_handshake(self):
|
||||
from geventwebsocket import get_version
|
||||
|
||||
welcome = [
|
||||
self.MSG_WELCOME,
|
||||
self.session_id,
|
||||
1,
|
||||
'gevent-websocket/' + get_version()
|
||||
]
|
||||
self.app.ws.send(serialize(welcome))
|
||||
|
||||
def _get_exception_info(self, e):
|
||||
uri = 'http://TODO#generic'
|
||||
desc = str(type(e))
|
||||
details = str(e)
|
||||
return [uri, desc, details]
|
||||
|
||||
def rpc_call(self, data):
|
||||
call_id, curie_or_uri = data[1:3]
|
||||
args = data[3:]
|
||||
|
||||
if not isinstance(call_id, (str, unicode)):
|
||||
raise Exception()
|
||||
if not isinstance(curie_or_uri, (str, unicode)):
|
||||
raise Exception()
|
||||
|
||||
uri = self.prefixes.resolve(curie_or_uri)
|
||||
|
||||
try:
|
||||
result = self.procedures.call(uri, args)
|
||||
result_msg = [self.MSG_CALL_RESULT, call_id, result]
|
||||
except Exception, e:
|
||||
result_msg = [self.MSG_CALL_ERROR,
|
||||
call_id] + self._get_exception_info(e)
|
||||
|
||||
self.app.on_message(serialize(result_msg))
|
||||
|
||||
def pubsub_action(self, data):
|
||||
action = data[0]
|
||||
curie_or_uri = data[1]
|
||||
|
||||
if not isinstance(action, int):
|
||||
raise Exception()
|
||||
if not isinstance(curie_or_uri, (str, unicode)):
|
||||
raise Exception()
|
||||
|
||||
uri = self.prefixes.resolve(curie_or_uri)
|
||||
|
||||
if action == self.MSG_SUBSCRIBE and len(data) == 2:
|
||||
self.server.channels.subscribe(data[1], self.handler.active_client)
|
||||
|
||||
elif action == self.MSG_UNSUBSCRIBE and len(data) == 2:
|
||||
self.server.channels.unsubscribe(
|
||||
data[1], self.handler.active_client)
|
||||
|
||||
elif action == self.MSG_PUBLISH and len(data) >= 3:
|
||||
payload = data[2] if len(data) >= 3 else None
|
||||
exclude = data[3] if len(data) >= 4 else None
|
||||
eligible = data[4] if len(data) >= 5 else None
|
||||
|
||||
self.server.channels.publish(uri, payload, exclude, eligible)
|
||||
|
||||
def on_open(self):
|
||||
self.app.on_open()
|
||||
self.do_handshake()
|
||||
|
||||
def on_message(self, message):
|
||||
data = json.loads(message)
|
||||
|
||||
if not isinstance(data, list):
|
||||
raise Exception('incoming data is no list')
|
||||
|
||||
if data[0] == self.MSG_PREFIX and len(data) == 3:
|
||||
prefix, uri = data[1:3]
|
||||
self.prefixes.add(prefix, uri)
|
||||
|
||||
elif data[0] == self.MSG_CALL and len(data) >= 3:
|
||||
return self.rpc_call(data)
|
||||
|
||||
elif data[0] in (self.MSG_SUBSCRIBE, self.MSG_UNSUBSCRIBE,
|
||||
self.MSG_PUBLISH):
|
||||
return self.pubsub_action(data)
|
||||
else:
|
||||
raise Exception("Unknown call")
|
||||
|
74
src/lib/geventwebsocket/resource.py
Normal file
74
src/lib/geventwebsocket/resource.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
import re
|
||||
|
||||
from .protocols.base import BaseProtocol
|
||||
from .exceptions import WebSocketError
|
||||
|
||||
|
||||
class WebSocketApplication(object):
|
||||
protocol_class = BaseProtocol
|
||||
|
||||
def __init__(self, ws):
|
||||
self.protocol = self.protocol_class(self)
|
||||
self.ws = ws
|
||||
|
||||
def handle(self):
|
||||
self.protocol.on_open()
|
||||
|
||||
while True:
|
||||
try:
|
||||
message = self.ws.receive()
|
||||
except WebSocketError:
|
||||
self.protocol.on_close()
|
||||
break
|
||||
|
||||
self.protocol.on_message(message)
|
||||
|
||||
def on_open(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def on_close(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def on_message(self, message, *args, **kwargs):
|
||||
self.ws.send(message, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def protocol_name(cls):
|
||||
return cls.protocol_class.PROTOCOL_NAME
|
||||
|
||||
|
||||
class Resource(object):
|
||||
def __init__(self, apps=None):
|
||||
self.apps = apps if apps else []
|
||||
|
||||
def _app_by_path(self, environ_path):
|
||||
# Which app matched the current path?
|
||||
|
||||
for path, app in self.apps.iteritems():
|
||||
if re.match(path, environ_path):
|
||||
return app
|
||||
|
||||
def app_protocol(self, path):
|
||||
app = self._app_by_path(path)
|
||||
|
||||
if hasattr(app, 'protocol_name'):
|
||||
return app.protocol_name()
|
||||
else:
|
||||
return ''
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
environ = environ
|
||||
current_app = self._app_by_path(environ['PATH_INFO'])
|
||||
|
||||
if current_app is None:
|
||||
raise Exception("No apps defined")
|
||||
|
||||
if 'wsgi.websocket' in environ:
|
||||
ws = environ['wsgi.websocket']
|
||||
current_app = current_app(ws)
|
||||
current_app.ws = ws # TODO: needed?
|
||||
current_app.handle()
|
||||
|
||||
return None
|
||||
else:
|
||||
return current_app(environ, start_response)
|
34
src/lib/geventwebsocket/server.py
Normal file
34
src/lib/geventwebsocket/server.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from gevent.pywsgi import WSGIServer
|
||||
|
||||
from .handler import WebSocketHandler
|
||||
from .logging import create_logger
|
||||
|
||||
|
||||
class WebSocketServer(WSGIServer):
|
||||
debug_log_format = (
|
||||
'-' * 80 + '\n' +
|
||||
'%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' +
|
||||
'%(message)s\n' +
|
||||
'-' * 80
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.debug = kwargs.pop('debug', False)
|
||||
self.pre_start_hook = kwargs.pop('pre_start_hook', None)
|
||||
self._logger = None
|
||||
self.clients = {}
|
||||
|
||||
kwargs['handler_class'] = WebSocketHandler
|
||||
super(WebSocketServer, self).__init__(*args, **kwargs)
|
||||
|
||||
def handle(self, socket, address):
|
||||
handler = self.handler_class(socket, address, self)
|
||||
handler.handle()
|
||||
|
||||
@property
|
||||
def logger(self):
|
||||
if not self._logger:
|
||||
self._logger = create_logger(
|
||||
__name__, self.debug, self.debug_log_format)
|
||||
|
||||
return self._logger
|
128
src/lib/geventwebsocket/utf8validator.py
Normal file
128
src/lib/geventwebsocket/utf8validator.py
Normal file
|
@ -0,0 +1,128 @@
|
|||
###############################################################################
|
||||
##
|
||||
## Copyright 2011-2013 Tavendo GmbH
|
||||
##
|
||||
## Note:
|
||||
##
|
||||
## This code is a Python implementation of the algorithm
|
||||
##
|
||||
## "Flexible and Economical UTF-8 Decoder"
|
||||
##
|
||||
## by Bjoern Hoehrmann
|
||||
##
|
||||
## bjoern@hoehrmann.de
|
||||
## http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
|
||||
##
|
||||
## Licensed under the Apache License, Version 2.0 (the "License");
|
||||
## you may not use this file except in compliance with the License.
|
||||
## You may obtain a copy of the License at
|
||||
##
|
||||
## http://www.apache.org/licenses/LICENSE-2.0
|
||||
##
|
||||
## Unless required by applicable law or agreed to in writing, software
|
||||
## distributed under the License is distributed on an "AS IS" BASIS,
|
||||
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
## See the License for the specific language governing permissions and
|
||||
## limitations under the License.
|
||||
##
|
||||
###############################################################################
|
||||
|
||||
|
||||
## use Cython implementation of UTF8 validator if available
|
||||
##
|
||||
try:
|
||||
from wsaccel.utf8validator import Utf8Validator
|
||||
except:
|
||||
## fallback to pure Python implementation
|
||||
|
||||
class Utf8Validator:
|
||||
"""
|
||||
Incremental UTF-8 validator with constant memory consumption (minimal
|
||||
state).
|
||||
|
||||
Implements the algorithm "Flexible and Economical UTF-8 Decoder" by
|
||||
Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/).
|
||||
"""
|
||||
|
||||
## DFA transitions
|
||||
UTF8VALIDATOR_DFA = [
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 00..1f
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 20..3f
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 40..5f
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 60..7f
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, # 80..9f
|
||||
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, # a0..bf
|
||||
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, # c0..df
|
||||
0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, # e0..ef
|
||||
0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, # f0..ff
|
||||
0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, # s0..s0
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, # s1..s2
|
||||
1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, # s3..s4
|
||||
1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, # s5..s6
|
||||
1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, # s7..s8
|
||||
]
|
||||
|
||||
UTF8_ACCEPT = 0
|
||||
UTF8_REJECT = 1
|
||||
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
|
||||
def decode(self, b):
|
||||
"""
|
||||
Eat one UTF-8 octet, and validate on the fly.
|
||||
|
||||
Returns UTF8_ACCEPT when enough octets have been consumed, in which case
|
||||
self.codepoint contains the decoded Unicode code point.
|
||||
|
||||
Returns UTF8_REJECT when invalid UTF-8 was encountered.
|
||||
|
||||
Returns some other positive integer when more octets need to be eaten.
|
||||
"""
|
||||
type = Utf8Validator.UTF8VALIDATOR_DFA[b]
|
||||
|
||||
if self.state != Utf8Validator.UTF8_ACCEPT:
|
||||
self.codepoint = (b & 0x3f) | (self.codepoint << 6)
|
||||
else:
|
||||
self.codepoint = (0xff >> type) & b
|
||||
|
||||
self.state = Utf8Validator.UTF8VALIDATOR_DFA[256 + self.state * 16 + type]
|
||||
|
||||
return self.state
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Reset validator to start new incremental UTF-8 decode/validation.
|
||||
"""
|
||||
self.state = Utf8Validator.UTF8_ACCEPT
|
||||
self.codepoint = 0
|
||||
self.i = 0
|
||||
|
||||
def validate(self, ba):
|
||||
"""
|
||||
Incrementally validate a chunk of bytes provided as string.
|
||||
|
||||
Will return a quad (valid?, endsOnCodePoint?, currentIndex, totalIndex).
|
||||
|
||||
As soon as an octet is encountered which renders the octet sequence
|
||||
invalid, a quad with valid? == False is returned. currentIndex returns
|
||||
the index within the currently consumed chunk, and totalIndex the
|
||||
index within the total consumed sequence that was the point of bail out.
|
||||
When valid? == True, currentIndex will be len(ba) and totalIndex the
|
||||
total amount of consumed bytes.
|
||||
"""
|
||||
|
||||
l = len(ba)
|
||||
|
||||
for i in xrange(l):
|
||||
## optimized version of decode(), since we are not interested in actual code points
|
||||
|
||||
self.state = Utf8Validator.UTF8VALIDATOR_DFA[256 + (self.state << 4) + Utf8Validator.UTF8VALIDATOR_DFA[ord(ba[i])]]
|
||||
|
||||
if self.state == Utf8Validator.UTF8_REJECT:
|
||||
self.i += i
|
||||
return False, False, i, self.i
|
||||
|
||||
self.i += l
|
||||
|
||||
return True, self.state == Utf8Validator.UTF8_ACCEPT, l, self.i
|
45
src/lib/geventwebsocket/utils.py
Normal file
45
src/lib/geventwebsocket/utils.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
import subprocess
|
||||
|
||||
|
||||
def get_version(version=None):
|
||||
"Returns a PEP 386-compliant version number from VERSION."
|
||||
|
||||
if version is None:
|
||||
from geventwebsocket import VERSION as version
|
||||
else:
|
||||
assert len(version) == 5
|
||||
assert version[3] in ('alpha', 'beta', 'rc', 'final')
|
||||
|
||||
# Now build the two parts of the version number:
|
||||
# main = X.Y[.Z]
|
||||
# sub = .devN - for pre-alpha releases
|
||||
# | {a|b|c}N - for alpha, beta and rc releases
|
||||
|
||||
parts = 2 if version[2] == 0 else 3
|
||||
main = '.'.join(str(x) for x in version[:parts])
|
||||
|
||||
sub = ''
|
||||
if version[3] == 'alpha' and version[4] == 0:
|
||||
hg_changeset = get_hg_changeset()
|
||||
if hg_changeset:
|
||||
sub = '.dev{0}'.format(hg_changeset)
|
||||
|
||||
elif version[3] != 'final':
|
||||
mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'}
|
||||
sub = mapping[version[3]] + str(version[4])
|
||||
|
||||
return str(main + sub)
|
||||
|
||||
|
||||
def get_hg_changeset():
|
||||
rev, err = subprocess.Popen(
|
||||
'hg id -i',
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE
|
||||
).communicate()
|
||||
|
||||
if err:
|
||||
return None
|
||||
else:
|
||||
return rev.strip().replace('+', '')
|
543
src/lib/geventwebsocket/websocket.py
Normal file
543
src/lib/geventwebsocket/websocket.py
Normal file
|
@ -0,0 +1,543 @@
|
|||
import struct
|
||||
|
||||
from socket import error
|
||||
|
||||
from .exceptions import ProtocolError
|
||||
from .exceptions import WebSocketError
|
||||
from .exceptions import FrameTooLargeException
|
||||
|
||||
from .utf8validator import Utf8Validator
|
||||
|
||||
|
||||
MSG_SOCKET_DEAD = "Socket is dead"
|
||||
MSG_ALREADY_CLOSED = "Connection is already closed"
|
||||
MSG_CLOSED = "Connection closed"
|
||||
|
||||
|
||||
class WebSocket(object):
|
||||
"""
|
||||
Base class for supporting websocket operations.
|
||||
|
||||
:ivar environ: The http environment referenced by this connection.
|
||||
:ivar closed: Whether this connection is closed/closing.
|
||||
:ivar stream: The underlying file like object that will be read from /
|
||||
written to by this WebSocket object.
|
||||
"""
|
||||
|
||||
__slots__ = ('utf8validator', 'utf8validate_last', 'environ', 'closed',
|
||||
'stream', 'raw_write', 'raw_read', 'handler')
|
||||
|
||||
OPCODE_CONTINUATION = 0x00
|
||||
OPCODE_TEXT = 0x01
|
||||
OPCODE_BINARY = 0x02
|
||||
OPCODE_CLOSE = 0x08
|
||||
OPCODE_PING = 0x09
|
||||
OPCODE_PONG = 0x0a
|
||||
|
||||
def __init__(self, environ, stream, handler):
|
||||
self.environ = environ
|
||||
self.closed = False
|
||||
|
||||
self.stream = stream
|
||||
|
||||
self.raw_write = stream.write
|
||||
self.raw_read = stream.read
|
||||
|
||||
self.utf8validator = Utf8Validator()
|
||||
self.handler = handler
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
self.close()
|
||||
except:
|
||||
# close() may fail if __init__ didn't complete
|
||||
pass
|
||||
|
||||
def _decode_bytes(self, bytestring):
|
||||
"""
|
||||
Internal method used to convert the utf-8 encoded bytestring into
|
||||
unicode.
|
||||
|
||||
If the conversion fails, the socket will be closed.
|
||||
"""
|
||||
|
||||
if not bytestring:
|
||||
return u''
|
||||
|
||||
try:
|
||||
return bytestring.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
self.close(1007)
|
||||
|
||||
raise
|
||||
|
||||
def _encode_bytes(self, text):
|
||||
"""
|
||||
:returns: The utf-8 byte string equivalent of `text`.
|
||||
"""
|
||||
|
||||
if isinstance(text, str):
|
||||
return text
|
||||
|
||||
if not isinstance(text, unicode):
|
||||
text = unicode(text or '')
|
||||
|
||||
return text.encode('utf-8')
|
||||
|
||||
def _is_valid_close_code(self, code):
|
||||
"""
|
||||
:returns: Whether the returned close code is a valid hybi return code.
|
||||
"""
|
||||
if code < 1000:
|
||||
return False
|
||||
|
||||
if 1004 <= code <= 1006:
|
||||
return False
|
||||
|
||||
if 1012 <= code <= 1016:
|
||||
return False
|
||||
|
||||
if code == 1100:
|
||||
# not sure about this one but the autobahn fuzzer requires it.
|
||||
return False
|
||||
|
||||
if 2000 <= code <= 2999:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def current_app(self):
|
||||
if hasattr(self.handler.server.application, 'current_app'):
|
||||
return self.handler.server.application.current_app
|
||||
else:
|
||||
# For backwards compatibility reasons
|
||||
class MockApp():
|
||||
def on_close(self, *args):
|
||||
pass
|
||||
|
||||
return MockApp()
|
||||
|
||||
@property
|
||||
def origin(self):
|
||||
if not self.environ:
|
||||
return
|
||||
|
||||
return self.environ.get('HTTP_ORIGIN')
|
||||
|
||||
@property
|
||||
def protocol(self):
|
||||
if not self.environ:
|
||||
return
|
||||
|
||||
return self.environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL')
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
if not self.environ:
|
||||
return
|
||||
|
||||
return self.environ.get('HTTP_SEC_WEBSOCKET_VERSION')
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
if not self.environ:
|
||||
return
|
||||
|
||||
return self.environ.get('PATH_INFO')
|
||||
|
||||
@property
|
||||
def logger(self):
|
||||
return self.handler.logger
|
||||
|
||||
def handle_close(self, header, payload):
|
||||
"""
|
||||
Called when a close frame has been decoded from the stream.
|
||||
|
||||
:param header: The decoded `Header`.
|
||||
:param payload: The bytestring payload associated with the close frame.
|
||||
"""
|
||||
if not payload:
|
||||
self.close(1000, None)
|
||||
|
||||
return
|
||||
|
||||
if len(payload) < 2:
|
||||
raise ProtocolError('Invalid close frame: {0} {1}'.format(
|
||||
header, payload))
|
||||
|
||||
code = struct.unpack('!H', str(payload[:2]))[0]
|
||||
payload = payload[2:]
|
||||
|
||||
if payload:
|
||||
validator = Utf8Validator()
|
||||
val = validator.validate(payload)
|
||||
|
||||
if not val[0]:
|
||||
raise UnicodeError
|
||||
|
||||
if not self._is_valid_close_code(code):
|
||||
raise ProtocolError('Invalid close code {0}'.format(code))
|
||||
|
||||
self.close(code, payload)
|
||||
|
||||
def handle_ping(self, header, payload):
|
||||
self.send_frame(payload, self.OPCODE_PONG)
|
||||
|
||||
def handle_pong(self, header, payload):
|
||||
pass
|
||||
|
||||
def read_frame(self):
|
||||
"""
|
||||
Block until a full frame has been read from the socket.
|
||||
|
||||
This is an internal method as calling this will not cleanup correctly
|
||||
if an exception is called. Use `receive` instead.
|
||||
|
||||
:return: The header and payload as a tuple.
|
||||
"""
|
||||
|
||||
header = Header.decode_header(self.stream)
|
||||
|
||||
if header.flags:
|
||||
raise ProtocolError
|
||||
|
||||
if not header.length:
|
||||
return header, ''
|
||||
|
||||
try:
|
||||
payload = self.raw_read(header.length)
|
||||
except error:
|
||||
payload = ''
|
||||
except Exception:
|
||||
# TODO log out this exception
|
||||
payload = ''
|
||||
|
||||
if len(payload) != header.length:
|
||||
raise WebSocketError('Unexpected EOF reading frame payload')
|
||||
|
||||
if header.mask:
|
||||
payload = header.unmask_payload(payload)
|
||||
|
||||
return header, payload
|
||||
|
||||
def validate_utf8(self, payload):
|
||||
# Make sure the frames are decodable independently
|
||||
self.utf8validate_last = self.utf8validator.validate(payload)
|
||||
|
||||
if not self.utf8validate_last[0]:
|
||||
raise UnicodeError("Encountered invalid UTF-8 while processing "
|
||||
"text message at payload octet index "
|
||||
"{0:d}".format(self.utf8validate_last[3]))
|
||||
|
||||
def read_message(self):
|
||||
"""
|
||||
Return the next text or binary message from the socket.
|
||||
|
||||
This is an internal method as calling this will not cleanup correctly
|
||||
if an exception is called. Use `receive` instead.
|
||||
"""
|
||||
opcode = None
|
||||
message = ""
|
||||
|
||||
while True:
|
||||
header, payload = self.read_frame()
|
||||
f_opcode = header.opcode
|
||||
|
||||
if f_opcode in (self.OPCODE_TEXT, self.OPCODE_BINARY):
|
||||
# a new frame
|
||||
if opcode:
|
||||
raise ProtocolError("The opcode in non-fin frame is "
|
||||
"expected to be zero, got "
|
||||
"{0!r}".format(f_opcode))
|
||||
|
||||
# Start reading a new message, reset the validator
|
||||
self.utf8validator.reset()
|
||||
self.utf8validate_last = (True, True, 0, 0)
|
||||
|
||||
opcode = f_opcode
|
||||
|
||||
elif f_opcode == self.OPCODE_CONTINUATION:
|
||||
if not opcode:
|
||||
raise ProtocolError("Unexpected frame with opcode=0")
|
||||
|
||||
elif f_opcode == self.OPCODE_PING:
|
||||
self.handle_ping(header, payload)
|
||||
continue
|
||||
|
||||
elif f_opcode == self.OPCODE_PONG:
|
||||
self.handle_pong(header, payload)
|
||||
continue
|
||||
|
||||
elif f_opcode == self.OPCODE_CLOSE:
|
||||
self.handle_close(header, payload)
|
||||
return
|
||||
|
||||
else:
|
||||
raise ProtocolError("Unexpected opcode={0!r}".format(f_opcode))
|
||||
|
||||
if opcode == self.OPCODE_TEXT:
|
||||
self.validate_utf8(payload)
|
||||
|
||||
message += payload
|
||||
|
||||
if header.fin:
|
||||
break
|
||||
|
||||
if opcode == self.OPCODE_TEXT:
|
||||
self.validate_utf8(message)
|
||||
return message
|
||||
else:
|
||||
return bytearray(message)
|
||||
|
||||
def receive(self):
|
||||
"""
|
||||
Read and return a message from the stream. If `None` is returned, then
|
||||
the socket is considered closed/errored.
|
||||
"""
|
||||
|
||||
if self.closed:
|
||||
self.current_app.on_close(MSG_ALREADY_CLOSED)
|
||||
raise WebSocketError(MSG_ALREADY_CLOSED)
|
||||
|
||||
try:
|
||||
return self.read_message()
|
||||
except UnicodeError:
|
||||
self.close(1007)
|
||||
except ProtocolError:
|
||||
self.close(1002)
|
||||
except error:
|
||||
self.close()
|
||||
self.current_app.on_close(MSG_CLOSED)
|
||||
|
||||
return None
|
||||
|
||||
def send_frame(self, message, opcode):
|
||||
"""
|
||||
Send a frame over the websocket with message as its payload
|
||||
"""
|
||||
if self.closed:
|
||||
self.current_app.on_close(MSG_ALREADY_CLOSED)
|
||||
raise WebSocketError(MSG_ALREADY_CLOSED)
|
||||
|
||||
if opcode == self.OPCODE_TEXT:
|
||||
message = self._encode_bytes(message)
|
||||
elif opcode == self.OPCODE_BINARY:
|
||||
message = str(message)
|
||||
|
||||
header = Header.encode_header(True, opcode, '', len(message), 0)
|
||||
|
||||
try:
|
||||
self.raw_write(header + message)
|
||||
except error:
|
||||
raise WebSocketError(MSG_SOCKET_DEAD)
|
||||
|
||||
def send(self, message, binary=None):
|
||||
"""
|
||||
Send a frame over the websocket with message as its payload
|
||||
"""
|
||||
if binary is None:
|
||||
binary = not isinstance(message, (str, unicode))
|
||||
|
||||
opcode = self.OPCODE_BINARY if binary else self.OPCODE_TEXT
|
||||
|
||||
try:
|
||||
self.send_frame(message, opcode)
|
||||
except WebSocketError:
|
||||
self.current_app.on_close(MSG_SOCKET_DEAD)
|
||||
raise WebSocketError(MSG_SOCKET_DEAD)
|
||||
|
||||
def close(self, code=1000, message=''):
|
||||
"""
|
||||
Close the websocket and connection, sending the specified code and
|
||||
message. The underlying socket object is _not_ closed, that is the
|
||||
responsibility of the initiator.
|
||||
"""
|
||||
|
||||
if self.closed:
|
||||
self.current_app.on_close(MSG_ALREADY_CLOSED)
|
||||
|
||||
try:
|
||||
message = self._encode_bytes(message)
|
||||
|
||||
self.send_frame(
|
||||
struct.pack('!H%ds' % len(message), code, message),
|
||||
opcode=self.OPCODE_CLOSE)
|
||||
except WebSocketError:
|
||||
# Failed to write the closing frame but it's ok because we're
|
||||
# closing the socket anyway.
|
||||
self.logger.debug("Failed to write closing frame -> closing socket")
|
||||
finally:
|
||||
self.logger.debug("Closed WebSocket")
|
||||
self.closed = True
|
||||
|
||||
self.stream = None
|
||||
self.raw_write = None
|
||||
self.raw_read = None
|
||||
|
||||
self.environ = None
|
||||
|
||||
#self.current_app.on_close(MSG_ALREADY_CLOSED)
|
||||
|
||||
|
||||
class Stream(object):
|
||||
"""
|
||||
Wraps the handler's socket/rfile attributes and makes it in to a file like
|
||||
object that can be read from/written to by the lower level websocket api.
|
||||
"""
|
||||
|
||||
__slots__ = ('handler', 'read', 'write')
|
||||
|
||||
def __init__(self, handler):
|
||||
self.handler = handler
|
||||
self.read = handler.rfile.read
|
||||
self.write = handler.socket.sendall
|
||||
|
||||
|
||||
class Header(object):
|
||||
__slots__ = ('fin', 'mask', 'opcode', 'flags', 'length')
|
||||
|
||||
FIN_MASK = 0x80
|
||||
OPCODE_MASK = 0x0f
|
||||
MASK_MASK = 0x80
|
||||
LENGTH_MASK = 0x7f
|
||||
|
||||
RSV0_MASK = 0x40
|
||||
RSV1_MASK = 0x20
|
||||
RSV2_MASK = 0x10
|
||||
|
||||
# bitwise mask that will determine the reserved bits for a frame header
|
||||
HEADER_FLAG_MASK = RSV0_MASK | RSV1_MASK | RSV2_MASK
|
||||
|
||||
def __init__(self, fin=0, opcode=0, flags=0, length=0):
|
||||
self.mask = ''
|
||||
self.fin = fin
|
||||
self.opcode = opcode
|
||||
self.flags = flags
|
||||
self.length = length
|
||||
|
||||
def mask_payload(self, payload):
|
||||
payload = bytearray(payload)
|
||||
mask = bytearray(self.mask)
|
||||
|
||||
for i in xrange(self.length):
|
||||
payload[i] ^= mask[i % 4]
|
||||
|
||||
return str(payload)
|
||||
|
||||
# it's the same operation
|
||||
unmask_payload = mask_payload
|
||||
|
||||
def __repr__(self):
|
||||
return ("<Header fin={0} opcode={1} length={2} flags={3} at "
|
||||
"0x{4:x}>").format(self.fin, self.opcode, self.length,
|
||||
self.flags, id(self))
|
||||
|
||||
@classmethod
|
||||
def decode_header(cls, stream):
|
||||
"""
|
||||
Decode a WebSocket header.
|
||||
|
||||
:param stream: A file like object that can be 'read' from.
|
||||
:returns: A `Header` instance.
|
||||
"""
|
||||
read = stream.read
|
||||
data = read(2)
|
||||
|
||||
if len(data) != 2:
|
||||
raise WebSocketError("Unexpected EOF while decoding header")
|
||||
|
||||
first_byte, second_byte = struct.unpack('!BB', data)
|
||||
|
||||
header = cls(
|
||||
fin=first_byte & cls.FIN_MASK == cls.FIN_MASK,
|
||||
opcode=first_byte & cls.OPCODE_MASK,
|
||||
flags=first_byte & cls.HEADER_FLAG_MASK,
|
||||
length=second_byte & cls.LENGTH_MASK)
|
||||
|
||||
has_mask = second_byte & cls.MASK_MASK == cls.MASK_MASK
|
||||
|
||||
if header.opcode > 0x07:
|
||||
if not header.fin:
|
||||
raise ProtocolError(
|
||||
"Received fragmented control frame: {0!r}".format(data))
|
||||
|
||||
# Control frames MUST have a payload length of 125 bytes or less
|
||||
if header.length > 125:
|
||||
raise FrameTooLargeException(
|
||||
"Control frame cannot be larger than 125 bytes: "
|
||||
"{0!r}".format(data))
|
||||
|
||||
if header.length == 126:
|
||||
# 16 bit length
|
||||
data = read(2)
|
||||
|
||||
if len(data) != 2:
|
||||
raise WebSocketError('Unexpected EOF while decoding header')
|
||||
|
||||
header.length = struct.unpack('!H', data)[0]
|
||||
elif header.length == 127:
|
||||
# 64 bit length
|
||||
data = read(8)
|
||||
|
||||
if len(data) != 8:
|
||||
raise WebSocketError('Unexpected EOF while decoding header')
|
||||
|
||||
header.length = struct.unpack('!Q', data)[0]
|
||||
|
||||
if has_mask:
|
||||
mask = read(4)
|
||||
|
||||
if len(mask) != 4:
|
||||
raise WebSocketError('Unexpected EOF while decoding header')
|
||||
|
||||
header.mask = mask
|
||||
|
||||
return header
|
||||
|
||||
@classmethod
|
||||
def encode_header(cls, fin, opcode, mask, length, flags):
|
||||
"""
|
||||
Encodes a WebSocket header.
|
||||
|
||||
:param fin: Whether this is the final frame for this opcode.
|
||||
:param opcode: The opcode of the payload, see `OPCODE_*`
|
||||
:param mask: Whether the payload is masked.
|
||||
:param length: The length of the frame.
|
||||
:param flags: The RSV* flags.
|
||||
:return: A bytestring encoded header.
|
||||
"""
|
||||
first_byte = opcode
|
||||
second_byte = 0
|
||||
extra = ''
|
||||
|
||||
if fin:
|
||||
first_byte |= cls.FIN_MASK
|
||||
|
||||
if flags & cls.RSV0_MASK:
|
||||
first_byte |= cls.RSV0_MASK
|
||||
|
||||
if flags & cls.RSV1_MASK:
|
||||
first_byte |= cls.RSV1_MASK
|
||||
|
||||
if flags & cls.RSV2_MASK:
|
||||
first_byte |= cls.RSV2_MASK
|
||||
|
||||
# now deal with length complexities
|
||||
if length < 126:
|
||||
second_byte += length
|
||||
elif length <= 0xffff:
|
||||
second_byte += 126
|
||||
extra = struct.pack('!H', length)
|
||||
elif length <= 0xffffffffffffffff:
|
||||
second_byte += 127
|
||||
extra = struct.pack('!Q', length)
|
||||
else:
|
||||
raise FrameTooLargeException
|
||||
|
||||
if mask:
|
||||
second_byte |= cls.MASK_MASK
|
||||
|
||||
extra += mask
|
||||
|
||||
return chr(first_byte) + chr(second_byte) + extra
|
23
src/lib/subtl/LICENCE
Normal file
23
src/lib/subtl/LICENCE
Normal file
|
@ -0,0 +1,23 @@
|
|||
Copyright (c) 2012, Packetloop. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of Packetloop 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 BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
28
src/lib/subtl/README.md
Normal file
28
src/lib/subtl/README.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
# subtl
|
||||
|
||||
## Overview
|
||||
|
||||
SUBTL is a **s**imple **U**DP **B**itTorrent **t**racker **l**ibrary for Python, licenced under the modified BSD license.
|
||||
|
||||
## Example
|
||||
|
||||
This short example will list a few IP Addresses from a certain hash:
|
||||
|
||||
from subtl import UdpTrackerClient
|
||||
utc = UdpTrackerClient('tracker.openbittorrent.com', 80)
|
||||
utc.connect()
|
||||
if not utc.poll_once():
|
||||
raise Exception('Could not connect')
|
||||
print('Success!')
|
||||
|
||||
utc.announce(info_hash='089184ED52AA37F71801391C451C5D5ADD0D9501')
|
||||
data = utc.poll_once()
|
||||
if not data:
|
||||
raise Exception('Could not announce')
|
||||
for a in data['response']['peers']:
|
||||
print(a)
|
||||
|
||||
## Caveats
|
||||
|
||||
* There is no automatic retrying of sending packets yet.
|
||||
* This library won't download torrent files--it is simply a tracker client.
|
0
src/lib/subtl/__init__.py
Normal file
0
src/lib/subtl/__init__.py
Normal file
220
src/lib/subtl/subtl.py
Normal file
220
src/lib/subtl/subtl.py
Normal file
|
@ -0,0 +1,220 @@
|
|||
'''
|
||||
Based on the specification at http://bittorrent.org/beps/bep_0015.html
|
||||
'''
|
||||
import random
|
||||
import struct
|
||||
import time
|
||||
import socket
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
__version__ = '0.0.1'
|
||||
|
||||
CONNECT = 0
|
||||
ANNOUNCE = 1
|
||||
SCRAPE = 2
|
||||
ERROR = 3
|
||||
|
||||
|
||||
def norm_info_hash(info_hash):
|
||||
if len(info_hash) == 40:
|
||||
info_hash = info_hash.decode('hex')
|
||||
if len(info_hash) != 20:
|
||||
raise UdpTrackerClientException(
|
||||
'info_hash length is not 20: {}'.format(len(info_hash)))
|
||||
return info_hash
|
||||
|
||||
|
||||
def info_hash_to_str(info_hash):
|
||||
return binascii.hexlify(info_hash)
|
||||
|
||||
|
||||
class UdpTrackerClientException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UdpTrackerClient:
|
||||
|
||||
def __init__(self, host, port):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.peer_port = 6881
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.conn_id = 0x41727101980
|
||||
self.transactions = {}
|
||||
self.peer_id = self._generate_peer_id()
|
||||
self.timeout = 2
|
||||
|
||||
def connect(self):
|
||||
return self._send(CONNECT)
|
||||
|
||||
def announce(self, **kwargs):
|
||||
if not kwargs:
|
||||
raise UdpTrackerClientException('arguments missing')
|
||||
args = {
|
||||
'peer_id': self.peer_id,
|
||||
'downloaded': 0,
|
||||
'left': 0,
|
||||
'uploaded': 0,
|
||||
'event': 0,
|
||||
'key': 0,
|
||||
'num_want': 10,
|
||||
'ip_address': 0,
|
||||
'port': self.peer_port,
|
||||
}
|
||||
args.update(kwargs)
|
||||
|
||||
fields = 'info_hash peer_id downloaded left uploaded event ' \
|
||||
'ip_address key num_want port'
|
||||
|
||||
# Check and raise if missing fields
|
||||
self._check_fields(args, fields)
|
||||
|
||||
# Humans tend to use hex representations of the hash. Wasteful humans.
|
||||
args['info_hash'] = norm_info_hash(args['info_hash'])
|
||||
|
||||
values = [args[a] for a in fields.split()]
|
||||
payload = struct.pack('!20s20sQQQLLLLH', *values)
|
||||
return self._send(ANNOUNCE, payload)
|
||||
|
||||
def scrape(self, info_hash_list):
|
||||
if len(info_hash_list) > 74:
|
||||
raise UdpTrackerClientException('Max info_hashes is 74')
|
||||
|
||||
payload = ''
|
||||
for info_hash in info_hash_list:
|
||||
info_hash = norm_info_hash(info_hash)
|
||||
payload += info_hash
|
||||
|
||||
trans = self._send(SCRAPE, payload)
|
||||
trans['sent_hashes'] = info_hash_list
|
||||
return trans
|
||||
|
||||
def poll_once(self):
|
||||
self.sock.settimeout(self.timeout)
|
||||
try:
|
||||
response = self.sock.recv(10240)
|
||||
except socket.timeout:
|
||||
return
|
||||
|
||||
header = response[:8]
|
||||
payload = response[8:]
|
||||
action, trans_id = struct.unpack('!LL', header)
|
||||
try:
|
||||
trans = self.transactions[trans_id]
|
||||
except KeyError:
|
||||
self.error('transaction_id not found')
|
||||
return
|
||||
trans['response'] = self._process_response(action, payload, trans)
|
||||
trans['completed'] = True
|
||||
del self.transactions[trans_id]
|
||||
return trans
|
||||
|
||||
def error(self, message):
|
||||
print('error: {}'.format(message))
|
||||
|
||||
def _send(self, action, payload=None):
|
||||
if not payload:
|
||||
payload = ''
|
||||
trans_id, header = self._request_header(action)
|
||||
self.transactions[trans_id] = trans = {
|
||||
'action': action,
|
||||
'time': time.time(),
|
||||
'payload': payload,
|
||||
'completed': False,
|
||||
}
|
||||
self.sock.sendto(header + payload, (self.host, self.port))
|
||||
return trans
|
||||
|
||||
def _request_header(self, action):
|
||||
trans_id = random.randint(0, (1 << 32) - 1)
|
||||
return trans_id, struct.pack('!QLL', self.conn_id, action, trans_id)
|
||||
|
||||
def _process_response(self, action, payload, trans):
|
||||
if action == CONNECT:
|
||||
return self._process_connect(payload, trans)
|
||||
elif action == ANNOUNCE:
|
||||
return self._process_announce(payload, trans)
|
||||
elif action == SCRAPE:
|
||||
return self._process_scrape(payload, trans)
|
||||
elif action == ERROR:
|
||||
return self._proecss_error(payload, trans)
|
||||
else:
|
||||
raise UdpTrackerClientException(
|
||||
'Unknown action response: {}'.format(action))
|
||||
|
||||
def _process_connect(self, payload, trans):
|
||||
self.conn_id = struct.unpack('!Q', payload)[0]
|
||||
return self.conn_id
|
||||
|
||||
def _process_announce(self, payload, trans):
|
||||
response = {}
|
||||
|
||||
info_struct = '!LLL'
|
||||
info_size = struct.calcsize(info_struct)
|
||||
info = payload[:info_size]
|
||||
interval, leechers, seeders = struct.unpack(info_struct, info)
|
||||
|
||||
peer_data = payload[info_size:]
|
||||
peer_struct = '!LH'
|
||||
peer_size = struct.calcsize(peer_struct)
|
||||
peer_count = len(peer_data) / peer_size
|
||||
peers = []
|
||||
|
||||
for peer_offset in xrange(peer_count):
|
||||
off = peer_size * peer_offset
|
||||
peer = peer_data[off:off + peer_size]
|
||||
addr, port = struct.unpack(peer_struct, peer)
|
||||
peers.append({
|
||||
'addr': socket.inet_ntoa(struct.pack('!L', addr)),
|
||||
'port': port,
|
||||
})
|
||||
|
||||
return {
|
||||
'interval': interval,
|
||||
'leechers': leechers,
|
||||
'seeders': seeders,
|
||||
'peers': peers,
|
||||
}
|
||||
|
||||
def _process_scrape(self, payload, trans):
|
||||
info_struct = '!LLL'
|
||||
info_size = struct.calcsize(info_struct)
|
||||
info_count = len(payload) / info_size
|
||||
hashes = trans['sent_hashes']
|
||||
response = {}
|
||||
for info_offset in xrange(info_count):
|
||||
off = info_size * info_offset
|
||||
info = payload[off:off + info_size]
|
||||
seeders, completed, leechers = struct.unpack(info_struct, info)
|
||||
response[hashes[info_offset]] = {
|
||||
'seeders': seeders,
|
||||
'completed': completed,
|
||||
'leechers': leechers,
|
||||
}
|
||||
return response
|
||||
|
||||
def _process_error(self, payload, trans):
|
||||
'''
|
||||
I haven't seen this action type be sent from a tracker, but I've left
|
||||
it here for the possibility.
|
||||
'''
|
||||
self.error(payload)
|
||||
return payload
|
||||
|
||||
def _generate_peer_id(self):
|
||||
'''http://www.bittorrent.org/beps/bep_0020.html'''
|
||||
peer_id = '-PU' + __version__.replace('.', '-') + '-'
|
||||
remaining = 20 - len(peer_id)
|
||||
numbers = [str(random.randint(0, 9)) for _ in xrange(remaining)]
|
||||
peer_id += ''.join(numbers)
|
||||
assert(len(peer_id) == 20)
|
||||
return peer_id
|
||||
|
||||
def _check_fields(self, args, fields):
|
||||
for f in fields:
|
||||
try:
|
||||
args.get(f)
|
||||
except KeyError:
|
||||
raise UdpTrackerClientException('field missing: {}'.format(f))
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue