@@ -1,13 +1,14 @@
#
#
# Copyright (c) 2020 Bubble, Inc. All rights reserved. For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/
# Copyright (c) 2020 Bubble, Inc. All rights reserved. For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/
#
#
import json
import re
import re
import requests
import requests
import urllib
import urllib
import uuid
import uuid
import traceback
import traceback
from mitmproxy.net.http import Headers
from mitmproxy.net.http import Headers
from bubble_config import bubble_port, bubble_host_alias, debug_capture_fqdn
from bubble_config import bubble_port, bubble_host_alias, debug_capture_fqdn, debug_stream_fqdn, debug_stream_uri
from bubble_api import CTX_BUBBLE_MATCHERS, CTX_BUBBLE_ABORT, CTX_BUBBLE_LOCATION, BUBBLE_URI_PREFIX, \
from bubble_api import CTX_BUBBLE_MATCHERS, CTX_BUBBLE_ABORT, CTX_BUBBLE_LOCATION, BUBBLE_URI_PREFIX, \
CTX_BUBBLE_REQUEST_ID, CTX_CONTENT_LENGTH, CTX_CONTENT_LENGTH_SENT, bubble_log, get_flow_ctx, add_flow_ctx, \
CTX_BUBBLE_REQUEST_ID, CTX_CONTENT_LENGTH, CTX_CONTENT_LENGTH_SENT, bubble_log, get_flow_ctx, add_flow_ctx, \
HEADER_USER_AGENT, HEADER_FILTER_PASSTHRU, HEADER_CONTENT_SECURITY_POLICY, REDIS, redis_set, parse_host_header
HEADER_USER_AGENT, HEADER_FILTER_PASSTHRU, HEADER_CONTENT_SECURITY_POLICY, REDIS, redis_set, parse_host_header
@@ -24,6 +25,7 @@ STANDARD_FILTER_HEADERS = {HEADER_CONTENT_TYPE: CONTENT_TYPE_BINARY}
REDIS_FILTER_PASSTHRU_PREFIX = '__chunk_filter_pass__'
REDIS_FILTER_PASSTHRU_PREFIX = '__chunk_filter_pass__'
REDIS_FILTER_PASSTHRU_DURATION = 600
REDIS_FILTER_PASSTHRU_DURATION = 600
DEBUG_STREAM_COUNTERS = {}
def add_csp_part(new_csp, part):
def add_csp_part(new_csp, part):
if len(new_csp) > 0:
if len(new_csp) > 0:
@@ -64,26 +66,12 @@ def ensure_bubble_script_csp(csp):
def filter_chunk(flow, chunk, req_id, user_agent, last, content_encoding=None, content_type=None, content_length=None, csp=None):
def filter_chunk(flow, chunk, req_id, user_agent, last, content_encoding=None, content_type=None, content_length=None, csp=None):
if debug_capture_fqdn:
if debug_capture_fqdn:
host = None
if flow.client_conn.tls_established:
sni = flow.client_conn.connection.get_servername()
if sni:
host = str(sni)
else:
host_header = flow.request.host_header
if host_header:
m = parse_host_header.match(host_header)
if m:
host = str(m.group("host").strip("[]"))
if host:
if host.startswith("b'"):
host = host[2:-1]
if host in debug_capture_fqdn:
bubble_log('filter_chunk: debug_capture_fqdn detected, capturing: '+host)
f = open('/tmp/bubble_capture_'+req_id, mode='ab', buffering=0)
f.write(chunk)
f.close()
return chunk
if debug_capture_fqdn in req_id:
bubble_log('filter_chunk: debug_capture_fqdn detected, capturing: '+debug_capture_fqdn)
f = open('/tmp/bubble_capture_'+req_id, mode='ab', buffering=0)
f.write(chunk)
f.close()
return chunk
# should we just passthru?
# should we just passthru?
redis_passthru_key = REDIS_FILTER_PASSTHRU_PREFIX + flow.request.method + '~~~' + user_agent + ':' + flow.request.url
redis_passthru_key = REDIS_FILTER_PASSTHRU_PREFIX + flow.request.method + '~~~' + user_agent + ':' + flow.request.url
@@ -97,8 +85,7 @@ def filter_chunk(flow, chunk, req_id, user_agent, last, content_encoding=None, c
params_added = False
params_added = False
if chunk and content_type:
if chunk and content_type:
params_added = True
params_added = True
url = (url
+ '?type=' + urllib.parse.quote_plus(content_type))
url = url + '?type=' + urllib.parse.quote_plus(content_type)
if content_encoding:
if content_encoding:
url = url + '&encoding=' + urllib.parse.quote_plus(content_encoding)
url = url + '&encoding=' + urllib.parse.quote_plus(content_encoding)
if content_length:
if content_length:
@@ -111,15 +98,33 @@ def filter_chunk(flow, chunk, req_id, user_agent, last, content_encoding=None, c
if csp:
if csp:
# bubble_log('filter_chunk: url='+url+' (csp='+csp+')')
# bubble_log('filter_chunk: url='+url+' (csp='+csp+')')
bubble_log('filter_chunk: url='+url+' (with csp)')
bubble_log('filter_chunk: url='+url+' (with csp) (last='+str(last)+') ')
filter_headers = {
filter_headers = {
HEADER_CONTENT_TYPE: CONTENT_TYPE_BINARY,
HEADER_CONTENT_TYPE: CONTENT_TYPE_BINARY,
HEADER_CONTENT_SECURITY_POLICY: csp
HEADER_CONTENT_SECURITY_POLICY: csp
}
}
else:
else:
bubble_log('filter_chunk: url='+url+' (no csp)')
bubble_log('filter_chunk: url='+url+' (no csp) (last='+str(last)+') ')
filter_headers = STANDARD_FILTER_HEADERS
filter_headers = STANDARD_FILTER_HEADERS
if debug_stream_fqdn and debug_stream_uri and debug_stream_fqdn in req_id and flow.request.path == debug_stream_uri:
if req_id in DEBUG_STREAM_COUNTERS:
count = DEBUG_STREAM_COUNTERS[req_id] + 1
else:
count = 0
DEBUG_STREAM_COUNTERS[req_id] = count
bubble_log('filter_chunk: debug_stream detected, capturing: '+debug_stream_fqdn)
f = open('/tmp/bubble_stream_'+req_id+'_chunk'+"{:04d}".format(count)+'.data', mode='wb', buffering=0)
if chunk is not None:
f.write(chunk)
f.close()
f = open('/tmp/bubble_stream_'+req_id+'_chunk'+"{:04d}".format(count)+'.headers.json', mode='w')
f.write(json.dumps(filter_headers))
f.close()
f = open('/tmp/bubble_stream_'+req_id+'_chunk'+"{:04d}".format(count)+'.url', mode='w')
f.write(url)
f.close()
response = requests.post(url, data=chunk, headers=filter_headers)
response = requests.post(url, data=chunk, headers=filter_headers)
if not response.ok:
if not response.ok:
err_message = 'filter_chunk: Error fetching ' + url + ', HTTP status ' + str(response.status_code)
err_message = 'filter_chunk: Error fetching ' + url + ', HTTP status ' + str(response.status_code)
@@ -174,8 +179,9 @@ def send_bubble_response(response):
def responseheaders(flow):
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):]
path = flow.request.path
if path and path.startswith(BUBBLE_URI_PREFIX):
uri = 'http://127.0.0.1:' + bubble_port + '/' + path[len(BUBBLE_URI_PREFIX):]
bubble_log('responseheaders: sending special bubble request to '+uri)
bubble_log('responseheaders: sending special bubble request to '+uri)
headers = {
headers = {
'Accept' : 'application/json',
'Accept' : 'application/json',
@@ -203,19 +209,27 @@ def responseheaders(flow):
if abort_code is not None:
if abort_code is not None:
abort_location = get_flow_ctx(flow, CTX_BUBBLE_LOCATION)
abort_location = get_flow_ctx(flow, CTX_BUBBLE_LOCATION)
if abort_location is not None:
if abort_location is not None:
bubble_log('responseheaders: redirecting request with HTTP status '+str(abort_code)+' to: '+abort_location)
bubble_log('responseheaders: redirecting request with HTTP status '+str(abort_code)+' to: '+abort_location+', path was: '+path )
flow.response.headers = Headers()
flow.response.headers = Headers()
flow.response.headers[HEADER_LOCATION] = abort_location
flow.response.headers[HEADER_LOCATION] = abort_location
flow.response.status_code = abort_code
flow.response.status_code = abort_code
flow.response.stream = lambda chunks: []
flow.response.stream = lambda chunks: []
else:
else:
bubble_log('responseheaders: aborting request with HTTP status '+str(abort_code))
bubble_log('responseheaders: aborting request with HTTP status '+str(abort_code)+', path was: '+path )
flow.response.headers = Headers()
flow.response.headers = Headers()
flow.response.status_code = abort_code
flow.response.status_code = abort_code
flow.response.stream = lambda chunks: []
flow.response.stream = lambda chunks: []
elif flow.response.status_code // 100 != 2:
elif flow.response.status_code // 100 != 2:
bubble_log('responseheaders: response had HTTP status '+str(flow.response.status_code)+', returning as-is')
bubble_log('responseheaders: response had HTTP status '+str(flow.response.status_code)+', returning as-is: '+path)
pass
elif flow.response.headers is None or len(flow.response.headers) == 0:
bubble_log('responseheaders: response had HTTP status '+str(flow.response.status_code)+', and NO response headers, returning as-is: '+path)
pass
elif HEADER_CONTENT_LENGTH in flow.response.headers and flow.response.headers[HEADER_CONTENT_LENGTH] == "0":
bubble_log('responseheaders: response had HTTP status '+str(flow.response.status_code)+', and '+HEADER_CONTENT_LENGTH+' was zero, returning as-is: '+path)
pass
pass
else:
else:
@@ -239,10 +253,10 @@ def responseheaders(flow):
typeRegex = '^text/html.*'
typeRegex = '^text/html.*'
if re.match(typeRegex, content_type):
if re.match(typeRegex, content_type):
any_content_type_matches = True
any_content_type_matches = True
bubble_log(prefix+'found at least one matcher for content_type ('+content_type+'), filtering')
bubble_log(prefix+'found at least one matcher for content_type ('+content_type+'), filtering: '+path )
break
break
if not any_content_type_matches:
if not any_content_type_matches:
bubble_log(prefix+'no matchers for content_type ('+content_type+'), passing thru')
bubble_log(prefix+'no matchers for content_type ('+content_type+'), passing thru: '+path )
return
return
if HEADER_CONTENT_ENCODING in flow.response.headers:
if HEADER_CONTENT_ENCODING in flow.response.headers:
@@ -257,7 +271,7 @@ def responseheaders(flow):
csp = None
csp = None
content_length_value = flow.response.headers.pop(HEADER_CONTENT_LENGTH, 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))
# bubble_log(prefix+'content_encoding='+repr(content_encoding) + ', content_type='+repr(content_type))
flow.response.stream = bubble_modify(flow, req_id, user_agent, content_encoding, content_type, csp)
flow.response.stream = bubble_modify(flow, req_id, user_agent, content_encoding, content_type, csp)
if content_length_value:
if content_length_value:
flow.response.headers['transfer-encoding'] = 'chunked'
flow.response.headers['transfer-encoding'] = 'chunked'
@@ -268,10 +282,10 @@ def responseheaders(flow):
if hasattr(ctx, 'ctx'):
if hasattr(ctx, 'ctx'):
ctx = ctx.ctx
ctx = ctx.ctx
else:
else:
bubble_log(prefix+'error finding server_conn. last ctx has no further ctx. type='+str(type(ctx))+' vars='+str(vars(ctx)))
bubble_log(prefix+'error finding server_conn for path '+path+' . last ctx has no further ctx. type='+str(type(ctx))+' vars='+str(vars(ctx)))
return
return
if not hasattr(ctx, 'server_conn'):
if not hasattr(ctx, 'server_conn'):
bubble_log(prefix+'error finding server_conn. ctx type='+str(type(ctx))+' vars='+str(vars(ctx)))
bubble_log(prefix+'error finding server_conn for path '+path+' . ctx type='+str(type(ctx))+' vars='+str(vars(ctx)))
return
return
content_length = int(content_length_value)
content_length = int(content_length_value)
ctx.server_conn.rfile.fake_chunks = content_length
ctx.server_conn.rfile.fake_chunks = content_length
@@ -279,11 +293,11 @@ def responseheaders(flow):
add_flow_ctx(flow, CTX_CONTENT_LENGTH_SENT, 0)
add_flow_ctx(flow, CTX_CONTENT_LENGTH_SENT, 0)
else:
else:
bubble_log(prefix+'no matchers, passing thru')
bubble_log(prefix+'no matchers, passing thru: '+path )
pass
pass
else:
else:
bubble_log(prefix+'no '+HEADER_CONTENT_TYPE +' header, passing thru')
bubble_log(prefix+'no '+HEADER_CONTENT_TYPE +' header, passing thru: '+path )
pass
pass
else:
else:
bubble_log(prefix+'no '+CTX_BUBBLE_MATCHERS +' in ctx, passing thru')
bubble_log(prefix+'no '+CTX_BUBBLE_MATCHERS +' in ctx, passing thru: '+path )
pass
pass