class RouterBase(object):
    """Base router controller class.

    All other controller classes inherit from this one,
    (hopefully) implementing all of its non-implemented methods.
    """

    def __init__(self, host, port, username, password):
        self._host = host
        self._port = int(port)
        self._username = username
        self._password = password

        self._requests_cache = {}
        self._is_first_request = True

    @property
    def host(self):
        return self._host

    @property
    def port(self):
        return self._port

    @property
    def username(self):
        return self._username

    @property
    def password(self):
        return self._password

    @property
    def url_base(self):
        """The base URL to which all requests would be relative to.

        Subclasses may wish to override this to make things
        more convenient.
        """
        return 'http://%s:%d/' % (self.host, self.port)

    def __not_implemented(self):
        raise NotImplementedError('Method needs to be implemented in a subclass.')

    def get_router_info(self):
        """Returns a :class:`roscraco.response.RouterInfo` object
        with system information about the router (hardware, firmware) version.
        """
        self.__not_implemented()

    def get_mac_address(self):
        """Returns the MAC address of the router in our so called
        generic format - lowercase HEX, no separators.
        """
        self.__not_implemented()

    def get_dns_servers(self):
        """Returns a list of IP addresses of the detected DNS servers."""
        self.__not_implemented()

    def get_pppoe_online_time(self):
        """Returns the time in seconds since the PPPoE connection
        was made.

        Returns None if such information is not available
        """
        self.__not_implemented()

    def get_uptime(self):
        """Returns the time in seconds since the router was last rebooted."""
        self.__not_implemented()

    def get_traffic_stats(self):
        """Returns a :class:`roscraco.response.TrafficStats` object
        with traffic statistics for the WAN interface.
        """
        self.__not_implemented()

    def get_connected_clients_list(self):
        """Returns a :class:`roscraco.response.ConnectedClientsList` object
        with information about clients that are current connected to the
        router, or have been connected to it recently.
        """
        self.__not_implemented()

    def get_dmz_settings(self):
        """Returns a :class:`roscraco.response.DMZSettings` object
        with DMZ settings information about the router.
        """
        self.__not_implemented()

    def push_dmz_settings(self, settings):
        """Updates the DMZ settings and returns True on success.

        :param settings: the DMZSettings object
        """
        self.__not_implemented()

    def get_dhcp_settings(self):
        """Returns a :class:`roscraco.response.DHCPServerSettings` object
        with information about the current status of the DHCP server
        (on/off, supported/non-supported, IP range, etc.).
        """
        self.__not_implemented()

    def get_addr_reservation_list(self):
        """Returns a :class:`roscraco.response.DHCPReservationList`
        with information about the current static entries in the DHCP
        reservation list.
        """
        self.__not_implemented()

    def push_addr_reservation_list(self, lst_new):
        """Updates the DHCP address reservation settings and returns True
        on success.

        :param lst_new: the DHCPReservationList object
        """
        self.__not_implemented()

    def get_wireless_settings(self):
        """Returns a :class:`roscraco.response.WirelessSettings` object
        with information about the current status of wireless connectivity
        for this router.
        """
        self.__not_implemented()

    def push_wireless_settings(self, settings):
        """Updates the wireless settings and returns True on success.

        :param settings: the WirelessSettings object
        """
        self.__not_implemented()

    @property
    def supports_reboot(self):
        """Tells whether the device can be rebooted from the web interface."""
        self.__not_implemented()

    def reboot(self):
        """Reboots the device."""
        self.__not_implemented()

    def confirm_identity(self):
        """Confirms that the router we're trying to access
        is of the expected type/model.

        If it's not it will throw an exception of type
        :class:`roscraco.exception.RouterIdentityError`.
        """
        self.__not_implemented()

    def invalidate_cache(self):
        """Clears any cached data that may have been gathered
        and starts clean.
        """
        self._invalidate_http_cache()

    def _invalidate_http_cache(self):
        """Clears the http requests cache."""
        self._requests_cache = {}

    def _make_http_request_read(self, path):
        """Makes an HTTP request to the given path.

        The path is prefixed with the base URL,
        obtained from :attr:`roscraco.router.RouterBase.url_base`.

        Read requests to the same URL are cached, until a write
        request comes along, which invalidates the cache.
        """
        url = self.url_base + path
        if url not in self._requests_cache:
            self._requests_cache[url] = self._perform_http_request(url)[2]
        return self._requests_cache[url]

    def _make_http_request_write(self, path, *args, **kwargs):
        """Makes a "write" HTTP request to the given path.

        ``Write`` here means that the request is potentially
        changing some data, so we're invalidating the http
        requests cache.

        Such requests sometimes use the POST request method,
        although not necessarily.
        """
        url = self.url_base + path
        self._invalidate_http_cache()
        return self._perform_http_request(url, *args, **kwargs)[2]

    def _handle_first_request(self):
        """To be implemented in a subclass
        (if it needs to do something special on the first request).
        """
        pass

    def _perform_http_request(self, url, data=None, headers=(), timeout=7.0):
        """Makes the actual HTTP request and returns the result.

        The result is a 3-tuple:
            - requested URL
            - info() - meta information, such as headers
            - contents
        """
        if self._is_first_request:
            self._is_first_request = False
            self._handle_first_request()

        if data is not None:
            if isinstance(data, dict) or isinstance(data, list):
                data = urlencoder.urlencode(data)
            else:
                raise RouterFetchError(
                    'POST data should be a dict, a list or None!'
                )

        try:
            req = requestor.Request(url, data)
            for header, value in headers:
                req.add_header(header, value)
            with contextlib.closing(requestor.urlopen(req, timeout=timeout)) as handle:
                self._is_logged_in = True
                return (
                    handle.geturl(),
                    handle.info(),
                    handle.read().decode('utf-8', 'ignore')
                )
        except Exception as e:
            raise RouterFetchError('Failed making request: %s' % repr(e))

    def _prepare_base64_auth_string(self):
        auth_string = '{0}:{1}'.format(self.username, self.password)
        if sys.version_info[0] >= 3:
            auth_string = bytes(auth_string, 'ascii')
        encoded = base64.b64encode(auth_string)
        if sys.version_info[0] >= 3:
            encoded = encoded.decode('ascii')
        return encoded

    def close(self):
        """Performs cleanup, logout or whatever the device requires."""

    def __del__(self):
        try:
            self.close()
        except Exception:
            # close() may fail if __init__ didn't complete
            pass WEP_CONSTRAINTS_MAP = {
    64: {'ascii': 5, 'hex': 10},
    128: {'ascii': 13, 'hex': 26},
    152: {'ascii': 16, 'hex': 32}
}
def is_valid_mac_address(mac):
def is_valid_mac_address_normalized(mac):
def is_valid_ip_address(ip):
def _is_hex_string(string):
def is_valid_wpa_psk_password(password):
def is_valid_wep_password(password, bit_length):
def is_wep_password_in_hex(password, bit_length):
def is_valid_ssid(ssid):
def is_ip_in_range(ip, ip_range_start, ip_range_end): def ip2long(ip_addr):
def long2ip(ip):
def normalize_mac(mac): class RouterFetchError(RouterError):
    pass class RouterParseError(RouterError):
    pass class RouterIdentityError(RouterError):
    pass class RouterInfo(object):
    """Represents some system information about the router,
    such as hardware and firmware version.
    """

    def __init__(self):
        self._hardware_version = None
        self._firmware_version = None

    def set_hardware_version(self, value):
        self._hardware_version = value

    @property
    def hardware_version(self):
        return self._hardware_version

    def set_firmware_version(self, value):
        self._firmware_version = value

    @property
    def firmware_version(self):
        return self._firmware_version class DMZSettings(object):
    """Represents the DMZ settings for a router."""

    #: List of properties to export using export()
    PROPERTIES = (
        'is_enabled', 'ip'
    )

    def __init__(self):
        self._is_supported = False
        self._is_enabled = False
        self._ip = None
        self._changes_require_reboot = True

    def set_reboot_requirement_status(self, value):
        self._changes_require_reboot = bool(value)

    @property
    def changes_require_reboot(self):
        """Tells whether the router needs rebooting
        for changes to take effect.
        """
        return self._changes_require_reboot

    @property
    def is_supported(self):
        """Tells whether the router supports DMZ."""
        return self._is_supported

    def set_supported_status(self, value):
        self._is_supported = bool(value)

    @property
    def is_enabled(self):
        """Tells whether the DMZ feature is enabled."""
        return self._is_enabled

    def set_enabled_status(self, value):
        self._is_enabled = bool(value)

    @property
    def ip(self):
        """The IP address of the DMZ host."""
        return self._ip

    def set_ip(self, value):
        self._ip = value

    def validate(self):
        errors = {}
        if not validator.is_valid_ip_address(self.ip):
            errors['ip'] = 'Invalid IP: %s' % self.ip
        return errors

    def ensure_valid(self):
        errors = self.validate()
        if len(errors) != 0:
            raise RouterSettingsError(str(errors))

    def __eq__(self, other):
        return self.ip == other.ip and self.is_enabled == other.is_enabled

    def __ne__(self, other):
        return not self == other

    def __hash__(self):
        return id(self)

    def __repr__(self):
        mode = 'ENABLED' if self.is_enabled else 'DISABLED'
        return '<%s: %s/%s>' % (self.__class__, mode, self.ip)

    def export(self):
        """Exports the most important settings attributes,
        omitting any internal attributes.
        """
        export = {}
        for attr in self.__class__.PROPERTIES:
            export[attr] = getattr(self, attr, None)
        return export class TrafficStats(object):
    """Represents traffic statistics information for a given interface."""

    def __init__(self, bytes_recv, bytes_sent, packets_recv, packets_sent):
        self._bytes_recv = bytes_recv
        self._bytes_sent = bytes_sent
        self._packets_recv = packets_recv
        self._packets_sent = packets_sent

    @property
    def bytes_recv(self):
        return self._bytes_recv

    @property
    def bytes_sent(self):
        return self._bytes_sent

    @property
    def packets_recv(self):
        return self._packets_recv

    @property
    def packets_sent(self):
        return self._packets_sent class WirelessSettings(object):
    """Represents all available Wireless settings for a router."""

    SECURITY_TYPE_NONE = 'none'
    SECURITY_TYPE_WEP64 = 'wep64'
    SECURITY_TYPE_WEP128 = 'wep128'
    SECURITY_TYPE_WPA = 'wpa'
    SECURITY_TYPE_WPA2 = 'wpa2'

    #: List of properties to export using export()
    PROPERTIES = (
        'security_type', 'ssid', 'is_enabled', 'is_broadcasting_ssid',
        'channel', 'password'
    )

    def __init__(self):
        self._supports_wireless = True
        self._ssid = None
        self._enabled_status = True
        self._ssid_broadcast_status = True
        self._channel = None
        self._password = None
        self._internal_params = {}
        self._supported_security_types = set([self.__class__.SECURITY_TYPE_NONE])
        self._security_type = None
        self._supports_ascii_wep_passwords = True
        self._supports_auto_channel = True
        self._changes_require_reboot = True

    def set_auto_channel_support(self, value):
        self._supports_auto_channel = bool(value)

    @property
    def supports_auto_channel(self):
        """Tells whether auto channel is supported.

        Channel 0 is considered the auto channel, because that's
        how most routers represent the ``Auto`` value.

        Some devices, however, do not support Auto channel at all.
        """
        return self._supports_auto_channel

    def add_security_support(self, security_type):
        """Adds a new security type to the list of supported
        security types.
        """
        self._supported_security_types.add(security_type)

    @property
    def supported_security_types(self):
        return self._supported_security_types

    def set_security_type(self, security_type):
        self._security_type = security_type

    @property
    def security_type_is_wep(self):
        """Tells whether the current security type is WEP.

        Returns true for both WEP64 and WEP128.
        """
        return self._security_type in (self.__class__.SECURITY_TYPE_WEP64, self.__class__.SECURITY_TYPE_WEP128)

    @property
    def security_type_is_wpa(self):
        """Tells whether the current security type is WPA.

        Returns true for both WPA and WPA2.
        """
        return self._security_type in (self.__class__.SECURITY_TYPE_WPA, self.__class__.SECURITY_TYPE_WPA2)

    @property
    def security_type(self):
        return self._security_type

    def set_reboot_requirement_status(self, value):
        self._changes_require_reboot = bool(value)

    @property
    def changes_require_reboot(self):
        """Tells whether the router needs rebooting
        for changes to take effect.
        """
        return self._changes_require_reboot

    def set_support_status(self, value):
        self._supports_wireless = bool(value)

    @property
    def is_supported(self):
        """Tells whether the router supports wireless (most of them do)."""
        return self._supports_wireless

    def set_ssid(self, value):
        self._ssid = value

    @property
    def ssid(self):
        """The current SSID (wireless network name)."""
        return self._ssid

    def set_enabled_status(self, value):
        self._enabled_status = bool(value)

    @property
    def is_enabled(self):
        return self._enabled_status

    def set_ssid_broadcast_status(self, value):
        self._ssid_broadcast_status = bool(value)

    @property
    def is_broadcasting_ssid(self):
        """Tells whether the SSID status is being broadcasted publicly.

        If it is, than the network is publicly visible by anyone.
        """
        return self._ssid_broadcast_status

    def set_channel(self, value):
        self._channel = int(value)

    @property
    def channel(self):
        """The transmission channel for wireless communications."""
        return self._channel

    def set_password(self, value):
        self._password = value

    @property
    def password(self):
        """The current password for the given security type.

        The password is sometimes None for some routers, to indicate
        that the password cannot be determined.
        Some routers hide the current password from their web-interface,
        so we can't detect it (but that doesn't mean that we can't change it
        with a new one).
        """
        return self._password

    @property
    def is_wep_password_in_hex(self):
        """Tells whether the current WEP password is in HEX or in ASCII.

        Detecting this allows us to set the ASCII/HEX
        field in the management interface automatically.
        """
        if not self.security_type_is_wep:
            raise RouterSettingsError('Not using WEP, but trying to inspect password!')
        bit_length = 128 if self.security_type == self.__class__.SECURITY_TYPE_WEP128 else 64
        return validator.is_wep_password_in_hex(self.password, bit_length)

    def set_ascii_wep_password_support_status(self, value):
        self._supports_ascii_wep_passwords = bool(value)

    @property
    def supports_ascii_wep_passwords(self):
        """Tells whether the current router supports ASCII passwords
        for WEP security.

        Some devices only support HEX passwords.
        """
        return self._supports_ascii_wep_passwords

    def set_internal_param(self, key, value):
        self._internal_params[key] = value

    def get_internal_param(self, key):
        return self._internal_params[key] if key in self._internal_params else None

    def validate(self):
        errors = {}

        if not validator.is_valid_ssid(self.ssid):
            errors['ssid'] = 'Invalid SSID: %s' % self.ssid

        # most routers use channel 0 as the 'Auto' channel
        channel_min = 0 if self.supports_auto_channel else 1
        if not (channel_min <= self.channel <= 13):
            errors['channel'] = 'Invalid channel %d' % self.channel

        if self.security_type not in self._supported_security_types:
            errors['security_type'] = 'Invalid security type: %s' % self.security_type
        else:
            result = self.__validate_password()
            if result is not None:
                errors['password'] = result

        return errors

    def ensure_valid(self):
        errors = self.validate()
        if len(errors) != 0:
            raise RouterSettingsError(str(errors))

    def __validate_password(self):
        if self.security_type in (self.__class__.SECURITY_TYPE_WPA, self.__class__.SECURITY_TYPE_WPA2):
            if not validator.is_valid_wpa_psk_password(self.password):
                return 'Invalid WPA PSK password: %s' % self.password

        if self.security_type in (self.__class__.SECURITY_TYPE_WEP64, self.__class__.SECURITY_TYPE_WEP128):
            bit_length = 128 if self.security_type == self.__class__.SECURITY_TYPE_WEP128 else 64
            if not validator.is_valid_wep_password(self.password, bit_length):
                return 'Invalid WEP password for bit length %d: %s' % (bit_length, self.password)

            # Some devices only support HEX values for the WEP password field
            if not self.supports_ascii_wep_passwords and not self.is_wep_password_in_hex:
                return 'ASCII WEP passwords are not supported!'

        return None

    def eq(self, other, skip_attrs=()):
        # WEP passwords that use HEX are not case-sensitive, so we want
        # to validate them separately
        if self.security_type_is_wep and other.security_type_is_wep and \
           self.is_wep_password_in_hex and other.is_wep_password_in_hex:
            skip_attrs = skip_attrs + ('password',)
            try:
                if self.password.lower() != other.password.lower():
                    return False
            except AttributeError:
                return False

        # Don't try to compare passwords when there's no security type
        if self.security_type == self.__class__.SECURITY_TYPE_NONE and \
           other.security_type == self.__class__.SECURITY_TYPE_NONE:
               skip_attrs = skip_attrs + ('password',)

        for attr in self.__class__.PROPERTIES:
            if attr in skip_attrs:
                continue
            if getattr(self, attr, None) != getattr(other, attr, None):
                #print('[%s] %s != %s' % (
                #               attr,
                #               getattr(self, attr, None),
                #               getattr(other, attr, None)
                #))
                return False

        return True

    def __eq__(self, other):
        return self.eq(other)

    def __ne__(self, other):
        return not self == other

    def __hash__(self):
        return id(self)

    def export(self):
        """Exports the most important settings attributes,
        omitting any internal attributes.
        """
        export = {}
        for attr in self.__class__.PROPERTIES:
            export[attr] = getattr(self, attr, None)
        return export class DHCPReservationList(list):
    """A response class representing the router's
    address reservation list (static DHCP entries).
    """

    def __init__(self, *args, **kwargs):
        list.__init__(self, *args, **kwargs)
        self._supports_reservations = True
        self._changes_require_reboot = True
        self._internal_params = {}

    def set_reservation_support_status(self, value):
        self._supports_reservations = bool(value)

    @property
    def supports_reservations(self):
        """Tells whether address reservation is supported
        by the given device.
        """
        return self._supports_reservations

    def set_reboot_requirement_status(self, value):
        self._changes_require_reboot = bool(value)

    @property
    def changes_require_reboot(self):
        """Tells whether changing address reservation settings
        requires a device reboot for changes to take effect.
        """
        return self._changes_require_reboot

    def set_internal_param(self, key, value):
        """Sets some internal information to be 'carried' with this instance.

        This information may be needed when pushing new settings.
        """
        self._internal_params[key] = value

    def get_internal_param(self, k):
        """Retrieves the internal information
        set by :meth:`set_internal_param`.
        """
        return self._internal_params[k] if k in self._internal_params else None

    def has_ip(self, ip):
        """Tells whether the given IP address has an entry
        in the DHCP reservation list.
        """
        for item in self:
            if item.ip == ip:
                return True
        return False

    def has_ip_mac_entry(self, ip, mac):
        """Tells whether there's an entry that matches both
        the given IP and MAC address.
        """
        for item in self:
            if item.ip == ip and item.mac == mac:
                return True
        return False

    def validate(self):
        for item in self:
            errors = item.validate()
            if len(errors) != 0:
                return errors
        return {}

    def ensure_valid(self):
        for item in self:
            item.ensure_valid()

    def __eq__(self, other):
        if len(self) != len(other):
            return False

        for idx, item in enumerate(self):
            if item != other[idx]:
                return False
        return True

    def __ne__(self, other):
        return not self == other

    def __hash__(self):
        return id(self)

    def export(self):
        """Exports the most important settings attributes,
        omitting any internal attributes.
        """
        export = {}
        for i, item in enumerate(self):
            export['entry_%d' % i] = item.export()
        return export class DHCPReservationListItem(object):
    """Represents a single entry in the DHCP address reservation table."""

    #: Properties to export()
    PROPERTIES = (
        'mac', 'ip'
    )

    def __init__(self):
        self._mac_address = None
        self._ip_address = None
        self._is_enabled = True

    def set_mac(self, value):
        self._mac_address = converter.normalize_mac(value)

    @property
    def mac(self):
        return self._mac_address

    def set_ip(self, value):
        self._ip_address = value

    @property
    def ip(self):
        return self._ip_address

    def set_enabled_status(self, value):
        self._is_enabled = bool(value)

    @property
    def is_enabled(self):
        return self._is_enabled

    def validate(self):
        errors = {}
        if not validator.is_valid_ip_address(self.ip):
            errors['ip'] = 'Invalid IP address: %s' % self.ip
        if not validator.is_valid_mac_address_normalized(self.mac):
            errors['mac'] = 'Invalid normalized MAC address: %s' % self.mac
        return errors

    def ensure_valid(self):
        errors = self.validate()
        if len(errors) != 0:
            raise RouterSettingsError(str(errors))

    def __repr__(self):
        return '<%s: %s; %s>' % (self.__class__, self._ip_address, self._mac_address)

    def __eq__(self, other):
        for attr in ('ip', 'mac', 'is_enabled'):
            if getattr(self, attr, None) != getattr(other, attr, None):
                return False
        return True

    def __ne__(self, other):
        return not self == other

    def __hash__(self):
        return id(self)

    def export(self):
        """Exports the most important settings attributes,
        omitting any internal attributes.
        """
        export = {}
        for attr in self.__class__.PROPERTIES:
            export[attr] = getattr(self, attr, None)
        return export class DHCPServerSettings(object):
    """Represents the DHCP server settings.

    This is important, because it gives us information on the IP
    range in which addresses can be reserved
    (if DHCP is supported/enabled at all).
    """

    def __init__(self):
        self._ip_start = '192.168.1.100'
        self._ip_end = '192.168.1.199'
        self._is_enabled = True

    @property
    def ip_start(self):
        return self._ip_start

    def set_ip_start(self, value):
        self._ip_start = value

    @property
    def ip_end(self):
        return self._ip_end

    def set_ip_end(self, value):
        self._ip_end = value

    @property
    def is_enabled(self):
        return self._is_enabled

    def set_enabled_status(self, value):
        self._is_enabled = bool(value)

    def validate(self):
        errors = {}

        if not validator.is_valid_ip_address(self._ip_start):
            errors['ip_start'] = 'Invalid start IP: %s' % self._ip_start

        if not validator.is_valid_ip_address(self._ip_end):
            errors['ip_end'] = 'Invalid end IP: %s' % self._ip_end

        if converter.ip2long(self._ip_start) > converter.ip2long(self._ip_end):
            errors['ip_range'] = 'Invalid IP range: from %s to %s' % (self._ip_start, self._ip_end)

        return errors

    def ensure_valid(self):
        errors = self.validate()
        if len(errors) != 0:
            raise RouterSettingsError(str(errors)) class ConnectedClientsList(list):
    """A response class representing the clients connected
    to the router at this time (or that have been recently connected).
    """
    pass class ConnectedClientsListItem(object):
    """A client entry in the :class:`ConnectedClientsList`."""

    LEASE_TIME_PERMANENT = 'Permanent'

    def __init__(self):
        self._client_name = None
        self._mac_address = None
        self._ip_address = None
        self._lease_time = None

    def set_client_name(self, value):
        self._client_name = value

    @property
    def client_name(self):
        return self._client_name

    def set_mac(self, value):
        self._mac_address = value

    @property
    def mac(self):
        return self._mac_address

    def set_ip(self, value):
        self._ip_address = value

    @property
    def ip(self):
        return self._ip_address

    def set_lease_time(self, value):
        self._lease_time = value

    @property
    def lease_time(self):
        return self._lease_time

    @property
    def is_permanent_lease(self):
        return (self._lease_time == self.__class__.LEASE_TIME_PERMANENT)

    def __repr__(self):
        return '<%s: %s; %s>' % (self.__class__, self._client_name, self._ip_address)
import re
import base64
from roscraco.router.base import RouterBase
from roscraco.helper import validator, converter
from roscraco.exception import RouterFetchError, RouterParseError, \
     RouterIdentityError
from roscraco.response import RouterInfo, TrafficStats, DMZSettings, \
     ConnectedClientsListItem, ConnectedClientsList, WirelessSettings, \
     DHCPReservationList, DHCPReservationListItem, DHCPServerSettings

        settings = _parse_wireless_settings(*args)
        # Only v7 doesn't support auto channel
        settings.set_auto_channel_support(False)

        return settings

    def push_wireless_settings(self, settings):
        # It's not really confirmed, but pushing advanced before basic looks better
        # because basic restarts the router shortly after it's being changed
        # i can't say the same about advanced though
        data_advanced = _generate_wireless_advanced_settings_WGR614v7(settings)
        data_basic = _generate_wireless_basic_settings_WGR614v9(settings)

        # Shortly after we push settings, the router reboots automatically
        # so the settings we try to push last may not make it,
        # especially if the connection is laggy
        self._make_http_request_write('wlg_adv.cgi', data_advanced)
        self._make_http_request_write('wireless.cgi', data_basic)

        # the router reboots automatically very soon
        # we can't make sure everything went fine..
        return True


def _parse_router_info(html):
    obj = RouterInfo()

    regex_hardware = re.compile('<b>Hardware Version</b>(?:.+?)>([a-zA-Z0-9]+)</td>', re.DOTALL)
    match_object = regex_hardware.search(html)
    if match_object is None:
        raise RouterParseError('Cannot parse hardware version')
    obj.set_hardware_version(match_object.group(1))

    regex_firmware = re.compile('<b>Firmware Version </b>(?:.+?)>([a-zA-Z0-9\s\.\(\)_]+)</td>', re.DOTALL)
    match_object = regex_firmware.search(html)
    if match_object is None:
        raise RouterParseError('Cannot parse firmware version')
    obj.set_firmware_version(match_object.group(1))

    return obj


def _parse_uptime_to_seconds(html):
    """Parses an uptime string such as ` 15 days 10:11:12` or `10:11:12` to seconds."""

    regex = re.compile('<!>(?: ([0-9]+) days? )?([0-9]+):([0-9]+):([0-9]+)<!>')
    match_object = regex.search(html)
    if match_object is None:
        raise RouterParseError('Cannot match uptime string')

    days, hours, minutes, seconds = map(lambda x: int(x) if x is not None else 0, match_object.groups())

    return days * 86400 + hours * 3600 + minutes * 60 + seconds


def _parse_traffic_stats(html):
    # Sadly, we can only get packets count information.. no size stats available

    regex = re.compile('WAN</span>(?:.+?)([0-9]+)</span>(?:.+?)([0-9]+)</span>', re.DOTALL)
    match_object = regex.search(html)
    if match_object is None:
        raise RouterParseError('Cannot find traffic stats information')

    packets_sent, packets_recv = map(int, match_object.groups())

    return TrafficStats(0, 0, packets_recv, packets_sent)


def _parse_pppoe_online_time(html):
    # <TD NOWRAP width="50%">23:05:47</td>
    # or
    # <TD NOWRAP width="50%"> 1 day 00:41:36</td>
    # or
    # <TD NOWRAP width="50%"> 10 days 00:41:36</td>

    regex = re.compile('<B>Connection Time</B>(?:.+?)>(?: ([0-9]+) days? )?([0-9]+):([0-9]+):([0-9]+)</td>', re.DOTALL)
    match_object = regex.search(html)
    if match_object is None:
        raise RouterParseError('Cannot parse online time information')

    days, hours, minutes, seconds = map(lambda x: int(x) if x is not None else 0, match_object.groups())

    return days * 86400 + hours * 3600 + minutes * 60 + seconds


def _parse_mac_address(html):
    regex = re.compile('Internet Port(?:.+?)MAC Address </b></td>(?:.+?)>(.+?)</td>', re.DOTALL)
    match_object = regex.search(html)
    if match_object is None:
        raise RouterParseError('Cannot parse MAC address information')

    return converter.normalize_mac(match_object.group(1))


def _parse_dns_servers(html):
    regex = re.compile('Domain Name Server <!><br></b></td>(?:.+?)>(.+?)</td>', re.DOTALL)
    match_object = regex.search(html)
    if match_object is None:
        raise RouterParseError('Cannot parse DNS servers information')

    ips = match_object.group(1) #<br> separated list of IPs
    return [ip for ip in ips.split('<br>') if validator.is_valid_ip_address(ip)]


def _parse_connected_clients_list(html):
    lst = ConnectedClientsList()

    regex = "<tr>(?:.+?)<span class=\"ttext\">(.+?)</span>(?:.+?)<span class=\"ttext\">(.+?)</span>(?:.+?)<span class=\"ttext\">(.+?)</span>(?:.+?)</tr>"

    for ip, name, mac in re.compile(regex, re.DOTALL).findall(html):
        if ip == '--':
            # I've seen such entries on WGR614v7 only.. let's ignore them
            continue

        if not validator.is_valid_ip_address(ip):
            raise RouterParseError('Invalid IP address: %s' % ip)

        item = ConnectedClientsListItem()
        item.set_client_name(name)

        item.set_mac(converter.normalize_mac(mac))