-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: proxy & browser capabilities #2
Changes from all commits
756fb9a
627d429
e955348
625e303
2f6d51c
fa6e16a
b32ba3e
b5df1bf
ede1647
34a5f9b
bc34546
a1f8c9d
82a84d5
a32af57
052f68e
75da8f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -90,3 +90,6 @@ typings/ | |
|
||
# Electron-Forge | ||
out/ | ||
|
||
# proxy certificates | ||
resources/certificates |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import base64 | ||
import hashlib | ||
import json | ||
import mitmproxy | ||
from mitmproxy import http | ||
from mitmproxy.tools.web.app import cert_to_json | ||
from mitmproxy.utils.strutils import always_str | ||
from mitmproxy.utils.emoji import emoji | ||
from mitmproxy.dns import DNSFlow | ||
from mitmproxy.http import HTTPFlow | ||
from mitmproxy.tcp import TCPFlow | ||
from mitmproxy.udp import UDPFlow | ||
|
||
|
||
def flow_to_json(flow: mitmproxy.flow.Flow) -> dict: | ||
""" | ||
`flow_to_json` method taken from `mitmproxy/tools/web/app.py` modified to also include content. | ||
""" | ||
f = { | ||
"id": flow.id, | ||
"intercepted": flow.intercepted, | ||
"isReplay": flow.is_replay, | ||
"type": flow.type, | ||
"modified": flow.modified(), | ||
"marked": emoji.get(flow.marked, "🔴") if flow.marked else "", | ||
"comment": flow.comment, | ||
"timestampCreated": flow.timestamp_created, | ||
} | ||
|
||
if flow.client_conn: | ||
f["clientConn"] = { | ||
"id": flow.client_conn.id, | ||
"peername": flow.client_conn.peername, | ||
"sockname": flow.client_conn.sockname, | ||
"tlsEstablished": flow.client_conn.tls_established, | ||
"cert": cert_to_json(flow.client_conn.certificate_list), | ||
"sni": flow.client_conn.sni, | ||
"cipher": flow.client_conn.cipher, | ||
"alpn": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"), | ||
"tlsVersion": flow.client_conn.tls_version, | ||
"timestampStart": flow.client_conn.timestamp_start, | ||
"timestampTlsSetup": flow.client_conn.timestamp_tls_setup, | ||
"timestampEnd": flow.client_conn.timestamp_end, | ||
} | ||
|
||
if flow.server_conn: | ||
f["serverConn"] = { | ||
"id": flow.server_conn.id, | ||
"peername": flow.server_conn.peername, | ||
"sockname": flow.server_conn.sockname, | ||
"address": flow.server_conn.address, | ||
"tlsEstablished": flow.server_conn.tls_established, | ||
"cert": cert_to_json(flow.server_conn.certificate_list), | ||
"sni": flow.server_conn.sni, | ||
"cipher": flow.server_conn.cipher, | ||
"alpn": always_str(flow.server_conn.alpn, "ascii", "backslashreplace"), | ||
"tlsVersion": flow.server_conn.tls_version, | ||
"timestampStart": flow.server_conn.timestamp_start, | ||
"timestampTcpSetup": flow.server_conn.timestamp_tcp_setup, | ||
"timestampTlsSetup": flow.server_conn.timestamp_tls_setup, | ||
"timestampEnd": flow.server_conn.timestamp_end, | ||
} | ||
if flow.error: | ||
f["error"] = flow.error.get_state() | ||
|
||
if isinstance(flow, HTTPFlow): | ||
content_length: int | None | ||
content_hash: str | None | ||
|
||
if flow.request.raw_content is not None: | ||
content_length = len(flow.request.raw_content) | ||
content_hash = hashlib.sha256(flow.request.raw_content).hexdigest() | ||
else: | ||
content_length = None | ||
content_hash = None | ||
|
||
# we base64 encode content and let the client deal with it depending on mimetype | ||
content = base64.b64encode(flow.request.content).decode() if flow.request.content else None | ||
|
||
f["request"] = { | ||
"method": flow.request.method, | ||
"scheme": flow.request.scheme, | ||
"host": flow.request.host, | ||
"port": flow.request.port, | ||
"path": flow.request.path, | ||
"httpVersion": flow.request.http_version, | ||
"headers": tuple(flow.request.headers.items(True)), | ||
"contentLength": content_length, | ||
"contentHash": content_hash, | ||
"timestampStart": flow.request.timestamp_start, | ||
"timestampEnd": flow.request.timestamp_end, | ||
"prettyHost": flow.request.pretty_host, | ||
"content": content, | ||
} | ||
if flow.response: | ||
if flow.response.raw_content is not None: | ||
content_length = len(flow.response.raw_content) | ||
content_hash = hashlib.sha256(flow.response.raw_content).hexdigest() | ||
else: | ||
content_length = None | ||
content_hash = None | ||
|
||
# we base64 encode content and let the client deal with it depending on mimetype | ||
content = base64.b64encode(flow.response.content).decode() if flow.response.content else None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. using base64 encoding for "stringifying" binary data since it doesn't play well with the json format. This should allow us to handle all mime-types and eventually showcase them in a better way 😃 |
||
|
||
f["response"] = { | ||
"httpVersion": flow.response.http_version, | ||
"statusCode": flow.response.status_code, | ||
"reason": flow.response.reason, | ||
"headers": tuple(flow.response.headers.items(True)), | ||
"contentLength": content_length, | ||
"contentHash": content_hash, | ||
"timestampStart": flow.response.timestamp_start, | ||
"timestampEnd": flow.response.timestamp_end, | ||
"content": content, | ||
} | ||
if flow.response.data.trailers: | ||
f["response"]["trailers"] = tuple( | ||
flow.response.data.trailers.items(True) | ||
) | ||
|
||
if flow.websocket: | ||
f["websocket"] = { | ||
"messagesMeta": { | ||
"contentLength": sum( | ||
len(x.content) for x in flow.websocket.messages | ||
), | ||
"count": len(flow.websocket.messages), | ||
"timestampLast": flow.websocket.messages[-1].timestamp | ||
if flow.websocket.messages | ||
else None, | ||
}, | ||
"closedByClient": flow.websocket.closed_by_client, | ||
"closeCode": flow.websocket.close_code, | ||
"closeReason": flow.websocket.close_reason, | ||
"timestampEnd": flow.websocket.timestamp_end, | ||
} | ||
elif isinstance(flow, (TCPFlow, UDPFlow)): | ||
f["messagesMeta"] = { | ||
"contentLength": sum(len(x.content) for x in flow.messages), | ||
"count": len(flow.messages), | ||
"timestampLast": flow.messages[-1].timestamp if flow.messages else None, | ||
} | ||
elif isinstance(flow, DNSFlow): | ||
f["request"] = flow.request.to_json() | ||
if flow.response: | ||
f["response"] = flow.response.to_json() | ||
|
||
return f | ||
|
||
|
||
def request(flow: http.HTTPFlow) -> None: | ||
|
||
data = flow_to_json(flow) | ||
data = json.dumps(data) | ||
print(data, flush=True) | ||
|
||
|
||
def response(flow: http.HTTPFlow) -> None: | ||
|
||
data = flow_to_json(flow) | ||
data = json.dumps(data) | ||
print(data, flush=True) | ||
|
||
|
||
# we flush it as we want to catch it as soon as it's emitted | ||
print("Proxy Started~", flush=True) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { computeSystemExecutablePath, Browser, ChromeReleaseChannel, launch } from '@puppeteer/browsers'; | ||
import { getCertificateSPKI } from './proxy'; | ||
import { mkdtemp } from 'fs/promises'; | ||
import path from 'path'; | ||
import os from 'os'; | ||
|
||
const createUserDataDir = async () => { | ||
return mkdtemp(path.join(os.tmpdir(), 'k6-studio-')); | ||
}; | ||
|
||
export const launchBrowser = async () => { | ||
const path = computeSystemExecutablePath({browser: Browser.CHROME, channel: ChromeReleaseChannel.STABLE}) | ||
console.info(`browser path: ${path}`); | ||
|
||
const userDataDir = await createUserDataDir(); | ||
console.log(userDataDir); | ||
const certificateSPKI = await getCertificateSPKI(); | ||
|
||
const optimizationsToDisable = [ | ||
"OptimizationGuideModelDownloading", | ||
"OptimizationHintsFetching", | ||
"OptimizationTargetPrediction", | ||
"OptimizationHints", | ||
] | ||
const disableChromeOptimizations = `--disable-features=${optimizationsToDisable.join(',')}` | ||
|
||
return launch({ | ||
executablePath: path, | ||
args: [ | ||
'--new', | ||
'--args', | ||
`--user-data-dir=${userDataDir}`, | ||
'--hide-crash-restore-bubble', | ||
'--test-type', | ||
'--no-default-browser-check', | ||
'--no-first-run', | ||
'--disable-background-networking', | ||
'--disable-component-update', | ||
'--proxy-server=http://localhost:8080', // TODO: consider passing in the proxy port | ||
`--ignore-certificate-errors-spki-list=${certificateSPKI}`, | ||
disableChromeOptimizations, | ||
], | ||
}); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
declare interface Window { | ||
studio: import('../src/preload').Studio; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is the trick for not having to specify manually every type |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// TODO: modify json_output.py to use CamelCase instead of snake_case | ||
export interface Request { | ||
headers: Array<Array<string>>; | ||
scheme: string, | ||
host: string, | ||
method: string, | ||
path: string, | ||
content: string, | ||
timestampStart: string, | ||
id?: string, | ||
response?: Response | ||
} | ||
|
||
export interface Response { | ||
headers: Array<Array<string>>; | ||
reason: string, | ||
statusCode: number, | ||
content: string, | ||
path: string, | ||
timestampStart: string, | ||
} | ||
|
||
export interface ProxyData { | ||
id: string, | ||
request: Request, | ||
response?: Response, | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this has more data but like this is enough to get us started |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,47 @@ | ||
// See the Electron documentation for details on how to use preload scripts: | ||
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts | ||
|
||
import { ipcRenderer, contextBridge } from "electron"; | ||
import { ProxyData } from "./lib/types"; | ||
|
||
const proxy = { | ||
launchProxy: () => { | ||
ipcRenderer.send('proxy:start'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is an actually confusing bit about electron. As of today per my understanding you have both the For single direction communication is totally fine to use For bi-directional communication, although you can still do the old pattern with So |
||
}, | ||
onProxyStarted: (callback: () => void) => { | ||
ipcRenderer.on('proxy:started', () => { | ||
callback(); | ||
}); | ||
}, | ||
stopProxy: () => { | ||
ipcRenderer.send('proxy:stop'); | ||
}, | ||
onProxyData: (callback: (data: ProxyData) => void) => { | ||
ipcRenderer.on('proxy:data', (_, data) => { | ||
callback(data); | ||
}); | ||
}, | ||
} as const; | ||
|
||
const browser = { | ||
launchBrowser: () => { | ||
ipcRenderer.send('browser:start'); | ||
}, | ||
onBrowserStarted: (callback: () => void) => { | ||
ipcRenderer.on('browser:started', () => { | ||
callback(); | ||
}); | ||
}, | ||
stopBrowser: () => { | ||
ipcRenderer.send('browser:stop'); | ||
}, | ||
} as const; | ||
|
||
const studio = { | ||
proxy: proxy, | ||
browser: browser, | ||
} as const; | ||
|
||
contextBridge.exposeInMainWorld('studio', studio); | ||
|
||
export type Studio = typeof studio; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we will pick up only the specific os/architecture combination when packaging for a specific system