UNPKG

@luminati-io/luminati-proxy

Version:

A configurable local proxy for luminati.io

500 lines (471 loc) 17.6 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 {withRouter} from 'react-router-dom'; import JSON_viewer from './json_viewer.js'; import codemirror from 'codemirror/lib/codemirror'; import 'codemirror/lib/codemirror.css'; import 'codemirror/mode/javascript/javascript'; import 'codemirror/mode/htmlmixed/htmlmixed'; import classnames from 'classnames'; import moment from 'moment'; import {trigger_types, action_types} from '../../util/rules_util.js'; import {Copy_btn} from './common.js'; import './css/har_preview.less'; import {T} from './common/i18n.js'; class Preview extends Pure_component { panes = [ {id: 'headers', width: 65, comp: Pane_headers}, {id: 'preview', width: 63, comp: Pane_preview}, {id: 'response', width: 72, comp: Pane_response}, {id: 'timing', width: 57, comp: Pane_timing}, {id: 'rules', width: 50, comp: Pane_rules}, ]; 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.panes[this.state.cur_pane].comp; 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.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.panes} cur_pane={this.state.cur_pane}/> </div> </div> <div className="tabbed_pane_content"> <Pane_content key={req.uuid} req={req}/> </div> </div>; } } const Pane = ({id, idx, width, on_click, active})=> <div onClick={()=>on_click(idx)} style={{width}} className={classnames('pane', id, {active})}> <span><T>{id}</T></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}/>; }; 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="lpm_har_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>; } } class Pane_response extends Pure_component { render(){ const req = this.props.req; const {port, content_type} = req.details; if (content_type=='unknown') return <Encrypted_response_data port={port}/>; if (!content_type||['xhr', 'css', 'js', 'font', 'html', 'other'] .includes(content_type)) { return <Codemirror_wrapper req={req}/>; } return <No_response_data/>; } } const Encrypted_response_data = withRouter( class Encrypted_response_data extends Pure_component { goto_ssl = ()=>{ this.props.history.push({ pathname: `/proxy/${this.props.port}`, state: {field: 'ssl'}, }); }; render(){ return <Pane_info> <div>This request is using SSL encryption.</div> <div> <span>You need to turn on </span> <a className="link" onClick={this.goto_ssl}> SSL analyzing</a> <span> to read the response here.</span> </div> </Pane_info>; } }); const Pane_info = ({children})=> <div className="empty_view"> <div className="block">{children}</div> </div>; const No_response_data = ()=> <div className="empty_view"> <div className="block">This request has no response data available.</div> </div>; class Codemirror_wrapper extends Pure_component { componentDidMount(){ this.cm = codemirror.fromTextArea(this.textarea, { readOnly: true, lineNumbers: true, }); this.cm.setSize('100%', '100%'); let text = this.props.req.response.content.text||''; try { text = JSON.stringify(JSON.parse(text), null, '\t'); } catch(e){} this.cm.doc.setValue(text); this.set_ct(); } componentDidUpdate(){ this.cm.doc.setValue(this.props.req.response.content.text||''); this.set_ct(); } set_ct(){ const content_type = this.props.req.details.content_type; let mode; if (!content_type||content_type=='xhr') mode = 'javascript'; if (content_type=='html') mode = 'htmlmixed'; this.cm.setOption('mode', mode); } set_textarea = ref=>{ this.textarea = ref; }; render(){ return <div className="codemirror_wrapper"> <textarea ref={this.set_textarea}/> </div>; } } 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})}> <T>{this.props.title}</T> {!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>; }; class Pane_rules extends Pure_component { render(){ const {details: {rules}} = this.props.req; if (!rules || !rules.length) { return <Pane_info> <div>No rules have been triggered on this request.</div> </Pane_info>; } return <div className="rules_view_wrapper"> <ol className="tree_outline"> {rules.map((r, idx)=> <Rule_preview key={idx} rule={r} idx={idx+1}/> )} </ol> </div>; } } class Rule_preview extends Pure_component { state = {open: true}; toggle = ()=>this.setState(prev=>({open: !prev.open})); render(){ const {rule, idx} = this.props; const children_classes = classnames('children', 'timeline', {open: this.state.open}); const first_trigger = trigger_types.find(t=>rule[t.value])||{}; return [ <li key="li" onClick={this.toggle} className={classnames('parent_title', 'expandable', {open: this.state.open})}> {idx}. {first_trigger.key} </li>, <ol key="ol" className={children_classes}> <Trigger_section rule={rule}/> <Action_section actions={rule.action}/> </ol> ]; } } const Trigger_section = ({rule})=> <div className="trigger_section"> {trigger_types.map(t=><Trigger key={t.value} type={t} rule={rule}/>)} </div>; const Trigger = ({type, rule})=>{ if (!rule[type.value]) return null; return <div className="trigger"> {type.key}: {rule[type.value]} </div>; }; const Action_section = ({actions})=> <div className="action_section"> {Object.keys(actions).map(a=> <Action key={a} action={a} value={actions[a]}/> )} </div>; const Action = ({action, value})=>{ const key = (action_types.find(a=>a.value==action)||{}).key; const val = action=='request_url' ? value&&value.url : value; return <div className="action"> {key} {val ? `: ${val}` : ''} </div>; }; class Pane_timing extends Pure_component { state = {}; componentDidMount(){ this.setdb_on('head.recent_stats', stats=>this.setState({stats})); } render(){ const {startedDateTime} = this.props.req; const started_at = moment(new Date(startedDateTime)).format( 'YYYY-MM-DD HH:mm:ss'); return <div className="timing_view_wrapper"> <div className="timeline_info">Started at {started_at}</div> <ol className="tree_outline"> {this.props.req.details.timeline.map((timeline, idx)=> <Single_timeline key={idx} timeline={timeline} time={this.props.req.time} req={this.props.req}/> )} </ol> <div className="timeline_info total"> Total: {this.props.req.time} ms</div> {this.props.req.request.url.endsWith('443') && this.state.stats && this.state.stats.ssl_enable && <Enable_https port={this.props.req.details.port}/> } </div>; } } class Single_timeline extends Pure_component { state = {open: true}; toggle = ()=>this.setState(prev=>({open: !prev.open})); render(){ const sections = ['Resource Scheduling', 'Request/Response']; const perc = [ {label: 'Queueing', id: 'blocked', section: 0}, {label: 'Connected', id: 'wait', section: 1}, {label: 'Response', id: 'receive', section: 1}, ].reduce((acc, el)=>{ const cur_time = this.props.timeline[el.id]; const left = acc.offset; const dur = Number((cur_time/this.props.time).toFixed(4)); const right = 1-acc.offset-dur; return {offset: acc.offset+dur, data: [...acc.data, {...el, left: `${left*100}%`, right: `${right*100}%`}]}; }, {offset: 0, data: []}).data .reduce((acc, el)=>{ if (el.section!=acc.last_section) return {last_section: el.section, data: [...acc.data, [el]]}; return { last_section: el.section, data: [ ...acc.data.slice(0, -1), [...acc.data.slice(-1)[0], el], ], }; }, {last_section: -1, data: []}).data; const children_classes = classnames('children', 'timeline', {open: this.state.open}); const {timeline} = this.props; return [ <li key="li" onClick={this.toggle} className={classnames('parent_title', 'expandable', {open: this.state.open})}> {timeline.port} </li>, <ol key="ol" className={children_classes}> <table> <colgroup> <col className="labels"/> <col className="bars"/> <col className="duration"/> </colgroup> <tbody> {perc.map((s, i)=> <Timing_header key={i} title={sections[s[0].section]}> {s.map(p=> <Timing_row title={p.label} id={p.id} left={p.left} key={p.id} right={p.right} time={this.props.timeline[p.id]}/> )} </Timing_header> )} </tbody> </table> </ol> ]; } } const Timing_header = ({title, children})=>[ <tr key="timing_header" className="table_header"> <td>{title}</td> <td></td> <td>TIME</td> </tr>, ...children, ]; const Timing_row = ({title, id, left, right, time})=> <tr className="timing_row"> <td>{title}</td> <td> <div className="timing_bar_wrapper"> <span className={classnames('timing_bar', id)} style={{left, right}}> &#8203;</span> </div> </td> <td><div className="timing_bar_title">{time} ms</div></td> </tr>; const Enable_https = withRouter(props=>{ const click = ()=>{ props.history.push({ pathname: `/proxy/${props.port}`, state: {field: 'ssl'}, }); }; return <div className="footer_link"> <a className="devtools_link" role="link" tabIndex="0" target="_blank" rel="noopener noreferrer" onClick={click} style={{display: 'inline', cursor: 'pointer'}}> Enable HTTPS logging </a> to view this timeline </div>; }); const is_json_str = str=>{ let resp; try { resp = JSON.parse(str); } catch(e){ return false; } return resp; }; class Pane_preview extends Pure_component { render(){ const content_type = this.props.req.details.content_type; const text = this.props.req.response.content.text; const port = this.props.req.details.port; let json; if (content_type=='unknown') return <Encrypted_response_data port={port}/>; if (content_type=='xhr' && (json = is_json_str(text))) return <JSON_viewer json={json}/>; if (content_type=='img') return <Img_viewer img={this.props.req.request.url}/>; if (content_type=='html') return <Codemirror_wrapper req={this.props.req}/>; return <div className="pane_preview"></div>; } } const Img_viewer = ({img})=> <div className="img_viewer"> <div className="image"> <img src={img}/> </div> </div>; export default Preview;