Skip to content

Commit

Permalink
Add HTTPS support to utilities.
Browse files Browse the repository at this point in the history
  • Loading branch information
gareth-palmer committed Jul 6, 2021
1 parent b0ae5e0 commit d54d2ef
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 92 deletions.
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy|unused
extension-pkg-whitelist=lxml

[MESSAGES CONTROL]
disable=missing-module-docstring,missing-class-docstring,missing-function-docstring,bad-whitespace,invalid-name,redefined-builtin,broad-except,consider-using-sys-exit,too-many-branches,too-many-statements,too-many-arguments,line-too-long,too-many-locals,len-as-condition,wrong-import-position,unused-argument,duplicate-code
disable=missing-module-docstring,missing-class-docstring,missing-function-docstring,bad-whitespace,invalid-name,redefined-builtin,broad-except,consider-using-sys-exit,too-many-branches,too-many-statements,too-many-arguments,line-too-long,too-many-locals,len-as-condition,wrong-import-position,unused-argument,duplicate-code,too-many-nested-blocks,arguments-differ

[REPORTS]
output-format=text
69 changes: 52 additions & 17 deletions cgiexecute
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,22 @@ import getopt
import traceback
from html import escape

from lxml import etree
import requests
import requests.auth
import requests.adapters
from lxml import etree


class ProgramError(Exception):
pass


class HTTPSAdapter(requests.adapters.HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
kwargs['assert_hostname'] = False
super().init_poolmanager(*args, **kwargs)


cgi_errors = {
'1': 'Error parsing CiscoIPPhone object',
'2': 'Error framing CiscoIPPhone object',
Expand All @@ -28,23 +35,28 @@ cgi_errors = {
}


def cgi_execute(hostname, timeout, urls, username, password):
content = '<?xml version="1.0" encoding="UTF-8"?>' \
def cgi_execute(hostname, port, timeout, username, password, certificate, urls):
xml = '<?xml version="1.0" encoding="UTF-8"?>' \
'<CiscoIPPhoneExecute>'

for url, priority in urls:
content += '<ExecuteItem URL="' + escape(url) + '" Priority="' + str(priority) + '" />'
xml += '<ExecuteItem URL="' + escape(url) + '" Priority="' + str(priority) + '" />'

content += '</CiscoIPPhoneExecute>'
xml += '</CiscoIPPhoneExecute>'

if username != '':
auth = requests.auth.HTTPBasicAuth(username, password)
else:
auth = None

scheme = 'https' if certificate else 'http'

try:
response = requests.post(f'http://{hostname}:80/CGI/Execute',
timeout = timeout, auth = auth, data = {'XML': content})
session = requests.Session()
session.mount('https://', HTTPSAdapter())

response = session.post(f'{scheme}://{hostname}:{port}/CGI/Execute',
timeout = timeout, auth = auth, verify = certificate, data = {'XML': xml})
response.raise_for_status()

except requests.RequestException as error:
Expand Down Expand Up @@ -72,8 +84,8 @@ def cgi_execute(hostname, timeout, urls, username, password):

def main():
try:
short_options = 'h:t:u:p:H'
long_options = ['host=', 'timeout=', 'username=', 'password=', 'help']
short_options = 'h:t:u:p:c:H'
long_options = ['host=', 'timeout=', 'username=', 'password=', 'certificate=', 'help']

try:
options, arguments = getopt.gnu_getopt(sys.argv[1:], short_options, long_options)
Expand All @@ -82,15 +94,29 @@ def main():
'. Try \'' + os.path.basename(sys.argv[0]) + ' --help\' for more information')

hostname = None
port = None
timeout = 10
username = ''
password = ''
certificate = None
help = False

for option, argument in options:
if option in ('-h', '--host'):
hostname = argument

if ':' in hostname:
hostname, port = hostname.rsplit(':', maxsplit = 1)

try:
port = int(port)

if port < 1 or port > 65535:
raise ValueError

except ValueError:
raise ProgramError(f'Invalid port: {port}')

if not re.search(r'(?xi) ^ (?: [a-z0-9\-]+ \.)* [a-z0-9\-]+ $', hostname):
raise ProgramError(f'Invalid host: {hostname}')

Expand All @@ -108,18 +134,22 @@ def main():
elif option in ('-p', '--password'):
password = argument

elif option in ('-c', '--certificate'):
certificate = argument

elif option in ('-H', '--help'):
help = True

if help:
print('Usage: ' + os.path.basename(sys.argv[0]) + ' [OPTIONS] URL[@PRIORITY]...\n'
'Send CGI Execute URLs to a Cisco IP Phone.\n'
'\n'
' -h, --host HOST host name or IP address of the phone\n'
' -t, --timeout TIMEOUT request timeout in seconds (default 10)\n'
' -u, --username USERNAME authentication username\n'
' -p, --password PASSWORD authentication password\n'
' -H, --help print this help and exit\n'
' -h, --host HOST[:PORT] host name or IP address and port of the phone\n'
' -t, --timeout TIMEOUT connection timeout in seconds (default 10)\n'
' -u, --username USERNAME authentication username\n'
' -p, --password PASSWORD authentication password\n'
' -c, --certificate CERT-FILE connect using SSL and verify using certificate\n'
' -H, --help print this help and exit\n'
'\n'
'Up to 3 URLs may be specified.\n'
'URL is one of Dial:, EditDial:, Key:, SoftKey:, Init:, Play:, Display:, http: or https:\n'
Expand All @@ -133,7 +163,9 @@ def main():
urls = []

for argument in arguments:
if '@' in argument:
url = argument

if '@' in url:
url, priority = argument.rsplit('@', maxsplit = 1)

try:
Expand All @@ -145,7 +177,7 @@ def main():
except ValueError:
raise ProgramError(f'Invalid priority: {priority}')
else:
url, priority = argument, 0
priority = 0

if not re.search(r'(?x) ^ (?: (?: Dial | EditDial) : [0-9#*]+'
r' | (?: Key | SoftKey | Init) : [a-zA-Z0-9]+'
Expand All @@ -162,7 +194,10 @@ def main():
if hostname is None:
raise ProgramError('No host specified')

cgi_execute(hostname, timeout, urls, username, password)
if port is None:
port = 443 if certificate else 80

cgi_execute(hostname, port, timeout, username, password, certificate, urls)

except ProgramError as error:
print(str(error), file = sys.stderr)
Expand Down
81 changes: 55 additions & 26 deletions mediastream
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import concurrent.futures
from lxml import etree
import requests
import requests.auth
import requests.adapters
import gi
gi.require_version('Gst', '1.0')
from gi.repository import GLib, Gst
Expand All @@ -26,15 +27,26 @@ class ProgramError(Exception):
pass


def cgi_execute(target_hostname, timeout, username, password, content):
class HTTPSAdapter(requests.adapters.HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
kwargs['assert_hostname'] = False
super().init_poolmanager(*args, **kwargs)


def cgi_execute(target_hostname, timeout, username, password, certificate, xml):
if username != '':
auth = requests.auth.HTTPBasicAuth(username, password)
else:
auth = None

scheme = 'https' if certificate else 'http'

try:
response = requests.post(f'http://{target_hostname}:80/CGI/Execute',
timeout = timeout, auth = auth, data = {'XML': content})
session = requests.Session()
session.mount('https://', HTTPSAdapter())

response = session.post(f'{scheme}://{target_hostname}/CGI/Execute',
timeout = timeout, auth = auth, verify = certificate, data = {'XML': xml})
response.raise_for_status()

except requests.RequestException as error:
Expand All @@ -46,18 +58,18 @@ def cgi_execute(target_hostname, timeout, username, password, content):
return response.content


def start_media(target_hostnames, multicast_address, port, timeout, volume, codec, username, password):
def start_media(target_hostnames, multicast_address, port, timeout, username, password, certificate, volume, codec):
if multicast_address is not None:
source_address = multicast_address
else:
# find our source address using the first target host
# Find our source address using the first target host
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_socket.connect((target_hostnames[0], 0))

source_address = udp_socket.getsockname()[0]
udp_socket.close()

content = '<?xml version="1.0" charset="UTF-8">' \
xml = '<?xml version="1.0" charset="UTF-8">' \
'<startMedia>' \
'<mediaStream' + (' receiveVolume="' + str(volume) + '"' if volume != -1 else '') + '>' \
'<type>audio</type>' \
Expand All @@ -72,7 +84,7 @@ def start_media(target_hostnames, multicast_address, port, timeout, volume, code
futures = {}

for target_hostname in target_hostnames:
future = executor.submit(cgi_execute, target_hostname, timeout, username, password, content)
future = executor.submit(cgi_execute, target_hostname, timeout, username, password, certificate, xml)
futures[future] = target_hostname

for future in concurrent.futures.as_completed(futures):
Expand All @@ -88,7 +100,7 @@ def start_media(target_hostnames, multicast_address, port, timeout, volume, code
raise ProgramError(f'Unexpected XML: {document.tag}')

except Exception as error:
# remove address so we don't try and send it audio
# Remove address so we don't try and send it audio
target_hostnames.remove(target_hostname)

print(f'Error {target_hostname}: {error}')
Expand Down Expand Up @@ -204,7 +216,7 @@ def stream_media(target_hostnames, multicast_address, port, codec, wav_file):

pipeline.add(udp_sink)

# only link elements after they have been added to the pipeline
# Only link elements after they have been added to the pipeline
file_src.link(wav_parse)
wav_parse.link(audio_convert)
audio_convert.link(audio_resample)
Expand All @@ -229,8 +241,8 @@ def stream_media(target_hostnames, multicast_address, port, codec, wav_file):
pipeline.set_state(Gst.State.NULL)


def stop_media(target_hostnames, timeout, username, password):
content = '<?xml version="1.0" charset="UTF-8">' \
def stop_media(target_hostnames, timeout, username, password, certificate):
xml = '<?xml version="1.0" charset="UTF-8">' \
'<stopMedia>' \
'<mediaStream />' \
'</stopMedia>'
Expand All @@ -239,7 +251,7 @@ def stop_media(target_hostnames, timeout, username, password):
futures = {}

for target_hostname in target_hostnames:
future = executor.submit(cgi_execute, target_hostname, timeout, username, password, content)
future = executor.submit(cgi_execute, target_hostname, timeout, username, password, certificate, xml)
futures[future] = target_hostname

for future in concurrent.futures.as_completed(futures):
Expand All @@ -257,8 +269,8 @@ def stop_media(target_hostnames, timeout, username, password):

def main():
try:
short_options = 'f:t:m:P:v:c:u:p:H'
long_options = ['file=', 'timeout=', 'multicast=', 'port=', 'volume=', 'codec=', 'username=', 'password=', 'help']
short_options = 'f:t:m:P:v:C:u:p:c:H'
long_options = ['file=', 'timeout=', 'multicast=', 'port=', 'volume=', 'codec=', 'username=', 'password=', 'certificate=', 'help']

try:
options, arguments = getopt.gnu_getopt(sys.argv[1:], short_options, long_options)
Expand All @@ -274,6 +286,7 @@ def main():
codec = 'g711'
username = ''
password = ''
certificate = None
help = False

for option, argument in options:
Expand Down Expand Up @@ -302,6 +315,18 @@ def main():

multicast_address = multicast_address.compressed

elif option in ('-P', '--port'):
port = argument

try:
port = int(port)

if port < 20480 or port > 65535 or port % 2:
raise ValueError

except ValueError:
raise ProgramError(f'Invalid port: {port}')

elif option in ('-v', '--volume'):
volume = argument

Expand All @@ -314,7 +339,7 @@ def main():
except ValueError:
raise ProgramError(f'Invalid volume: {volume}')

elif option in ('-c', '--codec'):
elif option in ('-C', '--codec'):
codec = argument

if codec not in ('g711', 'g722'):
Expand All @@ -326,22 +351,26 @@ def main():
elif option in ('-p', '--password'):
password = argument

elif option in ('-c', '--certificate'):
certificate = argument

elif option in ('-H', '--help'):
help = True

if help:
print('Usage: ' + os.path.basename(sys.argv[0]) + ' -f FILE [OPTIONS] TARGET-HOST...\n'
'Stream media to one or more Cisco IP Phones.\n'
'\n'
' -f, --file FILE .wav file to stream\n'
' -t, --timeout TIMEOUT request timeout in seconds (default 3)\n'
' -m, --multicast ADDRESS multicast the stream instead of using multiple unicast streams\n'
' -P, --port PORT destination port on phone (default 20480)\n'
' -v, --volume VOLUME volume percent (1-100) on phone\n'
' -c, --codec CODEC g711 or g722 (default g711)\n'
' -u, --username USERNAME authentication username\n'
' -p, --password PASSWORD authentication password\n'
' -H, --help print this help and exit\n')
' -f, --file FILE .wav file to stream\n'
' -t, --timeout TIMEOUT request timeout in seconds (default 3)\n'
' -m, --multicast ADDRESS multicast the stream instead of using multiple unicast streams\n'
' -P, --port PORT destination port on phone (default 20480)\n'
' -v, --volume VOLUME volume percent (1-100) on phone\n'
' -C, --codec CODEC g711 or g722 (default g711)\n'
' -u, --username USERNAME authentication username\n'
' -p, --password PASSWORD authentication password\n'
' -c, --certificate CERT-FILE connect using SSL and verify using certificate\n'
' -H, --help print this help and exit\n')

return

Expand All @@ -360,15 +389,15 @@ def main():
raise ProgramError('No .wav file specified')

# Invalid hostnames will be removed from the list
start_media(target_hostnames, multicast_address, port, timeout, volume, codec, username, password)
start_media(target_hostnames, multicast_address, port, timeout, username, password, certificate, volume, codec)

if len(target_hostnames):
try:
stream_media(target_hostnames, multicast_address, port, codec, wav_file)
except Exception as error:
print(f'{error}', file = sys.stderr)

stop_media(target_hostnames, timeout, username, password)
stop_media(target_hostnames, timeout, username, password, certificate)

except ProgramError as error:
print(str(error), file = sys.stderr)
Expand Down
Loading

0 comments on commit d54d2ef

Please sign in to comment.