diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 54becca5..b4b7bd61 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -12,6 +12,9 @@ Contents: * Updating * Get version command * Update command +* TLS + * API: Get TLS configuration + * API: Set TLS configuration * Device Names and Per-client Settings * Per-client settings * Get list of clients @@ -515,6 +518,66 @@ Response: 200 OK +## TLS + + +### API: Get TLS configuration + +Request: + + GET /control/tls/status + +Response: + + 200 OK + + { + "enabled":true, + "server_name":"...", + "port_https":443, + "port_dns_over_tls":853, + "certificate_chain":"...", + "private_key":"...", + "certificate_path":"...", + "private_key_path":"..." + + "subject":"CN=...", + "issuer":"CN=...", + "not_before":"2019-03-19T08:23:45Z", + "not_after":"2029-03-16T08:23:45Z", + "dns_names":null, + "key_type":"RSA", + "valid_cert":true, + "valid_key":true, + "valid_chain":false, + "valid_pair":true, + "warning_validation":"Your certificate does not verify: x509: certificate signed by unknown authority" + } + + +### API: Set TLS configuration + +Request: + + POST /control/tls/configure + + { + "enabled":true, + "server_name":"hostname", + "force_https":false, + "port_https":443, + "port_dns_over_tls":853, + "certificate_chain":"...", + "private_key":"...", + "certificate_path":"...", // if set, certificate_chain must be empty + "private_key_path":"..." // if set, private_key must be empty + } + +Response: + + 200 OK + + ## Device Names and Per-client Settings When a client requests information from DNS server, he's identified by IP address. diff --git a/README.md b/README.md index 3f75216c..82b8cf98 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ If you run into any problem or have a suggestion, head to [this page](https://gi If you want to help with AdGuard Home translations, please learn more about translating AdGuard products here: https://kb.adguard.com/en/general/adguard-translations -Here is a link to AdGuard Home project: https://crowdin.com/project/adguard-applications +Here is a link to AdGuard Home project: https://crowdin.com/project/adguard-applications/en#/adguard-home ## Acknowledgments diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index b08147eb..b6401284 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -353,5 +353,11 @@ "blocked_services_global": "Use global blocked services", "blocked_service": "Blocked service", "block_all": "Block all", - "unblock_all": "Unblock all" -} \ No newline at end of file + "unblock_all": "Unblock all", + "encryption_certificate_path": "Certificate path", + "encryption_private_key_path": "Private key path", + "encryption_certificates_source_path": "Set a certificates file path", + "encryption_certificates_source_content":"Paste the certificates contents", + "encryption_key_source_path": "Set a private key file", + "encryption_key_source_content": "Paste the private key contents" +} diff --git a/client/src/components/Settings/Encryption/CertificateStatus.js b/client/src/components/Settings/Encryption/CertificateStatus.js new file mode 100644 index 00000000..1ecc2742 --- /dev/null +++ b/client/src/components/Settings/Encryption/CertificateStatus.js @@ -0,0 +1,71 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { withNamespaces, Trans } from 'react-i18next'; +import format from 'date-fns/format'; + +import { EMPTY_DATE } from '../../../helpers/constants'; + +const CertificateStatus = ({ + validChain, + validCert, + subject, + issuer, + notAfter, + dnsNames, +}) => ( + +
+ encryption_status: +
+ +
+); + +CertificateStatus.propTypes = { + validChain: PropTypes.bool.isRequired, + validCert: PropTypes.bool.isRequired, + subject: PropTypes.string, + issuer: PropTypes.string, + notAfter: PropTypes.string, + dnsNames: PropTypes.string, +}; + +export default withNamespaces()(CertificateStatus); diff --git a/client/src/components/Settings/Encryption/Form.js b/client/src/components/Settings/Encryption/Form.js index 94e9923c..058cf918 100644 --- a/client/src/components/Settings/Encryption/Form.js +++ b/client/src/components/Settings/Encryption/Form.js @@ -1,14 +1,22 @@ -import React, { Fragment } from 'react'; +import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { Field, reduxForm, formValueSelector } from 'redux-form'; import { Trans, withNamespaces } from 'react-i18next'; import flow from 'lodash/flow'; -import format from 'date-fns/format'; -import { renderField, renderSelectField, toNumber, port, portTLS, isSafePort } from '../../../helpers/form'; -import { EMPTY_DATE } from '../../../helpers/constants'; +import { + renderField, + renderSelectField, + renderRadioField, + toNumber, + port, + portTLS, + isSafePort, +} from '../../../helpers/form'; import i18n from '../../../i18n'; +import KeyStatus from './KeyStatus'; +import CertificateStatus from './CertificateStatus'; const validate = (values) => { const errors = {}; @@ -27,6 +35,8 @@ const clearFields = (change, setTlsConfig, t) => { const fields = { private_key: '', certificate_chain: '', + private_key_path: '', + certificate_path: '', port_https: 443, port_dns_over_tls: 853, server_name: '', @@ -48,6 +58,8 @@ let Form = (props) => { isEnabled, certificateChain, privateKey, + certificatePath, + privateKeyPath, change, invalid, submitting, @@ -64,6 +76,8 @@ let Form = (props) => { subject, warning_validation, setTlsConfig, + certificateSource, + privateKeySource, } = props; const isSavingDisabled = @@ -71,10 +85,9 @@ let Form = (props) => { submitting || processingConfig || processingValidate || - (isEnabled && (!privateKey || !certificateChain)) || - (privateKey && !valid_key) || - (certificateChain && !valid_cert) || - (privateKey && certificateChain && !valid_pair); + !valid_key || + !valid_cert || + !valid_pair; return (
@@ -182,7 +195,7 @@ let Form = (props) => {
- -
- {certificateChain && ( - -
- encryption_status: -
-
    -
  • - {valid_chain ? ( - encryption_chain_valid - ) : ( - encryption_chain_invalid - )} -
  • - {valid_cert && ( - - {subject && ( -
  • - encryption_subject:  - {subject} -
  • - )} - {issuer && ( -
  • - encryption_issuer:  - {issuer} -
  • - )} - {not_after && not_after !== EMPTY_DATE && ( -
  • - encryption_expire:  - {format(not_after, 'YYYY-MM-DD HH:mm:ss')} -
  • - )} - {dns_names && ( -
  • - encryption_hostnames:  - {dns_names} -
  • - )} -
    - )} -
-
- )} + +
+
+ + +
+ + {certificateSource === 'content' && ( + + )} + {certificateSource === 'path' && ( + + )} +
+
+ {(certificateChain || certificatePath) && ( + + )}
-
+
- -
- {privateKey && ( - -
- encryption_status: -
-
    -
  • - {valid_key ? ( - - encryption_key_valid - - ) : ( - - encryption_key_invalid - - )} -
  • -
-
- )} + +
+
+ + +
+ + {privateKeySource === 'content' && ( + + )} + {privateKeySource === 'path' && ( + + )} +
+
+ {(privateKey || privateKeyPath) && ( + + )}
{warning_validation && ( @@ -334,6 +366,8 @@ Form.propTypes = { isEnabled: PropTypes.bool.isRequired, certificateChain: PropTypes.string.isRequired, privateKey: PropTypes.string.isRequired, + certificatePath: PropTypes.string.isRequired, + privateKeyPath: PropTypes.string.isRequired, change: PropTypes.func.isRequired, submitting: PropTypes.bool.isRequired, invalid: PropTypes.bool.isRequired, @@ -353,6 +387,8 @@ Form.propTypes = { subject: PropTypes.string, t: PropTypes.func.isRequired, setTlsConfig: PropTypes.func.isRequired, + certificateSource: PropTypes.string, + privateKeySource: PropTypes.string, }; const selector = formValueSelector('encryptionForm'); @@ -361,10 +397,18 @@ Form = connect((state) => { const isEnabled = selector(state, 'enabled'); const certificateChain = selector(state, 'certificate_chain'); const privateKey = selector(state, 'private_key'); + const certificatePath = selector(state, 'certificate_path'); + const privateKeyPath = selector(state, 'private_key_path'); + const certificateSource = selector(state, 'certificate_source'); + const privateKeySource = selector(state, 'key_source'); return { isEnabled, certificateChain, privateKey, + certificatePath, + privateKeyPath, + certificateSource, + privateKeySource, }; })(Form); diff --git a/client/src/components/Settings/Encryption/KeyStatus.js b/client/src/components/Settings/Encryption/KeyStatus.js new file mode 100644 index 00000000..08ae1df0 --- /dev/null +++ b/client/src/components/Settings/Encryption/KeyStatus.js @@ -0,0 +1,31 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { withNamespaces, Trans } from 'react-i18next'; + +const KeyStatus = ({ validKey, keyType }) => ( + +
+ encryption_status: +
+
    +
  • + {validKey ? ( + + encryption_key_valid + + ) : ( + + encryption_key_invalid + + )} +
  • +
+
+); + +KeyStatus.propTypes = { + validKey: PropTypes.bool.isRequired, + keyType: PropTypes.string.isRequired, +}; + +export default withNamespaces()(KeyStatus); diff --git a/client/src/components/Settings/Encryption/index.js b/client/src/components/Settings/Encryption/index.js index a8075dc1..06a3b73a 100644 --- a/client/src/components/Settings/Encryption/index.js +++ b/client/src/components/Settings/Encryption/index.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { withNamespaces } from 'react-i18next'; import debounce from 'lodash/debounce'; -import { DEBOUNCE_TIMEOUT } from '../../../helpers/constants'; +import { DEBOUNCE_TIMEOUT, ENCRYPTION_SOURCE } from '../../../helpers/constants'; import Form from './Form'; import Card from '../../ui/Card'; import PageTitle from '../../ui/PageTitle'; @@ -19,13 +19,45 @@ class Encryption extends Component { } handleFormSubmit = (values) => { - this.props.setTlsConfig(values); + const submitValues = this.getSubmitValues(values); + this.props.setTlsConfig(submitValues); }; handleFormChange = debounce((values) => { - this.props.validateTlsConfig(values); + const submitValues = this.getSubmitValues(values); + this.props.validateTlsConfig(submitValues); }, DEBOUNCE_TIMEOUT); + getInitialValues = (data) => { + const { certificate_chain, private_key } = data; + const certificate_source = certificate_chain ? 'content' : 'path'; + const key_source = private_key ? 'content' : 'path'; + + return { + ...data, + certificate_source, + key_source, + }; + }; + + getSubmitValues = (values) => { + const { certificate_source, key_source, ...config } = values; + + if (certificate_source === ENCRYPTION_SOURCE.PATH) { + config.certificate_chain = ''; + } else { + config.certificate_path = ''; + } + + if (values.key_source === ENCRYPTION_SOURCE.PATH) { + config.private_key = ''; + } else { + config.private_key_path = ''; + } + + return config; + }; + render() { const { encryption, t } = this.props; const { @@ -36,8 +68,22 @@ class Encryption extends Component { port_dns_over_tls, certificate_chain, private_key, + certificate_path, + private_key_path, } = encryption; + const initialValues = this.getInitialValues({ + enabled, + server_name, + force_https, + port_https, + port_dns_over_tls, + certificate_chain, + private_key, + certificate_path, + private_key_path, + }); + return (
@@ -49,15 +95,7 @@ class Encryption extends Component { bodyType="card-body box-body--settings" >