opensipscli 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opensipscli/__init__.py +20 -0
- opensipscli/args.py +56 -0
- opensipscli/cli.py +472 -0
- opensipscli/comm.py +57 -0
- opensipscli/config.py +162 -0
- opensipscli/db.py +989 -0
- opensipscli/defaults.py +91 -0
- opensipscli/libs/__init__.py +20 -0
- opensipscli/libs/sqlalchemy_utils.py +244 -0
- opensipscli/logger.py +85 -0
- opensipscli/main.py +86 -0
- opensipscli/module.py +69 -0
- opensipscli/modules/__init__.py +24 -0
- opensipscli/modules/database.py +1062 -0
- opensipscli/modules/diagnose.py +1089 -0
- opensipscli/modules/instance.py +53 -0
- opensipscli/modules/mi.py +200 -0
- opensipscli/modules/tls.py +354 -0
- opensipscli/modules/trace.py +292 -0
- opensipscli/modules/trap.py +138 -0
- opensipscli/modules/user.py +281 -0
- opensipscli/version.py +22 -0
- opensipscli-0.3.1.data/scripts/opensips-cli +9 -0
- opensipscli-0.3.1.dist-info/LICENSE +674 -0
- opensipscli-0.3.1.dist-info/METADATA +225 -0
- opensipscli-0.3.1.dist-info/RECORD +28 -0
- opensipscli-0.3.1.dist-info/WHEEL +5 -0
- opensipscli-0.3.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
##
|
|
3
|
+
## This file is part of OpenSIPS CLI
|
|
4
|
+
## (see https://github.com/OpenSIPS/opensips-cli).
|
|
5
|
+
##
|
|
6
|
+
## This program is free software: you can redistribute it and/or modify
|
|
7
|
+
## it under the terms of the GNU General Public License as published by
|
|
8
|
+
## the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
## (at your option) any later version.
|
|
10
|
+
##
|
|
11
|
+
## This program is distributed in the hope that it will be useful,
|
|
12
|
+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
## GNU General Public License for more details.
|
|
15
|
+
##
|
|
16
|
+
## You should have received a copy of the GNU General Public License
|
|
17
|
+
## along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
18
|
+
##
|
|
19
|
+
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from time import time
|
|
22
|
+
import random
|
|
23
|
+
import socket
|
|
24
|
+
from opensipscli import comm
|
|
25
|
+
from opensipscli.config import cfg
|
|
26
|
+
from opensipscli.logger import logger
|
|
27
|
+
from opensipscli.module import Module
|
|
28
|
+
|
|
29
|
+
TRACE_BUFFER_SIZE = 65535
|
|
30
|
+
|
|
31
|
+
'''
|
|
32
|
+
find out more information here:
|
|
33
|
+
* https://github.com/sipcapture/HEP/blob/master/docs/HEP3NetworkProtocolSpecification_REV26.pdf
|
|
34
|
+
'''
|
|
35
|
+
|
|
36
|
+
protocol_types = {
|
|
37
|
+
0x00: "UNKNOWN",
|
|
38
|
+
0x01: "SIP",
|
|
39
|
+
0x02: "XMPP",
|
|
40
|
+
0x03: "SDP",
|
|
41
|
+
0x04: "RTP",
|
|
42
|
+
0x05: "RTCP JSON",
|
|
43
|
+
0x56: "LOG",
|
|
44
|
+
0x57: "MI",
|
|
45
|
+
0x58: "REST",
|
|
46
|
+
0x59: "NET",
|
|
47
|
+
0x60: "CONTROL",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protocol_ids = {
|
|
51
|
+
num:name[8:] for name,num in vars(socket).items() if name.startswith("IPPROTO")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class HEPpacketException(Exception):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
class HEPpacket(object):
|
|
58
|
+
|
|
59
|
+
def __init__(self, payloads):
|
|
60
|
+
self.payloads = payloads
|
|
61
|
+
self.family = socket.AF_INET
|
|
62
|
+
self.protocol = "UNKNOWN"
|
|
63
|
+
self.src_addr = None
|
|
64
|
+
self.dst_addr = None
|
|
65
|
+
self.src_port = None
|
|
66
|
+
self.dst_port = None
|
|
67
|
+
self.data = None
|
|
68
|
+
self.correlation = None
|
|
69
|
+
self.ts = time()
|
|
70
|
+
self.tms = datetime.now().microsecond
|
|
71
|
+
|
|
72
|
+
def __str__(self):
|
|
73
|
+
time_str = "{}.{}".format(
|
|
74
|
+
self.ts,
|
|
75
|
+
self.tms)
|
|
76
|
+
protocol_str = " {}/{}".format(
|
|
77
|
+
self.protocol,
|
|
78
|
+
self.type)
|
|
79
|
+
|
|
80
|
+
if self.type == "SIP":
|
|
81
|
+
ip_str = " {}:{} -> {}:{}".format(
|
|
82
|
+
socket.inet_ntop(self.family, self.src_addr),
|
|
83
|
+
self.src_port,
|
|
84
|
+
socket.inet_ntop(self.family, self.dst_addr),
|
|
85
|
+
self.dst_port)
|
|
86
|
+
else:
|
|
87
|
+
ip_str = ""
|
|
88
|
+
if self.data:
|
|
89
|
+
data_str = self.data.decode()
|
|
90
|
+
else:
|
|
91
|
+
data_str = ""
|
|
92
|
+
|
|
93
|
+
return logger.color(logger.BLUE, time_str) + \
|
|
94
|
+
logger.color(logger.CYAN, protocol_str + ip_str) + \
|
|
95
|
+
"\n" + data_str
|
|
96
|
+
|
|
97
|
+
def parse(self):
|
|
98
|
+
length = len(self.payloads)
|
|
99
|
+
payloads = self.payloads
|
|
100
|
+
while length > 0:
|
|
101
|
+
if length < 6:
|
|
102
|
+
logger.error("payload too small {}".format(length))
|
|
103
|
+
return None
|
|
104
|
+
chunk_vendor_id = int.from_bytes(payloads[0:2],
|
|
105
|
+
byteorder="big", signed=False)
|
|
106
|
+
chunk_type_id = int.from_bytes(payloads[2:4],
|
|
107
|
+
byteorder="big", signed=False)
|
|
108
|
+
chunk_len = int.from_bytes(payloads[4:6],
|
|
109
|
+
byteorder="big", signed=False)
|
|
110
|
+
if chunk_len < 6:
|
|
111
|
+
logger.error("chunk too small {}".format(chunk_len))
|
|
112
|
+
return None
|
|
113
|
+
payload = payloads[6:chunk_len]
|
|
114
|
+
payloads = payloads[chunk_len:]
|
|
115
|
+
length = length - chunk_len
|
|
116
|
+
self.push_chunk(chunk_vendor_id, chunk_type_id, payload)
|
|
117
|
+
|
|
118
|
+
def push_chunk(self, vendor_id, type_id, payload):
|
|
119
|
+
|
|
120
|
+
if vendor_id != 0:
|
|
121
|
+
logger.warning("Unknown vendor id {}".format(vendor_id))
|
|
122
|
+
raise HEPpacketException
|
|
123
|
+
if type_id == 0x0001:
|
|
124
|
+
if len(payload) != 1:
|
|
125
|
+
raise HEPpacketException
|
|
126
|
+
self.family = payload[0]
|
|
127
|
+
elif type_id == 0x0002:
|
|
128
|
+
if len(payload) != 1:
|
|
129
|
+
raise HEPpacketException
|
|
130
|
+
if not payload[0] in protocol_ids:
|
|
131
|
+
self.protocol = str(payload[0])
|
|
132
|
+
else:
|
|
133
|
+
self.protocol = protocol_ids[payload[0]]
|
|
134
|
+
elif type_id >= 0x0003 and type_id <= 0x0006:
|
|
135
|
+
expected_payload_len = 4 if type_id <= 0x0004 else 16
|
|
136
|
+
if len(payload) != expected_payload_len:
|
|
137
|
+
raise HEPpacketException
|
|
138
|
+
if type_id == 0x0003 or type_id == 0x0005:
|
|
139
|
+
self.src_addr = payload
|
|
140
|
+
else:
|
|
141
|
+
self.dst_addr = payload
|
|
142
|
+
elif type_id == 0x0007 or type_id == 0x0008:
|
|
143
|
+
if len(payload) != 2:
|
|
144
|
+
raise HEPpacketException
|
|
145
|
+
port = int.from_bytes(payload,
|
|
146
|
+
byteorder="big", signed=False)
|
|
147
|
+
if type_id == 7:
|
|
148
|
+
self.src_port = port
|
|
149
|
+
else:
|
|
150
|
+
self.dst_port = port
|
|
151
|
+
elif type_id == 0x0009 or type_id == 0x000a:
|
|
152
|
+
if len(payload) != 4:
|
|
153
|
+
raise HEPpacketException
|
|
154
|
+
timespec = int.from_bytes(payload,
|
|
155
|
+
byteorder="big", signed=False)
|
|
156
|
+
if type_id == 0x0009:
|
|
157
|
+
self.ts = timespec
|
|
158
|
+
else:
|
|
159
|
+
self.tms = timespec
|
|
160
|
+
elif type_id == 0x000b:
|
|
161
|
+
if len(payload) != 1:
|
|
162
|
+
raise HEPpacketException
|
|
163
|
+
if not payload[0] in protocol_types:
|
|
164
|
+
self.type = str(payload[0])
|
|
165
|
+
else:
|
|
166
|
+
self.type = protocol_types[payload[0]]
|
|
167
|
+
elif type_id == 0x000c:
|
|
168
|
+
pass # capture id not used now
|
|
169
|
+
elif type_id == 0x000f:
|
|
170
|
+
self.data = payload
|
|
171
|
+
elif type_id == 0x0011:
|
|
172
|
+
self.correlation = payload
|
|
173
|
+
else:
|
|
174
|
+
logger.warning("unhandled payload type {}".format(type_id))
|
|
175
|
+
|
|
176
|
+
class trace(Module):
|
|
177
|
+
|
|
178
|
+
def __print_hep(self, packet):
|
|
179
|
+
# this works as a HEP parser
|
|
180
|
+
logger.debug("initial packet size is {}".format(len(packet)))
|
|
181
|
+
|
|
182
|
+
while len(packet) > 0:
|
|
183
|
+
if len(packet) < 4:
|
|
184
|
+
return packet
|
|
185
|
+
# currently only HEPv3 is accepted
|
|
186
|
+
if packet[0:4] != b'HEP3':
|
|
187
|
+
logger.warning("packet not HEPv3: [{}]".format(packet[0:4]))
|
|
188
|
+
return None
|
|
189
|
+
length = int.from_bytes(packet[4:6], byteorder="big", signed=False)
|
|
190
|
+
if length > len(packet):
|
|
191
|
+
logger.debug("partial packet: {} out of {}".
|
|
192
|
+
format(len(packet), length))
|
|
193
|
+
# wait for entire packet to parse it
|
|
194
|
+
return packet
|
|
195
|
+
logger.debug("packet size is {}".format(length))
|
|
196
|
+
# skip the header
|
|
197
|
+
hep_packet = HEPpacket(packet[6:length])
|
|
198
|
+
try:
|
|
199
|
+
hep_packet.parse()
|
|
200
|
+
except HEPpacketException:
|
|
201
|
+
return None
|
|
202
|
+
packet = packet[length:]
|
|
203
|
+
print(hep_packet)
|
|
204
|
+
|
|
205
|
+
return packet
|
|
206
|
+
|
|
207
|
+
def __complete__(self, command, text, line, begidx, endidx):
|
|
208
|
+
filters = [ "caller", "callee", "ip" ]
|
|
209
|
+
|
|
210
|
+
# remove the filters already used
|
|
211
|
+
filters = [f for f in filters if line.find(f + "=") == -1]
|
|
212
|
+
if not command:
|
|
213
|
+
return filters
|
|
214
|
+
|
|
215
|
+
if (not text or text == "") and line[-1] == "=":
|
|
216
|
+
return [""]
|
|
217
|
+
|
|
218
|
+
ret = [f for f in filters if (f.startswith(text) and line.find(f + "=") == -1)]
|
|
219
|
+
if len(ret) == 1 :
|
|
220
|
+
ret[0] = ret[0] + "="
|
|
221
|
+
return ret
|
|
222
|
+
|
|
223
|
+
def __get_methods__(self):
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
def do_trace(self, params, modifiers):
|
|
227
|
+
|
|
228
|
+
filters = []
|
|
229
|
+
|
|
230
|
+
if params is None:
|
|
231
|
+
caller_f = input("Caller filter: ")
|
|
232
|
+
if caller_f != "":
|
|
233
|
+
filters.append("caller={}".format(caller_f))
|
|
234
|
+
callee_f = input("Callee filter: ")
|
|
235
|
+
if callee_f != "":
|
|
236
|
+
filters.append("callee={}".format(callee_f))
|
|
237
|
+
ip_f = input("Source IP filter: ")
|
|
238
|
+
if ip_f != "":
|
|
239
|
+
filters.append("ip={}".format(ip_f))
|
|
240
|
+
if len(filters) == 0:
|
|
241
|
+
ans = cfg.read_param(None, "No filter specified! "\
|
|
242
|
+
"Continue without a filter?", False, True)
|
|
243
|
+
if not ans:
|
|
244
|
+
return False
|
|
245
|
+
filters = None
|
|
246
|
+
else:
|
|
247
|
+
filters = params
|
|
248
|
+
|
|
249
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
250
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
251
|
+
trace_ip = cfg.get("trace_listen_ip")
|
|
252
|
+
trace_port = int(cfg.get("trace_listen_port"))
|
|
253
|
+
s.bind((trace_ip, trace_port))
|
|
254
|
+
if trace_port == 0:
|
|
255
|
+
trace_port = s.getsockname()[1]
|
|
256
|
+
s.listen(1)
|
|
257
|
+
conn = None
|
|
258
|
+
trace_name = "opensips-cli.{}".format(random.randint(0, 65536))
|
|
259
|
+
trace_socket = "hep:{}:{};transport=tcp;version=3".format(
|
|
260
|
+
trace_ip, trace_port)
|
|
261
|
+
args = {
|
|
262
|
+
'id': trace_name,
|
|
263
|
+
'uri': trace_socket,
|
|
264
|
+
}
|
|
265
|
+
if filters:
|
|
266
|
+
args['filter'] = filters
|
|
267
|
+
|
|
268
|
+
logger.debug("filters are {}".format(filters))
|
|
269
|
+
trace_started = comm.execute('trace_start', args)
|
|
270
|
+
if not trace_started:
|
|
271
|
+
return False
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
conn, addr = s.accept()
|
|
275
|
+
logger.debug("New TCP connection from {}:{}".
|
|
276
|
+
format(addr[0], addr[1]))
|
|
277
|
+
remaining = b''
|
|
278
|
+
while True:
|
|
279
|
+
data = conn.recv(TRACE_BUFFER_SIZE)
|
|
280
|
+
if not data:
|
|
281
|
+
break
|
|
282
|
+
remaining = self.__print_hep(remaining + data)
|
|
283
|
+
if remaining is None:
|
|
284
|
+
break
|
|
285
|
+
except KeyboardInterrupt:
|
|
286
|
+
comm.execute('trace_stop', {'id' : trace_name }, True)
|
|
287
|
+
if conn is not None:
|
|
288
|
+
conn.close()
|
|
289
|
+
|
|
290
|
+
def __exclude__(self):
|
|
291
|
+
valid = comm.valid()
|
|
292
|
+
return (not valid[0], valid[1])
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
##
|
|
3
|
+
## This file is part of OpenSIPS CLI
|
|
4
|
+
## (see https://github.com/OpenSIPS/opensips-cli).
|
|
5
|
+
##
|
|
6
|
+
## This program is free software: you can redistribute it and/or modify
|
|
7
|
+
## it under the terms of the GNU General Public License as published by
|
|
8
|
+
## the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
## (at your option) any later version.
|
|
10
|
+
##
|
|
11
|
+
## This program is distributed in the hope that it will be useful,
|
|
12
|
+
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
## GNU General Public License for more details.
|
|
15
|
+
##
|
|
16
|
+
## You should have received a copy of the GNU General Public License
|
|
17
|
+
## along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
18
|
+
##
|
|
19
|
+
|
|
20
|
+
from opensipscli.module import Module
|
|
21
|
+
from opensipscli.logger import logger
|
|
22
|
+
from opensipscli.config import cfg
|
|
23
|
+
from opensipscli import comm
|
|
24
|
+
from threading import Thread
|
|
25
|
+
import subprocess
|
|
26
|
+
import shutil
|
|
27
|
+
import os
|
|
28
|
+
|
|
29
|
+
DEFAULT_PROCESS_NAME = 'opensips'
|
|
30
|
+
|
|
31
|
+
class trap(Module):
|
|
32
|
+
|
|
33
|
+
def get_process_name(self):
|
|
34
|
+
if cfg.exists("process_name"):
|
|
35
|
+
return cfg.get("process_name")
|
|
36
|
+
else:
|
|
37
|
+
return DEFAULT_PROCESS_NAME
|
|
38
|
+
|
|
39
|
+
def get_pids(self):
|
|
40
|
+
try:
|
|
41
|
+
mi_pids = comm.execute('ps')
|
|
42
|
+
self.pids = [str(pid['PID']) for pid in mi_pids['Processes']]
|
|
43
|
+
info = ["Process ID={} PID={} Type={}".
|
|
44
|
+
format(pid['ID'], pid['PID'], pid['Type'])
|
|
45
|
+
for pid in mi_pids['Processes']]
|
|
46
|
+
self.process_info = "\n".join(info)
|
|
47
|
+
except:
|
|
48
|
+
self.pids = []
|
|
49
|
+
|
|
50
|
+
def get_gdb_output(self, pid):
|
|
51
|
+
if os.path.islink("/proc/{}/exe".format(pid)):
|
|
52
|
+
# get process line of pid
|
|
53
|
+
process = os.readlink("/proc/{}/exe".format(pid))
|
|
54
|
+
else:
|
|
55
|
+
logger.error("could not find OpenSIPS process {} running on local machine".format(pid))
|
|
56
|
+
return -1
|
|
57
|
+
# Check if process is opensips (can be different if CLI is running on another host)
|
|
58
|
+
path, filename = os.path.split(process)
|
|
59
|
+
process_name = self.get_process_name()
|
|
60
|
+
if filename != process_name:
|
|
61
|
+
logger.error("process ID {}/{} is not OpenSIPS process".format(pid, filename))
|
|
62
|
+
return -1
|
|
63
|
+
logger.debug("Dumping backtrace for {} pid {}".format(process, pid))
|
|
64
|
+
cmd = ["gdb", process, pid, "-batch", "--eval-command", "bt full"]
|
|
65
|
+
out = subprocess.check_output(cmd)
|
|
66
|
+
if len(out) != 0:
|
|
67
|
+
self.gdb_outputs[pid] = out.decode()
|
|
68
|
+
|
|
69
|
+
def do_trap(self, params, modifiers):
|
|
70
|
+
|
|
71
|
+
self.pids = []
|
|
72
|
+
self.gdb_outputs = {}
|
|
73
|
+
self.process_info = ""
|
|
74
|
+
|
|
75
|
+
trap_file = cfg.get("trap_file")
|
|
76
|
+
process_name = self.get_process_name()
|
|
77
|
+
|
|
78
|
+
logger.info("Trapping {} in {}".format(process_name, trap_file))
|
|
79
|
+
if params and len(params) > 0:
|
|
80
|
+
self.pids = params
|
|
81
|
+
else:
|
|
82
|
+
thread = Thread(target=self.get_pids)
|
|
83
|
+
thread.start()
|
|
84
|
+
thread.join(timeout=1)
|
|
85
|
+
if len(self.pids) == 0:
|
|
86
|
+
logger.warning("could not get OpenSIPS pids through MI!")
|
|
87
|
+
try:
|
|
88
|
+
ps_pids = subprocess.check_output(["pidof", process_name])
|
|
89
|
+
self.pids = ps_pids.decode().split()
|
|
90
|
+
except:
|
|
91
|
+
logger.warning("could not find any OpenSIPS running!")
|
|
92
|
+
self.pids = []
|
|
93
|
+
|
|
94
|
+
if len(self.pids) < 1:
|
|
95
|
+
logger.error("could not find OpenSIPS' pids")
|
|
96
|
+
return -1
|
|
97
|
+
|
|
98
|
+
logger.debug("Dumping PIDs: {}".format(", ".join(self.pids)))
|
|
99
|
+
|
|
100
|
+
threads = []
|
|
101
|
+
for pid in self.pids:
|
|
102
|
+
thread = Thread(target=self.get_gdb_output, args=(pid,))
|
|
103
|
+
thread.start()
|
|
104
|
+
threads.append(thread)
|
|
105
|
+
|
|
106
|
+
for thread in threads:
|
|
107
|
+
thread.join()
|
|
108
|
+
|
|
109
|
+
if len(self.gdb_outputs) == 0:
|
|
110
|
+
logger.error("could not get output of gdb")
|
|
111
|
+
return -1
|
|
112
|
+
|
|
113
|
+
with open(trap_file, "w") as tf:
|
|
114
|
+
tf.write(self.process_info)
|
|
115
|
+
for pid in self.pids:
|
|
116
|
+
if pid not in self.gdb_outputs:
|
|
117
|
+
logger.warning("No output from pid {}".format(pid))
|
|
118
|
+
continue
|
|
119
|
+
try:
|
|
120
|
+
procinfo = subprocess.check_output(
|
|
121
|
+
["ps", "--no-headers", "-ww", "-fp", pid]).decode()[:-1]
|
|
122
|
+
except:
|
|
123
|
+
procinfo = "UNKNOWN"
|
|
124
|
+
|
|
125
|
+
tf.write("\n\n---start {} ({})\n{}".
|
|
126
|
+
format(pid, procinfo, self.gdb_outputs[pid]))
|
|
127
|
+
|
|
128
|
+
print("Trap file: {}".format(trap_file))
|
|
129
|
+
|
|
130
|
+
def __get_methods__(self):
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
def __exclude__(self):
|
|
134
|
+
valid = comm.valid()
|
|
135
|
+
if not valid[0]:
|
|
136
|
+
return False, valid[1]
|
|
137
|
+
# check to see if we have gdb installed
|
|
138
|
+
return (shutil.which("gdb") is None, None)
|