Skip to content

Commit d8fdbc1

Browse files
committed
[chip-tool] Add a fake local dcl server script for testing/developement purposes
1 parent 067ff58 commit d8fdbc1

File tree

1 file changed

+245
-0
lines changed

1 file changed

+245
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
#!/usr/bin/env -S python3 -B
2+
3+
# Copyright (c) 2025 Project CHIP Authors
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import base64
18+
import hashlib
19+
import http.server
20+
import json
21+
import os
22+
import re
23+
import ssl
24+
25+
DEFAULT_HOSTNAME = "localhost"
26+
DEFAULT_PORT = 4443
27+
28+
29+
TC = {
30+
0XFFF1: {
31+
0x8001: {
32+
"schemaVersion": 1,
33+
"esfRevision": 1,
34+
"defaultCountry": "US",
35+
"countryEntries": {
36+
"US": {
37+
"defaultLanguage": "en",
38+
"languageEntries": {
39+
"en": [
40+
{
41+
"ordinal": 0,
42+
"required": True,
43+
"title": "Terms and Conditions",
44+
"text": "<p><b>Feature 1 Text</b><br><br>Please accept these.</p>"
45+
},
46+
{
47+
"ordinal": 1,
48+
"required": False,
49+
"title": "Privacy Policy",
50+
"text": "<p>Feature 2 Text</p>"
51+
}
52+
],
53+
"es": [
54+
{
55+
"ordinal": 0,
56+
"required": True,
57+
"title": "Términos y condiciones",
58+
"text": "<p><b>Característica 1 Texto</b><br><br>Por favor acéptelos.</p>"
59+
},
60+
{
61+
"ordinal": 1,
62+
"required": False,
63+
"title": "Política de privacidad",
64+
"text": "<p>Característica 2 Texto</p>"
65+
}
66+
]
67+
}
68+
},
69+
"MX": {
70+
"defaultLanguage": "es",
71+
"languageEntries": {
72+
"es": [
73+
{
74+
"ordinal": 0,
75+
"required": True,
76+
"title": "Términos y condiciones",
77+
"text": "<p><b>Característica 1 Texto</b><br><br>Por favor acéptelos.</p>"
78+
}
79+
]
80+
}
81+
},
82+
"CN": {
83+
"defaultLanguage": "zh",
84+
"languageEntries": {
85+
"zh": [
86+
{
87+
"ordinal": 0,
88+
"required": True,
89+
"title": "条款和条件",
90+
"text": "<p><b>产品1文字</b></p>"
91+
},
92+
{
93+
"ordinal": 1,
94+
"required": False,
95+
"title": "隐私条款",
96+
"text": "<p><b>产品2文字</b></p>"
97+
}
98+
]
99+
}
100+
},
101+
"RU": {
102+
"defaultLanguage": "ru",
103+
"languageEntries": {
104+
"ru": [
105+
{
106+
"ordinal": 0,
107+
"required": True,
108+
"title": "Условия и положения",
109+
"text": "<p><b>Текст функции 1</b><br><br>Пожалуйста, примите эти условия пользования.</p>"
110+
},
111+
{
112+
"ordinal": 1,
113+
"required": False,
114+
"title": "Положение о конфиденциальности",
115+
"text": "<p>Текст функции 2</p>"
116+
}
117+
]
118+
}
119+
}
120+
}
121+
}
122+
}
123+
}
124+
125+
MODELS = {
126+
0XFFF1: {
127+
0x8001: {
128+
"model":
129+
{
130+
"vid": 65521,
131+
"pid": 32769,
132+
"deviceTypeId": 65535,
133+
"productName": "TEST_PRODUCT",
134+
"productLabel": "All Clusters App",
135+
"partNumber": "",
136+
"commissioningCustomFlow": 2,
137+
"commissioningCustomFlowUrl": "",
138+
"commissioningModeInitialStepsHint": 0,
139+
"commissioningModeInitialStepsInstruction": "",
140+
"commissioningModeSecondaryStepsHint": 0,
141+
"commissioningModeSecondaryStepsInstruction": "",
142+
"creator": "chip project",
143+
"lsfRevision": 0,
144+
"lsfUrl": "",
145+
"productUrl": "https://github.com/project-chip/connectedhomeip/tree/master/examples/all-clusters-app",
146+
"supportUrl": "https://github.com/project-chip/connectedhomeip/",
147+
"userManualUrl": "",
148+
"enhancedSetupFlowOptions": 1,
149+
"enhancedSetupFlowTCUrl": f"https://{DEFAULT_HOSTNAME}:{DEFAULT_PORT}/tc/65521/32769",
150+
"enhancedSetupFlowTCRevision": 1,
151+
"enhancedSetupFlowTCDigest": "",
152+
"enhancedSetupFlowTCFileSize": 0,
153+
"enhancedSetupFlowMaintenanceUrl": ""
154+
}
155+
}
156+
}
157+
}
158+
159+
160+
class RESTRequestHandler(http.server.BaseHTTPRequestHandler):
161+
def __init__(self, *args, **kwargs):
162+
self.routes = {
163+
r"/dcl/model/models/(\d+)/(\d+)": self.handle_model_request,
164+
r"/tc/(\d+)/(\d+)": self.handle_tc_request,
165+
}
166+
super().__init__(*args, **kwargs)
167+
168+
def do_GET(self):
169+
for pattern, handler in self.routes.items():
170+
match = re.match(pattern, self.path)
171+
if match:
172+
response = handler(*match.groups())
173+
if response:
174+
self.send_response(200)
175+
self.send_header("Content-Type", "application/json")
176+
self.end_headers()
177+
self.wfile.write(json.dumps(response).encode("utf-8"))
178+
return
179+
180+
# Handle 404 for unmatched paths
181+
self.send_response(404)
182+
self.send_header("Content-Type", "application/json")
183+
self.end_headers()
184+
self.wfile.write(json.dumps({"error": "Not found"}).encode("utf-8"))
185+
186+
def handle_model_request(self, vendor_id, product_id):
187+
vendor_id = int(vendor_id)
188+
product_id = int(product_id)
189+
if vendor_id in MODELS and product_id in MODELS[vendor_id]:
190+
model = MODELS[int(vendor_id)][int(product_id)]
191+
# We will return a model that contains the file size and the digest of the TC.
192+
# Instead of manually setting them, it is calculated on the fly.
193+
tc = TC[int(vendor_id)][int(product_id)]
194+
tc_encoded = json.dumps(tc).encode("utf-8")
195+
sha256_hash = hashlib.sha256(tc_encoded).digest()
196+
model['model']['enhancedSetupFlowTCFileSize'] = len(tc_encoded)
197+
model['model']['enhancedSetupFlowTCDigest'] = base64.b64encode(
198+
sha256_hash).decode("utf-8")
199+
200+
return model
201+
202+
return None
203+
204+
def handle_tc_request(self, vendor_id, product_id):
205+
vendor_id = int(vendor_id)
206+
product_id = int(product_id)
207+
if vendor_id in TC and product_id in TC[vendor_id]:
208+
return TC[int(vendor_id)][int(product_id)]
209+
210+
return None
211+
212+
213+
def run_https_server(cert_file="cert.pem", key_file="key.pem"):
214+
httpd = http.server.HTTPServer(
215+
(DEFAULT_HOSTNAME, DEFAULT_PORT), RESTRequestHandler)
216+
217+
httpd.socket = ssl.wrap_socket(
218+
httpd.socket,
219+
server_side=True,
220+
certfile=cert_file,
221+
keyfile=key_file,
222+
ssl_version=ssl.PROTOCOL_TLS,
223+
)
224+
225+
print(f"Serving on https://{DEFAULT_HOSTNAME}:{DEFAULT_PORT}")
226+
httpd.serve_forever()
227+
228+
229+
# Generate self-signed certificates if needed
230+
def generate_self_signed_cert(cert_file="cert.pem", key_file="key.pem"):
231+
from subprocess import run
232+
run([
233+
"openssl", "req", "-x509", "-nodes", "-days", "365", "-newkey", "rsa:2048",
234+
"-keyout", key_file, "-out", cert_file,
235+
"-subj", f"/C=US/ST=Test/L=Test/O=Test/OU=Test/CN={DEFAULT_HOSTNAME}"
236+
])
237+
238+
239+
# Check if certificates exist; if not, generate them
240+
if not os.path.exists("cert.pem") or not os.path.exists("key.pem"):
241+
print("Generating self-signed certificates...")
242+
generate_self_signed_cert()
243+
244+
# Run the server
245+
run_https_server()

0 commit comments

Comments
 (0)