-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathxampp-ssl-installer.py
More file actions
1197 lines (984 loc) · 46.4 KB
/
xampp-ssl-installer.py
File metadata and controls
1197 lines (984 loc) · 46.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
XAMPP Auto SSL Installer by CwmByte.com
Automatically generates and installs SSL certificates for XAMPP on Windows
Supports both self-signed certificates and Let's Encrypt with smart webroot detection
"""
import os
import sys
import subprocess
import time
import psutil
import socket
from pathlib import Path
from datetime import datetime, timedelta
import tempfile
import shutil
import requests
import json
import re
def is_admin():
"""Check if the script is running with administrator privileges."""
try:
return os.getuid() == 0
except AttributeError:
# Windows
import ctypes
return ctypes.windll.shell32.IsUserAnAdmin()
def find_xampp_path():
"""Find XAMPP installation path."""
default_paths = [
r"C:\xampp",
r"C:\Program Files\xampp",
r"C:\Program Files (x86)\xampp",
r"D:\xampp",
r"E:\xampp"
]
for path in default_paths:
if os.path.exists(path) and os.path.exists(os.path.join(path, "apache")):
return path
return None
def backup_file(file_path):
"""Create a backup of a file with timestamp."""
if os.path.exists(file_path):
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = f"{file_path}.backup_{timestamp}"
shutil.copy2(file_path, backup_path)
return backup_path
return None
def analyze_webroot_configuration(xampp_path, domain):
"""Analyze webroot configuration and detect potential issues."""
default_webroot = os.path.join(xampp_path, "htdocs")
vhosts_file = os.path.join(xampp_path, "apache", "conf", "extra", "httpd-vhosts.conf")
analysis = {
'default_webroot': default_webroot,
'domain_webroot': None,
'vhost_configured': False,
'webroot_conflict': False,
'recommended_webroot': default_webroot,
'issues': [],
'suggestions': []
}
# Check if VirtualHost file exists
if not os.path.exists(vhosts_file):
analysis['issues'].append("No VirtualHost configuration found")
analysis['suggestions'].append("Using default webroot (htdocs)")
return analysis
try:
with open(vhosts_file, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
analysis['issues'].append(f"Could not read VirtualHost file: {e}")
return analysis
# Look for VirtualHost blocks that match our domain
vhost_pattern = r'<VirtualHost[^>]*>\s*(.*?)\s*</VirtualHost>'
vhost_matches = list(re.finditer(vhost_pattern, content, re.DOTALL | re.IGNORECASE))
domain_vhosts = []
for vhost_match in vhost_matches:
vhost_content = vhost_match.group(1)
# Check if this VirtualHost is for our domain
server_name_match = re.search(r'ServerName\s+([^\s\n]+)', vhost_content, re.IGNORECASE)
server_alias_match = re.search(r'ServerAlias\s+([^\n]+)', vhost_content, re.IGNORECASE)
doc_root_match = re.search(r'DocumentRoot\s+"([^"]+)"', vhost_content, re.IGNORECASE)
if not doc_root_match:
doc_root_match = re.search(r'DocumentRoot\s+([^\s\n]+)', vhost_content, re.IGNORECASE)
# Check for port info
port_info = "unknown"
if "<VirtualHost *:80>" in vhost_match.group(0):
port_info = "HTTP (port 80)"
elif "<VirtualHost *:443>" in vhost_match.group(0):
port_info = "HTTPS (port 443)"
vhost_info = {
'server_name': server_name_match.group(1).strip() if server_name_match else None,
'server_alias': server_alias_match.group(1).strip().split() if server_alias_match else [],
'document_root': doc_root_match.group(1).strip('"') if doc_root_match else None,
'port_info': port_info,
'matches_domain': False
}
# Check if this VirtualHost matches our domain
if vhost_info['server_name'] == domain:
vhost_info['matches_domain'] = True
elif domain in vhost_info['server_alias'] or f"www.{domain}" in vhost_info['server_alias']:
vhost_info['matches_domain'] = True
if vhost_info['matches_domain'] or vhost_info['server_name'] or vhost_info['document_root']:
domain_vhosts.append(vhost_info)
analysis['vhost_configured'] = len(domain_vhosts) > 0
# Find the best matching VirtualHost
domain_match = None
for vhost in domain_vhosts:
if vhost['matches_domain'] and vhost['port_info'] == "HTTP (port 80)":
domain_match = vhost
break
if not domain_match:
for vhost in domain_vhosts:
if vhost['matches_domain']:
domain_match = vhost
break
if domain_match and domain_match['document_root']:
analysis['domain_webroot'] = domain_match['document_root']
analysis['recommended_webroot'] = domain_match['document_root']
# Check if webroot differs from default
if domain_match['document_root'] != default_webroot:
analysis['webroot_conflict'] = True
analysis['issues'].append(f"VirtualHost serves from custom directory: {domain_match['document_root']}")
analysis['suggestions'].append(f"Let's Encrypt challenge must use VirtualHost webroot")
# Additional analysis
if analysis['vhost_configured'] and not domain_match:
analysis['issues'].append(f"VirtualHost configured but no exact match for domain '{domain}'")
analysis['suggestions'].append("May need to update VirtualHost ServerName or create new VirtualHost")
return analysis
def get_optimal_webroot(xampp_path, domain):
"""Get the optimal webroot path for Let's Encrypt challenges."""
analysis = analyze_webroot_configuration(xampp_path, domain)
print(f"\nWebroot Analysis for {domain}:")
print("=" * 40)
if analysis['vhost_configured']:
print("✓ VirtualHost configuration detected")
if analysis['webroot_conflict']:
print(f"⚠ Domain serves from: {analysis['domain_webroot']}")
print(f" (not default htdocs)")
else:
print(f"✓ Domain uses standard webroot: {analysis['recommended_webroot']}")
else:
print("ℹ No VirtualHost configuration found")
print(" Using default XAMPP webroot")
if analysis['issues']:
print(f"\nDetected Issues:")
for issue in analysis['issues']:
print(f" • {issue}")
if analysis['suggestions']:
print(f"\nRecommendations:")
for suggestion in analysis['suggestions']:
print(f" • {suggestion}")
# Verify webroot exists and is accessible
webroot = analysis['recommended_webroot']
if not os.path.exists(webroot):
print(f"\n⚠ Webroot directory doesn't exist: {webroot}")
create_dir = input("Create this directory? (y/n): ").strip().lower()
if create_dir == 'y':
try:
os.makedirs(webroot, exist_ok=True)
print(f"✓ Created directory: {webroot}")
except Exception as e:
print(f"✗ Failed to create directory: {e}")
print(" Falling back to default webroot")
webroot = analysis['default_webroot']
else:
print(" Using default webroot instead")
webroot = analysis['default_webroot']
return webroot, analysis
def test_webroot_accessibility(domain, webroot):
"""Test if the webroot is accessible via HTTP."""
test_dir = os.path.join(webroot, ".well-known", "acme-challenge")
os.makedirs(test_dir, exist_ok=True)
test_file = os.path.join(test_dir, "test-accessibility")
test_content = f"webroot-test-{int(time.time())}"
try:
# Create test file
with open(test_file, 'w') as f:
f.write(test_content)
# Test HTTP access
test_url = f"http://{domain}/.well-known/acme-challenge/test-accessibility"
print(f" Testing webroot accessibility: {test_url}")
try:
response = requests.get(test_url, timeout=10)
if response.status_code == 200 and response.text.strip() == test_content:
print(" ✓ Webroot is accessible via HTTP")
return True, "Webroot accessible"
else:
print(f" ✗ HTTP test failed (Status: {response.status_code})")
return False, f"HTTP test failed: {response.status_code}"
except requests.exceptions.ConnectionError:
print(" ✗ Connection refused - server may not be accessible")
return False, "Connection refused"
except requests.exceptions.Timeout:
print(" ✗ Request timeout - server not responding")
return False, "Request timeout"
except Exception as e:
print(f" ✗ HTTP test error: {e}")
return False, f"HTTP test error: {e}"
except Exception as e:
print(f" ✗ Could not create test file: {e}")
return False, f"File creation failed: {e}"
finally:
# Clean up test file
try:
if os.path.exists(test_file):
os.remove(test_file)
except:
pass
def handle_webroot_issues(domain, webroot, analysis):
"""Handle webroot configuration issues with user guidance."""
print(f"\nWebroot Configuration Options:")
print("=" * 40)
if analysis['webroot_conflict']:
print("Your domain uses a custom document root in VirtualHost configuration.")
print("For Let's Encrypt to work, challenge files must be placed in the correct location.")
print()
print("Options:")
print(f"1. Use VirtualHost webroot: {analysis['domain_webroot']}")
print(f"2. Use default webroot: {analysis['default_webroot']}")
print("3. Let me configure this automatically (recommended)")
print()
choice = input("Choose option (1/2/3): ").strip()
if choice == "2":
return analysis['default_webroot']
elif choice == "3":
print("\nAutomatic configuration selected...")
print("This will use your VirtualHost webroot for optimal compatibility")
return analysis['domain_webroot']
else:
return analysis['domain_webroot']
return webroot
def check_vhost_ssl_conflicts(xampp_path, domain):
"""Check and temporarily fix VirtualHost SSL conflicts."""
vhosts_file = os.path.join(xampp_path, "apache", "conf", "extra", "httpd-vhosts.conf")
if not os.path.exists(vhosts_file):
print(" No VirtualHost file found - no conflicts")
return True, "No VirtualHost conflicts", None
# Read current configuration
try:
with open(vhosts_file, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
return False, f"Could not read VirtualHost file: {e}", None
# Check for SSL VirtualHost that might reference non-existent certificates
ssl_vhost_patterns = [
r'<VirtualHost[^>]*:443[^>]*>.*?</VirtualHost>',
r'<VirtualHost[^>]*\*:443[^>]*>.*?</VirtualHost>'
]
has_ssl_conflicts = False
ssl_vhosts = []
for pattern in ssl_vhost_patterns:
matches = re.finditer(pattern, content, re.DOTALL | re.IGNORECASE)
for match in matches:
vhost_content = match.group(0)
# Check if this VirtualHost references certificate files that don't exist
cert_file_matches = re.findall(r'SSLCertificateFile\s+"([^"]+)"', vhost_content)
key_file_matches = re.findall(r'SSLCertificateKeyFile\s+"([^"]+)"', vhost_content)
for cert_ref in cert_file_matches + key_file_matches:
# Convert relative paths to absolute
if not os.path.isabs(cert_ref):
cert_path = os.path.join(xampp_path, "apache", cert_ref)
else:
cert_path = cert_ref
if not os.path.exists(cert_path):
has_ssl_conflicts = True
ssl_vhosts.append(match)
print(f" Found SSL VirtualHost referencing missing file: {cert_ref}")
break
if not has_ssl_conflicts:
print(" No SSL VirtualHost conflicts found")
return True, "No SSL conflicts", None
# Create backup before making changes
print(" Creating backup of VirtualHost configuration...")
backup_path = backup_file(vhosts_file)
if backup_path:
print(f" Backup created: {os.path.basename(backup_path)}")
# Temporarily disable problematic SSL VirtualHosts
print(" Temporarily disabling SSL VirtualHosts with missing certificates...")
modified_content = content
for match in reversed(ssl_vhosts): # Reverse to maintain positions
vhost_block = match.group(0)
commented_block = '\n'.join(f'# {line}' if line.strip() else '#'
for line in vhost_block.split('\n'))
commented_block = f'\n# SSL VirtualHost temporarily disabled by SSL installer\n{commented_block}\n# End SSL VirtualHost disable\n'
modified_content = (modified_content[:match.start()] +
commented_block +
modified_content[match.end():])
# Write modified configuration
try:
with open(vhosts_file, 'w', encoding='utf-8') as f:
f.write(modified_content)
print(f" Disabled {len(ssl_vhosts)} SSL VirtualHost(s)")
return True, f"Temporarily disabled {len(ssl_vhosts)} SSL VirtualHost(s)", backup_path
except Exception as e:
return False, f"Could not modify VirtualHost file: {e}", backup_path
def restore_ssl_vhosts(xampp_path):
"""Restore SSL VirtualHosts after certificates are created."""
vhosts_file = os.path.join(xampp_path, "apache", "conf", "extra", "httpd-vhosts.conf")
if not os.path.exists(vhosts_file):
return True, "No VirtualHost file to restore"
try:
with open(vhosts_file, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
return False, f"Could not read VirtualHost file: {e}"
# Find and restore commented SSL VirtualHosts
pattern = r'# SSL VirtualHost temporarily disabled by SSL installer\n((?:# .*\n)+)# End SSL VirtualHost disable\n'
matches = list(re.finditer(pattern, content))
if not matches:
print(" No SSL VirtualHosts to restore")
return True, "No SSL VirtualHosts to restore"
print(f" Restoring {len(matches)} SSL VirtualHost(s)...")
modified_content = content
for match in reversed(matches): # Reverse to maintain positions
commented_block = match.group(1)
# Uncomment the block
restored_block = '\n'.join(line[2:] if line.startswith('# ') else line[1:] if line.startswith('#') else line
for line in commented_block.split('\n') if line.strip())
modified_content = (modified_content[:match.start()] +
restored_block + '\n' +
modified_content[match.end():])
# Write restored configuration
try:
with open(vhosts_file, 'w', encoding='utf-8') as f:
f.write(modified_content)
print(f" Restored {len(matches)} SSL VirtualHost(s)")
return True, f"Restored {len(matches)} SSL VirtualHost(s)"
except Exception as e:
return False, f"Could not restore VirtualHost file: {e}"
def check_processes():
"""Check if Apache and XAMPP Control Panel are running."""
apache_running = False
xampp_control_running = False
apache_processes = []
xampp_processes = []
print("Checking running processes...")
for proc in psutil.process_iter(['pid', 'name', 'exe']):
try:
proc_name = proc.info['name'].lower()
proc_exe = proc.info['exe']
# Check for Apache processes
if any(name in proc_name for name in ['httpd', 'apache']):
apache_running = True
apache_processes.append(f" • {proc.info['name']} (PID: {proc.info['pid']})")
# Check for XAMPP Control Panel
if 'xampp' in proc_name and 'control' in proc_name:
xampp_control_running = True
xampp_processes.append(f" • {proc.info['name']} (PID: {proc.info['pid']})")
elif proc_exe and 'xampp-control' in proc_exe.lower():
xampp_control_running = True
xampp_processes.append(f" • {proc.info['name']} (PID: {proc.info['pid']})")
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
return apache_running, xampp_control_running, apache_processes, xampp_processes
def stop_apache_processes():
"""Stop all Apache processes."""
print("Stopping Apache processes...")
stopped_processes = []
for proc in psutil.process_iter(['pid', 'name']):
try:
proc_name = proc.info['name'].lower()
if any(name in proc_name for name in ['httpd', 'apache']):
print(f" Stopping {proc.info['name']} (PID: {proc.info['pid']})")
proc.terminate()
stopped_processes.append(proc.info['pid'])
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
# Wait for processes to stop
if stopped_processes:
print(" Waiting for processes to stop...")
time.sleep(3)
return len(stopped_processes) > 0
def start_apache(xampp_path, with_ssl=False):
"""Start Apache with or without SSL support."""
apache_exe = os.path.join(xampp_path, "apache", "bin", "httpd.exe")
if not os.path.exists(apache_exe):
return False, "Apache executable not found"
try:
print(f"Starting Apache {'with SSL support' if with_ssl else '(HTTP only)'}...")
# Start Apache
if with_ssl:
subprocess.Popen([apache_exe, "-D", "SSL"],
cwd=os.path.join(xampp_path, "apache", "bin"))
else:
subprocess.Popen([apache_exe],
cwd=os.path.join(xampp_path, "apache", "bin"))
# Wait and check if Apache started
time.sleep(5)
for i in range(10):
for proc in psutil.process_iter(['name']):
try:
if 'httpd' in proc.info['name'].lower():
return True, "Apache started successfully"
except:
continue
time.sleep(1)
return False, "Apache did not start within expected time"
except Exception as e:
return False, f"Failed to start Apache: {str(e)}"
def test_apache_config(xampp_path):
"""Test Apache configuration for syntax errors."""
apache_exe = os.path.join(xampp_path, "apache", "bin", "httpd.exe")
if not os.path.exists(apache_exe):
return False, "Apache executable not found"
try:
print("Testing Apache configuration...")
result = subprocess.run([apache_exe, "-t"],
capture_output=True, text=True,
cwd=os.path.join(xampp_path, "apache", "bin"))
if result.returncode == 0:
print(" Apache configuration test passed")
return True, "Configuration OK"
else:
print(f" Apache configuration test failed:")
print(f" {result.stderr}")
return False, f"Configuration error: {result.stderr}"
except Exception as e:
return False, f"Could not test configuration: {str(e)}"
def check_domain_accessibility(domain):
"""Check if domain points to this server and is publicly accessible."""
if domain in ['localhost', '127.0.0.1', '::1']:
return False, "Localhost domains cannot use Let's Encrypt certificates"
try:
# Check DNS resolution
import socket
ip = socket.gethostbyname(domain)
print(f" Domain {domain} resolves to: {ip}")
# Try to connect to the domain on port 80
try:
response = requests.get(f"http://{domain}/.well-known/acme-challenge/test",
timeout=10, allow_redirects=False)
return True, "Domain is publicly accessible"
except requests.exceptions.ConnectionError:
return True, "Domain resolves but may not be serving HTTP yet (this is OK)"
except requests.exceptions.Timeout:
return False, "Domain is not accessible (timeout)"
except Exception as e:
return True, "Domain resolves, proceeding with Let's Encrypt"
except socket.gaierror:
return False, f"Domain {domain} does not resolve to any IP address"
except Exception as e:
return False, f"Error checking domain: {str(e)}"
def install_certbot():
"""Install Certbot if not present."""
try:
# Check if certbot is already installed
result = subprocess.run(['certbot', '--version'], capture_output=True, text=True)
if result.returncode == 0:
print(" Certbot is already installed")
return True, "Certbot already available"
except FileNotFoundError:
pass
print("Installing Certbot...")
# Try to install via pip
try:
print(" Installing certbot via pip...")
subprocess.run([sys.executable, '-m', 'pip', 'install', 'certbot'],
capture_output=True, text=True, check=True)
print(" Certbot installed via pip")
return True, "Certbot installed successfully"
except subprocess.CalledProcessError as e:
return False, f"Failed to install Certbot: {e.stderr}"
def get_lets_encrypt_certificate(domain, xampp_path, email=None):
"""Get certificate from Let's Encrypt with smart webroot detection."""
# Check if domain is accessible
accessible, message = check_domain_accessibility(domain)
if not accessible:
return False, message, None, None
print(f" {message}")
# Install Certbot if needed
success, install_msg = install_certbot()
if not success:
return False, install_msg, None, None
# Smart webroot detection and configuration
print("\nAnalyzing webroot configuration...")
webroot_path, analysis = get_optimal_webroot(xampp_path, domain)
# Handle any webroot issues
if analysis['webroot_conflict'] or analysis['issues']:
webroot_path = handle_webroot_issues(domain, webroot_path, analysis)
print(f"\nUsing webroot: {webroot_path}")
# Check and fix VirtualHost conflicts
print("\nChecking VirtualHost configuration for SSL conflicts...")
vhost_success, vhost_msg, vhost_backup = check_vhost_ssl_conflicts(xampp_path, domain)
if not vhost_success:
return False, f"VirtualHost conflict resolution failed: {vhost_msg}", None, None
print(f" {vhost_msg}")
# Test Apache configuration
config_ok, config_msg = test_apache_config(xampp_path)
if not config_ok:
return False, f"Apache configuration error: {config_msg}", None, None
# Ensure Apache is running
apache_running, _, _, _ = check_processes()
if apache_running:
print(" Stopping Apache to restart without SSL conflicts...")
stop_apache_processes()
time.sleep(3)
print(" Starting Apache for Let's Encrypt challenge...")
success, msg = start_apache(xampp_path, with_ssl=False)
if not success:
return False, f"Cannot start Apache for Let's Encrypt: {msg}", None, None
time.sleep(3)
# Test webroot accessibility
print("\nTesting webroot accessibility...")
accessible, access_msg = test_webroot_accessibility(domain, webroot_path)
if not accessible:
print(f" Warning: {access_msg}")
print(" Continuing anyway - this might be normal depending on your setup")
# Get email if not provided
if not email:
email = input(" Enter email address for Let's Encrypt account: ").strip()
if not email or '@' not in email:
return False, "Valid email address required for Let's Encrypt", None, None
# Setup challenge directory
well_known_path = os.path.join(webroot_path, ".well-known", "acme-challenge")
os.makedirs(well_known_path, exist_ok=True)
try:
# Run certbot with correct webroot
certbot_cmd = [
'certbot', 'certonly',
'--webroot', '--webroot-path', webroot_path, # Using detected webroot
'--email', email,
'--agree-tos',
'--no-eff-email',
'--domains', domain,
'--non-interactive',
'--verbose'
]
print(f" Requesting certificate from Let's Encrypt for {domain}...")
print(f" Using webroot: {webroot_path}")
print(" This may take a few minutes...")
result = subprocess.run(certbot_cmd, capture_output=True, text=True)
if result.returncode == 0:
print(" Let's Encrypt certificate obtained successfully!")
# Find certificate files
possible_cert_dirs = [
f"C:\\Certbot\\live\\{domain}",
f"C:\\Program Files\\Certbot\\live\\{domain}",
os.path.expanduser(f"~\\.certbot\\live\\{domain}"),
os.path.join(os.environ.get('LOCALAPPDATA', ''), f"certbot\\live\\{domain}"),
f"/etc/letsencrypt/live/{domain}",
]
cert_file = None
key_file = None
for cert_dir in possible_cert_dirs:
if os.path.exists(cert_dir):
potential_cert = os.path.join(cert_dir, "fullchain.pem")
potential_key = os.path.join(cert_dir, "privkey.pem")
if os.path.exists(potential_cert) and os.path.exists(potential_key):
cert_file = potential_cert
key_file = potential_key
print(f" Found certificates in: {cert_dir}")
break
if cert_file and key_file:
return True, "Let's Encrypt certificate obtained successfully", cert_file, key_file
else:
return False, "Certificate obtained but files not found in expected locations", None, None
else:
error_msg = result.stderr or result.stdout
print(f" Certbot failed with webroot: {webroot_path}")
print(f" Error: {error_msg}")
return False, f"Certbot failed: {error_msg}", None, None
except Exception as e:
return False, f"Error running Certbot: {str(e)}", None, None
def create_openssl_config(domain, config_file):
"""Create OpenSSL configuration file."""
print(f" Creating SSL config for domain: {domain}")
config_content = f"""[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
C = US
ST = State
L = City
O = {domain} Certificate
OU = SSL Certificate
CN = {domain}
[v3_req]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = {domain}
DNS.2 = www.{domain}
DNS.3 = localhost
IP.1 = 127.0.0.1
IP.2 = ::1
"""
with open(config_file, 'w') as f:
f.write(config_content)
print(f" SSL config created with CN: {domain}")
def generate_certificate(openssl_path, cert_file, key_file, domain):
"""Generate SSL certificate and private key."""
try:
# Create temporary config file
with tempfile.NamedTemporaryFile(mode='w', suffix='.cnf', delete=False) as config_file:
create_openssl_config(domain, config_file.name)
config_path = config_file.name
print(f"Generating private key for {domain}...")
# Generate private key
result = subprocess.run([openssl_path, "genrsa", "-out", key_file, "2048"],
capture_output=True, text=True)
if result.returncode != 0:
return False, f"Failed to generate private key: {result.stderr}"
print(f"Generating SSL certificate for {domain}...")
# Generate certificate
result = subprocess.run([openssl_path, "req", "-new", "-x509",
"-key", key_file, "-out", cert_file,
"-days", "365", "-config", config_path],
capture_output=True, text=True)
# Clean up config file
os.unlink(config_path)
if result.returncode != 0:
return False, f"Failed to generate certificate: {result.stderr}"
return True, "Certificate generated successfully"
except Exception as e:
return False, f"Exception during certificate generation: {str(e)}"
def copy_letsencrypt_to_xampp(le_cert_file, le_key_file, xampp_cert_file, xampp_key_file):
"""Copy Let's Encrypt certificates to XAMPP directories."""
try:
# Copy certificate
shutil.copy2(le_cert_file, xampp_cert_file)
print(f" Copied certificate to {xampp_cert_file}")
# Copy private key
shutil.copy2(le_key_file, xampp_key_file)
print(f" Copied private key to {xampp_key_file}")
return True, "Certificates copied successfully"
except Exception as e:
return False, f"Error copying certificates: {str(e)}"
def setup_auto_renewal(domain, xampp_path, email):
"""Setup automatic certificate renewal."""
try:
# Find certificate directory
possible_cert_dirs = [
f"C:\\Certbot\\live\\{domain}",
f"C:\\Program Files\\Certbot\\live\\{domain}",
os.path.expanduser(f"~\\.certbot\\live\\{domain}"),
os.path.join(os.environ.get('LOCALAPPDATA', ''), f"certbot\\live\\{domain}"),
]
cert_dir = None
for dir_path in possible_cert_dirs:
if os.path.exists(dir_path):
cert_dir = dir_path
break
if not cert_dir:
return False, "Could not locate certificate directory for renewal"
# Create renewal script
renewal_script = f"""@echo off
REM Auto-renewal script for {domain} - Created by XAMPP Auto SSL Installer
REM https://cwmbyte.com
echo ================================================
echo XAMPP SSL Certificate Auto-Renewal for {domain}
echo ================================================
echo Checking for certificate renewal...
REM Run certbot renewal
certbot renew --quiet --webroot
if %ERRORLEVEL% == 0 (
echo Certificate renewed successfully, copying to XAMPP...
REM Copy renewed certificates to XAMPP
copy /Y "{os.path.join(cert_dir, 'fullchain.pem')}" "{os.path.join(xampp_path, 'apache', 'conf', 'ssl.crt', f'{domain}.crt')}"
copy /Y "{os.path.join(cert_dir, 'privkey.pem')}" "{os.path.join(xampp_path, 'apache', 'conf', 'ssl.key', f'{domain}.key')}"
REM Restart Apache to load new certificates
echo Restarting Apache...
taskkill /F /IM httpd.exe >nul 2>&1
timeout /t 5 /nobreak >nul
start "" "{os.path.join(xampp_path, 'apache', 'bin', 'httpd.exe')}" -D SSL
echo Certificate renewal and Apache restart complete!
) else (
echo No renewal needed or renewal failed
)
echo.
echo Last checked: %date% %time%
pause
"""
script_path = os.path.join(xampp_path, f"ssl_renew_{domain.replace('.', '_')}.bat")
with open(script_path, 'w') as f:
f.write(renewal_script)
print(f" Created renewal script: {script_path}")
return True, "Auto-renewal script created"
except Exception as e:
return False, f"Error creating renewal script: {str(e)}"
def configure_apache_ssl(xampp_path, domain):
"""Configure Apache to use the SSL certificate."""
print("Configuring Apache SSL settings...")
# Paths
httpd_conf = os.path.join(xampp_path, "apache", "conf", "httpd.conf")
ssl_conf = os.path.join(xampp_path, "apache", "conf", "extra", "httpd-ssl.conf")
# Backup original files
httpd_backup = backup_file(httpd_conf)
ssl_backup = backup_file(ssl_conf)
if httpd_backup:
print(f" Backed up httpd.conf to {os.path.basename(httpd_backup)}")
if ssl_backup:
print(f" Backed up httpd-ssl.conf to {os.path.basename(ssl_backup)}")
# Enable SSL module and configuration in httpd.conf
if os.path.exists(httpd_conf):
with open(httpd_conf, 'r', encoding='utf-8') as f:
content = f.read()
# Enable SSL module
content = content.replace('#LoadModule ssl_module', 'LoadModule ssl_module')
# Enable SSL configuration
content = content.replace('#Include conf/extra/httpd-ssl.conf', 'Include conf/extra/httpd-ssl.conf')
with open(httpd_conf, 'w', encoding='utf-8') as f:
f.write(content)
print(" Enabled SSL module and configuration")
# Update certificate paths in httpd-ssl.conf
if os.path.exists(ssl_conf):
with open(ssl_conf, 'r', encoding='utf-8') as f:
content = f.read()
# Update certificate file paths
content = re.sub(r'SSLCertificateFile\s+.*', f'SSLCertificateFile "conf/ssl.crt/{domain}.crt"', content)
content = re.sub(r'SSLCertificateKeyFile\s+.*', f'SSLCertificateKeyFile "conf/ssl.key/{domain}.key"', content)
with open(ssl_conf, 'w', encoding='utf-8') as f:
f.write(content)
print(" Updated SSL certificate paths")
def main():
print("=" * 60)
print("XAMPP Auto SSL Installer by CwmByte.com")
print("Automatically installs SSL certificates for XAMPP")
print("Supports Let's Encrypt and self-signed certificates")
print("=" * 60)
print()
# Initialize variables
use_letsencrypt = False
# Check admin privileges
if not is_admin():
print("ERROR: This script must be run as Administrator!")
print(" Right-click and select 'Run as administrator'")
input("\nPress Enter to exit...")
sys.exit(1)
# Find XAMPP installation
xampp_path = find_xampp_path()
if not xampp_path:
xampp_path = input("XAMPP not found in standard locations. Enter XAMPP path: ").strip()
if not os.path.exists(xampp_path):
print("ERROR: XAMPP path not found!")
input("\nPress Enter to exit...")
sys.exit(1)
print(f"Found XAMPP installation at: {xampp_path}")
# Check OpenSSL
openssl_path = os.path.join(xampp_path, "apache", "bin", "openssl.exe")
if not os.path.exists(openssl_path):
print("ERROR: OpenSSL not found in XAMPP installation!")
input("\nPress Enter to exit...")
sys.exit(1)
# Check running processes
apache_running, xampp_control_running, apache_procs, xampp_procs = check_processes()
if apache_running:
print("Apache is RUNNING:")
for proc in apache_procs:
print(proc)
print(" -> Will need restart after SSL setup")
else:
print("Apache is NOT running")
if xampp_control_running:
print("XAMPP Control Panel is RUNNING:")
for proc in xampp_procs:
print(proc)
else:
print("XAMPP Control Panel is NOT running")
print()
# Get domain name
domain = input("Enter domain name (default: localhost): ").strip()
if not domain:
domain = "localhost"
# Setup certificate paths
cert_path = os.path.join(xampp_path, "apache", "conf", "ssl.crt")
key_path = os.path.join(xampp_path, "apache", "conf", "ssl.key")
# Create directories
os.makedirs(cert_path, exist_ok=True)
os.makedirs(key_path, exist_ok=True)
cert_file = os.path.join(cert_path, f"{domain}.crt")
key_file = os.path.join(key_path, f"{domain}.key")
# Ask about certificate type
print("\nCertificate Options:")
print("1. Self-signed certificate (works immediately, browser warnings)")
print("2. Let's Encrypt certificate (trusted by browsers, requires public domain)")
print()
if domain in ['localhost', '127.0.0.1', '::1']:
print("Note: Let's Encrypt cannot issue certificates for localhost")
cert_choice = "1"
print(" Automatically selecting self-signed certificate")
else:
cert_choice = input("Choose certificate type (1/2): ").strip()
use_letsencrypt = cert_choice == "2"
if use_letsencrypt:
print(f"\nLet's Encrypt Certificate Setup for {domain}")
print("Requirements:")
print(f" • Domain {domain} must point to this server's public IP")
print(" • Port 80 must be accessible from the internet")
print(" • Apache must be running to serve the HTTP-01 challenge")
print(" • Valid email address for Let's Encrypt account")
print()
proceed = input("Continue with Let's Encrypt? (y/n): ").strip().lower()
if proceed != 'y':
use_letsencrypt = False
print(" Falling back to self-signed certificate")
# Generate certificate based on choice
if use_letsencrypt:
print(f"\nGetting Let's Encrypt certificate for: {domain}")
success, message, le_cert_file, le_key_file = get_lets_encrypt_certificate(domain, xampp_path)
if success and le_cert_file and le_key_file:
print(f"{message}")
# Copy Let's Encrypt certificates to XAMPP
copy_success, copy_message = copy_letsencrypt_to_xampp(
le_cert_file, le_key_file, cert_file, key_file
)
if copy_success:
print(f"{copy_message}")
# Setup auto-renewal
print("\nSetting up automatic certificate renewal...")
email = input("Enter email for renewal notifications (optional): ").strip()
if email and '@' in email: