UNPKG

@luminati-io/luminati-proxy

Version:

A configurable local proxy for luminati.io

1,099 lines (1,072 loc) 40.5 kB
// LICENSE_CODE ZON ISC 'use strict'; /*jslint react:true, es6:true*/ import Pure_component from '/www/util/pub/pure_component.js'; import React from 'react'; import $ from 'jquery'; import _ from 'lodash'; import moment from 'moment'; import classnames from 'classnames'; import {Route, withRouter, Link} from 'react-router-dom'; import React_tooltip from 'react-tooltip'; import etask from '../../util/etask.js'; import setdb from '../../util/setdb.js'; import ajax from '../../util/ajax.js'; import zescape from '../../util/escape.js'; import {status_codes, bytes_format, report_exception} from './util.js'; import {Waypoint} from 'react-waypoint'; import {Toolbar_button, Devider, Sort_icon, with_resizable_cols, Toolbar_container, Toolbar_row, Search_box} from './chrome_widgets.js'; import {T} from './common/i18n.js'; import Preview from './har_preview.js'; import {Tooltip_bytes, Checkbox} from './common.js'; import Tooltip from './common/tooltip.js'; import ws from './ws.js'; import './css/har_viewer.less'; const loader = { start: ()=>$('#har_viewer').addClass('waiting'), end: ()=>$('#har_viewer').removeClass('waiting'), }; const enable_ssl_click = port=>etask(function*(){ this.on('finally', ()=>{ loader.end(); }); loader.start(); yield window.fetch('/api/enable_ssl', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({port}), }); const proxies = yield ajax.json({url: '/api/proxies_running'}); setdb.set('head.proxies_running', proxies); }); class Har_viewer extends Pure_component { moving_width = false; min_width = 50; state = { cur_preview: null, tables_width: 200, search: this.props.domain||'', type_filter: 'All', filters: { port: this.props.port||false, status_code: this.props.code||false, protocol: this.props.protocol||false, }, }; componentDidMount(){ window.document.addEventListener('mousemove', this.on_mouse_move); window.document.addEventListener('mouseup', this.on_mouse_up); this.setdb_on('head.proxies_running', proxies=>{ if (proxies) this.setState({proxies}); }); this.setdb_on('head.settings', settings=>{ if (settings) this.setState({logs: settings.logs}); }); this.etask(function*(){ const suggestions = yield ajax.json( {url: '/api/logs_suggestions'}); suggestions.status_codes.unshift(...[2, 3, 4, 5].map(v=>`${v}**`)); setdb.set('head.logs_suggestions', suggestions); }); } willUnmount(){ loader.end(); 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'); }; clear = ()=>{ const params = {}; if (this.props.match && this.props.match.params.port) params.port = this.props.match.params.port; const url = zescape.uri('/api/logs_reset', params); const _this = this; this.etask(function*(){ loader.start(); yield ajax({url}); _this.close_preview(); setdb.emit_path('head.har_viewer.reset_reqs'); loader.end(); }); }; 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'); }; on_change_search = e=>{ this.setState({search: e.target.value}); }; set_type_filter = name=>{ this.setState({type_filter: name}); }; set_filter = (name, {target: {value}})=>{ this.setState(prev=>({filters: {...prev.filters, [name]: value}})); }; 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(); }; render(){ if (!this.state.proxies) return null; const width = `calc(100% - ${this.state.tables_width}px`; const preview_style = {maxWidth: width, minWidth: width}; const show = this.state.logs>0; return <div id="har_viewer" className={(show ? 'har_viewer' : 'har_viewer_off')+' chrome'}> {!show && <Route path={['/logs', '/proxy/:port/logs/har']} component={Logs_off_notice}/> } {show && <div className="main_panel vbox" ref={this.set_main_panel_ref}> <Toolbar undock={this.undock} dock_mode={this.props.dock_mode} master_port={this.props.master_port} filters={this.state.filters} set_filter={this.set_filter} proxies={this.state.proxies} type_filter={this.state.type_filter} set_type_filter={this.set_type_filter} clear={this.clear} on_change_search={this.on_change_search} search_val={this.state.search}/> <div className="split_widget vbox flex_auto"> <Tables_container key={''+this.props.master_port} master_port={this.props.master_port} 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} search={this.state.search} type_filter={this.state.type_filter} filters={this.state.filters} cur_preview={this.state.cur_preview}/> <Preview 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>; } } class Toolbar extends Pure_component { state = {select_visible: false, filters_visible: false, actions_visible: false}; componentDidMount(){ this.setdb_on('har_viewer.select_visible', visible=> this.setState({select_visible: visible})); this.setdb_on('har_viewer.select_mode', actions_visible=> this.setState({actions_visible})); this.setdb_on('head.save_settings', save_settings=>{ this.save_settings = save_settings; if (this.disable) { this.disable_logs(); delete this.disable; } }); } toggle_filters = ()=> this.setState({filters_visible: !this.state.filters_visible}); toggle_actions = ()=>{ setdb.set('har_viewer.select_mode', !this.state.actions_visible); }; disable_logs = ()=>{ if (!this.save_settings) { this.disable = true; return; } const _this = this; this.etask(function*(){ const settings = Object.assign({}, setdb.get('head.settings')); settings.logs = 0; yield _this.save_settings(settings); }); }; render(){ const {clear, search_val, on_change_search, type_filter, set_type_filter, filters, set_filter, master_port, undock, dock_mode} = this.props; return <Toolbar_container> <T>{t=><Toolbar_row> <Toolbar_button id="clear" tooltip={t('Clear')} on_click={clear}/> {!dock_mode && <Toolbar_button id="docker" on_click={undock} tooltip={t('Undock into separate window')}/> } <Toolbar_button id="filters" tooltip={t('Show/hide filters')} on_click={this.toggle_filters} active={this.state.filters_visible}/> <Toolbar_button id="download" tooltip={t('Export as HAR file')} href="/api/logs_har"/> <Toolbar_button id="actions" on_click={this.toggle_actions} active={this.state.actions_visible} tooltip={t('Show/hide additional actions')}/> <Toolbar_button id="close_btn" tooltip={t('Disable')} placement="left" on_click={this.disable_logs}/> </Toolbar_row>}</T> {this.state.actions_visible && <Toolbar_row> <Actions/> </Toolbar_row> } {this.state.filters_visible && <Toolbar_row> <Search_box val={search_val} on_change={on_change_search}/> <Type_filters filter={type_filter} set={set_type_filter}/> <Devider/> <Filters set_filter={set_filter} filters={filters} master_port={master_port}/> </Toolbar_row> } </Toolbar_container>; } } class Actions extends Pure_component { state = {any_checked: false}; componentDidMount(){ this.setdb_on('har_viewer.checked_list', list=>{ if (!list) return; const any_checked = Object.keys(list).filter(o=>list[o]).length; this.setState({any_checked}); }); } resend = ()=>{ const list = setdb.get('har_viewer.checked_list')||[]; if (!Object.keys(list).length) return; const uuids = Object.keys(list).filter(o=>list[o]); const _this = this; this.etask(function*(){ this.on('uncaught', e=>_this.etask(function*(){ yield report_exception(e, 'har_viewer.Actions.resend'); })); yield window.fetch('/api/logs_resend', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({uuids}), }); }); }; render(){ const resend_classes = classnames('filter', {disabled: !this.state.any_checked}); return <div className="actions"> <div className="filters"> <Tooltip title="Resend requests" placement="bottom"> <div className={resend_classes} onClick={this.resend}>Resend</div> </Tooltip> </div> </div>; } } 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: this.props.master_port ? `Multiplied ${this.props.master_port}` : 'All proxy ports', tooltip: 'Filter requests by ports', }, { name: 'status_code', default_value: 'All status codes', tooltip: 'Filter requests by status codes', }, { name: 'protocol', default_value: 'All protocols', tooltip: 'Filter requests by protocols', }, ]; return <div className="filters"> {filters.map(f=> <Filter key={f.name} tooltip={f.tooltip} 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, tooltip})=> <Tooltip title={tooltip} placement="bottom"> <div className="custom_filter"> <select value={val} onChange={set}> <option value="">{default_value}</option> {vals.map(p=><option key={p} value={p}>{p}</option>)} </select> <span className="arrow"/> </div> </Tooltip>; const type_filters = [{name: 'XHR', tooltip: 'XHR and fetch'}, {name: 'HTML', tooltip: 'HTML'}, {name: 'JS', tooltip: 'Scripts'}, {name: 'CSS', tooltip: 'Stylesheets'}, {name: 'Img', tooltip: 'Images'}, {name: 'Media', tooltip: 'Media'}, {name: 'Font', tooltip: 'Fonts'}, {name: 'Other', tooltip: 'Other'}]; const Type_filters = ({filter, set})=> <div className="filters"> <Type_filter name="All" on_click={set.bind(null, 'All')} cur={filter} tooltip="All types"/> <Devider/> {type_filters.map(f=> <Type_filter on_click={set.bind(null, f.name)} key={f.name} name={f.name} cur={filter} tooltip={f.tooltip}/> )} </div>; const Type_filter = ({name, cur, tooltip, on_click})=> <Tooltip title={tooltip} placement="bottom"> <div className={classnames('filter', {active: cur==name})} onClick={on_click}>{name}</div> </Tooltip>; 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 Logs_off_notice = ()=> <div> <h4> Request logs are disabled. You can enable it back in &nbsp; <Link to="/settings">General settings</Link> </h4> </div>; const table_cols = [ {title: 'select', hidden: true, fixed: 27, tooltip: 'Select/unselect all'}, {title: 'Name', sort_by: 'url', data: 'request.url', tooltip: 'Request url'}, {title: 'Proxy port', sort_by: 'port', data: 'details.port'}, {title: 'Status', sort_by: 'status_code', data: 'response.status', tooltip: 'Status code'}, {title: 'Bandwidth', sort_by: 'bw', data: 'details.bw'}, {title: 'Time', sort_by: 'elapsed', data: 'time'}, {title: 'Peer proxy', sort_by: 'proxy_peer', data: 'details.proxy_peer'}, {title: 'Date', sort_by: 'timestamp', data: 'details.timestamp'}, ]; const Tables_container = withRouter(with_resizable_cols(table_cols, class Tables_container extends Pure_component { constructor(props){ super(props); this.uri = '/api/logs'; this.batch_size = 30; this.loaded = {from: 0, to: 0}; this.state = { focused: false, reqs: [], sorted: {field: 'timestamp', dir: 1}, }; this.reqs_to_render = []; this.temp_total = 0; this.take_reqs_from_pool = _.throttle(this.take_reqs_from_pool, 100); } componentDidUpdate(prev_props){ if (this.props.search!=prev_props.search) this.set_new_params_debounced(); if (this.props.type_filter!=prev_props.type_filter|| this.props.filters!=prev_props.filters) { this.set_new_params(); } if (prev_props.cur_preview!=this.props.cur_preview) this.props.resize_columns(); } componentDidMount(){ window.addEventListener('resize', this.props.resize_columns); this.setdb_on('head.har_viewer.reset_reqs', ()=>{ setdb.set('har_viewer.checked_list', []); this.loaded.to = 0; this.setState({ reqs: [], stats: {total: 0, sum_out: 0, sum_in: 0}, }); }, {init: false}); this.setdb_on('head.har_viewer.reqs', reqs=>{ if (reqs) this.setState({reqs}); }); this.setdb_on('head.har_viewer.stats', stats=>{ if (stats) this.setState({stats}); }); ws.addEventListener('message', this.on_message); this.setdb_on('har_viewer.select_mode', select=>{ if (select==undefined) return; if (select) this.props.show_column(0); else this.props.hide_column(0); }); } willUnmount(){ window.removeEventListener('resize', this.props.resize_columns); ws.removeEventListener('message', this.on_message); setdb.set('head.har_viewer.reqs', []); setdb.set('head.har_viewer.stats', null); setdb.set('har_viewer', null); this.take_reqs_from_pool.cancel(); } fetch_missing_data = pos=>{ if (this.state.stats && this.state.stats.total && this.state.reqs.length==this.state.stats.total) { return; } if (pos=='bottom') this.get_data({skip: this.loaded.to-this.temp_total}); }; get_params = opt=>{ const params = opt; params.limit = opt.limit||this.batch_size; params.skip = opt.skip||0; if (this.props.match.params.port) params.port = this.props.match.params.port; if (this.props.master_port) { const proxies = setdb.get('head.proxies_running'); const mp = proxies.find(p=>p.port==this.props.master_port); this.port_range = {from: mp.port, to: mp.port+mp.multiply-1}; params.port_from = this.port_range.from; params.port_to = this.port_range.to; } if (this.props.search&&this.props.search.trim()) params.search = this.props.search; if (this.state.sorted) { params.sort = this.state.sorted.field; if (this.state.sorted.dir==1) params.sort_desc = true; } if (this.props.type_filter&&this.props.type_filter!='All') params.content_type = this.props.type_filter.toLowerCase(); for (let filter in this.props.filters) { let val; if (val = this.props.filters[filter]) params[filter] = val; } return params; }; get_data = (opt={})=>{ if (this.sql_loading) return; const params = this.get_params(opt); const _this = this; this.sql_loading = true; this.etask(function*(){ this.on('finally', ()=>{ _this.sql_loading = false; loader.end(); }); loader.start(); const url = zescape.uri(_this.uri, params); const res = yield ajax.json({url}); const reqs = res.log.entries; const new_reqs = [...opt.replace ? [] : _this.state.reqs, ...reqs]; const uuids = new Set(); const new_reqs_unique = new_reqs.filter(r=>{ if (uuids.has(r.uuid)) return false; uuids.add(r.uuid); return true; }); setdb.set('head.har_viewer.reqs', new_reqs_unique); _this.loaded.to = opt.skip+reqs.length; const stats = { total: res.total+_this.temp_total, sum_out: res.sum_out, sum_in: res.sum_in, }; _this.temp_total = 0; if (!_this.state.stats) setdb.set('head.har_viewer.stats', stats); }); }; set_new_params = ()=>{ if (this.sql_loading) return; this.loaded.to = 0; setdb.emit_path('head.har_viewer.dc_top'); this.get_data({replace: true}); }; set_new_params_debounced = _.debounce(this.set_new_params, 400); set_sort = field=>{ if (this.sql_loading) return; let dir = 1; if (this.state.sorted.field==field) dir = -1*this.state.sorted.dir; this.setState({sorted: {field, dir}}, this.set_new_params); }; on_focus = ()=>this.setState({focused: true}); on_blur = ()=>this.setState({focused: false}); is_hidden = req=>{ const cur_port = req.details.port; const port = this.props.match.params.port; if (port && cur_port!=port) return true; if (this.port_range && (cur_port<this.port_range.from || cur_port>this.port_range.to)) { return true; } if (this.props.search && !req.request.url.match( new RegExp(this.props.search))) { return true; } if (this.props.type_filter && this.props.type_filter!='All' && req.details.content_type!=this.props.type_filter.toLowerCase()) { return true; } if (this.props.filters.port && this.props.filters.port!=req.details.port) { return true; } if (this.props.filters.protocol && this.props.filters.protocol!=req.details.protocol) { return true; } if (this.props.filters.status_code && this.props.filters.status_code!=req.response.status) { return true; } return false; }; is_visible = r=>!this.is_hidden(r); on_message = event=>{ const json = JSON.parse(event.data); if (json.type=='har_viewer') this.on_request_message(json.data); else if (json.type=='har_viewer_start') this.on_request_started_message(json.data); }; on_request_started_message = req=>{ req.pending = true; this.on_request_message(req); }; on_request_message = req=>{ this.reqs_to_render.push(req); this.take_reqs_from_pool(); }; take_reqs_from_pool = ()=>{ if (!this.reqs_to_render.length) return; const reqs = this.reqs_to_render.filter(this.is_visible); const all_reqs = this.reqs_to_render; if (this.batch_size>this.state.reqs.length) { this.loaded.to = Math.min(this.batch_size, this.state.reqs.length + reqs.length); } const new_reqs_set = {}; [...this.state.reqs, ...reqs].forEach(r=>{ if (!new_reqs_set[r.uuid]) return new_reqs_set[r.uuid] = r; if (new_reqs_set[r.uuid].pending) new_reqs_set[r.uuid] = r; }); const sorted_field = this.props.cols.find( c=>c.sort_by==this.state.sorted.field).data; const dir = this.state.sorted.dir; const new_reqs = Object.values(new_reqs_set) .sort((a, b)=>{ const val_a = _.get(a, sorted_field); const val_b = _.get(b, sorted_field); if (val_a==val_b) return a.uuid > b.uuid ? -1*dir : dir; return val_a > val_b ? -1*dir : dir; }).slice(0, Math.max(this.state.reqs.length, this.batch_size)); this.reqs_to_render = []; this.setState(prev=>{ const new_state = {reqs: new_reqs}; if (prev.stats) { new_state.stats = { total: prev.stats.total+ all_reqs.filter(r=>r.pending).length, sum_out: prev.stats.sum_out+all_reqs.reduce((acc, r)=> acc+r.details.out_bw||0, 0), sum_in: prev.stats.sum_in+all_reqs.reduce((acc, r)=> acc+r.details.in_bw||0, 0), }; } else this.temp_total += all_reqs.filter(r=>r.pending).length; return new_state; }); }; 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.state.reqs} sort={this.set_sort} sorted={this.state.sorted} only_name={!!this.props.cur_preview}/> <Data_container cols={this.props.cols} fetch_missing_data={this.fetch_missing_data} reqs={this.state.reqs} focused={this.state.focused} cur_preview={this.props.cur_preview} open_preview={this.props.open_preview}/> </div> <Summary_bar stats={this.state.stats}/> </div>; } })); class Summary_bar extends Pure_component { render(){ let {total, sum_in, sum_out} = this.props.stats|| {total: 0, sum_in: 0, sum_out: 0}; sum_out = bytes_format(sum_out)||'0 B'; sum_in = bytes_format(sum_in)||'0 B'; const txt = t=>`${total} ${t('requests')} | ${sum_out} ${t('sent')} ` +`| ${sum_in} ${t('received')}`; return <div className="summary_bar"> <span> <T>{t=><Tooltip title={txt(t)}>{txt(t)}</Tooltip>}</T> </span> </div>; } } class Header_container extends Pure_component { state = {checked_all: false}; componentDidMount(){ this.setdb_on('har_viewer.checked_all', checked_all=>{ if (checked_all==undefined) return; this.setState({checked_all}); }); } toggle_all = ()=>{ const checked_all = !this.state.checked_all; this.setState({checked_all}); const uuids = this.props.reqs.map(r=>r.uuid); if (checked_all) { uuids.forEach(id=> setdb.set('har_viewer.checked_list.'+id, true)); } else { Object.keys(setdb.get('har_viewer').checked_list).forEach(id=> setdb.set('har_viewer.checked_list.'+id, false)); } setdb.emit_path('har_viewer.checked_list'); }; click = col=>{ if (col.fixed) this.toggle_all(); else this.props.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> <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=> <T key={c.title}>{t=> <Tooltip title={t(c.tooltip||c.title)}> <th key={c.title} onClick={()=>this.click(c)} style={{textAlign: only_name ? 'left' : null}}> <div> {c.title=='select' && <Checkbox checked={this.state.checked_all} // no-op to remove React warning on_change={()=>null}/>} {c.title!='select' && t(c.title)} </div> <Sort_icon show={c.sort_by==sorted.field} dir={sorted.dir}/> </th> </Tooltip> }</T> )} </tr> </tbody> </table> </div>; } } class Data_container extends Pure_component { state = {checked_all: false}; componentDidMount(){ this.setdb_on('head.har_viewer.dc_top', ()=>{ if (this.dc.current) this.dc.current.scrollTop = 0; }); this.setdb_on('har_viewer.checked_all', checked_all=>{ if (checked_all!=undefined) this.setState({checked_all}); }); } handle_viewpoint_enter = ()=>{ this.props.fetch_missing_data('bottom'); }; 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}; }); return <div ref={this.dc} className="data_container"> <table> <colgroup> {cols.map((c, idx)=> <col key={c.title} style={{width: !preview_mode && idx==cols.length-1 ? 'auto': c.width}}/> )} </colgroup> <Data_rows reqs={reqs} cols={cols} open_preview={open_preview} cur_preview={cur_preview} checked_all={this.state.checked_all} focused={focused}/> </table> <Waypoint key={reqs.length} scrollableAncestor={this.dc.current} bottomOffset="-50px" onEnter={this.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|| next_props.checked_all!=this.props.checked_all; } render(){ return <tbody> {this.props.reqs.map(r=> <Data_row cols={this.props.cols} key={r.uuid} open_preview={this.props.open_preview} cur_preview={this.props.cur_preview} checked_all={this.props.checked_all} 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 = _.get(this.props.cur_preview, 'uuid')== this.props.req.uuid; const will_selected = _.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 checked_all_changed = this.props.checked_all!= next_props.checked_all; const pending_changed = this.props.req.pending!=next_props.req.pending; return selection_changed||focused_changed&&selected|| checked_all_changed||pending_changed; } render(){ const {cur_preview, open_preview, cols, focused, req} = this.props; const selected = _.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={()=>idx!=0 && open_preview(req)}> <Cell_value col={c.title} req={req} checked_all={this.props.checked_all}/> </td> )} </tr>; } } const maybe_pending = Component=>function pies(props){ if (props.pending) { return <Tooltip title="The request is still loading"> <div className="disp_value">pending</div> </Tooltip>; } return <Component {...props}/>; }; class Cell_value extends React.Component { render(){ const {col, req, req: {details: {timeline, rules}}} = this.props; if (col=='select') { return <Select_cell uuid={req.uuid} checked_all={this.props.checked_all}/>; } if (col=='Name') return <Name_cell req={req} timeline={timeline} rules={rules}/>; else if (col=='Status') { return <Status_code_cell status={req.response.status} pending={!!req.pending} uuid={req.uuid} req={req}/>; } else if (col=='Proxy port') return <Tooltip_and_value val={req.details.port}/>; else if (col=='Bandwidth') return <Tooltip_bytes chrome_style bytes={req.details.bw}/>; else if (col=='Time') { return <Time_cell time={req.time} url={req.request.url} pending={!!req.pending} uuid={req.uuid} port={req.details.port}/>; } else if (col=='Peer proxy') { const ext_proxy = (setdb.get('head.proxies_running')||[]) .some(p=>p.port==req.details.port && p.ext_proxies); return <Tooltip_and_value val={req.details.proxy_peer} tip={ext_proxy ? 'This feature is only available when using ' +'proxies by Luminati network' : req.details.proxy_peer} pending={!!req.pending}/>; } else if (col=='Date') { const local = moment(new Date(req.startedDateTime|| req.details.timestamp)).format('YYYY-MM-DD HH:mm:ss'); return <Tooltip_and_value val={local}/>; } return col; } } class Name_cell extends Pure_component { go_to_rules = e=>setdb.emit('har_viewer.set_pane', 4); render(){ const {req, rules} = this.props; const rule_tip = 'At least one rule has been applied to this ' +'request. Click to see more details'; const status_check = req.details.context=='STATUS CHECK'; const is_ban = r=>Object.keys(r.action||{}) .some(a=>a.startsWith('ban_ip')); const bad = (rules||[]).some(is_ban); const icon_classes = classnames('small_icon', 'rules', { good: !bad, bad}); return <div className="col_name"> <div> <div className="icon script"/> {!!rules && !!rules.length && <Tooltip title={rule_tip}> <div onClick={this.go_to_rules} className={icon_classes}/> </Tooltip> } <Tooltip title={req.request.url}> <div className="disp_value"> {req.request.url + (status_check ? ' (status check)' : '')} </div> </Tooltip> </div> </div>; } } const Status_code_cell = maybe_pending(props=>{ const {status, uuid, req} = props; const get_desc = ()=>{ const err_header = req.response.headers.find( r=>r.name=='x-luminati-error'||r.name=='x-lpm-error'); if (status==502 && err_header) return err_header.value; return status=='canceled' ? '' : status_codes[status]; }; if (status=='unknown') { return <Encrypted_cell name="Status code" id={`s${uuid}`} port={req.details.port}/>; } const desc = get_desc(status); return <Tooltip title={`${status} ${desc}`}> <div className="disp_value">{status}</div> </Tooltip>; }); const Time_cell = maybe_pending(props=>{ const {port, time, url, uuid} = props; if (!url.endsWith(':443') || !time) return <Tooltip_and_value val={time && time+' ms'}/>; return <Encrypted_cell name="Timing" id={`t${uuid}`} port={port}/>; }); class Encrypted_cell extends Pure_component { state = {proxies: []}; componentDidMount(){ this.setdb_on('head.proxies_running', proxies=>{ if (!proxies) return; this.setState({proxies}); }); } is_ssl_on = port=>{ const proxy = this.state.proxies.find(p=>p.port==port); if (!proxy) return false; return proxy.ssl; }; render(){ const {id, name, port} = this.props; const ssl = this.is_ssl_on(port); return <div onClick={e=>e.stopPropagation()} className="disp_value"> <React_tooltip id={id} type="info" effect="solid" delayHide={100} delayShow={0} delayUpdate={500} offset={{top: -10}}> <div> {name} of this request could not be parsed because the connection is encrypted. </div> {!ssl && <div style={{marginTop: 10}}> <a onClick={()=>enable_ssl_click(port)} className="link"> Enable SSL analyzing </a> <span> to see {name} and other information about requests </span> </div> } {ssl && <div style={{marginTop: 10}}> SSL analyzing is already turned on and all the future requestes will be decoded. This request can't be decoded retroactively </div> } </React_tooltip> <div data-tip="React-tooltip" data-for={id}> <span>unknown</span> <div className="small_icon status info"/> </div> </div>; } } class Select_cell extends React.Component { state = {checked: false}; shouldComponentUpdate(next_props, next_state){ return next_state.checked!=this.state.checked|| next_props.checked_all!=this.props.checked_all; } componentDidMount(){ this.checked_listener = setdb.on( 'har_viewer.checked_list.'+this.props.uuid, checked=>{ if (checked==undefined) return; this.setState({checked}); }); } componentWillUnmount(){ setdb.off(this.checked_listener); } toggle = ()=>{ setdb.set('har_viewer.checked_list.'+this.props.uuid, !this.state.checked); setdb.emit_path('har_viewer.checked_list'); }; render(){ return <Checkbox checked={this.state.checked||this.props.checked_all} on_change={this.toggle}/>; } } const Tooltip_and_value = maybe_pending(({val, tip})=> <Tooltip title={tip||val}> <div className="disp_value">{val||'—'}</div> </Tooltip> ); export default Har_viewer;