Merge branch 'development' into feature/auto-login-if-no-password

This commit is contained in:
Mark Drobnak
2019-07-10 14:23:41 -04:00
committed by GitHub
14 changed files with 464 additions and 71 deletions
+42 -34
View File
@@ -2529,15 +2529,15 @@
}
},
"@types/chart.js": {
"version": "2.7.54",
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.7.54.tgz",
"integrity": "sha512-BxIUR4mfk0zOqOPEu4gxLP5herra6INQLyFmgVE6JVRNNB+r36g2cd67nDUEEdD/EShZvaR33xausxOGv1+nbw==",
"version": "2.7.55",
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.7.55.tgz",
"integrity": "sha512-CACGbxPkr0vcyrZFpDAr4dqpfgu/PiHWzsKAhXdY+g72Laebjn/dijr7ExwVv9JS/99KrhtKmfJ0vBEhPTJBTQ==",
"dev": true
},
"@types/cheerio": {
"version": "0.22.11",
"resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.11.tgz",
"integrity": "sha512-x0X3kPbholdJZng9wDMhb2swvUi3UYRNAuWAmIPIWlfgAJZp//cql/qblE7181Mg7SjWVwq6ldCPCLn5AY/e7w==",
"version": "0.22.12",
"resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.12.tgz",
"integrity": "sha512-aczowyAJNfrkBV+HS8DyAA87OnvkqGrrOmm5s7V6Jbgimzv/1ZoAy91cLJX8GQrUS60KufD7EIzA2LbK8HV4hg==",
"dev": true,
"requires": {
"@types/node": "*"
@@ -2554,9 +2554,9 @@
}
},
"@types/enzyme": {
"version": "3.9.4",
"resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.9.4.tgz",
"integrity": "sha512-bQcwt5gcKnekrbci4hcapfE2J6rkkFbHM1l4VobLtSl4ogOfj0lvSxrdS6FftCakmJqqPBqdQCwb5KnlivL6SQ==",
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.1.tgz",
"integrity": "sha512-Jd2hvn3w/0rBH8qD2n+JLMq2GjmZoKTZc5TTg8q9cDnJ9jkR7gzPV+ZdipKwGkWpMRXKDm9kuc9UiUqsW5s20w==",
"dev": true,
"requires": {
"@types/cheerio": "*",
@@ -2658,9 +2658,9 @@
}
},
"@types/node": {
"version": "12.0.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.10.tgz",
"integrity": "sha512-LcsGbPomWsad6wmMNv7nBLw7YYYyfdYcz6xryKYQhx89c3XXan+8Q6AJ43G5XDIaklaVkK3mE4fCb0SBvMiPSQ==",
"version": "12.0.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.12.tgz",
"integrity": "sha512-Uy0PN4R5vgBUXFoJrKryf5aTk3kJ8Rv3PdlHjl6UaX+Cqp1QE0yPQ68MPXGrZOfG7gZVNDIJZYyot0B9ubXUrQ==",
"dev": true
},
"@types/prop-types": {
@@ -2676,9 +2676,9 @@
"dev": true
},
"@types/react": {
"version": "16.8.22",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.22.tgz",
"integrity": "sha512-C3O1yVqk4sUXqWyx0wlys76eQfhrQhiDhDlHBrjER76lR2S2Agiid/KpOU9oCqj1dISStscz7xXz1Cg8+sCQeA==",
"version": "16.8.23",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.23.tgz",
"integrity": "sha512-abkEOIeljniUN9qB5onp++g0EY38h7atnDHxwKUFz1r3VH1+yG1OKi2sNPTyObL40goBmfKFpdii2lEzwLX1cA==",
"dev": true,
"requires": {
"@types/prop-types": "*",
@@ -8955,14 +8955,22 @@
}
},
"fs-extra": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.0.1.tgz",
"integrity": "sha512-W+XLrggcDzlle47X/XnS7FXrXu9sDo+Ze9zpndeBxdgv88FHLm1HtmkhEwavruS6koanBjp098rUpHs65EmG7A==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
},
"dependencies": {
"graceful-fs": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz",
"integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==",
"dev": true
}
}
},
"fs-minipass": {
@@ -10519,17 +10527,17 @@
"dev": true
},
"i18next": {
"version": "17.0.4",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-17.0.4.tgz",
"integrity": "sha512-+lwmv3FT8Sv/HwVPjkR6rtEFhgOqt9L/CTehzyxvL/NdkeUYbFZJfE57MsBToB6LFWg3d0sZJIVgYqCpWzUyLQ==",
"version": "17.0.6",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-17.0.6.tgz",
"integrity": "sha512-bdNhzhcM6RG5m82RypVguCrAQNie/ycxW0Q5C6K9UDWD5hqApZfdJFbj4Ikz9jxIR+Ja1eg0yCQLhlCT+opwIg==",
"requires": {
"@babel/runtime": "^7.3.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz",
"integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==",
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.1.tgz",
"integrity": "sha512-g+hmPKs16iewFSmW57NkH9xpPkuYD1RV3UE2BCkXx9j+nhhRb9hsiSxPmEa67j35IecTQdn4iyMtHMbt5VoREg==",
"requires": {
"regenerator-runtime": "^0.13.2"
}
@@ -10850,9 +10858,9 @@
"dev": true
},
"ionicons": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-4.5.8.tgz",
"integrity": "sha512-Zib/fFGHt+r9tWrvZnrxVj22cO4NVj+ZOb76v5yiOyvb0LSTbbReECDg7lEU8e+YR69qCtWHruStJ+b4G/OWxg=="
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-4.6.1.tgz",
"integrity": "sha512-HIT6nAozXnM5yO9an0QpqABM4QFs1xtOjTz4Neyqbs0+fbPqN3p2PwJuh99WR9YaYGrFPzSCW6/n+LLL6C+xeg=="
},
"ip": {
"version": "1.1.5",
@@ -18665,18 +18673,18 @@
"dev": true
},
"react-i18next": {
"version": "10.11.2",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-10.11.2.tgz",
"integrity": "sha512-Ru+WonG9BLILDg/By7Pw0Lne2I7HVLRyhyvDtar22tNNu6de2D89zAKl2RzK2jgq7m7l+BFSnLpz4kX7ZPV+JQ==",
"version": "10.11.4",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-10.11.4.tgz",
"integrity": "sha512-/CWXaf3a5BLNeVnBGxzWOIZLQgSNEc2LWHX4ZaJb7ww0xgY0S5K9HRAMzJIHeHGe7jfpSraprD66VDblWb4ZXA==",
"requires": {
"@babel/runtime": "^7.3.1",
"html-parse-stringify2": "2.0.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz",
"integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==",
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.1.tgz",
"integrity": "sha512-g+hmPKs16iewFSmW57NkH9xpPkuYD1RV3UE2BCkXx9j+nhhRb9hsiSxPmEa67j35IecTQdn4iyMtHMbt5VoREg==",
"requires": {
"regenerator-runtime": "^0.13.2"
}
+8 -8
View File
@@ -4,15 +4,15 @@
"private": true,
"homepage": ".",
"devDependencies": {
"@types/chart.js": "^2.7.54",
"@types/enzyme": "^3.9.4",
"@types/chart.js": "^2.7.55",
"@types/enzyme": "^3.10.1",
"@types/enzyme-adapter-react-16": "^1.0.5",
"@types/fetch-mock": "^7.3.1",
"@types/jest": "^24.0.15",
"@types/lodash.debounce": "^4.0.6",
"@types/lodash.isequal": "^4.5.5",
"@types/node": "^12.0.10",
"@types/react": "^16.8.22",
"@types/node": "^12.0.12",
"@types/react": "^16.8.23",
"@types/react-bootstrap-daterangepicker": "0.0.26",
"@types/react-bootstrap-typeahead": "^3.4.5",
"@types/react-dom": "^16.8.4",
@@ -24,7 +24,7 @@
"enzyme-adapter-react-16": "^1.14.0",
"faker": "^4.1.0",
"fetch-mock": "^7.3.3",
"fs-extra": "^8.0.1",
"fs-extra": "^8.1.0",
"jest-enzyme": "^7.0.2",
"jest-localstorage-mock": "^2.4.0",
"multi-progress": "^2.0.0",
@@ -44,10 +44,10 @@
"bootstrap": "^4.3.1",
"bootstrap-daterangepicker": "^3.0.5",
"chart.js": "^2.8.0",
"i18next": "^17.0.4",
"i18next": "^17.0.6",
"i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^3.0.0",
"ionicons": "^4.5.8",
"ionicons": "^4.6.1",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"moment": "^2.24.0",
@@ -57,7 +57,7 @@
"react-bootstrap-typeahead": "^3.4.5",
"react-chartjs-2": "^2.7.6",
"react-dom": "^16.8.6",
"react-i18next": "^10.11.2",
"react-i18next": "^10.11.4",
"react-router-dom": "^5.0.1",
"react-table": "^6.10.0",
"reactstrap": "^8.0.0",
+2 -1
View File
@@ -27,5 +27,6 @@
"Successfully saved settings": "Successfully saved settings",
"Detected custom upstream server": "Detected custom upstream server",
"DNS Options": "DNS Options",
"Rapid Commit": "Rapid Commit"
"Rapid Commit": "Rapid Commit",
"Prefix length (CIDR)": "Prefix length (CIDR)"
}
+3 -2
View File
@@ -377,11 +377,12 @@ function getDNSInfo() {
},
conditional_forwarding: {
enabled: faker.random.boolean(),
router_ip: faker.internet.ip(),
ip: faker.internet.ip(),
domain: faker.random
.word()
.toLowerCase()
.split(" ", 2)[0]
.split(" ", 2)[0],
cidr: faker.random.arrayElement([8, 16, 24, 32])
}
};
}
@@ -13,5 +13,5 @@ import { shallow } from "enzyme";
import Footer from "../Footer";
it("renders without crashing", () => {
shallow(<Footer />);
shallow(<Footer />).dive();
});
+14 -4
View File
@@ -14,7 +14,7 @@ import { Line } from "react-chartjs-2";
import { WithTranslation, withTranslation } from "react-i18next";
import moment from "moment";
import { getIntervalForRange } from "../../util/graphUtils";
import api from "../../util/api";
import api, { ApiClient } from "../../util/api";
import ChartTooltip from "./ChartTooltip";
import { WithAPIData } from "../common/WithAPIData";
import { ChartDataSets, ChartOptions, TimeUnit } from "chart.js";
@@ -242,17 +242,23 @@ export const TranslatedClientsGraph = withTranslation([
"time-ranges"
])(ClientsGraph);
export const ClientsGraphContainer = () => (
export interface ClientsGraphContainerProps {
apiClient: ApiClient;
}
export const ClientsGraphContainer = ({
apiClient
}: ClientsGraphContainerProps) => (
<TimeRangeContext.Consumer>
{context => (
<WithAPIData
apiCall={() =>
context.range
? api.getClientsGraphDb(
? apiClient.getClientsGraphDb(
context.range,
getIntervalForRange(context.range)
)
: api.getClientsGraph()
: apiClient.getClientsGraph()
}
repeatOptions={
context.range
@@ -271,3 +277,7 @@ export const ClientsGraphContainer = () => (
)}
</TimeRangeContext.Consumer>
);
ClientsGraphContainer.defaultProps = {
apiClient: api
};
@@ -17,7 +17,7 @@ import { TimeRangeContext } from "../common/context/TimeRangeContext";
export interface TopBlockedClientsData {
blockedQueries: number;
topClients: Array<ApiClient>;
topClients: Array<ApiClientData>;
}
/**
+1 -1
View File
@@ -17,7 +17,7 @@ import { TimeRangeContext } from "../common/context/TimeRangeContext";
export interface TopClientsData {
totalQueries: number;
topClients: Array<ApiClient>;
topClients: Array<ApiClientData>;
}
/**
@@ -11,10 +11,21 @@
import React from "react";
import { shallow } from "enzyme";
import {
ClientsGraph,
ClientsGraphContainer,
loadingProps,
transformData,
TranslatedClientsGraph
} from "../ClientsGraph";
import {
TimeRange,
TimeRangeContext,
TimeRangeContextType
} from "../../common/context/TimeRangeContext";
import moment from "moment";
import { Line } from "react-chartjs-2";
import { ChartData, ChartTooltipItem } from "chart.js";
import { ApiClient } from "../../../util/api";
const fakeData: ApiClientsGraph = {
over_time: [
@@ -38,6 +49,8 @@ const fakeData: ApiClientsGraph = {
]
};
const tick = global.tick;
it("shows loading indicator correctly", () => {
const wrapper = shallow(<TranslatedClientsGraph {...loadingProps} />).dive();
@@ -62,3 +75,188 @@ it("loads API data correctly", async () => {
expect(props.datasets[1].label).toEqual(fakeData.clients[1].ip);
expect(props.datasets[0].data!.length).toEqual(fakeData.over_time.length - 1);
});
it("should use all of the data if there is a time range set", () => {
const range: TimeRange = {
name: "test",
from: moment().subtract(1, "hour"),
until: moment()
};
const props = transformData(fakeData, range);
expect(props.datasets[0].data!.length).toEqual(fakeData.over_time.length);
});
it("should use hour as the time unit if the time range is less than a day", () => {
const range: TimeRange = {
name: "test",
from: moment().subtract(1, "hour"),
until: moment()
};
const props = transformData(fakeData, range);
expect(props.timeUnit).toEqual("hour");
});
it("should use day as the time unit if the time range is more than a day", () => {
const range: TimeRange = {
name: "test",
from: moment().subtract(2, "days"),
until: moment()
};
const props = transformData(fakeData, range);
expect(props.timeUnit).toEqual("day");
});
it("should show the date range in the tooltip title", () => {
const t = jest.fn(key => key);
const wrapper = shallow(
// @ts-ignore Ignore the missing i18n props
<ClientsGraph {...transformData(fakeData, null)} t={t} />
);
const titleFunc = wrapper.find(Line).props().options!.tooltips!.callbacks!
.title!;
const tooltipItem: ChartTooltipItem = {
datasetIndex: 0,
index: 0,
xLabel: "04:02",
yLabel: ""
};
const result = titleFunc([tooltipItem], {});
expect(result).toEqual("Client activity from {{from}} to {{to}}");
expect(t).toHaveBeenCalledWith("Client activity from {{from}} to {{to}}", {
from: "03:57:00",
to: "04:06:59"
});
});
it("should show the client and count in the tooltip label", () => {
const wrapper = shallow(
<TranslatedClientsGraph {...transformData(fakeData, null)} />
).dive();
const labelFunc = wrapper.find(Line).props().options!.tooltips!.callbacks!
.label!;
const tooltipItem: ChartTooltipItem = {
datasetIndex: 0,
index: 0,
xLabel: "xLabel",
yLabel: "yLabel"
};
const data: ChartData = {
datasets: [
{
label: "datasetLabel"
}
]
};
expect(labelFunc(tooltipItem, data)).toEqual("datasetLabel: yLabel");
});
it("should use the normal API call when there is no time range", () => {
const context: TimeRangeContextType = {
range: null,
update: () => {}
};
const apiClient = ({
getClientsGraph: jest.fn(() => Promise.reject({ isCanceled: true })),
getClientsGraphDb: jest.fn(() => Promise.reject({ isCanceled: true }))
} as any) as ApiClient;
shallow(
<TimeRangeContext.Provider value={context}>
<ClientsGraphContainer apiClient={apiClient} />
</TimeRangeContext.Provider>
)
.dive()
.dive()
.dive();
expect(apiClient.getClientsGraph).toHaveBeenCalled();
expect(apiClient.getClientsGraphDb).not.toHaveBeenCalled();
});
it("should use the DB API call when there is a time range", () => {
const context: TimeRangeContextType = {
range: {
name: "test",
from: moment().subtract(1, "day"),
until: moment()
},
update: () => {}
};
const apiClient = ({
getClientsGraph: jest.fn(() => Promise.reject({ isCanceled: true })),
getClientsGraphDb: jest.fn(() => Promise.reject({ isCanceled: true }))
} as any) as ApiClient;
shallow(
<TimeRangeContext.Provider value={context}>
<ClientsGraphContainer apiClient={apiClient} />
</TimeRangeContext.Provider>
)
.dive()
.dive()
.dive();
expect(apiClient.getClientsGraph).not.toHaveBeenCalled();
expect(apiClient.getClientsGraphDb).toHaveBeenCalledWith(context.range, 600);
});
it("should transform the data and render if the API returns data", async () => {
const context: TimeRangeContextType = {
range: null,
update: () => {}
};
const apiClient = ({
getClientsGraph: () => Promise.resolve(fakeData)
} as any) as ApiClient;
const wrapper = shallow(
<TimeRangeContext.Provider value={context}>
<ClientsGraphContainer apiClient={apiClient} />
</TimeRangeContext.Provider>
)
.dive()
.dive()
.dive();
// Let the API call resolve
await tick();
wrapper.update();
const actualProps = wrapper.find(TranslatedClientsGraph).props();
const expectedProps = transformData(fakeData, null);
expect(actualProps).toEqual(expectedProps);
});
it("should render as loading if the API fails to return data", async () => {
const context: TimeRangeContextType = {
range: null,
update: () => {}
};
const apiClient = ({
getClientsGraph: () => Promise.reject({ error: {} })
} as any) as ApiClient;
const wrapper = shallow(
<TimeRangeContext.Provider value={context}>
<ClientsGraphContainer apiClient={apiClient} />
</TimeRangeContext.Provider>
)
.dive()
.dive()
.dive();
// Let the API call resolve
await tick();
wrapper.update();
const actualProps = wrapper.find(TranslatedClientsGraph).props();
expect(actualProps).toEqual(loadingProps);
});
@@ -14,14 +14,16 @@ import i18next from "i18next";
export interface ConditionalForwardingObject {
enabled: boolean;
routerIp: string;
ip: string;
domain: string;
cidr: number;
}
export interface ConditionalForwardingSettingsProps {
settings: ConditionalForwardingObject;
onUpdate: (settings: ConditionalForwardingObject) => void;
isRouterIpValid: boolean;
isCidrValid: boolean;
isDomainValid: boolean;
t: i18next.TFunction;
}
@@ -30,6 +32,7 @@ const ConditionalForwardingSettings = ({
settings,
onUpdate,
isRouterIpValid,
isCidrValid,
isDomainValid,
t
}: ConditionalForwardingSettingsProps) => (
@@ -52,12 +55,39 @@ const ConditionalForwardingSettings = ({
<Input
id="routerIP"
disabled={!settings.enabled}
value={settings.routerIp}
onChange={e => onUpdate({ ...settings, routerIp: e.target.value })}
value={settings.ip}
onChange={e => onUpdate({ ...settings, ip: e.target.value })}
invalid={!isRouterIpValid}
/>
</Col>
</FormGroup>
<FormGroup row>
<Label for="cidr" sm={5}>
{t("Prefix length (CIDR)")}
</Label>
<Col sm={7}>
<Input
id="cidr"
disabled={!settings.enabled}
value={settings.cidr === -1 ? "" : settings.cidr}
invalid={!isCidrValid}
onChange={e => {
let cidr = parseInt(e.target.value);
if (e.target.value.length === 0) {
// Use -1 as an internal representation of the empty string.
// It will show the form as invalid and make the text field
// function as the user expects it to.
cidr = -1;
} else if (isNaN(cidr)) {
return;
}
onUpdate({ ...settings, cidr });
}}
/>
</Col>
</FormGroup>
<FormGroup row>
<Label for="localDomain" sm={5}>
{t("Local Domain Name")}
+26 -13
View File
@@ -23,7 +23,13 @@ import ConditionalForwardingSettings, {
} from "./ConditionalForwardingSettings";
import DnsOptionSettings, { DnsOptionsObject } from "./DnsOptionSettings";
import Alert, { AlertType } from "../common/Alert";
import { isValidHostname, isValidIpv4 } from "../../util/validate";
import {
isValidHostname,
isValidIpv4,
isValidIpv4Cidr,
isValidIpv6,
isValidIpv6Cidr
} from "../../util/validate";
export interface DNSInfoState {
alertMessage: string;
@@ -44,8 +50,9 @@ class DNSInfo extends Component<WithTranslation, DNSInfoState> {
upstreamDns: [],
conditionalForwarding: {
enabled: false,
routerIp: "",
domain: ""
ip: "",
domain: "",
cidr: 24
},
options: {
fqdnRequired: false,
@@ -69,8 +76,7 @@ class DNSInfo extends Component<WithTranslation, DNSInfoState> {
this.setState({
upstreamDns: res.upstream_dns,
conditionalForwarding: {
enabled: res.conditional_forwarding.enabled,
routerIp: res.conditional_forwarding.router_ip,
...res.conditional_forwarding,
domain
},
options: {
@@ -136,11 +142,7 @@ class DNSInfo extends Component<WithTranslation, DNSInfoState> {
this.updateHandler = makeCancelable(
api.updateDNSInfo({
upstream_dns: this.state.upstreamDns,
conditional_forwarding: {
enabled: this.state.conditionalForwarding.enabled,
router_ip: this.state.conditionalForwarding.routerIp,
domain: this.state.conditionalForwarding.domain
},
conditional_forwarding: this.state.conditionalForwarding,
options: {
fqdn_required: this.state.options.fqdnRequired,
bogus_priv: this.state.options.bogusPriv,
@@ -195,8 +197,13 @@ class DNSInfo extends Component<WithTranslation, DNSInfoState> {
const { t } = this.props;
const isRouterIpValid = this.isCFSettingValid(
this.state.conditionalForwarding.routerIp,
isValidIpv4
this.state.conditionalForwarding.ip,
address => isValidIpv4(address) || isValidIpv6(address)
);
const isCidrValid = this.isCFSettingValid(
this.state.conditionalForwarding.cidr.toString(),
cidr => isValidIpv4Cidr(cidr) || isValidIpv6Cidr(cidr)
);
const isDomainValid = this.isCFSettingValid(
@@ -230,6 +237,7 @@ class DNSInfo extends Component<WithTranslation, DNSInfoState> {
settings={this.state.conditionalForwarding}
onUpdate={this.handleConditionalForwardingUpdate}
isRouterIpValid={isRouterIpValid}
isCidrValid={isCidrValid}
isDomainValid={isDomainValid}
t={t}
/>
@@ -243,7 +251,12 @@ class DNSInfo extends Component<WithTranslation, DNSInfoState> {
</FormGroup>
<Button
type="submit"
disabled={this.state.processing || !isRouterIpValid || !isDomainValid}
disabled={
this.state.processing ||
!isRouterIpValid ||
!isCidrValid ||
!isDomainValid
}
>
{t("Apply")}
</Button>
+4 -3
View File
@@ -54,8 +54,9 @@ interface ApiDnsSettings {
};
conditional_forwarding: {
enabled: boolean;
router_ip: string;
ip: string;
domain: string;
cidr: number;
};
}
@@ -188,7 +189,7 @@ interface ApiTopDomains {
}
interface ApiTopClients {
top_clients: Array<ApiClient>;
top_clients: Array<ApiClientData>;
total_queries: number;
}
@@ -197,7 +198,7 @@ interface ApiTopBlockedClients {
blocked_queries: number;
}
interface ApiClient {
interface ApiClientData {
name: string;
ip: string;
count: number;
+101
View File
@@ -12,7 +12,10 @@ import {
isPositiveNumber,
isValidDomain,
isValidIpv4,
isValidIpv4Cidr,
isValidIpv4OptionalPort,
isValidIpv6,
isValidIpv6Cidr,
isValidIpv6OptionalPort,
isValidRegex
} from "../validate";
@@ -229,6 +232,70 @@ describe("Testing the validation functions", () => {
});
});
describe("isValidIpv4Cidr", () => {
it("passes 8", () => {
expect(isValidIpv4Cidr("8")).toBe(true);
});
it("passes 16", () => {
expect(isValidIpv4Cidr("16")).toBe(true);
});
it("passes 24", () => {
expect(isValidIpv4Cidr("24")).toBe(true);
});
it("passes 32", () => {
expect(isValidIpv4Cidr("32")).toBe(true);
});
it("fails -1", () => {
expect(isValidIpv4Cidr("-1")).toBe(false);
});
it("fails 0", () => {
expect(isValidIpv4Cidr("0")).toBe(false);
});
it("fails 28", () => {
expect(isValidIpv4Cidr("28")).toBe(false);
});
it("fails 40", () => {
expect(isValidIpv4Cidr("40")).toBe(false);
});
});
describe("isValidIpv6", () => {
it("passes 1fff:0:a88:85a3::ac1f", () => {
expect(isValidIpv6("1fff:0:a88:85a3::ac1f")).toBe(true);
});
it("passes ::1", () => {
expect(isValidIpv6("::1")).toBe(true);
});
it("fails [::1]:5353", () => {
expect(isValidIpv6("[::1]:5353")).toBe(false);
});
it("fails [1fff:0:a88:85a3::ac1f]:8001", () => {
expect(isValidIpv6("[1fff:0:a88:85a3::ac1f]:8001")).toBe(false);
});
it("fails 192.168.1.1", () => {
expect(isValidIpv6("192.168.1.1")).toBe(false);
});
it("fails 192.168.1.1:53", () => {
expect(isValidIpv6("192.168.1.1:53")).toBe(false);
});
it("fails [::1]", () => {
expect(isValidIpv6("[::1]")).toBe(false);
});
});
describe("isValidIpv6OptionalPort", () => {
it("passes 1fff:0:a88:85a3::ac1f", () => {
expect(isValidIpv6OptionalPort("1fff:0:a88:85a3::ac1f")).toBe(true);
@@ -260,4 +327,38 @@ describe("Testing the validation functions", () => {
expect(isValidIpv6OptionalPort("[::1]")).toBe(false);
});
});
describe("isValidIpv6Cidr", () => {
it("passes 8", () => {
expect(isValidIpv6Cidr("8")).toBe(true);
});
it("passes 64", () => {
expect(isValidIpv6Cidr("64")).toBe(true);
});
it("passes 56", () => {
expect(isValidIpv6Cidr("56")).toBe(true);
});
it("passes 128", () => {
expect(isValidIpv6Cidr("128")).toBe(true);
});
it("fails -1", () => {
expect(isValidIpv6Cidr("-1")).toBe(false);
});
it("fails 0", () => {
expect(isValidIpv6Cidr("0")).toBe(false);
});
it("fails 63", () => {
expect(isValidIpv6Cidr("63")).toBe(false);
});
it("fails 150", () => {
expect(isValidIpv6Cidr("150")).toBe(false);
});
});
});
+30
View File
@@ -95,6 +95,25 @@ export function isValidIpv4OptionalPort(address: string): boolean {
);
}
/**
* Check if the string is a valid IPv4 CIDR number which dnsmasq accepts
*
* @param cidr The string to check
*/
export function isValidIpv4Cidr(cidr: string): boolean {
return ["8", "16", "24", "32"].includes(cidr);
}
/**
* Check if the string is a valid IPv6 address. This function does not do a
* thorough check of the IPv6 address; the backend will check it in detail.
*
* @param address
*/
export function isValidIpv6(address: string): boolean {
return /^[a-fA-F0-9:]+$/.test(address);
}
/**
* Check if the string is a valid IPv6 address, and it can contain an optional
* port. This function does not do a thorough check of the IPv6 address; the
@@ -108,3 +127,14 @@ export function isValidIpv4OptionalPort(address: string): boolean {
export function isValidIpv6OptionalPort(address: string): boolean {
return /^(\[[a-fA-F0-9:]+]:\d+|[a-fA-F0-9:]+)$/.test(address);
}
/**
* Check if the string is a valid IPv6 CIDR number which dnsmasq accepts
*
* @param cidr The string to check
*/
export function isValidIpv6Cidr(cidr: string): boolean {
const cidrNum = parseInt(cidr);
return !isNaN(cidrNum) && cidrNum > 0 && cidrNum <= 128 && cidrNum % 4 === 0;
}