[bug] Fix "DeprecationWarning: Setting the TLS ServerName to an IP address"; Add https tab fallback note; Fix bug 79830

This commit is contained in:
Sergey Konovalov
2026-02-05 12:33:32 +03:00
parent 094841a20c
commit d4c045199c
3 changed files with 83 additions and 34 deletions

View File

@ -6,11 +6,39 @@ import Note from '../../../components/Note/Note';
import {getLetsEncryptStatus, installLetsEncryptCertificate} from '../../../api';
import styles from './HttpsTab.module.scss';
// Documentation links for manual HTTPS configuration
const HTTPS_DOCS = {
linux: 'https://helpcenter.onlyoffice.com/docs/installation/docs-community-https-linux.aspx',
windows: 'https://helpcenter.onlyoffice.com/docs/installation/docs-community-https-windows.aspx',
kubernetes: 'https://github.com/ONLYOFFICE/Kubernetes-Docs-Shards?tab=readme-ov-file#5322-expose-onlyoffice-docs-shards-via-http'
};
/**
* Check if a string is an IP address (IPv4 or IPv6)
* @param {string} str - String to check
* @returns {boolean} True if IP address
*/
const isIPAddress = str => {
if (!str) return false;
// IPv4: digits and dots only (e.g., 192.168.1.1)
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
// IPv6: hex digits and colons, optionally with brackets (e.g., ::1, [::1])
const ipv6Pattern = /^(\[)?([0-9a-fA-F:]+)(])?$/;
return ipv4Pattern.test(str) || ipv6Pattern.test(str);
};
const HttpsTab = () => {
// Check if user accessed the page by IP address
const currentHostname = window.location.hostname;
const accessedByIP = isIPAddress(currentHostname);
const [available, setAvailable] = useState(null); // null = loading
const [certInfo, setCertInfo] = useState(null);
const [loading, setLoading] = useState(true);
const [email, setEmail] = useState('');
const [domain, setDomain] = useState(window.location.hostname);
// Don't pre-fill domain if accessed by IP - Let's Encrypt requires domain names
const [domain, setDomain] = useState(accessedByIP ? '' : currentHostname);
const [domainInitialized, setDomainInitialized] = useState(false);
const [installing, setInstalling] = useState(false);
const [error, setError] = useState(null);
const [errorDetails, setErrorDetails] = useState(null);
@ -19,13 +47,18 @@ const HttpsTab = () => {
// Current HTTPS status from browser
const isHttps = window.location.protocol === 'https:';
// Load certificate details from server
// Validation: check if entered domain is an IP address
const domainIsIP = isIPAddress(domain);
// Load certificate details and availability from server
const loadCertInfo = useCallback(async () => {
try {
const result = await getLetsEncryptStatus();
setAvailable(result.available);
setCertInfo(result.certificate || null);
} catch (err) {
console.error('Failed to load certificate info:', err);
setAvailable(false);
} finally {
setLoading(false);
}
@ -35,6 +68,14 @@ const HttpsTab = () => {
loadCertInfo();
}, [loadCertInfo]);
// Pre-fill domain from certificate info when available (higher priority than hostname)
useEffect(() => {
if (!domainInitialized && certInfo?.domain) {
setDomain(certInfo.domain);
setDomainInitialized(true);
}
}, [certInfo, domainInitialized]);
// Calculate certificate health
const getCertHealth = () => {
if (!certInfo?.expiresAt) return null;
@ -61,6 +102,10 @@ const HttpsTab = () => {
setError('Please enter a domain name');
return;
}
if (domainIsIP) {
setError("Let's Encrypt does not issue certificates for IP addresses. Please enter a domain name.");
return;
}
// Confirmation
const action = isHttps ? 'renew' : 'install';
@ -95,6 +140,35 @@ const HttpsTab = () => {
const certHealth = getCertHealth();
// Fallback when Let's Encrypt script is not available
if (available === false) {
return (
<Section title='HTTPS Certificate' description='Configure SSL/TLS certificate manually'>
<Note type='note'>
Automatic HTTPS configuration via Let's Encrypt is not available for this installation. Please follow the manual configuration instructions
for your platform:
<ul>
<li>
<a href={HTTPS_DOCS.linux} target='_blank' rel='noopener noreferrer'>
Linux installation guide
</a>
</li>
<li>
<a href={HTTPS_DOCS.windows} target='_blank' rel='noopener noreferrer'>
Windows installation guide
</a>
</li>
<li>
<a href={HTTPS_DOCS.kubernetes} target='_blank' rel='noopener noreferrer'>
Kubernetes configuration
</a>
</li>
</ul>
</Note>
</Section>
);
}
return (
<Section title="HTTPS Certificate (Let's Encrypt)" description="Install a free SSL/TLS certificate from Let's Encrypt to enable HTTPS">
{/* Information Note */}
@ -167,6 +241,7 @@ const HttpsTab = () => {
placeholder='docs.example.com'
description='Domain name for the SSL certificate. Must resolve to this server.'
disabled={installing}
error={domainIsIP ? "Let's Encrypt does not issue certificates for IP addresses" : null}
/>
<Input
@ -179,7 +254,7 @@ const HttpsTab = () => {
/>
<div className={styles.actions}>
<Button onClick={handleInstall} disabled={installing || !email || !domain}>
<Button onClick={handleInstall} disabled={installing || !email || !domain || domainIsIP}>
{installing ? 'Installing... Please wait' : isHttps ? 'Renew Certificate' : 'Install Certificate'}
</Button>
</div>

View File

@ -1,6 +1,6 @@
import {useState, useEffect, useRef, useCallback} from 'react';
import {useSelector, useDispatch} from 'react-redux';
import {uploadSigningCertificate, deleteSigningCertificate, getSigningCertificateStatus, getLetsEncryptStatus} from '../../api';
import {uploadSigningCertificate, deleteSigningCertificate, getSigningCertificateStatus} from '../../api';
import {saveConfig, resetConfig, selectConfig} from '../../store/slices/configSlice';
import {getNestedValue} from '../../utils/getNestedValue';
import {mergeNestedObjects} from '../../utils/mergeNestedObjects';
@ -19,6 +19,7 @@ import './Settings.scss';
const baseTabs = [
{key: 'configuration', label: 'Configuration'},
{key: 'pdf-signing', label: 'PDF Signing'},
{key: 'https', label: 'HTTPS / SSL'},
{key: 'shutdown', label: 'Shutdown'}
];
@ -36,34 +37,6 @@ const Settings = () => {
const [error, setError] = useState(null);
const [successMessage, setSuccessMessage] = useState(null);
// Dynamic tabs based on feature availability
const [settingsTabs, setSettingsTabs] = useState(baseTabs);
// Check Let's Encrypt availability on mount
useEffect(() => {
const checkHttpsAvailability = async () => {
try {
const status = await getLetsEncryptStatus();
// Add HTTPS tab if feature is available
if (status.available) {
setSettingsTabs(prev => {
if (prev.some(tab => tab.key === 'https')) return prev;
// Insert HTTPS tab after PDF Signing
const pdfSigningIndex = prev.findIndex(tab => tab.key === 'pdf-signing');
const newTabs = [...prev];
newTabs.splice(pdfSigningIndex + 1, 0, {key: 'https', label: 'HTTPS / SSL'});
return newTabs;
});
}
} catch (err) {
console.error('Failed to check HTTPS availability:', err);
}
};
checkHttpsAvailability();
}, []);
// Check certificate status on server (only when PDF Signing tab is active)
const checkCertificateStatus = useCallback(async () => {
try {
@ -327,7 +300,7 @@ const Settings = () => {
</div>
<div className='settings-content' title='Settings'>
<Tabs tabs={settingsTabs} activeTab={activeTab} onTabChange={handleTabChange}>
<Tabs tabs={baseTabs} activeTab={activeTab} onTabChange={handleTabChange}>
{renderTabContent()}
</Tabs>
</div>

View File

@ -5,6 +5,7 @@ const cookieParser = require('cookie-parser');
const fs = require('fs');
const path = require('path');
const tls = require('tls');
const net = require('net');
const {spawn} = require('child_process');
const {validateJWT} = require('../../middleware/auth');
const tenantManager = require('../../../../../Common/sources/tenantManager');
@ -97,7 +98,7 @@ function getCertificate(hostname) {
host: hostname,
port: 443,
rejectUnauthorized: false,
servername: hostname,
servername: net.isIP(hostname) ? undefined : hostname,
timeout: 2000
},
() => {