# زبان های اسکریپتی > دیگر زبان های اسکریپتی >  پروژهء STUN protocol client

## eshpilen

این پروژه رو بعنوان قسمتی ضروری از یک پروژهء ارتباط 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 وجود داره، اما با زبان پایتون خبر ندارم هست یا نه. بهرحال خواستم این رو خودم بنویسم چون بنظرم تجربهء خوبی بود و تحلیل و کدنویسیش باعث تقویتم میشد. خیلی وقته همش تئوری کار کردم و کدنویسی خیلی کم داشتم و دنبال اینطور پروژه های غیر حجیم ولی در عین حال جذاب، خلاق، و نو میگردم چون احساس میکنم باید با کار عملی آموخته های خودم رو تثبیت کنم و محک بزنم.

----------

