Skip to content

Commit 7cdfe57

Browse files
committed
feat: add HTTPS support with configuration options in .env and update documentation
1 parent e8c9f63 commit 7cdfe57

File tree

6 files changed

+93
-15
lines changed

6 files changed

+93
-15
lines changed

.env.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ LOG_FILE_COUNT=3
2525
RUN_WEB_VIEWER=True
2626
PORT=88
2727
HOST=0.0.0.0
28+
# HTTPS support (optional)
29+
# Set HTTPS_ENABLED=true to enable HTTPS listener on HTTPS_PORT.
30+
HTTPS_ENABLED=false
31+
# Optional separate port for HTTPS. If not set, it will default to PORT + 1.
32+
HTTPS_PORT=8443
33+
# Paths to the certificate and key (PEM format)
34+
HTTPS_CERT_FILE=/path/to/fullchain.pem
35+
HTTPS_KEY_FILE=/path/to/privkey.pem
36+
# Optional password for encrypted private key
37+
HTTPS_CERT_PASSWORD=
38+
2839
DB_NAME=db/web.db
2940
ABNORMAL_DETECTION_ENABLED=true
3041
ABNORMAL_CHECK_COOLDOWN_HOURS=3

README-vi.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ Web server tích hợp cũng cung cấp API cho ứng dụng di động:
4040

4141
## Trình xem web
4242
* Biên dịch giao diện với lệnh `cd web_viewer/fe_src && yarn install && yarn build` (Bỏ qua bước này nếu bạn chạy bằng docker)
43-
* Bây giờ bạn có thể xem giao diện web LuxPower theo thời gian thực tại http://localhost:88, giao diện sẽ tương tự như hình ảnh bên dưới (URL này có thể thay đổi bằng cách chỉnh sửa biến `PORT` trong tập tin `.env`)
43+
* Bây giờ bạn có thể xem giao diện web LuxPower theo thời gian thực tại http://localhost:88 (hoặc ở cổng khác nếu bạn thay đổi `PORT` trong `.env`).
44+
* HTTPS cũng được hỗ trợ; bật bằng cách đặt `HTTPS_ENABLED=true` và cung cấp `HTTPS_PORT`, `HTTPS_CERT_FILE`, `HTTPS_KEY_FILE` trong `.env`.
4445

4546
<center>
4647
<picture style="max-width: 800px">

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ The built-in web server also exposes mobile-friendly APIs:
3939
* `GET /mobile/state` to read the current grid connection state and state-change history
4040

4141
## Web Viewer
42-
* Build FE with command `cd web_viewer/fe_src && yarn install && yarn build` (Ignore this step if you runing via docker)
43-
* Now you can see LuxPower realtime web viewer in http://locahost:88, UI layout will be similar with the image bellow (This url can be change by modify `PORT` variable in `.env` file)
42+
* Build FE with command `cd web_viewer/fe_src && yarn install && yarn build` (Ignore this step if you run via docker)
43+
* Now you can see LuxPower realtime web viewer in http://localhost:88 (or another port if you changed `PORT` in `.env`).
44+
* HTTPS is also supported; enable it by setting `HTTPS_ENABLED=true` and providing `HTTPS_PORT`, `HTTPS_CERT_FILE`, and `HTTPS_KEY_FILE` in `.env`.
4445

4546
<center>
4647
<picture style="max-width: 800px">

docker/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ COPY --from=fe_builder /fe_builder/build /app/web_viewer/build
1919

2020
# Install dependencies
2121
RUN pip install --no-cache-dir -r requirements.txt
22-
# Expose the port the app runs on
22+
# Expose the ports the app runs on
2323
EXPOSE 88
24+
EXPOSE 8443
2425

2526
VOLUME [ "/app/.env", "/app/db/web.db" ]
2627

docker/docker-compose.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ services:
1212
- ../.env:/app/.env:ro
1313
- ../db:/app/db:rw
1414
ports:
15-
- 8088:8088
15+
- 8088:8088
16+
- 8443:8443

web_viewer/__init__.py

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
from datetime import datetime, timedelta
33
import sqlite3
4+
import ssl
45
import threading
56
from aiohttp.aiohttp import web
67
from aiohttp import aiohttp
@@ -30,6 +31,45 @@ def get_db_connection() -> sqlite3.Connection:
3031
_db_conn = sqlite3.connect(db_name, check_same_thread=False)
3132
return _db_conn
3233

34+
35+
def get_ssl_context() -> Optional[ssl.SSLContext]:
36+
"""Create an SSLContext for HTTPS if configured.
37+
38+
Expects the following env vars:
39+
- HTTPS_ENABLED (true/false)
40+
- HTTPS_CERT_FILE (path to PEM cert file)
41+
- HTTPS_KEY_FILE (path to PEM key file)
42+
- HTTPS_CERT_PASSWORD (optional password for key)
43+
"""
44+
if config.get("HTTPS_ENABLED", "false").lower() != "true":
45+
return None
46+
47+
cert_file = config.get("HTTPS_CERT_FILE")
48+
key_file = config.get("HTTPS_KEY_FILE")
49+
key_password = config.get("HTTPS_CERT_PASSWORD")
50+
51+
if not cert_file or not key_file:
52+
logger.warning("HTTPS_ENABLED=true but HTTPS_CERT_FILE or HTTPS_KEY_FILE is not set; HTTPS will not start")
53+
return None
54+
55+
if not path.exists(cert_file):
56+
logger.warning("HTTPS certificate file does not exist: %s; HTTPS will not start", cert_file)
57+
return None
58+
59+
if not path.exists(key_file):
60+
logger.warning("HTTPS key file does not exist: %s; HTTPS will not start", key_file)
61+
return None
62+
63+
try:
64+
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
65+
ctx.load_cert_chain(certfile=cert_file, keyfile=key_file, password=key_password)
66+
logger.info("HTTPS enabled using cert=%s key=%s", cert_file, key_file)
67+
return ctx
68+
except Exception as e:
69+
logger.error("Failed to create SSL context for HTTPS: %s", e)
70+
return None
71+
72+
3373
def dict_factory(cursor, row):
3474
return {col[0]: row[idx] for idx, col in enumerate(cursor.description)}
3575

@@ -441,11 +481,11 @@ def create_runner():
441481
])
442482
return web.AppRunner(app, access_log=None)
443483

444-
async def start_server(host="127.0.0.1", port=1337):
484+
async def start_server(host="127.0.0.1", port=1337, ssl_context: Optional[ssl.SSLContext] = None):
445485
runner = create_runner()
446486
logger.info(f"Start server on {host}:{port}")
447487
await runner.setup()
448-
site = web.TCPSite(runner, host, port)
488+
site = web.TCPSite(runner, host, port, ssl_context=ssl_context)
449489
await site.start()
450490

451491
loop: Optional[asyncio.AbstractEventLoop] = None
@@ -455,19 +495,42 @@ def run_http_server():
455495
loop = asyncio.new_event_loop()
456496
asyncio.set_event_loop(loop)
457497
host = config.get("HOST", "127.0.0.1")
458-
port = int(config.get("PORT", 1337))
459-
# Start primary server (IPv4 or hostname)
460-
loop.run_until_complete(start_server(host=host, port=port))
498+
http_port = int(config.get("PORT", 1337))
499+
ssl_context = get_ssl_context()
500+
501+
# Start HTTP server (always on the configured PORT)
502+
loop.run_until_complete(start_server(host=host, port=http_port, ssl_context=None))
503+
504+
# If HTTPS is enabled and we successfully created an SSL context, start an HTTPS listener on a configured port.
505+
https_port = None
506+
https_port_config = config.get("HTTPS_PORT")
507+
if ssl_context is not None:
508+
if https_port_config:
509+
try:
510+
https_port = int(https_port_config)
511+
except Exception:
512+
logger.error("Invalid HTTPS_PORT value '%s'; HTTPS listener will not start.", https_port_config)
513+
https_port = None
514+
515+
if https_port is None:
516+
logger.info("HTTPS_ENABLED set but HTTPS_PORT is not configured; HTTPS listener will not start.")
517+
elif https_port == http_port:
518+
logger.warning("HTTPS_PORT is the same as PORT (%s); HTTPS will not start to avoid port conflict.", http_port)
519+
https_port = None
520+
else:
521+
loop.run_until_complete(start_server(host=host, port=https_port, ssl_context=ssl_context))
461522

462-
# If HOST_IPV6 is configured, start a second server bound to that IPv6 address
463-
# using the same port as requested.
523+
# If HOST_IPV6 is configured, also bind to that address for whichever ports are in use.
464524
host_ipv6 = config.get("HOST_IPV6")
465525
if host_ipv6:
466526
try:
467-
ipv6_port = port # explicitly use same port for both
468-
loop.run_until_complete(start_server(host=host_ipv6, port=ipv6_port))
527+
ipv6_http_port = http_port
528+
loop.run_until_complete(start_server(host=host_ipv6, port=ipv6_http_port, ssl_context=None))
529+
530+
if ssl_context is not None and https_port is not None:
531+
loop.run_until_complete(start_server(host=host_ipv6, port=https_port, ssl_context=ssl_context))
469532
except Exception as e:
470-
logger.error(f"Failed to start IPv6 server on {host_ipv6}:{port} - {e}")
533+
logger.error(f"Failed to start IPv6 server on {host_ipv6}:{http_port} - {e}")
471534
loop.run_forever()
472535

473536
class WebViewer(threading.Thread):

0 commit comments

Comments
 (0)