├── message.txt ├── TODO.md ├── LICENSE ├── README.md └── siprig.py /message.txt: -------------------------------------------------------------------------------- 1 | OPTIONS sip:ideasip.com SIP/2.0 2 | Via: SIP/2.0/UDP 192.168.0.26:55220 3 | Max-Forwards: 70 4 | From: "Test" ;tag=98765 5 | To: 6 | Contact: 7 | Call-ID: 1234567@192.168.0.26 8 | CSeq: 1 OPTIONS 9 | Accept: application/sdp 10 | Content-Length: 0 11 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## TODO 2 | 3 | - EASY. Add a suite of example SIP messages. 4 | - MEDIUM. Automatically populate SIP values based on destination and source values. 5 | - MEDIUM. Some tests would be nice... 6 | - MEDIUM. Automatically calculate the Content-Length header. 7 | - DOCUMENTATION. Full functional description. 8 | 9 | #### Rejected Ideas 10 | 11 | - Vaildate the input is valid SIP. 12 | 13 | SIPRig is not intended to be a SIP message validator, but rather a tool to send SIP, valid or not. If you're not sure that you have a valid SIP message then it is recommended to use one of the many excellent SIP parsers available. 14 | 15 | An exception to this rule is the Content-Length header. It is very easy to get this wrong unintentionally when manually editing the body of a SIP message. 16 | 17 | - Populate a set of default headers if they're not specified in the input. 18 | 19 | As above. Missing headers are crucial test cases, so it makes sense for SIPRig to defer control of this to the user. 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Martin Craig Young 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SIPRig 2 | 3 | SIPRig is a lightweight tool for sending a SIP message. 4 | 5 | ## Motivation 6 | 7 | [SIPp](http://sipp.sourceforge.net) is a fantastic tool for generating SIP dialogs. However there is a slight learning curve and setup overhead. 8 | 9 | If you don't care for valid SIP dialog, or even valid SIP, then SIPRig is for you! Construct your SIP message in an input file and send it anywhere. 10 | 11 | ## Features 12 | 13 | - Send any SIP message anywhere 14 | 15 | SIPRig allows you to send a SIP message to a destination of your choice. Construct (or copy) your SIP message in an input file and use the `-f` command line option to specify the location. Your input does not have to be valid SIP (discussed later). You have complete control over the headers and content of your message. 16 | 17 | Use the `-d` flag to specify a destination as an IP or a FQDN. 18 | 19 | You can also specify the destination port, src address and port, and several other options. SIPRig will use sensible defaults if these are not provided. See the usage below. 20 | 21 | - Guard against malformed SIP 22 | 23 | SIPRig automatically ensures your input file ends in two blank lines to guard against malformed SIP. This can be disabled using the `--no-validation` option. 24 | 25 | - Protocol detection 26 | 27 | SIPRig automatically detects the protocol based on the input file. You can override this by explicitly specifying the protcol with the `--tcp` and `--udp` options. 28 | 29 | - SIPRig works under both python 2.7 and python 3 30 | 31 | ## Example Usage 32 | 33 | $ cat message.txt 34 | OPTIONS sip:sip.iptel.org SIP/2.0 35 | Via: SIP/2.0/UDP 192.168.0.26:55220 36 | Max-Forwards: 70 37 | From: "Test" ;tag=98765 38 | To: 39 | Contact: 40 | Call-ID: 1234567@192.168.0.26 41 | CSeq: 1 OPTIONS 42 | Accept: application/sdp 43 | Content-Length: 0 44 | 45 | $ python siprig.py -f message.txt -d sip.iptel.org -p 5060 -P 55220 -v 46 | 47 | Request sent to sip.iptel.org:5060 48 | 49 | OPTIONS sip:sip.iptel.org SIP/2.0 50 | Via: SIP/2.0/UDP 192.168.0.26:55220 51 | Max-Forwards: 70 52 | From: "Test" ;tag=98765 53 | To: 54 | Contact: 55 | Call-ID: 1234567@192.168.0.26 56 | CSeq: 1 OPTIONS 57 | Accept: application/sdp 58 | Content-Length: 0 59 | 60 | 61 | Response from sip.iptel.org:5060 62 | 63 | SIP/2.0 200 OK 64 | Via: SIP/2.0/UDP 192.168.0.26:55220 65 | From: "Test" ;tag=98765 66 | To: ;tag=0D4E4EA6-56B7B180000D950B-55778700 67 | Call-ID: 1234567@192.168.0.26 68 | CSeq: 1 OPTIONS 69 | Accept: */* 70 | Accept-Language: en 71 | Server: ser (3.3.0-pre1 (i386/linux)) 72 | Contact: 73 | Content-Length: 0 74 | 75 | ## Full Usage 76 | 77 | $ python siprig.py -h 78 | usage: siprig.py [-h] [-f INPUT_FILE] [-d DEST_ADDR] [-p DEST_PORT] 79 | [-S SRC_IP] [-P SRC_PORT] [-q] [-v] [--tcp] [--udp] 80 | [--timeout TIMEOUT] [--no-validation] 81 | 82 | optional arguments: 83 | -h, --help show this help message and exit 84 | -f INPUT_FILE, --input_file INPUT_FILE 85 | *Required - Input file 86 | -d DEST_ADDR, --dest-addr DEST_ADDR 87 | *Required - Destination address. IP or FQDN. 88 | -p DEST_PORT, --dest-port DEST_PORT 89 | Destination port. Default 5060. 90 | -S SRC_IP, --src-ip SRC_IP 91 | Source IP address. 92 | -P SRC_PORT, --src-port SRC_PORT 93 | Source port. 94 | -q, --quiet Suppress all output. 95 | -v, --verbose Show request and response in stdout. 96 | --tcp Force TCP protocol. 97 | --udp Force UDP protocol. 98 | --timeout TIMEOUT Seconds to wait for a response. Default 1s. 99 | --no-validation Disable input file blank line validation. 100 | 101 | ## Installation 102 | 103 | $ git clone git://github.com/martincyoung/SIPRig 104 | 105 | ## License 106 | 107 | The MIT License (MIT) 108 | 109 | Copyright (c) 2016 Martin Craig Young 110 | 111 | Permission is hereby granted, free of charge, to any person obtaining a copy 112 | of this software and associated documentation files (the "Software"), to deal 113 | in the Software without restriction, including without limitation the rights 114 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 115 | copies of the Software, and to permit persons to whom the Software is 116 | furnished to do so, subject to the following conditions: 117 | 118 | The above copyright notice and this permission notice shall be included in all 119 | copies or substantial portions of the Software. 120 | 121 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 122 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 123 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 124 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 125 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 126 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 127 | SOFTWARE. 128 | -------------------------------------------------------------------------------- /siprig.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import re 3 | import socket 4 | import sys 5 | from argparse import ArgumentParser 6 | 7 | 8 | class ArgumentsException(Exception): 9 | pass 10 | 11 | 12 | class Request(): 13 | def __init__(self, input_file, validate, quiet): 14 | self.bytes = self.get_req_from_file(input_file) 15 | self.validate_request(validate, quiet) 16 | 17 | def get_req_from_file(self, input_file): 18 | # Read the input file as a byte array, and convert to Unix line 19 | # endings if required. 20 | file_handle = open(input_file, "rbU") 21 | sip_bytes = file_handle.read() 22 | file_handle.close() 23 | 24 | return sip_bytes 25 | 26 | def validate_request(self, validate, quiet): 27 | if (self.bytes[-2:] != '\n\n'.encode()): 28 | # Input file does not end in two blank lines. 29 | if validate: 30 | self.add_blank_lines() 31 | elif not quiet: 32 | # Input file will result in malformed SIP. 33 | sys.stderr.write("\nWARNING: Malformed SIP - two blank lines " 34 | "required at the end of the input file\n") 35 | 36 | def add_blank_lines(self): 37 | while (self.bytes[-2:] != '\n\n'.encode()): 38 | self.bytes += '\n'.encode() 39 | 40 | def protocol(self): 41 | pattern = re.compile(b"Via: SIP/2.0/(UDP|TCP)") 42 | try: 43 | return pattern.search(self.bytes).group(1).lower().decode() 44 | except AttributeError: 45 | # The regex didn't match anything in the source file. Default to 46 | # UDP. 47 | return "udp" 48 | 49 | 50 | class Arguments(): 51 | def __init__(self): 52 | self.parser = ArgumentParser() 53 | self.add_arguments() 54 | self.parse_args() 55 | self.validate() 56 | 57 | def parse_args(self): 58 | # Parse the supplied arguments and map each one to an attribute on 59 | # the Argument object. 60 | for k, v in self.parser.parse_args().__dict__.items(): 61 | setattr(self, k, v) 62 | 63 | def validate(self): 64 | if self.tcp and self.udp: 65 | raise ArgumentsException("Please specify only one of TCP or UDP") 66 | 67 | if not self.input_file: 68 | raise ArgumentsException("Please specify an input file with '-f'") 69 | 70 | if not self.dest_addr: 71 | raise ArgumentsException("Please specify a destination with '-d'") 72 | 73 | def add_arguments(self): 74 | self.parser.add_argument('-f', 75 | '--input_file', 76 | dest='input_file', 77 | default=None, 78 | help='*Required - Input file') 79 | self.parser.add_argument('-d', 80 | '--dest-addr', 81 | dest='dest_addr', 82 | default=None, 83 | help='*Required - Destination address. IP ' 84 | 'or FQDN.') 85 | self.parser.add_argument('-p', 86 | '--dest-port', 87 | dest='dest_port', 88 | type=int, 89 | default=5060, 90 | help='Destination port. Default 5060.') 91 | self.parser.add_argument('-S', 92 | '--src-ip', 93 | dest='src_ip', 94 | default='', 95 | help='Source IP address.') 96 | self.parser.add_argument('-P', 97 | '--src-port', 98 | dest='src_port', 99 | type=int, 100 | default=0, 101 | help='Source port.') 102 | self.parser.add_argument('-q', 103 | '--quiet', 104 | dest='quiet', 105 | action='store_true', 106 | default=False, 107 | help='Suppress all output.') 108 | self.parser.add_argument('-v', 109 | '--verbose', 110 | dest='verbose', 111 | action='store_true', 112 | default=False, 113 | help='Show request and response in stdout.') 114 | self.parser.add_argument('--tcp', 115 | dest='tcp', 116 | action='store_true', 117 | default=False, 118 | help='Force TCP protocol.') 119 | self.parser.add_argument('--udp', 120 | dest='udp', 121 | action='store_true', 122 | default=False, 123 | help='Force UDP protocol.') 124 | self.parser.add_argument('--timeout', 125 | dest='timeout', 126 | type=float, 127 | default=1.0, 128 | help='Seconds to wait for a response. ' 129 | 'Default 1s.') 130 | self.parser.add_argument('--no-validation', 131 | dest='validate_request', 132 | action='store_false', 133 | default=True, 134 | help='Disable input file blank line ' 135 | 'validation.') 136 | 137 | 138 | def get_socket(src_address, src_port, timeout, protocol): 139 | if protocol == "tcp": 140 | # Create an IPv4 TCP socket. Set REUSEADDR so that the port can be 141 | # reused without waiting for the TIME_WAIT state to pass. 142 | s = socket.socket(socket.AF_INET, 143 | socket.SOCK_STREAM, 144 | socket.IPPROTO_TCP) 145 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 146 | else: 147 | # Create an IPv4 UDP socket. 148 | s = socket.socket(socket.AF_INET, 149 | socket.SOCK_DGRAM, 150 | socket.IPPROTO_UDP) 151 | 152 | # If no source address or source port is provided, the socket module 153 | # assigns this automatically. 154 | s.bind((src_address, src_port)) 155 | s.settimeout(timeout) 156 | return s 157 | 158 | 159 | def main(): 160 | try: 161 | args = Arguments() 162 | 163 | request = Request(args.input_file, args.validate_request, args.quiet) 164 | 165 | # Set the protocol from the arguments or the request. Default to UDP 166 | # in the case where nothing is specified and it is not possible to 167 | # deduce from the request. 168 | protocol = "udp" 169 | 170 | if not args.tcp and not args.udp: 171 | # No protocol was specified in the arguments. Work it out from 172 | # the request. 173 | protocol = request.protocol() 174 | elif args.tcp: 175 | protocol = "tcp" 176 | 177 | # Configure a socket and send the request. 178 | s = get_socket(args.src_ip, args.src_port, args.timeout, protocol) 179 | s.connect((args.dest_addr, args.dest_port)) 180 | s.send(request.bytes) 181 | 182 | # Depending on the 'quiet' and 'verbose' options, print information 183 | # to stdout with the status of the send and receive operations. 184 | if not args.quiet: 185 | sys.stdout.write("\nRequest sent to %s:%d\n\n" % 186 | (args.dest_addr, args.dest_port)) 187 | if args.verbose: 188 | sys.stdout.write(request.bytes.decode() + "\n") 189 | 190 | response = s.recv(65535) 191 | 192 | if not args.quiet: 193 | sys.stdout.write("Response from %s:%d\n\n" % 194 | (args.dest_addr, args.dest_port)) 195 | if args.verbose: 196 | sys.stdout.write(response.decode() + "\n") 197 | 198 | except ArgumentsException as e: 199 | sys.stderr.write("\nERROR: " + str(e) + ". Use '-h' for info.\n") 200 | sys.exit(-1) 201 | 202 | except socket.timeout: 203 | # Socket timed out. This could mean that no response was received 204 | # from the far end for a sent SIP packet, or that a TCP connection 205 | # was not established within the timeout limit. 206 | if not args.quiet: 207 | sys.stdout.write("No response received within %0.1f seconds\n" % 208 | args.timeout) 209 | 210 | finally: 211 | try: 212 | # Regardless of what happened, try to gracefully close down the 213 | # socket. 214 | s.shutdown(1) 215 | s.close() 216 | except UnboundLocalError: 217 | # Socket has not been assigned. 218 | pass 219 | 220 | if __name__ == '__main__': 221 | main() 222 | --------------------------------------------------------------------------------