# Copyright 3D Control Systems, Inc. All Rights Reserved 2017-2025.
# Built in San Francisco.

# This software is distributed under a commercial license for personal,
# educational, corporate or any other use.
# The software as a whole or any parts of it is prohibited for distribution or
# use without obtaining a license from 3D Control Systems, Inc.

# All software licenses are subject to the 3DPrinterOS terms of use
# (available at https://www.3dprinteros.com/terms-and-conditions/),
# and privacy policy (available at https://www.3dprinteros.com/privacy-policy/)

import json
import ipaddress
import socket
import pprint
import time

import requests
import findssh
try:
    import findssh.coro
except:
    findssh.coro = None

import base_scanner

requests.packages.urllib3.disable_warnings()

class HTTPDetector(base_scanner.Scanner):

    PORT = 80
    TIMEOUT = 2
    INIT_SCAN_TIMEOUT = 0.02
    FINDSSH_SERVICE = "http"
    PATH = "/"
    RESPONSE_FIELD_NAME = ""
    RESPONSE_TEXT_MATCH = ""
    METHOD = "GET"
    REQ_BODY = None
    REQ_HEADERS = None
    RESP_MATCH_IS_SNR = False
    PASS_CACHE_ATTR_NAME = 'cached_http_hosts'

    def __init__(self, parent=None, profile=None, conn=None):
        super().__init__(parent, profile, conn)
        self.cached_http_hosts = []

    def process_hosts(self, hosts):
        matched_hosts = {}
        for host in hosts:
            host_ip = str(host[0])
            if self.parent_stop():
                break
            match = self.check_ip(host_ip)
            if match:
                matched_hosts[host_ip] = match
        return matched_hosts

    def request(self, host):
        if self.port in (443, 8443):
            proto = "https://"
        else:
            proto = "http://"
        url = proto + host
        if self.port not in (80, 443):
            url += ":" + str(self.port)
        if self.PATH:
            url += self.PATH
        kwargs = {}
        if self.REQ_BODY:
            kwargs['data'] = self.REQ_BODY
        if self.REQ_HEADERS:
            kwargs['headers'] = self.REQ_HEADERS
        try:
            #self.logger.debug(f"Requesting {host}")
            resp = requests.request(self.METHOD, url, timeout=self.TIMEOUT, verify=False, **kwargs)
            #self.logger.debug(f"Response {host}: " + str(resp))
            return resp.text
        except:
            pass
        finally:
            try:
                resp.close()
            except:
                pass

    def check_ip(self, host):
        resp_body = self.request(host)
        if resp_body:
            #self.logger.debug(f"Response from {host}: " + resp_body)
            if self.RESPONSE_FIELD_NAME:
                try:
                    decoded = json.loads(resp_body)
                    if type(decoded) != dict:
                        raise TypeError("Response is not json dictionary")
                except (ValueError, TypeError) as e:
                    pass
                    #self.logger.debug(f"HTTP detector got non json response from {host}: {e}")
                else:
                    #self.logger.debug('Response: %s', pprint.pformat(decoded))
                    return decoded.get(self.RESPONSE_FIELD_NAME)
            elif self.RESPONSE_TEXT_MATCH:
                self.logger.debug("Searching for text match in the response from " + str(host))
                if self.RESPONSE_TEXT_MATCH in resp_body:
                    self.logger.debug("Found")
                    return self.RESPONSE_TEXT_MATCH
                self.logger.debug("Not found")
            else:
                self.logger.error('Invalid use of HTTPDetector. Ether RESPONSE_FIELD_NAME or RESPONSE_FIELD_NAME class attributes should be set, but none is.')

    def parent_stop(self):
        return self.parent and self.parent.stop_flag

    def get_all_http_hosts(self):
        hosts = []
        for interface in self.interfaces:
            try:
                network = findssh.netfromaddress(ipaddress.ip_address(interface['ip']), interface['prefix'])
            except:
                self.logger.error(f'Error on getting network from ip from {interface}')
            else:
                exception_logged = False
                try:
                    result = findssh.runner(findssh.coro.get_hosts, network, self.port, self.FINDSSH_SERVICE, self.TIMEOUT)
                except:
                    hosts = []
                else:
                    for host, _ in result:
                        hosts.append((host, self.port))
                if not hosts:
                    for host in network.hosts():
                        try:
                            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                                host = host.exploded
                                s.settimeout(self.INIT_SCAN_TIMEOUT)
                                if not s.connect_ex((host, self.port)):
                                    hosts.append((host, self.port))
                        except socket.gaierror:
                            pass
                        except Exception:
                            if not exception_logged:
                                self.logger.exception(f'Exception on scanning network {network}')
                self.logger.info(f"All HTTP hosts on port {self.PORT} of {network}: " + str(hosts))
        self.cached_http_hosts = hosts
        return hosts

    def detect(self, already_know_ip=None, non_default_port=None):
        if non_default_port:
            self.port = non_default_port
        else:
            self.port = self.PORT
        hosts = []
        if already_know_ip:
            hosts = [(already_know_ip, self.port)]
        elif self.cached_http_hosts and self.port == HTTPDetector.PORT:
            hosts = self.cached_http_hosts
        else:
            hosts = self.get_all_http_hosts()
        self.logger.info("All detected HTTP hosts: " + str(hosts))
        discovered_printers = []
        for ip, host_match_data in self.process_hosts(hosts).items():
            discovered_printers.append(self.parse_matched_hosts(ip, host_match_data))
        self.discovered_printers = discovered_printers
        return discovered_printers

    def parse_matched_hosts(self, ip, match_data):
        printer = {'IP': ip}
        if isinstance(match_data, dict):
            printer.update(match_data)
        elif isinstance(match_data, str) and self.RESP_MATCH_IS_SNR:
            printer['SNR'] = match_data
        return printer


if __name__ == "__main__":

    detector = HTTPDetector(None)
    pprint.pprint(detector.detect())
