#
# Copyright (c) 2020 Bubble, Inc.  All rights reserved. For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/
#
import re
import requests
import urllib
import traceback
from mitmproxy.net.http import Headers
from bubble_config import bubble_port, bubble_host_alias
from bubble_api import CTX_BUBBLE_MATCHERS, CTX_BUBBLE_ABORT, BUBBLE_URI_PREFIX, \
    CTX_BUBBLE_REQUEST_ID, CTX_CONTENT_LENGTH, CTX_CONTENT_LENGTH_SENT, bubble_log, get_flow_ctx, add_flow_ctx

BUFFER_SIZE = 4096
HEADER_CONTENT_TYPE = 'Content-Type'
HEADER_CONTENT_LENGTH = 'Content-Length'
HEADER_CONTENT_ENCODING = 'Content-Encoding'
HEADER_TRANSFER_ENCODING = 'Transfer-Encoding'
BINARY_DATA_HEADER = {HEADER_CONTENT_TYPE: 'application/octet-stream'}


def filter_chunk(chunk, req_id, last, content_encoding=None, content_type=None, content_length=None):
    url = 'http://127.0.0.1:' + bubble_port + '/api/filter/apply/' + req_id
    params_added = False
    if chunk and content_type:
        params_added = True
        url = (url
               + '?type=' + urllib.parse.quote_plus(content_type))
        if content_encoding:
            url = url + '&encoding=' + urllib.parse.quote_plus(content_encoding)
        if content_length:
            url = url + '&length=' + str(content_length)
    if last:
        if params_added:
            url = url + '&last=true'
        else:
            url = url + '?last=true'

    bubble_log('filter_chunk: url='+url)

    response = requests.post(url, data=chunk, headers=BINARY_DATA_HEADER)
    if not response.ok:
        err_message = 'filter_chunk: Error fetching ' + url + ', HTTP status ' + str(response.status_code)
        bubble_log(err_message)
        return b''

    return response.content


def bubble_filter_chunks(flow, chunks, req_id, content_encoding, content_type):
    """
    chunks is a generator that can be used to iterate over all chunks.
    """
    first = True
    content_length = get_flow_ctx(flow, CTX_CONTENT_LENGTH)
    try:
        for chunk in chunks:
            if content_length:
                bytes_sent = get_flow_ctx(flow, CTX_CONTENT_LENGTH_SENT)
                chunk_len = len(chunk)
                last = chunk_len + bytes_sent >= content_length
                bubble_log('bubble_filter_chunks: content_length = '+str(content_length)+', bytes_sent = '+str(bytes_sent))
                add_flow_ctx(flow, CTX_CONTENT_LENGTH_SENT, bytes_sent + chunk_len)
            else:
                last = False
            if first:
                yield filter_chunk(chunk, req_id, last, content_encoding, content_type, content_length)
                first = False
            else:
                yield filter_chunk(chunk, req_id, last)
        if not content_length:
            yield filter_chunk(None, req_id, True)  # get the last bits of data
    except Exception as e:
        bubble_log('bubble_filter_chunks: exception='+repr(e))
        traceback.print_exc()
        yield None


def bubble_modify(flow, req_id, content_encoding, content_type):
    return lambda chunks: bubble_filter_chunks(flow, chunks, req_id, content_encoding, content_type)


def send_bubble_response(response):
    for chunk in response.iter_content(8192):
        yield chunk


def responseheaders(flow):

    if flow.request.path and flow.request.path.startswith(BUBBLE_URI_PREFIX):
        uri = 'http://127.0.0.1:' + bubble_port + '/' + flow.request.path[len(BUBBLE_URI_PREFIX):]
        bubble_log('responseheaders: sending special bubble request to '+uri)
        headers = {
            'Accept' : 'application/json',
            'Content-Type': 'application/json'
        }
        response = None
        if flow.request.method == 'GET':
            response = requests.get(uri, headers=headers, stream=True)
        elif flow.request.method == 'POST':
            bubble_log('responseheaders: special bubble request: POST content is '+str(flow.request.content))
            headers['Content-Length'] = str(len(flow.request.content))
            response = requests.post(uri, data=flow.request.content, headers=headers)
        else:
            bubble_log('responseheaders: special bubble request: method '+flow.request.method+' not supported')
        if response is not None:
            bubble_log('responseheaders: special bubble request: response status = '+str(response.status_code))
            flow.response.headers = Headers()
            for key, value in response.headers.items():
                flow.response.headers[key] = value
            flow.response.status_code = response.status_code
            flow.response.stream = lambda chunks: send_bubble_response(response)

    else:
        abort_code = get_flow_ctx(flow, CTX_BUBBLE_ABORT)
        if abort_code is not None:
            bubble_log('responseheaders: aborting request with HTTP status '+str(abort_code))
            flow.response.headers = Headers()
            flow.response.status_code = abort_code
            flow.response.stream = lambda chunks: []

        else:
            req_id = get_flow_ctx(flow, CTX_BUBBLE_REQUEST_ID)
            matchers = get_flow_ctx(flow, CTX_BUBBLE_MATCHERS)
            prefix = 'responseheaders(req_id='+str(req_id)+'): '
            if req_id is not None and matchers is not None:
                bubble_log(prefix+' matchers: '+repr(matchers))
                if HEADER_CONTENT_TYPE in flow.response.headers:
                    content_type = flow.response.headers[HEADER_CONTENT_TYPE]
                    if matchers:
                        any_content_type_matches = False
                        for m in matchers:
                            if 'contentTypeRegex' in m:
                                typeRegex = m['contentTypeRegex']
                                if typeRegex is None:
                                    typeRegex = '^text/html.*'
                                if re.match(typeRegex, content_type):
                                    any_content_type_matches = True
                                    bubble_log(prefix+': found at least one matcher for content_type ('+content_type+'), filtering')
                                    break
                        if not any_content_type_matches:
                            bubble_log(prefix+': no matchers for content_type ('+content_type+'), passing thru')
                            return

                        if HEADER_CONTENT_ENCODING in flow.response.headers:
                            content_encoding = flow.response.headers[HEADER_CONTENT_ENCODING]
                        else:
                            content_encoding = None

                        content_length_value = flow.response.headers.pop(HEADER_CONTENT_LENGTH, None)
                        bubble_log(prefix+': content_encoding='+repr(content_encoding) + ', content_type='+repr(content_type))
                        flow.response.stream = bubble_modify(flow, req_id, content_encoding, content_type)
                        if content_length_value:
                            flow.response.headers['transfer-encoding'] = 'chunked'
                            # find server_conn to set fake_chunks on
                            if flow.live and flow.live.ctx:
                                ctx = flow.live.ctx
                                while not hasattr(ctx, 'server_conn'):
                                    if hasattr(ctx, 'ctx'):
                                        ctx = ctx.ctx
                                    else:
                                        bubble_log(prefix+'error finding server_conn. last ctx has no further ctx. type='+str(type(ctx))+' vars='+str(vars(ctx)))
                                        return
                                if not hasattr(ctx, 'server_conn'):
                                    bubble_log(prefix+'error finding server_conn. ctx type='+str(type(ctx))+' vars='+str(vars(ctx)))
                                    return
                                content_length = int(content_length_value)
                                ctx.server_conn.rfile.fake_chunks = content_length
                                add_flow_ctx(flow, CTX_CONTENT_LENGTH, content_length)
                                add_flow_ctx(flow, CTX_CONTENT_LENGTH_SENT, 0)

                    else:
                        bubble_log(prefix+'no matchers, passing thru')
                        pass
                else:
                    bubble_log(prefix+'no '+HEADER_CONTENT_TYPE +' header, passing thru')
                    pass
            else:
                bubble_log(prefix+'no '+CTX_BUBBLE_MATCHERS +' in ctx, passing thru')
                pass
