@luminati-io/luminati-proxy
Version:
A configurable local proxy for brightdata.com
806 lines (791 loc) • 29.2 kB
JavaScript
// LICENSE_CODE ZON
; /*jslint react:true, es6:true*/
import React from 'react';
import {withRouter} from 'react-router-dom';
import Pure_component from '/www/util/pub/pure_component.js';
import classnames from 'classnames';
import setdb from '../../../util/setdb.js';
import conv from '../../../util/conv.js';
import {migrate_trigger, no_ssl_trigger_types, trigger_types,
action_types, default_action, WWW_API} from '../../../util/rules_util.js';
import {ms} from '../../../util/date.js';
import zutil from '../../../util/util.js';
import {Labeled_controller, with_proxy_ports, Cm_wrapper,
Warning, Faq_link, with_www_api} from '../common.js';
import Proxy_tester from '../proxy_tester.js';
import Tooltip from '../common/tooltip.js';
import {T} from '../common/i18n.js';
import Toggle_on_off from '../common/toggle_on_off.js';
import ws from '../ws.js';
import {tabs} from './fields.js';
const DEFAULT_ACTION = 'retry_same';
const rule_prepare = (rule={})=>{
let action = {};
if (!rule.action)
rule.action = DEFAULT_ACTION;
if (['retry_same', 'retry', 'refresh_ip'].includes(rule.action))
action.retry = true;
if (rule.action=='retry_same')
action.retry_same = true;
if (rule.action=='retry' && rule.retry_number)
action.retries = rule.retry_number;
else if (rule.action=='retry_port')
action.retry_port = Number(rule.retry_port);
else if (rule.action=='ban_ip')
action.ban_ip = (rule.ban_ip_duration||0)*ms.MIN;
else if (rule.action=='ban_ip_global')
action.ban_ip_global = (rule.ban_ip_duration||0)*ms.MIN;
else if (rule.action=='ban_ip_domain')
action.ban_ip_domain = (rule.ban_ip_duration||0)*ms.MIN;
else if (rule.action=='cache')
action.cache = true;
else if (rule.action=='refresh_ip')
action.refresh_ip = true;
else if (rule.action=='save_to_pool')
action.reserve_session = true;
else if (rule.action=='request_url')
{
action.request_url = {
url: /^https?:\/\//.test(rule.request_url) ?
rule.request_url : 'http://'+rule.request_url,
method: rule.request_method,
payload: rule.request_payload && JSON.parse(rule.request_payload),
};
}
else if (rule.action=='null_response')
action.null_response = true;
else if (rule.action=='bypass_proxy')
action.bypass_proxy = true;
else if (rule.action=='direct')
action.direct = true;
let result = null;
if (rule.trigger_type)
{
result = {
action,
action_type: rule.action,
trigger_type: rule.trigger_type,
url: rule.trigger_url_regex,
domain: rule.trigger_url_domain,
};
if (rule.active===false)
result.active = false;
}
if (rule.trigger_type=='status')
result.status = rule.status||'';
else if (rule.trigger_type=='body' && rule.body_regex)
result.body = rule.body_regex;
else if (rule.trigger_type=='min_conn_time' && rule.min_conn_time)
result.min_conn_time = rule.min_conn_time;
else if (rule.trigger_type=='min_req_time' && rule.min_req_time)
result.min_req_time = rule.min_req_time;
else if (rule.trigger_type=='max_req_time' && rule.max_req_time)
result.max_req_time = rule.max_req_time;
if (result && (rule.type || rule.trigger_code))
{
const {type, trigger_code} = migrate_trigger(result);
if (rule.type!=type || rule.trigger_code!=trigger_code)
{
result.type = rule.type||type;
result.trigger_code = rule.trigger_code||trigger_code;
}
}
return result;
};
export const map_rule_to_form = rule=>{
const result = {};
result.status = rule.status;
result.trigger_url_regex = rule.url;
result.trigger_url_domain = rule.domain;
result.trigger_type = rule.trigger_type;
result.body_regex = rule.body;
result.min_conn_time = rule.min_conn_time;
result.min_req_time = rule.min_req_time;
result.max_req_time = rule.max_req_time;
result.action = rule.action_type;
result.retry_port = rule.action.retry_port;
result.retry_number = rule.action.retry;
if (rule.action.request_url)
{
result.request_url = rule.action.request_url.url;
result.request_method = rule.action.request_url.method;
result.request_payload =
JSON.stringify(rule.action.request_url.payload, null, ' ');
}
if (rule.action.ban_ip)
result.ban_ip_duration = rule.action.ban_ip/ms.MIN;
if (rule.action.ban_ip_global)
result.ban_ip_duration = rule.action.ban_ip_global/ms.MIN;
if (rule.action.ban_ip_domain)
result.ban_ip_duration = rule.action.ban_ip_domain/ms.MIN;
result.trigger_code = rule.trigger_code;
result.type = rule.type;
result.active = rule.active;
return result;
};
const Rule_warning = props=>{
const {text, link_title, link_click, faq_anchor, faq_article} = props;
const show_faq = !!faq_anchor || !!faq_article;
return <Warning text={
<React.Fragment>
<span>
{text&&<T>{text}</T>}
{' '}
{link_title && link_click &&
<a className="link" onClick={link_click}>
<T>{link_title}</T>
</a>
}
{show_faq &&
<Faq_link article={faq_article} anchor={faq_anchor}/>
}
</span>
</React.Fragment>
}/>;
};
export class Rules extends Pure_component {
constructor(props){
super(props);
this.state = {
rules: [{id: 0}],
max_id: 0,
disabled_fields: {},
defaults: {},
};
this.suggestions = {
custom: 'New custom rule',
savebw: 'Save bandwidth',
retry: 'Retry failed requests',
};
this.set_field = setdb.get('head.proxy_edit.set_field');
this.goto_field = setdb.get('head.proxy_edit.goto_field');
}
componentDidMount(){
this.setdb_on('head.proxy_edit.form', form=>{
form && this.setState({form});
});
this.setdb_on('head.proxy_edit.rules', rules=>{
if (!rules||!rules.length)
return;
this.setState({rules, max_id: Math.max(...rules.map(r=>r.id))});
});
this.setdb_on('head.proxy_edit.update_rule', this.update_rule);
this.setdb_on('head.proxy_edit.disabled_fields', disabled_fields=>
disabled_fields&&this.setState({disabled_fields}));
this.setdb_on('head.defaults',
defaults=>this.setState({defaults: defaults||{}}));
}
suggestion_event(type){
ws.post_event('Rule Suggestion Click', {type});
}
update_rule = rule=>{
if (!rule)
return;
this.setState(prev=>({
rules: prev.rules.map(r=>{
if (r.id!=rule.rule_id)
return r;
return {...r, [rule.field]: rule.value};
}),
}), this.rules_update);
};
rule_del = id=>{
this.setState(prev=>{
const new_state = {rules: prev.rules.filter(r=>r.id!=id)};
if (!new_state.rules.length)
{
new_state.rules.push({id: prev.max_id+1});
new_state.max_id = prev.max_id+1;
}
return new_state;
}, this.rules_update);
};
rules_update = ()=>{
setdb.set('head.proxy_edit.rules', this.state.rules);
const rules = this.state.rules.map(rule_prepare).filter(Boolean);
this.set_field('rules', rules);
};
turn_ssl = ()=>this.set_field('ssl', true);
turn_debug = ()=>this.set_field('debug', 'full');
rule_add = (rule={})=>{
this.setState(prev=>{
rule.id = prev.max_id+1;
return {
rules: [rule, ...prev.rules],
max_id: prev.max_id+1,
};
}, this.rules_update);
};
rule_add_cb = ()=>{
this.suggestion_event(this.suggestions.custom);
this.rule_add();
};
savebw_rule_exists = ()=>{
return this.state.rules.some(r=>{
return r.action=='bypass_proxy' &&
(r.trigger_url_regex||'').includes('jpg');
});
};
savebw_rule_add = ()=>{
this.suggestion_event(this.suggestions.savebw);
this.rule_add({
action: 'bypass_proxy',
trigger_type: 'url',
trigger_url_regex: '\\.(png|jpg|jpeg|svg|gif|mp3|avi|mp4)'
+'(#.*|\\?.*)?$',
});
};
retry_rule_exists = ()=>{
return this.state.rules.some(r=>{
return r.action=='retry' && r.status=='(4|5)..';
});
};
retry_rule_add = ()=>{
this.suggestion_event(this.suggestions.retry);
this.rule_add({
action: 'retry',
trigger_type: 'status',
status: '(4|5)..',
retry_number: 1,
});
};
render(){
const {form, rules, disabled_fields, www} = this.state;
if (!form)
return null;
const {custom, savebw, retry} = this.suggestions;
let {ssl, debug: dbg} = form, def_ssl = this.state.defaults.ssl;
let def_dbg = this.state.defaults.debug;
let ssl_analyzing_enabled = ssl||ssl!==false&&def_ssl;
let req_details_enabled = dbg=='full'||dbg!='none'&&def_dbg=='full';
return <div className="rules">
{!ssl_analyzing_enabled &&
<Rule_warning link_title="SSL analyzing"
link_click={this.turn_ssl} faq_anchor="ssl_analyzing"
text="Most of the options here are available only when using"
/>
}
{!req_details_enabled &&
<Rule_warning link_title="Request details"
link_click={this.turn_debug} faq_anchor="request_details"
text="Ban IP actions are available only with enabled"
/>
}
<New_rule_btn
disabled={disabled_fields.rules}
on_click={this.rule_add_cb}>
<T>{custom}</T>
</New_rule_btn>
<New_rule_btn
disabled={disabled_fields.rules || this.savebw_rule_exists()}
on_click={this.savebw_rule_add}>
<T>{savebw}</T>
</New_rule_btn>
<New_rule_btn
disabled={disabled_fields.rules || this.retry_rule_exists()}
on_click={this.retry_rule_add}>
<T>{retry}</T>
</New_rule_btn>
{rules.map(r=>
<Rule key={r.id}
rule={r}
rule_del={this.rule_del}
www={www}
ssl={ssl_analyzing_enabled}
req_details={req_details_enabled}
disabled={disabled_fields.rules}
/>
)}
<Tester_wrapper/>
</div>;
}
}
const New_rule_btn = ({on_click, disabled, children})=>{
return <button className="btn btn_lpm btn_lpm_small rule_add_btn"
onClick={on_click} disabled={disabled}>
{children}
<i className="fa fa-plus"/>
</button>;
};
const Tester_wrapper = withRouter(class Tester_wrapper extends Pure_component {
render(){
return <div className="tester_wrapper">
<div className="nav_header" style={{marginBottom: 5}}>
<h3><T>Test rules</T></h3>
</div>
<Proxy_tester port={this.props.match.params.port} no_labels
test_event="Test Rules Click"/>
</div>;
}
});
class Rule_config extends Pure_component {
state = {disabled_fields: {}};
componentDidMount(){
this.setdb_on('head.proxy_edit.disabled_fields', disabled_fields=>
disabled_fields&&this.setState({disabled_fields}));
}
value_change = value=>{
if (this.props.on_change)
this.props.on_change(value);
setdb.emit('head.proxy_edit.update_rule', {field: this.props.id,
rule_id: this.props.rule.id, value});
};
render(){
const id = this.props.id;
const tab_id = 'rules';
const disabled = this.props.disabled||this.state.disabled_fields[id];
const cls = classnames('rule_toggle', this.props.class_name);
return <Labeled_controller
id={id}
style={this.props.style}
desc_style={this.props.desc_style}
field_row_inner_style={this.props.field_row_inner_style}
sufix={this.props.sufix}
data={this.props.data}
type={this.props.type}
range={this.props.range}
on_change_wrapper={this.value_change}
val={this.props.val||this.props.rule[id]||''}
disabled={disabled}
note={this.props.note}
placeholder={tabs[tab_id].fields[id].placeholder||''}
on_blur={this.on_blur}
label={tabs[tab_id].fields[id].label}
tooltip={tabs[tab_id].fields[id].tooltip}
class_name={cls}
/>;
}
}
const Ban_ips_note = withRouter(({match, history})=>{
const goto_banlist = ()=>{
const port = match.params.port;
history.push({pathname: `/proxy/${port}/logs/banned_ips`});
};
return <span>
<a className="link" onClick={goto_banlist}>
<T>Currently banned IPs</T>
</a>
</span>;
});
class Rule extends Pure_component {
state = {expanded: false};
componentDidMount(){
const rule = this.props.rule;
if (rule && (rule.trigger_code || rule.type))
this.setState({ui_blocked: true});
}
set_rule_field = (field, value)=>{
setdb.emit('head.proxy_edit.update_rule', {rule_id: this.props.rule.id,
field, value});
};
change_ui_block = blocked=>{
if (blocked)
this.setState({ui_blocked: true});
else
{
this.set_rule_field('trigger_code', undefined);
this.set_rule_field('type', undefined);
this.setState({ui_blocked: false});
}
};
toggle_active = e=>{
e.stopPropagation();
const active = this.props.rule.active===undefined||
this.props.rule.active;
this.set_rule_field('active', !active);
};
expand = ()=>{
this.setState({expanded: true});
};
collapse = ()=>{
this.setState({expanded: false});
};
render(){
let {rule_del, rule, ssl, disabled, req_details} = this.props;
const active = rule.active===undefined||rule.active;
const {ui_blocked} = this.state;
const trigger = trigger_types.find(t=>t.value==rule.trigger_type);
let trigger_label;
if (rule.trigger_code)
trigger_label = 'Custom code';
else if (!trigger)
trigger_label = 'Trigger not set';
else
{
const tv = trigger && ': '+(rule[trigger.value] ?
rule[trigger.value] : 'not set');
trigger_label = trigger.key+tv;
}
const action = action_types.find(a=>a.value==rule.action);
const action_label = action ? action.key : 'Action not set';
let rule_label;
if (!trigger && !action)
rule_label = 'Empty rule - click to edit';
else
rule_label = trigger_label+' -> '+action_label;
return <div>
<div className={classnames('rule_wrapper',
{collapsed: !this.state.expanded})}
onClick={this.expand}>
{this.state.expanded &&
<React.Fragment>
<Trigger rule={rule} ui_blocked={ui_blocked} ssl={ssl}
set_rule_field={this.set_rule_field} disabled={disabled}
change_ui_block={this.change_ui_block}/>
<Action rule={rule} set_rule_field={this.set_rule_field}
change_ui_block={this.change_ui_block}
req_details={req_details}/>
</React.Fragment>
}
{!this.state.expanded &&
<div className="ui">
{rule_label}
</div>
}
<Btn_rule_del on_click={()=>rule_del(rule.id)}/>
<Btn_rule_toggle expanded={this.state.expanded}
collapse={this.collapse} expand={this.expand}/>
<Toggle_on_off val={active} on_click={this.toggle_active}/>
</div>
</div>;
}
}
const Action = with_www_api(with_proxy_ports(withRouter(
class Action extends Pure_component {
state = {ports: []};
componentDidMount(){
this.setdb_on('head.proxy_edit.form', form=>{
if (form)
this.setState({form});
});
this.setdb_on('head.defaults', defaults=>{
if (defaults)
this.setState({defaults});
});
this.setdb_on('head.settings', settings=>{
if (settings)
this.setState({settings});
});
this.setdb_on('ws.zones', zones=>{
if (zones)
this.setState({zones});
});
this.setdb_on('head.proxy_edit.zone_name', curr_zone=>{
if (curr_zone)
this.setState({curr_zone});
});
}
action_changed = val=>{
const {ports_opt, match, rule, set_rule_field,
change_ui_block} = this.props;
if (val=='retry_port'||val=='switch_port')
{
const def_port = ports_opt.find(p=>p.value!=match.params.port);
set_rule_field(val, def_port && def_port.value || '');
}
if (val=='ban_ip' || val=='ban_ip_domain' || val=='ban_ip_global')
{
if (rule.trigger_type=='url' && !rule.type)
{
set_rule_field('type', 'after_hdr');
change_ui_block(true);
}
}
else
set_rule_field('ban_ip_duration', '');
if (val=='retry')
set_rule_field('retry_number', 3);
};
request_method_changed = val=>{
if (val=='GET')
delete this.props.rule.request_payload;
};
goto_tester = ()=>{
this.props.history.push({pathname: `/proxy_tester`, state: {
url: `${this.props.www_api}/lpm/templates/product`,
port: this.props.match.params.port,
}});
};
should_show_refresh = ()=>{
const {zones, curr_zone} = this.state;
const zone = (zones.zones||[]).find(z=>z.name==curr_zone);
const plan = zone && zone.plan || {};
if (['static', 'static_res'].includes(plan.type))
return plan.ips>0;
if (plan.type=='resident')
return plan.vips>0||plan.vips_type=='shared';
return false;
};
request_methods = ()=>
['GET', 'POST', 'PUT', 'DELETE'].map(m=>({key: m, value: m}));
action_types_with_updated_domain = ()=>{
const _action_types = zutil.clone_deep(action_types);
_action_types.forEach(at=>at.tooltip = (at.tooltip||'')
.replace(WWW_API, this.props.www_api));
return _action_types;
};
render(){
const {rule, match, ports_opt, req_details} = this.props;
const {defaults, settings, zones, curr_zone, form} = this.state;
if (!rule.trigger_type || !settings || !defaults || !form)
return null;
if (!zones || !curr_zone)
return null;
const zone = (zones.zones||[]).find(z=>z.name==curr_zone);
const refresh_cost = zone && zone.refresh_cost;
let _action_types = this.action_types_with_updated_domain()
.filter(at=>rule.trigger_type=='url' && at.url ||
rule.trigger_type!='url' && !at.only_url)
.filter(at=>rule.trigger_type!='min_req_time' ||
at.min_req_time);
if (this.should_show_refresh())
{
const refresh_ip_at = _action_types.find(
at=>at.value=='refresh_ip');
if (refresh_ip_at)
{
refresh_ip_at.key += refresh_cost ?
` (${conv.fmt_currency(refresh_cost)})` : '';
}
}
else
_action_types = _action_types.filter(at=>at.value!='refresh_ip');
if (form.pool_size==1 || !req_details)
{
_action_types = _action_types.filter(({value: v})=>
!v.startsWith('ban_ip'));
}
_action_types = [default_action].concat(_action_types);
const current_port = match.params.port;
const ports = ports_opt.filter(p=>p.value!=current_port);
ports.unshift({key: '--Select--', value: ''});
const ban_action = ['ban_ip', 'ban_ip_domain', 'ban_ip_global']
.includes(rule.action);
const duration_opt = [
{value: 0, label: 'Until Proxy Manager restarts'},
{value: 1, label: '1 minute'},
{value: 5, label: '5 minutes'},
{value: 10, label: '10 minutes'},
{value: 30, label: '30 minutes'},
{value: 60, label: '60 minutes'},
];
return <React.Fragment>
<div className="action ui">
{rule.trigger_type &&
<Rule_config
id="action"
type="select"
data={_action_types}
on_change={this.action_changed}
rule={rule}
/>
}
{rule.action=='retry' &&
<Rule_config
id="retry_number"
type="select_number"
rule={rule}
/>
}
{rule.action=='retry_port' &&
<Rule_config
id="retry_port"
type="select"
data={ports}
rule={rule}
/>
}
{rule.action=='switch_port' &&
<Rule_config
id="switch_port"
type="select"
data={ports}
rule={rule}
/>
}
{ban_action &&
<Rule_config
id="ban_ip_duration"
type="select_number"
data={duration_opt}
rule={rule}
note={<Ban_ips_note/>}
class_name="duration"
/>
}
{rule.action=='request_url' &&
<div>
<Rule_config id="request_url" type="url" rule={rule}/>
<Rule_config
id="request_method"
type="select"
rule={rule}
data={this.request_methods()}
on_change={this.request_method_changed}
/>
{rule.request_method && rule.request_method!='GET' &&
<Rule_config
id="request_payload"
type="json"
rule={rule}
/>
}
</div>
}
</div>
</React.Fragment>;
}
})));
class Trigger extends Pure_component {
trigger_changed = val=>{
const {rule, set_rule_field} = this.props;
if (rule.trigger_type=='url' && val!='url' ||
rule.trigger_type!='url' && val=='url' || !val)
{
set_rule_field('action', '');
}
if (val!='status')
set_rule_field('status', '');
if (val!='body')
set_rule_field('body_regex', '');
if (val!='min_conn_time')
set_rule_field('min_conn_time', '');
if (val!='min_req_time')
set_rule_field('min_req_time', '');
if (val!='max_req_time')
set_rule_field('max_req_time', '');
if (!val)
set_rule_field('trigger_url_regex', '');
};
trigger_code_changed = val=>{
this.props.change_ui_block(true);
this.props.set_rule_field('trigger_code', val);
};
render(){
const {rule, ui_blocked, change_ui_block, ssl, disabled} = this.props;
let tip = ' ';
if (ui_blocked)
{
tip = `Trigger function was modified. Click 'restore' to generate
it based on your selections.`;
}
return <React.Fragment>
<div className="trigger ui" onFocus={e=>e.stopPropagation()}>
<Tooltip title={tip}>
<div className={classnames('mask', {active: ui_blocked})}>
<button className="btn btn_lpm btn_lpm_small reset_btn"
onClick={()=>change_ui_block(false)}>
Restore
</button>
</div>
</Tooltip>
<Rule_config id="trigger_type" type="select"
data={ssl ? trigger_types : no_ssl_trigger_types}
on_change={this.trigger_changed} rule={rule}/>
{rule.trigger_type=='body' &&
<Rule_config id="body_regex" type="regex_text" rule={rule}
field_row_inner_style={{paddingBottom: '1em'}}
style={{borderRadius: '4px'}}/>
}
{rule.trigger_type=='min_conn_time' &&
<Rule_config id="min_conn_time" type="select_number"
data={[0, 2000, 3000, 5000, 10000]}
sufix="milliseconds" rule={rule}/>
}
{rule.trigger_type=='min_req_time' &&
<Rule_config id="min_req_time" type="select_number"
range="ms" sufix="milliseconds" rule={rule}/>
}
{rule.trigger_type=='max_req_time' &&
<Rule_config id="max_req_time" type="select_number"
range="ms" sufix="milliseconds" rule={rule}/>
}
{rule.trigger_type=='status' &&
<Rule_config id="status" type="select_status" rule={rule}/>
}
{rule.trigger_type &&
<Rule_config id="trigger_url_regex" type="regex"
rule={rule} style={{width: '100%'}}
field_row_inner_style={{paddingBottom: '1em'}}/>
}
{rule.trigger_type && ssl &&
<Rule_config id="trigger_url_domain" type="yes_no"
rule={rule} style={{width: '100%'}}
field_row_inner_style={{paddingBottom: '1em'}}/>
}
</div>
<Trigger_code
rule={rule}
disabled={disabled}
type_changed={()=>change_ui_block(true)}
trigger_code_changed={this.trigger_code_changed}
/>
</React.Fragment>;
}
}
class Trigger_code extends Pure_component {
type_opt = [
{key: 'Before send', value: 'before_send'},
{key: 'After headers', value: 'after_hdr'},
{key: 'After body', value: 'after_body'},
{key: 'Timeout', value: 'timeout'},
];
state = {};
static getDerivedStateFromProps(props, state){
const rule = props.rule;
let prepared = rule_prepare(rule);
if (!prepared)
return {trigger_code: null};
let {trigger_code, type} = migrate_trigger(prepared);
if (rule && rule.type)
type = rule.type;
if (rule && rule.trigger_code)
trigger_code = rule.trigger_code;
return {trigger_code, type};
}
render(){
const {rule, type_changed, trigger_code_changed,
disabled} = this.props;
if (!this.state.trigger_code)
return null;
return <div className="trigger code">
<Rule_config
id="type"
type="select"
data={this.type_opt}
rule={rule}
val={this.state.type}
desc_style={{width: 'auto', minWidth: 'initial'}}
field_row_inner_style={{paddingBottom: 6}}
on_change={type_changed}
/>
<Cm_wrapper
on_change={trigger_code_changed}
val={this.state.trigger_code}
readonly={disabled}
/>
</div>;
}
}
const Btn_rule_del = ({on_click})=>
<Tooltip title="Delete">
<button tabIndex={-1} className="btn_rule del" onClick={on_click}
onFocus={e=>e.stopPropagation()}/>
</Tooltip>;
const Btn_rule_toggle = ({expanded, expand, collapse})=>{
const on_click = e=>{
e.stopPropagation();
if (expanded)
return collapse();
expand();
};
const tip = expanded ? 'Collapse' : 'Expand';
return <Tooltip title={tip}>
<div tabIndex={-1}
className="btn_rule toggle"
onClick={on_click}
onFocus={e=>e.stopPropagation()}>
<button className={classnames({expanded, collapsed: !expanded})}/>
</div>
</Tooltip>;
};