├── .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 | [![Docker Pulls](https://img.shields.io/docker/pulls/jchavanton/voip_patrol.svg)](https://hub.docker.com/r/jchavanton/voip_patrol/) 2 | 3 | # VoIP Patrol 4 | ![GitHub Logo](VP_Logo_1200px-11th_Airborne_Division.patch_small2.jpg) 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 ", 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, ""))) 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 | --------------------------------------------------------------------------------