Merge pull request #115 from pi-hole/feature/save-dns-settings

DNS Settings Page
This commit is contained in:
Mark Drobnak
2018-12-16 12:38:58 -05:00
committed by GitHub
27 changed files with 986 additions and 80 deletions
+1 -1
View File
@@ -351,7 +351,7 @@ function getDNSInfo() {
fqdn_required: faker.random.boolean(),
bogus_priv: faker.random.boolean(),
dnssec: faker.random.boolean(),
listening_type: faker.random.arrayElement(["single", "lan", "all"])
listening_type: faker.random.arrayElement(["single", "local", "all"])
},
conditional_forwarding: {
enabled: faker.random.boolean(),
+109 -8
View File
@@ -4004,6 +4004,15 @@
"sha.js": "^2.4.8"
}
},
"create-react-context": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.2.tgz",
"integrity": "sha512-KkpaLARMhsTsgp0d2NA/R94F/eDLbhXERdIq3LvX2biCAXcDvHYoOqHfWCHf1+OLj+HKBotLG3KqaOOf+C1C+A==",
"requires": {
"fbjs": "^0.8.0",
"gud": "^1.0.0"
}
},
"cross-spawn": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
@@ -5252,8 +5261,7 @@
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"escodegen": {
"version": "1.9.1",
@@ -7224,6 +7232,11 @@
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
"dev": true
},
"gud": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz",
"integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw=="
},
"gzip-size": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.0.0.tgz",
@@ -11317,10 +11330,9 @@
}
},
"merge": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz",
"integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=",
"dev": true
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz",
"integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ=="
},
"merge-deep": {
"version": "3.0.2",
@@ -17753,6 +17765,15 @@
}
}
},
"prop-types-extra": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.0.tgz",
"integrity": "sha512-QFyuDxvMipmIVKD2TwxLVPzMnO4e5oOf1vr3tJIomL8E7d0lr6phTHd5nkPhFIzTD1idBLLEPeylL9g+rrTzRg==",
"requires": {
"react-is": "^16.3.2",
"warning": "^3.0.0"
}
},
"proxy-addr": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
@@ -18034,6 +18055,64 @@
}
}
},
"react-bootstrap-typeahead": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/react-bootstrap-typeahead/-/react-bootstrap-typeahead-3.2.4.tgz",
"integrity": "sha512-wFXIKbOHY4Q1AtnhSQIAnUvSDxmcFdDc5wIX/yd7YJug2jL5TvKspJ3XfX/FaWEWyn1pgHukRuFGnbYnL8rtUQ==",
"requires": {
"classnames": "^2.2.0",
"escape-string-regexp": "^1.0.5",
"invariant": "^2.2.1",
"lodash": "^4.17.2",
"prop-types": "^15.5.8",
"prop-types-extra": "^1.0.1",
"react-onclickoutside": "^6.1.1",
"react-overlays": "^0.8.1",
"react-popper": "^1.0.0",
"warning": "^4.0.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz",
"integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==",
"requires": {
"regenerator-runtime": "^0.12.0"
}
},
"popper.js": {
"version": "1.14.6",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.6.tgz",
"integrity": "sha512-AGwHGQBKumlk/MDfrSOf0JHhJCImdDMcGNoqKmKkU+68GFazv3CQ6q9r7Ja1sKDZmYWTckY/uLyEznheTDycnA=="
},
"react-popper": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.2.tgz",
"integrity": "sha512-UbFWj55Yt9uqvy0oZ+vULDL2Bw1oxeZF9/JzGyxQ5ypgauRH/XlarA5+HLZWro/Zss6Ht2kqpegtb6sYL8GUGw==",
"requires": {
"@babel/runtime": "^7.1.2",
"create-react-context": "<=0.2.2",
"popper.js": "^1.14.4",
"prop-types": "^15.6.1",
"typed-styles": "^0.0.7",
"warning": "^4.0.2"
}
},
"regenerator-runtime": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
},
"warning": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz",
"integrity": "sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug==",
"requires": {
"loose-envify": "^1.0.0"
}
}
}
},
"react-chartjs-2": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-2.7.2.tgz",
@@ -18294,8 +18373,25 @@
"react-is": {
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.4.1.tgz",
"integrity": "sha512-xpb0PpALlFWNw/q13A+1aHeyJyLYCg0/cCHPUA43zYluZuIPHaHL3k8OBsTgQtxqW0FhyDEMvi8fZ/+7+r4OSQ==",
"dev": true
"integrity": "sha512-xpb0PpALlFWNw/q13A+1aHeyJyLYCg0/cCHPUA43zYluZuIPHaHL3k8OBsTgQtxqW0FhyDEMvi8fZ/+7+r4OSQ=="
},
"react-onclickoutside": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz",
"integrity": "sha512-p84kBqGaMoa7VYT0vZ/aOYRfJB+gw34yjpda1Z5KeLflg70HipZOT+MXQenEhdkPAABuE2Astq4zEPdMqUQxcg=="
},
"react-overlays": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.8.3.tgz",
"integrity": "sha512-h6GT3jgy90PgctleP39Yu3eK1v9vaJAW73GOA/UbN9dJ7aAN4BTZD6793eI1D5U+ukMk17qiqN/wl3diK1Z5LA==",
"requires": {
"classnames": "^2.2.5",
"dom-helpers": "^3.2.1",
"prop-types": "^15.5.10",
"prop-types-extra": "^1.0.1",
"react-transition-group": "^2.2.0",
"warning": "^3.0.0"
}
},
"react-perfect-scrollbar": {
"version": "1.1.1",
@@ -21286,6 +21382,11 @@
}
}
},
"typed-styles": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz",
"integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q=="
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+4 -3
View File
@@ -25,14 +25,14 @@
"@coreui/react": "^2.0.4",
"bootstrap": "^4.1.1",
"chart.js": "^2.7.2",
"font-awesome": "^4.7.0",
"i18next": "^11.3.2",
"i18next-browser-languagedetector": "^2.2.0",
"i18next-xhr-backend": "^1.5.1",
"ionicons": "^3.0.0",
"font-awesome": "^4.7.0",
"simple-line-icons": "^2.4.1",
"prop-types": "^15.6.1",
"react": "^16.4.1",
"react-bootstrap-typeahead": "^3.2.4",
"react-chartjs-2": "^2.7.2",
"react-dom": "^16.4.1",
"react-i18next": "^7.6.1",
@@ -40,7 +40,8 @@
"react-table": "^6.8.2",
"react-transition-group": "^2.3.1",
"reactstrap": "^6.1.0",
"sha.js": "^2.4.11"
"sha.js": "^2.4.11",
"simple-line-icons": "^2.4.1"
},
"scripts": {
"start": "npm run build-css && npm-run-all -p watch-css start-js",
+3 -1
View File
@@ -25,5 +25,7 @@
"Only forward public reverse lookups": "Only forward public reverse lookups",
"Processing...": "Processing...",
"Successfully saved settings": "Successfully saved settings",
"Apply": "Apply"
"Apply": "Apply",
"Detected custom upstream server": "Detected custom upstream server",
"DNS Options": "DNS Options"
}
+22 -1
View File
@@ -1 +1,22 @@
{}
{
"unknown": "desconocido",
"gravity_error": "Error_Gravity",
"ftl_connection_fail": "Fallo_Conexión_FTL",
"ftl_read_error": "Error_lectura_FTL",
"ftl_eom_error": "Error_eom_FTL",
"not_found": "No_se_encuentra",
"already_exists": "Ya_existe",
"invalid_domain": "Domino_inválido",
"bad_request": "Petición_inválida",
"unauthorized": "Sin_autorización",
"file_read": "Archivo_leido",
"file_write": "Archivo_escrito",
"config_parsing_error": "Error_en_la_configuración",
"invalid_setting_value": "Valor_inválido",
"restart_dns_error": "Error_reinicio_DNS",
"dnsmasq_config_write": "Escritura_config_dnsmasq",
"shared_memory_open": "shared_memory_open",
"shared_memory_read": "shared_memory_read",
"shared_memory_lock": "shared_memory_lock",
"API Error: {{error}}": "API Error: {{error}}"
}
+4 -1
View File
@@ -22,5 +22,8 @@
"Branch": "Rama",
"Hash": "Hash",
"Tag": "Etiqueta",
"Only forward public reverse lookups": "Solo búsquedas públicas inversas hacia adelante"
"Only forward public reverse lookups": "Solo búsquedas públicas inversas hacia adelante",
"Processing...": "Procesando...",
"Successfully saved settings": "Opciones salvadas con exito",
"Apply": "Aplicar"
}
+22
View File
@@ -0,0 +1,22 @@
{
"unknown": "Errore sconosciuto",
"gravity_error": "Errore nella creazione della lista dei bloccati",
"ftl_connection_fail": "Errore nella connessione a FTL",
"ftl_read_error": "Errore nella lettura da FTL",
"ftl_eom_error": "Lettura EOM inaspettata da FTL",
"not_found": "Non trovato",
"already_exists": "L'oggetto esiste già",
"invalid_domain": "Dominio non valido",
"bad_request": "Richiesta non valida",
"unauthorized": "Non hai le autorizzazioni necessarie",
"file_read": "Errore nella lettura da {{file}}",
"file_write": "Errore nella scrittura su {{file}}",
"config_parsing_error": "Errore nell'operazione di parsing della configurazione",
"invalid_setting_value": "Valore del parametro non valido",
"restart_dns_error": "Errore nel riavvio del server DNS",
"dnsmasq_config_write": "Errore nella generazione della configurazione di dnsmasq",
"shared_memory_open": "Errore nell'apertura della memoria condivisa",
"shared_memory_read": "Errore nella lettura della memoria condivisa",
"shared_memory_lock": "Errore nel blocco della memoria condivisa",
"API Error: {{error}}": "Errore dell'API: {{error}}"
}
+24
View File
@@ -0,0 +1,24 @@
{
"Details": "Dettagli\n",
"Status": "Stato",
"DNS": "DNS",
"Donate": "Dona",
"Domain": "Dominî",
"Hits": "Nº richieste",
"Frequency": "Frequenza",
"Client": "Client",
"Requests": "Richieste",
"Add": "Aggiungi",
"Time": "Data",
"Type": "Tipo",
"Network": "Rete",
"Action": "Azione",
"Enabled": "Attivato",
"Disabled": "Disattivato",
"DHCP": "DHCP",
"Core": "Core",
"FTL": "FTL",
"API": "API",
"Web": "Web",
"Version": "Versione"
}
+23
View File
@@ -0,0 +1,23 @@
{
"Domains On Blocklist": "Dominî nella lista dei bloccati",
"Query Types": "Tipi di interrogazioni",
"Forward Destinations": "Destinazioni inviate",
"Top Permitted Domains": "Classifica dei dominî consentiti più richiesti",
"Top Blocked Domains": "Classifica dei dominî bloccati più richiesti",
"Top Clients": "Classifica dei client",
"Total Queries ({{count}} clients)": "Totale delle interrogazioni (tramite 1 client)",
"Total Queries ({{count}} clients)_plural": "Totale delle interrogazioni (tramite {{count}} client)",
"Queries Blocked": "Interrogazioni bloccate",
"Percent Blocked": "Percentuale bloccata",
"Queries Over Last 24 Hours": "Le interrogazioni effettuate nelle ultime 24 ore",
"Clients Over Last 24 Hours": "I client che hanno effettuato richieste nelle ultime 24 ore",
"Queries Answered By": "Interrogazioni fornite da",
"Queries from {{from}} to {{to}}": "Interrogazioni dalle {{from}} alle {{to}}",
"Total Queries": "Totale delle interrogazioni",
"Blocked Queries": "Interrogazioni bloccate",
"Client activity from {{from}} to {{to}}": "Attività del client dalle {{from}} alle {{to}}",
"{{percent}}% of {{total}}": "{{percent}}% di {{total}}",
"No Clients Found": "Nessun client trovato",
"No Domains Found": "Nessun dominio trovato",
"Queries Answered By Destination": "Interrogazioni fornite dalla destinazione"
}
+6
View File
@@ -0,0 +1,6 @@
{
"if you found this useful": "se ritieni questo software utile",
"Core Version": "Versione Pi-hole",
"Web Version": "Versione interfaccia web",
"Update Available": "È disponibile un aggiornamento"
}
+14
View File
@@ -0,0 +1,14 @@
{
"Adding {{domain}}...": "Aggiunta di {{domain}} in corso...",
"Successfully added {{domain}}": "{{domain}} è stato aggiunto alla lista",
"Failed to add {{domain}}": "Errore nell'aggiunta di {{domain}}",
"Note: Whitelisting a subdomain of a wildcard blocked domain is not possible.": "Nota: inserire nella lista bianca un sottodominio di un dominio già bloccato da wildcard non è possibile.",
"Note: Only the domain and subdomains of the blocked domain will be blocked.": "Nota: solo il dominio e i suoi sottodominî saranno bloccati.",
"Add a domain (example.com or sub.example.com)": "Aggiungi un dominio (come example.com o sub.example.com)",
"{{domain}} is already added": "{{domain}} è già stato aggiunto",
"Failed to remove {{domain}}": "Errore nella rimozione di {{domain}}",
"There are no domains in this list": "Non ci sono dominî in questa lista",
"Input a regular expression": "Inserisci un'espressione regolare",
"Not a valid domain": "Non è un dominio valido",
"Not a valid regular expression": "Non è un'espressione regolare valida"
}
+14
View File
@@ -0,0 +1,14 @@
{
"Query Log": "Registro delle interrogazioni",
"Whitelist": "Lista bianca",
"Blacklist": "Lista nera",
"Dashboard": "Pannello delle statistiche",
"Exact": "L'esatto",
"Wildcard": "Wildcard",
"Login": "Accedi",
"Logout": "Esci",
"Regex": "Espressione regolare",
"Settings": "Impostazioni",
"Networking": "Networking",
"Versions": "Versioni"
}
+10
View File
@@ -0,0 +1,10 @@
{
"Password": "Password",
"Sign in to start your session": "Accedi per avviare la sessione",
"Log in": "Accedi",
"Forgot Password": "Hai dimenticato la password?",
"forgot_password_description": "Una volta configurato Pi-hole per la prima volta, viene generata e mostrata una password all'utente. La password non può essere più recuperata in futuro, ma è possibile impostare una nuova password digitando questo comando (la password può essere disattivata se si lascia il campo vuoto):",
"Wrong Password!": "Password errata!",
"You will be transferred to the \"{{page}}\" page": "Verrai reindirizzato alla pagina «{{page}}»",
"Verify that cookies are allowed for {{host}}": "Verifica che i cookie sono consentiti per {{host}}"
}
+7
View File
@@ -0,0 +1,7 @@
{
"Blocked": "Bloccato",
"Allowed (forwarded)": "Permesso (inviato)",
"Allowed (cached)": "Permesso (dalla cache)",
"Blocked (regex)": "Bloccato (da espressione regolare)",
"Blocked (blacklist)": "Bloccato (dalla lista nera)"
}
+29
View File
@@ -0,0 +1,29 @@
{
"DHCP Active": "DHCP attivo",
"Start IP": "IP iniziale",
"End IP": "IP finale",
"Router IP": "IP del router",
"Lease Time": "Tempo del lease",
"IPv6 Support": "Supporto a IPv6",
"Upstream DNS Servers": "Server DNS di Upstream",
"Interface listening behavior": "Comportamento dell'interfaccia di ascolto",
"Forward FQDNs only": "Invia solo FQDNs",
"Use DNSSEC": "Utilizza DNSSEC",
"Conditional Forwarding": "Invio condizionale",
"Local Domain Name": "Nome del dominio locale",
"FTL Database": "Database FTL",
"Queries": "Interrogazioni",
"Filesize": "Dimensioni del file",
"SQLite version": "Versione di SQLite",
"Interface": "Interfaccia",
"IPv4 address": "Indirizzo IPv4",
"IPv6 address": "Indirizzo IPv6",
"Hostname": "Nome dell'host",
"Branch": "Branch",
"Hash": "Hash",
"Tag": "Etichetta",
"Only forward public reverse lookups": "Invia solo i lookup pubblici invertiti",
"Processing...": "Elaborazione in corso...",
"Successfully saved settings": "Impostazioni salvate con successo",
"Apply": "Applica"
}
@@ -0,0 +1,72 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2017 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* Settings :: DNS :: Upstream DNS Servers
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React, { Fragment } from "react";
import PropTypes from "prop-types";
import { Col, FormGroup, Input, Label } from "reactstrap";
const ConditionalForwardingSettings = ({
settings,
onUpdate,
isRouterIpValid,
isDomainValid,
t
}) => (
<Fragment>
<FormGroup check>
<Label check>
<Input
type="checkbox"
checked={settings.enabled}
onChange={e => onUpdate({ ...settings, enabled: e.target.checked })}
/>
{t("Enabled")}
</Label>
</FormGroup>
<FormGroup row>
<Label for="routerIP" sm={5}>
{t("Router IP")}
</Label>
<Col sm={7}>
<Input
id="routerIP"
disabled={!settings.enabled}
value={settings.routerIp}
onChange={e => onUpdate({ ...settings, routerIp: e.target.value })}
invalid={!isRouterIpValid}
/>
</Col>
</FormGroup>
<FormGroup row>
<Label for="localDomain" sm={5}>
{t("Local Domain Name")}
</Label>
<Col sm={7}>
<Input
id="localDomain"
disabled={!settings.enabled}
value={settings.domain}
onChange={e => onUpdate({ ...settings, domain: e.target.value })}
invalid={!isDomainValid}
/>
</Col>
</FormGroup>
</Fragment>
);
ConditionalForwardingSettings.propTypes = {
settings: PropTypes.object.isRequired,
onUpdate: PropTypes.func.isRequired,
isRouterIpValid: PropTypes.bool.isRequired,
isDomainValid: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired
};
export default ConditionalForwardingSettings;
+189 -63
View File
@@ -3,7 +3,7 @@
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* Settings :: DHCP
* Settings :: DNS
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
@@ -12,93 +12,219 @@ import React, { Component } from "react";
import { translate } from "react-i18next";
import { ignoreCancel, makeCancelable } from "../../util";
import api from "../../util/api";
import DnsList from "./DnsList";
import { Button, Col, Form, FormGroup } from "reactstrap";
import ConditionalForwardingSettings from "./ConditionalForwardingSettings";
import DnsOptionSettings from "./DnsOptionSettings";
import Alert from "../common/Alert";
import { isValidHostname, isValidIpv4 } from "../../validate";
class DNSInfo extends Component {
state = {
upstream_dns: [],
conditional_forwarding: {
alertMessage: "",
alertType: "",
showAlert: false,
processing: false,
upstreamDns: [],
conditionalForwarding: {
enabled: false,
router_ip: "",
routerIp: "",
domain: ""
},
options: {
fqdn_required: false,
bogus_priv: false,
fqdnRequired: false,
bogusPriv: false,
dnssec: false,
listening_type: "single"
listeningType: "single"
}
};
constructor(props) {
super(props);
this.updateDNSInfo = this.updateDNSInfo.bind(this);
}
updateDNSInfo() {
this.updateHandler = makeCancelable(api.getDNSInfo(), {
repeat: this.updateDNSInfo,
interval: 600000
});
this.updateHandler.promise
loadDNSInfo = () => {
this.loadHandler = makeCancelable(api.getDNSInfo());
this.loadHandler.promise
.then(res => {
this.setState(res);
// If the domain is empty, fill it in with a default.
const savedDomain = res.conditional_forwarding.domain;
const domain = savedDomain.length === 0 ? "lan" : savedDomain;
this.setState({
upstreamDns: res.upstream_dns,
conditionalForwarding: {
enabled: res.conditional_forwarding.enabled,
routerIp: res.conditional_forwarding.router_ip,
domain
},
options: {
fqdnRequired: res.options.fqdn_required,
bogusPriv: res.options.bogus_priv,
dnssec: res.options.dnssec,
listeningType: res.options.listening_type
}
});
})
.catch(ignoreCancel);
}
};
componentDidMount() {
this.updateDNSInfo();
this.loadDNSInfo();
}
componentWillUnmount() {
this.updateHandler.cancel();
this.loadHandler.cancel();
}
handleUpstreamAdd = upstream => {
this.setState({
upstreamDns: this.state.upstreamDns.concat(upstream)
});
};
handleUpstreamRemove = upstream => {
this.setState({
upstreamDns: this.state.upstreamDns.filter(item => item !== upstream)
});
};
handleConditionalForwardingUpdate = conditionalForwarding => {
this.setState({ conditionalForwarding });
};
handleDnsOptionsUpdate = options => {
this.setState({ options });
};
/**
* Save changes to DNS settings
*
* @param e the submit event
*/
saveSettings = e => {
e.preventDefault();
const { t } = this.props;
this.setState({
alertMessage: t("Processing..."),
alertType: "info",
showAlert: true,
processing: true
});
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
},
options: {
fqdn_required: this.state.options.fqdnRequired,
bogus_priv: this.state.options.bogusPriv,
dnssec: this.state.options.dnssec,
listening_type: this.state.options.listeningType
}
})
);
this.updateHandler.promise
.then(() => {
this.setState({
alertMessage: t("Successfully saved settings"),
alertType: "success",
showAlert: true,
processing: false
});
})
.catch(ignoreCancel)
.catch(error => {
let message = "";
if (error instanceof Error) {
message = error.message;
} else {
// Translate the API's error message
message = t("API Error: {{error}}", {
error: t(error.key, error.data)
});
}
this.setState({
alertMessage: message,
alertType: "danger",
showAlert: true,
processing: false
});
});
};
isCFSettingValid = (value, validator) => {
return (
(!this.state.conditionalForwarding.enabled && value.length === 0) ||
validator(value)
);
};
hideAlert = () => {
this.setState({ showAlert: false });
};
render() {
const { t } = this.props;
const isRouterIpValid = this.isCFSettingValid(
this.state.conditionalForwarding.routerIp,
isValidIpv4
);
const isDomainValid = this.isCFSettingValid(
this.state.conditionalForwarding.domain,
isValidHostname
);
const alert = this.state.showAlert ? (
<Alert
message={this.state.alertMessage}
type={this.state.alertType}
onClick={this.hideAlert}
/>
) : null;
return (
<div className="row">
<div className="col-lg-4 col-sm-4 col-xs-4">
<pre>
<br />
{t("Upstream DNS Servers")}:<br />
{this.state.upstream_dns.map(item => item + "\n")}
<br />
</pre>
</div>
<div className="col-lg-4 col-sm-4 col-xs-4">
<pre>
<br />
{t("Interface listening behavior")}:{" "}
{this.state.options.listening_type}
<br />
{t("Forward FQDNs only")}:{" "}
{this.state.options.fqdn_required.toString()}
<br />
{t("Only forward public reverse lookups")}:
{this.state.options.bogus_priv.toString()}
<br />
{t("Use DNSSEC")}: {this.state.options.dnssec.toString()}
</pre>
</div>
<div className="col-lg-4 col-sm-4 col-xs-4">
<pre>
<br />
{t("Conditional Forwarding")}
<br />
{t("Enabled")}:{" "}
{this.state.conditional_forwarding.enabled.toString()}
<br />
{t("Router IP")}:{" "}
{this.state.conditional_forwarding.router_ip.toString()}
<br />
{t("Local Domain Name")}:{" "}
{this.state.conditional_forwarding.domain.toString()}
<br />
</pre>
</div>
</div>
<Form onSubmit={this.saveSettings}>
{alert}
<FormGroup row>
<Col sm={6}>
<h3>{t("Upstream DNS Servers")}</h3>
<DnsList
onAdd={this.handleUpstreamAdd}
onRemove={this.handleUpstreamRemove}
upstreams={this.state.upstreamDns}
/>
</Col>
<Col sm={6}>
<h3>{t("Conditional Forwarding")}</h3>
<ConditionalForwardingSettings
settings={this.state.conditionalForwarding}
onUpdate={this.handleConditionalForwardingUpdate}
isRouterIpValid={isRouterIpValid}
isDomainValid={isDomainValid}
t={t}
/>
<h3>{t("DNS Options")}</h3>
<DnsOptionSettings
settings={this.state.options}
onUpdate={this.handleDnsOptionsUpdate}
t={t}
/>
</Col>
</FormGroup>
<Button
type="submit"
disabled={this.state.processing || !isRouterIpValid || !isDomainValid}
>
{t("Apply")}
</Button>
</Form>
);
}
}
+51
View File
@@ -0,0 +1,51 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2017 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* An Editable List of Upstream DNS Servers
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from "react";
import PropTypes from "prop-types";
import { ListGroup } from "reactstrap";
import DnsListItem from "./DnsListItem";
import DnsListNewItem from "./DnsListNewItem";
import { isValidIpv4OptionalPort } from "../../validate";
/**
* Check if an upstream address is unique and valid
*
* @param address the address to check
* @param upstreams the list of current upstreams
* @returns {boolean} if the upstream address is unique and valid
*/
export const isAddressValid = (address, upstreams) =>
!upstreams.includes(address) && isValidIpv4OptionalPort(address);
const DnsList = ({ upstreams, onAdd, onRemove }) => (
<ListGroup style={{ overflowY: "scroll", maxHeight: "350px" }}>
{upstreams.map(upstream => (
<DnsListItem
key={upstream}
onRemove={() => onRemove(upstream)}
address={upstream}
/>
))}
<DnsListNewItem
onAdd={onAdd}
isValid={address => isAddressValid(address, upstreams)}
upstreams={upstreams}
/>
</ListGroup>
);
DnsList.propTypes = {
upstreams: PropTypes.arrayOf(PropTypes.string).isRequired,
onAdd: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired
};
export default DnsList;
+29
View File
@@ -0,0 +1,29 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2017 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* A Upstream DNS List Item
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from "react";
import PropTypes from "prop-types";
import { Button, ListGroupItem } from "reactstrap";
const DnsListItem = ({ address, onRemove }) => (
<ListGroupItem>
{address}
<Button color="danger" size="sm" className="pull-right" onClick={onRemove}>
<span className="fa fa-trash-o" />
</Button>
</ListGroupItem>
);
DnsListItem.propTypes = {
address: PropTypes.string.isRequired,
onRemove: PropTypes.func.isRequired
};
export default DnsListItem;
+88
View File
@@ -0,0 +1,88 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2017 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* Input For New DNS List Items
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Button, InputGroup, InputGroupAddon, ListGroupItem } from "reactstrap";
import { Typeahead } from "react-bootstrap-typeahead";
import { translate } from "react-i18next";
import { preconfiguredUpstreamOptions } from "./preconfiguredUpstreams";
/**
* A component to add upstream DNS servers. The servers are either selected from
* a list of preconfigured upstreams, or custom servers can be input.
*/
class DnsListNewItem extends Component {
static propTypes = {
onAdd: PropTypes.func.isRequired,
isValid: PropTypes.func.isRequired,
upstreams: PropTypes.arrayOf(PropTypes.string).isRequired
};
state = {
address: "",
selected: []
};
/**
* Get the currently selected upstream address (either custom or preconfigured)
*
* @returns {string} the selected upstream address
*/
getAddress = () => {
if (this.state.selected.length === 0) {
return this.state.address;
}
return this.state.selected[0].address;
};
render() {
const { t } = this.props;
const isAddressValid = this.props.isValid(this.getAddress());
return (
<ListGroupItem>
<InputGroup>
<Typeahead
onInputChange={address => this.setState({ address })}
onChange={selected => this.setState({ selected })}
options={preconfiguredUpstreamOptions.filter(
upstream => !this.props.upstreams.includes(upstream.address)
)}
selected={this.state.selected}
emptyLabel={t("Detected custom upstream server")}
bodyContainer
/>
<InputGroupAddon addonType="append">
<Button
color="success"
size="sm"
onClick={() => {
// Add the server to the list
this.props.onAdd(this.getAddress());
// Reset the input
this.setState({ address: "", selected: [] });
}}
disabled={!isAddressValid}
>
<span className="fa fa-plus" />
</Button>
</InputGroupAddon>
</InputGroup>
</ListGroupItem>
);
}
}
export default translate("settings")(DnsListNewItem);
@@ -0,0 +1,77 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2017 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* Settings :: DNS :: DNS Options
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React, { Fragment } from "react";
import PropTypes from "prop-types";
import { Col, FormGroup, Input, Label } from "reactstrap";
const DnsOptionSettings = ({ settings, onUpdate, t }) => (
<Fragment>
<FormGroup row>
<Label for="listeningBehavior" sm={5}>
{t("Interface listening behavior")}
</Label>
<Col sm={7}>
<Input
id="listeningBehavior"
type="select"
value={settings.listeningType}
onChange={e =>
onUpdate({ ...settings, listeningType: e.target.value })
}
>
<option>all</option>
<option>local</option>
<option>single</option>
</Input>
</Col>
</FormGroup>
<FormGroup check>
<Label check>
<Input
type="checkbox"
checked={settings.fqdnRequired}
onChange={e =>
onUpdate({ ...settings, fqdnRequired: e.target.checked })
}
/>
{t("Forward FQDNs only")}
</Label>
</FormGroup>
<FormGroup check>
<Label check>
<Input
type="checkbox"
checked={settings.bogusPriv}
onChange={e => onUpdate({ ...settings, bogusPriv: e.target.checked })}
/>
{t("Only forward public reverse lookups")}
</Label>
</FormGroup>
<FormGroup check>
<Label check>
<Input
type="checkbox"
checked={settings.dnssec}
onChange={e => onUpdate({ ...settings, dnssec: e.target.checked })}
/>
{t("Use DNSSEC")}
</Label>
</FormGroup>
</Fragment>
);
DnsOptionSettings.propTypes = {
settings: PropTypes.object.isRequired,
onUpdate: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
export default DnsOptionSettings;
@@ -0,0 +1,121 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2017 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* Preconfigured Upstream DNS Servers
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
/**
* A list of preconfigured upstream DNS servers. Each item has primary and
* secondary IPv4 and IPv6 server entries. Some addresses may be empty.
*/
export const preconfiguredUpstreams = [
{
name: "OpenDNS (ECS)",
primaryIpv4: "208.67.222.222",
secondaryIpv4: "208.67.220.220",
primaryIpv6: "2620:0:ccc::2",
secondaryIpv6: "2620:0:ccd::2"
},
{
name: "Google (ECS)",
primaryIpv4: "8.8.8.8",
secondaryIpv4: "8.8.4.4",
primaryIpv6: "2001:4860:4860:0:0:0:0:8888",
secondaryIpv6: "2001:4860:4860:0:0:0:0:8844"
},
{
name: "Quad9 (filtered, DNSSEC)",
primaryIpv4: "9.9.9.9",
secondaryIpv4: "149.112.112.112",
primaryIpv6: "2620:fe::fe",
secondaryIpv6: "2620:fe::9"
},
{
name: "Quad9 (unfiltered, no DNSSEC)",
primaryIpv4: "9.9.9.10",
secondaryIpv4: "149.112.112.10",
primaryIpv6: "2620:fe::10",
secondaryIpv6: "2620:fe::fe:10"
},
{
name: "Quad9 (filtered + ECS)",
primaryIpv4: "9.9.9.11",
secondaryIpv4: "149.112.112.11",
primaryIpv6: "2620:fe::11",
secondaryIpv6: ""
},
{
name: "Cloudflare (ECS)",
primaryIpv4: "1.1.1.1",
secondaryIpv4: "1.0.0.1",
primaryIpv6: "2606:4700:4700::1111",
secondaryIpv6: "2606:4700:4700::1001"
},
{
name: "Level3",
primaryIpv4: "4.2.2.1",
secondaryIpv4: "4.2.2.2",
primaryIpv6: "",
secondaryIpv6: ""
},
{
name: "Comodo",
primaryIpv4: "8.26.56.26",
secondaryIpv4: "8.20.247.20",
primaryIpv6: "",
secondaryIpv6: ""
},
{
name: "DNS.WATCH",
primaryIpv4: "84.200.69.80",
secondaryIpv4: "84.200.70.40",
primaryIpv6: "2001:1608:10:25:0:0:1c04:b12f",
secondaryIpv6: "2001:1608:10:25:0:0:9249:d69b"
}
];
/**
* The preconfigured servers, but with an entry for each address. This is used
* when displaying the servers in a list.
*/
export const preconfiguredUpstreamOptions = preconfiguredUpstreams.flatMap(
upstream => {
// Parse the upstream address information into the new format
const parseUpstream = (address, order, type) => ({
label: upstream.name + " " + order + " " + type + " (" + address + ")",
address
});
let parsedUpstreams = [];
if (upstream.primaryIpv4.length > 0) {
parsedUpstreams.push(
parseUpstream(upstream.primaryIpv4, "Primary", "IPv4")
);
}
if (upstream.secondaryIpv4.length > 0) {
parsedUpstreams.push(
parseUpstream(upstream.secondaryIpv4, "Secondary", "IPv4")
);
}
if (upstream.primaryIpv6.length > 0) {
parsedUpstreams.push(
parseUpstream(upstream.primaryIpv6, "Primary", "IPv6")
);
}
if (upstream.secondaryIpv6.length > 0) {
parsedUpstreams.push(
parseUpstream(upstream.secondaryIpv6, "Secondary", "IPv6")
);
}
return parsedUpstreams;
}
);
+1
View File
@@ -14,6 +14,7 @@ import { BrowserRouter, Route, Switch } from "react-router-dom";
import "ionicons/dist/css/ionicons.min.css";
import "font-awesome/css/font-awesome.min.css";
import "simple-line-icons/css/simple-line-icons.css";
import "react-bootstrap-typeahead/css/Typeahead.css";
import "./scss/style.css";
import Full from "./containers/Full";
import api from "./util/api";
+4
View File
@@ -182,3 +182,7 @@
.form-check-label {
margin-bottom: 10px;
}
.form-check-input {
margin-top: 0.2rem;
}
+3
View File
@@ -91,5 +91,8 @@ export default {
},
updateDHCPInfo(settings) {
return http.put("settings/dhcp", settings);
},
updateDNSInfo(settings) {
return http.put("settings/dns", settings);
}
};
+24 -2
View File
@@ -65,10 +65,32 @@ export function isValidIpv4(address) {
return false;
}
// No segment can be longer than 3 characters or be empty
// All segments must be numbers (positive)
return segments.every(
segment => isPositiveNumber(segment) && parseInt(segment) < 256
);
}
/**
* Check if the string is a valid IPv4 address, and it can contain an optional
* port after the address, separated with a #.
* Example: 127.0.0.1#5353
*
* @param address {string} the address to check
* @returns {boolean} if the address is a valid IPv4 address and the port
* (if it exists) is valid.
*/
export function isValidIpv4OptionalPort(address) {
const split = address.split("#");
const ipv4 = split[0];
// Check the IPv4 address
if (!isValidIpv4(ipv4)) {
return false;
}
// If no port is given or if the port is valid, return true
return (
split.length === 1 || (split.length === 2 && isPositiveNumber(split[1]))
);
}
+35
View File
@@ -12,6 +12,7 @@ import {
isPositiveNumber,
isValidDomain,
isValidIpv4,
isValidIpv4OptionalPort,
isValidRegex
} from "./validate";
@@ -188,4 +189,38 @@ describe("Testing the validation functions", () => {
expect(isValidIpv4("555.666.777.888")).toBe(false);
});
});
describe("isValidIpv4OptionalPort", () => {
it("passes 127.0.0.1", () => {
expect(isValidIpv4OptionalPort("127.0.0.1")).toBe(true);
});
it("passes 127.0.0.1#53", () => {
expect(isValidIpv4OptionalPort("127.0.0.1#53")).toBe(true);
});
it("passes 8.8.8.8#5353", () => {
expect(isValidIpv4OptionalPort("8.8.8.8#5353")).toBe(true);
});
it("fails 1111.1.1.1#53", () => {
expect(isValidIpv4OptionalPort("1111.1.1.1#53")).toBe(false);
});
it("fails 8.8.8.8#", () => {
expect(isValidIpv4OptionalPort("8.8.8.8#")).toBe(false);
});
it("fails 8.8.8.8##", () => {
expect(isValidIpv4OptionalPort("8.8.8.8##")).toBe(false);
});
it("fails 8.8.8.8#53#", () => {
expect(isValidIpv4OptionalPort("8.8.8.8#53#")).toBe(false);
});
it("fails 8.8.8.8#abc", () => {
expect(isValidIpv4OptionalPort("8.8.8.8#abc")).toBe(false);
});
});
});