# -*- coding: utf-8 -*-

"""
    This program 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, either version 3 of the License.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

    Development Team: Brain Storm Team
"""

import re
from src.core import filesystem
from src.core import helper


class ResponseProvider(object):
    """ ResponseProvider class"""

    HTTP_DBG_LEVEL = 3
    DEFAULT_WAF_STATUS = 'blocked'

    DEFAULT_WAF_BLOCK_STATUSES = [
        401, 403, 405, 406, 409, 412, 415, 418, 423, 429, 430, 451,
        493, 494, 495, 496, 497, 499, 500, 501, 502, 503, 511,
        520, 521, 522, 523, 524, 525, 526, 527, 530
    ]

    DEFAULT_WAF_SIGNATURES = [
        {
            'name': 'Anubis',
            'confidence': 95,
            'strong_header_markers': [
                'techaro.lol-anubis-cookie-verification',
                'techaro.lol-anubis-auth=;',
            ],
            'header_markers': [
                'server: anubis',
            ],
            'strong_body_markers': [
                'making sure you\'re not a bot',
                'proof-of-work',
            ],
            'body_markers': [
                'anubis',
                'proof-of-work',
                'making sure you\'re not a bot',
            ],
        },
        {
            'name': 'Cloudflare',
            'confidence': 92,
            'strong_header_markers': [
                'cf-ray',
                'cf-mitigated: challenge',
            ],
            'header_markers': [
                'cf-chl-',
                'server: cloudflare',
                '__cf_bm',
                'cf-cache-status',
            ],
            'strong_body_markers': [
                'checking your browser before accessing',
                'cf-browser-verification',
                'attention required! | cloudflare',
            ],
            'body_markers': [
                'checking your browser',
                'checking your browser before accessing',
                'just a moment',
                'cf-browser-verification',
                'cloudflare ray id',
                'attention required! | cloudflare',
            ],
        },
        {
            'name': 'Sucuri',
            'confidence': 90,
            'strong_header_markers': [
                'x-sucuri-block',
                'x-sucuri-id',
            ],
            'header_markers': [
                'server: sucuri/cloudproxy',
            ],
            'strong_body_markers': [
                'access denied - sucuri website firewall',
            ],
            'body_markers': [
                'sucuri website firewall',
                'access denied - sucuri website firewall',
            ],
        },
        {
            'name': 'Akamai',
            'confidence': 88,
            'strong_header_markers': [
                'akamai-grn',
            ],
            'header_markers': [
                'x-akamai-',
                'server: akamai',
            ],
            'strong_body_markers': [
                'access denied reference #',
                'generated by akamai',
            ],
            'body_markers': [
                'akamai bot manager',
                'access denied reference #',
                'generated by akamai',
            ],
        },
        {
            'name': 'Imperva',
            'confidence': 88,
            'strong_header_markers': [
                'x-iinfo',
                'visid_incap',
                'incap_ses',
            ],
            'header_markers': [
                'x-cdn: imperva',
            ],
            'strong_body_markers': [
                'incapsula incident id',
                'powered by imperva',
            ],
            'body_markers': [
                'incident id',
                'request unsuccessful',
                'powered by imperva',
                'incapsula incident id',
            ],
        },
        {
            'name': 'Distil',
            'confidence': 85,
            'strong_header_markers': [
                'x-distil-cs',
            ],
            'header_markers': [
                'x-distil-',
                'distil_r_captcha',
            ],
            'strong_body_markers': [
                'distil networks',
            ],
            'body_markers': [
                'distil networks',
            ],
        },
        {
            'name': 'F5 BIG-IP ASM',
            'confidence': 84,
            'strong_header_markers': [
                'x-wa-info',
            ],
            'header_markers': [
                'bigip',
                'f5avr',
            ],
            'strong_body_markers': [
                'the requested url was rejected',
                'request rejected',
            ],
            'body_markers': [
                'the requested url was rejected',
                'support id',
                'request rejected',
            ],
        },
        {
            'name': 'AWS WAF',
            'confidence': 86,
            'strong_header_markers': [
                'x-amzn-requestid',
                'x-amz-cf-id',
            ],
            'header_markers': [
                'server: awselb/2.0',
                'x-amzn-errortype',
            ],
            'strong_body_markers': [
                'aws waf',
            ],
            'body_markers': [
                'request blocked',
                'generated by cloudfront',
                'aws waf',
            ],
        },
        {
            'name': 'Azure Front Door',
            'confidence': 85,
            'strong_header_markers': [
                'x-azure-ref',
                'x-azure-fdid',
            ],
            'header_markers': [
                'server: envoy',
                'via: 1.1 azure',
            ],
            'strong_body_markers': [
                'azure front door',
            ],
            'body_markers': [
                'azure front door',
                'request blocked',
                'the request has been blocked',
            ],
        },
        {
            'name': 'Fastly',
            'confidence': 82,
            'strong_header_markers': [
                'x-served-by: cache-',
                'x-cache: hit from fastly',
                'x-cache: miss from fastly',
            ],
            'header_markers': [
                'via: 1.1 varnish',
                'fastly-debug',
            ],
            'strong_body_markers': [
                'fastly error',
            ],
            'body_markers': [
                'fastly error',
                'guru mediation',
            ],
        },
        {
            'name': 'ModSecurity',
            'confidence': 80,
            'strong_header_markers': [
                'mod_security',
                'modsecurity',
            ],
            'header_markers': [
                'server: mod_security',
            ],
            'strong_body_markers': [
                'modsecurity action',
            ],
            'body_markers': [
                'mod_security',
                'modsecurity action',
                'not acceptable',
            ],
        },
        {
            'name': 'DataDome',
            'confidence': 87,
            'strong_header_markers': [
                'x-datadome',
                'datadome=',
            ],
            'header_markers': [
                'server: datadome',
            ],
            'strong_body_markers': [
                'captcha delivery network',
                'datadome',
            ],
            'body_markers': [
                'datadome',
                'please enable js',
                'captcha delivery network',
            ],
        },
        {
            'name': 'PerimeterX / HUMAN',
            'confidence': 85,
            'strong_header_markers': [
                '_pxhd',
                '_pxvid',
                'x-px',
            ],
            'header_markers': [
                'perimeterx',
                'humansecurity',
            ],
            'strong_body_markers': [
                'access to this page has been denied',
                'perimeterx',
            ],
            'body_markers': [
                'perimeterx',
                'human security',
                'access to this page has been denied',
            ],
        },
        {
            'name': 'Kasada',
            'confidence': 84,
            'strong_header_markers': [
                'x-kpsdk-',
                'x-kasada-',
            ],
            'header_markers': [
                'kpsdk',
            ],
            'strong_body_markers': [
                'kasada',
                'kpsdk',
            ],
            'body_markers': [
                'kasada',
                'kpsdk',
            ],
        },
        {
            'name': 'Barracuda',
            'confidence': 82,
            'strong_header_markers': [
                'barra_counter_session',
            ],
            'header_markers': [
                'barracuda',
            ],
            'strong_body_markers': [
                'barracuda web application firewall',
            ],
            'body_markers': [
                'barracuda web application firewall',
                'you have been blocked',
            ],
        },
        {
            'name': 'Radware',
            'confidence': 81,
            'strong_header_markers': [
                'x-sl-compstate',
            ],
            'header_markers': [
                'x-slid',
                'x-sl-compstate',
            ],
            'strong_body_markers': [
                'appwall',
                'radware',
            ],
            'body_markers': [
                'radware',
                'appwall',
            ],
        },

        {
            'name': 'FortiWeb',
            'confidence': 84,
            'strong_header_markers': [
                'fortiweb',
                'x-forti-cache',
            ],
            'header_markers': [
                'server: fortiweb',
                'fortiwafsid',
            ],
            'strong_body_markers': [
                'fortiweb web application firewall',
                'fortinet',
            ],
            'body_markers': [
                'fortiweb',
                'fortinet',
                'web page blocked',
            ],
        },
        {
            'name': 'Reblaze',
            'confidence': 83,
            'strong_header_markers': [
                'rbzid',
                'x-rbzid',
            ],
            'header_markers': [
                'reblaze',
                'x-rbz-',
            ],
            'strong_body_markers': [
                'reblaze secure web gateway',
            ],
            'body_markers': [
                'reblaze',
                'secure web gateway',
                'access denied',
            ],
        },
        {
            'name': 'NetScaler / Citrix WAF',
            'confidence': 85,
            'strong_header_markers': [
                'ns_af=',
                'citrix_ns_id',
            ],
            'header_markers': [
                'netscaler',
                'citrix',
                'x-citrix-application',
            ],
            'strong_body_markers': [
                'request denied by netscaler',
            ],
            'body_markers': [
                'netscaler',
                'citrix adc',
                'request denied',
            ],
        },
        {
            'name': 'AppTrana',
            'confidence': 82,
            'strong_header_markers': [
                'x-apptrana-',
            ],
            'header_markers': [
                'apptrana',
                'x-request-protection',
            ],
            'strong_body_markers': [
                'apptrana',
            ],
            'body_markers': [
                'apptrana',
                'request blocked',
                'website protection',
            ],
        },
        {
            'name': 'Huawei Cloud WAF',
            'confidence': 83,
            'strong_header_markers': [
                'x-hwwaf-',
                'hwwafsesid',
            ],
            'header_markers': [
                'huaweicloud',
                'huawei cloud waf',
            ],
            'strong_body_markers': [
                'huawei cloud waf',
            ],
            'body_markers': [
                'huawei cloud',
                'cloud waf',
                'request blocked',
            ],
        },
    ]

    DEFAULT_WAF_HEADER_MARKERS = [
        'techaro.lol-anubis-cookie-verification',
        'techaro.lol-anubis-auth=;',
        'cf-chl-',
        'cf-mitigated: challenge',
        'x-sucuri-block',
        'x-sucuri-id',
        'x-distil-cs',
        'x-iinfo',
        'visid_incap',
        'incap_ses',
        'akamai-grn',
        'x-wa-info',
        'mod_security',
        'modsecurity',
        'x-datadome',
        '_pxhd',
        '_pxvid',
        'x-kpsdk-',
        'x-kasada-',
        'fortiweb',
        'x-forti-cache',
        'fortiwafsid',
        'rbzid',
        'x-rbzid',
        'x-rbz-',
        'ns_af=',
        'citrix_ns_id',
        'x-citrix-application',
        'x-apptrana-',
        'apptrana',
        'x-hwwaf-',
        'hwwafsesid',
    ]

    DEFAULT_WAF_BODY_MARKERS = [
        'anubis',
        'making sure you\'re not a bot',
        'proof-of-work',
        'checking your browser',
        'just a moment',
        'cf-browser-verification',
        'cloudflare ray id',
        'sucuri website firewall',
        'akamai bot manager',
        'distil networks',
        'request blocked',
        'access denied reference #',
        'the requested url was rejected',
        'modsecurity',
        'datadome',
        'perimeterx',
        'human security',
        'kasada',
        'barracuda web application firewall',
        'appwall',
        'fortiweb',
        'fortinet',
        'reblaze',
        'secure web gateway',
        'netscaler',
        'citrix adc',
        'apptrana',
        'website protection',
        'huawei cloud waf',
        'cloud waf',
    ]

    DEFAULT_HTTP_SUCCESS_STATUSES = [100, 101, 200, 201, 202, 203, 204, 205, 206, 207, 208]
    DEFAULT_HTTP_REDIRECT_STATUSES = [300, 301, 302, 303, 304, 307, 308]
    DEFAULT_HTTP_FAILED_STATUSES = [404, 406, 410, 409, 411, 412, 424, 429, 440, 500, 501, 502, 503, 504, 509, 511, 520, 522]
    DEFAULT_SSL_CERT_REQUIRED_STATUSES = [423, 496, 525]
    DEFAULT_HTTP_FORBIDDEN_STATUSES = [403, 405]
    DEFAULT_HTTP_AUTH_STATUSES = [401]
    DEFAULT_HTTP_BAD_REQUEST_STATUSES = [400, 415, 508]

    def __init__(self, config):
        """
        Response instance
        :param src.lib.browser.config.Config config: configurations
        """

        self._cfg = config
        self._response_plugins = []
        self._last_waf_detection = None

    @property
    def waf_detection(self):
        """
        Return last detected WAF payload.

        :return: dict | None
        """

        return self._last_waf_detection

    @classmethod
    def _get_redirect_url(cls, url, response):
        """
        Get redirect url
        :param str url: redirect url
        :param urllib3.response.HTTPResponse response: response object
        :return: str
        """

        redirect_url = None
        location = response.get_redirect_location()

        if location is not False:
            matches = re.search(r"(?P<url>https?://[^\s]+)", location)
            if matches is not None:
                redirect_url = matches.group("url")
            else:
                urlp = helper.parse_url(url)
                location = location if True is location.startswith('/') else ''.join(('/', location))
                redirect_url = urlp.scheme + '://' + urlp.netloc + location

        return redirect_url

    def detect(self, request_url, response):
        """
        Detect response by status code
        :param str request_url: request url
        :param urllib3.response.HTTPResponse response: response object
        :raise Exception
        :return: str
        """

        status = None
        self._last_waf_detection = None

        if True is getattr(self._cfg, 'is_waf_detect', False):
            waf_detection = self.__detect_waf(response)
            if waf_detection is not None:
                self._last_waf_detection = waf_detection
                return self.DEFAULT_WAF_STATUS

        for _, resp_plugin in enumerate(self._response_plugins):
            status = resp_plugin.process(response)
            if None is not status:
                break

        if None is status:
            if response.status in self.DEFAULT_HTTP_SUCCESS_STATUSES:
                return 'success'
            elif response.status in self.DEFAULT_HTTP_FAILED_STATUSES:
                return 'failed'
            elif response.status in self.DEFAULT_SSL_CERT_REQUIRED_STATUSES:
                return 'certificate'
            elif response.status in self.DEFAULT_HTTP_REDIRECT_STATUSES:
                return self.__is_redirect(response, request_url)
            elif response.status in self.DEFAULT_HTTP_BAD_REQUEST_STATUSES:
                return 'bad'
            elif response.status in self.DEFAULT_HTTP_FORBIDDEN_STATUSES:
                return 'forbidden'
            elif response.status in self.DEFAULT_HTTP_AUTH_STATUSES:
                return 'auth'
            else:
                raise Exception('Unknown response status : `{0}`'.format(response.status))
        else:
            return status

    def handle(self, response, request_url, items_size, total_size, ignore_list):
        """
        Response handler
        :param urllib3.response.HTTPResponse response: response object
        :param str request_url: url from request
        :param int items_size: current items sizes
        :param int total_size: response object
        :param list ignore_list: ignore list
        :raise ResponseError
        :return: dict
        """

        pass

    @staticmethod
    def _get_content_size(response):
        """
        Get content size
        :param urllib3.response.HTTPResponse response: response object
        :return: str
        """

        size = 0

        try:
            size = 0 if not hasattr(response, 'headers') else int(response.headers['Content-Length'])
        except (KeyError, ValueError):
            size = len(response.data)
        finally:
            size = filesystem.human_size(size, 0)
        return size

    @staticmethod
    def __is_redirect(response, request_url):
        """
        Return redirect status
        :param str request_url: request url
        :param urllib3.response.HTTPResponse response: response object
        :return: str
        """

        location = response.get_redirect_location()
        if location is not False and location is not None:
            urlfrag = helper.parse_url(request_url)
            redirect_url = location.rstrip('/')

            redirectfrag = helper.parse_url(redirect_url)
            url = "{0}://{1}".format(urlfrag.scheme, urlfrag.netloc)
            if url == redirect_url \
                    or (0 < len(redirectfrag.query) and redirectfrag.query in urlfrag.path):
                return 'failed'
            return 'redirect'
        else:
            return 'failed'

    @staticmethod
    def __collect_header_blob(response):
        """
        Normalize response headers into a lower-case searchable blob.

        :param urllib3.response.HTTPResponse response: response object
        :return: str
        """

        try:
            headers = response.headers
        except AttributeError:
            headers = {}

        try:
            items = dict(headers).items()
        except Exception:
            try:
                items = headers.items()
            except Exception:
                items = []

        return ' '.join([
            '{0}: {1}'.format(key, value)
            for key, value in items
        ]).lower()

    @staticmethod
    def __collect_body_blob(response):
        """
        Normalize response body into a lower-case searchable blob.

        :param urllib3.response.HTTPResponse response: response object
        :return: str
        """

        try:
            return helper.decode(response.data).lower()
        except (AttributeError, TypeError, UnicodeDecodeError):
            return ''

    @staticmethod
    def __match_markers(source_name, blob, markers):
        """
        Collect matching markers from a normalized blob.

        :param str source_name:
        :param str blob:
        :param list markers:
        :return: list[str]
        """

        signals = []
        for marker in markers:
            if marker in blob:
                signals.append('{0}:{1}'.format(source_name, marker))
        return signals

    @classmethod
    def __looks_like_blocked_status(cls, response):
        """
        Return True when response status looks like a block/challenge flow.

        :param urllib3.response.HTTPResponse response: response object
        :return: bool
        """

        try:
            return int(response.status) in cls.DEFAULT_WAF_BLOCK_STATUSES
        except (AttributeError, TypeError, ValueError):
            return False

    @staticmethod
    def __score_detection(strong_header_signals, header_signals, strong_body_signals, body_signals):
        """
        Score WAF signals with stronger priority for header and strong body markers.

        :param list strong_header_signals:
        :param list header_signals:
        :param list strong_body_signals:
        :param list body_signals:
        :return: int
        """

        return (
            (len(strong_header_signals) * 60) +
            (len(header_signals) * 35) +
            (len(strong_body_signals) * 30) +
            (len(body_signals) * 20)
        )

    def __build_waf_result(self, signature, signals):
        """
        Build normalized WAF detection payload.

        :param dict signature:
        :param list signals:
        :return: dict
        """

        return {
            'name': signature.get('name'),
            'confidence': int(signature.get('confidence', 80)),
            'signals': signals[:5],
        }

    def __detect_waf(self, response):
        """
        Detect known WAF or anti-bot vendors.

        Detection rules:
        - any strong header marker is enough
        - two strong body markers are enough even on 200 OK
        - one strong body marker plus another body/header marker is enough
        - header markers are enough when score is high enough
        - body-only detections without strong markers still require blocked-like status
        - generic fallback is stricter than vendor-specific matching

        :param urllib3.response.HTTPResponse response: response object
        :return: dict | None
        """

        header_blob = self.__collect_header_blob(response)
        body_blob = self.__collect_body_blob(response)
        blocked_like_status = self.__looks_like_blocked_status(response)

        for signature in self.DEFAULT_WAF_SIGNATURES:
            strong_header_signals = self.__match_markers(
                'header',
                header_blob,
                signature.get('strong_header_markers', [])
            )
            header_signals = self.__match_markers(
                'header',
                header_blob,
                signature.get('header_markers', [])
            )
            strong_body_signals = self.__match_markers(
                'body',
                body_blob,
                signature.get('strong_body_markers', [])
            )
            body_signals = self.__match_markers(
                'body',
                body_blob,
                signature.get('body_markers', [])
            )

            if len(strong_header_signals) > 0:
                return self.__build_waf_result(
                    signature,
                    strong_header_signals + header_signals + strong_body_signals + body_signals
                )

            if len(strong_body_signals) >= 1:
                return self.__build_waf_result(
                    signature,
                    strong_body_signals + header_signals + body_signals
                )

            score = self.__score_detection(
                strong_header_signals,
                header_signals,
                strong_body_signals,
                body_signals
            )

            if len(header_signals) > 0 and score >= 35:
                return self.__build_waf_result(
                    signature,
                    header_signals + strong_body_signals + body_signals
                )

            if blocked_like_status is True and len(body_signals) >= 2 and score >= 40:
                return self.__build_waf_result(
                    signature,
                    strong_body_signals + body_signals
                )

            if blocked_like_status is True and len(header_signals) >= 1 and len(body_signals) >= 1:
                return self.__build_waf_result(
                    signature,
                    header_signals + strong_body_signals + body_signals
                )

        generic_header_signals = self.__match_markers('header', header_blob, self.DEFAULT_WAF_HEADER_MARKERS)
        generic_body_signals = self.__match_markers('body', body_blob, self.DEFAULT_WAF_BODY_MARKERS)

        if len(generic_header_signals) > 0:
            return {
                'name': 'Generic WAF',
                'confidence': 80,
                'signals': (generic_header_signals + generic_body_signals)[:5],
            }

        if blocked_like_status is True and len(generic_body_signals) >= 2:
            return {
                'name': 'Generic WAF',
                'confidence': 78,
                'signals': generic_body_signals[:5],
            }

        return None