[PYTHON] Make SIP Server as concise as possible (in the middle of explanation)

I used to be a VoIP engineer on the server side and control signal side, so when learning a new language, I first create a SIP Server.

In this article, I will write that if you implement at least this much, you can operate it as a SIP Server if the other party properly follows RFC3261.

The language uses python. The code was written about 10 years ago, so it's 2.7. I think it would be cooler if it was written by a brilliant programmer, but it still works, so please read it.

Reception of SIP signal

According to RFC3261, TCP must be supported, but UDP must also be supported, so expect the other party to speak with UDP and support only UDP. To. With UDP, you basically don't have to worry about signal breaks (with TCP, you have to look at where the signals are separated because data flows slowly), so it is effective for simplifying the program. is.

I think it's the same for most languages, but I'll create a socket for UDP, bind that socket to the IP and port, and then continue recvfrom on that socket. This is the image in the code.

class Proxy:

  def __init__(self, ip, port):
    self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    self.sock.bind((ip, port))
    
  def run(self):
    while True:
      buf, addr = self.sock.recvfrom(0xffff)
      #This contains the signal received by buf and the address of the other party sent to addr.

It's easy. This socket will also be used for signal transmission in a different location The receive buffer size is set to 0xffff because it can only receive up to this size due to the UDP specifications. (Since the size includes the header part of the UDP packet, the data size that can be received is a little smaller, but at first glance 0xffff is easier to see, so I don't care)

SIP signal analysis (roughly for the time being)

Roughly analyze the received signal to facilitate further processing. Personally, I'm packing secret techniques here. Well, it may be something that anyone can think of as long as they do some research.

In addition, since it is assumed that the other party will send the correct SIP signal, if you aim and send it, you can cause a bug.

The whole picture is like this.

class Message:

  def __init__(self, buf):
    buf = re.sub(r'^((\r\n)|(\r)|(\n))*', "", buf)
    m = re.search(r'((\r\n\r\n)|(\r\r)|(\n\n))', buf)
    self.body = buf[m.end():]
    buf = re.sub(r'\n[ \t]+',' ', re.sub(r'\r\n?', "\n", buf[:m.start()]))
    ary = buf.split("\n")
    m = re.match(r'(([A-Z]+) ([^ ]+) )?SIP\/2\.0( (\d+) ([^\n]+))?', ary[0])
    self.method, self.requri, self.stcode, self.reason = \
      m.group(2), m.group(3), m.group(5), m.group(6)
    self.hdrs = []
    for buf in ary[1:]:
      name, buf = re.split(r'\s*:\s*', buf, 1)
      self.hdrs.append(Header(name, re.split(r'\s*,\s*', buf)))

I will explain each part.

Looking at RFC3261, it is allowed to have multiple CRLFs at the beginning of the SIP signal. CRLF is either \ r, \ n, or \ r \ n. This is not meaningful information, so I will delete it for the time being. \ R \ n, \ r, or \ n can be written as a python regular expression:

r'(\r\n)|(\r)|(\n)'

In addition, I prefer to write parentheses more than necessary so that I can understand various things clearly.

If there are 0 or more of these

r'((\r\n)|(\r)|(\n))*'

I will call you. If you add the condition to the beginning

r'^((\r\n)|(\r)|(\n))*'

Will be. If you use this regular expression to replace buf,

buf = re.sub(r'^((\r\n)|(\r)|(\n))*', '', buf)

It will be.

Next, find the empty-line that separates the header and body, and divide it into two.

The empty-line is expressed as two consecutive CRLFs, which can be expressed as a regular expression.

r'((\r\n\r\n)|(\r\r)|(\n\n))'

If you search for this character string from the SIP signal and cut out the part behind it as the body

m = re.search(r'((\r\n\r\n)|(\r\r)|(\n\n))', buf)
self.body = buf[m.end():]

have become.

By the way, I've seen CRLF as \ r \ n, \ r, or \ n so far, but it's not a hassle, so I'll replace it with \ n here.

Then shouldn't we have replaced it sooner? However, if the body part contains information that makes sense to be \ r \ n, and if you want to see the data length properly using the Content-Length header, , It would be a problem if the body is rewritten, so replace only the part other than the body.

It will be replaced with \ n, so \ r \ n or \ r will be the target. It means that there may or may not be \ r followed by \ n, so if you express this with a regular expression,

r'¥r¥n?'

is not it.

I have found out how much of the data received just before this is the header, so based on that,

re.sub(r'\r\n?', '\n', buf[:m.start()])

Can be replaced with.

By the way, the SIP header is troublesomely designed so that CRLF can be included in the middle. The CRLF in the middle has no meaning, it is just for appearance. Who would be happy with this? This will not be an obstacle when processing, so I want to erase it somehow. Fortunately, if there is a CRLF in the middle of the header, it is a rule to put WSP (white space, one or more half-width spaces or tabs) after it. Since CRLF has been replaced with \ n in the previous process, \ n followed by one or more spaces or tabs will be replaced with a single half-width space including that space or tab.

Regular expression is

r'¥n[ ¥t]+'

is not it.

As for the process of converting the CRLF I wrote earlier to \ n, I will write it in one line.

buf = re.sub(r'\n[ \t]+',' ', re.sub(r'\r\n?', '\n', buf[:m.start()]))

It will be beautiful. Now that the header and the Start-Line before it are already contained in one line separated by \ n, divide them into an array.

ary = buf.split("\n")

It's better to process each element in this ary. The zeroth element is the Start-Line, and the first and subsequent elements are the header.

Next, analyze the Start-Line. There are two types of Start-Line, Request-Line and Status-Line.

Request-Line has a form in which method, Request-URI, and SIP-Version are lined up with one half-width space in between, method has multiple uppercase and half-width alphabetic characters, and Request-URI allows various characters. However, at least half-width spaces are not allowed.

r'([A-Z]+) ([^ ]+) SIP\/2\.0'

Status-Line is a form in which SIP-Version, Status-Code, and Reason-Phrase are lined up with one half-width space in between.

r'SIP\/2\.0 (\d+) ([^\n]+)'

Except for the last line break, the line break has already disappeared by dividing each line break, so there is no problem even if it is analyzed as an arbitrary character string. Don't make a mistake when you copy and paste in a place where you can't assume that there are no line breaks.

Request-Line has SIP-Version at the end, and Status-Line has SIP-Version at the beginning, so you can easily analyze it all at once.

r'(([A-Z]+) ([^ ]+) )?SIP\/2\.0( (\d+) ([^\n]+))?'

If you analyze using this and keep the result

m = re.match(r'(([A-Z]+) ([^ ]+) )?SIP\/2\.0( (\d+) ([^\n]+))?', ary[0])
self.method, self.requri, self.stcode, self.reason = m.group(2), m.group(3), m.group(5), m.group(6)

Continue parsing the header. Since the raw data of the header is included from the first line of the array called ary,

for buf in ary[1:]:
  pass #Analyze buf here

I will analyze it like this. The header has a header name, HCOLON, followed by the header value. HCOLON is the one that may or may not have spaces before and after the colon (:). So, first split it by a string with spaces before and after the colon.

for buf in ary[1:]:
  name, buf = re.split(r'\s*:\s*', buf, 1)

The header value is separated by commas, so separate them there.

re.split(r'\s*,\s*', buf)

However, this isn't really cool, and it's a bug if some of the header values contain commas in the double-quoted strings. You can do it by writing a slightly more difficult regular expression.

Create a class like this to store the analysis results

class Header:
  def __init__(self, name, vals):
    self.name, self.vals = name, vals

I decided to put the header analysis result into this

self.hdrs = []
for buf in ary[1:]:
  name, buf = re.split(r'\s*:\s*', buf, 1)
  self.hdrs.append(Header(name, re.split(r'\s*,\s*', buf)))

By the way, if you stick them all together

class Message:

  def __init__(self, buf):
    buf = re.sub(r'^((\r\n)|(\r)|(\n))*', "", buf)
    m = re.search(r'((\r\n\r\n)|(\r\r)|(\n\n))', buf)
    self.body = buf[m.end():]
    buf = re.sub(r'\n[ \t]+',' ', re.sub(r'\r\n?', "\n", buf[:m.start()]))
    ary = buf.split("\n")
    m = re.match(r'(([A-Z]+) ([^ ]+) )?SIP\/2\.0( (\d+) ([^\n]+))?', ary[0])
    self.method, self.requri, self.stcode, self.reason = \
      m.group(2), m.group(3), m.group(5), m.group(6)
    self.hdrs = []
    for buf in ary[1:]:
      name, buf = re.split(r'\s*:\s*', buf, 1)
      self.hdrs.append(Header(name, re.split(r'\s*,\s*', buf)))

have become.

A paragraph

I'm tired, so I'll continue next time. I'm exhausted and the explanation is already appropriate, so I'll update it soon. The finished product is as follows.

https://github.com/zurustar/xylitol/blob/master/xylitol.py

Adjust priority with updates of other articles depending on the number of likes

Recommended Posts

Make SIP Server as concise as possible (in the middle of explanation)
Make a copy of the list in Python
Make progress of dd visible in the progress bar
In the middle of development, we will introduce Alembic
Make the default value of the argument immutable (article explanation)
The world's most easy-to-understand explanation of how to make LINE BOT (3) [Linkage with server with Git]
Django + MongoDB development environment maintenance (in the middle of writing)
Make the function of drawing Japanese fonts in OpenCV general
Sphinx single html suppresses page breaks in the middle of the table
The story of participating in AtCoder
The story of the "hole" in the file
The meaning of ".object" in Django
The story of remounting the application server
Supplement to the explanation of vscode
Explanation and implementation of the XMPP protocol used in Slack, HipChat, and IRC
If you want a singleton in python, think of the module as a singleton
[Introduction to Python] Thorough explanation of the character string type used in Python!