Merge: Set SSL certificate & privatekey from file
Close #634 * commit 'c05917bce0a4c823a4a3f58973e09f7dd8dae877': - client: remove unused strings * README: update link to the crowdin + client: handle fields for certificate path and private key path * openapi: update "TlsConfig" + config: add certificate_path, private_key_path
This commit is contained in:
commit
a9524448b1
|
@ -12,6 +12,9 @@ Contents:
|
||||||
* Updating
|
* Updating
|
||||||
* Get version command
|
* Get version command
|
||||||
* Update command
|
* Update command
|
||||||
|
* TLS
|
||||||
|
* API: Get TLS configuration
|
||||||
|
* API: Set TLS configuration
|
||||||
* Device Names and Per-client Settings
|
* Device Names and Per-client Settings
|
||||||
* Per-client settings
|
* Per-client settings
|
||||||
* Get list of clients
|
* Get list of clients
|
||||||
|
@ -515,6 +518,66 @@ Response:
|
||||||
200 OK
|
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
|
## Device Names and Per-client Settings
|
||||||
|
|
||||||
When a client requests information from DNS server, he's identified by IP address.
|
When a client requests information from DNS server, he's identified by IP address.
|
||||||
|
|
|
@ -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
|
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
|
||||||
|
|
||||||
<a id="acknowledgments"></a>
|
<a id="acknowledgments"></a>
|
||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
|
@ -353,5 +353,11 @@
|
||||||
"blocked_services_global": "Use global blocked services",
|
"blocked_services_global": "Use global blocked services",
|
||||||
"blocked_service": "Blocked service",
|
"blocked_service": "Blocked service",
|
||||||
"block_all": "Block all",
|
"block_all": "Block all",
|
||||||
"unblock_all": "Unblock all"
|
"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"
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
}) => (
|
||||||
|
<Fragment>
|
||||||
|
<div className="form__label form__label--bold">
|
||||||
|
<Trans>encryption_status</Trans>:
|
||||||
|
</div>
|
||||||
|
<ul className="encryption__list">
|
||||||
|
<li
|
||||||
|
className={validChain ? 'text-success' : 'text-danger'}
|
||||||
|
>
|
||||||
|
{validChain ? (
|
||||||
|
<Trans>encryption_chain_valid</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>encryption_chain_invalid</Trans>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
{validCert && (
|
||||||
|
<Fragment>
|
||||||
|
{subject && (
|
||||||
|
<li>
|
||||||
|
<Trans>encryption_subject</Trans>:
|
||||||
|
{subject}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{issuer && (
|
||||||
|
<li>
|
||||||
|
<Trans>encryption_issuer</Trans>:
|
||||||
|
{issuer}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{notAfter && notAfter !== EMPTY_DATE && (
|
||||||
|
<li>
|
||||||
|
<Trans>encryption_expire</Trans>:
|
||||||
|
{format(notAfter, 'YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{dnsNames && (
|
||||||
|
<li>
|
||||||
|
<Trans>encryption_hostnames</Trans>:
|
||||||
|
{dnsNames}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
|
@ -1,14 +1,22 @@
|
||||||
import React, { Fragment } from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
||||||
import { Trans, withNamespaces } from 'react-i18next';
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
import flow from 'lodash/flow';
|
import flow from 'lodash/flow';
|
||||||
import format from 'date-fns/format';
|
|
||||||
|
|
||||||
import { renderField, renderSelectField, toNumber, port, portTLS, isSafePort } from '../../../helpers/form';
|
import {
|
||||||
import { EMPTY_DATE } from '../../../helpers/constants';
|
renderField,
|
||||||
|
renderSelectField,
|
||||||
|
renderRadioField,
|
||||||
|
toNumber,
|
||||||
|
port,
|
||||||
|
portTLS,
|
||||||
|
isSafePort,
|
||||||
|
} from '../../../helpers/form';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
|
import KeyStatus from './KeyStatus';
|
||||||
|
import CertificateStatus from './CertificateStatus';
|
||||||
|
|
||||||
const validate = (values) => {
|
const validate = (values) => {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
|
@ -27,6 +35,8 @@ const clearFields = (change, setTlsConfig, t) => {
|
||||||
const fields = {
|
const fields = {
|
||||||
private_key: '',
|
private_key: '',
|
||||||
certificate_chain: '',
|
certificate_chain: '',
|
||||||
|
private_key_path: '',
|
||||||
|
certificate_path: '',
|
||||||
port_https: 443,
|
port_https: 443,
|
||||||
port_dns_over_tls: 853,
|
port_dns_over_tls: 853,
|
||||||
server_name: '',
|
server_name: '',
|
||||||
|
@ -48,6 +58,8 @@ let Form = (props) => {
|
||||||
isEnabled,
|
isEnabled,
|
||||||
certificateChain,
|
certificateChain,
|
||||||
privateKey,
|
privateKey,
|
||||||
|
certificatePath,
|
||||||
|
privateKeyPath,
|
||||||
change,
|
change,
|
||||||
invalid,
|
invalid,
|
||||||
submitting,
|
submitting,
|
||||||
|
@ -64,6 +76,8 @@ let Form = (props) => {
|
||||||
subject,
|
subject,
|
||||||
warning_validation,
|
warning_validation,
|
||||||
setTlsConfig,
|
setTlsConfig,
|
||||||
|
certificateSource,
|
||||||
|
privateKeySource,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isSavingDisabled =
|
const isSavingDisabled =
|
||||||
|
@ -71,10 +85,9 @@ let Form = (props) => {
|
||||||
submitting ||
|
submitting ||
|
||||||
processingConfig ||
|
processingConfig ||
|
||||||
processingValidate ||
|
processingValidate ||
|
||||||
(isEnabled && (!privateKey || !certificateChain)) ||
|
!valid_key ||
|
||||||
(privateKey && !valid_key) ||
|
!valid_cert ||
|
||||||
(certificateChain && !valid_cert) ||
|
!valid_pair;
|
||||||
(privateKey && certificateChain && !valid_pair);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
|
@ -182,7 +195,7 @@ let Form = (props) => {
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
<label
|
<label
|
||||||
className="form__label form__label--bold"
|
className="form__label form__label--with-desc form__label--bold"
|
||||||
htmlFor="certificate_chain"
|
htmlFor="certificate_chain"
|
||||||
>
|
>
|
||||||
<Trans>encryption_certificates</Trans>
|
<Trans>encryption_certificates</Trans>
|
||||||
|
@ -199,105 +212,124 @@ let Form = (props) => {
|
||||||
encryption_certificates_desc
|
encryption_certificates_desc
|
||||||
</Trans>
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
<Field
|
|
||||||
id="certificate_chain"
|
<div className="form__inline mb-2">
|
||||||
name="certificate_chain"
|
<div className="custom-controls-stacked">
|
||||||
component="textarea"
|
<Field
|
||||||
type="text"
|
name="certificate_source"
|
||||||
className="form-control form-control--textarea"
|
component={renderRadioField}
|
||||||
placeholder={t('encryption_certificates_input')}
|
type="radio"
|
||||||
onChange={handleChange}
|
className="form-control mr-2"
|
||||||
disabled={!isEnabled}
|
value="path"
|
||||||
/>
|
placeholder={t('encryption_certificates_source_path')}
|
||||||
<div className="form__status">
|
/>
|
||||||
{certificateChain && (
|
<Field
|
||||||
<Fragment>
|
name="certificate_source"
|
||||||
<div className="form__label form__label--bold">
|
component={renderRadioField}
|
||||||
<Trans>encryption_status</Trans>:
|
type="radio"
|
||||||
</div>
|
className="form-control mr-2"
|
||||||
<ul className="encryption__list">
|
value="content"
|
||||||
<li
|
placeholder={t('encryption_certificates_source_content')}
|
||||||
className={valid_chain ? 'text-success' : 'text-danger'}
|
/>
|
||||||
>
|
</div>
|
||||||
{valid_chain ? (
|
|
||||||
<Trans>encryption_chain_valid</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>encryption_chain_invalid</Trans>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
{valid_cert && (
|
|
||||||
<Fragment>
|
|
||||||
{subject && (
|
|
||||||
<li>
|
|
||||||
<Trans>encryption_subject</Trans>:
|
|
||||||
{subject}
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{issuer && (
|
|
||||||
<li>
|
|
||||||
<Trans>encryption_issuer</Trans>:
|
|
||||||
{issuer}
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{not_after && not_after !== EMPTY_DATE && (
|
|
||||||
<li>
|
|
||||||
<Trans>encryption_expire</Trans>:
|
|
||||||
{format(not_after, 'YYYY-MM-DD HH:mm:ss')}
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{dns_names && (
|
|
||||||
<li>
|
|
||||||
<Trans>encryption_hostnames</Trans>:
|
|
||||||
{dns_names}
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{certificateSource === 'content' && (
|
||||||
|
<Field
|
||||||
|
id="certificate_chain"
|
||||||
|
name="certificate_chain"
|
||||||
|
component="textarea"
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control--textarea"
|
||||||
|
placeholder={t('encryption_certificates_input')}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={!isEnabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{certificateSource === 'path' && (
|
||||||
|
<Field
|
||||||
|
id="certificate_path"
|
||||||
|
name="certificate_path"
|
||||||
|
component={renderField}
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder={t('encryption_certificate_path')}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={!isEnabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="form__status">
|
||||||
|
{(certificateChain || certificatePath) && (
|
||||||
|
<CertificateStatus
|
||||||
|
validChain={valid_chain}
|
||||||
|
validCert={valid_cert}
|
||||||
|
subject={subject}
|
||||||
|
issuer={issuer}
|
||||||
|
notAfter={not_after}
|
||||||
|
dnsNames={dns_names}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings mt-3">
|
||||||
<label className="form__label form__label--bold" htmlFor="private_key">
|
<label className="form__label form__label--bold" htmlFor="private_key">
|
||||||
<Trans>encryption_key</Trans>
|
<Trans>encryption_key</Trans>
|
||||||
</label>
|
</label>
|
||||||
<Field
|
|
||||||
id="private_key"
|
<div className="form__inline mb-2">
|
||||||
name="private_key"
|
<div className="custom-controls-stacked">
|
||||||
component="textarea"
|
<Field
|
||||||
type="text"
|
name="key_source"
|
||||||
className="form-control form-control--textarea"
|
component={renderRadioField}
|
||||||
placeholder={t('encryption_key_input')}
|
type="radio"
|
||||||
onChange={handleChange}
|
className="form-control mr-2"
|
||||||
disabled={!isEnabled}
|
value="path"
|
||||||
/>
|
placeholder={t('encryption_key_source_path')}
|
||||||
<div className="form__status">
|
/>
|
||||||
{privateKey && (
|
<Field
|
||||||
<Fragment>
|
name="key_source"
|
||||||
<div className="form__label form__label--bold">
|
component={renderRadioField}
|
||||||
<Trans>encryption_status</Trans>:
|
type="radio"
|
||||||
</div>
|
className="form-control mr-2"
|
||||||
<ul className="encryption__list">
|
value="content"
|
||||||
<li className={valid_key ? 'text-success' : 'text-danger'}>
|
placeholder={t('encryption_key_source_content')}
|
||||||
{valid_key ? (
|
/>
|
||||||
<Trans values={{ type: key_type }}>
|
</div>
|
||||||
encryption_key_valid
|
|
||||||
</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans values={{ type: key_type }}>
|
|
||||||
encryption_key_invalid
|
|
||||||
</Trans>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{privateKeySource === 'content' && (
|
||||||
|
<Field
|
||||||
|
id="private_key"
|
||||||
|
name="private_key"
|
||||||
|
component="textarea"
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control--textarea"
|
||||||
|
placeholder={t('encryption_key_input')}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={!isEnabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{privateKeySource === 'path' && (
|
||||||
|
<Field
|
||||||
|
id="private_key_path"
|
||||||
|
name="private_key_path"
|
||||||
|
component={renderField}
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder={t('encryption_private_key_path')}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={!isEnabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="form__status">
|
||||||
|
{(privateKey || privateKeyPath) && (
|
||||||
|
<KeyStatus validKey={valid_key} keyType={key_type} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{warning_validation && (
|
{warning_validation && (
|
||||||
|
@ -334,6 +366,8 @@ Form.propTypes = {
|
||||||
isEnabled: PropTypes.bool.isRequired,
|
isEnabled: PropTypes.bool.isRequired,
|
||||||
certificateChain: PropTypes.string.isRequired,
|
certificateChain: PropTypes.string.isRequired,
|
||||||
privateKey: PropTypes.string.isRequired,
|
privateKey: PropTypes.string.isRequired,
|
||||||
|
certificatePath: PropTypes.string.isRequired,
|
||||||
|
privateKeyPath: PropTypes.string.isRequired,
|
||||||
change: PropTypes.func.isRequired,
|
change: PropTypes.func.isRequired,
|
||||||
submitting: PropTypes.bool.isRequired,
|
submitting: PropTypes.bool.isRequired,
|
||||||
invalid: PropTypes.bool.isRequired,
|
invalid: PropTypes.bool.isRequired,
|
||||||
|
@ -353,6 +387,8 @@ Form.propTypes = {
|
||||||
subject: PropTypes.string,
|
subject: PropTypes.string,
|
||||||
t: PropTypes.func.isRequired,
|
t: PropTypes.func.isRequired,
|
||||||
setTlsConfig: PropTypes.func.isRequired,
|
setTlsConfig: PropTypes.func.isRequired,
|
||||||
|
certificateSource: PropTypes.string,
|
||||||
|
privateKeySource: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const selector = formValueSelector('encryptionForm');
|
const selector = formValueSelector('encryptionForm');
|
||||||
|
@ -361,10 +397,18 @@ Form = connect((state) => {
|
||||||
const isEnabled = selector(state, 'enabled');
|
const isEnabled = selector(state, 'enabled');
|
||||||
const certificateChain = selector(state, 'certificate_chain');
|
const certificateChain = selector(state, 'certificate_chain');
|
||||||
const privateKey = selector(state, 'private_key');
|
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 {
|
return {
|
||||||
isEnabled,
|
isEnabled,
|
||||||
certificateChain,
|
certificateChain,
|
||||||
privateKey,
|
privateKey,
|
||||||
|
certificatePath,
|
||||||
|
privateKeyPath,
|
||||||
|
certificateSource,
|
||||||
|
privateKeySource,
|
||||||
};
|
};
|
||||||
})(Form);
|
})(Form);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { withNamespaces, Trans } from 'react-i18next';
|
||||||
|
|
||||||
|
const KeyStatus = ({ validKey, keyType }) => (
|
||||||
|
<Fragment>
|
||||||
|
<div className="form__label form__label--bold">
|
||||||
|
<Trans>encryption_status</Trans>:
|
||||||
|
</div>
|
||||||
|
<ul className="encryption__list">
|
||||||
|
<li className={validKey ? 'text-success' : 'text-danger'}>
|
||||||
|
{validKey ? (
|
||||||
|
<Trans values={{ type: keyType }}>
|
||||||
|
encryption_key_valid
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans values={{ type: keyType }}>
|
||||||
|
encryption_key_invalid
|
||||||
|
</Trans>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
KeyStatus.propTypes = {
|
||||||
|
validKey: PropTypes.bool.isRequired,
|
||||||
|
keyType: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withNamespaces()(KeyStatus);
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||||
import { withNamespaces } from 'react-i18next';
|
import { withNamespaces } from 'react-i18next';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
import { DEBOUNCE_TIMEOUT } from '../../../helpers/constants';
|
import { DEBOUNCE_TIMEOUT, ENCRYPTION_SOURCE } from '../../../helpers/constants';
|
||||||
import Form from './Form';
|
import Form from './Form';
|
||||||
import Card from '../../ui/Card';
|
import Card from '../../ui/Card';
|
||||||
import PageTitle from '../../ui/PageTitle';
|
import PageTitle from '../../ui/PageTitle';
|
||||||
|
@ -19,13 +19,45 @@ class Encryption extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFormSubmit = (values) => {
|
handleFormSubmit = (values) => {
|
||||||
this.props.setTlsConfig(values);
|
const submitValues = this.getSubmitValues(values);
|
||||||
|
this.props.setTlsConfig(submitValues);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleFormChange = debounce((values) => {
|
handleFormChange = debounce((values) => {
|
||||||
this.props.validateTlsConfig(values);
|
const submitValues = this.getSubmitValues(values);
|
||||||
|
this.props.validateTlsConfig(submitValues);
|
||||||
}, DEBOUNCE_TIMEOUT);
|
}, 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() {
|
render() {
|
||||||
const { encryption, t } = this.props;
|
const { encryption, t } = this.props;
|
||||||
const {
|
const {
|
||||||
|
@ -36,8 +68,22 @@ class Encryption extends Component {
|
||||||
port_dns_over_tls,
|
port_dns_over_tls,
|
||||||
certificate_chain,
|
certificate_chain,
|
||||||
private_key,
|
private_key,
|
||||||
|
certificate_path,
|
||||||
|
private_key_path,
|
||||||
} = encryption;
|
} = 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 (
|
return (
|
||||||
<div className="encryption">
|
<div className="encryption">
|
||||||
<PageTitle title={t('encryption_settings')} />
|
<PageTitle title={t('encryption_settings')} />
|
||||||
|
@ -49,15 +95,7 @@ class Encryption extends Component {
|
||||||
bodyType="card-body box-body--settings"
|
bodyType="card-body box-body--settings"
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
initialValues={{
|
initialValues={initialValues}
|
||||||
enabled,
|
|
||||||
server_name,
|
|
||||||
force_https,
|
|
||||||
port_https,
|
|
||||||
port_dns_over_tls,
|
|
||||||
certificate_chain,
|
|
||||||
private_key,
|
|
||||||
}}
|
|
||||||
onSubmit={this.handleFormSubmit}
|
onSubmit={this.handleFormSubmit}
|
||||||
onChange={this.handleFormChange}
|
onChange={this.handleFormChange}
|
||||||
setTlsConfig={this.props.setTlsConfig}
|
setTlsConfig={this.props.setTlsConfig}
|
||||||
|
|
|
@ -248,3 +248,8 @@ export const SERVICES = [
|
||||||
name: 'TikTok',
|
name: 'TikTok',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const ENCRYPTION_SOURCE = {
|
||||||
|
PATH: 'path',
|
||||||
|
CONTENT: 'content',
|
||||||
|
};
|
||||||
|
|
|
@ -77,6 +77,8 @@ const encryption = handleActions({
|
||||||
private_key: '',
|
private_key: '',
|
||||||
server_name: '',
|
server_name: '',
|
||||||
warning_validation: '',
|
warning_validation: '',
|
||||||
|
certificate_path: '',
|
||||||
|
private_key_path: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
export default encryption;
|
export default encryption;
|
||||||
|
|
|
@ -108,6 +108,12 @@ type TLSConfig struct {
|
||||||
TLSListenAddr *net.TCPAddr `yaml:"-" json:"-"`
|
TLSListenAddr *net.TCPAddr `yaml:"-" json:"-"`
|
||||||
CertificateChain string `yaml:"certificate_chain" json:"certificate_chain"` // PEM-encoded certificates chain
|
CertificateChain string `yaml:"certificate_chain" json:"certificate_chain"` // PEM-encoded certificates chain
|
||||||
PrivateKey string `yaml:"private_key" json:"private_key"` // PEM-encoded private key
|
PrivateKey string `yaml:"private_key" json:"private_key"` // PEM-encoded private key
|
||||||
|
|
||||||
|
CertificatePath string `yaml:"certificate_path" json:"certificate_path"` // certificate file name
|
||||||
|
PrivateKeyPath string `yaml:"private_key_path" json:"private_key_path"` // private key file name
|
||||||
|
|
||||||
|
CertificateChainData []byte `yaml:"-" json:"-"`
|
||||||
|
PrivateKeyData []byte `yaml:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerConfig represents server configuration.
|
// ServerConfig represents server configuration.
|
||||||
|
@ -216,9 +222,9 @@ func (s *Server) startInternal(config *ServerConfig) error {
|
||||||
|
|
||||||
convertArrayToMap(&s.BlockedHosts, s.conf.BlockedHosts)
|
convertArrayToMap(&s.BlockedHosts, s.conf.BlockedHosts)
|
||||||
|
|
||||||
if s.conf.TLSListenAddr != nil && s.conf.CertificateChain != "" && s.conf.PrivateKey != "" {
|
if s.conf.TLSListenAddr != nil && len(s.conf.CertificateChainData) != 0 && len(s.conf.PrivateKeyData) != 0 {
|
||||||
proxyConfig.TLSListenAddr = s.conf.TLSListenAddr
|
proxyConfig.TLSListenAddr = s.conf.TLSListenAddr
|
||||||
keypair, err := tls.X509KeyPair([]byte(s.conf.CertificateChain), []byte(s.conf.PrivateKey))
|
keypair, err := tls.X509KeyPair(s.conf.CertificateChainData, s.conf.PrivateKeyData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.Decorate(err, "Failed to parse TLS keypair")
|
return errorx.Decorate(err, "Failed to parse TLS keypair")
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,9 +118,9 @@ func TestDotServer(t *testing.T) {
|
||||||
defer removeDataDir(t)
|
defer removeDataDir(t)
|
||||||
|
|
||||||
s.conf.TLSConfig = TLSConfig{
|
s.conf.TLSConfig = TLSConfig{
|
||||||
TLSListenAddr: &net.TCPAddr{Port: 0},
|
TLSListenAddr: &net.TCPAddr{Port: 0},
|
||||||
CertificateChain: string(certPem),
|
CertificateChainData: certPem,
|
||||||
PrivateKey: string(keyPem),
|
PrivateKeyData: keyPem,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starting the server
|
// Starting the server
|
||||||
|
|
|
@ -279,6 +279,12 @@ func parseConfig() error {
|
||||||
}
|
}
|
||||||
config.Clients = nil
|
config.Clients = nil
|
||||||
|
|
||||||
|
status := tlsConfigStatus{}
|
||||||
|
if !tlsLoadConfig(&config.TLS, &status) {
|
||||||
|
log.Error("%s", status.WarningValidation)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Deduplicate filters
|
// Deduplicate filters
|
||||||
deduplicateFilters()
|
deduplicateFilters()
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -23,6 +24,41 @@ import (
|
||||||
"github.com/joomcode/errorx"
|
"github.com/joomcode/errorx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Set certificate and private key data
|
||||||
|
func tlsLoadConfig(tls *tlsConfig, status *tlsConfigStatus) bool {
|
||||||
|
tls.CertificateChainData = []byte(tls.CertificateChain)
|
||||||
|
tls.PrivateKeyData = []byte(tls.PrivateKey)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if tls.CertificatePath != "" {
|
||||||
|
if tls.CertificateChain != "" {
|
||||||
|
status.WarningValidation = "certificate data and file can't be set together"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
tls.CertificateChainData, err = ioutil.ReadFile(tls.CertificatePath)
|
||||||
|
if err != nil {
|
||||||
|
status.WarningValidation = err.Error()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
status.ValidCert = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if tls.PrivateKeyPath != "" {
|
||||||
|
if tls.PrivateKey != "" {
|
||||||
|
status.WarningValidation = "private key data and file can't be set together"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
tls.PrivateKeyData, err = ioutil.ReadFile(tls.PrivateKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
status.WarningValidation = err.Error()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
status.ValidKey = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterTLSHandlers registers HTTP handlers for TLS configuration
|
// RegisterTLSHandlers registers HTTP handlers for TLS configuration
|
||||||
func RegisterTLSHandlers() {
|
func RegisterTLSHandlers() {
|
||||||
httpRegister(http.MethodGet, "/control/tls/status", handleTLSStatus)
|
httpRegister(http.MethodGet, "/control/tls/status", handleTLSStatus)
|
||||||
|
@ -55,7 +91,12 @@ func handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data.tlsConfigStatus = validateCertificates(data.CertificateChain, data.PrivateKey, data.ServerName)
|
status := tlsConfigStatus{}
|
||||||
|
if tlsLoadConfig(&data, &status) {
|
||||||
|
status = validateCertificates(string(data.CertificateChainData), string(data.PrivateKeyData), data.ServerName)
|
||||||
|
}
|
||||||
|
data.tlsConfigStatus = status
|
||||||
|
|
||||||
marshalTLS(w, data)
|
marshalTLS(w, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,8 +121,14 @@ func handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
status := tlsConfigStatus{}
|
||||||
|
if !tlsLoadConfig(&data, &status) {
|
||||||
|
data.tlsConfigStatus = status
|
||||||
|
marshalTLS(w, data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.tlsConfigStatus = validateCertificates(string(data.CertificateChainData), string(data.PrivateKeyData), data.ServerName)
|
||||||
restartHTTPS := false
|
restartHTTPS := false
|
||||||
data.tlsConfigStatus = validateCertificates(data.CertificateChain, data.PrivateKey, data.ServerName)
|
|
||||||
if !reflect.DeepEqual(config.TLS.tlsConfigSettings, data.tlsConfigSettings) {
|
if !reflect.DeepEqual(config.TLS.tlsConfigSettings, data.tlsConfigSettings) {
|
||||||
log.Printf("tls config settings have changed, will restart HTTPS server")
|
log.Printf("tls config settings have changed, will restart HTTPS server")
|
||||||
restartHTTPS = true
|
restartHTTPS = true
|
||||||
|
@ -300,6 +347,9 @@ func unmarshalTLS(r *http.Request) (tlsConfig, error) {
|
||||||
return data, errorx.Decorate(err, "Failed to base64-decode certificate chain")
|
return data, errorx.Decorate(err, "Failed to base64-decode certificate chain")
|
||||||
}
|
}
|
||||||
data.CertificateChain = string(certPEM)
|
data.CertificateChain = string(certPEM)
|
||||||
|
if data.CertificatePath != "" {
|
||||||
|
return data, fmt.Errorf("certificate data and file can't be set together")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.PrivateKey != "" {
|
if data.PrivateKey != "" {
|
||||||
|
@ -309,6 +359,9 @@ func unmarshalTLS(r *http.Request) (tlsConfig, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
data.PrivateKey = string(keyPEM)
|
data.PrivateKey = string(keyPEM)
|
||||||
|
if data.PrivateKeyPath != "" {
|
||||||
|
return data, fmt.Errorf("private key data and file can't be set together")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
|
|
14
home/home.go
14
home/home.go
|
@ -218,13 +218,13 @@ func httpServerLoop() {
|
||||||
// this mechanism doesn't let us through until all conditions are met
|
// this mechanism doesn't let us through until all conditions are met
|
||||||
for config.TLS.Enabled == false ||
|
for config.TLS.Enabled == false ||
|
||||||
config.TLS.PortHTTPS == 0 ||
|
config.TLS.PortHTTPS == 0 ||
|
||||||
config.TLS.PrivateKey == "" ||
|
len(config.TLS.PrivateKeyData) == 0 ||
|
||||||
config.TLS.CertificateChain == "" { // sleep until necessary data is supplied
|
len(config.TLS.CertificateChainData) == 0 { // sleep until necessary data is supplied
|
||||||
config.httpsServer.cond.Wait()
|
config.httpsServer.cond.Wait()
|
||||||
}
|
}
|
||||||
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS))
|
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS))
|
||||||
// validate current TLS config and update warnings (it could have been loaded from file)
|
// validate current TLS config and update warnings (it could have been loaded from file)
|
||||||
data := validateCertificates(config.TLS.CertificateChain, config.TLS.PrivateKey, config.TLS.ServerName)
|
data := validateCertificates(string(config.TLS.CertificateChainData), string(config.TLS.PrivateKeyData), config.TLS.ServerName)
|
||||||
if !data.ValidPair {
|
if !data.ValidPair {
|
||||||
cleanupAlways()
|
cleanupAlways()
|
||||||
log.Fatal(data.WarningValidation)
|
log.Fatal(data.WarningValidation)
|
||||||
|
@ -235,10 +235,10 @@ func httpServerLoop() {
|
||||||
|
|
||||||
// prepare certs for HTTPS server
|
// prepare certs for HTTPS server
|
||||||
// important -- they have to be copies, otherwise changing the contents in config.TLS will break encryption for in-flight requests
|
// important -- they have to be copies, otherwise changing the contents in config.TLS will break encryption for in-flight requests
|
||||||
certchain := make([]byte, len(config.TLS.CertificateChain))
|
certchain := make([]byte, len(config.TLS.CertificateChainData))
|
||||||
copy(certchain, []byte(config.TLS.CertificateChain))
|
copy(certchain, config.TLS.CertificateChainData)
|
||||||
privatekey := make([]byte, len(config.TLS.PrivateKey))
|
privatekey := make([]byte, len(config.TLS.PrivateKeyData))
|
||||||
copy(privatekey, []byte(config.TLS.PrivateKey))
|
copy(privatekey, config.TLS.PrivateKeyData)
|
||||||
cert, err := tls.X509KeyPair(certchain, privatekey)
|
cert, err := tls.X509KeyPair(certchain, privatekey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cleanupAlways()
|
cleanupAlways()
|
||||||
|
|
|
@ -1478,6 +1478,12 @@ definitions:
|
||||||
private_key:
|
private_key:
|
||||||
type: "string"
|
type: "string"
|
||||||
description: "Base64 string with PEM-encoded private key"
|
description: "Base64 string with PEM-encoded private key"
|
||||||
|
certificate_path:
|
||||||
|
type: "string"
|
||||||
|
description: "Path to certificate file"
|
||||||
|
private_key_path:
|
||||||
|
type: "string"
|
||||||
|
description: "Path to private key file"
|
||||||
# Below goes validation fields
|
# Below goes validation fields
|
||||||
valid_cert:
|
valid_cert:
|
||||||
type: "boolean"
|
type: "boolean"
|
||||||
|
|
Loading…
Reference in New Issue