├── .bumpversion.cfg
├── .gitmodules
├── CMakeLists.txt
├── QUICK_START.md
├── README.md
├── VP_Logo_1200px-11th_Airborne_Division.patch.jpg
├── VP_Logo_1200px-11th_Airborne_Division.patch_small.jpg
├── VP_Logo_1200px-11th_Airborne_Division.patch_small2.jpg
├── conf.xml
├── doc
└── cluecon2018_voip_patrol.pdf
├── docker
├── Dockerfile
├── build_image.sh
├── entry.sh
├── run.sh
├── voip_patrol.sh
└── xml
│ └── basic_server.xml
├── include
├── config_site.h
├── log.h
├── pj_util.hpp
├── util.hh
└── version.h
├── load_test
├── LOAD_TEST.md
├── load.xml
└── run.sh
├── src
├── curl
│ └── email.h
├── ezxml
│ ├── changelog.txt
│ ├── ezxml.c
│ ├── ezxml.h
│ ├── ezxml.html
│ ├── ezxml.txt
│ └── license.txt
└── voip_patrol
│ ├── action.cc
│ ├── action.hh
│ ├── check.cc
│ ├── check.hh
│ ├── mod_voip_patrol.cc
│ ├── mod_voip_patrol.hh
│ ├── voip_patrol.cc
│ └── voip_patrol.hh
├── tasks.py
├── test
└── tls.xml
├── voice_ref_files
├── reference_8000.wav
└── reference_8000_12s.wav
└── xml
└── tls_server.xml
/.bumpversion.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 0.7.8
3 | commit = True
4 | tag = True
5 | tag_name = {new_version}
6 |
7 | [bumpversion:file:include/version.h]
8 |
9 | [bumpversion:file:docker/Dockerfile]
10 |
11 | [bumpversion:file:docker/build_image.sh]
12 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "pjproject"]
2 | path = pjproject
3 | url = https://github.com/jchavanton/pjproject.git
4 |
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.2)
2 | project(voip_patrol VERSION 1.0.0 LANGUAGES C CXX)
3 |
4 | set(CMAKE_VERBOSE_MAKEFILE ON)
5 |
6 | message("CMAKE_SYSTEM_PROCESSOR:${CMAKE_SYSTEM_PROCESSOR} CMAKE_SYSTEM:${CMAKE_SYSTEM} CMAKE_BUILD_TYPE:${CMAKE_BUILD_TYPE} OV:${OV}")
7 |
8 | set(ROOT_DIR ".")
9 | set(SRC_DIR "${ROOT_DIR}/src")
10 | set(CURL_SRC_DIR "${SRC_DIR}/curl")
11 | set(EZXML_SRC_DIR "${SRC_DIR}/ezxml")
12 | set(VOIP_PATROL_SRC_DIR "${SRC_DIR}/voip_patrol")
13 |
14 | set(EZXML_SRCS
15 | ${EZXML_SRC_DIR}/ezxml.c
16 | )
17 | include_directories("${SRC_DIR}")
18 |
19 | set(VOIP_PATROL_SRCS_CPP
20 | ${VOIP_PATROL_SRC_DIR}/mod_voip_patrol.cc
21 | ${VOIP_PATROL_SRC_DIR}/voip_patrol.cc
22 | ${VOIP_PATROL_SRC_DIR}/action.cc
23 | ${VOIP_PATROL_SRC_DIR}/check.cc
24 | )
25 |
26 | set(VOIP_PATROL_SRCS_C
27 |
28 | )
29 |
30 | include_directories(${ROOT_DIR}/pjproject/pjmedia/include)
31 | include_directories(${ROOT_DIR}/pjproject/pjsip/include)
32 | include_directories(${ROOT_DIR}/pjproject/pjlib/include)
33 | include_directories(${ROOT_DIR}/pjproject/pjnath/include)
34 | include_directories(${ROOT_DIR}/pjproject/pjlib-util/include)
35 |
36 | include_directories(${ROOT_DIR}/include)
37 |
38 | set(SOURCE_FILES ${VOIP_PATROL_SRCS_CPP} ${VOIP_PATROL_SRCS_C} ${CURL_SRCS} ${EZXML_SRCS})
39 |
40 | add_executable(voip_patrol ${SOURCE_FILES})
41 |
42 | set(CMAKE_LIBRARY_PATH
43 | "${ROOT_DIR}/pjproject/pjsip/lib"
44 | "${ROOT_DIR}/pjproject/pjnath/lib"
45 | "${ROOT_DIR}/pjproject/pjlib/lib"
46 | "${ROOT_DIR}/pjproject/media/lib"
47 | "${ROOT_DIR}/pjproject/pjlib-util/lib"
48 | )
49 |
50 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -g")
51 |
52 | execute_process(COMMAND "./pjproject/config.guess" OUTPUT_VARIABLE AC_SYSTEM)
53 | string(STRIP ${AC_SYSTEM} AC_SYSTEM)
54 | target_link_libraries(voip_patrol
55 | pjsua2-${AC_SYSTEM}
56 | stdc++
57 | pjsua-${AC_SYSTEM}
58 | pjsip-ua-${AC_SYSTEM}
59 | pjsip-simple-${AC_SYSTEM}
60 | pjsip-${AC_SYSTEM}
61 | pjmedia-codec-${AC_SYSTEM}
62 | pjmedia-${AC_SYSTEM}
63 | pjmedia-videodev-${AC_SYSTEM}
64 | pjmedia-audiodev-${AC_SYSTEM}
65 | pjmedia-${AC_SYSTEM}
66 | pjnath-${AC_SYSTEM}
67 | pjlib-util-${AC_SYSTEM}
68 | srtp-${AC_SYSTEM}
69 | resample-${AC_SYSTEM}
70 | gsmcodec-${AC_SYSTEM}
71 | speex-${AC_SYSTEM}
72 | ilbccodec-${AC_SYSTEM}
73 | g7221codec-${AC_SYSTEM}
74 | pj-${AC_SYSTEM}
75 | pthread
76 | curl
77 | m
78 | asound
79 | ssl
80 | -std=c++11
81 | )
82 |
83 | find_package(PkgConfig REQUIRED)
84 | pkg_search_module(OPENSSL openssl)
85 | if( OPENSSL_FOUND )
86 | message(">> openssl found")
87 | target_link_libraries(voip_patrol ssl crypto)
88 | else()
89 | message(">> openssl not found")
90 | endif()
91 |
92 | pkg_search_module(OPUS opus)
93 | if( OPUS_FOUND )
94 | message(">> opus found")
95 | target_link_libraries(voip_patrol opus)
96 | else()
97 | message(">> opus not found")
98 | endif()
99 |
100 | pkg_search_module(UUID uuid)
101 | if( UUID_FOUND )
102 | message(">> uuid found")
103 | target_link_libraries(voip_patrol uuid)
104 | else()
105 | message(">> uuid not found")
106 | endif()
107 |
--------------------------------------------------------------------------------
/QUICK_START.md:
--------------------------------------------------------------------------------
1 | ## Quick start with docker
2 |
3 | ### cloning voip_patrol
4 | (only needed to get the shell scripts to build your own image)
5 | ```
6 | git clone https://github.com/jchavanton/voip_patrol.git
7 | ```
8 |
9 | ### example: building docker image
10 | (optional, you can pull unless you want to build your own image)
11 | ```
12 | cd voip_patrol/docker
13 | ./build_image.sh
14 | ```
15 |
16 | ### example: pulling image from dockerhub
17 | ```
18 | docker pull jchavanton/voip_patrol:latest
19 | ```
20 |
21 | ### example: running the container
22 | ```
23 | cd voip_patrol/docker
24 | ./run.sh
25 | ```
26 | #### or use the following
27 | ```bash
28 | #!/bin/sh
29 | IMAGE=voip_patrol:latest
30 | CONTAINER_NAME=voip_patrol
31 | docker run -d --net=host --name=${CONTAINER_NAME} ${IMAGE}
32 | ```
33 |
34 | ### done, voip_patrol should running as a server on 5060 !
35 | ```
36 | docker logs voip_patrol
37 | XML_CONF[basic_server.xml] RESULT FILE[result.json] PORT[5060]
38 | Running /git/voip_patrol/voip_patrol --port 5060 --conf /xml/basic_server.xml --output /output/result.json
39 | ```
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://hub.docker.com/r/jchavanton/voip_patrol/)
2 |
3 | # VoIP Patrol
4 | 
5 |
6 | ## VoIP signaling and media test automaton
7 | Designed to automate end2end and or integration tests.
8 |
9 | VoIP patrol will follow a scenario in XML format and will output results in JSON.
10 |
11 | Each line in the output file is a separate JSON structure, note that the entire file is not a valid JSON file,
12 | this is because VoIP patrol will output results as they become available.
13 |
14 | It is possible to test many scenarios that are not easy to test manually like a re-invite with a new codec.
15 |
16 | ### Docker quick start
17 | [quick start with docker](QUICK_START.md)
18 |
19 |
20 | ### Linux Debian building from sources
21 | [see commands in Dockerfile](docker/Dockerfile)
22 |
23 | ### Load test example
24 | [load test example](load_test/LOAD_TEST.md)
25 |
26 | ### run
27 | ```
28 | ./voip_patrol --help
29 | ```
30 |
31 | ### Example: making a test call
32 | ```xml
33 |
34 |
35 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | ```
55 | ### Sample JSON output
56 | ```json
57 | {
58 | "2": {
59 | "label": "us-east-va",
60 | "start": "17-07-2018 00:00:05",
61 | "end": "17-07-2018 00:00:24",
62 | "action": "call",
63 | "from": "15147371787",
64 | "to": "12012665228",
65 | "result": "PASS",
66 | "expected_cause_code": 200,
67 | "cause_code": 200,
68 | "reason": "Normal call clearing",
69 | "callid": "7iYDFukJr-9BOLOmWg.7fZyHZeZUAwao",
70 | "transport": "TLS",
71 | "peer_socket": "34.226.136.32:5061",
72 | "duration": 16,
73 | "expected_duration": 0,
74 | "max_duration": 20,
75 | "hangup_duration": 16,
76 | "rtp_stats_0": {
77 | "rtt": 0,
78 | "remote_rtp_socket": "10.250.7.88:4028",
79 | "codec_name": "PCMU",
80 | "clock_rate": "8000",
81 | "Tx": {
82 | "jitter_avg": 0,
83 | "jitter_max": 0,
84 | "pkt": 816,
85 | "kbytes": 127,
86 | "loss": 0,
87 | "discard": 0,
88 | "mos_lq": 4.5
89 | },
90 | "Rx": {
91 | "jitter_avg": 0,
92 | "jitter_max": 0,
93 | "pkt": 813,
94 | "kbytes": 127,
95 | "loss": 0,
96 | "discard": 0,
97 | "mos_lq": 4.5
98 | }
99 | }
100 | }
101 | }
102 | ```
103 |
104 | ### Example: starting a TLS server
105 | ```bash
106 | ./voip_patrol \
107 | --port 5060 \ # TLS port 5061 +1
108 | --conf "xml/tls_server.xml" \
109 | --tls-calist "tls/ca_list.pem" \
110 | --tls-privkey "tls/key.pem" \
111 | --tls-cert "tls/certificate.pem" \
112 | --tls-verify-server \
113 | ```
114 |
115 | ```xml
116 |
117 |
118 |
120 |
128 |
129 |
131 |
132 |
133 |
134 | ```
135 |
136 | ### Example: accepting calls and checking for specific header
137 | ```xml
138 |
139 |
140 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | ```
152 |
153 | ### Example: accepting calls and checking for specific header with exact match or regular expression and no match on other
154 | ```xml
155 |
156 |
157 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 | ```
177 |
178 | ### Example: accepting calls and searching the message with a regular expression
179 | ```xml
180 |
181 |
182 |
187 |
188 |
189 |
190 |
191 |
192 |
193 | ```
194 |
195 | ### Example: making tests calls with wait_until
196 | Scenario execution is sequential and non-blocking.
197 | We can use “wait” command with previously set “wait_until” params
198 | to control parallel execution.
199 |
200 | ```
201 | Call States
202 | NULL : Before INVITE is sent or received
203 | CALLING : After INVITE is sent
204 | INCOMING : After INVITE is received.
205 | EARLY : After response with To tag.
206 | CONNECTING : After 2xx is sent/received.
207 | CONFIRMED : After ACK is sent/received.
208 | DISCONNECTED
209 | ```
210 | ```xml
211 |
212 |
213 |
220 |
221 |
222 |
229 |
230 |
231 |
232 | ```
233 |
234 | ### Example: testing registration
235 | ```xml
236 |
237 |
238 |
239 |
248 |
249 |
250 |
251 | ```
252 |
253 | ### Example: re-invite with new codec
254 | ```xml
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 | ```
283 |
284 | ### Example: Overwriting local contact header
285 | ```xml
286 |
287 |
288 |
289 |
290 |
291 |
292 |
304 |
305 |
306 |
307 |
308 |
309 | ```
310 |
311 | ### Example: WAIT action
312 | #### wait forever:
313 | ```xml
314 |
315 | ```
316 | #### wait until you receive a certain amount of calls
317 | ```xml
318 |
319 |
320 | ```
321 | #### wait 5 seconds or one call
322 | ```xml
323 |
324 |
325 | ```
326 |
327 | ### Sample JSON output RTP stats report with multiples sessions
328 | #### one block is generated everytime a session is created
329 | ```json
330 | {
331 | "rtp_stats_0": {
332 | "rtt": 0,
333 | "remote_rtp_socket": "10.250.7.88:4028",
334 | "codec_name": "PCMA",
335 | "clock_rate": "8000",
336 | "Tx": {
337 | "jitter_avg": 0,
338 | "jitter_max": 0,
339 | "pkt": 105,
340 | "kbytes": 16,
341 | "loss": 0,
342 | "discard": 0,
343 | "mos_lq": 4.5
344 | },
345 | "Rx": {
346 | "jitter_avg": 0,
347 | "jitter_max": 0,
348 | "pkt": 104,
349 | "kbytes": 16,
350 | "loss": 0,
351 | "discard": 0,
352 | "mos_lq": 4.5
353 | }
354 | },
355 | "rtp_stats_1": {
356 | "rtt": 0,
357 | "remote_rtp_socket": "10.250.7.89:40230",
358 | "codec_name": "PCMU",
359 | "clock_rate": "8000",
360 | "Tx": {
361 | "jitter_avg": 0,
362 | "jitter_max": 0,
363 | "pkt": 501,
364 | "kbytes": 78,
365 | "loss": 0,
366 | "discard": 0,
367 | "mos_lq": 4.5
368 | },
369 | "Rx": {
370 | "jitter_avg": 0,
371 | "jitter_max": 0,
372 | "pkt": 501,
373 | "kbytes": 78,
374 | "loss": 0,
375 | "discard": 0,
376 | "mos_lq": 4.5
377 | }
378 | }
379 | }
380 | ```
381 | ### Example: email reporting
382 | ```xml
383 |
384 |
385 |
390 |
391 |
392 |
393 |
394 | ```
395 |
396 | ### accept command parameters
397 |
398 | | Name | Type | Description |
399 | | ---- | ---- | ----------- |
400 | | ring_duration | int | ringing duration in seconds |
401 | | early_media | bool | if "true" 183 with SDP and early media is used |
402 | | timer | string | control SIP session timers, possible values are : inactive, optional, required or always |
403 | | code | int | SIP cause code to return must be >100 and <700 |
404 | | account | string | Account will be used if it matches the user part of an incoming call RURI or "default" will catch all |
405 | | response_delay | int | ms delay before reponse is sent, useful to test timeouts and race conditions |
406 | | call_count | int | The amount of calls to receive to consider the command completed, default -1 (considered completed) |
407 | | transport | string | Force a specific transport for all messages on accepted calls, default to all transport available |
408 | | re_invite_interval | int | Interval in seconds at which a re-invite with SDP will be sent |
409 | | rtp_stats | bool | if "true" the json report will include a report on RTP transmission |
410 | | srtp | string | Comma-separated values of the following "sdes" - add SDES support, "dtls" - add DTLS-SRTP support, "force" - make SRTP mandatory |
411 | | hangup | int | call duration in second before hangup |
412 | | label | string | test description or label |
413 | | record | bool | if "true" the call will be recorded once connected in /voice_files |
414 | | record_early | bool | if "true" the call will be recorded when early media starts in /voice_files. If call is answered after, recording will continue in the same file |
415 |
416 | ### call command parameters
417 |
418 | | Name | Type | Description |
419 | | ---- | ---- | ----------- |
420 | | timer | string | control SIP session timers, possible values are : inactive, optional, required or always |
421 | | proxy | string | ip/hostname of a proxy where to send the call |
422 | | caller | string | From header user@host, only used if from it not specified |
423 | | display_name | string | From and Contact header display name, example: "Alice" |
424 | | callee | string | request URI user@host (also used in the To header unless to_uri is specified) |
425 | | to_uri | string | used@host part of the URI in the To header |
426 | | transport | string | force a specific transport |
427 | | re_invite_interval | int | Interval in seconds at which a re-invite with SDP will be sent |
428 | | rtp_stats | bool | if "true" the json report will include a report on RTP transmission |
429 | | srtp | string | Comma-separated values of the following "sdes" - add SDES support, "dtls" - add DTLS-SRTP support, "force" - make SRTP mandatory. Note, if you don't specify "force", call would be made with plain RTP. If you specify both "sdes" and "dtls", DTLS-SRTP would be used regardless of order. |
430 | | late_start | bool | if "true" no SDP will be included in the INVITE and will result in a late offer in 200 OK/ACK |
431 | | record | bool | if "true" the call will be recorded once connected in /voice_files |
432 | | record_early | bool | if "true" the call will be recorded when early media starts in /voice_files. If call is answered after, recording will continue in the same file |
433 | | force_contact | string | local contact header will be overwritten by the given string |
434 | | max_ringing_duration | int | max ringing duration in seconds before cancel, default 60 |
435 | | hangup | int | call duration in second before hangup |
436 | | repeat | int | do this call multiple times |
437 | | username | string | authentication username, account name, From/To/Contact header user part |
438 | | password | string | authentication password |
439 | | label | string | test description or label |
440 |
441 | ### register command parameters
442 |
443 | | Name | Type | Description |
444 | | ---- | ---- | ----------- |
445 | | proxy | string | ip/hostname of a proxy where to send the register |
446 | | username | string | authentication username, account name, From/To/Contact header user part |
447 | | password | string | authentication password |
448 | | account | string | if not specified username is used, this is the the account name and From/To/Contact header user part |
449 | | registrar | string | SIP UAS handling registration where the messages will be sent |
450 | | transport | string | force a specific transport |
451 | | unregister | bool | unregister the account |
452 | | reg_id | int | if present outbound and other related parameters will be added see RFC5626 |
453 | | instance_id | int | same as reg_id, if not present, it will be generated automatically |
454 | | rewrite_contact | bool | default true, detect public IP when registering and rewrite the contact header |
455 | | srtp | string | Comma-separated values of the following "sdes" - add SDES support, "dtls" - add DTLS-SRTP support, "force" - make SRTP mandatory. Used for incoming calls to this account |
456 | | account | string | if not specified username is used, this is the the account name and From/To/Contact header user part |
457 | | registrar | string | SIP UAS handling registration where the messages will be sent |
458 | | transport | string | force a specific transport |
459 | | unregister | bool | unregister the account |
460 | | reg_id | int | if present outbound and other related parameters will be added see RFC5626 |
461 | | instance_id | int | same as reg_id, if not present, it will be generated automatically |
462 | | rewrite_contact | bool | default true, detect public IP when registering and rewrite the contact header |
463 | | srtp | string | Comma-separated values of the following "sdes" - add SDES support, "dtls" - add DTLS-SRTP support, "force" - make SRTP mandatory. Used for incoming calls to this account |
464 |
465 | ### message command parameters
466 |
467 | | Name | Type | Description |
468 | | ---- | ---- | ----------- |
469 | | from | string | From header complete "\"Display Name\" " |
470 | | to_uri | string | used@host part of the URI in the To header |
471 | | transport | string | force a specific transport |
472 | | username | string | authentication username, account name, From/To/Contact header user part |
473 | | password | string | authentication password |
474 | | label | string | test description or label |
475 |
476 | ### Example: sending a message
477 | ```xml
478 |
479 |
480 |
481 |
489 |
490 |
491 |
492 | ```
493 |
494 | ### accept_message command parameters
495 |
496 | | Name | Type | Description |
497 | | ---- | ---- | ----------- |
498 | | account | string | Account will be used if it matches the user part of an incoming message RURI or "default" will catch all |
499 | | message_count | int | The amount of messages to receive to consider the command completed, default -1 (considered completed) |
500 | | transport | string | Force a specific transport for all messages on accepted messages, default to all transport available |
501 | | label | string | test description or label |
502 |
503 | ### Example: receiving a message
504 | ```xml
505 |
506 |
507 |
508 |
514 |
515 |
519 |
520 |
521 |
522 | ```
523 |
524 | ### wait command parameters
525 |
526 | | Name | Type | Description |
527 | | ---- | ---- | ----------- |
528 | | complete | bool | if "true" wait for all the test to complete (or reach their wait_until state) before executing next action or disconnecting calls and exiting, needed in most cases |
529 | | ms | int | the amount of milliseconds to wait before executing next action or disconnecting calls and exiting, if -1 wait forever |
530 |
531 | ### Example: codec configuration
532 | ```xml
533 |
534 |
535 |
536 |
537 |
538 |
539 |
540 |
541 | ```
542 |
543 | ### codec command parameters
544 |
545 | | Name | Type | Description |
546 | | ---- | ---- | ----------- |
547 | | priority | int | 0-255, where zero means to disable the codec |
548 | | enable | string | Codec payload type ID, ex. "g722", "pcma", "opus" or "all" |
549 | | disable | string | Codec payload type ID, ex. "g722", "pcma", "opus" or "all" |
550 |
551 | ### Example: TURN configuration
552 | ```xml
553 |
554 |
555 |
556 |
557 |
558 |
559 |
560 | ```
561 |
562 | ### turn command parameters
563 |
564 | | Name | Type | Description |
565 | | ---- | ---- | ----------- |
566 | | enabled | bool | if "true" turn server usage will be enabled |
567 | | server | string | turn server URI or IP:port |
568 | | username | string | turn server username |
569 | | password | string | turn server password |
570 | | password_hashed | bool | if "true" us hashed password, default plain password |
571 | | sip_stun_use | bool | if "true" SIP reflective IP is use with signaling |
572 | | media_stun_use | bool | if "true" STUN reflective IP is use with media/SDP |
573 | | stun_only | bool | if "true" TURN and ICE are disabled and only STUN is use |
574 |
575 | ### using multiple accounts
576 | When using multiple accounts, accounts can be created and selected with the following parameters.
577 |
578 | |command | account parameter |
579 | | ------ | ----------------- |
580 | | accept | account |
581 | | register | account |
582 | | call | caller |
583 | |accept_message| account |
584 | | message | from |
585 |
586 | ### using env variable in scenario actions parameters
587 | Any value starting with `VP_ENV` will be replaced by the envrironment variable of the same name.
588 | Example : `username="VP_ENV_USERNAME"`
589 | ```bash
590 | export VP_ENV_PASSWORD=????????
591 | export VP_ENV_USERNAME=username
592 | ```
593 |
594 | ### Docker
595 | ```bash
596 | voip_patrol/docker$ tree
597 | .
598 | ├── build.sh # docker build command example
599 | ├── Dockerfile # docker build file for Linux Alpine
600 | └── voip_patrol.sh # docker run example starting
601 | ```
602 |
603 | ## Dependencies
604 |
605 | #### PJSUA2
606 | PJSUA2 : A C++ High Level Softphone API : built on top of PJSIP and PJMEDIA
607 | http://www.pjsip.org
608 | http://www.pjsip.org/docs/book-latest/PJSUA2Doc.pdf
609 |
610 | ## External tool to test audio quality
611 |
612 | #### PESQ
613 | P.862 : Perceptual evaluation of speech quality (PESQ): An objective method for end-to-end speech quality assessment of narrow-band telephone networks and speech codecs
614 | http://www.itu.int/rec/T-REC-P.862
615 | ```
616 | ./run_pesq +16000 voice_files/reference.wav voice_files/recording.wav
617 | ```
618 |
--------------------------------------------------------------------------------
/VP_Logo_1200px-11th_Airborne_Division.patch.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jchavanton/voip_patrol/215253585efa4ec9dbaea1cd99accd49821fb9cc/VP_Logo_1200px-11th_Airborne_Division.patch.jpg
--------------------------------------------------------------------------------
/VP_Logo_1200px-11th_Airborne_Division.patch_small.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jchavanton/voip_patrol/215253585efa4ec9dbaea1cd99accd49821fb9cc/VP_Logo_1200px-11th_Airborne_Division.patch_small.jpg
--------------------------------------------------------------------------------
/VP_Logo_1200px-11th_Airborne_Division.patch_small2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jchavanton/voip_patrol/215253585efa4ec9dbaea1cd99accd49821fb9cc/VP_Logo_1200px-11th_Airborne_Division.patch_small2.jpg
--------------------------------------------------------------------------------
/conf.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/doc/cluecon2018_voip_patrol.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jchavanton/voip_patrol/215253585efa4ec9dbaea1cd99accd49821fb9cc/doc/cluecon2018_voip_patrol.pdf
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.7-buster
2 |
3 | ARG VERSION="0.7.8"
4 |
5 | RUN echo "installing dependencies" \
6 | && apt-get update && apt-get install -y build-essential libcurl4-openssl-dev cmake pkg-config libasound2-dev \
7 | && apt-get -y install libssl-dev git
8 |
9 | RUN echo "building VoIP Patrol" \
10 | && mkdir /git && cd /git && git clone https://github.com/jchavanton/voip_patrol.git \
11 | && cd voip_patrol && git checkout ${VERSION} \
12 | && git submodule update --init \
13 | && cp include/config_site.h pjproject/pjlib/include/pj/config_site.h \
14 | && cd pjproject && ./configure --disable-libwebrtc --disable-opencore-amr \
15 | && make dep && make && make install \
16 | && cd .. && cmake CMakeLists.txt && make
17 |
18 | RUN ln -s /git/voip_patrol/voice_ref_files /voice_ref_files
19 |
20 | RUN mkdir /xml
21 | RUN mkdir /output
22 |
23 | COPY xml/basic_server.xml /xml
24 | COPY entry.sh /
25 | ENTRYPOINT ["/entry.sh"]
26 |
--------------------------------------------------------------------------------
/docker/build_image.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | TAG="0.7.8"
3 | docker build . --no-cache -t voip_patrol
4 | docker tag voip_patrol:latest jchavanton/voip_patrol:latest
5 | docker tag voip_patrol:latest jchavanton/voip_patrol:${TAG}
6 | echo "Don't forget to push !"
7 | echo "docker push jchavanton/voip_patrol:latest && docker push jchavanton/voip_patrol:${TAG}"
8 |
--------------------------------------------------------------------------------
/docker/entry.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | XML_CONF=${XML_CONF-"basic_server.xml"}
4 | RESULT_FILE=${RESULT_FILE-"result.json"}
5 | PORT=${PORT="5060"}
6 |
7 | echo "XML_CONF[${XML_CONF}] RESULT FILE[${RESULT_FILE}] PORT[$PORT]"
8 |
9 | if [ "$1" = "" ]; then
10 | CMD="/git/voip_patrol/voip_patrol --port ${PORT} --conf /xml/${XML_CONF} --output /output/${RESULT_FILE}"
11 | else
12 | CMD="$*"
13 | fi
14 |
15 | echo "Running ${CMD}"
16 | exec ${CMD}
17 |
--------------------------------------------------------------------------------
/docker/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | DIR_PREFIX=`pwd`
3 | IMAGE=voip_patrol:latest
4 | CONTAINER_NAME=voip_patrol
5 | docker rm ${CONTAINER_NAME}
6 | docker run -d --net=host --name=${CONTAINER_NAME} ${IMAGE}
7 |
8 |
9 | # PORT=5060
10 | # XML_CONF="basic_server.xml"
11 | # RESULT_FILE="result.json"
12 | #
13 | # docker run -d --net=host \
14 | # --name=${CONTAINER_NAME} \
15 | # --env XML_CONF=`echo ${XML_CONF}` \
16 | # --env PORT=`echo ${PORT}` \
17 | # --env RESULT_FILE=`echo ${RESULT_FILE}` \
18 | # --env EDGEPROXY=`echo $EDGEPROXY` \
19 | # --volume $DIR_PREFIX/xml:/xml \
20 | # --volume $DIR_PREFIX/log:/output \
21 | # --volume $DIR_PREFIX/voice_files:/voice_ref_files \
22 | # ${IMAGE}
23 |
--------------------------------------------------------------------------------
/docker/voip_patrol.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | DIR_PREFIX=`pwd`
3 | IMAGE=voip_patrol:latest
4 | docker stop voip_patrol
5 | docker rm voip_patrol
6 | docker run -d --net=host --name=voip_patrol --volume $DIR_PREFIX/../xml:/xml \
7 | $IMAGE \
8 | ./git/voip_patrol/voip_patrol -p 5555 -c /xml/tls_server.xml -l /log/voip_patrol.log -o result.json
9 | # /bin/sh -c "tail -f /dev/null"
10 |
--------------------------------------------------------------------------------
/docker/xml/basic_server.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/include/config_site.h:
--------------------------------------------------------------------------------
1 | // #define PJ_IOQUEUE_MAX_HANDLES 1024
2 | // #define FD_SETSIZE PJ_IOQUEUE_MAX_HANDLES
3 | //
4 | //
5 | #define PJ_IOQUEUE_MAX_HANDLES 1024
6 | #define FD_SETSIZE_SETABLE 1
7 | #define __FD_SETSIZE 1024
8 |
9 | #define PJSIP_MAX_TRANSPORTS 32
10 | #define PJSIP_MAX_RESOLVED_ADDRESSES 32
11 |
12 | #define PJSUA_MAX_ACC 512
13 | #define PJSUA_MAX_CALLS 512
14 | #define PJSUA_MAX_PLAYERS 512
15 |
16 | // SRTP
17 | #define PJMEDIA_SRTP_HAS_DTLS 1
18 | // Make send of "100 - Trying" explicit
19 | #define PJSUA_DISABLE_AUTO_SEND_100 1
20 |
--------------------------------------------------------------------------------
/include/log.h:
--------------------------------------------------------------------------------
1 | #ifndef __LOG_H__
2 | #define __LOG_H__
3 |
4 | #include
5 | #include
6 | #include
7 |
8 | inline std::string NowTime();
9 |
10 | enum TLogLevel {logERROR, logWARNING, logINFO, logDEBUG, logDEBUG1, logDEBUG2, logDEBUG3, logDEBUG4};
11 |
12 | template
13 | class Log
14 | {
15 | public:
16 | Log();
17 | virtual ~Log();
18 | std::ostringstream& Get(TLogLevel level = logINFO);
19 | public:
20 | static TLogLevel& ReportingLevel();
21 | static std::string ToString(TLogLevel level);
22 | static TLogLevel FromString(const std::string& level);
23 | protected:
24 | std::ostringstream os;
25 | private:
26 | Log(const Log&);
27 | Log& operator =(const Log&);
28 | };
29 |
30 | template
31 | Log::Log()
32 | {
33 | }
34 |
35 | template
36 | std::ostringstream& Log::Get(TLogLevel level)
37 | {
38 | os << "[" << NowTime();
39 | os << "][" << ToString(level) << "] ";
40 | os << std::string(level > logDEBUG ? level - logDEBUG : 0, '\t');
41 | return os;
42 | }
43 |
44 | template
45 | Log::~Log()
46 | {
47 | os << std::endl;
48 | T::Output(os.str());
49 | }
50 |
51 | template
52 | TLogLevel& Log::ReportingLevel()
53 | {
54 | static TLogLevel reportingLevel = logDEBUG4;
55 | return reportingLevel;
56 | }
57 |
58 | template
59 | std::string Log::ToString(TLogLevel level)
60 | {
61 | static const char* const buffer[] = {"ERROR", "WARNING", "INFO", "DEBUG", "DEBUG1", "DEBUG2", "DEBUG3", "DEBUG4"};
62 | return buffer[level];
63 | }
64 |
65 | template
66 | TLogLevel Log::FromString(const std::string& level)
67 | {
68 | if (level == "DEBUG4")
69 | return logDEBUG4;
70 | if (level == "DEBUG3")
71 | return logDEBUG3;
72 | if (level == "DEBUG2")
73 | return logDEBUG2;
74 | if (level == "DEBUG1")
75 | return logDEBUG1;
76 | if (level == "DEBUG")
77 | return logDEBUG;
78 | if (level == "INFO")
79 | return logINFO;
80 | if (level == "WARNING")
81 | return logWARNING;
82 | if (level == "ERROR")
83 | return logERROR;
84 | Log().Get(logWARNING) << "Unknown logging level '" << level << "'. Using INFO level as default.";
85 | return logINFO;
86 | }
87 |
88 | class Output2FILE
89 | {
90 | public:
91 | static FILE*& Stream();
92 | static void Output(const std::string& msg);
93 | };
94 |
95 | inline FILE*& Output2FILE::Stream()
96 | {
97 | static FILE* pStream = stderr;
98 | return pStream;
99 | }
100 |
101 | inline void Output2FILE::Output(const std::string& msg)
102 | {
103 | FILE* pStream = Stream();
104 | if (!pStream)
105 | return;
106 | fprintf(pStream, "%s", msg.c_str());
107 | fflush(pStream);
108 | }
109 |
110 | #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__)
111 | # if defined (BUILDING_FILELOG_DLL)
112 | # define FILELOG_DECLSPEC __declspec (dllexport)
113 | # elif defined (USING_FILELOG_DLL)
114 | # define FILELOG_DECLSPEC __declspec (dllimport)
115 | # else
116 | # define FILELOG_DECLSPEC
117 | # endif // BUILDING_DBSIMPLE_DLL
118 | #else
119 | # define FILELOG_DECLSPEC
120 | #endif // _WIN32
121 |
122 | class FILELOG_DECLSPEC FILELog : public Log {};
123 | //typedef Log FILELog;
124 |
125 | #ifndef FILELOG_MAX_LEVEL
126 | #define FILELOG_MAX_LEVEL logDEBUG4
127 | #endif
128 |
129 | #define LOG(level) \
130 | if (level > FILELOG_MAX_LEVEL) ;\
131 | else if (level > FILELog::ReportingLevel() || !Output2FILE::Stream()) ; \
132 | else FILELog().Get(level)
133 |
134 | #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__)
135 |
136 | #include
137 |
138 | inline std::string NowTime()
139 | {
140 | const int MAX_LEN = 200;
141 | char buffer[MAX_LEN];
142 | if (GetTimeFormatA(LOCALE_USER_DEFAULT, 0, 0,
143 | "HH':'mm':'ss", buffer, MAX_LEN) == 0)
144 | return "Error in NowTime()";
145 |
146 | char result[100] = {0};
147 | static DWORD first = GetTickCount();
148 | std::sprintf(result, "%s.%03ld", buffer, (long)(GetTickCount() - first) % 1000);
149 | return result;
150 | }
151 |
152 | #else
153 |
154 | #include
155 |
156 | inline std::string NowTime()
157 | {
158 | char buffer[11];
159 | time_t t;
160 | time(&t);
161 | tm r = {0};
162 | strftime(buffer, sizeof(buffer), "%X", localtime_r(&t, &r));
163 | struct timeval tv;
164 | gettimeofday(&tv, 0);
165 | char result[100] = {0};
166 | std::sprintf(result, "%s.%03ld", buffer, (long)tv.tv_usec / 1000);
167 | return result;
168 | }
169 |
170 | #endif //WIN32
171 |
172 | #endif //__LOG_H__
173 |
--------------------------------------------------------------------------------
/include/pj_util.hpp:
--------------------------------------------------------------------------------
1 | /* $Id: util.hpp 4704 2014-01-16 05:30:46Z ming $ */
2 | /*
3 | * Copyright (C) 2013 Teluu Inc. (http://www.teluu.com)
4 | *
5 | * This program is free software; you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation; either version 2 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program; if not, write to the Free Software
17 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 | */
19 |
20 | #include
21 | #include
22 |
23 | #define PJ2BOOL(var) ((var) != PJ_FALSE)
24 |
25 | namespace pj
26 | {
27 | using std::string;
28 |
29 | inline pj_str_t str2Pj(const string &input_str)
30 | {
31 | pj_str_t output_str;
32 | output_str.ptr = (char*)input_str.c_str();
33 | output_str.slen = input_str.size();
34 | return output_str;
35 | }
36 |
37 | inline string pj2Str(const pj_str_t &input_str)
38 | {
39 | if (input_str.ptr)
40 | return string(input_str.ptr, input_str.slen);
41 | return string();
42 | }
43 |
44 |
45 | } // namespace
46 |
--------------------------------------------------------------------------------
/include/util.hh:
--------------------------------------------------------------------------------
1 | #include
2 |
3 |
4 | namespace vp {
5 |
6 | void tolower(string s) {
7 | std::transform(s.begin(), s.end(), s.begin(), ::tolower);
8 | }
9 |
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/include/version.h:
--------------------------------------------------------------------------------
1 | #ifndef __VERSION_H__
2 | #define __VERSION_H__
3 |
4 | const std::string VERSION = "0.7.8";
5 | #endif
6 |
--------------------------------------------------------------------------------
/load_test/LOAD_TEST.md:
--------------------------------------------------------------------------------
1 | ## Example of load tests with voip_patrol
2 |
3 | ### this will start a multi window tmux with four instances of voip_patrol
4 | [run.sh](run.sh)
5 | ```
6 | ./run.sh "10.0.0.1:5060"
7 | ```
8 |
9 | ### validating results
10 | Voip_patrol json test results can be use to confirm that the transmission quality was not degraded.
11 | ```
12 | grep "action\": \"call\"" --no-filename perf[1-5].json | jq . | grep mos_lq | sort | uniq -c
13 | ```
14 |
15 | ### notes
16 | In the provided example scenario, each voip_patrol instance will do 100 calls and may expect receiving them back since it is registering the same callee.
17 |
18 | ```xml
19 |
30 | ```
31 |
--------------------------------------------------------------------------------
/load_test/load.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
19 |
20 |
31 |
32 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/load_test/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | SESSION=$USER
3 |
4 | SERVER="$1"
5 |
6 | ulimit -c unlimited
7 |
8 | if [ "$1" == "stop" ]
9 | then
10 | tmux kill-session -t $SESSION
11 | exit
12 | fi
13 |
14 | if [ "$1" = "" ]
15 | then
16 | echo "missing server argument."
17 | exit
18 | fi
19 |
20 | # clean result files
21 | rm *.json
22 |
23 | tmux kill-session -t $SESSION
24 | tmux -2 new-session -d -s $SESSION
25 |
26 | tmux new-window -t $SESSION:1 -n 'Logs'
27 | tmux split-window -h
28 |
29 | run_voip_patrol () {
30 | P=$((${ID}+1))
31 | DELAY=$(((${ID}-1)*2))
32 | ENVS="VP_ENV_SERVER=\"${SERVER}\" VP_ENV_CALLEE=\"perf_callee${ID}@${SERVER}\" VP_ENV_U=perf_callee${ID} "
33 | CMD="sleep ${DELAY} && ../voip_patrol -p ${P}000 -c load.xml -o perf${ID}.json --rtp-port ${ID}0010"
34 | # echo "$ENVS bash -c \"$CMD\""
35 | tmux send-keys "${ENVS} bash -c \"${CMD}\"" Enter
36 | }
37 |
38 | # start a voip_patrol
39 | tmux select-pane -t 0
40 | ID=1
41 | run_voip_patrol
42 | tmux split-window -v
43 |
44 | # start a voip_patrol
45 | tmux select-pane -t 1
46 | ID=2
47 | run_voip_patrol
48 | tmux select-pane -t 2
49 | tmux send-keys "htop" C-m
50 | tmux split-window -v
51 |
52 | # start a voip_patrol
53 | tmux select-pane -t 3
54 | ID=3
55 | run_voip_patrol
56 | tmux split-window -v
57 |
58 | # start a voip_patrol
59 | tmux select-pane -t 4
60 | ID=4
61 | run_voip_patrol
62 |
63 | # Set default window
64 | tmux select-window -t $SESSION:3
65 |
66 | # Attach to session
67 | tmux -2 attach-session -t $SESSION
68 |
69 | echo "grep \"action\\\": \\\"call\\\"\" --no-filename perf[1-4].json | jq . | grep mos_lq | sort | uniq -c"
70 |
--------------------------------------------------------------------------------
/src/curl/email.h:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | #ifdef __cplusplus
6 | extern "C" {
7 | #endif
8 |
9 | int curl_send_email(const char* to, const char *server_url);
10 |
11 | #ifdef __cplusplus
12 | }
13 | #endif
14 |
15 |
--------------------------------------------------------------------------------
/src/ezxml/changelog.txt:
--------------------------------------------------------------------------------
1 | ezXML 0.8.6
2 | - fixed a bug in ezxml_add_child() that can occur when adding tags out of order
3 | - for consistency, ezxml_set_attr() now returns the tag given
4 | - added ezxml_move() and supporting functions ezxml_cut() and ezxml_insert()
5 | - fixed a bug where parsing an empty file could cause a segfault
6 |
7 | ezXML 0.8.5
8 | - fixed ezxml_toxml() to not output siblings of tag being converted
9 | - fixed a segfault when ezxml_set_attr() was used on a new root tag
10 | - added ezxml_name() function macro
11 | - all external functions now handle NULL ezxml_t structs without segfaulting
12 |
13 | ezXML 0.8.4
14 | - fixed to compile under win-doze when NOMMAP make option is set
15 | - fixed a bug where ezxml_toxml() could segfault if tag offset is out of bounds
16 | - ezxml_add_child() now works properly when tags are added out of order
17 | - improved error messages now include line numbers
18 | - fixed memory leak when entity reference is shorter than replacement text
19 | - added ezxml_new_d(), ezxml_add_child_d(), ezxml_set_txt_d() and
20 | ezxml_set_attr_d() function macros as wrappers that strdup() their arguments
21 |
22 | ezXML 0.8.3
23 | - fixed a UTF-16 decoding bug affecting larger unicode values
24 | - added internal dtd processing for entity declarations and default attributes
25 | - now correctly normalizes attribute values in compliance with the XML 1.0 spec
26 | - added check for correct tag nesting
27 | - ezxml_toxml() now generates canonical xml (apart from the namespace stuff)
28 |
29 | ezXML 0.8.2
30 | - fixed compiler warning about lvalue type casting
31 | - ezxml_get() argument list can now be terminated by an empty string tag name
32 | - added NOMMAP make option for systems without posix memory mapping
33 | - added support for UTF-16
34 | - fixed bug in ezxml_toxml() where UTF-8 sequences were being ampersand encoded
35 | - added ezxml_new(), ezxml_add_child(), ezxml_set_txt(), ezxml_set_attr(),
36 | and ezxml_remove() to facilitate creating and modifying xml
37 |
38 | ezXML 0.8.1
39 | - fixed bug where tags of same name were not recognized as such
40 | - fixed a memory allocation bug in ezxml_toxml() that could cause a segfault
41 | - added an extra check for missing root tag
42 | - now allows for space between ] and > when closing
43 | - now allows : as tag name start char
44 | - added ezxml_next() and ezxml_txt() function macros
45 |
46 | ezXML 0.8
47 | - added ezxml_toxml() function
48 | - removed ezxml_print(), just use printf() with ezxml_toxml() (minor version
49 | api changes will all be backwards compatible after 1.0 release)
50 | - added ezxml_pi() for retrieving ?> parsing instructions
51 | - whitespace in tag data is now preserved in compliance with the XML 1.0 spec
52 |
53 | ezXML 0.7
54 | - initial public release
55 |
--------------------------------------------------------------------------------
/src/ezxml/ezxml.c:
--------------------------------------------------------------------------------
1 | /* ezxml.c
2 | *
3 | * Copyright 2004-2006 Aaron Voisine
4 | *
5 | * Permission is hereby granted, free of charge, to any person obtaining
6 | * a copy of this software and associated documentation files (the
7 | * "Software"), to deal in the Software without restriction, including
8 | * without limitation the rights to use, copy, modify, merge, publish,
9 | * distribute, sublicense, and/or sell copies of the Software, and to
10 | * permit persons to whom the Software is furnished to do so, subject to
11 | * the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included
14 | * in all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 | */
24 |
25 | #include
26 | #include
27 | #include
28 | #include
29 | #include
30 | #include
31 | #include
32 | #ifndef EZXML_NOMMAP
33 | #include
34 | #endif // EZXML_NOMMAP
35 | #include
36 | #include "ezxml.h"
37 |
38 | #define EZXML_WS "\t\r\n " // whitespace
39 | #define EZXML_ERRL 128 // maximum error string length
40 |
41 | typedef struct ezxml_root *ezxml_root_t;
42 | struct ezxml_root { // additional data for the root tag
43 | struct ezxml xml; // is a super-struct built on top of ezxml struct
44 | ezxml_t cur; // current xml tree insertion point
45 | char *m; // original xml string
46 | size_t len; // length of allocated memory for mmap, -1 for malloc
47 | char *u; // UTF-8 conversion of string if original was UTF-16
48 | char *s; // start of work area
49 | char *e; // end of work area
50 | char **ent; // general entities (ampersand sequences)
51 | char ***attr; // default attributes
52 | char ***pi; // processing instructions
53 | short standalone; // non-zero if
54 | char err[EZXML_ERRL]; // error string
55 | };
56 |
57 | char *EZXML_NIL[] = { NULL }; // empty, null terminated array of strings
58 |
59 | // returns the first child tag with the given name or NULL if not found
60 | ezxml_t ezxml_child(ezxml_t xml, const char *name)
61 | {
62 | xml = (xml) ? xml->child : NULL;
63 | while (xml && strcmp(name, xml->name)) xml = xml->sibling;
64 | return xml;
65 | }
66 |
67 | // returns the Nth tag with the same name in the same subsection or NULL if not
68 | // found
69 | ezxml_t ezxml_idx(ezxml_t xml, int idx)
70 | {
71 | for (; xml && idx; idx--) xml = xml->next;
72 | return xml;
73 | }
74 |
75 | // returns the value of the requested tag attribute or NULL if not found
76 | const char *ezxml_attr(ezxml_t xml, const char *attr)
77 | {
78 | int i = 0, j = 1;
79 | ezxml_root_t root = (ezxml_root_t)xml;
80 |
81 | if (! xml || ! xml->attr) return NULL;
82 | while (xml->attr[i] && strcmp(attr, xml->attr[i])) i += 2;
83 | if (xml->attr[i]) return xml->attr[i + 1]; // found attribute
84 |
85 | while (root->xml.parent) root = (ezxml_root_t)root->xml.parent; // root tag
86 | for (i = 0; root->attr[i] && strcmp(xml->name, root->attr[i][0]); i++);
87 | if (! root->attr[i]) return NULL; // no matching default attributes
88 | while (root->attr[i][j] && strcmp(attr, root->attr[i][j])) j += 3;
89 | return (root->attr[i][j]) ? root->attr[i][j + 1] : NULL; // found default
90 | }
91 |
92 | // same as ezxml_get but takes an already initialized va_list
93 | ezxml_t ezxml_vget(ezxml_t xml, va_list ap)
94 | {
95 | char *name = va_arg(ap, char *);
96 | int idx = -1;
97 |
98 | if (name && *name) {
99 | idx = va_arg(ap, int);
100 | xml = ezxml_child(xml, name);
101 | }
102 | return (idx < 0) ? xml : ezxml_vget(ezxml_idx(xml, idx), ap);
103 | }
104 |
105 | // Traverses the xml tree to retrieve a specific subtag. Takes a variable
106 | // length list of tag names and indexes. The argument list must be terminated
107 | // by either an index of -1 or an empty string tag name. Example:
108 | // title = ezxml_get(library, "shelf", 0, "book", 2, "title", -1);
109 | // This retrieves the title of the 3rd book on the 1st shelf of library.
110 | // Returns NULL if not found.
111 | ezxml_t ezxml_get(ezxml_t xml, ...)
112 | {
113 | va_list ap;
114 | ezxml_t r;
115 |
116 | va_start(ap, xml);
117 | r = ezxml_vget(xml, ap);
118 | va_end(ap);
119 | return r;
120 | }
121 |
122 | // returns a null terminated array of processing instructions for the given
123 | // target
124 | const char **ezxml_pi(ezxml_t xml, const char *target)
125 | {
126 | ezxml_root_t root = (ezxml_root_t)xml;
127 | int i = 0;
128 |
129 | if (! root) return (const char **)EZXML_NIL;
130 | while (root->xml.parent) root = (ezxml_root_t)root->xml.parent; // root tag
131 | while (root->pi[i] && strcmp(target, root->pi[i][0])) i++; // find target
132 | return (const char **)((root->pi[i]) ? root->pi[i] + 1 : EZXML_NIL);
133 | }
134 |
135 | // set an error string and return root
136 | ezxml_t ezxml_err(ezxml_root_t root, char *s, const char *err, ...)
137 | {
138 | va_list ap;
139 | int line = 1;
140 | char *t, fmt[EZXML_ERRL];
141 |
142 | for (t = root->s; t < s; t++) if (*t == '\n') line++;
143 | snprintf(fmt, EZXML_ERRL, "[error near line %d]: %s", line, err);
144 |
145 | va_start(ap, err);
146 | vsnprintf(root->err, EZXML_ERRL, fmt, ap);
147 | va_end(ap);
148 |
149 | return &root->xml;
150 | }
151 |
152 | // Recursively decodes entity and character references and normalizes new lines
153 | // ent is a null terminated array of alternating entity names and values. set t
154 | // to '&' for general entity decoding, '%' for parameter entity decoding, 'c'
155 | // for cdata sections, ' ' for attribute normalization, or '*' for non-cdata
156 | // attribute normalization. Returns s, or if the decoded string is longer than
157 | // s, returns a malloced string that must be freed.
158 | char *ezxml_decode(char *s, char **ent, char t)
159 | {
160 | char *e, *r = s, *m = s;
161 | long b, c, d, l;
162 |
163 | for (; *s; s++) { // normalize line endings
164 | while (*s == '\r') {
165 | *(s++) = '\n';
166 | if (*s == '\n') memmove(s, (s + 1), strlen(s));
167 | }
168 | }
169 |
170 | for (s = r; ; ) {
171 | while (*s && *s != '&' && (*s != '%' || t != '%') && !isspace(*s)) s++;
172 |
173 | if (! *s) break;
174 | else if (t != 'c' && ! strncmp(s, "", 2)) { // character reference
175 | if (s[2] == 'x') c = strtol(s + 3, &e, 16); // base 16
176 | else c = strtol(s + 2, &e, 10); // base 10
177 | if (! c || *e != ';') { s++; continue; } // not a character ref
178 |
179 | if (c < 0x80) *(s++) = c; // US-ASCII subset
180 | else { // multi-byte UTF-8 sequence
181 | for (b = 0, d = c; d; d /= 2) b++; // number of bits in c
182 | b = (b - 2) / 5; // number of bytes in payload
183 | *(s++) = (0xFF << (7 - b)) | (c >> (6 * b)); // head
184 | while (b) *(s++) = 0x80 | ((c >> (6 * --b)) & 0x3F); // payload
185 | }
186 |
187 | memmove(s, strchr(s, ';') + 1, strlen(strchr(s, ';')));
188 | }
189 | else if ((*s == '&' && (t == '&' || t == ' ' || t == '*')) ||
190 | (*s == '%' && t == '%')) { // entity reference
191 | for (b = 0; ent[b] && strncmp(s + 1, ent[b], strlen(ent[b]));
192 | b += 2); // find entity in entity list
193 |
194 | if (ent[b++]) { // found a match
195 | if ((c = strlen(ent[b])) - 1 > (e = strchr(s, ';')) - s) {
196 | l = (d = (s - r)) + c + strlen(e); // new length
197 | r = (r == m) ? strcpy(malloc(l), r) : realloc(r, l);
198 | e = strchr((s = r + d), ';'); // fix up pointers
199 | }
200 |
201 | memmove(s + c, e + 1, strlen(e)); // shift rest of string
202 | strncpy(s, ent[b], c); // copy in replacement text
203 | }
204 | else s++; // not a known entity
205 | }
206 | else if ((t == ' ' || t == '*') && isspace(*s)) *(s++) = ' ';
207 | else s++; // no decoding needed
208 | }
209 |
210 | if (t == '*') { // normalize spaces for non-cdata attributes
211 | for (s = r; *s; s++) {
212 | if ((l = strspn(s, " "))) memmove(s, s + l, strlen(s + l) + 1);
213 | while (*s && *s != ' ') s++;
214 | }
215 | if (--s >= r && *s == ' ') *s = '\0'; // trim any trailing space
216 | }
217 | return r;
218 | }
219 |
220 | // called when parser finds start of new tag
221 | void ezxml_open_tag(ezxml_root_t root, char *name, char **attr)
222 | {
223 | ezxml_t xml = root->cur;
224 |
225 | if (xml->name) xml = ezxml_add_child(xml, name, strlen(xml->txt));
226 | else xml->name = name; // first open tag
227 |
228 | xml->attr = attr;
229 | root->cur = xml; // update tag insertion point
230 | }
231 |
232 | // called when parser finds character content between open and closing tag
233 | void ezxml_char_content(ezxml_root_t root, char *s, size_t len, char t)
234 | {
235 | ezxml_t xml = root->cur;
236 | char *m = s;
237 | size_t l;
238 |
239 | if (! xml || ! xml->name || ! len) return; // sanity check
240 |
241 | s[len] = '\0'; // null terminate text (calling functions anticipate this)
242 | len = strlen(s = ezxml_decode(s, root->ent, t)) + 1;
243 |
244 | if (! *(xml->txt)) xml->txt = s; // initial character content
245 | else { // allocate our own memory and make a copy
246 | xml->txt = (xml->flags & EZXML_TXTM) // allocate some space
247 | ? realloc(xml->txt, (l = strlen(xml->txt)) + len)
248 | : strcpy(malloc((l = strlen(xml->txt)) + len), xml->txt);
249 | strcpy(xml->txt + l, s); // add new char content
250 | if (s != m) free(s); // free s if it was malloced by ezxml_decode()
251 | }
252 |
253 | if (xml->txt != m) ezxml_set_flag(xml, EZXML_TXTM);
254 | }
255 |
256 | // called when parser finds closing tag
257 | ezxml_t ezxml_close_tag(ezxml_root_t root, char *name, char *s)
258 | {
259 | if (! root->cur || ! root->cur->name || strcmp(name, root->cur->name))
260 | return ezxml_err(root, s, "unexpected closing tag %s>", name);
261 |
262 | root->cur = root->cur->parent;
263 | return NULL;
264 | }
265 |
266 | // checks for circular entity references, returns non-zero if no circular
267 | // references are found, zero otherwise
268 | int ezxml_ent_ok(char *name, char *s, char **ent)
269 | {
270 | int i;
271 |
272 | for (; ; s++) {
273 | while (*s && *s != '&') s++; // find next entity reference
274 | if (! *s) return 1;
275 | if (! strncmp(s + 1, name, strlen(name))) return 0; // circular ref.
276 | for (i = 0; ent[i] && strncmp(ent[i], s + 1, strlen(ent[i])); i += 2);
277 | if (ent[i] && ! ezxml_ent_ok(name, ent[i + 1], ent)) return 0;
278 | }
279 | }
280 |
281 | // called when the parser finds a processing instruction
282 | void ezxml_proc_inst(ezxml_root_t root, char *s, size_t len)
283 | {
284 | int i = 0, j = 1;
285 | char *target = s;
286 |
287 | s[len] = '\0'; // null terminate instruction
288 | if (*(s += strcspn(s, EZXML_WS))) {
289 | *s = '\0'; // null terminate target
290 | s += strspn(s + 1, EZXML_WS) + 1; // skip whitespace after target
291 | }
292 |
293 | if (! strcmp(target, "xml")) { //
294 | if ((s = strstr(s, "standalone")) && ! strncmp(s + strspn(s + 10,
295 | EZXML_WS "='\"") + 10, "yes", 3)) root->standalone = 1;
296 | return;
297 | }
298 |
299 | if (! root->pi[0]) *(root->pi = malloc(sizeof(char **))) = NULL; //first pi
300 |
301 | while (root->pi[i] && strcmp(target, root->pi[i][0])) i++; // find target
302 | if (! root->pi[i]) { // new target
303 | root->pi = realloc(root->pi, sizeof(char **) * (i + 2));
304 | root->pi[i] = malloc(sizeof(char *) * 3);
305 | root->pi[i][0] = target;
306 | root->pi[i][1] = (char *)(root->pi[i + 1] = NULL); // terminate pi list
307 | root->pi[i][2] = strdup(""); // empty document position list
308 | }
309 |
310 | while (root->pi[i][j]) j++; // find end of instruction list for this target
311 | root->pi[i] = realloc(root->pi[i], sizeof(char *) * (j + 3));
312 | root->pi[i][j + 2] = realloc(root->pi[i][j + 1], j + 1);
313 | strcpy(root->pi[i][j + 2] + j - 1, (root->xml.name) ? ">" : "<");
314 | root->pi[i][j + 1] = NULL; // null terminate pi list for this target
315 | root->pi[i][j] = s; // set instruction
316 | }
317 |
318 | // called when the parser finds an internal doctype subset
319 | short ezxml_internal_dtd(ezxml_root_t root, char *s, size_t len)
320 | {
321 | char q, *c, *t, *n = NULL, *v, **ent, **pe;
322 | int i, j;
323 |
324 | pe = memcpy(malloc(sizeof(EZXML_NIL)), EZXML_NIL, sizeof(EZXML_NIL));
325 |
326 | for (s[len] = '\0'; s; ) {
327 | while (*s && *s != '<' && *s != '%') s++; // find next declaration
328 |
329 | if (! *s) break;
330 | else if (! strncmp(s, "');
338 | continue;
339 | }
340 |
341 | for (i = 0, ent = (*c == '%') ? pe : root->ent; ent[i]; i++);
342 | ent = realloc(ent, (i + 3) * sizeof(char *)); // space for next ent
343 | if (*c == '%') pe = ent;
344 | else root->ent = ent;
345 |
346 | *(++s) = '\0'; // null terminate name
347 | if ((s = strchr(v, q))) *(s++) = '\0'; // null terminate value
348 | ent[i + 1] = ezxml_decode(v, pe, '%'); // set value
349 | ent[i + 2] = NULL; // null terminate entity list
350 | if (! ezxml_ent_ok(n, ent[i + 1], ent)) { // circular reference
351 | if (ent[i + 1] != v) free(ent[i + 1]);
352 | ezxml_err(root, v, "circular entity declaration &%s", n);
353 | break;
354 | }
355 | else ent[i] = n; // set entity name
356 | }
357 | else if (! strncmp(s, "")) == '>') continue;
361 | else *s = '\0'; // null terminate tag name
362 | for (i = 0; root->attr[i] && strcmp(n, root->attr[i][0]); i++);
363 |
364 | while (*(n = ++s + strspn(s, EZXML_WS)) && *n != '>') {
365 | if (*(s = n + strcspn(n, EZXML_WS))) *s = '\0'; // attr name
366 | else { ezxml_err(root, t, "malformed ") - 1;
380 | if (*c == ' ') continue; // cdata is default, nothing to do
381 | v = NULL;
382 | }
383 | else if ((*s == '"' || *s == '\'') && // default value
384 | (s = strchr(v = s + 1, *s))) *s = '\0';
385 | else { ezxml_err(root, t, "malformed attr[i]) { // new tag name
388 | root->attr = (! i) ? malloc(2 * sizeof(char **))
389 | : realloc(root->attr,
390 | (i + 2) * sizeof(char **));
391 | root->attr[i] = malloc(2 * sizeof(char *));
392 | root->attr[i][0] = t; // set tag name
393 | root->attr[i][1] = (char *)(root->attr[i + 1] = NULL);
394 | }
395 |
396 | for (j = 1; root->attr[i][j]; j += 3); // find end of list
397 | root->attr[i] = realloc(root->attr[i],
398 | (j + 4) * sizeof(char *));
399 |
400 | root->attr[i][j + 3] = NULL; // null terminate list
401 | root->attr[i][j + 2] = c; // is it cdata?
402 | root->attr[i][j + 1] = (v) ? ezxml_decode(v, root->ent, *c)
403 | : NULL;
404 | root->attr[i][j] = n; // attribute name
405 | }
406 | }
407 | else if (! strncmp(s, ""); // comments
408 | else if (! strncmp(s, "", 2)) { // processing instructions
409 | if ((s = strstr(c = s + 2, "?>")))
410 | ezxml_proc_inst(root, c, s++ - c);
411 | }
412 | else if (*s == '<') s = strchr(s, '>'); // skip other declarations
413 | else if (*(s++) == '%' && ! root->standalone) break;
414 | }
415 |
416 | free(pe);
417 | return ! *root->err;
418 | }
419 |
420 | // Converts a UTF-16 string to UTF-8. Returns a new string that must be freed
421 | // or NULL if no conversion was needed.
422 | char *ezxml_str2utf8(char **s, size_t *len)
423 | {
424 | char *u;
425 | size_t l = 0, sl, max = *len;
426 | long c, d;
427 | int b, be = (**s == '\xFE') ? 1 : (**s == '\xFF') ? 0 : -1;
428 |
429 | if (be == -1) return NULL; // not UTF-16
430 |
431 | u = malloc(max);
432 | for (sl = 2; sl < *len - 1; sl += 2) {
433 | c = (be) ? (((*s)[sl] & 0xFF) << 8) | ((*s)[sl + 1] & 0xFF) //UTF-16BE
434 | : (((*s)[sl + 1] & 0xFF) << 8) | ((*s)[sl] & 0xFF); //UTF-16LE
435 | if (c >= 0xD800 && c <= 0xDFFF && (sl += 2) < *len - 1) { // high-half
436 | d = (be) ? (((*s)[sl] & 0xFF) << 8) | ((*s)[sl + 1] & 0xFF)
437 | : (((*s)[sl + 1] & 0xFF) << 8) | ((*s)[sl] & 0xFF);
438 | c = (((c & 0x3FF) << 10) | (d & 0x3FF)) + 0x10000;
439 | }
440 |
441 | while (l + 6 > max) u = realloc(u, max += EZXML_BUFSIZE);
442 | if (c < 0x80) u[l++] = c; // US-ASCII subset
443 | else { // multi-byte UTF-8 sequence
444 | for (b = 0, d = c; d; d /= 2) b++; // bits in c
445 | b = (b - 2) / 5; // bytes in payload
446 | u[l++] = (0xFF << (7 - b)) | (c >> (6 * b)); // head
447 | while (b) u[l++] = 0x80 | ((c >> (6 * --b)) & 0x3F); // payload
448 | }
449 | }
450 | return *s = realloc(u, *len = l);
451 | }
452 |
453 | // frees a tag attribute list
454 | void ezxml_free_attr(char **attr) {
455 | int i = 0;
456 | char *m;
457 |
458 | if (! attr || attr == EZXML_NIL) return; // nothing to free
459 | while (attr[i]) i += 2; // find end of attribute list
460 | m = attr[i + 1]; // list of which names and values are malloced
461 | for (i = 0; m[i]; i++) {
462 | if (m[i] & EZXML_NAMEM) free(attr[i * 2]);
463 | if (m[i] & EZXML_TXTM) free(attr[(i * 2) + 1]);
464 | }
465 | free(m);
466 | free(attr);
467 | }
468 |
469 | // parse the given xml string and return an ezxml structure
470 | ezxml_t ezxml_parse_str(char *s, size_t len)
471 | {
472 | ezxml_root_t root = (ezxml_root_t)ezxml_new(NULL);
473 | char q, e, *d, **attr, **a = NULL; // initialize a to avoid compile warning
474 | int l, i, j;
475 |
476 | root->m = s;
477 | if (! len) return ezxml_err(root, NULL, "root tag missing");
478 | root->u = ezxml_str2utf8(&s, &len); // convert utf-16 to utf-8
479 | root->e = (root->s = s) + len; // record start and end of work area
480 |
481 | e = s[len - 1]; // save end char
482 | s[len - 1] = '\0'; // turn end char into null terminator
483 |
484 | while (*s && *s != '<') s++; // find first tag
485 | if (! *s) return ezxml_err(root, s, "root tag missing");
486 |
487 | for (; ; ) {
488 | attr = (char **)EZXML_NIL;
489 | d = ++s;
490 |
491 | if (isalpha(*s) || *s == '_' || *s == ':' || *s < '\0') { // new tag
492 | if (! root->cur)
493 | return ezxml_err(root, d, "markup outside of root element");
494 |
495 | s += strcspn(s, EZXML_WS "/>");
496 | while (isspace(*s)) *(s++) = '\0'; // null terminate tag name
497 |
498 | if (*s && *s != '/' && *s != '>') // find tag in default attr list
499 | for (i = 0; (a = root->attr[i]) && strcmp(a[0], d); i++);
500 |
501 | for (l = 0; *s && *s != '/' && *s != '>'; l += 2) { // new attrib
502 | attr = (l) ? realloc(attr, (l + 4) * sizeof(char *))
503 | : malloc(4 * sizeof(char *)); // allocate space
504 | attr[l + 3] = (l) ? realloc(attr[l + 1], (l / 2) + 2)
505 | : malloc(2); // mem for list of maloced vals
506 | strcpy(attr[l + 3] + (l / 2), " "); // value is not malloced
507 | attr[l + 2] = NULL; // null terminate list
508 | attr[l + 1] = ""; // temporary attribute value
509 | attr[l] = s; // set attribute name
510 |
511 | s += strcspn(s, EZXML_WS "=/>");
512 | if (*s == '=' || isspace(*s)) {
513 | *(s++) = '\0'; // null terminate tag attribute name
514 | q = *(s += strspn(s, EZXML_WS "="));
515 | if (q == '"' || q == '\'') { // attribute value
516 | attr[l + 1] = ++s;
517 | while (*s && *s != q) s++;
518 | if (*s) *(s++) = '\0'; // null terminate attribute val
519 | else {
520 | ezxml_free_attr(attr);
521 | return ezxml_err(root, d, "missing %c", q);
522 | }
523 |
524 | for (j = 1; a && a[j] && strcmp(a[j], attr[l]); j +=3);
525 | attr[l + 1] = ezxml_decode(attr[l + 1], root->ent, (a
526 | && a[j]) ? *a[j + 2] : ' ');
527 | if (attr[l + 1] < d || attr[l + 1] > s)
528 | attr[l + 3][l / 2] = EZXML_TXTM; // value malloced
529 | }
530 | }
531 | while (isspace(*s)) s++;
532 | }
533 |
534 | if (*s == '/') { // self closing tag
535 | *(s++) = '\0';
536 | if ((*s && *s != '>') || (! *s && e != '>')) {
537 | if (l) ezxml_free_attr(attr);
538 | return ezxml_err(root, d, "missing >");
539 | }
540 | ezxml_open_tag(root, d, attr);
541 | ezxml_close_tag(root, d, s);
542 | }
543 | else if ((q = *s) == '>' || (! *s && e == '>')) { // open tag
544 | *s = '\0'; // temporarily null terminate tag name
545 | ezxml_open_tag(root, d, attr);
546 | *s = q;
547 | }
548 | else {
549 | if (l) ezxml_free_attr(attr);
550 | return ezxml_err(root, d, "missing >");
551 | }
552 | }
553 | else if (*s == '/') { // close tag
554 | s += strcspn(d = s + 1, EZXML_WS ">") + 1;
555 | if (! (q = *s) && e != '>') return ezxml_err(root, d, "missing >");
556 | *s = '\0'; // temporarily null terminate tag name
557 | if (ezxml_close_tag(root, d, s)) return &root->xml;
558 | if (isspace(*s = q)) s += strspn(s, EZXML_WS);
559 | }
560 | else if (! strncmp(s, "!--", 3)) { // xml comment
561 | if (! (s = strstr(s + 3, "--")) || (*(s += 2) != '>' && *s) ||
562 | (! *s && e != '>')) return ezxml_err(root, d, "unclosed
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------