eshpilen
پنج شنبه 30 دی 1389, 19:24 عصر
این پروژه رو بعنوان قسمتی ضروری از یک پروژهء ارتباط P2P تهیه کردم. در واقع یک زیرپروژه هست. اما بنظرم چیز خوبی از آب درآمد و کار مفیدی بود؛ بخاطر همین بصورت شیء گرا و تر و تمیز درآوردم و در اینجا اون رو ارائه میکنم.
الگوریتمی که در اینجا ارائه میشه یک کلاینت برای پروتکل STUN هست. STUN پروتکلی هست که برای گرفتن آدرس و پورت عمومی قابل استفاده برای یک برنامه استفاده میشه. وقتی که برنامه پشت NAT قرار داره (دقت کنید که اکثر مودمهای ADSL هم بخاطر تنظیم بودن روی حالت PPPoE، یک NAT رو پیاده سازی میکنن)، آدرس محلی و آدرس عمومی اون متفاوت هست و از آدرس عمومی خودش اطلاعی نداره. برای برقراری ارتباط P2P ما نیاز داریم که آدرس عمومی برنامه (IP و Port) رو پیشاپیش بدست بیاریم و بعد اون آدرس رو از راه جانبی ای در اختیار برنامهء دیگر که میخواد با برنامه ارتباط مستقیم (بدون سرور واسط) برقرار کنه قرار بدیم. کار پروتکل STUN صرفا اطلاع دادن IP و پورت خارجی برنامه بهش هست.
در برنامه های P2P از پروتکل UDP بخاطر بعضی خصوصیاتش و امکان موفقیت زیادی که در برقرار ارتباط دو طرفه داره زیاد استفاده میشه. البته این پروتکل در کاربردهایی مثل صدا و تصویر زنده از طریق اینترنت هم کاربرد زیادی داره و بنابراین از دو جهت مورد نیاز و استفاده هست.
ما این کار رو (گرفتن پیشاپیش آدرس و پورت خارجی قابل استفاده توسط برنامه) نمیتونیم با سرورهای معمولی وب انجام بدیم. بخاطر همین از یک سرور STUN استفاده میکنیم. در کلاسی که بنده ساختم تعدادی سرور STUN عمومی/مجانی تعریف شدن که میتونید براحتی از اونها استفاده کنید، اما اگر بخواید یا نیاز بود میتونید آدرس یا پورت دیگری رو هم برای برقراری ارتباط معرفی کنید.
ناگفته نماند که پیاده سازی الگوریتمی که تمامی امکانات و حالتهای پروتکل و سرورهای STUN رو پشتیبانی بکنه از پروژه ای که بنده انجام دادم پیچیده تر و خیلی حجیم تر هست؛ بنده نوعی رو پیاده کردم که برای کاربرد بنده کافی بود؛ یعنی یک ارتباط UDP رمزگذاری نشده؛ و فکر میکنم در خیلی از کاربردهای دیگر هم متداول هست و کاملا کفایت میکنه.
پروتکل STUN ابتدا در RFC 3489 تعریف شد (تاریخش رو 2003 زده)، اما بعدا (2008) RFC 5389 جایگزین اون شده که تغییرات و اصلاحاتی و اعمال کرد. البته این دو RFC تاحد زیادی با هم سازگار نگه داشته شدن. اگر به موردی برخورد کردید که الگوریتم بنده با سرور STUN خاصی کار نمیکرد، خوبه که بهم اطلاع بدید. بنده این الگوریتم رو اساسا با مطالعهء RFC 5389 تهیه کردم؛ بنظرم کم و بیش سازگاری خوبی داره، و فکر میکنم با سرورهای RFC 3489 هم باید بدون مشکل کار کنه. البته یک مورد خاص بود که در RFC 5389 نیامده بود که چون با تست و بررسی فهمیدم که سرورهای مورد نظر دارن از اون روش و کد خاص برای ارسال آدرس XOR شده استفاده میکنن، اون رو به برنامه اضافه کردم.
خب دیگه توضیح کافیه و میریم سراغ کدنویسی.
این کل کلاس و الگوریتم پیاده شده هست:
# this a simple and ad-hoc STUN client (UDP), partially compliant with RFC5389
# it gets the public(reflective) IP and Port of a UDP socket
# written in Python v3.1 by hamidreza_mz -=At=- yahoo -=Dot=- com
# version: 0.5
# license: GPLv2 or any newer version
import socket
import os
import struct
import time
class STUNClient:
# the following is a list of some public/free stun servers
# some of them send the trasport address as both MAPPED-ADDRESS and XOR-MAPPED-ADDRESS -
# and others send only MAPPED-ADDRESS
stun_servers_list = (
'stun.xten.com', # 0
'stun01.sipphone.com', # 1
'stunserver.org', # 2
'stun.ideasip.com', # 3 - no XOR-MAPPED-ADDRESS
'stun.softjoys.com', # 4 - no XOR-MAPPED-ADDRESS
'stun.voipbuster.com', # 5 - no XOR-MAPPED-ADDRESS
'stun.voxgratia.org', # 6
'numb.viagenie.ca', # 7
'stun.sipgate.net' # 8 - ports 3478 & 10000
)
# for explanations about the following variables see the section 7.2.1 of RFC5389
rc=7 # maximum number of the requests to send
rm=16 # used for calculating the last receive timeout
rto=0.5 # Retransmission TimeOut
stun_server=None
# whether debugging messages are printed or not
print_debug_msgs=False
def __init__(self, server=0, port=3478):
if type(server)==int:
self.stun_server=(self.stun_servers_list[server], port)
else:
self.stun_server=(server, port)
def show_binary_data(self, title, data):
if not self.print_debug_msgs: return
if title:
print(title, end='')
for b in data:
print('\\x{0:02X}'.format(b), end='')
print()
def myprint(self, msg=''):
if not self.print_debug_msgs: return
print(msg)
def extract_ip(self, binary):
ip=struct.unpack('BBBB', binary)
ip=str(ip[0])+'.'+str(ip[1])+'.'+str(ip[2])+'.'+str(ip[3])
return ip
def extract_port(self, binary):
return struct.unpack('!H', binary)[0]
def get_public_address_of_udp_socket(self, udp_socket):
self.myprint('STUN server: {0}:{1}'.format(self.stun_server[0], self.stun_server[1]))
timeout_save=udp_socket.gettimeout()
try:
udp_socket.settimeout(0)
try: udp_socket.recv(1280)
except: pass
maddr=b'' # mapped ip
mport=b'' # mapped port
xmaddr=b'' # xor mapped ip
xmport=b'' # xor mapped port
msg_type=b'\x00\x01' # STUN binding request
body_length=b'\x00\x00' # we have/need no attributes, so message body length is zero
magic_cookie=b'\x21\x12\xA4\x42'
transaction_id=os.urandom(12)
stun_request=msg_type+body_length+magic_cookie+tra nsaction_id
self.show_binary_data('\nSTUN request: ', stun_request)
tout=self.rto/2 # since it is multiplied by 2 in the for loop, the first timeout will be equal to rto
for r in range(0, self.rc):
udp_socket.sendto(stun_request, self.stun_server)
self.myprint("\n{0}th STUN request sent.".format(r+1))
# the following timeout calculation algorithm is according to the section 7.2.1 of RFC5389
if r<self.rc-1:
tout*=2
else:
tout=self.rm*self.rto
remaining_time=tout
outer_countinue=False
while remaining_time > 0:
udp_socket.settimeout(remaining_time)
try:
self.myprint('waiting for response {0} sec(s)...'.format(remaining_time))
time1=time.time()
data=udp_socket.recv(1280) # this client has no IPv6 support yet, but enough data can be received here
except Exception as e:
if type(e)==socket.timeout:
self.myprint("timeout exception occured in receiving STUN response: "+str(e))
outer_countinue=True
break
elif type(e)==socket.error and hasattr(e, 'errno') and e.errno==10040:
self.myprint('socket.error 10040 (data too big) occured.')
continue
else: raise
finally:
time2=time.time()
remaining_time-=(time2-time1)
self.show_binary_data('\nUDP data received:\n', data)
if(len(data)<20):
self.myprint('length too short. (not a STUN response)')
# because we received an irrelevant udp packet that most probably caused the current recv to -
# terminate prematurely, we continue the inner loop (waiting for receiving a STUN response)
continue
if data[4:20]!=magic_cookie+transaction_id:
self.myprint('magic cookie and/or transaction id check failed!')
# because we received an irrelevant udp packet that most probably caused the current recv to -
# terminate prematurely, we continue the inner loop (waiting for receiving a STUN response)
continue
break # break the inner (waiting for receiving a STUN response) loop
if outer_countinue: continue # recv timeout occured in the inner loop; so we countinue the main loop
if data[0:2]!=b'\x01\x01':
raise Exception('a non-success STUN response received.')
response_body_length=struct.unpack('!h', data[2:4])[0]
self.myprint()
i=20 # current reading position in the response binary data
while i < response_body_length+20: # proccessing the response
self.show_binary_data('STUN attribute in response: ', data[i:i+2])
if data[i:i+2]==b'\00\01': # MAPPED-ADDRESS
maddr_start_pos=i+2+2+1+1
mport=data[maddr_start_pos:maddr_start_pos+2]
maddr=data[maddr_start_pos+2:maddr_start_pos+2+4]
if data[i:i+2]==b'\x80\x20' or data[i:i+2]==b'\x00\x20':
# apparently, all public stun servers tested use 0x8020 (in the Comprehension-optional range) -
# as the XOR-MAPPED-ADDRESS Attribute type number instead of 0x0020 specified in RFC5389
xmaddr_start_pos=i+2+2+1+1
xmport=data[xmaddr_start_pos:xmaddr_start_pos+2]
xmaddr=data[xmaddr_start_pos+2:xmaddr_start_pos+2+4]
i+=2
attrib_value_length=struct.unpack('!h', data[i:i+2])[0]
if attrib_value_length%4:
attrib_value_length+=4-(attrib_value_length%4) # adds stun attribute value padding
i+=2
i+=attrib_value_length
break # quit the STUN request loop
if maddr:
self.show_binary_data('\nMAPPED-ADDRESS: ', maddr)
self.show_binary_data('mport: ', mport)
else:
self.myprint('\nno MAPPED-ADDRESS found.')
if xmaddr:
n=struct.unpack('!I', xmaddr)[0]
m=struct.unpack('!I', magic_cookie)[0]
n=n^m
self.show_binary_data('\nXOR-MAPPED-ADDRESS: ', struct.pack('!I', n))
n2=struct.unpack('!H', xmport)[0]
m2=struct.unpack('!H', magic_cookie[0:2])[0]
n2=n2^m2
self.show_binary_data('xmport: ', struct.pack('!H', n2))
else:
self.myprint('\nno XOR-MAPPED-ADDRESS found.')
ip=None
port=None
if xmaddr: # we must prefer using XOR-MAPPED-ADDRESS over MAPPED-ADDRESS
ip=self.extract_ip(struct.pack('!I', n))
port=self.extract_port(struct.pack('!H', n2))
elif maddr:
ip=self.extract_ip(maddr)
port=self.extract_port(mport)
else:
raise Exception('STUN query failed!')
finally: udp_socket.settimeout(timeout_save)
self.myprint('\n=======STUN========')
self.myprint('IP: {0}'.format(ip))
self.myprint('Port: {0}'.format(port))
self.myprint('===================')
return (ip, port)
کدهای بالا رو در فایلی بنام stun_client.py ذخیره کنید و در اختیار هر برنامهء پایتون دیگری قرار بدید که قراره ازش استفاده کنه.
و اینهم قطعه کد مثالی برای طرز استفاده:
import stun_client
import socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sc=stun_client.STUNClient()
sc.print_debug_msgs=True
try:
print(sc.get_public_address_of_udp_socket(udp_sock et))
except Exception as e:
print('\nAn exception occured in get_public_address_of_udp_socket:\n', e)
input("\nhit Enter to quit...")
به متد get_public_address_of_udp_socket سوکتی رو که میخواید برای ارتباط ازش استفاده کنید پاس میکنید و این متد بهتون IP و پورت خارجی اون Socket رو بصورت یک Tuple برمیگردونه؛ البته درصورت موفقیت. بخاطر خطاهای پروتکل و خطاهای سرور و کلاینت و کانکشن اینترنت و غیره، فراخوانی این متد رو در بلاک try قرار بدید و exception های ایجاد شده رو هندل کنید.
موقع ایجاد یک شیء از کلاس STUNClient میشه یک آدرس (IP یا نام دامین) و بعدش بصورت اختیاری یک پورت رو پاس کرد تا الگوریتم از اون سرور خاصی که شما معرفی میکنید استفاده کنه. پورت روی پورت پیشفرض پروتکل STUN هست و نیازی نیست شما پورت رو هم مشخص کنید، مگر اینکه پورت غیراستانداردی رو مجبور باشید یا به دلیل دیگری بخواید معرفی کنید.
ضمنا با پاس کردن صرفا یک عدد صحیح از صفر تا ایندکس آخرین سروری که در stun_servers_list در کلاس STUNClient تعریف شده که محتوی آدرس سرورهای عمومی/مجانی STUN هست، میتونید سرور مورد نظر برای برقراری ارتباط رو انتخاب کنید (اگر یک سرور جواب نمیداد، این روش راحتی برای تعویض سرور هست). پیشفرض سرور اول هست که ایندکس اون عدد صفر هست.
چنتا مثال میزنم تا روشن بشید:
STUNClient('stun.example.com')
STUNClient('stun.example.com', 10000)
STUNClient(5)
STUNClient(3, 40005)
دست آخر باید بگم نظر به اینکه هیچ نمونه کدی رو برای پیاده سازی این الگوریتم جایی مطالعه نکردم، نمیتونم بگم کدم ممکن نیست اشتباهی داشته باشه. اما تاجایی که تست کردم به خوبی کار میکنه.
بخاطر این این کد رو با پایتون نوشتم چون برای نوشتن برنامهء آزمایش ارتباط P2P خودم، پایتون رو بخاطر سادگی و خوانایی و سرعت توسعه انتخاب کردم. باید بگم علاوه بر ویژگیهای دیگر، اسکریپتی بودن این زبان باعث میشه بخصوص در برنامه های نمونهء اولیه (Prototype) و آزمایش الگوریتمهای مختلف و نو که نیاز به بارها تغییر و اجرا دارن، سرعت توسعه واقعا بالا بره و زحمت برنامه نویس کم بشه. خیلی ساده در یک ادیتور برنامه نویسی سبک هم میشه کدهای پایتون رو نوشت و تغییر داد و فقط Save کرد و بعدش هم فوراً اجرا!
البته پایتون زبانی نیست که بگیم کاربرد و مزایاش فقط Prototype و تست هست. بلکه بنظرم خیلی از برنامه ها رو هم میشه بصورت کامل و نهایی و حتی برای همیشه با پایتون نوشت و استفاده کرد.
با زبانهای دیگر مثل سی و سی++ حتما کتابخانهء آماده برای استفاده از پروتکل STUN وجود داره، اما با زبان پایتون خبر ندارم هست یا نه. بهرحال خواستم این رو خودم بنویسم چون بنظرم تجربهء خوبی بود و تحلیل و کدنویسیش باعث تقویتم میشد. خیلی وقته همش تئوری کار کردم و کدنویسی خیلی کم داشتم و دنبال اینطور پروژه های غیر حجیم ولی در عین حال جذاب، خلاق، و نو میگردم چون احساس میکنم باید با کار عملی آموخته های خودم رو تثبیت کنم و محک بزنم.
الگوریتمی که در اینجا ارائه میشه یک کلاینت برای پروتکل STUN هست. STUN پروتکلی هست که برای گرفتن آدرس و پورت عمومی قابل استفاده برای یک برنامه استفاده میشه. وقتی که برنامه پشت NAT قرار داره (دقت کنید که اکثر مودمهای ADSL هم بخاطر تنظیم بودن روی حالت PPPoE، یک NAT رو پیاده سازی میکنن)، آدرس محلی و آدرس عمومی اون متفاوت هست و از آدرس عمومی خودش اطلاعی نداره. برای برقراری ارتباط P2P ما نیاز داریم که آدرس عمومی برنامه (IP و Port) رو پیشاپیش بدست بیاریم و بعد اون آدرس رو از راه جانبی ای در اختیار برنامهء دیگر که میخواد با برنامه ارتباط مستقیم (بدون سرور واسط) برقرار کنه قرار بدیم. کار پروتکل STUN صرفا اطلاع دادن IP و پورت خارجی برنامه بهش هست.
در برنامه های P2P از پروتکل UDP بخاطر بعضی خصوصیاتش و امکان موفقیت زیادی که در برقرار ارتباط دو طرفه داره زیاد استفاده میشه. البته این پروتکل در کاربردهایی مثل صدا و تصویر زنده از طریق اینترنت هم کاربرد زیادی داره و بنابراین از دو جهت مورد نیاز و استفاده هست.
ما این کار رو (گرفتن پیشاپیش آدرس و پورت خارجی قابل استفاده توسط برنامه) نمیتونیم با سرورهای معمولی وب انجام بدیم. بخاطر همین از یک سرور STUN استفاده میکنیم. در کلاسی که بنده ساختم تعدادی سرور STUN عمومی/مجانی تعریف شدن که میتونید براحتی از اونها استفاده کنید، اما اگر بخواید یا نیاز بود میتونید آدرس یا پورت دیگری رو هم برای برقراری ارتباط معرفی کنید.
ناگفته نماند که پیاده سازی الگوریتمی که تمامی امکانات و حالتهای پروتکل و سرورهای STUN رو پشتیبانی بکنه از پروژه ای که بنده انجام دادم پیچیده تر و خیلی حجیم تر هست؛ بنده نوعی رو پیاده کردم که برای کاربرد بنده کافی بود؛ یعنی یک ارتباط UDP رمزگذاری نشده؛ و فکر میکنم در خیلی از کاربردهای دیگر هم متداول هست و کاملا کفایت میکنه.
پروتکل STUN ابتدا در RFC 3489 تعریف شد (تاریخش رو 2003 زده)، اما بعدا (2008) RFC 5389 جایگزین اون شده که تغییرات و اصلاحاتی و اعمال کرد. البته این دو RFC تاحد زیادی با هم سازگار نگه داشته شدن. اگر به موردی برخورد کردید که الگوریتم بنده با سرور STUN خاصی کار نمیکرد، خوبه که بهم اطلاع بدید. بنده این الگوریتم رو اساسا با مطالعهء RFC 5389 تهیه کردم؛ بنظرم کم و بیش سازگاری خوبی داره، و فکر میکنم با سرورهای RFC 3489 هم باید بدون مشکل کار کنه. البته یک مورد خاص بود که در RFC 5389 نیامده بود که چون با تست و بررسی فهمیدم که سرورهای مورد نظر دارن از اون روش و کد خاص برای ارسال آدرس XOR شده استفاده میکنن، اون رو به برنامه اضافه کردم.
خب دیگه توضیح کافیه و میریم سراغ کدنویسی.
این کل کلاس و الگوریتم پیاده شده هست:
# this a simple and ad-hoc STUN client (UDP), partially compliant with RFC5389
# it gets the public(reflective) IP and Port of a UDP socket
# written in Python v3.1 by hamidreza_mz -=At=- yahoo -=Dot=- com
# version: 0.5
# license: GPLv2 or any newer version
import socket
import os
import struct
import time
class STUNClient:
# the following is a list of some public/free stun servers
# some of them send the trasport address as both MAPPED-ADDRESS and XOR-MAPPED-ADDRESS -
# and others send only MAPPED-ADDRESS
stun_servers_list = (
'stun.xten.com', # 0
'stun01.sipphone.com', # 1
'stunserver.org', # 2
'stun.ideasip.com', # 3 - no XOR-MAPPED-ADDRESS
'stun.softjoys.com', # 4 - no XOR-MAPPED-ADDRESS
'stun.voipbuster.com', # 5 - no XOR-MAPPED-ADDRESS
'stun.voxgratia.org', # 6
'numb.viagenie.ca', # 7
'stun.sipgate.net' # 8 - ports 3478 & 10000
)
# for explanations about the following variables see the section 7.2.1 of RFC5389
rc=7 # maximum number of the requests to send
rm=16 # used for calculating the last receive timeout
rto=0.5 # Retransmission TimeOut
stun_server=None
# whether debugging messages are printed or not
print_debug_msgs=False
def __init__(self, server=0, port=3478):
if type(server)==int:
self.stun_server=(self.stun_servers_list[server], port)
else:
self.stun_server=(server, port)
def show_binary_data(self, title, data):
if not self.print_debug_msgs: return
if title:
print(title, end='')
for b in data:
print('\\x{0:02X}'.format(b), end='')
print()
def myprint(self, msg=''):
if not self.print_debug_msgs: return
print(msg)
def extract_ip(self, binary):
ip=struct.unpack('BBBB', binary)
ip=str(ip[0])+'.'+str(ip[1])+'.'+str(ip[2])+'.'+str(ip[3])
return ip
def extract_port(self, binary):
return struct.unpack('!H', binary)[0]
def get_public_address_of_udp_socket(self, udp_socket):
self.myprint('STUN server: {0}:{1}'.format(self.stun_server[0], self.stun_server[1]))
timeout_save=udp_socket.gettimeout()
try:
udp_socket.settimeout(0)
try: udp_socket.recv(1280)
except: pass
maddr=b'' # mapped ip
mport=b'' # mapped port
xmaddr=b'' # xor mapped ip
xmport=b'' # xor mapped port
msg_type=b'\x00\x01' # STUN binding request
body_length=b'\x00\x00' # we have/need no attributes, so message body length is zero
magic_cookie=b'\x21\x12\xA4\x42'
transaction_id=os.urandom(12)
stun_request=msg_type+body_length+magic_cookie+tra nsaction_id
self.show_binary_data('\nSTUN request: ', stun_request)
tout=self.rto/2 # since it is multiplied by 2 in the for loop, the first timeout will be equal to rto
for r in range(0, self.rc):
udp_socket.sendto(stun_request, self.stun_server)
self.myprint("\n{0}th STUN request sent.".format(r+1))
# the following timeout calculation algorithm is according to the section 7.2.1 of RFC5389
if r<self.rc-1:
tout*=2
else:
tout=self.rm*self.rto
remaining_time=tout
outer_countinue=False
while remaining_time > 0:
udp_socket.settimeout(remaining_time)
try:
self.myprint('waiting for response {0} sec(s)...'.format(remaining_time))
time1=time.time()
data=udp_socket.recv(1280) # this client has no IPv6 support yet, but enough data can be received here
except Exception as e:
if type(e)==socket.timeout:
self.myprint("timeout exception occured in receiving STUN response: "+str(e))
outer_countinue=True
break
elif type(e)==socket.error and hasattr(e, 'errno') and e.errno==10040:
self.myprint('socket.error 10040 (data too big) occured.')
continue
else: raise
finally:
time2=time.time()
remaining_time-=(time2-time1)
self.show_binary_data('\nUDP data received:\n', data)
if(len(data)<20):
self.myprint('length too short. (not a STUN response)')
# because we received an irrelevant udp packet that most probably caused the current recv to -
# terminate prematurely, we continue the inner loop (waiting for receiving a STUN response)
continue
if data[4:20]!=magic_cookie+transaction_id:
self.myprint('magic cookie and/or transaction id check failed!')
# because we received an irrelevant udp packet that most probably caused the current recv to -
# terminate prematurely, we continue the inner loop (waiting for receiving a STUN response)
continue
break # break the inner (waiting for receiving a STUN response) loop
if outer_countinue: continue # recv timeout occured in the inner loop; so we countinue the main loop
if data[0:2]!=b'\x01\x01':
raise Exception('a non-success STUN response received.')
response_body_length=struct.unpack('!h', data[2:4])[0]
self.myprint()
i=20 # current reading position in the response binary data
while i < response_body_length+20: # proccessing the response
self.show_binary_data('STUN attribute in response: ', data[i:i+2])
if data[i:i+2]==b'\00\01': # MAPPED-ADDRESS
maddr_start_pos=i+2+2+1+1
mport=data[maddr_start_pos:maddr_start_pos+2]
maddr=data[maddr_start_pos+2:maddr_start_pos+2+4]
if data[i:i+2]==b'\x80\x20' or data[i:i+2]==b'\x00\x20':
# apparently, all public stun servers tested use 0x8020 (in the Comprehension-optional range) -
# as the XOR-MAPPED-ADDRESS Attribute type number instead of 0x0020 specified in RFC5389
xmaddr_start_pos=i+2+2+1+1
xmport=data[xmaddr_start_pos:xmaddr_start_pos+2]
xmaddr=data[xmaddr_start_pos+2:xmaddr_start_pos+2+4]
i+=2
attrib_value_length=struct.unpack('!h', data[i:i+2])[0]
if attrib_value_length%4:
attrib_value_length+=4-(attrib_value_length%4) # adds stun attribute value padding
i+=2
i+=attrib_value_length
break # quit the STUN request loop
if maddr:
self.show_binary_data('\nMAPPED-ADDRESS: ', maddr)
self.show_binary_data('mport: ', mport)
else:
self.myprint('\nno MAPPED-ADDRESS found.')
if xmaddr:
n=struct.unpack('!I', xmaddr)[0]
m=struct.unpack('!I', magic_cookie)[0]
n=n^m
self.show_binary_data('\nXOR-MAPPED-ADDRESS: ', struct.pack('!I', n))
n2=struct.unpack('!H', xmport)[0]
m2=struct.unpack('!H', magic_cookie[0:2])[0]
n2=n2^m2
self.show_binary_data('xmport: ', struct.pack('!H', n2))
else:
self.myprint('\nno XOR-MAPPED-ADDRESS found.')
ip=None
port=None
if xmaddr: # we must prefer using XOR-MAPPED-ADDRESS over MAPPED-ADDRESS
ip=self.extract_ip(struct.pack('!I', n))
port=self.extract_port(struct.pack('!H', n2))
elif maddr:
ip=self.extract_ip(maddr)
port=self.extract_port(mport)
else:
raise Exception('STUN query failed!')
finally: udp_socket.settimeout(timeout_save)
self.myprint('\n=======STUN========')
self.myprint('IP: {0}'.format(ip))
self.myprint('Port: {0}'.format(port))
self.myprint('===================')
return (ip, port)
کدهای بالا رو در فایلی بنام stun_client.py ذخیره کنید و در اختیار هر برنامهء پایتون دیگری قرار بدید که قراره ازش استفاده کنه.
و اینهم قطعه کد مثالی برای طرز استفاده:
import stun_client
import socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sc=stun_client.STUNClient()
sc.print_debug_msgs=True
try:
print(sc.get_public_address_of_udp_socket(udp_sock et))
except Exception as e:
print('\nAn exception occured in get_public_address_of_udp_socket:\n', e)
input("\nhit Enter to quit...")
به متد get_public_address_of_udp_socket سوکتی رو که میخواید برای ارتباط ازش استفاده کنید پاس میکنید و این متد بهتون IP و پورت خارجی اون Socket رو بصورت یک Tuple برمیگردونه؛ البته درصورت موفقیت. بخاطر خطاهای پروتکل و خطاهای سرور و کلاینت و کانکشن اینترنت و غیره، فراخوانی این متد رو در بلاک try قرار بدید و exception های ایجاد شده رو هندل کنید.
موقع ایجاد یک شیء از کلاس STUNClient میشه یک آدرس (IP یا نام دامین) و بعدش بصورت اختیاری یک پورت رو پاس کرد تا الگوریتم از اون سرور خاصی که شما معرفی میکنید استفاده کنه. پورت روی پورت پیشفرض پروتکل STUN هست و نیازی نیست شما پورت رو هم مشخص کنید، مگر اینکه پورت غیراستانداردی رو مجبور باشید یا به دلیل دیگری بخواید معرفی کنید.
ضمنا با پاس کردن صرفا یک عدد صحیح از صفر تا ایندکس آخرین سروری که در stun_servers_list در کلاس STUNClient تعریف شده که محتوی آدرس سرورهای عمومی/مجانی STUN هست، میتونید سرور مورد نظر برای برقراری ارتباط رو انتخاب کنید (اگر یک سرور جواب نمیداد، این روش راحتی برای تعویض سرور هست). پیشفرض سرور اول هست که ایندکس اون عدد صفر هست.
چنتا مثال میزنم تا روشن بشید:
STUNClient('stun.example.com')
STUNClient('stun.example.com', 10000)
STUNClient(5)
STUNClient(3, 40005)
دست آخر باید بگم نظر به اینکه هیچ نمونه کدی رو برای پیاده سازی این الگوریتم جایی مطالعه نکردم، نمیتونم بگم کدم ممکن نیست اشتباهی داشته باشه. اما تاجایی که تست کردم به خوبی کار میکنه.
بخاطر این این کد رو با پایتون نوشتم چون برای نوشتن برنامهء آزمایش ارتباط P2P خودم، پایتون رو بخاطر سادگی و خوانایی و سرعت توسعه انتخاب کردم. باید بگم علاوه بر ویژگیهای دیگر، اسکریپتی بودن این زبان باعث میشه بخصوص در برنامه های نمونهء اولیه (Prototype) و آزمایش الگوریتمهای مختلف و نو که نیاز به بارها تغییر و اجرا دارن، سرعت توسعه واقعا بالا بره و زحمت برنامه نویس کم بشه. خیلی ساده در یک ادیتور برنامه نویسی سبک هم میشه کدهای پایتون رو نوشت و تغییر داد و فقط Save کرد و بعدش هم فوراً اجرا!
البته پایتون زبانی نیست که بگیم کاربرد و مزایاش فقط Prototype و تست هست. بلکه بنظرم خیلی از برنامه ها رو هم میشه بصورت کامل و نهایی و حتی برای همیشه با پایتون نوشت و استفاده کرد.
با زبانهای دیگر مثل سی و سی++ حتما کتابخانهء آماده برای استفاده از پروتکل STUN وجود داره، اما با زبان پایتون خبر ندارم هست یا نه. بهرحال خواستم این رو خودم بنویسم چون بنظرم تجربهء خوبی بود و تحلیل و کدنویسیش باعث تقویتم میشد. خیلی وقته همش تئوری کار کردم و کدنویسی خیلی کم داشتم و دنبال اینطور پروژه های غیر حجیم ولی در عین حال جذاب، خلاق، و نو میگردم چون احساس میکنم باید با کار عملی آموخته های خودم رو تثبیت کنم و محک بزنم.