@luminati-io/luminati-proxy
Version:
A configurable local proxy for luminati.io
633 lines (620 loc) • 23.6 kB
JavaScript
// LICENSE_CODE ZON ISC
'use strict'; /*jslint react:true, es6:true*/
import React from 'react';
import Pure_component from '/www/util/pub/pure_component.js';
import $ from 'jquery';
import _ from 'lodash';
import classnames from 'classnames';
import etask from '../../../util/etask.js';
import ajax from '../../../util/ajax.js';
import setdb from '../../../util/setdb.js';
import {qw} from '../../../util/string.js';
import {Loader, Loader_small, Preset_description, Ext_tooltip,
Checkbox} from '../common.js';
import {Nav_tabs, Nav_tab} from '../common/nav_tabs.js';
import React_tooltip from 'react-tooltip';
import {tabs, all_fields} from './fields.js';
import presets from '../common/presets.js';
import {withRouter, Switch, Route, Redirect} from 'react-router-dom';
import Rules from './rules.js';
import Targeting from './targeting.js';
import General from './general.js';
import Rotation from './rotation.js';
import Browser from './browser.js';
import Logs from './logs.js';
import Alloc_modal from './alloc_modal.js';
import {map_rule_to_form} from './rules.js';
import Tooltip from '../common/tooltip.js';
import {Modal} from '../common/modals.js';
import {T} from '../common/i18n.js';
import {Select_zone} from '../common/controls.js';
import {report_exception, bind_all, is_local} from '../util.js';
import Warnings_modal from '../common/warnings_modal.js';
import '../css/proxy_edit.less';
const Index = withRouter(class Index extends Pure_component {
constructor(props){
super(props);
this.state = {form: {}, errors: {}, show_loader: false,
saving: false};
this.debounced_save = _.debounce(this.save, 500);
this.debounced = [];
setdb.set('head.proxy_edit.set_field', this.set_field);
setdb.set('head.proxy_edit.is_valid_field', this.is_valid_field);
setdb.set('head.proxy_edit.is_disabled_ext_proxy',
this.is_disabled_ext_proxy);
setdb.set('head.proxy_edit.goto_field', this.goto_field);
setdb.set('head.proxy_edit.get_curr_plan', this.get_curr_plan);
}
componentDidMount(){
setdb.set('head.proxies_running', null);
this.etask(function*(){
const proxies_running = yield ajax.json(
{url: '/api/proxies_running'});
setdb.set('head.proxies_running', proxies_running);
});
this.setdb_on('head.proxies_running', proxies=>{
if (!proxies||this.state.proxies)
return;
const port = this.props.match.params.port;
const proxy = proxies.filter(p=>p.port==port)[0];
if (!proxy)
return this.props.history.push('/overview');
const form = Object.assign({}, proxy);
this.apply_preset(form);
this.setState({proxies}, this.delayed_loader());
});
this.setdb_on('ws.zones', zones=>{
if (zones)
this.setState({zones}, this.delayed_loader());
});
this.setdb_on('head.defaults', defaults=>
this.setState({defaults}, this.delayed_loader()));
this.setdb_on('head.callbacks', callbacks=>this.setState({callbacks}));
this.setdb_on('head.proxy_edit.loading', loading=>
this.setState({loading}));
let state;
if ((state = this.props.location.state) && state.field)
this.goto_field(state.field);
}
willUnmount(){
setdb.set('head.proxy_edit.form', undefined);
setdb.set('head.proxy_edit', undefined);
this.debounced.forEach(d=>d.cancel());
}
update_loader = ()=>{
this.setState(state=>{
const show_loader = !state.proxies || !state.defaults ||
!state.zones;
const zone_name = !show_loader &&
(state.form.zone || state.zones.def);
setdb.set('head.proxy_edit.zone_name', zone_name);
return {show_loader};
});
};
delayed_loader = ()=>{
const fn = _.debounce(this.update_loader);
this.debounced.push(fn);
return fn;
};
goto_field = field=>{
let tab;
for (let [tab_id, tab_o] of Object.entries(tabs))
{
if (Object.keys(tab_o.fields).includes(field))
{
tab = tab_id;
break;
}
}
if (tab)
{
const port = this.props.match.params.port;
const pathname = `/proxy/${port}/${tab}`;
this.props.history.push({pathname, state: {field}});
}
};
set_field = (field_name, value, opt={})=>{
this.setState(prev_state=>{
const new_form = {...prev_state.form, [field_name]: value};
const pending_form = {...prev_state.pending_form,
[field_name]: value};
return {form: new_form, pending_form};
}, this.start_saving.bind(null, opt));
setdb.set('head.proxy_edit.form.'+field_name, value);
};
start_saving = opt=>{
if (opt.skip_save)
return;
this.setState({saving: true}, ()=>this.lock_nav(true));
this.debounced_save();
};
is_valid_field = field_name=>{
const zones = this.state.zones;
const form = this.state.form;
if (!zones)
return false;
if (form.ext_proxies && all_fields[field_name] &&
!all_fields[field_name].ext)
{
return false;
}
if (['city', 'state'].includes(field_name) &&
(!form.country||form.country=='*'))
{
return false;
}
const zone = zones.zones.find(z=>z.name==(form.zone||zones.def));
if (!zone || !zone.plan)
return false;
const permissions = zone.perm.split(' ') || [];
if (field_name=='vip')
return !!zone.plan.vip;
if (field_name=='country' && zone.plan.ip_alloc_preset=='shared_block')
return true;
if (field_name=='country' && zone.plan.type=='static')
return zone.plan.country || zone.plan.ip_alloc_preset;
if (['country', 'state', 'city', 'asn', 'ip'].includes(field_name))
return permissions.includes(field_name);
if (field_name=='country' && (zone.plan.type=='static'||
['domain', 'domain_p'].includes(zone.plan.vips_type)))
{
return false;
}
if (field_name=='carrier')
return permissions.includes('asn');
return true;
};
is_disabled_ext_proxy = field_name=>{
const form = this.state.form;
if (form.ext_proxies && all_fields[field_name] &&
!all_fields[field_name].ext)
{
return true;
}
return false;
};
apply_preset = (_form, preset)=>{
const form_upd = {};
const form = Object.assign({}, _form);
if (!preset)
preset = form.preset;
if (!presets.get(preset))
preset = presets.get_default().key;
const last_preset = form.preset ? presets.get(form.preset) : null;
if (last_preset && last_preset.key!=preset && last_preset.clean)
last_preset.clean(form_upd);
form_upd.preset = preset;
presets.get(preset).set(form_upd, form);
const disabled_fields = presets.get(preset).disabled||{};
setdb.set('head.proxy_edit.disabled_fields', disabled_fields);
this.apply_rules(form);
if (form.reverse_lookup===undefined)
{
if (form.reverse_lookup_dns)
form_upd.reverse_lookup = 'dns';
else if (form.reverse_lookup_file)
form_upd.reverse_lookup = 'file';
else if (form.reverse_lookup_values)
{
form_upd.reverse_lookup = 'values';
form_upd.reverse_lookup_values = form.reverse_lookup_values
.join('\n');
}
}
if (!form.ips)
form.ips = [];
if (!form.vips)
form.vips = [];
if (!form.users)
form.users = [];
qw`country state`.forEach(k=>{
form[k] = (form[k]||'').toLowerCase();
});
if (form.city && !form.city.includes('|') && form.state)
form.city = form.city+'|'+form.state;
Object.assign(form, form_upd);
if (!this.original_form)
this.original_form = form;
if (form.session==='true'||form.session===true)
{
delete form.session;
delete form_upd.session;
}
this.setState({form, pending_form: form_upd});
setdb.set('head.proxy_edit.form', form);
for (let i in form)
setdb.emit('head.proxy_edit.form.'+i, form[i]);
};
apply_rules = ({rules})=>{
if (!rules)
return;
const _rules = rules.map(map_rule_to_form)
.map((r, i)=>({...r, id: i}));
setdb.set('head.proxy_edit.rules', _rules);
};
set_errors = _errors=>{
const errors = _errors.reduce((acc, e)=>
Object.assign(acc, {[e.field]: e.msg}), {});
this.setState({errors, error_list: _errors});
};
update_proxies = ()=>{
return etask(function*(){
const proxies = yield ajax.json({url: '/api/proxies_running'});
setdb.set('head.proxies_running', proxies);
});
};
lock_nav = lock=>setdb.set('head.lock_navigation', lock);
save = ()=>{
if (this.saving)
{
this.resave = true;
return;
}
const data = this.prepare_to_save();
this.setState({pending_form: {}});
this.saving = true;
const _this = this;
this.etask(function*(){
this.on('uncaught', e=>_this.etask(function*(){
yield report_exception(e, 'proxy_edit/index.Index.save');
_this.setState({error_list: [{msg: 'Something went wrong'}]});
$('#save_proxy_errors').modal('show');
}));
this.on('finally', ()=>{
_this.setState({saving: false}, ()=>_this.lock_nav(false));
_this.saving = false;
});
const update_url = '/api/proxies/'+_this.props.match.params.port;
const raw_resp = yield window.fetch(update_url, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({proxy: data}),
});
const json_resp = yield raw_resp.json();
if (json_resp.errors)
{
_this.set_errors(json_resp.errors);
return $('#save_proxy_errors').modal('show');
}
if (_this.props.match.params.port!=_this.state.form.port)
{
const port = _this.state.form.port;
_this.props.history.push({pathname: `/proxy/${port}/general`});
}
if (_this.resave)
{
_this.resave = false;
_this.save();
}
_this.update_proxies();
});
};
prepare_to_save = ()=>{
const save_form = Object.assign({}, this.state.pending_form);
for (let field in save_form)
{
if (!this.is_valid_field(field)||save_form[field]===null)
save_form[field] = '';
if (field=='reverse_lookup')
{
qw`dns file values`.forEach(f=>{
if (save_form[field]!=f)
save_form[field+'_'+f] = '';
});
if (save_form[field]=='dns')
save_form.reverse_lookup_dns = true;
if (save_form[field]=='values')
{
save_form.reverse_lookup_values =
(save_form.reverse_lookup_values||'').split(' ');
}
delete save_form.reverse_lookup;
}
if (field=='reverse_lookup_values')
{
let values;
if (Array.isArray(values = save_form.reverse_lookup_values) &&
!values.length)
{
save_form.reverse_lookup = '';
delete save_form.reverse_lookup_values;
}
else if (values && !Array.isArray(values))
save_form.reverse_lookup_values = values.split(' ');
}
if (field=='smtp' && save_form[field])
{
save_form.smtp = save_form.smtp ?
save_form.smtp.filter(Boolean) : [];
}
if (field=='city' && save_form[field])
{
const [city, state] = save_form.city.split('|');
save_form.city = city;
if (state)
save_form.state = state;
}
if (field=='asn' && save_form[field])
save_form.asn = Number(save_form.asn);
if (field=='headers' && save_form[field])
{
save_form.headers = save_form.headers.filter(h=>
h.name&&h.value);
}
if (field=='session' && typeof save_form[field]=='string')
{
const {session} = save_form;
if (!session.trim())
save_form.session = true;
else
save_form.session = session.replace(/(-| )/g, '');
}
}
return save_form;
};
get_curr_plan = ()=>{
if (!this.state.zones)
return {};
const zone_name = this.state.form.zone || this.state.zones.def;
const zone = this.state.zones.zones.find(p=>p.name==zone_name) || {};
return zone.plan || {};
};
on_back_click = ()=>{
this.props.history.push({pathname: '/overview'});
};
render(){
// XXX krzysztof: cleanup type (index.js rotation.js general.js)
const curr_plan = this.get_curr_plan();
let type;
if ((curr_plan.type||'').startsWith('static'))
type = 'ips';
else if (curr_plan.vip)
type = 'vips';
const zone = this.state.form.zone ||
this.state.zones && this.state.zones.def;
return <div className="proxy_edit vbox">
<div className="cp_panel vbox">
<Loader show={this.state.show_loader||this.state.loading}/>
<div>
<Header
match={this.props.match}
internal_name={this.state.form.internal_name}
is_saving={this.state.saving}
on_back_click={this.on_back_click}
/>
<Nav
disabled={!!this.state.form.ext_proxies}
form={this.state.form}
plan={curr_plan}
on_change_preset={this.apply_preset}
/>
<Nav_tabs_wrapper/>
</div>
{this.state.zones && <Main_window/>}
<Warnings_modal id="save_proxy_errors"
warnings={this.state.error_list}/>
<Alloc_modal
type={type}
form={this.state.form}
zone={zone}
zones={this.state.zones}
plan={curr_plan}
/>
</div>
</div>;
}
});
const Nav_tabs_wrapper = withRouter(
class Nav_tabs_wrapper extends Pure_component {
tabs = ['logs', 'target', 'rotation', 'rules', 'browser', 'general'];
set_tab = id=>{
const port = this.props.match.params.port;
const pathname = `/proxy/${port}/${id}`;
this.props.history.push({pathname});
};
render(){
return <Nav_tabs set_tab={this.set_tab}>
{this.tabs.map(t=><Nav_tab key={t} id={t} title={tabs[t].label}
tooltip={tabs[t].tooltip}/>)}
</Nav_tabs>;
}
});
const Header = props=>
<T>{t=>
<div className="cp_panel_header">
<Back_btn click={props.on_back_click}/>
<Port_title port={props.match.params.port}
name={props.internal_name} t={t}/>
<Loader_small saving={props.is_saving}
std_msg={t('All changes saved in LPM')}
std_tooltip=
{t('All changes are automatically saved to LPM')}/>
</div>
}</T>;
export class Back_btn extends Pure_component {
state = {lock: false};
componentDidMount(){
this.setdb_on('head.lock_navigation', lock=>
lock!==undefined && this.setState({lock}));
}
render(){
const {lock} = this.state;
return <div className={classnames('back_wrapper', {lock})}
onClick={this.props.click}>
<div className="cp_icon back"/>
<span>Back to overview</span>
</div>;
}
}
const Port_title = ({port, name, t})=>{
if (name)
port = port+` (${name})`;
return <h2>{t('Proxy on port')} {port}</h2>;
};
class Open_browser_btn extends Pure_component {
open_browser = ()=>{
const _this = this;
this.etask(function*(){
const url = `/api/browser/${_this.props.port}`;
const res = yield window.fetch(url);
if (res.status==206)
$('#fetching_chrome_modal').modal();
});
};
render(){
return <T>{t=>
<Tooltip title={t('Open browser configured with this port')}
placement="bottom">
<button className="btn btn_lpm btn_browse"
onClick={this.open_browser}>
{t('Browse')}
<div className="icon browse_icon"></div>
</button>
</Tooltip>
}</T>;
}
}
const Main_window = withRouter(({match})=>
<div className="main_window">
<Switch>
<Route path={`${match.path}/target`} component={Targeting}/>
<Route path={`${match.path}/rules`} component={Rules}/>
<Route path={`${match.path}/rotation`} component={Rotation}/>
<Route path={`${match.path}/browser`} component={Browser}/>
<Route path={`${match.path}/general`} component={General}/>
<Route path={`${match.path}/logs`} component={Logs}/>
<Route exact path={match.path} component={({location})=>
<Redirect to={`${location.pathname}/logs`}/>}/>
</Switch>
</div>
);
class Nav extends Pure_component {
state = {};
set_field = setdb.get('head.proxy_edit.set_field');
is_valid_field = setdb.get('head.proxy_edit.is_valid_field');
componentDidMount(){
this.setdb_on('ws.zones', zones=>zones && this.setState({zones}));
}
_reset_fields = ()=>{
this.set_field('ips', []);
this.set_field('vips', []);
this.set_field('users', []);
this.set_field('multiply_ips', false);
this.set_field('multiply_vips', false);
this.set_field('multiply_users', false);
this.set_field('multiply', 0);
};
update_preset = val=>{
this.props.on_change_preset(this.props.form, val);
const disabled_fields = presets.get(val).disabled||{};
setdb.set('head.proxy_edit.disabled_fields', disabled_fields);
this._reset_fields();
};
update_zone = new_zone=>{
let new_preset;
const curr_zone = this.props.form.zone;
if (this.is_unblocker(curr_zone) && !this.is_unblocker(new_zone))
new_preset = presets.get_default().key;
else if (!this.is_unblocker(curr_zone) && this.is_unblocker(new_zone))
new_preset = 'unblocker';
if (this.props.form.ips.length || this.props.form.vips.length)
this.set_field('pool_size', 0);
if (new_preset)
this.update_preset(new_preset);
else
this._reset_fields();
setdb.set('head.proxy_edit.zone_name', new_zone);
this.set_field('zone', new_zone);
const save_form = Object.assign({}, this.props.form);
for (let field in save_form)
{
if (!this.is_valid_field(field, new_zone))
this.set_field(field, '');
}
};
is_unblocker = zone_name=>{
if (!this.state.zones)
return;
const {plan} = this.state.zones.zones.find(z=>z.name==zone_name)||{};
return (plan||{}).type=='unblocker';
};
confirm_update(cb){
let no_confirm = localStorage.getItem('no-confirm-zone-preset');
if (no_confirm && JSON.parse(no_confirm))
return cb();
this.setState({confirm_action: cb}, ()=>$('#confirm_modal').modal());
}
render(){
const opts = presets.opts(this.is_unblocker(this.props.form.zone));
const preset = this.props.form.preset;
const is_unblocker = this.props.plan.type=='unblocker';
const preset_disabled = this.props.disabled;
return <div className="nav">
<Select_zone val={this.props.form.zone} on_change_wrapper={val=>
this.confirm_update(()=>this.update_zone(val))}
disabled={this.props.disabled} preview/>
<Field i18n options={opts}
on_change={val=>this.confirm_update(()=>
this.update_preset(val))}
value={preset} disabled={preset_disabled}
ext_tooltip={!is_unblocker} id="preset"
tooltip={
<Preset_description preset={preset} rule_clicked={()=>0}/>
}/>
{is_local() &&
<Open_browser_btn port={this.props.form.port}/>
}
<Confirmation_modal on_ok={this.state.confirm_action}/>
</div>;
}
}
class Confirmation_modal extends Pure_component {
constructor(props){
super(props);
this.state = {no_confirm: false};
bind_all(this, qw`toggle_dismiss handle_ok handle_dismiss`);
}
componentDidMount(){
let no_confirm = localStorage.getItem('no-confirm-zone-preset');
this.setState({no_confirm: !!no_confirm && JSON.parse(no_confirm)});
}
toggle_dismiss(){
this.setState({no_confirm: !this.state.no_confirm});
}
handle_ok(){
localStorage.setItem('no-confirm-zone-preset', this.state.no_confirm);
this.props.on_ok();
}
handle_dismiss(){
this.setState({no_confirm: false});
}
render(){
let left_item = <Checkbox text="Don't show this message again"
value={this.state.no_confirm} checked={!!this.state.no_confirm}
on_change={this.toggle_dismiss}/>;
return <Modal title="Confirm changing preset or zone"
id="confirm_modal" click_ok={this.handle_ok} ok_btn_title="Yes"
left_footer_item={left_item} on_hidden={this.handle_dismiss}>
<h4>Changing preset or zone may reset some other options. Are you
sure you want to continue?</h4>
</Modal>;
}
}
const Field = ({id, disabled, children, i18n, ext_tooltip, ...props})=>{
const options = props.options||[];
return <T>{t=><div className="field" data-tip data-for={id+'tip'}>
<React_tooltip id={id+'tip'} type="light" effect="solid"
place="bottom" delayHide={0} delayUpdate={300}>
{disabled && ext_tooltip ? <Ext_tooltip/> : props.tooltip}
</React_tooltip>
<select value={props.value} disabled={disabled}
onChange={e=>props.on_change(e.target.value)}>
{options.map(o=>
<option key={o.key} value={o.value}>
{i18n ? t(o.key) : o.key}
</option>
)}
</select>
</div>}</T>;
};
export default Index;