mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-11-17 02:48:28 -07:00
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
|
||||
* 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.
|
||||
|
@ -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
|
||||
|
||||
<a id="acknowledgments"></a>
|
||||
## Acknowledgments
|
||||
|
@ -353,5 +353,11 @@
|
||||
"blocked_services_global": "Use global blocked services",
|
||||
"blocked_service": "Blocked service",
|
||||
"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 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 (
|
||||
<form onSubmit={handleSubmit}>
|
||||
@ -182,7 +195,7 @@ let Form = (props) => {
|
||||
<div className="col-12">
|
||||
<div className="form__group form__group--settings">
|
||||
<label
|
||||
className="form__label form__label--bold"
|
||||
className="form__label form__label--with-desc form__label--bold"
|
||||
htmlFor="certificate_chain"
|
||||
>
|
||||
<Trans>encryption_certificates</Trans>
|
||||
@ -199,105 +212,124 @@ let Form = (props) => {
|
||||
encryption_certificates_desc
|
||||
</Trans>
|
||||
</div>
|
||||
<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}
|
||||
/>
|
||||
<div className="form__status">
|
||||
{certificateChain && (
|
||||
<Fragment>
|
||||
<div className="form__label form__label--bold">
|
||||
<Trans>encryption_status</Trans>:
|
||||
</div>
|
||||
<ul className="encryption__list">
|
||||
<li
|
||||
className={valid_chain ? 'text-success' : 'text-danger'}
|
||||
>
|
||||
{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 className="form__inline mb-2">
|
||||
<div className="custom-controls-stacked">
|
||||
<Field
|
||||
name="certificate_source"
|
||||
component={renderRadioField}
|
||||
type="radio"
|
||||
className="form-control mr-2"
|
||||
value="path"
|
||||
placeholder={t('encryption_certificates_source_path')}
|
||||
/>
|
||||
<Field
|
||||
name="certificate_source"
|
||||
component={renderRadioField}
|
||||
type="radio"
|
||||
className="form-control mr-2"
|
||||
value="content"
|
||||
placeholder={t('encryption_certificates_source_content')}
|
||||
/>
|
||||
</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 className="row">
|
||||
<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">
|
||||
<Trans>encryption_key</Trans>
|
||||
</label>
|
||||
<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}
|
||||
/>
|
||||
<div className="form__status">
|
||||
{privateKey && (
|
||||
<Fragment>
|
||||
<div className="form__label form__label--bold">
|
||||
<Trans>encryption_status</Trans>:
|
||||
</div>
|
||||
<ul className="encryption__list">
|
||||
<li className={valid_key ? 'text-success' : 'text-danger'}>
|
||||
{valid_key ? (
|
||||
<Trans values={{ type: key_type }}>
|
||||
encryption_key_valid
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans values={{ type: key_type }}>
|
||||
encryption_key_invalid
|
||||
</Trans>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
<div className="form__inline mb-2">
|
||||
<div className="custom-controls-stacked">
|
||||
<Field
|
||||
name="key_source"
|
||||
component={renderRadioField}
|
||||
type="radio"
|
||||
className="form-control mr-2"
|
||||
value="path"
|
||||
placeholder={t('encryption_key_source_path')}
|
||||
/>
|
||||
<Field
|
||||
name="key_source"
|
||||
component={renderRadioField}
|
||||
type="radio"
|
||||
className="form-control mr-2"
|
||||
value="content"
|
||||
placeholder={t('encryption_key_source_content')}
|
||||
/>
|
||||
</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>
|
||||
{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);
|
||||
|
||||
|
31
client/src/components/Settings/Encryption/KeyStatus.js
Normal file
31
client/src/components/Settings/Encryption/KeyStatus.js
Normal file
@ -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 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 (
|
||||
<div className="encryption">
|
||||
<PageTitle title={t('encryption_settings')} />
|
||||
@ -49,15 +95,7 @@ class Encryption extends Component {
|
||||
bodyType="card-body box-body--settings"
|
||||
>
|
||||
<Form
|
||||
initialValues={{
|
||||
enabled,
|
||||
server_name,
|
||||
force_https,
|
||||
port_https,
|
||||
port_dns_over_tls,
|
||||
certificate_chain,
|
||||
private_key,
|
||||
}}
|
||||
initialValues={initialValues}
|
||||
onSubmit={this.handleFormSubmit}
|
||||
onChange={this.handleFormChange}
|
||||
setTlsConfig={this.props.setTlsConfig}
|
||||
|
@ -248,3 +248,8 @@ export const SERVICES = [
|
||||
name: 'TikTok',
|
||||
},
|
||||
];
|
||||
|
||||
export const ENCRYPTION_SOURCE = {
|
||||
PATH: 'path',
|
||||
CONTENT: 'content',
|
||||
};
|
||||
|
@ -77,6 +77,8 @@ const encryption = handleActions({
|
||||
private_key: '',
|
||||
server_name: '',
|
||||
warning_validation: '',
|
||||
certificate_path: '',
|
||||
private_key_path: '',
|
||||
});
|
||||
|
||||
export default encryption;
|
||||
|
@ -108,6 +108,12 @@ type TLSConfig struct {
|
||||
TLSListenAddr *net.TCPAddr `yaml:"-" json:"-"`
|
||||
CertificateChain string `yaml:"certificate_chain" json:"certificate_chain"` // PEM-encoded certificates chain
|
||||
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.
|
||||
@ -216,9 +222,9 @@ func (s *Server) startInternal(config *ServerConfig) error {
|
||||
|
||||
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
|
||||
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 {
|
||||
return errorx.Decorate(err, "Failed to parse TLS keypair")
|
||||
}
|
||||
|
@ -118,9 +118,9 @@ func TestDotServer(t *testing.T) {
|
||||
defer removeDataDir(t)
|
||||
|
||||
s.conf.TLSConfig = TLSConfig{
|
||||
TLSListenAddr: &net.TCPAddr{Port: 0},
|
||||
CertificateChain: string(certPem),
|
||||
PrivateKey: string(keyPem),
|
||||
TLSListenAddr: &net.TCPAddr{Port: 0},
|
||||
CertificateChainData: certPem,
|
||||
PrivateKeyData: keyPem,
|
||||
}
|
||||
|
||||
// Starting the server
|
||||
|
@ -279,6 +279,12 @@ func parseConfig() error {
|
||||
}
|
||||
config.Clients = nil
|
||||
|
||||
status := tlsConfigStatus{}
|
||||
if !tlsLoadConfig(&config.TLS, &status) {
|
||||
log.Error("%s", status.WarningValidation)
|
||||
return err
|
||||
}
|
||||
|
||||
// Deduplicate filters
|
||||
deduplicateFilters()
|
||||
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
@ -23,6 +24,41 @@ import (
|
||||
"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
|
||||
func RegisterTLSHandlers() {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
data.tlsConfigStatus = validateCertificates(data.CertificateChain, data.PrivateKey, data.ServerName)
|
||||
if !reflect.DeepEqual(config.TLS.tlsConfigSettings, data.tlsConfigSettings) {
|
||||
log.Printf("tls config settings have changed, will restart HTTPS server")
|
||||
restartHTTPS = true
|
||||
@ -300,6 +347,9 @@ func unmarshalTLS(r *http.Request) (tlsConfig, error) {
|
||||
return data, errorx.Decorate(err, "Failed to base64-decode certificate chain")
|
||||
}
|
||||
data.CertificateChain = string(certPEM)
|
||||
if data.CertificatePath != "" {
|
||||
return data, fmt.Errorf("certificate data and file can't be set together")
|
||||
}
|
||||
}
|
||||
|
||||
if data.PrivateKey != "" {
|
||||
@ -309,6 +359,9 @@ func unmarshalTLS(r *http.Request) (tlsConfig, error) {
|
||||
}
|
||||
|
||||
data.PrivateKey = string(keyPEM)
|
||||
if data.PrivateKeyPath != "" {
|
||||
return data, fmt.Errorf("private key data and file can't be set together")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
for config.TLS.Enabled == false ||
|
||||
config.TLS.PortHTTPS == 0 ||
|
||||
config.TLS.PrivateKey == "" ||
|
||||
config.TLS.CertificateChain == "" { // sleep until necessary data is supplied
|
||||
len(config.TLS.PrivateKeyData) == 0 ||
|
||||
len(config.TLS.CertificateChainData) == 0 { // sleep until necessary data is supplied
|
||||
config.httpsServer.cond.Wait()
|
||||
}
|
||||
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.TLS.PortHTTPS))
|
||||
// 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 {
|
||||
cleanupAlways()
|
||||
log.Fatal(data.WarningValidation)
|
||||
@ -235,10 +235,10 @@ func httpServerLoop() {
|
||||
|
||||
// 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
|
||||
certchain := make([]byte, len(config.TLS.CertificateChain))
|
||||
copy(certchain, []byte(config.TLS.CertificateChain))
|
||||
privatekey := make([]byte, len(config.TLS.PrivateKey))
|
||||
copy(privatekey, []byte(config.TLS.PrivateKey))
|
||||
certchain := make([]byte, len(config.TLS.CertificateChainData))
|
||||
copy(certchain, config.TLS.CertificateChainData)
|
||||
privatekey := make([]byte, len(config.TLS.PrivateKeyData))
|
||||
copy(privatekey, config.TLS.PrivateKeyData)
|
||||
cert, err := tls.X509KeyPair(certchain, privatekey)
|
||||
if err != nil {
|
||||
cleanupAlways()
|
||||
|
@ -1478,6 +1478,12 @@ definitions:
|
||||
private_key:
|
||||
type: "string"
|
||||
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
|
||||
valid_cert:
|
||||
type: "boolean"
|
||||
|
Loading…
Reference in New Issue
Block a user