UNPKG

@luminati-io/luminati-proxy

Version:

A configurable local proxy for luminati.io

560 lines (551 loc) 21.5 kB
// 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 etask from '../../../util/etask.js'; import ajax from '../../../util/ajax.js'; import setdb from '../../../util/setdb.js'; import {Loader, Warnings, Loader_small, Preset_description, Ext_tooltip} 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 Speed from './speed.js'; import Headers from './headers.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} from '../util.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.config); this.apply_preset(form, form.preset||'session_long'); this.setState({proxies}, this.delayed_loader()); }); this.setdb_on('head.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}; return {form: new_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 = Object.assign({}, _form); const last_preset = form.preset ? presets[form.preset] : null; if (last_preset && last_preset.key!=preset && last_preset.clean) last_preset.clean(form); form.preset = preset; presets[preset].set(form); const disabled_fields = presets[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.reverse_lookup = 'dns'; else if (form.reverse_lookup_file) form.reverse_lookup = 'file'; else if (form.reverse_lookup_values) { form.reverse_lookup = 'values'; form.reverse_lookup_values = form.reverse_lookup_values .join('\n'); } } if (!form.ips) form.ips = []; if (!form.vips) form.vips = []; if (!form.users) form.users = []; if (form.city && !Array.isArray(form.city) && form.state) { form.city = [{id: form.city, label: form.city+' ('+form.state+')'}]; } else if (!Array.isArray(form.city)) form.city = []; if (form.asn && !Array.isArray(form.asn)) form.asn = [{id: ''+form.asn, label: ''+form.asn}]; else if (!Array.isArray(form.asn)) form.asn = []; if (!this.original_form) this.original_form = form; form.country = (form.country||'').toLowerCase(); form.state = (form.state||'').toLowerCase(); if (form.session==='true'||form.session===true) delete form.session; this.setState({form}); 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.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.form); for (let field in save_form) { let before_save; if (before_save = all_fields[field] && all_fields[field].before_save) { save_form[field] = before_save(save_form[field]); } if (!this.is_valid_field(field)||save_form[field]===null) save_form[field] = ''; } save_form.zone = save_form.zone || this.state.zones.def; save_form.proxy_type = 'persist'; if (save_form.reverse_lookup=='dns') save_form.reverse_lookup_dns = true; else save_form.reverse_lookup_dns = ''; if (save_form.reverse_lookup!='file') save_form.reverse_lookup_file = ''; if (save_form.reverse_lookup=='values') { save_form.reverse_lookup_values = save_form.reverse_lookup_values.split('\n'); } else save_form.reverse_lookup_values = ''; delete save_form.reverse_lookup; // XXX krzysztof: extract the logic of mapping specific fields if (save_form.smtp) save_form.smtp = save_form.smtp.filter(Boolean); else save_form.smtp = []; if (save_form.city.length) save_form.city = save_form.city[0].id; else save_form.city = ''; if (save_form.asn.length==1) save_form.asn = Number(save_form.asn[0].id); else if (!save_form.asn.length) save_form.asn = ''; if (!save_form.max_requests) save_form.max_requests = 0; if (save_form.headers) save_form.headers = save_form.headers.filter(h=>h.name&&h.value); if (save_form.session && save_form.session.replace) { save_form.session = save_form.session.replace(/-/g, '') .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 || {}; }; 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 <T>{t=><div className="proxy_edit"> <Loader show={this.state.show_loader||this.state.loading}/> <div className="nav_wrapper"> <div className="nav_header"> <Port_title port={this.props.match.params.port} name={this.state.form.internal_name} t={t}/> <Loader_small saving={this.state.saving} std_msg={t('All changes saved in LPM')} std_tooltip= {t('All changes are automatically saved to LPM')}/> </div> <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/>} <Modal className="warnings_modal" id="save_proxy_errors" title={t('Error')} no_cancel_btn> <Warnings warnings={this.state.error_list}/> </Modal> <Alloc_modal type={type} form={this.state.form} zone={zone} plan={curr_plan}/> </div>}</T>; } }); const Nav_tabs_wrapper = withRouter( class Nav_tabs_wrapper extends Pure_component { tabs = ['logs', 'target', 'rotation', 'speed', 'rules', 'headers', '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 Port_title = ({port, name, t})=>{ if (name) port = port+` (${name})`; return <h3>{t('Proxy on port')} {port}</h3>; }; 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}/speed`} component={Speed}/> <Route path={`${match.path}/rules`} component={Rules}/> <Route path={`${match.path}/rotation`} component={Rotation}/> <Route path={`${match.path}/headers`} component={Headers}/> <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('head.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[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 = Object.keys(presets).find(p=>presets[p].default); 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)) { let v = ''; if (field=='city'||field=='asn') v = []; this.set_field(field, v); } } }; is_unblocker(zone_name){ if (!this.state.zones) return false; const zone = this.state.zones.zones.find(z=>z.name==zone_name)||{}; return zone.plan && zone.plan.type=='unblocker'; } render(){ let presets_opt; // XXX krzysztof: hide this logic to presets module: filtering, // preparing options if (this.is_unblocker(this.props.form.zone)) presets_opt = [{key: presets.unblocker.title, value: 'unblocker'}]; else { presets_opt = Object.keys(presets).filter(p=>!presets[p].hidden) .map(p=>{ let key = presets[p].title; if (presets[p].default) key = `Default (${key})`; return {key, value: p}; }); } const preset = this.props.form.preset; const is_unblocker = this.props.plan.type=='unblocker'; const preset_disabled = this.props.disabled || is_unblocker; const href = window.location.href; const is_local = href.includes('localhost')|| href.includes('127.0.0.1'); return <div className="nav"> <Select_zone val={this.props.form.zone} on_change_wrapper={this.update_zone} disabled={this.props.disabled} preview/> <Field i18n on_change={this.update_preset} options={presets_opt} 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}/> } </div>; } } 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;