@luminati-io/luminati-proxy
Version:
A configurable local proxy for brightdata.com
918 lines (876 loc) • 30.7 kB
JavaScript
// LICENSE_CODE ZON ISC
'use strict'; /*jslint react:true, es6:true*/
define(['react', 'jquery', 'classnames', '/www/util/pub/pure_component.js',
'/util/util.js', '/util/setdb.js', '/www/util/pub/tooltip.js'],
function(React, $, classnames, Pure_component, zutil,
setdb, Tooltip)
{
const E = {};
// XXX krzysztof: temporarily copied from chrome_widgets
const Toolbar_button = ({id, tooltip, active, href, placement, ...props})=>
<a className={classnames('toolbar_item toolbar_button', id, {active})}
onClick={props.on_click||(()=>null)} href={href}
>
<Tooltip title={tooltip}>
<span className={classnames(id, 't_button', 'chrome_icon')}/>
</Tooltip>
</a>;
class Preview extends Pure_component {
state = {cur_pane: 0};
select_pane = id=>{ this.setState({cur_pane: id}); };
componentDidMount(){
this.setdb_on('har_viewer.set_pane', pane=>{
if (pane===undefined)
return;
this.setState({cur_pane: pane});
});
}
render(){
if (!this.props.cur_preview)
return null;
const Pane_content = this.props.panes[this.state.cur_pane];
const req = this.props.cur_preview;
return <div style={this.props.style} className="har_preview chrome">
<div className="tabbed_pane_header">
<div className="left_pane">
<div onClick={this.props.close}
className="close_btn_wrapper">
<div className="small_icon close_btn"/>
<div className="medium_icon close_btn_h"/>
</div>
</div>
<div className="right_panes">
{this.props.panes.map((p, idx)=>
<Pane
key={p.id}
width={p.width}
id={p.id}
idx={idx}
on_click={this.select_pane}
active={this.state.cur_pane==idx}
/>
)}
<Pane_slider panes={this.props.panes}
cur_pane={this.state.cur_pane}/>
</div>
</div>
<div className="tabbed_pane_content">
<Pane_content key={req.uuid} req={req}/>
</div>
</div>;
}
}
E.Preview = Preview;
const Pane = ({id, idx, width, on_click, active})=>{
return <div onClick={()=>on_click(idx)} style={{width}}
className={classnames('pane', id, {active})}>
<span>{id}</span>
</div>;
};
const Pane_slider = ({panes, cur_pane})=>{
const slider_class = classnames('pane_slider');
const offset = panes.slice(0, cur_pane).reduce((acc, e)=>acc+e.width, 0);
const slider_style = {
width: panes[cur_pane].width,
transform: `translateX(${offset+24}px)`,
};
return <div className={slider_class} style={slider_style}/>;
};
// XX krzysztof: import from util
const Copy_btn = ()=>null;
E.Pane_headers = class Pane_headers extends Pure_component {
get_curl = ()=>{
const req = this.props.req;
const {username, password, super_proxy} = req.details;
const headers = req.request.headers.map(h=>`-H "${h.name}: `
+`${h.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`);
const proxy = super_proxy ?
'-x '+(username||'')+':'+(password||'')+'@'+super_proxy : '';
const url = '"'+req.request.url+'"';
return ['curl', proxy, '-X', req.request.method, url, ...headers]
.filter(Boolean).join(' ');
};
render(){
const req = this.props.req;
const general_entries = [
{name: 'Request URL', value: req.request.url},
{name: 'Request method', value: req.request.method},
{name: 'Status code', value: req.response.status},
{name: 'Super proxy IP', value: req.details.super_proxy},
{name: 'Peer proxy IP', value: req.details.proxy_peer},
{name: 'Username', value: req.details.username},
{name: 'Password', value: req.details.password},
{name: 'Sent from', value: req.details.remote_address},
].filter(e=>e.value!==undefined);
return <React.Fragment>
<Copy_btn
val={this.get_curl()}
title="Copy as cURL"
style={{position: 'absolute', right: 5, top: 5}}
inner_style={{width: 'auto'}}
/>
<ol className="tree_outline">
<Preview_section title="General"
pairs={general_entries}/>
<Preview_section title="Response headers"
pairs={req.response.headers}/>
<Preview_section title="Request headers"
pairs={req.request.headers}/>
<Body_section title="Request body"
body={req.request.postData && req.request.postData.text}/>
</ol>
</React.Fragment>;
}
};
E.Pane_headers.width = 65;
E.Pane_headers.id = 'headers';
const Pane_info = ({children})=>
<div className="empty_view">
<div className="block">{children}</div>
</div>;
E.Pane_info = Pane_info;
class Body_section extends Pure_component {
state = {open: true};
toggle = ()=>this.setState(prev=>({open: !prev.open}));
render(){
if (!this.props.body)
return null;
let json;
let raw_body;
try { json = JSON.parse(this.props.body); }
catch(e){ raw_body = this.props.body; }
return [
<li key="li" onClick={this.toggle}
className={classnames('parent_title', 'expandable',
{open: this.state.open})}>
{this.props.title}
</li>,
<ol key="ol"
className={classnames('children', {open: this.state.open})}>
{!!json && <JSON_viewer json={json}/>}
{!!raw_body && <Header_pair name="raw-data" value={raw_body}/>}
</ol>,
];
}
}
class Preview_section extends Pure_component {
state = {open: true};
toggle = ()=>this.setState(prev=>({open: !prev.open}));
render(){
if (!this.props.pairs||!this.props.pairs.length)
return null;
return [
<li key="li" onClick={this.toggle}
className={classnames('parent_title', 'expandable',
{open: this.state.open})}>
{this.props.title}
{!this.state.open ? ` (${this.props.pairs.length})` : ''}
</li>,
<ol key="ol"
className={classnames('children', {open: this.state.open})}>
{this.props.pairs.map(p=>
<Header_pair key={p.name} name={p.name} value={p.value}/>
)}
</ol>,
];
}
}
const Header_pair = ({name, value})=>{
if (name=='Status code')
value = <Status_value value={value}/>;
return <li className="treeitem">
<div className="header_name">{name}: </div>
<div className="header_value">{value}</div>
</li>;
};
const Status_value = ({value})=>{
const info = value=='unknown';
const green = /2../.test(value);
const yellow = /3../.test(value);
const red = /(canceled)|([45]..)/.test(value);
const classes = classnames('small_icon', 'status', {
info, green, yellow, red});
return <div className="status_wrapper">
<div className={classes}/>{value}
</div>;
};
const Img_viewer = ({img})=>
<div className="img_viewer">
<div className="image">
<img src={img}/>
</div>
</div>;
E.Img_viewer = Img_viewer;
const has_children = o=>!!o && typeof o=='object' && Object.keys(o).length;
const JSON_viewer = ({json})=>
<div className="json_viewer">
<ol className="tree_root">
<Pair open val={json}/>
</ol>
</div>;
E.JSON_viewer = JSON_viewer;
const Children = ({val, expanded})=>{
if (has_children(val) && expanded)
{
return <ol className="tree_children">
{Object.entries(val).map(e=>
<Pair key={e[0]} label={e[0]} val={e[1]}/>
)}
</ol>;
}
return null;
};
class Pair extends React.PureComponent {
state = {expanded: this.props.open};
toggle = ()=>{ this.setState(prev=>({expanded: !prev.expanded})); };
render(){
const {label, val} = this.props;
return [
<Tree_item
expanded={this.state.expanded}
label={label}
val={val}
toggle={this.toggle}
key="tree_item"/>,
<Children val={val} expanded={this.state.expanded} key="val"/>,
];
}
}
const Tree_item = ({label, val, expanded, toggle})=>{
const classes = classnames('tree_item', {
parent: has_children(val),
expanded,
});
return <li className={classes} onClick={toggle}>
{label ? [
<span key="name" className="name">{label}</span>,
<span key="separator" className="separator">: </span>
] : null}
<Value val={val} expanded={expanded}/>
</li>;
};
const Value = ({val})=>{
if (typeof val=='object')
return <Value_object val={val}/>;
else if (typeof val=='number')
return <span className="value number">{val}</span>;
else if (typeof val=='boolean')
return <span className="value boolean">{val.toString()}</span>;
else if (typeof val=='string')
return <span className="value string">"{val}"</span>;
else if (typeof val=='undefined')
return <span className="value undefined">"{val}"</span>;
else if (typeof val=='function')
return null;
};
const Value_object = ({val})=>{
if (val===null)
return <span className="value null">null</span>;
if (Array.isArray(val))
{
if (!val.length)
return <span className="value array empty">[]</span>;
return <span className="value array long">[,...]</span>;
}
if (!Object.keys(val).length)
return <span className="value object empty">{'{}'}</span>;
return <span className="value object">{JSON.stringify(val)}</span>;
};
const with_resizable_cols = Table=>{
class Resizable extends React.PureComponent {
constructor(props){
super(props);
this.state = {};
this.cols = zutil.clone_deep(this.props.table_cols);
this.min_width = 22;
this.moving_col = null;
this.style = {
position: 'relative',
display: 'flex',
flex: 'auto',
width: '100%',
};
}
componentDidMount(){
this.resize_columns();
window.document.addEventListener('mousemove', this.on_mouse_move);
window.document.addEventListener('mouseup', this.on_mouse_up);
}
componentWillUnmount(){
window.document.removeEventListener('mousemove',
this.on_mouse_move);
window.document.removeEventListener('mouseup', this.on_mouse_up);
}
set_ref = ref=>{ this.ref = ref; };
resize_columns = ()=>{
const total_width = this.ref.offsetWidth;
const resizable_cols = this.cols.filter(c=>!c.hidden && !c.fixed);
const total_fixed = this.cols.reduce((acc, c)=>
acc+(!c.hidden && c.fixed || 0), 0);
const width = (total_width-total_fixed)/resizable_cols.length;
const next_cols = this.cols.reduce((acc, c, idx)=>{
const w = !c.fixed && width||!c.hidden && c.fixed || 0;
return {
cols: [...acc.cols, {
...c,
width: w,
offset: acc.offset,
border: acc.border,
}],
offset: acc.offset+w,
border: !!w,
};
}, {cols: [], offset: 0, border: true});
this.setState({cols: next_cols.cols});
};
start_moving = (e, idx)=>{
if (e.nativeEvent.which!=1)
return;
this.start_offset = e.pageX;
this.start_width = this.state.cols[idx].width;
this.start_width_last = this.state.cols.slice(-1)[0].width;
this.moving_col = idx;
this.setState({moving: true});
};
on_mouse_move = e=>{
if (this.moving_col===null)
return;
this.setState(prev=>{
let offset = e.pageX-this.start_offset;
if (this.start_width_last-offset<this.min_width)
offset = this.start_width_last-this.min_width;
if (this.start_width+offset<this.min_width)
offset = this.min_width-this.start_width;
let total_width = 0;
const next_cols = prev.cols.map((c, idx)=>{
if (idx<this.moving_col)
{
total_width = total_width+c.width;
return c;
}
else if (idx==this.moving_col)
{
const width = this.start_width+offset;
total_width = total_width+width;
return {...c, width, offset: total_width-width};
}
else if (idx==this.state.cols.length-1)
{
const width = this.start_width_last-offset;
return {...c, width, offset: total_width};
}
total_width = total_width+c.width;
return {...c, offset: total_width-c.width};
});
return {cols: next_cols};
});
};
on_mouse_up = ()=>{
this.moving_col = null;
this.setState({moving: false});
};
render(){
const style = Object.assign({}, this.style, this.props.style||{});
return <div
style={style}
ref={this.set_ref}
className={classnames({moving: this.state.moving})}
>
<Table {...this.props}
cols={this.state.cols}
resize_columns={this.resize_columns}
/>
<Grid_resizers show={!this.props.cur_preview}
start_moving={this.start_moving}
cols={this.state.cols}
/>
</div>;
}
}
return Resizable;
};
const Grid_resizers = ({cols, start_moving, show})=>{
if (!show||!cols)
return null;
return <div>
{cols.slice(0, -1).map((c, idx)=>
!c.fixed &&
<div key={c.title||idx} style={{left: c.width+c.offset-2}}
onMouseDown={e=>start_moving(e, idx)}
className="data_grid_resizer"/>
)}
</div>;
};
const Search_box = ({val, on_change})=>
<div className="search_box">
<input value={val}
onChange={on_change}
type="text"
placeholder="Filter"
/>
</div>;
const Toolbar_row = ({children})=>
<div className="toolbar">
{children}
</div>;
const Toolbar_container = ({children})=>
<div className="toolbar_container">
{children}
</div>;
const Sort_icon = ({show, dir})=>{
if (!show)
return null;
const classes = classnames('small_icon_mask', {
sort_asc: dir==-1,
sort_desc: dir==1,
});
return <div className="sort_icon"><span className={classes}/></div>;
};
const Devider = ()=><div className="devider"/>;
class Har_viewer extends Pure_component {
moving_width = false;
min_width = 50;
state = {
cur_preview: null,
tables_width: 200,
};
componentDidMount(){
window.document.addEventListener('mousemove', this.on_mouse_move);
window.document.addEventListener('mouseup', this.on_mouse_up);
}
willUnmount(){
window.document.removeEventListener('mousemove', this.on_mouse_move);
window.document.removeEventListener('mouseup', this.on_mouse_up);
}
open_preview = req=>this.setState({cur_preview: req});
close_preview = ()=>this.setState({cur_preview: null});
start_moving_width = e=>{
if (e.nativeEvent.which!=1)
return;
this.moving_width = true;
$(this.main_panel).addClass('moving');
this.start_offset = e.pageX;
this.start_width = this.state.tables_width;
};
on_resize_width = e=>{
const offset = e.pageX-this.start_offset;
let new_width = this.start_width+offset;
if (new_width<this.min_width)
new_width = this.min_width;
const max_width = this.main_panel.offsetWidth-this.min_width;
if (new_width>max_width)
new_width = max_width;
this.setState({tables_width: new_width});
};
on_mouse_move = e=>{
if (this.moving_width)
this.on_resize_width(e);
};
on_mouse_up = ()=>{
this.moving_width = false;
$(this.main_panel).removeClass('moving');
};
set_main_panel_ref = ref=>{ this.main_panel = ref; };
main_panel_moving = ()=>{ $(this.main_panel).addClass('moving'); };
main_panel_stopped_moving = ()=>{
$(this.main_panel).removeClass('moving'); };
undock = ()=>{
if (this.props.dock_mode)
return;
const url = '/dock_logs';
const opts = 'directories=0,titlebar=0,toolbar=0,location=0,'
+'status=0,menubar=0,scrollbars=0,resizable=0,height=500,'
+'width=800';
const har_window = window.open(url, 'har_window', opts);
if (window.focus)
har_window.focus();
};
clear = ()=>{
this.props.clear_logs(()=>{
this.close_preview();
setdb.emit_path('head.har_viewer.reset_reqs');
});
};
render(){
const width = `calc(100% - ${this.state.tables_width}px`;
const preview_style = {maxWidth: width, minWidth: width};
return <div id="har_viewer" className="har_viewer chrome">
<div className="main_panel vbox" ref={this.set_main_panel_ref}>
{this.props.toolbar &&
<Toolbar
undock={this.undock}
clear={this.clear}
dock_mode={this.props.dock_mode}
filters={this.props.filters}
set_filter={this.props.set_filter}
type_filter={this.props.type_filter}
set_type_filter={this.props.set_type_filter}
on_change_search={this.props.on_change_search}
search_val={this.props.search}
disable_logs={this.props.disable_logs}
/>
}
<div className="split_widget vbox flex_auto">
<Tables_container
Cell_value={this.props.Cell_value}
table_cols={this.props.table_cols}
main_panel_moving={this.main_panel_moving}
main_panel_stopped_moving=
{this.main_panel_stopped_moving}
main_panel={this.main_panel}
open_preview={this.open_preview}
width={this.state.tables_width}
cur_preview={this.state.cur_preview}
set_sort={this.props.set_sort}
sorted={this.props.sorted}
reqs={this.props.reqs}
handle_viewpoint_enter={this.props.handle_viewpoint_enter}
Waypoint={this.props.Waypoint}
/>
<Preview
panes={this.props.panes}
cur_preview={this.state.cur_preview}
style={preview_style}
close={this.close_preview}
/>
<Tables_resizer show={!!this.state.cur_preview}
start_moving={this.start_moving_width}
offset={this.state.tables_width}
/>
</div>
</div>
</div>;
}
}
E.Har_viewer = Har_viewer;
class Toolbar extends Pure_component {
state = {filters_visible: false};
toggle_filters = ()=>
this.setState({filters_visible: !this.state.filters_visible});
render(){
return <Toolbar_container>
<Toolbar_row>
<Toolbar_button id="clear"
tooltip="Clear"
on_click={this.props.clear}
/>
{!this.props.dock_mode &&
<Toolbar_button id="docker"
on_click={this.props.undock}
tooltip="Undock into separate window"
/>
}
<Toolbar_button id="filters"
tooltip="Show/hide filters"
on_click={this.toggle_filters}
active={this.state.filters_visible}
/>
<Toolbar_button id="download"
tooltip="Export as HAR file"
href="/api/logs_har"
/>
<Toolbar_button id="close_btn"
tooltip="Disable"
placement="left"
on_click={this.props.disable_logs}
/>
</Toolbar_row>
{this.state.filters_visible &&
<Toolbar_row>
<Search_box
val={this.props.search_val}
on_change={this.props.on_change_search}
/>
<Type_filters
filter={this.props.type_filter}
set={this.props.set_type_filter}
/>
<Devider/>
<Filters
set_filter={this.props.set_filter}
filters={this.props.filters}
/>
</Toolbar_row>
}
</Toolbar_container>;
}
}
class Filters extends Pure_component {
state = {};
componentDidMount(){
this.setdb_on('head.logs_suggestions', suggestions=>{
suggestions && this.setState({suggestions});
});
}
render(){
if (!this.state.suggestions)
return null;
const filters = [
{
name: 'port',
default_value: 'All proxy ports',
},
{
name: 'status_code',
default_value: 'All status codes',
},
{
name: 'protocol',
default_value: 'All protocols',
},
];
return <div className="filters">
{filters.map(f=>
<Filter key={f.name}
vals={this.state.suggestions[f.name+'s']}
val={this.props.filters[f.name]}
set={this.props.set_filter.bind(null, f.name)}
default_value={f.default_value}
/>
)}
</div>;
}
}
const Filter = ({vals, val, set, default_value, format_text})=>
<div className="custom_filter">
<select value={val} onChange={set}>
<option value="">{default_value}</option>
{vals.map(p=>
<option key={p} value={p}>
{format_text ? format_text(p) : p}
</option>
)}
</select>
<span className="arrow"/>
</div>;
E.Filter = Filter;
const type_filters = ['XHR', 'HTML', 'JS', 'CSS', 'Img', 'Media', 'Font',
'Other'];
const Type_filters = ({filter, set})=>
<div className="filters">
<Type_filter name="All" on_click={set.bind(null, 'All')} cur={filter}/>
<Devider/>
{type_filters.map(f=>
<Type_filter
on_click={set.bind(null, f)}
key={f}
name={f}
cur={filter}
/>
)}
</div>;
const Type_filter = ({name, cur, on_click})=>
<div className={classnames('filter', {active: cur==name})}
onClick={on_click}>
{name}
</div>;
const Tables_resizer = ({show, offset, start_moving})=>{
if (!show)
return null;
return <div className="data_grid_resizer"
style={{left: offset-2}}
onMouseDown={start_moving}
/>;
};
const Tables_container = with_resizable_cols(
class Tables_container extends Pure_component {
constructor(props){
super(props);
this.state = {focused: false};
}
componentDidUpdate(prev_props){
if (prev_props.cur_preview!=this.props.cur_preview)
this.props.resize_columns();
}
componentDidMount(){
window.addEventListener('resize', this.props.resize_columns);
}
willUnmount(){
window.removeEventListener('resize', this.props.resize_columns);
}
on_focus = ()=>this.setState({focused: true});
on_blur = ()=>this.setState({focused: false});
on_mouse_up = ()=>{
this.moving_col = null;
this.props.main_panel_stopped_moving();
};
render(){
const style = {};
if (this.props.cur_preview)
{
style.flex = `0 0 ${this.props.width}px`;
style.width = this.props.width;
style.maxWidth = this.props.width;
}
return <div className="tables_container vbox"
tabIndex="-1"
style={style}
onFocus={this.on_focus}
onBlur={this.on_blur}>
<div className="reqs_container">
<Header_container
cols={this.props.cols}
reqs={this.props.reqs}
set_sort={this.props.set_sort}
sorted={this.props.sorted}
only_name={!!this.props.cur_preview}
/>
<Data_container
Cell_value={this.props.Cell_value}
cols={this.props.cols}
reqs={this.props.reqs}
handle_viewpoint_enter={this.props.handle_viewpoint_enter}
focused={this.state.focused}
cur_preview={this.props.cur_preview}
open_preview={this.props.open_preview}
Waypoint={this.props.Waypoint}
/>
</div>
</div>;
}
});
class Header_container extends Pure_component {
click = col=>{
this.props.set_sort(col.sort_by);
};
render(){
let {cols, only_name, sorted} = this.props;
if (!cols)
return null;
if (only_name)
cols = [cols[1]];
return <div className="header_container">
<table className="chrome_table">
<colgroup>
{cols.map((c, idx)=>
<col key={c.title}
style={{width: only_name||idx==cols.length-1 ?
'auto' : c.width}}
/>
)}
</colgroup>
<tbody>
<tr>
{cols.map(c=>
<th key={c.title} onClick={()=>this.click(c)}
style={{textAlign: only_name ? 'left' : null}}>
<div>{c.title}</div>
<Sort_icon show={c.sort_by==sorted.field}
dir={sorted.dir}/>
</th>
)}
</tr>
</tbody>
</table>
</div>;
}
}
class Data_container extends Pure_component {
componentDidMount(){
this.setdb_on('head.har_viewer.dc_top', ()=>{
if (this.dc.current)
this.dc.current.scrollTop = 0;
});
}
dc = React.createRef();
render(){
let {cols, open_preview, cur_preview, focused, reqs} = this.props;
const preview_mode = !!cur_preview;
cols = (cols||[]).map((c, idx)=>{
if (!preview_mode)
return c;
if (preview_mode && idx==1)
return {...c, width: 'auto'};
return {...c, width: 0};
});
const Waypoint = this.props.Waypoint;
return <div ref={this.dc} className="data_container">
<table className="chrome_table">
<colgroup>
{cols.map((c, idx)=>
<col key={c.title}
style={{width: !preview_mode && idx==cols.length-1 ?
'auto': c.width}}
/>
)}
</colgroup>
<Data_rows
Cell_value={this.props.Cell_value}
reqs={reqs}
cols={cols}
open_preview={open_preview}
cur_preview={cur_preview}
focused={focused}
/>
</table>
{Waypoint &&
<Waypoint
key={reqs.length}
scrollableAncestor={this.dc.current}
bottomOffset="-50px"
onEnter={this.props.handle_viewpoint_enter}
/>
}
</div>;
}
}
class Data_rows extends React.Component {
shouldComponentUpdate(next_props){
return next_props.reqs!=this.props.reqs ||
next_props.cur_preview!=this.props.cur_preview ||
next_props.focused!=this.props.focused;
}
render(){
return <tbody>
{this.props.reqs.map(r=>
<Data_row
Cell_value={this.props.Cell_value}
cols={this.props.cols}
key={r.uuid}
open_preview={this.props.open_preview}
cur_preview={this.props.cur_preview}
focused={this.props.focused}
req={r}
/>
)}
<tr className="filler">
{this.props.cols.map(c=><td key={c.title}/>)}
</tr>
</tbody>;
}
}
class Data_row extends React.Component {
shouldComponentUpdate(next_props){
const selected = zutil.get(this.props.cur_preview, 'uuid')==
this.props.req.uuid;
const will_selected = zutil.get(next_props.cur_preview, 'uuid')==
next_props.req.uuid;
const selection_changed = selected!=will_selected;
const focused_changed = this.props.focused!=next_props.focused;
const pending_changed = this.props.req.pending!=next_props.req.pending;
return selection_changed || focused_changed && selected ||
pending_changed;
}
cell_clicked = ()=>{
this.props.open_preview(this.props.req);
};
render(){
const {cur_preview, cols, focused, req} = this.props;
const selected = zutil.get(cur_preview, 'uuid')==req.uuid;
const classes = classnames({
selected,
focused: selected && focused,
error: !req.details.success && !req.pending,
pending: !!req.pending,
});
return <tr className={classes}>
{cols.map((c, idx)=>
<td key={c.title} onClick={this.cell_clicked}>
<this.props.Cell_value col={c.title} req={req}/>
</td>
)}
</tr>;
}
}
return E;
});