Compare commits
5 Commits
f5c44cb323
...
5f16ec48aa
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f16ec48aa | |||
| 93f7c9cb4e | |||
| a17a7f8027 | |||
| f8de824752 | |||
| d886a8a523 |
@@ -36,6 +36,15 @@ jobs:
|
||||
run: |
|
||||
set -o pipefail
|
||||
mkdir -p build
|
||||
python3 -u tools/tls_fixture.py > build/tls-fixture.log 2>&1 &
|
||||
TLS_PID=$!
|
||||
trap 'kill "$TLS_PID" 2>/dev/null || true' EXIT
|
||||
for attempt in {1..30}; do
|
||||
if grep -q 'TLS fixtures ready' build/tls-fixture.log; then break; fi
|
||||
if ! kill -0 "$TLS_PID" 2>/dev/null; then cat build/tls-fixture.log; exit 1; fi
|
||||
sleep 1
|
||||
done
|
||||
grep -q 'TLS fixtures ready' build/tls-fixture.log || { cat build/tls-fixture.log; exit 1; }
|
||||
xcodebuild test \
|
||||
-project MusicBridge.xcodeproj \
|
||||
-scheme MusicBridge \
|
||||
@@ -53,6 +62,7 @@ jobs:
|
||||
name: ios-test-results
|
||||
path: |
|
||||
build/xcodebuild.log
|
||||
build/tls-fixture.log
|
||||
build/MusicBridge.xcresult
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
|
||||
@@ -81,11 +81,21 @@
|
||||
"isa" = "PBXBuildFile";
|
||||
"fileRef" = "ACD0393DCDDDF044F2966340";
|
||||
};
|
||||
"8BED860F1BD59C500D215E8D" = {
|
||||
"isa" = "PBXFileReference";
|
||||
"lastKnownFileType" = "sourcecode.swift";
|
||||
"path" = "TLSTests.swift";
|
||||
"sourceTree" = "<group>";
|
||||
};
|
||||
"DC1A5B0B94F4C98F1CA36676" = {
|
||||
"isa" = "PBXBuildFile";
|
||||
"fileRef" = "8BED860F1BD59C500D215E8D";
|
||||
};
|
||||
"E1AD768E105BF42350625991" = {
|
||||
"isa" = "PBXGroup";
|
||||
"path" = "MusicBridgeTests";
|
||||
"sourceTree" = "<group>";
|
||||
"children" = ("ACD0393DCDDDF044F2966340", "F16D05EC6B29248D2C61ADB1",);
|
||||
"children" = ("ACD0393DCDDDF044F2966340", "8BED860F1BD59C500D215E8D", "F16D05EC6B29248D2C61ADB1", "4731785CAB9BB914B8834AE6",);
|
||||
};
|
||||
"F16D05EC6B29248D2C61ADB1" = {
|
||||
"isa" = "PBXFileReference";
|
||||
@@ -97,6 +107,16 @@
|
||||
"isa" = "PBXBuildFile";
|
||||
"fileRef" = "F16D05EC6B29248D2C61ADB1";
|
||||
};
|
||||
"4731785CAB9BB914B8834AE6" = {
|
||||
"isa" = "PBXFileReference";
|
||||
"lastKnownFileType" = "text.json";
|
||||
"path" = "tls-fixture.json";
|
||||
"sourceTree" = "<group>";
|
||||
};
|
||||
"7D079549A35384F9CAE8A5C4" = {
|
||||
"isa" = "PBXBuildFile";
|
||||
"fileRef" = "4731785CAB9BB914B8834AE6";
|
||||
};
|
||||
"7C35A0276A762371ABAE5596" = {
|
||||
"isa" = "PBXFileReference";
|
||||
"explicitFileType" = "wrapper.application";
|
||||
@@ -136,8 +156,8 @@
|
||||
"CODE_SIGN_STYLE" = "Automatic";
|
||||
"GENERATE_INFOPLIST_FILE" = "NO";
|
||||
"INFOPLIST_FILE" = "MusicBridge/Info.plist";
|
||||
"CURRENT_PROJECT_VERSION" = "1";
|
||||
"MARKETING_VERSION" = "0.1.0";
|
||||
"CURRENT_PROJECT_VERSION" = "2";
|
||||
"MARKETING_VERSION" = "0.1.1";
|
||||
"SWIFT_OPTIMIZATION_LEVEL" = "-Onone";
|
||||
"ENABLE_TESTABILITY" = "YES";
|
||||
"SWIFT_ACTIVE_COMPILATION_CONDITIONS" = "DEBUG";
|
||||
@@ -157,8 +177,8 @@
|
||||
"CODE_SIGN_STYLE" = "Automatic";
|
||||
"GENERATE_INFOPLIST_FILE" = "NO";
|
||||
"INFOPLIST_FILE" = "MusicBridge/Info.plist";
|
||||
"CURRENT_PROJECT_VERSION" = "1";
|
||||
"MARKETING_VERSION" = "0.1.0";
|
||||
"CURRENT_PROJECT_VERSION" = "2";
|
||||
"MARKETING_VERSION" = "0.1.1";
|
||||
"SWIFT_OPTIMIZATION_LEVEL" = "-O";
|
||||
};
|
||||
};
|
||||
@@ -189,7 +209,7 @@
|
||||
"AB453A3F0E1490D48374A69A" = {
|
||||
"isa" = "PBXSourcesBuildPhase";
|
||||
"buildActionMask" = 2147483647;
|
||||
"files" = ("17E248E58CB43C680B48DD51",);
|
||||
"files" = ("17E248E58CB43C680B48DD51", "DC1A5B0B94F4C98F1CA36676",);
|
||||
"runOnlyForDeploymentPostprocessing" = 0;
|
||||
};
|
||||
"9AA2A4C344B5C474AF5D336C" = {
|
||||
@@ -201,7 +221,7 @@
|
||||
"5D92C2C610AA78B83DDF2133" = {
|
||||
"isa" = "PBXResourcesBuildPhase";
|
||||
"buildActionMask" = 2147483647;
|
||||
"files" = ("F8A2DE539EBA355A3807D729",);
|
||||
"files" = ("F8A2DE539EBA355A3807D729", "7D079549A35384F9CAE8A5C4",);
|
||||
"runOnlyForDeploymentPostprocessing" = 0;
|
||||
};
|
||||
"DAB01D73CBBCF1FEDE84E2D6" = {
|
||||
|
||||
@@ -9,6 +9,9 @@ final class PinnedDelegate: NSObject, URLSessionDelegate, URLSessionTaskDelegate
|
||||
|
||||
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
#if DEBUG
|
||||
print("MusicBridge TLS: received server authentication challenge")
|
||||
#endif
|
||||
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
|
||||
challenge.protectionSpace.host == connection.endpoint.host,
|
||||
challenge.protectionSpace.port == (connection.endpoint.port ?? 443),
|
||||
@@ -19,18 +22,31 @@ final class PinnedDelegate: NSObject, URLSessionDelegate, URLSessionTaskDelegate
|
||||
let digest = SHA256.hash(data: SecCertificateCopyData(leaf) as Data)
|
||||
.map { String(format: "%02X", $0) }.joined()
|
||||
guard digest == connection.fingerprint else {
|
||||
#if DEBUG
|
||||
print("MusicBridge TLS: fingerprint mismatch")
|
||||
#endif
|
||||
completionHandler(.cancelAuthenticationChallenge, nil); return
|
||||
}
|
||||
// Trust only this exact out-of-band certificate, while checking its validity dates.
|
||||
SecTrustSetAnchorCertificates(trust, [leaf] as CFArray)
|
||||
SecTrustSetAnchorCertificatesOnly(trust, true)
|
||||
SecTrustSetPolicies(trust, SecPolicyCreateBasicX509())
|
||||
guard SecTrustEvaluateWithError(trust, nil) else {
|
||||
var trustError: CFError?
|
||||
guard SecTrustEvaluateWithError(trust, &trustError) else {
|
||||
#if DEBUG
|
||||
print("MusicBridge TLS: pinned certificate rejected: \(String(describing: trustError))")
|
||||
#endif
|
||||
completionHandler(.cancelAuthenticationChallenge, nil); return
|
||||
}
|
||||
completionHandler(.useCredential, URLCredential(trust: trust))
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
urlSession(session, didReceive: challenge, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask,
|
||||
willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest,
|
||||
completionHandler: @escaping (URLRequest?) -> Void) {
|
||||
|
||||
@@ -34,6 +34,24 @@
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>10.0.0.0/8</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>172.16.0.0/12</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>192.168.0.0/16</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import XCTest
|
||||
@testable import MusicBridge
|
||||
|
||||
final class TLSTests: XCTestCase {
|
||||
private func connection(_ name: String, wrongPin: Bool = false) throws -> Connection {
|
||||
let url = try XCTUnwrap(Bundle(for: Self.self).url(forResource: "tls-fixture", withExtension: "json"))
|
||||
let fixtures = try JSONDecoder().decode([String: [String: String]].self, from: Data(contentsOf: url))
|
||||
guard let fixture = fixtures[name] else { throw XCTSkip("Start tools/tls_fixture.py before building TLS tests.") }
|
||||
return try Connection(endpoint: XCTUnwrap(fixture["endpoint"]),
|
||||
fingerprint: wrongPin ? String(repeating: "0", count: 64) : XCTUnwrap(fixture["fingerprint"]))
|
||||
}
|
||||
|
||||
func testPinnedSelfSignedHTTPSAndWSS() async throws {
|
||||
let client = BridgeClient(try connection("valid"))
|
||||
defer { client.close() }
|
||||
let state = try await client.state()
|
||||
XCTAssertEqual(state.protocolVersion, 1)
|
||||
client.openEvents()
|
||||
let event = try await client.nextState()
|
||||
XCTAssertEqual(event.title, state.title)
|
||||
}
|
||||
|
||||
func testWrongFingerprintIsRejected() async throws {
|
||||
let client = BridgeClient(try connection("valid", wrongPin: true))
|
||||
defer { client.close() }
|
||||
do { _ = try await client.state(); XCTFail("Accepted the wrong fingerprint") }
|
||||
catch { XCTAssertTrue(error is URLError) }
|
||||
}
|
||||
|
||||
func testExpiredPinnedCertificateIsRejected() async throws {
|
||||
let client = BridgeClient(try connection("expired"))
|
||||
defer { client.close() }
|
||||
do { _ = try await client.state(); XCTFail("Accepted an expired certificate") }
|
||||
catch { XCTAssertTrue(error is URLError) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -1,6 +1,6 @@
|
||||
# MusicBridge iOS — план
|
||||
|
||||
Обновлено: 2026-09-10. Пользователь разрешил начать нативное iOS-приложение.
|
||||
Обновлено: 2026-09-12. Пользователь разрешил начать нативное iOS-приложение.
|
||||
Репозиторий: `C:/Users/areso/iOS MusicBridge`; origin — dev.yukinoki.ru/musicbridge/ios.
|
||||
Windows-агент — соседний проект `C:/Users/areso/MusicBridge/MusicBridge.Agent`.
|
||||
|
||||
@@ -30,9 +30,10 @@ Windows-агент — соседний проект `C:/Users/areso/MusicBridge
|
||||
|
||||
- [x] Первый успешный xcodebuild + XCTest на macOS: 7 тестов, 0 отказов.
|
||||
- [x] Подключён GitHub remote polskikh13/musicbridgeios; Actions и чтение логов через gh работают.
|
||||
- [ ] Интеграционные тесты TLS: верный/чужой/истёкший сертификат, redirect, WSS.
|
||||
- [x] Интеграционные тесты TLS: верный/чужой/истёкший сертификат, HTTPS и WSS.
|
||||
- [ ] Интеграционный тест отказа от redirect.
|
||||
- [ ] Проверить 401 и отзыв устройства, Keychain после переустановки/блокировки.
|
||||
- [ ] Настроить подпись и установку на физический iPhone.
|
||||
- [x] Установить на физический iPhone: пользователь подтвердил установку через iloader 2026-09-12.
|
||||
- [ ] Проверить LAN/плеер/команды/обложку с настоящим Windows-агентом.
|
||||
- [ ] Камера для QR, системные разрешения и обработка отказа.
|
||||
- [ ] Bonjour-поиск `_musicbridge._tcp`, список ПК и обновление адреса при смене IP.
|
||||
@@ -45,6 +46,34 @@ Windows-агент — соседний проект `C:/Users/areso/MusicBridge
|
||||
Потеря ответа на успешное сопряжение может оставить устройство на ПК: удалить его
|
||||
в окне агента перед повтором. Облачный runner не имеет доступа к домашней LAN.
|
||||
|
||||
## Проверка на iPhone 2026-09-12
|
||||
|
||||
- Windows GUI запущен с `--tray --lan`, порт TCP 8765 слушает.
|
||||
- На iPhone первая попытка сопряжения завершилась ошибкой недействительного
|
||||
сертификата сервера. Установка приложения уже работает; это отдельная TLS-проблема.
|
||||
- Ошибка воспроизведена настоящим HTTPS-запросом в iOS Simulator: старый код
|
||||
возвращает NSURLErrorDomain -1202 даже с верным отпечатком. Одни ATS-исключения
|
||||
её не устранили. Исправление: обрабатывать server trust также в
|
||||
`URLSessionTaskDelegate.didReceive`, направляя его в общую проверку отпечатка
|
||||
и срока сертификата. После этого HTTPS и WSS проходят.
|
||||
- Подготовлены ATS-исключения только для RFC1918 CIDR: `10.0.0.0/8`,
|
||||
`172.16.0.0/12`, `192.168.0.0/16`. Начиная с iOS 17, для IP нужны явные
|
||||
исключения. Клиент продолжает требовать HTTPS, точный SHA-256 отпечаток,
|
||||
действующий сертификат и запрещает redirect. Глобального отключения ATS нет.
|
||||
- Добавлены настоящие HTTPS/WSS проверки в симуляторе с сертификатом того же
|
||||
типа, что у агента: RSA 2048, самоподписанный, CA:FALSE, SAN localhost/127.0.0.1,
|
||||
срок пять лет. Отдельно проверяются чужой отпечаток и истёкший сертификат.
|
||||
`tools/tls_fixture.py` запускается в CI перед сборкой тестов; закрытые ключи
|
||||
создаются временно на runner и не включаются в приложение или артефакты.
|
||||
- Сообщение «непригодные данные» при импорте приглашения пока не разобрано:
|
||||
пользователь попросил отложить его. Следующий ручной шаг — повторить сопряжение
|
||||
после установки новой IPA, затем проверить команды, прогресс и обложку.
|
||||
- `93f7c9c`: [Actions 34694138394](https://github.com/polskikh13/musicbridgeios/actions/runs/34694138394)
|
||||
— 10 XCTest, 0 отказов, успешный Release archive. Версия 0.1.1 (build 2),
|
||||
IPA скачана в `build/iPhone-0.1.1/MusicBridge-iPhone.ipa`; локально проверены ZIP,
|
||||
iPhoneOS ARM64, версия и ATS-настройки внутри готовой IPA.
|
||||
Сопряжение на физическом iPhone после этого исправления ещё не подтверждено.
|
||||
|
||||
## Карта
|
||||
|
||||
- `MusicBridge/Models.swift`: контракт и проверка входных данных.
|
||||
|
||||
@@ -53,6 +53,11 @@ Apple ID и сертификаты для этой проверки не тре
|
||||
|
||||
## Установка на iPhone через Sideloadly
|
||||
|
||||
Пользователь также подтвердил установку через iloader. Для повторной установки
|
||||
после исправления TLS используйте версию **0.1.1 (build 2)**:
|
||||
`build/iPhone-0.1.1/MusicBridge-iPhone.ipa`. Выберите этот файл в iloader и
|
||||
подпишите тем же Apple Account, которым установлена первая версия.
|
||||
|
||||
Workflow также выполняет Release archive для `generic/platform=iOS` и создаёт
|
||||
`MusicBridge-iPhone.ipa` в артефакте `MusicBridge-iPhone`. Это ARM64-сборка для
|
||||
iOS 17+, без подписи. Подпись создаёт Sideloadly на вашем ПК с вашим Apple Account.
|
||||
|
||||
@@ -39,6 +39,9 @@ test_group, test_sources = source_group("MusicBridgeTests")
|
||||
fixture = add("fixture", "PBXFileReference", lastKnownFileType="text.json", path="state-v1.json", sourceTree="<group>")
|
||||
objects[test_group]["children"].append(fixture)
|
||||
fixture_build = add("fixture-build", "PBXBuildFile", fileRef=fixture)
|
||||
tls_fixture = add("tls-fixture", "PBXFileReference", lastKnownFileType="text.json", path="tls-fixture.json", sourceTree="<group>")
|
||||
objects[test_group]["children"].append(tls_fixture)
|
||||
tls_fixture_build = add("tls-fixture-build", "PBXBuildFile", fileRef=tls_fixture)
|
||||
products = []
|
||||
targets = []
|
||||
for name, sources, is_test in [("MusicBridge", app_sources, False), ("MusicBridgeTests", test_sources, True)]:
|
||||
@@ -47,7 +50,7 @@ for name, sources, is_test in [("MusicBridge", app_sources, False), ("MusicBridg
|
||||
products.append(product)
|
||||
phases = [add(name + "sources", "PBXSourcesBuildPhase", buildActionMask=2147483647, files=sources, runOnlyForDeploymentPostprocessing=0),
|
||||
add(name + "frameworks", "PBXFrameworksBuildPhase", buildActionMask=2147483647, files=[], runOnlyForDeploymentPostprocessing=0),
|
||||
add(name + "resources", "PBXResourcesBuildPhase", buildActionMask=2147483647, files=[fixture_build] if is_test else [], runOnlyForDeploymentPostprocessing=0)]
|
||||
add(name + "resources", "PBXResourcesBuildPhase", buildActionMask=2147483647, files=[fixture_build, tls_fixture_build] if is_test else [], runOnlyForDeploymentPostprocessing=0)]
|
||||
settings = dict(PRODUCT_NAME="$(TARGET_NAME)", PRODUCT_BUNDLE_IDENTIFIER="ru.yukinoki.musicbridge" + (".tests" if is_test else ""),
|
||||
SWIFT_VERSION="5.0", IPHONEOS_DEPLOYMENT_TARGET="17.0", SDKROOT="iphoneos",
|
||||
SUPPORTED_PLATFORMS="iphoneos iphonesimulator", TARGETED_DEVICE_FAMILY="1", CODE_SIGN_STYLE="Automatic",
|
||||
@@ -58,7 +61,7 @@ for name, sources, is_test in [("MusicBridge", app_sources, False), ("MusicBridg
|
||||
proxy = add("test-proxy", "PBXContainerItemProxy", containerPortal=uid("project"), proxyType=1, remoteGlobalIDString=uid("MusicBridgetarget"), remoteInfo="MusicBridge")
|
||||
deps = [add("test-dependency", "PBXTargetDependency", target=uid("MusicBridgetarget"), targetProxy=proxy)]
|
||||
else:
|
||||
settings.update(INFOPLIST_FILE="MusicBridge/Info.plist", CURRENT_PROJECT_VERSION="1", MARKETING_VERSION="0.1.0")
|
||||
settings.update(INFOPLIST_FILE="MusicBridge/Info.plist", CURRENT_PROJECT_VERSION="2", MARKETING_VERSION="0.1.1")
|
||||
targets.append(add(name + "target", "PBXNativeTarget", name=name, productName=name, productReference=product,
|
||||
productType="com.apple.product-type.bundle.unit-test" if is_test else "com.apple.product-type.application",
|
||||
buildConfigurationList=configurations(name, settings), buildPhases=phases, buildRules=[], dependencies=deps))
|
||||
@@ -103,6 +106,12 @@ info = dict(CFBundleDevelopmentRegion="ru", CFBundleDisplayName="MusicBridge", C
|
||||
LSRequiresIPhoneOS=True, UILaunchScreen={},
|
||||
UISupportedInterfaceOrientations=["UIInterfaceOrientationPortrait"],
|
||||
NSLocalNetworkUsageDescription="MusicBridge подключается к вашему ПК, чтобы показывать музыку и управлять воспроизведением.",
|
||||
NSAppTransportSecurity=dict(NSAllowsLocalNetworking=True))
|
||||
# iOS 17+ requires explicit IP exceptions for custom self-signed trust.
|
||||
# BridgeClient still requires HTTPS, an exact SHA-256 pin and valid dates.
|
||||
# Never disable ATS globally or include public IP ranges.
|
||||
NSAppTransportSecurity=dict(NSAllowsLocalNetworking=True, NSExceptionDomains={
|
||||
network: dict(NSExceptionAllowsInsecureHTTPLoads=True)
|
||||
for network in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")
|
||||
}))
|
||||
(ROOT / "MusicBridge" / "Info.plist").write_bytes(plistlib.dumps(info, sort_keys=False))
|
||||
print("Generated Xcode project, shared scheme and Info.plist.")
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""CI-only HTTPS/WSS server with the same certificate shape as the Windows agent."""
|
||||
import base64
|
||||
import hashlib
|
||||
import http.server
|
||||
import ipaddress
|
||||
import json
|
||||
import pathlib
|
||||
import socket
|
||||
import socketserver
|
||||
import ssl
|
||||
import struct
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
root = pathlib.Path(__file__).resolve().parents[1]
|
||||
work = root / "build/tls"
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
# Discover the runner's LAN address without sending a packet.
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe:
|
||||
probe.connect(("192.0.2.1", 80))
|
||||
address = probe.getsockname()[0]
|
||||
assert any(ipaddress.ip_address(address) in ipaddress.ip_network(n)
|
||||
for n in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")), address
|
||||
config = work / "cert.cnf"
|
||||
config.write_text("""[req]
|
||||
distinguished_name = dn
|
||||
prompt = no
|
||||
[dn]
|
||||
CN = MusicBridge Agent
|
||||
[ext]
|
||||
basicConstraints = critical,CA:FALSE
|
||||
keyUsage = critical,digitalSignature,keyEncipherment
|
||||
extendedKeyUsage = serverAuth
|
||||
subjectAltName = DNS:localhost,IP:127.0.0.1
|
||||
""")
|
||||
state = json.loads((root / "MusicBridgeTests/state-v1.json").read_text())
|
||||
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/v1/events":
|
||||
accept = base64.b64encode(hashlib.sha1(
|
||||
(self.headers["Sec-WebSocket-Key"] + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode()).digest()).decode()
|
||||
self.send_response(101)
|
||||
self.send_header("Upgrade", "websocket")
|
||||
self.send_header("Connection", "Upgrade")
|
||||
self.send_header("Sec-WebSocket-Accept", accept)
|
||||
self.end_headers()
|
||||
payload = json.dumps({"type": "state", "state": state}).encode()
|
||||
self.wfile.write(b"\x81\x7e" + struct.pack("!H", len(payload)) + payload)
|
||||
self.wfile.flush()
|
||||
else:
|
||||
payload = json.dumps(state).encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(payload)))
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
|
||||
|
||||
fixtures = {}
|
||||
class Server(http.server.ThreadingHTTPServer):
|
||||
def server_bind(self):
|
||||
# HTTPServer's reverse DNS lookup can stall on hosted runner addresses.
|
||||
socketserver.TCPServer.server_bind(self)
|
||||
self.server_name, self.server_port = self.server_address
|
||||
|
||||
|
||||
for name, days in (("valid", "1825"), ("expired", "-1")):
|
||||
print(f"Generating {name} certificate for {address}", flush=True)
|
||||
key, csr, cert = (work / (name + suffix) for suffix in (".key", ".csr", ".pem"))
|
||||
subprocess.run(["openssl", "req", "-new", "-newkey", "rsa:2048", "-nodes", "-config", str(config),
|
||||
"-keyout", str(key), "-out", str(csr)], check=True, capture_output=True, timeout=15)
|
||||
subprocess.run(["openssl", "x509", "-req", "-in", str(csr), "-signkey", str(key), "-sha256",
|
||||
"-days", days, "-extfile", str(config), "-extensions", "ext", "-out", str(cert)],
|
||||
check=True, capture_output=True, timeout=15)
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||
context.load_cert_chain(cert, key)
|
||||
server = Server((address, 0), Handler)
|
||||
server.socket = context.wrap_socket(server.socket, server_side=True)
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
fixtures[name] = {"endpoint": f"https://{address}:{server.server_port}",
|
||||
"fingerprint": hashlib.sha256(ssl.PEM_cert_to_DER_cert(cert.read_text())).hexdigest()}
|
||||
(root / "MusicBridgeTests/tls-fixture.json").write_text(json.dumps(fixtures))
|
||||
print("TLS fixtures ready", flush=True)
|
||||
threading.Event().wait()
|
||||
@@ -25,7 +25,11 @@ referenced = set(re.findall(r'"([A-F0-9]{24})"', project))
|
||||
assert defined == referenced, "Project has dangling object references."
|
||||
info = plistlib.loads(generated[2].read_bytes())
|
||||
assert info["NSLocalNetworkUsageDescription"]
|
||||
assert info["NSAppTransportSecurity"] == {"NSAllowsLocalNetworking": True}
|
||||
assert info["NSAppTransportSecurity"] == {
|
||||
"NSAllowsLocalNetworking": True,
|
||||
"NSExceptionDomains": {network: {"NSExceptionAllowsInsecureHTTPLoads": True}
|
||||
for network in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")},
|
||||
}
|
||||
ET.parse(generated[1])
|
||||
fixture = json.loads((root / "MusicBridgeTests/state-v1.json").read_text(encoding="utf-8"))
|
||||
assert fixture["protocolVersion"] == 1 and fixture["positionSeconds"] == 42.5
|
||||
|
||||
Reference in New Issue
Block a user