@@ -1,13 +1,14 @@
#
# 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 requests
import urllib
import uuid
import traceback
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, \
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
@@ -24,6 +25,7 @@ STANDARD_FILTER_HEADERS = {HEADER_CONTENT_TYPE: CONTENT_TYPE_BINARY}
REDIS_FILTER_PASSTHRU_PREFIX = '__chunk_filter_pass__'
REDIS_FILTER_PASSTHRU_DURATION = 600
DEBUG_STREAM_COUNTERS = {}
def add_csp_part(new_csp, part):
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):
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?
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
if chunk and content_type:
params_added = True
url = (url
+ '?type=' + urllib.parse.quote_plus(content_type))
url = url + '?type=' + urllib.parse.quote_plus(content_type)
if content_encoding:
url = url + '&encoding=' + urllib.parse.quote_plus(content_encoding)
if content_length:
@@ -111,15 +98,33 @@ def filter_chunk(flow, chunk, req_id, user_agent, last, content_encoding=None, c
if 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 = {
HEADER_CONTENT_TYPE: CONTENT_TYPE_BINARY,
HEADER_CONTENT_SECURITY_POLICY: csp
}
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
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)
if not response.ok:
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):
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)
headers = {
'Accept' : 'application/json',
@@ -203,19 +209,27 @@ def responseheaders(flow):
if abort_code is not None:
abort_location = get_flow_ctx(flow, CTX_BUBBLE_LOCATION)
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[HEADER_LOCATION] = abort_location
flow.response.status_code = abort_code
flow.response.stream = lambda chunks: []
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.status_code = abort_code
flow.response.stream = lambda chunks: []
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
else:
@@ -239,10 +253,10 @@ def responseheaders(flow):
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')
bubble_log(prefix+'found at least one matcher for content_type ('+content_type+'), filtering: '+path )
break
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
if HEADER_CONTENT_ENCODING in flow.response.headers:
@@ -257,7 +271,7 @@ def responseheaders(flow):
csp = 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)
if content_length_value:
flow.response.headers['transfer-encoding'] = 'chunked'
@@ -268,10 +282,10 @@ def responseheaders(flow):
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)))
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
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
content_length = int(content_length_value)
ctx.server_conn.rfile.fake_chunks = content_length
@@ -279,11 +293,11 @@ def responseheaders(flow):
add_flow_ctx(flow, CTX_CONTENT_LENGTH_SENT, 0)
else:
bubble_log(prefix+'no matchers, passing thru')
bubble_log(prefix+'no matchers, passing thru: '+path )
pass
else:
bubble_log(prefix+'no '+HEADER_CONTENT_TYPE +' header, passing thru')
bubble_log(prefix+'no '+HEADER_CONTENT_TYPE +' header, passing thru: '+path )
pass
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