├── .boring ├── AUTHORS ├── LICENSE ├── MANIFEST.in ├── build_inplace ├── debian ├── changelog ├── compat ├── control ├── copyright ├── dirs ├── docs ├── rules └── source │ └── format ├── docs ├── Clients.txt └── Install.txt ├── resources └── sounds │ ├── dtmf_0_tone.wav │ ├── dtmf_1_tone.wav │ ├── dtmf_2_tone.wav │ ├── dtmf_3_tone.wav │ ├── dtmf_4_tone.wav │ ├── dtmf_5_tone.wav │ ├── dtmf_6_tone.wav │ ├── dtmf_7_tone.wav │ ├── dtmf_8_tone.wav │ ├── dtmf_9_tone.wav │ ├── dtmf_A_tone.wav │ ├── dtmf_B_tone.wav │ ├── dtmf_C_tone.wav │ ├── dtmf_D_tone.wav │ ├── dtmf_pound_tone.wav │ ├── dtmf_star_tone.wav │ ├── file_received.wav │ ├── file_sent.wav │ ├── hangup_tone.wav │ ├── hold_tone.wav │ ├── message_received.wav │ ├── message_sent.wav │ ├── ring_inbound.wav │ ├── ring_outbound.wav │ └── ring_tone.wav ├── setup.py ├── sip-audio-session ├── sip-message ├── sip-publish-presence ├── sip-register ├── sip-session ├── sip-settings ├── sip-subscribe-mwi ├── sip-subscribe-presence ├── sip-subscribe-rls ├── sip-subscribe-winfo ├── sip-subscribe-xcap-diff └── sipclient ├── __init__.py ├── configuration ├── __init__.py ├── account.py ├── datatypes.py └── settings.py ├── log.py ├── system.py └── ui.py /.boring: -------------------------------------------------------------------------------- 1 | # Boring file regexps: 2 | (^|/)_darcs($|/) 3 | (^|/)CVS($|/) 4 | (^|/)\.svn($|/) 5 | (^|/)\.DS_Store$ 6 | (^|/)Thumbs\.db$ 7 | \# 8 | ~$ 9 | (^|/)core(\.[0-9]+)?$ 10 | \.(pyc|pyo|o|so|orig|bak|BAK|prof|wpu|cvsignore)$ 11 | (^|/)build($|/) 12 | (^|/)dist($|/) 13 | ^MANIFEST$ 14 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Adrian Georgescu 2 | Dan Pascu 3 | Ruud Klaver 4 | Denis Bilenko 5 | Lucian Stanescu 6 | Saul Ibarra 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2008-2020 AG Projects (http://ag-projects.com) 2 | 3 | License: GPL-3+ 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 3 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 | For a copy of the license see https://www.gnu.org/licenses/gpl.html 16 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include LICENSE 3 | include README 4 | include MANIFEST.in 5 | include build_inplace 6 | 7 | include debian/changelog 8 | include debian/compat 9 | include debian/control 10 | include debian/copyright 11 | include debian/dirs 12 | include debian/docs 13 | include debian/rules 14 | include debian/source/format 15 | 16 | graft docs 17 | graft resources 18 | -------------------------------------------------------------------------------- /build_inplace: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python setup.py build_ext --inplace "$@" 4 | test -d build && python setup.py clean 5 | 6 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | sipclients (3.5.6) unstable; urgency=medium 2 | 3 | * Added SIP message support for sip-session script 4 | * Added OTR encryption for SIP MESSAGE sessions to sip-session 5 | * Display GRUU URIs in sip-session and sip-register 6 | * Fixed logging when registration ended in sip-session script 7 | * Automatically changed default account after enrollment 8 | * Added python-requests dependency 9 | 10 | -- Adrian Georgescu Fri, 15 Jan 2021 15:39:38 +0100 11 | 12 | sipclients (3.5.5) unstable; urgency=medium 13 | 14 | * Added sip2sip.info enrollment for sip-session script 15 | * Enable usage of multiple accounts with sip-session 16 | * Use environment set python2 path 17 | 18 | -- Adrian Georgescu Sun, 10 Jan 2021 16:29:04 +0100 19 | 20 | sipclients (3.5.4) unstable; urgency=medium 21 | 22 | * Added ZRTP management for sip-session script 23 | * Added OTR management for sip-session chat stream 24 | * Preserve command history between restarts for sip-session script 25 | * Capture error when SIP session did not start yet 26 | * Moved spool directory under home user config folder 27 | * Added video support to sip-audio-session script 28 | * Print DNS lookup results for sip-message and sip-audio-session 29 | * Fixed printing XCAP document url 30 | 31 | -- Adrian Georgescu Fri, 08 Jan 2021 16:02:11 +0100 32 | 33 | sipclients (3.5.3) unstable; urgency=medium 34 | 35 | * Disable presence and xcap for sip-session 36 | * Disable presence and xcap for sip-audio-session 37 | * Disabled presence and xcap for sip-message 38 | * Enable sip trace for sip-message 39 | * Fixed publish presence script 40 | * Added sip and pjsip trace for sip-register 41 | 42 | -- Adrian Georgescu Sat, 14 Nov 2020 12:22:11 +0100 43 | 44 | sipclients (3.5.2) unstable; urgency=medium 45 | 46 | * Fixed enabling tracing for audio session at start 47 | 48 | -- Adrian Georgescu Sat, 10 Oct 2020 10:48:25 +0200 49 | 50 | sipclients (3.5.1) unstable; urgency=medium 51 | 52 | * Added posibility to start and end audio calls from external applications 53 | * Disable audio echo cancellation for arm7 architecture 54 | * Fixed starting publish presence application 55 | * Don't display key usage in batch mode for sip-audio-session 56 | * Fixed cancelling of incoming sessions for sip-audio-session 57 | * Added access list for auto answer for sip-audio-session 58 | * Unregister on exit for for sip-audio-session 59 | 60 | -- Adrian Georgescu Mon, 06 Apr 2020 23:51:43 +0200 61 | 62 | sipclients (3.5.0) unstable; urgency=medium 63 | 64 | * Pass command line arguments from build_inplace to setup.py 65 | * Cleanup after build_inplace 66 | * Removed commented out variable in debian rules 67 | * Explicitly use python2 in shebang lines 68 | * Refactored setup.py for PEP-8 compliance 69 | * Simplified MANIFEST.in 70 | * Split debian dependencies one per line 71 | * Increased debian compatibility level to 11 72 | * Increased debian standards version to 4.5.0 73 | * Updated minimum versions for debian dependencies 74 | * Use pybuild as the debian build system 75 | * Updated copyright years 76 | 77 | -- Dan Pascu Fri, 14 Feb 2020 14:20:48 +0200 78 | 79 | sipclients (3.4.0) unstable; urgency=medium 80 | 81 | * Synchronized with python-sipsimple 3.4.0 82 | 83 | -- Dan Pascu Mon, 25 Feb 2019 13:26:11 +0200 84 | 85 | sipclients (3.3.0) unstable; urgency=medium 86 | 87 | * Adjusted scripts for API change 88 | 89 | -- Dan Pascu Wed, 12 Dec 2018 07:24:59 +0200 90 | 91 | sipclients (3.2.1) unstable; urgency=medium 92 | 93 | * Updated outdated website links 94 | * Moved copyright and licensing info from source files into LICENSE 95 | * Changed license from LGPL to GPL-3+ and updated license files 96 | * Adjusted some module docstrings 97 | * Removed no longer necessary future import 98 | * Removed spurious whitespace 99 | * Removed unnecessary .PHONY targets 100 | * Added dh-python build dependency 101 | * Removed no longer necessary pycompat and pyversions debian files 102 | * Updated installation instructions 103 | * Updated copyright years 104 | 105 | -- Dan Pascu Sat, 06 Oct 2018 12:29:31 +0300 106 | 107 | sipclients (3.1.0) unstable; urgency=medium 108 | 109 | * Raised python-sipsimple version dependency 110 | * Increased debian compatibility level to 9 111 | * Updated debian standards version 112 | * Updated debian uploaders 113 | 114 | -- Dan Pascu Fri, 20 Jan 2017 06:51:52 +0200 115 | 116 | sipclients (3.0.0) unstable; urgency=medium 117 | 118 | * Generalised use of streams 119 | * Raised python-sipsimple version dependency 120 | 121 | -- Saul Ibarra Tue, 08 Mar 2016 17:16:07 +0100 122 | 123 | sipclients (2.6.0) unstable; urgency=medium 124 | 125 | * Remove use of no longer existing setting 126 | * Adapted to API changes in the SDK 127 | * Raised python-sipsimple version dependency 128 | 129 | -- Saul Ibarra Fri, 04 Dec 2015 12:11:46 +0100 130 | 131 | sipclients (2.5.0) unstable; urgency=medium 132 | 133 | * Adapted sip-session to changes in file transfers 134 | * Raised python-sipsimple dependency 135 | 136 | -- Saul Ibarra Wed, 10 Jun 2015 14:31:13 +0200 137 | 138 | sipclients (2.3.0) unstable; urgency=medium 139 | 140 | * Adapted to API changes in the SDK 141 | * Raised python-sipsimple version dependency 142 | 143 | -- Saul Ibarra Tue, 17 Mar 2015 11:20:11 +0100 144 | 145 | sipclients (2.2.0) unstable; urgency=medium 146 | 147 | * Raised python-sipsimple version dependency 148 | 149 | -- Saul Ibarra Mon, 26 Jan 2015 17:15:25 +0100 150 | 151 | sipclients (2.0.0) unstable; urgency=medium 152 | 153 | * Reworded some messages 154 | * Bumped Debian Standards-Version 155 | * Raised python-sipsimple version dependency 156 | 157 | -- Saul Ibarra Fri, 21 Nov 2014 12:25:35 +0100 158 | 159 | sipclients (1.4.2) unstable; urgency=medium 160 | 161 | * Removed no longer needed future imports 162 | * Fixed handling html and multiline messages in chat 163 | * Fixed doubly defined notification handler 164 | 165 | -- Saul Ibarra Mon, 28 Jul 2014 14:43:38 +0200 166 | 167 | sipclients (1.4.1) unstable; urgency=medium 168 | 169 | * Raised python-sipsimple version dependency 170 | 171 | -- Saul Ibarra Fri, 27 Jun 2014 09:50:38 +0200 172 | 173 | sipclients (1.3.0) unstable; urgency=medium 174 | 175 | * Fixed debug tracing in sip-session 176 | * Fix playing hold tone when using multiple RTP streams 177 | * Avoid creating multiple bogus ringtone players 178 | 179 | -- Saul Ibarra Fri, 11 Apr 2014 13:24:26 +0200 180 | 181 | sipclients (1.2.0) unstable; urgency=medium 182 | 183 | * Raised python-sipsimple version dependency 184 | * Bumped Debian Standards-Version 185 | 186 | -- Saul Ibarra Wed, 19 Feb 2014 13:44:37 +0100 187 | 188 | sipclients (1.1.0) unstable; urgency=medium 189 | 190 | * Raised python-sipsimple version dependency 191 | * Adapted to API changes in SIP SIMPLE SDK 192 | 193 | -- Saul Ibarra Fri, 13 Dec 2013 13:46:36 +0100 194 | 195 | sipclients (1.0.0) unstable; urgency=low 196 | 197 | * Don't render non-text payloads in chat session 198 | * Raised python-sipsimple version dependency 199 | * Adapted to API changes in SIP SIMPLE SDK 200 | * Dropped Python 2.6 support 201 | 202 | -- Saul Ibarra Fri, 09 Aug 2013 11:57:28 +0200 203 | 204 | sipclients (0.35.0) unstable; urgency=low 205 | 206 | * Raised python-sipsimple version dependency 207 | 208 | -- Saul Ibarra Wed, 26 Jun 2013 16:37:35 +0200 209 | 210 | sipclients (0.34.0) unstable; urgency=low 211 | 212 | * Raised python-sipsimple version dependency 213 | 214 | -- Saul Ibarra Tue, 19 Mar 2013 11:35:12 +0100 215 | 216 | sipclients (0.33.0) unstable; urgency=low 217 | 218 | * Fixed sip-register script to only register 219 | * Raised python-sipsimple version dependency 220 | 221 | -- Saul Ibarra Fri, 25 Jan 2013 16:39:31 +0100 222 | 223 | sipclients (0.32.0) unstable; urgency=low 224 | 225 | * Removed lxml dependency 226 | * Raised python-sipsimple version dependency 227 | 228 | -- Saul Ibarra Fri, 11 Jan 2013 12:18:01 +0100 229 | 230 | sipclients (0.31.1) unstable; urgency=low 231 | 232 | * Fixed variable names in sip-message and sip-audio-session 233 | 234 | -- Saul Ibarra Wed, 28 Nov 2012 15:04:43 +0100 235 | 236 | sipclients (0.31.0) unstable; urgency=low 237 | 238 | * Adapted to API changes in streams 239 | * Fixed attribute access in sip-session 240 | 241 | -- Saul Ibarra Fri, 26 Oct 2012 12:44:11 +0200 242 | 243 | sipclients (0.30.1) unstable; urgency=low 244 | 245 | * Fixed variable name after middleware API change 246 | 247 | -- Saul Ibarra Mon, 17 Sep 2012 15:52:48 +0200 248 | 249 | sipclients (0.30.0) unstable; urgency=low 250 | 251 | * Added ability to set nickname in sip-session 252 | * Print account GRUU if available 253 | * Fixed scripts to match API changes 254 | * Removed obsolete scripts 255 | * Bumped debian standards version to 3.9.3 256 | 257 | -- Saul Ibarra Thu, 06 Sep 2012 21:28:28 +0200 258 | 259 | sipclients (0.20.0) unstable; urgency=low 260 | 261 | * Fixed code to work with latest changes in FileTransferStream in 262 | sipsimple 263 | * Dropped support for Python 2.5 264 | 265 | -- Saul Ibarra Mon, 19 Dec 2011 15:27:01 +0100 266 | 267 | sipclients (0.19.0) unstable; urgency=low 268 | 269 | * Added blind transfer support to sip-session 270 | 271 | -- Saul Ibarra Fri, 16 Sep 2011 13:01:12 +0200 272 | 273 | sipclients (0.18.2) unstable; urgency=low 274 | 275 | * Adapted to changes in python-application 276 | * Reworked Debian packaging 277 | 278 | -- Saul Ibarra Tue, 07 Jun 2011 16:50:54 +0200 279 | 280 | sipclients (0.18.1) unstable; urgency=low 281 | 282 | * Added add/remove participant commands to sip-session 283 | * Adapted to API changes in the middleware 284 | * Fixed exception when using auto-publish-presence with Bonjour account 285 | 286 | -- Saul Ibarra Tue, 24 May 2011 14:24:53 +0200 287 | 288 | sipclients (0.18.0) unstable; urgency=low 289 | 290 | * Adapted scripts to changes in Subscription API 291 | * Avoid exception if MSRPTransportTrace notification is handled too late 292 | * Fixed unicode support 293 | * Only display text/plain and text/html MESSAGEs 294 | 295 | -- Saul Ibarra Fri, 18 Mar 2011 15:44:27 +0100 296 | 297 | sipclients (0.17.0) unstable; urgency=low 298 | 299 | * Adapted to changes in account contact building 300 | * Fixed saving settings after ThreadManager was introduced in the middleware 301 | * Removed obsolete option 302 | 303 | -- Saul Ibarra Thu, 27 Jan 2011 13:23:52 +0100 304 | 305 | sipclients (0.16.5) unstable; urgency=low 306 | 307 | * Adapt to changes in Bonjour neighbours handling 308 | * Fixed accessing nonexistent attributes in Bonjour account 309 | * Fixed exception when closing session without streams 310 | * Adapted to the latest package changes in sipsimple 311 | * Updated version number to match SIPSIMPLE SDK 312 | 313 | -- Saul Ibarra Tue, 14 Dec 2010 18:58:49 +0100 314 | 315 | sipclients (0.15.2) unstable; urgency=low 316 | 317 | * Adapted to changes in notifications 318 | * Updated python-sipsimple dependency 319 | 320 | -- Saul Ibarra Fri, 26 Nov 2010 16:08:51 +0100 321 | 322 | sipclients (0.15.1) unstable; urgency=low 323 | 324 | * Added python-lxml dependency 325 | * Fixed inband DTMF dialing 326 | * Always send RFC2833 DTMFs 327 | * Adapted to rename of Icon to PresenceContent 328 | * Added debian source format file 329 | * Removed use_xcap_diff setting 330 | * Adapt scripts to use always_use_my_proxy setting 331 | * Bumped Debian Standards Version to 3.9.1 332 | 333 | -- Saul Ibarra Thu, 11 Nov 2010 15:29:31 +0100 334 | 335 | sipclients (0.15.0) unstable; urgency=low 336 | 337 | * Fixed xcap_diff settings name 338 | * Exit if 489 is received for xcapdiff subscription 339 | * Reworked xcap-icon options and fixed document selector 340 | * Fixed dialog-rules auid 341 | * Added ICE debug to the console output 342 | * Improved formatting of long configuration values 343 | * Improved error handling in sip-subscribe-winfo 344 | * Fixed calls to AudioStream.start_recording 345 | * Use the builtin NAT detector 346 | * Avoid error when printing session duration if it failed to start 347 | * Release reference to session when it ended abnormally 348 | 349 | -- Saul Ibarra Mon, 21 Jun 2010 13:03:32 +0200 350 | 351 | sipclients (0.14.2) unstable; urgency=low 352 | 353 | * Raised python-sipsimple version dependency 354 | 355 | -- Saul Ibarra Tue, 20 Apr 2010 13:09:17 +0200 356 | 357 | sipclients (0.14.1) unstable; urgency=low 358 | 359 | * Adapted to move of account.password setting 360 | * Update dependencies 361 | 362 | -- Saul Ibarra Tue, 20 Apr 2010 11:40:22 +0200 363 | 364 | sipclients (0.14.0) unstable; urgency=low 365 | 366 | * Add ability to CANCEL re-INVITEs 367 | * Check if enable_outbound_proxy is True when outbound_proxy is set 368 | * Updated settings to match the middleware 369 | * Adapted to ChatStream interface changes 370 | * Display ICE negotiation related information 371 | * Adapted to audio support refactoring 372 | * Adapt scripts to changes in ICE status notifications 373 | * Print message according to CANCEL reason 374 | * Improved support for bonjour in scripts 375 | 376 | -- Saul Ibarra Fri, 09 Apr 2010 16:27:02 +0200 377 | 378 | sipclients (0.12.0) unstable; urgency=low 379 | 380 | * Initial release 381 | 382 | -- Lucian Stanescu Tue, 19 Jan 2010 11:22:06 +0000 383 | 384 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 11 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: sipclients 2 | Section: python 3 | Priority: optional 4 | Maintainer: Adrian Georgescu 5 | Uploaders: Tijmen de Mes 6 | Build-Depends: debhelper (>= 11), dh-python, python (>= 2.7) 7 | Standards-Version: 4.5.0 8 | Homepage: http://sipsimpleclient.org 9 | 10 | Package: sipclients 11 | Section: net 12 | Architecture: all 13 | Depends: ${python:Depends}, ${misc:Depends}, 14 | libavahi-compat-libdnssd1, 15 | python-application (>= 2.8.0), 16 | python-eventlib, 17 | python-requests, 18 | python-lxml, 19 | python-sipsimple (>= 3.5.0), 20 | python-twisted-core, 21 | python-zope.interface 22 | Conflicts: sipsimple-cli 23 | Description: SIP SIMPLE Command Line Clients 24 | This package contains Command Line Clients for testing SIP SIMPLE client 25 | SDK from http://sipsimpleclient.org. They demonstrate the SDK capabilities 26 | and can be used as examples or for testing purposes. 27 | 28 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Copyright: 2008-2020 AG Projects 2 | 3 | License: GPL-3+ 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 3 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 | For a copy of the license see /usr/share/common-licenses/GPL-3 16 | -------------------------------------------------------------------------------- /debian/dirs: -------------------------------------------------------------------------------- 1 | usr/bin 2 | usr/share/sipclients 3 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | docs/Clients.txt 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ --with python2 --buildsystem=pybuild 5 | 6 | override_dh_clean: 7 | dh_clean 8 | rm -rf build dist MANIFEST 9 | 10 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /docs/Clients.txt: -------------------------------------------------------------------------------- 1 | 2 | SIP Clients 3 | ----------- 4 | 5 | To test SIP SIMPLE client SDK (http://sipsimpleclient.org), a set of command 6 | line tools are provided by this package for setting up Audio, Instant 7 | Messaging (IM) and File Transfer sessions, Publish and Subscribe for 8 | presence or other events. 9 | 10 | The SIP clients can be used in a terminal window on Linux and MacOSX 11 | operating systems. 12 | 13 | You need a SIP account from a service provider or to setup your own SIP 14 | infrastructure using the following elements: 15 | 16 | * OpenSIPS from [http://opensips.org http://opensips.org] 17 | * OpenXCAP from [http://openxcap.org http://openxcap.org] 18 | * MSRPRelay from [http://msrprelay.org http://msrprelay.org] 19 | 20 | A SIP account with all SIP SIMPLE client SDK features can be obtained for 21 | free from http://sip2sip.info. 22 | 23 | The following SIP clients are available: 24 | 25 | * sip-settings - Manage the settings used by all clients 26 | * sip-register - REGISTER a SIP end-point with a SIP Registrar 27 | * sip-session - Supports multiple Audio, IM and File Transfers sessions 28 | * sip-audio-session - Setup an Audio session 29 | * sip-message - Exchange text in page mode using SIP MESSAGE method 30 | * sip-publish-presence - Publish presence event to a SIP Presence Agent 31 | * sip-subscribe-winfo - SUBSCRIBE to watcher list on a SIP Presence Agent 32 | * sip-subscribe-presence - SUBSCRIBE to presence event 33 | * sip-auto-publish-presence - Publish randomly generated presence event 34 | * sip-subscribe-rls - SUBSCRIBE to lists managed by a SIP Resource List Server 35 | * sip-subscribe-xcap-diff - SUBSCRIBE to XCAP resources changes 36 | * sip-subscribe-mwi - SUBSCRIBE to Message Waiting Indication on a Voicemail server 37 | * xcap-directory - List documents stored on a XCAP server for a given user 38 | * xcap-pres-rules - Manage the content of a pres-rules XCAP document 39 | * xcap-dialog-rules - Manage the content of a dialog-rules XCAP document 40 | * xcap-icon - Manage an icon document stored on the XCAP server 41 | * xcap-rls-services - Manage the content of a rls-services XCAP document 42 | 43 | For detailed information about how to setup and use the command line tools 44 | go to http://sipsimpleclient.org/projects/sipsimpleclient/wiki/SipTesting 45 | 46 | -------------------------------------------------------------------------------- /docs/Install.txt: -------------------------------------------------------------------------------- 1 | 2 | Installation procedure for SIP SIMPLE clients 3 | --------------------------------------------- 4 | 5 | Home page: http://sipsimpleclient.org 6 | 7 | This document described the manual installation procedure for Linux and 8 | MacOSX operating systems. 9 | 10 | 11 | Step 1. Install SIP SIMPLE client SDK 12 | ------------------------------------- 13 | 14 | Follow the instructions in the docs directory of python-sipsimple, 15 | available from: 16 | 17 | https://github.com/AGProjects/python-sipsimple 18 | 19 | 20 | Step 2. Install SIP SIMPLE clients 21 | ---------------------------------- 22 | 23 | cd sipclients 24 | sudo python setup.py install 25 | 26 | -------------------------------------------------------------------------------- /resources/sounds/dtmf_0_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/dtmf_0_tone.wav -------------------------------------------------------------------------------- /resources/sounds/dtmf_1_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/dtmf_1_tone.wav -------------------------------------------------------------------------------- /resources/sounds/dtmf_2_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/dtmf_2_tone.wav -------------------------------------------------------------------------------- /resources/sounds/dtmf_3_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/dtmf_3_tone.wav -------------------------------------------------------------------------------- /resources/sounds/dtmf_4_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/dtmf_4_tone.wav -------------------------------------------------------------------------------- /resources/sounds/dtmf_5_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/dtmf_5_tone.wav -------------------------------------------------------------------------------- /resources/sounds/dtmf_6_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/dtmf_6_tone.wav -------------------------------------------------------------------------------- /resources/sounds/dtmf_7_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/dtmf_7_tone.wav -------------------------------------------------------------------------------- /resources/sounds/dtmf_8_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/dtmf_8_tone.wav -------------------------------------------------------------------------------- /resources/sounds/dtmf_9_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/dtmf_9_tone.wav -------------------------------------------------------------------------------- /resources/sounds/dtmf_A_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/dtmf_A_tone.wav -------------------------------------------------------------------------------- /resources/sounds/dtmf_B_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/dtmf_B_tone.wav -------------------------------------------------------------------------------- /resources/sounds/dtmf_C_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/dtmf_C_tone.wav -------------------------------------------------------------------------------- /resources/sounds/dtmf_D_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/dtmf_D_tone.wav -------------------------------------------------------------------------------- /resources/sounds/dtmf_pound_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/dtmf_pound_tone.wav -------------------------------------------------------------------------------- /resources/sounds/dtmf_star_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/dtmf_star_tone.wav -------------------------------------------------------------------------------- /resources/sounds/file_received.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/file_received.wav -------------------------------------------------------------------------------- /resources/sounds/file_sent.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/file_sent.wav -------------------------------------------------------------------------------- /resources/sounds/hangup_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/hangup_tone.wav -------------------------------------------------------------------------------- /resources/sounds/hold_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/hold_tone.wav -------------------------------------------------------------------------------- /resources/sounds/message_received.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/message_received.wav -------------------------------------------------------------------------------- /resources/sounds/message_sent.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/message_sent.wav -------------------------------------------------------------------------------- /resources/sounds/ring_inbound.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/ring_inbound.wav -------------------------------------------------------------------------------- /resources/sounds/ring_outbound.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/ring_outbound.wav -------------------------------------------------------------------------------- /resources/sounds/ring_tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AGProjects/sipclients/5912c41ba7d7088812d739f1550092b3c1ad8010/resources/sounds/ring_tone.wav -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | 3 | from distutils.core import setup 4 | import os 5 | import glob 6 | 7 | import sipclient 8 | 9 | setup( 10 | name='sipclients', 11 | version=sipclient.__version__, 12 | 13 | description='SIP SIMPLE client', 14 | long_description='Python command line clients using the SIP SIMPLE framework', 15 | url='http://sipsimpleclient.org', 16 | 17 | author='AG Projects', 18 | author_email='support@ag-projects.com', 19 | 20 | platforms=['Platform Independent'], 21 | classifiers=[ 22 | 'Development Status :: 5 - Production/Stable', 23 | 'Intended Audience :: Service Providers', 24 | 'License :: GNU Lesser General Public License (LGPL)', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python' 27 | ], 28 | 29 | packages=['sipclient', 'sipclient.configuration'], 30 | data_files=[('share/sipclients/sounds', glob.glob(os.path.join('resources', 'sounds', '*.wav')))], 31 | scripts=[ 32 | 'sip-audio-session', 33 | 'sip-message', 34 | 'sip-publish-presence', 35 | 'sip-register', 36 | 'sip-session', 37 | 'sip-settings', 38 | 'sip-subscribe-mwi', 39 | 'sip-subscribe-presence', 40 | 'sip-subscribe-rls', 41 | 'sip-subscribe-winfo', 42 | 'sip-subscribe-xcap-diff' 43 | ] 44 | ) 45 | -------------------------------------------------------------------------------- /sip-message: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import atexit 4 | import os 5 | import select 6 | import signal 7 | import sys 8 | import termios 9 | 10 | from datetime import datetime 11 | from optparse import OptionParser 12 | from threading import Thread 13 | from time import sleep 14 | 15 | from application import log 16 | from application.notification import NotificationCenter, NotificationData 17 | from application.python.queue import EventQueue 18 | 19 | from sipsimple.core import FromHeader, Message, RouteHeader, SIPCoreError, SIPURI, ToHeader 20 | 21 | from sipsimple.account import Account, AccountManager, BonjourAccount 22 | from sipsimple.application import SIPApplication 23 | from sipsimple.configuration import ConfigurationError 24 | from sipsimple.configuration.settings import SIPSimpleSettings 25 | from sipsimple.core import Engine 26 | from sipsimple.lookup import DNSLookup 27 | from sipsimple.storage import FileStorage 28 | 29 | from sipclient.configuration import config_directory 30 | from sipclient.configuration.account import AccountExtension 31 | from sipclient.configuration.settings import SIPSimpleSettingsExtension 32 | from sipclient.log import Logger 33 | from sipclient.system import IPAddressMonitor 34 | 35 | 36 | class InputThread(Thread): 37 | def __init__(self, read_message, batch_mode): 38 | Thread.__init__(self) 39 | self.setDaemon(True) 40 | self.read_message = read_message 41 | self.batch_mode = batch_mode 42 | self._old_terminal_settings = None 43 | 44 | def start(self): 45 | atexit.register(self._termios_restore) 46 | Thread.start(self) 47 | 48 | def run(self): 49 | notification_center = NotificationCenter() 50 | 51 | if self.read_message: 52 | lines = [] 53 | try: 54 | while True: 55 | lines.append(raw_input()) 56 | except EOFError: 57 | message = '\n'.join(lines) 58 | notification_center.post_notification('SIPApplicationGotInputMessage', sender=self, data=NotificationData(message=message)) 59 | 60 | if not self.batch_mode: 61 | while True: 62 | chars = list(self._getchars()) 63 | while chars: 64 | char = chars.pop(0) 65 | if char == '\x1b': # escape 66 | if len(chars) >= 2 and chars[0] == '[' and chars[1] in ('A', 'B', 'C', 'D'): # one of the arrow keys 67 | char = char + chars.pop(0) + chars.pop(0) 68 | notification_center.post_notification('SIPApplicationGotInput', sender=self, data=NotificationData(input=char)) 69 | 70 | def stop(self): 71 | self._termios_restore() 72 | 73 | def _termios_restore(self): 74 | if self._old_terminal_settings is not None: 75 | termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) 76 | 77 | def _getchars(self): 78 | fd = sys.stdin.fileno() 79 | if os.isatty(fd): 80 | self._old_terminal_settings = termios.tcgetattr(fd) 81 | new = termios.tcgetattr(fd) 82 | new[3] = new[3] & ~termios.ICANON & ~termios.ECHO 83 | new[6][termios.VMIN] = '\000' 84 | try: 85 | termios.tcsetattr(fd, termios.TCSADRAIN, new) 86 | if select.select([fd], [], [], None)[0]: 87 | return sys.stdin.read(4192) 88 | finally: 89 | self._termios_restore() 90 | else: 91 | return os.read(fd, 4192) 92 | 93 | 94 | class SIPMessageApplication(SIPApplication): 95 | def __init__(self): 96 | self.account = None 97 | self.options = None 98 | self.target = None 99 | 100 | self.routes = [] 101 | self.registration_succeeded = False 102 | 103 | self.input = None 104 | self.output = None 105 | self.ip_address_monitor = IPAddressMonitor() 106 | self.logger = None 107 | 108 | def _write(self, message): 109 | if isinstance(message, unicode): 110 | message = message.encode(sys.getfilesystemencoding()) 111 | sys.stdout.write(message) 112 | sys.stdout.flush() 113 | 114 | def start(self, target, options): 115 | notification_center = NotificationCenter() 116 | 117 | self.options = options 118 | self.message = options.message 119 | self.target = target 120 | self.input = InputThread(read_message=self.target is not None and options.message is None, batch_mode=options.batch_mode) 121 | self.output = EventQueue(self._write) 122 | self.logger = Logger(sip_to_stdout=options.trace_sip, pjsip_to_stdout=options.trace_pjsip, notifications_to_stdout=options.trace_notifications) 123 | 124 | notification_center.add_observer(self, sender=self) 125 | notification_center.add_observer(self, sender=self.input) 126 | notification_center.add_observer(self, name='SIPEngineGotMessage') 127 | 128 | if self.input: 129 | self.input.start() 130 | self.output.start() 131 | 132 | log.level.current = log.level.WARNING # get rid of twisted messages 133 | 134 | Account.register_extension(AccountExtension) 135 | BonjourAccount.register_extension(AccountExtension) 136 | SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) 137 | try: 138 | SIPApplication.start(self, FileStorage(options.config_directory or config_directory)) 139 | except ConfigurationError, e: 140 | self.output.put("Failed to load sipclient's configuration: %s\n" % str(e)) 141 | self.output.put("If an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script.\n") 142 | self.output.stop() 143 | 144 | def _NH_SIPApplicationWillStart(self, notification): 145 | account_manager = AccountManager() 146 | notification_center = NotificationCenter() 147 | settings = SIPSimpleSettings() 148 | 149 | for account in account_manager.iter_accounts(): 150 | if isinstance(account, Account): 151 | account.sip.register = False 152 | account.presence.enabled = False 153 | account.xcap.enabled = False 154 | account.message_summary.enabled = False 155 | 156 | if self.options.account is None: 157 | self.account = account_manager.default_account 158 | else: 159 | possible_accounts = [account for account in account_manager.iter_accounts() if self.options.account in account.id and account.enabled] 160 | if len(possible_accounts) > 1: 161 | self.output.put('More than one account exists which matches %s: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in possible_accounts)))) 162 | self.output.stop() 163 | self.stop() 164 | return 165 | elif len(possible_accounts) == 0: 166 | self.output.put('No enabled account that matches %s was found. Available and enabled accounts: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) 167 | self.output.stop() 168 | self.stop() 169 | return 170 | else: 171 | self.account = possible_accounts[0] 172 | 173 | if isinstance(self.account, Account) and self.target is None: 174 | self.account.sip.register = True 175 | self.account.presence.enabled = False 176 | self.account.xcap.enabled = False 177 | self.account.message_summary.enabled = False 178 | notification_center.add_observer(self, sender=self.account) 179 | self.output.put('Using account %s\n' % self.account.id) 180 | 181 | self.logger.start() 182 | if settings.logs.trace_sip and self.logger._siptrace_filename is not None: 183 | self.output.put('Logging SIP trace to file "%s"\n' % self.logger._siptrace_filename) 184 | if settings.logs.trace_pjsip and self.logger._pjsiptrace_filename is not None: 185 | self.output.put('Logging PJSIP trace to file "%s"\n' % self.logger._pjsiptrace_filename) 186 | if settings.logs.trace_notifications and self.logger._notifications_filename is not None: 187 | self.output.put('Logging notifications trace to file "%s"\n' % self.logger._notifications_filename) 188 | 189 | def _NH_SIPApplicationDidStart(self, notification): 190 | notification_center = NotificationCenter() 191 | settings = SIPSimpleSettings() 192 | 193 | engine = Engine() 194 | 195 | engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip 196 | engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 197 | 198 | self.ip_address_monitor.start() 199 | 200 | if isinstance(self.account, BonjourAccount) and self.target is None: 201 | for transport in settings.sip.transport_list: 202 | try: 203 | self.output.put('Listening on: %s\n' % self.account.contact[transport]) 204 | except KeyError: 205 | pass 206 | 207 | if self.target is not None: 208 | if '@' not in self.target: 209 | self.target = '%s@%s' % (self.target, self.account.id.domain) 210 | if not self.target.startswith('sip:') and not self.target.startswith('sips:'): 211 | self.target = 'sip:' + self.target 212 | try: 213 | self.target = SIPURI.parse(self.target) 214 | except SIPCoreError: 215 | self.output.put('Illegal SIP URI: %s\n' % self.target) 216 | self.stop() 217 | if self.message is None: 218 | self.output.put('Press Ctrl+D on an empty line to end input and send the MESSAGE request.\n') 219 | else: 220 | settings = SIPSimpleSettings() 221 | lookup = DNSLookup() 222 | notification_center.add_observer(self, sender=lookup) 223 | if isinstance(self.account, Account) and self.account.sip.outbound_proxy is not None: 224 | uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) 225 | elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: 226 | uri = SIPURI(host=self.account.id.domain) 227 | else: 228 | uri = self.target 229 | lookup.lookup_sip_proxy(uri, settings.sip.transport_list) 230 | else: 231 | self.output.put('Press Ctrl+D to stop the program.\n') 232 | 233 | def _NH_SIPApplicationWillEnd(self, notification): 234 | self.ip_address_monitor.stop() 235 | 236 | def _NH_SIPApplicationDidEnd(self, notification): 237 | if self.input: 238 | self.input.stop() 239 | self.output.stop() 240 | self.output.join() 241 | 242 | def _NH_SIPApplicationGotInput(self, notification): 243 | if notification.data.input == '\x04': 244 | self.stop() 245 | 246 | def _NH_SIPApplicationGotInputMessage(self, notification): 247 | if not notification.data.message: 248 | self.stop() 249 | else: 250 | notification_center = NotificationCenter() 251 | settings = SIPSimpleSettings() 252 | self.message = notification.data.message 253 | lookup = DNSLookup() 254 | notification_center.add_observer(self, sender=lookup) 255 | if isinstance(self.account, Account) and self.account.sip.outbound_proxy is not None: 256 | uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) 257 | elif isinstance(self.account, Account) and self.account.sip.always_use_my_proxy: 258 | uri = SIPURI(host=self.account.id.domain) 259 | else: 260 | uri = self.target 261 | lookup.lookup_sip_proxy(uri, settings.sip.transport_list) 262 | 263 | def _NH_SIPEngineGotException(self, notification): 264 | self.output.put('An exception occured within the SIP core:\n%s\n' % notification.data.traceback) 265 | 266 | def _NH_SIPAccountRegistrationDidSucceed(self, notification): 267 | if self.registration_succeeded: 268 | return 269 | contact_header = notification.data.contact_header 270 | contact_header_list = notification.data.contact_header_list 271 | expires = notification.data.expires 272 | registrar = notification.data.registrar 273 | message = '%s Registered contact "%s" for sip:%s at %s:%d;transport=%s (expires in %d seconds).\n' % (datetime.now().replace(microsecond=0), contact_header.uri, self.account.id, registrar.address, registrar.port, registrar.transport, expires) 274 | if len(contact_header_list) > 1: 275 | message += 'Other registered contacts:\n%s\n' % '\n'.join([' %s (expires in %s seconds)' % (str(other_contact_header.uri), other_contact_header.expires) for other_contact_header in contact_header_list if other_contact_header.uri != notification.data.contact_header.uri]) 276 | self.output.put(message) 277 | 278 | self.registration_succeeded = True 279 | 280 | def _NH_SIPAccountRegistrationDidFail(self, notification): 281 | self.output.put('%s Failed to register contact for sip:%s: %s (retrying in %.2f seconds)\n' % (datetime.now().replace(microsecond=0), self.account.id, notification.data.error, notification.data.retry_after)) 282 | self.registration_succeeded = False 283 | 284 | def _NH_SIPAccountRegistrationDidEnd(self, notification): 285 | self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0)) 286 | 287 | def _NH_DNSLookupDidSucceed(self, notification): 288 | self.routes = notification.data.result 289 | self._send_message() 290 | 291 | def _NH_DNSLookupDidFail(self, notification): 292 | self.output.put('DNS lookup failed: %s\n' % notification.data.error) 293 | self.stop() 294 | 295 | def _NH_SIPEngineGotMessage(self, notification): 296 | content_type = notification.data.content_type 297 | if content_type not in ('text/plain', 'text/html'): 298 | return 299 | from_header = FromHeader.new(notification.data.from_header) 300 | from_header.parameters = {} 301 | from_header.uri.parameters = {} 302 | identity = str(from_header.uri) 303 | if from_header.display_name: 304 | identity = '"%s" <%s>' % (from_header.display_name, identity) 305 | body = notification.data.body 306 | self.output.put("Got MESSAGE from '%s', Content-Type: %s\n%s\n" % (identity, content_type, body)) 307 | 308 | def _NH_SIPMessageDidSucceed(self, notification): 309 | self.output.put('MESSAGE was accepted by remote party\n') 310 | self.stop() 311 | 312 | def _NH_SIPMessageDidFail(self, notification): 313 | notification_center = NotificationCenter() 314 | notification_center.remove_observer(self, sender=notification.sender) 315 | self.output.put('Could not deliver MESSAGE: %d %s\n' % (notification.data.code, notification.data.reason)) 316 | self._send_message() 317 | 318 | def _send_message(self): 319 | notification_center = NotificationCenter() 320 | if self.routes: 321 | route = self.routes.pop(0) 322 | identity = str(self.account.uri) 323 | if self.account.display_name: 324 | identity = '"%s" <%s>' % (self.account.display_name, identity) 325 | self.output.put("Sending MESSAGE from '%s' to '%s' using proxy %s\n" % (identity, self.target, route)) 326 | self.output.put('Press Ctrl+D to stop the program.\n') 327 | message_request = Message(FromHeader(self.account.uri, self.account.display_name), ToHeader(self.target), RouteHeader(route.uri), 'text/plain', self.message, credentials=self.account.credentials) 328 | notification_center.add_observer(self, sender=message_request) 329 | message_request.send() 330 | else: 331 | self.output.put('No more routes to try. Aborting.\n') 332 | self.stop() 333 | 334 | if __name__ == '__main__': 335 | description = "This script will either sit idle waiting for an incoming MESSAGE request, or send a MESSAGE request to the specified SIP target. In outgoing mode the program will read the contents of the messages to be sent from standard input, Ctrl+D signalling EOF as usual. In listen mode the program will quit when Ctrl+D is pressed." 336 | usage = '%prog [options] [user@domain]' 337 | parser = OptionParser(usage=usage, description=description) 338 | parser.print_usage = parser.print_help 339 | parser.add_option('-a', '--account', type='string', dest='account', help='The account name to use for any outgoing traffic. If not supplied, the default account will be used.', metavar='NAME') 340 | parser.add_option('-c', '--config-directory', type='string', dest='config_directory', help='The configuration directory to use. This overrides the default location.') 341 | parser.add_option('-s', '--trace-sip', action='store_true', dest='trace_sip', default=False, help='Dump the raw contents of incoming and outgoing SIP messages.') 342 | parser.add_option('-j', '--trace-pjsip', action='store_true', dest='trace_pjsip', default=False, help='Print PJSIP logging output.') 343 | parser.add_option('-n', '--trace-notifications', action='store_true', dest='trace_notifications', default=False, help='Print all notifications (disabled by default).') 344 | parser.add_option('-b', '--batch', action='store_true', dest='batch_mode', default=False, help='Run the program in batch mode: reading control input from the console is disabled. This is particularly useful when running this script in a non-interactive environment.') 345 | parser.add_option('-m', '--message', type='string', dest='message', help='Contents of the message to send. This disables reading the message from standard input.') 346 | options, args = parser.parse_args() 347 | 348 | target = args[0] if args else None 349 | 350 | 351 | application = SIPMessageApplication() 352 | application.start(target, options) 353 | 354 | signal.signal(signal.SIGINT, signal.SIG_DFL) 355 | application.output.join() 356 | sleep(0.1) 357 | -------------------------------------------------------------------------------- /sip-register: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import atexit 4 | import os 5 | import select 6 | import signal 7 | import sys 8 | import termios 9 | 10 | from datetime import datetime 11 | from optparse import OptionParser 12 | from threading import Thread 13 | from time import sleep 14 | 15 | from application import log 16 | from application.notification import NotificationCenter, NotificationData 17 | from application.python.queue import EventQueue 18 | 19 | from sipsimple.account import Account, AccountManager, BonjourAccount 20 | from sipsimple.application import SIPApplication 21 | from sipsimple.configuration import ConfigurationError 22 | from sipsimple.configuration.settings import SIPSimpleSettings 23 | from sipsimple.core import Engine 24 | from sipsimple.storage import FileStorage 25 | 26 | from sipclient.configuration import config_directory 27 | from sipclient.configuration.account import AccountExtension 28 | from sipclient.configuration.settings import SIPSimpleSettingsExtension 29 | from sipclient.log import Logger 30 | from sipclient.system import IPAddressMonitor 31 | 32 | 33 | class BonjourNeighbour(object): 34 | def __init__(self, neighbour, uri, display_name, host): 35 | self.display_name = display_name 36 | self.host = host 37 | self.neighbour = neighbour 38 | self.uri = uri 39 | 40 | 41 | class InputThread(Thread): 42 | def __init__(self): 43 | Thread.__init__(self) 44 | self.daemon = True 45 | self._old_terminal_settings = None 46 | 47 | def start(self): 48 | atexit.register(self._termios_restore) 49 | Thread.start(self) 50 | 51 | def run(self): 52 | notification_center = NotificationCenter() 53 | while True: 54 | for char in self._getchars(): 55 | notification_center.post_notification('SIPApplicationGotInput', sender=self, data=NotificationData(input=char)) 56 | 57 | def stop(self): 58 | self._termios_restore() 59 | 60 | def _termios_restore(self): 61 | if self._old_terminal_settings is not None: 62 | termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) 63 | 64 | def _getchars(self): 65 | fd = sys.stdin.fileno() 66 | if os.isatty(fd): 67 | self._old_terminal_settings = termios.tcgetattr(fd) 68 | new = termios.tcgetattr(fd) 69 | new[3] = new[3] & ~termios.ICANON & ~termios.ECHO 70 | new[6][termios.VMIN] = '\000' 71 | try: 72 | termios.tcsetattr(fd, termios.TCSADRAIN, new) 73 | if select.select([fd], [], [], None)[0]: 74 | return sys.stdin.read(4192) 75 | finally: 76 | self._termios_restore() 77 | else: 78 | return os.read(fd, 4192) 79 | 80 | 81 | class RegistrationApplication(SIPApplication): 82 | def __init__(self): 83 | self.account = None 84 | self.options = None 85 | 86 | self.input = None 87 | self.output = None 88 | self.ip_address_monitor = IPAddressMonitor() 89 | self.logger = None 90 | self.max_registers = None 91 | self.neighbours = {} 92 | self.success = False 93 | 94 | def _write(self, message): 95 | if isinstance(message, unicode): 96 | message = message.encode(sys.getfilesystemencoding()) 97 | sys.stdout.write(message) 98 | sys.stdout.flush() 99 | 100 | def start(self, options): 101 | notification_center = NotificationCenter() 102 | 103 | self.options = options 104 | self.max_registers = options.max_registers if options.max_registers > 0 else None 105 | 106 | self.input = InputThread() if not options.batch_mode else None 107 | self.output = EventQueue(self._write) 108 | self.logger = Logger(sip_to_stdout=options.trace_sip, pjsip_to_stdout=options.trace_pjsip, notifications_to_stdout=options.trace_notifications) 109 | 110 | notification_center.add_observer(self, sender=self) 111 | notification_center.add_observer(self, sender=self.input) 112 | notification_center.add_observer(self, name='SIPSessionNewIncoming') 113 | notification_center.add_observer(self, name='DNSLookupDidSucceed') 114 | 115 | if self.input: 116 | self.input.start() 117 | self.output.start() 118 | 119 | log.level.current = log.level.WARNING # get rid of twisted messages 120 | 121 | Account.register_extension(AccountExtension) 122 | BonjourAccount.register_extension(AccountExtension) 123 | SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) 124 | try: 125 | SIPApplication.start(self, FileStorage(options.config_directory or config_directory)) 126 | except ConfigurationError, e: 127 | self.output.put("Failed to load sipclient's configuration: %s\n" % str(e)) 128 | self.output.put("If an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script.\n") 129 | self.output.stop() 130 | 131 | def print_help(self): 132 | message = 'Available control keys:\n' 133 | message += ' s: toggle SIP trace on the console\n' 134 | message += ' j: toggle PJSIP trace on the console\n' 135 | message += ' n: toggle notifications trace on the console\n' 136 | message += ' Ctrl-d: quit the program\n' 137 | message += ' ?: display this help message\n' 138 | self.output.put('\n'+message) 139 | 140 | def _NH_SIPApplicationWillStart(self, notification): 141 | account_manager = AccountManager() 142 | notification_center = NotificationCenter() 143 | settings = SIPSimpleSettings() 144 | 145 | if self.options.account is None: 146 | self.account = account_manager.default_account 147 | else: 148 | possible_accounts = [account for account in account_manager.iter_accounts() if self.options.account in account.id and account.enabled] 149 | if len(possible_accounts) > 1: 150 | self.output.put('More than one account exists which matches %s: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in possible_accounts)))) 151 | self.output.stop() 152 | self.stop() 153 | return 154 | elif len(possible_accounts) == 0: 155 | self.output.put('No enabled account that matches %s was found. Available and enabled accounts: %s\n' % (self.options.account, ', '.join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) 156 | self.output.stop() 157 | self.stop() 158 | return 159 | else: 160 | self.account = possible_accounts[0] 161 | for account in account_manager.iter_accounts(): 162 | if account is self.account: 163 | if isinstance(account, Account): 164 | account.sip.register = True 165 | account.message_summary.enabled = False 166 | account.presence.enabled = False 167 | account.xcap.enabled = False 168 | account.enabled = True 169 | else: 170 | account.enabled = False 171 | self.output.put('Using account %s\n' % self.account.id) 172 | notification_center.add_observer(self, sender=self.account) 173 | if self.account is BonjourAccount() and self.max_registers is not None: 174 | if self.max_registers == 1: 175 | self.max_registers = len([transport for transport in settings.sip.transport_list if (transport!='tls' or self.account.tls.certificate is not None)]) 176 | else: 177 | self.output.put('--max-registers option only accepts 0 or 1 if using Bonjour account\n') 178 | self.output.stop() 179 | self.stop() 180 | return 181 | 182 | # start logging 183 | self.logger.start() 184 | if settings.logs.trace_sip and self.logger._siptrace_filename is not None: 185 | self.output.put('Logging SIP trace to file "%s"\n' % self.logger._siptrace_filename) 186 | if settings.logs.trace_pjsip and self.logger._pjsiptrace_filename is not None: 187 | self.output.put('Logging PJSIP trace to file "%s"\n' % self.logger._pjsiptrace_filename) 188 | if settings.logs.trace_notifications and self.logger._notifications_filename is not None: 189 | self.output.put('Logging notifications trace to file "%s"\n' % self.logger._notifications_filename) 190 | 191 | def _NH_SIPApplicationDidStart(self, notification): 192 | engine = Engine() 193 | settings = SIPSimpleSettings() 194 | 195 | engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip 196 | engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 197 | 198 | self.ip_address_monitor.start() 199 | if self.account is not BonjourAccount() and self.max_registers != 1 and not self.options.batch_mode: 200 | self.print_help() 201 | elif self.account is BonjourAccount() and self.max_registers is None: 202 | self.print_help() 203 | 204 | def _NH_SIPApplicationWillEnd(self, notification): 205 | self.ip_address_monitor.stop() 206 | 207 | def _NH_SIPApplicationDidEnd(self, notification): 208 | if self.input: 209 | self.input.stop() 210 | self.output.stop() 211 | self.output.join() 212 | 213 | def _NH_DNSLookupDidSucceed(self, notification): 214 | notification_center = NotificationCenter() 215 | 216 | targets = {} 217 | for result in notification.data.result: 218 | try: 219 | ports = targets[result.address] 220 | except KeyError: 221 | targets[result.address] = set() 222 | 223 | port = '%s-%s' % (result.port, result.transport.upper()) 224 | targets[result.address].add(port) 225 | 226 | result_text = '' 227 | i = 1 228 | for t in targets.keys(): 229 | result_text = result_text + ' ' + str(i) + ') ' + t + ': ' 230 | result_text = result_text + ' '.join(sorted(list(targets[t]))) 231 | i = i + 1 232 | 233 | self.output.put(u"\n%s DNS lookup for %s succeeded:%s\n" % (datetime.now().replace(microsecond=0), self.account.id.domain, result_text)) 234 | 235 | def _NH_SIPApplicationGotInput(self, notification): 236 | engine = Engine() 237 | settings = SIPSimpleSettings() 238 | key = notification.data.input 239 | if key == '\x04': 240 | self.stop() 241 | elif key == 's': 242 | self.logger.sip_to_stdout = not self.logger.sip_to_stdout 243 | engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip 244 | self.output.put('SIP tracing to console is now %s.\n' % ('activated' if self.logger.sip_to_stdout else 'deactivated')) 245 | elif key == 'j': 246 | self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout 247 | engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 248 | self.output.put('PJSIP tracing to console is now %s.\n' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated')) 249 | elif key == 'n': 250 | self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout 251 | self.output.put('Notification tracing to console is now %s.\n' % ('activated' if self.logger.notifications_to_stdout else 'deactivated')) 252 | elif key == '?': 253 | self.print_help() 254 | 255 | def _NH_SIPAccountRegistrationDidSucceed(self, notification): 256 | contact_header = notification.data.contact_header 257 | contact_header_list = notification.data.contact_header_list 258 | expires = notification.data.expires 259 | registrar = notification.data.registrar 260 | if not self.success: 261 | message = '%s Registered contact "%s" at %s:%d;transport=%s for %d seconds\n' % (datetime.now().replace(microsecond=0), contact_header.uri, registrar.address, registrar.port, registrar.transport, expires) 262 | if notification.sender.contact.public_gruu: 263 | message += '%s Public GRUU: "%s"\n' % (datetime.now().replace(microsecond=0), notification.sender.contact.public_gruu) 264 | 265 | if notification.sender.contact.temporary_gruu: 266 | message += '%s Temporary GRUU: "%s"\n' % (datetime.now().replace(microsecond=0), notification.sender.contact.temporary_gruu) 267 | 268 | if len(contact_header_list) > 1: 269 | message += 'Other registered contacts:\n%s\n' % '\n'.join([' %s (expires in %s seconds)' % (str(other_contact_header.uri), other_contact_header.expires) for other_contact_header in contact_header_list if other_contact_header.uri != notification.data.contact_header.uri]) 270 | self.output.put(message) 271 | 272 | self.success = True 273 | else: 274 | self.output.put('%s Refreshed contact "%s" at %s:%d;transport=%s for %d seconds\n' % (datetime.now().replace(microsecond=0), contact_header.uri, registrar.address, registrar.port, registrar.transport, expires)) 275 | 276 | if self.max_registers is not None: 277 | self.max_registers -= 1 278 | if self.max_registers == 0: 279 | self.stop() 280 | 281 | def _NH_SIPAccountRegistrationGotAnswer(self, notification): 282 | if notification.data.code >= 300: 283 | registrar = notification.data.registrar 284 | code = notification.data.code 285 | reason = notification.data.reason 286 | self.output.put('%s Registration failed at %s:%d;transport=%s: %d %s\n' % (datetime.now().replace(microsecond=0), registrar.address, registrar.port, registrar.transport, code, reason)) 287 | 288 | def _NH_SIPAccountRegistrationDidFail(self, notification): 289 | self.output.put('%s Failed to register contact for sip:%s: %s (retrying in %.2f seconds)\n' % (datetime.now().replace(microsecond=0), self.account.id, notification.data.error, notification.data.retry_after)) 290 | self.success = False 291 | 292 | if self.max_registers is not None: 293 | self.max_registers -= 1 294 | if self.max_registers == 0: 295 | self.stop() 296 | 297 | def _NH_SIPAccountRegistrationDidEnd(self, notification): 298 | self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0)) 299 | 300 | def _NH_BonjourAccountRegistrationDidSucceed(self, notification): 301 | self.output.put('%s Registered Bonjour contact "%s"\n' % (datetime.now().replace(microsecond=0), notification.data.name)) 302 | if self.max_registers is not None: 303 | self.max_registers -= 1 304 | if self.max_registers == 0: 305 | self.stop() 306 | 307 | def _NH_BonjourAccountRegistrationDidFail(self, notification): 308 | self.output.put('%s Failed to register Bonjour contact: %s\n' % (datetime.now().replace(microsecond=0), notification.data.reason)) 309 | if self.max_registers is not None: 310 | self.max_registers -= 1 311 | if self.max_registers == 0: 312 | self.stop() 313 | 314 | def _NH_BonjourAccountRegistrationDidEnd(self, notification): 315 | self.output.put('%s Registration ended.\n' % datetime.now().replace(microsecond=0)) 316 | 317 | def _NH_BonjourAccountDidAddNeighbour(self, notification): 318 | neighbour, record = notification.data.neighbour, notification.data.record 319 | now = datetime.now().replace(microsecond=0) 320 | self.output.put('%s Discovered Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri)) 321 | self.neighbours[neighbour] = BonjourNeighbour(neighbour, record.uri, record.name, record.host) 322 | 323 | def _NH_BonjourAccountDidUpdateNeighbour(self, notification): 324 | neighbour, record = notification.data.neighbour, notification.data.record 325 | now = datetime.now().replace(microsecond=0) 326 | try: 327 | bonjour_neighbour = self.neighbours[neighbour] 328 | except KeyError: 329 | self.output.put('%s Discovered Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri)) 330 | self.neighbours[neighbour] = BonjourNeighbour(neighbour, record.uri, record.name, record.host) 331 | else: 332 | self.output.put('%s Updated Bonjour neighbour: "%s (%s)" <%s>\n' % (now, record.name, record.host, record.uri)) 333 | bonjour_neighbour.display_name = record.name 334 | bonjour_neighbour.host = record.host 335 | bonjour_neighbour.uri = record.uri 336 | 337 | def _NH_BonjourAccountDidRemoveNeighbour(self, notification): 338 | neighbour = notification.data.neighbour 339 | now = datetime.now().replace(microsecond=0) 340 | try: 341 | bonjour_neighbour = self.neighbours.pop(neighbour) 342 | except KeyError: 343 | pass 344 | else: 345 | self.output.put('%s Bonjour neighbour left: "%s (%s)" <%s>\n' % (now, bonjour_neighbour.display_name, bonjour_neighbour.host, bonjour_neighbour.uri)) 346 | 347 | def _NH_SIPEngineGotException(self, notification): 348 | self.output.put('%s An exception occured within the SIP core:\n%s\n' % (datetime.now().replace(microsecond=0), notification.data.traceback)) 349 | 350 | 351 | if __name__ == "__main__": 352 | description = 'This script registers the contact address of the given SIP account to the SIP registrar and refresh it while the program is running. When Ctrl+D is pressed it will unregister.' 353 | usage = '%prog [options]' 354 | parser = OptionParser(usage=usage, description=description) 355 | parser.print_usage = parser.print_help 356 | parser.add_option('-a', '--account', type='string', dest='account', help='The name of the account to use. If not supplied, the default account will be used.', metavar='NAME') 357 | parser.add_option('-c', '--config-directory', type='string', dest='config_directory', help='The configuration directory to use. This overrides the default location.') 358 | parser.add_option('-s', '--trace-sip', action='store_true', dest='trace_sip', default=False, help='Dump the raw contents of incoming and outgoing SIP messages (disabled by default).') 359 | parser.add_option('-j', '--trace-pjsip', action='store_true', dest='trace_pjsip', default=False, help='Print PJSIP logging output (disabled by default).') 360 | parser.add_option('-n', '--trace-notifications', action='store_true', dest='trace_notifications', default=False, help='Print all notifications (disabled by default).') 361 | parser.add_option('-r', '--max-registers', type='int', dest='max_registers', default=1, help='Max number of REGISTERs sent (default 1, set to 0 for infinite).') 362 | parser.add_option('-b', '--batch', action='store_true', dest='batch_mode', default=False, help='Run the program in batch mode: reading input from the console is disabled. This is particularly useful when running this script in a non-interactive environment.') 363 | options, args = parser.parse_args() 364 | 365 | application = RegistrationApplication() 366 | application.start(options) 367 | 368 | signal.signal(signal.SIGINT, signal.SIG_DFL) 369 | application.output.join() 370 | sleep(0.1) 371 | sys.exit(0 if application.success else 1) 372 | -------------------------------------------------------------------------------- /sip-settings: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import fcntl 4 | import re 5 | import struct 6 | import sys 7 | import termios 8 | 9 | from collections import deque 10 | from optparse import OptionParser 11 | 12 | from sipsimple.account import Account, BonjourAccount, AccountManager 13 | from sipsimple.application import SIPApplication 14 | from sipsimple.configuration import ConfigurationError, ConfigurationManager, DefaultValue, Setting, SettingsGroupMeta 15 | from sipsimple.configuration.datatypes import List, STUNServerAddress 16 | from sipsimple.configuration.settings import SIPSimpleSettings 17 | from sipsimple.storage import FileStorage 18 | from sipsimple.threading import ThreadManager 19 | 20 | from sipclient.configuration import config_directory 21 | from sipclient.configuration.account import AccountExtension 22 | from sipclient.configuration.settings import SIPSimpleSettingsExtension 23 | 24 | 25 | def format_child(obj, attrname, maxchars): 26 | linebuf = attrname 27 | if isinstance(getattr(type(obj), attrname, None), Setting): 28 | attr = getattr(obj, attrname) 29 | if isinstance(attr, unicode): 30 | string = attr.encode(sys.getfilesystemencoding()) 31 | else: 32 | string = str(attr) 33 | if maxchars is not None: 34 | maxchars -= len(attrname)+4 35 | if len(string) > maxchars: 36 | string = string[:maxchars-3]+'...' 37 | linebuf += ' = ' + string 38 | return linebuf 39 | 40 | def display_object(obj, name): 41 | # get terminal width 42 | if sys.stdout.isatty(): 43 | width = struct.unpack('HHHH', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))[1] 44 | else: 45 | width = None 46 | 47 | children = deque([child for child in dir(type(obj)) if isinstance(getattr(type(obj), child, None), Setting)] + \ 48 | [child for child in dir(type(obj)) if isinstance(getattr(type(obj), child, None), SettingsGroupMeta)]) 49 | # display first line 50 | linebuf = ' '*(len(name)+3) + '+' 51 | if children: 52 | linebuf += '-- ' + format_child(obj, children.popleft(), width-(len(name)+7) if width is not None else None) 53 | print linebuf 54 | # display second line 55 | linebuf = name + ' --|' 56 | if children: 57 | linebuf += '-- ' + format_child(obj, children.popleft(), width-(len(name)+7) if width is not None else None) 58 | print linebuf 59 | # display the rest of the lines 60 | if children: 61 | while children: 62 | child = children.popleft() 63 | linebuf = ' '*(len(name)+3) + ('|' if children else '+') + '-- ' + format_child(obj, child, width-(len(name)+7) if width is not None else None) 64 | print linebuf 65 | else: 66 | linebuf = ' '*(len(name)+3) + '+' 67 | print linebuf 68 | 69 | print 70 | 71 | [display_object(getattr(obj, child), child) for child in dir(type(obj)) if isinstance(getattr(type(obj), child, None), SettingsGroupMeta)] 72 | 73 | class SettingsParser(object): 74 | 75 | @classmethod 76 | def parse_default(cls, type, value): 77 | if issubclass(type, List): 78 | values = re.split(r'\s*,\s*', value) 79 | return values 80 | elif issubclass(type, bool): 81 | if value.lower() == 'true': 82 | return True 83 | else: 84 | return False 85 | elif issubclass(type, unicode): 86 | if isinstance(value, str): 87 | return value.decode(sys.getfilesystemencoding()) 88 | return value 89 | else: 90 | return value 91 | 92 | @classmethod 93 | def parse_MSRPRelayAddress(cls, type, value): 94 | return type.from_description(value) 95 | 96 | @classmethod 97 | def parse_SIPProxyAddress(cls, type, value): 98 | return type.from_description(value) 99 | 100 | @classmethod 101 | def parse_STUNServerAddress(cls, type, value): 102 | return type.from_description(value) 103 | 104 | @classmethod 105 | def parse_STUNServerAddressList(cls, type, value): 106 | values = re.split(r'\s*,\s*', value) 107 | return [STUNServerAddress.from_description(v) for v in values] 108 | 109 | @classmethod 110 | def parse_PortRange(cls, type, value): 111 | return type(*value.split(':', 1)) 112 | 113 | @classmethod 114 | def parse_Resolution(cls, type, value): 115 | return type(*value.split('x', 1)) 116 | 117 | @classmethod 118 | def parse_SoundFile(cls, type, value): 119 | if ',' in value: 120 | path, volume = value.split(',', 1) 121 | else: 122 | path, volume = value, 100 123 | return type(path, volume) 124 | 125 | @classmethod 126 | def parse_AccountSoundFile(cls, type, value): 127 | if ',' in value: 128 | path, volume = value.split(',', 1) 129 | else: 130 | path, volume = value, 100 131 | return type(path, volume) 132 | 133 | @classmethod 134 | def parse(cls, type, value): 135 | if value == 'None': 136 | return None 137 | if value == 'DEFAULT': 138 | return DefaultValue 139 | parser = getattr(cls, 'parse_%s' % type.__name__, cls.parse_default) 140 | return parser(type, value) 141 | 142 | 143 | class AccountConfigurator(object): 144 | def __init__(self): 145 | Account.register_extension(AccountExtension) 146 | BonjourAccount.register_extension(AccountExtension) 147 | self.configuration_manager = ConfigurationManager() 148 | self.configuration_manager.start() 149 | self.account_manager = AccountManager() 150 | self.account_manager.load() 151 | 152 | def list(self): 153 | print 'Accounts:' 154 | bonjour_account = BonjourAccount() 155 | accounts = [account for account in self.account_manager.get_accounts() if account.id != bonjour_account.id] 156 | accounts.sort(cmp=lambda a, b: cmp(a.id, b.id)) 157 | accounts.append(bonjour_account) 158 | for account in accounts: 159 | print ' %s (%s)%s' % (account.id, 'enabled' if account.enabled else 'disabled', ' - default_account' if account is self.account_manager.default_account else '') 160 | 161 | def add(self, sip_address, password): 162 | if self.account_manager.has_account(sip_address): 163 | print 'Account %s already exists' % sip_address 164 | return 165 | try: 166 | account = Account(sip_address) 167 | except ValueError, e: 168 | print 'Cannot add SIP account: %s' % str(e) 169 | return 170 | account.auth.password = password 171 | account.enabled = True 172 | account.save() 173 | print 'Account added' 174 | 175 | def delete(self, sip_address): 176 | if sip_address != 'ALL': 177 | possible_accounts = [account for account in self.account_manager.iter_accounts() if sip_address in account.id] 178 | if len(possible_accounts) > 1: 179 | print "More than one account exists which matches %s: %s" % (sip_address, ", ".join(sorted(account.id for account in possible_accounts))) 180 | return 181 | if len(possible_accounts) == 0: 182 | print 'Account %s does not exist' % sip_address 183 | return 184 | account = possible_accounts[0] 185 | if account == BonjourAccount(): 186 | print 'Cannot delete bonjour account' 187 | return 188 | account.delete() 189 | print 'Account deleted' 190 | else: 191 | for account in self.account_manager.get_accounts(): 192 | account.delete() 193 | print 'Accounts deleted' 194 | 195 | def show(self, sip_address=None): 196 | if sip_address is None: 197 | accounts = [self.account_manager.default_account] 198 | if accounts[0] is None: 199 | print "No accounts configured" 200 | return 201 | else: 202 | if sip_address != 'ALL': 203 | accounts = [account for account in self.account_manager.iter_accounts() if sip_address in account.id] 204 | else: 205 | accounts = self.account_manager.get_accounts() 206 | if not accounts: 207 | print 'No accounts which match %s' % sip_address 208 | return 209 | for account in accounts: 210 | print 'Account %s:' % account.id 211 | display_object(account, 'account') 212 | 213 | def set(self, *args): 214 | if not args: 215 | raise TypeError("set must receive at least one argument") 216 | if '=' in args[0]: 217 | accounts = [self.account_manager.default_account] 218 | if accounts[0] is None: 219 | print "No accounts configured" 220 | return 221 | else: 222 | sip_address = args[0] 223 | args = args[1:] 224 | if sip_address != 'ALL': 225 | accounts = [account for account in self.account_manager.iter_accounts() if sip_address in account.id] 226 | else: 227 | accounts = self.account_manager.get_accounts() 228 | if not accounts: 229 | print 'No accounts which match %s' % sip_address 230 | return 231 | 232 | try: 233 | settings = dict(arg.split('=', 1) for arg in args) 234 | except ValueError: 235 | print 'Illegal arguments: %s' % ' '.join(args) 236 | return 237 | 238 | for account in accounts: 239 | for attrname, value in settings.iteritems(): 240 | object = account 241 | name = attrname 242 | while '.' in name: 243 | local_name, name = name.split('.', 1) 244 | try: 245 | object = getattr(object, local_name) 246 | except AttributeError: 247 | print 'Unknown setting: %s' % attrname 248 | object = None 249 | break 250 | if object is not None: 251 | try: 252 | attribute = getattr(type(object), name) 253 | value = SettingsParser.parse(attribute.type, value) 254 | setattr(object, name, value) 255 | except AttributeError: 256 | print 'Unknown setting: %s' % attrname 257 | except ValueError, e: 258 | print '%s: %s' % (attrname, str(e)) 259 | 260 | account.save() 261 | print 'Account%s updated' % ('s' if len(accounts) > 1 else '') 262 | 263 | def default(self, sip_address): 264 | possible_accounts = [account for account in self.account_manager.iter_accounts() if sip_address in account.id] 265 | if len(possible_accounts) > 1: 266 | print "More than one account exists which matches %s: %s" % (sip_address, ", ".join(sorted(account.id for account in possible_accounts))) 267 | return 268 | if len(possible_accounts) == 0: 269 | print 'Account %s does not exist' % sip_address 270 | return 271 | account = possible_accounts[0] 272 | try: 273 | self.account_manager.default_account = account 274 | except ValueError, e: 275 | print str(e) 276 | return 277 | print 'Account %s is now default account' % account.id 278 | 279 | 280 | class SIPSimpleConfigurator(object): 281 | def __init__(self): 282 | SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) 283 | self.configuration_manager = ConfigurationManager() 284 | self.configuration_manager.start() 285 | SIPSimpleSettings() 286 | 287 | def show(self): 288 | print 'SIP SIMPLE settings:' 289 | display_object(SIPSimpleSettings(), 'SIP SIMPLE') 290 | 291 | def set(self, *args): 292 | sipsimple_settings = SIPSimpleSettings() 293 | try: 294 | settings = dict(arg.split('=', 1) for arg in args) 295 | except ValueError: 296 | print 'Illegal arguments: %s' % ' '.join(args) 297 | return 298 | 299 | for attrname, value in settings.iteritems(): 300 | object = sipsimple_settings 301 | name = attrname 302 | while '.' in name: 303 | local_name, name = name.split('.', 1) 304 | try: 305 | object = getattr(object, local_name) 306 | except AttributeError: 307 | print 'Unknown setting: %s' % attrname 308 | object = None 309 | break 310 | if object is not None: 311 | try: 312 | attribute = getattr(type(object), name) 313 | value = SettingsParser.parse(attribute.type, value) 314 | setattr(object, name, value) 315 | except AttributeError: 316 | print 'Unknown setting: %s' % attrname 317 | except ValueError, e: 318 | print '%s: %s' % (attrname, str(e)) 319 | 320 | sipsimple_settings.save() 321 | print 'SIP SIMPLE general settings updated' 322 | 323 | 324 | if __name__ == '__main__': 325 | description = "This script manages the SIP SIMPLE client SDK settings." 326 | usage = """%prog [--general|--account] [options] command [arguments] 327 | %prog --general show 328 | %prog --general set key1=value1 [key2=value2 ...] 329 | %prog --account list 330 | %prog --account add user@domain password 331 | %prog --account delete user@domain|ALL 332 | %prog --account show [user@domain|ALL] 333 | %prog --account set [user@domain|ALL] key1=value1|DEFAULT [key2=value2|DEFAULT ...] 334 | %prog --account default user@domain""" 335 | parser = OptionParser(usage=usage, description=description) 336 | parser.print_usage = parser.print_help 337 | parser.add_option('-c', '--config-directory', type='string', dest='config_directory', help='The configuration directory to use. This overrides the default location.') 338 | parser.add_option("-a", "--account", action="store_true", dest="account", help="Manage SIP accounts' settings") 339 | parser.add_option("-g", "--general", action="store_true", dest="general", help="Manage general SIP SIMPLE middleware settings") 340 | options, args = parser.parse_args() 341 | # exactly one of -a or -g must be specified 342 | if (not (options.account or options.general)) or (options.account and options.general): 343 | parser.print_usage() 344 | sys.exit(1) 345 | 346 | # there must be at least one command 347 | if not args: 348 | sys.stderr.write("Error: no command specified\n") 349 | parser.print_usage() 350 | sys.exit(1) 351 | 352 | SIPApplication.storage = FileStorage(options.config_directory or config_directory) 353 | thread_manager = ThreadManager() 354 | thread_manager.start() 355 | 356 | # execute the handlers 357 | try: 358 | if options.account: 359 | object = AccountConfigurator() 360 | else: 361 | object = SIPSimpleConfigurator() 362 | except ConfigurationError, e: 363 | sys.stderr.write("Failed to load sipclient's configuration: %s\n" % str(e)) 364 | sys.stderr.write("If an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script.\n") 365 | else: 366 | command, args = args[0], args[1:] 367 | handler = getattr(object, command, None) 368 | if handler is None or not callable(handler): 369 | sys.stderr.write("Error: illegal command: %s\n" % command) 370 | parser.print_usage() 371 | sys.exit(1) 372 | 373 | try: 374 | handler(*args) 375 | except TypeError: 376 | sys.stderr.write("Error: illegal usage of command %s\n" % command) 377 | parser.print_usage() 378 | sys.exit(1) 379 | finally: 380 | thread_manager.stop() 381 | -------------------------------------------------------------------------------- /sip-subscribe-mwi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import os 4 | import random 5 | import select 6 | import sys 7 | import termios 8 | 9 | from collections import deque 10 | from optparse import OptionParser 11 | from threading import Thread 12 | from time import time 13 | 14 | from application import log 15 | from application.notification import IObserver, NotificationCenter, NotificationData 16 | from application.python.queue import EventQueue 17 | from eventlib.twistedutil import join_reactor 18 | from twisted.internet import reactor 19 | from twisted.internet.error import ReactorNotRunning 20 | from zope.interface import implements 21 | 22 | from sipsimple.account import Account, AccountManager, BonjourAccount 23 | from sipsimple.application import SIPApplication 24 | from sipsimple.configuration import ConfigurationError, ConfigurationManager 25 | from sipsimple.configuration.settings import SIPSimpleSettings 26 | from sipsimple.core import ContactHeader, Engine, FromHeader, Header, Route, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader 27 | from sipsimple.lookup import DNSLookup 28 | from sipsimple.payloads.messagesummary import MessageSummary 29 | from sipsimple.storage import FileStorage 30 | from sipsimple.threading import run_in_twisted_thread 31 | 32 | from sipclient.configuration import config_directory 33 | from sipclient.configuration.account import AccountExtension 34 | from sipclient.configuration.settings import SIPSimpleSettingsExtension 35 | from sipclient.log import Logger 36 | 37 | 38 | class InputThread(Thread): 39 | def __init__(self, application): 40 | Thread.__init__(self) 41 | self.application = application 42 | self.daemon = True 43 | self._old_terminal_settings = None 44 | 45 | def run(self): 46 | notification_center = NotificationCenter() 47 | while True: 48 | for char in self._getchars(): 49 | if char == "\x04": 50 | self.application.stop() 51 | return 52 | else: 53 | notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char)) 54 | 55 | def stop(self): 56 | self._termios_restore() 57 | 58 | def _termios_restore(self): 59 | if self._old_terminal_settings is not None: 60 | termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) 61 | 62 | def _getchars(self): 63 | fd = sys.stdin.fileno() 64 | if os.isatty(fd): 65 | self._old_terminal_settings = termios.tcgetattr(fd) 66 | new = termios.tcgetattr(fd) 67 | new[3] = new[3] & ~termios.ICANON & ~termios.ECHO 68 | new[6][termios.VMIN] = '\000' 69 | try: 70 | termios.tcsetattr(fd, termios.TCSADRAIN, new) 71 | if select.select([fd], [], [], None)[0]: 72 | return sys.stdin.read(4192) 73 | finally: 74 | self._termios_restore() 75 | else: 76 | return os.read(fd, 4192) 77 | 78 | 79 | class SubscriptionApplication(object): 80 | implements(IObserver) 81 | 82 | def __init__(self, account_name, target, trace_sip, trace_pjsip, trace_notifications): 83 | self.account_name = account_name 84 | self.target = target 85 | self.input = InputThread(self) 86 | self.output = EventQueue(lambda event: sys.stdout.write(event+'\n')) 87 | self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications) 88 | self.success = False 89 | self.account = None 90 | self.subscription = None 91 | self.stopping = False 92 | 93 | self._subscription_routes = None 94 | self._subscription_timeout = 0.0 95 | self._subscription_wait = 0.5 96 | 97 | account_manager = AccountManager() 98 | engine = Engine() 99 | notification_center = NotificationCenter() 100 | notification_center.add_observer(self, sender=account_manager) 101 | notification_center.add_observer(self, sender=engine) 102 | notification_center.add_observer(self, sender=self.input) 103 | 104 | log.level.current = log.level.WARNING 105 | 106 | def run(self): 107 | account_manager = AccountManager() 108 | configuration = ConfigurationManager() 109 | engine = Engine() 110 | 111 | # start output thread 112 | self.output.start() 113 | 114 | # startup configuration 115 | Account.register_extension(AccountExtension) 116 | BonjourAccount.register_extension(AccountExtension) 117 | SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) 118 | SIPApplication.storage = FileStorage(config_directory) 119 | try: 120 | configuration.start() 121 | except ConfigurationError, e: 122 | raise RuntimeError("Failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e)) 123 | account_manager.load() 124 | if self.account_name is None: 125 | self.account = account_manager.default_account 126 | else: 127 | possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled] 128 | if len(possible_accounts) > 1: 129 | raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts)))) 130 | if len(possible_accounts) == 0: 131 | raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) 132 | self.account = possible_accounts[0] 133 | if self.account is None: 134 | raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts()))) 135 | elif self.account == BonjourAccount(): 136 | raise RuntimeError("Cannot use bonjour account for message summary subscription") 137 | elif not self.account.message_summary.enabled: 138 | raise RuntimeError("Message summary is not enabled for account %s" % self.account.id) 139 | for account in account_manager.iter_accounts(): 140 | if account == self.account: 141 | account.sip.register = False 142 | else: 143 | account.enabled = False 144 | self.output.put('Using account %s' % self.account.id) 145 | settings = SIPSimpleSettings() 146 | 147 | # start logging 148 | self.logger.start() 149 | 150 | # start the engine 151 | engine.start( 152 | auto_sound=False, 153 | events={'message-summary': ['application/simple-message-summary']}, 154 | udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None, 155 | tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None, 156 | tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None, 157 | tls_verify_server=self.account.tls.verify_server, 158 | tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None, 159 | tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, 160 | tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, 161 | user_agent=settings.user_agent, 162 | sample_rate=settings.audio.sample_rate, 163 | rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end), 164 | trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout, 165 | log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0 166 | ) 167 | 168 | if self.target is None: 169 | self.target = ToHeader(SIPURI(user=self.account.id.username, host=self.account.id.domain)) 170 | else: 171 | if '@' not in self.target: 172 | self.target = '%s@%s' % (self.target, self.account.id.domain) 173 | if not self.target.startswith('sip:') and not self.target.startswith('sips:'): 174 | self.target = 'sip:' + self.target 175 | try: 176 | self.target = ToHeader(SIPURI.parse(self.target)) 177 | except SIPCoreError: 178 | self.output.put('Illegal SIP URI: %s' % self.target) 179 | engine.stop() 180 | return 1 181 | self.output.put('Subscribing to %s for the message-summary event' % self.target.uri) 182 | 183 | # start the input thread 184 | self.input.start() 185 | 186 | reactor.callLater(0, self._subscribe) 187 | 188 | # start twisted 189 | try: 190 | reactor.run() 191 | finally: 192 | self.input.stop() 193 | 194 | # stop the output 195 | self.output.stop() 196 | self.output.join() 197 | 198 | self.logger.stop() 199 | 200 | return 0 if self.success else 1 201 | 202 | def stop(self): 203 | self.stopping = True 204 | if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'): 205 | self.subscription.end(timeout=1) 206 | else: 207 | engine = Engine() 208 | engine.stop() 209 | 210 | def print_help(self): 211 | message = 'Available control keys:\n' 212 | message += ' t: toggle SIP trace on the console\n' 213 | message += ' j: toggle PJSIP trace on the console\n' 214 | message += ' n: toggle notifications trace on the console\n' 215 | message += ' Ctrl-d: quit the program\n' 216 | message += ' ?: display this help message\n' 217 | self.output.put('\n'+message) 218 | 219 | def handle_notification(self, notification): 220 | handler = getattr(self, '_NH_%s' % notification.name, None) 221 | if handler is not None: 222 | handler(notification) 223 | 224 | def _NH_SIPSubscriptionDidStart(self, notification): 225 | route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) 226 | self._subscription_routes = None 227 | self._subscription_wait = 0.5 228 | self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport)) 229 | self.success = True 230 | 231 | def _NH_SIPSubscriptionChangedState(self, notification): 232 | route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) 233 | if notification.data.state.lower() == "pending": 234 | self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport)) 235 | elif notification.data.state.lower() == "active": 236 | self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport)) 237 | 238 | def _NH_SIPSubscriptionDidEnd(self, notification): 239 | notification_center = NotificationCenter() 240 | notification_center.remove_observer(self, sender=notification.sender) 241 | self.subscription = None 242 | route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) 243 | self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport)) 244 | self.stop() 245 | 246 | def _NH_SIPSubscriptionDidFail(self, notification): 247 | notification_center = NotificationCenter() 248 | notification_center.remove_observer(self, sender=notification.sender) 249 | self.subscription = None 250 | route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) 251 | if notification.data.code: 252 | status = ': %d %s' % (notification.data.code, notification.data.reason) 253 | else: 254 | status = ': %s' % notification.data.reason 255 | self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status)) 256 | if self.stopping or notification.data.code in (401, 403, 407) or self.success: 257 | self.success = False 258 | self.stop() 259 | else: 260 | if not self._subscription_routes or time() > self._subscription_timeout: 261 | self._subscription_wait = min(self._subscription_wait*2, 30) 262 | timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait) 263 | reactor.callFromThread(reactor.callLater, timeout, self._subscribe) 264 | else: 265 | route = self._subscription_routes.popleft() 266 | route_header = RouteHeader(route.uri) 267 | self.subscription = Subscription(self.target.uri, 268 | FromHeader(self.account.uri, self.account.display_name), 269 | self.target, 270 | ContactHeader(self.account.contact[route]), 271 | "message-summary", 272 | route_header, 273 | credentials=self.account.credentials, 274 | refresh=self.account.sip.subscribe_interval) 275 | notification_center.add_observer(self, sender=self.subscription) 276 | self.subscription.subscribe(extra_headers=[Header('Supported', 'eventlist')], timeout=5) 277 | 278 | def _NH_SIPSubscriptionGotNotify(self, notification): 279 | if notification.data.body: 280 | ms = MessageSummary.parse(notification.data.body) 281 | self.output.put('\nReceived NOTIFY:\n' + ms.to_string()) 282 | self.print_help() 283 | 284 | def _NH_DNSLookupDidSucceed(self, notification): 285 | # create subscription and register to get notifications from it 286 | self._subscription_routes = deque(notification.data.result) 287 | route = self._subscription_routes.popleft() 288 | route_header = RouteHeader(route.uri) 289 | self.subscription = Subscription(self.target.uri, 290 | FromHeader(self.account.uri, self.account.display_name), 291 | self.target, 292 | ContactHeader(self.account.contact[route]), 293 | "message-summary", 294 | route_header, 295 | credentials=self.account.credentials, 296 | refresh=self.account.sip.subscribe_interval) 297 | notification_center = NotificationCenter() 298 | notification_center.add_observer(self, sender=self.subscription) 299 | self.subscription.subscribe(extra_headers=[Header('Supported', 'eventlist')], timeout=5) 300 | 301 | def _NH_DNSLookupDidFail(self, notification): 302 | self.output.put('DNS lookup failed: %s' % notification.data.error) 303 | timeout = random.uniform(1.0, 2.0) 304 | reactor.callLater(timeout, self._subscribe) 305 | 306 | def _NH_SAInputWasReceived(self, notification): 307 | engine = Engine() 308 | settings = SIPSimpleSettings() 309 | key = notification.data.input 310 | if key == 't': 311 | self.logger.sip_to_stdout = not self.logger.sip_to_stdout 312 | engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip 313 | self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated')) 314 | elif key == 'j': 315 | self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout 316 | engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 317 | self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated')) 318 | elif key == 'n': 319 | self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout 320 | self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated')) 321 | elif key == '?': 322 | self.print_help() 323 | 324 | @run_in_twisted_thread 325 | def _NH_SIPEngineDidEnd(self, notification): 326 | self._stop_reactor() 327 | 328 | @run_in_twisted_thread 329 | def _NH_SIPEngineDidFail(self, notification): 330 | self.output.put('Engine failed.') 331 | self._stop_reactor() 332 | 333 | def _NH_SIPEngineGotException(self, notification): 334 | self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback) 335 | 336 | def _stop_reactor(self): 337 | try: 338 | reactor.stop() 339 | except ReactorNotRunning: 340 | pass 341 | 342 | def _subscribe(self): 343 | settings = SIPSimpleSettings() 344 | 345 | self._subscription_timeout = time()+30 346 | 347 | lookup = DNSLookup() 348 | notification_center = NotificationCenter() 349 | notification_center.add_observer(self, sender=lookup) 350 | if self.account.sip.outbound_proxy is not None: 351 | uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) 352 | elif self.account.sip.always_use_my_proxy: 353 | uri = SIPURI(host=self.account.id.domain) 354 | else: 355 | uri = self.target.uri 356 | lookup.lookup_sip_proxy(uri, settings.sip.transport_list) 357 | 358 | 359 | if __name__ == "__main__": 360 | description = "This script subscribes to the message summary event package for the specified SIP target. When a NOTIFY is received with the message summary information it will be displayed. The program will un-SUBSCRIBE and quit when CTRL+D is pressed." 361 | usage = "%prog [options] [target-user@target-domain.com]" 362 | parser = OptionParser(usage=usage, description=description) 363 | parser.print_usage = parser.print_help 364 | parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.") 365 | parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).") 366 | parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).") 367 | parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).") 368 | options, args = parser.parse_args() 369 | 370 | try: 371 | application = SubscriptionApplication(options.account_name, args[0] if args else None, options.trace_sip, options.trace_pjsip, options.trace_notifications) 372 | return_code = application.run() 373 | except RuntimeError, e: 374 | print "Error: %s" % str(e) 375 | sys.exit(1) 376 | except SIPCoreError, e: 377 | print "Error: %s" % str(e) 378 | sys.exit(1) 379 | else: 380 | sys.exit(return_code) 381 | -------------------------------------------------------------------------------- /sip-subscribe-rls: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import os 4 | import random 5 | import select 6 | import sys 7 | import termios 8 | 9 | from collections import deque 10 | from optparse import OptionParser 11 | from threading import Thread 12 | from time import time 13 | 14 | from application import log 15 | from application.notification import IObserver, NotificationCenter, NotificationData 16 | from application.python.queue import EventQueue 17 | from eventlib.twistedutil import join_reactor 18 | from twisted.internet import reactor 19 | from twisted.internet.error import ReactorNotRunning 20 | from zope.interface import implements 21 | 22 | from sipsimple.account import Account, AccountManager, BonjourAccount 23 | from sipsimple.application import SIPApplication 24 | from sipsimple.configuration import ConfigurationError, ConfigurationManager 25 | from sipsimple.configuration.settings import SIPSimpleSettings 26 | from sipsimple.core import ContactHeader, Engine, FromHeader, Header, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader, Route 27 | from sipsimple.lookup import DNSLookup 28 | from sipsimple.storage import FileStorage 29 | from sipsimple.threading import run_in_twisted_thread 30 | 31 | from sipclient.configuration import config_directory 32 | from sipclient.configuration.account import AccountExtension 33 | from sipclient.configuration.settings import SIPSimpleSettingsExtension 34 | from sipclient.log import Logger 35 | 36 | 37 | class InputThread(Thread): 38 | def __init__(self, application): 39 | Thread.__init__(self) 40 | self.application = application 41 | self.daemon = True 42 | self._old_terminal_settings = None 43 | 44 | def run(self): 45 | notification_center = NotificationCenter() 46 | while True: 47 | for char in self._getchars(): 48 | if char == "\x04": 49 | self.application.stop() 50 | return 51 | else: 52 | notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char)) 53 | 54 | def stop(self): 55 | self._termios_restore() 56 | 57 | def _termios_restore(self): 58 | if self._old_terminal_settings is not None: 59 | termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) 60 | 61 | def _getchars(self): 62 | fd = sys.stdin.fileno() 63 | if os.isatty(fd): 64 | self._old_terminal_settings = termios.tcgetattr(fd) 65 | new = termios.tcgetattr(fd) 66 | new[3] = new[3] & ~termios.ICANON & ~termios.ECHO 67 | new[6][termios.VMIN] = '\000' 68 | try: 69 | termios.tcsetattr(fd, termios.TCSADRAIN, new) 70 | if select.select([fd], [], [], None)[0]: 71 | return sys.stdin.read(4192) 72 | finally: 73 | self._termios_restore() 74 | else: 75 | return os.read(fd, 4192) 76 | 77 | 78 | class SubscriptionApplication(object): 79 | implements(IObserver) 80 | 81 | def __init__(self, account_name, target, trace_sip, trace_pjsip, trace_notifications): 82 | self.account_name = account_name 83 | self.target = target 84 | self.input = InputThread(self) 85 | self.output = EventQueue(lambda event: sys.stdout.write(event+'\n')) 86 | self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications) 87 | self.success = False 88 | self.account = None 89 | self.subscription = None 90 | self.stopping = False 91 | 92 | self._subscription_routes = None 93 | self._subscription_timeout = 0.0 94 | self._subscription_wait = 0.5 95 | 96 | account_manager = AccountManager() 97 | engine = Engine() 98 | notification_center = NotificationCenter() 99 | notification_center.add_observer(self, sender=account_manager) 100 | notification_center.add_observer(self, sender=engine) 101 | notification_center.add_observer(self, sender=self.input) 102 | 103 | log.level.current = log.level.WARNING 104 | 105 | def run(self): 106 | account_manager = AccountManager() 107 | configuration = ConfigurationManager() 108 | engine = Engine() 109 | 110 | # start output thread 111 | self.output.start() 112 | 113 | # startup configuration 114 | Account.register_extension(AccountExtension) 115 | BonjourAccount.register_extension(AccountExtension) 116 | SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) 117 | SIPApplication.storage = FileStorage(config_directory) 118 | try: 119 | configuration.start() 120 | except ConfigurationError, e: 121 | raise RuntimeError("Failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e)) 122 | account_manager.load() 123 | if self.account_name is None: 124 | self.account = account_manager.default_account 125 | else: 126 | possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled] 127 | if len(possible_accounts) > 1: 128 | raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts)))) 129 | if len(possible_accounts) == 0: 130 | raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) 131 | self.account = possible_accounts[0] 132 | if self.account is None: 133 | raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts()))) 134 | elif self.account == BonjourAccount(): 135 | raise RuntimeError("Cannot use bonjour account for presence subscription") 136 | elif not self.account.presence.enabled: 137 | raise RuntimeError("Presence is not enabled for account %s" % self.account.id) 138 | for account in account_manager.iter_accounts(): 139 | if account == self.account: 140 | account.sip.register = False 141 | else: 142 | account.enabled = False 143 | self.output.put('Using account %s' % self.account.id) 144 | settings = SIPSimpleSettings() 145 | 146 | # start logging 147 | self.logger.start() 148 | 149 | # start the engine 150 | engine.start( 151 | auto_sound=False, 152 | events={'presence': ['multipart/related', 'application/rlmi+xml', 'application/pidf+xml']}, 153 | udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None, 154 | tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None, 155 | tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None, 156 | tls_verify_server=self.account.tls.verify_server, 157 | tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None, 158 | tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, 159 | tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, 160 | user_agent=settings.user_agent, 161 | sample_rate=settings.audio.sample_rate, 162 | rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end), 163 | trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout, 164 | log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0 165 | ) 166 | 167 | if self.target is None: 168 | self.target = ToHeader(SIPURI(user='%s+presence' % self.account.id.username, host=self.account.id.domain)) 169 | else: 170 | if '@' not in self.target: 171 | self.target = '%s@%s' % (self.target, self.account.id.domain) 172 | if not self.target.startswith('sip:') and not self.target.startswith('sips:'): 173 | self.target = 'sip:' + self.target 174 | try: 175 | self.target = ToHeader(SIPURI.parse(self.target)) 176 | except SIPCoreError: 177 | self.output.put('Illegal SIP URI: %s' % self.target) 178 | engine.stop() 179 | return 1 180 | self.output.put('Subscribing to %s for the presence event' % self.target.uri) 181 | 182 | # start the input thread 183 | self.input.start() 184 | 185 | reactor.callLater(0, self._subscribe) 186 | 187 | # start twisted 188 | try: 189 | reactor.run() 190 | finally: 191 | self.input.stop() 192 | 193 | # stop the output 194 | self.output.stop() 195 | self.output.join() 196 | 197 | self.logger.stop() 198 | 199 | return 0 if self.success else 1 200 | 201 | def stop(self): 202 | self.stopping = True 203 | if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'): 204 | self.subscription.end(timeout=1) 205 | else: 206 | engine = Engine() 207 | engine.stop() 208 | 209 | def print_help(self): 210 | message = 'Available control keys:\n' 211 | message += ' t: toggle SIP trace on the console\n' 212 | message += ' j: toggle PJSIP trace on the console\n' 213 | message += ' n: toggle notifications trace on the console\n' 214 | message += ' Ctrl-d: quit the program\n' 215 | message += ' ?: display this help message\n' 216 | self.output.put('\n'+message) 217 | 218 | def handle_notification(self, notification): 219 | handler = getattr(self, '_NH_%s' % notification.name, None) 220 | if handler is not None: 221 | handler(notification) 222 | 223 | def _NH_SIPSubscriptionDidStart(self, notification): 224 | route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) 225 | self._subscription_routes = None 226 | self._subscription_wait = 0.5 227 | self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport)) 228 | self.success = True 229 | 230 | def _NH_SIPSubscriptionChangedState(self, notification): 231 | route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) 232 | if notification.data.state.lower() == "pending": 233 | self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport)) 234 | elif notification.data.state.lower() == "active": 235 | self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport)) 236 | 237 | def _NH_SIPSubscriptionDidEnd(self, notification): 238 | notification_center = NotificationCenter() 239 | notification_center.remove_observer(self, sender=notification.sender) 240 | self.subscription = None 241 | route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) 242 | self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport)) 243 | self.stop() 244 | 245 | def _NH_SIPSubscriptionDidFail(self, notification): 246 | notification_center = NotificationCenter() 247 | notification_center.remove_observer(self, sender=notification.sender) 248 | self.subscription = None 249 | route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) 250 | if notification.data.code: 251 | status = ': %d %s' % (notification.data.code, notification.data.reason) 252 | else: 253 | status = ': %s' % notification.data.reason 254 | self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status)) 255 | if self.stopping or notification.data.code in (401, 403, 407) or self.success: 256 | self.success = False 257 | self.stop() 258 | else: 259 | if not self._subscription_routes or time() > self._subscription_timeout: 260 | self._subscription_wait = min(self._subscription_wait*2, 30) 261 | timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait) 262 | reactor.callFromThread(reactor.callLater, timeout, self._subscribe) 263 | else: 264 | route = self._subscription_routes.popleft() 265 | route_header = RouteHeader(route.uri) 266 | self.subscription = Subscription(self.target.uri, 267 | FromHeader(self.account.uri, self.account.display_name), 268 | self.target, 269 | ContactHeader(self.account.contact[route]), 270 | "presence", 271 | route_header, 272 | credentials=self.account.credentials, 273 | refresh=self.account.sip.subscribe_interval) 274 | notification_center.add_observer(self, sender=self.subscription) 275 | self.subscription.subscribe(extra_headers=[Header('Supported', 'eventlist')], timeout=5) 276 | 277 | def _NH_SIPSubscriptionGotNotify(self, notification): 278 | if notification.data.body: 279 | self.output.put('Received NOTIFY:\n' + notification.data.body) 280 | self.print_help() 281 | 282 | def _NH_DNSLookupDidSucceed(self, notification): 283 | # create subscription and register to get notifications from it 284 | self._subscription_routes = deque(notification.data.result) 285 | route = self._subscription_routes.popleft() 286 | route_header = RouteHeader(route.uri) 287 | self.subscription = Subscription(self.target.uri, 288 | FromHeader(self.account.uri, self.account.display_name), 289 | self.target, 290 | ContactHeader(self.account.contact[route]), 291 | "presence", 292 | route_header, 293 | credentials=self.account.credentials, 294 | refresh=self.account.sip.subscribe_interval) 295 | notification_center = NotificationCenter() 296 | notification_center.add_observer(self, sender=self.subscription) 297 | self.subscription.subscribe(extra_headers=[Header('Supported', 'eventlist')], timeout=5) 298 | 299 | def _NH_DNSLookupDidFail(self, notification): 300 | self.output.put('DNS lookup failed: %s' % notification.data.error) 301 | timeout = random.uniform(1.0, 2.0) 302 | reactor.callLater(timeout, self._subscribe) 303 | 304 | def _NH_SAInputWasReceived(self, notification): 305 | engine = Engine() 306 | settings = SIPSimpleSettings() 307 | key = notification.data.input 308 | if key == 't': 309 | self.logger.sip_to_stdout = not self.logger.sip_to_stdout 310 | engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip 311 | self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated')) 312 | elif key == 'j': 313 | self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout 314 | engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 315 | self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated')) 316 | elif key == 'n': 317 | self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout 318 | self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated')) 319 | elif key == '?': 320 | self.print_help() 321 | 322 | @run_in_twisted_thread 323 | def _NH_SIPEngineDidEnd(self, notification): 324 | self._stop_reactor() 325 | 326 | @run_in_twisted_thread 327 | def _NH_SIPEngineDidFail(self, notification): 328 | self.output.put('Engine failed.') 329 | self._stop_reactor() 330 | 331 | def _NH_SIPEngineGotException(self, notification): 332 | self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback) 333 | 334 | def _stop_reactor(self): 335 | try: 336 | reactor.stop() 337 | except ReactorNotRunning: 338 | pass 339 | 340 | def _subscribe(self): 341 | settings = SIPSimpleSettings() 342 | 343 | self._subscription_timeout = time()+30 344 | 345 | lookup = DNSLookup() 346 | notification_center = NotificationCenter() 347 | notification_center.add_observer(self, sender=lookup) 348 | if self.account.sip.outbound_proxy is not None: 349 | uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) 350 | else: 351 | uri = self.target.uri 352 | lookup.lookup_sip_proxy(uri, settings.sip.transport_list) 353 | 354 | 355 | if __name__ == "__main__": 356 | description = "This script subscribes to the presence event package published by the specified SIP target assuming it is a resource list handled by a RLS server. The RLS server will then SUBSCRIBE in behalf of the account, collect NOTIFYs with the presence information of the recipients and provide periodically aggregated NOTIFYs back to the subscriber. If a target address is not specified, it will subscribe to the address 'username-buddies@domain.com', where username and domain are taken from the account's SIP address. It will then interprete PIDF bodies contained in NOTIFYs and display their meaning. The program will un-SUBSCRIBE and quit when CTRL+D is pressed." 357 | usage = "%prog [options] [target-user@target-domain.com]" 358 | parser = OptionParser(usage=usage, description=description) 359 | parser.print_usage = parser.print_help 360 | parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.") 361 | parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).") 362 | parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).") 363 | parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).") 364 | options, args = parser.parse_args() 365 | 366 | try: 367 | application = SubscriptionApplication(options.account_name, args[0] if args else None, options.trace_sip, options.trace_pjsip, options.trace_notifications) 368 | return_code = application.run() 369 | except RuntimeError, e: 370 | print "Error: %s" % str(e) 371 | sys.exit(1) 372 | except SIPCoreError, e: 373 | print "Error: %s" % str(e) 374 | sys.exit(1) 375 | else: 376 | sys.exit(return_code) 377 | -------------------------------------------------------------------------------- /sip-subscribe-winfo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import os 4 | import random 5 | import select 6 | import sys 7 | import termios 8 | 9 | from collections import deque 10 | from optparse import OptionParser 11 | from threading import Thread 12 | from time import time 13 | 14 | from application import log 15 | from application.notification import IObserver, NotificationCenter, NotificationData 16 | from application.python.queue import EventQueue 17 | from eventlib.twistedutil import join_reactor 18 | from twisted.internet import reactor 19 | from twisted.internet.error import ReactorNotRunning 20 | from zope.interface import implements 21 | 22 | from sipsimple.account import Account, AccountManager, BonjourAccount 23 | from sipsimple.application import SIPApplication 24 | from sipsimple.configuration import ConfigurationError, ConfigurationManager 25 | from sipsimple.configuration.settings import SIPSimpleSettings 26 | from sipsimple.core import ContactHeader, Engine, FromHeader, Route, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader 27 | from sipsimple.lookup import DNSLookup 28 | from sipsimple.payloads import ParserError 29 | from sipsimple.payloads.watcherinfo import WatcherInfoDocument 30 | from sipsimple.storage import FileStorage 31 | from sipsimple.threading import run_in_twisted_thread 32 | 33 | from sipclient.configuration import config_directory 34 | from sipclient.configuration.account import AccountExtension 35 | from sipclient.configuration.settings import SIPSimpleSettingsExtension 36 | from sipclient.log import Logger 37 | 38 | 39 | class InputThread(Thread): 40 | def __init__(self, application): 41 | Thread.__init__(self) 42 | self.application = application 43 | self.daemon = True 44 | self._old_terminal_settings = None 45 | 46 | def run(self): 47 | notification_center = NotificationCenter() 48 | while True: 49 | for char in self._getchars(): 50 | if char == "\x04": 51 | self.application.stop() 52 | return 53 | else: 54 | notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char)) 55 | 56 | def stop(self): 57 | self._termios_restore() 58 | 59 | def _termios_restore(self): 60 | if self._old_terminal_settings is not None: 61 | termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) 62 | 63 | def _getchars(self): 64 | fd = sys.stdin.fileno() 65 | if os.isatty(fd): 66 | self._old_terminal_settings = termios.tcgetattr(fd) 67 | new = termios.tcgetattr(fd) 68 | new[3] = new[3] & ~termios.ICANON & ~termios.ECHO 69 | new[6][termios.VMIN] = '\000' 70 | try: 71 | termios.tcsetattr(fd, termios.TCSADRAIN, new) 72 | if select.select([fd], [], [], None)[0]: 73 | return sys.stdin.read(4192) 74 | finally: 75 | self._termios_restore() 76 | else: 77 | return os.read(fd, 4192) 78 | 79 | 80 | class WinfoApplication(object): 81 | implements(IObserver) 82 | 83 | def __init__(self, account_name, trace_sip, trace_pjsip, trace_notifications): 84 | self.account_name = account_name 85 | self.input = InputThread(self) 86 | self.output = EventQueue(lambda event: sys.stdout.write(event+'\n')) 87 | self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications) 88 | self.success = False 89 | self.account = None 90 | self.subscription = None 91 | self.stopping = False 92 | 93 | self._subscription_routes = None 94 | self._subscription_timeout = 0.0 95 | self._subscription_wait = 0.5 96 | 97 | account_manager = AccountManager() 98 | engine = Engine() 99 | notification_center = NotificationCenter() 100 | notification_center.add_observer(self, sender=account_manager) 101 | notification_center.add_observer(self, sender=engine) 102 | notification_center.add_observer(self, sender=self.input) 103 | 104 | log.level.current = log.level.WARNING 105 | 106 | def run(self): 107 | account_manager = AccountManager() 108 | configuration = ConfigurationManager() 109 | engine = Engine() 110 | 111 | # start output thread 112 | self.output.start() 113 | 114 | # startup configuration 115 | Account.register_extension(AccountExtension) 116 | BonjourAccount.register_extension(AccountExtension) 117 | SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) 118 | SIPApplication.storage = FileStorage(config_directory) 119 | try: 120 | configuration.start() 121 | except ConfigurationError, e: 122 | raise RuntimeError("Failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e)) 123 | account_manager.load() 124 | if self.account_name is None: 125 | self.account = account_manager.default_account 126 | else: 127 | possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled] 128 | if len(possible_accounts) > 1: 129 | raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts)))) 130 | if len(possible_accounts) == 0: 131 | raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) 132 | self.account = possible_accounts[0] 133 | if self.account is None: 134 | raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts()))) 135 | elif self.account == BonjourAccount(): 136 | raise RuntimeError("Cannot use bonjour account for watcherinfo subscription") 137 | elif not self.account.presence.enabled: 138 | raise RuntimeError("Presence is not enabled for account %s" % self.account.id) 139 | elif not self.account.xcap.enabled: 140 | raise RuntimeError("XCAP is not enabled for account %s" % self.account.id) 141 | elif self.account.xcap.xcap_root is None: 142 | raise RuntimeError("XCAP root is not defined for account %s" % self.account.id) 143 | for account in account_manager.iter_accounts(): 144 | if account == self.account: 145 | account.sip.register = False 146 | else: 147 | account.enabled = False 148 | self.output.put('Using account %s' % self.account.id) 149 | settings = SIPSimpleSettings() 150 | 151 | # start logging 152 | self.logger.start() 153 | 154 | # start the engine 155 | engine.start( 156 | auto_sound=False, 157 | events={'presence.winfo': [WatcherInfoDocument.content_type]}, 158 | udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None, 159 | tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None, 160 | tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None, 161 | tls_verify_server=self.account.tls.verify_server, 162 | tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None, 163 | tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, 164 | tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, 165 | user_agent=settings.user_agent, 166 | sample_rate=settings.audio.sample_rate, 167 | rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end), 168 | trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout, 169 | log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0 170 | ) 171 | 172 | self.output.put('Subscribing to the presence.winfo event') 173 | 174 | # start the input thread 175 | self.input.start() 176 | 177 | reactor.callLater(0, self._subscribe) 178 | 179 | # start twisted 180 | try: 181 | reactor.run() 182 | finally: 183 | self.input.stop() 184 | 185 | # stop the output 186 | self.output.stop() 187 | self.output.join() 188 | 189 | self.logger.stop() 190 | 191 | return 0 if self.success else 1 192 | 193 | def stop(self): 194 | self.stopping = True 195 | if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'): 196 | self.subscription.end(timeout=1) 197 | else: 198 | engine = Engine() 199 | engine.stop() 200 | 201 | def print_help(self): 202 | message = 'Available control keys:\n' 203 | message += ' t: toggle SIP trace on the console\n' 204 | message += ' j: toggle PJSIP trace on the console\n' 205 | message += ' Ctrl-d: quit the program\n' 206 | message += ' ?: display this help message\n' 207 | self.output.put('\n'+message) 208 | 209 | def handle_notification(self, notification): 210 | handler = getattr(self, '_NH_%s' % notification.name, None) 211 | if handler is not None: 212 | handler(notification) 213 | 214 | def _NH_SIPSubscriptionDidStart(self, notification): 215 | route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) 216 | self._subscription_routes = None 217 | self._subscription_wait = 0.5 218 | self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport)) 219 | self.success = True 220 | 221 | def _NH_SIPSubscriptionChangedState(self, notification): 222 | route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) 223 | if notification.data.state.lower() == "pending": 224 | self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport)) 225 | elif notification.data.state.lower() == "active": 226 | self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport)) 227 | 228 | def _NH_SIPSubscriptionDidEnd(self, notification): 229 | notification_center = NotificationCenter() 230 | notification_center.remove_observer(self, sender=notification.sender) 231 | self.subscription = None 232 | route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) 233 | self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport)) 234 | self.stop() 235 | 236 | def _NH_SIPSubscriptionDidFail(self, notification): 237 | notification_center = NotificationCenter() 238 | notification_center.remove_observer(self, sender=notification.sender) 239 | self.subscription = None 240 | route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) 241 | if notification.data.code: 242 | status = ': %d %s' % (notification.data.code, notification.data.reason) 243 | else: 244 | status = ': %s' % notification.data.reason 245 | self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status)) 246 | if self.stopping or notification.data.code in (401, 403, 407) or self.success: 247 | self.success = False 248 | self.stop() 249 | else: 250 | if not self._subscription_routes or time() > self._subscription_timeout: 251 | self._subscription_wait = min(self._subscription_wait*2, 30) 252 | timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait) 253 | reactor.callFromThread(reactor.callLater, timeout, self._subscribe) 254 | else: 255 | route = self._subscription_routes.popleft() 256 | route_header = RouteHeader(route.uri) 257 | self.subscription = Subscription(self.account.uri, 258 | FromHeader(self.account.uri, self.account.display_name), 259 | ToHeader(self.account.uri, self.account.display_name), 260 | ContactHeader(self.account.contact[route]), 261 | "presence.winfo", 262 | route_header, 263 | credentials=self.account.credentials, 264 | refresh=self.account.sip.subscribe_interval) 265 | notification_center.add_observer(self, sender=self.subscription) 266 | self.subscription.subscribe(timeout=5) 267 | 268 | def _NH_SIPSubscriptionGotNotify(self, notification): 269 | if notification.data.content_type == WatcherInfoDocument.content_type: 270 | self._handle_winfo(notification.data.body) 271 | 272 | def _NH_DNSLookupDidSucceed(self, notification): 273 | # create subscription and register to get notifications from it 274 | self._subscription_routes = deque(notification.data.result) 275 | route = self._subscription_routes.popleft() 276 | route_header = RouteHeader(route.uri) 277 | self.subscription = Subscription(self.account.uri, 278 | FromHeader(self.account.uri, self.account.display_name), 279 | ToHeader(self.account.uri, self.account.display_name), 280 | ContactHeader(self.account.contact[route]), 281 | "presence.winfo", 282 | route_header, 283 | credentials=self.account.credentials, 284 | refresh=self.account.sip.subscribe_interval) 285 | notification_center = NotificationCenter() 286 | notification_center.add_observer(self, sender=self.subscription) 287 | self.subscription.subscribe(timeout=5) 288 | 289 | def _NH_DNSLookupDidFail(self, notification): 290 | self.output.put('DNS lookup failed: %s' % notification.data.error) 291 | timeout = random.uniform(1.0, 2.0) 292 | reactor.callLater(timeout, self._subscribe) 293 | 294 | def _NH_SAInputWasReceived(self, notification): 295 | engine = Engine() 296 | settings = SIPSimpleSettings() 297 | key = notification.data.input 298 | if key == 't': 299 | self.logger.sip_to_stdout = not self.logger.sip_to_stdout 300 | engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip 301 | self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated')) 302 | elif key == 'j': 303 | self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout 304 | engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 305 | self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated')) 306 | elif key == 'n': 307 | self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout 308 | self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated')) 309 | elif key == '?': 310 | self.print_help() 311 | 312 | @run_in_twisted_thread 313 | def _NH_SIPEngineDidEnd(self, notification): 314 | self._stop_reactor() 315 | 316 | @run_in_twisted_thread 317 | def _NH_SIPEngineDidFail(self, notification): 318 | self.output.put('Engine failed.') 319 | self._stop_reactor() 320 | 321 | def _NH_SIPEngineGotException(self, notification): 322 | self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback) 323 | 324 | def _stop_reactor(self): 325 | try: 326 | reactor.stop() 327 | except ReactorNotRunning: 328 | pass 329 | 330 | def _subscribe(self): 331 | settings = SIPSimpleSettings() 332 | 333 | self._subscription_timeout = time()+30 334 | 335 | lookup = DNSLookup() 336 | notification_center = NotificationCenter() 337 | notification_center.add_observer(self, sender=lookup) 338 | if self.account.sip.outbound_proxy is not None: 339 | uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) 340 | else: 341 | uri = SIPURI(host=self.account.id.domain) 342 | lookup.lookup_sip_proxy(uri, settings.sip.transport_list) 343 | 344 | def _handle_winfo(self, body): 345 | try: 346 | watcher_info = WatcherInfoDocument.parse(body) 347 | except ParserError, e: 348 | self.output.put("Got illegal winfo document: %s\n%s" % (str(e), body)) 349 | else: 350 | try: 351 | wlist = watcher_info['sip:' + self.account.id] 352 | except KeyError: 353 | self.output.put("Expected an entry for account %s in the winfo document" % self.account.id) 354 | else: 355 | buf = ["Received NOTIFY:", "----"] 356 | buf.append("Active watchers:") 357 | for watcher in wlist.active: 358 | buf.append(" %s" % watcher) 359 | buf.append("Terminated watchers:") 360 | for watcher in wlist.terminated: 361 | buf.append(" %s" % watcher) 362 | buf.append("Pending watchers:") 363 | for watcher in wlist.pending: 364 | buf.append(" %s" % watcher) 365 | buf.append("Waiting watchers:") 366 | for watcher in wlist.waiting: 367 | buf.append(" %s" % watcher) 368 | buf.append("----") 369 | self.output.put('\n'.join(buf)) 370 | 371 | 372 | if __name__ == "__main__": 373 | description = "This script subscribes to the presence.winfo event package and shows the received watcher info document's content. The program will un-SUBSCRIBE and quit when CTRL+D is pressed." 374 | usage = "%prog [options] [target-user@target-domain.com]" 375 | parser = OptionParser(usage=usage, description=description) 376 | parser.print_usage = parser.print_help 377 | parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.") 378 | parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).") 379 | parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).") 380 | parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).") 381 | options, args = parser.parse_args() 382 | 383 | try: 384 | application = WinfoApplication(options.account_name, options.trace_sip, options.trace_pjsip, options.trace_notifications) 385 | return_code = application.run() 386 | except RuntimeError, e: 387 | print "Error: %s" % str(e) 388 | sys.exit(1) 389 | except SIPCoreError, e: 390 | print "Error: %s" % str(e) 391 | sys.exit(1) 392 | else: 393 | sys.exit(return_code) 394 | -------------------------------------------------------------------------------- /sip-subscribe-xcap-diff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import os 4 | import random 5 | import select 6 | import sys 7 | import termios 8 | 9 | from collections import deque 10 | from optparse import OptionParser 11 | from threading import Thread 12 | from time import time 13 | 14 | from application import log 15 | from application.notification import IObserver, NotificationCenter, NotificationData 16 | from application.python.queue import EventQueue 17 | from eventlib.twistedutil import join_reactor 18 | from twisted.internet import reactor 19 | from twisted.internet.error import ReactorNotRunning 20 | from zope.interface import implements 21 | 22 | from sipsimple.account import Account, AccountManager, BonjourAccount 23 | from sipsimple.application import SIPApplication 24 | from sipsimple.configuration import ConfigurationError, ConfigurationManager 25 | from sipsimple.configuration.settings import SIPSimpleSettings 26 | from sipsimple.core import ContactHeader, Engine, FromHeader, Route, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader 27 | from sipsimple.lookup import DNSLookup 28 | from sipsimple.payloads import ParserError 29 | from sipsimple.payloads.xcapdiff import XCAPDiffDocument, Document, Element, Attribute 30 | from sipsimple.payloads.resourcelists import ResourceListsDocument, ResourceLists, List, Entry 31 | from sipsimple.storage import FileStorage 32 | from sipsimple.threading import run_in_twisted_thread 33 | 34 | from sipclient.configuration import config_directory 35 | from sipclient.configuration.account import AccountExtension 36 | from sipclient.configuration.settings import SIPSimpleSettingsExtension 37 | from sipclient.log import Logger 38 | 39 | 40 | class InputThread(Thread): 41 | def __init__(self, application): 42 | Thread.__init__(self) 43 | self.application = application 44 | self.daemon = True 45 | self._old_terminal_settings = None 46 | 47 | def run(self): 48 | notification_center = NotificationCenter() 49 | while True: 50 | for char in self._getchars(): 51 | if char == "\x04": 52 | self.application.stop() 53 | return 54 | else: 55 | notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char)) 56 | 57 | def stop(self): 58 | self._termios_restore() 59 | 60 | def _termios_restore(self): 61 | if self._old_terminal_settings is not None: 62 | termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) 63 | 64 | def _getchars(self): 65 | fd = sys.stdin.fileno() 66 | if os.isatty(fd): 67 | self._old_terminal_settings = termios.tcgetattr(fd) 68 | new = termios.tcgetattr(fd) 69 | new[3] = new[3] & ~termios.ICANON & ~termios.ECHO 70 | new[6][termios.VMIN] = '\000' 71 | try: 72 | termios.tcsetattr(fd, termios.TCSADRAIN, new) 73 | if select.select([fd], [], [], None)[0]: 74 | return sys.stdin.read(4192) 75 | finally: 76 | self._termios_restore() 77 | else: 78 | return os.read(fd, 4192) 79 | 80 | 81 | class SubscriptionApplication(object): 82 | implements(IObserver) 83 | 84 | def __init__(self, account_name, trace_sip, trace_pjsip, trace_notifications): 85 | self.account_name = account_name 86 | self.target = None 87 | self.input = InputThread(self) 88 | self.output = EventQueue(lambda event: sys.stdout.write(event+'\n')) 89 | self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications) 90 | self.success = False 91 | self.account = None 92 | self.subscription = None 93 | self.stopping = False 94 | self.body = None 95 | self.content_type = None 96 | 97 | self._subscription_routes = None 98 | self._subscription_timeout = 0.0 99 | self._subscription_wait = 0.5 100 | 101 | account_manager = AccountManager() 102 | engine = Engine() 103 | notification_center = NotificationCenter() 104 | notification_center.add_observer(self, sender=account_manager) 105 | notification_center.add_observer(self, sender=engine) 106 | notification_center.add_observer(self, sender=self.input) 107 | 108 | log.level.current = log.level.WARNING 109 | 110 | def run(self): 111 | account_manager = AccountManager() 112 | configuration = ConfigurationManager() 113 | engine = Engine() 114 | 115 | # start output thread 116 | self.output.start() 117 | 118 | # startup configuration 119 | Account.register_extension(AccountExtension) 120 | BonjourAccount.register_extension(AccountExtension) 121 | SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) 122 | SIPApplication.storage = FileStorage(config_directory) 123 | try: 124 | configuration.start() 125 | except ConfigurationError, e: 126 | raise RuntimeError("Failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e)) 127 | account_manager.load() 128 | if self.account_name is None: 129 | self.account = account_manager.default_account 130 | else: 131 | possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled] 132 | if len(possible_accounts) > 1: 133 | raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts)))) 134 | if len(possible_accounts) == 0: 135 | raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) 136 | self.account = possible_accounts[0] 137 | if self.account is None: 138 | raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts()))) 139 | elif not self.account.enabled: 140 | raise RuntimeError("Account %s is not enabled" % self.account.id) 141 | elif self.account == BonjourAccount(): 142 | raise RuntimeError("Cannot use bonjour account for presence subscription") 143 | elif not self.account.xcap.enabled: 144 | raise RuntimeError("XCAP is not enabled for account %s" % self.account.id) 145 | elif self.account.xcap.xcap_root is None: 146 | raise RuntimeError("XCAP root is not defined for account %s" % self.account.id) 147 | 148 | for account in account_manager.iter_accounts(): 149 | if account == self.account: 150 | account.sip.register = False 151 | else: 152 | account.enabled = False 153 | self.output.put('Using account %s' % self.account.id) 154 | settings = SIPSimpleSettings() 155 | 156 | # generate the body 157 | list = List() 158 | resource_lists = ResourceLists([list]) 159 | list.add(Entry('resource-lists/users/sip:%s/index' % self.account.id)) 160 | list.add(Entry('rls-services/users/sip:%s/index' % self.account.id)) 161 | list.add(Entry('pres-rules/users/sip:%s/index' % self.account.id)) 162 | self.body = resource_lists.toxml(pretty_print=True) 163 | self.content_type = ResourceListsDocument.content_type 164 | 165 | # start logging 166 | self.logger.start() 167 | 168 | # start the engine 169 | engine.start( 170 | auto_sound=False, 171 | events={'xcap-diff': ['application/xcap-diff+xml']}, 172 | udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None, 173 | tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None, 174 | tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None, 175 | tls_verify_server=self.account.tls.verify_server, 176 | tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None, 177 | tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, 178 | tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, 179 | user_agent=settings.user_agent, 180 | sample_rate=settings.audio.sample_rate, 181 | rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end), 182 | trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout, 183 | log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0 184 | ) 185 | 186 | self.target = ToHeader(SIPURI(user=self.account.id.username, host=self.account.id.domain)) 187 | self.output.put('Subscribing to %s for the xcap-diff event' % self.target.uri) 188 | 189 | # start the input thread 190 | self.input.start() 191 | 192 | reactor.callLater(0, self._subscribe) 193 | 194 | # start twisted 195 | try: 196 | reactor.run() 197 | finally: 198 | self.input.stop() 199 | 200 | # stop the output 201 | self.output.stop() 202 | self.output.join() 203 | 204 | self.logger.stop() 205 | 206 | return 0 if self.success else 1 207 | 208 | def stop(self): 209 | self.stopping = True 210 | if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'): 211 | self.subscription.end(timeout=1) 212 | else: 213 | engine = Engine() 214 | engine.stop() 215 | 216 | def print_help(self): 217 | message = 'Available control keys:\n' 218 | message += ' t: toggle SIP trace on the console\n' 219 | message += ' j: toggle PJSIP trace on the console\n' 220 | message += ' n: toggle notifications trace on the console\n' 221 | message += ' Ctrl-d: quit the program\n' 222 | message += ' ?: display this help message\n' 223 | self.output.put('\n'+message) 224 | 225 | def handle_notification(self, notification): 226 | handler = getattr(self, '_NH_%s' % notification.name, None) 227 | if handler is not None: 228 | handler(notification) 229 | 230 | def _NH_SIPSubscriptionDidStart(self, notification): 231 | route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) 232 | self._subscription_routes = None 233 | self._subscription_wait = 0.5 234 | self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport)) 235 | self.success = True 236 | 237 | def _NH_SIPSubscriptionChangedState(self, notification): 238 | route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) 239 | if notification.data.state.lower() == "pending": 240 | self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport)) 241 | elif notification.data.state.lower() == "active": 242 | self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport)) 243 | 244 | def _NH_SIPSubscriptionDidEnd(self, notification): 245 | notification_center = NotificationCenter() 246 | notification_center.remove_observer(self, sender=notification.sender) 247 | self.subscription = None 248 | route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) 249 | self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport)) 250 | self.stop() 251 | 252 | def _NH_SIPSubscriptionDidFail(self, notification): 253 | notification_center = NotificationCenter() 254 | notification_center.remove_observer(self, sender=notification.sender) 255 | self.subscription = None 256 | route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) 257 | if notification.data.code: 258 | status = ': %d %s' % (notification.data.code, notification.data.reason) 259 | else: 260 | status = ': %s' % notification.data.reason 261 | self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status)) 262 | if self.stopping or notification.data.code in (401, 403, 407, 489) or self.success: 263 | self.success = False 264 | self.stop() 265 | else: 266 | if not self._subscription_routes or time() > self._subscription_timeout: 267 | self._subscription_wait = min(self._subscription_wait*2, 30) 268 | timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait) 269 | reactor.callFromThread(reactor.callLater, timeout, self._subscribe) 270 | else: 271 | route = self._subscription_routes.popleft() 272 | route_header = RouteHeader(route.uri) 273 | self.subscription = Subscription(self.target.uri, 274 | FromHeader(self.account.uri, self.account.display_name), 275 | self.target, 276 | ContactHeader(self.account.contact[route]), 277 | "xcap-diff", 278 | route_header, 279 | credentials=self.account.credentials, 280 | refresh=self.account.sip.subscribe_interval) 281 | notification_center.add_observer(self, sender=self.subscription) 282 | self.subscription.subscribe(body=self.body, content_type=self.content_type, timeout=5) 283 | 284 | def _NH_SIPSubscriptionGotNotify(self, notification): 285 | if notification.data.content_type == XCAPDiffDocument.content_type: 286 | try: 287 | xcap_diff = XCAPDiffDocument.parse(notification.data.body) 288 | except ParserError, e: 289 | self.output.put("xcap-diff document is invalid: %s" % str(e)) 290 | else: 291 | self._display_xcapdiff(xcap_diff) 292 | self.print_help() 293 | 294 | def _NH_DNSLookupDidSucceed(self, notification): 295 | # create subscription and register to get notifications from it 296 | self._subscription_routes = deque(notification.data.result) 297 | route = self._subscription_routes.popleft() 298 | route_header = RouteHeader(route.uri) 299 | self.subscription = Subscription(self.target.uri, 300 | FromHeader(self.account.uri, self.account.display_name), 301 | self.target, 302 | ContactHeader(self.account.contact[route]), 303 | "xcap-diff", 304 | route_header, 305 | credentials=self.account.credentials, 306 | refresh=self.account.sip.subscribe_interval) 307 | notification_center = NotificationCenter() 308 | notification_center.add_observer(self, sender=self.subscription) 309 | self.subscription.subscribe(body=self.body, content_type=self.content_type, timeout=5) 310 | 311 | def _NH_DNSLookupDidFail(self, notification): 312 | self.output.put('DNS lookup failed: %s' % notification.data.error) 313 | timeout = random.uniform(1.0, 2.0) 314 | reactor.callLater(timeout, self._subscribe) 315 | 316 | def _NH_SAInputWasReceived(self, notification): 317 | engine = Engine() 318 | settings = SIPSimpleSettings() 319 | key = notification.data.input 320 | if key == 't': 321 | self.logger.sip_to_stdout = not self.logger.sip_to_stdout 322 | engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip 323 | self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated')) 324 | elif key == 'j': 325 | self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout 326 | engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 327 | self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated')) 328 | elif key == 'n': 329 | self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout 330 | self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated')) 331 | elif key == '?': 332 | self.print_help() 333 | 334 | @run_in_twisted_thread 335 | def _NH_SIPEngineDidEnd(self, notification): 336 | self._stop_reactor() 337 | 338 | @run_in_twisted_thread 339 | def _NH_SIPEngineDidFail(self, notification): 340 | self.output.put('Engine failed.') 341 | self._stop_reactor() 342 | 343 | def _NH_SIPEngineGotException(self, notification): 344 | self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback) 345 | 346 | def _stop_reactor(self): 347 | try: 348 | reactor.stop() 349 | except ReactorNotRunning: 350 | pass 351 | 352 | def _subscribe(self): 353 | settings = SIPSimpleSettings() 354 | 355 | self._subscription_timeout = time()+30 356 | 357 | lookup = DNSLookup() 358 | notification_center = NotificationCenter() 359 | notification_center.add_observer(self, sender=lookup) 360 | if self.account.sip.outbound_proxy is not None: 361 | uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) 362 | else: 363 | uri = self.target.uri 364 | lookup.lookup_sip_proxy(uri, settings.sip.transport_list) 365 | 366 | def _display_xcapdiff(self, xcap_diff): 367 | message = [] 368 | message.append('XCAP diff for XCAP root %s' % xcap_diff.xcap_root) 369 | for child in xcap_diff: 370 | if isinstance(child, Document): 371 | message.append(' %s document %s for AUID %s changed' % ('Global' if child.selector.globaltree is not None else "User's %s" % child.selector.userstree, child.selector.document, child.selector.auid)) 372 | message.append(' URL: %s' % child.selector) 373 | if child.previous_etag: 374 | message.append(' Previous ETag: %s' % child.previous_etag) 375 | if child.new_etag: 376 | message.append(' New ETag: %s' % child.new_etag) 377 | if child.empty_body: 378 | message.append(' Body did not change') 379 | elif isinstance(child, Element): 380 | message.append(' %s element %s in document %s for AUID %s changed' % ('Global' if child.selector.globaltree is not None else "User's %s" % child.selector.userstree, child.selector.node, child.selector.document, child.selector.auid)) 381 | message.append(' URL: %s/%s' % (xcap_diff.xcap_root, child.selector)) 382 | elif isinstance(child, Attribute): 383 | message.append(' %s attribute %s in document %s for AUID %s changed' % ('Global' if child.selector.globaltree is not None else "User's %s" % child.selector.userstree, child.selector.node, child.selector.document, child.selector.auid)) 384 | message.append(' URL: %s/%s' % (xcap_diff.xcap_root, child.selector)) 385 | if child.value: 386 | message.append(' New value: %s' % child.value) 387 | self.output.put('\n'.join(message)) 388 | 389 | 390 | if __name__ == "__main__": 391 | description = "This script subscribes to the xcap-diff event package for the given SIP account. The program will un-SUBSCRIBE and quit when CTRL+D is pressed." 392 | usage = "%prog [options]" 393 | parser = OptionParser(usage=usage, description=description) 394 | parser.print_usage = parser.print_help 395 | parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.") 396 | parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).") 397 | parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).") 398 | parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).") 399 | options, args = parser.parse_args() 400 | 401 | try: 402 | application = SubscriptionApplication(options.account_name, options.trace_sip, options.trace_pjsip, options.trace_notifications) 403 | return_code = application.run() 404 | except RuntimeError, e: 405 | print "Error: %s" % str(e) 406 | sys.exit(1) 407 | except SIPCoreError, e: 408 | print "Error: %s" % str(e) 409 | sys.exit(1) 410 | else: 411 | sys.exit(return_code) 412 | -------------------------------------------------------------------------------- /sipclient/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = '3.5.6' 3 | -------------------------------------------------------------------------------- /sipclient/configuration/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | """SIP SIMPLE Client configuration""" 3 | 4 | __all__ = ['config_directory'] 5 | 6 | import os 7 | 8 | 9 | config_directory = os.path.expanduser('~/.sipclient') 10 | 11 | 12 | -------------------------------------------------------------------------------- /sipclient/configuration/account.py: -------------------------------------------------------------------------------- 1 | 2 | """SIP SIMPLE Client account settings extensions""" 3 | 4 | __all__ = ['AccountExtension'] 5 | 6 | from sipsimple.configuration import Setting, SettingsGroup, SettingsObjectExtension 7 | from sipsimple.account import RTPSettings 8 | 9 | from sipclient.configuration.datatypes import AccountSoundFile 10 | 11 | 12 | class RTPSettingsExtension(RTPSettings): 13 | inband_dtmf = Setting(type=bool, default=False) 14 | 15 | 16 | class SoundsSettings(SettingsGroup): 17 | audio_inbound = Setting(type=AccountSoundFile, default=AccountSoundFile(AccountSoundFile.DefaultSoundFile('sounds.audio_inbound')), nillable=True) 18 | 19 | 20 | class AccountExtension(SettingsObjectExtension): 21 | rtp = RTPSettingsExtension 22 | sounds = SoundsSettings 23 | 24 | 25 | -------------------------------------------------------------------------------- /sipclient/configuration/datatypes.py: -------------------------------------------------------------------------------- 1 | 2 | """Definitions of datatypes for use in settings extensions""" 3 | 4 | __all__ = ['ResourcePath', 'UserDataPath', 'SoundFile', 'AccountSoundFile'] 5 | 6 | import os 7 | import sys 8 | import urlparse 9 | 10 | from application.python.descriptor import classproperty, WriteOnceAttribute 11 | from sipsimple.configuration.datatypes import Hostname 12 | 13 | ## Path datatypes 14 | 15 | class ResourcePath(object): 16 | def __init__(self, path): 17 | if not isinstance(path, unicode): 18 | path = path.decode(sys.getfilesystemencoding()) 19 | self.path = os.path.normpath(path) 20 | 21 | def __getstate__(self): 22 | return unicode(self.path) 23 | 24 | def __setstate__(self, state): 25 | self.__init__(state) 26 | 27 | @property 28 | def normalized(self): 29 | path = os.path.expanduser(self.path) 30 | if os.path.isabs(path): 31 | return os.path.realpath(path) 32 | return os.path.realpath(os.path.join(self.resources_directory, path)) 33 | 34 | @classproperty 35 | def resources_directory(cls): 36 | binary_directory = os.path.dirname(os.path.realpath(sys.argv[0])) 37 | if os.path.basename(binary_directory) == 'bin': 38 | application_directory = os.path.dirname(binary_directory) 39 | else: 40 | application_directory = binary_directory 41 | from sipsimple.configuration.settings import SIPSimpleSettings 42 | settings = SIPSimpleSettings() 43 | if os.path.basename(binary_directory) == 'bin': 44 | resources_component = settings.resources_directory or 'share/sipclients' 45 | else: 46 | resources_component = settings.resources_directory or 'resources' 47 | return os.path.realpath(os.path.join(application_directory, resources_component)) 48 | 49 | def __eq__(self, other): 50 | try: 51 | return self.path == other.path 52 | except AttributeError: 53 | return False 54 | 55 | def __hash__(self): 56 | return hash(self.path) 57 | 58 | def __repr__(self): 59 | return '%s(%r)' % (self.__class__.__name__, self.path) 60 | 61 | def __unicode__(self): 62 | return unicode(self.path) 63 | 64 | 65 | class UserDataPath(object): 66 | def __init__(self, path): 67 | if not isinstance(path, unicode): 68 | path = path.decode(sys.getfilesystemencoding()) 69 | self.path = os.path.normpath(path) 70 | 71 | def __getstate__(self): 72 | return unicode(self.path) 73 | 74 | def __setstate__(self, state): 75 | self.__init__(state) 76 | 77 | @property 78 | def normalized(self): 79 | path = os.path.expanduser(self.path) 80 | if os.path.isabs(path): 81 | return path 82 | from sipsimple.configuration.settings import SIPSimpleSettings 83 | settings = SIPSimpleSettings() 84 | return os.path.realpath(os.path.join(settings.user_data_directory, path)) 85 | 86 | def __eq__(self, other): 87 | try: 88 | return self.path == other.path 89 | except AttributeError: 90 | return False 91 | 92 | def __hash__(self): 93 | return hash(self.path) 94 | 95 | def __repr__(self): 96 | return '%s(%r)' % (self.__class__.__name__, self.path) 97 | 98 | def __unicode__(self): 99 | return unicode(self.path) 100 | 101 | 102 | class SoundFile(object): 103 | def __init__(self, path, volume=100): 104 | self.path = ResourcePath(path) 105 | self.volume = int(volume) 106 | if self.volume < 0 or self.volume > 100: 107 | raise ValueError("illegal volume level: %d" % self.volume) 108 | 109 | def __getstate__(self): 110 | return u'%s,%s' % (self.path.__getstate__(), self.volume) 111 | 112 | def __setstate__(self, state): 113 | try: 114 | path, volume = state.rsplit(u',', 1) 115 | except ValueError: 116 | self.__init__(state) 117 | else: 118 | self.__init__(path, volume) 119 | 120 | def __repr__(self): 121 | return '%s(%r, %r)' % (self.__class__.__name__, self.path, self.volume) 122 | 123 | def __unicode__(self): 124 | return u'%s,%d' % (self.path, self.volume) 125 | 126 | 127 | class AccountSoundFile(object): 128 | class DefaultSoundFile(object): 129 | def __init__(self, setting): 130 | self.setting = setting 131 | def __repr__(self): 132 | return 'AccountSoundFile.DefaultSoundFile(%s)' % self.setting 133 | __str__ = __repr__ 134 | 135 | def __init__(self, sound_file, *args, **kwargs): 136 | if isinstance(sound_file, self.DefaultSoundFile): 137 | self._sound_file = sound_file 138 | if args or kwargs: 139 | raise ValueError("other parameters cannot be specified if sound file is instance of DefaultSoundFile") 140 | else: 141 | self._sound_file = SoundFile(sound_file, *args, **kwargs) 142 | 143 | def __getstate__(self): 144 | if isinstance(self._sound_file, self.DefaultSoundFile): 145 | return u'default:%s' % self._sound_file.setting 146 | else: 147 | return u'file:%s' % self._sound_file.__getstate__() 148 | 149 | def __setstate__(self, state): 150 | type, value = state.split(u':', 1) 151 | if type == u'default': 152 | self._sound_file = self.DefaultSoundFile(value) 153 | elif type == u'file': 154 | self._sound_file = SoundFile.__new__(SoundFile) 155 | self._sound_file.__setstate__(value) 156 | 157 | @property 158 | def sound_file(self): 159 | if isinstance(self._sound_file, self.DefaultSoundFile): 160 | from sipsimple.configuration.settings import SIPSimpleSettings 161 | setting = SIPSimpleSettings() 162 | for comp in self._sound_file.setting.split('.'): 163 | setting = getattr(setting, comp) 164 | return setting 165 | else: 166 | return self._sound_file 167 | 168 | def __repr__(self): 169 | if isinstance(self._sound_file, self.DefaultSoundFile): 170 | return '%s(%r)' % (self.__class__.__name__, self._sound_file) 171 | else: 172 | return '%s(%r, volume=%d)' % (self.__class__.__name__, self._sound_file.path, self._sound_file.volume) 173 | 174 | def __unicode__(self): 175 | if isinstance(self._sound_file, self.DefaultSoundFile): 176 | return u'DEFAULT' 177 | else: 178 | return u'%s,%d' % (self._sound_file.path, self._sound_file.volume) 179 | 180 | 181 | class HTTPURL(object): 182 | url = WriteOnceAttribute() 183 | 184 | def __init__(self, value): 185 | url = urlparse.urlparse(value) 186 | if url.scheme not in (u'http', u'https'): 187 | raise ValueError(NSLocalizedString("Illegal HTTP URL scheme (http and https only): %s", "Preference option error") % url.scheme) 188 | # check port and hostname 189 | Hostname(url.hostname) 190 | if url.port is not None: 191 | if not (0 < url.port < 65536): 192 | raise ValueError(NSLocalizedString("Illegal port value: %d", "Preference option error") % url.port) 193 | self.url = url 194 | 195 | def __getstate__(self): 196 | return unicode(self.url.geturl()) 197 | 198 | def __setstate__(self, state): 199 | self.__init__(state) 200 | 201 | def __getitem__(self, index): 202 | return self.url.__getitem__(index) 203 | 204 | def __getattr__(self, attr): 205 | if attr in ('scheme', 'netloc', 'path', 'params', 'query', 'fragment', 'username', 'password', 'hostname', 'port'): 206 | return getattr(self.url, attr) 207 | else: 208 | raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, attr)) 209 | 210 | def __unicode__(self): 211 | return unicode(self.url.geturl()) 212 | 213 | -------------------------------------------------------------------------------- /sipclient/configuration/settings.py: -------------------------------------------------------------------------------- 1 | 2 | """SIP SIMPLE Client settings extensions""" 3 | 4 | __all__ = ['SIPSimpleSettingsExtension'] 5 | 6 | import os 7 | 8 | from sipsimple.configuration import Setting, SettingsGroup, SettingsObjectExtension 9 | from sipsimple.configuration.datatypes import Path 10 | from sipsimple.configuration.settings import AudioSettings, LogsSettings 11 | 12 | 13 | from sipclient.configuration.datatypes import SoundFile, UserDataPath, HTTPURL 14 | 15 | 16 | class AudioSettingsExtension(AudioSettings): 17 | directory = Setting(type=UserDataPath, default=UserDataPath('history')) 18 | 19 | 20 | class LogsSettingsExtension(LogsSettings): 21 | directory = Setting(type=UserDataPath, default=UserDataPath('logs')) 22 | trace_notifications = Setting(type=bool, default=False) 23 | 24 | 25 | class SoundsSettings(SettingsGroup): 26 | audio_inbound = Setting(type=SoundFile, default=SoundFile('sounds/ring_inbound.wav'), nillable=True) 27 | audio_outbound = Setting(type=SoundFile, default=SoundFile('sounds/ring_outbound.wav'), nillable=True) 28 | message_received = Setting(type=SoundFile, default=SoundFile('sounds/message_received.wav'), nillable=True) 29 | message_sent = Setting(type=SoundFile, default=SoundFile('sounds/message_sent.wav'), nillable=True) 30 | file_received = Setting(type=SoundFile, default=SoundFile('sounds/file_received.wav'), nillable=True) 31 | file_sent = Setting(type=SoundFile, default=SoundFile('sounds/file_sent.wav'), nillable=True) 32 | 33 | 34 | class EnrollmentSettings(SettingsGroup): 35 | default_domain = Setting(type=str, default='sip2sip.info', nillable=False) 36 | url = Setting(type=HTTPURL, default="https://blink.sipthor.net/enrollment.phtml", nillable=True) 37 | 38 | 39 | class SIPSimpleSettingsExtension(SettingsObjectExtension): 40 | user_data_directory = Setting(type=Path, default=Path(os.path.expanduser('~/.sipclient'))) 41 | resources_directory = Setting(type=Path, default=None, nillable=True) 42 | 43 | audio = AudioSettingsExtension 44 | logs = LogsSettingsExtension 45 | sounds = SoundsSettings 46 | enrollment = EnrollmentSettings 47 | 48 | -------------------------------------------------------------------------------- /sipclient/log.py: -------------------------------------------------------------------------------- 1 | 2 | """Logging support for SIP SIMPLE Client""" 3 | 4 | __all__ = ["Logger"] 5 | 6 | import datetime 7 | import os 8 | import sys 9 | 10 | from pprint import pformat 11 | 12 | from application import log 13 | from application.notification import IObserver, NotificationCenter 14 | from application.python.queue import EventQueue 15 | from application.system import makedirs 16 | from zope.interface import implements 17 | 18 | from sipsimple.configuration.settings import SIPSimpleSettings 19 | 20 | 21 | class Logger(object): 22 | implements(IObserver) 23 | 24 | # public methods 25 | # 26 | 27 | def __init__(self, sip_to_stdout=False, msrp_to_stdout=False, pjsip_to_stdout=False, notifications_to_stdout=False, msrp_level=log.level.ERROR): 28 | self.sip_to_stdout = sip_to_stdout 29 | self.msrp_to_stdout = msrp_to_stdout 30 | self.pjsip_to_stdout = pjsip_to_stdout 31 | self.notifications_to_stdout = notifications_to_stdout 32 | self.msrp_level = msrp_level 33 | 34 | self._siptrace_filename = None 35 | self._siptrace_file = None 36 | self._siptrace_error = False 37 | self._siptrace_start_time = None 38 | self._siptrace_packet_count = 0 39 | 40 | self._msrptrace_filename = None 41 | self._msrptrace_file = None 42 | self._msrptrace_error = False 43 | 44 | self._pjsiptrace_filename = None 45 | self._pjsiptrace_file = None 46 | self._pjsiptrace_error = False 47 | 48 | self._notifications_filename = None 49 | self._notifications_file = None 50 | self._notifications_error = False 51 | 52 | self._event_queue = EventQueue(handler=self._process_notification, name='Log handling') 53 | self._log_directory_error = False 54 | 55 | def start(self): 56 | # try to create the log directory 57 | try: 58 | self._init_log_directory() 59 | except Exception: 60 | pass 61 | 62 | # register to receive log notifications 63 | notification_center = NotificationCenter() 64 | notification_center.add_observer(self) 65 | 66 | # start the thread processing the notifications 67 | self._event_queue.start() 68 | 69 | def stop(self): 70 | # stop the thread processing the notifications 71 | self._event_queue.stop() 72 | self._event_queue.join() 73 | 74 | # close sip trace file 75 | if self._siptrace_file is not None: 76 | self._siptrace_file.close() 77 | self._siptrace_file = None 78 | 79 | # close msrp trace file 80 | if self._msrptrace_file is not None: 81 | self._msrptrace_file.close() 82 | self._msrptrace_file = None 83 | 84 | # close pjsip trace file 85 | if self._pjsiptrace_file is not None: 86 | self._pjsiptrace_file.close() 87 | self._pjsiptrace_file = None 88 | 89 | # close notifications trace file 90 | if self._notifications_file is not None: 91 | self._notifications_file.close() 92 | self._notifications_file = None 93 | 94 | # unregister from receiving notifications 95 | notification_center = NotificationCenter() 96 | notification_center.remove_observer(self) 97 | 98 | def handle_notification(self, notification): 99 | self._event_queue.put(notification) 100 | 101 | def _process_notification(self, notification): 102 | settings = SIPSimpleSettings() 103 | handler = getattr(self, '_NH_%s' % notification.name, None) 104 | if handler is not None: 105 | handler(notification) 106 | 107 | handler = getattr(self, '_LH_%s' % notification.name, None) 108 | if handler is not None: 109 | handler(notification) 110 | 111 | if notification.name not in ('SIPEngineLog', 'SIPEngineSIPTrace') and (self.notifications_to_stdout or settings.logs.trace_notifications): 112 | message = 'Notification name=%s sender=%s' % (notification.name, notification.sender) 113 | if notification.data is not None: 114 | message += '\n%s' % pformat(notification.data.__dict__) 115 | if self.notifications_to_stdout: 116 | print '%s: %s' % (datetime.datetime.now(), message) 117 | if settings.logs.trace_notifications: 118 | try: 119 | self._init_log_file('notifications') 120 | except Exception: 121 | pass 122 | else: 123 | self._notifications_file.write('%s [%s %d]: %s\n' % (datetime.datetime.now(), os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) 124 | self._notifications_file.flush() 125 | 126 | # notification handlers 127 | # 128 | 129 | def _NH_CFGSettingsObjectDidChange(self, notification): 130 | settings = SIPSimpleSettings() 131 | if notification.sender is settings: 132 | if 'logs.directory' in notification.data.modified: 133 | # sip trace 134 | if self._siptrace_file is not None: 135 | self._siptrace_file.close() 136 | self._siptrace_file = None 137 | # pjsip trace 138 | if self._pjsiptrace_file is not None: 139 | self._pjsiptrace_file.close() 140 | self._pjsiptrace_file = None 141 | # notifications trace 142 | if self._notifications_file is not None: 143 | self._notifications_file.close() 144 | self._notifications_file = None 145 | # try to create the log directory 146 | try: 147 | self._init_log_directory() 148 | except Exception: 149 | pass 150 | 151 | # log handlers 152 | # 153 | 154 | def _LH_SIPEngineSIPTrace(self, notification): 155 | settings = SIPSimpleSettings() 156 | if not self.sip_to_stdout and not settings.logs.trace_sip: 157 | return 158 | if self._siptrace_start_time is None: 159 | self._siptrace_start_time = notification.datetime 160 | self._siptrace_packet_count += 1 161 | if notification.data.received: 162 | direction = "RECEIVED" 163 | else: 164 | direction = "SENDING" 165 | buf = ["%s: Packet %d, +%s" % (direction, self._siptrace_packet_count, (notification.datetime - self._siptrace_start_time))] 166 | buf.append("%(source_ip)s:%(source_port)d -(SIP over %(transport)s)-> %(destination_ip)s:%(destination_port)d" % notification.data.__dict__) 167 | buf.append(notification.data.data) 168 | buf.append('--') 169 | message = '\n'.join(buf) 170 | if self.sip_to_stdout: 171 | print '%s: %s\n' % (notification.datetime, message) 172 | if settings.logs.trace_sip: 173 | try: 174 | self._init_log_file('siptrace') 175 | except Exception: 176 | pass 177 | else: 178 | self._siptrace_file.write('%s [%s %d]: %s\n' % (notification.datetime, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) 179 | self._siptrace_file.flush() 180 | 181 | def _LH_SIPEngineLog(self, notification): 182 | settings = SIPSimpleSettings() 183 | if not self.pjsip_to_stdout and not settings.logs.trace_pjsip: 184 | return 185 | message = "(%(level)d) %(message)s" % notification.data.__dict__ 186 | if self.pjsip_to_stdout: 187 | print message 188 | if settings.logs.trace_pjsip: 189 | try: 190 | self._init_log_file('pjsiptrace') 191 | except Exception: 192 | pass 193 | else: 194 | self._pjsiptrace_file.write('[%s %d] %s\n' % (os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) 195 | self._pjsiptrace_file.flush() 196 | 197 | def _LH_DNSLookupTrace(self, notification): 198 | settings = SIPSimpleSettings() 199 | if not self.sip_to_stdout and not settings.logs.trace_sip: 200 | return 201 | message = 'DNS lookup %(query_type)s %(query_name)s' % notification.data.__dict__ 202 | if notification.data.error is None: 203 | message += ' succeeded, ttl=%d: ' % notification.data.answer.ttl 204 | if notification.data.query_type == 'A': 205 | message += ", ".join(record.address for record in notification.data.answer) 206 | elif notification.data.query_type == 'SRV': 207 | message += ", ".join('%d %d %d %s' % (record.priority, record.weight, record.port, record.target) for record in notification.data.answer) 208 | elif notification.data.query_type == 'NAPTR': 209 | message += ", ".join('%d %d "%s" "%s" "%s" %s' % (record.order, record.preference, record.flags, record.service, record.regexp, record.replacement) for record in notification.data.answer) 210 | else: 211 | import dns.resolver 212 | message_map = {dns.resolver.NXDOMAIN: 'DNS record does not exist', 213 | dns.resolver.NoAnswer: 'DNS response contains no answer', 214 | dns.resolver.NoNameservers: 'no DNS name servers could be reached', 215 | dns.resolver.Timeout: 'no DNS response received, the query has timed out'} 216 | message += ' failed: %s' % message_map.get(notification.data.error.__class__, '') 217 | if self.sip_to_stdout: 218 | print '%s: %s' % (notification.datetime, message) 219 | if settings.logs.trace_sip: 220 | try: 221 | self._init_log_file('siptrace') 222 | except Exception: 223 | pass 224 | else: 225 | self._siptrace_file.write('%s [%s %d]: %s\n' % (notification.datetime, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) 226 | self._siptrace_file.flush() 227 | 228 | def _LH_MSRPTransportTrace(self, notification): 229 | settings = SIPSimpleSettings() 230 | if not self.msrp_to_stdout and not settings.logs.trace_msrp: 231 | return 232 | if getattr(notification.sender, 'socket', None) is None: 233 | return 234 | arrow = {'incoming': '<--', 'outgoing': '-->'}[notification.data.direction] 235 | local_address = notification.sender.getHost() 236 | local_address = '%s:%d' % (local_address.host, local_address.port) 237 | remote_address = notification.sender.getPeer() 238 | remote_address = '%s:%d' % (remote_address.host, remote_address.port) 239 | message = '%s %s %s\n' % (local_address, arrow, remote_address) + notification.data.data 240 | if self.msrp_to_stdout: 241 | print '%s: %s' % (notification.datetime, message) 242 | if settings.logs.trace_msrp: 243 | try: 244 | self._init_log_file('msrptrace') 245 | except Exception: 246 | pass 247 | else: 248 | self._msrptrace_file.write('%s [%s %d]: %s\n' % (notification.datetime, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) 249 | self._msrptrace_file.flush() 250 | 251 | def _LH_MSRPLibraryLog(self, notification): 252 | settings = SIPSimpleSettings() 253 | if not self.msrp_to_stdout and not settings.logs.trace_msrp: 254 | return 255 | if notification.data.level < self.msrp_level: 256 | return 257 | message = '%s%s' % (notification.data.level.prefix, notification.data.message) 258 | if self.msrp_to_stdout: 259 | print '%s: %s' % (notification.datetime, message) 260 | if settings.logs.trace_msrp: 261 | try: 262 | self._init_log_file('msrptrace') 263 | except Exception: 264 | pass 265 | else: 266 | self._msrptrace_file.write('%s [%s %d]: %s\n' % (notification.datetime, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) 267 | self._msrptrace_file.flush() 268 | 269 | # private methods 270 | # 271 | 272 | def _init_log_directory(self): 273 | settings = SIPSimpleSettings() 274 | log_directory = settings.logs.directory.normalized 275 | try: 276 | makedirs(log_directory) 277 | except Exception, e: 278 | if not self._log_directory_error: 279 | print "failed to create logs directory '%s': %s" % (log_directory, e) 280 | self._log_directory_error = True 281 | self._siptrace_error = True 282 | self._pjsiptrace_error = True 283 | self._notifications_error = True 284 | raise 285 | else: 286 | self._log_directory_error = False 287 | # sip trace 288 | if self._siptrace_filename is None: 289 | self._siptrace_filename = os.path.join(log_directory, 'sip_trace.txt') 290 | self._siptrace_error = False 291 | 292 | # msrp trace 293 | if self._msrptrace_filename is None: 294 | self._msrptrace_filename = os.path.join(log_directory, 'msrp_trace.txt') 295 | self._msrptrace_error = False 296 | 297 | # pjsip trace 298 | if self._pjsiptrace_filename is None: 299 | self._pjsiptrace_filename = os.path.join(log_directory, 'pjsip_trace.txt') 300 | self._pjsiptrace_error = False 301 | 302 | # notifications trace 303 | if self._notifications_filename is None: 304 | self._notifications_filename = os.path.join(log_directory, 'notifications_trace.txt') 305 | self._notifications_error = False 306 | 307 | def _init_log_file(self, type): 308 | if getattr(self, '_%s_file' % type) is None: 309 | self._init_log_directory() 310 | filename = getattr(self, '_%s_filename' % type) 311 | try: 312 | setattr(self, '_%s_file' % type, open(filename, 'a')) 313 | except Exception, e: 314 | if not getattr(self, '_%s_error' % type): 315 | print "failed to create log file '%s': %s" % (filename, e) 316 | setattr(self, '_%s_error' % type, True) 317 | raise 318 | else: 319 | setattr(self, '_%s_error' % type, False) 320 | 321 | 322 | -------------------------------------------------------------------------------- /sipclient/system.py: -------------------------------------------------------------------------------- 1 | 2 | """System utilities used by the sipclient scripts""" 3 | 4 | __all__ = ['IPAddressMonitor'] 5 | 6 | from application.notification import NotificationCenter, NotificationData 7 | from application.system import host 8 | from eventlib import api 9 | 10 | from sipsimple.threading import run_in_twisted_thread 11 | from sipsimple.threading.green import run_in_green_thread 12 | 13 | 14 | class IPAddressMonitor(object): 15 | """ 16 | An object which monitors the IP address used for the default route of the 17 | host and posts a SystemIPAddressDidChange notification when a change is 18 | detected. 19 | """ 20 | 21 | def __init__(self): 22 | self.greenlet = None 23 | 24 | @run_in_green_thread 25 | def start(self): 26 | notification_center = NotificationCenter() 27 | 28 | if self.greenlet is not None: 29 | return 30 | self.greenlet = api.getcurrent() 31 | 32 | current_address = host.default_ip 33 | while True: 34 | new_address = host.default_ip 35 | # make sure the address stabilized 36 | api.sleep(5) 37 | if new_address != host.default_ip: 38 | continue 39 | if new_address != current_address: 40 | notification_center.post_notification(name='SystemIPAddressDidChange', sender=self, data=NotificationData(old_ip_address=current_address, new_ip_address=new_address)) 41 | current_address = new_address 42 | api.sleep(5) 43 | 44 | @run_in_twisted_thread 45 | def stop(self): 46 | if self.greenlet is not None: 47 | api.kill(self.greenlet, api.GreenletExit()) 48 | self.greenlet = None 49 | 50 | 51 | -------------------------------------------------------------------------------- /sipclient/ui.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Implements a library that can be used for buildina a fully featured SIP 4 | User Agent working in a terminal text window. See sip_session.py script for 5 | an actual implementation. 6 | """ 7 | 8 | __all__ = ["RichText", "CompoundRichText", "Prompt", "Question", "UI"] 9 | 10 | import atexit 11 | import cPickle as pickle 12 | import fcntl 13 | import os 14 | import re 15 | import select 16 | import signal 17 | import struct 18 | import sys 19 | import termios 20 | from collections import deque 21 | from threading import RLock, Thread 22 | 23 | from application.python.decorator import decorator, preserve_signature 24 | from application.python.queue import EventQueue 25 | from application.python.types import Singleton 26 | from application.system import openfile 27 | from application.notification import NotificationCenter, NotificationData 28 | 29 | 30 | @decorator 31 | def run_in_ui_thread(func): 32 | @preserve_signature(func) 33 | def wrapper(self, *args, **kwargs): 34 | self.event_queue.put((func, self, args, kwargs)) 35 | return wrapper 36 | 37 | 38 | class RichText(object): 39 | colors = {'default': 9, 40 | 'red': 61, 41 | 'darkred': 1, 42 | 'lightgreen': 62, 43 | 'darkgreen': 2, 44 | 'yellow': 63, 45 | 'darkyellow': 3, 46 | 'cyan': 66, 47 | 'lightblue': 6, 48 | 'blue': 64, 49 | 'darkblue': 4, 50 | 'magenta': 65, 51 | 'purple': 5, 52 | 'white': 67, 53 | 'lightgrey': 7, 54 | 'darkgrey': 60, 55 | 'black': 0} 56 | def __init__(self, text, foreground='default', background='default', bold=False, underline=False, blink=False): 57 | self.text = text 58 | self.foreground = foreground 59 | self.background = background 60 | self.bold = bold 61 | self.underline = underline 62 | self.blink = blink 63 | 64 | def __str__(self): 65 | if isinstance(self.text, unicode): 66 | text = self.text.encode(sys.getfilesystemencoding()) 67 | else: 68 | text = self.text 69 | return '\x1b[%sm%s\x1b[0m' % (self.mode, text) 70 | 71 | def __len__(self): 72 | return len(self.text) 73 | 74 | def __getitem__(self, index): 75 | return self.__class__(self.text.__getitem__(index), foreground=self.foreground, background=self.background, bold=self.bold, underline=self.underline, blink=self.blink) 76 | 77 | def __add__(self, other): 78 | return CompoundRichText([self, other]) 79 | 80 | @property 81 | def mode(self): 82 | attributes = [str(30+self.colors.get(self.foreground)), 83 | str(40+self.colors.get(self.background)), 84 | '1' if self.bold else '22', 85 | '4' if self.underline else '24', 86 | '5' if self.blink else '25'] 87 | return ';'.join(attributes) 88 | 89 | 90 | class CompoundRichText(RichText): 91 | def __init__(self, text_list): 92 | self.text_list = text_list 93 | 94 | def __str__(self): 95 | txt = '' 96 | for text in self.text_list: 97 | if isinstance(text, unicode): 98 | text = text.encode(sys.getfilesystemencoding()) 99 | else: 100 | text = str(text) 101 | txt += text 102 | return txt 103 | 104 | def __len__(self): 105 | return sum(len(text) for text in self.text_list) 106 | 107 | def __add__(self, other): 108 | if isinstance(other, CompoundRichText): 109 | return CompoundRichText(self.text_list+other.text_list) 110 | else: 111 | return CompoundRichText(self.text_list+[other]) 112 | 113 | def __iadd__(self, other): 114 | self.text_list.append(other) 115 | return self 116 | 117 | 118 | class Prompt(RichText): 119 | def __str__(self): 120 | if isinstance(self.text, unicode): 121 | text = self.text.encode(sys.getfilesystemencoding()) 122 | else: 123 | text = self.text 124 | return '\x1b[%sm%s>\x1b[0m ' % (self.mode, text) 125 | 126 | def __len__(self): 127 | return len(self.text)+2 128 | 129 | 130 | class Question(RichText): 131 | def __init__(self, text, answers, *args, **kwargs): 132 | RichText.__init__(self, text, *args, **kwargs) 133 | self.answers = answers 134 | 135 | def __getitem__(self, index): 136 | return self.__class__(self.text.__getitem__(index), answers=self.answers, foreground=self.foreground, background=self.background, bold=self.bold, underline=self.underline, blink=self.blink) 137 | 138 | 139 | class Input(object): 140 | def __init__(self): 141 | self.history_file = None 142 | self.lines = [] 143 | self.current_line_index = None 144 | self.cursor_position = None 145 | 146 | def _get_current_line(self): 147 | if self.current_line_index is None: 148 | raise RuntimeError('no current line available') 149 | return self.lines[self.current_line_index] 150 | def _set_current_line(self, value): 151 | if value is None: 152 | self.current_line_index = None 153 | return 154 | if self.current_line_index is None: 155 | raise RuntimeError('no current line available') 156 | self.lines[self.current_line_index] = value 157 | current_line = property(_get_current_line, _set_current_line) 158 | del _get_current_line, _set_current_line 159 | 160 | def add_history(self, history_file): 161 | self.history_file = history_file 162 | try: 163 | self.lines = pickle.load(open(history_file, 'rb')) 164 | except (IOError, TypeError, EOFError): 165 | self.lines = [] 166 | 167 | def save_history(self): 168 | with openfile(self.history_file, 'wb', permissions=0600) as history_file: 169 | pickle.dump(self.lines, history_file) 170 | 171 | def add_line(self, text=''): 172 | self.lines.append(text) 173 | self.current_line_index = len(self.lines)-1 174 | self.cursor_position = len(text) 175 | 176 | def copy_current_line(self): 177 | if self.current_line_index != len(self.lines) - 1: 178 | self.lines[-1] = self.current_line 179 | 180 | def line_up(self, count=1): 181 | if self.current_line_index is None: 182 | raise RuntimeError('no current line available') 183 | if self.current_line_index - count < 0: 184 | raise KeyError('too many lines up') 185 | self.current_line_index -= count 186 | self.cursor_position = len(self.current_line) 187 | 188 | def line_down(self, count=1): 189 | if self.current_line_index is None: 190 | raise RuntimeError('no current line available') 191 | if self.current_line_index + count >= len(self.lines): 192 | raise KeyError('too many lines down') 193 | self.current_line_index += count 194 | self.cursor_position = len(self.current_line) 195 | 196 | 197 | class TTYFileWrapper(object): 198 | def __init__(self, file): 199 | if not file.isatty(): 200 | raise RuntimeError('TTYFileWrapper is supposed to wrap a tty file') 201 | self.file = file 202 | self.buffer = '' 203 | self.lock = RLock() 204 | # no-ops / simple ops 205 | def close(self): pass 206 | def fileno(self): return self.file.fileno() 207 | def isatty(self): return True 208 | def tell(self): return self.file.tell() 209 | 210 | def write(self, str): 211 | with self.lock: 212 | if not str: 213 | return 214 | ui = UI() 215 | if ui.stopping: 216 | self.file.write(str) 217 | else: 218 | lines = re.split(r'\r\n|\r|\n', str) 219 | lines[0] = self.buffer + lines[0] 220 | self.buffer = lines[-1] 221 | ui.writelines(lines[:-1]) 222 | 223 | def writelines(self, sequence): 224 | with self.lock: 225 | for text in sequence: 226 | self.write(text) 227 | 228 | def flush(self): 229 | with self.lock: 230 | if self.buffer: 231 | ui = UI() 232 | ui.writelines([self.buffer]) 233 | self.buffer = '' 234 | 235 | def send_to_file(self): 236 | if self.buffer: 237 | self.file.write(self.buffer) 238 | 239 | 240 | class UI(Thread): 241 | __metaclass__ = Singleton 242 | 243 | control_chars = {'\x01': 'home', 244 | '\x04': 'eof', 245 | '\x05': 'end', 246 | '\x0a': 'newline', 247 | '\x0d': 'newline', 248 | '\x1b[A': 'cursorup', 249 | '\x1b[B': 'cursordown', 250 | '\x1b[C': 'cursorright', 251 | '\x1b[D': 'cursorleft', 252 | '\x1b[F': 'end', 253 | '\x1b[H': 'home', 254 | '\x7f': 'delete'} 255 | 256 | # public functions 257 | # 258 | 259 | def __init__(self, history_file=None): 260 | Thread.__init__(self, target=self._run, name='UI-Thread') 261 | self.setDaemon(True) 262 | 263 | self.__dict__['prompt'] = Prompt('') 264 | self.__dict__['status'] = None 265 | self.command_sequence = '/' 266 | self.application_control_char = '\x18' # ctrl-X 267 | self.application_control_bindings = {} 268 | self.display_commands = True 269 | self.display_text = True 270 | 271 | self.cursor_x = None 272 | self.cursor_y = None 273 | self.displaying_question = False 274 | self.input = Input() 275 | self.input.add_history(history_file) 276 | self.last_window_size = None 277 | self.prompt_y = None 278 | self.questions = deque() 279 | self.stopping = False 280 | self.lock = RLock() 281 | self.event_queue = EventQueue(handler=lambda (function, self, args, kwargs): function(self, *args, **kwargs), name='UI operation handling') 282 | 283 | def start(self, prompt='', command_sequence='/', control_char='\x18', control_bindings={}, display_commands=True, display_text=True): 284 | with self.lock: 285 | if self.isAlive(): 286 | raise RuntimeError('UI already active') 287 | if not sys.stdin.isatty(): 288 | raise RuntimeError('UI cannot be used on a non-TTY') 289 | if not sys.stdout.isatty(): 290 | raise RuntimeError('UI cannot be used on a non-TTY') 291 | stdin_fd = sys.stdin.fileno() 292 | 293 | self.command_sequence = command_sequence 294 | self.application_control_char = control_char 295 | self.application_control_bindings = control_bindings 296 | self.display_commands = display_commands 297 | self.display_text = display_text 298 | 299 | # wrap sys.stdout 300 | sys.stdout = TTYFileWrapper(sys.stdout) 301 | # and possibly sys.stderr 302 | if sys.stderr.isatty(): 303 | sys.stderr = TTYFileWrapper(sys.stderr) 304 | 305 | # change input to character-mode 306 | old_settings = termios.tcgetattr(stdin_fd) 307 | new_settings = termios.tcgetattr(stdin_fd) 308 | new_settings[3] &= ~termios.ECHO & ~termios.ICANON 309 | new_settings[6][termios.VMIN] = '\000' 310 | termios.tcsetattr(stdin_fd, termios.TCSADRAIN, new_settings) 311 | atexit.register(termios.tcsetattr, stdin_fd, termios.TCSADRAIN, old_settings) 312 | 313 | # find out cursor position in terminal 314 | self._raw_write('\x1b[6n') 315 | if select.select([stdin_fd], [], [], None)[0]: 316 | line, col = os.read(stdin_fd, 10)[2:-1].split(';') 317 | line = int(line) 318 | col = int(col) 319 | 320 | # scroll down the terminal until everything goes up 321 | self._scroll_up(line-1) 322 | # move the cursor to the upper left side corner 323 | self._raw_write('\x1b[H') 324 | self.cursor_x = 1 325 | self.cursor_y = 1 326 | # display the prompt 327 | self.prompt_y = 1 328 | self.input.add_line() 329 | self._update_prompt() 330 | # make sure we know when the window gets resized 331 | self.last_window_size = self.window_size 332 | signal.signal(signal.SIGWINCH, lambda signum, frame: self._window_resized()) 333 | 334 | self.event_queue.start() 335 | Thread.start(self) 336 | 337 | # this will trigger the update of the prompt 338 | self.prompt = prompt 339 | 340 | @run_in_ui_thread 341 | def stop(self): 342 | with self.lock: 343 | self.stopping = True 344 | self.status = None 345 | sys.stdout.send_to_file() 346 | if isinstance(sys.stderr, TTYFileWrapper): 347 | sys.stderr.send_to_file() 348 | self._raw_write('\n\x1b[2K') 349 | self.input.save_history() 350 | 351 | def write(self, text): 352 | self.writelines([text]) 353 | 354 | @run_in_ui_thread 355 | def writelines(self, text_lines): 356 | with self.lock: 357 | if not text_lines: 358 | return 359 | # go to beginning of prompt line 360 | self._raw_write('\x1b[%d;%dH' % (self.prompt_y, 1)) 361 | # erase everything beneath it 362 | self._raw_write('\x1b[0J') 363 | # start writing lines 364 | window_size = self.window_size 365 | for text in text_lines: 366 | # write the line 367 | self._raw_write('%s\n' % text) 368 | # calculate the number of lines the text will produce 369 | text_lines = (len(text)-1)/window_size.x + 1 370 | # calculate how much the text will automatically scroll the window 371 | window_height = struct.unpack('HHHH', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))[0] 372 | auto_scroll_amount = max(0, (self.prompt_y+text_lines-1) - (window_height-1)) 373 | # calculate the new position of the prompt 374 | self.prompt_y += text_lines - auto_scroll_amount 375 | # we might need to scroll up to make the prompt position visible again 376 | scroll_up = self.prompt_y - window_height 377 | if scroll_up > 0: 378 | self.prompt_y -= scroll_up 379 | self._scroll_up(scroll_up) 380 | # redraw the prompt 381 | self._update_prompt() 382 | 383 | @run_in_ui_thread 384 | def add_question(self, question): 385 | with self.lock: 386 | self.questions.append(question) 387 | if len(self.questions) == 1: 388 | self._update_prompt() 389 | 390 | @run_in_ui_thread 391 | def remove_question(self, question): 392 | with self.lock: 393 | first_question = (question == self.questions[0]) 394 | self.questions.remove(question) 395 | if not self.questions or first_question: 396 | self.displaying_question = False 397 | self._update_prompt() 398 | 399 | # properties 400 | # 401 | 402 | @property 403 | def window_size(self): 404 | class WindowSize(tuple): 405 | def __init__(ws_self, (y, x)): 406 | ws_self.x = x 407 | ws_self.y = y if self.status is None else y-1 408 | return WindowSize(struct.unpack('HHHH', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))[:2]) 409 | 410 | def _get_prompt(self): 411 | return self.__dict__['prompt'] 412 | @run_in_ui_thread 413 | def _set_prompt(self, value): 414 | with self.lock: 415 | if not isinstance(value, Prompt): 416 | value = Prompt(value) 417 | self.__dict__['prompt'] = value 418 | self._update_prompt() 419 | prompt = property(_get_prompt, _set_prompt) 420 | del _get_prompt, _set_prompt 421 | 422 | def _get_status(self): 423 | return self.__dict__['status'] 424 | @run_in_ui_thread 425 | def _set_status(self, status): 426 | with self.lock: 427 | if isinstance(status, unicode): 428 | status = status.encode(sys.getfilesystemencoding()) 429 | try: 430 | old_status = self.__dict__['status'] 431 | except KeyError: 432 | self.__dict__['status'] = status 433 | else: 434 | self.__dict__['status'] = status 435 | if old_status is not None and status is None: 436 | status_y, window_length = struct.unpack('HHHH', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))[:2] 437 | # save current cursor position 438 | self._raw_write('\x1b[s') 439 | # goto line status_y 440 | self._raw_write('\x1b[%d;%dH' % (status_y, 1)) 441 | # erase it 442 | self._raw_write('\x1b[2K') 443 | # restore the cursor position 444 | self._raw_write('\x1b[u') 445 | else: 446 | self._update_prompt() 447 | status = property(_get_status, _set_status) 448 | del _get_status, _set_status 449 | 450 | 451 | # private functions 452 | # 453 | 454 | def _run(self): 455 | wait_control_char = False 456 | while True: 457 | stdin_fd = sys.__stdin__.fileno() 458 | if select.select([stdin_fd], [], [], None)[0]: 459 | chars = list(os.read(stdin_fd, 4096)) 460 | while chars: 461 | if self.stopping: 462 | return 463 | with self.lock: 464 | char = chars.pop(0) 465 | if ord(char) < 32 or ord(char) == 127: 466 | if char == '\x1b': 467 | if chars and chars[0] == '[': 468 | char += chars.pop(0) 469 | while chars and not chars[0].isalpha(): 470 | char += chars.pop(0) 471 | if chars: 472 | char += chars.pop(0) 473 | if self.questions: 474 | pass 475 | elif char == self.application_control_char: 476 | wait_control_char = not wait_control_char 477 | elif not self.questions: 478 | wait_control_char = False 479 | handler = getattr(self, '_CH_%s' % self.control_chars.get(char, 'default')) 480 | handler(char) 481 | elif wait_control_char: 482 | wait_control_char = False 483 | if char in self.application_control_bindings: 484 | notification_center = NotificationCenter() 485 | words = [word for word in re.split(r'\s+', self.application_control_bindings[char]) if word] 486 | notification_center.post_notification('UIInputGotCommand', sender=self, data=NotificationData(command=words[0], args=words[1:])) 487 | elif self.questions: 488 | question = self.questions[0] 489 | if char in question.answers: 490 | self._raw_write(char) 491 | self.displaying_question = False 492 | self.remove_question(question) 493 | notification_center = NotificationCenter() 494 | notification_center.post_notification('UIQuestionGotAnswer', sender=question, data=NotificationData(answer=char)) 495 | else: 496 | # insert char in input.current_line at input.cursor_position and advance cursor 497 | self.input.current_line = self.input.current_line[:self.input.cursor_position] + char + self.input.current_line[self.input.cursor_position:] 498 | self.input.cursor_position += 1 499 | self._update_prompt() 500 | 501 | def _raw_write(self, text): 502 | sys.__stdout__.write(str(text)) 503 | sys.__stdout__.flush() 504 | 505 | def _window_resized(self): 506 | pass 507 | 508 | def _update_prompt(self): 509 | # The (X-1)/window_size.x+1 are because the position in the terminal is 510 | # a 1-based index; the + 1 when calculating the indexes are because the 511 | # positions we keep are 0-based. 512 | if self.displaying_question or self.stopping: 513 | return 514 | if self.questions: 515 | window_size = self.window_size 516 | question = self.questions[0] 517 | # we also want to leave a space after the question and we need an 518 | # extra position for the cursor 519 | text_len = len(question) + 2 520 | # calculate how much we need to scroll up 521 | text_lines = (text_len-1)/window_size.x + 1 522 | scroll_up = text_lines - (window_size.y - self.prompt_y + 1) 523 | if scroll_up > 0: 524 | self._scroll_up(scroll_up) 525 | self.prompt_y -= scroll_up 526 | # go to the position where the question will be rendered 527 | self._raw_write('\x1b[%d;%dH' % (self.prompt_y, 1)) 528 | # erase everything beneath it 529 | self._raw_write('\x1b[0J') 530 | # might need to draw the status 531 | self._draw_status() 532 | # draw the question 533 | self._raw_write(question) 534 | # and a space 535 | self._raw_write(' ') 536 | # calculate the cursor position 537 | self.cursor_y = (text_len-1)/window_size.x + self.prompt_y # no need to add 1 since we had to subtract 1 538 | self.cursor_x = (text_len-1)%window_size.x + 1 539 | # the new prompt will now be just under the question 540 | self.prompt_y += text_lines 541 | self.displaying_question = True 542 | else: 543 | window_size = self.window_size 544 | text_len = len(self.prompt) + len(self.input.current_line) 545 | # we also need a position for the cursor if it's at the end of the line 546 | if self.input.cursor_position == len(self.input.current_line): 547 | text_len += 1 548 | # calculate how much we need to scroll up 549 | text_lines = (text_len-1)/window_size.x + 1 550 | scroll_up = text_lines - (window_size.y - self.prompt_y + 1) 551 | if scroll_up > 0: 552 | self._scroll_up(scroll_up) 553 | self.prompt_y -= scroll_up 554 | # goto the position of the new prompt 555 | self._raw_write('\x1b[%d;%dH' % (self.prompt_y, 1)) 556 | # erase everything beneath it 557 | self._raw_write('\x1b[0J') 558 | # might need to draw the status 559 | self._draw_status() 560 | # force going to the new prompt position 561 | self._raw_write('\x1b[%d;%dH' % (self.prompt_y, 1)) 562 | # draw the prompt and the text 563 | self._raw_write(self.prompt) 564 | self._raw_write(self.input.current_line) 565 | # move the cursor to it's correct position 566 | cursor_position = len(self.prompt) + self.input.cursor_position + 1 567 | self.cursor_y = (cursor_position-1)/window_size.x + self.prompt_y # no need to add 1 since we had to subtract 1 568 | self.cursor_x = (cursor_position-1)%window_size.x + 1 569 | self._raw_write('\x1b[%d;%dH' % (self.cursor_y, self.cursor_x)) 570 | 571 | def _draw_status(self): 572 | status = self.status 573 | if status is not None: 574 | status_y, window_length = struct.unpack('HHHH', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))[:2] 575 | # save current cursor position 576 | self._raw_write('\x1b[s') 577 | # goto line status_y 578 | self._raw_write('\x1b[%d;%dH' % (status_y, 1)) 579 | # erase it 580 | self._raw_write('\x1b[2K') 581 | # display the status 582 | if len(status) > window_length: 583 | status = status[:window_length] 584 | self._raw_write(status) 585 | # restore the cursor position 586 | self._raw_write('\x1b[u') 587 | 588 | def _scroll_up(self, lines): 589 | window_height = struct.unpack('HHHH', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))[0] 590 | self._raw_write('\x1b[s\x1b[%d;1H' % window_height + '\x1bD' * lines + '\x1b[u') 591 | 592 | # control character handlers 593 | # 594 | 595 | def _CH_default(self, char): 596 | #print 'Got control char %s' % ''.join('%02X' % ord(c) for c in char) 597 | pass 598 | 599 | def _CH_home(self, char): 600 | if self.input.cursor_position > 0: 601 | self.input.cursor_position = 0 602 | self._update_prompt() 603 | 604 | def _CH_eof(self, char): 605 | notification_center = NotificationCenter() 606 | notification_center.post_notification('UIInputGotCommand', sender=self, data=NotificationData(command='eof', args=[])) 607 | 608 | def _CH_end(self, char): 609 | if self.input.cursor_position < len(self.input.current_line): 610 | self.input.cursor_position = len(self.input.current_line) 611 | self._update_prompt() 612 | 613 | def _CH_newline(self, char): 614 | if self.input.current_line: 615 | # copy the current line to the last line 616 | self.input.copy_current_line() 617 | window_size = self.window_size 618 | # calculate the length of the line just entered 619 | text_len = len(self.prompt) + len(self.input.current_line) 620 | text_lines = (text_len-1)/window_size.x + 1 621 | # save the current line and add a new input line 622 | current_line = self.input.current_line 623 | self.input.add_line() 624 | # see if it's a command or plain text 625 | notification_center = NotificationCenter() 626 | if current_line.startswith(self.command_sequence): 627 | # calculate the new position of the prompt 628 | if self.display_commands: 629 | self.prompt_y += text_lines 630 | # we need to scroll if the new prompt position is below the window margin, otherwise 631 | # some text might go over it 632 | scroll_up = self.prompt_y - window_size.y 633 | if scroll_up > 0: 634 | self.prompt_y -= scroll_up 635 | self._scroll_up(scroll_up) 636 | # send a notification about the new input 637 | words = [word for word in re.split(r'\s+', current_line[len(self.command_sequence):]) if word] 638 | notification_center.post_notification('UIInputGotCommand', sender=self, data=NotificationData(command=words[0], args=words[1:])) 639 | else: 640 | # calculate the new position of the prompt 641 | if self.display_text: 642 | self.prompt_y += text_lines 643 | # we need to scroll if the new prompt position is below the window margin, otherwise 644 | # some text might go over it 645 | scroll_up = self.prompt_y - window_size.y 646 | if scroll_up > 0: 647 | self.prompt_y -= scroll_up 648 | self._scroll_up(scroll_up) 649 | # send a notification about the new input 650 | notification_center.post_notification('UIInputGotText', sender=self, data=NotificationData(text=current_line)) 651 | # redisplay the prompt 652 | self._update_prompt() 653 | 654 | def _CH_cursorup(self, char): 655 | try: 656 | self.input.line_up() 657 | except KeyError: 658 | pass 659 | else: 660 | self._update_prompt() 661 | 662 | def _CH_cursordown(self, char): 663 | try: 664 | self.input.line_down() 665 | except KeyError: 666 | pass 667 | else: 668 | self._update_prompt() 669 | 670 | def _CH_cursorright(self, char): 671 | if self.input.cursor_position < len(self.input.current_line): 672 | self.input.cursor_position += 1 673 | self._update_prompt() 674 | 675 | def _CH_cursorleft(self, char): 676 | if self.input.cursor_position > 0: 677 | self.input.cursor_position -= 1 678 | self._update_prompt() 679 | 680 | def _CH_delete(self, char): 681 | # delete the character in input.current_line at input.cursor_position 682 | if self.input.cursor_position > 0: 683 | self.input.current_line = self.input.current_line[:self.input.cursor_position-1]+self.input.current_line[self.input.cursor_position:] 684 | self.input.cursor_position -= 1 685 | self._update_prompt() 686 | 687 | 688 | --------------------------------------------------------------------------------