Merge: * WHOIS: improvements

Close #861

* commit '563f8031dce889b076c91a4b3b6489241cd2b322':
  - client: fix naming
  - client: fix typo in translations
  + client: add icons to the whois info
  * clients: remove an auto-client if a client with the same IP was added manually
  + whois: use "descr" or "netname" in case there's no "orgname"
This commit is contained in:
Simon Zolin 2019-09-26 16:46:07 +03:00
commit 407917241a
15 changed files with 236 additions and 99 deletions

View File

@ -393,8 +393,9 @@
"sign_out": "Sign out",
"forgot_password": "Forgot password?",
"forgot_password_desc": "Please follow <0>these steps</0> to create a new password for your user account.",
"city": "<0>City:</0> {{value}}",
"country": "<0>Country:</0> {{value}}",
"orgname": "<0>OrgName:</0> {{value}}",
"location": "Location",
"orgname": "Organization name",
"netname": "Network name",
"descr": "Description",
"whois": "Whois"
}

View File

@ -28,13 +28,13 @@ const countCell = dnsQueries =>
return <Cell value={value} percent={percent} color={percentColor} />;
};
const clientCell = (clients, autoClients) =>
const clientCell = (clients, autoClients, t) =>
function cell(row) {
const { value } = row;
return (
<div className="logs__row logs__row--overflow logs__row--column">
{formatClientCell(value, clients, autoClients)}
{formatClientCell(value, clients, autoClients, t)}
</div>
);
};
@ -59,11 +59,13 @@ const Clients = ({
accessor: 'ip',
sortMethod: (a, b) =>
parseInt(a.replace(/\./g, ''), 10) - parseInt(b.replace(/\./g, ''), 10),
Cell: clientCell(clients, autoClients),
Cell: clientCell(clients, autoClients, t),
},
{
Header: <Trans>requests_count</Trans>,
accessor: 'count',
minWidth: 180,
maxWidth: 200,
Cell: countCell(dnsQueries),
},
]}

View File

@ -129,3 +129,25 @@
white-space: nowrap;
overflow: hidden;
}
.logs__whois {
display: inline;
}
.logs__whois::after {
content: "|";
padding: 0 5px;
}
.logs__whois:last-child::after {
content: "";
}
.logs__whois-icon.icons {
position: relative;
top: -2px;
width: 16px;
height: 16px;
margin-right: 2px;
opacity: 0.6;
}

View File

@ -191,7 +191,7 @@ class Logs extends Component {
};
getClientCell = ({ original, value }) => {
const { dashboard } = this.props;
const { dashboard, t } = this.props;
const { clients, autoClients } = dashboard;
const { reason, domain } = original;
const isFiltered = this.checkFiltered(reason);
@ -200,7 +200,7 @@ class Logs extends Component {
return (
<Fragment>
<div className="logs__row logs__row--overflow logs__row--column">
{formatClientCell(value, clients, autoClients)}
{formatClientCell(value, clients, autoClients, t)}
</div>
{isRewrite ? (
<div className="logs__action">

View File

@ -4,9 +4,12 @@ import { withNamespaces } from 'react-i18next';
import ReactTable from 'react-table';
import Card from '../../ui/Card';
import WhoisCell from './WhoisCell';
import WrapCell from './WrapCell';
import whoisCell from './whoisCell';
const COLUMN_MIN_WIDTH = 200;
class AutoClients extends Component {
getStats = (ip, stats) => {
if (stats) {
@ -21,26 +24,31 @@ class AutoClients extends Component {
{
Header: this.props.t('table_client'),
accessor: 'ip',
minWidth: COLUMN_MIN_WIDTH,
Cell: WrapCell,
},
{
Header: this.props.t('table_name'),
accessor: 'name',
minWidth: COLUMN_MIN_WIDTH,
Cell: WrapCell,
},
{
Header: this.props.t('source_label'),
accessor: 'source',
minWidth: COLUMN_MIN_WIDTH,
Cell: WrapCell,
},
{
Header: this.props.t('whois'),
accessor: 'whois_info',
Cell: WhoisCell,
minWidth: COLUMN_MIN_WIDTH,
Cell: whoisCell(this.props.t),
},
{
Header: this.props.t('requests_count'),
accessor: 'statistics',
minWidth: COLUMN_MIN_WIDTH,
Cell: (row) => {
const clientIP = row.original.ip;
const clientStats = clientIP && this.getStats(clientIP, this.props.topClients);

View File

@ -7,7 +7,8 @@ import { MODAL_TYPE, CLIENT_ID } from '../../../helpers/constants';
import Card from '../../ui/Card';
import Modal from './Modal';
import WrapCell from './WrapCell';
import WhoisCell from './WhoisCell';
import whoisCell from './whoisCell';
class ClientsTable extends Component {
handleFormAdd = (values) => {
@ -138,11 +139,17 @@ class ClientsTable extends Component {
return (
<div className="logs__row logs__row--icons">
{value && value.length > 0 ? value.map(service => (
<svg className="service__icon service__icon--table" title={service} key={service}>
<use xlinkHref={`#service_${service}`} />
</svg>
)) : ''}
{value && value.length > 0
? value.map(service => (
<svg
className="service__icon service__icon--table"
title={service}
key={service}
>
<use xlinkHref={`#service_${service}`} />
</svg>
))
: ''}
</div>
);
},
@ -151,7 +158,7 @@ class ClientsTable extends Component {
Header: this.props.t('whois'),
accessor: 'whois_info',
minWidth: 200,
Cell: WhoisCell,
Cell: whoisCell(this.props.t),
},
{
Header: this.props.t('requests_count'),

View File

@ -1,38 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Trans } from 'react-i18next';
const getFormattedWhois = (value) => {
const keys = Object.keys(value);
if (keys.length > 0) {
return (
keys.map(key => (
<div key={key} title={value[key]}>
<Trans
values={{ value: value[key] }}
components={[<small key="0">text</small>]}
>
{key}
</Trans>
</div>
))
);
}
return '';
};
const WhoisCell = ({ value }) => (
<div className="logs__row logs__row--overflow">
<span className="logs__text logs__text--wrap">
{getFormattedWhois(value)}
</span>
</div>
);
WhoisCell.propTypes = {
value: PropTypes.object.isRequired,
};
export default WhoisCell;

View File

@ -10,10 +10,7 @@ const WrapCell = ({ value }) => (
);
WrapCell.propTypes = {
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default WrapCell;

View File

@ -0,0 +1,43 @@
import React, { Fragment } from 'react';
import { normalizeWhois } from '../../../helpers/helpers';
import { WHOIS_ICONS } from '../../../helpers/constants';
const getFormattedWhois = (value, t) => {
const whoisInfo = normalizeWhois(value);
const whoisKeys = Object.keys(whoisInfo);
if (whoisKeys.length > 0) {
return whoisKeys.map((key) => {
const icon = WHOIS_ICONS[key];
return (
<div key={key} title={t(key)}>
{icon && (
<Fragment>
<svg className="logs__whois-icon text-muted-dark icons">
<use xlinkHref={`#${icon}`} />
</svg>
&nbsp;
</Fragment>
)}
{whoisInfo[key]}
</div>
);
});
}
return '';
};
const whoisCell = t =>
function cell(row) {
const { value } = row;
return (
<div className="logs__row logs__row--overflow">
<span className="logs__text logs__text--wrap">{getFormattedWhois(value, t)}</span>
</div>
);
};
export default whoisCell;

View File

@ -131,6 +131,18 @@ const Icons = () => (
<symbol id="question" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<circle cx="12" cy="12" r="10" /><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /><line x1="12" y1="17" x2="12" y2="17" />
</symbol>
<symbol id="question" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
<circle cx="12" cy="12" r="10" /><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /><line x1="12" y1="17" x2="12" y2="17" />
</symbol>
<symbol id="network" viewBox="0 0 50 50" fill="currentColor" strokeLinecap="round" strokeLinejoin="round">
<path d="M 25 7 C 15.941406 7 7.339844 10.472656 0.78125 16.773438 L 0.0625 17.464844 L 5.59375 23.230469 L 6.320313 22.539063 C 11.378906 17.679688 18.015625 15 25 15 C 31.984375 15 38.621094 17.679688 43.683594 22.539063 L 44.40625 23.230469 L 49.941406 17.464844 L 49.21875 16.769531 C 42.660156 10.46875 34.058594 7 25 7 Z M 25 19 C 19.046875 19 13.394531 21.28125 9.085938 25.421875 L 8.363281 26.113281 L 13.921875 31.90625 L 14.644531 31.210938 C 17.464844 28.496094 21.144531 27 25 27 C 28.855469 27 32.535156 28.496094 35.355469 31.210938 L 36.078125 31.90625 L 41.636719 26.113281 L 40.917969 25.421875 C 36.605469 21.28125 30.953125 19 25 19 Z M 25 31 C 22.15625 31 19.453125 32.089844 17.390625 34.074219 L 16.671875 34.765625 L 25 43.441406 L 33.328125 34.765625 L 32.609375 34.074219 C 30.546875 32.089844 27.84375 31 25 31 Z"/>
</symbol>
<symbol id="location" viewBox="0 0 24 24" fill="currentColor" strokeLinecap="round" strokeLinejoin="round">
<path d="M12,2C8.134,2,5,5.134,5,9c0,5,7,13,7,13s7-8,7-13C19,5.134,15.866,2,12,2z M12,11.5c-1.381,0-2.5-1.119-2.5-2.5 c0-1.381,1.119-2.5,2.5-2.5s2.5,1.119,2.5,2.5C14.5,10.381,13.381,11.5,12,11.5z"/>
</symbol>
</svg>
);

View File

@ -267,3 +267,10 @@ export const STATS_INTERVALS_DAYS = [1, 7, 30, 90];
export const QUERY_LOG_INTERVALS_DAYS = [1, 7, 30, 90];
export const FILTERS_INTERVALS_HOURS = [0, 1, 12, 24, 72, 168];
export const WHOIS_ICONS = {
location: 'location',
orgname: 'network',
netname: 'network',
descr: '',
};

View File

@ -1,32 +1,55 @@
import React, { Fragment } from 'react';
import { getClientInfo } from './helpers';
import { getClientInfo, normalizeWhois } from './helpers';
import { WHOIS_ICONS } from './constants';
export const formatClientCell = (value, clients, autoClients) => {
const getFormattedWhois = (whois, t) => {
const whoisInfo = normalizeWhois(whois);
return (
Object.keys(whoisInfo).map((key) => {
const icon = WHOIS_ICONS[key];
return (
<span className="logs__whois text-muted" key={key} title={t(key)}>
{icon && (
<Fragment>
<svg className="logs__whois-icon icons">
<use xlinkHref={`#${icon}`} />
</svg>&nbsp;
</Fragment>
)}{whoisInfo[key]}
</span>
);
})
);
};
export const formatClientCell = (value, clients, autoClients, t) => {
const clientInfo = getClientInfo(clients, value) || getClientInfo(autoClients, value);
const { name, whois } = clientInfo;
let whoisContainer = '';
let nameContainer = value;
if (whois && name) {
return (
<Fragment>
<div className="logs__text logs__text--wrap" title={`${name} (${value})`}>
{name} <small className="text-muted-dark">({value})</small>
</div>
<div className="logs__text logs__text--wrap" title={whois}>
<small className="text-muted">{whois}</small>
</div>
</Fragment>
);
} else if (name) {
return (
if (name) {
nameContainer = (
<span className="logs__text logs__text--wrap" title={`${name} (${value})`}>
{name} <small>({value})</small>
</span>
);
}
if (whois) {
whoisContainer = (
<div className="logs__text logs__text--wrap mt-1">
{getFormattedWhois(whois, t)}
</div>
);
}
return (
<span className="logs__text" title={value}>
{value}
<span className="logs__text">
<Fragment>
{nameContainer}
{whoisContainer}
</Fragment>
</span>
);
};

View File

@ -245,21 +245,6 @@ export const redirectToCurrentProtocol = (values, httpPort = 80) => {
export const normalizeTextarea = text => text && text.replace(/[;, ]/g, '\n').split('\n').filter(n => n);
const formatWhois = (whois) => {
if (!whois) {
return '';
}
const keys = Object.keys(whois);
if (keys.length > 0) {
return (
keys.map(key => whois[key])
);
}
return '';
};
export const getClientInfo = (clients, ip) => {
const client = clients.find(item => ip === item.ip);
@ -268,8 +253,7 @@ export const getClientInfo = (clients, ip) => {
}
const { name, whois_info } = client;
const formattedWhois = formatWhois(whois_info);
const whois = formattedWhois && formattedWhois.length > 0 && formattedWhois.join(' | ');
const whois = Object.keys(whois_info).length > 0 ? whois_info : '';
return { name, whois };
};
@ -308,3 +292,29 @@ export const normalizeRulesTextarea = text => text && text.replace(/^\n/g, '').r
export const isVersionGreater = (currentVersion, previousVersion) => (
versionCompare(currentVersion, previousVersion) === -1
);
export const normalizeWhois = (whois) => {
if (Object.keys(whois).length > 0) {
const {
city, country, ...values
} = whois;
let location = (country && country) || '';
if (city && location) {
location = `${location}, ${city}`;
} else if (city) {
location = city;
}
if (location) {
return {
location,
...values,
};
}
return { ...values };
}
return whois;
};

View File

@ -206,6 +206,12 @@ func (clients *clientsContainer) Add(c Client) (bool, error) {
}
}
ch, ok := clients.ipHost[c.IP]
if ok {
c.WhoisInfo = ch.WhoisInfo
delete(clients.ipHost, c.IP)
}
clients.list[c.Name] = &c
if len(c.IP) != 0 {
clients.ipIndex[c.IP] = &c
@ -287,12 +293,14 @@ func (clients *clientsContainer) SetWhoisInfo(ip string, info [][]string) {
if ok {
c.WhoisInfo = info
log.Debug("Clients: set WHOIS info for client %s: %v", c.Name, c.WhoisInfo)
return
}
ch, ok := clients.ipHost[ip]
if ok {
ch.WhoisInfo = info
log.Debug("Clients: set WHOIS info for auto-client %s: %v", ch.Host, ch.WhoisInfo)
return
}
ch = ClientHost{
@ -310,6 +318,12 @@ func (clients *clientsContainer) AddHost(ip, host string, source clientSource) (
clients.lock.Lock()
defer clients.lock.Unlock()
// check index
_, ok := clients.ipIndex[ip]
if ok {
return false, nil
}
// check index
c, ok := clients.ipHost[ip]
if ok && c.Source > source {

View File

@ -8,6 +8,8 @@ import (
whois "github.com/likexian/whois-go"
)
const maxValueLength = 250
// Whois - module context
type Whois struct {
clients *clientsContainer
@ -26,9 +28,19 @@ func initWhois(clients *clientsContainer) *Whois {
return &w
}
// If the value is too large - cut it and append "..."
func trimValue(s string) string {
if len(s) <= maxValueLength {
return s
}
return s[:maxValueLength-3] + "..."
}
// Parse plain-text data from the response
func whoisParse(data string) map[string]string {
m := map[string]string{}
descr := ""
netname := ""
lines := strings.Split(data, "\n")
for _, ln := range lines {
ln = strings.TrimSpace(ln)
@ -45,14 +57,31 @@ func whoisParse(data string) map[string]string {
k = strings.ToLower(k)
v := strings.TrimSpace(kv[1])
if k == "orgname" || k == "org-name" {
m["orgname"] = v
} else if k == "city" {
m["city"] = v
} else if k == "country" {
m["country"] = v
switch k {
case "org-name":
m["orgname"] = trimValue(v)
case "orgname":
fallthrough
case "city":
fallthrough
case "country":
m[k] = trimValue(v)
case "descr":
descr = v
case "netname":
netname = v
}
}
// descr or netname -> orgname
_, ok := m["orgname"]
if !ok && len(descr) != 0 {
m["orgname"] = trimValue(descr)
} else if !ok && len(netname) != 0 {
m["orgname"] = trimValue(netname)
}
return m
}