Pull request: 3013 querylog idna

Merge in DNS/adguard-home from 3013-idna to master

Closes #3013.

Squashed commit of the following:

commit 567d4c3beef3cf3ee995ad9d8c3aba6616c74c6c
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Jun 29 13:11:10 2021 +0300

    client: mv punycode label

commit 6585dcaece9f590d7f02afb5aa25953ab0c2555b
Author: Ildar Kamalov <ik@adguard.com>
Date:   Tue Jun 29 12:32:40 2021 +0300

    client: handle unicode name

commit c0f61bfbb9bdf919be7b07c112c4b7a52f3ad6a1
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Jun 28 20:00:57 2021 +0300

    all: imp log of changes

commit 41388abc8770ce164bcba327fcf0013133b5e6f7
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Jun 28 19:52:23 2021 +0300

    scripts: imp hooks

commit 9c4ba933fbd9340e1de061d4f451218238650c0f
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Jun 28 19:47:27 2021 +0300

    all: imp code, docs

commit 61bd6d6f926480cb8c2f9bd3cd2b61e1532f71cf
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Mon Jun 28 16:09:25 2021 +0300

    querylog: add ascii hostname, convert to unicode
This commit is contained in:
Eugene Burkov 2021-06-29 13:36:52 +03:00
parent 9d1656b5c1
commit 16e5e09c2e
11 changed files with 139 additions and 52 deletions

View File

@ -39,6 +39,8 @@ and this project adheres to
### Changed ### Changed
- Internationalized domains are now shown decoded in the query log with the
original encoded version shown in request details. ([#3013]).
- When /etc/hosts-type rules have several IPs for one host, all IPs are now - When /etc/hosts-type rules have several IPs for one host, all IPs are now
returned instead of only the first one ([#1381]). returned instead of only the first one ([#1381]).
- The setting `rlimit_nofile` is now in the `os` block of the configuration - The setting `rlimit_nofile` is now in the `os` block of the configuration
@ -79,6 +81,7 @@ released by then.
[#2441]: https://github.com/AdguardTeam/AdGuardHome/issues/2441 [#2441]: https://github.com/AdguardTeam/AdGuardHome/issues/2441
[#2443]: https://github.com/AdguardTeam/AdGuardHome/issues/2443 [#2443]: https://github.com/AdguardTeam/AdGuardHome/issues/2443
[#2763]: https://github.com/AdguardTeam/AdGuardHome/issues/2763 [#2763]: https://github.com/AdguardTeam/AdGuardHome/issues/2763
[#3013]: https://github.com/AdguardTeam/AdGuardHome/issues/3013
[#3136]: https://github.com/AdguardTeam/AdGuardHome/issues/3136 [#3136]: https://github.com/AdguardTeam/AdGuardHome/issues/3136
[#3166]: https://github.com/AdguardTeam/AdGuardHome/issues/3166 [#3166]: https://github.com/AdguardTeam/AdGuardHome/issues/3166
[#3172]: https://github.com/AdguardTeam/AdGuardHome/issues/3172 [#3172]: https://github.com/AdguardTeam/AdGuardHome/issues/3172

View File

@ -488,6 +488,7 @@
"interval_days": "{{count}} day", "interval_days": "{{count}} day",
"interval_days_plural": "{{count}} days", "interval_days_plural": "{{count}} days",
"domain": "Domain", "domain": "Domain",
"punycode": "Punycode",
"answer": "Answer", "answer": "Answer",
"filter_added_successfully": "The list has been successfully added", "filter_added_successfully": "The list has been successfully added",
"filter_removed_successfully": "The list has been successfully removed", "filter_removed_successfully": "The list has been successfully removed",

View File

@ -16,6 +16,7 @@ const DomainCell = ({
answer_dnssec, answer_dnssec,
client_proto, client_proto,
domain, domain,
unicodeName,
time, time,
tracker, tracker,
type, type,
@ -41,10 +42,22 @@ const DomainCell = ({
const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || ''; const protocol = t(SCHEME_TO_PROTOCOL_MAP[client_proto]) || '';
const ip = type ? `${t('type_table_header')}: ${type}` : ''; const ip = type ? `${t('type_table_header')}: ${type}` : '';
const requestDetailsObj = { let requestDetailsObj = {
time_table_header: formatTime(time, LONG_TIME_FORMAT), time_table_header: formatTime(time, LONG_TIME_FORMAT),
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS), date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
domain, domain,
};
if (domain && unicodeName) {
requestDetailsObj = {
...requestDetailsObj,
domain: unicodeName,
punycode: domain,
};
}
requestDetailsObj = {
...requestDetailsObj,
type_table_header: type, type_table_header: type,
protocol, protocol,
}; };
@ -54,23 +67,40 @@ const DomainCell = ({
const knownTrackerDataObj = { const knownTrackerDataObj = {
name_table_header: tracker?.name, name_table_header: tracker?.name,
category_label: hasTracker && captitalizeWords(tracker.category), category_label: hasTracker && captitalizeWords(tracker.category),
source_label: sourceData source_label: sourceData && (
&& <a href={sourceData.url} target="_blank" rel="noopener noreferrer" <a
className="link--green">{sourceData.name}</a>, href={sourceData.url}
target="_blank"
rel="noopener noreferrer"
className="link--green"
>
{sourceData.name}
</a>
),
}; };
const renderGrid = (content, idx) => { const renderGrid = (content, idx) => {
const preparedContent = typeof content === 'string' ? t(content) : content; const preparedContent = typeof content === 'string' ? t(content) : content;
const className = classNames('text-truncate o-hidden', {
'overflow-break': preparedContent.length > 100, const className = classNames(
}); 'text-truncate o-hidden',
return <div key={idx} className={className}>{preparedContent}</div>; { 'overflow-break': preparedContent?.length > 100 },
);
return (
<div key={idx} className={className}>
{preparedContent}
</div>
);
}; };
const getGrid = (contentObj, title, className) => [ const getGrid = (contentObj, title, className) => [
<div key={title} className={classNames('pb-2 grid--title', className)}>{t(title)}</div>, <div key={title} className={classNames('pb-2 grid--title', className)}>
<div key={`${title}-1`} {t(title)}
className="grid grid--limited">{React.Children.map(Object.entries(contentObj), renderGrid)}</div>, </div>,
<div key={`${title}-1`} className="grid grid--limited">
{React.Children.map(Object.entries(contentObj), renderGrid)}
</div>,
]; ];
const requestDetails = getGrid(requestDetailsObj, 'request_details'); const requestDetails = getGrid(requestDetailsObj, 'request_details');
@ -81,35 +111,60 @@ const DomainCell = ({
'px-2 d-flex justify-content-center flex-column': isDetailed, 'px-2 d-flex justify-content-center flex-column': isDetailed,
}); });
const details = [ip, protocol].filter(Boolean) const details = [ip, protocol].filter(Boolean).join(', ');
.join(', ');
return <div className="d-flex o-hidden logs__cell logs__cell logs__cell--domain" role="gridcell"> return (
{dnssec_enabled && <IconTooltip <div
className={lockIconClass} className="d-flex o-hidden logs__cell logs__cell logs__cell--domain"
tooltipClass='py-4 px-5 pb-45' role="gridcell"
canShowTooltip={!!answer_dnssec} >
xlinkHref='lock' {dnssec_enabled && (
columnClass='w-100' <IconTooltip
content='validated_with_dnssec' className={lockIconClass}
placement='bottom' tooltipClass="py-4 px-5 pb-45"
/>} canShowTooltip={!!answer_dnssec}
<IconTooltip className={privacyIconClass} tooltipClass='pt-4 pb-5 px-5 mw-75' xlinkHref="lock"
xlinkHref='privacy' contentItemClass='key-colon' renderContent={renderContent} columnClass="w-100"
place='bottom' /> content="validated_with_dnssec"
<div className={valueClass}> placement="bottom"
<div className="text-truncate" title={domain}>{domain}</div> />
{details && isDetailed )}
&& <div className="detailed-info d-none d-sm-block text-truncate" <IconTooltip
title={details}>{details}</div>} className={privacyIconClass}
tooltipClass="pt-4 pb-5 px-5 mw-75"
xlinkHref="privacy"
contentItemClass="key-colon"
renderContent={renderContent}
place="bottom"
/>
<div className={valueClass}>
{unicodeName ? (
<div className="text-truncate" title={unicodeName}>
{unicodeName}
</div>
) : (
<div className="text-truncate" title={domain}>
{domain}
</div>
)}
{details && isDetailed && (
<div
className="detailed-info d-none d-sm-block text-truncate"
title={details}
>
{details}
</div>
)}
</div>
</div> </div>
</div>; );
}; };
DomainCell.propTypes = { DomainCell.propTypes = {
answer_dnssec: propTypes.bool.isRequired, answer_dnssec: propTypes.bool.isRequired,
client_proto: propTypes.string.isRequired, client_proto: propTypes.string.isRequired,
domain: propTypes.string.isRequired, domain: propTypes.string.isRequired,
unicodeName: propTypes.string,
time: propTypes.string.isRequired, time: propTypes.string.isRequired,
type: propTypes.string.isRequired, type: propTypes.string.isRequired,
tracker: propTypes.object, tracker: propTypes.object,

View File

@ -77,7 +77,7 @@ export const normalizeLogs = (logs) => logs.map((log) => {
upstream, upstream,
} = log; } = log;
const { host: domain, type } = question; const { name: domain, unicode_name: unicodeName, type } = question;
const processResponse = (data) => (data ? data.map((response) => { const processResponse = (data) => (data ? data.map((response) => {
const { value, type, ttl } = response; const { value, type, ttl } = response;
@ -96,6 +96,7 @@ export const normalizeLogs = (logs) => logs.map((log) => {
return { return {
time, time,
domain, domain,
unicodeName,
type, type,
response: processResponse(answer), response: processResponse(answer),
reason, reason,

View File

@ -127,7 +127,7 @@ func getDoubleQuotesEnclosedValue(s *string) bool {
} }
// parseSearchCriterion parses a search criterion from the query parameter. // parseSearchCriterion parses a search criterion from the query parameter.
func (l *queryLog) parseSearchCriterion(q url.Values, name string, ct criterionType) (bool, searchCriterion, error) { func (l *queryLog) parseSearchCriterion(q url.Values, name string, ct criterionType) (ok bool, sc searchCriterion, err error) {
val := q.Get(name) val := q.Get(name)
if len(val) == 0 { if len(val) == 0 {
return false, searchCriterion{}, nil return false, searchCriterion{}, nil
@ -176,7 +176,7 @@ func (l *queryLog) parseSearchParams(r *http.Request) (p *searchParams, err erro
} }
paramNames := map[string]criterionType{ paramNames := map[string]criterionType{
"search": ctDomainOrClient, "search": ctTerm,
"response_status": ctFilteringStatus, "response_status": ctFilteringStatus,
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns" "github.com/miekg/dns"
"golang.org/x/net/idna"
) )
// TODO(a.garipov): Use a proper structured approach here. // TODO(a.garipov): Use a proper structured approach here.
@ -66,6 +67,20 @@ func (l *queryLog) logEntryToJSONEntry(entry *logEntry) (jsonEntry jobject) {
} }
} }
hostname := entry.QHost
question := jobject{
"type": entry.QType,
"class": entry.QClass,
"name": hostname,
}
if qhost, err := idna.ToUnicode(hostname); err == nil {
if qhost != hostname && qhost != "" {
question["unicode_name"] = qhost
}
} else {
log.Debug("translating %q into unicode: %s", hostname, err)
}
jsonEntry = jobject{ jsonEntry = jobject{
"reason": entry.Result.Reason.String(), "reason": entry.Result.Reason.String(),
"elapsedMs": strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64), "elapsedMs": strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64),
@ -74,11 +89,7 @@ func (l *queryLog) logEntryToJSONEntry(entry *logEntry) (jsonEntry jobject) {
"client_info": entry.client, "client_info": entry.client,
"client_proto": entry.ClientProto, "client_proto": entry.ClientProto,
"upstream": entry.Upstream, "upstream": entry.Upstream,
"question": jobject{ "question": question,
"host": entry.QHost,
"type": entry.QType,
"class": entry.QClass,
},
} }
if entry.ClientID != "" { if entry.ClientID != "" {

View File

@ -67,7 +67,7 @@ func TestQueryLog(t *testing.T) {
}, { }, {
name: "by_domain_strict", name: "by_domain_strict",
sCr: []searchCriterion{{ sCr: []searchCriterion{{
criterionType: ctDomainOrClient, criterionType: ctTerm,
strict: true, strict: true,
value: "TEST.example.org", value: "TEST.example.org",
}}, }},
@ -77,7 +77,7 @@ func TestQueryLog(t *testing.T) {
}, { }, {
name: "by_domain_non-strict", name: "by_domain_non-strict",
sCr: []searchCriterion{{ sCr: []searchCriterion{{
criterionType: ctDomainOrClient, criterionType: ctTerm,
strict: false, strict: false,
value: "example.ORG", value: "example.ORG",
}}, }},
@ -89,7 +89,7 @@ func TestQueryLog(t *testing.T) {
}, { }, {
name: "by_client_ip_strict", name: "by_client_ip_strict",
sCr: []searchCriterion{{ sCr: []searchCriterion{{
criterionType: ctDomainOrClient, criterionType: ctTerm,
strict: true, strict: true,
value: "2.2.2.2", value: "2.2.2.2",
}}, }},
@ -99,7 +99,7 @@ func TestQueryLog(t *testing.T) {
}, { }, {
name: "by_client_ip_non-strict", name: "by_client_ip_non-strict",
sCr: []searchCriterion{{ sCr: []searchCriterion{{
criterionType: ctDomainOrClient, criterionType: ctTerm,
strict: false, strict: false,
value: "2.2.2", value: "2.2.2",
}}, }},

View File

@ -11,9 +11,11 @@ import (
type criterionType int type criterionType int
const ( const (
// ctDomainOrClient is for searching by the domain name, the client's IP // ctTerm is for searching by the domain name, the client's IP
// address, or the clinet's ID. // address, the client's ID or the client's name.
ctDomainOrClient criterionType = iota //
// TODO(e.burkov): Make it support IDNA while #3012.
ctTerm criterionType = iota
// ctFilteringStatus is for searching by the filtering status. // ctFilteringStatus is for searching by the filtering status.
// //
// See (*searchCriterion).ctFilteringStatusCase for details. // See (*searchCriterion).ctFilteringStatusCase for details.
@ -114,7 +116,7 @@ func (c *searchCriterion) ctDomainOrClientCaseNonStrict(
// optimisation purposes. // optimisation purposes.
func (c *searchCriterion) quickMatch(line string, findClient quickMatchClientFunc) (ok bool) { func (c *searchCriterion) quickMatch(line string, findClient quickMatchClientFunc) (ok bool) {
switch c.criterionType { switch c.criterionType {
case ctDomainOrClient: case ctTerm:
host := readJSONValue(line, `"QH":"`) host := readJSONValue(line, `"QH":"`)
ip := readJSONValue(line, `"IP":"`) ip := readJSONValue(line, `"IP":"`)
clientID := readJSONValue(line, `"CID":"`) clientID := readJSONValue(line, `"CID":"`)
@ -141,7 +143,7 @@ func (c *searchCriterion) quickMatch(line string, findClient quickMatchClientFun
// match checks if the log entry matches this search criterion. // match checks if the log entry matches this search criterion.
func (c *searchCriterion) match(entry *logEntry) bool { func (c *searchCriterion) match(entry *logEntry) bool {
switch c.criterionType { switch c.criterionType {
case ctDomainOrClient: case ctTerm:
return c.ctDomainOrClientCase(entry) return c.ctDomainOrClientCase(entry)
case ctFilteringStatus: case ctFilteringStatus:
return c.ctFilteringStatusCase(entry.Result) return c.ctFilteringStatusCase(entry.Result)

View File

@ -4,6 +4,17 @@
## v0.107: API changes ## v0.107: API changes
### The new field `"unicode_name"` in `DNSQuestion`
* The new optional field `"unicode_name"` is the Unicode representation of
question's domain name. It is only presented if the original question's
domain name is an IDN.
### Documentation fix of `DNSQuestion`
* Previously incorrectly named field `"host"` in `DNSQuestion` is now named
`"name"`.
### Disabling Statistics ### Disabling Statistics
* The API `POST /control/stats_config` HTTP API allows disabling statistics by * The API `POST /control/stats_config` HTTP API allows disabling statistics by

View File

@ -1823,9 +1823,12 @@
'class': 'class':
'type': 'string' 'type': 'string'
'example': 'IN' 'example': 'IN'
'host': 'name':
'type': 'string' 'type': 'string'
'example': 'example.org' 'example': 'xn--d1abbgf6aiiy.xn--p1ai'
'unicode_name':
'type': 'string'
'example': 'президент.рф'
'type': 'type':
'type': 'string' 'type': 'string'
'example': 'A' 'example': 'A'

View File

@ -4,7 +4,7 @@ set -e -f -u
# Show all temporary todos to the programmer but don't fail the commit # Show all temporary todos to the programmer but don't fail the commit
# if there are any, because the commit could be in a temporary branch. # if there are any, because the commit could be in a temporary branch.
git grep -e 'TODO.*!!' -- ':!HACKING.md' ':!scripts/hooks/pre-commit' | more || : git grep -e 'TODO.*!!' -- ':!HACKING.md' ':!scripts/hooks/pre-commit' | cat || :
if [ "$( git diff --cached --name-only -- 'client/*.js' )" ] if [ "$( git diff --cached --name-only -- 'client/*.js' )" ]
then then