UNPKG

luminati-proxy-stripped

Version:

A configurable local proxy for luminati.io

1,299 lines (1,281 loc) 63.3 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 classnames from 'classnames'; 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 zurl from '../../util/url.js'; import {Modal, Loader, Warnings, Link_icon, Checkbox, Tooltip, Pagination_panel, Loader_small, Note, Input, Labeled_controller, Remove_icon, Add_icon} from './common.js'; import Har_viewer from './har_viewer.js'; import * as util from './util.js'; import {Netmask} from 'netmask'; import {getContext, withContext} from 'recompose'; import PropTypes from 'prop-types'; import {withRouter} from 'react-router-dom'; import {tabs, all_fields} from './proxy_fields.js'; import filesaver from 'file-saver'; const presets = util.presets; const provider = provide=>withContext({provide: PropTypes.object}, ()=>({provide})); const event_tracker = {}; const ga_event = (action, label, opt={})=>{ const id = action+label; if (!event_tracker[id] || !opt.single) { event_tracker[id] = true; util.ga_event('proxy_edit', action, label); } }; const validators = { number: (min, max, req=false)=>val=>{ val = Number(val); if (isNaN(val)) { if (req) return min; return undefined; } else if (val < min) return min; else if (val > max) return max; return val; }, ips_list: val=>{ val = val.replace(/\s/g, ''); const ips = val.split(','); const res = []; ips.forEach(ip=>{ try { res.push(new Netmask(ip).base); } catch(e){ console.log('incorrect ip format'); } }); return res.join(','); }, }; const Index = withRouter(class Index extends Pure_component { constructor(props){ super(props); this.state = {form: {zones: {}}, warnings: [], errors: {}, show_loader: false, saving: false}; this.debounced_save = _.debounce(this.save, 500); 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.goto_field', this.goto_field); } 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; this.port = this.props.match.params.port; const proxy = proxies.filter(p=>p.port==this.port)[0].config; const form = Object.assign({}, proxy); const preset = this.guess_preset(form); this.apply_preset(form, preset); this.setState({proxies}, this.delayed_loader()); }); this.setdb_on('head.consts', consts=> this.setState({consts}, 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})); this.setdb_on('head.proxy_edit.tab', (tab='logs')=> this.setState({tab})); 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); } delayed_loader(){ return _.debounce(this.update_loader.bind(this)); } update_loader(){ this.setState(state=>{ const show_loader = !state.consts || !state.proxies || !state.defaults; const zone_name = !show_loader && (state.form.zone||state.consts.proxy.zone.def); setdb.set('head.proxy_edit.zone_name', zone_name); return {show_loader}; }); } 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) setdb.set('head.proxy_edit.tab', tab); }; guess_preset(form){ let res; for (let p in presets) { const preset = presets[p]; if (preset.check(form)) { res = p; break; } } if (form.last_preset_applied && presets[form.last_preset_applied]) res = form.last_preset_applied; return res; } set_field = (field_name, value)=>{ this.setState(prev_state=>{ const new_form = {...prev_state.form, [field_name]: value}; return {form: new_form}; }, this.debounced_save); setdb.set('head.proxy_edit.form.'+field_name, value); this.send_ga(field_name, value); }; send_ga(id, value){ if (id=='zone') { ga_event('edit zone', value); return; } ga_event('edit '+id, value, {single: true}); } is_valid_field = field_name=>{ const proxy = this.state.consts.proxy; const form = this.state.form; if (!proxy) return false; if (form.ext_proxies && all_fields[field_name] && !all_fields[field_name].ext) { return false; } const zone = form.zone||proxy.zone.def; if (['city', 'state'].includes(field_name) && (!form.country||form.country=='*')) { return false; } const details = proxy.zone.values.filter(z=>z.value==zone)[0]; const permissions = details&&details.perm.split(' ')||[]; const plan = details&&details.plans[details.plans.length-1]||{}; if (field_name=='vip') return !!plan.vip; if (field_name=='country'&&plan.type=='static') return false; if (field_name=='country'&&plan.ip_alloc_preset=='shared_block') return true; if (['country', 'state', 'city', 'asn', 'ip'].includes(field_name)) return permissions.includes(field_name); if (field_name=='country'&&(plan.type=='static'|| ['domain', 'domain_p'].includes(plan.vips_type))) { return false; } if (field_name=='carrier') return permissions.includes('asn'); return true; }; apply_preset(_form, preset){ const form = Object.assign({}, _form); const last_preset = form.last_preset_applied ? presets[form.last_preset_applied] : null; if (last_preset&&last_preset.key!=preset&&last_preset.clean) last_preset.clean(form); if (form.ext_proxies) { form.preset = ''; form.zone = ''; form.password = ''; } else { form.preset = preset; form.last_preset_applied = 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.session===true) { form.session_random = true; form.session = ''; } else form.session_random = false; 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 (Array.isArray(form.whitelist_ips)) form.whitelist_ips = form.whitelist_ips.join(','); 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(); this.setState({form}); setdb.set('head.proxy_edit.form', form); for (let i in form) setdb.emit('head.proxy_edit.form.'+i, form[i]); } rule_map_to_form = (rule, id)=>{ const result = {id}; const res = rule.res[0]; if (res.status) { if (!res.status_custom) result.status_code = res.status.arg; else { result.status_code = 'Custom'; result.status_custom = res.status.arg; } } result.trigger_url_regex = rule.url; result.trigger_type = res.trigger_type; result.body_regex = res.body&&res.body.arg; if (res.min_req_time) { const min_req_time = res.min_req_time.match(/\d+/); result.min_req_time = Number(min_req_time&&min_req_time[0]); } if (res.max_req_time) { const max_req_time = res.max_req_time.match(/\d+/); result.max_req_time = Number(max_req_time&&max_req_time[0]); } result.action = res.action_type; result.retry_port = res.action.retry_port; result.retry_number = res.action.retry; if (res.action.ban_ip) { result.ban_ip_duration = 'custom'; const minutes = res.action.ban_ip.match(/\d+/); result.ban_ip_custom = Number(minutes&&minutes[0]); } return result; }; apply_rules = form=>{ if (form.rules&&form.rules.post) { const rules = form.rules.post.map(this.rule_map_to_form); setdb.set('head.proxy_edit.rules', rules); } }; default_opt = option=>{ const default_label = this.state.defaults[option] ? 'Yes' : 'No'; return [ {key: 'No', value: false}, {key: 'Default ('+default_label+')', value: ''}, {key: 'Yes', value: true}, ]; }; 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(); const check_url = '/api/proxy_check/'+this.port; this.saving = true; this.setState({saving: true}, ()=>this.lock_nav(true)); const _this = this; this.etask(function*(){ this.on('uncaught', e=>{ console.log(e); ga_event('save failed', e.message); _this.setState({error_list: [{msg: 'Something went wrong'}], saving: false}, ()=>_this.lock_nav(false)); _this.saving = false; $('#save_proxy_errors').modal('show'); }); // XXX krzysztof: switch fetch->ajax const raw_check = yield window.fetch(check_url, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data), }); const json_check = yield raw_check.json(); const errors = json_check.filter(e=>e.lvl=='err'); _this.set_errors(errors); if (errors.length) { ga_event('save failed', JSON.stringify(errors)); $('#save_proxy_errors').modal('show'); _this.setState({saving: false}, ()=>_this.lock_nav(false)); _this.saving = false; return; } const warnings = json_check.filter(w=>w.lvl=='warn'); if (warnings.length) _this.setState({warnings}); const update_url = '/api/proxies/'+_this.port; // XXX krzysztof: switch fetch->ajax yield window.fetch(update_url, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({proxy: data}), }); _this.setState({saving: false}, ()=>_this.lock_nav(false)); _this.saving = false; if (_this.resave) { _this.resave = false; _this.save(); } _this.update_proxies(); }); }; prepare_to_save = ()=>{ const save_form = Object.assign({}, this.state.form); delete save_form.zones; 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] = ''; } const effective = attr=>{ return save_form[attr]===undefined ? this.state.defaults[attr] : save_form[attr]; }; save_form.zone = save_form.zone||this.state.consts.proxy.zone.def; save_form.ssl = effective('ssl'); save_form.max_requests = effective('max_requests'); save_form.session_duration = effective('session_duration'); save_form.keep_alive = effective('keep_alive'); save_form.pool_size = effective('pool_size'); 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; if (save_form.whitelist_ips) { save_form.whitelist_ips = save_form.whitelist_ips.split(',') .filter(Boolean); } if (save_form.city.length) save_form.city = save_form.city[0].id; else save_form.city = ''; if (save_form.asn.length) save_form.asn = Number(save_form.asn[0].id); else save_form.asn = ''; if (!save_form.max_requests) save_form.max_requests = 0; delete save_form.preset; if (!save_form.session) save_form.session = false; if (save_form.session_random) save_form.session = true; delete save_form.session_random; if (save_form.headers) save_form.headers = save_form.headers.filter(h=>h.name&&h.value); return save_form; }; get_curr_plan = ()=>{ const zone_name = this.state.form.zone|| this.state.consts.proxy.zone.def; const zones = this.state.consts.proxy.zone.values; const curr_zone = zones.filter(p=>p.key==zone_name); let curr_plan; if (curr_zone.length) curr_plan = curr_zone[0].plans.slice(-1)[0]; return curr_plan; }; render(){ const tab = this.state.tab; // XXX krzysztof: transform support into disabled_fields const support = presets && this.state.form.preset && presets[this.state.form.preset].support||{}; let zones = this.state.consts&& this.state.consts.proxy.zone.values||[]; zones = zones.filter(z=>{ const plan = z.plans && z.plans.slice(-1)[0] || {}; return !plan.archive && !plan.disable; }); let sett = setdb.get('head.settings')||{}, def; if (zones[0] && !zones[0].value && (def = sett.zone||zones[0].key)) zones[0] = {key: `Default (${def})`, value: ''}; const default_zone=this.state.consts&& this.state.consts.proxy.zone.def; const curr_plan = this.state.consts&&this.get_curr_plan(); let type; if (curr_plan&&curr_plan.type=='static') type = 'ips'; else if (curr_plan&&!!curr_plan.vip) type = 'vips'; const port = this.props.match.params.port; const show_main_window = this.state.consts&&this.state.defaults&& this.state.proxies; return <div className="proxy_edit"> <Loader show={this.state.show_loader||this.state.loading}/> <div className="nav_wrapper"> <div className="nav_header"> <h3>Proxy on port {this.port}</h3> <Loader_small saving={this.state.saving} std_msg="All changes saved in LPM" std_tooltip="All changes are automatically saved to LPM"/> </div> <Nav zones={zones} default_zone={default_zone} disabled={!!this.state.form.ext_proxies} form={this.state.form} on_change_preset={this.apply_preset.bind(this)}/> <Nav_tabs form={this.state.form} errors={this.state.errors}/> </div> <div className={classnames('main_window', {[tab]: true})}> <Main_window show={show_main_window} tab={tab} port={port} proxy={this.state.consts&&this.state.consts.proxy} defaults={this.state.defaults} form={this.state.form} support={support} default_opt={this.default_opt} get_curr_plan={this.get_curr_plan}/> </div> <Modal className="warnings_modal" id="save_proxy_errors" title="Errors:" no_cancel_btn> <Warnings warnings={this.state.error_list}/> </Modal> <Alloc_modal type={type} form={this.state.form} support={support} zone={this.state.form.zone||default_zone}/> </div>; } }); const Main_window = ({show, tab, ...props})=>{ if (!show) return null; let Comp; switch (tab) { case 'target': Comp = Targeting; break; case 'speed': Comp = Speed; break; case 'rules': Comp = Rules; break; case 'rotation': Comp = Rotation; break; case 'debug': Comp = Debug; break; case 'headers': Comp = Headers; break; case 'general': Comp = General; break; case 'logs': default: Comp = Har_viewer; } return <Comp {...props}/>; }; class Nav extends Pure_component { set_field = setdb.get('head.proxy_edit.set_field'); _reset_fields = ()=>{ this.set_field('ips', []); this.set_field('vips', []); this.set_field('multiply_ips', false); this.set_field('multiply_vips', false); this.set_field('multiply', 1); }; 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(); ga_event('edit preset', val); }; update_zone = val=>{ const zone_name = val||this.props.default_zone; setdb.set('head.proxy_edit.zone_name', zone_name); const zone = this.props.zones.filter(z=>z.key==zone_name)[0]||{}; this.set_field('zone', val); this.set_field('password', zone.password); if (this.props.form.ips.length || this.props.form.vips.length) this.set_field('pool_size', 0); this._reset_fields(); }; render(){ const presets_opt = Object.keys(presets).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 preset_tooltip = preset&&presets[preset].subtitle +(presets[preset].rules&& '<ul>'+presets[preset].rules.map(r=>`<li>${r.label}</li>`).join('') +'</ul>'); return <div className="nav"> <Field on_change={this.update_zone} options={this.props.zones} tooltip="Zone" value={this.props.form.zone} disabled={this.props.disabled}/> <Field on_change={this.update_preset} tooltip={preset_tooltip} options={presets_opt} value={preset} disabled={this.props.disabled}/> </div>; } } const Field = ({disabled, tooltip, ...props})=>{ const options = props.options||[]; return <Tooltip title={tooltip} placement="bottom"> <div className="field"> <select value={props.value} disabled={disabled} onChange={e=>props.on_change(e.target.value)}> {options.map(o=> <option key={o.key} value={o.value}>{o.key}</option> )} </select> </div> </Tooltip>; }; class Nav_tabs extends Pure_component { state = {}; componentDidMount(){ this.setdb_on('head.proxy_edit.tab', (tab='logs')=> this.setState({tab})); } render(){ // XXX krzysztof: remove ...props return <div className="nav_tabs"> <Tab_btn {...this.props} curr_tab={this.state.tab} id="logs"/> <Tab_btn {...this.props} curr_tab={this.state.tab} id="target"/> <Tab_btn {...this.props} curr_tab={this.state.tab} id="speed"/> <Tab_btn {...this.props} curr_tab={this.state.tab} id="rules"/> <Tab_btn {...this.props} curr_tab={this.state.tab} id="rotation"/> <Tab_btn {...this.props} curr_tab={this.state.tab} id="debug"/> <Tab_btn {...this.props} curr_tab={this.state.tab} id="headers"/> <Tab_btn {...this.props} curr_tab={this.state.tab} id="general"/> </div>; } } const Tab_btn = props=>{ const btn_class = classnames('btn_tab', {active: props.curr_tab==props.id}); const tab_fields = Object.keys(tabs[props.id].fields||{}); let changes; if (props.id=='rules') changes = _.get(props, 'form.rules.post.length'); else { changes = Object.keys(props.form).filter(f=>{ const val = props.form[f]; const is_empty_arr = Array.isArray(val) && !val[0]; return tab_fields.includes(f) && val && !is_empty_arr; }).length; } if (props.id=='headers') changes = changes+(props.form.headers||[]).length; const errors = Object.keys(props.errors).filter(f=>tab_fields.includes(f)); return <Tooltip title={tabs[props.id].tooltip}> <div onClick={()=>setdb.set('head.proxy_edit.tab', props.id)} className={btn_class}> <Tab_icon id={props.id} changes={changes} error={errors.length}/> <div className="title">{tabs[props.id].label}</div> <div className="arrow"/> </div> </Tooltip>; }; const Tab_icon = props=>{ const circle_class = classnames('circle_wrapper', { active: props.error||props.changes, error: props.error}); const content = props.error ? '!' : props.changes; return <div className={classnames('icon', props.id)}> <div className={circle_class}> <div className="circle">{content}</div> </div> </div>; }; const Config = getContext({provide: PropTypes.object})( class Config extends Pure_component { state = {disabled_fields: {}}; set_field = setdb.get('head.proxy_edit.set_field'); is_valid_field = setdb.get('head.proxy_edit.is_valid_field'); on_blur = ({target: {value}})=>{ if (this.props.validator) this.set_field(this.props.id, this.props.validator(value)); }; on_input_change = val=>{ if (this.props.update_on_input) this.set_field(this.props.id, [{id: val, label: val}]); }; on_change_wrapper = (value, _id)=>{ const curr_id = _id||this.props.id; if (this.props.on_change) this.props.on_change(value); this.set_field(curr_id, value); }; componentDidMount(){ const val_id = this.props.val_id ? this.props.val_id : this.props.id; this.setdb_on('head.proxy_edit.form.'+val_id, val=> this.setState({val, show: true})); this.setdb_on('head.proxy_edit.disabled_fields', disabled_fields=> disabled_fields&&this.setState({disabled_fields})); } render(){ if (!this.state.show) return null; const id = this.props.id; const tab_id = this.props.provide.tab_id; const disabled = this.props.disabled||!this.is_valid_field(id)|| this.state.disabled_fields[id]; return <Labeled_controller id={id} sufix={this.props.sufix} data={this.props.data} type={this.props.type} on_input_change={this.on_input_change} on_change_wrapper={this.on_change_wrapper} val={this.state.val===undefined ? '' : this.state.val} disabled={disabled} min={this.props.min} max={this.props.max} 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}/>; } }); const Rule_config = getContext({provide: PropTypes.object})( class Rule_config extends Pure_component { 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 = this.props.provide.tab_id; return <Labeled_controller id={id} sufix={this.props.sufix} data={this.props.data} type={this.props.type} on_change_wrapper={this.value_change} val={this.props.rule[id]||''} disabled={this.props.disabled} min={this.props.min} max={this.props.max} 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}/>; } }); const Targeting = provider({tab_id: 'target'})( class Targeting extends Pure_component { constructor(props){ super(props); this.state = {}; this.def_value = {key: 'Any (default)', value: ''}; this.init_carriers(); this.set_field = setdb.get('head.proxy_edit.set_field'); } componentDidMount(){ this.setdb_on('head.locations', locations=>{ if (!locations) return; const asns = Object.keys(locations.asns) .map(a=>({id: a, label: a})); this.setState({locations, asns}); }); } init_carriers(){ const subject = 'Add new carrier option'; const n = '%0D%0A'; const body = `Hi,${n}${n}Didn't find the carrier you're looking for?` +`${n}${n}Write here the carrier's name: __________${n}${n}We will add` +` it in less than 2 business days!`; const mail = 'lumext@luminati.io'; const mailto = `mailto:${mail}?subject=${subject}&body=${body}`; this.carriers_note = <a className="link" href={mailto}> More carriers</a>; this.carriers = [ {value: '', key: 'None'}, {value: 'a1', key: 'A1 Austria'}, {value: 'aircel', key: 'Aircel'}, {value: 'airtel', key: 'Airtel'}, {value: 'att', key: 'AT&T'}, {value: 'vimpelcom', key: 'Beeline Russia'}, {value: 'celcom', key: 'Celcom'}, {value: 'chinamobile', key: 'China Mobile'}, {value: 'claro', key: 'Claro'}, {value: 'comcast', key: 'Comcast'}, {value: 'cox', key: 'Cox'}, {value: 'dt', key: 'Deutsche Telekom'}, {value: 'digi', key: 'Digi Malaysia'}, {value: 'docomo', key: 'Docomo'}, {value: 'dtac', key: 'DTAC Trinet'}, {value: 'etisalat', key: 'Etisalat'}, {value: 'idea', key: 'Idea India'}, {value: 'kyivstar', key: 'Kyivstar'}, {value: 'meo', key: 'MEO Portugal'}, {value: 'megafont', key: 'Megafon Russia'}, {value: 'mtn', key: 'MTN - Mahanager Telephone'}, {value: 'mtnza', key: 'MTN South Africa'}, {value: 'mts', key: 'MTS Russia'}, {value: 'optus', key: 'Optus'}, {value: 'orange', key: 'Orange'}, {value: 'qwest', key: 'Qwest'}, {value: 'reliance_jio', key: 'Reliance Jio'}, {value: 'robi', key: 'Robi'}, {value: 'sprint', key: 'Sprint'}, {value: 'telefonica', key: 'Telefonica'}, {value: 'telstra', key: 'Telstra'}, {value: 'tmobile', key: 'T-Mobile'}, {value: 'tigo', key: 'Tigo'}, {value: 'tim', key: 'TIM (Telecom Italia)'}, {value: 'vodacomza', key: 'Vodacom South Africa'}, {value: 'vodafone', key: 'Vodafone'}, {value: 'verizon', key: 'Verizon'}, {value: 'vivo', key: 'Vivo'}, {value: 'zain', key: 'Zain'} ]; } allowed_countries = ()=>{ let res = this.state.locations.countries.map(c=>({ key: c.country_name, value: c.country_id, mob: c.mob})); const curr_plan = this.props.get_curr_plan(); if (curr_plan&&curr_plan.ip_alloc_preset=='shared_block') { res = res.filter(r=> this.state.locations.shared_countries.includes(r.value)); } if (curr_plan&&curr_plan.mobile) res = res.filter(r=>r.mob); return [this.def_value, ...res]; }; country_changed = ()=>{ this.set_field('city', []); this.set_field('state', ''); }; states = ()=>{ const country = this.props.form.country; if (!country||country=='*') return []; const curr_plan = this.props.get_curr_plan(); const res = (this.state.locations.regions[country]||[]) .filter(r=>!curr_plan||!curr_plan.mobile||r.mob) .map(r=>({key: r.region_name, value: r.region_id})); return [this.def_value, ...res]; }; state_changed = ()=>this.set_field('city', []); cities = ()=>{ const {country, state} = this.props.form; let res; if (!country) return []; const curr_plan = this.props.get_curr_plan(); res = this.state.locations.cities .filter(c=>c.country_id==country) .filter(c=>!curr_plan||!curr_plan.mobile||c.mob); if (state) res = res.filter(c=>c.region_id==state); const regions = this.states(); res = res.map(c=>{ const region = regions.filter(r=>r.value==c.region_id)[0]; return {label: c.city_name+' ('+region.value+')', id: c.city_name, region: region.value}; }); return res; }; city_changed = e=>{ if (e&&e.length) this.set_field('state', e[0].region); }; render(){ if (!this.state.locations) return null; const curr_plan = this.props.get_curr_plan(); const show_dc_note = curr_plan&&curr_plan.type=='static'; const show_vips_note = curr_plan&& (curr_plan.vips_type=='domain'||curr_plan.vips_type=='domain_p'); return <div> {(show_dc_note || show_vips_note) && <Note> {show_dc_note && <span>To change Data Center country visit your </span> } {show_vips_note && <span>To change Exclusive gIP country visit your </span> } <a className="link" target="_blank" rel="noopener noreferrer" href="https://luminati.io/cp/zones">zone page</a> <span> and change your zone plan.</span> </Note> } <Config type="select" id="country" data={this.allowed_countries()} on_change={this.country_changed}/> <Config type="select" id="state" data={this.states()} on_change={this.state_changed}/> <Config type="typeahead" id="city" data={this.cities()} on_change={this.city_changed}/> <Config type="typeahead" id="asn" data={this.state.asns} disabled={this.props.form.carrier} update_on_input/> <Config type="select" id="carrier" data={this.carriers} note={this.carriers_note} disabled={this.props.form.asn&&this.props.form.asn.length}/> </div>; } }); const Headers = provider({tab_id: 'headers'})( class Headers extends Pure_component { first_header = {name: '', value: ''}; state = {headers: [this.first_header]}; boolean_opt = [{key: 'No (Default)', value: ''}, {key: 'Yes', value: 'true'}]; set_field = setdb.get('head.proxy_edit.set_field'); componentDidMount(){ this.setdb_on('head.proxy_edit.form.headers', headers=>{ if (headers&&headers.length) this.setState({headers}); else this.setState({headers: [this.first_header]}); }); } add = ()=>this.set_field('headers', [ ...this.state.headers, {name: '', value: ''}]); remove = idx=>{ let new_headers = [ ...this.state.headers.slice(0, idx), ...this.state.headers.slice(idx+1), ]; if (!new_headers.length) new_headers = [this.first_header]; this.set_field('headers', new_headers); }; update = idx=>name=>value=>this.set_field('headers', this.state.headers.map((h, i)=>{ if (i!=idx) return h; return {...h, [name]: value}; })); random_user_agent_changed = val=>{ if (val) this.set_field('user_agent', ''); }; render(){ return <div> <Config type="select" id="user_agent" data={util.user_agents} disabled={this.props.form.random_user_agent}/> <Config type="select" id="random_user_agent" on_change={this.random_user_agent_changed} data={this.boolean_opt}/> <Config type="select" id="override_headers" data={this.boolean_opt}/> <div className="field_row headers"> <div className="desc"> <Tooltip title="Custom headers"> <span>Headers</span> </Tooltip> </div> <div className="list"> {this.state.headers.map((h, i)=> <Header last={i+1==this.state.headers.length} key={i} name={h.name} value={h.value} update={this.update(i)} remove_clicked={this.remove} add_clicked={this.add} idx={i}/> )} </div> </div> </div>; } }); const Header = ({name, value, idx, add_clicked, remove_clicked, last, update})=> <div className="single_header"> <div className="desc">Name</div> <Input type="text" val={name} on_change_wrapper={update('name')}/> <div className="desc">Value</div> <Input type="text" val={value} on_change_wrapper={update('value')}/> <div className="action_icons"> <Remove_icon tooltip="Remove header" click={()=>remove_clicked(idx)}/> {last && <Add_icon tooltip="Add header" click={add_clicked}/>} </div> </div>; const Speed = provider({tab_id: 'speed'})( class Speed extends Pure_component { constructor(props){ super(props); this.dns_options = [ {key: 'Local (default) - resolved by the super proxy', value: 'local'}, {key: 'Remote - resolved by peer', value: 'remote'}, ]; this.reverse_lookup_options = [{key: 'No', value: ''}, {key: 'DNS', value: 'dns'}, {key: 'File', value: 'file'}, {key: 'Values', value: 'values'}]; } open_modal(){ $('#allocated_ips').modal('show'); } get_type(){ const curr_plan = this.props.get_curr_plan(); let type; if (curr_plan&&curr_plan.type=='static') type = 'ips'; else if (curr_plan&&!!curr_plan.vip) type = 'vips'; return type; } render(){ const {form, support} = this.props; const pool_size_disabled = !support.pool_size || form.ips.length || form.vips.length; const type = this.get_type(); const render_modal = ['ips', 'vips'].includes(type); let pool_size_note; if (this.props.support.pool_size&&render_modal) { pool_size_note = <a className="link" onClick={()=>this.open_modal()}> {'set from allocated '+(type=='ips' ? 'IPs' : 'vIPs')} </a>; } return <div> <Config type="select" id="dns" data={this.dns_options}/> <Config type="number" id="pool_size" min="0" note={pool_size_note} disabled={pool_size_disabled}/> <Config type="number" id="request_timeout" sufix="seconds" min="0"/> <Config type="number" id="race_reqs" min="1" max="3"/> <Config type="number" id="proxy_count" min="1"/> <Config type="number" id="proxy_switch" min="0"/> <Config type="number" id="throttle" min="0"/> <Config type="select" id="reverse_lookup" data={this.reverse_lookup_options}/> {this.props.form.reverse_lookup=='file' && <Config type="text" id="reverse_lookup_file"/> } {this.props.form.reverse_lookup=='values' && <Config type="textarea" id="reverse_lookup_values"/> } </div>; } }); const Rules = provider({tab_id: 'rules'})( class Rules extends Pure_component { set_field = setdb.get('head.proxy_edit.set_field'); goto_field = setdb.get('head.proxy_edit.goto_field'); state = {rules: [{id: 0}], max_id: 0}; componentDidMount(){ this.setdb_on('head.proxy_edit.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); } 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_add = ()=>{ this.setState(prev=>({ rules: [...prev.rules, {id: prev.max_id+1}], max_id: prev.max_id+1, })); }; rule_del = id=>{ if (this.state.rules.length==1) this.setState({rules: [{id: 0}], max_id: 0}, this.rules_update); else { this.setState(prev=>({rules: prev.rules.filter(r=>r.id!=id)}), this.rules_update); } }; rule_prepare = rule=>{ const action_raw = {}; if (['retry', 'retry_port', 'ban_ip', 'refresh_ip'].includes( rule.action)) { action_raw.retry = true; } if (rule.action=='retry' && rule.retry_number) action_raw.retry = rule.retry_number; else if (rule.action=='retry_port') action_raw.retry_port = rule.retry_port; else if (rule.action=='ban_ip') { if (rule.ban_ip_duration!='custom') action_raw.ban_ip = rule.ban_ip_duration||'10min'; else action_raw.ban_ip = rule.ban_ip_custom+'min'; } else if (rule.action=='refresh_ip') action_raw.refresh_ip = true; else if (rule.action=='save_to_pool') action_raw.reserve_session = true; let result = null; if (rule.trigger_type) { result = { res: [{ head: true, action: action_raw, action_type: rule.action, trigger_type: rule.trigger_type, }], url: rule.trigger_url_regex||'**', }; } if (rule.trigger_type=='status') { let rule_status = rule.status_code=='Custom' ? rule.status_custom : rule.status_code; rule_status = rule_status||''; result.res[0].status = {type: 'in', arg: rule_status}; result.res[0].status_custom = rule.status_code=='Custom'; } else if (rule.trigger_type=='body'&&rule.body_regex) result.res[0].body = {type: '=~', arg: rule.body_regex}; else if (rule.trigger_type=='min_req_time'&&rule.min_req_time) result.res[0].min_req_time = rule.min_req_time+'ms'; else if (rule.trigger_type=='max_req_time'&&rule.max_req_time) result.res[0].max_req_time = rule.max_req_time+'ms'; return result; }; rules_update = ()=>{ setdb.set('head.proxy_edit.rules', this.state.rules); const post = this.state.rules.map(this.rule_prepare).filter(Boolean); let rules = this.state.form.rules||{}; if (post.length) rules.post = post; else delete rules.post; if (!rules.post&&(!rules.pre||!rules.pre.length)) rules = null; this.set_field('rules', rules); }; goto_ssl = ()=>this.goto_field('ssl'); render(){ return <div> {!this.props.form.ssl && <Note> <span><strong>Warning: </strong></span> <span>we can't apply rules to HTTPS requests unless </span> <a onClick={this.goto_ssl} className="link">SSL proxy</a> <span> is turned on</span> </Note> } {this.state.rules.map(r=> <Rule key={r.id} rule={r} rule_del={this.rule_del}/> )} <button className="btn btn_lpm btn_lpm_small rule_add_btn" onClick={this.rule_add}> New rule <i className="glyphicon glyphicon-plus"/> </button> <Ips_lists/> </div>; } }); class Ips_lists extends Pure_component { state = {}; componentDidMount(){ this.setdb_on('head.proxy_edit.form.port', port=> port&&this.setState({port})); } banned_ips = ()=>this.download_data('banlist'); reserved_ips = ()=>this.download_data('reserved'); download_data = type=>{ const _this = this; this.etask(function*(){ const data = yield ajax.json({ url: `/api/${type}/${_this.state.port}`}); const blob = new Blob([data.ips], {type: 'text/plain;charset=utf-8'}); filesaver.saveAs(blob, `${type}_${_this.state.port}.json`); }); }; render(){ return <div> <div><a onClick={this.banned_ips} className="link"> Download banned IPs</a></div> <div><a onClick={this.reserved_ips} className="link"> Download reserved IPs</a></div> </div>; } } const Rule = withRouter(class Rule extends Pure_component { state = {ports: []}; trigger_types = [ {key: 'i.e. Status code', value: ''}, {key: 'Status code', value: 'status'}, {key: 'HTML body element', value: 'body'}, {key: 'Minimum request time', value: 'min_req_time'}, {key: 'Maximum request time', value: 'max_req_time'}, ]; action_types = [ {key: 'i.e. Retry with new IP', value: ''}, {key: 'Retry with new IP', value: 'retry'}, {key: 'Retry with new proxy port (Waterfall)', value: 'retry_port'}, {key: 'Ban IP', value: 'ban_ip'}, {key: 'Refresh IP', value: 'refresh_ip'}, {key: 'Save IP to reserved pool', value: 'save_to_pool'}, ]; ban_options = [ {key: '10 minutes', value: '10min'}, {key: '20 minutes', value: '20min'}, {key: '30 minutes', value: '30min'}, {key: '40 minutes', value: '40min'}, {key: '50 minutes', value: '50min'}, {key: 'Custom', value: 'custom'}, ]; status_types = [ 'i.e. 200 - Succeeded requests', '200 - Succeeded requests', '403 - Forbidden', '404 - Not found', '500 - Internal server error', '502 - Bad gateway', '503 - Service unavailable', '504 - Gateway timeout', 'Custom' ].map(s=>({key: s, value: s})); componentDidMount(){ this.setdb_on('head.proxies_running', proxies=>{ const cur_port = this.props.match.params.port; const ports = (proxies||[]) .filter(p=>p.port!=cur_port) .map(p=>({key: p.port, value: p.port})); this.setState({ports}); }); } set_rule_field = (field, value)=>{ setdb.emit('head.proxy_edit.update_rule', {rule_id: this.props.rule.id, field, value}); }; trigger_change = val=>{ if (val!='status') { this.set_rule_field('status_code', ''); this.set_rule_field('status_custom', ''); } if (val!='body') this.set_rule_field('body_regex', ''); if (val!='min_req_time') this.set_rule_field('min_req_time', ''); if (val!='max_req_time') this.set_rule_field('max_req_time', ''); if (!val) this.set_rule_field('trigger_url_regex', ''); }; action_changed = val=>{ if (val=='retry_port') { const def_port = this.state.ports.length&&this.state.ports[0].key; this.set_rule_field(val, def_port||''); } if (val!='ban_ip') { this.set_rule_field('ban_ip_duration', ''); this.set_rule_field('ban_ip_custom', ''); } }; status_changed = val=>{ if (val!='Custom') this.set_rule_field('status_custom', ''); }; render(){ const rule = this.props.rule; return <div className="rule_wrapper"> <Btn_rule_del on_click={()=>this.props.rule_del(this.props.rule.id)}/> <Rule_config id="trigger_type" type="select" data={this.trigger_types} on_change={this.trigger_change} rule={this.props.rule}/> {rule.trigger_type=='body' && <Rule_config id="body_regex" type="text" rule={this.props.rule}/> } {rule.trigger_type=='min_req_time' && <Rule_config id="min_req_time" type="number" sufix="milliseconds" rule={this.props.rule}/> } {rule.trigger_type=='max_req_time' && <Rule_config id="max_req_time" type="number" sufix="milliseconds" rule={this.props.rule}/> } {rule.trigger_type=='status' && <Rule_config id="status_code" type="select" data={this.status_types} on_change={this.status_changed} rule={this.props.rule}/> } {rule.status_code=='Custom' && <Rule_config id="status_custom" type="text" rule={this.props.rule}/> } <Rule_config id="trigger_url_