diff --git a/make-fake-data.js b/make-fake-data.js index 6f3afdc..b0cb7f6 100644 --- a/make-fake-data.js +++ b/make-fake-data.js @@ -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(), diff --git a/package-lock.json b/package-lock.json index 9536876..1c38b83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 0ddb3bf..fa37f42 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/i18n/en/settings.json b/public/i18n/en/settings.json index 60197be..decdd15 100644 --- a/public/i18n/en/settings.json +++ b/public/i18n/en/settings.json @@ -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" } \ No newline at end of file diff --git a/public/i18n/es/api-errors.json b/public/i18n/es/api-errors.json index 9e26dfe..a4a620b 100644 --- a/public/i18n/es/api-errors.json +++ b/public/i18n/es/api-errors.json @@ -1 +1,22 @@ -{} \ No newline at end of file +{ + "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}}" +} \ No newline at end of file diff --git a/public/i18n/es/settings.json b/public/i18n/es/settings.json index faf4d89..88bb88d 100644 --- a/public/i18n/es/settings.json +++ b/public/i18n/es/settings.json @@ -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" } \ No newline at end of file diff --git a/public/i18n/it/api-errors.json b/public/i18n/it/api-errors.json new file mode 100644 index 0000000..e528c69 --- /dev/null +++ b/public/i18n/it/api-errors.json @@ -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}}" +} \ No newline at end of file diff --git a/public/i18n/it/common.json b/public/i18n/it/common.json new file mode 100644 index 0000000..c42af49 --- /dev/null +++ b/public/i18n/it/common.json @@ -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" +} \ No newline at end of file diff --git a/public/i18n/it/dashboard.json b/public/i18n/it/dashboard.json new file mode 100644 index 0000000..42ef5a2 --- /dev/null +++ b/public/i18n/it/dashboard.json @@ -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" +} \ No newline at end of file diff --git a/public/i18n/it/footer.json b/public/i18n/it/footer.json new file mode 100644 index 0000000..afa3af6 --- /dev/null +++ b/public/i18n/it/footer.json @@ -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" +} \ No newline at end of file diff --git a/public/i18n/it/lists.json b/public/i18n/it/lists.json new file mode 100644 index 0000000..5f1fbed --- /dev/null +++ b/public/i18n/it/lists.json @@ -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" +} \ No newline at end of file diff --git a/public/i18n/it/location.json b/public/i18n/it/location.json new file mode 100644 index 0000000..3482db0 --- /dev/null +++ b/public/i18n/it/location.json @@ -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" +} \ No newline at end of file diff --git a/public/i18n/it/login.json b/public/i18n/it/login.json new file mode 100644 index 0000000..f8110d3 --- /dev/null +++ b/public/i18n/it/login.json @@ -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}}" +} \ No newline at end of file diff --git a/public/i18n/it/query-log.json b/public/i18n/it/query-log.json new file mode 100644 index 0000000..75d4426 --- /dev/null +++ b/public/i18n/it/query-log.json @@ -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)" +} \ No newline at end of file diff --git a/public/i18n/it/settings.json b/public/i18n/it/settings.json new file mode 100644 index 0000000..2bd4822 --- /dev/null +++ b/public/i18n/it/settings.json @@ -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" +} \ No newline at end of file diff --git a/src/components/settings/ConditionalForwardingSettings.js b/src/components/settings/ConditionalForwardingSettings.js new file mode 100644 index 0000000..39daf87 --- /dev/null +++ b/src/components/settings/ConditionalForwardingSettings.js @@ -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 +}) => ( + + + + + + + + onUpdate({ ...settings, routerIp: e.target.value })} + invalid={!isRouterIpValid} + /> + + + + + + onUpdate({ ...settings, domain: e.target.value })} + invalid={!isDomainValid} + /> + + + +); + +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; diff --git a/src/components/settings/DNSInfo.js b/src/components/settings/DNSInfo.js index 7e7c278..3aa5cc6 100644 --- a/src/components/settings/DNSInfo.js +++ b/src/components/settings/DNSInfo.js @@ -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 ? ( + + ) : null; + return ( -
-
-
-            
- {t("Upstream DNS Servers")}:
- {this.state.upstream_dns.map(item => item + "\n")} -
-
-
-
-
-            
- {t("Interface listening behavior")}:{" "} - {this.state.options.listening_type} -
- {t("Forward FQDNs only")}:{" "} - {this.state.options.fqdn_required.toString()} -
- {t("Only forward public reverse lookups")}: - {this.state.options.bogus_priv.toString()} -
- {t("Use DNSSEC")}: {this.state.options.dnssec.toString()} -
-
-
-
-            
- {t("Conditional Forwarding")} -
- {t("Enabled")}:{" "} - {this.state.conditional_forwarding.enabled.toString()} -
- {t("Router IP")}:{" "} - {this.state.conditional_forwarding.router_ip.toString()} -
- {t("Local Domain Name")}:{" "} - {this.state.conditional_forwarding.domain.toString()} -
-
-
-
+
+ {alert} + + +

{t("Upstream DNS Servers")}

+ + + +

{t("Conditional Forwarding")}

+ +

{t("DNS Options")}

+ + +
+ +
); } } diff --git a/src/components/settings/DnsList.js b/src/components/settings/DnsList.js new file mode 100644 index 0000000..7beaee2 --- /dev/null +++ b/src/components/settings/DnsList.js @@ -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 }) => ( + + {upstreams.map(upstream => ( + onRemove(upstream)} + address={upstream} + /> + ))} + isAddressValid(address, upstreams)} + upstreams={upstreams} + /> + +); + +DnsList.propTypes = { + upstreams: PropTypes.arrayOf(PropTypes.string).isRequired, + onAdd: PropTypes.func.isRequired, + onRemove: PropTypes.func.isRequired +}; + +export default DnsList; diff --git a/src/components/settings/DnsListItem.js b/src/components/settings/DnsListItem.js new file mode 100644 index 0000000..276c50c --- /dev/null +++ b/src/components/settings/DnsListItem.js @@ -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 }) => ( + + {address} + + +); + +DnsListItem.propTypes = { + address: PropTypes.string.isRequired, + onRemove: PropTypes.func.isRequired +}; + +export default DnsListItem; diff --git a/src/components/settings/DnsListNewItem.js b/src/components/settings/DnsListNewItem.js new file mode 100644 index 0000000..23d8f7b --- /dev/null +++ b/src/components/settings/DnsListNewItem.js @@ -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 ( + + + 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 + /> + + + + + + + ); + } +} + +export default translate("settings")(DnsListNewItem); diff --git a/src/components/settings/DnsOptionSettings.js b/src/components/settings/DnsOptionSettings.js new file mode 100644 index 0000000..782d58f --- /dev/null +++ b/src/components/settings/DnsOptionSettings.js @@ -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 }) => ( + + + + + + onUpdate({ ...settings, listeningType: e.target.value }) + } + > + + + + + + + + + + + + + + + + +); + +DnsOptionSettings.propTypes = { + settings: PropTypes.object.isRequired, + onUpdate: PropTypes.func.isRequired, + t: PropTypes.func.isRequired +}; + +export default DnsOptionSettings; diff --git a/src/components/settings/preconfiguredUpstreams.js b/src/components/settings/preconfiguredUpstreams.js new file mode 100644 index 0000000..817e285 --- /dev/null +++ b/src/components/settings/preconfiguredUpstreams.js @@ -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; + } +); diff --git a/src/index.js b/src/index.js index 828ba91..c389036 100644 --- a/src/index.js +++ b/src/index.js @@ -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"; diff --git a/src/scss/_custom.scss b/src/scss/_custom.scss index 5548db6..2b3feb9 100644 --- a/src/scss/_custom.scss +++ b/src/scss/_custom.scss @@ -182,3 +182,7 @@ .form-check-label { margin-bottom: 10px; } + +.form-check-input { + margin-top: 0.2rem; +} diff --git a/src/util/api.js b/src/util/api.js index 6e077fb..1057c97 100644 --- a/src/util/api.js +++ b/src/util/api.js @@ -91,5 +91,8 @@ export default { }, updateDHCPInfo(settings) { return http.put("settings/dhcp", settings); + }, + updateDNSInfo(settings) { + return http.put("settings/dns", settings); } }; diff --git a/src/validate.js b/src/validate.js index 2bb61ce..dfcffad 100644 --- a/src/validate.js +++ b/src/validate.js @@ -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])) + ); +} diff --git a/src/validate.test.js b/src/validate.test.js index 17eda8b..4c46022 100644 --- a/src/validate.test.js +++ b/src/validate.test.js @@ -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); + }); + }); });