Sip registrace je podobná http digest autentizaci. Aby se nepřenášelo nechráněné heslo, provádí se ve dvou krocích. Na první REGISTER odpoví SIP proxy server Not authorized. Z této odpovědi je použito políčko nonce - náhodný řetězec generovaný SIP proxy, se kterým se zašifruje heslo hashovací funkcí MD5 a provede se druhý REGISTER. V druhém REGISTER zašifrované heslo je obsaženo v políčku response, což vede k úspěšné registraci.
Program má demonstrovat co nejednodušším způsobem registraci a reakci na příchozí hovory (INVITE). Program neošetřuje možné problémy s internetem a navazováním/vypadávání TCP spojení. Číslo linky, heslo a doba platnosti registrace v sekundách se nastavuje na začátku skriptu. Po vypršení registrace se automaticky provede přeregistrace.
require 'socket' require "digest/md5" $cislo_linky="300100" $sip_heslo="222eg4bf" $expires=120 $s = TCPSocket.open("sip.odorik.cz", 5060) $lokalni_ip_a_port="#{$s.addr[2]}:#{$s.addr[1]}" def nacti_sip_hlavicky radek_cislo=0 while (line = $s.gets)!="\r\n" # čteme jednotlivé řádky hlavičky, prázdná řádka znamená konec hlavičky radek_cislo=radek_cislo+1 if radek_cislo==1 # Podle prvního řádku se pozná, jaký tip SIP zprávy to je - jmenuje se sip request line puts $sip_request_line=line.dup if line.start_with?("SIP/2.0 ") # je to response $sip_request_line="response" $response_code=/SIP\/2.0 ([0-9]*) /.match(line)[1] next end if line.start_with?("INVITE ") # Volají nám. Na jaké číslo? $called_phone_number=/([0-9]*)@/.match(line)[1] next end else if line.start_with?("WWW-Authenticate:") $nonce=/nonce="([^"]*)"/.match(line)[1] $digest_realm=/Digest realm="([^"]*)"/.match(line)[1] next end if line.start_with?("Call-ID: ") $callid=/Call-ID: ([^\s]*)/.match(line)[1] next end if line.start_with?("CSeq: ") $cseq=/CSeq: ([0-9]*)/.match(line)[1].to_i next end if line.start_with?("Content-Length: ") $content_length=line.dup.gsub("Content-Length: ","").to_i next end if line.start_with?("From:") $from_header=/sip:([^@]*)@/.match(line)[1] next end end end if $content_length>0 $header_body=$s.read($content_length) end end def registrace_faze1(expires=$expires) sip= <<-END_HEADERS REGISTER sip:sip.odorik.cz SIP/2.0 Via: SIP/2.0/TCP #{$lokalni_ip_a_port};branch=z9hG4bK-#{('a'..'z').to_a.shuffle[0,8].join} From: <sip:#{$cislo_linky}@sip.odorik.cz>;tag=3778c6ce2e0777ceo2 To: <sip:#{$cislo_linky}@sip.odorik.cz> Call-ID: #{$last_reg_callid=(('a'..'z').to_a.shuffle[0,15].join).to_s} CSeq: 1 REGISTER Max-Forwards: 69 Contact: <sip:<#{$cislo_linky}@#{$lokalni_ip_a_port};transport=tcp>;expires=#{expires} User-Agent: Ruby script Content-Length: 0 Allow: ACK, BYE, CANCEL, INFO, INVITE, NOTIFY, OPTIONS, PRACK, REFER, UPDATE, MESSAGE Supported: 100rel, replaces END_HEADERS sip.gsub!(/^[\s\t]*/,"") # odstraním mezery a tabulátory zleva, které zde jsou kvůli přehlednosti sip.gsub!(/\n/,"\r\n") # HTTP i SIP vymýšleli lidé od Windows, tedy namysleli si dva znaky pro nový řádek místo jednoho sip<< "\r\n" # konec hlavičky je zakončen prázdným řádkem # https://www.sitepoint.com/understanding-scope-in-ruby/ $s.write(sip) $second_reg_stage_expires=expires # druhá fáze registrace bude mít expiraci vždy stejnou jako ta první end def registrace_faze2 # viz https://en.wikipedia.org/wiki/Digest_access_authentication#Overview ha1=Digest::MD5.hexdigest("#{$cislo_linky}:#{$digest_realm}:#{$sip_heslo}") ha2=Digest::MD5.hexdigest("REGISTER:sip:sip.odorik.cz") response=Digest::MD5.hexdigest("#{ha1}:#{$nonce}:#{ha2}") sip= <<-END_HEADERS REGISTER sip:sip.odorik.cz SIP/2.0 Via: SIP/2.0/TCP #{$lokalni_ip_a_port};branch=z9hG4bK-#{('a'..'z').to_a.shuffle[0,8].join} From: <sip:#{$cislo_linky}@sip.odorik.cz>;tag=3778c6ce2e0777ceo2 To: <sip:#{$cislo_linky}@sip.odorik.cz> Call-ID: #{$callid} CSeq: #{$cseq+1} REGISTER Max-Forwards: 69 Authorization: Digest username="#{$cislo_linky}",realm="sip.odorik.cz",nonce="#{$nonce}",uri="sip:sip.odorik.cz",algorithm=MD5,response="#{response}" Contact: <sip:#{$cislo_linky}@#{$lokalni_ip_a_port};transport=tcp>;expires=#{$second_reg_stage_expires} User-Agent: Ruby script Content-Length: 0 Allow: ACK, BYE, CANCEL, INFO, INVITE, NOTIFY, OPTIONS, PRACK, REFER, UPDATE, MESSAGE Supported: 100rel, replaces END_HEADERS sip.gsub!(/^[\s\t]*/,"") sip.gsub!(/\n/,"\r\n") sip<< "\r\n" # konec hlaviček je zakončen prázdným řádkem $s.write(sip) end puts " Pošleme registraci fáze 1" registrace_faze1 Thread.new { # tohle vlákno čeká na a zpracovává příchozí SIP pakety while true nacti_sip_hlavicky if $sip_request_line=="response" and $response_code=="401" and $last_reg_callid == $callid puts " Dostali jsme odpověď na fázi registrace 1 - pošleme registrace fáze 2 " registrace_faze2 end if $sip_request_line=="response" and $response_code=="200" and $last_reg_callid == $callid puts " Jsme úspěšně (od)registrováni !!!!" end if $sip_request_line.start_with?("INVITE") puts "volá nám číslo #{$from_header} na číslo #{$called_phone_number} " end end } Thread.new { # tohle vlákno zajistí novou registraci po expiraci while true sleep($expires) registrace_faze1 end } # hlavní vlákno bude obsluhovat menu def precti_moznosti_menu sleep 1 # počkáme, až se zpracují předchozí požadavky v jiných vláknech puts "" puts "Zadej příkaz a zmáčkněte Enter:" puts "ukončit program - k" puts "registrovat znovu - r" puts "odregistrace - o" end precti_moznosti_menu while (prikaz=gets.chomp)!="k" if prikaz=="r" puts "znovu zaregistrujeme" registrace_faze1 end if prikaz=="o" puts "odregistrujeme = zaregistrujeme s nulovou expirací" registrace_faze1(0) end precti_moznosti_menu end puts "odregistrujeme = zaregistrujeme s nulovou expirací" registrace_faze1(0) sleep 1 # čekáme než proběhne 2. fáze odregistrace v jiných vláknech $s.close
Sledujte, co se opravdu posílá a co opravdu odpovídá server:
ngrep -W byline host 81.31.45.51 and port 5060
Podrobnosti: http://forum.odorik.cz/viewtopic.php?f=29&t=3513