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:
Simon Zolin 2019-08-30 19:36:51 +03:00
commit a9524448b1
15 changed files with 458 additions and 127 deletions

View File

@ -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.

View File

@ -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

View File

@ -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"
} }

View File

@ -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>:&nbsp;
{subject}
</li>
)}
{issuer && (
<li>
<Trans>encryption_issuer</Trans>:&nbsp;
{issuer}
</li>
)}
{notAfter && notAfter !== EMPTY_DATE && (
<li>
<Trans>encryption_expire</Trans>:&nbsp;
{format(notAfter, 'YYYY-MM-DD HH:mm:ss')}
</li>
)}
{dnsNames && (
<li>
<Trans>encryption_hostnames</Trans>:&nbsp;
{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);

View File

@ -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,6 +212,29 @@ let Form = (props) => {
encryption_certificates_desc encryption_certificates_desc
</Trans> </Trans>
</div> </div>
<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 <Field
id="certificate_chain" id="certificate_chain"
name="certificate_chain" name="certificate_chain"
@ -209,63 +245,63 @@ let Form = (props) => {
onChange={handleChange} onChange={handleChange}
disabled={!isEnabled} 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"> <div className="form__status">
{certificateChain && ( {(certificateChain || certificatePath) && (
<Fragment> <CertificateStatus
<div className="form__label form__label--bold"> validChain={valid_chain}
<Trans>encryption_status</Trans>: validCert={valid_cert}
</div> subject={subject}
<ul className="encryption__list"> issuer={issuer}
<li notAfter={not_after}
className={valid_chain ? 'text-success' : 'text-danger'} dnsNames={dns_names}
> />
{valid_chain ? (
<Trans>encryption_chain_valid</Trans>
) : (
<Trans>encryption_chain_invalid</Trans>
)} )}
</li>
{valid_cert && (
<Fragment>
{subject && (
<li>
<Trans>encryption_subject</Trans>:&nbsp;
{subject}
</li>
)}
{issuer && (
<li>
<Trans>encryption_issuer</Trans>:&nbsp;
{issuer}
</li>
)}
{not_after && not_after !== EMPTY_DATE && (
<li>
<Trans>encryption_expire</Trans>:&nbsp;
{format(not_after, 'YYYY-MM-DD HH:mm:ss')}
</li>
)}
{dns_names && (
<li>
<Trans>encryption_hostnames</Trans>:&nbsp;
{dns_names}
</li>
)}
</Fragment>
)}
</ul>
</Fragment>
)}
</div>
</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>
<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 <Field
id="private_key" id="private_key"
name="private_key" name="private_key"
@ -276,28 +312,24 @@ let Form = (props) => {
onChange={handleChange} onChange={handleChange}
disabled={!isEnabled} 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"> <div className="form__status">
{privateKey && ( {(privateKey || privateKeyPath) && (
<Fragment> <KeyStatus validKey={valid_key} keyType={key_type} />
<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>
</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);

View 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);

View File

@ -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}

View File

@ -248,3 +248,8 @@ export const SERVICES = [
name: 'TikTok', name: 'TikTok',
}, },
]; ];
export const ENCRYPTION_SOURCE = {
PATH: 'path',
CONTENT: 'content',
};

View File

@ -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;

View File

@ -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")
} }

View File

@ -119,8 +119,8 @@ func TestDotServer(t *testing.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

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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"