#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# |             ____ _               _        __  __ _  __           |
# |            / ___| |__   ___  ___| | __   |  \/  | |/ /           |
# |           | |   | '_ \ / _ \/ __| |/ /   | |\/| | ' /            |
# |           | |___| | | |  __/ (__|   <    | |  | | . \            |
# |            \____|_| |_|\___|\___|_|\_\___|_|  |_|_|\_\           |
# |                                                                  |
# | Copyright Mathias Kettner 2014             mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk is free software;  you can redistribute it and/or modify it
# under the  terms of the  GNU General Public License  as published by
# the Free Software Foundation in version 2.  check_mk is  distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY;  with-
# out even the implied warranty of  MERCHANTABILITY  or  FITNESS FOR A
# PARTICULAR PURPOSE. See the  GNU General Public License for more de-
# ails.  You should have  received  a copy of the  GNU  General Public
# License along with GNU Make; see the file  COPYING.  If  not,  write
# to the Free Software Foundation, Inc., 51 Franklin St,  Fifth Floor,
# Boston, MA 02110-1301 USA.

# This agent uses UPNP API calls to the Fritz!Box to gather information
# about connection configuration and status.

# UPNP API CALLS THAT HAVE BEEN PROVEN WORKING
# Tested on:
# - AVM FRITZ!Box Fon WLAN 7360 111.05.51
# General Device Infos:
# http://fritz.box:49000/igddesc.xml
#
# http://fritz.box:49000/igdconnSCPD.xml
#get_upnp_info('WANIPConn1', 'urn:schemas-upnp-org:service:WANIPConnection:1', 'GetStatusInfo')
#get_upnp_info('WANIPConn1', 'urn:schemas-upnp-org:service:WANIPConnection:1', 'GetExternalIPAddress')
#get_upnp_info('WANIPConn1', 'urn:schemas-upnp-org:service:WANIPConnection:1', 'GetConnectionTypeInfo')
#get_upnp_info('WANIPConn1', 'urn:schemas-upnp-org:service:WANIPConnection:1', 'GetNATRSIPStatus')
#
# http://fritz.box:49000/igdicfgSCPD.xml
#get_upnp_info('WANCommonIFC1', 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1', 'GetAddonInfos')
#get_upnp_info('WANCommonIFC1', 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1', 'GetCommonLinkProperties')
#
# http://fritz.box:49000/igddslSCPD.xml
#get_upnp_info('WANDSLLinkC1', 'urn:schemas-upnp-org:service:WANDSLLinkConfig:1', 'GetDSLLinkInfo')

import getopt, sys, socket, urllib2, traceback, re, pprint

def usage():
    sys.stderr.write("""Check_MK Fritz!Box Agent

USAGE: agent_fritzbox [OPTIONS] HOST
       agent_fritzbox -h

ARGUMENTS:
  HOST                          Host name or IP address of your Fritz!Box

OPTIONS:
  -h, --help                    Show this help message and exit
  -t, --timeout SEC             Set the network timeout to <SEC> seconds.
                                Default is 10 seconds. Note: the timeout is not
                                applied to the whole check, instead it is used for
                                each API query.
  --debug                       Debug mode: let Python exceptions come through
""")

short_options = 'h:t:d'
long_options  = [
    'help', 'timeout=', 'debug'
]

host_address      = None
opt_debug         = False
opt_timeout       = 10

try:
    opts, args = getopt.getopt(sys.argv[1:], short_options, long_options)
except getopt.GetoptError, err:
    sys.stderr.write("%s\n" % err)
    sys.exit(1)

for o,a in opts:
    if o in [ '--debug' ]:
        opt_debug = True
    elif o in [ '-t', '--timeout' ]:
        opt_timeout = int(a)
    elif o in [ '-h', '--help' ]:
        usage()
        sys.exit(0)

if len(args) == 1:
    host_address = args[0]
elif not args:
    sys.stderr.write("ERROR: No host given.\n")
    sys.exit(1)
else:
    sys.stderr.write("ERROR: Please specify exactly one host.\n")
    sys.exit(1)

socket.setdefaulttimeout(opt_timeout)

class RequestError(Exception):
    pass

g_device  = None
g_version = None

base_urls = [ 'http://%s:49000/upnp' % host_address, 'http://%s:49000/igdupnp' % host_address ]

def get_upnp_info(control, namespace, action):
    global g_device, g_version

    headers = {
        'User-agent':   'Check_MK agent_fritzbox',
        'Content-Type': 'text/xml',
        'SoapAction':   namespace + '#' + action,
    }

    data = '''<?xml version='1.0' encoding='utf-8'?>
    <s:Envelope s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/' xmlns:s='http://schemas.xmlsoap.org/soap/envelope/'>
        <s:Body>
            <u:%s xmlns:u="%s" />
        </s:Body>
    </s:Envelope>''' % (action, namespace)

    # Fritz!Box with firmware >= 6.0 use a new url. We try the newer one first and
    # try the other one, when the first one did not succeed.
    for base_url in base_urls[:]:
        url = base_url + '/control/' + control
        try:
            if opt_debug:
                sys.stdout.write('============================\n')
                sys.stdout.write('URL: %s\n' % url)
                sys.stdout.write('SoapAction: %s\n' % headers['SoapAction'])
            req = urllib2.Request(url, data, headers)
            handle = urllib2.urlopen(req)
            break # got a good response
        except urllib2.HTTPError, e:
            if e.code == 500:
                # Is the result when the old URL can not be found, continue in this
                # case and revert the order of base urls in the hope that the other
                # url gets a successful result to have only one try on future requests
                # during an agent execution
                base_urls.reverse()
                continue
        except Exception, e:
            if opt_debug:
                sys.stdout.write('----------------------------\n')
                sys.stdout.write(traceback.format_exc())
                sys.stdout.write('============================\n')
            raise RequestError('Error during UPNP call')

    infos    = handle.info()
    contents = handle.read()

    parts = infos['SERVER'].split("UPnP/1.0 ")[1].split(' ')
    g_device  = ' '.join(parts[:-1])
    g_version = parts[-1]

    if opt_debug:
        sys.stdout.write('----------------------------\n')
        sys.stdout.write('Server: %s\n' % infos['SERVER'])
        sys.stdout.write('----------------------------\n')
        sys.stdout.write(contents + '\n')
        sys.stdout.write('============================\n')

    # parse the response body
    match = re.search('<u:%sResponse[^>]+>(.*)</u:%sResponse>' % (action, action), contents, re.M | re.S)
    if not match:
        raise APIError('Response is not parsable')
    response = match.group(1)
    matches = re.findall('<([^>]+)>([^<]+)<[^>]+>', response, re.M | re.S)

    attrs = {}
    for key, val in matches:
        attrs[key] = val

    if opt_debug:
        sys.stdout.write('Parsed: %s\n' % pprint.pformat(attrs))

    return attrs

try:
    status = {}
    for control, namespace, action in [
        ('WANIPConn1', 'urn:schemas-upnp-org:service:WANIPConnection:1', 'GetStatusInfo'),
        ('WANIPConn1', 'urn:schemas-upnp-org:service:WANIPConnection:1', 'GetExternalIPAddress'),
        ('WANCommonIFC1', 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1', 'GetAddonInfos'),
        ('WANCommonIFC1', 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1', 'GetCommonLinkProperties'),
        ('WANDSLLinkC1', 'urn:schemas-upnp-org:service:WANDSLLinkConfig:1', 'GetDSLLinkInfo'),
        ]:
        try:
            status.update(get_upnp_info(control, namespace, action))
        except:
            if opt_debug:
                raise

    sys.stdout.write('<<<check_mk>>>\n')
    sys.stdout.write('Version: %s\n' % g_version)
    sys.stdout.write('AgentOS: %s\n' % g_device)

    sys.stdout.write('<<<fritz>>>\n')
    for key, value in status.items():
        sys.stdout.write('%s %s\n' % (key, value))

except:
    if opt_debug:
        raise
    sys.stderr.write('Unhandled error: %s' % traceback.format_exc())
