Tristan Matthews | 0a329cc | 2013-07-17 13:20:14 -0400 | [diff] [blame] | 1 | # $Id$ |
| 2 | # |
| 3 | from socket import * |
| 4 | import re |
| 5 | import random |
| 6 | import time |
| 7 | import sys |
| 8 | import inc_cfg as cfg |
| 9 | from select import * |
| 10 | |
| 11 | # SIP request template |
| 12 | req_templ = \ |
| 13 | """$METHOD $TARGET_URI SIP/2.0\r |
| 14 | Via: SIP/2.0/UDP $LOCAL_IP:$LOCAL_PORT;rport;branch=z9hG4bK$BRANCH\r |
| 15 | Max-Forwards: 70\r |
| 16 | From: <sip:caller@pjsip.org>$FROM_TAG\r |
| 17 | To: <$TARGET_URI>$TO_TAG\r |
| 18 | Contact: <sip:$LOCAL_IP:$LOCAL_PORT;transport=udp>\r |
| 19 | Call-ID: $CALL_ID@pjsip.org\r |
| 20 | CSeq: $CSEQ $METHOD\r |
| 21 | Allow: PRACK, INVITE, ACK, BYE, CANCEL, UPDATE, REFER\r |
| 22 | Supported: replaces, 100rel, norefersub\r |
| 23 | User-Agent: pjsip.org Python tester\r |
| 24 | Content-Length: $CONTENT_LENGTH\r |
| 25 | $SIP_HEADERS""" |
| 26 | |
| 27 | |
| 28 | def is_request(msg): |
| 29 | return msg.split(" ", 1)[0] != "SIP/2.0" |
| 30 | |
| 31 | def is_response(msg): |
| 32 | return msg.split(" ", 1)[0] == "SIP/2.0" |
| 33 | |
| 34 | def get_code(msg): |
| 35 | if msg=="": |
| 36 | return 0 |
| 37 | return int(msg.split(" ", 2)[1]) |
| 38 | |
| 39 | def get_tag(msg, hdr="To"): |
| 40 | pat = "^" + hdr + ":.*" |
| 41 | result = re.search(pat, msg, re.M | re.I) |
| 42 | if result==None: |
| 43 | return "" |
| 44 | line = result.group() |
| 45 | #print "line=", line |
| 46 | tags = line.split(";tag=") |
| 47 | if len(tags)>1: |
| 48 | return tags[1] |
| 49 | return "" |
| 50 | #return re.split("[;& ]", s) |
| 51 | |
| 52 | def get_header(msg, hname): |
| 53 | headers = msg.splitlines() |
| 54 | for hdr in headers: |
| 55 | hfields = hdr.split(": ", 2) |
| 56 | if hfields[0]==hname: |
| 57 | return hfields[1] |
| 58 | return None |
| 59 | |
| 60 | class Dialog: |
| 61 | sock = None |
| 62 | dst_addr = "" |
| 63 | dst_port = 5060 |
| 64 | local_ip = "" |
| 65 | local_port = 0 |
| 66 | tcp = False |
| 67 | call_id = str(random.random()) |
| 68 | cseq = 0 |
| 69 | local_tag = ";tag=" + str(random.random()) |
| 70 | rem_tag = "" |
| 71 | last_resp_code = 0 |
| 72 | inv_branch = "" |
| 73 | trace_enabled = True |
| 74 | last_request = "" |
| 75 | def __init__(self, dst_addr, dst_port=5060, tcp=False, trace=True, local_port=0): |
| 76 | self.dst_addr = dst_addr |
| 77 | self.dst_port = dst_port |
| 78 | self.tcp = tcp |
| 79 | self.trace_enabled = trace |
| 80 | if tcp==True: |
| 81 | self.sock = socket(AF_INET, SOCK_STREAM) |
| 82 | self.sock.connect(dst_addr, dst_port) |
| 83 | else: |
| 84 | self.sock = socket(AF_INET, SOCK_DGRAM) |
| 85 | self.sock.bind(("127.0.0.1", local_port)) |
| 86 | |
| 87 | self.local_ip, self.local_port = self.sock.getsockname() |
| 88 | self.trace("Dialog socket bound to " + self.local_ip + ":" + str(self.local_port)) |
| 89 | |
| 90 | def trace(self, txt): |
| 91 | if self.trace_enabled: |
| 92 | print str(time.strftime("%H:%M:%S ")) + txt |
| 93 | |
| 94 | def update_fields(self, msg): |
| 95 | if self.tcp: |
| 96 | transport_param = ";transport=tcp" |
| 97 | else: |
| 98 | transport_param = "" |
| 99 | msg = msg.replace("$TARGET_URI", "sip:"+self.dst_addr+":"+str(self.dst_port) + transport_param) |
| 100 | msg = msg.replace("$LOCAL_IP", self.local_ip) |
| 101 | msg = msg.replace("$LOCAL_PORT", str(self.local_port)) |
| 102 | msg = msg.replace("$FROM_TAG", self.local_tag) |
| 103 | msg = msg.replace("$TO_TAG", self.rem_tag) |
| 104 | msg = msg.replace("$CALL_ID", self.call_id) |
| 105 | msg = msg.replace("$CSEQ", str(self.cseq)) |
| 106 | branch=str(random.random()) |
| 107 | msg = msg.replace("$BRANCH", branch) |
| 108 | return msg |
| 109 | |
| 110 | def create_req(self, method, sdp, branch="", extra_headers="", body=""): |
| 111 | if branch=="": |
| 112 | self.cseq = self.cseq + 1 |
| 113 | msg = req_templ |
| 114 | msg = msg.replace("$METHOD", method) |
| 115 | msg = msg.replace("$SIP_HEADERS", extra_headers) |
| 116 | if branch=="": |
| 117 | branch=str(random.random()) |
| 118 | msg = msg.replace("$BRANCH", branch) |
| 119 | if sdp!="": |
| 120 | msg = msg.replace("$CONTENT_LENGTH", str(len(sdp))) |
| 121 | msg = msg + "Content-Type: application/sdp\r\n" |
| 122 | msg = msg + "\r\n" |
| 123 | msg = msg + sdp |
| 124 | elif body!="": |
| 125 | msg = msg.replace("$CONTENT_LENGTH", str(len(body))) |
| 126 | msg = msg + "\r\n" |
| 127 | msg = msg + body |
| 128 | else: |
| 129 | msg = msg.replace("$CONTENT_LENGTH", "0") |
| 130 | return self.update_fields(msg) |
| 131 | |
| 132 | def create_response(self, request, code, reason, to_tag=""): |
| 133 | response = "SIP/2.0 " + str(code) + " " + reason + "\r\n" |
| 134 | lines = request.splitlines() |
| 135 | for line in lines: |
| 136 | hdr = line.split(":", 1)[0] |
| 137 | if hdr in ["Via", "From", "To", "CSeq", "Call-ID"]: |
| 138 | if hdr=="To" and to_tag!="": |
| 139 | line = line + ";tag=" + to_tag |
| 140 | elif hdr=="Via": |
| 141 | line = line + ";received=127.0.0.1" |
| 142 | response = response + line + "\r\n" |
| 143 | return response |
| 144 | |
| 145 | def create_invite(self, sdp, extra_headers="", body=""): |
| 146 | self.inv_branch = str(random.random()) |
| 147 | return self.create_req("INVITE", sdp, branch=self.inv_branch, extra_headers=extra_headers, body=body) |
| 148 | |
| 149 | def create_ack(self, sdp="", extra_headers=""): |
| 150 | return self.create_req("ACK", sdp, extra_headers=extra_headers, branch=self.inv_branch) |
| 151 | |
| 152 | def create_bye(self, extra_headers=""): |
| 153 | return self.create_req("BYE", "", extra_headers) |
| 154 | |
| 155 | def send_msg(self, msg, dst_addr=None): |
| 156 | if (is_request(msg)): |
| 157 | self.last_request = msg.split(" ", 1)[0] |
| 158 | if not dst_addr: |
| 159 | dst_addr = (self.dst_addr, self.dst_port) |
| 160 | self.trace("============== TX MSG to " + str(dst_addr) + " ============= \n" + msg) |
| 161 | self.sock.sendto(msg, 0, dst_addr) |
| 162 | |
| 163 | def wait_msg_from(self, timeout): |
| 164 | endtime = time.time() + timeout |
| 165 | msg = "" |
| 166 | src_addr = None |
| 167 | while time.time() < endtime: |
| 168 | readset = select([self.sock], [], [], 1) |
| 169 | if len(readset[0]) < 1 or not self.sock in readset[0]: |
| 170 | if len(readset[0]) < 1: |
| 171 | print "select() timeout (will wait for " + str(int(endtime - time.time())) + "more secs)" |
| 172 | elif not self.sock in readset[0]: |
| 173 | print "select() alien socket" |
| 174 | else: |
| 175 | print "select other error" |
| 176 | continue |
| 177 | try: |
| 178 | msg, src_addr = self.sock.recvfrom(4096) |
| 179 | break |
| 180 | except: |
| 181 | print "recv() exception: ", sys.exc_info()[0] |
| 182 | continue |
| 183 | |
| 184 | if msg=="": |
| 185 | return "", None |
| 186 | if self.last_request=="INVITE" and self.rem_tag=="": |
| 187 | self.rem_tag = get_tag(msg, "To") |
| 188 | self.rem_tag = self.rem_tag.rstrip("\r\n;") |
| 189 | if self.rem_tag != "": |
| 190 | self.rem_tag = ";tag=" + self.rem_tag |
| 191 | self.trace("=== rem_tag:" + self.rem_tag) |
| 192 | self.trace("=========== RX MSG from " + str(src_addr) + " ===========\n" + msg) |
| 193 | return (msg, src_addr) |
| 194 | |
| 195 | def wait_msg(self, timeout): |
| 196 | return self.wait_msg_from(timeout)[0] |
| 197 | |
| 198 | # Send request and wait for final response |
| 199 | def send_request_wait(self, msg, timeout): |
| 200 | t1 = 1.0 |
| 201 | endtime = time.time() + timeout |
| 202 | resp = "" |
| 203 | code = 0 |
| 204 | for i in range(0,5): |
| 205 | self.send_msg(msg) |
| 206 | resp = self.wait_msg(t1) |
| 207 | if resp!="" and is_response(resp): |
| 208 | code = get_code(resp) |
| 209 | break |
| 210 | last_resp = resp |
| 211 | while code < 200 and time.time() < endtime: |
| 212 | resp = self.wait_msg(endtime - time.time()) |
| 213 | if resp != "" and is_response(resp): |
| 214 | code = get_code(resp) |
| 215 | last_resp = resp |
| 216 | elif resp=="": |
| 217 | break |
| 218 | return last_resp |
| 219 | |
| 220 | def hangup(self, last_code=0): |
| 221 | self.trace("====== hangup =====") |
| 222 | if last_code!=0: |
| 223 | self.last_resp_code = last_code |
| 224 | if self.last_resp_code>0 and self.last_resp_code<200: |
| 225 | msg = self.create_req("CANCEL", "", branch=self.inv_branch, extra_headers="") |
| 226 | self.send_request_wait(msg, 5) |
| 227 | msg = self.create_ack() |
| 228 | self.send_msg(msg) |
| 229 | elif self.last_resp_code>=200 and self.last_resp_code<300: |
| 230 | msg = self.create_ack() |
| 231 | self.send_msg(msg) |
| 232 | msg = self.create_bye() |
| 233 | self.send_request_wait(msg, 5) |
| 234 | else: |
| 235 | msg = self.create_ack() |
| 236 | self.send_msg(msg) |
| 237 | |
| 238 | |
| 239 | class SendtoCfg: |
| 240 | # Test name |
| 241 | name = "" |
| 242 | # pjsua InstanceParam |
| 243 | inst_param = None |
| 244 | # Complete INVITE message. If this is not empty, then this |
| 245 | # message will be sent instead and the "sdp" and "extra_headers" |
| 246 | # settings will be ignored. |
| 247 | complete_msg = "" |
| 248 | # Initial SDP |
| 249 | sdp = "" |
| 250 | # Extra headers to add to request |
| 251 | extra_headers = "" |
| 252 | # Expected code |
| 253 | resp_code = 0 |
| 254 | # Use TCP? |
| 255 | use_tcp = False |
| 256 | # List of RE patterns that must exist in response |
| 257 | resp_include = [] |
| 258 | # List of RE patterns that must NOT exist in response |
| 259 | resp_exclude = [] |
| 260 | # Full (non-SDP) body |
| 261 | body = "" |
| 262 | # Constructor |
| 263 | def __init__(self, name, pjsua_args, sdp, resp_code, |
| 264 | resp_inc=[], resp_exc=[], use_tcp=False, |
| 265 | extra_headers="", body="", complete_msg="", |
| 266 | enable_buffer = False): |
| 267 | self.complete_msg = complete_msg |
| 268 | self.sdp = sdp |
| 269 | self.resp_code = resp_code |
| 270 | self.resp_include = resp_inc |
| 271 | self.resp_exclude = resp_exc |
| 272 | self.use_tcp = use_tcp |
| 273 | self.extra_headers = extra_headers |
| 274 | self.body = body |
| 275 | self.inst_param = cfg.InstanceParam("pjsua", pjsua_args) |
| 276 | self.inst_param.enable_buffer = enable_buffer |
| 277 | |
| 278 | |
| 279 | class RecvfromTransaction: |
| 280 | # The test title for this transaction |
| 281 | title = "" |
| 282 | # Optinal list of pjsua command and optional expect patterns |
| 283 | # to be invoked to make pjsua send a request |
| 284 | # Sample: |
| 285 | # (to make call and wait for INVITE to be sent) |
| 286 | # cmds = [ ["m"], ["sip:127.0.0.1", "INVITE sip:"] ] |
| 287 | cmds = [] |
| 288 | # Check if the CSeq must be greater than last Cseq? |
| 289 | check_cseq = True |
| 290 | # List of RE patterns that must exists in incoming request |
| 291 | include = [] |
| 292 | # List of RE patterns that MUST NOT exist in incoming request |
| 293 | exclude = [] |
| 294 | # Response code to send |
| 295 | resp_code = 0 |
| 296 | # Additional list of headers to be sent on the response |
| 297 | # Note: no need to add CRLF on the header |
| 298 | resp_hdr = [] |
| 299 | # Message body. This should include the Content-Type header too. |
| 300 | # Sample: |
| 301 | # body = """Content-Type: application/sdp\r\n |
| 302 | # \r\n |
| 303 | # v=0\r\n |
| 304 | # ... |
| 305 | # """ |
| 306 | body = None |
| 307 | # Pattern to be expected on pjsua when receiving the response |
| 308 | expect = "" |
| 309 | |
| 310 | def __init__(self, title, resp_code, check_cseq=True, |
| 311 | include=[], exclude=[], cmds=[], resp_hdr=[], resp_body=None, expect=""): |
| 312 | self.title = title |
| 313 | self.cmds = cmds |
| 314 | self.include = include |
| 315 | self.exclude = exclude |
| 316 | self.resp_code = resp_code |
| 317 | self.resp_hdr = resp_hdr |
| 318 | self.body = resp_body |
| 319 | self.expect = expect |
| 320 | |
| 321 | |
| 322 | class RecvfromCfg: |
| 323 | # Test name |
| 324 | name = "" |
| 325 | # pjsua InstanceParam |
| 326 | inst_param = None |
| 327 | # List of RecvfromTransaction |
| 328 | transaction = None |
| 329 | # Use TCP? |
| 330 | tcp = False |
| 331 | |
| 332 | # Note: |
| 333 | # Any "$PORT" string in the pjsua_args will be replaced |
| 334 | # by server port |
| 335 | def __init__(self, name, pjsua_args, transaction, tcp=False): |
| 336 | self.name = name |
| 337 | self.inst_param = cfg.InstanceParam("pjsua", pjsua_args) |
| 338 | self.transaction = transaction |
| 339 | self.tcp=tcp |
| 340 | |
| 341 | |
| 342 | |
| 343 | |