@luminati-io/luminati-proxy
Version:
A configurable local proxy for brightdata.com
425 lines (418 loc) • 16.7 kB
JavaScript
// LICENSE_CODE ZON ISC
; /*jslint react:true, es6:true*/
import Pure_component from '/www/util/pub/pure_component.js';
import React, {useState} from 'react';
import {Labeled_controller, Loader_small, Alert} from './common.js';
import setdb from '../../util/setdb.js';
import zurl from '../../util/url.js';
import {report_exception} from './util.js';
import _ from 'lodash4';
import $ from 'jquery';
import {Select_zone, Pins} from './common/controls.js';
import Warnings_modal from './common/warnings_modal.js';
import {Modal} from './common/modals.js';
import Logs_settings_modal from './common/logs_settings_modal.js';
import {Back_btn} from './proxy_edit/index.js';
import {tips} from './proxy_edit/fields.js';
import './css/settings.less';
import {T} from './common/i18n.js';
import {main as Api} from './api.js';
export default function Settings(props){
const [show_alert, set_show_alert] = useState(false);
const [show_conf, set_show_conf] = useState(false);
const back_func = ()=>props.history.push({pathname: '/overview'});
const btn_click = ()=>show_conf ? $('#settings_confirmation_modal')
.modal('show') : back_func();
return <div className="settings vbox">
<div className="cp_panel vbox">
<div className="cp_panel_header">
{!props.zagent && <Back_btn click={btn_click}/>}
<h2><T>General settings</T></h2>
</div>
<Form show_alert={set_show_alert}
show_conf={set_show_conf}/>
</div>
{show_alert &&
<Alert
variant="success"
dismissible
text="Settings changes saved"
on_close={()=>set_show_alert(false)}
/>
}
<Modal
id="settings_confirmation_modal"
title="You have unsaved settings changes"
ok_btn_title="Yes"
click_ok={back_func}>
<h4>Are you sure you want to exit?</h4>
</Modal>
</div>;
}
const tooltips = {
zone: `Default zone will be used automatically when creating a new
port, if you don't specify any specific zone. This value can be
overridden in each proxy port settings`,
www_whitelist_ips: `List of IPs that are allowed to access web UI
(including all API endpoints at http://localhost:22999/api) and
make changes. can also include ranges of ips like so 0.0.0.0/0.
Default value is 127.0.0.1, which means that remote access from
any other IP is blocked unless list of IPs are added in this
field.`,
whitelist_ips: `Default access grant for all proxies. Only those
IPs will be able to send requests to all proxies by default. Can
be changed per proxy`,
request_stats: `Enable saving statistics to database`,
logs: `Specify how many requests you want to keep in database. The
limit may be set as a number or maximum database size.`,
har_limit: `Define the limit for the size of the response body to save in
the logs`,
debug: `Default value for Bright Data request details like response
timeline or peer IP that was used to send a final request`,
lpm_auth: 'Default value for "Set x-lpm-authorization header" setting',
log_level: `Define how much log you want to see in the terminal</br>
<ul>
<li><strong>error: </strong>only error messages</li>
<li><strong>warn: </strong>all the above and warnings, potential
unsuccessful operations</li>
<li><strong>notice: </strong>all the above and the essential
notifications like creating proxy ports, refreshing sessions</li>
<li><strong>info: </strong>all the above and proxy requests, API
calls</li>
<li><strong>debug: </strong>all the above and debug info</li>
</ul>`,
sync_config: `All changes on Proxy Manager instances with enabled config
synchronization will be propagated and applied immediately.`,
bw_limit_webhook_url: `URL to send webhook messages to when BW limit is
reached`,
bw_th_webhook_url: `URL to send webhook messages to when BW limit threshold
is reached`,
socket_inactivity_timeout: 'The amount of milliseconds a socket can be'
+' inactive before it times out and closes',
};
for (let f in tooltips)
tooltips[f] = tooltips[f].replace(/\s+/g, ' ').replace(/\n/g, ' ');
let har_limit_options = [
{value: -1, label: 'Disabled'},
{value: 1024, label: '1Kb (default)'},
{value: 100*1024, label: '100Kb'},
{value: 0, label: 'Unlimited'},
];
let socket_timeout_options = [
{value: 120000, label: 'Default'},
{value: 240000, label: '240000'},
{value: 480000, label: '480000'},
];
class Form extends Pure_component {
state = {saving: false, form: {}, pending_settings: {}};
logs_metric_opts = [
{key: 'requests', value: 'requests'},
{key: 'megabytes', value: 'megabytes'},
];
log_level_opts = ['error', 'warn', 'notice', 'info', 'debug'];
default_yes_no_select_opts = [
{key: 'No', value: 'none'},
{key: 'Yes', value: 'full'}
];
componentDidMount(){
this.setdb_on('head.settings', settings=>{
if (!settings)
return;
const c_settings = _.cloneDeep(settings);
const res_settings = {...c_settings,
...this.state.pending_settings};
this.setState({
settings: res_settings, default_settings: c_settings,
is_changed: !_.isEqual(c_settings, res_settings)});
});
this.setdb_on('head.save_settings', save_settings=>{
this.save_settings = save_settings;
if (save_settings && this.resave)
{
delete this.resave;
this.save();
}
});
}
zone_change = val=>{
this.setState(prev=>({settings: {...prev.settings, zone: val}}),
this.debounced_save);
};
prepare_change = ({field, value, opt})=>{
opt = opt||{};
const {settings} = this.state;
let val = value;
if (field=='whitelist_ips')
val = value.filter(ip=>!settings.fixed_whitelist_ips.includes(ip));
if (opt.number)
val = +val;
return {field, value: val};
};
apply_changes = changes=>{
const {settings, pending_settings, default_settings} = this.state;
let changes_obj = changes.reduce((acc, ch)=>Object.assign(acc,
{[ch.field]: ch.value}), {});
const c_settings = {...settings, ...changes_obj};
const is_changed = !_.isEqual(default_settings, c_settings);
this.setState({
settings: c_settings,
pending_settings: {...pending_settings, ...changes_obj},
is_changed
});
this.props.show_conf(is_changed);
};
on_change_handler = (field, opt)=>value=>
this.on_multi_change_handler([{field, opt, value}]);
on_multi_change_handler = changes=>
this.apply_changes(changes.map(this.prepare_change));
remote_logs_enabled = ()=>this.state.settings
&& this.state.settings.logs_settings
&& this.state.settings.logs_settings.type;
logs_enabled = ()=>this.state.settings.logs || this.remote_logs_enabled();
toggle_logs = ()=>{
let settings = [
{field: 'logs', value: 0, opt: {number: 1}},
{field: 'logs_settings', value: {}},
];
if (this.logs_enabled())
return this.apply_changes(settings.map(this.prepare_change));
settings[0].value = 1000;
this.apply_changes(settings.map(this.prepare_change));
};
lock_nav = lock=>setdb.set('head.lock_navigation', lock);
urls_valid = ()=>{
const {bw_limit_webhook_url: limit_url,
bw_th_webhook_url: th_url} = this.state.pending_settings;
for (const url of [limit_url, th_url].filter(Boolean))
{
if (zurl.is_valid_url(url))
continue;
this.setState({error: [{msg: 'Invalid webhook url'}]},
()=>$('#upd_settings_error').modal('show'));
return false;
}
return true;
};
save = ()=>{
if (!this.urls_valid())
return;
if (this.saving || !this.save_settings)
{
this.resave = true;
return;
}
this.lock_nav(true);
this.setState({saving: true});
this.saving = true;
const _this = this;
this.etask(function*(){
this.on('uncaught', e=>_this.etask(function*(){
yield report_exception(e, 'settings.Form.save');
}));
this.on('finally', ()=>{
_this.setState({saving: false});
_this.saving = false;
_this.lock_nav(false);
if (_this.resave)
{
delete _this.resave;
_this.save();
}
});
const body = {..._this.state.pending_settings};
const save_res = yield _this.save_settings(body);
if (save_res.err)
{
return _this.setState({error: [{msg: save_res.err}]}, ()=>
$('#upd_settings_error').modal('show'));
}
_this.setState({is_changed: false, pending_settings: {},
default_settings: _this.state.settings});
_this.props.show_alert(true);
_this.props.show_conf(false);
if (_this.state.settings.sync_config)
{
const proxies = yield Api.json.get('proxies_running');
setdb.set('head.proxies_running', proxies);
}
});
};
open_logs_settings_modal = ()=>$('#logs_settings_modal').modal('show');
debounced_save = _.debounce(this.save, 500);
render(){
const s = this.state.settings;
if (!s)
return null;
const wl = s.fixed_whitelist_ips.concat(s.whitelist_ips);
const logs_data = s.zagent ? [0, 1000] : [0, 100, 1000, 10000];
const har_limit_data = s.zagent ? har_limit_options.filter(({value})=>
[-1, 1024].includes(value)) : har_limit_options;
const note_logs = this.logs_enabled() ?
<a className="link" onClick={this.open_logs_settings_modal}>
<T>Logs settings</T>
</a> : null;
return <div className="settings_form">
<Warnings_modal
id='upd_settings_error'
warnings={this.state.error}
/>
{this.logs_enabled() &&
<Logs_settings_modal
tooltip={tooltips.logs}
logs_data={logs_data}
logs_disabled_num={logs_data[0]||0}
logs_enabled_num={logs_data[1]||1000}
settings={s.logs_settings}
on_save={this.on_multi_change_handler}
remote_enabled={this.remote_logs_enabled()}
/>
}
<Labeled_controller label="Default zone" tooltip={tooltips.zone}>
<Select_zone
val={s.zone}
preview
on_change_wrapper={this.on_change_handler('zone')}
/>
</Labeled_controller>
<Labeled_controller
label="Admin whitelisted IPs"
faq={{anchor: 'whitelisted_ips'}}
tooltip={tooltips.www_whitelist_ips}>
<Pins
val={s.www_whitelist_ips}
pending={s.pending_www_ips}
no_any={s.zagent}
on_change_wrapper={this.on_change_handler('www_whitelist_ips')}
/>
</Labeled_controller>
<Labeled_controller
type="pins"
faq={{anchor: 'whitelisted_ips'}}
label="Proxy whitelisted IPs"
tooltip={tooltips.whitelist_ips}>
<Pins
val={wl}
pending={s.pending_ips}
no_any={s.zagent}
disabled_ips={s.fixed_whitelist_ips}
on_change_wrapper={this.on_change_handler('whitelist_ips')}
/>
</Labeled_controller>
{!s.zagent &&
<Labeled_controller
val={s.request_stats}
type="yes_no"
on_change_wrapper={this.on_change_handler('request_stats')}
label="Enable recent stats"
default={true}
tooltip={tooltips.request_stats}
/>
}
{!s.zagent && <Labeled_controller
val={s.logs}
type="select_number"
on_change_wrapper={this.on_change_handler('logs', {number: 1})}
data={logs_data}
label="Limit for request logs"
default tooltip={tooltips.logs}
/>}
{s.zagent && <Labeled_controller
val={this.logs_enabled()}
type="yes_no"
on_change_wrapper={this.toggle_logs}
label="Enable request logs"
default tooltip={tooltips.logs}
note={note_logs}
/>}
<Labeled_controller
val={s.socket_inactivity_timeout}
type="select_number"
on_change_wrapper={this.on_change_handler(
'socket_inactivity_timeout', {number: 1})}
data={socket_timeout_options}
label="Inactivity timeout"
default={120000}
tooltip={tooltips.socket_inactivity_timeout}
/>
<Labeled_controller
val={s.har_limit}
type="select_number"
on_change_wrapper={this.on_change_handler('har_limit',
{number: 1})}
data={har_limit_data}
label="Response limit to save"
default={1024}
tooltip={tooltips.har_limit}
/>
<Labeled_controller
val={s.debug}
type="select"
faq={{anchor: 'request_details'}}
on_change_wrapper={this.on_change_handler('debug')}
data={this.default_yes_no_select_opts}
label="Default requests details"
tooltip={tooltips.debug}
/>
<Labeled_controller
val={s.lpm_auth}
type="select"
on_change_wrapper={this.on_change_handler('lpm_auth')}
data={this.default_yes_no_select_opts}
label="Default LPM auth. header"
tooltip={tooltips.lpm_auth}
/>
{!s.zagent && <Labeled_controller
val={s.log}
type="select"
on_change_wrapper={this.on_change_handler('log')}
data={this.log_level_opts}
label="Log level / API logs"
tooltip={tooltips.log_level}
faq={{article: '13596408374417', anchor: 'gathering_logs'}}
/>}
<Labeled_controller
val={s.sync_config}
type="yes_no"
on_change_wrapper={this.on_change_handler('sync_config')}
label="Sync configuration"
default={!!s.zagent}
tooltip={tooltips.sync_config}
disabled={s.zagent}
faq={{anchor: 'sync_configuration'}}
/>
{s.zagent && <Labeled_controller
val={s.bw_limit_webhook_url || ''}
allow_empty_url
allow_bad_url_change
type="url"
on_change_wrapper={this.on_change_handler('bw_limit_webhook_url')}
label="BW limit webhook URL"
tooltip={tooltips.bw_limit_webhook_url}
/>}
{s.zagent && <Labeled_controller
val={s.bw_th_webhook_url || ''}
allow_empty_url
allow_bad_url_change
type="url"
on_change_wrapper={this.on_change_handler('bw_th_webhook_url')}
label="BW threshold webhook URL"
tooltip={tooltips.bw_th_webhook_url}
/>}
<Loader_small show={this.state.saving}/>
<button className="btn btn_lpm btn_lpm_primary fit"
onClick={()=>$('#save_settings_confirmation_modal').modal('show')}
disabled={!this.state.is_changed || this.state.saving}>
<T>Save changes</T>
</button>
<Modal
id="save_settings_confirmation_modal"
title="Accept save changes"
ok_btn_title="Yes"
click_ok={this.save}>
{s.sync_config && !s.zagent && <span>
{tips.sync_config_warn}
</span>}
</Modal>
</div>;
}
}