Skip to content

Commit ec1f7b2

Browse files
authored
Merge pull request #457 from mrrobot47/feat/ssl-info
feat: ssl-info command to show SSL details and DNS challange records
2 parents 3e0923a + 9171dd0 commit ec1f7b2

File tree

1 file changed

+143
-44
lines changed

1 file changed

+143
-44
lines changed

src/helper/class-ee-site.php

Lines changed: 143 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1720,13 +1720,16 @@ public function ssl_verify( $args = [], $assoc_args = [], $www_or_non_www = fals
17201720
}
17211721

17221722
/**
1723-
* Prints the DNS TXT record(s) required for DNS-based SSL challenge for a site.
1723+
* Shows SSL info and DNS challenge records for a site.
17241724
*
17251725
* ## OPTIONS
17261726
*
17271727
* [<site-name>]
17281728
* : Name of website.
17291729
*
1730+
* [--get-dns-records]
1731+
* : Show DNS challenge records (if using DNS-01 challenge).
1732+
*
17301733
* [--format=<format>]
17311734
* : Render output in a particular format.
17321735
* ---
@@ -1736,19 +1739,19 @@ public function ssl_verify( $args = [], $assoc_args = [], $www_or_non_www = fals
17361739
* - csv
17371740
* - yaml
17381741
* - json
1739-
* - count
1740-
* - text
17411742
* ---
17421743
*
17431744
* ## EXAMPLES
17441745
*
1746+
* # Show SSL info for a site
1747+
* $ ee site ssl-info example.com
1748+
*
17451749
* # Show DNS challenge info for a site
1746-
* $ ee site ssl-dns-info example.com
1747-
* $ ee site ssl-dns-info example.com --format=json
1750+
* $ ee site ssl-info example.com --get-dns-records
17481751
*
1749-
* @subcommand ssl-dns-info
1752+
* @subcommand ssl-info
17501753
*/
1751-
public function ssl_dns_info( $args, $assoc_args ) {
1754+
public function ssl_info( $args, $assoc_args ) {
17521755
$args = auto_site_name( $args, 'site', __FUNCTION__ );
17531756
$this->site_data = get_site_info( $args, false, true, false );
17541757

@@ -1758,54 +1761,150 @@ public function ssl_dns_info( $args, $assoc_args ) {
17581761
$domains = $this->get_cert_domains( $site_url, $wildcard );
17591762
$domains = array_unique( array_merge( $domains, $alias_domains ) );
17601763

1761-
$preferred_challenge = get_preferred_ssl_challenge( $domains );
1762-
$is_dns = $wildcard || $preferred_challenge === 'dns';
1764+
$output = [];
1765+
$warnings = [];
17631766

1764-
if ( ! $is_dns ) {
1765-
\EE::log( 'This site does not use DNS-based (DNS-01) SSL challenge.' );
1767+
// If --get-dns-records is passed, show DNS challenge info (old behavior)
1768+
if ( \EE\Utils\get_flag_value( $assoc_args, 'get-dns-records', false ) ) {
1769+
$preferred_challenge = get_preferred_ssl_challenge( $domains );
1770+
$is_dns = $wildcard || $preferred_challenge === 'dns';
1771+
1772+
if ( ! $is_dns ) {
1773+
$warnings[] = 'This site does not use DNS-based (DNS-01) SSL challenge.';
1774+
} else {
1775+
$client = new \EE\Site\Type\Site_Letsencrypt();
1776+
$rows = [];
1777+
foreach ( $domains as $domain ) {
1778+
if ( $client->hasDomainAuthorizationChallenge( $domain ) ) {
1779+
$challenge = $client->loadDomainAuthorizationChallenge( $domain );
1780+
if ( method_exists( $challenge, 'toArray' ) ) {
1781+
$data = $challenge->toArray();
1782+
$record_name = isset( $data['dnsRecordName'] ) ? $data['dnsRecordName'] : '_acme-challenge.' . $domain;
1783+
if ( isset( $data['dnsRecordValue'] ) ) {
1784+
$record_value = $data['dnsRecordValue'];
1785+
} elseif ( isset( $data['payload'] ) ) {
1786+
$keyAuthorization = $data['payload'];
1787+
$digest = rtrim( strtr( base64_encode( hash( 'sha256', $keyAuthorization, true ) ), '+/', '-_' ), '=' );
1788+
$record_value = $digest;
1789+
} else {
1790+
$record_value = '';
1791+
}
1792+
$rows[] = [
1793+
'domain' => $domain,
1794+
'record_name' => $record_name,
1795+
'record_value' => $record_value,
1796+
];
1797+
} else {
1798+
$warnings[] = "Could not extract DNS challenge for $domain.";
1799+
}
1800+
} else {
1801+
$warnings[] = "No pending DNS challenge found for $domain. (Try running 'ee site ssl-verify $site_url' if you are setting up SSL)";
1802+
}
1803+
}
1804+
$output['dns_challenges'] = $rows;
1805+
}
1806+
$output['warnings'] = $warnings;
1807+
$formatter = new \EE\Formatter( $assoc_args, array_keys( $output ) );
1808+
$formatter->display_items( [ $output ] );
17661809

17671810
return;
17681811
}
17691812

1770-
$format = \EE\Utils\get_flag_value( $assoc_args, 'format', 'table' );
1771-
$client = new \EE\Site\Type\Site_Letsencrypt();
1772-
$rows = [];
1773-
foreach ( $domains as $domain ) {
1774-
if ( $client->hasDomainAuthorizationChallenge( $domain ) ) {
1775-
$challenge = $client->loadDomainAuthorizationChallenge( $domain );
1776-
if ( method_exists( $challenge, 'toArray' ) ) {
1777-
$data = $challenge->toArray();
1778-
$record_name = isset( $data['dnsRecordName'] ) ? $data['dnsRecordName'] : '_acme-challenge.' . $domain;
1779-
if ( isset( $data['dnsRecordValue'] ) ) {
1780-
$record_value = $data['dnsRecordValue'];
1781-
} elseif ( isset( $data['payload'] ) ) {
1782-
// Compute digest for DNS-01 TXT value
1783-
$keyAuthorization = $data['payload'];
1784-
$digest = rtrim( strtr( base64_encode( hash( 'sha256', $keyAuthorization, true ) ), '+/', '-_' ), '=' );
1785-
$record_value = $digest;
1786-
} else {
1787-
$record_value = '';
1813+
// Otherwise, show SSL status and cert details
1814+
$ssl_type = $this->site_data->site_ssl;
1815+
$output['ssl_type'] = $ssl_type ? $ssl_type : 'off';
1816+
1817+
if ( ! $ssl_type || $ssl_type === 'off' ) {
1818+
$output['status'] = 'SSL is not enabled for this site.';
1819+
$output['warnings'] = $warnings;
1820+
$formatter = new \EE\Formatter( $assoc_args, array_keys( $output ) );
1821+
$formatter->display_items( [ $output ] );
1822+
1823+
return;
1824+
}
1825+
1826+
// Determine which cert to show (le, self, inherit, custom)
1827+
$cert_site_name = $site_url;
1828+
if ( $ssl_type === 'inherit' ) {
1829+
$cert_site_name = implode( '.', array_slice( explode( '.', $site_url ), 1 ) );
1830+
}
1831+
1832+
$certs_dir = EE_ROOT_DIR . '/services/nginx-proxy/certs/';
1833+
$crt_file = $certs_dir . $cert_site_name . '.crt';
1834+
1835+
if ( ! file_exists( $crt_file ) ) {
1836+
$warnings[] = "Certificate file not found for $cert_site_name ($crt_file)";
1837+
$output['status'] = 'Certificate file not found / yet to be issued.';
1838+
$output['warnings'] = $warnings;
1839+
$formatter = new \EE\Formatter( $assoc_args, array_keys( $output ) );
1840+
$formatter->display_items( [ $output ] );
1841+
1842+
return;
1843+
}
1844+
1845+
try {
1846+
$certificate = new \AcmePhp\Ssl\Certificate( file_get_contents( $crt_file ) );
1847+
$certificateParser = new \AcmePhp\Ssl\Parser\CertificateParser();
1848+
$parsedCertificate = $certificateParser->parse( $certificate );
1849+
1850+
$issuer = $parsedCertificate->getIssuer();
1851+
$subject = $parsedCertificate->getSubject();
1852+
$validFrom = $parsedCertificate->getValidFrom()->format( 'Y-m-d H:i:s' );
1853+
$validTo = $parsedCertificate->getValidTo()->format( 'Y-m-d H:i:s' );
1854+
$serial = $parsedCertificate->getSerialNumber();
1855+
1856+
// Use openssl_x509_parse for CN fields, as in migration
1857+
$crt_pem = file_get_contents( $crt_file );
1858+
if ( function_exists( 'openssl_x509_parse' ) ) {
1859+
$cert_data = openssl_x509_parse( $crt_pem );
1860+
$subjectCN = isset( $cert_data['subject']['CN'] ) ? $cert_data['subject']['CN'] : '';
1861+
$issuer_full = isset( $cert_data['issuer'] ) ? $cert_data['issuer'] : [];
1862+
$le_found = false;
1863+
foreach ( $issuer_full as $field => $value ) {
1864+
if ( stripos( $value, "Let's Encrypt" ) !== false ) {
1865+
$le_found = true;
1866+
break;
17881867
}
1789-
$rows[] = [
1790-
'domain' => $domain,
1791-
'record_name' => $record_name,
1792-
'record_value' => $record_value,
1793-
];
1868+
}
1869+
if ( $le_found ) {
1870+
$issuerCN = "Let's Encrypt";
17941871
} else {
1795-
\EE::warning( "Could not extract DNS challenge for $domain." );
1872+
$issuerCN = isset( $issuer_full['CN'] ) ? $issuer_full['CN'] : implode( ', ', $issuer_full );
17961873
}
17971874
} else {
1798-
\EE::warning( "No pending DNS challenge found for $domain. (Try running 'ee site ssl-verify $site_url' if you are setting up SSL)" );
1799-
}
1800-
}
1801-
if ( $rows ) {
1802-
$formatter = new \EE\Formatter( $assoc_args, [ 'domain', 'record_name', 'record_value' ] );
1803-
$formatter->display_items( $rows );
1804-
} else {
1805-
\EE::log( 'No DNS challenge records found for this site.' );
1875+
if ( is_object( $subject ) && method_exists( $subject, 'getField' ) ) {
1876+
$subjectCN = $subject->getField( 'CN' );
1877+
} else {
1878+
$subjectCN = is_string( $subject ) ? $subject : json_encode( $subject );
1879+
$warnings[] = 'Could not parse subject CN: unexpected type.';
1880+
}
1881+
if ( is_object( $issuer ) && method_exists( $issuer, 'getField' ) ) {
1882+
$issuerCN = $issuer->getField( 'CN' );
1883+
} else {
1884+
$issuerCN = is_string( $issuer ) ? $issuer : json_encode( $issuer );
1885+
$warnings[] = 'Could not parse issuer CN: unexpected type.';
1886+
}
1887+
$warnings[] = 'openssl_x509_parse() not available in PHP. Used fallback parser.';
1888+
}
1889+
$san = $parsedCertificate->getSubjectAlternativeNames();
1890+
1891+
$output['cert_file'] = $crt_file;
1892+
$output['issued_to_CN'] = $subjectCN;
1893+
$output['issued_by_CN'] = $issuerCN;
1894+
$output['valid_from'] = $validFrom;
1895+
$output['valid_till'] = $validTo;
1896+
$output['serial_number'] = $serial;
1897+
$output['SANs'] = implode( ', ', $san );
1898+
$output['status'] = 'SSL certificate details loaded.';
1899+
} catch ( \Exception $e ) {
1900+
$warnings[] = 'Could not parse certificate: ' . $e->getMessage();
1901+
$output['status'] = 'Could not parse certificate.';
18061902
}
1807-
}
1903+
$output['warnings'] = $warnings;
18081904

1905+
$formatter = new \EE\Formatter( $assoc_args, array_keys( $output ) );
1906+
$formatter->display_items( [ $output ] );
1907+
}
18091908

18101909
/**
18111910
* Renews letsencrypt ssl certificates.

0 commit comments

Comments
 (0)