Skip to content

Commit 0703db2

Browse files
authored
Merge pull request #1 from pioarduino/test
5.3.0
2 parents fcd7572 + ddcf07d commit 0703db2

File tree

3 files changed

+284
-88
lines changed

3 files changed

+284
-88
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "tool-esp_install",
3-
"version": "5.2.0",
4-
"description": "espressif installer tool: idf_tools.py",
3+
"version": "5.3.0",
4+
"description": "modified espressif installer tool: idf_tools.py",
55
"keywords": [
66
"tools",
77
"installer"

tools/idf_tools.py

Lines changed: 282 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
import tarfile
4747
import tempfile
4848
import time
49+
import urllib.parse
50+
import urllib.request
4951
from collections import OrderedDict
5052
from collections import namedtuple
5153
from json import JSONEncoder
@@ -99,7 +101,7 @@ class WindowsError(OSError): # type: ignore
99101
VERSION_REGEX_REPLACE_DEFAULT = r'\1'
100102
IDF_MAINTAINER = os.environ.get('IDF_MAINTAINER') or False
101103
TODO_MESSAGE = 'TODO'
102-
DOWNLOAD_RETRY_COUNT = 3
104+
DOWNLOAD_RETRY_COUNT = 5
103105
URL_PREFIX_MAP_SEPARATOR = ','
104106
IDF_TOOLS_INSTALL_CMD = os.environ.get('IDF_TOOLS_INSTALL_CMD')
105107
IDF_TOOLS_EXPORT_CMD = os.environ.get('IDF_TOOLS_INSTALL_CMD')
@@ -460,6 +462,126 @@ def parse_platform_arg(platform_str: str) -> str:
460462
DL_CERT_DICT = {'dl.espressif.com': DIGICERT_ROOT_G2_CERT, 'github.com': DIGICERT_ROOT_CA_CERT}
461463

462464

465+
def create_esp_idf_ssl_context(url: str) -> ssl.SSLContext:
466+
"""
467+
Creates ESP-IDF optimized SSL context with OpenSSL version detection.
468+
469+
This function detects the SSL backend (LibreSSL vs OpenSSL) and creates
470+
an appropriate SSL context with version-specific optimizations. It also
471+
handles custom DigiCert certificates for known domains.
472+
473+
Args:
474+
url: The URL to create SSL context for
475+
476+
Returns:
477+
Configured SSL context optimized for the detected backend
478+
"""
479+
ssl_version = ssl.OPENSSL_VERSION
480+
ssl_version_info = ssl.OPENSSL_VERSION_INFO
481+
482+
info(f"SSL Backend: {ssl_version} ({ssl_version_info})")
483+
484+
# Create context based on detected SSL backend version
485+
if "LibreSSL" in ssl_version:
486+
# macOS LibreSSL - more conservative settings
487+
ctx = ssl.create_default_context()
488+
ctx.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS')
489+
ctx.check_hostname = True
490+
ctx.verify_mode = ssl.CERT_REQUIRED
491+
info("LibreSSL-compatible configuration activated")
492+
493+
elif ssl_version_info >= (3, 0, 0):
494+
# OpenSSL 3.x - use modern features
495+
ctx = ssl.create_default_context()
496+
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
497+
if hasattr(ssl, 'TLSVersion') and hasattr(ssl.TLSVersion, 'TLSv1_3'):
498+
ctx.maximum_version = ssl.TLSVersion.TLSv1_3
499+
info("OpenSSL 3.x modern configuration activated")
500+
501+
elif ssl_version_info >= (1, 1, 1):
502+
# OpenSSL 1.1.1+ - proven configuration
503+
ctx = ssl.create_default_context()
504+
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
505+
info("OpenSSL 1.1.1+ standard configuration activated")
506+
507+
else:
508+
# Legacy OpenSSL - basic functionality
509+
warn("Outdated OpenSSL version detected, using legacy mode")
510+
ctx = ssl.create_default_context()
511+
ctx.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
512+
513+
# ESP-IDF DigiCert Certificate Handling
514+
parsed_url = urllib.parse.urlparse(url)
515+
domain = parsed_url.netloc.lower()
516+
517+
if domain in DL_CERT_DICT:
518+
cert_data = DL_CERT_DICT[domain]
519+
ctx.load_verify_locations(cadata=cert_data)
520+
# Disable hostname checking for custom certificates
521+
ctx.check_hostname = False
522+
ctx.verify_mode = ssl.CERT_REQUIRED
523+
info(f"✓ Custom DigiCert certificate loaded for {domain}")
524+
525+
return ctx
526+
527+
528+
def get_ssl_fallback_contexts(url: str) -> List[Tuple[str, ssl.SSLContext]]:
529+
"""
530+
Creates fallback SSL contexts for different scenarios.
531+
532+
This function provides multiple SSL context configurations that are tried
533+
in order when downloading fails. This approach maximizes compatibility
534+
across different systems and SSL configurations.
535+
536+
Args:
537+
url: The URL to create contexts for
538+
539+
Returns:
540+
List of tuples containing (config_name, ssl_context) pairs
541+
"""
542+
contexts = []
543+
544+
# 1. Primary context with backend detection
545+
try:
546+
primary_ctx = create_esp_idf_ssl_context(url)
547+
contexts.append(('esp_idf_optimized', primary_ctx))
548+
except Exception as e:
549+
warn(f"Primary SSL context failed: {e}")
550+
551+
# 2. Standard context with custom certificates
552+
try:
553+
standard_ctx = ssl.create_default_context()
554+
parsed_url = urllib.parse.urlparse(url)
555+
domain = parsed_url.netloc.lower()
556+
557+
if domain in DL_CERT_DICT:
558+
cert_data = DL_CERT_DICT[domain]
559+
standard_ctx.load_verify_locations(cadata=cert_data)
560+
standard_ctx.check_hostname = False
561+
562+
contexts.append(('standard_with_custom_cert', standard_ctx))
563+
except Exception as e:
564+
warn(f"Standard SSL context with custom cert failed: {e}")
565+
566+
# 3. System default without modifications
567+
try:
568+
system_ctx = ssl.create_default_context()
569+
contexts.append(('system_default', system_ctx))
570+
except Exception as e:
571+
warn(f"System default SSL context failed: {e}")
572+
573+
# 4. Unverified as last resort
574+
try:
575+
unverified_ctx = ssl.create_default_context()
576+
unverified_ctx.check_hostname = False
577+
unverified_ctx.verify_mode = ssl.CERT_NONE
578+
contexts.append(('unverified', unverified_ctx))
579+
except Exception as e:
580+
warn(f"Unverified SSL context failed: {e}")
581+
582+
return contexts
583+
584+
463585
def run_cmd_check_output(
464586
cmd: List[str], input_text: Optional[str] = None, extra_paths: Optional[List[str]] = None
465587
) -> bytes:
@@ -540,11 +662,53 @@ def get_file_size_sha256(filename: str, block_size: int = 65536) -> Tuple[int, s
540662
def report_progress(count: int, block_size: int, total_size: int) -> None:
541663
"""
542664
Prints progress (count * block_size * 100 / total_size) to stdout.
665+
666+
Args:
667+
count: Number of blocks downloaded
668+
block_size: Size of each block in bytes
669+
total_size: Total file size in bytes
543670
"""
544-
percent = int(count * block_size * 100 / total_size)
545-
percent = min(100, percent)
546-
sys.stdout.write('\r%d%%' % percent)
547-
sys.stdout.flush()
671+
if total_size > 0:
672+
percent = int(count * block_size * 100 / total_size)
673+
percent = min(100, percent)
674+
sys.stdout.write('\r%d%%' % percent)
675+
sys.stdout.flush()
676+
677+
def download_file_with_progress(response, destination: str) -> None:
678+
"""
679+
Downloads file from urllib response object with progress display.
680+
681+
This function replaces the manual implementation that was in the original
682+
urlretrieve_ctx function, providing progress feedback during download.
683+
684+
Args:
685+
response: urllib response object to read from
686+
destination: Local file path to save the downloaded content
687+
"""
688+
with open(destination, 'wb') as f:
689+
total_size = int(response.getheader('Content-Length', 0))
690+
downloaded = 0
691+
block_size = 8192
692+
blocknum = 0
693+
694+
if total_size > 0:
695+
info(f'File size: {total_size} bytes')
696+
697+
while True:
698+
chunk = response.read(block_size)
699+
if not chunk:
700+
break
701+
f.write(chunk)
702+
downloaded += len(chunk)
703+
blocknum += 1
704+
705+
# Show progress using report_progress() (compatible with original version)
706+
if total_size > 0:
707+
report_progress(blocknum, block_size, total_size)
708+
else:
709+
# Fallback for unknown file size
710+
sys.stdout.write(f'\r{downloaded} bytes downloaded')
711+
sys.stdout.flush()
548712

549713

550714
def mkdir_p(path: str) -> None:
@@ -595,6 +759,12 @@ def unpack(filename: str, destination: str) -> None:
595759
def splittype(url: str) -> Tuple[Optional[str], str]:
596760
"""
597761
Splits given url into its type (e.g. https, file) and the rest.
762+
763+
Args:
764+
url: URL to split
765+
766+
Returns:
767+
Tuple of (scheme, data) where scheme is lowercase protocol name or None
598768
"""
599769
match = re.match('([^/:]+):(.*)', url, re.DOTALL)
600770
if match:
@@ -603,6 +773,64 @@ def splittype(url: str) -> Tuple[Optional[str], str]:
603773
return None, url
604774

605775

776+
def classify_ssl_error(exception: Exception) -> str:
777+
"""
778+
Classifies SSL errors for better debugging output.
779+
780+
Args:
781+
exception: The exception to classify
782+
783+
Returns:
784+
String description of the error type
785+
"""
786+
error_msg = str(exception).upper()
787+
788+
if 'CERTIFICATE_VERIFY_FAILED' in error_msg:
789+
return 'Certificate verification failed'
790+
elif 'SSL_HANDSHAKE_FAILURE' in error_msg or 'HANDSHAKE_FAILURE' in error_msg:
791+
return 'SSL handshake failure'
792+
elif 'TIMEOUT' in error_msg:
793+
return 'Connection timeout'
794+
elif 'CONNECTION_RESET' in error_msg or 'CONNECTION RESET' in error_msg:
795+
return 'Connection reset by peer'
796+
elif 'HOSTNAME_MISMATCH' in error_msg:
797+
return 'Hostname mismatch'
798+
else:
799+
return f'Unknown SSL error: {str(exception)[:50]}'
800+
801+
802+
def setup_mac_certificate_paths(ctx: ssl.SSLContext) -> ssl.SSLContext:
803+
"""
804+
Add macOS specific certificate paths for better compatibility.
805+
806+
This function attempts to load certificates from various macOS-specific
807+
paths to improve SSL compatibility, especially with Homebrew installations.
808+
809+
Args:
810+
ctx: SSL context to enhance with additional certificate paths
811+
812+
Returns:
813+
Enhanced SSL context with additional certificate paths loaded
814+
"""
815+
mac_cert_paths = [
816+
'/System/Library/OpenSSL/certs/cert.pem', # System OpenSSL
817+
'/usr/local/etc/openssl/cert.pem', # Homebrew OpenSSL
818+
'/opt/homebrew/etc/openssl@3/cert.pem', # Homebrew M1/M2
819+
'/etc/ssl/cert.pem' # Generic Unix
820+
]
821+
822+
for cert_path in mac_cert_paths:
823+
if os.path.exists(cert_path):
824+
try:
825+
ctx.load_verify_locations(cert_path)
826+
info(f"Loaded certificates from: {cert_path}")
827+
break
828+
except Exception as e:
829+
warn(f"Failed to load {cert_path}: {e}")
830+
831+
return ctx
832+
833+
606834
def urlretrieve_ctx(
607835
url: str,
608836
filename: str,
@@ -659,49 +887,69 @@ def urlretrieve_ctx(
659887
def download(url: str, destination: str) -> Union[None, Exception]:
660888
"""
661889
Download from given url and save into given destination.
890+
891+
This is the new urllib-based implementation with SSL backend detection.
892+
It automatically detects the SSL backend (LibreSSL vs OpenSSL) and adapts the
893+
SSL configuration accordingly. Multiple fallback contexts are tried to maximize
894+
compatibility across different systems, especially macOS.
895+
896+
Args:
897+
url: URL to download from
898+
destination: Local file path to save to
899+
900+
Returns:
901+
None on success, Exception object on failure
662902
"""
663903
info(f'Downloading {url}')
664904
info(f'Destination: {destination}')
665-
# Try multiple SSL configurations for better Mac compatibility
666-
ssl_configs = []
667-
# First try: Custom certificates for known sites
668-
for site, cert in DL_CERT_DICT.items():
669-
if site in url:
670-
ctx = ssl.create_default_context()
671-
ctx.check_hostname = False
672-
ctx.verify_mode = ssl.CERT_NONE
673-
ctx.load_verify_locations(cadata=cert)
674-
ssl_configs.append(('custom_cert', ctx))
675-
break
676-
# Second try: Default SSL context
677-
ssl_configs.append(('default', ssl.create_default_context()))
678-
# Third try: Unverified SSL (less secure but might work on problematic systems)
679-
unverified_ctx = ssl.create_default_context()
680-
unverified_ctx.check_hostname = False
681-
unverified_ctx.verify_mode = ssl.CERT_NONE
682-
ssl_configs.append(('unverified', unverified_ctx))
683-
# Fourth try: No SSL context (for HTTP or as last resort)
684-
ssl_configs.append(('none', None))
685905

906+
# Get SSL fallback contexts for robust SSL handling
907+
ssl_contexts = get_ssl_fallback_contexts(url)
686908
last_exception = None
687-
for config_name, ctx in ssl_configs:
909+
910+
for config_name, ctx in ssl_contexts:
688911
try:
689-
if config_name != 'none':
690-
info(f'Trying SSL configuration: {config_name}')
691-
urlretrieve_ctx(url, destination, None, context=ctx)
692-
if config_name != 'none':
693-
info(f'Successfully downloaded using SSL configuration: {config_name}')
912+
info(f'Trying SSL configuration: {config_name}')
913+
914+
if url.startswith('https'):
915+
# HTTPS with specific SSL context
916+
req = urllib.request.Request(url, headers={
917+
'User-Agent': 'pioarduino'
918+
})
919+
920+
with urllib.request.urlopen(req, context=ctx, timeout=60) as response:
921+
download_file_with_progress(response, destination)
922+
923+
elif url.startswith('http'):
924+
# HTTP without SSL context
925+
urllib.request.urlretrieve(url, destination, report_progress)
926+
927+
else:
928+
# Other protocols (file://, etc.)
929+
urllib.request.urlretrieve(url, destination, report_progress)
930+
931+
info(f'Successfully downloaded using SSL configuration: {config_name}')
694932
sys.stdout.write('\rDone\n')
933+
sys.stdout.flush()
695934
return None
935+
696936
except Exception as e:
697937
last_exception = e
698-
# Only show SSL-related errors for debugging
699-
if 'SSL' in str(e) or 'CERTIFICATE' in str(e):
938+
error_msg = str(e).upper()
939+
940+
# Detect and log SSL-specific errors
941+
if any(keyword in error_msg for keyword in ['SSL', 'CERTIFICATE', 'TLS', 'HANDSHAKE']):
700942
warn(f'SSL configuration "{config_name}" failed: {str(e)[:100]}...')
943+
elif 'TIMEOUT' in error_msg:
944+
warn(f'Timeout with configuration "{config_name}"')
945+
else:
946+
warn(f'Configuration "{config_name}" failed: {str(e)[:100]}...')
701947
continue
702948

703-
# If all configurations failed, return the last exception
949+
# If all configurations failed
704950
sys.stdout.flush()
951+
error_msg = f"All SSL configurations failed. Last error: {last_exception}"
952+
warn(error_msg)
705953
return last_exception
706954

707955

0 commit comments

Comments
 (0)