mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-28 00:10:50 +08:00
Compare commits
168 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5d13ee16e | ||
|
|
66a13d3d8c | ||
|
|
8b4ca6d080 | ||
|
|
9a1f4ff4b5 | ||
|
|
3b8a4a6f34 | ||
|
|
4f59e78179 | ||
|
|
2f60e61dd5 | ||
|
|
318b58b26a | ||
|
|
48cd06c7a4 | ||
|
|
99da8a3c61 | ||
|
|
1e0b0407d3 | ||
|
|
7e6dc91095 | ||
|
|
d854b6a220 | ||
|
|
f8fb64ad8c | ||
|
|
0e8c323659 | ||
|
|
0956da4ae2 | ||
|
|
03cff9d9a8 | ||
|
|
0a041c2caa | ||
|
|
83cbfb6924 | ||
|
|
6010c39cd8 | ||
|
|
4cf3f4e252 | ||
|
|
3d3110c3d7 | ||
|
|
22912707b5 | ||
|
|
e85f0a9bfe | ||
|
|
9ef5011760 | ||
|
|
d71112380c | ||
|
|
491bfbecb5 | ||
|
|
8a6cc98806 | ||
|
|
728373b067 | ||
|
|
c62ff2da87 | ||
|
|
572bf859ba | ||
|
|
aef136241a | ||
|
|
78af9f67de | ||
|
|
9f34b4109b | ||
|
|
1967d22dff | ||
|
|
0cba8ab127 | ||
|
|
d002226acb | ||
|
|
1db572e020 | ||
|
|
5de5ee42c2 | ||
|
|
2c19208094 | ||
|
|
3014edffbb | ||
|
|
ece4f303ce | ||
|
|
db9d79554a | ||
|
|
620fe0be54 | ||
|
|
79a4dcf2b0 | ||
|
|
a8ee9552a0 | ||
|
|
3749bafae0 | ||
|
|
b573f478fc | ||
|
|
d67a3ab7f8 | ||
|
|
dbe5672df4 | ||
|
|
48fd64319e | ||
|
|
ccf821ab22 | ||
|
|
b074aa5779 | ||
|
|
95a1fdf42d | ||
|
|
e678ec394d | ||
|
|
628909a192 | ||
|
|
5cab585115 | ||
|
|
dcfa3eadaf | ||
|
|
1647e70243 | ||
|
|
9c6d8a614c | ||
|
|
db7a845d3b | ||
|
|
1d48d45592 | ||
|
|
d613739b06 | ||
|
|
6e1194fe4c | ||
|
|
f101ddb918 | ||
|
|
0054100414 | ||
|
|
486f3887c2 | ||
|
|
37af369b43 | ||
|
|
827d24fde4 | ||
|
|
b8ea38b5f3 | ||
|
|
4530d8663d | ||
|
|
a4073d64cb | ||
|
|
16400df182 | ||
|
|
47af17bebc | ||
|
|
814f23a27c | ||
|
|
6bbe137f09 | ||
|
|
6fdcf5c7c9 | ||
|
|
ce1712ff2e | ||
|
|
8b09824e04 | ||
|
|
9fb23476ac | ||
|
|
971c263bf8 | ||
|
|
cb3fd3e646 | ||
|
|
d414557d74 | ||
|
|
fbcd95b617 | ||
|
|
887f29096f | ||
|
|
e0bddc2faa | ||
|
|
4f770a1eea | ||
|
|
376dc97134 | ||
|
|
27c8da9a75 | ||
|
|
5963dfa01a | ||
|
|
f86cba6ce5 | ||
|
|
e1b82519b9 | ||
|
|
32d8a62fec | ||
|
|
4cd622ff07 | ||
|
|
6ec4361f43 | ||
|
|
e9bed3688f | ||
|
|
8d5366444b | ||
|
|
4e32b05124 | ||
|
|
e0055bf491 | ||
|
|
d60ddb21dc | ||
|
|
219d1ccd08 | ||
|
|
6f697e56c8 | ||
|
|
05535b9bb2 | ||
|
|
5ce2cb658c | ||
|
|
a86df52d9a | ||
|
|
275c2ca343 | ||
|
|
5d871e8b21 | ||
|
|
694afdd07f | ||
|
|
d31e70515a | ||
|
|
701b141347 | ||
|
|
eaa114f40c | ||
|
|
563c808836 | ||
|
|
79b6683817 | ||
|
|
8bd16df6f8 | ||
|
|
fdea52d59d | ||
|
|
4031abd8d6 | ||
|
|
cf2bfaf6a7 | ||
|
|
d35021ee5d | ||
|
|
57d1597264 | ||
|
|
aca847e76c | ||
|
|
8036c164b4 | ||
|
|
0482987bf1 | ||
|
|
50dd419e5a | ||
|
|
213359b6c8 | ||
|
|
63c2bb4ca2 | ||
|
|
6693020e6d | ||
|
|
05c5f45995 | ||
|
|
1ded7a8a66 | ||
|
|
809d0ca65c | ||
|
|
139064de35 | ||
|
|
54e0cbc84e | ||
|
|
7c9166a8cf | ||
|
|
a9a16f2218 | ||
|
|
c9ae9dd893 | ||
|
|
010d50b727 | ||
|
|
c88fb03243 | ||
|
|
7bae9b633c | ||
|
|
fc3f1d6aac | ||
|
|
b719be3e38 | ||
|
|
db2410cc7f | ||
|
|
d9c2854911 | ||
|
|
99c051a874 | ||
|
|
a0ad151fbc | ||
|
|
4806a5e0a9 | ||
|
|
e60152d38b | ||
|
|
f6e97a06f5 | ||
|
|
618599266e | ||
|
|
696c1b4bb2 | ||
|
|
b346af35d3 | ||
|
|
16a65289f2 | ||
|
|
bb2691fe8f | ||
|
|
7753c83e27 | ||
|
|
1d6fb2e3c5 | ||
|
|
6c1bff5d0c | ||
|
|
f1e362a820 | ||
|
|
e2c19d550c | ||
|
|
c101ef1c80 | ||
|
|
4b7be20fe0 | ||
|
|
5c98aea7e3 | ||
|
|
da61644dbc | ||
|
|
5de7ecd4c5 | ||
|
|
3fc6b055a0 | ||
|
|
4b65eebd5d | ||
|
|
921c00c472 | ||
|
|
b6bd76534f | ||
|
|
780cfb4cca | ||
|
|
8f9272d985 | ||
|
|
f679647ec9 |
146
.github/workflows/build.yml
vendored
146
.github/workflows/build.yml
vendored
@@ -34,13 +34,11 @@ jobs:
|
|||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: npm
|
|
||||||
cache-dependency-path: web/package-lock.json
|
|
||||||
|
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
working-directory: web
|
working-directory: web
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
- name: Upload frontend dist
|
- name: Upload frontend dist
|
||||||
@@ -66,8 +64,16 @@ jobs:
|
|||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
# The Docker workflow builds the same targets with the same Cross images.
|
||||||
|
shared-key: linux-cross-release
|
||||||
|
key: ${{ hashFiles('Cross.toml', 'build/cross/Dockerfile.*') }}
|
||||||
|
cache-all-crates: true
|
||||||
|
|
||||||
- name: Install cross
|
- name: Install cross
|
||||||
run: cargo install cross --locked
|
run: cargo install cross --version 0.2.5 --locked
|
||||||
|
|
||||||
- name: Build linux binary
|
- name: Build linux binary
|
||||||
run: bash build/build-images.sh
|
run: bash build/build-images.sh
|
||||||
@@ -127,6 +133,12 @@ jobs:
|
|||||||
"TURBOJPEG_LIB_DIR=$env:TURBOJPEG_LIB_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
|
"TURBOJPEG_LIB_DIR=$env:TURBOJPEG_LIB_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||||
"TURBOJPEG_INCLUDE_DIR=$env:TURBOJPEG_INCLUDE_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
|
"TURBOJPEG_INCLUDE_DIR=$env:TURBOJPEG_INCLUDE_DIR" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
shared-key: windows-msvc-release
|
||||||
|
key: ${{ hashFiles('vcpkg.json', 'vcpkg-configuration.json') }}
|
||||||
|
|
||||||
- name: Build Windows exe
|
- name: Build Windows exe
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: .\build\windows\build.ps1 -Configuration release -Package
|
run: .\build\windows\build.ps1 -Configuration release -Package
|
||||||
@@ -139,126 +151,9 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
android:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
needs: frontend
|
|
||||||
timeout-minutes: 240
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Download frontend dist
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: web-dist
|
|
||||||
path: web/dist
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Cache Android Docker layers
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: /tmp/.buildx-cache/android
|
|
||||||
key: android-buildx-${{ runner.os }}-${{ hashFiles('build/cross/Dockerfile.android') }}
|
|
||||||
restore-keys: |
|
|
||||||
android-buildx-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Build Android Docker image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: build/cross
|
|
||||||
file: build/cross/Dockerfile.android
|
|
||||||
tags: one-kvm-android-build:ci
|
|
||||||
load: true
|
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache/android
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache/android-new,mode=max
|
|
||||||
|
|
||||||
- name: Rotate Android Docker layer cache
|
|
||||||
run: |
|
|
||||||
rm -rf /tmp/.buildx-cache/android
|
|
||||||
mv /tmp/.buildx-cache/android-new /tmp/.buildx-cache/android
|
|
||||||
|
|
||||||
- name: Cache Android build dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
.github/android-cache/gradle
|
|
||||||
.github/android-cache/cargo-registry
|
|
||||||
.github/android-cache/cargo-git
|
|
||||||
.tmp/android-ffmpeg-check
|
|
||||||
.tmp/android-turbojpeg-src
|
|
||||||
.tmp/android-libyuv-src
|
|
||||||
.tmp/android-alsa-src
|
|
||||||
.tmp/android-opus-src
|
|
||||||
dist/android-ffmpeg-mediacodec
|
|
||||||
dist/android-turbojpeg
|
|
||||||
dist/android-libyuv
|
|
||||||
dist/android-alsa
|
|
||||||
dist/android-opus
|
|
||||||
key: android-deps-${{ runner.os }}-${{ hashFiles('android/**/*.gradle.kts', 'android/gradle/wrapper/gradle-wrapper.properties', 'android/native/Cargo.lock', 'Cargo.lock', 'scripts/build-android-*.sh') }}
|
|
||||||
restore-keys: |
|
|
||||||
android-deps-${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: Prepare Android FFmpeg source
|
|
||||||
run: |
|
|
||||||
chmod +x android/gradlew
|
|
||||||
if [ ! -x .tmp/android-ffmpeg-check/src/ffmpeg-rockchip/configure ]; then
|
|
||||||
rm -rf .tmp/android-ffmpeg-check/src
|
|
||||||
mkdir -p .tmp/android-ffmpeg-check/src
|
|
||||||
wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz -O .tmp/android-ffmpeg-check/ffmpeg.tar.gz
|
|
||||||
tar -xzf .tmp/android-ffmpeg-check/ffmpeg.tar.gz -C .tmp/android-ffmpeg-check/src --strip-components=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build Android APK
|
|
||||||
env:
|
|
||||||
ONE_KVM_ANDROID_DOCKER_IMAGE: one-kvm-android-build:ci
|
|
||||||
ONE_KVM_ANDROID_SKIP_DOCKER_BUILD: "1"
|
|
||||||
ONE_KVM_ANDROID_GRADLE_CACHE_DIR: ${{ github.workspace }}/.github/android-cache/gradle
|
|
||||||
ONE_KVM_ANDROID_CARGO_REGISTRY_CACHE_DIR: ${{ github.workspace }}/.github/android-cache/cargo-registry
|
|
||||||
ONE_KVM_ANDROID_CARGO_GIT_CACHE_DIR: ${{ github.workspace }}/.github/android-cache/cargo-git
|
|
||||||
run: bash build/build-android.sh all
|
|
||||||
|
|
||||||
- name: Fix Android build permissions
|
|
||||||
if: ${{ always() }}
|
|
||||||
run: |
|
|
||||||
paths=(
|
|
||||||
.github/android-cache
|
|
||||||
.tmp/android-ffmpeg-check
|
|
||||||
.tmp/android-turbojpeg-src
|
|
||||||
.tmp/android-libyuv-src
|
|
||||||
.tmp/android-alsa-src
|
|
||||||
.tmp/android-opus-src
|
|
||||||
dist/android-ffmpeg-mediacodec
|
|
||||||
dist/android-turbojpeg
|
|
||||||
dist/android-libyuv
|
|
||||||
dist/android-alsa
|
|
||||||
dist/android-opus
|
|
||||||
target/android
|
|
||||||
android/.gradle
|
|
||||||
android/app/build
|
|
||||||
android/native/target
|
|
||||||
)
|
|
||||||
existing=()
|
|
||||||
for path in "${paths[@]}"; do
|
|
||||||
if [ -e "$path" ]; then
|
|
||||||
existing+=("$path")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "${#existing[@]}" -gt 0 ]; then
|
|
||||||
sudo chown -R "$USER:$USER" "${existing[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload Android APK
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: one-kvm-android-apk
|
|
||||||
path: target/android/one-kvm_*.apk
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: [deb, windows, android]
|
needs: [deb, windows]
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_release }}
|
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_release }}
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
permissions:
|
permissions:
|
||||||
@@ -283,12 +178,6 @@ jobs:
|
|||||||
name: one-kvm-windows-exe
|
name: one-kvm-windows-exe
|
||||||
path: release-artifacts/windows
|
path: release-artifacts/windows
|
||||||
|
|
||||||
- name: Download Android artifact
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: one-kvm-android-apk
|
|
||||||
path: release-artifacts/android
|
|
||||||
|
|
||||||
- name: Publish GitHub Release
|
- name: Publish GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
@@ -298,4 +187,3 @@ jobs:
|
|||||||
files: |
|
files: |
|
||||||
release-artifacts/deb/*.deb
|
release-artifacts/deb/*.deb
|
||||||
release-artifacts/windows/*.exe
|
release-artifacts/windows/*.exe
|
||||||
release-artifacts/android/*.apk
|
|
||||||
|
|||||||
91
.github/workflows/docker.yml
vendored
Normal file
91
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
name: Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: Docker image tag
|
||||||
|
required: false
|
||||||
|
default: latest
|
||||||
|
platforms:
|
||||||
|
description: Docker platforms
|
||||||
|
required: false
|
||||||
|
default: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
DOCKER_PLATFORMS: ${{ github.event_name == 'workflow_dispatch' && inputs.platforms || 'linux/amd64,linux/arm64,linux/arm/v7' }}
|
||||||
|
DOCKER_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 360
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
# Reuse dependency artifacts produced by the deb job and vice versa.
|
||||||
|
shared-key: linux-cross-release
|
||||||
|
key: ${{ hashFiles('Cross.toml', 'build/cross/Dockerfile.*') }}
|
||||||
|
cache-all-crates: true
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y unzip xz-utils
|
||||||
|
cargo install cross --version 0.2.5 --locked
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
working-directory: web
|
||||||
|
run: |
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build linux binaries
|
||||||
|
run: bash build/build-images.sh
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: docker.io
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to Aliyun Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: registry.cn-hangzhou.aliyuncs.com
|
||||||
|
username: ${{ secrets.ALIYUN_USERNAME }}
|
||||||
|
password: ${{ secrets.ALIYUN_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Publish Docker images
|
||||||
|
run: |
|
||||||
|
./build/package-docker.sh --platform "$DOCKER_PLATFORMS" --registry docker.io/silentwind0 --tag "$DOCKER_TAG" --push
|
||||||
|
./build/package-docker.sh --platform "$DOCKER_PLATFORMS" --registry registry.cn-hangzhou.aliyuncs.com/silentwind --tag "$DOCKER_TAG" --push
|
||||||
|
./build/package-docker.sh --platform "$DOCKER_PLATFORMS" --registry docker.io/silentwind0 --variant full --tag "$DOCKER_TAG" --push
|
||||||
|
./build/package-docker.sh --platform "$DOCKER_PLATFORMS" --registry registry.cn-hangzhou.aliyuncs.com/silentwind --variant full --tag "$DOCKER_TAG" --push
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,5 @@
|
|||||||
# Rust
|
# Rust
|
||||||
target/
|
target/
|
||||||
Cargo.lock
|
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
5652
Cargo.lock
generated
Normal file
5652
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
115
Cargo.toml
115
Cargo.toml
@@ -1,10 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "one-kvm"
|
name = "one-kvm"
|
||||||
version = "0.2.2"
|
version = "0.2.8"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["SilentWind"]
|
authors = ["SilentWind"]
|
||||||
description = "A open and lightweight IP-KVM solution written in Rust"
|
description = "A open and lightweight IP-KVM solution written in Rust"
|
||||||
license = "GPL-2.0"
|
license = "AGPL-3.0-only"
|
||||||
repository = "https://github.com/mofeng-git/One-KVM"
|
repository = "https://github.com/mofeng-git/One-KVM"
|
||||||
keywords = ["kvm", "ipkvm", "remote-management", "embedded"]
|
keywords = ["kvm", "ipkvm", "remote-management", "embedded"]
|
||||||
categories = ["embedded", "network-programming"]
|
categories = ["embedded", "network-programming"]
|
||||||
@@ -20,12 +20,14 @@ desktop = [
|
|||||||
"dep:sqlx",
|
"dep:sqlx",
|
||||||
"dep:serde",
|
"dep:serde",
|
||||||
"dep:serde_json",
|
"dep:serde_json",
|
||||||
|
"dep:toml_edit",
|
||||||
"dep:tracing",
|
"dep:tracing",
|
||||||
"dep:tracing-subscriber",
|
"dep:tracing-subscriber",
|
||||||
"dep:thiserror",
|
"dep:thiserror",
|
||||||
"dep:anyhow",
|
"dep:anyhow",
|
||||||
"dep:argon2",
|
"dep:argon2",
|
||||||
"dep:rand",
|
"dep:rand",
|
||||||
|
"dep:totp-rs",
|
||||||
"dep:uuid",
|
"dep:uuid",
|
||||||
"dep:base64",
|
"dep:base64",
|
||||||
"dep:nix",
|
"dep:nix",
|
||||||
@@ -33,11 +35,12 @@ desktop = [
|
|||||||
"dep:urlencoding",
|
"dep:urlencoding",
|
||||||
"dep:rust-embed",
|
"dep:rust-embed",
|
||||||
"dep:mime_guess",
|
"dep:mime_guess",
|
||||||
"dep:rustls",
|
"dep:openssl",
|
||||||
"dep:rcgen",
|
"dep:rcgen",
|
||||||
"dep:axum-server",
|
"dep:axum-server",
|
||||||
"dep:clap",
|
"dep:clap",
|
||||||
"dep:time",
|
"dep:time",
|
||||||
|
"dep:tempfile",
|
||||||
"dep:bytes",
|
"dep:bytes",
|
||||||
"dep:bytemuck",
|
"dep:bytemuck",
|
||||||
"dep:xxhash-rust",
|
"dep:xxhash-rust",
|
||||||
@@ -56,6 +59,7 @@ desktop = [
|
|||||||
"dep:ventoy-img",
|
"dep:ventoy-img",
|
||||||
"dep:protobuf",
|
"dep:protobuf",
|
||||||
"dep:sodiumoxide",
|
"dep:sodiumoxide",
|
||||||
|
"dep:des",
|
||||||
"dep:sha2",
|
"dep:sha2",
|
||||||
"dep:typeshare",
|
"dep:typeshare",
|
||||||
"dep:hwcodec",
|
"dep:hwcodec",
|
||||||
@@ -65,71 +69,13 @@ desktop = [
|
|||||||
"dep:v4l2r",
|
"dep:v4l2r",
|
||||||
"dep:alsa",
|
"dep:alsa",
|
||||||
"dep:gpio-cdev",
|
"dep:gpio-cdev",
|
||||||
|
"dep:one-kvm-bluetooth-hid",
|
||||||
"dep:cpal",
|
"dep:cpal",
|
||||||
"dep:windows-sys",
|
"dep:windows-sys",
|
||||||
]
|
]
|
||||||
android = [
|
|
||||||
"dep:anyhow",
|
|
||||||
"dep:argon2",
|
|
||||||
"dep:arc-swap",
|
|
||||||
"dep:async-stream",
|
|
||||||
"dep:async-trait",
|
|
||||||
"dep:axum",
|
|
||||||
"dep:axum-extra",
|
|
||||||
"dep:base64",
|
|
||||||
"dep:bytemuck",
|
|
||||||
"dep:bytes",
|
|
||||||
"dep:futures",
|
|
||||||
"dep:gpio-cdev",
|
|
||||||
"dep:hwcodec",
|
|
||||||
"dep:libc",
|
|
||||||
"dep:libyuv",
|
|
||||||
"dep:mime_guess",
|
|
||||||
"dep:nix",
|
|
||||||
"dep:parking_lot",
|
|
||||||
"dep:protobuf",
|
|
||||||
"dep:rand",
|
|
||||||
"dep:rcgen",
|
|
||||||
"dep:reqwest",
|
|
||||||
"dep:rtp",
|
|
||||||
"dep:rtsp-types",
|
|
||||||
"dep:rust-embed",
|
|
||||||
"dep:rustls",
|
|
||||||
"dep:sdp-types",
|
|
||||||
"dep:serde",
|
|
||||||
"dep:serde_json",
|
|
||||||
"dep:serialport",
|
|
||||||
"dep:sha2",
|
|
||||||
"dep:sodiumoxide",
|
|
||||||
"dep:sqlx",
|
|
||||||
"dep:alsa",
|
|
||||||
"dep:audiopus",
|
|
||||||
"dep:thiserror",
|
|
||||||
"dep:time",
|
|
||||||
"dep:tokio",
|
|
||||||
"dep:tokio-tungstenite",
|
|
||||||
"dep:tokio-util",
|
|
||||||
"dep:axum-server",
|
|
||||||
"dep:tower-http",
|
|
||||||
"dep:tracing",
|
|
||||||
"dep:tracing-log",
|
|
||||||
"dep:tracing-subscriber",
|
|
||||||
"dep:turbojpeg",
|
|
||||||
"dep:typeshare",
|
|
||||||
"dep:urlencoding",
|
|
||||||
"dep:uuid",
|
|
||||||
"dep:ventoy-img",
|
|
||||||
"dep:v4l2r",
|
|
||||||
"dep:webrtc",
|
|
||||||
"dep:xxhash-rust",
|
|
||||||
]
|
|
||||||
android-mediacodec = [
|
|
||||||
"android",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Async runtime
|
# Async runtime
|
||||||
tokio = { version = "1", features = ["full"], optional = true }
|
tokio = { version = "1", features = ["fs", "io-util", "macros", "net", "process", "rt-multi-thread", "signal", "sync", "time"], optional = true }
|
||||||
tokio-util = { version = "0.7", features = ["rt"], optional = true }
|
tokio-util = { version = "0.7", features = ["rt"], optional = true }
|
||||||
|
|
||||||
# Web framework
|
# Web framework
|
||||||
@@ -138,15 +84,15 @@ axum-extra = { version = "0.12", features = ["cookie"], optional = true }
|
|||||||
tower-http = { version = "0.6", features = ["cors", "trace", "set-header"], optional = true }
|
tower-http = { version = "0.6", features = ["cors", "trace", "set-header"], optional = true }
|
||||||
|
|
||||||
# Database - Use bundled SQLite for static linking
|
# Database - Use bundled SQLite for static linking
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"], optional = true }
|
sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "sqlite-bundled"], optional = true }
|
||||||
|
|
||||||
# Serialization
|
# Serialization
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
serde_json = { version = "1", optional = true }
|
serde_json = { version = "1", optional = true }
|
||||||
|
toml_edit = { version = "0.25", optional = true }
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
tracing = { version = "0.1", optional = true }
|
tracing = { version = "0.1", optional = true }
|
||||||
tracing-log = { version = "0.2", optional = true }
|
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "tracing-log"], optional = true }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "tracing-log"], optional = true }
|
||||||
|
|
||||||
# Error handling
|
# Error handling
|
||||||
@@ -155,15 +101,16 @@ anyhow = { version = "1", optional = true }
|
|||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
argon2 = { version = "0.5", optional = true }
|
argon2 = { version = "0.5", optional = true }
|
||||||
rand = { version = "0.9", optional = true }
|
rand = { version = "0.10", optional = true }
|
||||||
|
totp-rs = { version = "5.7", features = ["gen_secret", "otpauth", "zeroize"], optional = true }
|
||||||
|
|
||||||
# Utilities
|
# Utilities
|
||||||
uuid = { version = "1", features = ["v4", "serde"], optional = true }
|
uuid = { version = "1", features = ["v4", "serde"], optional = true }
|
||||||
base64 = { version = "0.22", optional = true }
|
base64 = { version = "0.22", optional = true }
|
||||||
|
tempfile = { version = "3", optional = true }
|
||||||
|
|
||||||
# HTTP client (for URL downloads)
|
# HTTP client (for URL downloads)
|
||||||
# Use rustls by default, but allow native-tls for systems with older GLIBC
|
reqwest = { version = "0.13", features = ["stream", "native-tls-vendored", "json"], default-features = false, optional = true }
|
||||||
reqwest = { version = "0.13", features = ["stream", "rustls", "json"], default-features = false, optional = true }
|
|
||||||
urlencoding = { version = "2", optional = true }
|
urlencoding = { version = "2", optional = true }
|
||||||
|
|
||||||
# Static file embedding
|
# Static file embedding
|
||||||
@@ -171,9 +118,9 @@ rust-embed = { version = "8", features = ["compression", "debug-embed"], optiona
|
|||||||
mime_guess = { version = "2", optional = true }
|
mime_guess = { version = "2", optional = true }
|
||||||
|
|
||||||
# TLS/HTTPS
|
# TLS/HTTPS
|
||||||
rustls = { version = "0.23", features = ["ring"], optional = true }
|
openssl = { version = "0.10", features = ["vendored"], optional = true }
|
||||||
rcgen = { version = "0.14", optional = true }
|
rcgen = { version = "0.14", optional = true }
|
||||||
axum-server = { version = "0.8", features = ["tls-rustls"], optional = true }
|
axum-server = { version = "0.8", features = ["tls-openssl"], optional = true }
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
clap = { version = "4", features = ["derive"], optional = true }
|
clap = { version = "4", features = ["derive"], optional = true }
|
||||||
@@ -193,15 +140,15 @@ async-stream = { version = "0.3", optional = true }
|
|||||||
futures = { version = "0.3", optional = true }
|
futures = { version = "0.3", optional = true }
|
||||||
|
|
||||||
# WebSocket client (for ttyd proxy)
|
# WebSocket client (for ttyd proxy)
|
||||||
tokio-tungstenite = { version = "0.28", optional = true }
|
tokio-tungstenite = { version = "0.29", optional = true }
|
||||||
|
|
||||||
# High-performance synchronization
|
# High-performance synchronization
|
||||||
parking_lot = { version = "0.12", optional = true }
|
parking_lot = { version = "0.12", optional = true }
|
||||||
arc-swap = { version = "1.8", optional = true }
|
arc-swap = { version = "1.8", optional = true }
|
||||||
|
|
||||||
# WebRTC
|
# WebRTC
|
||||||
webrtc = { version = "0.14", optional = true }
|
webrtc = { version = "0.17", features = ["vendored-openssl"], optional = true }
|
||||||
rtp = { version = "0.14", optional = true }
|
rtp = { version = "0.17", optional = true }
|
||||||
rtsp-types = { version = "0.1", optional = true }
|
rtsp-types = { version = "0.1", optional = true }
|
||||||
sdp-types = { version = "0.1", optional = true }
|
sdp-types = { version = "0.1", optional = true }
|
||||||
|
|
||||||
@@ -216,11 +163,12 @@ ventoy-img = { path = "libs/ventoy-img-rs", optional = true }
|
|||||||
# RustDesk protocol support
|
# RustDesk protocol support
|
||||||
protobuf = { version = "3.7", features = ["with-bytes"], optional = true }
|
protobuf = { version = "3.7", features = ["with-bytes"], optional = true }
|
||||||
sodiumoxide = { version = "0.2", optional = true }
|
sodiumoxide = { version = "0.2", optional = true }
|
||||||
sha2 = { version = "0.10", optional = true }
|
des = { version = "0.9", optional = true }
|
||||||
|
sha2 = { version = "0.11", optional = true }
|
||||||
# TypeScript type generation
|
# TypeScript type generation
|
||||||
typeshare = { version = "1.0", optional = true }
|
typeshare = { version = "1.0", optional = true }
|
||||||
|
|
||||||
[target.'cfg(any(unix, windows))'.dependencies]
|
[target.'cfg(any(target_os = "linux", windows))'.dependencies]
|
||||||
# Video encoding/decoding (FFmpeg/libjpeg-turbo/libyuv; available on Windows and Linux)
|
# Video encoding/decoding (FFmpeg/libjpeg-turbo/libyuv; available on Windows and Linux)
|
||||||
hwcodec = { path = "libs/hwcodec", features = ["bytes"], optional = true }
|
hwcodec = { path = "libs/hwcodec", features = ["bytes"], optional = true }
|
||||||
libyuv = { path = "res/vcpkg/libyuv", optional = true }
|
libyuv = { path = "res/vcpkg/libyuv", optional = true }
|
||||||
@@ -228,15 +176,12 @@ turbojpeg = { version = "1.3", optional = true }
|
|||||||
# Note: audiopus links to libopus.so (unavoidable for audio support)
|
# Note: audiopus links to libopus.so (unavoidable for audio support)
|
||||||
audiopus = { version = "0.2", optional = true }
|
audiopus = { version = "0.2", optional = true }
|
||||||
|
|
||||||
[target.'cfg(all(unix, not(target_os = "android")))'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
# Utilities
|
one-kvm-bluetooth-hid = { path = "libs/bluetooth-hid", optional = true }
|
||||||
nix = { version = "0.30", default-features = false, features = ["fs", "socket", "net", "hostname", "poll"], optional = true }
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
|
||||||
# Utilities
|
# Utilities
|
||||||
nix = { version = "0.30", default-features = false, features = ["fs", "socket", "hostname", "poll"], optional = true }
|
nix = { version = "0.31", default-features = false, features = ["fs", "socket", "net", "hostname", "poll"], optional = true }
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
|
||||||
# Video capture (V4L2)
|
# Video capture (V4L2)
|
||||||
v4l2r = { path = "libs/v4l2r", optional = true }
|
v4l2r = { path = "libs/v4l2r", optional = true }
|
||||||
|
|
||||||
@@ -247,7 +192,7 @@ alsa = { version = "0.11", optional = true }
|
|||||||
gpio-cdev = { version = "0.6", optional = true }
|
gpio-cdev = { version = "0.6", optional = true }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
cpal = { version = "0.17", default-features = false, optional = true }
|
cpal = { version = "0.18", default-features = false, optional = true }
|
||||||
windows-sys = { version = "0.61", features = [
|
windows-sys = { version = "0.61", features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
"Win32_NetworkManagement_IpHelper",
|
"Win32_NetworkManagement_IpHelper",
|
||||||
@@ -257,12 +202,12 @@ windows-sys = { version = "0.61", features = [
|
|||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tempfile = "3"
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
protobuf-codegen = "3.7"
|
protobuf-codegen = "3.7"
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
dtls = { path = "libs/dtls" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
lto = true
|
lto = true
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Cross-compilation configuration for One-KVM
|
# Cross-compilation configuration for One-KVM
|
||||||
# Uses pre-built custom Debian 11 based images for consistent build/runtime environment
|
# Uses pre-built custom Debian 12 based images for consistent build/runtime environment
|
||||||
# See: https://github.com/cross-rs/cross
|
# See: https://github.com/cross-rs/cross
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
|
|||||||
141
LICENSE
141
LICENSE
@@ -1,5 +1,5 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
@@ -7,17 +7,15 @@
|
|||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
software and other kinds of works.
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
software for all its users.
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
|||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
Developers that use our General Public Licenses protect your rights
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
you this License which gives you legal permission to copy, distribute
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
and/or modify the software.
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
A secondary benefit of defending all users' freedom is that
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
improvements made in alternate versions of the program, if they
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
receive widespread use, become available for other developers to
|
||||||
or can get the source code. And you must show them these terms so they
|
incorporate. Many developers of free software are heartened and
|
||||||
know their rights.
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
The GNU Affero General Public License is designed specifically to
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
ensure that, in such cases, the modified source code becomes available
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
An older license, called the Affero General Public License and
|
||||||
that there is no warranty for this free software. For both users' and
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
changed, so that their problems will not be attributed erroneously to
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
authors of previous versions.
|
this license.
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
@@ -72,7 +60,7 @@ modification follow.
|
|||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
|||||||
the Program, the only way you could satisfy both those terms and this
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
under version 3 of the GNU General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the special requirements of the GNU Affero General Public License,
|
but the work with which it is combined will remain governed by version
|
||||||
section 13, concerning interaction through a network will apply to the
|
3 of the GNU General Public License.
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU General Public License from time to time. Such new versions will
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU General
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU General Public License, you may choose any version ever published
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If your software can interact with users remotely through a computer
|
||||||
notice like this when it starts in an interactive mode:
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
<program> Copyright (C) <year> <name of author>
|
interface could display a "Source" link that leads users to an archive
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
of the code. There are many ways you could offer source, and different
|
||||||
This is free software, and you are welcome to redistribute it
|
solutions will be better for different programs; see section 13 for the
|
||||||
under certain conditions; type `show c' for details.
|
specific requirements.
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
|
|||||||
27
README.en.md
27
README.en.md
@@ -22,6 +22,8 @@ Goals: an open, lightweight, easy-to-use IP-KVM stack.
|
|||||||
- **Lightweight**: shipped as a binary with minimal moving parts for deployment.
|
- **Lightweight**: shipped as a binary with minimal moving parts for deployment.
|
||||||
- **Easy to use**: no hand-edited config files required; settings are done in the web UI.
|
- **Easy to use**: no hand-edited config files required; settings are done in the web UI.
|
||||||
|
|
||||||
|
For more information, see the [One-KVM Rust documentation](https://docs.one-kvm.cn/).
|
||||||
|
|
||||||
> **One-KVM (Python)** is no longer maintained. If you still need it, see <https://github.com/mofeng-git/One-KVM/tree/python>.
|
> **One-KVM (Python)** is no longer maintained. If you still need it, see <https://github.com/mofeng-git/One-KVM/tree/python>.
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
@@ -32,7 +34,7 @@ Goals: an open, lightweight, easy-to-use IP-KVM stack.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Core
|
**Core**
|
||||||
|
|
||||||
| Area | Capabilities |
|
| Area | Capabilities |
|
||||||
|------|----------------|
|
|------|----------------|
|
||||||
@@ -40,11 +42,9 @@ Goals: an open, lightweight, easy-to-use IP-KVM stack.
|
|||||||
| Video encoding | VAAPI / QSV / RKMPP / V4L2 M2M hardware paths, with software fallback |
|
| Video encoding | VAAPI / QSV / RKMPP / V4L2 M2M hardware paths, with software fallback |
|
||||||
| Keyboard & mouse | USB OTG HID or CH340 + CH9329 HID; absolute / relative mouse |
|
| Keyboard & mouse | USB OTG HID or CH340 + CH9329 HID; absolute / relative mouse |
|
||||||
| Virtual media | USB mass storage; ISO/IMG mount and Ventoy-style virtual USB |
|
| Virtual media | USB mass storage; ISO/IMG mount and Ventoy-style virtual USB |
|
||||||
| ATX power | GPIO or USB relay; power and reset control |
|
| ATX power | GPIO or USB relay; power control |
|
||||||
| Audio | ALSA capture + Opus (HTTP / WebRTC) |
|
| Audio | ALSA capture + Opus (HTTP / WebRTC) |
|
||||||
|
|
||||||
The web UI supports visual configuration and Chinese/English locales. Built-ins include a web terminal (ttyd), intranet tunnel (gostc), P2P (EasyTier), RustDesk protocol (optional cross-platform remote access), and RTSP streaming.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Release artifacts are on [GitHub Releases](https://github.com/mofeng-git/One-KVM/releases). Below are short paths for common setups. For **system requirements, hardware, Docker env vars, USB OTG**, and full troubleshooting, see the [One-KVM documentation](https://docs.one-kvm.cn/) (Chinese; use a translator if needed).
|
Release artifacts are on [GitHub Releases](https://github.com/mofeng-git/One-KVM/releases). Below are short paths for common setups. For **system requirements, hardware, Docker env vars, USB OTG**, and full troubleshooting, see the [One-KVM documentation](https://docs.one-kvm.cn/) (Chinese; use a translator if needed).
|
||||||
@@ -216,15 +216,22 @@ One-KVM builds on many great open-source projects; a lot of time goes into testi
|
|||||||
|
|
||||||
- 爱发电用户_d3d9c
|
- 爱发电用户_d3d9c
|
||||||
|
|
||||||
|
- 爱发电用户_97b41
|
||||||
|
|
||||||
|
- 偶然
|
||||||
|
|
||||||
|
- 爱发电用户_dba45
|
||||||
|
|
||||||
|
- 爱发电用户_d4f8b
|
||||||
|
|
||||||
|
- 故人。
|
||||||
|
|
||||||
- ......
|
- ......
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Sponsors
|
### Sponsors
|
||||||
|
|
||||||
**Mirror Download Services:**
|
|
||||||
- **[Chongqing University Open Source Software Mirror](https://mirrors.cqu.edu.cn/)** — provides mirror download services
|
|
||||||
|
|
||||||
**File hosting**
|
**File hosting**
|
||||||
|
|
||||||
- **[Huang1111 public-interest program](https://pan.huang1111.cn/s/mxkx3T1)** — login-free downloads
|
- **[Huang1111 public-interest program](https://pan.huang1111.cn/s/mxkx3T1)** — login-free downloads
|
||||||
@@ -236,3 +243,9 @@ One-KVM builds on many great open-source projects; a lot of time goes into testi
|
|||||||

|

|
||||||
|
|
||||||
林枫云 offers premium network routes, high-frequency game servers, and high-bandwidth servers in China and abroad.
|
林枫云 offers premium network routes, high-frequency game servers, and high-bandwidth servers in China and abroad.
|
||||||
|
|
||||||
|
- **[Beta Network](https://my.beita.cc/?ref=github_onekvm)** — project server sponsorship
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Remote computers, consumer GPU servers, and dedicated physical machines with fully automated online delivery.
|
||||||
|
|||||||
27
README.md
27
README.md
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📖 项目概述
|
## 项目概述
|
||||||
|
|
||||||
**One-KVM Rust** 是一个用 Rust 编写的轻量级 IP-KVM 解决方案,可通过网络远程管理服务器和工作站,实现 BIOS 级远程控制。
|
**One-KVM Rust** 是一个用 Rust 编写的轻量级 IP-KVM 解决方案,可通过网络远程管理服务器和工作站,实现 BIOS 级远程控制。
|
||||||
|
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
- **轻量**:以二进制文件形式分发,无繁杂的依赖项,部署过程简单。
|
- **轻量**:以二进制文件形式分发,无繁杂的依赖项,部署过程简单。
|
||||||
- **易用**:无需手动编辑配置文件,参数设置均可通过网页界面完成。
|
- **易用**:无需手动编辑配置文件,参数设置均可通过网页界面完成。
|
||||||
|
|
||||||
|
更多内容可访问 [One-KVM Rust 文档站点](https://docs.one-kvm.cn/)。
|
||||||
|
|
||||||
> **One-KVM Python** 已停止开发,如有需要可访问 <https://github.com/mofeng-git/One-KVM/tree/python>。
|
> **One-KVM Python** 已停止开发,如有需要可访问 <https://github.com/mofeng-git/One-KVM/tree/python>。
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
@@ -30,9 +32,9 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 📊 功能介绍
|
## 功能介绍
|
||||||
|
|
||||||
### 核心功能
|
**核心功能**
|
||||||
|
|
||||||
| 功能 | 能力说明 |
|
| 功能 | 能力说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
@@ -40,12 +42,10 @@
|
|||||||
| 视频编码 | VAAPI/QSV/RKMPP/V4L2M2M 硬件编码支持,以及软件编码兜底 |
|
| 视频编码 | VAAPI/QSV/RKMPP/V4L2M2M 硬件编码支持,以及软件编码兜底 |
|
||||||
| 键鼠控制 | USB OTG HID 或 CH340 + CH9329 HID,支持绝对/相对鼠标模式 |
|
| 键鼠控制 | USB OTG HID 或 CH340 + CH9329 HID,支持绝对/相对鼠标模式 |
|
||||||
| 虚拟媒体 | USB Mass Storage,支持 ISO/IMG 镜像挂载和 Ventoy 虚拟U盘模式 |
|
| 虚拟媒体 | USB Mass Storage,支持 ISO/IMG 镜像挂载和 Ventoy 虚拟U盘模式 |
|
||||||
| ATX 电源控制 | GPIO /USB 继电器,支持控制电源、重启按钮 |
|
| ATX 电源控制 | GPIO/USB 继电器,支持电源控制 |
|
||||||
| 音频传输 | ALSA 采集 + Opus 编码(HTTP/WebRTC) |
|
| 音频传输 | ALSA 采集 + Opus 编码(HTTP/WebRTC) |
|
||||||
|
|
||||||
此外提供基于 Web UI 的可视化配置与中英文界面;并集成 Web 终端(ttyd)、内网穿透(gostc)、P2P 组网(EasyTier)、RustDesk 协议(扩展跨平台远程访问)以及 RTSP 推流等能力。
|
## 安装使用
|
||||||
|
|
||||||
## ⚡ 安装使用
|
|
||||||
|
|
||||||
构建产物见 [GitHub Releases](https://github.com/mofeng-git/One-KVM/releases)。以下为常见安装方式的简要步骤;**系统要求、硬件准备、Docker 环境变量与 USB OTG 等完整说明**请查阅 [One-KVM Rust 文档站点](https://docs.one-kvm.cn/)。
|
构建产物见 [GitHub Releases](https://github.com/mofeng-git/One-KVM/releases)。以下为常见安装方式的简要步骤;**系统要求、硬件准备、Docker 环境变量与 USB OTG 等完整说明**请查阅 [One-KVM Rust 文档站点](https://docs.one-kvm.cn/)。
|
||||||
|
|
||||||
@@ -210,6 +210,16 @@ One-KVM 已上架飞牛 **应用市场**,在 NAS 上直接搜索安装即可
|
|||||||
|
|
||||||
- 爱发电用户_d3d9c
|
- 爱发电用户_d3d9c
|
||||||
|
|
||||||
|
- 爱发电用户_97b41
|
||||||
|
|
||||||
|
- 偶然
|
||||||
|
|
||||||
|
- 爱发电用户_dba45
|
||||||
|
|
||||||
|
- 爱发电用户_d4f8b
|
||||||
|
|
||||||
|
- 故人。
|
||||||
|
|
||||||
- ......
|
- ......
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -218,9 +228,6 @@ One-KVM 已上架飞牛 **应用市场**,在 NAS 上直接搜索安装即可
|
|||||||
|
|
||||||
本项目得到以下赞助商的支持:
|
本项目得到以下赞助商的支持:
|
||||||
|
|
||||||
**镜像下载服务:**
|
|
||||||
- **[重庆大学开源软件镜像站](https://mirrors.cqu.edu.cn/)** - 提供镜像站下载服务
|
|
||||||
|
|
||||||
**文件存储服务:**
|
**文件存储服务:**
|
||||||
- **[Huang1111公益计划](https://pan.huang1111.cn/s/mxkx3T1)** - 提供免登录下载服务
|
- **[Huang1111公益计划](https://pan.huang1111.cn/s/mxkx3T1)** - 提供免登录下载服务
|
||||||
|
|
||||||
|
|||||||
14
agents.md
14
agents.md
@@ -1,14 +0,0 @@
|
|||||||
# Agents Notes
|
|
||||||
|
|
||||||
## Windows MSVC Build
|
|
||||||
|
|
||||||
Run from the repository root in PowerShell:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:VCPKG_ROOT='C:\Users\mofen\code\vcpkg'
|
|
||||||
$env:TURBOJPEG_SOURCE='explicit'
|
|
||||||
$env:TURBOJPEG_LIB_DIR='C:\Users\mofen\code\vcpkg\installed\x64-windows-static\lib'
|
|
||||||
$env:TURBOJPEG_INCLUDE_DIR='C:\Users\mofen\code\vcpkg\installed\x64-windows-static\include'
|
|
||||||
|
|
||||||
cargo build --target x86_64-pc-windows-msvc
|
|
||||||
```
|
|
||||||
7
android/.gitignore
vendored
7
android/.gitignore
vendored
@@ -1,7 +0,0 @@
|
|||||||
.gradle/
|
|
||||||
.kotlin/
|
|
||||||
build/
|
|
||||||
local.properties
|
|
||||||
app/build/
|
|
||||||
app/src/main/jniLibs/
|
|
||||||
native/target/
|
|
||||||
@@ -1,571 +0,0 @@
|
|||||||
import org.gradle.api.tasks.Exec
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.util.Properties
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id("com.android.application")
|
|
||||||
}
|
|
||||||
|
|
||||||
val androidNdkVersion = "27.3.13750724"
|
|
||||||
val androidApiLevel = 21
|
|
||||||
val nativeCrateDir = layout.projectDirectory.dir("../native")
|
|
||||||
val rootCrateDir = layout.projectDirectory.dir("../..")
|
|
||||||
val nativeCargoOutputDir = layout.buildDirectory.dir("generated/oneKvm/cargoJniLibs")
|
|
||||||
val nativeOutputRoot = layout.buildDirectory.dir("generated/oneKvm/jniLibs")
|
|
||||||
val nativeAssetRoot = layout.buildDirectory.dir("generated/oneKvm/assets")
|
|
||||||
val defaultAndroidFfmpegRoot = rootProject.layout.projectDirectory.dir("../dist/android-ffmpeg-mediacodec")
|
|
||||||
val defaultAndroidLibyuvRoot = rootProject.layout.projectDirectory.dir("../dist/android-libyuv")
|
|
||||||
val defaultAndroidTurbojpegRoot = rootProject.layout.projectDirectory.dir("../dist/android-turbojpeg")
|
|
||||||
val defaultAndroidAlsaRoot = rootProject.layout.projectDirectory.dir("../dist/android-alsa")
|
|
||||||
val defaultAndroidOpusRoot = rootProject.layout.projectDirectory.dir("../dist/android-opus")
|
|
||||||
val androidFfmpegRoot = providers.environmentVariable("ONE_KVM_ANDROID_FFMPEG_ROOT")
|
|
||||||
.orElse(defaultAndroidFfmpegRoot.asFile.absolutePath)
|
|
||||||
val androidLibyuvRoot = providers.environmentVariable("ONE_KVM_ANDROID_LIBYUV_ROOT")
|
|
||||||
.orElse(defaultAndroidLibyuvRoot.asFile.absolutePath)
|
|
||||||
val androidTurbojpegRoot = providers.environmentVariable("ONE_KVM_ANDROID_TURBOJPEG_ROOT")
|
|
||||||
.orElse(defaultAndroidTurbojpegRoot.asFile.absolutePath)
|
|
||||||
val androidAlsaRoot = providers.environmentVariable("ONE_KVM_ANDROID_ALSA_ROOT")
|
|
||||||
.orElse(defaultAndroidAlsaRoot.asFile.absolutePath)
|
|
||||||
val androidOpusRoot = providers.environmentVariable("ONE_KVM_ANDROID_OPUS_ROOT")
|
|
||||||
.orElse(defaultAndroidOpusRoot.asFile.absolutePath)
|
|
||||||
val selectedAndroidAbis = providers.environmentVariable("ONE_KVM_ANDROID_ABIS")
|
|
||||||
.orElse("arm64-v8a,armeabi-v7a")
|
|
||||||
.get()
|
|
||||||
.split(',', ' ', ';')
|
|
||||||
.map { it.trim() }
|
|
||||||
.filter { it.isNotEmpty() }
|
|
||||||
.distinct()
|
|
||||||
val androidBuildProfile = providers.environmentVariable("ONE_KVM_ANDROID_PROFILE")
|
|
||||||
.orElse("debug")
|
|
||||||
.get()
|
|
||||||
.lowercase()
|
|
||||||
val oneKvmVersion = Regex("""(?m)^version\s*=\s*"([^"]+)"""")
|
|
||||||
.find(rootCrateDir.file("Cargo.toml").asFile.readText())
|
|
||||||
?.groupValues
|
|
||||||
?.get(1)
|
|
||||||
?: throw GradleException("Failed to resolve version from root Cargo.toml")
|
|
||||||
val androidFfmpegSourceDir = rootProject.layout.projectDirectory
|
|
||||||
.dir("../.tmp/android-ffmpeg-check/src/ffmpeg-rockchip")
|
|
||||||
val localProperties = Properties().apply {
|
|
||||||
val file = rootProject.file("local.properties")
|
|
||||||
if (file.exists()) {
|
|
||||||
file.inputStream().use { load(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val androidSdkDir = file(
|
|
||||||
providers.environmentVariable("ANDROID_HOME")
|
|
||||||
.orElse(providers.environmentVariable("ANDROID_SDK_ROOT"))
|
|
||||||
.orElse(localProperties.getProperty("sdk.dir") ?: "/root/android-sdk")
|
|
||||||
.get(),
|
|
||||||
)
|
|
||||||
val androidNdkDir = androidSdkDir.resolve("ndk/$androidNdkVersion")
|
|
||||||
|
|
||||||
val androidFfmpegBuildScript = rootProject.layout.projectDirectory
|
|
||||||
.dir("..")
|
|
||||||
.file("scripts/build-android-ffmpeg-mediacodec.sh")
|
|
||||||
val androidLibyuvBuildScript = rootProject.layout.projectDirectory
|
|
||||||
.dir("..")
|
|
||||||
.file("scripts/build-android-libyuv.sh")
|
|
||||||
val androidTurbojpegBuildScript = rootProject.layout.projectDirectory
|
|
||||||
.dir("..")
|
|
||||||
.file("scripts/build-android-turbojpeg.sh")
|
|
||||||
val androidAlsaBuildScript = rootProject.layout.projectDirectory
|
|
||||||
.dir("..")
|
|
||||||
.file("scripts/build-android-alsa.sh")
|
|
||||||
val androidOpusBuildScript = rootProject.layout.projectDirectory
|
|
||||||
.dir("..")
|
|
||||||
.file("scripts/build-android-opus.sh")
|
|
||||||
|
|
||||||
val androidAbiTargets = mapOf(
|
|
||||||
"arm64-v8a" to Triple("arm64", "aarch64-linux-android", "aarch64-linux-android"),
|
|
||||||
"armeabi-v7a" to Triple("arm32", "armv7-linux-androideabi", "arm-linux-androideabi"),
|
|
||||||
)
|
|
||||||
|
|
||||||
val selectedAndroidAbiTargets = selectedAndroidAbis.associateWith { abi ->
|
|
||||||
androidAbiTargets[abi] ?: throw GradleException(
|
|
||||||
"Unsupported ONE_KVM_ANDROID_ABIS entry: $abi. Supported values: ${androidAbiTargets.keys.joinToString(", ")}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (androidBuildProfile != "debug" && androidBuildProfile != "release") {
|
|
||||||
throw GradleException("Unsupported ONE_KVM_ANDROID_PROFILE: $androidBuildProfile. Use debug or release.")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun androidFfmpegBuildStamp(script: File): String {
|
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
|
||||||
.digest(script.readBytes())
|
|
||||||
.joinToString("") { "%02x".format(it) }
|
|
||||||
return "api=$androidApiLevel;abis=${selectedAndroidAbis.joinToString(",")};script=$digest"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun androidFfmpegRequiredFiles(root: File): List<File> = listOf(
|
|
||||||
"include/libavcodec/avcodec.h",
|
|
||||||
"lib/libavcodec.a",
|
|
||||||
"lib/libavutil.a",
|
|
||||||
).flatMap { path -> selectedAndroidAbis.map { abi -> root.resolve("$abi/$path") } }
|
|
||||||
|
|
||||||
fun androidLibyuvBuildStamp(script: File): String {
|
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
|
||||||
.digest(script.readBytes())
|
|
||||||
.joinToString("") { "%02x".format(it) }
|
|
||||||
val turbojpegScriptDigest = MessageDigest.getInstance("SHA-256")
|
|
||||||
.digest(androidTurbojpegBuildScript.asFile.readBytes())
|
|
||||||
.joinToString("") { "%02x".format(it) }
|
|
||||||
return "api=$androidApiLevel;abis=${selectedAndroidAbis.joinToString(",")};script=$digest;turbojpegScript=$turbojpegScriptDigest"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun androidLibyuvRequiredFiles(root: File): List<File> = listOf(
|
|
||||||
"include/libyuv.h",
|
|
||||||
"lib/libyuv.a",
|
|
||||||
).flatMap { path -> selectedAndroidAbis.map { abi -> root.resolve("$abi/$path") } }
|
|
||||||
|
|
||||||
fun androidTurbojpegBuildStamp(script: File): String {
|
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
|
||||||
.digest(script.readBytes())
|
|
||||||
.joinToString("") { "%02x".format(it) }
|
|
||||||
return "api=$androidApiLevel;abis=${selectedAndroidAbis.joinToString(",")};script=$digest"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun androidAlsaBuildStamp(script: File): String {
|
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
|
||||||
.digest(script.readBytes())
|
|
||||||
.joinToString("") { "%02x".format(it) }
|
|
||||||
return "api=$androidApiLevel;abis=${selectedAndroidAbis.joinToString(",")};script=$digest"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun androidOpusBuildStamp(script: File): String {
|
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
|
||||||
.digest(script.readBytes())
|
|
||||||
.joinToString("") { "%02x".format(it) }
|
|
||||||
return "api=$androidApiLevel;abis=${selectedAndroidAbis.joinToString(",")};script=$digest"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun androidTurbojpegRequiredFiles(root: File): List<File> = listOf(
|
|
||||||
"include/turbojpeg.h",
|
|
||||||
"include/jpeglib.h",
|
|
||||||
"lib/libjpeg.a",
|
|
||||||
"lib/libturbojpeg.a",
|
|
||||||
).flatMap { path -> selectedAndroidAbis.map { abi -> root.resolve("$abi/$path") } }
|
|
||||||
|
|
||||||
fun androidAlsaRequiredFiles(root: File): List<File> = listOf(
|
|
||||||
"include/alsa/asoundlib.h",
|
|
||||||
"lib/libasound.so",
|
|
||||||
).flatMap { path -> selectedAndroidAbis.map { abi -> root.resolve("$abi/$path") } }
|
|
||||||
|
|
||||||
fun androidOpusRequiredFiles(root: File): List<File> = listOf(
|
|
||||||
"include/opus/opus.h",
|
|
||||||
"lib/libopus.so",
|
|
||||||
).flatMap { path -> selectedAndroidAbis.map { abi -> root.resolve("$abi/$path") } }
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "cn.one_kvm.androidhost"
|
|
||||||
compileSdk = 36
|
|
||||||
ndkVersion = androidNdkVersion
|
|
||||||
flavorDimensions += "abi"
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId = "cn.one_kvm.androidhost"
|
|
||||||
minSdk = androidApiLevel
|
|
||||||
targetSdk = 36
|
|
||||||
versionCode = 1
|
|
||||||
versionName = oneKvmVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
productFlavors {
|
|
||||||
create("arm32") {
|
|
||||||
dimension = "abi"
|
|
||||||
ndk {
|
|
||||||
abiFilters += "armeabi-v7a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
create("arm64") {
|
|
||||||
dimension = "abi"
|
|
||||||
ndk {
|
|
||||||
abiFilters += "arm64-v8a"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
getByName("main") {
|
|
||||||
assets.directories.clear()
|
|
||||||
jniLibs.directories.clear()
|
|
||||||
}
|
|
||||||
getByName("arm32") {
|
|
||||||
assets.directories.add("build/generated/oneKvm/assets/arm32")
|
|
||||||
jniLibs.directories.add("build/generated/oneKvm/jniLibs/arm32")
|
|
||||||
}
|
|
||||||
getByName("arm64") {
|
|
||||||
assets.directories.add("build/generated/oneKvm/assets/arm64")
|
|
||||||
jniLibs.directories.add("build/generated/oneKvm/jniLibs/arm64")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Exec>("buildAndroidFfmpegMediaCodec") {
|
|
||||||
description = "Builds the default Android FFmpeg MediaCodec static libraries."
|
|
||||||
group = "build"
|
|
||||||
|
|
||||||
val ffmpegRoot = file(androidFfmpegRoot.get())
|
|
||||||
val sourceDir = androidFfmpegSourceDir.asFile
|
|
||||||
val scriptFile = androidFfmpegBuildScript.asFile
|
|
||||||
val stampFile = ffmpegRoot.resolve(".one-kvm-android-ffmpeg.stamp")
|
|
||||||
|
|
||||||
workingDir = rootProject.layout.projectDirectory.dir("..").asFile
|
|
||||||
commandLine(
|
|
||||||
"bash",
|
|
||||||
scriptFile.absolutePath,
|
|
||||||
"--source",
|
|
||||||
sourceDir.absolutePath,
|
|
||||||
"--output",
|
|
||||||
ffmpegRoot.absolutePath,
|
|
||||||
"--ndk",
|
|
||||||
androidNdkDir.absolutePath,
|
|
||||||
"--api",
|
|
||||||
androidApiLevel.toString(),
|
|
||||||
"--abis",
|
|
||||||
selectedAndroidAbis.joinToString(","),
|
|
||||||
)
|
|
||||||
|
|
||||||
inputs.dir(sourceDir)
|
|
||||||
inputs.file(scriptFile)
|
|
||||||
outputs.dir(ffmpegRoot)
|
|
||||||
|
|
||||||
onlyIf {
|
|
||||||
val hasAndroidFfmpeg = androidFfmpegRequiredFiles(ffmpegRoot).all { it.exists() }
|
|
||||||
val hasCurrentBuildStamp =
|
|
||||||
stampFile.exists() && stampFile.readText() == androidFfmpegBuildStamp(scriptFile)
|
|
||||||
if (!hasAndroidFfmpeg && !sourceDir.resolve("configure").exists()) {
|
|
||||||
throw GradleException(
|
|
||||||
"Missing Android FFmpeg MediaCodec build at ${ffmpegRoot.absolutePath}, " +
|
|
||||||
"and source was not found at ${sourceDir.absolutePath}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
!hasAndroidFfmpeg || !hasCurrentBuildStamp
|
|
||||||
}
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
stampFile.writeText(androidFfmpegBuildStamp(scriptFile))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Exec>("buildAndroidLibyuv") {
|
|
||||||
description = "Builds Android libyuv static libraries."
|
|
||||||
group = "build"
|
|
||||||
|
|
||||||
val libyuvRoot = file(androidLibyuvRoot.get())
|
|
||||||
val turbojpegRoot = file(androidTurbojpegRoot.get())
|
|
||||||
val scriptFile = androidLibyuvBuildScript.asFile
|
|
||||||
val stampFile = libyuvRoot.resolve(".one-kvm-android-libyuv.stamp")
|
|
||||||
|
|
||||||
dependsOn("buildAndroidTurbojpeg")
|
|
||||||
|
|
||||||
workingDir = rootProject.layout.projectDirectory.dir("..").asFile
|
|
||||||
commandLine(
|
|
||||||
"bash",
|
|
||||||
scriptFile.absolutePath,
|
|
||||||
"--output",
|
|
||||||
libyuvRoot.absolutePath,
|
|
||||||
"--ndk",
|
|
||||||
androidNdkDir.absolutePath,
|
|
||||||
"--api",
|
|
||||||
androidApiLevel.toString(),
|
|
||||||
"--abis",
|
|
||||||
selectedAndroidAbis.joinToString(","),
|
|
||||||
"--jpeg-root",
|
|
||||||
turbojpegRoot.absolutePath,
|
|
||||||
)
|
|
||||||
|
|
||||||
inputs.file(scriptFile)
|
|
||||||
outputs.dir(libyuvRoot)
|
|
||||||
|
|
||||||
onlyIf {
|
|
||||||
val hasAndroidLibyuv = androidLibyuvRequiredFiles(libyuvRoot).all { it.exists() }
|
|
||||||
val hasCurrentBuildStamp =
|
|
||||||
stampFile.exists() && stampFile.readText() == androidLibyuvBuildStamp(scriptFile)
|
|
||||||
!hasAndroidLibyuv || !hasCurrentBuildStamp
|
|
||||||
}
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
stampFile.writeText(androidLibyuvBuildStamp(scriptFile))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Exec>("buildAndroidTurbojpeg") {
|
|
||||||
description = "Builds Android TurboJPEG static libraries."
|
|
||||||
group = "build"
|
|
||||||
|
|
||||||
val turbojpegRoot = file(androidTurbojpegRoot.get())
|
|
||||||
val scriptFile = androidTurbojpegBuildScript.asFile
|
|
||||||
val stampFile = turbojpegRoot.resolve(".one-kvm-android-turbojpeg.stamp")
|
|
||||||
|
|
||||||
workingDir = rootProject.layout.projectDirectory.dir("..").asFile
|
|
||||||
commandLine(
|
|
||||||
"bash",
|
|
||||||
scriptFile.absolutePath,
|
|
||||||
"--output",
|
|
||||||
turbojpegRoot.absolutePath,
|
|
||||||
"--ndk",
|
|
||||||
androidNdkDir.absolutePath,
|
|
||||||
"--api",
|
|
||||||
androidApiLevel.toString(),
|
|
||||||
"--abis",
|
|
||||||
selectedAndroidAbis.joinToString(","),
|
|
||||||
)
|
|
||||||
|
|
||||||
inputs.file(scriptFile)
|
|
||||||
outputs.dir(turbojpegRoot)
|
|
||||||
|
|
||||||
onlyIf {
|
|
||||||
val hasAndroidTurbojpeg = androidTurbojpegRequiredFiles(turbojpegRoot).all { it.exists() }
|
|
||||||
val hasCurrentBuildStamp =
|
|
||||||
stampFile.exists() && stampFile.readText() == androidTurbojpegBuildStamp(scriptFile)
|
|
||||||
!hasAndroidTurbojpeg || !hasCurrentBuildStamp
|
|
||||||
}
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
stampFile.writeText(androidTurbojpegBuildStamp(scriptFile))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Exec>("buildAndroidAlsa") {
|
|
||||||
description = "Builds Android ALSA shared libraries."
|
|
||||||
group = "build"
|
|
||||||
|
|
||||||
val alsaRoot = file(androidAlsaRoot.get())
|
|
||||||
val scriptFile = androidAlsaBuildScript.asFile
|
|
||||||
val stampFile = alsaRoot.resolve(".one-kvm-android-alsa.stamp")
|
|
||||||
|
|
||||||
workingDir = rootProject.layout.projectDirectory.dir("..").asFile
|
|
||||||
commandLine(
|
|
||||||
"bash",
|
|
||||||
scriptFile.absolutePath,
|
|
||||||
"--output",
|
|
||||||
alsaRoot.absolutePath,
|
|
||||||
"--ndk",
|
|
||||||
androidNdkDir.absolutePath,
|
|
||||||
"--api",
|
|
||||||
androidApiLevel.toString(),
|
|
||||||
"--abis",
|
|
||||||
selectedAndroidAbis.joinToString(","),
|
|
||||||
)
|
|
||||||
|
|
||||||
inputs.file(scriptFile)
|
|
||||||
outputs.dir(alsaRoot)
|
|
||||||
|
|
||||||
onlyIf {
|
|
||||||
val hasAndroidAlsa = androidAlsaRequiredFiles(alsaRoot).all { it.exists() }
|
|
||||||
val hasCurrentBuildStamp =
|
|
||||||
stampFile.exists() && stampFile.readText() == androidAlsaBuildStamp(scriptFile)
|
|
||||||
!hasAndroidAlsa || !hasCurrentBuildStamp
|
|
||||||
}
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
stampFile.writeText(androidAlsaBuildStamp(scriptFile))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Exec>("buildAndroidOpus") {
|
|
||||||
description = "Builds Android Opus shared libraries."
|
|
||||||
group = "build"
|
|
||||||
|
|
||||||
val opusRoot = file(androidOpusRoot.get())
|
|
||||||
val scriptFile = androidOpusBuildScript.asFile
|
|
||||||
val stampFile = opusRoot.resolve(".one-kvm-android-opus.stamp")
|
|
||||||
|
|
||||||
workingDir = rootProject.layout.projectDirectory.dir("..").asFile
|
|
||||||
commandLine(
|
|
||||||
"bash",
|
|
||||||
scriptFile.absolutePath,
|
|
||||||
"--output",
|
|
||||||
opusRoot.absolutePath,
|
|
||||||
"--ndk",
|
|
||||||
androidNdkDir.absolutePath,
|
|
||||||
"--api",
|
|
||||||
androidApiLevel.toString(),
|
|
||||||
"--abis",
|
|
||||||
selectedAndroidAbis.joinToString(","),
|
|
||||||
)
|
|
||||||
|
|
||||||
inputs.file(scriptFile)
|
|
||||||
outputs.dir(opusRoot)
|
|
||||||
|
|
||||||
onlyIf {
|
|
||||||
val hasAndroidOpus = androidOpusRequiredFiles(opusRoot).all { it.exists() }
|
|
||||||
val hasCurrentBuildStamp =
|
|
||||||
stampFile.exists() && stampFile.readText() == androidOpusBuildStamp(scriptFile)
|
|
||||||
!hasAndroidOpus || !hasCurrentBuildStamp
|
|
||||||
}
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
stampFile.writeText(androidOpusBuildStamp(scriptFile))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val cargoBuildAndroidAbiTaskNames = selectedAndroidAbiTargets.map { (abi, targets) ->
|
|
||||||
val (flavor, _, _) = targets
|
|
||||||
val taskName = "cargoBuildAndroid" + flavor.replaceFirstChar {
|
|
||||||
if (it.isLowerCase()) it.titlecase() else it.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Exec>(taskName) {
|
|
||||||
description = "Builds the Android Rust bootstrap libraries for $abi."
|
|
||||||
group = "build"
|
|
||||||
|
|
||||||
dependsOn(
|
|
||||||
"buildAndroidFfmpegMediaCodec",
|
|
||||||
"buildAndroidLibyuv",
|
|
||||||
"buildAndroidTurbojpeg",
|
|
||||||
"buildAndroidAlsa",
|
|
||||||
"buildAndroidOpus",
|
|
||||||
)
|
|
||||||
|
|
||||||
val cargoCommand = mutableListOf(
|
|
||||||
"cargo",
|
|
||||||
"ndk",
|
|
||||||
"-t",
|
|
||||||
abi,
|
|
||||||
"-P",
|
|
||||||
androidApiLevel.toString(),
|
|
||||||
"-o",
|
|
||||||
nativeCargoOutputDir.get().asFile.absolutePath,
|
|
||||||
"build",
|
|
||||||
"--lib",
|
|
||||||
"--bins",
|
|
||||||
)
|
|
||||||
if (androidBuildProfile == "release") {
|
|
||||||
cargoCommand.add("--release")
|
|
||||||
}
|
|
||||||
|
|
||||||
workingDir = nativeCrateDir.asFile
|
|
||||||
commandLine(cargoCommand)
|
|
||||||
args("--features", "android-mediacodec")
|
|
||||||
environment("ONE_KVM_ANDROID_FFMPEG_ROOT", androidFfmpegRoot.get())
|
|
||||||
environment("ONE_KVM_ANDROID_LIBYUV_ROOT", androidLibyuvRoot.get())
|
|
||||||
environment("ONE_KVM_ANDROID_LIBYUV_STATIC", "1")
|
|
||||||
environment("TURBOJPEG_SOURCE", "explicit")
|
|
||||||
environment("TURBOJPEG_STATIC", "1")
|
|
||||||
environment(
|
|
||||||
"TURBOJPEG_LIB_DIR",
|
|
||||||
file(androidTurbojpegRoot.get()).resolve("$abi/lib").absolutePath,
|
|
||||||
)
|
|
||||||
environment(
|
|
||||||
"TURBOJPEG_INCLUDE_DIR",
|
|
||||||
file(androidTurbojpegRoot.get()).resolve("$abi/include").absolutePath,
|
|
||||||
)
|
|
||||||
environment("PKG_CONFIG_ALLOW_CROSS", "1")
|
|
||||||
environment(
|
|
||||||
"PKG_CONFIG_LIBDIR",
|
|
||||||
file(androidAlsaRoot.get()).resolve("$abi/lib/pkgconfig").absolutePath,
|
|
||||||
)
|
|
||||||
environment("PKG_CONFIG_SYSROOT_DIR", "")
|
|
||||||
environment("LIBOPUS_NO_PKG", "1")
|
|
||||||
environment("LIBOPUS_LIB_DIR", file(androidOpusRoot.get()).resolve("$abi/lib").absolutePath)
|
|
||||||
environment("ANDROID_HOME", androidSdkDir.absolutePath)
|
|
||||||
environment("ANDROID_SDK_ROOT", androidSdkDir.absolutePath)
|
|
||||||
environment("ANDROID_NDK_HOME", androidNdkDir.absolutePath)
|
|
||||||
environment("ANDROID_NDK", androidNdkDir.absolutePath)
|
|
||||||
environment("ANDROID_NDK_ROOT", androidNdkDir.absolutePath)
|
|
||||||
|
|
||||||
inputs.files(
|
|
||||||
nativeCrateDir.file("Cargo.toml"),
|
|
||||||
nativeCrateDir.dir("src"),
|
|
||||||
rootCrateDir.file("Cargo.lock"),
|
|
||||||
rootCrateDir.file("Cargo.toml"),
|
|
||||||
rootCrateDir.file("build.rs"),
|
|
||||||
rootCrateDir.dir("libs"),
|
|
||||||
rootCrateDir.dir("res/vcpkg/libyuv"),
|
|
||||||
rootCrateDir.dir("src"),
|
|
||||||
)
|
|
||||||
outputs.dir(nativeCargoOutputDir)
|
|
||||||
outputs.dir(file(androidFfmpegRoot.get()))
|
|
||||||
outputs.dir(file(androidLibyuvRoot.get()))
|
|
||||||
outputs.dir(file(androidTurbojpegRoot.get()))
|
|
||||||
outputs.dir(file(androidAlsaRoot.get()))
|
|
||||||
outputs.dir(file(androidOpusRoot.get()))
|
|
||||||
}
|
|
||||||
|
|
||||||
taskName
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register("cargoBuildAndroid") {
|
|
||||||
description = "Builds the Android Rust bootstrap libraries."
|
|
||||||
group = "build"
|
|
||||||
|
|
||||||
dependsOn(cargoBuildAndroidAbiTaskNames)
|
|
||||||
|
|
||||||
outputs.dir(nativeOutputRoot)
|
|
||||||
outputs.dir(nativeAssetRoot)
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
selectedAndroidAbiTargets.forEach { (abi, targets) ->
|
|
||||||
val (flavor, rustTriple, ndkTriple) = targets
|
|
||||||
val nativeLibSource = nativeCargoOutputDir.get().file("$abi/libone_kvm_android_bootstrap.so").asFile
|
|
||||||
if (!nativeLibSource.exists()) {
|
|
||||||
throw GradleException("Missing Android JNI library: ${nativeLibSource.absolutePath}")
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(nativeLibSource)
|
|
||||||
into(nativeOutputRoot.get().dir(flavor).dir(abi))
|
|
||||||
}
|
|
||||||
|
|
||||||
val source = nativeCrateDir.file("target/$rustTriple/$androidBuildProfile/one-kvm-android-host").asFile
|
|
||||||
if (!source.exists()) {
|
|
||||||
throw GradleException("Missing Android host binary: ${source.absolutePath}")
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(source)
|
|
||||||
into(nativeAssetRoot.get().dir(flavor).dir("bin/$abi"))
|
|
||||||
rename { "one-kvm-android-host" }
|
|
||||||
}
|
|
||||||
|
|
||||||
val cxxShared = androidNdkDir
|
|
||||||
.resolve("toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/$ndkTriple/libc++_shared.so")
|
|
||||||
if (!cxxShared.exists()) {
|
|
||||||
throw GradleException("Missing NDK libc++_shared.so: ${cxxShared.absolutePath}")
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(cxxShared)
|
|
||||||
into(nativeOutputRoot.get().dir(flavor).dir(abi))
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(cxxShared)
|
|
||||||
into(nativeAssetRoot.get().dir(flavor).dir("bin/$abi"))
|
|
||||||
}
|
|
||||||
|
|
||||||
val alsaShared = file(androidAlsaRoot.get()).resolve("$abi/lib/libasound.so")
|
|
||||||
if (!alsaShared.exists()) {
|
|
||||||
throw GradleException("Missing Android ALSA library: ${alsaShared.absolutePath}")
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(alsaShared)
|
|
||||||
into(nativeOutputRoot.get().dir(flavor).dir(abi))
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(alsaShared)
|
|
||||||
into(nativeAssetRoot.get().dir(flavor).dir("bin/$abi"))
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(file(androidAlsaRoot.get()).resolve("$abi/share/alsa"))
|
|
||||||
into(nativeAssetRoot.get().dir(flavor).dir("bin/$abi/alsa"))
|
|
||||||
}
|
|
||||||
|
|
||||||
val opusShared = file(androidOpusRoot.get()).resolve("$abi/lib/libopus.so")
|
|
||||||
if (!opusShared.exists()) {
|
|
||||||
throw GradleException("Missing Android Opus library: ${opusShared.absolutePath}")
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(opusShared)
|
|
||||||
into(nativeOutputRoot.get().dir(flavor).dir(abi))
|
|
||||||
}
|
|
||||||
copy {
|
|
||||||
from(opusShared)
|
|
||||||
into(nativeAssetRoot.get().dir(flavor).dir("bin/$abi"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("preBuild") {
|
|
||||||
dependsOn("cargoBuildAndroid")
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="false"
|
|
||||||
android:icon="@drawable/ic_launcher_one_kvm"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/AppTheme">
|
|
||||||
<service
|
|
||||||
android:name=".OneKvmService"
|
|
||||||
android:exported="false"
|
|
||||||
android:foregroundServiceType="connectedDevice" />
|
|
||||||
<receiver
|
|
||||||
android:name=".BootReceiver"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package cn.one_kvm.androidhost
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
|
|
||||||
class BootReceiver : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED && HostSettings.getAutoStart(context)) {
|
|
||||||
OneKvmService.start(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package cn.one_kvm.androidhost
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
|
|
||||||
object HostSettings {
|
|
||||||
private const val PREFS = "one_kvm_android"
|
|
||||||
private const val KEY_AUTO_START = "auto_start"
|
|
||||||
private const val KEY_CLEAR_EXISTING_OTG = "clear_existing_otg"
|
|
||||||
|
|
||||||
fun getAutoStart(context: Context): Boolean {
|
|
||||||
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
.getBoolean(KEY_AUTO_START, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAutoStart(context: Context, enabled: Boolean) {
|
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
.edit()
|
|
||||||
.putBoolean(KEY_AUTO_START, enabled)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getClearExistingOtg(context: Context): Boolean {
|
|
||||||
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
.getBoolean(KEY_CLEAR_EXISTING_OTG, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setClearExistingOtg(context: Context, enabled: Boolean) {
|
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
.edit()
|
|
||||||
.putBoolean(KEY_CLEAR_EXISTING_OTG, enabled)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package cn.one_kvm.androidhost
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
|
|
||||||
object LogConfig {
|
|
||||||
private const val PREFS = "one_kvm_android"
|
|
||||||
private const val KEY_LOG_LEVEL = "log_level"
|
|
||||||
const val DEFAULT_LEVEL = "info"
|
|
||||||
val LEVELS = arrayOf("error", "warn", "info", "debug", "trace")
|
|
||||||
|
|
||||||
fun getLevel(context: Context): String {
|
|
||||||
val value = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
.getString(KEY_LOG_LEVEL, DEFAULT_LEVEL)
|
|
||||||
?: DEFAULT_LEVEL
|
|
||||||
return if (LEVELS.contains(value)) value else DEFAULT_LEVEL
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLevel(context: Context, level: String) {
|
|
||||||
val safeLevel = if (LEVELS.contains(level)) level else DEFAULT_LEVEL
|
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
.edit()
|
|
||||||
.putString(KEY_LOG_LEVEL, safeLevel)
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun rustLogFilter(level: String): String {
|
|
||||||
val safeLevel = if (LEVELS.contains(level)) level else DEFAULT_LEVEL
|
|
||||||
return "one_kvm=$safeLevel,hwcodec=$safeLevel,tower_http=$safeLevel,webrtc_sctp=warn"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
package cn.one_kvm.androidhost
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
object LogStore {
|
|
||||||
private const val FLUSH_DELAY_MS = 250L
|
|
||||||
private const val MAX_BUFFER_CHARS = 64 * 1024
|
|
||||||
|
|
||||||
private val lock = Any()
|
|
||||||
private val buffer = StringBuilder()
|
|
||||||
private val executor = Executors.newSingleThreadScheduledExecutor { runnable ->
|
|
||||||
Thread(runnable, "OneKvmLogStore").apply { isDaemon = true }
|
|
||||||
}
|
|
||||||
|
|
||||||
private var logFile: File? = null
|
|
||||||
private var flushScheduled = false
|
|
||||||
|
|
||||||
fun defaultLogFile(context: Context): File {
|
|
||||||
return File(File(context.getExternalFilesDir(null), "runtime"), "one-kvm.log")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun configure(file: File) {
|
|
||||||
synchronized(lock) {
|
|
||||||
flushLocked()
|
|
||||||
file.parentFile?.mkdirs()
|
|
||||||
file.writeText("")
|
|
||||||
buffer.clear()
|
|
||||||
logFile = file
|
|
||||||
flushScheduled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun append(line: String) {
|
|
||||||
synchronized(lock) {
|
|
||||||
if (logFile == null) return
|
|
||||||
|
|
||||||
buffer.append(line).append('\n')
|
|
||||||
if (buffer.length >= MAX_BUFFER_CHARS) {
|
|
||||||
flushLocked()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!flushScheduled) {
|
|
||||||
flushScheduled = true
|
|
||||||
executor.schedule({ flush() }, FLUSH_DELAY_MS, TimeUnit.MILLISECONDS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun flush() {
|
|
||||||
synchronized(lock) {
|
|
||||||
flushLocked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun flushLocked() {
|
|
||||||
val file = logFile ?: return
|
|
||||||
if (buffer.isEmpty()) {
|
|
||||||
flushScheduled = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val text = buffer.toString()
|
|
||||||
buffer.clear()
|
|
||||||
flushScheduled = false
|
|
||||||
file.appendText(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,452 +0,0 @@
|
|||||||
package cn.one_kvm.androidhost
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.graphics.drawable.GradientDrawable
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.ArrayAdapter
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.CompoundButton
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.ScrollView
|
|
||||||
import android.widget.Spinner
|
|
||||||
import android.widget.Switch
|
|
||||||
import android.widget.TextView
|
|
||||||
import java.net.Inet4Address
|
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import java.net.NetworkInterface
|
|
||||||
import java.net.Socket
|
|
||||||
import java.util.Collections
|
|
||||||
|
|
||||||
class MainActivity : Activity() {
|
|
||||||
private lateinit var statusValue: TextView
|
|
||||||
private lateinit var hostActionButton: Button
|
|
||||||
private lateinit var logLevelSpinner: Spinner
|
|
||||||
private lateinit var autoStartSwitch: Switch
|
|
||||||
private lateinit var clearOtgSwitch: Switch
|
|
||||||
private val statusHandler = Handler(Looper.getMainLooper())
|
|
||||||
private var statusPollsRemaining = 0
|
|
||||||
private val statusPoller = object : Runnable {
|
|
||||||
override fun run() {
|
|
||||||
refreshStatus()
|
|
||||||
statusPollsRemaining -= 1
|
|
||||||
if (statusPollsRemaining > 0) {
|
|
||||||
statusHandler.postDelayed(this, STATUS_POLL_INTERVAL_MS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
window.statusBarColor = color("#F8FAFC")
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
window.navigationBarColor = color("#F8FAFC")
|
|
||||||
}
|
|
||||||
|
|
||||||
val content = LinearLayout(this).apply {
|
|
||||||
orientation = LinearLayout.VERTICAL
|
|
||||||
setPadding(20.dp(), 24.dp(), 20.dp(), 28.dp())
|
|
||||||
background = solid("#F8FAFC")
|
|
||||||
}
|
|
||||||
|
|
||||||
content.addView(startCard())
|
|
||||||
content.addView(settingsCard())
|
|
||||||
content.addView(infoCard())
|
|
||||||
|
|
||||||
setContentView(ScrollView(this).apply {
|
|
||||||
isFillViewport = true
|
|
||||||
setBackgroundColor(color("#F8FAFC"))
|
|
||||||
addView(content)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
reconcilePersistedStatus()
|
|
||||||
refreshStatus()
|
|
||||||
autoStartSwitch.isChecked = HostSettings.getAutoStart(this)
|
|
||||||
clearOtgSwitch.isChecked = HostSettings.getClearExistingOtg(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
statusHandler.removeCallbacks(statusPoller)
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startCard(): View {
|
|
||||||
return card {
|
|
||||||
addView(sectionTitle("启动管理"))
|
|
||||||
addView(TextView(this@MainActivity).apply {
|
|
||||||
text = "管理本机 One-KVM 服务进程。暂停会停止前台服务并释放运行资源。"
|
|
||||||
textSize = 14f
|
|
||||||
setTextColor(color("#64748B"))
|
|
||||||
setPadding(0, 6.dp(), 0, 14.dp())
|
|
||||||
})
|
|
||||||
|
|
||||||
statusValue = TextView(this@MainActivity).apply {
|
|
||||||
textSize = 14f
|
|
||||||
typeface = Typeface.DEFAULT_BOLD
|
|
||||||
setTextColor(color("#0F172A"))
|
|
||||||
background = rounded("#EFF6FF", "#BFDBFE", 8)
|
|
||||||
setPadding(12.dp(), 8.dp(), 12.dp(), 8.dp())
|
|
||||||
}
|
|
||||||
addView(statusValue, matchWrap())
|
|
||||||
|
|
||||||
addView(LinearLayout(this@MainActivity).apply {
|
|
||||||
orientation = LinearLayout.HORIZONTAL
|
|
||||||
gravity = Gravity.CENTER_VERTICAL
|
|
||||||
setPadding(0, 14.dp(), 0, 0)
|
|
||||||
hostActionButton = actionButton("启动", primary = true) { toggleHost() }
|
|
||||||
addView(hostActionButton, matchButton())
|
|
||||||
})
|
|
||||||
refreshStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun settingsCard(): View {
|
|
||||||
return card {
|
|
||||||
addView(sectionTitle("运行设置"))
|
|
||||||
|
|
||||||
val (autoStartRow, autoStartControl) = settingSwitchRow(
|
|
||||||
title = "开机自启动",
|
|
||||||
subtitle = "系统启动完成后自动拉起 One-KVM 前台服务。",
|
|
||||||
checked = HostSettings.getAutoStart(this@MainActivity),
|
|
||||||
) { _, checked ->
|
|
||||||
HostSettings.setAutoStart(this@MainActivity, checked)
|
|
||||||
LogStore.append("Boot auto-start ${if (checked) "enabled" else "disabled"}")
|
|
||||||
}
|
|
||||||
autoStartSwitch = autoStartControl
|
|
||||||
addView(autoStartRow)
|
|
||||||
|
|
||||||
addView(divider())
|
|
||||||
|
|
||||||
val (clearOtgRow, clearOtgControl) = settingSwitchRow(
|
|
||||||
title = "清除已有 OTG Gadget",
|
|
||||||
subtitle = "启动 root host 前尝试解绑并删除 configfs 中已有的 USB gadget。",
|
|
||||||
checked = HostSettings.getClearExistingOtg(this@MainActivity),
|
|
||||||
) { _, checked ->
|
|
||||||
HostSettings.setClearExistingOtg(this@MainActivity, checked)
|
|
||||||
LogStore.append("Clear existing OTG gadget ${if (checked) "enabled" else "disabled"}")
|
|
||||||
}
|
|
||||||
clearOtgSwitch = clearOtgControl
|
|
||||||
addView(clearOtgRow)
|
|
||||||
|
|
||||||
addView(divider())
|
|
||||||
addView(logLevelRow())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun infoCard(): View {
|
|
||||||
return card {
|
|
||||||
addView(sectionTitle("应用信息"))
|
|
||||||
addView(infoRow("软件内核版本", kernelVersion()))
|
|
||||||
addView(infoRow("访问地址", accessAddresses(), selectable = true))
|
|
||||||
addView(infoRow("日志文件", LogStore.defaultLogFile(this@MainActivity).absolutePath, selectable = true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun settingSwitchRow(
|
|
||||||
title: String,
|
|
||||||
subtitle: String,
|
|
||||||
checked: Boolean,
|
|
||||||
listener: CompoundButton.OnCheckedChangeListener,
|
|
||||||
): Pair<View, Switch> {
|
|
||||||
val switch = Switch(this).apply {
|
|
||||||
isChecked = checked
|
|
||||||
setOnCheckedChangeListener(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
val row = LinearLayout(this).apply {
|
|
||||||
orientation = LinearLayout.HORIZONTAL
|
|
||||||
gravity = Gravity.CENTER_VERTICAL
|
|
||||||
setPadding(0, 12.dp(), 0, 12.dp())
|
|
||||||
addView(LinearLayout(this@MainActivity).apply {
|
|
||||||
orientation = LinearLayout.VERTICAL
|
|
||||||
addView(TextView(this@MainActivity).apply {
|
|
||||||
text = title
|
|
||||||
textSize = 15f
|
|
||||||
typeface = Typeface.DEFAULT_BOLD
|
|
||||||
setTextColor(color("#0F172A"))
|
|
||||||
})
|
|
||||||
addView(TextView(this@MainActivity).apply {
|
|
||||||
text = subtitle
|
|
||||||
textSize = 13f
|
|
||||||
setTextColor(color("#64748B"))
|
|
||||||
setPadding(0, 4.dp(), 12.dp(), 0)
|
|
||||||
})
|
|
||||||
}, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
|
|
||||||
addView(switch)
|
|
||||||
}
|
|
||||||
|
|
||||||
return row to switch
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun infoRow(label: String, value: String, selectable: Boolean = false): View {
|
|
||||||
return LinearLayout(this).apply {
|
|
||||||
orientation = LinearLayout.VERTICAL
|
|
||||||
setPadding(0, 12.dp(), 0, 12.dp())
|
|
||||||
addView(TextView(this@MainActivity).apply {
|
|
||||||
text = label
|
|
||||||
textSize = 13f
|
|
||||||
setTextColor(color("#64748B"))
|
|
||||||
})
|
|
||||||
addView(TextView(this@MainActivity).apply {
|
|
||||||
text = value
|
|
||||||
textSize = 15f
|
|
||||||
setTextColor(color("#0F172A"))
|
|
||||||
setPadding(0, 4.dp(), 0, 0)
|
|
||||||
setTextIsSelectable(selectable)
|
|
||||||
})
|
|
||||||
addView(divider())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun logLevelRow(): View {
|
|
||||||
return LinearLayout(this).apply {
|
|
||||||
orientation = LinearLayout.HORIZONTAL
|
|
||||||
gravity = Gravity.CENTER_VERTICAL
|
|
||||||
setPadding(0, 12.dp(), 0, 0)
|
|
||||||
addView(TextView(this@MainActivity).apply {
|
|
||||||
text = "日志级别"
|
|
||||||
textSize = 15f
|
|
||||||
typeface = Typeface.DEFAULT_BOLD
|
|
||||||
setTextColor(color("#0F172A"))
|
|
||||||
}, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
|
|
||||||
|
|
||||||
logLevelSpinner = Spinner(this@MainActivity).apply {
|
|
||||||
adapter = ArrayAdapter(
|
|
||||||
this@MainActivity,
|
|
||||||
android.R.layout.simple_spinner_dropdown_item,
|
|
||||||
LogConfig.LEVELS,
|
|
||||||
)
|
|
||||||
setSelection(LogConfig.LEVELS.indexOf(LogConfig.getLevel(this@MainActivity)).coerceAtLeast(0))
|
|
||||||
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
||||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
|
||||||
val level = LogConfig.LEVELS[position]
|
|
||||||
if (level != LogConfig.getLevel(this@MainActivity)) {
|
|
||||||
LogConfig.setLevel(this@MainActivity, level)
|
|
||||||
LogStore.append("Log level set to $level; restart service to apply")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addView(logLevelSpinner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun card(build: LinearLayout.() -> Unit): View {
|
|
||||||
return LinearLayout(this).apply {
|
|
||||||
orientation = LinearLayout.VERTICAL
|
|
||||||
setPadding(16.dp(), 16.dp(), 16.dp(), 16.dp())
|
|
||||||
background = rounded("#FFFFFF", "#E2E8F0", 10)
|
|
||||||
elevation = 1.5f.dpFloat()
|
|
||||||
build()
|
|
||||||
}.also {
|
|
||||||
it.layoutParams = LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
||||||
).apply { setMargins(0, 0, 0, 14.dp()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sectionTitle(text: String): View {
|
|
||||||
return TextView(this).apply {
|
|
||||||
this.text = text
|
|
||||||
textSize = 17f
|
|
||||||
typeface = Typeface.DEFAULT_BOLD
|
|
||||||
setTextColor(color("#0F172A"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun actionButton(text: String, primary: Boolean, action: () -> Unit): Button {
|
|
||||||
return Button(this).apply {
|
|
||||||
this.text = text
|
|
||||||
textSize = 15f
|
|
||||||
isAllCaps = false
|
|
||||||
minHeight = 44.dp()
|
|
||||||
setTextColor(color(if (primary) "#FFFFFF" else "#0F172A"))
|
|
||||||
background = if (primary) rounded("#2563EB", "#2563EB", 8) else rounded("#FFFFFF", "#CBD5E1", 8)
|
|
||||||
setOnClickListener { action() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toggleHost() {
|
|
||||||
when (ServiceStatusStore.snapshot(this).state) {
|
|
||||||
ServiceStatusStore.STATE_RUNNING -> pauseHost()
|
|
||||||
ServiceStatusStore.STATE_STOPPED, ServiceStatusStore.STATE_ERROR -> startHost()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startHost() {
|
|
||||||
ServiceStatusStore.setStarting(this)
|
|
||||||
refreshStatus()
|
|
||||||
OneKvmService.start(this)
|
|
||||||
LogStore.append("Start requested from app UI")
|
|
||||||
pollStatusForAWhile()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pauseHost() {
|
|
||||||
ServiceStatusStore.setStopping(this)
|
|
||||||
refreshStatus()
|
|
||||||
OneKvmService.stop(this)
|
|
||||||
LogStore.append("Pause requested from app UI")
|
|
||||||
pollStatusForAWhile()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshStatus() {
|
|
||||||
if (::statusValue.isInitialized) {
|
|
||||||
statusValue.text = "状态:${hostStatusSummary()}"
|
|
||||||
}
|
|
||||||
updateHostActionButton()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hostStatusSummary(): String {
|
|
||||||
val serviceStatus = ServiceStatusStore.snapshot(this)
|
|
||||||
if (serviceStatus.state != ServiceStatusStore.STATE_STOPPED) {
|
|
||||||
return serviceStatus.labelText()
|
|
||||||
}
|
|
||||||
|
|
||||||
val nativeRunning = runCatching {
|
|
||||||
NativeBridge.hostStatus().contains("running", ignoreCase = true)
|
|
||||||
}.getOrDefault(false)
|
|
||||||
|
|
||||||
return if (nativeRunning) "运行中" else "已停止"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun reconcilePersistedStatus() {
|
|
||||||
val serviceStatus = ServiceStatusStore.snapshot(this)
|
|
||||||
if (serviceStatus.state == ServiceStatusStore.STATE_STOPPED) return
|
|
||||||
if (
|
|
||||||
serviceStatus.state == ServiceStatusStore.STATE_STARTING &&
|
|
||||||
System.currentTimeMillis() - serviceStatus.updatedAt < STARTING_RECONCILE_GRACE_MS
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Thread {
|
|
||||||
val portOpen = isLocalWebPortOpen()
|
|
||||||
val nativeRunning = runCatching { NativeBridge.hostStatus().contains("running", ignoreCase = true) }
|
|
||||||
.getOrDefault(false)
|
|
||||||
if (!portOpen && !nativeRunning) {
|
|
||||||
ServiceStatusStore.setStopped(this, "服务未运行")
|
|
||||||
runOnUiThread { refreshStatus() }
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isLocalWebPortOpen(): Boolean {
|
|
||||||
return runCatching {
|
|
||||||
Socket().use { socket ->
|
|
||||||
socket.connect(InetSocketAddress("127.0.0.1", 8080), 250)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}.getOrDefault(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateHostActionButton() {
|
|
||||||
if (!::hostActionButton.isInitialized) return
|
|
||||||
|
|
||||||
when (ServiceStatusStore.snapshot(this).state) {
|
|
||||||
ServiceStatusStore.STATE_STARTING -> setHostActionButton("启动中...", enabled = false, primary = true)
|
|
||||||
ServiceStatusStore.STATE_RUNNING -> setHostActionButton("停止", enabled = true, primary = false)
|
|
||||||
ServiceStatusStore.STATE_STOPPING -> setHostActionButton("停止中...", enabled = false, primary = false)
|
|
||||||
else -> setHostActionButton("启动", enabled = true, primary = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setHostActionButton(text: String, enabled: Boolean, primary: Boolean) {
|
|
||||||
hostActionButton.text = text
|
|
||||||
hostActionButton.isEnabled = enabled
|
|
||||||
hostActionButton.alpha = if (enabled) 1f else 0.65f
|
|
||||||
hostActionButton.setTextColor(color(if (primary) "#FFFFFF" else "#0F172A"))
|
|
||||||
hostActionButton.background = if (primary) {
|
|
||||||
rounded("#2563EB", "#2563EB", 8)
|
|
||||||
} else {
|
|
||||||
rounded("#FFFFFF", "#CBD5E1", 8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pollStatusForAWhile() {
|
|
||||||
statusPollsRemaining = 20
|
|
||||||
statusHandler.removeCallbacks(statusPoller)
|
|
||||||
statusHandler.postDelayed(statusPoller, STATUS_POLL_INTERVAL_MS)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun kernelVersion(): String {
|
|
||||||
return runCatching { NativeBridge.kernelVersion() }
|
|
||||||
.getOrElse { "unknown" }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun accessAddresses(): String {
|
|
||||||
val addresses = runCatching {
|
|
||||||
Collections.list(NetworkInterface.getNetworkInterfaces())
|
|
||||||
.filter { it.isUp && !it.isLoopback }
|
|
||||||
.flatMap { iface -> Collections.list(iface.inetAddresses) }
|
|
||||||
.filterIsInstance<Inet4Address>()
|
|
||||||
.filter { !it.isLoopbackAddress }
|
|
||||||
.map { "http://${it.hostAddress}:8080" }
|
|
||||||
.distinct()
|
|
||||||
}.getOrDefault(emptyList())
|
|
||||||
|
|
||||||
return (addresses.ifEmpty { listOf("http://127.0.0.1:8080") }).joinToString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun divider(): View {
|
|
||||||
return View(this).apply {
|
|
||||||
setBackgroundColor(color("#E2E8F0"))
|
|
||||||
layoutParams = LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
1,
|
|
||||||
).apply { setMargins(0, 0, 0, 0) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun matchWrap(): LinearLayout.LayoutParams {
|
|
||||||
return LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun matchButton(): LinearLayout.LayoutParams {
|
|
||||||
return LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
48.dp(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun solid(hex: String): GradientDrawable = GradientDrawable().apply {
|
|
||||||
setColor(color(hex))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun rounded(fill: String, stroke: String, radiusDp: Int): GradientDrawable {
|
|
||||||
return GradientDrawable().apply {
|
|
||||||
setColor(color(fill))
|
|
||||||
cornerRadius = radiusDp.dpFloat()
|
|
||||||
setStroke(1.dp(), color(stroke))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun color(hex: String): Int = Color.parseColor(hex)
|
|
||||||
|
|
||||||
private fun Int.dp(): Int = (this * resources.displayMetrics.density + 0.5f).toInt()
|
|
||||||
|
|
||||||
private fun Int.dpFloat(): Float = this * resources.displayMetrics.density
|
|
||||||
|
|
||||||
private fun Float.dpFloat(): Float = this * resources.displayMetrics.density
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val STATUS_POLL_INTERVAL_MS = 500L
|
|
||||||
private const val STARTING_RECONCILE_GRACE_MS = 15_000L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package cn.one_kvm.androidhost
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
|
|
||||||
object NativeBridge {
|
|
||||||
init {
|
|
||||||
System.loadLibrary("one_kvm_android_bootstrap")
|
|
||||||
}
|
|
||||||
|
|
||||||
external fun initTlsVerifier(context: Context): Int
|
|
||||||
|
|
||||||
external fun setEnv(name: String, value: String): Int
|
|
||||||
|
|
||||||
external fun startHost(dataDir: String, bindAddress: String, port: Int): String
|
|
||||||
|
|
||||||
external fun stopHost(): String
|
|
||||||
|
|
||||||
external fun hostStatus(): String
|
|
||||||
|
|
||||||
external fun kernelVersion(): String
|
|
||||||
}
|
|
||||||
@@ -1,413 +0,0 @@
|
|||||||
package cn.one_kvm.androidhost
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.io.InterruptedIOException
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
class OneKvmService : Service() {
|
|
||||||
private var rootProcess: Process? = null
|
|
||||||
private val commandExecutor = Executors.newSingleThreadExecutor { runnable ->
|
|
||||||
Thread(runnable, "OneKvmServiceCommand")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
ensureNotificationChannel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
when (intent?.action ?: ACTION_START) {
|
|
||||||
ACTION_STOP -> {
|
|
||||||
ServiceStatusStore.setStopping(this)
|
|
||||||
commandExecutor.execute {
|
|
||||||
stopHostRuntime()
|
|
||||||
stopSelfResult(startId)
|
|
||||||
}
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
ACTION_START -> {
|
|
||||||
ServiceStatusStore.setStarting(this)
|
|
||||||
startForegroundCompat(NOTIFICATION_ID, notification("启动中"))
|
|
||||||
commandExecutor.execute {
|
|
||||||
val currentState = ServiceStatusStore.snapshot(this).state
|
|
||||||
if (currentState == ServiceStatusStore.STATE_RUNNING && isPortOpen(8080, 100)) {
|
|
||||||
return@execute
|
|
||||||
}
|
|
||||||
val dataDir = File(getExternalFilesDir(null), "runtime")
|
|
||||||
if (!dataDir.exists()) dataDir.mkdirs()
|
|
||||||
val result = startRustHost(dataDir)
|
|
||||||
if (result.startsWith("Running") && !result.contains("start failed", ignoreCase = true)) {
|
|
||||||
ServiceStatusStore.setRunning(this, "服务已启动")
|
|
||||||
notificationManager().notify(NOTIFICATION_ID, notification("运行中"))
|
|
||||||
} else {
|
|
||||||
ServiceStatusStore.setError(this, "启动失败")
|
|
||||||
notificationManager().notify(NOTIFICATION_ID, notification("启动失败"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
stopHostRuntime(updateNotification = false)
|
|
||||||
commandExecutor.shutdownNow()
|
|
||||||
ServiceStatusStore.setStopped(this)
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
|
||||||
|
|
||||||
private fun notification(state: String): Notification {
|
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
|
||||||
val pendingIntent = createContentIntent(intent)
|
|
||||||
val builder = createNotificationBuilder()
|
|
||||||
return builder
|
|
||||||
.setSmallIcon(R.drawable.ic_stat_one_kvm)
|
|
||||||
.setContentTitle("One-KVM Android Host")
|
|
||||||
.setContentText(state)
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.setOngoing(true)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ensureNotificationChannel() {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
|
||||||
val channel = NotificationChannel(
|
|
||||||
CHANNEL_ID,
|
|
||||||
"One-KVM Host",
|
|
||||||
NotificationManager.IMPORTANCE_LOW,
|
|
||||||
)
|
|
||||||
notificationManager().createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun createNotificationBuilder(): Notification.Builder {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
Notification.Builder(this, CHANNEL_ID)
|
|
||||||
} else {
|
|
||||||
Notification.Builder(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createContentIntent(intent: Intent): PendingIntent {
|
|
||||||
val flags = pendingIntentFlags()
|
|
||||||
return PendingIntent.getActivity(this, 0, intent, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pendingIntentFlags(): Int {
|
|
||||||
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
flags = flags or pendingIntentImmutableFlag()
|
|
||||||
}
|
|
||||||
return flags
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pendingIntentImmutableFlag(): Int {
|
|
||||||
return try {
|
|
||||||
PendingIntent::class.java.getField("FLAG_IMMUTABLE").getInt(null)
|
|
||||||
} catch (_: ReflectiveOperationException) {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun notificationManager(): NotificationManager {
|
|
||||||
return getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopHostRuntime(updateNotification: Boolean = true) {
|
|
||||||
stopRootHost()
|
|
||||||
NativeBridge.stopHost()
|
|
||||||
waitForPortRelease(8080, 2_000)
|
|
||||||
LogStore.flush()
|
|
||||||
ServiceStatusStore.setStopped(this)
|
|
||||||
if (updateNotification) {
|
|
||||||
notificationManager().notify(NOTIFICATION_ID, notification("已停止"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startRustHost(dataDir: File): String {
|
|
||||||
val logLevel = LogConfig.getLevel(this)
|
|
||||||
val rustLog = LogConfig.rustLogFilter(logLevel)
|
|
||||||
val appLogFile = LogStore.defaultLogFile(this)
|
|
||||||
LogStore.configure(appLogFile)
|
|
||||||
val rustLogFile = appLogFile
|
|
||||||
LogStore.append("Starting One-KVM Rust host, data_dir=${dataDir.absolutePath}, log_level=$logLevel")
|
|
||||||
val executable = extractHostBinary()
|
|
||||||
return runCatching {
|
|
||||||
val tlsInit = NativeBridge.initTlsVerifier(this)
|
|
||||||
if (tlsInit != 0) {
|
|
||||||
throw IllegalStateException("rustls platform verifier init failed with code $tlsInit")
|
|
||||||
}
|
|
||||||
stopRootHost(executable)
|
|
||||||
clearExistingOtgGadgetsIfEnabled()
|
|
||||||
startRootHost(executable, dataDir, rustLog, rustLogFile, logLevel)
|
|
||||||
LogStore.append("Rust host running as root on port 8080")
|
|
||||||
"Running as root on port 8080"
|
|
||||||
}.getOrElse { rootError ->
|
|
||||||
LogStore.append("Root host unavailable: ${rootError.message ?: rootError::class.java.simpleName}")
|
|
||||||
configureAlsaEnvironment(executable)
|
|
||||||
NativeBridge.setEnv("RUST_LOG", rustLog)
|
|
||||||
NativeBridge.setEnv("ONE_KVM_FFMPEG_LOG", ffmpegLogLevel(logLevel))
|
|
||||||
NativeBridge.setEnv("ONE_KVM_ANDROID_LOG_FILE", rustLogFile.absolutePath)
|
|
||||||
val jniResult = NativeBridge.startHost(dataDir.absolutePath, "0.0.0.0", 8080)
|
|
||||||
LogStore.append("Rust host running in app process on port 8080: $jniResult")
|
|
||||||
"Running in app process on port 8080 (${rootError.message ?: "root unavailable"}; $jniResult)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearExistingOtgGadgetsIfEnabled() {
|
|
||||||
if (!HostSettings.getClearExistingOtg(this)) return
|
|
||||||
|
|
||||||
val command = """
|
|
||||||
root=/sys/kernel/config/usb_gadget
|
|
||||||
[ -d "${'$'}root" ] || exit 0
|
|
||||||
for gadget in "${'$'}root"/*; do
|
|
||||||
[ -d "${'$'}gadget" ] || continue
|
|
||||||
[ -w "${'$'}gadget/UDC" ] && echo "" > "${'$'}gadget/UDC" 2>/dev/null || true
|
|
||||||
find "${'$'}gadget/configs" -type l -delete 2>/dev/null || true
|
|
||||||
rm -rf "${'$'}gadget" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
runCatching {
|
|
||||||
ProcessBuilder("/system/xbin/su", "0", "sh", "-c", command)
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start()
|
|
||||||
.waitFor()
|
|
||||||
}.onSuccess { exit ->
|
|
||||||
LogStore.append("Existing OTG gadget cleanup finished with exit code $exit")
|
|
||||||
}.onFailure { err ->
|
|
||||||
LogStore.append("Existing OTG gadget cleanup failed: ${err.message ?: err::class.java.simpleName}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun configureAlsaEnvironment(executable: File) {
|
|
||||||
val binDir = executable.parentFile
|
|
||||||
?: throw IllegalStateException("host binary has no parent directory")
|
|
||||||
val alsaConfigDir = File(binDir, "alsa")
|
|
||||||
val alsaConfigPath = File(alsaConfigDir, "alsa.conf")
|
|
||||||
NativeBridge.setEnv("ALSA_CONFIG_DIR", alsaConfigDir.absolutePath)
|
|
||||||
NativeBridge.setEnv("ALSA_CONFIG_PATH", alsaConfigPath.absolutePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractHostBinary(): File {
|
|
||||||
val abi = Build.SUPPORTED_ABIS.firstOrNull { it == "arm64-v8a" || it == "armeabi-v7a" }
|
|
||||||
?: throw IllegalStateException("unsupported ABI: ${Build.SUPPORTED_ABIS.joinToString()}")
|
|
||||||
val binDir = File(filesDir, "bin/$abi")
|
|
||||||
val target = File(binDir, "one-kvm-android-host")
|
|
||||||
copyAssetIfChanged("bin/$abi/one-kvm-android-host", target)
|
|
||||||
copyAssetIfChanged("bin/$abi/libc++_shared.so", File(binDir, "libc++_shared.so"))
|
|
||||||
copyAssetIfChanged("bin/$abi/libasound.so", File(binDir, "libasound.so"))
|
|
||||||
copyAssetIfChanged("bin/$abi/libopus.so", File(binDir, "libopus.so"))
|
|
||||||
copyAssetDirectoryIfChanged("bin/$abi/alsa", File(binDir, "alsa"))
|
|
||||||
if (!target.setExecutable(true, false)) {
|
|
||||||
throw IllegalStateException("cannot mark host binary executable")
|
|
||||||
}
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyAssetIfChanged(assetPath: String, target: File) {
|
|
||||||
val stamp = File(target.parentFile, "${target.name}.stamp")
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val packageInfo = packageManager.getPackageInfo(packageName, 0)
|
|
||||||
val expectedStamp = "${packageInfo.lastUpdateTime}:$assetPath"
|
|
||||||
if (target.exists() && stamp.exists() && stamp.readText() == expectedStamp) return
|
|
||||||
target.parentFile?.mkdirs()
|
|
||||||
assets.open(assetPath).use { input ->
|
|
||||||
target.outputStream().use { output -> input.copyTo(output) }
|
|
||||||
}
|
|
||||||
stamp.writeText(expectedStamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyAssetDirectoryIfChanged(assetDir: String, targetDir: File) {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val packageInfo = packageManager.getPackageInfo(packageName, 0)
|
|
||||||
val stamp = File(targetDir, ".stamp")
|
|
||||||
val expectedStamp = "${packageInfo.lastUpdateTime}:$assetDir"
|
|
||||||
if (targetDir.exists() && stamp.exists() && stamp.readText() == expectedStamp) return
|
|
||||||
if (targetDir.exists()) targetDir.deleteRecursively()
|
|
||||||
copyAssetDirectory(assetDir, targetDir)
|
|
||||||
stamp.writeText(expectedStamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyAssetDirectory(assetDir: String, targetDir: File) {
|
|
||||||
targetDir.mkdirs()
|
|
||||||
val children = assets.list(assetDir)?.filter { it.isNotEmpty() }.orEmpty()
|
|
||||||
for (child in children) {
|
|
||||||
val childAsset = "$assetDir/$child"
|
|
||||||
val childTarget = File(targetDir, child)
|
|
||||||
val grandChildren = assets.list(childAsset)?.filter { it.isNotEmpty() }.orEmpty()
|
|
||||||
if (grandChildren.isEmpty()) {
|
|
||||||
copyAssetIfChanged(childAsset, childTarget)
|
|
||||||
} else {
|
|
||||||
copyAssetDirectory(childAsset, childTarget)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startRootHost(
|
|
||||||
executable: File,
|
|
||||||
dataDir: File,
|
|
||||||
rustLog: String,
|
|
||||||
rustLogFile: File,
|
|
||||||
logLevel: String,
|
|
||||||
) {
|
|
||||||
stopRootHost(executable)
|
|
||||||
waitForPortRelease(8080, 2_000)
|
|
||||||
val libDir = executable.parentFile?.absolutePath
|
|
||||||
?: throw IllegalStateException("host binary has no parent directory")
|
|
||||||
val alsaConfigDir = File(executable.parentFile, "alsa")
|
|
||||||
val alsaConfigPath = File(alsaConfigDir, "alsa.conf")
|
|
||||||
val command =
|
|
||||||
"export LD_LIBRARY_PATH=${shellQuote(libDir)}:\${LD_LIBRARY_PATH:-}; " +
|
|
||||||
"export ALSA_CONFIG_DIR=${shellQuote(alsaConfigDir.absolutePath)}; " +
|
|
||||||
"export ALSA_CONFIG_PATH=${shellQuote(alsaConfigPath.absolutePath)}; " +
|
|
||||||
"export RUST_LOG=${shellQuote(rustLog)}; " +
|
|
||||||
"export ONE_KVM_FFMPEG_LOG=${shellQuote(ffmpegLogLevel(logLevel))}; " +
|
|
||||||
"export ONE_KVM_ANDROID_LOG_FILE=${shellQuote(rustLogFile.absolutePath)}; " +
|
|
||||||
"${shellQuote(executable.absolutePath)} ${shellQuote(dataDir.absolutePath)} 0.0.0.0 8080"
|
|
||||||
val process = ProcessBuilder("/system/xbin/su", "0", "sh", "-c", command)
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start()
|
|
||||||
rootProcess = process
|
|
||||||
|
|
||||||
Thread {
|
|
||||||
val readError = runCatching {
|
|
||||||
BufferedReader(InputStreamReader(process.inputStream)).useLines { lines ->
|
|
||||||
lines.forEach {
|
|
||||||
android.util.Log.i("OneKvmService", it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.exceptionOrNull()
|
|
||||||
if (readError != null && readError !is InterruptedIOException) {
|
|
||||||
android.util.Log.w("OneKvmService", "Root host log reader stopped", readError)
|
|
||||||
LogStore.append("Root host log reader stopped: ${readError.message ?: readError::class.java.simpleName}")
|
|
||||||
}
|
|
||||||
val exit = runCatching { process.waitFor() }.getOrNull()
|
|
||||||
if (rootProcess === process && exit != null) {
|
|
||||||
rootProcess = null
|
|
||||||
ServiceStatusStore.setError(this, "Root host exited with code $exit")
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
|
|
||||||
Thread.sleep(500)
|
|
||||||
val exit = runCatching { process.exitValue() }.getOrNull()
|
|
||||||
if (exit != null) {
|
|
||||||
rootProcess = null
|
|
||||||
throw IllegalStateException("root host exited immediately: $exit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startForegroundCompat(id: Int, notification: Notification) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
val invoked = runCatching {
|
|
||||||
val method = Service::class.java.getMethod(
|
|
||||||
"startForeground",
|
|
||||||
Int::class.javaPrimitiveType,
|
|
||||||
Notification::class.java,
|
|
||||||
Int::class.javaPrimitiveType,
|
|
||||||
)
|
|
||||||
method.invoke(this, id, notification, foregroundServiceTypeConnectedDevice())
|
|
||||||
}.isSuccess
|
|
||||||
if (invoked) return
|
|
||||||
}
|
|
||||||
super.startForeground(id, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun foregroundServiceTypeConnectedDevice(): Int {
|
|
||||||
return try {
|
|
||||||
Service::class.java.getField("FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE").getInt(null)
|
|
||||||
} catch (_: ReflectiveOperationException) {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopRootHost(executable: File? = null) {
|
|
||||||
rootProcess?.destroy()
|
|
||||||
rootProcess = null
|
|
||||||
stopRootHostProcess(executable)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopRootHostProcess(executable: File? = null) {
|
|
||||||
val command = buildString {
|
|
||||||
append("pkill -TERM -f '[o]ne-kvm-android-host' 2>/dev/null || true; ")
|
|
||||||
append("for pid in $(pidof one-kvm-android-host 2>/dev/null); do kill -TERM \"${'$'}pid\" 2>/dev/null || true; done; ")
|
|
||||||
append("sleep 0.2; ")
|
|
||||||
append("pkill -KILL -f '[o]ne-kvm-android-host' 2>/dev/null || true; ")
|
|
||||||
append("for pid in $(pidof one-kvm-android-host 2>/dev/null); do kill -KILL \"${'$'}pid\" 2>/dev/null || true; done; ")
|
|
||||||
}
|
|
||||||
|
|
||||||
runCatching {
|
|
||||||
ProcessBuilder("/system/xbin/su", "0", "sh", "-c", command)
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start()
|
|
||||||
.waitFor()
|
|
||||||
}.onFailure { err ->
|
|
||||||
LogStore.append("Failed to stop stale root host: ${err.message ?: err::class.java.simpleName}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun waitForPortRelease(port: Int, timeoutMs: Long) {
|
|
||||||
val deadline = System.currentTimeMillis() + timeoutMs
|
|
||||||
while (System.currentTimeMillis() < deadline) {
|
|
||||||
val inUse = isPortOpen(port, 100)
|
|
||||||
if (!inUse) return
|
|
||||||
Thread.sleep(100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isPortOpen(port: Int, timeoutMs: Int): Boolean {
|
|
||||||
return runCatching {
|
|
||||||
java.net.Socket().use { socket ->
|
|
||||||
socket.connect(java.net.InetSocketAddress("127.0.0.1", port), timeoutMs)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}.getOrDefault(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shellQuote(value: String): String {
|
|
||||||
return "'" + value.replace("'", "'\\''") + "'"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ffmpegLogLevel(level: String): String {
|
|
||||||
return when (level) {
|
|
||||||
"trace" -> "trace"
|
|
||||||
"debug" -> "debug"
|
|
||||||
"info" -> "info"
|
|
||||||
"warn" -> "warning"
|
|
||||||
else -> "error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val CHANNEL_ID = "one_kvm_host"
|
|
||||||
private const val NOTIFICATION_ID = 1001
|
|
||||||
const val ACTION_START = "cn.one_kvm.androidhost.START"
|
|
||||||
const val ACTION_STOP = "cn.one_kvm.androidhost.STOP"
|
|
||||||
|
|
||||||
fun start(context: Context) {
|
|
||||||
val intent = Intent(context, OneKvmService::class.java).setAction(ACTION_START)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
context.startForegroundService(intent)
|
|
||||||
} else {
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop(context: Context) {
|
|
||||||
context.startService(Intent(context, OneKvmService::class.java).setAction(ACTION_STOP))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package cn.one_kvm.androidhost
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
|
|
||||||
object ServiceStatusStore {
|
|
||||||
private const val PREFS = "one_kvm_android_status"
|
|
||||||
private const val KEY_STATE = "state"
|
|
||||||
private const val KEY_MESSAGE = "message"
|
|
||||||
private const val KEY_UPDATED_AT = "updated_at"
|
|
||||||
|
|
||||||
const val STATE_STOPPED = "stopped"
|
|
||||||
const val STATE_STARTING = "starting"
|
|
||||||
const val STATE_RUNNING = "running"
|
|
||||||
const val STATE_STOPPING = "stopping"
|
|
||||||
const val STATE_ERROR = "error"
|
|
||||||
|
|
||||||
data class Snapshot(
|
|
||||||
val state: String,
|
|
||||||
val message: String,
|
|
||||||
val updatedAt: Long,
|
|
||||||
) {
|
|
||||||
fun labelText(): String {
|
|
||||||
return when (state) {
|
|
||||||
STATE_STARTING -> "启动中"
|
|
||||||
STATE_RUNNING -> "运行中"
|
|
||||||
STATE_STOPPING -> "停止中"
|
|
||||||
STATE_ERROR -> "错误"
|
|
||||||
else -> "已停止"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun displayText(): String {
|
|
||||||
val label = labelText()
|
|
||||||
return if (message.isBlank()) label else "$label:$message"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setStarting(context: Context, message: String = "正在启动服务") {
|
|
||||||
write(context, STATE_STARTING, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setRunning(context: Context, message: String) {
|
|
||||||
write(context, STATE_RUNNING, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setStopping(context: Context, message: String = "正在停止服务") {
|
|
||||||
write(context, STATE_STOPPING, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setStopped(context: Context, message: String = "服务已停止") {
|
|
||||||
write(context, STATE_STOPPED, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setError(context: Context, message: String) {
|
|
||||||
write(context, STATE_ERROR, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun snapshot(context: Context): Snapshot {
|
|
||||||
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
return Snapshot(
|
|
||||||
state = prefs.getString(KEY_STATE, STATE_STOPPED) ?: STATE_STOPPED,
|
|
||||||
message = prefs.getString(KEY_MESSAGE, "") ?: "",
|
|
||||||
updatedAt = prefs.getLong(KEY_UPDATED_AT, 0L),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun write(context: Context, state: String, message: String) {
|
|
||||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
||||||
.edit()
|
|
||||||
.putString(KEY_STATE, state)
|
|
||||||
.putString(KEY_MESSAGE, message)
|
|
||||||
.putLong(KEY_UPDATED_AT, System.currentTimeMillis())
|
|
||||||
.apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#1D7BF2"
|
|
||||||
android:pathData="M24,0h60a24,24 0,0 1,24 24v60a24,24 0,0 1,-24 24H24a24,24 0,0 1,-24 -24V24a24,24 0,0 1,24 -24z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#AED8E8"
|
|
||||||
android:pathData="M29,25h50a3,3 0,0 1,3 3v31a3,3 0,0 1,-3 3H29a3,3 0,0 1,-3 -3V28a3,3 0,0 1,3 -3z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#3F3F3D"
|
|
||||||
android:pathData="M31,30h46v27H31z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#E7F1F4"
|
|
||||||
android:pathData="M31,26h10a1.4,1.4 0,0 1,0 2.8H31a1.4,1.4 0,0 1,0 -2.8z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#8BBFD1"
|
|
||||||
android:pathData="M49,62h10l1.5,8h-13z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#9FCFE0"
|
|
||||||
android:pathData="M40,70a14,4.5 0,1 0,28 0a14,4.5 0,1 0,-28 0z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#E8F5F8"
|
|
||||||
android:pathData="M45,70a7,1.8 0,1 0,14 0a7,1.8 0,1 0,-14 0z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#BFE6F1"
|
|
||||||
android:pathData="M32,76h38l5,8H27z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#76ADC2"
|
|
||||||
android:pathData="M28,84h47v2H28z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillAlpha="0.82"
|
|
||||||
android:pathData="M37,79h6v2h-6zM46,79h5v2h-5zM54,79h5v2h-5zM62,79h6v2h-6zM34,82h7v2h-7zM44,82h6v2h-6zM53,82h11v2H53zM67,82h4v2h-4z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFFFF"
|
|
||||||
android:pathData="M4,5h16v10H4z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFFFF"
|
|
||||||
android:pathData="M9,17h6v2h3v2H6v-2h3z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="app_name">One-KVM Android Host</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<style name="AppTheme" parent="android:style/Theme.Material.Light.NoActionBar">
|
|
||||||
<item name="android:fontFamily">sans</item>
|
|
||||||
<item name="android:colorAccent">#2563EB</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("com.android.application") version "9.0.0" apply false
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
android.useAndroidX=true
|
|
||||||
android.nonTransitiveRClass=true
|
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
||||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
@@ -1,7 +0,0 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
251
android/gradlew
vendored
251
android/gradlew
vendored
@@ -1,251 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
#
|
|
||||||
# Copyright © 2015 the original authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
|
||||||
#
|
|
||||||
# Important for running:
|
|
||||||
#
|
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or
|
|
||||||
# bash, then to run this script, type that shell name before the whole
|
|
||||||
# command line, like:
|
|
||||||
#
|
|
||||||
# ksh Gradle
|
|
||||||
#
|
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
|
||||||
# requires all of these POSIX shell features:
|
|
||||||
# * functions;
|
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
|
||||||
# * compound commands having a testable exit status, especially «case»;
|
|
||||||
# * various built-in commands including «command», «set», and «ulimit».
|
|
||||||
#
|
|
||||||
# Important for patching:
|
|
||||||
#
|
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
|
||||||
#
|
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
|
||||||
# space-separated string is a well documented source of bugs and security
|
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
|
||||||
# options in "$@", and eventually passing that to Java.
|
|
||||||
#
|
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
|
||||||
# see the in-line comments for details.
|
|
||||||
#
|
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
|
||||||
# Darwin, MinGW, and NonStop.
|
|
||||||
#
|
|
||||||
# (3) This script is generated from the Groovy template
|
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
|
||||||
# within the Gradle project.
|
|
||||||
#
|
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
app_path=$0
|
|
||||||
|
|
||||||
# Need this for daisy-chained symlinks.
|
|
||||||
while
|
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
|
||||||
[ -h "$app_path" ]
|
|
||||||
do
|
|
||||||
ls=$( ls -ld "$app_path" )
|
|
||||||
link=${ls#*' -> '}
|
|
||||||
case $link in #(
|
|
||||||
/*) app_path=$link ;; #(
|
|
||||||
*) app_path=$APP_HOME$link ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# This is normally unused
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
APP_BASE_NAME=${0##*/}
|
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD=maximum
|
|
||||||
|
|
||||||
warn () {
|
|
||||||
echo "$*"
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
die () {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
nonstop=false
|
|
||||||
case "$( uname )" in #(
|
|
||||||
CYGWIN* ) cygwin=true ;; #(
|
|
||||||
Darwin* ) darwin=true ;; #(
|
|
||||||
MSYS* | MINGW* ) msys=true ;; #(
|
|
||||||
NONSTOP* ) nonstop=true ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CLASSPATH="\\\"\\\""
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
|
||||||
else
|
|
||||||
JAVACMD=$JAVA_HOME/bin/java
|
|
||||||
fi
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD=java
|
|
||||||
if ! command -v java >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|
||||||
case $MAX_FD in #(
|
|
||||||
max*)
|
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
|
||||||
warn "Could not query maximum file descriptor limit"
|
|
||||||
esac
|
|
||||||
case $MAX_FD in #(
|
|
||||||
'' | soft) :;; #(
|
|
||||||
*)
|
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
ulimit -n "$MAX_FD" ||
|
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order:
|
|
||||||
# * args from the command line
|
|
||||||
# * the main class name
|
|
||||||
# * -classpath
|
|
||||||
# * -D...appname settings
|
|
||||||
# * --module-path (only if needed)
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if "$cygwin" || "$msys" ; then
|
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
for arg do
|
|
||||||
if
|
|
||||||
case $arg in #(
|
|
||||||
-*) false ;; # don't mess with options #(
|
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
|
||||||
[ -e "$t" ] ;; #(
|
|
||||||
*) false ;;
|
|
||||||
esac
|
|
||||||
then
|
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
|
||||||
fi
|
|
||||||
# Roll the args list around exactly as many times as the number of
|
|
||||||
# args, so each arg winds up back in the position where it started, but
|
|
||||||
# possibly modified.
|
|
||||||
#
|
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so
|
|
||||||
# changing the positional parameters here affects neither the number of
|
|
||||||
# iterations, nor the values presented in `arg`.
|
|
||||||
shift # remove old arg
|
|
||||||
set -- "$@" "$arg" # push replacement arg
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
|
||||||
# and any embedded shellness will be escaped.
|
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
|
||||||
# treated as '${Hostname}' itself on the command line.
|
|
||||||
|
|
||||||
set -- \
|
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
|
||||||
-classpath "$CLASSPATH" \
|
|
||||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
|
||||||
if ! command -v xargs >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "xargs is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
|
||||||
#
|
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
|
||||||
#
|
|
||||||
# In Bash we could simply go:
|
|
||||||
#
|
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
|
||||||
# set -- "${ARGS[@]}" "$@"
|
|
||||||
#
|
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
|
||||||
# character that might be a shell metacharacter, then use eval to reverse
|
|
||||||
# that process (while maintaining the separation between arguments), and wrap
|
|
||||||
# the whole thing up as a single "set" statement.
|
|
||||||
#
|
|
||||||
# This will of course break if any of these variables contains a newline or
|
|
||||||
# an unmatched quote.
|
|
||||||
#
|
|
||||||
|
|
||||||
eval "set -- $(
|
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
|
||||||
xargs -n1 |
|
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
|
||||||
tr '\n' ' '
|
|
||||||
)" '"$@"'
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
|
||||||
94
android/gradlew.bat
vendored
94
android/gradlew.bat
vendored
@@ -1,94 +0,0 @@
|
|||||||
@rem
|
|
||||||
@rem Copyright 2015 the original author or authors.
|
|
||||||
@rem
|
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
@rem you may not use this file except in compliance with the License.
|
|
||||||
@rem You may obtain a copy of the License at
|
|
||||||
@rem
|
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@rem
|
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
@rem See the License for the specific language governing permissions and
|
|
||||||
@rem limitations under the License.
|
|
||||||
@rem
|
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
|
||||||
@rem ##########################################################################
|
|
||||||
@rem
|
|
||||||
@rem Gradle startup script for Windows
|
|
||||||
@rem
|
|
||||||
@rem ##########################################################################
|
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
|
||||||
@rem This is normally unused
|
|
||||||
set APP_BASE_NAME=%~n0
|
|
||||||
set APP_HOME=%DIRNAME%
|
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
|
||||||
|
|
||||||
@rem Find java.exe
|
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
|
||||||
|
|
||||||
echo. 1>&2
|
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
|
||||||
echo. 1>&2
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
|
||||||
echo location of your Java installation. 1>&2
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
|
||||||
|
|
||||||
echo. 1>&2
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
|
||||||
echo. 1>&2
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
|
||||||
echo location of your Java installation. 1>&2
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:execute
|
|
||||||
@rem Setup the command line
|
|
||||||
|
|
||||||
set CLASSPATH=
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
|
||||||
|
|
||||||
:end
|
|
||||||
@rem End local scope for the variables with windows NT shell
|
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "one-kvm-android-bootstrap"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "one_kvm_android_bootstrap"
|
|
||||||
crate-type = ["cdylib"]
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "one-kvm-android-host"
|
|
||||||
path = "src/bin/one-kvm-android-host.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
jni = "0.22.4"
|
|
||||||
one-kvm = { path = "../..", default-features = false, features = ["android", "android-mediacodec"] }
|
|
||||||
rustls-platform-verifier = "0.7"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
android-mediacodec = ["one-kvm/android-mediacodec"]
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
use one_kvm::runtime::android::{self, AndroidRuntimeConfig};
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let mut args = std::env::args().skip(1);
|
|
||||||
let data_dir = args
|
|
||||||
.next()
|
|
||||||
.unwrap_or_else(|| "/data/local/tmp/one-kvm".to_string());
|
|
||||||
let bind_address = args.next().unwrap_or_else(|| "0.0.0.0".to_string());
|
|
||||||
let port = args
|
|
||||||
.next()
|
|
||||||
.and_then(|value| value.parse::<u16>().ok())
|
|
||||||
.unwrap_or(8080);
|
|
||||||
|
|
||||||
one_kvm::runtime::android::init_rustls_provider();
|
|
||||||
|
|
||||||
if let Err(err) = android::run_foreground(AndroidRuntimeConfig {
|
|
||||||
data_dir,
|
|
||||||
bind_address,
|
|
||||||
port,
|
|
||||||
}) {
|
|
||||||
eprintln!("one-kvm android host failed: {err}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
use jni::errors::{ErrorPolicy, ThrowRuntimeExAndDefault};
|
|
||||||
use jni::objects::{JClass, JObject, JString};
|
|
||||||
use jni::sys::{jint, jstring};
|
|
||||||
use jni::{Env, EnvOutcome, EnvUnowned};
|
|
||||||
use one_kvm::runtime::android::{self, AndroidRuntimeConfig};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct BridgeError(String);
|
|
||||||
|
|
||||||
impl From<jni::errors::Error> for BridgeError {
|
|
||||||
fn from(err: jni::errors::Error) -> Self {
|
|
||||||
Self(err.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for BridgeError {
|
|
||||||
fn from(err: String) -> Self {
|
|
||||||
Self(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for BridgeError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(&self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct StatusPolicy;
|
|
||||||
|
|
||||||
impl ErrorPolicy<jint, BridgeError> for StatusPolicy {
|
|
||||||
type Captures<'unowned_env_local: 'native_method, 'native_method> = ();
|
|
||||||
|
|
||||||
fn on_error<'unowned_env_local: 'native_method, 'native_method>(
|
|
||||||
_env: &mut Env<'unowned_env_local>,
|
|
||||||
_cap: &mut Self::Captures<'unowned_env_local, 'native_method>,
|
|
||||||
_err: BridgeError,
|
|
||||||
) -> jni::errors::Result<jint> {
|
|
||||||
Ok(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_panic<'unowned_env_local: 'native_method, 'native_method>(
|
|
||||||
_env: &mut Env<'unowned_env_local>,
|
|
||||||
_cap: &mut Self::Captures<'unowned_env_local, 'native_method>,
|
|
||||||
_payload: Box<dyn std::any::Any + Send + 'static>,
|
|
||||||
) -> jni::errors::Result<jint> {
|
|
||||||
Ok(-1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct StringResultPolicy;
|
|
||||||
|
|
||||||
impl ErrorPolicy<String, BridgeError> for StringResultPolicy {
|
|
||||||
type Captures<'unowned_env_local: 'native_method, 'native_method> = ();
|
|
||||||
|
|
||||||
fn on_error<'unowned_env_local: 'native_method, 'native_method>(
|
|
||||||
_env: &mut Env<'unowned_env_local>,
|
|
||||||
_cap: &mut Self::Captures<'unowned_env_local, 'native_method>,
|
|
||||||
err: BridgeError,
|
|
||||||
) -> jni::errors::Result<String> {
|
|
||||||
Ok(format!("start failed: {err}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_panic<'unowned_env_local: 'native_method, 'native_method>(
|
|
||||||
_env: &mut Env<'unowned_env_local>,
|
|
||||||
_cap: &mut Self::Captures<'unowned_env_local, 'native_method>,
|
|
||||||
_payload: Box<dyn std::any::Any + Send + 'static>,
|
|
||||||
) -> jni::errors::Result<String> {
|
|
||||||
Ok("start failed: panic in native bridge".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_setEnv<'local>(
|
|
||||||
mut env: EnvUnowned<'local>,
|
|
||||||
_class: JClass<'local>,
|
|
||||||
name: JString<'local>,
|
|
||||||
value: JString<'local>,
|
|
||||||
) -> jint {
|
|
||||||
let outcome: EnvOutcome<'local, jint, BridgeError> = env.with_env_no_catch(|env| {
|
|
||||||
let name = name
|
|
||||||
.try_to_string(env)
|
|
||||||
.map_err(|err| BridgeError(format!("invalid env name: {err}")))?;
|
|
||||||
let value = value
|
|
||||||
.try_to_string(env)
|
|
||||||
.map_err(|err| BridgeError(format!("invalid env value: {err}")))?;
|
|
||||||
if name.contains('\0') || value.contains('\0') {
|
|
||||||
return Err(BridgeError("env contains NUL".to_string()));
|
|
||||||
}
|
|
||||||
std::env::set_var(name, value);
|
|
||||||
Ok(0)
|
|
||||||
});
|
|
||||||
|
|
||||||
outcome.resolve_with::<StatusPolicy, _>(|| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_initTlsVerifier<'local>(
|
|
||||||
mut env: EnvUnowned<'local>,
|
|
||||||
_class: JClass<'local>,
|
|
||||||
context: JObject<'local>,
|
|
||||||
) -> jint {
|
|
||||||
let outcome: EnvOutcome<'local, jint, BridgeError> =
|
|
||||||
env.with_env_no_catch(|env| init_tls_verifier(env, context));
|
|
||||||
|
|
||||||
outcome.resolve_with::<StatusPolicy, _>(|| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
fn init_tls_verifier(env: &mut Env<'_>, context: JObject<'_>) -> Result<jint, BridgeError> {
|
|
||||||
rustls_platform_verifier::android::init_with_env(env, context)
|
|
||||||
.map_err(|err| BridgeError(format!("failed to initialize rustls platform verifier: {err}")))?;
|
|
||||||
Ok(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "android"))]
|
|
||||||
fn init_tls_verifier(_env: &mut Env<'_>, _context: JObject<'_>) -> Result<jint, BridgeError> {
|
|
||||||
Ok(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_startHost<'local>(
|
|
||||||
mut env: EnvUnowned<'local>,
|
|
||||||
_class: JClass<'local>,
|
|
||||||
data_dir: JString<'local>,
|
|
||||||
bind_address: JString<'local>,
|
|
||||||
port: i32,
|
|
||||||
) -> jstring {
|
|
||||||
let outcome: EnvOutcome<'local, String, BridgeError> = env.with_env_no_catch(|env| {
|
|
||||||
let data_dir = data_dir
|
|
||||||
.try_to_string(env)
|
|
||||||
.map_err(|err| BridgeError(format!("invalid data dir: {err}")))?;
|
|
||||||
let bind_address = bind_address
|
|
||||||
.try_to_string(env)
|
|
||||||
.map_err(|err| BridgeError(format!("invalid bind address: {err}")))?;
|
|
||||||
let port = u16::try_from(port).map_err(|_| BridgeError("invalid port".to_string()))?;
|
|
||||||
|
|
||||||
android::start(AndroidRuntimeConfig {
|
|
||||||
data_dir,
|
|
||||||
bind_address,
|
|
||||||
port,
|
|
||||||
})
|
|
||||||
.map_err(BridgeError)
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = outcome.resolve_with::<StringResultPolicy, _>(|| ());
|
|
||||||
|
|
||||||
env.with_env_no_catch(|env| env.new_string(result))
|
|
||||||
.resolve_with::<ThrowRuntimeExAndDefault, _>(|| ())
|
|
||||||
.into_raw()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_stopHost<'local>(
|
|
||||||
mut env: EnvUnowned<'local>,
|
|
||||||
_class: JClass<'local>,
|
|
||||||
) -> jstring {
|
|
||||||
env.with_env_no_catch(|env| env.new_string(android::stop()))
|
|
||||||
.resolve_with::<ThrowRuntimeExAndDefault, _>(|| ())
|
|
||||||
.into_raw()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_hostStatus<'local>(
|
|
||||||
mut env: EnvUnowned<'local>,
|
|
||||||
_class: JClass<'local>,
|
|
||||||
) -> jstring {
|
|
||||||
env.with_env_no_catch(|env| env.new_string(android::status()))
|
|
||||||
.resolve_with::<ThrowRuntimeExAndDefault, _>(|| ())
|
|
||||||
.into_raw()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "system" fn Java_cn_one_1kvm_androidhost_NativeBridge_kernelVersion<'local>(
|
|
||||||
mut env: EnvUnowned<'local>,
|
|
||||||
_class: JClass<'local>,
|
|
||||||
) -> jstring {
|
|
||||||
env.with_env_no_catch(|env| env.new_string(env!("CARGO_PKG_VERSION")))
|
|
||||||
.resolve_with::<ThrowRuntimeExAndDefault, _>(|| ())
|
|
||||||
.into_raw()
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
pluginManagement {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
gradlePluginPortal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootProject.name = "OneKvmAndroidHost"
|
|
||||||
include(":app")
|
|
||||||
31
build.rs
31
build.rs
@@ -2,21 +2,6 @@ use std::fs;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Set BUILD_DATE environment variable for compile-time access
|
|
||||||
// Use system time to avoid adding chrono as a build dependency
|
|
||||||
let now = std::time::SystemTime::now();
|
|
||||||
let duration = now.duration_since(std::time::UNIX_EPOCH).unwrap();
|
|
||||||
let secs = duration.as_secs();
|
|
||||||
|
|
||||||
// Convert Unix timestamp to date (simplified calculation)
|
|
||||||
// Days since epoch
|
|
||||||
let days = secs / 86400;
|
|
||||||
// Calculate year, month, day from days since 1970-01-01
|
|
||||||
let (year, month, day) = days_to_ymd(days as i64);
|
|
||||||
let build_date = format!("{:04}-{:02}-{:02}", year, month, day);
|
|
||||||
|
|
||||||
println!("cargo:rustc-env=BUILD_DATE={}", build_date);
|
|
||||||
|
|
||||||
// Compile protobuf files for RustDesk protocol
|
// Compile protobuf files for RustDesk protocol
|
||||||
compile_protos();
|
compile_protos();
|
||||||
|
|
||||||
@@ -86,19 +71,3 @@ pub mod rustdesk {
|
|||||||
|
|
||||||
fs::write(&dest_path, code).expect("Failed to write secrets_generated.rs");
|
fs::write(&dest_path, code).expect("Failed to write secrets_generated.rs");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert days since Unix epoch to year-month-day
|
|
||||||
fn days_to_ymd(days: i64) -> (i32, u32, u32) {
|
|
||||||
// Algorithm from http://howardhinnant.github.io/date_algorithms.html
|
|
||||||
let z = days + 719468;
|
|
||||||
let era = if z >= 0 { z } else { z - 146096 } / 146097;
|
|
||||||
let doe = (z - era * 146097) as u32;
|
|
||||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
|
||||||
let y = yoe as i64 + era * 400;
|
|
||||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
||||||
let mp = (5 * doy + 2) / 153;
|
|
||||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
|
||||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
|
||||||
let year = if m <= 2 { y + 1 } else { y };
|
|
||||||
(year as i32, m, d)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,24 +1,35 @@
|
|||||||
# One-KVM Runtime Image
|
# One-KVM Runtime Image
|
||||||
# This Dockerfile only packages pre-compiled binaries (no compilation)
|
# This Dockerfile only packages pre-compiled binaries (no compilation)
|
||||||
# Used after cross-compiling with `cross build`
|
# Used after cross-compiling with `cross build`
|
||||||
# Using Debian 11 for maximum compatibility (GLIBC 2.31)
|
# Using Debian 12 to match the build images (GLIBC 2.36)
|
||||||
|
|
||||||
ARG TARGETPLATFORM=linux/amd64
|
ARG TARGETPLATFORM=linux/amd64
|
||||||
|
|
||||||
FROM debian:11-slim
|
FROM debian:12-slim
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
ARG APT_MIRROR=
|
||||||
|
|
||||||
# Install runtime dependencies in a single layer
|
# Install runtime dependencies in a single layer
|
||||||
# All codec libraries (libx264, libx265, libopus) are now statically linked
|
# All codec libraries (libx264, libx265, libopus) are now statically linked
|
||||||
# Only hardware acceleration drivers and core system libraries remain dynamic
|
# Only hardware acceleration drivers and core system libraries remain dynamic
|
||||||
RUN sed -i 's/ main$/ main contrib non-free/' /etc/apt/sources.list && \
|
# Set APT_MIRROR=mirrors.tuna.tsinghua.edu.cn to speed up builds in China
|
||||||
|
RUN if [ -n "$APT_MIRROR" ]; then \
|
||||||
|
sed -i "s@deb.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list.d/debian.sources; \
|
||||||
|
fi \
|
||||||
|
&& sed -i 's/^Components: main$/Components: main contrib non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
# Core runtime (all platforms) - no codec libs needed
|
# Core runtime (all platforms) - no codec libs needed
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
# Bluetooth HID uses the host BlueZ system bus; btmgmt manages a dedicated adapter.
|
||||||
|
bluez \
|
||||||
libudev1 \
|
libudev1 \
|
||||||
libasound2 \
|
libasound2 \
|
||||||
|
# OTG Ethernet bridge control (nmcli talks to the host NetworkManager over D-Bus)
|
||||||
|
network-manager \
|
||||||
|
iproute2 \
|
||||||
|
iputils-ping \
|
||||||
# v4l2 is handled by kernel, minimal userspace needed
|
# v4l2 is handled by kernel, minimal userspace needed
|
||||||
libv4l-0 \
|
libv4l-0 \
|
||||||
&& \
|
&& \
|
||||||
|
|||||||
@@ -1,24 +1,35 @@
|
|||||||
# One-KVM Runtime Image (full)
|
# One-KVM Runtime Image (full)
|
||||||
# This Dockerfile only packages pre-compiled binaries (no compilation)
|
# This Dockerfile only packages pre-compiled binaries (no compilation)
|
||||||
# Used after cross-compiling with `cross build`
|
# Used after cross-compiling with `cross build`
|
||||||
# Using Debian 11 for maximum compatibility (GLIBC 2.31)
|
# Using Debian 12 to match the build images (GLIBC 2.36)
|
||||||
|
|
||||||
ARG TARGETPLATFORM=linux/amd64
|
ARG TARGETPLATFORM=linux/amd64
|
||||||
|
|
||||||
FROM debian:11-slim
|
FROM debian:12-slim
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
ARG APT_MIRROR=
|
||||||
|
|
||||||
# Install runtime dependencies in a single layer
|
# Install runtime dependencies in a single layer
|
||||||
# All codec libraries (libx264, libx265, libopus) are now statically linked
|
# All codec libraries (libx264, libx265, libopus) are now statically linked
|
||||||
# Only hardware acceleration drivers and core system libraries remain dynamic
|
# Only hardware acceleration drivers and core system libraries remain dynamic
|
||||||
RUN sed -i 's/ main$/ main contrib non-free/' /etc/apt/sources.list && \
|
# Set APT_MIRROR=mirrors.tuna.tsinghua.edu.cn to speed up builds in China
|
||||||
|
RUN if [ -n "$APT_MIRROR" ]; then \
|
||||||
|
sed -i "s@deb.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list.d/debian.sources; \
|
||||||
|
fi \
|
||||||
|
&& sed -i 's/^Components: main$/Components: main contrib non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
# Core runtime (all platforms) - no codec libs needed
|
# Core runtime (all platforms) - no codec libs needed
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
# Bluetooth HID uses the host BlueZ system bus; btmgmt manages a dedicated adapter.
|
||||||
|
bluez \
|
||||||
libudev1 \
|
libudev1 \
|
||||||
libasound2 \
|
libasound2 \
|
||||||
|
# OTG Ethernet bridge control (nmcli talks to the host NetworkManager over D-Bus)
|
||||||
|
network-manager \
|
||||||
|
iproute2 \
|
||||||
|
iputils-ping \
|
||||||
# v4l2 is handled by kernel, minimal userspace needed
|
# v4l2 is handled by kernel, minimal userspace needed
|
||||||
libv4l-0 \
|
libv4l-0 \
|
||||||
&& \
|
&& \
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Build Android APKs using the Docker build image.
|
|
||||||
# Usage: ./build/build-android.sh [arm64|armv7|all|help]
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
DOCKERFILE="$PROJECT_ROOT/build/cross/Dockerfile.android"
|
|
||||||
IMAGE_NAME="${ONE_KVM_ANDROID_DOCKER_IMAGE:-one-kvm-android-build:cn}"
|
|
||||||
|
|
||||||
fail() {
|
|
||||||
echo "Error: $*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
build_android() {
|
|
||||||
local arch="$1"
|
|
||||||
local docker_build_args=()
|
|
||||||
local docker_mount_args=()
|
|
||||||
local gradle_distribution_url="${ONE_KVM_GRADLE_DISTRIBUTION_URL:-}"
|
|
||||||
local gradle_distribution_url_cn="${ONE_KVM_GRADLE_DISTRIBUTION_URL_CN:-https://mirrors.cloud.tencent.com/gradle/gradle-9.1.0-bin.zip}"
|
|
||||||
local gradle_network_timeout="${ONE_KVM_GRADLE_NETWORK_TIMEOUT:-120000}"
|
|
||||||
local gradle_cache="${ONE_KVM_ANDROID_GRADLE_CACHE_DIR:-one-kvm-android-gradle-cache}"
|
|
||||||
local cargo_registry_cache="${ONE_KVM_ANDROID_CARGO_REGISTRY_CACHE_DIR:-one-kvm-android-cargo-registry}"
|
|
||||||
local cargo_git_cache="${ONE_KVM_ANDROID_CARGO_GIT_CACHE_DIR:-one-kvm-android-cargo-git}"
|
|
||||||
|
|
||||||
add_cache_mount() {
|
|
||||||
local source="$1"
|
|
||||||
local target="$2"
|
|
||||||
|
|
||||||
if [[ "$source" == /* || "$source" == ./* || "$source" == ../* ]]; then
|
|
||||||
mkdir -p "$source"
|
|
||||||
source="$(cd "$source" && pwd)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker_mount_args+=("-v" "$source:$target")
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "${CHINAMIRRO:-}" == "1" ]]; then
|
|
||||||
docker_build_args+=("--build-arg" "CHINAMIRRO=1")
|
|
||||||
if [[ -z "$gradle_distribution_url" ]]; then
|
|
||||||
gradle_distribution_url="$gradle_distribution_url_cn"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${ONE_KVM_ANDROID_SKIP_DOCKER_BUILD:-0}" == "1" ]]; then
|
|
||||||
echo "=== Skipping Android image build: $IMAGE_NAME ==="
|
|
||||||
else
|
|
||||||
echo "=== Building Android image: $IMAGE_NAME ==="
|
|
||||||
docker build \
|
|
||||||
-f "$DOCKERFILE" \
|
|
||||||
-t "$IMAGE_NAME" \
|
|
||||||
"${docker_build_args[@]}" \
|
|
||||||
"$PROJECT_ROOT/build/cross"
|
|
||||||
fi
|
|
||||||
|
|
||||||
add_cache_mount "$gradle_cache" "/root/.gradle"
|
|
||||||
add_cache_mount "$cargo_registry_cache" "/root/.cargo/registry"
|
|
||||||
add_cache_mount "$cargo_git_cache" "/root/.cargo/git"
|
|
||||||
|
|
||||||
echo "=== Building Android APK: $arch ==="
|
|
||||||
docker run --rm \
|
|
||||||
-v "$PROJECT_ROOT:/workspace" \
|
|
||||||
"${docker_mount_args[@]}" \
|
|
||||||
-w /workspace \
|
|
||||||
-e "CHINAMIRRO=${CHINAMIRRO:-0}" \
|
|
||||||
-e "ONE_KVM_GRADLE_DISTRIBUTION_URL=$gradle_distribution_url" \
|
|
||||||
-e "ONE_KVM_GRADLE_DISTRIBUTION_URL_CN=$gradle_distribution_url_cn" \
|
|
||||||
-e "ONE_KVM_GRADLE_NETWORK_TIMEOUT=$gradle_network_timeout" \
|
|
||||||
"$IMAGE_NAME" \
|
|
||||||
"$arch"
|
|
||||||
}
|
|
||||||
|
|
||||||
[[ -f "$DOCKERFILE" ]] || fail "Android Dockerfile not found: $DOCKERFILE"
|
|
||||||
command -v docker >/dev/null 2>&1 || fail "docker is required"
|
|
||||||
|
|
||||||
case "${1:-all}" in
|
|
||||||
all)
|
|
||||||
build_android all
|
|
||||||
;;
|
|
||||||
arm64)
|
|
||||||
build_android arm64
|
|
||||||
;;
|
|
||||||
armv7)
|
|
||||||
build_android armv7
|
|
||||||
;;
|
|
||||||
help | --help | -h)
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: build/build-android.sh [arch|help]
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
all (default) Build arm64 and armv7 APKs
|
|
||||||
arm64 Build only arm64 APK
|
|
||||||
armv7 Build only ARMv7 APK
|
|
||||||
help Show this help
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
build/build-android.sh
|
|
||||||
build/build-android.sh arm64
|
|
||||||
CHINAMIRRO=1 build/build-android.sh all
|
|
||||||
CHINAMIRRO=1 ONE_KVM_GRADLE_DISTRIBUTION_URL=https://mirrors.aliyun.com/macports/distfiles/gradle/gradle-9.1.0-bin.zip build/build-android.sh all
|
|
||||||
|
|
||||||
Environment:
|
|
||||||
ONE_KVM_ANDROID_GRADLE_CACHE_DIR Host Gradle cache path or Docker volume name
|
|
||||||
ONE_KVM_ANDROID_CARGO_REGISTRY_CACHE_DIR Host Cargo registry cache path or Docker volume name
|
|
||||||
ONE_KVM_ANDROID_CARGO_GIT_CACHE_DIR Host Cargo git cache path or Docker volume name
|
|
||||||
ONE_KVM_ANDROID_SKIP_DOCKER_BUILD=1 Reuse an already loaded Docker image
|
|
||||||
|
|
||||||
APK output:
|
|
||||||
target/android/one-kvm_<version>_<arm32|arm64>.apk
|
|
||||||
EOF
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
fail "Unknown argument: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@@ -21,16 +21,25 @@ build_arch() {
|
|||||||
|
|
||||||
case "${CHINAMIRRO:-}" in
|
case "${CHINAMIRRO:-}" in
|
||||||
1|true|TRUE|yes|YES|on|ON)
|
1|true|TRUE|yes|YES|on|ON)
|
||||||
local cross_build_opts="${CROSS_BUILD_OPTS:+$CROSS_BUILD_OPTS }--build-arg CHINAMIRRO=1"
|
# Default Rust toolchain mirror is USTC (rsproxy.cn is unreachable via the
|
||||||
echo "=== China mirror acceleration: enabled (Tsinghua) ==="
|
# Docker bridge on some networks because its CDN blocks the SNAT source IP).
|
||||||
|
local rust_dist="${RUSTUP_DIST_SERVER_MIRROR:-https://mirrors.ustc.edu.cn/rust-static}"
|
||||||
|
local rust_root="${RUSTUP_UPDATE_ROOT_MIRROR:-https://mirrors.ustc.edu.cn/rust-static/rustup}"
|
||||||
|
local crates_sparse="${CARGO_SPARSE_MIRROR:-sparse+https://mirrors.ustc.edu.cn/crates.io-index/}"
|
||||||
|
local crates_git="${CARGO_GIT_MIRROR:-https://mirrors.ustc.edu.cn/crates.io-index}"
|
||||||
|
local cross_build_opts="${CROSS_BUILD_OPTS:+$CROSS_BUILD_OPTS }--progress=plain --build-arg CHINAMIRRO=1 --build-arg GH_PROXY=${GH_PROXY:-https://ghfast.top/} --build-arg DEBIAN_IMAGE=${DEBIAN_IMAGE:-dockerproxy.net/library/debian:12}"
|
||||||
|
cross_build_opts="$cross_build_opts --build-arg HTTP_PROXY= --build-arg HTTPS_PROXY= --build-arg ALL_PROXY= --build-arg NO_PROXY="
|
||||||
|
cross_build_opts="$cross_build_opts --build-arg http_proxy= --build-arg https_proxy= --build-arg all_proxy= --build-arg no_proxy="
|
||||||
|
echo "=== China mirror acceleration: enabled ==="
|
||||||
echo "=== Building: $rust_target (via cross with custom Dockerfile) ==="
|
echo "=== Building: $rust_target (via cross with custom Dockerfile) ==="
|
||||||
env \
|
env \
|
||||||
CROSS_BUILD_OPTS="$cross_build_opts" \
|
CROSS_BUILD_OPTS="$cross_build_opts" \
|
||||||
CARGO_SOURCE_CRATES_IO_REPLACE_WITH=tuna \
|
CARGO_SOURCE_CRATES_IO_REPLACE_WITH=cn-mirror \
|
||||||
CARGO_SOURCE_TUNA_REGISTRY=sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/ \
|
CARGO_SOURCE_CN-MIRROR_REGISTRY="$crates_sparse" \
|
||||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup \
|
CARGO_NET_GIT_FETCH_WITH_CLI=true \
|
||||||
RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup \
|
RUSTUP_DIST_SERVER="$rust_dist" \
|
||||||
|
RUSTUP_UPDATE_ROOT="$rust_root" \
|
||||||
cross build --release --target "$rust_target"
|
cross build --release --target "$rust_target"
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
@@ -66,7 +75,7 @@ case "${1:-all}" in
|
|||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " $0 # Build all"
|
echo " $0 # Build all"
|
||||||
echo " $0 x86_64 # Build x86_64 only"
|
echo " $0 x86_64 # Build x86_64 only"
|
||||||
echo " CHINAMIRRO=1 $0 arm64 # Build with Tsinghua mirrors"
|
echo " CHINAMIRRO=1 $0 arm64 # Build with China mirrors"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -83,16 +92,4 @@ for target in "${ARCH_MAP[@]}"; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
echo ""
|
echo ""
|
||||||
echo "Static libraries:"
|
|
||||||
for target in "${ARCH_MAP[@]}"; do
|
|
||||||
case "$target" in
|
|
||||||
x86_64-unknown-linux-gnu) gnu_target="x86_64-linux-gnu" ;;
|
|
||||||
aarch64-unknown-linux-gnu) gnu_target="aarch64-linux-gnu" ;;
|
|
||||||
armv7-unknown-linux-gnueabihf) gnu_target="armv7-linux-gnueabihf" ;;
|
|
||||||
esac
|
|
||||||
if [ -d "$PROJECT_DIR/target/one-kvm-libs/$gnu_target/lib" ]; then
|
|
||||||
echo " $gnu_target: OK"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
echo "Next step: ./build/package-docker.sh or ./build/package-deb.sh"
|
echo "Next step: ./build/package-docker.sh or ./build/package-deb.sh"
|
||||||
|
|||||||
@@ -1,309 +0,0 @@
|
|||||||
# Android build image for One-KVM
|
|
||||||
# Based on Debian 11 for stable toolchain/runtime compatibility
|
|
||||||
|
|
||||||
FROM debian:11
|
|
||||||
|
|
||||||
ARG CHINAMIRRO=0
|
|
||||||
ARG ANDROID_SDK_ROOT=/root/android-sdk
|
|
||||||
ARG ANDROID_CMDLINE_TOOLS_VERSION=11076708_latest
|
|
||||||
ARG ANDROID_NDK_VERSION=27.3.13750724
|
|
||||||
ARG ANDROID_PLATFORM=36
|
|
||||||
ARG ANDROID_BUILD_TOOLS=36.0.0
|
|
||||||
ARG CARGO_NDK_VERSION=4.1.2
|
|
||||||
ARG RUSTUP_DIST_SERVER_CN=https://mirrors.tuna.tsinghua.edu.cn/rustup
|
|
||||||
ARG RUSTUP_UPDATE_ROOT_CN=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup
|
|
||||||
ARG CARGO_REGISTRY_CN=sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/
|
|
||||||
ARG MAVEN_REPOSITORY_CN=https://maven.aliyun.com/repository/public
|
|
||||||
ARG GOOGLE_MAVEN_REPOSITORY_CN=https://maven.aliyun.com/repository/google
|
|
||||||
ARG GRADLE_DISTRIBUTION_URL_CN=https://mirrors.cloud.tencent.com/gradle/gradle-9.1.0-bin.zip
|
|
||||||
ARG ANDROID_CMDLINE_TOOLS_URL=
|
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
ENV ANDROID_HOME=${ANDROID_SDK_ROOT}
|
|
||||||
ENV ANDROID_SDK_ROOT=${ANDROID_SDK_ROOT}
|
|
||||||
ENV ANDROID_NDK_HOME=${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION}
|
|
||||||
ENV ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION}
|
|
||||||
ENV ANDROID_BUILD_TOOLS=${ANDROID_BUILD_TOOLS}
|
|
||||||
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
|
||||||
ENV PATH=/root/.cargo/bin:${PATH}
|
|
||||||
ENV ONE_KVM_GRADLE_DISTRIBUTION_URL_CN=${GRADLE_DISTRIBUTION_URL_CN}
|
|
||||||
|
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
|
||||||
sed -i \
|
|
||||||
-e 's|http://deb.debian.org/debian|http://mirrors.tuna.tsinghua.edu.cn/debian|g' \
|
|
||||||
-e 's|http://security.debian.org/debian-security|http://mirrors.tuna.tsinghua.edu.cn/debian-security|g' \
|
|
||||||
/etc/apt/sources.list; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
wget \
|
|
||||||
unzip \
|
|
||||||
zip \
|
|
||||||
git \
|
|
||||||
bash \
|
|
||||||
build-essential \
|
|
||||||
pkg-config \
|
|
||||||
cmake \
|
|
||||||
ninja-build \
|
|
||||||
autoconf \
|
|
||||||
automake \
|
|
||||||
libtool \
|
|
||||||
nasm \
|
|
||||||
yasm \
|
|
||||||
python3 \
|
|
||||||
openjdk-17-jdk-headless \
|
|
||||||
libstdc++6 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
|
||||||
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
|
||||||
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
|
||||||
mkdir -p /root/.cargo; \
|
|
||||||
printf '%s\n' \
|
|
||||||
'[source.crates-io]' \
|
|
||||||
"replace-with = 'tuna'" \
|
|
||||||
'[source.tuna]' \
|
|
||||||
"registry = '${CARGO_REGISTRY_CN}'" \
|
|
||||||
'[registries.tuna]' \
|
|
||||||
"index = '${CARGO_REGISTRY_CN}'" \
|
|
||||||
> /root/.cargo/config.toml; \
|
|
||||||
fi \
|
|
||||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
|
||||||
&& cargo install cargo-ndk --version ${CARGO_NDK_VERSION} --locked \
|
|
||||||
&& rustup target add armv7-linux-androideabi aarch64-linux-android
|
|
||||||
|
|
||||||
RUN mkdir -p /opt/android-cmdline-tools \
|
|
||||||
&& cd /tmp \
|
|
||||||
&& if [ -n "$ANDROID_CMDLINE_TOOLS_URL" ]; then \
|
|
||||||
wget -q "$ANDROID_CMDLINE_TOOLS_URL" -O cmdline-tools.zip; \
|
|
||||||
else \
|
|
||||||
wget -q https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS_VERSION}.zip -O cmdline-tools.zip; \
|
|
||||||
fi \
|
|
||||||
&& unzip -q cmdline-tools.zip -d /opt/android-cmdline-tools \
|
|
||||||
&& mkdir -p ${ANDROID_SDK_ROOT}/cmdline-tools/latest \
|
|
||||||
&& mv /opt/android-cmdline-tools/cmdline-tools/* ${ANDROID_SDK_ROOT}/cmdline-tools/latest/ \
|
|
||||||
&& rm -rf /tmp/cmdline-tools.zip /opt/android-cmdline-tools
|
|
||||||
|
|
||||||
RUN mkdir -p ${ANDROID_SDK_ROOT}/licenses \
|
|
||||||
&& yes | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=${ANDROID_SDK_ROOT} --licenses >/dev/null \
|
|
||||||
&& ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=${ANDROID_SDK_ROOT} \
|
|
||||||
"platform-tools" \
|
|
||||||
"platforms;android-${ANDROID_PLATFORM}" \
|
|
||||||
"build-tools;${ANDROID_BUILD_TOOLS}" \
|
|
||||||
"ndk;${ANDROID_NDK_VERSION}" \
|
|
||||||
"cmake;3.22.1" \
|
|
||||||
&& mkdir -p ${ANDROID_NDK_HOME}
|
|
||||||
|
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
|
||||||
mkdir -p /root/.gradle; \
|
|
||||||
printf '%s\n' \
|
|
||||||
"beforeSettings { settings ->" \
|
|
||||||
" settings.pluginManagement.repositories.maven { url = uri('${GOOGLE_MAVEN_REPOSITORY_CN}') }" \
|
|
||||||
" settings.pluginManagement.repositories.maven { url = uri('${MAVEN_REPOSITORY_CN}') }" \
|
|
||||||
" settings.dependencyResolutionManagement.repositories.maven { url = uri('${GOOGLE_MAVEN_REPOSITORY_CN}') }" \
|
|
||||||
" settings.dependencyResolutionManagement.repositories.maven { url = uri('${MAVEN_REPOSITORY_CN}') }" \
|
|
||||||
"}" \
|
|
||||||
"allprojects {" \
|
|
||||||
" buildscript.repositories.maven { url = uri('${GOOGLE_MAVEN_REPOSITORY_CN}') }" \
|
|
||||||
" buildscript.repositories.maven { url = uri('${MAVEN_REPOSITORY_CN}') }" \
|
|
||||||
"}" \
|
|
||||||
> /root/.gradle/init.gradle; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
libclang-dev \
|
|
||||||
llvm \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
ENV LIBCLANG_PATH=/usr/lib/llvm-11/lib
|
|
||||||
|
|
||||||
RUN printf '%s\n' \
|
|
||||||
'#!/usr/bin/env bash' \
|
|
||||||
'set -euo pipefail' \
|
|
||||||
'' \
|
|
||||||
'PROJECT_ROOT="${ONE_KVM_ANDROID_PROJECT_ROOT:-/workspace}"' \
|
|
||||||
'ANDROID_DIR="${PROJECT_ROOT}/android"' \
|
|
||||||
'BUILD_TYPE="release"' \
|
|
||||||
'ARCH="${1:-all}"' \
|
|
||||||
'FFMPEG_ROOT="${ONE_KVM_ANDROID_FFMPEG_ROOT:-${PROJECT_ROOT}/dist/android-ffmpeg-mediacodec}"' \
|
|
||||||
'OUTPUT_DIR="${PROJECT_ROOT}/target/android"' \
|
|
||||||
'SIGNING_DIR="${PROJECT_ROOT}/target/android-signing"' \
|
|
||||||
'KEYSTORE_PATH="${SIGNING_DIR}/one-kvm-release.jks"' \
|
|
||||||
'KEY_ALIAS="one-kvm-release"' \
|
|
||||||
'KEY_PASSWORD="one-kvm-release"' \
|
|
||||||
'ANDROID_BUILD_TOOLS_DIR="${ANDROID_SDK_ROOT}/build-tools/${ANDROID_BUILD_TOOLS}"' \
|
|
||||||
'WRAPPER_PROPERTIES="$ANDROID_DIR/gradle/wrapper/gradle-wrapper.properties"' \
|
|
||||||
'GRADLE_DISTRIBUTION_URL="${ONE_KVM_GRADLE_DISTRIBUTION_URL:-}"' \
|
|
||||||
'GRADLE_DISTRIBUTION_URL_CN="${ONE_KVM_GRADLE_DISTRIBUTION_URL_CN:-https://mirrors.cloud.tencent.com/gradle/gradle-9.1.0-bin.zip}"' \
|
|
||||||
'GRADLE_NETWORK_TIMEOUT="${ONE_KVM_GRADLE_NETWORK_TIMEOUT:-120000}"' \
|
|
||||||
'' \
|
|
||||||
'usage() {' \
|
|
||||||
' cat <<EOF' \
|
|
||||||
'Usage:' \
|
|
||||||
' docker run --rm -v "$PWD:/workspace" one-kvm-android-build:cn [arm64|armv7|all|help]' \
|
|
||||||
'' \
|
|
||||||
'Commands:' \
|
|
||||||
' all Build arm64 and armv7 APKs. Default.' \
|
|
||||||
' arm64 Build only arm64 APK.' \
|
|
||||||
' armv7 Build only ARMv7 APK.' \
|
|
||||||
' help Show this help.' \
|
|
||||||
'' \
|
|
||||||
'APK output:' \
|
|
||||||
' target/android/one-kvm_<version>_<arm32|arm64>.apk' \
|
|
||||||
'EOF' \
|
|
||||||
'}' \
|
|
||||||
'' \
|
|
||||||
'fail() {' \
|
|
||||||
' echo "Error: $*" >&2' \
|
|
||||||
' exit 1' \
|
|
||||||
'}' \
|
|
||||||
'' \
|
|
||||||
'read_project_version() {' \
|
|
||||||
' local version' \
|
|
||||||
' version="$(awk -F "\"" '"'"'/^version[[:space:]]*=/ { print $2; exit }'"'"' "$PROJECT_ROOT/Cargo.toml")"' \
|
|
||||||
' [[ -n "$version" ]] || fail "Failed to resolve version from $PROJECT_ROOT/Cargo.toml"' \
|
|
||||||
' printf "%s\n" "$version"' \
|
|
||||||
'}' \
|
|
||||||
'' \
|
|
||||||
'copy_apks() {' \
|
|
||||||
' local flavor="$1"' \
|
|
||||||
' local src_dir="$ANDROID_DIR/app/build/outputs/apk/$flavor/$BUILD_TYPE"' \
|
|
||||||
' local found=0' \
|
|
||||||
' mkdir -p "$OUTPUT_DIR"' \
|
|
||||||
' for apk in "$src_dir"/*.apk; do' \
|
|
||||||
' [[ -f "$apk" ]] || continue' \
|
|
||||||
' sign_apk "$apk" "$OUTPUT_DIR/one-kvm_${PROJECT_VERSION}_${flavor}.apk"' \
|
|
||||||
' found=1' \
|
|
||||||
' done' \
|
|
||||||
' [[ "$found" == "1" ]] || fail "No APK files found in: $src_dir"' \
|
|
||||||
'}' \
|
|
||||||
'' \
|
|
||||||
'ensure_keystore() {' \
|
|
||||||
' if [[ -f "$KEYSTORE_PATH" ]]; then' \
|
|
||||||
' return' \
|
|
||||||
' fi' \
|
|
||||||
' mkdir -p "$SIGNING_DIR"' \
|
|
||||||
' keytool -genkeypair -noprompt -keystore "$KEYSTORE_PATH" -storetype PKCS12 -alias "$KEY_ALIAS" -keyalg RSA -keysize 2048 -validity 10000 -storepass "$KEY_PASSWORD" -keypass "$KEY_PASSWORD" -dname "CN=One-KVM, OU=One-KVM, O=One-KVM, L=Local, S=Local, C=US" >/dev/null' \
|
|
||||||
'}' \
|
|
||||||
'' \
|
|
||||||
'sign_apk() {' \
|
|
||||||
' local input_apk="$1"' \
|
|
||||||
' local output_apk="$2"' \
|
|
||||||
' local aligned_apk' \
|
|
||||||
' aligned_apk="$(mktemp --suffix=.apk)"' \
|
|
||||||
' "$ANDROID_BUILD_TOOLS_DIR/zipalign" -f -p 4 "$input_apk" "$aligned_apk"' \
|
|
||||||
' "$ANDROID_BUILD_TOOLS_DIR/apksigner" sign --ks "$KEYSTORE_PATH" --ks-key-alias "$KEY_ALIAS" --ks-pass "pass:$KEY_PASSWORD" --key-pass "pass:$KEY_PASSWORD" --out "$output_apk" "$aligned_apk"' \
|
|
||||||
' "$ANDROID_BUILD_TOOLS_DIR/apksigner" verify --verbose "$output_apk" >/dev/null' \
|
|
||||||
' rm -f "$aligned_apk"' \
|
|
||||||
'}' \
|
|
||||||
'' \
|
|
||||||
'cd "$PROJECT_ROOT"' \
|
|
||||||
'' \
|
|
||||||
'case "$ARCH" in' \
|
|
||||||
'help | --help | -h)' \
|
|
||||||
' usage' \
|
|
||||||
' exit 0' \
|
|
||||||
' ;;' \
|
|
||||||
'esac' \
|
|
||||||
'' \
|
|
||||||
'[[ -d "$ANDROID_DIR" ]] || fail "Android project not found: $ANDROID_DIR"' \
|
|
||||||
'[[ -x "$ANDROID_DIR/gradlew" ]] || fail "Gradle wrapper is not executable: $ANDROID_DIR/gradlew"' \
|
|
||||||
'[[ -f "$WRAPPER_PROPERTIES" ]] || fail "Gradle wrapper properties not found: $WRAPPER_PROPERTIES"' \
|
|
||||||
'' \
|
|
||||||
'ORIGINAL_WRAPPER_PROPERTIES="$(mktemp)"' \
|
|
||||||
'cp "$WRAPPER_PROPERTIES" "$ORIGINAL_WRAPPER_PROPERTIES"' \
|
|
||||||
'cleanup_wrapper_properties() {' \
|
|
||||||
' cp "$ORIGINAL_WRAPPER_PROPERTIES" "$WRAPPER_PROPERTIES"' \
|
|
||||||
' rm -f "$ORIGINAL_WRAPPER_PROPERTIES"' \
|
|
||||||
'}' \
|
|
||||||
'trap cleanup_wrapper_properties EXIT' \
|
|
||||||
'' \
|
|
||||||
'if [[ "${CHINAMIRRO:-0}" == "1" && -z "$GRADLE_DISTRIBUTION_URL" ]]; then' \
|
|
||||||
' GRADLE_DISTRIBUTION_URL="$GRADLE_DISTRIBUTION_URL_CN"' \
|
|
||||||
'fi' \
|
|
||||||
'' \
|
|
||||||
'if [[ -n "$GRADLE_DISTRIBUTION_URL" ]]; then' \
|
|
||||||
' WRAPPER_PROPERTIES_TMP="$(mktemp)"' \
|
|
||||||
' awk -v url="$GRADLE_DISTRIBUTION_URL" -v timeout="$GRADLE_NETWORK_TIMEOUT" '"'"'' \
|
|
||||||
' BEGIN { seen_url = 0; seen_timeout = 0 }' \
|
|
||||||
' /^distributionUrl=/ { print "distributionUrl=" url; seen_url = 1; next }' \
|
|
||||||
' /^networkTimeout=/ { print "networkTimeout=" timeout; seen_timeout = 1; next }' \
|
|
||||||
' { print }' \
|
|
||||||
' END {' \
|
|
||||||
' if (!seen_url) print "distributionUrl=" url;' \
|
|
||||||
' if (!seen_timeout) print "networkTimeout=" timeout;' \
|
|
||||||
' }' \
|
|
||||||
' '"'"' "$WRAPPER_PROPERTIES" > "$WRAPPER_PROPERTIES_TMP"' \
|
|
||||||
' cp "$WRAPPER_PROPERTIES_TMP" "$WRAPPER_PROPERTIES"' \
|
|
||||||
' rm -f "$WRAPPER_PROPERTIES_TMP"' \
|
|
||||||
' find /root/.gradle/wrapper/dists -name "*.lck" -o -name "*.part" 2>/dev/null | xargs -r rm -f' \
|
|
||||||
'fi' \
|
|
||||||
'' \
|
|
||||||
'ensure_keystore' \
|
|
||||||
'' \
|
|
||||||
'case "$ARCH" in' \
|
|
||||||
'arm64)' \
|
|
||||||
' ANDROID_ABIS="arm64-v8a"' \
|
|
||||||
' GRADLE_TASK=":app:assembleArm64Release"' \
|
|
||||||
' APK_FLAVORS="arm64"' \
|
|
||||||
' ;;' \
|
|
||||||
'armv7)' \
|
|
||||||
' ANDROID_ABIS="armeabi-v7a"' \
|
|
||||||
' GRADLE_TASK=":app:assembleArm32Release"' \
|
|
||||||
' APK_FLAVORS="arm32"' \
|
|
||||||
' ;;' \
|
|
||||||
'all)' \
|
|
||||||
' ANDROID_ABIS="arm64-v8a,armeabi-v7a"' \
|
|
||||||
' GRADLE_TASK=":app:assembleRelease"' \
|
|
||||||
' APK_FLAVORS="arm64 arm32"' \
|
|
||||||
' ;;' \
|
|
||||||
'*) fail "Unsupported architecture: $ARCH (expected arm64, armv7, or all)" ;;' \
|
|
||||||
'esac' \
|
|
||||||
'' \
|
|
||||||
'printf "sdk.dir=%s\n" "$ANDROID_HOME" > "$ANDROID_DIR/local.properties"' \
|
|
||||||
'mkdir -p "$OUTPUT_DIR"' \
|
|
||||||
'PROJECT_VERSION="$(read_project_version)"' \
|
|
||||||
'' \
|
|
||||||
'export ONE_KVM_ANDROID_PROFILE="$BUILD_TYPE"' \
|
|
||||||
'export ONE_KVM_ANDROID_ABIS="$ANDROID_ABIS"' \
|
|
||||||
'export ONE_KVM_ANDROID_FFMPEG_ROOT="$FFMPEG_ROOT"' \
|
|
||||||
'export ANDROID_HOME' \
|
|
||||||
'export ANDROID_SDK_ROOT' \
|
|
||||||
'export ANDROID_NDK_HOME' \
|
|
||||||
'export ANDROID_NDK_ROOT' \
|
|
||||||
'' \
|
|
||||||
'echo "Building Android APK"' \
|
|
||||||
'echo " task: $GRADLE_TASK"' \
|
|
||||||
'echo " profile: $ONE_KVM_ANDROID_PROFILE"' \
|
|
||||||
'echo " version: $PROJECT_VERSION"' \
|
|
||||||
'echo " abis: $ONE_KVM_ANDROID_ABIS"' \
|
|
||||||
'echo " output: $OUTPUT_DIR"' \
|
|
||||||
'echo " sdk: $ANDROID_HOME"' \
|
|
||||||
'echo " ndk: $ANDROID_NDK_HOME"' \
|
|
||||||
'echo " build tools: $ANDROID_BUILD_TOOLS_DIR"' \
|
|
||||||
'echo " ffmpeg root: $ONE_KVM_ANDROID_FFMPEG_ROOT"' \
|
|
||||||
'if [[ -n "$GRADLE_DISTRIBUTION_URL" ]]; then' \
|
|
||||||
' echo " gradle distribution: $GRADLE_DISTRIBUTION_URL"' \
|
|
||||||
'fi' \
|
|
||||||
'' \
|
|
||||||
'(' \
|
|
||||||
' cd "$ANDROID_DIR"' \
|
|
||||||
' ./gradlew "$GRADLE_TASK"' \
|
|
||||||
')' \
|
|
||||||
'' \
|
|
||||||
'for flavor in $APK_FLAVORS; do' \
|
|
||||||
' copy_apks "$flavor"' \
|
|
||||||
'done' \
|
|
||||||
'' \
|
|
||||||
'echo' \
|
|
||||||
'echo "APK output:"' \
|
|
||||||
'ls -1 "$OUTPUT_DIR"' \
|
|
||||||
> /usr/local/bin/build-one-kvm-android \
|
|
||||||
&& chmod +x /usr/local/bin/build-one-kvm-android
|
|
||||||
|
|
||||||
WORKDIR /workspace
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/build-one-kvm-android"]
|
|
||||||
CMD ["all"]
|
|
||||||
@@ -1,19 +1,29 @@
|
|||||||
# Cross-compilation image for ARM64 based on Debian 11
|
# Cross-compilation image for ARM64 based on Debian 12
|
||||||
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
|
# Build on Debian 12 (GLIBC 2.36) to match the runtime images
|
||||||
|
|
||||||
FROM debian:11
|
ARG DEBIAN_IMAGE=debian:12
|
||||||
|
FROM ${DEBIAN_IMAGE}
|
||||||
|
|
||||||
# Linux headers used by v4l2r bindgen
|
|
||||||
ARG LINUX_HEADERS_VERSION=6.6
|
|
||||||
ARG LINUX_HEADERS_SHA256=
|
|
||||||
ARG CHINAMIRRO=0
|
ARG CHINAMIRRO=0
|
||||||
|
ARG GH_PROXY=https://gh-proxy.com/
|
||||||
|
ARG RUSTUP_DIST_SERVER_CN=https://mirrors.ustc.edu.cn/rust-static
|
||||||
|
ARG RUSTUP_UPDATE_ROOT_CN=https://mirrors.ustc.edu.cn/rust-static/rustup
|
||||||
|
ARG CARGO_INDEX_CN=https://mirrors.ustc.edu.cn/crates.io-index
|
||||||
|
ARG CARGO_REGISTRY_CN=sparse+https://mirrors.ustc.edu.cn/crates.io-index/
|
||||||
|
ARG LIBJPEG_TURBO_VERSION=3.1.4.1
|
||||||
|
ARG LIBYUV_REV=957f295ea946cbbd13fcfc46e7066f2efa801233
|
||||||
|
ARG LIBVPX_VERSION=1.16.0
|
||||||
|
ARG X265_VERSION=3.4
|
||||||
|
ARG OPUS_VERSION=1.5.2
|
||||||
|
ARG RKMPP_BRANCH=jellyfin-mpp
|
||||||
|
ARG RKRGA_BRANCH=jellyfin-rga
|
||||||
|
ARG FFMPEG_ROCKCHIP_REV=40c412daccf08164493da0de990eb99a8948116b
|
||||||
|
|
||||||
# Optionally use Tsinghua mirrors for builds in China.
|
# Optionally use China mirrors for builds in China.
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
sed -i \
|
sed -i -E \
|
||||||
-e 's|http://deb.debian.org/debian|http://mirrors.tuna.tsinghua.edu.cn/debian|g' \
|
-e 's@http://deb.debian.org/debian([[:space:]]|$)@http://mirrors.tuna.tsinghua.edu.cn/debian\1@g' \
|
||||||
-e 's|http://security.debian.org/debian-security|http://mirrors.tuna.tsinghua.edu.cn/debian-security|g' \
|
/etc/apt/sources.list.d/debian.sources; \
|
||||||
/etc/apt/sources.list; \
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install Rust toolchain
|
# Install Rust toolchain
|
||||||
@@ -21,19 +31,23 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
mkdir -p /root/.cargo; \
|
mkdir -p /root/.cargo; \
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'[source.crates-io]' \
|
'[source.crates-io]' \
|
||||||
"replace-with = 'tuna'" \
|
"replace-with = 'cn-mirror-sparse'" \
|
||||||
'[source.tuna]' \
|
'[source.cn-mirror]' \
|
||||||
'registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_INDEX_CN}'" \
|
||||||
'[registries.tuna]' \
|
'[source.cn-mirror-sparse]' \
|
||||||
'index = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_REGISTRY_CN}'" \
|
||||||
|
'[registries.cn-mirror]' \
|
||||||
|
"index = '${CARGO_INDEX_CN}'" \
|
||||||
|
'[net]' \
|
||||||
|
'git-fetch-with-cli = true' \
|
||||||
> /root/.cargo/config.toml; \
|
> /root/.cargo/config.toml; \
|
||||||
fi \
|
fi \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -46,6 +60,7 @@ RUN dpkg --add-architecture arm64
|
|||||||
# Install cross-compiler and native build tools
|
# Install cross-compiler and native build tools
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
|
patch \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
cmake \
|
cmake \
|
||||||
nasm \
|
nasm \
|
||||||
@@ -69,6 +84,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
|
|
||||||
# Install ARM64 development libraries (without VAAPI/X11 for ARM)
|
# Install ARM64 development libraries (without VAAPI/X11 for ARM)
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libssl3 \
|
||||||
|
libssl3:arm64 \
|
||||||
libasound2-dev:arm64 \
|
libasound2-dev:arm64 \
|
||||||
libv4l-dev:arm64 \
|
libv4l-dev:arm64 \
|
||||||
libudev-dev:arm64 \
|
libudev-dev:arm64 \
|
||||||
@@ -78,20 +95,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libdrm-dev:arm64 \
|
libdrm-dev:arm64 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install newer V4L2 headers for v4l2r bindgen
|
|
||||||
RUN mkdir -p /opt/v4l2-headers \
|
|
||||||
&& wget -q https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${LINUX_HEADERS_VERSION}.tar.xz -O /tmp/linux-headers.tar.xz \
|
|
||||||
&& if [ -n "$LINUX_HEADERS_SHA256" ]; then echo "$LINUX_HEADERS_SHA256 /tmp/linux-headers.tar.xz" | sha256sum -c -; fi \
|
|
||||||
&& tar -xf /tmp/linux-headers.tar.xz -C /tmp \
|
|
||||||
&& cd /tmp/linux-${LINUX_HEADERS_VERSION} \
|
|
||||||
&& make ARCH=arm64 headers_install INSTALL_HDR_PATH=/opt/v4l2-headers \
|
|
||||||
&& rm -rf /tmp/linux-${LINUX_HEADERS_VERSION} /tmp/linux-headers.tar.xz
|
|
||||||
|
|
||||||
ENV V4L2R_VIDEODEV2_H_PATH=/opt/v4l2-headers/include
|
|
||||||
|
|
||||||
# Build static libjpeg-turbo from source (cross-compile for ARM64)
|
# Build static libjpeg-turbo from source (cross-compile for ARM64)
|
||||||
RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libjpeg-turbo \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/libjpeg-turbo \
|
||||||
&& cd /tmp/libjpeg-turbo \
|
&& cd /tmp/libjpeg-turbo \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/libjpeg-turbo/libjpeg-turbo.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/${LIBJPEG_TURBO_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& mkdir build && cd build \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
||||||
@@ -105,8 +116,13 @@ RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libj
|
|||||||
&& rm -rf /tmp/libjpeg-turbo
|
&& rm -rf /tmp/libjpeg-turbo
|
||||||
|
|
||||||
# Build static libyuv from source (cross-compile for ARM64)
|
# Build static libyuv from source (cross-compile for ARM64)
|
||||||
RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
RUN git init /tmp/libyuv \
|
||||||
&& cd /tmp/libyuv \
|
&& cd /tmp/libyuv \
|
||||||
|
&& github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/lemenkov/libyuv" \
|
||||||
|
&& git fetch --depth 1 origin ${LIBYUV_REV} \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& mkdir build && cd build \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
||||||
@@ -125,8 +141,13 @@ RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
|||||||
|
|
||||||
# Build static libvpx from source (cross-compile for ARM64)
|
# Build static libvpx from source (cross-compile for ARM64)
|
||||||
# CC/CXX/LD/AR must be environment variables, not configure arguments
|
# CC/CXX/LD/AR must be environment variables, not configure arguments
|
||||||
RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/libvpx \
|
||||||
&& cd /tmp/libvpx \
|
&& cd /tmp/libvpx \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/webmproject/libvpx.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/v${LIBVPX_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& echo "=== libvpx: Configuring for ARM64 ===" \
|
&& echo "=== libvpx: Configuring for ARM64 ===" \
|
||||||
&& export CC=aarch64-linux-gnu-gcc \
|
&& export CC=aarch64-linux-gnu-gcc \
|
||||||
&& export CXX=aarch64-linux-gnu-g++ \
|
&& export CXX=aarch64-linux-gnu-g++ \
|
||||||
@@ -149,7 +170,9 @@ RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
|||||||
&& rm -rf /tmp/libvpx
|
&& rm -rf /tmp/libvpx
|
||||||
|
|
||||||
# Build static libx264 from source (cross-compile for ARM64)
|
# Build static libx264 from source (cross-compile for ARM64)
|
||||||
RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git clone --depth 1 "${github_prefix}https://github.com/mirror/x264.git" /tmp/x264 \
|
||||||
&& cd /tmp/x264 \
|
&& cd /tmp/x264 \
|
||||||
&& export CC=aarch64-linux-gnu-gcc \
|
&& export CC=aarch64-linux-gnu-gcc \
|
||||||
&& export AR=aarch64-linux-gnu-ar \
|
&& export AR=aarch64-linux-gnu-ar \
|
||||||
@@ -165,12 +188,18 @@ RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
|||||||
&& rm -rf /tmp/x264
|
&& rm -rf /tmp/x264
|
||||||
|
|
||||||
# Build static libx265 from source (cross-compile for ARM64)
|
# Build static libx265 from source (cross-compile for ARM64)
|
||||||
RUN git clone --depth 1 https://bitbucket.org/multicoreware/x265_git /tmp/x265 \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/x265 \
|
||||||
&& cd /tmp/x265 \
|
&& cd /tmp/x265 \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/videolan/x265.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/${X265_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& cd source \
|
&& cd source \
|
||||||
&& mkdir -p build \
|
&& mkdir -p build \
|
||||||
&& cd build \
|
&& cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_ASM_NASM_FLAGS="-w-macro-params-legacy" \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
-DCMAKE_INSTALL_PREFIX=/usr/aarch64-linux-gnu \
|
||||||
-DCMAKE_SYSTEM_NAME=Linux \
|
-DCMAKE_SYSTEM_NAME=Linux \
|
||||||
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
|
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
|
||||||
@@ -204,12 +233,15 @@ Cflags: -I\${includedir}
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Build static libopus from source (cross-compile for ARM64)
|
# Build static libopus from source (cross-compile for ARM64)
|
||||||
RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& wget -O /tmp/opus.tar.gz "${github_prefix}https://github.com/xiph/opus/releases/download/v${OPUS_VERSION}/opus-${OPUS_VERSION}.tar.gz" \
|
||||||
|
&& tar -xzf /tmp/opus.tar.gz -C /tmp \
|
||||||
|
&& mv "/tmp/opus-${OPUS_VERSION}" /tmp/opus \
|
||||||
&& cd /tmp/opus \
|
&& cd /tmp/opus \
|
||||||
&& export CC=aarch64-linux-gnu-gcc \
|
&& export CC=aarch64-linux-gnu-gcc \
|
||||||
&& export AR=aarch64-linux-gnu-ar \
|
&& export AR=aarch64-linux-gnu-ar \
|
||||||
&& export RANLIB=aarch64-linux-gnu-ranlib \
|
&& export RANLIB=aarch64-linux-gnu-ranlib \
|
||||||
&& ./autogen.sh \
|
|
||||||
&& ./configure \
|
&& ./configure \
|
||||||
--prefix=/usr/aarch64-linux-gnu \
|
--prefix=/usr/aarch64-linux-gnu \
|
||||||
--host=aarch64-linux-gnu \
|
--host=aarch64-linux-gnu \
|
||||||
@@ -217,13 +249,24 @@ RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
|||||||
--disable-doc \
|
--disable-doc \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf /tmp/opus
|
&& rm -rf /tmp/opus /tmp/opus.tar.gz
|
||||||
|
|
||||||
|
# Reject raw formats smaller than the requested frame before any V4L2 upload.
|
||||||
|
COPY build/cross/patches/0001-v4l2-m2m-reject-undersized-encoder-format.patch /tmp/ffmpeg-v4l2-size.patch
|
||||||
|
|
||||||
# Download and build FFmpeg with RKMPP support
|
# Download and build FFmpeg with RKMPP support
|
||||||
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||||
&& wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz \
|
&& github_prefix="" \
|
||||||
&& tar -xzf ffmpeg.tar.gz \
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
&& cd ffmpeg \
|
&& git clone --depth 1 --branch ${RKMPP_BRANCH} "${github_prefix}https://github.com/nyanmisaka/mpp.git" rkmpp \
|
||||||
|
&& git clone --depth 1 --branch ${RKRGA_BRANCH} "${github_prefix}https://github.com/nyanmisaka/rk-mirrors.git" rkrga \
|
||||||
|
&& git init ffmpeg-rockchip \
|
||||||
|
&& cd ffmpeg-rockchip \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/nyanmisaka/ffmpeg-rockchip.git" \
|
||||||
|
&& git fetch --depth 1 origin ${FFMPEG_ROCKCHIP_REV} \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
|
&& patch -p1 < /tmp/ffmpeg-v4l2-size.patch \
|
||||||
|
&& cd .. \
|
||||||
# Build RKMPP
|
# Build RKMPP
|
||||||
&& mkdir -p rkmpp/build && cd rkmpp/build \
|
&& mkdir -p rkmpp/build && cd rkmpp/build \
|
||||||
&& cmake .. \
|
&& cmake .. \
|
||||||
@@ -238,6 +281,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/aarch64-linux-gnu/lib/pkgconfig/rockchip_mpp.pc \
|
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/aarch64-linux-gnu/lib/pkgconfig/rockchip_mpp.pc \
|
||||||
|
&& rm -f /usr/aarch64-linux-gnu/lib/librockchip_mpp.so* \
|
||||||
&& cd ../.. \
|
&& cd ../.. \
|
||||||
# Build RKRGA - create cross file for meson
|
# Build RKRGA - create cross file for meson
|
||||||
&& echo '[binaries]' > /tmp/aarch64-cross.txt \
|
&& echo '[binaries]' > /tmp/aarch64-cross.txt \
|
||||||
@@ -265,6 +309,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
&& ar rcs /usr/aarch64-linux-gnu/lib/librga.a $(find build -name '*.o') \
|
&& ar rcs /usr/aarch64-linux-gnu/lib/librga.a $(find build -name '*.o') \
|
||||||
&& ranlib /usr/aarch64-linux-gnu/lib/librga.a \
|
&& ranlib /usr/aarch64-linux-gnu/lib/librga.a \
|
||||||
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/aarch64-linux-gnu/lib/pkgconfig/librga.pc \
|
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/aarch64-linux-gnu/lib/pkgconfig/librga.pc \
|
||||||
|
&& rm -f /usr/aarch64-linux-gnu/lib/librga.so* \
|
||||||
&& cd .. \
|
&& cd .. \
|
||||||
# Create pkg-config wrapper for cross-compilation
|
# Create pkg-config wrapper for cross-compilation
|
||||||
&& echo '#!/bin/sh' > /tmp/aarch64-pkg-config \
|
&& echo '#!/bin/sh' > /tmp/aarch64-pkg-config \
|
||||||
@@ -354,8 +399,8 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
|
|
||||||
# Add Rust target
|
# Add Rust target
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& rustup target add aarch64-unknown-linux-gnu
|
&& rustup target add aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
@@ -371,4 +416,4 @@ ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
|
|||||||
LIBYUV_STATIC=1 \
|
LIBYUV_STATIC=1 \
|
||||||
OPUS_STATIC=1 \
|
OPUS_STATIC=1 \
|
||||||
PKG_CONFIG_ALL_STATIC=1 \
|
PKG_CONFIG_ALL_STATIC=1 \
|
||||||
RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc"
|
RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc -C link-arg=-Wl,--allow-multiple-definition"
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
# Cross-compilation image for ARMv7 based on Debian 11
|
# Cross-compilation image for ARMv7 based on Debian 12
|
||||||
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
|
# Build on Debian 12 (GLIBC 2.36) to match the runtime images
|
||||||
|
|
||||||
FROM debian:11
|
ARG DEBIAN_IMAGE=debian:12
|
||||||
|
FROM ${DEBIAN_IMAGE}
|
||||||
|
|
||||||
# Linux headers used by v4l2r bindgen
|
|
||||||
ARG LINUX_HEADERS_VERSION=6.6
|
|
||||||
ARG LINUX_HEADERS_SHA256=
|
|
||||||
ARG CHINAMIRRO=0
|
ARG CHINAMIRRO=0
|
||||||
|
ARG GH_PROXY=https://gh-proxy.com/
|
||||||
|
ARG RUSTUP_DIST_SERVER_CN=https://mirrors.ustc.edu.cn/rust-static
|
||||||
|
ARG RUSTUP_UPDATE_ROOT_CN=https://mirrors.ustc.edu.cn/rust-static/rustup
|
||||||
|
ARG CARGO_INDEX_CN=https://mirrors.ustc.edu.cn/crates.io-index
|
||||||
|
ARG CARGO_REGISTRY_CN=sparse+https://mirrors.ustc.edu.cn/crates.io-index/
|
||||||
|
ARG LIBJPEG_TURBO_VERSION=3.1.4.1
|
||||||
|
ARG LIBYUV_REV=957f295ea946cbbd13fcfc46e7066f2efa801233
|
||||||
|
ARG LIBVPX_VERSION=1.16.0
|
||||||
|
ARG X265_VERSION=3.4
|
||||||
|
ARG OPUS_VERSION=1.5.2
|
||||||
|
ARG RKMPP_BRANCH=jellyfin-mpp
|
||||||
|
ARG RKRGA_BRANCH=jellyfin-rga
|
||||||
|
ARG FFMPEG_ROCKCHIP_REV=40c412daccf08164493da0de990eb99a8948116b
|
||||||
|
|
||||||
# Optionally use Tsinghua mirrors for builds in China.
|
# Optionally use China mirrors for builds in China.
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
sed -i \
|
sed -i -E \
|
||||||
-e 's|http://deb.debian.org/debian|http://mirrors.tuna.tsinghua.edu.cn/debian|g' \
|
-e 's@http://deb.debian.org/debian([[:space:]]|$)@http://mirrors.tuna.tsinghua.edu.cn/debian\1@g' \
|
||||||
-e 's|http://security.debian.org/debian-security|http://mirrors.tuna.tsinghua.edu.cn/debian-security|g' \
|
/etc/apt/sources.list.d/debian.sources; \
|
||||||
/etc/apt/sources.list; \
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install Rust toolchain
|
# Install Rust toolchain
|
||||||
@@ -21,19 +31,23 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
mkdir -p /root/.cargo; \
|
mkdir -p /root/.cargo; \
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'[source.crates-io]' \
|
'[source.crates-io]' \
|
||||||
"replace-with = 'tuna'" \
|
"replace-with = 'cn-mirror-sparse'" \
|
||||||
'[source.tuna]' \
|
'[source.cn-mirror]' \
|
||||||
'registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_INDEX_CN}'" \
|
||||||
'[registries.tuna]' \
|
'[source.cn-mirror-sparse]' \
|
||||||
'index = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_REGISTRY_CN}'" \
|
||||||
|
'[registries.cn-mirror]' \
|
||||||
|
"index = '${CARGO_INDEX_CN}'" \
|
||||||
|
'[net]' \
|
||||||
|
'git-fetch-with-cli = true' \
|
||||||
> /root/.cargo/config.toml; \
|
> /root/.cargo/config.toml; \
|
||||||
fi \
|
fi \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -46,6 +60,7 @@ RUN dpkg --add-architecture armhf
|
|||||||
# Install cross-compiler and native build tools
|
# Install cross-compiler and native build tools
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
|
patch \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
cmake \
|
cmake \
|
||||||
nasm \
|
nasm \
|
||||||
@@ -69,6 +84,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
|
|
||||||
# Install ARMv7 development libraries (without VAAPI/X11 for ARM)
|
# Install ARMv7 development libraries (without VAAPI/X11 for ARM)
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libssl3 \
|
||||||
|
libssl3:armhf \
|
||||||
libasound2-dev:armhf \
|
libasound2-dev:armhf \
|
||||||
libv4l-dev:armhf \
|
libv4l-dev:armhf \
|
||||||
libudev-dev:armhf \
|
libudev-dev:armhf \
|
||||||
@@ -77,20 +94,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libdrm-dev:armhf \
|
libdrm-dev:armhf \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install newer V4L2 headers for v4l2r bindgen
|
|
||||||
RUN mkdir -p /opt/v4l2-headers \
|
|
||||||
&& wget -q https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${LINUX_HEADERS_VERSION}.tar.xz -O /tmp/linux-headers.tar.xz \
|
|
||||||
&& if [ -n "$LINUX_HEADERS_SHA256" ]; then echo "$LINUX_HEADERS_SHA256 /tmp/linux-headers.tar.xz" | sha256sum -c -; fi \
|
|
||||||
&& tar -xf /tmp/linux-headers.tar.xz -C /tmp \
|
|
||||||
&& cd /tmp/linux-${LINUX_HEADERS_VERSION} \
|
|
||||||
&& make ARCH=arm headers_install INSTALL_HDR_PATH=/opt/v4l2-headers \
|
|
||||||
&& rm -rf /tmp/linux-${LINUX_HEADERS_VERSION} /tmp/linux-headers.tar.xz
|
|
||||||
|
|
||||||
ENV V4L2R_VIDEODEV2_H_PATH=/opt/v4l2-headers/include
|
|
||||||
|
|
||||||
# Build static libjpeg-turbo from source (cross-compile for ARMv7)
|
# Build static libjpeg-turbo from source (cross-compile for ARMv7)
|
||||||
RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libjpeg-turbo \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/libjpeg-turbo \
|
||||||
&& cd /tmp/libjpeg-turbo \
|
&& cd /tmp/libjpeg-turbo \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/libjpeg-turbo/libjpeg-turbo.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/${LIBJPEG_TURBO_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& mkdir build && cd build \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
||||||
@@ -104,8 +115,13 @@ RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libj
|
|||||||
&& rm -rf /tmp/libjpeg-turbo
|
&& rm -rf /tmp/libjpeg-turbo
|
||||||
|
|
||||||
# Build static libyuv from source (cross-compile for ARMv7)
|
# Build static libyuv from source (cross-compile for ARMv7)
|
||||||
RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
RUN git init /tmp/libyuv \
|
||||||
&& cd /tmp/libyuv \
|
&& cd /tmp/libyuv \
|
||||||
|
&& github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/lemenkov/libyuv" \
|
||||||
|
&& git fetch --depth 1 origin ${LIBYUV_REV} \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& mkdir build && cd build \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
||||||
@@ -123,8 +139,13 @@ RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
|||||||
&& rm -rf /tmp/libyuv
|
&& rm -rf /tmp/libyuv
|
||||||
|
|
||||||
# Build static libvpx from source (cross-compile for ARMv7)
|
# Build static libvpx from source (cross-compile for ARMv7)
|
||||||
RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/libvpx \
|
||||||
&& cd /tmp/libvpx \
|
&& cd /tmp/libvpx \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/webmproject/libvpx.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/v${LIBVPX_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& export CC=arm-linux-gnueabihf-gcc \
|
&& export CC=arm-linux-gnueabihf-gcc \
|
||||||
&& export CXX=arm-linux-gnueabihf-g++ \
|
&& export CXX=arm-linux-gnueabihf-g++ \
|
||||||
&& export LD=arm-linux-gnueabihf-ld \
|
&& export LD=arm-linux-gnueabihf-ld \
|
||||||
@@ -141,7 +162,9 @@ RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
|||||||
&& rm -rf /tmp/libvpx
|
&& rm -rf /tmp/libvpx
|
||||||
|
|
||||||
# Build static libx264 from source (cross-compile for ARMv7)
|
# Build static libx264 from source (cross-compile for ARMv7)
|
||||||
RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git clone --depth 1 "${github_prefix}https://github.com/mirror/x264.git" /tmp/x264 \
|
||||||
&& cd /tmp/x264 \
|
&& cd /tmp/x264 \
|
||||||
&& export CC=arm-linux-gnueabihf-gcc \
|
&& export CC=arm-linux-gnueabihf-gcc \
|
||||||
&& export AR=arm-linux-gnueabihf-ar \
|
&& export AR=arm-linux-gnueabihf-ar \
|
||||||
@@ -158,12 +181,18 @@ RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
|||||||
&& rm -rf /tmp/x264
|
&& rm -rf /tmp/x264
|
||||||
|
|
||||||
# Build static libx265 from source (cross-compile for ARMv7)
|
# Build static libx265 from source (cross-compile for ARMv7)
|
||||||
RUN git clone --depth 1 https://bitbucket.org/multicoreware/x265_git /tmp/x265 \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/x265 \
|
||||||
&& cd /tmp/x265 \
|
&& cd /tmp/x265 \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/videolan/x265.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/${X265_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& cd source \
|
&& cd source \
|
||||||
&& mkdir -p build \
|
&& mkdir -p build \
|
||||||
&& cd build \
|
&& cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_ASM_NASM_FLAGS="-w-macro-params-legacy" \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
-DCMAKE_INSTALL_PREFIX=/usr/arm-linux-gnueabihf \
|
||||||
-DCMAKE_SYSTEM_NAME=Linux \
|
-DCMAKE_SYSTEM_NAME=Linux \
|
||||||
-DCMAKE_SYSTEM_PROCESSOR=arm \
|
-DCMAKE_SYSTEM_PROCESSOR=arm \
|
||||||
@@ -193,12 +222,15 @@ Cflags: -I\${includedir}
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Build static libopus from source (cross-compile for ARMv7)
|
# Build static libopus from source (cross-compile for ARMv7)
|
||||||
RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& wget -O /tmp/opus.tar.gz "${github_prefix}https://github.com/xiph/opus/releases/download/v${OPUS_VERSION}/opus-${OPUS_VERSION}.tar.gz" \
|
||||||
|
&& tar -xzf /tmp/opus.tar.gz -C /tmp \
|
||||||
|
&& mv "/tmp/opus-${OPUS_VERSION}" /tmp/opus \
|
||||||
&& cd /tmp/opus \
|
&& cd /tmp/opus \
|
||||||
&& export CC=arm-linux-gnueabihf-gcc \
|
&& export CC=arm-linux-gnueabihf-gcc \
|
||||||
&& export AR=arm-linux-gnueabihf-ar \
|
&& export AR=arm-linux-gnueabihf-ar \
|
||||||
&& export RANLIB=arm-linux-gnueabihf-ranlib \
|
&& export RANLIB=arm-linux-gnueabihf-ranlib \
|
||||||
&& ./autogen.sh \
|
|
||||||
&& ./configure \
|
&& ./configure \
|
||||||
--prefix=/usr/arm-linux-gnueabihf \
|
--prefix=/usr/arm-linux-gnueabihf \
|
||||||
--host=arm-linux-gnueabihf \
|
--host=arm-linux-gnueabihf \
|
||||||
@@ -206,13 +238,24 @@ RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
|||||||
--disable-doc \
|
--disable-doc \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf /tmp/opus
|
&& rm -rf /tmp/opus /tmp/opus.tar.gz
|
||||||
|
|
||||||
|
# Reject raw formats smaller than the requested frame before any V4L2 upload.
|
||||||
|
COPY build/cross/patches/0001-v4l2-m2m-reject-undersized-encoder-format.patch /tmp/ffmpeg-v4l2-size.patch
|
||||||
|
|
||||||
# Download and build FFmpeg with RKMPP support
|
# Download and build FFmpeg with RKMPP support
|
||||||
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||||
&& wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz \
|
&& github_prefix="" \
|
||||||
&& tar -xzf ffmpeg.tar.gz \
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
&& cd ffmpeg \
|
&& git clone --depth 1 --branch ${RKMPP_BRANCH} "${github_prefix}https://github.com/nyanmisaka/mpp.git" rkmpp \
|
||||||
|
&& git clone --depth 1 --branch ${RKRGA_BRANCH} "${github_prefix}https://github.com/nyanmisaka/rk-mirrors.git" rkrga \
|
||||||
|
&& git init ffmpeg-rockchip \
|
||||||
|
&& cd ffmpeg-rockchip \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/nyanmisaka/ffmpeg-rockchip.git" \
|
||||||
|
&& git fetch --depth 1 origin ${FFMPEG_ROCKCHIP_REV} \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
|
&& patch -p1 < /tmp/ffmpeg-v4l2-size.patch \
|
||||||
|
&& cd .. \
|
||||||
# Build RKMPP
|
# Build RKMPP
|
||||||
&& mkdir -p rkmpp/build && cd rkmpp/build \
|
&& mkdir -p rkmpp/build && cd rkmpp/build \
|
||||||
&& cmake .. \
|
&& cmake .. \
|
||||||
@@ -227,6 +270,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/arm-linux-gnueabihf/lib/pkgconfig/rockchip_mpp.pc \
|
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/arm-linux-gnueabihf/lib/pkgconfig/rockchip_mpp.pc \
|
||||||
|
&& rm -f /usr/arm-linux-gnueabihf/lib/librockchip_mpp.so* \
|
||||||
&& cd ../.. \
|
&& cd ../.. \
|
||||||
# Build RKRGA - create cross file for meson
|
# Build RKRGA - create cross file for meson
|
||||||
&& echo '[binaries]' > /tmp/armhf-cross.txt \
|
&& echo '[binaries]' > /tmp/armhf-cross.txt \
|
||||||
@@ -254,6 +298,7 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
&& ar rcs /usr/arm-linux-gnueabihf/lib/librga.a $(find build -name '*.o') \
|
&& ar rcs /usr/arm-linux-gnueabihf/lib/librga.a $(find build -name '*.o') \
|
||||||
&& ranlib /usr/arm-linux-gnueabihf/lib/librga.a \
|
&& ranlib /usr/arm-linux-gnueabihf/lib/librga.a \
|
||||||
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/arm-linux-gnueabihf/lib/pkgconfig/librga.pc \
|
&& sed -i 's/^Libs:.*$/& -lstdc++ -lm -lpthread/' /usr/arm-linux-gnueabihf/lib/pkgconfig/librga.pc \
|
||||||
|
&& rm -f /usr/arm-linux-gnueabihf/lib/librga.so* \
|
||||||
&& cd .. \
|
&& cd .. \
|
||||||
# Create pkg-config wrapper for cross-compilation
|
# Create pkg-config wrapper for cross-compilation
|
||||||
&& echo '#!/bin/sh' > /tmp/armhf-pkg-config \
|
&& echo '#!/bin/sh' > /tmp/armhf-pkg-config \
|
||||||
@@ -343,8 +388,8 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
|
|
||||||
# Add Rust target
|
# Add Rust target
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& rustup target add armv7-unknown-linux-gnueabihf
|
&& rustup target add armv7-unknown-linux-gnueabihf
|
||||||
|
|
||||||
@@ -360,7 +405,7 @@ ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc \
|
|||||||
LIBYUV_STATIC=1 \
|
LIBYUV_STATIC=1 \
|
||||||
OPUS_STATIC=1 \
|
OPUS_STATIC=1 \
|
||||||
PKG_CONFIG_ALL_STATIC=1 \
|
PKG_CONFIG_ALL_STATIC=1 \
|
||||||
RUSTFLAGS="-C linker=arm-linux-gnueabihf-gcc"
|
RUSTFLAGS="-C linker=arm-linux-gnueabihf-gcc -C link-arg=-Wl,--allow-multiple-definition"
|
||||||
|
|
||||||
# Default command
|
# Default command
|
||||||
CMD ["bash"]
|
CMD ["bash"]
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
# Cross-compilation image for x86_64 based on Debian 11
|
# Cross-compilation image for x86_64 based on Debian 12
|
||||||
# Build on Debian 11 (GLIBC 2.31) for maximum runtime compatibility
|
# Build on Debian 12 (GLIBC 2.36) to match the runtime images
|
||||||
|
|
||||||
FROM debian:11
|
ARG DEBIAN_IMAGE=debian:12
|
||||||
|
FROM ${DEBIAN_IMAGE}
|
||||||
|
|
||||||
# Linux headers used by v4l2r bindgen
|
|
||||||
ARG LINUX_HEADERS_VERSION=6.6
|
|
||||||
ARG LINUX_HEADERS_SHA256=
|
|
||||||
ARG CHINAMIRRO=0
|
ARG CHINAMIRRO=0
|
||||||
|
ARG GH_PROXY=https://gh-proxy.com/
|
||||||
|
ARG RUSTUP_DIST_SERVER_CN=https://mirrors.ustc.edu.cn/rust-static
|
||||||
|
ARG RUSTUP_UPDATE_ROOT_CN=https://mirrors.ustc.edu.cn/rust-static/rustup
|
||||||
|
ARG CARGO_INDEX_CN=https://mirrors.ustc.edu.cn/crates.io-index
|
||||||
|
ARG CARGO_REGISTRY_CN=sparse+https://mirrors.ustc.edu.cn/crates.io-index/
|
||||||
|
ARG LIBJPEG_TURBO_VERSION=3.1.4.1
|
||||||
|
ARG LIBYUV_REV=957f295ea946cbbd13fcfc46e7066f2efa801233
|
||||||
|
ARG LIBVPX_VERSION=1.16.0
|
||||||
|
ARG X265_VERSION=3.4
|
||||||
|
ARG OPUS_VERSION=1.5.2
|
||||||
|
ARG FFMPEG_ROCKCHIP_REV=40c412daccf08164493da0de990eb99a8948116b
|
||||||
|
|
||||||
# Optionally use Tsinghua mirrors for builds in China.
|
# Optionally use China mirrors for builds in China.
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
sed -i \
|
sed -i -E \
|
||||||
-e 's|http://deb.debian.org/debian|http://mirrors.tuna.tsinghua.edu.cn/debian|g' \
|
-e 's@http://deb.debian.org/debian([[:space:]]|$)@http://mirrors.tuna.tsinghua.edu.cn/debian\1@g' \
|
||||||
-e 's|http://security.debian.org/debian-security|http://mirrors.tuna.tsinghua.edu.cn/debian-security|g' \
|
/etc/apt/sources.list.d/debian.sources; \
|
||||||
/etc/apt/sources.list; \
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install Rust toolchain
|
# Install Rust toolchain
|
||||||
@@ -21,19 +29,23 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \
|
||||||
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
&& if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
mkdir -p /root/.cargo; \
|
mkdir -p /root/.cargo; \
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'[source.crates-io]' \
|
'[source.crates-io]' \
|
||||||
"replace-with = 'tuna'" \
|
"replace-with = 'cn-mirror-sparse'" \
|
||||||
'[source.tuna]' \
|
'[source.cn-mirror]' \
|
||||||
'registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_INDEX_CN}'" \
|
||||||
'[registries.tuna]' \
|
'[source.cn-mirror-sparse]' \
|
||||||
'index = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"' \
|
"registry = '${CARGO_REGISTRY_CN}'" \
|
||||||
|
'[registries.cn-mirror]' \
|
||||||
|
"index = '${CARGO_INDEX_CN}'" \
|
||||||
|
'[net]' \
|
||||||
|
'git-fetch-with-cli = true' \
|
||||||
> /root/.cargo/config.toml; \
|
> /root/.cargo/config.toml; \
|
||||||
fi \
|
fi \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -76,20 +88,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libxdmcp-dev \
|
libxdmcp-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install newer V4L2 headers for v4l2r bindgen
|
|
||||||
RUN mkdir -p /opt/v4l2-headers \
|
|
||||||
&& wget -q https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-${LINUX_HEADERS_VERSION}.tar.xz -O /tmp/linux-headers.tar.xz \
|
|
||||||
&& if [ -n "$LINUX_HEADERS_SHA256" ]; then echo "$LINUX_HEADERS_SHA256 /tmp/linux-headers.tar.xz" | sha256sum -c -; fi \
|
|
||||||
&& tar -xf /tmp/linux-headers.tar.xz -C /tmp \
|
|
||||||
&& cd /tmp/linux-${LINUX_HEADERS_VERSION} \
|
|
||||||
&& make ARCH=x86 headers_install INSTALL_HDR_PATH=/opt/v4l2-headers \
|
|
||||||
&& rm -rf /tmp/linux-${LINUX_HEADERS_VERSION} /tmp/linux-headers.tar.xz
|
|
||||||
|
|
||||||
ENV V4L2R_VIDEODEV2_H_PATH=/opt/v4l2-headers/include
|
|
||||||
|
|
||||||
# Build static libjpeg-turbo from source (needed by libyuv)
|
# Build static libjpeg-turbo from source (needed by libyuv)
|
||||||
RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libjpeg-turbo \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/libjpeg-turbo \
|
||||||
&& cd /tmp/libjpeg-turbo \
|
&& cd /tmp/libjpeg-turbo \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/libjpeg-turbo/libjpeg-turbo.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/${LIBJPEG_TURBO_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& mkdir build && cd build \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
-DCMAKE_INSTALL_PREFIX=/usr/local \
|
-DCMAKE_INSTALL_PREFIX=/usr/local \
|
||||||
@@ -100,8 +106,13 @@ RUN git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo /tmp/libj
|
|||||||
&& rm -rf /tmp/libjpeg-turbo
|
&& rm -rf /tmp/libjpeg-turbo
|
||||||
|
|
||||||
# Build static libyuv from source (uses libjpeg-turbo headers)
|
# Build static libyuv from source (uses libjpeg-turbo headers)
|
||||||
RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
RUN git init /tmp/libyuv \
|
||||||
&& cd /tmp/libyuv \
|
&& cd /tmp/libyuv \
|
||||||
|
&& github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/lemenkov/libyuv" \
|
||||||
|
&& git fetch --depth 1 origin ${LIBYUV_REV} \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& mkdir build && cd build \
|
&& mkdir build && cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||||
-DJPEG_FOUND=TRUE \
|
-DJPEG_FOUND=TRUE \
|
||||||
@@ -114,17 +125,25 @@ RUN git clone --depth 1 https://github.com/lemenkov/libyuv /tmp/libyuv \
|
|||||||
&& rm -rf /tmp/libyuv
|
&& rm -rf /tmp/libyuv
|
||||||
|
|
||||||
# Build static libvpx from source
|
# Build static libvpx from source
|
||||||
RUN git clone --depth 1 https://github.com/webmproject/libvpx /tmp/libvpx \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/libvpx \
|
||||||
&& cd /tmp/libvpx \
|
&& cd /tmp/libvpx \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/webmproject/libvpx.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/v${LIBVPX_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& ./configure \
|
&& ./configure \
|
||||||
--enable-static --disable-shared --enable-pic \
|
--enable-static --disable-shared --enable-pic \
|
||||||
--disable-examples --disable-tools --disable-docs \
|
--disable-examples --disable-tools --disable-docs \
|
||||||
|
--disable-unit-tests \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf /tmp/libvpx
|
&& rm -rf /tmp/libvpx
|
||||||
|
|
||||||
# Build static libx264 from source
|
# Build static libx264 from source
|
||||||
RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git clone --depth 1 "${github_prefix}https://github.com/mirror/x264.git" /tmp/x264 \
|
||||||
&& cd /tmp/x264 \
|
&& cd /tmp/x264 \
|
||||||
&& ./configure --enable-static --disable-cli \
|
&& ./configure --enable-static --disable-cli \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
@@ -132,12 +151,18 @@ RUN git clone --depth 1 https://code.videolan.org/videolan/x264.git /tmp/x264 \
|
|||||||
&& rm -rf /tmp/x264
|
&& rm -rf /tmp/x264
|
||||||
|
|
||||||
# Build static libx265 from source
|
# Build static libx265 from source
|
||||||
RUN git clone --depth 1 https://bitbucket.org/multicoreware/x265_git /tmp/x265 \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& git init /tmp/x265 \
|
||||||
&& cd /tmp/x265 \
|
&& cd /tmp/x265 \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/videolan/x265.git" \
|
||||||
|
&& git fetch --depth 1 origin "refs/tags/${X265_VERSION}" \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& cd source \
|
&& cd source \
|
||||||
&& mkdir -p build \
|
&& mkdir -p build \
|
||||||
&& cd build \
|
&& cd build \
|
||||||
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_ASM_NASM_FLAGS="-w-macro-params-legacy" \
|
||||||
-DENABLE_SHARED=OFF \
|
-DENABLE_SHARED=OFF \
|
||||||
-DENABLE_CLI=OFF \
|
-DENABLE_CLI=OFF \
|
||||||
-DBUILD_SHARED_LIBS=OFF \
|
-DBUILD_SHARED_LIBS=OFF \
|
||||||
@@ -162,21 +187,28 @@ Cflags: -I\${includedir}
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Build static libopus from source
|
# Build static libopus from source
|
||||||
RUN git clone --depth 1 https://github.com/xiph/opus /tmp/opus \
|
RUN github_prefix="" \
|
||||||
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
|
&& wget -O /tmp/opus.tar.gz "${github_prefix}https://github.com/xiph/opus/releases/download/v${OPUS_VERSION}/opus-${OPUS_VERSION}.tar.gz" \
|
||||||
|
&& tar -xzf /tmp/opus.tar.gz -C /tmp \
|
||||||
|
&& mv "/tmp/opus-${OPUS_VERSION}" /tmp/opus \
|
||||||
&& cd /tmp/opus \
|
&& cd /tmp/opus \
|
||||||
&& ./autogen.sh \
|
|
||||||
&& ./configure \
|
&& ./configure \
|
||||||
--enable-static --disable-shared \
|
--enable-static --disable-shared \
|
||||||
--disable-doc \
|
--disable-doc \
|
||||||
&& make -j$(nproc) \
|
&& make -j$(nproc) \
|
||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf /tmp/opus
|
&& rm -rf /tmp/opus /tmp/opus.tar.gz
|
||||||
|
|
||||||
# Download and build FFmpeg with minimal configuration for encoding only
|
# Download and build FFmpeg with minimal configuration for encoding only
|
||||||
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
||||||
&& wget -q https://files.mofeng.run/src/image/other/ffmpeg.tar.gz \
|
&& github_prefix="" \
|
||||||
&& tar -xzf ffmpeg.tar.gz \
|
&& if [ "$CHINAMIRRO" = "1" ]; then github_prefix="${GH_PROXY%/}/"; fi \
|
||||||
&& cd ffmpeg/ffmpeg-rockchip \
|
&& git init ffmpeg-rockchip \
|
||||||
|
&& cd ffmpeg-rockchip \
|
||||||
|
&& git remote add origin "${github_prefix}https://github.com/nyanmisaka/ffmpeg-rockchip.git" \
|
||||||
|
&& git fetch --depth 1 origin ${FFMPEG_ROCKCHIP_REV} \
|
||||||
|
&& git checkout --detach FETCH_HEAD \
|
||||||
&& ./configure \
|
&& ./configure \
|
||||||
--enable-gpl \
|
--enable-gpl \
|
||||||
--enable-version3 \
|
--enable-version3 \
|
||||||
@@ -249,8 +281,8 @@ RUN mkdir -p /tmp/ffmpeg-build && cd /tmp/ffmpeg-build \
|
|||||||
|
|
||||||
# Add Rust target
|
# Add Rust target
|
||||||
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
RUN if [ "$CHINAMIRRO" = "1" ]; then \
|
||||||
export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup; \
|
export RUSTUP_DIST_SERVER=${RUSTUP_DIST_SERVER_CN}; \
|
||||||
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup; \
|
export RUSTUP_UPDATE_ROOT=${RUSTUP_UPDATE_ROOT_CN}; \
|
||||||
fi \
|
fi \
|
||||||
&& rustup target add x86_64-unknown-linux-gnu
|
&& rustup target add x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
--- a/libavcodec/v4l2_m2m_enc.c
|
||||||
|
+++ b/libavcodec/v4l2_m2m_enc.c
|
||||||
|
@@ -338,6 +338,7 @@
|
||||||
|
V4L2m2mPriv *priv = avctx->priv_data;
|
||||||
|
enum AVPixelFormat pix_fmt_output;
|
||||||
|
uint32_t v4l2_fmt_output;
|
||||||
|
+ unsigned int output_width, output_height;
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
ret = ff_v4l2_m2m_create_context(priv, &s);
|
||||||
|
@@ -370,6 +371,25 @@
|
||||||
|
v4l2_fmt_output = output->format.fmt.pix_mp.pixelformat;
|
||||||
|
else
|
||||||
|
v4l2_fmt_output = output->format.fmt.pix.pixelformat;
|
||||||
|
+
|
||||||
|
+ /* Drivers may clamp the requested raw format to their supported size.
|
||||||
|
+ * The software-frame upload still uses avctx/frame dimensions, so a
|
||||||
|
+ * smaller negotiated format would overrun the mapped output buffers.
|
||||||
|
+ * Allow upward alignment, but reject frames that do not fit.
|
||||||
|
+ */
|
||||||
|
+ if (V4L2_TYPE_IS_MULTIPLANAR(output->type)) {
|
||||||
|
+ output_width = output->format.fmt.pix_mp.width;
|
||||||
|
+ output_height = output->format.fmt.pix_mp.height;
|
||||||
|
+ } else {
|
||||||
|
+ output_width = output->format.fmt.pix.width;
|
||||||
|
+ output_height = output->format.fmt.pix.height;
|
||||||
|
+ }
|
||||||
|
+ if (output_width < avctx->width || output_height < avctx->height) {
|
||||||
|
+ av_log(avctx, AV_LOG_ERROR,
|
||||||
|
+ "Encoder cannot fit requested %dx%d frame in negotiated %ux%u format\n",
|
||||||
|
+ avctx->width, avctx->height, output_width, output_height);
|
||||||
|
+ return AVERROR(EINVAL);
|
||||||
|
+ }
|
||||||
|
|
||||||
|
pix_fmt_output = ff_v4l2_format_v4l2_to_avfmt(v4l2_fmt_output, AV_CODEC_ID_RAWVIDEO);
|
||||||
|
if (pix_fmt_output != avctx->pix_fmt) {
|
||||||
@@ -6,6 +6,7 @@ Maintainer: SilentWind <admin@mofeng.run>
|
|||||||
Package: one-kvm
|
Package: one-kvm
|
||||||
Architecture: {arch}
|
Architecture: {arch}
|
||||||
Depends: ${{auto}}, ca-certificates{distsuffix}
|
Depends: ${{auto}}, ca-certificates{distsuffix}
|
||||||
|
Recommends: bluez
|
||||||
Description: A open and lightweight IP-KVM solution written in Rust
|
Description: A open and lightweight IP-KVM solution written in Rust
|
||||||
Enables BIOS-level remote management of servers and workstations.
|
Enables BIOS-level remote management of servers and workstations.
|
||||||
.
|
.
|
||||||
@@ -17,6 +18,7 @@ Description: A open and lightweight IP-KVM solution written in Rust
|
|||||||
* Hardware-accelerated video encoding (VAAPI, QSV, RKMPP)
|
* Hardware-accelerated video encoding (VAAPI, QSV, RKMPP)
|
||||||
* WebRTC and MJPEG streaming with low latency
|
* WebRTC and MJPEG streaming with low latency
|
||||||
* USB HID emulation via OTG gadget
|
* USB HID emulation via OTG gadget
|
||||||
|
* Classic Bluetooth keyboard and mouse via BlueZ
|
||||||
* Mass storage device for ISO/IMG mounting
|
* Mass storage device for ISO/IMG mounting
|
||||||
* ATX power control via GPIO or USB relay
|
* ATX power control via GPIO or USB relay
|
||||||
Homepage: https://github.com/mofeng-git/One-KVM
|
Homepage: https://github.com/mofeng-git/One-KVM
|
||||||
|
|||||||
@@ -1,16 +1,130 @@
|
|||||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||||
Upstream-Name: one-kvm
|
Upstream-Name: one-kvm
|
||||||
Source: https://github.com/mofeng-git/one-KVM
|
Upstream-Contact: https://github.com/mofeng-git/One-KVM/issues
|
||||||
|
Source: https://github.com/mofeng-git/One-KVM
|
||||||
|
|
||||||
Files: *
|
Files: *
|
||||||
Copyright: 2025 One-KVM contributors
|
Copyright: 2025 One-KVM contributors
|
||||||
License: GPL-2.0
|
License: AGPL-3.0-only
|
||||||
|
|
||||||
|
Files: protos/*
|
||||||
|
Copyright: the RustDesk contributors
|
||||||
|
2025 One-KVM contributors
|
||||||
|
License: AGPL-3.0-only
|
||||||
|
Comment:
|
||||||
|
Protocol definition files derived from RustDesk
|
||||||
|
(https://github.com/rustdesk/rustdesk). Modified for One-KVM.
|
||||||
|
See protos/NOTICE for the full attribution.
|
||||||
|
|
||||||
|
Files: src/rustdesk/*
|
||||||
|
Copyright: the RustDesk contributors
|
||||||
|
2025 One-KVM contributors
|
||||||
|
License: AGPL-3.0-only
|
||||||
|
Comment:
|
||||||
|
Code derived from the RustDesk project, modified for One-KVM.
|
||||||
|
See src/rustdesk/NOTICE for the full attribution.
|
||||||
|
|
||||||
|
Files: libs/hwcodec/*
|
||||||
|
Copyright: the RustDesk contributors
|
||||||
|
2025 One-KVM contributors
|
||||||
|
License: AGPL-3.0-only
|
||||||
|
Comment:
|
||||||
|
Derived from the RustDesk hwcodec component. See libs/hwcodec/NOTICE.
|
||||||
|
|
||||||
|
Files: libs/v4l2r/*
|
||||||
|
Copyright: 2023-2024 Andrei Lytkin, Daniel Almeida, Collabora Ltd. and other v4l2r contributors
|
||||||
|
License: Expat
|
||||||
|
|
||||||
Files: libs/ventoy-img-rs/resources/*
|
Files: libs/ventoy-img-rs/resources/*
|
||||||
Copyright: Ventoy contributors
|
Copyright: Ventoy contributors
|
||||||
License: GPL-2.0
|
License: GPL-3.0+
|
||||||
|
Comment:
|
||||||
|
Pre-built Ventoy boot files (boot.img, core.img.xz, ventoy.disk.img.xz).
|
||||||
|
Ventoy upstream: https://github.com/ventoy/Ventoy, licensed GPL-3.0-or-later.
|
||||||
|
|
||||||
License: GPL-2.0
|
Files: res/vcpkg/libyuv/*
|
||||||
|
Copyright: The LibYuv Project Authors
|
||||||
|
License: BSD-3-Clause
|
||||||
|
|
||||||
|
Files: third-party/ffmpeg
|
||||||
|
Copyright: FFmpeg developers and contributors
|
||||||
|
License: GPL-2.0+
|
||||||
|
Comment:
|
||||||
|
FFmpeg is built with --enable-gpl --enable-version3 and linked against
|
||||||
|
x264/x265; the resulting build is GPL-compatible with the AGPL-3.0 main
|
||||||
|
program. Upstream: https://ffmpeg.org/
|
||||||
|
The Linux builds use nyanmisaka/ffmpeg-rockchip at the commit pinned by
|
||||||
|
FFMPEG_ROCKCHIP_REV in build/cross/Dockerfile.*.
|
||||||
|
|
||||||
|
Files: third-party/x264
|
||||||
|
Copyright: x264 LLC and x264 contributors
|
||||||
|
License: GPL-2.0+
|
||||||
|
Comment: Upstream: https://www.videolan.org/developers/x264.html
|
||||||
|
|
||||||
|
Files: third-party/x265
|
||||||
|
Copyright: MulticoreWare, Inc. and x265 contributors
|
||||||
|
License: GPL-2.0+
|
||||||
|
Comment: Upstream: https://www.x265.org/
|
||||||
|
|
||||||
|
Files: third-party/libvpx
|
||||||
|
Copyright: The WebM Project authors
|
||||||
|
License: BSD-3-Clause
|
||||||
|
Comment: Upstream: https://www.webmproject.org/code/
|
||||||
|
|
||||||
|
Files: third-party/libopus
|
||||||
|
Copyright: Opus contributors
|
||||||
|
License: BSD-3-Clause
|
||||||
|
Comment: Upstream: https://opus-codec.org/
|
||||||
|
|
||||||
|
Files: third-party/libjpeg-turbo
|
||||||
|
Copyright: D. R. Commander and other libjpeg-turbo contributors
|
||||||
|
License: BSD-3-Clause
|
||||||
|
Comment: Upstream: https://libjpeg-turbo.org/
|
||||||
|
|
||||||
|
Files: third-party/libsodium
|
||||||
|
Copyright: 2013-2025 Frank Denis
|
||||||
|
License: Expat
|
||||||
|
Comment: Upstream: https://libsodium.gitbook.io/
|
||||||
|
|
||||||
|
Files: third-party/sqlite
|
||||||
|
Copyright: 1994-2010 Hipp, Wyrick & Company, Inc.
|
||||||
|
License: SQLite-public-domain
|
||||||
|
Comment: Upstream: https://www.sqlite.org/
|
||||||
|
|
||||||
|
License: AGPL-3.0-only
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License.
|
||||||
|
.
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
.
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
.
|
||||||
|
On Debian systems, the full text of the GNU Affero General Public License
|
||||||
|
version 3 can be found in the file `/usr/share/common-licenses/AGPL-3'.
|
||||||
|
|
||||||
|
License: GPL-3.0+
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
.
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
.
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
.
|
||||||
|
On Debian systems, the full text of the GNU General Public License
|
||||||
|
version 3 can be found in the file `/usr/share/common-licenses/GPL-3'.
|
||||||
|
|
||||||
|
License: GPL-2.0+
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation, either version 2 of the License, or
|
the Free Software Foundation, either version 2 of the License, or
|
||||||
@@ -22,8 +136,61 @@ License: GPL-2.0
|
|||||||
GNU General Public License for more details.
|
GNU General Public License for more details.
|
||||||
.
|
.
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, write to the Free Software
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
.
|
||||||
|
|
||||||
On Debian systems, the full text of the GNU General Public License
|
On Debian systems, the full text of the GNU General Public License
|
||||||
version 2 can be found in the file `/usr/share/common-licenses/GPL-2'.
|
version 2 can be found in the file `/usr/share/common-licenses/GPL-2'.
|
||||||
|
|
||||||
|
License: Expat
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
.
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Software.
|
||||||
|
.
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
License: BSD-3-Clause
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
.
|
||||||
|
1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
.
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
.
|
||||||
|
3. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
.
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
License: SQLite-public-domain
|
||||||
|
The author disclaims copyright to this source code. In place of a legal
|
||||||
|
notice, here is a blessing:
|
||||||
|
.
|
||||||
|
May you do good and not evil.
|
||||||
|
May you find forgiveness for yourself and understand yourself.
|
||||||
|
May you share freely, never taking more than you give.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=One-KVM IP-KVM Service
|
Description=One-KVM IP-KVM Service
|
||||||
Documentation=https://github.com/mofeng-git/One-KVM
|
Documentation=https://github.com/mofeng-git/One-KVM
|
||||||
After=network.target
|
After=network-online.target bluetooth.service
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Build deb packages from pre-compiled binaries
|
# Build deb packages from pre-compiled binaries
|
||||||
# Binaries are compiled once on Debian 11 (GLIBC 2.31) via build-images.sh
|
# Binaries are compiled once on Debian 12 (GLIBC 2.36) via build-images.sh
|
||||||
# This script packages them directly on the host using dpkg-deb
|
# This script packages them directly on the host using dpkg-deb
|
||||||
# Usage: ./build/build-deb.sh [arch]
|
# Usage: ./build/build-deb.sh [arch]
|
||||||
# Example: ./build/build-deb.sh aarch64
|
# Example: ./build/build-deb.sh aarch64
|
||||||
@@ -125,8 +125,8 @@ EOF
|
|||||||
chmod 755 "$PKG_DIR/DEBIAN/prerm"
|
chmod 755 "$PKG_DIR/DEBIAN/prerm"
|
||||||
|
|
||||||
# Create control file
|
# Create control file
|
||||||
BASE_DEPS="libc6 (>= 2.31), libgcc-s1, libstdc++6, libasound2 (>= 1.1), libdrm2 (>= 2.4)"
|
BASE_DEPS="libc6 (>= 2.36), libgcc-s1, libstdc++6, libasound2t64 (>= 1.1) | libasound2 (>= 1.1), libdrm2 (>= 2.4)"
|
||||||
AMD64_DEPS="libva2 (>= 2.0), libva-drm2 (>= 2.10), libva-x11-2 (>= 2.10), libmfx1 (>= 21.1), libx11-6 (>= 1.6), libxcb1 (>= 1.14), i965-va-driver-shaders (>= 2.4), intel-media-va-driver-non-free (>= 21.1)"
|
AMD64_DEPS="libva2 (>= 2.0), libva-drm2 (>= 2.10), libva-x11-2 (>= 2.10), libmfx-gen1.2 (>= 22.0) | libmfx1 (>= 21.1), libx11-6 (>= 1.6), libxcb1 (>= 1.14), i965-va-driver-shaders (>= 2.4), intel-media-va-driver-non-free (>= 21.1)"
|
||||||
DEPS="$BASE_DEPS"
|
DEPS="$BASE_DEPS"
|
||||||
if [ "$DEB_ARCH" = "amd64" ]; then
|
if [ "$DEB_ARCH" = "amd64" ]; then
|
||||||
DEPS="$DEPS, $AMD64_DEPS"
|
DEPS="$DEPS, $AMD64_DEPS"
|
||||||
@@ -139,10 +139,11 @@ Section: admin
|
|||||||
Priority: optional
|
Priority: optional
|
||||||
Architecture: $DEB_ARCH
|
Architecture: $DEB_ARCH
|
||||||
Depends: $DEPS
|
Depends: $DEPS
|
||||||
|
Recommends: bluez
|
||||||
Maintainer: SilentWind <admin@mofeng.run>
|
Maintainer: SilentWind <admin@mofeng.run>
|
||||||
Description: A open and lightweight IP-KVM solution
|
Description: A open and lightweight IP-KVM solution
|
||||||
Enables BIOS-level remote management of servers and workstations.
|
Enables BIOS-level remote management of servers and workstations.
|
||||||
Built on Debian 11, compatible with Debian 11+, Ubuntu 20.04+.
|
Built on Debian 12, compatible with Debian 12+, Ubuntu 24.04+.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Build deb directly on host
|
# Build deb directly on host
|
||||||
|
|||||||
@@ -204,9 +204,21 @@ get_tool_urls() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Download tools for a platform
|
# Download tools for a platform
|
||||||
|
# Set CURL_PROXY=http://host:port to route GitHub downloads through an HTTP proxy
|
||||||
|
# Set GHPROXY_PREFIX=https://ghfast.top/ to prefix GitHub URLs instead
|
||||||
download_tools() {
|
download_tools() {
|
||||||
local platform="$1"
|
local platform="$1"
|
||||||
local staging="$2"
|
local staging="$2"
|
||||||
|
local curl_proxy=()
|
||||||
|
if [ -n "${CURL_PROXY:-}" ]; then
|
||||||
|
curl_proxy=(-x "$CURL_PROXY")
|
||||||
|
fi
|
||||||
|
dl() {
|
||||||
|
# dl <url> <output>
|
||||||
|
local url="$1"
|
||||||
|
[ -n "${GHPROXY_PREFIX:-}" ] && url="${GHPROXY_PREFIX%/}/$url"
|
||||||
|
curl -fsSL "${curl_proxy[@]}" "$url" -o "$2"
|
||||||
|
}
|
||||||
|
|
||||||
get_tool_urls "$platform"
|
get_tool_urls "$platform"
|
||||||
|
|
||||||
@@ -214,14 +226,14 @@ download_tools() {
|
|||||||
|
|
||||||
# ttyd
|
# ttyd
|
||||||
if [ ! -f "$staging/ttyd" ]; then
|
if [ ! -f "$staging/ttyd" ]; then
|
||||||
curl -fsSL "$TTYD_URL" -o "$staging/ttyd"
|
dl "$TTYD_URL" "$staging/ttyd"
|
||||||
chmod +x "$staging/ttyd"
|
chmod +x "$staging/ttyd"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$INCLUDE_THIRD_PARTY" = true ]; then
|
if [ "$INCLUDE_THIRD_PARTY" = true ]; then
|
||||||
# gostc
|
# gostc
|
||||||
if [ ! -f "$staging/gostc" ]; then
|
if [ ! -f "$staging/gostc" ]; then
|
||||||
curl -fsSL "$GOSTC_URL" -o /tmp/gostc.tar.gz
|
dl "$GOSTC_URL" /tmp/gostc.tar.gz
|
||||||
tar -xzf /tmp/gostc.tar.gz -C "$staging"
|
tar -xzf /tmp/gostc.tar.gz -C "$staging"
|
||||||
chmod +x "$staging/gostc"
|
chmod +x "$staging/gostc"
|
||||||
rm /tmp/gostc.tar.gz
|
rm /tmp/gostc.tar.gz
|
||||||
@@ -229,7 +241,7 @@ download_tools() {
|
|||||||
|
|
||||||
# easytier
|
# easytier
|
||||||
if [ ! -f "$staging/easytier-core" ]; then
|
if [ ! -f "$staging/easytier-core" ]; then
|
||||||
curl -fsSL "$EASYTIER_URL" -o /tmp/easytier.zip
|
dl "$EASYTIER_URL" /tmp/easytier.zip
|
||||||
unzip -o /tmp/easytier.zip -d /tmp/easytier
|
unzip -o /tmp/easytier.zip -d /tmp/easytier
|
||||||
cp "/tmp/easytier/$EASYTIER_DIR/easytier-core" "$staging/easytier-core"
|
cp "/tmp/easytier/$EASYTIER_DIR/easytier-core" "$staging/easytier-core"
|
||||||
chmod +x "$staging/easytier-core"
|
chmod +x "$staging/easytier-core"
|
||||||
@@ -311,9 +323,16 @@ build_for_platform() {
|
|||||||
|
|
||||||
local build_cmd="docker buildx build --platform $platform"
|
local build_cmd="docker buildx build --platform $platform"
|
||||||
build_cmd="$build_cmd --build-arg TARGETPLATFORM=$platform"
|
build_cmd="$build_cmd --build-arg TARGETPLATFORM=$platform"
|
||||||
|
if [ -n "${APT_MIRROR:-}" ]; then
|
||||||
|
build_cmd="$build_cmd --build-arg APT_MIRROR=$APT_MIRROR"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$PUSH" = true ]; then
|
if [ "$PUSH" = true ]; then
|
||||||
build_cmd="$build_cmd --push"
|
build_cmd="$build_cmd --push"
|
||||||
|
# Aliyun ACR rejects the OCI artifact manifests used by BuildKit attestations.
|
||||||
|
if [[ "$REGISTRY" == registry.cn-*.aliyuncs.com/* ]]; then
|
||||||
|
build_cmd="$build_cmd --provenance=false --sbom=false"
|
||||||
|
fi
|
||||||
elif [ "$LOAD" = true ]; then
|
elif [ "$LOAD" = true ]; then
|
||||||
build_cmd="$build_cmd --load"
|
build_cmd="$build_cmd --load"
|
||||||
fi
|
fi
|
||||||
|
|||||||
24
docs/macos-drag-compatibility.md
Normal file
24
docs/macos-drag-compatibility.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# macOS 拖拽兼容模式(实验性)
|
||||||
|
|
||||||
|
适用于 OTG 和 CH9329 后端的绝对鼠标输入。遇到目标 Mac 上拖拽只能移动一小段便停止时,可在设置页的 HID 配置中开启「macOS 拖拽兼容模式」。默认关闭。
|
||||||
|
|
||||||
|
配置字段为 `hid.mouse_macos_drag`,旧 PR 的 `ch9329_macos_drag` 字段仍可作为读取别名。OTG 必须同时启用相对鼠标和绝对鼠标接口,否则配置保存会被拒绝。CH9329 同时启用 Linux 兼容开关时,此模式优先。
|
||||||
|
|
||||||
|
## 报告行为
|
||||||
|
|
||||||
|
- 未按键时仍使用绝对坐标定位。
|
||||||
|
- 从绝对输入开始的拖拽:按下使用绝对报告,移动和滚轮使用相对报告,释放时先更新相对按钮状态,再发送绝对按钮释放。
|
||||||
|
- 按键报告的通道保持到所有按钮释放,不随拖拽期间的输入模式切换而改变。
|
||||||
|
- 从相对输入开始的点击、拖拽继续使用相对按钮报告;此选项不修复原生相对模式的点击兼容问题。
|
||||||
|
- 位移从原始 15 位输入坐标换算,累计小数余量,大位移按单包范围分包并保留总量及方向。
|
||||||
|
- 运行时使用当前视频采集尺寸,并在 HID 重载后恢复尺寸;视频尚未提供尺寸时使用配置尺寸。
|
||||||
|
|
||||||
|
## 验证范围与限制
|
||||||
|
|
||||||
|
本次 OTG 扩展及位移换算修改没有 macOS 实机验证。原 PR 作者对旧 CH9329 实现的实机反馈不能视为本版本已经验证。
|
||||||
|
|
||||||
|
本地自动化覆盖位移累计/分包、1080p/4K 换算、往返移动、滚轮、输入模式切换、复位、CH9329 命令队列、OTG 临时文件模拟端点及配置校验。临时文件测试仅验证写出的字节,不验证 USB 枚举或目标系统的事件解释。
|
||||||
|
|
||||||
|
相对位移受 macOS 鼠标速度、加速、显示缩放及多屏布局影响,无法保证与绝对定位逐像素一致。松手报告携带最后的客户端绝对坐标,仍可能出现落点偏移或光标跳位;大位移分包也可能增加低波特率串口延迟。
|
||||||
|
|
||||||
|
后续实机验证应分别覆盖两个后端:窗口/文件/文本拖拽,快速及慢速移动,滚轮和多按钮组合,1080p/4K 与缩放显示,输入模式切换、断开重连、精确落点。异常时关闭此选项恢复默认行为。
|
||||||
16
libs/bluetooth-hid/Cargo.toml
Normal file
16
libs/bluetooth-hid/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "one-kvm-bluetooth-hid"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "AGPL-3.0-only"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bluer = { version = "0.17", features = ["bluetoothd", "l2cap"] }
|
||||||
|
dbus = { version = "0.9", features = ["vendored"] }
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "process"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
dbus-crossroads = "0.5"
|
||||||
|
dbus-tokio = "0.7"
|
||||||
|
libc = "0.2"
|
||||||
|
tracing = "0.1"
|
||||||
49
libs/bluetooth-hid/examples/pairing_probe.rs
Normal file
49
libs/bluetooth-hid/examples/pairing_probe.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
//! Explicit hardware regression: opens a 10-second pairing window without
|
||||||
|
//! removing existing bonds or sending input. Stop the production HID first.
|
||||||
|
use one_kvm_bluetooth_hid::{Action, Config, Peripheral};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), String> {
|
||||||
|
let peripheral = Peripheral::start(Config {
|
||||||
|
adapter: "hci0".into(),
|
||||||
|
name: "One-KVM HID".into(),
|
||||||
|
peer: None,
|
||||||
|
})?;
|
||||||
|
let result = async {
|
||||||
|
for _ in 0..50 {
|
||||||
|
if peripheral.status().initialized {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
let before = peripheral.status();
|
||||||
|
if !before.initialized {
|
||||||
|
return Err(format!("Initialization failed: {:?}", before.error));
|
||||||
|
}
|
||||||
|
if !before
|
||||||
|
.devices
|
||||||
|
.iter()
|
||||||
|
.any(|d| Some(&d.address) == before.peer.as_ref() && d.paired)
|
||||||
|
{
|
||||||
|
return Err("This regression needs an existing bonded computer".into());
|
||||||
|
}
|
||||||
|
peripheral.action(Action::Pair(10)).await?;
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
let open = peripheral.status();
|
||||||
|
println!("open: {}", serde_json::to_string(&open).unwrap());
|
||||||
|
if open.pairing_seconds == 0 {
|
||||||
|
return Err("Existing bond prematurely closed pairing".into());
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||||
|
let expired = peripheral.status();
|
||||||
|
println!("expired: {}", serde_json::to_string(&expired).unwrap());
|
||||||
|
if expired.pairing_seconds != 0 || expired.peer != before.peer {
|
||||||
|
return Err("Window did not expire while preserving the bonded peer".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
let cleanup = peripheral.shutdown().await;
|
||||||
|
result.and(cleanup)
|
||||||
|
}
|
||||||
24
libs/bluetooth-hid/examples/probe.rs
Normal file
24
libs/bluetooth-hid/examples/probe.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
//! Native hardware smoke check. Registers for 12 seconds, sends no input,
|
||||||
|
//! opens no pairing window, then restores the adapter configuration.
|
||||||
|
use one_kvm_bluetooth_hid::{Config, Peripheral};
|
||||||
|
use std::time::Duration;
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), String> {
|
||||||
|
let peripheral = Peripheral::start(Config {
|
||||||
|
adapter: "hci0".into(),
|
||||||
|
name: "One-KVM HID".into(),
|
||||||
|
peer: None,
|
||||||
|
})?;
|
||||||
|
let mut initialized = false;
|
||||||
|
for _ in 0..12 {
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
let status = peripheral.status();
|
||||||
|
initialized |= status.initialized;
|
||||||
|
println!("{}", serde_json::to_string(&status).unwrap());
|
||||||
|
}
|
||||||
|
peripheral.shutdown().await?;
|
||||||
|
if !initialized {
|
||||||
|
return Err("Native BlueZ peripheral never initialized".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
236
libs/bluetooth-hid/src/agent.rs
Normal file
236
libs/bluetooth-hid/src/agent.rs
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
use crate::Shared;
|
||||||
|
use dbus::{channel::MatchingReceiver, message::MatchRule, nonblock::Proxy, Path};
|
||||||
|
use dbus_crossroads::Crossroads;
|
||||||
|
use std::{
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
const PATH: &str = "/org/onekvm/bluetooth/agent";
|
||||||
|
pub struct Agent {
|
||||||
|
connection: Arc<dbus::nonblock::SyncConnection>,
|
||||||
|
task: tokio::task::JoinHandle<()>,
|
||||||
|
}
|
||||||
|
impl Agent {
|
||||||
|
pub async fn register(shared: Arc<Shared>, adapter: String) -> Result<Self, String> {
|
||||||
|
let (resource, connection) =
|
||||||
|
dbus_tokio::connection::new_system_sync().map_err(|e| e.to_string())?;
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let _ = resource.await;
|
||||||
|
});
|
||||||
|
let mut cr = Crossroads::new();
|
||||||
|
let iface = cr.register("org.bluez.Agent1", |b| {
|
||||||
|
b.method("Release", (), (), |_, _: &mut (), ()| Ok(()));
|
||||||
|
b.method("Cancel", (), (), |_, _: &mut (), ()| Ok(()));
|
||||||
|
let state = shared.clone();
|
||||||
|
let name = adapter.clone();
|
||||||
|
b.method(
|
||||||
|
"RequestAuthorization",
|
||||||
|
("device",),
|
||||||
|
(),
|
||||||
|
move |_, _: &mut (), (path,): (Path<'static>,)| {
|
||||||
|
authorize(&state, &name, &path, true)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let state = shared.clone();
|
||||||
|
let name = adapter.clone();
|
||||||
|
b.method(
|
||||||
|
"RequestConfirmation",
|
||||||
|
("device", "passkey"),
|
||||||
|
(),
|
||||||
|
move |_, _: &mut (), (path, _): (Path<'static>, u32)| {
|
||||||
|
authorize(&state, &name, &path, true)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
b.method(
|
||||||
|
"AuthorizeService",
|
||||||
|
("device", "uuid"),
|
||||||
|
(),
|
||||||
|
move |_, _: &mut (), (path, uuid): (Path<'static>, String)| {
|
||||||
|
if !matches!(uuid.as_str(), "00001124-0000-1000-8000-00805f9b34fb") {
|
||||||
|
return Err(dbus::MethodErr::from((
|
||||||
|
"org.bluez.Error.Rejected",
|
||||||
|
"Not an HID service",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
authorize(&shared, &adapter, &path, false)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cr.insert(PATH, &[iface], ());
|
||||||
|
let profile = cr.register("org.bluez.Profile1", |b| {
|
||||||
|
b.method("Release", (), (), |_, _: &mut (), ()| Ok(()));
|
||||||
|
b.method(
|
||||||
|
"RequestDisconnection",
|
||||||
|
("device",),
|
||||||
|
(),
|
||||||
|
|_, _: &mut (), (_device,): (Path<'static>,)| Ok(()),
|
||||||
|
);
|
||||||
|
b.method(
|
||||||
|
"NewConnection",
|
||||||
|
("device", "fd", "properties"),
|
||||||
|
(),
|
||||||
|
|_,
|
||||||
|
_: &mut (),
|
||||||
|
(_device, _fd, _props): (
|
||||||
|
Path<'static>,
|
||||||
|
dbus::arg::OwnedFd,
|
||||||
|
dbus::arg::PropMap,
|
||||||
|
)| {
|
||||||
|
// This profile only publishes SDP. Our L2CAP sockets own both HID channels.
|
||||||
|
Err::<(), _>(dbus::MethodErr::from((
|
||||||
|
"org.bluez.Error.Rejected",
|
||||||
|
"Unexpected profile connection",
|
||||||
|
)))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cr.insert("/org/onekvm/bluetooth/profile", &[profile], ());
|
||||||
|
connection.start_receive(
|
||||||
|
MatchRule::new_method_call(),
|
||||||
|
Box::new(move |msg, conn| {
|
||||||
|
let _ = cr.handle_message(msg, conn);
|
||||||
|
true
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let agent = Self { connection, task };
|
||||||
|
let proxy = Proxy::new(
|
||||||
|
"org.bluez",
|
||||||
|
"/org/bluez",
|
||||||
|
Duration::from_secs(5),
|
||||||
|
agent.connection.clone(),
|
||||||
|
);
|
||||||
|
let result: Result<(), dbus::Error> = proxy
|
||||||
|
.method_call(
|
||||||
|
"org.bluez.AgentManager1",
|
||||||
|
"RegisterAgent",
|
||||||
|
(Path::from(PATH), "NoInputNoOutput"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if let Err(error) = result {
|
||||||
|
return Err(error.to_string());
|
||||||
|
}
|
||||||
|
let result: Result<(), dbus::Error> = proxy
|
||||||
|
.method_call(
|
||||||
|
"org.bluez.AgentManager1",
|
||||||
|
"RequestDefaultAgent",
|
||||||
|
(Path::from(PATH),),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
result.map_err(|e| e.to_string())?;
|
||||||
|
let mut options: dbus::arg::PropMap = std::collections::HashMap::new();
|
||||||
|
options.insert(
|
||||||
|
"ServiceRecord".into(),
|
||||||
|
dbus::arg::Variant(Box::new(crate::protocol::sdp())),
|
||||||
|
);
|
||||||
|
options.insert(
|
||||||
|
"Role".into(),
|
||||||
|
dbus::arg::Variant(Box::new("server".to_string())),
|
||||||
|
);
|
||||||
|
options.insert(
|
||||||
|
"RequireAuthentication".into(),
|
||||||
|
dbus::arg::Variant(Box::new(true)),
|
||||||
|
);
|
||||||
|
options.insert(
|
||||||
|
"RequireAuthorization".into(),
|
||||||
|
dbus::arg::Variant(Box::new(false)),
|
||||||
|
);
|
||||||
|
let result: Result<(), dbus::Error> = proxy
|
||||||
|
.method_call(
|
||||||
|
"org.bluez.ProfileManager1",
|
||||||
|
"RegisterProfile",
|
||||||
|
(
|
||||||
|
Path::from("/org/onekvm/bluetooth/profile"),
|
||||||
|
crate::protocol::HID_UUID,
|
||||||
|
options,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
result.map_err(|e| format!("Register HID SDP: {e}"))?;
|
||||||
|
Ok(agent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for Agent {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.task.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn authorize(
|
||||||
|
shared: &Shared,
|
||||||
|
adapter: &str,
|
||||||
|
path: &str,
|
||||||
|
pairing: bool,
|
||||||
|
) -> Result<(), dbus::MethodErr> {
|
||||||
|
let rejected = || {
|
||||||
|
dbus::MethodErr::from((
|
||||||
|
"org.bluez.Error.Rejected",
|
||||||
|
"Open pairing in One-KVM or select the paired computer",
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let prefix = format!("/org/bluez/{adapter}/dev_");
|
||||||
|
let address = path
|
||||||
|
.strip_prefix(&prefix)
|
||||||
|
.ok_or_else(rejected)?
|
||||||
|
.replace('_', ":");
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
if pairing
|
||||||
|
&& !state
|
||||||
|
.pairing_until
|
||||||
|
.is_some_and(|until| until > Instant::now())
|
||||||
|
{
|
||||||
|
return Err(rejected());
|
||||||
|
}
|
||||||
|
if state.peer.as_ref().is_some_and(|peer| peer != &address) {
|
||||||
|
return Err(rejected());
|
||||||
|
}
|
||||||
|
if state.peer.is_none() {
|
||||||
|
if !pairing || state.bonded_before_pairing.contains(&address) {
|
||||||
|
return Err(rejected());
|
||||||
|
}
|
||||||
|
state.peer = Some(address);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn existing_unrelated_bond_cannot_be_adopted_by_agent_callback() {
|
||||||
|
let shared = Shared::new(None);
|
||||||
|
{
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||||
|
state
|
||||||
|
.bonded_before_pairing
|
||||||
|
.insert("10:6F:D9:66:97:88".into());
|
||||||
|
}
|
||||||
|
assert!(authorize(
|
||||||
|
&shared,
|
||||||
|
"hci0",
|
||||||
|
"/org/bluez/hci0/dev_10_6F_D9_66_97_88",
|
||||||
|
true
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
assert!(shared.state.lock().unwrap().peer.is_none());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn only_pair_during_window_and_pin_first_peer() {
|
||||||
|
let state = Shared::new(None);
|
||||||
|
let first = "/org/bluez/hci0/dev_10_6F_D9_66_97_88";
|
||||||
|
assert!(authorize(&state, "hci0", first, true).is_err());
|
||||||
|
state.state.lock().unwrap().pairing_until = Some(Instant::now() + Duration::from_secs(60));
|
||||||
|
assert!(authorize(&state, "hci0", first, true).is_ok());
|
||||||
|
assert!(authorize(
|
||||||
|
&state,
|
||||||
|
"hci0",
|
||||||
|
"/org/bluez/hci0/dev_10_6F_D9_66_97_89",
|
||||||
|
true
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
assert!(authorize(&state, "hci1", first, true).is_err());
|
||||||
|
state.state.lock().unwrap().pairing_until = None;
|
||||||
|
assert!(authorize(&state, "hci0", first, false).is_ok());
|
||||||
|
assert!(authorize(&state, "hci0", first, true).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
145
libs/bluetooth-hid/src/bonds.rs
Normal file
145
libs/bluetooth-hid/src/bonds.rs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
//! Only bonds explicitly owned by this HID peripheral may be removed.
|
||||||
|
use bluer::{Adapter, Session};
|
||||||
|
use std::{future::Future, pin::Pin};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Bond {
|
||||||
|
pub adapter: String,
|
||||||
|
pub peer: String,
|
||||||
|
pub pending: bool,
|
||||||
|
}
|
||||||
|
pub type Operation<'a, T> = Pin<Box<dyn Future<Output = Result<T, String>> + Send + 'a>>;
|
||||||
|
pub trait BondStore: Send + Sync {
|
||||||
|
fn list(&self) -> Operation<'_, Vec<Bond>>;
|
||||||
|
fn save(&self, bond: Bond) -> Operation<'_, ()>;
|
||||||
|
fn remove(&self, bond: Bond) -> Operation<'_, ()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clean_adapter(store: &dyn BondStore, adapter: &Adapter) -> Result<(), String> {
|
||||||
|
let address = adapter
|
||||||
|
.address()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.to_string();
|
||||||
|
clean_owned(store, &address, |peer| async move {
|
||||||
|
let peer = peer.parse().map_err(|_| "Invalid stored HID peer")?;
|
||||||
|
if adapter
|
||||||
|
.device_addresses()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.contains(&peer)
|
||||||
|
{
|
||||||
|
adapter
|
||||||
|
.remove_device(peer)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clean_owned<F, Fut>(
|
||||||
|
store: &dyn BondStore,
|
||||||
|
address: &str,
|
||||||
|
mut remove: F,
|
||||||
|
) -> Result<(), String>
|
||||||
|
where
|
||||||
|
F: FnMut(String) -> Fut,
|
||||||
|
Fut: Future<Output = Result<(), String>>,
|
||||||
|
{
|
||||||
|
for bond in store
|
||||||
|
.list()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|b| b.pending && b.adapter == address)
|
||||||
|
{
|
||||||
|
remove(bond.peer.clone()).await?;
|
||||||
|
// Keep the tombstone until BlueZ confirms removal; retry safely after interruption.
|
||||||
|
store.remove(bond).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reset(store: &dyn BondStore, explicit: Vec<Bond>) -> Result<(), String> {
|
||||||
|
let session = Session::new().await.map_err(|e| e.to_string())?;
|
||||||
|
let mut owned = store.list().await?;
|
||||||
|
owned.extend(explicit);
|
||||||
|
for mut bond in owned {
|
||||||
|
bond.pending = true;
|
||||||
|
store.save(bond).await?;
|
||||||
|
}
|
||||||
|
for name in session.adapter_names().await.map_err(|e| e.to_string())? {
|
||||||
|
clean_adapter(store, &session.adapter(&name).map_err(|e| e.to_string())?).await?;
|
||||||
|
}
|
||||||
|
// Records for absent adapters remain pending and are cleaned before their next use.
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Memory(Mutex<Vec<Bond>>);
|
||||||
|
impl BondStore for Memory {
|
||||||
|
fn list(&self) -> Operation<'_, Vec<Bond>> {
|
||||||
|
Box::pin(async { Ok(self.0.lock().unwrap().clone()) })
|
||||||
|
}
|
||||||
|
fn save(&self, bond: Bond) -> Operation<'_, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let mut rows = self.0.lock().unwrap();
|
||||||
|
rows.retain(|b| b.adapter != bond.adapter || b.peer != bond.peer);
|
||||||
|
rows.push(bond);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn remove(&self, bond: Bond) -> Operation<'_, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.0.lock().unwrap().retain(|b| b != &bond);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn bond(adapter: &str, peer: &str, pending: bool) -> Bond {
|
||||||
|
Bond {
|
||||||
|
adapter: adapter.into(),
|
||||||
|
peer: peer.into(),
|
||||||
|
pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cleanup_only_removes_owned_pending_bonds_on_the_selected_hardware() {
|
||||||
|
let current = bond("adapter-A", "host-1", true);
|
||||||
|
let absent = bond("adapter-B", "host-2", true);
|
||||||
|
let retained = bond("adapter-A", "host-3", false);
|
||||||
|
let store = Memory(Mutex::new(vec![current, absent.clone(), retained.clone()]));
|
||||||
|
let removed = Mutex::new(Vec::new());
|
||||||
|
clean_owned(&store, "adapter-A", |peer| {
|
||||||
|
removed.lock().unwrap().push(peer);
|
||||||
|
async { Ok(()) }
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(*removed.lock().unwrap(), vec!["host-1"]);
|
||||||
|
assert_eq!(store.list().await.unwrap(), vec![absent, retained]);
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn cleanup_failure_keeps_tombstone_and_is_reported() {
|
||||||
|
let record = bond("adapter-A", "host-1", true);
|
||||||
|
let store = Memory(Mutex::new(vec![record.clone()]));
|
||||||
|
assert_eq!(
|
||||||
|
clean_owned(&store, "adapter-A", |_| async {
|
||||||
|
Err("BlueZ denied removal".into())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_err(),
|
||||||
|
"BlueZ denied removal"
|
||||||
|
);
|
||||||
|
assert_eq!(store.list().await.unwrap(), vec![record]);
|
||||||
|
clean_owned(&store, "adapter-A", |_| async { Ok(()) })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(store.list().await.unwrap().is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
135
libs/bluetooth-hid/src/controller.rs
Normal file
135
libs/bluetooth-hid/src/controller.rs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
//! Exclusive controller ownership. No BLE switching or power cycling.
|
||||||
|
use std::{
|
||||||
|
fs::{File, OpenOptions},
|
||||||
|
os::{fd::AsRawFd, unix::fs::OpenOptionsExt},
|
||||||
|
process::Stdio,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use tokio::process::Command;
|
||||||
|
pub struct Controller {
|
||||||
|
_lock: File,
|
||||||
|
adapter: String,
|
||||||
|
old_class: (u8, u8),
|
||||||
|
old_connectable: bool,
|
||||||
|
}
|
||||||
|
async fn run_once(adapter: &str, args: &[&str]) -> Result<String, String> {
|
||||||
|
let mut cmd = Command::new("btmgmt");
|
||||||
|
cmd.arg("--index")
|
||||||
|
.arg(adapter.trim_start_matches("hci"))
|
||||||
|
.args(args)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.kill_on_drop(true);
|
||||||
|
let mut child = cmd
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("Install bluez/btmgmt: {e}"))?;
|
||||||
|
// BlueZ 5.55's btmgmt exits on stdin EOF, even with a command argument.
|
||||||
|
// Keep a pipe open until it finishes; systemd normally supplies /dev/null.
|
||||||
|
let stdin = child.stdin.take();
|
||||||
|
let result = tokio::time::timeout(Duration::from_secs(5), child.wait_with_output())
|
||||||
|
.await
|
||||||
|
.map_err(|_| "btmgmt timeout")?
|
||||||
|
.map_err(|e| format!("Install bluez/btmgmt: {e}"))?;
|
||||||
|
drop(stdin);
|
||||||
|
let text = String::from_utf8_lossy(&result.stdout).into_owned();
|
||||||
|
if !result.status.success()
|
||||||
|
|| !result.stderr.is_empty()
|
||||||
|
|| text.to_lowercase().contains("failed")
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"btmgmt {}: {text} {}",
|
||||||
|
args.join(" "),
|
||||||
|
String::from_utf8_lossy(&result.stderr)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
async fn run(adapter: &str, args: &[&str]) -> Result<String, String> {
|
||||||
|
// BlueZ updates the EIR/class asynchronously after registering SDP.
|
||||||
|
// Kernel management rejects a simultaneous class change with Busy.
|
||||||
|
for attempt in 0..5 {
|
||||||
|
match run_once(adapter, args).await {
|
||||||
|
Err(error) if error.contains("(Busy)") && attempt < 4 => {
|
||||||
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||||
|
}
|
||||||
|
result => return result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
impl Controller {
|
||||||
|
pub async fn acquire(adapter: &str) -> Result<Self, String> {
|
||||||
|
let lock = OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(false)
|
||||||
|
.mode(0o600)
|
||||||
|
.custom_flags(libc::O_NOFOLLOW)
|
||||||
|
.open(format!("/run/one-kvm-bluetooth-{adapter}.lock"))
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if unsafe { libc::flock(lock.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } != 0 {
|
||||||
|
return Err("Bluetooth adapter already owned by One-KVM".into());
|
||||||
|
}
|
||||||
|
let info = run(adapter, &["info"]).await?;
|
||||||
|
let settings = info
|
||||||
|
.lines()
|
||||||
|
.find_map(|l| l.trim().strip_prefix("current settings:"))
|
||||||
|
.ok_or("Cannot read Bluetooth settings")?;
|
||||||
|
if !settings.split_whitespace().any(|s| s == "br/edr") {
|
||||||
|
return Err(
|
||||||
|
"Classic Bluetooth disabled; enable BR/EDR with btmgmt before using HID".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let class = info
|
||||||
|
.split_whitespace()
|
||||||
|
.skip_while(|s| *s != "class")
|
||||||
|
.nth(1)
|
||||||
|
.ok_or("Missing Bluetooth class")?;
|
||||||
|
let class =
|
||||||
|
u32::from_str_radix(class.trim_start_matches("0x"), 16).map_err(|e| e.to_string())?;
|
||||||
|
Ok(Self {
|
||||||
|
_lock: lock,
|
||||||
|
adapter: adapter.into(),
|
||||||
|
old_class: (((class >> 8) & 0x1f) as u8, (class & 0xfc) as u8),
|
||||||
|
old_connectable: settings.split_whitespace().any(|s| s == "connectable"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub async fn configure(&self) -> Result<(), String> {
|
||||||
|
run(&self.adapter, &["class", "5", "192"]).await?;
|
||||||
|
run(&self.adapter, &["connectable", "on"]).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub async fn restore(&self) -> Result<(), String> {
|
||||||
|
let mut errors = vec![];
|
||||||
|
if let Err(e) = run(
|
||||||
|
&self.adapter,
|
||||||
|
&[
|
||||||
|
"class",
|
||||||
|
&self.old_class.0.to_string(),
|
||||||
|
&self.old_class.1.to_string(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
if let Err(e) = run(
|
||||||
|
&self.adapter,
|
||||||
|
&[
|
||||||
|
"connectable",
|
||||||
|
if self.old_connectable { "on" } else { "off" },
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(errors.join("; "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
964
libs/bluetooth-hid/src/lib.rs
Normal file
964
libs/bluetooth-hid/src/lib.rs
Normal file
@@ -0,0 +1,964 @@
|
|||||||
|
//! Classic Bluetooth HID peripheral: SDP + L2CAP. Linux/BlueZ only.
|
||||||
|
mod agent;
|
||||||
|
pub mod bonds;
|
||||||
|
mod controller;
|
||||||
|
mod protocol;
|
||||||
|
|
||||||
|
use bluer::{
|
||||||
|
l2cap::{Security, SecurityLevel, SeqPacket, SeqPacketListener, Socket, SocketAddr},
|
||||||
|
Adapter, Address, AddressType, Session,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
use tokio::sync::{mpsc, oneshot, watch, Mutex as AsyncMutex};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub adapter: String,
|
||||||
|
pub name: String,
|
||||||
|
pub peer: Option<String>,
|
||||||
|
}
|
||||||
|
impl Config {
|
||||||
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
|
if !self
|
||||||
|
.adapter
|
||||||
|
.strip_prefix("hci")
|
||||||
|
.is_some_and(|n| !n.is_empty() && n.bytes().all(|c| c.is_ascii_digit()))
|
||||||
|
{
|
||||||
|
return Err("Bluetooth adapter must be hci followed by an index".into());
|
||||||
|
}
|
||||||
|
if self.name.is_empty() || self.name.len() > 64 || self.name.chars().any(char::is_control) {
|
||||||
|
return Err(
|
||||||
|
"Bluetooth name must contain 1–64 UTF-8 bytes without control characters".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(peer) = &self.peer {
|
||||||
|
peer.parse::<Address>()
|
||||||
|
.map_err(|_| "Invalid Bluetooth peer address")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Status {
|
||||||
|
pub initialized: bool,
|
||||||
|
pub connected: bool,
|
||||||
|
pub ready: bool,
|
||||||
|
pub adapter: String,
|
||||||
|
pub adapter_address: String,
|
||||||
|
pub peer: Option<String>,
|
||||||
|
pub pairing_seconds: u32,
|
||||||
|
pub control_connected: bool,
|
||||||
|
pub interrupt_connected: bool,
|
||||||
|
pub leds: u8,
|
||||||
|
pub generation: u64,
|
||||||
|
pub error: Option<String>,
|
||||||
|
pub devices: Vec<Device>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Device {
|
||||||
|
pub address: String,
|
||||||
|
pub name: String,
|
||||||
|
pub paired: bool,
|
||||||
|
pub connected: bool,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Report {
|
||||||
|
Keyboard,
|
||||||
|
Mouse,
|
||||||
|
Consumer,
|
||||||
|
}
|
||||||
|
impl Report {
|
||||||
|
fn len(self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::Keyboard => 8,
|
||||||
|
Self::Mouse => 4,
|
||||||
|
Self::Consumer => 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn id(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
Self::Keyboard => 1,
|
||||||
|
Self::Mouse => 2,
|
||||||
|
Self::Consumer => 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct State {
|
||||||
|
peer: Option<String>,
|
||||||
|
pairing_until: Option<Instant>,
|
||||||
|
bonded_before_pairing: HashSet<String>,
|
||||||
|
}
|
||||||
|
impl State {
|
||||||
|
fn observe_bond(&mut self, address: &str, paired: bool) {
|
||||||
|
// Just Works may complete without an Agent1 authorization callback.
|
||||||
|
// Adopt only a new bond from our explicit window, respecting a pinned peer.
|
||||||
|
if paired
|
||||||
|
&& self.peer.is_none()
|
||||||
|
&& self
|
||||||
|
.pairing_until
|
||||||
|
.is_some_and(|until| until > Instant::now())
|
||||||
|
&& !self.bonded_before_pairing.contains(address)
|
||||||
|
{
|
||||||
|
self.peer = Some(address.to_owned());
|
||||||
|
tracing::info!(
|
||||||
|
peer = address,
|
||||||
|
"Bluetooth HID selected newly bonded computer"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn close_pairing(&mut self, paired: bool, fallback: Option<String>) {
|
||||||
|
self.pairing_until = None;
|
||||||
|
if !paired {
|
||||||
|
self.peer = fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn pairing_completed(&self, selected_paired: bool) -> bool {
|
||||||
|
selected_paired
|
||||||
|
&& self
|
||||||
|
.peer
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|peer| !self.bonded_before_pairing.contains(peer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct Shared {
|
||||||
|
state: Mutex<State>,
|
||||||
|
}
|
||||||
|
impl Shared {
|
||||||
|
fn new(peer: Option<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
state: Mutex::new(State {
|
||||||
|
peer,
|
||||||
|
pairing_until: None,
|
||||||
|
bonded_before_pairing: HashSet::new(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn allowed(&self, address: Address) -> bool {
|
||||||
|
self.state.lock().unwrap().peer.as_deref() == Some(address.to_string().as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub enum Action {
|
||||||
|
Pair(u32),
|
||||||
|
ClosePairing,
|
||||||
|
Forget,
|
||||||
|
Disconnect,
|
||||||
|
Reset,
|
||||||
|
}
|
||||||
|
enum Command {
|
||||||
|
Report(Report, Vec<u8>),
|
||||||
|
Action(Action),
|
||||||
|
Stop,
|
||||||
|
}
|
||||||
|
struct Request {
|
||||||
|
command: Command,
|
||||||
|
result: oneshot::Sender<Result<(), String>>,
|
||||||
|
expires: Instant,
|
||||||
|
}
|
||||||
|
pub struct Peripheral {
|
||||||
|
tx: mpsc::Sender<Request>,
|
||||||
|
status: watch::Receiver<Status>,
|
||||||
|
task: AsyncMutex<Option<tokio::task::JoinHandle<()>>>,
|
||||||
|
}
|
||||||
|
impl Peripheral {
|
||||||
|
pub fn start(config: Config) -> Result<Self, String> {
|
||||||
|
Self::start_with_store(config, None)
|
||||||
|
}
|
||||||
|
pub fn start_with_store(
|
||||||
|
config: Config,
|
||||||
|
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||||
|
) -> Result<Self, String> {
|
||||||
|
config.validate()?;
|
||||||
|
let (tx, rx) = mpsc::channel(64);
|
||||||
|
let (state_tx, status) = watch::channel(Status {
|
||||||
|
adapter: config.adapter.clone(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let task = tokio::spawn(supervise(config, bonds, rx, state_tx));
|
||||||
|
Ok(Self {
|
||||||
|
tx,
|
||||||
|
status,
|
||||||
|
task: AsyncMutex::new(Some(task)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn status(&self) -> Status {
|
||||||
|
self.status.borrow().clone()
|
||||||
|
}
|
||||||
|
pub fn subscribe(&self) -> watch::Receiver<Status> {
|
||||||
|
self.status.clone()
|
||||||
|
}
|
||||||
|
async fn request(&self, command: Command, duration: Duration) -> Result<(), String> {
|
||||||
|
let (result, rx) = oneshot::channel();
|
||||||
|
let request = Request {
|
||||||
|
command,
|
||||||
|
result,
|
||||||
|
expires: Instant::now() + duration,
|
||||||
|
};
|
||||||
|
self.tx
|
||||||
|
.try_send(request)
|
||||||
|
.map_err(|_| "Bluetooth input queue unavailable or full".to_string())?;
|
||||||
|
tokio::time::timeout(duration, rx)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Bluetooth operation timed out".to_string())?
|
||||||
|
.map_err(|_| "Bluetooth worker stopped".to_string())?
|
||||||
|
}
|
||||||
|
pub async fn send(&self, report: Report, value: Vec<u8>) -> Result<(), String> {
|
||||||
|
if value.len() != report.len() {
|
||||||
|
return Err("Invalid HID report size".into());
|
||||||
|
}
|
||||||
|
self.request(Command::Report(report, value), Duration::from_millis(500))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
pub async fn action(&self, action: Action) -> Result<(), String> {
|
||||||
|
self.request(Command::Action(action), Duration::from_secs(15))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
pub async fn shutdown(&self) -> Result<(), String> {
|
||||||
|
if let Some(task) = self.task.lock().await.take() {
|
||||||
|
let result = self.request(Command::Stop, Duration::from_secs(45)).await;
|
||||||
|
// Do not abort: the worker must restore the radio before a backend switch.
|
||||||
|
task.await.map_err(|e| e.to_string())?;
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn listen(address: Address, psm: u16) -> Result<SeqPacketListener, String> {
|
||||||
|
let socket = Socket::<SeqPacket>::new_seq_packet().map_err(|e| e.to_string())?;
|
||||||
|
socket
|
||||||
|
.set_security(Security {
|
||||||
|
level: SecurityLevel::Medium,
|
||||||
|
key_size: 0,
|
||||||
|
})
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
socket.bind(SocketAddr::new(address,AddressType::BrEdr,psm))
|
||||||
|
.map_err(|e|format!("Cannot bind classic HID PSM 0x{psm:02x}: {e}. Disable BlueZ input plugin; see Bluetooth HID setup."))?;
|
||||||
|
socket.listen(2).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
struct Runtime {
|
||||||
|
adapter: Adapter,
|
||||||
|
_session: Session,
|
||||||
|
shared: Arc<Shared>,
|
||||||
|
agent: Option<agent::Agent>,
|
||||||
|
controller: controller::Controller,
|
||||||
|
listeners: [SeqPacketListener; 2],
|
||||||
|
channels: [Option<Arc<SeqPacket>>; 2],
|
||||||
|
channel_peer: Option<Address>,
|
||||||
|
partial_since: Option<Instant>,
|
||||||
|
hid: protocol::HidProtocol,
|
||||||
|
generation: u64,
|
||||||
|
old_pairable: bool,
|
||||||
|
old_powered: bool,
|
||||||
|
old_alias: String,
|
||||||
|
old_discoverable: bool,
|
||||||
|
old_discoverable_timeout: u32,
|
||||||
|
old_pairable_timeout: u32,
|
||||||
|
config: Config,
|
||||||
|
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||||
|
recorded_peer: Option<String>,
|
||||||
|
}
|
||||||
|
impl Runtime {
|
||||||
|
async fn open(
|
||||||
|
config: &Config,
|
||||||
|
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||||
|
) -> Result<Self, String> {
|
||||||
|
let controller = controller::Controller::acquire(&config.adapter).await?;
|
||||||
|
let session = Session::new().await.map_err(|e| e.to_string())?;
|
||||||
|
let adapter = session
|
||||||
|
.adapter(&config.adapter)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let address = adapter.address().await.map_err(|e| e.to_string())?;
|
||||||
|
let mut config = config.clone();
|
||||||
|
if let Some(store) = &bonds {
|
||||||
|
bonds::clean_adapter(store.as_ref(), &adapter).await?;
|
||||||
|
if config.peer.is_none() {
|
||||||
|
config.peer = store
|
||||||
|
.list()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.find(|bond| bond.adapter == address.to_string() && !bond.pending)
|
||||||
|
.map(|bond| bond.peer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Acquire BOTH channels before changing adapter state. Conflict is a startup error.
|
||||||
|
let listeners = [listen(address, 0x11)?, listen(address, 0x13)?];
|
||||||
|
let old_powered = adapter.is_powered().await.map_err(|e| e.to_string())?;
|
||||||
|
let old_pairable = adapter.is_pairable().await.map_err(|e| e.to_string())?;
|
||||||
|
let old_alias = adapter.alias().await.map_err(|e| e.to_string())?;
|
||||||
|
let old_discoverable = adapter.is_discoverable().await.map_err(|e| e.to_string())?;
|
||||||
|
let old_discoverable_timeout = adapter
|
||||||
|
.discoverable_timeout()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let old_pairable_timeout = adapter
|
||||||
|
.pairable_timeout()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let shared = Arc::new(Shared::new(
|
||||||
|
config.peer.as_ref().map(|p| p.to_ascii_uppercase()),
|
||||||
|
));
|
||||||
|
let mut runtime = Self {
|
||||||
|
adapter,
|
||||||
|
_session: session,
|
||||||
|
shared,
|
||||||
|
agent: None,
|
||||||
|
controller,
|
||||||
|
listeners,
|
||||||
|
channels: [None, None],
|
||||||
|
channel_peer: None,
|
||||||
|
partial_since: None,
|
||||||
|
hid: Default::default(),
|
||||||
|
generation: 0,
|
||||||
|
old_pairable,
|
||||||
|
old_powered,
|
||||||
|
old_alias,
|
||||||
|
old_discoverable,
|
||||||
|
old_discoverable_timeout,
|
||||||
|
old_pairable_timeout,
|
||||||
|
config: config.clone(),
|
||||||
|
bonds,
|
||||||
|
recorded_peer: None,
|
||||||
|
};
|
||||||
|
if let Err(e) = runtime.setup().await {
|
||||||
|
let restore = runtime.close().await;
|
||||||
|
return Err(format!("{e}; cleanup: {restore:?}"));
|
||||||
|
}
|
||||||
|
Ok(runtime)
|
||||||
|
}
|
||||||
|
async fn setup(&mut self) -> Result<(), String> {
|
||||||
|
self.adapter
|
||||||
|
.set_powered(true)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_discoverable(false)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_pairable(false)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_alias(self.config.name.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.agent =
|
||||||
|
Some(agent::Agent::register(self.shared.clone(), self.config.adapter.clone()).await?);
|
||||||
|
self.controller.configure().await?;
|
||||||
|
self.public(None).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn public(&self, seconds: Option<u32>) -> Result<(), String> {
|
||||||
|
if let Some(seconds) = seconds {
|
||||||
|
self.adapter
|
||||||
|
.set_pairable_timeout(seconds)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_discoverable_timeout(seconds)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_pairable(true)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if let Err(e) = self.adapter.set_discoverable(true).await {
|
||||||
|
let _ = self.adapter.set_pairable(false).await;
|
||||||
|
return Err(e.to_string());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.adapter
|
||||||
|
.set_discoverable(false)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.adapter
|
||||||
|
.set_pairable(false)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn drop_channels(&mut self) {
|
||||||
|
for socket in self.channels.iter_mut().filter_map(Option::take) {
|
||||||
|
let _ = socket.shutdown(std::net::Shutdown::Both);
|
||||||
|
}
|
||||||
|
self.channel_peer = None;
|
||||||
|
self.partial_since = None;
|
||||||
|
self.hid = Default::default();
|
||||||
|
self.generation = self.generation.wrapping_add(1);
|
||||||
|
}
|
||||||
|
async fn packet(&self, index: usize, data: &[u8]) -> Result<(), String> {
|
||||||
|
let socket = self.channels[index]
|
||||||
|
.as_ref()
|
||||||
|
.ok_or("Classic HID channel not connected")?;
|
||||||
|
let sent = tokio::time::timeout(Duration::from_millis(250), socket.send(data))
|
||||||
|
.await
|
||||||
|
.map_err(|_| "HID send timeout")?
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
if sent != data.len() {
|
||||||
|
return Err("Short HID packet write".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn release(&mut self) {
|
||||||
|
for packet in self.hid.release() {
|
||||||
|
if self.channels[1].is_some() && self.packet(1, &packet).await.is_err() {
|
||||||
|
self.drop_channels();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.generation = self.generation.wrapping_add(1);
|
||||||
|
}
|
||||||
|
async fn accept(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
socket: SeqPacket,
|
||||||
|
peer: SocketAddr,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if peer.addr_type != AddressType::BrEdr {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// A host can open HID channels before the next status poll.
|
||||||
|
let paired = self
|
||||||
|
.adapter
|
||||||
|
.device(peer.addr)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.is_paired()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.shared
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.observe_bond(&peer.addr.to_string(), paired);
|
||||||
|
if !self.shared.allowed(peer.addr) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if self.channel_peer.is_some_and(|p| p != peer.addr) || self.channels[index].is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// Kernel BT_SECURITY_MEDIUM negotiates encryption before accept completes.
|
||||||
|
if socket
|
||||||
|
.as_ref()
|
||||||
|
.security()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.key_size
|
||||||
|
< 7
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.channel_peer = Some(peer.addr);
|
||||||
|
self.channels[index] = Some(Arc::new(socket));
|
||||||
|
if self.channels.iter().all(Option::is_some) {
|
||||||
|
self.partial_since = None;
|
||||||
|
self.release().await;
|
||||||
|
} else {
|
||||||
|
self.partial_since = Some(Instant::now());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn status(&mut self) -> Result<Status, String> {
|
||||||
|
if !self.adapter.is_powered().await.map_err(|e| e.to_string())? {
|
||||||
|
return Err("Bluetooth adapter powered off".into());
|
||||||
|
}
|
||||||
|
// A daemon restart loses the SDP registration even if the adapter comes back powered.
|
||||||
|
if !self
|
||||||
|
.adapter
|
||||||
|
.uuids()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.any(|id| id.to_string() == protocol::HID_UUID)
|
||||||
|
{
|
||||||
|
return Err("Classic HID SDP registration lost; restarting Bluetooth backend".into());
|
||||||
|
}
|
||||||
|
let mut status = Status {
|
||||||
|
initialized: true,
|
||||||
|
adapter: self.config.adapter.clone(),
|
||||||
|
adapter_address: self
|
||||||
|
.adapter
|
||||||
|
.address()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
for address in self
|
||||||
|
.adapter
|
||||||
|
.device_addresses()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
{
|
||||||
|
let device = self.adapter.device(address).map_err(|e| e.to_string())?;
|
||||||
|
let connected = device.is_connected().await.unwrap_or(false);
|
||||||
|
let paired = device.is_paired().await.unwrap_or(false);
|
||||||
|
self.shared
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.observe_bond(&address.to_string(), paired);
|
||||||
|
if paired || connected {
|
||||||
|
status.devices.push(Device {
|
||||||
|
address: address.to_string(),
|
||||||
|
name: device.alias().await.unwrap_or_default(),
|
||||||
|
paired,
|
||||||
|
connected,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status.devices.sort_by(|a, b| a.address.cmp(&b.address));
|
||||||
|
let (peer, until) = {
|
||||||
|
let state = self.shared.state.lock().unwrap();
|
||||||
|
(state.peer.clone(), state.pairing_until)
|
||||||
|
};
|
||||||
|
status.peer = peer.clone();
|
||||||
|
let selected = status
|
||||||
|
.devices
|
||||||
|
.iter()
|
||||||
|
.find(|device| Some(&device.address) == peer.as_ref());
|
||||||
|
let selected_paired = selected.is_some_and(|device| device.paired);
|
||||||
|
if selected_paired && self.recorded_peer != peer {
|
||||||
|
if let (Some(store), Some(peer)) = (&self.bonds, &peer) {
|
||||||
|
store
|
||||||
|
.save(bonds::Bond {
|
||||||
|
adapter: status.adapter_address.clone(),
|
||||||
|
peer: peer.clone(),
|
||||||
|
pending: false,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
self.recorded_peer = Some(peer.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status.connected = selected.is_some_and(|device| device.paired && device.connected);
|
||||||
|
if self.channels.iter().any(Option::is_some)
|
||||||
|
&& (!status.connected
|
||||||
|
|| self
|
||||||
|
.partial_since
|
||||||
|
.is_some_and(|time| time.elapsed() > Duration::from_secs(10)))
|
||||||
|
{
|
||||||
|
self.drop_channels();
|
||||||
|
}
|
||||||
|
if let Some(until) = until {
|
||||||
|
let completed = self
|
||||||
|
.shared
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.pairing_completed(selected_paired);
|
||||||
|
if until <= Instant::now() || completed {
|
||||||
|
{
|
||||||
|
let mut state = self.shared.state.lock().unwrap();
|
||||||
|
state.close_pairing(
|
||||||
|
selected_paired,
|
||||||
|
self.config.peer.as_ref().map(|p| p.to_ascii_uppercase()),
|
||||||
|
);
|
||||||
|
status.peer = state.peer.clone();
|
||||||
|
}
|
||||||
|
self.public(None).await?;
|
||||||
|
tracing::info!(reason = if completed { "bonded" } else { "expired" }, peer = ?status.peer, "Bluetooth HID pairing window closed");
|
||||||
|
} else {
|
||||||
|
status.pairing_seconds =
|
||||||
|
until.saturating_duration_since(Instant::now()).as_secs() as u32 + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status.control_connected = self.channels[0].is_some();
|
||||||
|
status.interrupt_connected = self.channels[1].is_some();
|
||||||
|
status.ready = status.connected
|
||||||
|
&& status.control_connected
|
||||||
|
&& status.interrupt_connected
|
||||||
|
&& !self.hid.suspended;
|
||||||
|
status.leds = self.hid.leds;
|
||||||
|
status.generation = self.generation;
|
||||||
|
Ok(status)
|
||||||
|
}
|
||||||
|
async fn forget(&mut self) -> Result<(), String> {
|
||||||
|
self.release().await;
|
||||||
|
self.drop_channels();
|
||||||
|
let peer = self.shared.state.lock().unwrap().peer.clone();
|
||||||
|
if let Some(peer) = &peer {
|
||||||
|
let address = peer.parse().map_err(|_| "Invalid peer")?;
|
||||||
|
if self
|
||||||
|
.adapter
|
||||||
|
.device_addresses()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.contains(&address)
|
||||||
|
{
|
||||||
|
self.adapter
|
||||||
|
.remove_device(address)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
self.shared
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.bonded_before_pairing
|
||||||
|
.remove(peer);
|
||||||
|
tracing::info!(%peer, "Bluetooth HID forgot computer");
|
||||||
|
}
|
||||||
|
if let Some(store) = &self.bonds {
|
||||||
|
let address = self
|
||||||
|
.adapter
|
||||||
|
.address()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.to_string();
|
||||||
|
for bond in store
|
||||||
|
.list()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|b| b.adapter == address && Some(&b.peer) == peer.as_ref())
|
||||||
|
{
|
||||||
|
store.remove(bond).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.recorded_peer = None;
|
||||||
|
self.config.peer = None;
|
||||||
|
self.shared.state.lock().unwrap().peer = None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn incoming(&mut self, index: usize, data: &[u8]) -> Result<(), String> {
|
||||||
|
if index == 1 {
|
||||||
|
self.hid.output(data);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let result = self.hid.control(data);
|
||||||
|
if let Some(reply) = result.reply {
|
||||||
|
self.packet(0, &reply).await?;
|
||||||
|
}
|
||||||
|
if result.reset {
|
||||||
|
self.release().await;
|
||||||
|
}
|
||||||
|
if result.unplug {
|
||||||
|
self.forget().await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn execute(&mut self, command: Command) -> Result<(), String> {
|
||||||
|
match command {
|
||||||
|
Command::Report(kind, value) => {
|
||||||
|
let peer = self.channel_peer.ok_or("Classic HID is not connected")?;
|
||||||
|
if !self.shared.allowed(peer) || self.channels.iter().any(Option::is_none) {
|
||||||
|
return Err("Classic HID channels are not ready".into());
|
||||||
|
}
|
||||||
|
let device = self.adapter.device(peer).map_err(|e| e.to_string())?;
|
||||||
|
if !device.is_paired().await.unwrap_or(false)
|
||||||
|
|| !device.is_connected().await.unwrap_or(false)
|
||||||
|
{
|
||||||
|
self.drop_channels();
|
||||||
|
return Err("Selected Bluetooth computer disconnected".into());
|
||||||
|
}
|
||||||
|
let packet = self.hid.input(kind, &value)?;
|
||||||
|
if let Err(e) = self.packet(1, &packet).await {
|
||||||
|
self.drop_channels();
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Action(Action::Pair(seconds)) => {
|
||||||
|
if !(10..=300).contains(&seconds) {
|
||||||
|
return Err("Pairing window must be 10–300 seconds".into());
|
||||||
|
}
|
||||||
|
let mut bonded = HashSet::new();
|
||||||
|
for address in self
|
||||||
|
.adapter
|
||||||
|
.device_addresses()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
{
|
||||||
|
if self
|
||||||
|
.adapter
|
||||||
|
.device(address)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.is_paired()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
{
|
||||||
|
bonded.insert(address.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut state = self.shared.state.lock().unwrap();
|
||||||
|
// Preserve the original baseline when extending an open window.
|
||||||
|
if !state
|
||||||
|
.pairing_until
|
||||||
|
.is_some_and(|until| until > Instant::now())
|
||||||
|
{
|
||||||
|
state.bonded_before_pairing = bonded;
|
||||||
|
}
|
||||||
|
state.pairing_until =
|
||||||
|
Some(Instant::now() + Duration::from_secs(seconds.into()));
|
||||||
|
}
|
||||||
|
if let Err(e) = self.public(Some(seconds)).await {
|
||||||
|
self.shared.state.lock().unwrap().pairing_until = None;
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
tracing::info!(seconds, "Bluetooth HID pairing window opened");
|
||||||
|
}
|
||||||
|
Command::Action(Action::ClosePairing) => {
|
||||||
|
self.public(None).await?;
|
||||||
|
let peer = self.shared.state.lock().unwrap().peer.clone();
|
||||||
|
let paired = if let Some(peer) = peer {
|
||||||
|
self.adapter
|
||||||
|
.device(peer.parse().map_err(|_| "Invalid HID peer")?)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.is_paired()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
self.shared
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.close_pairing(paired, self.config.peer.clone());
|
||||||
|
tracing::info!(reason = "manual", "Bluetooth HID pairing window closed");
|
||||||
|
}
|
||||||
|
Command::Action(Action::Forget) => self.forget().await?,
|
||||||
|
Command::Action(Action::Disconnect) => {
|
||||||
|
self.release().await;
|
||||||
|
self.drop_channels();
|
||||||
|
let peer = self.shared.state.lock().unwrap().peer.clone();
|
||||||
|
if let Some(peer) = peer {
|
||||||
|
self.adapter
|
||||||
|
.device(peer.parse().map_err(|_| "Invalid peer")?)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.disconnect()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Action(Action::Reset) => self.release().await,
|
||||||
|
Command::Stop => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn close(&mut self) -> Result<(), String> {
|
||||||
|
self.release().await;
|
||||||
|
self.drop_channels();
|
||||||
|
self.agent.take();
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
let mut errors = vec![];
|
||||||
|
if let Err(e) = self.adapter.set_discoverable(false).await {
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self.controller.restore().await {
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
if let Err(e) = self.adapter.set_alias(self.old_alias.clone()).await {
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self
|
||||||
|
.adapter
|
||||||
|
.set_discoverable_timeout(self.old_discoverable_timeout)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self
|
||||||
|
.adapter
|
||||||
|
.set_pairable_timeout(self.old_pairable_timeout)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self.adapter.set_pairable(self.old_pairable).await {
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self.adapter.set_discoverable(self.old_discoverable).await {
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if let Err(e) = self.adapter.set_powered(self.old_powered).await {
|
||||||
|
errors.push(e.to_string());
|
||||||
|
}
|
||||||
|
if errors.is_empty() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(errors.join("; "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn receive(socket: Option<Arc<SeqPacket>>, buffer: &mut [u8]) -> std::io::Result<usize> {
|
||||||
|
match socket {
|
||||||
|
Some(socket) => socket.recv(buffer).await,
|
||||||
|
None => std::future::pending().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn supervise(
|
||||||
|
config: Config,
|
||||||
|
bonds: Option<Arc<dyn bonds::BondStore>>,
|
||||||
|
mut rx: mpsc::Receiver<Request>,
|
||||||
|
tx: watch::Sender<Status>,
|
||||||
|
) {
|
||||||
|
let mut generation = 0;
|
||||||
|
loop {
|
||||||
|
let mut runtime = match Runtime::open(&config, bonds.clone()).await {
|
||||||
|
Ok(runtime) => runtime,
|
||||||
|
Err(error) => {
|
||||||
|
tx.send_replace(Status {
|
||||||
|
adapter: config.adapter.clone(),
|
||||||
|
error: Some(error.clone()),
|
||||||
|
generation,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let delay = tokio::time::sleep(Duration::from_secs(5));
|
||||||
|
tokio::pin!(delay);
|
||||||
|
loop {
|
||||||
|
tokio::select! {_=&mut delay=>break,request=rx.recv()=>{
|
||||||
|
let Some(request)=request else{return;};
|
||||||
|
if matches!(request.command,Command::Stop){let _=request.result.send(Ok(()));return;}
|
||||||
|
let _=request.result.send(Err(error.clone()));
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
runtime.generation = generation;
|
||||||
|
let mut timer = tokio::time::interval(Duration::from_millis(500));
|
||||||
|
let failure = loop {
|
||||||
|
let ctl = runtime.channels[0].clone();
|
||||||
|
let intr = runtime.channels[1].clone();
|
||||||
|
let mut ctl_buffer = [0; 1024];
|
||||||
|
let mut intr_buffer = [0; 1024];
|
||||||
|
tokio::select! {
|
||||||
|
accepted=runtime.listeners[0].accept()=>{match accepted{Ok((socket,peer))=>{if let Err(e)=runtime.accept(0,socket,peer).await{break e;}},Err(e)=>break e.to_string()}}
|
||||||
|
accepted=runtime.listeners[1].accept()=>{match accepted{Ok((socket,peer))=>{if let Err(e)=runtime.accept(1,socket,peer).await{break e;}},Err(e)=>break e.to_string()}}
|
||||||
|
read=receive(ctl,&mut ctl_buffer)=>{match read{Ok(n)if n>0=>{if runtime.incoming(0,&ctl_buffer[..n]).await.is_err(){runtime.drop_channels();}},_=>runtime.drop_channels()}}
|
||||||
|
read=receive(intr,&mut intr_buffer)=>{match read{Ok(n)if n>0=>{if runtime.incoming(1,&intr_buffer[..n]).await.is_err(){runtime.drop_channels();}},_=>runtime.drop_channels()}}
|
||||||
|
_=timer.tick()=>{match runtime.status().await{Ok(status)=>{tx.send_if_modified(|old|{if *old!=status{*old=status;true}else{false}});},Err(e)=>break e}}
|
||||||
|
request=rx.recv()=>{
|
||||||
|
let Some(request)=request else{let _=runtime.close().await;return;};
|
||||||
|
if matches!(request.command,Command::Stop){let result=runtime.close().await;tx.send_replace(Status{adapter:config.adapter.clone(),..Default::default()});let _=request.result.send(result);return;}
|
||||||
|
if request.expires<=Instant::now()||request.result.is_closed(){runtime.release().await;continue;}
|
||||||
|
let action = matches!(&request.command, Command::Action(_));
|
||||||
|
let result=runtime.execute(request.command).await;
|
||||||
|
if result.is_err(){runtime.release().await;}
|
||||||
|
if action {
|
||||||
|
if let Ok(status) = runtime.status().await { tx.send_replace(status); }
|
||||||
|
}
|
||||||
|
let _=request.result.send(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let restore = runtime.close().await;
|
||||||
|
generation = runtime.generation.wrapping_add(1);
|
||||||
|
tx.send_replace(Status {
|
||||||
|
adapter: config.adapter.clone(),
|
||||||
|
error: Some(format!("{failure}; cleanup: {restore:?}")),
|
||||||
|
generation,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
drop(runtime);
|
||||||
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn closing_pairing_preserves_completed_bonds_but_releases_unpaired_targets() {
|
||||||
|
let shared = Shared::new(Some("10:6F:D9:66:97:88".into()));
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||||
|
state.close_pairing(true, None);
|
||||||
|
assert_eq!(state.peer.as_deref(), Some("10:6F:D9:66:97:88"));
|
||||||
|
assert!(state.pairing_until.is_none());
|
||||||
|
state.close_pairing(false, None);
|
||||||
|
assert!(state.peer.is_none());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn configuration_validation() {
|
||||||
|
let mut c = Config {
|
||||||
|
adapter: "hci0".into(),
|
||||||
|
name: "One-KVM".into(),
|
||||||
|
peer: None,
|
||||||
|
};
|
||||||
|
assert!(c.validate().is_ok());
|
||||||
|
c.adapter = "hci0;reboot".into();
|
||||||
|
assert!(c.validate().is_err());
|
||||||
|
c.adapter = "hci1".into();
|
||||||
|
c.peer = Some("bogus".into());
|
||||||
|
assert!(c.validate().is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn peer_isolation() {
|
||||||
|
let state = Shared::new(Some("10:6F:D9:66:97:88".into()));
|
||||||
|
assert!(state.allowed("10:6F:D9:66:97:88".parse().unwrap()));
|
||||||
|
assert!(!state.allowed("10:6F:D9:66:97:89".parse().unwrap()));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn new_bond_without_agent_callback_completes_pairing() {
|
||||||
|
let shared = Shared::new(None);
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||||
|
let peer = "10:6F:D9:66:97:88";
|
||||||
|
state.observe_bond(peer, false);
|
||||||
|
assert_eq!(state.peer, None);
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert_eq!(state.peer.as_deref(), Some(peer));
|
||||||
|
assert!(state.pairing_completed(true));
|
||||||
|
// A second computer cannot replace the one selected in this window.
|
||||||
|
state.observe_bond("10:6F:D9:66:97:89", true);
|
||||||
|
assert_eq!(state.peer.as_deref(), Some(peer));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn old_bond_does_not_finish_a_new_window() {
|
||||||
|
let peer = "10:6F:D9:66:97:88";
|
||||||
|
let shared = Shared::new(Some(peer.into()));
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||||
|
state.bonded_before_pairing.insert(peer.into());
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert!(!state.pairing_completed(true));
|
||||||
|
state.peer = None;
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert_eq!(state.peer, None);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn bond_observation_respects_closed_window_and_pinned_peer() {
|
||||||
|
let peer = "10:6F:D9:66:97:88";
|
||||||
|
let shared = Shared::new(None);
|
||||||
|
let mut state = shared.state.lock().unwrap();
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert_eq!(state.peer, None);
|
||||||
|
state.pairing_until = Some(Instant::now() - Duration::from_secs(1));
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert_eq!(state.peer, None);
|
||||||
|
state.pairing_until = Some(Instant::now() + Duration::from_secs(120));
|
||||||
|
state.peer = Some("10:6F:D9:66:97:89".into());
|
||||||
|
state.observe_bond(peer, true);
|
||||||
|
assert_eq!(state.peer.as_deref(), Some("10:6F:D9:66:97:89"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct AdapterInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub address: String,
|
||||||
|
pub powered: bool,
|
||||||
|
}
|
||||||
|
pub async fn adapters() -> Result<Vec<AdapterInfo>, String> {
|
||||||
|
let session = Session::new().await.map_err(|e| e.to_string())?;
|
||||||
|
let mut output = vec![];
|
||||||
|
for name in session.adapter_names().await.map_err(|e| e.to_string())? {
|
||||||
|
let adapter = session.adapter(&name).map_err(|e| e.to_string())?;
|
||||||
|
output.push(AdapterInfo {
|
||||||
|
name,
|
||||||
|
address: adapter
|
||||||
|
.address()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.to_string(),
|
||||||
|
powered: adapter.is_powered().await.map_err(|e| e.to_string())?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
267
libs/bluetooth-hid/src/protocol.rs
Normal file
267
libs/bluetooth-hid/src/protocol.rs
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
//! Classic HIDP framing and report descriptor.
|
||||||
|
use crate::Report;
|
||||||
|
pub const MAP: &[u8] = &[
|
||||||
|
0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x85, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00,
|
||||||
|
0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05,
|
||||||
|
0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x01,
|
||||||
|
0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, 0x81, 0x00,
|
||||||
|
0xc0, 0x05, 0x01, 0x09, 0x02, 0xa1, 0x01, 0x85, 0x02, 0x09, 0x01, 0xa1, 0x00, 0x05, 0x09, 0x19,
|
||||||
|
0x01, 0x29, 0x05, 0x15, 0x00, 0x25, 0x01, 0x95, 0x05, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75,
|
||||||
|
0x03, 0x81, 0x01, 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x38, 0x15, 0x81, 0x25, 0x7f, 0x75,
|
||||||
|
0x08, 0x95, 0x03, 0x81, 0x06, 0xc0, 0xc0, 0x05, 0x0c, 0x09, 0x01, 0xa1, 0x01, 0x85, 0x03, 0x15,
|
||||||
|
0x00, 0x26, 0xff, 0x03, 0x19, 0x00, 0x2a, 0xff, 0x03, 0x75, 0x10, 0x95, 0x01, 0x81, 0x00, 0xc0,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const HID_UUID: &str = "00001124-0000-1000-8000-00805f9b34fb";
|
||||||
|
pub fn sdp() -> String {
|
||||||
|
let hex: String = MAP.iter().map(|byte| format!("{byte:02x}")).collect();
|
||||||
|
format!(
|
||||||
|
r#"<?xml version="1.0"?>
|
||||||
|
<record>
|
||||||
|
<attribute id="0x0001"><sequence><uuid value="0x1124"/></sequence></attribute>
|
||||||
|
<attribute id="0x0004"><sequence><sequence><uuid value="0x0100"/><uint16 value="0x0011"/></sequence><sequence><uuid value="0x0011"/></sequence></sequence></attribute>
|
||||||
|
<attribute id="0x0005"><sequence><uuid value="0x1002"/></sequence></attribute>
|
||||||
|
<attribute id="0x0006"><sequence><uint16 value="0x656e"/><uint16 value="0x006a"/><uint16 value="0x0100"/></sequence></attribute>
|
||||||
|
<attribute id="0x0009"><sequence><sequence><uuid value="0x1124"/><uint16 value="0x0100"/></sequence></sequence></attribute>
|
||||||
|
<attribute id="0x000d"><sequence><sequence><sequence><uuid value="0x0100"/><uint16 value="0x0013"/></sequence><sequence><uuid value="0x0011"/></sequence></sequence></sequence></attribute>
|
||||||
|
<attribute id="0x0100"><text value="One-KVM Keyboard and Mouse"/></attribute>
|
||||||
|
<attribute id="0x0101"><text value="Classic Bluetooth HID"/></attribute>
|
||||||
|
<attribute id="0x0102"><text value="One-KVM"/></attribute>
|
||||||
|
<attribute id="0x0200"><uint16 value="0x0100"/></attribute>
|
||||||
|
<attribute id="0x0201"><uint16 value="0x0111"/></attribute>
|
||||||
|
<attribute id="0x0202"><uint8 value="0xc0"/></attribute>
|
||||||
|
<attribute id="0x0203"><uint8 value="0x00"/></attribute>
|
||||||
|
<attribute id="0x0204"><boolean value="false"/></attribute>
|
||||||
|
<attribute id="0x0205"><boolean value="false"/></attribute>
|
||||||
|
<attribute id="0x0206"><sequence><sequence><uint8 value="0x22"/><text encoding="hex" value="{hex}"/></sequence></sequence></attribute>
|
||||||
|
<attribute id="0x0207"><sequence><sequence><uint16 value="0x0409"/><uint16 value="0x0100"/></sequence></sequence></attribute>
|
||||||
|
<attribute id="0x0209"><boolean value="false"/></attribute>
|
||||||
|
<attribute id="0x020a"><boolean value="false"/></attribute>
|
||||||
|
<attribute id="0x020b"><uint16 value="0x0100"/></attribute>
|
||||||
|
<attribute id="0x020c"><uint16 value="0x0c80"/></attribute>
|
||||||
|
<attribute id="0x020d"><boolean value="true"/></attribute>
|
||||||
|
<attribute id="0x020e"><boolean value="true"/></attribute>
|
||||||
|
</record>"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct HidProtocol {
|
||||||
|
pub boot: bool,
|
||||||
|
pub suspended: bool,
|
||||||
|
pub leds: u8,
|
||||||
|
pub keyboard: [u8; 8],
|
||||||
|
pub buttons: u8,
|
||||||
|
pub consumer: [u8; 2],
|
||||||
|
}
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct ControlResult {
|
||||||
|
pub reply: Option<Vec<u8>>,
|
||||||
|
pub unplug: bool,
|
||||||
|
pub reset: bool,
|
||||||
|
}
|
||||||
|
impl HidProtocol {
|
||||||
|
pub fn input(&mut self, kind: Report, value: &[u8]) -> Result<Vec<u8>, String> {
|
||||||
|
if value.len() != kind.len() {
|
||||||
|
return Err("Invalid HID report length".into());
|
||||||
|
}
|
||||||
|
if self.suspended {
|
||||||
|
return Err("Bluetooth HID is suspended".into());
|
||||||
|
}
|
||||||
|
let mut packet = vec![0xa1, kind.id()];
|
||||||
|
match kind {
|
||||||
|
Report::Keyboard => {
|
||||||
|
self.keyboard.copy_from_slice(value);
|
||||||
|
packet.extend_from_slice(value);
|
||||||
|
}
|
||||||
|
Report::Mouse => {
|
||||||
|
self.buttons = value[0];
|
||||||
|
packet.extend_from_slice(&value[..if self.boot { 3 } else { 4 }]);
|
||||||
|
if self.boot {
|
||||||
|
packet[2] &= 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Report::Consumer if !self.boot => {
|
||||||
|
self.consumer.copy_from_slice(value);
|
||||||
|
packet.extend_from_slice(value);
|
||||||
|
}
|
||||||
|
Report::Consumer => return Err("Consumer keys unavailable in boot protocol".into()),
|
||||||
|
}
|
||||||
|
Ok(packet)
|
||||||
|
}
|
||||||
|
pub fn release(&mut self) -> Vec<Vec<u8>> {
|
||||||
|
self.keyboard = [0; 8];
|
||||||
|
self.buttons = 0;
|
||||||
|
self.consumer = [0; 2];
|
||||||
|
let mut packets = vec![
|
||||||
|
vec![0xa1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
if self.boot {
|
||||||
|
vec![0xa1, 2, 0, 0, 0]
|
||||||
|
} else {
|
||||||
|
vec![0xa1, 2, 0, 0, 0, 0]
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if !self.boot {
|
||||||
|
packets.push(vec![0xa1, 3, 0, 0]);
|
||||||
|
}
|
||||||
|
packets
|
||||||
|
}
|
||||||
|
pub fn output(&mut self, data: &[u8]) -> bool {
|
||||||
|
if data.len() == 3 && data[0] == 0xa2 && data[1] == 1 {
|
||||||
|
self.leds = data[2] & 0x1f;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn control(&mut self, data: &[u8]) -> ControlResult {
|
||||||
|
let mut result = ControlResult::default();
|
||||||
|
let handshake = |code| ControlResult {
|
||||||
|
reply: Some(vec![code]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let Some(&header) = data.first() else {
|
||||||
|
return handshake(4);
|
||||||
|
};
|
||||||
|
match header {
|
||||||
|
0x13 if data.len() == 1 => {
|
||||||
|
self.suspended = true;
|
||||||
|
result.reset = true;
|
||||||
|
}
|
||||||
|
0x14 if data.len() == 1 => {
|
||||||
|
self.suspended = false;
|
||||||
|
result.reset = true;
|
||||||
|
}
|
||||||
|
0x15 if data.len() == 1 => {
|
||||||
|
result.unplug = true;
|
||||||
|
}
|
||||||
|
0x11 | 0x12 if data.len() == 1 => {
|
||||||
|
self.suspended = false;
|
||||||
|
result.reset = true;
|
||||||
|
}
|
||||||
|
0x60 if data.len() == 1 => {
|
||||||
|
result.reply = Some(vec![0xa0, if self.boot { 0 } else { 1 }]);
|
||||||
|
}
|
||||||
|
0x70 | 0x71 if data.len() == 1 => {
|
||||||
|
self.boot = header == 0x70;
|
||||||
|
result.reply = Some(vec![0]);
|
||||||
|
result.reset = true;
|
||||||
|
}
|
||||||
|
0x52 if data.len() == 3 && data[1] == 1 => {
|
||||||
|
self.leds = data[2] & 0x1f;
|
||||||
|
return handshake(0);
|
||||||
|
}
|
||||||
|
0x41 | 0x49 | 0x42 | 0x4a => {
|
||||||
|
let sized = header & 8 != 0;
|
||||||
|
if data.len() != if sized { 4 } else { 2 } {
|
||||||
|
return handshake(4);
|
||||||
|
}
|
||||||
|
let mut report = vec![0xa0 | (header & 3), data[1]];
|
||||||
|
match (header & 3, data[1]) {
|
||||||
|
(1, 1) => report.extend(self.keyboard),
|
||||||
|
(1, 2) => report.extend(if self.boot {
|
||||||
|
vec![self.buttons & 7, 0, 0]
|
||||||
|
} else {
|
||||||
|
vec![self.buttons, 0, 0, 0]
|
||||||
|
}),
|
||||||
|
(1, 3) if !self.boot => report.extend(self.consumer),
|
||||||
|
(2, 1) => report.push(self.leds),
|
||||||
|
_ => return handshake(2),
|
||||||
|
}
|
||||||
|
if sized {
|
||||||
|
report.truncate(1 + u16::from_le_bytes([data[2], data[3]]) as usize);
|
||||||
|
}
|
||||||
|
result.reply = Some(report);
|
||||||
|
}
|
||||||
|
_ => return handshake(3), // ERR_UNSUPPORTED_REQUEST
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn classic_frames_and_boot_protocol() {
|
||||||
|
let mut hid = HidProtocol::default();
|
||||||
|
assert_eq!(
|
||||||
|
hid.input(Report::Keyboard, &[2, 0, 4, 0, 0, 0, 0, 0])
|
||||||
|
.unwrap(),
|
||||||
|
[0xa1, 1, 2, 0, 4, 0, 0, 0, 0, 0]
|
||||||
|
);
|
||||||
|
assert_eq!(hid.control(&[0x60]).reply.unwrap(), [0xa0, 1]);
|
||||||
|
assert_eq!(hid.control(&[0x70]).reply.unwrap(), [0]);
|
||||||
|
assert_eq!(
|
||||||
|
hid.input(Report::Mouse, &[0x1f, 127, 128, 1]).unwrap(),
|
||||||
|
[0xa1, 2, 7, 127, 128]
|
||||||
|
);
|
||||||
|
assert!(hid.input(Report::Consumer, &[0, 0]).is_err());
|
||||||
|
assert!(hid.input(Report::Keyboard, &[0]).is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn led_read_write_and_errors() {
|
||||||
|
let mut hid = HidProtocol::default();
|
||||||
|
assert!(hid.output(&[0xa2, 1, 3]));
|
||||||
|
assert_eq!(hid.leds, 3);
|
||||||
|
assert_eq!(hid.control(&[0x42, 1]).reply.unwrap(), [0xa2, 1, 3]);
|
||||||
|
assert_eq!(hid.control(&[0x52, 1, 2]).reply.unwrap(), [0]);
|
||||||
|
assert_eq!(hid.leds, 2);
|
||||||
|
assert_eq!(hid.control(&[0x41, 99]).reply.unwrap(), [2]);
|
||||||
|
assert_eq!(hid.control(&[0x41]).reply.unwrap(), [4]);
|
||||||
|
assert_eq!(hid.control(&[0x90, 0]).reply.unwrap(), [3]);
|
||||||
|
assert_eq!(
|
||||||
|
hid.control(&[0x49, 1, 3, 0]).reply.unwrap(),
|
||||||
|
[0xa1, 1, 0, 0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn suspend_reset_and_virtual_unplug() {
|
||||||
|
let mut hid = HidProtocol::default();
|
||||||
|
assert!(hid.control(&[0x13]).reset);
|
||||||
|
assert!(hid.input(Report::Mouse, &[0; 4]).is_err());
|
||||||
|
assert!(hid.control(&[0x14]).reset);
|
||||||
|
assert!(hid.input(Report::Mouse, &[0; 4]).is_ok());
|
||||||
|
assert!(hid.control(&[0x15]).unplug);
|
||||||
|
hid.keyboard[2] = 4;
|
||||||
|
hid.buttons = 1;
|
||||||
|
assert_eq!(hid.release().len(), 3);
|
||||||
|
assert_eq!(hid.keyboard, [0; 8]);
|
||||||
|
assert_eq!(hid.buttons, 0);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn sdp_describes_both_classic_channels() {
|
||||||
|
let record = sdp();
|
||||||
|
assert!(record.contains("uuid value=\"0x1124\""));
|
||||||
|
assert!(record.contains("uint16 value=\"0x0011\""));
|
||||||
|
assert!(record.contains("uint16 value=\"0x0013\""));
|
||||||
|
assert!(!record.contains("0x1812"));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn descriptor_report_sizes() {
|
||||||
|
let (mut pos, mut size, mut count, mut id, mut depth) = (0, 0, 0, 0, 0);
|
||||||
|
let mut input = [0; 4];
|
||||||
|
let mut output = [0; 4];
|
||||||
|
while pos < MAP.len() {
|
||||||
|
let prefix = MAP[pos];
|
||||||
|
pos += 1;
|
||||||
|
let length = [0, 1, 2, 4][(prefix & 3) as usize];
|
||||||
|
let mut value = 0;
|
||||||
|
for i in 0..length {
|
||||||
|
value |= (MAP[pos + i] as usize) << (8 * i);
|
||||||
|
}
|
||||||
|
pos += length;
|
||||||
|
match ((prefix >> 2) & 3, prefix >> 4) {
|
||||||
|
(1, 7) => size = value,
|
||||||
|
(1, 8) => id = value,
|
||||||
|
(1, 9) => count = value,
|
||||||
|
(0, 8) => input[id] += size * count,
|
||||||
|
(0, 9) => output[id] += size * count,
|
||||||
|
(0, 10) => depth += 1,
|
||||||
|
(0, 12) => depth -= 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(depth, 0);
|
||||||
|
assert_eq!(input, [0, 64, 32, 16]);
|
||||||
|
assert_eq!(output, [0, 8, 0, 0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
155
libs/dtls/Cargo.toml
Normal file
155
libs/dtls/Cargo.toml
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||||
|
#
|
||||||
|
# When uploading crates to the registry Cargo will automatically
|
||||||
|
# "normalize" Cargo.toml files for maximal compatibility
|
||||||
|
# with all versions of Cargo and also rewrite `path` dependencies
|
||||||
|
# to registry (e.g., crates.io) dependencies.
|
||||||
|
#
|
||||||
|
# If you are reading this file be aware that the original Cargo.toml
|
||||||
|
# will likely look very different (and much more reasonable).
|
||||||
|
# See Cargo.toml.orig for the original contents.
|
||||||
|
|
||||||
|
[package]
|
||||||
|
edition = "2021"
|
||||||
|
name = "dtls"
|
||||||
|
version = "0.17.2"
|
||||||
|
authors = ["Rain Liu <yliu@webrtc.rs>"]
|
||||||
|
build = false
|
||||||
|
autolib = false
|
||||||
|
autobins = false
|
||||||
|
autoexamples = false
|
||||||
|
autotests = false
|
||||||
|
autobenches = false
|
||||||
|
description = "A pure Rust implementation of DTLS"
|
||||||
|
homepage = "https://webrtc.rs"
|
||||||
|
documentation = "https://docs.rs/dtls"
|
||||||
|
readme = "README.md"
|
||||||
|
license = "MIT/Apache-2.0"
|
||||||
|
repository = "https://github.com/webrtc-rs/webrtc/tree/master/dtls"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
pem = ["dep:pem"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "dtls"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies.aes]
|
||||||
|
version = "0.8"
|
||||||
|
|
||||||
|
[dependencies.aes-gcm]
|
||||||
|
version = "0.10"
|
||||||
|
|
||||||
|
[dependencies.async-trait]
|
||||||
|
version = "0.1"
|
||||||
|
|
||||||
|
[dependencies.bytecheck]
|
||||||
|
version = "0.8"
|
||||||
|
|
||||||
|
[dependencies.byteorder]
|
||||||
|
version = "1"
|
||||||
|
|
||||||
|
[dependencies.cbc]
|
||||||
|
version = "0.1"
|
||||||
|
features = [
|
||||||
|
"block-padding",
|
||||||
|
"alloc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies.ccm]
|
||||||
|
version = "0.5"
|
||||||
|
|
||||||
|
[dependencies.chacha20poly1305]
|
||||||
|
version = "0.10.1"
|
||||||
|
|
||||||
|
[dependencies.der-parser]
|
||||||
|
version = "9.0"
|
||||||
|
|
||||||
|
[dependencies.hmac]
|
||||||
|
version = "0.12"
|
||||||
|
|
||||||
|
[dependencies.log]
|
||||||
|
version = "0.4"
|
||||||
|
|
||||||
|
[dependencies.p256]
|
||||||
|
version = "0.13"
|
||||||
|
features = [
|
||||||
|
"default",
|
||||||
|
"ecdh",
|
||||||
|
"ecdsa",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies.p384]
|
||||||
|
version = "0.13"
|
||||||
|
|
||||||
|
[dependencies.pem]
|
||||||
|
version = "3"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.portable-atomic]
|
||||||
|
version = "1.6"
|
||||||
|
|
||||||
|
[dependencies.rand]
|
||||||
|
version = "0.9"
|
||||||
|
|
||||||
|
[dependencies.rand_core]
|
||||||
|
version = "0.6"
|
||||||
|
|
||||||
|
[dependencies.rcgen]
|
||||||
|
version = "0.13"
|
||||||
|
|
||||||
|
[dependencies.ring]
|
||||||
|
version = "0.17.14"
|
||||||
|
|
||||||
|
[dependencies.rkyv]
|
||||||
|
version = "0.8"
|
||||||
|
|
||||||
|
[dependencies.openssl]
|
||||||
|
version = "0.10"
|
||||||
|
features = ["vendored"]
|
||||||
|
|
||||||
|
[dependencies.rustls-pki-types]
|
||||||
|
version = "1"
|
||||||
|
|
||||||
|
[dependencies.sec1]
|
||||||
|
version = "0.7"
|
||||||
|
features = ["std"]
|
||||||
|
|
||||||
|
[dependencies.sha1]
|
||||||
|
version = "0.10"
|
||||||
|
|
||||||
|
[dependencies.sha2]
|
||||||
|
version = "0.10"
|
||||||
|
|
||||||
|
[dependencies.thiserror]
|
||||||
|
version = "1"
|
||||||
|
|
||||||
|
[dependencies.tokio]
|
||||||
|
version = "1.32.0"
|
||||||
|
features = [
|
||||||
|
"fs",
|
||||||
|
"io-util",
|
||||||
|
"io-std",
|
||||||
|
"macros",
|
||||||
|
"net",
|
||||||
|
"parking_lot",
|
||||||
|
"rt",
|
||||||
|
"rt-multi-thread",
|
||||||
|
"sync",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies.util]
|
||||||
|
version = "0.17.1"
|
||||||
|
features = ["conn"]
|
||||||
|
default-features = false
|
||||||
|
package = "webrtc-util"
|
||||||
|
|
||||||
|
[dependencies.x25519-dalek]
|
||||||
|
version = "2"
|
||||||
|
features = ["static_secrets"]
|
||||||
|
|
||||||
|
[dependencies.x509-parser]
|
||||||
|
version = "0.16"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
201
libs/dtls/LICENSE-APACHE
Normal file
201
libs/dtls/LICENSE-APACHE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
21
libs/dtls/LICENSE-MIT
Normal file
21
libs/dtls/LICENSE-MIT
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 WebRTC.rs
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
20
libs/dtls/README.md
Normal file
20
libs/dtls/README.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# DTLS certificate verification backend
|
||||||
|
|
||||||
|
This directory contains `dtls` 0.17.2 from the crates.io release of webrtc-rs.
|
||||||
|
The original MIT and Apache-2.0 licenses are preserved.
|
||||||
|
|
||||||
|
One-KVM patches only certificate-chain verification and its configuration to
|
||||||
|
use vendored OpenSSL instead of rustls/WebPKI. DTLS flights, record protection,
|
||||||
|
certificate signatures, SRTP negotiation and the application fingerprint check
|
||||||
|
remain unchanged. `rustls-pki-types` remains a certificate-data representation,
|
||||||
|
not a TLS implementation.
|
||||||
|
|
||||||
|
`certificate_verifier.rs` implements trust-anchor, purpose, certificate-validity
|
||||||
|
and DNS/IP identity checks. Server names do not fall back to the certificate
|
||||||
|
subject, and partial-label wildcards are rejected. No trust checks are skipped
|
||||||
|
by this backend. The upstream application-controlled `insecure_skip_verify`
|
||||||
|
policy is preserved, including WebRTC's separate certificate fingerprint check.
|
||||||
|
Authentication security level 2 rejects weak RSA keys and SHA-1 leaf signatures.
|
||||||
|
|
||||||
|
Run `cargo test --manifest-path libs/dtls/Cargo.toml --lib` after updating this
|
||||||
|
fork, and verify the application dependency tree contains no `rustls` engine.
|
||||||
50
libs/dtls/src/alert/alert_test.rs
Normal file
50
libs/dtls/src/alert/alert_test.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
use std::io::{BufReader, BufWriter};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::error::Error;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_alert() -> Result<()> {
|
||||||
|
let tests = vec![
|
||||||
|
(
|
||||||
|
"Valid Alert",
|
||||||
|
vec![0x02, 0x0A],
|
||||||
|
Alert {
|
||||||
|
alert_level: AlertLevel::Fatal,
|
||||||
|
alert_description: AlertDescription::UnexpectedMessage,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Invalid alert length",
|
||||||
|
vec![0x00],
|
||||||
|
Alert {
|
||||||
|
alert_level: AlertLevel::Invalid,
|
||||||
|
alert_description: AlertDescription::Invalid,
|
||||||
|
},
|
||||||
|
Some(Error::Other("io".to_owned())),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (name, data, wanted, unmarshal_error) in tests {
|
||||||
|
let mut reader = BufReader::new(data.as_slice());
|
||||||
|
let result = Alert::unmarshal(&mut reader);
|
||||||
|
|
||||||
|
if let Some(err) = unmarshal_error {
|
||||||
|
assert!(result.is_err(), "{name} expected error: {err}");
|
||||||
|
} else if let Ok(alert) = result {
|
||||||
|
assert_eq!(wanted, alert, "{name} expected {wanted}, but got {alert}");
|
||||||
|
|
||||||
|
let mut data2: Vec<u8> = vec![];
|
||||||
|
{
|
||||||
|
let mut writer = BufWriter::<&mut Vec<u8>>::new(data2.as_mut());
|
||||||
|
alert.marshal(&mut writer)?;
|
||||||
|
}
|
||||||
|
assert_eq!(data, data2, "{name} expected {data:?}, but got {data2:?}");
|
||||||
|
} else {
|
||||||
|
assert!(result.is_ok(), "{name} expected Ok, but has error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
189
libs/dtls/src/alert/mod.rs
Normal file
189
libs/dtls/src/alert/mod.rs
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod alert_test;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
|
use super::content::*;
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub(crate) enum AlertLevel {
|
||||||
|
Warning = 1,
|
||||||
|
Fatal = 2,
|
||||||
|
Invalid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for AlertLevel {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
AlertLevel::Warning => write!(f, "LevelWarning"),
|
||||||
|
AlertLevel::Fatal => write!(f, "LevelFatal"),
|
||||||
|
_ => write!(f, "Invalid alert level"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for AlertLevel {
|
||||||
|
fn from(val: u8) -> Self {
|
||||||
|
match val {
|
||||||
|
1 => AlertLevel::Warning,
|
||||||
|
2 => AlertLevel::Fatal,
|
||||||
|
_ => AlertLevel::Invalid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub(crate) enum AlertDescription {
|
||||||
|
CloseNotify = 0,
|
||||||
|
UnexpectedMessage = 10,
|
||||||
|
BadRecordMac = 20,
|
||||||
|
DecryptionFailed = 21,
|
||||||
|
RecordOverflow = 22,
|
||||||
|
DecompressionFailure = 30,
|
||||||
|
HandshakeFailure = 40,
|
||||||
|
NoCertificate = 41,
|
||||||
|
BadCertificate = 42,
|
||||||
|
UnsupportedCertificate = 43,
|
||||||
|
CertificateRevoked = 44,
|
||||||
|
CertificateExpired = 45,
|
||||||
|
CertificateUnknown = 46,
|
||||||
|
IllegalParameter = 47,
|
||||||
|
UnknownCa = 48,
|
||||||
|
AccessDenied = 49,
|
||||||
|
DecodeError = 50,
|
||||||
|
DecryptError = 51,
|
||||||
|
ExportRestriction = 60,
|
||||||
|
ProtocolVersion = 70,
|
||||||
|
InsufficientSecurity = 71,
|
||||||
|
InternalError = 80,
|
||||||
|
UserCanceled = 90,
|
||||||
|
NoRenegotiation = 100,
|
||||||
|
UnsupportedExtension = 110,
|
||||||
|
UnknownPskIdentity = 115,
|
||||||
|
Invalid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for AlertDescription {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
AlertDescription::CloseNotify => write!(f, "CloseNotify"),
|
||||||
|
AlertDescription::UnexpectedMessage => write!(f, "UnexpectedMessage"),
|
||||||
|
AlertDescription::BadRecordMac => write!(f, "BadRecordMac"),
|
||||||
|
AlertDescription::DecryptionFailed => write!(f, "DecryptionFailed"),
|
||||||
|
AlertDescription::RecordOverflow => write!(f, "RecordOverflow"),
|
||||||
|
AlertDescription::DecompressionFailure => write!(f, "DecompressionFailure"),
|
||||||
|
AlertDescription::HandshakeFailure => write!(f, "HandshakeFailure"),
|
||||||
|
AlertDescription::NoCertificate => write!(f, "NoCertificate"),
|
||||||
|
AlertDescription::BadCertificate => write!(f, "BadCertificate"),
|
||||||
|
AlertDescription::UnsupportedCertificate => write!(f, "UnsupportedCertificate"),
|
||||||
|
AlertDescription::CertificateRevoked => write!(f, "CertificateRevoked"),
|
||||||
|
AlertDescription::CertificateExpired => write!(f, "CertificateExpired"),
|
||||||
|
AlertDescription::CertificateUnknown => write!(f, "CertificateUnknown"),
|
||||||
|
AlertDescription::IllegalParameter => write!(f, "IllegalParameter"),
|
||||||
|
AlertDescription::UnknownCa => write!(f, "UnknownCA"),
|
||||||
|
AlertDescription::AccessDenied => write!(f, "AccessDenied"),
|
||||||
|
AlertDescription::DecodeError => write!(f, "DecodeError"),
|
||||||
|
AlertDescription::DecryptError => write!(f, "DecryptError"),
|
||||||
|
AlertDescription::ExportRestriction => write!(f, "ExportRestriction"),
|
||||||
|
AlertDescription::ProtocolVersion => write!(f, "ProtocolVersion"),
|
||||||
|
AlertDescription::InsufficientSecurity => write!(f, "InsufficientSecurity"),
|
||||||
|
AlertDescription::InternalError => write!(f, "InternalError"),
|
||||||
|
AlertDescription::UserCanceled => write!(f, "UserCanceled"),
|
||||||
|
AlertDescription::NoRenegotiation => write!(f, "NoRenegotiation"),
|
||||||
|
AlertDescription::UnsupportedExtension => write!(f, "UnsupportedExtension"),
|
||||||
|
AlertDescription::UnknownPskIdentity => write!(f, "UnknownPskIdentity"),
|
||||||
|
_ => write!(f, "Invalid alert description"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for AlertDescription {
|
||||||
|
fn from(val: u8) -> Self {
|
||||||
|
match val {
|
||||||
|
0 => AlertDescription::CloseNotify,
|
||||||
|
10 => AlertDescription::UnexpectedMessage,
|
||||||
|
20 => AlertDescription::BadRecordMac,
|
||||||
|
21 => AlertDescription::DecryptionFailed,
|
||||||
|
22 => AlertDescription::RecordOverflow,
|
||||||
|
30 => AlertDescription::DecompressionFailure,
|
||||||
|
40 => AlertDescription::HandshakeFailure,
|
||||||
|
41 => AlertDescription::NoCertificate,
|
||||||
|
42 => AlertDescription::BadCertificate,
|
||||||
|
43 => AlertDescription::UnsupportedCertificate,
|
||||||
|
44 => AlertDescription::CertificateRevoked,
|
||||||
|
45 => AlertDescription::CertificateExpired,
|
||||||
|
46 => AlertDescription::CertificateUnknown,
|
||||||
|
47 => AlertDescription::IllegalParameter,
|
||||||
|
48 => AlertDescription::UnknownCa,
|
||||||
|
49 => AlertDescription::AccessDenied,
|
||||||
|
50 => AlertDescription::DecodeError,
|
||||||
|
51 => AlertDescription::DecryptError,
|
||||||
|
60 => AlertDescription::ExportRestriction,
|
||||||
|
70 => AlertDescription::ProtocolVersion,
|
||||||
|
71 => AlertDescription::InsufficientSecurity,
|
||||||
|
80 => AlertDescription::InternalError,
|
||||||
|
90 => AlertDescription::UserCanceled,
|
||||||
|
100 => AlertDescription::NoRenegotiation,
|
||||||
|
110 => AlertDescription::UnsupportedExtension,
|
||||||
|
115 => AlertDescription::UnknownPskIdentity,
|
||||||
|
_ => AlertDescription::Invalid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One of the content types supported by the TLS record layer is the
|
||||||
|
// alert type. Alert messages convey the severity of the message
|
||||||
|
// (warning or fatal) and a description of the alert. Alert messages
|
||||||
|
// with a level of fatal result in the immediate termination of the
|
||||||
|
// connection. In this case, other connections corresponding to the
|
||||||
|
// session may continue, but the session identifier MUST be invalidated,
|
||||||
|
// preventing the failed session from being used to establish new
|
||||||
|
// connections. Like other messages, alert messages are encrypted and
|
||||||
|
// compressed, as specified by the current connection state.
|
||||||
|
/// ## Specifications
|
||||||
|
///
|
||||||
|
/// * [RFC 5246 §7.2]
|
||||||
|
///
|
||||||
|
/// [RFC 5246 §7.2]: https://tools.ietf.org/html/rfc5246#section-7.2
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub struct Alert {
|
||||||
|
pub(crate) alert_level: AlertLevel,
|
||||||
|
pub(crate) alert_description: AlertDescription,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Alert {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "Alert {}: {}", self.alert_level, self.alert_description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Alert {
|
||||||
|
pub fn content_type(&self) -> ContentType {
|
||||||
|
ContentType::Alert
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> usize {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn marshal<W: Write>(&self, writer: &mut W) -> Result<()> {
|
||||||
|
writer.write_u8(self.alert_level as u8)?;
|
||||||
|
writer.write_u8(self.alert_description as u8)?;
|
||||||
|
|
||||||
|
Ok(writer.flush()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unmarshal<R: Read>(reader: &mut R) -> Result<Self> {
|
||||||
|
let alert_level = reader.read_u8()?.into();
|
||||||
|
let alert_description = reader.read_u8()?.into();
|
||||||
|
|
||||||
|
Ok(Alert {
|
||||||
|
alert_level,
|
||||||
|
alert_description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
41
libs/dtls/src/application_data.rs
Normal file
41
libs/dtls/src/application_data.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
use super::content::*;
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
// Application data messages are carried by the record layer and are
|
||||||
|
// fragmented, compressed, and encrypted based on the current connection
|
||||||
|
// state. The messages are treated as transparent data to the record
|
||||||
|
// layer.
|
||||||
|
/// ## Specifications
|
||||||
|
///
|
||||||
|
/// * [RFC 5246 §10]
|
||||||
|
///
|
||||||
|
/// [RFC 5246 §10]: https://tools.ietf.org/html/rfc5246#section-10
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct ApplicationData {
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationData {
|
||||||
|
pub fn content_type(&self) -> ContentType {
|
||||||
|
ContentType::ApplicationData
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> usize {
|
||||||
|
self.data.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn marshal<W: Write>(&self, writer: &mut W) -> Result<()> {
|
||||||
|
writer.write_all(&self.data)?;
|
||||||
|
|
||||||
|
Ok(writer.flush()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unmarshal<R: Read>(reader: &mut R) -> Result<Self> {
|
||||||
|
let mut data: Vec<u8> = vec![];
|
||||||
|
reader.read_to_end(&mut data)?;
|
||||||
|
|
||||||
|
Ok(ApplicationData { data })
|
||||||
|
}
|
||||||
|
}
|
||||||
378
libs/dtls/src/certificate_verifier.rs
Normal file
378
libs/dtls/src/certificate_verifier.rs
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
use openssl::stack::Stack;
|
||||||
|
use openssl::x509::store::X509StoreBuilder;
|
||||||
|
use openssl::x509::verify::{X509CheckFlags, X509VerifyParam};
|
||||||
|
use openssl::x509::{X509PurposeId, X509StoreContext, X509};
|
||||||
|
use rustls_pki_types::{CertificateDer, ServerName};
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct RootCertStore {
|
||||||
|
certificates: Vec<X509>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RootCertStore {
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, certificate: CertificateDer<'_>) -> Result<()> {
|
||||||
|
self.certificates
|
||||||
|
.push(X509::from_der(certificate.as_ref()).map_err(openssl_error)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct CertificateVerifier {
|
||||||
|
roots: RootCertStore,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CertificateVerifier {
|
||||||
|
pub(crate) fn new(roots: RootCertStore) -> Self {
|
||||||
|
Self { roots }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn verify_client_cert(
|
||||||
|
&self,
|
||||||
|
end_entity: &CertificateDer<'_>,
|
||||||
|
intermediates: &[CertificateDer<'_>],
|
||||||
|
) -> Result<()> {
|
||||||
|
self.verify(end_entity, intermediates, X509PurposeId::SSL_CLIENT, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn verify_server_cert(
|
||||||
|
&self,
|
||||||
|
end_entity: &CertificateDer<'_>,
|
||||||
|
intermediates: &[CertificateDer<'_>],
|
||||||
|
server_name: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
ServerName::try_from(server_name).map_err(|error| Error::Other(error.to_string()))?;
|
||||||
|
self.verify(
|
||||||
|
end_entity,
|
||||||
|
intermediates,
|
||||||
|
X509PurposeId::SSL_SERVER,
|
||||||
|
Some(server_name),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify(
|
||||||
|
&self,
|
||||||
|
end_entity: &CertificateDer<'_>,
|
||||||
|
intermediates: &[CertificateDer<'_>],
|
||||||
|
purpose: X509PurposeId,
|
||||||
|
server_name: Option<&str>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let leaf = X509::from_der(end_entity.as_ref()).map_err(openssl_error)?;
|
||||||
|
let mut chain = Stack::new().map_err(openssl_error)?;
|
||||||
|
for certificate in intermediates {
|
||||||
|
chain
|
||||||
|
.push(X509::from_der(certificate.as_ref()).map_err(openssl_error)?)
|
||||||
|
.map_err(openssl_error)?;
|
||||||
|
}
|
||||||
|
let mut parameters = X509VerifyParam::new().map_err(openssl_error)?;
|
||||||
|
parameters.set_auth_level(2);
|
||||||
|
parameters.set_purpose(purpose).map_err(openssl_error)?;
|
||||||
|
parameters.set_hostflags(
|
||||||
|
X509CheckFlags::NO_PARTIAL_WILDCARDS | X509CheckFlags::NEVER_CHECK_SUBJECT,
|
||||||
|
);
|
||||||
|
if let Some(server_name) = server_name {
|
||||||
|
if let Ok(address) = server_name.parse() {
|
||||||
|
parameters.set_ip(address).map_err(openssl_error)?;
|
||||||
|
} else {
|
||||||
|
parameters.set_host(server_name).map_err(openssl_error)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut store = X509StoreBuilder::new().map_err(openssl_error)?;
|
||||||
|
store.set_param(¶meters).map_err(openssl_error)?;
|
||||||
|
for certificate in &self.roots.certificates {
|
||||||
|
store.add_cert(certificate.clone()).map_err(openssl_error)?;
|
||||||
|
}
|
||||||
|
let store = store.build();
|
||||||
|
let mut context = X509StoreContext::new().map_err(openssl_error)?;
|
||||||
|
context
|
||||||
|
.init(&store, &leaf, &chain, |context| {
|
||||||
|
if context.verify_cert()? {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
Ok(Some(context.error().to_string()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map_err(openssl_error)?
|
||||||
|
.map_or(Ok(()), |error| Err(Error::Other(error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn openssl_error(error: openssl::error::ErrorStack) -> Error {
|
||||||
|
Error::Other(error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use rcgen::{BasicConstraints, CertificateParams, ExtendedKeyUsagePurpose, IsCa, KeyPair};
|
||||||
|
|
||||||
|
fn signed_leaf(
|
||||||
|
parameters: CertificateParams,
|
||||||
|
) -> (CertificateVerifier, CertificateDer<'static>) {
|
||||||
|
let mut issuer_parameters = CertificateParams::new(Vec::<String>::new()).unwrap();
|
||||||
|
issuer_parameters
|
||||||
|
.distinguished_name
|
||||||
|
.push(rcgen::DnType::CommonName, "test root");
|
||||||
|
issuer_parameters.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
|
||||||
|
let issuer_key = KeyPair::generate().unwrap();
|
||||||
|
let issuer = issuer_parameters.self_signed(&issuer_key).unwrap();
|
||||||
|
let leaf_key = KeyPair::generate().unwrap();
|
||||||
|
let leaf = parameters
|
||||||
|
.signed_by(&leaf_key, &issuer, &issuer_key)
|
||||||
|
.unwrap();
|
||||||
|
let mut roots = RootCertStore::empty();
|
||||||
|
roots.add(issuer.der().clone()).unwrap();
|
||||||
|
(CertificateVerifier::new(roots), leaf.der().clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trusted_server_matches_dns_and_ip_subject_alternative_names() {
|
||||||
|
let parameters =
|
||||||
|
CertificateParams::new(vec!["localhost".into(), "127.0.0.1".into()]).unwrap();
|
||||||
|
let (verifier, leaf) = signed_leaf(parameters);
|
||||||
|
verifier
|
||||||
|
.verify_server_cert(&leaf, &[], "localhost")
|
||||||
|
.unwrap();
|
||||||
|
assert!(verifier.verify_server_cert(&leaf, &[], "127.0.0.1").is_ok());
|
||||||
|
assert!(verifier
|
||||||
|
.verify_server_cert(&leaf, &[], "wrong.local")
|
||||||
|
.is_err());
|
||||||
|
assert!(verifier
|
||||||
|
.verify_server_cert(&leaf, &[], "127.0.0.2")
|
||||||
|
.is_err());
|
||||||
|
assert!(verifier.verify_server_cert(&leaf, &[], "").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_untrusted_and_tampered_certificates() {
|
||||||
|
let (verifier, leaf) =
|
||||||
|
signed_leaf(CertificateParams::new(vec!["localhost".into()]).unwrap());
|
||||||
|
let untrusted = CertificateVerifier::new(RootCertStore::empty());
|
||||||
|
assert!(untrusted
|
||||||
|
.verify_server_cert(&leaf, &[], "localhost")
|
||||||
|
.is_err());
|
||||||
|
let mut tampered = leaf.as_ref().to_vec();
|
||||||
|
let last = tampered.len() - 1;
|
||||||
|
tampered[last] ^= 1;
|
||||||
|
assert!(verifier
|
||||||
|
.verify_server_cert(&CertificateDer::from(tampered), &[], "localhost")
|
||||||
|
.is_err());
|
||||||
|
assert!(verifier
|
||||||
|
.verify_server_cert(&CertificateDer::from(vec![0]), &[], "localhost")
|
||||||
|
.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_expired_certificate_and_wrong_extended_key_usage() {
|
||||||
|
let mut expired = CertificateParams::new(vec!["localhost".into()]).unwrap();
|
||||||
|
expired.not_before = rcgen::date_time_ymd(2019, 1, 1);
|
||||||
|
expired.not_after = rcgen::date_time_ymd(2020, 1, 1);
|
||||||
|
let (verifier, leaf) = signed_leaf(expired);
|
||||||
|
assert!(verifier
|
||||||
|
.verify_server_cert(&leaf, &[], "localhost")
|
||||||
|
.is_err());
|
||||||
|
let mut server_only = CertificateParams::new(vec!["localhost".into()]).unwrap();
|
||||||
|
server_only.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
|
||||||
|
let (verifier, leaf) = signed_leaf(server_only);
|
||||||
|
verifier
|
||||||
|
.verify_server_cert(&leaf, &[], "localhost")
|
||||||
|
.unwrap();
|
||||||
|
assert!(verifier.verify_client_cert(&leaf, &[]).is_err());
|
||||||
|
let mut client_only = CertificateParams::new(vec!["localhost".into()]).unwrap();
|
||||||
|
client_only.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
|
||||||
|
let (verifier, leaf) = signed_leaf(client_only);
|
||||||
|
assert!(verifier.verify_client_cert(&leaf, &[]).is_ok());
|
||||||
|
assert!(verifier
|
||||||
|
.verify_server_cert(&leaf, &[], "localhost")
|
||||||
|
.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn does_not_fall_back_to_common_name_or_partial_wildcard() {
|
||||||
|
let mut parameters = CertificateParams::new(Vec::<String>::new()).unwrap();
|
||||||
|
parameters
|
||||||
|
.distinguished_name
|
||||||
|
.push(rcgen::DnType::CommonName, "localhost");
|
||||||
|
let (verifier, leaf) = signed_leaf(parameters);
|
||||||
|
assert!(verifier
|
||||||
|
.verify_server_cert(&leaf, &[], "localhost")
|
||||||
|
.is_err());
|
||||||
|
let (verifier, leaf) =
|
||||||
|
signed_leaf(CertificateParams::new(vec!["local*.example".into()]).unwrap());
|
||||||
|
assert!(verifier
|
||||||
|
.verify_server_cert(&leaf, &[], "localhost.example")
|
||||||
|
.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_weak_rsa_keys_and_sha1_leaf_signatures() {
|
||||||
|
use openssl::asn1::Asn1Time;
|
||||||
|
use openssl::hash::MessageDigest;
|
||||||
|
use openssl::pkey::PKey;
|
||||||
|
use openssl::rsa::Rsa;
|
||||||
|
use openssl::x509::extension::{BasicConstraints, SubjectAlternativeName};
|
||||||
|
use openssl::x509::X509NameBuilder;
|
||||||
|
|
||||||
|
let issuer_key = PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap();
|
||||||
|
let mut issuer_name = X509NameBuilder::new().unwrap();
|
||||||
|
issuer_name
|
||||||
|
.append_entry_by_text("CN", "strong root")
|
||||||
|
.unwrap();
|
||||||
|
let issuer_name = issuer_name.build();
|
||||||
|
let mut issuer = X509::builder().unwrap();
|
||||||
|
issuer.set_version(2).unwrap();
|
||||||
|
issuer.set_subject_name(&issuer_name).unwrap();
|
||||||
|
issuer.set_issuer_name(&issuer_name).unwrap();
|
||||||
|
issuer.set_pubkey(&issuer_key).unwrap();
|
||||||
|
issuer
|
||||||
|
.set_not_before(&Asn1Time::days_from_now(0).unwrap())
|
||||||
|
.unwrap();
|
||||||
|
issuer
|
||||||
|
.set_not_after(&Asn1Time::days_from_now(1).unwrap())
|
||||||
|
.unwrap();
|
||||||
|
issuer
|
||||||
|
.append_extension(BasicConstraints::new().critical().ca().build().unwrap())
|
||||||
|
.unwrap();
|
||||||
|
issuer.sign(&issuer_key, MessageDigest::sha256()).unwrap();
|
||||||
|
let issuer = issuer.build();
|
||||||
|
let mut roots = RootCertStore::empty();
|
||||||
|
roots
|
||||||
|
.add(CertificateDer::from(issuer.to_der().unwrap()))
|
||||||
|
.unwrap();
|
||||||
|
let verifier = CertificateVerifier::new(roots);
|
||||||
|
for (bits, digest, expected) in [
|
||||||
|
(2048, MessageDigest::sha256(), true),
|
||||||
|
(1024, MessageDigest::sha256(), false),
|
||||||
|
(2048, MessageDigest::sha1(), false),
|
||||||
|
] {
|
||||||
|
let leaf_key = PKey::from_rsa(Rsa::generate(bits).unwrap()).unwrap();
|
||||||
|
let mut leaf_name = X509NameBuilder::new().unwrap();
|
||||||
|
leaf_name.append_entry_by_text("CN", "localhost").unwrap();
|
||||||
|
let leaf_name = leaf_name.build();
|
||||||
|
let mut leaf = X509::builder().unwrap();
|
||||||
|
leaf.set_version(2).unwrap();
|
||||||
|
leaf.set_subject_name(&leaf_name).unwrap();
|
||||||
|
leaf.set_issuer_name(issuer.subject_name()).unwrap();
|
||||||
|
leaf.set_pubkey(&leaf_key).unwrap();
|
||||||
|
leaf.set_not_before(&Asn1Time::days_from_now(0).unwrap())
|
||||||
|
.unwrap();
|
||||||
|
leaf.set_not_after(&Asn1Time::days_from_now(1).unwrap())
|
||||||
|
.unwrap();
|
||||||
|
let names = SubjectAlternativeName::new()
|
||||||
|
.dns("localhost")
|
||||||
|
.build(&leaf.x509v3_context(Some(&issuer), None))
|
||||||
|
.unwrap();
|
||||||
|
leaf.append_extension(names).unwrap();
|
||||||
|
leaf.sign(&issuer_key, digest).unwrap();
|
||||||
|
let leaf = CertificateDer::from(leaf.build().to_der().unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
verifier.verify_server_cert(&leaf, &[], "localhost").is_ok(),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn dtls_handshake_preserves_mutual_certificate_authentication() {
|
||||||
|
use crate::config::{ClientAuthType, Config};
|
||||||
|
use crate::conn::DTLSConn;
|
||||||
|
use crate::crypto::Certificate;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
let client_certificate =
|
||||||
|
Certificate::generate_self_signed(vec!["localhost".into()]).unwrap();
|
||||||
|
let server_certificate =
|
||||||
|
Certificate::generate_self_signed(vec!["localhost".into()]).unwrap();
|
||||||
|
let mut client_roots = RootCertStore::empty();
|
||||||
|
client_roots
|
||||||
|
.add(server_certificate.certificate[0].clone())
|
||||||
|
.unwrap();
|
||||||
|
let mut server_roots = RootCertStore::empty();
|
||||||
|
server_roots
|
||||||
|
.add(client_certificate.certificate[0].clone())
|
||||||
|
.unwrap();
|
||||||
|
let client_socket = Arc::new(tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap());
|
||||||
|
let server_socket = Arc::new(tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap());
|
||||||
|
client_socket
|
||||||
|
.connect(server_socket.local_addr().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
server_socket
|
||||||
|
.connect(client_socket.local_addr().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let client_config = Config {
|
||||||
|
certificates: vec![client_certificate],
|
||||||
|
roots_cas: client_roots,
|
||||||
|
server_name: "localhost".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let server_config = Config {
|
||||||
|
certificates: vec![server_certificate],
|
||||||
|
client_cas: server_roots,
|
||||||
|
client_auth: ClientAuthType::RequireAndVerifyClientCert,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let (client, server) = tokio::time::timeout(Duration::from_secs(10), async {
|
||||||
|
tokio::try_join!(
|
||||||
|
DTLSConn::new(client_socket, client_config, true, None),
|
||||||
|
DTLSConn::new(server_socket, server_config, false, None),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
client
|
||||||
|
.write(b"authenticated", Some(Duration::from_secs(5)))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut buffer = [0; 64];
|
||||||
|
let count = server
|
||||||
|
.read(&mut buffer, Some(Duration::from_secs(5)))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(&buffer[..count], b"authenticated");
|
||||||
|
client.close().await.unwrap();
|
||||||
|
server.close().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verifies_intermediate_chain_without_implicitly_trusting_it() {
|
||||||
|
let mut root_parameters = CertificateParams::new(Vec::<String>::new()).unwrap();
|
||||||
|
root_parameters
|
||||||
|
.distinguished_name
|
||||||
|
.push(rcgen::DnType::CommonName, "test root");
|
||||||
|
root_parameters.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
|
||||||
|
let root_key = KeyPair::generate().unwrap();
|
||||||
|
let root = root_parameters.self_signed(&root_key).unwrap();
|
||||||
|
let mut intermediate_parameters = CertificateParams::new(Vec::<String>::new()).unwrap();
|
||||||
|
intermediate_parameters
|
||||||
|
.distinguished_name
|
||||||
|
.push(rcgen::DnType::CommonName, "test intermediate");
|
||||||
|
intermediate_parameters.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
|
||||||
|
let intermediate_key = KeyPair::generate().unwrap();
|
||||||
|
let intermediate = intermediate_parameters
|
||||||
|
.signed_by(&intermediate_key, &root, &root_key)
|
||||||
|
.unwrap();
|
||||||
|
let leaf_key = KeyPair::generate().unwrap();
|
||||||
|
let leaf = CertificateParams::new(vec!["localhost".into()])
|
||||||
|
.unwrap()
|
||||||
|
.signed_by(&leaf_key, &intermediate, &intermediate_key)
|
||||||
|
.unwrap();
|
||||||
|
let mut roots = RootCertStore::empty();
|
||||||
|
roots.add(root.der().clone()).unwrap();
|
||||||
|
let verifier = CertificateVerifier::new(roots);
|
||||||
|
assert!(verifier
|
||||||
|
.verify_server_cert(leaf.der(), &[intermediate.der().clone()], "localhost")
|
||||||
|
.is_ok());
|
||||||
|
assert!(verifier
|
||||||
|
.verify_server_cert(leaf.der(), &[], "localhost")
|
||||||
|
.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
37
libs/dtls/src/change_cipher_spec/change_cipher_spec_test.rs
Normal file
37
libs/dtls/src/change_cipher_spec/change_cipher_spec_test.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
use std::io::{BufReader, BufWriter};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_change_cipher_spec_round_trip() -> Result<()> {
|
||||||
|
let c = ChangeCipherSpec {};
|
||||||
|
let mut raw = vec![];
|
||||||
|
{
|
||||||
|
let mut writer = BufWriter::<&mut Vec<u8>>::new(raw.as_mut());
|
||||||
|
c.marshal(&mut writer)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut reader = BufReader::new(raw.as_slice());
|
||||||
|
let cnew = ChangeCipherSpec::unmarshal(&mut reader)?;
|
||||||
|
assert_eq!(
|
||||||
|
c, cnew,
|
||||||
|
"ChangeCipherSpec round trip: got {cnew:?}, want {c:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_change_cipher_spec_invalid() -> Result<()> {
|
||||||
|
let data = vec![0x00];
|
||||||
|
|
||||||
|
let mut reader = BufReader::new(data.as_slice());
|
||||||
|
let result = ChangeCipherSpec::unmarshal(&mut reader);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => panic!("must be error"),
|
||||||
|
Err(err) => assert_eq!(err.to_string(), Error::ErrInvalidCipherSpec.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
46
libs/dtls/src/change_cipher_spec/mod.rs
Normal file
46
libs/dtls/src/change_cipher_spec/mod.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod change_cipher_spec_test;
|
||||||
|
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
|
use super::content::*;
|
||||||
|
use super::error::*;
|
||||||
|
|
||||||
|
// The change cipher spec protocol exists to signal transitions in
|
||||||
|
// ciphering strategies. The protocol consists of a single message,
|
||||||
|
// which is encrypted and compressed under the current (not the pending)
|
||||||
|
// connection state. The message consists of a single byte of value 1.
|
||||||
|
/// ## Specifications
|
||||||
|
///
|
||||||
|
/// * [RFC 5246 §7.1]
|
||||||
|
///
|
||||||
|
/// [RFC 5246 §7.1]: https://tools.ietf.org/html/rfc5246#section-7.1
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct ChangeCipherSpec;
|
||||||
|
|
||||||
|
impl ChangeCipherSpec {
|
||||||
|
pub fn content_type(&self) -> ContentType {
|
||||||
|
ContentType::ChangeCipherSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn marshal<W: Write>(&self, writer: &mut W) -> Result<()> {
|
||||||
|
writer.write_u8(0x01)?;
|
||||||
|
|
||||||
|
Ok(writer.flush()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unmarshal<R: Read>(reader: &mut R) -> Result<Self> {
|
||||||
|
let data = reader.read_u8()?;
|
||||||
|
if data != 0x01 {
|
||||||
|
return Err(Error::ErrInvalidCipherSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ChangeCipherSpec {})
|
||||||
|
}
|
||||||
|
}
|
||||||
112
libs/dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs
Normal file
112
libs/dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::client_certificate_type::ClientCertificateType;
|
||||||
|
use crate::crypto::crypto_ccm::{CryptoCcm, CryptoCcmTagLen};
|
||||||
|
use crate::prf::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CipherSuiteAes128Ccm {
|
||||||
|
ccm: Option<CryptoCcm>,
|
||||||
|
client_certificate_type: ClientCertificateType,
|
||||||
|
id: CipherSuiteId,
|
||||||
|
psk: bool,
|
||||||
|
crypto_ccm_tag_len: CryptoCcmTagLen,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CipherSuiteAes128Ccm {
|
||||||
|
const PRF_MAC_LEN: usize = 0;
|
||||||
|
const PRF_KEY_LEN: usize = 16;
|
||||||
|
const PRF_IV_LEN: usize = 4;
|
||||||
|
|
||||||
|
pub fn new(
|
||||||
|
client_certificate_type: ClientCertificateType,
|
||||||
|
id: CipherSuiteId,
|
||||||
|
psk: bool,
|
||||||
|
crypto_ccm_tag_len: CryptoCcmTagLen,
|
||||||
|
) -> Self {
|
||||||
|
CipherSuiteAes128Ccm {
|
||||||
|
ccm: None,
|
||||||
|
client_certificate_type,
|
||||||
|
id,
|
||||||
|
psk,
|
||||||
|
crypto_ccm_tag_len,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CipherSuite for CipherSuiteAes128Ccm {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
format!("{}", self.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> CipherSuiteId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn certificate_type(&self) -> ClientCertificateType {
|
||||||
|
self.client_certificate_type
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_func(&self) -> CipherSuiteHash {
|
||||||
|
CipherSuiteHash::Sha256
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_psk(&self) -> bool {
|
||||||
|
self.psk
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_initialized(&self) -> bool {
|
||||||
|
self.ccm.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(
|
||||||
|
&mut self,
|
||||||
|
master_secret: &[u8],
|
||||||
|
client_random: &[u8],
|
||||||
|
server_random: &[u8],
|
||||||
|
is_client: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let keys = prf_encryption_keys(
|
||||||
|
master_secret,
|
||||||
|
client_random,
|
||||||
|
server_random,
|
||||||
|
CipherSuiteAes128Ccm::PRF_MAC_LEN,
|
||||||
|
CipherSuiteAes128Ccm::PRF_KEY_LEN,
|
||||||
|
CipherSuiteAes128Ccm::PRF_IV_LEN,
|
||||||
|
self.hash_func(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if is_client {
|
||||||
|
self.ccm = Some(CryptoCcm::new(
|
||||||
|
&self.crypto_ccm_tag_len,
|
||||||
|
&keys.client_write_key,
|
||||||
|
&keys.client_write_iv,
|
||||||
|
&keys.server_write_key,
|
||||||
|
&keys.server_write_iv,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
self.ccm = Some(CryptoCcm::new(
|
||||||
|
&self.crypto_ccm_tag_len,
|
||||||
|
&keys.server_write_key,
|
||||||
|
&keys.server_write_iv,
|
||||||
|
&keys.client_write_key,
|
||||||
|
&keys.client_write_iv,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let ccm = self.ccm.as_ref().ok_or(Error::Other(
|
||||||
|
"CipherSuite has not been initialized, unable to encrypt".to_owned(),
|
||||||
|
))?;
|
||||||
|
ccm.encrypt(pkt_rlh, raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt(&self, input: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let ccm = self.ccm.as_ref().ok_or(Error::Other(
|
||||||
|
"CipherSuite has not been initialized, unable to decrypt".to_owned(),
|
||||||
|
))?;
|
||||||
|
ccm.decrypt(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
107
libs/dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs
Normal file
107
libs/dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::crypto::crypto_gcm::*;
|
||||||
|
use crate::prf::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CipherSuiteAes128GcmSha256 {
|
||||||
|
gcm: Option<CryptoGcm>,
|
||||||
|
rsa: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CipherSuiteAes128GcmSha256 {
|
||||||
|
const PRF_MAC_LEN: usize = 0;
|
||||||
|
const PRF_KEY_LEN: usize = 16;
|
||||||
|
const PRF_IV_LEN: usize = 4;
|
||||||
|
|
||||||
|
pub fn new(rsa: bool) -> Self {
|
||||||
|
CipherSuiteAes128GcmSha256 { gcm: None, rsa }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CipherSuite for CipherSuiteAes128GcmSha256 {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
if self.rsa {
|
||||||
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256".to_owned()
|
||||||
|
} else {
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> CipherSuiteId {
|
||||||
|
if self.rsa {
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256
|
||||||
|
} else {
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn certificate_type(&self) -> ClientCertificateType {
|
||||||
|
if self.rsa {
|
||||||
|
ClientCertificateType::RsaSign
|
||||||
|
} else {
|
||||||
|
ClientCertificateType::EcdsaSign
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_func(&self) -> CipherSuiteHash {
|
||||||
|
CipherSuiteHash::Sha256
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_psk(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_initialized(&self) -> bool {
|
||||||
|
self.gcm.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(
|
||||||
|
&mut self,
|
||||||
|
master_secret: &[u8],
|
||||||
|
client_random: &[u8],
|
||||||
|
server_random: &[u8],
|
||||||
|
is_client: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let keys = prf_encryption_keys(
|
||||||
|
master_secret,
|
||||||
|
client_random,
|
||||||
|
server_random,
|
||||||
|
CipherSuiteAes128GcmSha256::PRF_MAC_LEN,
|
||||||
|
CipherSuiteAes128GcmSha256::PRF_KEY_LEN,
|
||||||
|
CipherSuiteAes128GcmSha256::PRF_IV_LEN,
|
||||||
|
self.hash_func(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if is_client {
|
||||||
|
self.gcm = Some(CryptoGcm::new(
|
||||||
|
&keys.client_write_key,
|
||||||
|
&keys.client_write_iv,
|
||||||
|
&keys.server_write_key,
|
||||||
|
&keys.server_write_iv,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
self.gcm = Some(CryptoGcm::new(
|
||||||
|
&keys.server_write_key,
|
||||||
|
&keys.server_write_iv,
|
||||||
|
&keys.client_write_key,
|
||||||
|
&keys.client_write_iv,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let cg = self.gcm.as_ref().ok_or(Error::Other(
|
||||||
|
"CipherSuite has not been initialized, unable to encrypt".to_owned(),
|
||||||
|
))?;
|
||||||
|
cg.encrypt(pkt_rlh, raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt(&self, input: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let cg = self.gcm.as_ref().ok_or(Error::Other(
|
||||||
|
"CipherSuite has not been initialized, unable to decrypt".to_owned(),
|
||||||
|
))?;
|
||||||
|
cg.decrypt(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
107
libs/dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs
Normal file
107
libs/dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::crypto::crypto_cbc::*;
|
||||||
|
use crate::prf::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CipherSuiteAes256CbcSha {
|
||||||
|
cbc: Option<CryptoCbc>,
|
||||||
|
rsa: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CipherSuiteAes256CbcSha {
|
||||||
|
const PRF_MAC_LEN: usize = 20;
|
||||||
|
const PRF_KEY_LEN: usize = 32;
|
||||||
|
const PRF_IV_LEN: usize = 16;
|
||||||
|
|
||||||
|
pub fn new(rsa: bool) -> Self {
|
||||||
|
CipherSuiteAes256CbcSha { cbc: None, rsa }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CipherSuite for CipherSuiteAes256CbcSha {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
if self.rsa {
|
||||||
|
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA".to_owned()
|
||||||
|
} else {
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> CipherSuiteId {
|
||||||
|
if self.rsa {
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha
|
||||||
|
} else {
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn certificate_type(&self) -> ClientCertificateType {
|
||||||
|
if self.rsa {
|
||||||
|
ClientCertificateType::RsaSign
|
||||||
|
} else {
|
||||||
|
ClientCertificateType::EcdsaSign
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_func(&self) -> CipherSuiteHash {
|
||||||
|
CipherSuiteHash::Sha256
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_psk(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_initialized(&self) -> bool {
|
||||||
|
self.cbc.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(
|
||||||
|
&mut self,
|
||||||
|
master_secret: &[u8],
|
||||||
|
client_random: &[u8],
|
||||||
|
server_random: &[u8],
|
||||||
|
is_client: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let keys = prf_encryption_keys(
|
||||||
|
master_secret,
|
||||||
|
client_random,
|
||||||
|
server_random,
|
||||||
|
CipherSuiteAes256CbcSha::PRF_MAC_LEN,
|
||||||
|
CipherSuiteAes256CbcSha::PRF_KEY_LEN,
|
||||||
|
CipherSuiteAes256CbcSha::PRF_IV_LEN,
|
||||||
|
self.hash_func(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if is_client {
|
||||||
|
self.cbc = Some(CryptoCbc::new(
|
||||||
|
&keys.client_write_key,
|
||||||
|
&keys.client_mac_key,
|
||||||
|
&keys.server_write_key,
|
||||||
|
&keys.server_mac_key,
|
||||||
|
)?);
|
||||||
|
} else {
|
||||||
|
self.cbc = Some(CryptoCbc::new(
|
||||||
|
&keys.server_write_key,
|
||||||
|
&keys.server_mac_key,
|
||||||
|
&keys.client_write_key,
|
||||||
|
&keys.client_mac_key,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let cg = self.cbc.as_ref().ok_or(Error::Other(
|
||||||
|
"CipherSuite has not been initialized, unable to encrypt".to_owned(),
|
||||||
|
))?;
|
||||||
|
cg.encrypt(pkt_rlh, raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt(&self, input: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let cg = self.cbc.as_ref().ok_or(Error::Other(
|
||||||
|
"CipherSuite has not been initialized, unable to decrypt".to_owned(),
|
||||||
|
))?;
|
||||||
|
cg.decrypt(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::crypto::crypto_chacha20::*;
|
||||||
|
use crate::prf::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CipherSuiteChaCha20Poly1305Sha256 {
|
||||||
|
rsa: bool,
|
||||||
|
cipher: Option<CryptoChaCha20>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CipherSuiteChaCha20Poly1305Sha256 {
|
||||||
|
const PRF_MAC_LEN: usize = 0;
|
||||||
|
const PRF_KEY_LEN: usize = 32;
|
||||||
|
const PRF_IV_LEN: usize = 12;
|
||||||
|
|
||||||
|
pub fn new(rsa: bool) -> Self {
|
||||||
|
CipherSuiteChaCha20Poly1305Sha256 { rsa, cipher: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CipherSuite for CipherSuiteChaCha20Poly1305Sha256 {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
if self.rsa {
|
||||||
|
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256".to_owned()
|
||||||
|
} else {
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> CipherSuiteId {
|
||||||
|
if self.rsa {
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256
|
||||||
|
} else {
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn certificate_type(&self) -> ClientCertificateType {
|
||||||
|
if self.rsa {
|
||||||
|
ClientCertificateType::RsaSign
|
||||||
|
} else {
|
||||||
|
ClientCertificateType::EcdsaSign
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_func(&self) -> CipherSuiteHash {
|
||||||
|
CipherSuiteHash::Sha256
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_psk(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_initialized(&self) -> bool {
|
||||||
|
self.cipher.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(
|
||||||
|
&mut self,
|
||||||
|
master_secret: &[u8],
|
||||||
|
client_random: &[u8],
|
||||||
|
server_random: &[u8],
|
||||||
|
is_client: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let keys = prf_encryption_keys(
|
||||||
|
master_secret,
|
||||||
|
client_random,
|
||||||
|
server_random,
|
||||||
|
CipherSuiteChaCha20Poly1305Sha256::PRF_MAC_LEN,
|
||||||
|
CipherSuiteChaCha20Poly1305Sha256::PRF_KEY_LEN,
|
||||||
|
CipherSuiteChaCha20Poly1305Sha256::PRF_IV_LEN,
|
||||||
|
self.hash_func(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if is_client {
|
||||||
|
self.cipher = Some(CryptoChaCha20::new(
|
||||||
|
&keys.client_write_key,
|
||||||
|
&keys.client_write_iv,
|
||||||
|
&keys.server_write_key,
|
||||||
|
&keys.server_write_iv,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
self.cipher = Some(CryptoChaCha20::new(
|
||||||
|
&keys.server_write_key,
|
||||||
|
&keys.server_write_iv,
|
||||||
|
&keys.client_write_key,
|
||||||
|
&keys.client_write_iv,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let cg = self.cipher.as_ref().ok_or(Error::Other(
|
||||||
|
"CipherSuite has not been initialized, unable to encrypt".to_owned(),
|
||||||
|
))?;
|
||||||
|
cg.encrypt(pkt_rlh, raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt(&self, input: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let cg = self.cipher.as_ref().ok_or(Error::Other(
|
||||||
|
"CipherSuite has not been initialized, unable to decrypt".to_owned(),
|
||||||
|
))?;
|
||||||
|
cg.decrypt(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm;
|
||||||
|
use crate::crypto::crypto_ccm::CryptoCcmTagLen;
|
||||||
|
|
||||||
|
pub fn new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm() -> CipherSuiteAes128Ccm {
|
||||||
|
CipherSuiteAes128Ccm::new(
|
||||||
|
ClientCertificateType::EcdsaSign,
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm,
|
||||||
|
false,
|
||||||
|
CryptoCcmTagLen::CryptoCcmTagLength,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm;
|
||||||
|
use crate::crypto::crypto_ccm::CryptoCcmTagLen;
|
||||||
|
|
||||||
|
pub fn new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8() -> CipherSuiteAes128Ccm {
|
||||||
|
CipherSuiteAes128Ccm::new(
|
||||||
|
ClientCertificateType::EcdsaSign,
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8,
|
||||||
|
false,
|
||||||
|
CryptoCcmTagLen::CryptoCcm8TagLength,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm;
|
||||||
|
use crate::crypto::crypto_ccm::CryptoCcmTagLen;
|
||||||
|
|
||||||
|
pub fn new_cipher_suite_tls_psk_with_aes_128_ccm() -> CipherSuiteAes128Ccm {
|
||||||
|
CipherSuiteAes128Ccm::new(
|
||||||
|
ClientCertificateType::Unsupported,
|
||||||
|
CipherSuiteId::Tls_Psk_With_Aes_128_Ccm,
|
||||||
|
true,
|
||||||
|
CryptoCcmTagLen::CryptoCcmTagLength,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm;
|
||||||
|
use crate::crypto::crypto_ccm::CryptoCcmTagLen;
|
||||||
|
|
||||||
|
pub fn new_cipher_suite_tls_psk_with_aes_128_ccm8() -> CipherSuiteAes128Ccm {
|
||||||
|
CipherSuiteAes128Ccm::new(
|
||||||
|
ClientCertificateType::Unsupported,
|
||||||
|
CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8,
|
||||||
|
true,
|
||||||
|
CryptoCcmTagLen::CryptoCcm8TagLength,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::crypto::crypto_gcm::*;
|
||||||
|
use crate::prf::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct CipherSuiteTlsPskWithAes128GcmSha256 {
|
||||||
|
gcm: Option<CryptoGcm>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CipherSuiteTlsPskWithAes128GcmSha256 {
|
||||||
|
const PRF_MAC_LEN: usize = 0;
|
||||||
|
const PRF_KEY_LEN: usize = 16;
|
||||||
|
const PRF_IV_LEN: usize = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CipherSuite for CipherSuiteTlsPskWithAes128GcmSha256 {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
"TLS_PSK_WITH_AES_128_GCM_SHA256".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> CipherSuiteId {
|
||||||
|
CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256
|
||||||
|
}
|
||||||
|
|
||||||
|
fn certificate_type(&self) -> ClientCertificateType {
|
||||||
|
ClientCertificateType::Unsupported
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_func(&self) -> CipherSuiteHash {
|
||||||
|
CipherSuiteHash::Sha256
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_psk(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_initialized(&self) -> bool {
|
||||||
|
self.gcm.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(
|
||||||
|
&mut self,
|
||||||
|
master_secret: &[u8],
|
||||||
|
client_random: &[u8],
|
||||||
|
server_random: &[u8],
|
||||||
|
is_client: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let keys = prf_encryption_keys(
|
||||||
|
master_secret,
|
||||||
|
client_random,
|
||||||
|
server_random,
|
||||||
|
CipherSuiteTlsPskWithAes128GcmSha256::PRF_MAC_LEN,
|
||||||
|
CipherSuiteTlsPskWithAes128GcmSha256::PRF_KEY_LEN,
|
||||||
|
CipherSuiteTlsPskWithAes128GcmSha256::PRF_IV_LEN,
|
||||||
|
self.hash_func(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if is_client {
|
||||||
|
self.gcm = Some(CryptoGcm::new(
|
||||||
|
&keys.client_write_key,
|
||||||
|
&keys.client_write_iv,
|
||||||
|
&keys.server_write_key,
|
||||||
|
&keys.server_write_iv,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
self.gcm = Some(CryptoGcm::new(
|
||||||
|
&keys.server_write_key,
|
||||||
|
&keys.server_write_iv,
|
||||||
|
&keys.client_write_key,
|
||||||
|
&keys.client_write_iv,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let cg = self.gcm.as_ref().ok_or(Error::Other(
|
||||||
|
"CipherSuite has not been initialized, unable to encrypt".to_owned(),
|
||||||
|
))?;
|
||||||
|
cg.encrypt(pkt_rlh, raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt(&self, input: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let cg = self.gcm.as_ref().ok_or(Error::Other(
|
||||||
|
"CipherSuite has not been initialized, unable to decrypt".to_owned(),
|
||||||
|
))?;
|
||||||
|
cg.decrypt(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
287
libs/dtls/src/cipher_suite/mod.rs
Normal file
287
libs/dtls/src/cipher_suite/mod.rs
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
pub mod cipher_suite_aes_128_ccm;
|
||||||
|
pub mod cipher_suite_aes_128_gcm_sha256;
|
||||||
|
pub mod cipher_suite_aes_256_cbc_sha;
|
||||||
|
pub mod cipher_suite_chacha20_poly1305_sha256;
|
||||||
|
pub mod cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm;
|
||||||
|
pub mod cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8;
|
||||||
|
pub mod cipher_suite_tls_psk_with_aes_128_ccm;
|
||||||
|
pub mod cipher_suite_tls_psk_with_aes_128_ccm8;
|
||||||
|
pub mod cipher_suite_tls_psk_with_aes_128_gcm_sha256;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
use std::marker::{Send, Sync};
|
||||||
|
|
||||||
|
use cipher_suite_aes_128_gcm_sha256::*;
|
||||||
|
use cipher_suite_aes_256_cbc_sha::*;
|
||||||
|
use cipher_suite_chacha20_poly1305_sha256::*;
|
||||||
|
use cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm::*;
|
||||||
|
use cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8::*;
|
||||||
|
use cipher_suite_tls_psk_with_aes_128_ccm::*;
|
||||||
|
use cipher_suite_tls_psk_with_aes_128_ccm8::*;
|
||||||
|
use cipher_suite_tls_psk_with_aes_128_gcm_sha256::*;
|
||||||
|
|
||||||
|
use super::client_certificate_type::*;
|
||||||
|
use super::error::*;
|
||||||
|
use super::record_layer::record_layer_header::*;
|
||||||
|
|
||||||
|
// CipherSuiteID is an ID for our supported CipherSuites
|
||||||
|
// Supported Cipher Suites
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum CipherSuiteId {
|
||||||
|
// AES-128-CCM
|
||||||
|
Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm = 0xc0ac,
|
||||||
|
Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 = 0xc0ae,
|
||||||
|
|
||||||
|
// AES-128-GCM-SHA256
|
||||||
|
Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 = 0xc02b,
|
||||||
|
Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 = 0xc02f,
|
||||||
|
|
||||||
|
// AES-256-CBC-SHA
|
||||||
|
Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha = 0xc00a,
|
||||||
|
Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha = 0xc014,
|
||||||
|
|
||||||
|
Tls_Psk_With_Aes_128_Ccm = 0xc0a4,
|
||||||
|
Tls_Psk_With_Aes_128_Ccm_8 = 0xc0a8,
|
||||||
|
Tls_Psk_With_Aes_128_Gcm_Sha256 = 0x00a8,
|
||||||
|
|
||||||
|
// CHACHA20_POLY1305_SHA256
|
||||||
|
Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 = 0xcca8,
|
||||||
|
Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256 = 0xcca9,
|
||||||
|
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CipherSuiteId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm => {
|
||||||
|
write!(f, "TLS_ECDHE_ECDSA_WITH_AES_128_CCM")
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 => {
|
||||||
|
write!(f, "TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8")
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 => {
|
||||||
|
write!(f, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256")
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 => {
|
||||||
|
write!(f, "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256")
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha => {
|
||||||
|
write!(f, "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA")
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha => {
|
||||||
|
write!(f, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA")
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Psk_With_Aes_128_Ccm => write!(f, "TLS_PSK_WITH_AES_128_CCM"),
|
||||||
|
CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8 => write!(f, "TLS_PSK_WITH_AES_128_CCM_8"),
|
||||||
|
CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256 => {
|
||||||
|
write!(f, "TLS_PSK_WITH_AES_128_GCM_SHA256")
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 => {
|
||||||
|
write!(f, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256")
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256 => {
|
||||||
|
write!(f, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256")
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => write!(f, "Unsupported CipherSuiteID"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u16> for CipherSuiteId {
|
||||||
|
fn from(val: u16) -> Self {
|
||||||
|
match val {
|
||||||
|
// AES-128-CCM
|
||||||
|
0xc0ac => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm,
|
||||||
|
0xc0ae => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8,
|
||||||
|
|
||||||
|
// AES-128-GCM-SHA256
|
||||||
|
0xc02b => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256,
|
||||||
|
0xc02f => CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256,
|
||||||
|
|
||||||
|
// AES-256-CBC-SHA
|
||||||
|
0xc00a => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha,
|
||||||
|
0xc014 => CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha,
|
||||||
|
|
||||||
|
0xc0a4 => CipherSuiteId::Tls_Psk_With_Aes_128_Ccm,
|
||||||
|
0xc0a8 => CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8,
|
||||||
|
0x00a8 => CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256,
|
||||||
|
|
||||||
|
// CHACHA20_POLY1305_SHA256
|
||||||
|
0xcca8 => CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256,
|
||||||
|
0xcca9 => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256,
|
||||||
|
|
||||||
|
_ => CipherSuiteId::Unsupported,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for CipherSuiteId {
|
||||||
|
fn from(val: &str) -> Self {
|
||||||
|
match val {
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_AES_128_CCM" => CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm,
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8" => {
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8
|
||||||
|
}
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" => {
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256
|
||||||
|
}
|
||||||
|
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" => {
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256
|
||||||
|
}
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA" => {
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha
|
||||||
|
}
|
||||||
|
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" => {
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha
|
||||||
|
}
|
||||||
|
"TLS_PSK_WITH_AES_128_CCM" => CipherSuiteId::Tls_Psk_With_Aes_128_Ccm,
|
||||||
|
"TLS_PSK_WITH_AES_128_CCM_8" => CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8,
|
||||||
|
"TLS_PSK_WITH_AES_128_GCM_SHA256" => CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256,
|
||||||
|
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" => {
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256
|
||||||
|
}
|
||||||
|
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" => {
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256
|
||||||
|
}
|
||||||
|
_ => CipherSuiteId::Unsupported,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub enum CipherSuiteHash {
|
||||||
|
Sha256,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CipherSuiteHash {
|
||||||
|
pub(crate) fn size(&self) -> usize {
|
||||||
|
match *self {
|
||||||
|
CipherSuiteHash::Sha256 => 32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait CipherSuite {
|
||||||
|
fn to_string(&self) -> String;
|
||||||
|
fn id(&self) -> CipherSuiteId;
|
||||||
|
fn certificate_type(&self) -> ClientCertificateType;
|
||||||
|
fn hash_func(&self) -> CipherSuiteHash;
|
||||||
|
fn is_psk(&self) -> bool;
|
||||||
|
fn is_initialized(&self) -> bool;
|
||||||
|
|
||||||
|
// Generate the internal encryption state
|
||||||
|
fn init(
|
||||||
|
&mut self,
|
||||||
|
master_secret: &[u8],
|
||||||
|
client_random: &[u8],
|
||||||
|
server_random: &[u8],
|
||||||
|
is_client: bool,
|
||||||
|
) -> Result<()>;
|
||||||
|
|
||||||
|
fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>>;
|
||||||
|
fn decrypt(&self, input: &[u8]) -> Result<Vec<u8>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taken from https://www.iana.org/assignments/tls-parameters/tls-parameters.xml
|
||||||
|
// A cipher_suite is a specific combination of key agreement, cipher and MAC
|
||||||
|
// function.
|
||||||
|
pub fn cipher_suite_for_id(id: CipherSuiteId) -> Result<Box<dyn CipherSuite + Send + Sync>> {
|
||||||
|
match id {
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm => {
|
||||||
|
Ok(Box::new(new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm()))
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 => Ok(Box::new(
|
||||||
|
new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8(),
|
||||||
|
)),
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 => {
|
||||||
|
Ok(Box::new(CipherSuiteAes128GcmSha256::new(false)))
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 => {
|
||||||
|
Ok(Box::new(CipherSuiteAes128GcmSha256::new(true)))
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha => {
|
||||||
|
Ok(Box::new(CipherSuiteAes256CbcSha::new(true)))
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha => {
|
||||||
|
Ok(Box::new(CipherSuiteAes256CbcSha::new(false)))
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Psk_With_Aes_128_Ccm => {
|
||||||
|
Ok(Box::new(new_cipher_suite_tls_psk_with_aes_128_ccm()))
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8 => {
|
||||||
|
Ok(Box::new(new_cipher_suite_tls_psk_with_aes_128_ccm8()))
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256 => {
|
||||||
|
Ok(Box::<CipherSuiteTlsPskWithAes128GcmSha256>::default())
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 => {
|
||||||
|
Ok(Box::new(CipherSuiteChaCha20Poly1305Sha256::new(true)))
|
||||||
|
}
|
||||||
|
CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256 => {
|
||||||
|
Ok(Box::new(CipherSuiteChaCha20Poly1305Sha256::new(false)))
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => Err(Error::ErrInvalidCipherSuite),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CipherSuites we support in order of preference
|
||||||
|
pub(crate) fn default_cipher_suites() -> Vec<Box<dyn CipherSuite + Send + Sync>> {
|
||||||
|
vec![
|
||||||
|
Box::new(CipherSuiteAes128GcmSha256::new(false)),
|
||||||
|
Box::new(CipherSuiteAes256CbcSha::new(false)),
|
||||||
|
Box::new(CipherSuiteAes128GcmSha256::new(true)),
|
||||||
|
Box::new(CipherSuiteAes256CbcSha::new(true)),
|
||||||
|
Box::new(CipherSuiteChaCha20Poly1305Sha256::new(false)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_cipher_suites() -> Vec<Box<dyn CipherSuite + Send + Sync>> {
|
||||||
|
vec![
|
||||||
|
Box::new(new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm()),
|
||||||
|
Box::new(new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8()),
|
||||||
|
Box::new(CipherSuiteAes128GcmSha256::new(false)),
|
||||||
|
Box::new(CipherSuiteAes128GcmSha256::new(true)),
|
||||||
|
Box::new(CipherSuiteAes256CbcSha::new(false)),
|
||||||
|
Box::new(CipherSuiteAes256CbcSha::new(true)),
|
||||||
|
Box::new(new_cipher_suite_tls_psk_with_aes_128_ccm()),
|
||||||
|
Box::new(new_cipher_suite_tls_psk_with_aes_128_ccm8()),
|
||||||
|
Box::<CipherSuiteTlsPskWithAes128GcmSha256>::default(),
|
||||||
|
Box::new(CipherSuiteChaCha20Poly1305Sha256::new(false)),
|
||||||
|
Box::new(CipherSuiteChaCha20Poly1305Sha256::new(true)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cipher_suites_for_ids(ids: &[CipherSuiteId]) -> Result<Vec<Box<dyn CipherSuite + Send + Sync>>> {
|
||||||
|
let mut cipher_suites = vec![];
|
||||||
|
for id in ids {
|
||||||
|
cipher_suites.push(cipher_suite_for_id(*id)?);
|
||||||
|
}
|
||||||
|
Ok(cipher_suites)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_cipher_suites(
|
||||||
|
user_selected_suites: &[CipherSuiteId],
|
||||||
|
exclude_psk: bool,
|
||||||
|
exclude_non_psk: bool,
|
||||||
|
) -> Result<Vec<Box<dyn CipherSuite + Send + Sync>>> {
|
||||||
|
let cipher_suites = if !user_selected_suites.is_empty() {
|
||||||
|
cipher_suites_for_ids(user_selected_suites)?
|
||||||
|
} else {
|
||||||
|
default_cipher_suites()
|
||||||
|
};
|
||||||
|
|
||||||
|
let filtered_cipher_suites: Vec<Box<dyn CipherSuite + Send + Sync>> = cipher_suites
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| !((exclude_psk && c.is_psk()) || (exclude_non_psk && !c.is_psk())))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if filtered_cipher_suites.is_empty() {
|
||||||
|
Err(Error::ErrNoAvailableCipherSuites)
|
||||||
|
} else {
|
||||||
|
Ok(filtered_cipher_suites)
|
||||||
|
}
|
||||||
|
}
|
||||||
16
libs/dtls/src/client_certificate_type.rs
Normal file
16
libs/dtls/src/client_certificate_type.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum ClientCertificateType {
|
||||||
|
RsaSign = 1,
|
||||||
|
EcdsaSign = 64,
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for ClientCertificateType {
|
||||||
|
fn from(val: u8) -> Self {
|
||||||
|
match val {
|
||||||
|
1 => ClientCertificateType::RsaSign,
|
||||||
|
64 => ClientCertificateType::EcdsaSign,
|
||||||
|
_ => ClientCertificateType::Unsupported,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
libs/dtls/src/compression_methods.rs
Normal file
60
libs/dtls/src/compression_methods.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum CompressionMethodId {
|
||||||
|
Null = 0,
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for CompressionMethodId {
|
||||||
|
fn from(val: u8) -> Self {
|
||||||
|
match val {
|
||||||
|
0 => CompressionMethodId::Null,
|
||||||
|
_ => CompressionMethodId::Unsupported,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct CompressionMethods {
|
||||||
|
pub ids: Vec<CompressionMethodId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompressionMethods {
|
||||||
|
pub fn size(&self) -> usize {
|
||||||
|
1 + self.ids.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn marshal<W: Write>(&self, writer: &mut W) -> Result<()> {
|
||||||
|
writer.write_u8(self.ids.len() as u8)?;
|
||||||
|
|
||||||
|
for id in &self.ids {
|
||||||
|
writer.write_u8(*id as u8)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(writer.flush()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unmarshal<R: Read>(reader: &mut R) -> Result<Self> {
|
||||||
|
let compression_methods_count = reader.read_u8()? as usize;
|
||||||
|
let mut ids = vec![];
|
||||||
|
for _ in 0..compression_methods_count {
|
||||||
|
let id = reader.read_u8()?.into();
|
||||||
|
if id != CompressionMethodId::Unsupported {
|
||||||
|
ids.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(CompressionMethods { ids })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_compression_methods() -> CompressionMethods {
|
||||||
|
CompressionMethods {
|
||||||
|
ids: vec![CompressionMethodId::Null],
|
||||||
|
}
|
||||||
|
}
|
||||||
193
libs/dtls/src/config.rs
Normal file
193
libs/dtls/src/config.rs
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use tokio::time::Duration;
|
||||||
|
|
||||||
|
use crate::cipher_suite::*;
|
||||||
|
use crate::crypto::*;
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::extension::extension_use_srtp::SrtpProtectionProfile;
|
||||||
|
use crate::handshaker::VerifyPeerCertificateFn;
|
||||||
|
use crate::signature_hash_algorithm::SignatureScheme;
|
||||||
|
|
||||||
|
/// Config is used to configure a DTLS client or server.
|
||||||
|
/// After a Config is passed to a DTLS function it must not be modified.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
/// certificates contains certificate chain to present to the other side of the connection.
|
||||||
|
/// Server MUST set this if psk is non-nil
|
||||||
|
/// client SHOULD sets this so CertificateRequests can be handled if psk is non-nil
|
||||||
|
pub certificates: Vec<Certificate>,
|
||||||
|
|
||||||
|
/// cipher_suites is a list of supported cipher suites.
|
||||||
|
/// If cipher_suites is nil, a default list is used
|
||||||
|
pub cipher_suites: Vec<CipherSuiteId>,
|
||||||
|
|
||||||
|
/// signature_schemes contains the signature and hash schemes that the peer requests to verify.
|
||||||
|
pub signature_schemes: Vec<SignatureScheme>,
|
||||||
|
|
||||||
|
/// srtp_protection_profiles are the supported protection profiles
|
||||||
|
/// Clients will send this via use_srtp and assert that the server properly responds
|
||||||
|
/// Servers will assert that clients send one of these profiles and will respond as needed
|
||||||
|
pub srtp_protection_profiles: Vec<SrtpProtectionProfile>,
|
||||||
|
|
||||||
|
/// client_auth determines the server's policy for
|
||||||
|
/// TLS Client Authentication. The default is NoClientCert.
|
||||||
|
pub client_auth: ClientAuthType,
|
||||||
|
|
||||||
|
/// extended_master_secret determines if the "Extended Master Secret" extension
|
||||||
|
/// should be disabled, requested, or required (default requested).
|
||||||
|
pub extended_master_secret: ExtendedMasterSecretType,
|
||||||
|
|
||||||
|
/// flight_interval controls how often we send outbound handshake messages
|
||||||
|
/// defaults to time.Second
|
||||||
|
pub flight_interval: Duration,
|
||||||
|
|
||||||
|
/// psk sets the pre-shared key used by this DTLS connection
|
||||||
|
/// If psk is non-nil only psk cipher_suites will be used
|
||||||
|
pub psk: Option<PskCallback>,
|
||||||
|
pub psk_identity_hint: Option<Vec<u8>>,
|
||||||
|
|
||||||
|
/// insecure_skip_verify controls whether a client verifies the
|
||||||
|
/// server's certificate chain and host name.
|
||||||
|
/// If insecure_skip_verify is true, TLS accepts any certificate
|
||||||
|
/// presented by the server and any host name in that certificate.
|
||||||
|
/// In this mode, TLS is susceptible to man-in-the-middle attacks.
|
||||||
|
/// This should be used only for testing.
|
||||||
|
pub insecure_skip_verify: bool,
|
||||||
|
|
||||||
|
/// insecure_hashes allows the use of hashing algorithms that are known
|
||||||
|
/// to be vulnerable.
|
||||||
|
pub insecure_hashes: bool,
|
||||||
|
|
||||||
|
/// insecure_verification allows the use of verification algorithms that are
|
||||||
|
/// known to be vulnerable or deprecated
|
||||||
|
pub insecure_verification: bool,
|
||||||
|
/// VerifyPeerCertificate, if not nil, is called after normal
|
||||||
|
/// certificate verification by either a client or server. It
|
||||||
|
/// receives the certificate provided by the peer and also a flag
|
||||||
|
/// that tells if normal verification has succeeded. If it returns a
|
||||||
|
/// non-nil error, the handshake is aborted and that error results.
|
||||||
|
///
|
||||||
|
/// If normal verification fails then the handshake will abort before
|
||||||
|
/// considering this callback. If normal verification is disabled by
|
||||||
|
/// setting insecure_skip_verify, or (for a server) when client_auth is
|
||||||
|
/// RequestClientCert or RequireAnyClientCert, then this callback will
|
||||||
|
/// be considered but the verifiedChains will always be nil.
|
||||||
|
pub verify_peer_certificate: Option<VerifyPeerCertificateFn>,
|
||||||
|
|
||||||
|
/// roots_cas defines the set of root certificate authorities
|
||||||
|
/// that one peer uses when verifying the other peer's certificates.
|
||||||
|
/// If RootCAs is nil, TLS uses the host's root CA set.
|
||||||
|
/// Used by Client to verify server's certificate
|
||||||
|
pub roots_cas: crate::certificate_verifier::RootCertStore,
|
||||||
|
|
||||||
|
/// client_cas defines the set of root certificate authorities
|
||||||
|
/// that servers use if required to verify a client certificate
|
||||||
|
/// by the policy in client_auth.
|
||||||
|
/// Used by Server to verify client's certificate
|
||||||
|
pub client_cas: crate::certificate_verifier::RootCertStore,
|
||||||
|
|
||||||
|
/// server_name is used to verify the hostname on the returned
|
||||||
|
/// certificates unless insecure_skip_verify is given.
|
||||||
|
pub server_name: String,
|
||||||
|
|
||||||
|
/// mtu is the length at which handshake messages will be fragmented to
|
||||||
|
/// fit within the maximum transmission unit (default is 1200 bytes)
|
||||||
|
pub mtu: usize,
|
||||||
|
|
||||||
|
/// replay_protection_window is the size of the replay attack protection window.
|
||||||
|
/// Duplication of the sequence number is checked in this window size.
|
||||||
|
/// Packet with sequence number older than this value compared to the latest
|
||||||
|
/// accepted packet will be discarded. (default is 64)
|
||||||
|
pub replay_protection_window: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Config {
|
||||||
|
certificates: vec![],
|
||||||
|
cipher_suites: vec![],
|
||||||
|
signature_schemes: vec![],
|
||||||
|
srtp_protection_profiles: vec![],
|
||||||
|
client_auth: ClientAuthType::default(),
|
||||||
|
extended_master_secret: ExtendedMasterSecretType::default(),
|
||||||
|
flight_interval: Duration::default(),
|
||||||
|
psk: None,
|
||||||
|
psk_identity_hint: None,
|
||||||
|
insecure_skip_verify: false,
|
||||||
|
insecure_hashes: false,
|
||||||
|
insecure_verification: false,
|
||||||
|
verify_peer_certificate: None,
|
||||||
|
roots_cas: crate::certificate_verifier::RootCertStore::empty(),
|
||||||
|
client_cas: crate::certificate_verifier::RootCertStore::empty(),
|
||||||
|
server_name: String::default(),
|
||||||
|
mtu: 0,
|
||||||
|
replay_protection_window: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const DEFAULT_MTU: usize = 1200; // bytes
|
||||||
|
|
||||||
|
// PSKCallback is called once we have the remote's psk_identity_hint.
|
||||||
|
// If the remote provided none it will be nil
|
||||||
|
pub(crate) type PskCallback =
|
||||||
|
Arc<dyn (Fn(&[u8]) -> Pin<Box<dyn Future<Output = Result<Vec<u8>>> + Send>>) + Send + Sync>;
|
||||||
|
|
||||||
|
// ClientAuthType declares the policy the server will follow for
|
||||||
|
// TLS Client Authentication.
|
||||||
|
#[derive(Default, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ClientAuthType {
|
||||||
|
#[default]
|
||||||
|
NoClientCert = 0,
|
||||||
|
RequestClientCert = 1,
|
||||||
|
RequireAnyClientCert = 2,
|
||||||
|
VerifyClientCertIfGiven = 3,
|
||||||
|
RequireAndVerifyClientCert = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtendedMasterSecretType declares the policy the client and server
|
||||||
|
// will follow for the Extended Master Secret extension
|
||||||
|
#[derive(Default, PartialEq, Eq, Copy, Clone)]
|
||||||
|
pub enum ExtendedMasterSecretType {
|
||||||
|
#[default]
|
||||||
|
Request = 0,
|
||||||
|
Require = 1,
|
||||||
|
Disable = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn validate_config(is_client: bool, config: &Config) -> Result<()> {
|
||||||
|
if is_client && config.psk.is_some() && config.psk_identity_hint.is_none() {
|
||||||
|
return Err(Error::ErrPskAndIdentityMustBeSetForClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_client && config.psk.is_none() && config.certificates.is_empty() {
|
||||||
|
return Err(Error::ErrServerMustHaveCertificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.certificates.is_empty() && config.psk.is_some() {
|
||||||
|
return Err(Error::ErrPskAndCertificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.psk_identity_hint.is_some() && config.psk.is_none() {
|
||||||
|
return Err(Error::ErrIdentityNoPsk);
|
||||||
|
}
|
||||||
|
|
||||||
|
for cert in &config.certificates {
|
||||||
|
match cert.private_key.kind {
|
||||||
|
CryptoPrivateKeyKind::Ed25519(_) => {}
|
||||||
|
CryptoPrivateKeyKind::Ecdsa256(_) => {}
|
||||||
|
_ => return Err(Error::ErrInvalidPrivateKey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_cipher_suites(
|
||||||
|
&config.cipher_suites,
|
||||||
|
config.psk.is_none(),
|
||||||
|
config.psk.is_some(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
2464
libs/dtls/src/conn/conn_test.rs
Normal file
2464
libs/dtls/src/conn/conn_test.rs
Normal file
File diff suppressed because it is too large
Load Diff
1212
libs/dtls/src/conn/mod.rs
Normal file
1212
libs/dtls/src/conn/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
85
libs/dtls/src/content.rs
Normal file
85
libs/dtls/src/content.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
use super::alert::*;
|
||||||
|
use super::application_data::*;
|
||||||
|
use super::change_cipher_spec::*;
|
||||||
|
use super::handshake::*;
|
||||||
|
use crate::error::*;
|
||||||
|
|
||||||
|
/// ## Specifications
|
||||||
|
///
|
||||||
|
/// * [RFC 4346 §6.2.1]
|
||||||
|
///
|
||||||
|
/// [RFC 4346 §6.2.1]: https://tools.ietf.org/html/rfc4346#section-6.2.1
|
||||||
|
#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub enum ContentType {
|
||||||
|
ChangeCipherSpec = 20,
|
||||||
|
Alert = 21,
|
||||||
|
Handshake = 22,
|
||||||
|
ApplicationData = 23,
|
||||||
|
#[default]
|
||||||
|
Invalid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for ContentType {
|
||||||
|
fn from(val: u8) -> Self {
|
||||||
|
match val {
|
||||||
|
20 => ContentType::ChangeCipherSpec,
|
||||||
|
21 => ContentType::Alert,
|
||||||
|
22 => ContentType::Handshake,
|
||||||
|
23 => ContentType::ApplicationData,
|
||||||
|
_ => ContentType::Invalid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
|
pub enum Content {
|
||||||
|
ChangeCipherSpec(ChangeCipherSpec),
|
||||||
|
Alert(Alert),
|
||||||
|
Handshake(Handshake),
|
||||||
|
ApplicationData(ApplicationData),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Content {
|
||||||
|
pub fn content_type(&self) -> ContentType {
|
||||||
|
match self {
|
||||||
|
Content::ChangeCipherSpec(c) => c.content_type(),
|
||||||
|
Content::Alert(c) => c.content_type(),
|
||||||
|
Content::Handshake(c) => c.content_type(),
|
||||||
|
Content::ApplicationData(c) => c.content_type(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Content::ChangeCipherSpec(c) => c.size(),
|
||||||
|
Content::Alert(c) => c.size(),
|
||||||
|
Content::Handshake(c) => c.size(),
|
||||||
|
Content::ApplicationData(c) => c.size(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn marshal<W: Write>(&self, writer: &mut W) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Content::ChangeCipherSpec(c) => c.marshal(writer),
|
||||||
|
Content::Alert(c) => c.marshal(writer),
|
||||||
|
Content::Handshake(c) => c.marshal(writer),
|
||||||
|
Content::ApplicationData(c) => c.marshal(writer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unmarshal<R: Read>(content_type: ContentType, reader: &mut R) -> Result<Self> {
|
||||||
|
match content_type {
|
||||||
|
ContentType::ChangeCipherSpec => Ok(Content::ChangeCipherSpec(
|
||||||
|
ChangeCipherSpec::unmarshal(reader)?,
|
||||||
|
)),
|
||||||
|
ContentType::Alert => Ok(Content::Alert(Alert::unmarshal(reader)?)),
|
||||||
|
ContentType::Handshake => Ok(Content::Handshake(Handshake::unmarshal(reader)?)),
|
||||||
|
ContentType::ApplicationData => Ok(Content::ApplicationData(
|
||||||
|
ApplicationData::unmarshal(reader)?,
|
||||||
|
)),
|
||||||
|
_ => Err(Error::ErrInvalidContentType),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
136
libs/dtls/src/crypto/crypto_cbc.rs
Normal file
136
libs/dtls/src/crypto/crypto_cbc.rs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
// AES-CBC (Cipher Block Chaining)
|
||||||
|
// First historic block cipher for AES.
|
||||||
|
// CBC mode is insecure and must not be used. It’s been progressively deprecated and
|
||||||
|
// removed from SSL libraries.
|
||||||
|
// Introduced with TLS 1.0 year 2002. Superseded by GCM in TLS 1.2 year 2008.
|
||||||
|
// Removed in TLS 1.3 year 2018.
|
||||||
|
// RFC 3268 year 2002 https://tools.ietf.org/html/rfc3268
|
||||||
|
|
||||||
|
// https://github.com/RustCrypto/block-ciphers
|
||||||
|
|
||||||
|
use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
|
||||||
|
use p256::elliptic_curve::subtle::ConstantTimeEq;
|
||||||
|
use rand::Rng;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::ops::Not;
|
||||||
|
|
||||||
|
use super::padding::DtlsPadding;
|
||||||
|
use crate::content::*;
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::prf::*;
|
||||||
|
use crate::record_layer::record_layer_header::*;
|
||||||
|
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
|
||||||
|
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
|
||||||
|
|
||||||
|
// State needed to handle encrypted input/output
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CryptoCbc {
|
||||||
|
local_key: Vec<u8>,
|
||||||
|
remote_key: Vec<u8>,
|
||||||
|
write_mac: Vec<u8>,
|
||||||
|
read_mac: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CryptoCbc {
|
||||||
|
const BLOCK_SIZE: usize = 16;
|
||||||
|
const MAC_SIZE: usize = 20;
|
||||||
|
|
||||||
|
pub fn new(
|
||||||
|
local_key: &[u8],
|
||||||
|
local_mac: &[u8],
|
||||||
|
remote_key: &[u8],
|
||||||
|
remote_mac: &[u8],
|
||||||
|
) -> Result<Self> {
|
||||||
|
Ok(CryptoCbc {
|
||||||
|
local_key: local_key.to_vec(),
|
||||||
|
write_mac: local_mac.to_vec(),
|
||||||
|
|
||||||
|
remote_key: remote_key.to_vec(),
|
||||||
|
read_mac: remote_mac.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let mut payload = raw[RECORD_LAYER_HEADER_SIZE..].to_vec();
|
||||||
|
let raw = &raw[..RECORD_LAYER_HEADER_SIZE];
|
||||||
|
|
||||||
|
// Generate + Append MAC
|
||||||
|
let h = pkt_rlh;
|
||||||
|
|
||||||
|
let mac = prf_mac(
|
||||||
|
h.epoch,
|
||||||
|
h.sequence_number,
|
||||||
|
h.content_type,
|
||||||
|
h.protocol_version,
|
||||||
|
&payload,
|
||||||
|
&self.write_mac,
|
||||||
|
)?;
|
||||||
|
payload.extend_from_slice(&mac);
|
||||||
|
|
||||||
|
let mut iv: Vec<u8> = vec![0; Self::BLOCK_SIZE];
|
||||||
|
rand::rng().fill(iv.as_mut_slice());
|
||||||
|
|
||||||
|
let write_cbc = Aes256CbcEnc::new_from_slices(&self.local_key, &iv)?;
|
||||||
|
let encrypted = write_cbc.encrypt_padded_vec_mut::<DtlsPadding>(&payload);
|
||||||
|
|
||||||
|
// Prepend unencrypte header with encrypted payload
|
||||||
|
let mut r = vec![];
|
||||||
|
r.extend_from_slice(raw);
|
||||||
|
r.extend_from_slice(&iv);
|
||||||
|
r.extend_from_slice(&encrypted);
|
||||||
|
|
||||||
|
let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16;
|
||||||
|
r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE]
|
||||||
|
.copy_from_slice(&r_len.to_be_bytes());
|
||||||
|
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt(&self, r: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let mut reader = Cursor::new(r);
|
||||||
|
let h = RecordLayerHeader::unmarshal(&mut reader)?;
|
||||||
|
if h.content_type == ContentType::ChangeCipherSpec {
|
||||||
|
// Nothing to encrypt with ChangeCipherSpec
|
||||||
|
return Ok(r.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.len() < RECORD_LAYER_HEADER_SIZE + Self::BLOCK_SIZE {
|
||||||
|
return Err(Error::ErrInvalidPacketLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = &r[RECORD_LAYER_HEADER_SIZE..];
|
||||||
|
let iv = &body[0..Self::BLOCK_SIZE];
|
||||||
|
let body = &body[Self::BLOCK_SIZE..];
|
||||||
|
|
||||||
|
if body.is_empty() || !body.len().is_multiple_of(Self::BLOCK_SIZE) {
|
||||||
|
return Err(Error::ErrInvalidPacketLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
let read_cbc = Aes256CbcDec::new_from_slices(&self.remote_key, iv)?;
|
||||||
|
|
||||||
|
let decrypted = read_cbc
|
||||||
|
.decrypt_padded_vec_mut::<DtlsPadding>(body)
|
||||||
|
.map_err(|_| Error::ErrInvalidPacketLength)?;
|
||||||
|
|
||||||
|
let recv_mac = &decrypted[decrypted.len() - Self::MAC_SIZE..];
|
||||||
|
let decrypted = &decrypted[0..decrypted.len() - Self::MAC_SIZE];
|
||||||
|
let mac = prf_mac(
|
||||||
|
h.epoch,
|
||||||
|
h.sequence_number,
|
||||||
|
h.content_type,
|
||||||
|
h.protocol_version,
|
||||||
|
decrypted,
|
||||||
|
&self.read_mac,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if recv_mac.ct_eq(&mac).not().into() {
|
||||||
|
return Err(Error::ErrInvalidMac);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + decrypted.len());
|
||||||
|
d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]);
|
||||||
|
d.extend_from_slice(decrypted);
|
||||||
|
|
||||||
|
Ok(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
188
libs/dtls/src/crypto/crypto_ccm.rs
Normal file
188
libs/dtls/src/crypto/crypto_ccm.rs
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
// AES-CCM (Counter with CBC-MAC)
|
||||||
|
// Alternative to GCM mode.
|
||||||
|
// Available in OpenSSL as of TLS 1.3 (2018), but disabled by default.
|
||||||
|
// Two AES computations per block, thus expected to be somewhat slower than AES-GCM.
|
||||||
|
// RFC 6655 year 2012 https://tools.ietf.org/html/rfc6655
|
||||||
|
// Much lower adoption, probably because it came after GCM and offer no significant benefit.
|
||||||
|
|
||||||
|
// https://github.com/RustCrypto/AEADs
|
||||||
|
// https://docs.rs/ccm/0.3.0/ccm/ Or https://crates.io/crates/aes-ccm?
|
||||||
|
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use aes::Aes128;
|
||||||
|
use ccm::aead::generic_array::GenericArray;
|
||||||
|
use ccm::aead::AeadInPlace;
|
||||||
|
use ccm::consts::{U12, U16, U8};
|
||||||
|
use ccm::Ccm;
|
||||||
|
use ccm::KeyInit;
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::content::*;
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::record_layer::record_layer_header::*;
|
||||||
|
|
||||||
|
const CRYPTO_CCM_8_TAG_LENGTH: usize = 8;
|
||||||
|
const CRYPTO_CCM_TAG_LENGTH: usize = 16;
|
||||||
|
const CRYPTO_CCM_NONCE_LENGTH: usize = 12;
|
||||||
|
|
||||||
|
type AesCcm8 = Ccm<Aes128, U8, U12>;
|
||||||
|
type AesCcm = Ccm<Aes128, U16, U12>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum CryptoCcmTagLen {
|
||||||
|
CryptoCcm8TagLength,
|
||||||
|
CryptoCcmTagLength,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CryptoCcmType {
|
||||||
|
CryptoCcm8(AesCcm8),
|
||||||
|
CryptoCcm(AesCcm),
|
||||||
|
}
|
||||||
|
|
||||||
|
// State needed to handle encrypted input/output
|
||||||
|
pub struct CryptoCcm {
|
||||||
|
local_ccm: CryptoCcmType,
|
||||||
|
remote_ccm: CryptoCcmType,
|
||||||
|
local_write_iv: Vec<u8>,
|
||||||
|
remote_write_iv: Vec<u8>,
|
||||||
|
// used by clone()
|
||||||
|
local_write_key: Vec<u8>,
|
||||||
|
remote_write_key: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for CryptoCcm {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
match self.local_ccm {
|
||||||
|
CryptoCcmType::CryptoCcm(_) => Self::new(
|
||||||
|
&CryptoCcmTagLen::CryptoCcmTagLength,
|
||||||
|
&self.local_write_key,
|
||||||
|
&self.local_write_iv,
|
||||||
|
&self.remote_write_key,
|
||||||
|
&self.remote_write_iv,
|
||||||
|
),
|
||||||
|
CryptoCcmType::CryptoCcm8(_) => Self::new(
|
||||||
|
&CryptoCcmTagLen::CryptoCcm8TagLength,
|
||||||
|
&self.local_write_key,
|
||||||
|
&self.local_write_iv,
|
||||||
|
&self.remote_write_key,
|
||||||
|
&self.remote_write_iv,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CryptoCcm {
|
||||||
|
pub fn new(
|
||||||
|
tag_len: &CryptoCcmTagLen,
|
||||||
|
local_key: &[u8],
|
||||||
|
local_write_iv: &[u8],
|
||||||
|
remote_key: &[u8],
|
||||||
|
remote_write_iv: &[u8],
|
||||||
|
) -> Self {
|
||||||
|
let key = GenericArray::from_slice(local_key);
|
||||||
|
let local_ccm = match tag_len {
|
||||||
|
CryptoCcmTagLen::CryptoCcmTagLength => CryptoCcmType::CryptoCcm(AesCcm::new(key)),
|
||||||
|
CryptoCcmTagLen::CryptoCcm8TagLength => CryptoCcmType::CryptoCcm8(AesCcm8::new(key)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let key = GenericArray::from_slice(remote_key);
|
||||||
|
let remote_ccm = match tag_len {
|
||||||
|
CryptoCcmTagLen::CryptoCcmTagLength => CryptoCcmType::CryptoCcm(AesCcm::new(key)),
|
||||||
|
CryptoCcmTagLen::CryptoCcm8TagLength => CryptoCcmType::CryptoCcm8(AesCcm8::new(key)),
|
||||||
|
};
|
||||||
|
|
||||||
|
CryptoCcm {
|
||||||
|
local_ccm,
|
||||||
|
local_write_key: local_key.to_vec(),
|
||||||
|
local_write_iv: local_write_iv.to_vec(),
|
||||||
|
remote_ccm,
|
||||||
|
remote_write_key: remote_key.to_vec(),
|
||||||
|
remote_write_iv: remote_write_iv.to_vec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let payload = &raw[RECORD_LAYER_HEADER_SIZE..];
|
||||||
|
let raw = &raw[..RECORD_LAYER_HEADER_SIZE];
|
||||||
|
|
||||||
|
let mut nonce = vec![0u8; CRYPTO_CCM_NONCE_LENGTH];
|
||||||
|
nonce[..4].copy_from_slice(&self.local_write_iv[..4]);
|
||||||
|
rand::rng().fill(&mut nonce[4..]);
|
||||||
|
let nonce = GenericArray::from_slice(&nonce);
|
||||||
|
|
||||||
|
let additional_data = generate_aead_additional_data(pkt_rlh, payload.len());
|
||||||
|
|
||||||
|
let mut buffer: Vec<u8> = Vec::new();
|
||||||
|
buffer.extend_from_slice(payload);
|
||||||
|
|
||||||
|
match &self.local_ccm {
|
||||||
|
CryptoCcmType::CryptoCcm(ccm) => {
|
||||||
|
ccm.encrypt_in_place(nonce, &additional_data, &mut buffer)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?;
|
||||||
|
}
|
||||||
|
CryptoCcmType::CryptoCcm8(ccm8) => {
|
||||||
|
ccm8.encrypt_in_place(nonce, &additional_data, &mut buffer)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut r = Vec::with_capacity(raw.len() + nonce.len() + buffer.len());
|
||||||
|
|
||||||
|
r.extend_from_slice(raw);
|
||||||
|
r.extend_from_slice(&nonce[4..]);
|
||||||
|
r.extend_from_slice(&buffer);
|
||||||
|
|
||||||
|
// Update recordLayer size to include explicit nonce
|
||||||
|
let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16;
|
||||||
|
r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE]
|
||||||
|
.copy_from_slice(&r_len.to_be_bytes());
|
||||||
|
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt(&self, r: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let mut reader = Cursor::new(r);
|
||||||
|
let h = RecordLayerHeader::unmarshal(&mut reader)?;
|
||||||
|
if h.content_type == ContentType::ChangeCipherSpec {
|
||||||
|
// Nothing to encrypt with ChangeCipherSpec
|
||||||
|
return Ok(r.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.len() <= (RECORD_LAYER_HEADER_SIZE + 8) {
|
||||||
|
return Err(Error::ErrNotEnoughRoomForNonce);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut nonce = vec![];
|
||||||
|
nonce.extend_from_slice(&self.remote_write_iv[..4]);
|
||||||
|
nonce.extend_from_slice(&r[RECORD_LAYER_HEADER_SIZE..RECORD_LAYER_HEADER_SIZE + 8]);
|
||||||
|
let nonce = GenericArray::from_slice(&nonce);
|
||||||
|
|
||||||
|
let out = &r[RECORD_LAYER_HEADER_SIZE + 8..];
|
||||||
|
|
||||||
|
let mut buffer: Vec<u8> = Vec::new();
|
||||||
|
buffer.extend_from_slice(out);
|
||||||
|
|
||||||
|
match &self.remote_ccm {
|
||||||
|
CryptoCcmType::CryptoCcm(ccm) => {
|
||||||
|
let additional_data =
|
||||||
|
generate_aead_additional_data(&h, out.len() - CRYPTO_CCM_TAG_LENGTH);
|
||||||
|
ccm.decrypt_in_place(nonce, &additional_data, &mut buffer)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?;
|
||||||
|
}
|
||||||
|
CryptoCcmType::CryptoCcm8(ccm8) => {
|
||||||
|
let additional_data =
|
||||||
|
generate_aead_additional_data(&h, out.len() - CRYPTO_CCM_8_TAG_LENGTH);
|
||||||
|
ccm8.decrypt_in_place(nonce, &additional_data, &mut buffer)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + buffer.len());
|
||||||
|
d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]);
|
||||||
|
d.extend_from_slice(&buffer);
|
||||||
|
|
||||||
|
Ok(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
123
libs/dtls/src/crypto/crypto_chacha20.rs
Normal file
123
libs/dtls/src/crypto/crypto_chacha20.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use chacha20poly1305::aead::generic_array::GenericArray;
|
||||||
|
use chacha20poly1305::aead::AeadInPlace;
|
||||||
|
use chacha20poly1305::{ChaCha20Poly1305, KeyInit};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::content::*;
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::record_layer::record_layer_header::*; // what about Aes256Gcm?
|
||||||
|
|
||||||
|
const CRYPTO_CHACHA20_TAG_LENGTH: usize = 16;
|
||||||
|
const CRYPTO_CHACHA20_NONCE_LENGTH: usize = 12;
|
||||||
|
|
||||||
|
// State needed to handle encrypted input/output
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CryptoChaCha20 {
|
||||||
|
local_cc: ChaCha20Poly1305,
|
||||||
|
remote_cc: ChaCha20Poly1305,
|
||||||
|
local_key: Vec<u8>,
|
||||||
|
remote_key: Vec<u8>,
|
||||||
|
local_write_iv: Vec<u8>,
|
||||||
|
remote_write_iv: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn noncegen(nonce: &mut [u8], epoch: u16, seqnum: u64) {
|
||||||
|
let epoch: u64 = epoch.into();
|
||||||
|
let seqnum = (seqnum & 0xFFFFFFFFFFFF) | (epoch << 48);
|
||||||
|
for i in 0..8 {
|
||||||
|
nonce[i + 4] ^= ((seqnum >> (8 * (7 - i))) & 0xFF) as u8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CryptoChaCha20 {
|
||||||
|
pub fn new(
|
||||||
|
local_key: &[u8],
|
||||||
|
local_write_iv: &[u8],
|
||||||
|
remote_key: &[u8],
|
||||||
|
remote_write_iv: &[u8],
|
||||||
|
) -> Self {
|
||||||
|
let key = GenericArray::from_slice(local_key);
|
||||||
|
let local_cc = ChaCha20Poly1305::new(key);
|
||||||
|
|
||||||
|
let key = GenericArray::from_slice(remote_key);
|
||||||
|
let remote_cc = ChaCha20Poly1305::new(key);
|
||||||
|
|
||||||
|
CryptoChaCha20 {
|
||||||
|
local_cc,
|
||||||
|
local_write_iv: local_write_iv.to_vec(),
|
||||||
|
remote_cc,
|
||||||
|
local_key: local_key.to_vec(),
|
||||||
|
remote_key: remote_key.to_vec(),
|
||||||
|
remote_write_iv: remote_write_iv.to_vec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let payload = &raw[RECORD_LAYER_HEADER_SIZE..];
|
||||||
|
let raw = &raw[..RECORD_LAYER_HEADER_SIZE];
|
||||||
|
|
||||||
|
let mut nonce = vec![0u8; CRYPTO_CHACHA20_NONCE_LENGTH];
|
||||||
|
nonce[..CRYPTO_CHACHA20_NONCE_LENGTH]
|
||||||
|
.copy_from_slice(&self.local_write_iv[..CRYPTO_CHACHA20_NONCE_LENGTH]);
|
||||||
|
|
||||||
|
noncegen(&mut nonce[..], pkt_rlh.epoch, pkt_rlh.sequence_number);
|
||||||
|
let nonce = GenericArray::from_slice(&nonce);
|
||||||
|
|
||||||
|
let additional_data = generate_aead_additional_data(pkt_rlh, payload.len());
|
||||||
|
|
||||||
|
let mut buffer: Vec<u8> = Vec::new();
|
||||||
|
buffer.extend_from_slice(payload);
|
||||||
|
|
||||||
|
let tag = self
|
||||||
|
.local_cc
|
||||||
|
.encrypt_in_place_detached(nonce, &additional_data, &mut buffer)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?;
|
||||||
|
|
||||||
|
let mut r = Vec::with_capacity(raw.len() + buffer.len() + tag.len());
|
||||||
|
r.extend_from_slice(raw);
|
||||||
|
r.extend_from_slice(&buffer);
|
||||||
|
r.extend_from_slice(&tag);
|
||||||
|
|
||||||
|
// Update recordLayer size to include explicit nonce
|
||||||
|
let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16;
|
||||||
|
r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE]
|
||||||
|
.copy_from_slice(&r_len.to_be_bytes());
|
||||||
|
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt(&self, r: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let mut reader = Cursor::new(r);
|
||||||
|
let h = RecordLayerHeader::unmarshal(&mut reader)?;
|
||||||
|
if h.content_type == ContentType::ChangeCipherSpec {
|
||||||
|
// Nothing to encrypt with ChangeCipherSpec
|
||||||
|
return Ok(r.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut nonce = vec![];
|
||||||
|
nonce.extend_from_slice(&self.remote_write_iv[..]);
|
||||||
|
|
||||||
|
noncegen(&mut nonce[..], h.epoch, h.sequence_number);
|
||||||
|
let nonce = GenericArray::from_slice(&nonce);
|
||||||
|
|
||||||
|
let out = &r[RECORD_LAYER_HEADER_SIZE..];
|
||||||
|
|
||||||
|
let additional_data =
|
||||||
|
generate_aead_additional_data(&h, out.len() - CRYPTO_CHACHA20_TAG_LENGTH);
|
||||||
|
|
||||||
|
let mut buffer: Vec<u8> = Vec::new();
|
||||||
|
buffer.extend_from_slice(out);
|
||||||
|
|
||||||
|
self.remote_cc
|
||||||
|
.decrypt_in_place(nonce, &additional_data, &mut buffer)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?;
|
||||||
|
|
||||||
|
let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + buffer.len());
|
||||||
|
d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]);
|
||||||
|
d.extend_from_slice(&buffer);
|
||||||
|
|
||||||
|
Ok(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
119
libs/dtls/src/crypto/crypto_gcm.rs
Normal file
119
libs/dtls/src/crypto/crypto_gcm.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
// AES-GCM (Galois Counter Mode)
|
||||||
|
// The most widely used block cipher worldwide.
|
||||||
|
// Mandatory as of TLS 1.2 (2008) and used by default by most clients.
|
||||||
|
// RFC 5288 year 2008 https://tools.ietf.org/html/rfc5288
|
||||||
|
|
||||||
|
// https://github.com/RustCrypto/AEADs
|
||||||
|
// https://docs.rs/aes-gcm/0.8.0/aes_gcm/
|
||||||
|
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use aes_gcm::aead::generic_array::GenericArray;
|
||||||
|
use aes_gcm::aead::AeadInPlace;
|
||||||
|
use aes_gcm::{Aes128Gcm, KeyInit};
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::content::*;
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::record_layer::record_layer_header::*; // what about Aes256Gcm?
|
||||||
|
|
||||||
|
const CRYPTO_GCM_TAG_LENGTH: usize = 16;
|
||||||
|
const CRYPTO_GCM_NONCE_LENGTH: usize = 12;
|
||||||
|
|
||||||
|
// State needed to handle encrypted input/output
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CryptoGcm {
|
||||||
|
local_gcm: Aes128Gcm,
|
||||||
|
remote_gcm: Aes128Gcm,
|
||||||
|
local_write_iv: Vec<u8>,
|
||||||
|
remote_write_iv: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CryptoGcm {
|
||||||
|
pub fn new(
|
||||||
|
local_key: &[u8],
|
||||||
|
local_write_iv: &[u8],
|
||||||
|
remote_key: &[u8],
|
||||||
|
remote_write_iv: &[u8],
|
||||||
|
) -> Self {
|
||||||
|
let key = GenericArray::from_slice(local_key);
|
||||||
|
let local_gcm = Aes128Gcm::new(key);
|
||||||
|
|
||||||
|
let key = GenericArray::from_slice(remote_key);
|
||||||
|
let remote_gcm = Aes128Gcm::new(key);
|
||||||
|
|
||||||
|
CryptoGcm {
|
||||||
|
local_gcm,
|
||||||
|
local_write_iv: local_write_iv.to_vec(),
|
||||||
|
remote_gcm,
|
||||||
|
remote_write_iv: remote_write_iv.to_vec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let payload = &raw[RECORD_LAYER_HEADER_SIZE..];
|
||||||
|
let raw = &raw[..RECORD_LAYER_HEADER_SIZE];
|
||||||
|
|
||||||
|
let mut nonce = vec![0u8; CRYPTO_GCM_NONCE_LENGTH];
|
||||||
|
nonce[..4].copy_from_slice(&self.local_write_iv[..4]);
|
||||||
|
rand::rng().fill(&mut nonce[4..]);
|
||||||
|
let nonce = GenericArray::from_slice(&nonce);
|
||||||
|
|
||||||
|
let additional_data = generate_aead_additional_data(pkt_rlh, payload.len());
|
||||||
|
|
||||||
|
let mut buffer: Vec<u8> = Vec::new();
|
||||||
|
buffer.extend_from_slice(payload);
|
||||||
|
|
||||||
|
self.local_gcm
|
||||||
|
.encrypt_in_place(nonce, &additional_data, &mut buffer)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?;
|
||||||
|
|
||||||
|
let mut r = Vec::with_capacity(raw.len() + nonce.len() + buffer.len());
|
||||||
|
r.extend_from_slice(raw);
|
||||||
|
r.extend_from_slice(&nonce[4..]);
|
||||||
|
r.extend_from_slice(&buffer);
|
||||||
|
|
||||||
|
// Update recordLayer size to include explicit nonce
|
||||||
|
let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16;
|
||||||
|
r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE]
|
||||||
|
.copy_from_slice(&r_len.to_be_bytes());
|
||||||
|
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt(&self, r: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let mut reader = Cursor::new(r);
|
||||||
|
let h = RecordLayerHeader::unmarshal(&mut reader)?;
|
||||||
|
if h.content_type == ContentType::ChangeCipherSpec {
|
||||||
|
// Nothing to encrypt with ChangeCipherSpec
|
||||||
|
return Ok(r.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.len() <= (RECORD_LAYER_HEADER_SIZE + 8) {
|
||||||
|
return Err(Error::ErrNotEnoughRoomForNonce);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut nonce = vec![];
|
||||||
|
nonce.extend_from_slice(&self.remote_write_iv[..4]);
|
||||||
|
nonce.extend_from_slice(&r[RECORD_LAYER_HEADER_SIZE..RECORD_LAYER_HEADER_SIZE + 8]);
|
||||||
|
let nonce = GenericArray::from_slice(&nonce);
|
||||||
|
|
||||||
|
let out = &r[RECORD_LAYER_HEADER_SIZE + 8..];
|
||||||
|
|
||||||
|
let additional_data = generate_aead_additional_data(&h, out.len() - CRYPTO_GCM_TAG_LENGTH);
|
||||||
|
|
||||||
|
let mut buffer: Vec<u8> = Vec::new();
|
||||||
|
buffer.extend_from_slice(out);
|
||||||
|
|
||||||
|
self.remote_gcm
|
||||||
|
.decrypt_in_place(nonce, &additional_data, &mut buffer)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?;
|
||||||
|
|
||||||
|
let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + buffer.len());
|
||||||
|
d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]);
|
||||||
|
d.extend_from_slice(&buffer);
|
||||||
|
|
||||||
|
Ok(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
221
libs/dtls/src/crypto/crypto_test.rs
Normal file
221
libs/dtls/src/crypto/crypto_test.rs
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use x509_parser::pem::Pem;
|
||||||
|
|
||||||
|
use super::crypto_ccm::*;
|
||||||
|
use super::*;
|
||||||
|
use crate::content::ContentType;
|
||||||
|
use crate::record_layer::record_layer_header::{ProtocolVersion, RECORD_LAYER_HEADER_SIZE};
|
||||||
|
|
||||||
|
const RAW_PRIVATE_KEY: &str = "
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEAxIA2BrrnR2sIlATsp7aRBD/3krwZ7vt9dNeoDQAee0s6SuYP
|
||||||
|
6MBx/HPnAkwNvPS90R05a7pwRkoT6Ur4PfPhCVlUe8lV+0Eto3ZSEeHz3HdsqlM3
|
||||||
|
bso67L7Dqrc7MdVstlKcgJi8yeAoGOIL9/igOv0XBFCeznm9nznx6mnsR5cugw+1
|
||||||
|
ypXelaHmBCLV7r5SeVSh57+KhvZGbQ2fFpUaTPegRpJZXBNS8lSeWvtOv9d6N5UB
|
||||||
|
ROTAJodMZT5AfX0jB0QB9IT/0I96H6BSENH08NXOeXApMuLKvnAf361rS7cRAfRL
|
||||||
|
rWZqERMP4u6Cnk0Cnckc3WcW27kGGIbtwbqUIQIDAQABAoIBAGF7OVIdZp8Hejn0
|
||||||
|
N3L8HvT8xtUEe9kS6ioM0lGgvX5s035Uo4/T6LhUx0VcdXRH9eLHnLTUyN4V4cra
|
||||||
|
ZkxVsE3zAvZl60G6E+oDyLMWZOP6Wu4kWlub9597A5atT7BpMIVCdmFVZFLB4SJ3
|
||||||
|
AXkC3nplFAYP+Lh1rJxRIrIn2g+pEeBboWbYA++oDNuMQffDZaokTkJ8Bn1JZYh0
|
||||||
|
xEXKY8Bi2Egd5NMeZa1UFO6y8tUbZfwgVs6Enq5uOgtfayq79vZwyjj1kd29MBUD
|
||||||
|
8g8byV053ZKxbUOiOuUts97eb+fN3DIDRTcT2c+lXt/4C54M1FclJAbtYRK/qwsl
|
||||||
|
pYWKQAECgYEA4ZUbqQnTo1ICvj81ifGrz+H4LKQqe92Hbf/W51D/Umk2kP702W22
|
||||||
|
HP4CvrJRtALThJIG9m2TwUjl/WAuZIBrhSAbIvc3Fcoa2HjdRp+sO5U1ueDq7d/S
|
||||||
|
Z+PxRI8cbLbRpEdIaoR46qr/2uWZ943PHMv9h4VHPYn1w8b94hwD6vkCgYEA3v87
|
||||||
|
mFLzyM9ercnEv9zHMRlMZFQhlcUGQZvfb8BuJYl/WogyT6vRrUuM0QXULNEPlrin
|
||||||
|
mBQTqc1nCYbgkFFsD2VVt1qIyiAJsB9MD1LNV6YuvE7T2KOSadmsA4fa9PUqbr71
|
||||||
|
hf3lTTq+LeR09LebO7WgSGYY+5YKVOEGpYMR1GkCgYEAxPVQmk3HKHEhjgRYdaG5
|
||||||
|
lp9A9ZE8uruYVJWtiHgzBTxx9TV2iST+fd/We7PsHFTfY3+wbpcMDBXfIVRKDVwH
|
||||||
|
BMwchXH9+Ztlxx34bYJaegd0SmA0Hw9ugWEHNgoSEmWpM1s9wir5/ELjc7dGsFtz
|
||||||
|
uzvsl9fpdLSxDYgAAdzeGtkCgYBAzKIgrVox7DBzB8KojhtD5ToRnXD0+H/M6OKQ
|
||||||
|
srZPKhlb0V/tTtxrIx0UUEFLlKSXA6mPw6XDHfDnD86JoV9pSeUSlrhRI+Ysy6tq
|
||||||
|
eIE7CwthpPZiaYXORHZ7wCqcK/HcpJjsCs9rFbrV0yE5S3FMdIbTAvgXg44VBB7O
|
||||||
|
UbwIoQKBgDuY8gSrA5/A747wjjmsdRWK4DMTMEV4eCW1BEP7Tg7Cxd5n3xPJiYhr
|
||||||
|
nhLGN+mMnVIcv2zEMS0/eNZr1j/0BtEdx+3IC6Eq+ONY0anZ4Irt57/5QeKgKn/L
|
||||||
|
JPhfPySIPG4UmwE4gW8t79vfOKxnUu2fDD1ZXUYopan6EckACNH/
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generate_key_signature() -> Result<()> {
|
||||||
|
let reader = Cursor::new(RAW_PRIVATE_KEY.as_bytes());
|
||||||
|
let pem = match Pem::read(reader) {
|
||||||
|
Ok((pem, _)) => pem,
|
||||||
|
Err(_) => return Err(Error::Other("Pem::read error".to_owned())),
|
||||||
|
};
|
||||||
|
//let private_key = rsa::RSAPrivateKey::from_pkcs1(&pem.contents)?;
|
||||||
|
|
||||||
|
let client_random = vec![
|
||||||
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
|
||||||
|
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
|
||||||
|
0x1e, 0x1f,
|
||||||
|
];
|
||||||
|
let server_random = vec![
|
||||||
|
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e,
|
||||||
|
0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d,
|
||||||
|
0x8e, 0x8f,
|
||||||
|
];
|
||||||
|
let public_key = vec![
|
||||||
|
0x20, 0x9f, 0xd7, 0xad, 0x6d, 0xcf, 0xf4, 0x29, 0x8d, 0xd3, 0xf9, 0x6d, 0x5b, 0x1b, 0x2a,
|
||||||
|
0xf9, 0x10, 0xa0, 0x53, 0x5b, 0x14, 0x88, 0xd7, 0xf8, 0xfa, 0xbb, 0x34, 0x9a, 0x98, 0x28,
|
||||||
|
0x80, 0xb6, 0x15,
|
||||||
|
];
|
||||||
|
let expected_signature = vec![
|
||||||
|
0x6f, 0x47, 0x97, 0x85, 0xcc, 0x76, 0x50, 0x93, 0xbd, 0xe2, 0x6a, 0x69, 0x0b, 0xc3, 0x03,
|
||||||
|
0xd1, 0xb7, 0xe4, 0xab, 0x88, 0x7b, 0xa6, 0x52, 0x80, 0xdf, 0xaa, 0x25, 0x7a, 0xdb, 0x29,
|
||||||
|
0x32, 0xe4, 0xd8, 0x28, 0x28, 0xb3, 0xe8, 0x04, 0x3c, 0x38, 0x16, 0xfc, 0x78, 0xe9, 0x15,
|
||||||
|
0x7b, 0xc5, 0xbd, 0x7d, 0xfc, 0xcd, 0x83, 0x00, 0x57, 0x4a, 0x3c, 0x23, 0x85, 0x75, 0x6b,
|
||||||
|
0x37, 0xd5, 0x89, 0x72, 0x73, 0xf0, 0x44, 0x8c, 0x00, 0x70, 0x1f, 0x6e, 0xa2, 0x81, 0xd0,
|
||||||
|
0x09, 0xc5, 0x20, 0x36, 0xab, 0x23, 0x09, 0x40, 0x1f, 0x4d, 0x45, 0x96, 0x62, 0xbb, 0x81,
|
||||||
|
0xb0, 0x30, 0x72, 0xad, 0x3a, 0x0a, 0xac, 0x31, 0x63, 0x40, 0x52, 0x0a, 0x27, 0xf3, 0x34,
|
||||||
|
0xde, 0x27, 0x7d, 0xb7, 0x54, 0xff, 0x0f, 0x9f, 0x5a, 0xfe, 0x07, 0x0f, 0x4e, 0x9f, 0x53,
|
||||||
|
0x04, 0x34, 0x62, 0xf4, 0x30, 0x74, 0x83, 0x35, 0xfc, 0xe4, 0x7e, 0xbf, 0x5a, 0xc4, 0x52,
|
||||||
|
0xd0, 0xea, 0xf9, 0x61, 0x4e, 0xf5, 0x1c, 0x0e, 0x58, 0x02, 0x71, 0xfb, 0x1f, 0x34, 0x55,
|
||||||
|
0xe8, 0x36, 0x70, 0x3c, 0xc1, 0xcb, 0xc9, 0xb7, 0xbb, 0xb5, 0x1c, 0x44, 0x9a, 0x6d, 0x88,
|
||||||
|
0x78, 0x98, 0xd4, 0x91, 0x2e, 0xeb, 0x98, 0x81, 0x23, 0x30, 0x73, 0x39, 0x43, 0xd5, 0xbb,
|
||||||
|
0x70, 0x39, 0xba, 0x1f, 0xdb, 0x70, 0x9f, 0x91, 0x83, 0x56, 0xc2, 0xde, 0xed, 0x17, 0x6d,
|
||||||
|
0x2c, 0x3e, 0x21, 0xea, 0x36, 0xb4, 0x91, 0xd8, 0x31, 0x05, 0x60, 0x90, 0xfd, 0xc6, 0x74,
|
||||||
|
0xa9, 0x7b, 0x18, 0xfc, 0x1c, 0x6a, 0x1c, 0x6e, 0xec, 0xd3, 0xc1, 0xc0, 0x0d, 0x11, 0x25,
|
||||||
|
0x48, 0x37, 0x3d, 0x45, 0x11, 0xa2, 0x31, 0x14, 0x0a, 0x66, 0x9f, 0xd8, 0xac, 0x74, 0xa2,
|
||||||
|
0xcd, 0xc8, 0x79, 0xb3, 0x9e, 0xc6, 0x66, 0x25, 0xcf, 0x2c, 0x87, 0x5e, 0x5c, 0x36, 0x75,
|
||||||
|
0x86,
|
||||||
|
];
|
||||||
|
|
||||||
|
let signature = generate_key_signature(
|
||||||
|
&client_random,
|
||||||
|
&server_random,
|
||||||
|
&public_key,
|
||||||
|
NamedCurve::X25519,
|
||||||
|
&CryptoPrivateKey {
|
||||||
|
kind: CryptoPrivateKeyKind::Rsa256(
|
||||||
|
ring::rsa::KeyPair::from_der(&pem.contents)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?,
|
||||||
|
),
|
||||||
|
serialized_der: pem.contents.clone(),
|
||||||
|
}, //hashAlgorithmSHA256,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
signature, expected_signature,
|
||||||
|
"Signature generation failed \nexp {expected_signature:?} \nactual {signature:?} "
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ccm_encryption_and_decryption() -> Result<()> {
|
||||||
|
let key = vec![
|
||||||
|
0x18, 0x78, 0xac, 0xc2, 0x2a, 0xd8, 0xbd, 0xd8, 0xc6, 0x01, 0xa6, 0x17, 0x12, 0x6f, 0x63,
|
||||||
|
0x54,
|
||||||
|
];
|
||||||
|
let iv = vec![0x0e, 0xb2, 0x09, 0x06];
|
||||||
|
|
||||||
|
let ccm = CryptoCcm::new(&CryptoCcmTagLen::CryptoCcmTagLength, &key, &iv, &key, &iv);
|
||||||
|
|
||||||
|
let rlh = RecordLayerHeader {
|
||||||
|
content_type: ContentType::ApplicationData,
|
||||||
|
protocol_version: ProtocolVersion {
|
||||||
|
major: 0xfe,
|
||||||
|
minor: 0xff,
|
||||||
|
},
|
||||||
|
epoch: 0,
|
||||||
|
sequence_number: 18,
|
||||||
|
content_len: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
let raw = vec![
|
||||||
|
0x17, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x03, 0xff, 0xaa,
|
||||||
|
0xbb,
|
||||||
|
];
|
||||||
|
|
||||||
|
let cipher_text = ccm.encrypt(&rlh, &raw)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
&cipher_text[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE],
|
||||||
|
[0, 27],
|
||||||
|
"RecordLayer size updating failed \nexp: {:?} \nactual {:?} ",
|
||||||
|
[0, 27],
|
||||||
|
&cipher_text[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE]
|
||||||
|
);
|
||||||
|
|
||||||
|
let plain_text = ccm.decrypt(&cipher_text)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
raw[RECORD_LAYER_HEADER_SIZE..],
|
||||||
|
plain_text[RECORD_LAYER_HEADER_SIZE..],
|
||||||
|
"Decryption failed \nexp: {:?} \nactual {:?} ",
|
||||||
|
&raw[RECORD_LAYER_HEADER_SIZE..],
|
||||||
|
&plain_text[RECORD_LAYER_HEADER_SIZE..]
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_certificate_verify() -> Result<()> {
|
||||||
|
let plain_text: Vec<u8> = vec![
|
||||||
|
0x6f, 0x47, 0x97, 0x85, 0xcc, 0x76, 0x50, 0x93, 0xbd, 0xe2, 0x6a, 0x69, 0x0b, 0xc3, 0x03,
|
||||||
|
0xd1, 0xb7, 0xe4, 0xab, 0x88, 0x7b, 0xa6, 0x52, 0x80, 0xdf, 0xaa, 0x25, 0x7a, 0xdb, 0x29,
|
||||||
|
0x32, 0xe4, 0xd8, 0x28, 0x28, 0xb3, 0xe8, 0x04, 0x3c, 0x38, 0x16, 0xfc, 0x78, 0xe9, 0x15,
|
||||||
|
0x7b, 0xc5, 0xbd, 0x7d, 0xfc, 0xcd, 0x83, 0x00, 0x57, 0x4a, 0x3c, 0x23, 0x85, 0x75, 0x6b,
|
||||||
|
0x37, 0xd5, 0x89, 0x72, 0x73, 0xf0, 0x44, 0x8c, 0x00, 0x70, 0x1f, 0x6e, 0xa2, 0x81, 0xd0,
|
||||||
|
0x09, 0xc5, 0x20, 0x36, 0xab, 0x23, 0x09, 0x40, 0x1f, 0x4d, 0x45, 0x96, 0x62, 0xbb, 0x81,
|
||||||
|
0xb0, 0x30, 0x72, 0xad, 0x3a, 0x0a, 0xac, 0x31, 0x63, 0x40, 0x52, 0x0a, 0x27, 0xf3, 0x34,
|
||||||
|
0xde, 0x27, 0x7d, 0xb7, 0x54, 0xff, 0x0f, 0x9f, 0x5a, 0xfe, 0x07, 0x0f, 0x4e, 0x9f, 0x53,
|
||||||
|
0x04, 0x34, 0x62, 0xf4, 0x30, 0x74, 0x83, 0x35, 0xfc, 0xe4, 0x7e, 0xbf, 0x5a, 0xc4, 0x52,
|
||||||
|
0xd0, 0xea, 0xf9, 0x61, 0x4e, 0xf5, 0x1c, 0x0e, 0x58, 0x02, 0x71, 0xfb, 0x1f, 0x34, 0x55,
|
||||||
|
0xe8, 0x36, 0x70, 0x3c, 0xc1, 0xcb, 0xc9, 0xb7, 0xbb, 0xb5, 0x1c, 0x44, 0x9a, 0x6d, 0x88,
|
||||||
|
0x78, 0x98, 0xd4, 0x91, 0x2e, 0xeb, 0x98, 0x81, 0x23, 0x30, 0x73, 0x39, 0x43, 0xd5, 0xbb,
|
||||||
|
0x70, 0x39, 0xba, 0x1f, 0xdb, 0x70, 0x9f, 0x91, 0x83, 0x56, 0xc2, 0xde, 0xed, 0x17, 0x6d,
|
||||||
|
0x2c, 0x3e, 0x21, 0xea, 0x36, 0xb4, 0x91, 0xd8, 0x31, 0x05, 0x60, 0x90, 0xfd, 0xc6, 0x74,
|
||||||
|
0xa9, 0x7b, 0x18, 0xfc, 0x1c, 0x6a, 0x1c, 0x6e, 0xec, 0xd3, 0xc1, 0xc0, 0x0d, 0x11, 0x25,
|
||||||
|
0x48, 0x37, 0x3d, 0x45, 0x11, 0xa2, 0x31, 0x14, 0x0a, 0x66, 0x9f, 0xd8, 0xac, 0x74, 0xa2,
|
||||||
|
0xcd, 0xc8, 0x79, 0xb3, 0x9e, 0xc6, 0x66, 0x25, 0xcf, 0x2c, 0x87, 0x5e, 0x5c, 0x36, 0x75,
|
||||||
|
0x86,
|
||||||
|
];
|
||||||
|
|
||||||
|
//test ECDSA256
|
||||||
|
let certificate_ecdsa256 = Certificate::generate_self_signed(vec!["localhost".to_owned()])?;
|
||||||
|
let cert_verify_ecdsa256 =
|
||||||
|
generate_certificate_verify(&plain_text, &certificate_ecdsa256.private_key)?;
|
||||||
|
verify_certificate_verify(
|
||||||
|
&plain_text,
|
||||||
|
&SignatureHashAlgorithm {
|
||||||
|
hash: HashAlgorithm::Sha256,
|
||||||
|
signature: SignatureAlgorithm::Ecdsa,
|
||||||
|
},
|
||||||
|
&cert_verify_ecdsa256,
|
||||||
|
&certificate_ecdsa256
|
||||||
|
.certificate
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.as_ref().to_owned())
|
||||||
|
.collect::<Vec<Vec<u8>>>(),
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
//test ED25519
|
||||||
|
let certificate_ed25519 = Certificate::generate_self_signed_with_alg(
|
||||||
|
vec!["localhost".to_owned()],
|
||||||
|
&rcgen::PKCS_ED25519,
|
||||||
|
)?;
|
||||||
|
let cert_verify_ed25519 =
|
||||||
|
generate_certificate_verify(&plain_text, &certificate_ed25519.private_key)?;
|
||||||
|
verify_certificate_verify(
|
||||||
|
&plain_text,
|
||||||
|
&SignatureHashAlgorithm {
|
||||||
|
hash: HashAlgorithm::Sha256,
|
||||||
|
signature: SignatureAlgorithm::Ed25519,
|
||||||
|
},
|
||||||
|
&cert_verify_ed25519,
|
||||||
|
&certificate_ed25519
|
||||||
|
.certificate
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.as_ref().to_owned())
|
||||||
|
.collect::<Vec<Vec<u8>>>(),
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
514
libs/dtls/src/crypto/mod.rs
Normal file
514
libs/dtls/src/crypto/mod.rs
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod crypto_test;
|
||||||
|
|
||||||
|
pub mod crypto_cbc;
|
||||||
|
pub mod crypto_ccm;
|
||||||
|
pub mod crypto_chacha20;
|
||||||
|
pub mod crypto_gcm;
|
||||||
|
pub mod padding;
|
||||||
|
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use der_parser::oid;
|
||||||
|
use der_parser::oid::Oid;
|
||||||
|
|
||||||
|
use crate::certificate_verifier::CertificateVerifier;
|
||||||
|
use rustls_pki_types::CertificateDer;
|
||||||
|
|
||||||
|
use rcgen::{generate_simple_self_signed, CertifiedKey, KeyPair};
|
||||||
|
use ring::rand::SystemRandom;
|
||||||
|
use ring::signature::{EcdsaKeyPair, Ed25519KeyPair};
|
||||||
|
|
||||||
|
use crate::curve::named_curve::*;
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::record_layer::record_layer_header::*;
|
||||||
|
use crate::signature_hash_algorithm::{HashAlgorithm, SignatureAlgorithm, SignatureHashAlgorithm};
|
||||||
|
|
||||||
|
/// A X.509 certificate(s) used to authenticate a DTLS connection.
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
pub struct Certificate {
|
||||||
|
/// DER-encoded certificates.
|
||||||
|
pub certificate: Vec<CertificateDer<'static>>,
|
||||||
|
/// Private key.
|
||||||
|
pub private_key: CryptoPrivateKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Certificate {
|
||||||
|
/// Generate a self-signed certificate.
|
||||||
|
///
|
||||||
|
/// See [`rcgen::generate_simple_self_signed`].
|
||||||
|
pub fn generate_self_signed(subject_alt_names: impl Into<Vec<String>>) -> Result<Self> {
|
||||||
|
let CertifiedKey { cert, key_pair } =
|
||||||
|
generate_simple_self_signed(subject_alt_names).unwrap();
|
||||||
|
Ok(Certificate {
|
||||||
|
certificate: vec![cert.der().to_owned()],
|
||||||
|
private_key: CryptoPrivateKey::try_from(&key_pair)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a self-signed certificate with the given algorithm.
|
||||||
|
///
|
||||||
|
/// See [`rcgen::Certificate::from_params`].
|
||||||
|
pub fn generate_self_signed_with_alg(
|
||||||
|
subject_alt_names: impl Into<Vec<String>>,
|
||||||
|
alg: &'static rcgen::SignatureAlgorithm,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let params = rcgen::CertificateParams::new(subject_alt_names).unwrap();
|
||||||
|
let key_pair = rcgen::KeyPair::generate_for(alg).unwrap();
|
||||||
|
let cert = params.self_signed(&key_pair).unwrap();
|
||||||
|
|
||||||
|
Ok(Certificate {
|
||||||
|
certificate: vec![cert.der().to_owned()],
|
||||||
|
private_key: CryptoPrivateKey::try_from(&key_pair)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a certificate from the ASCII PEM format.
|
||||||
|
#[cfg(feature = "pem")]
|
||||||
|
pub fn from_pem(pem_str: &str) -> Result<Self> {
|
||||||
|
let mut pems = pem::parse_many(pem_str).map_err(|e| Error::InvalidPEM(e.to_string()))?;
|
||||||
|
if pems.len() < 2 {
|
||||||
|
return Err(Error::InvalidPEM(format!(
|
||||||
|
"expected at least two PEM blocks, got {}",
|
||||||
|
pems.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if pems[0].tag() != "PRIVATE_KEY" {
|
||||||
|
return Err(Error::InvalidPEM(format!(
|
||||||
|
"invalid tag (expected: 'PRIVATE_KEY', got: '{}')",
|
||||||
|
pems[0].tag()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let keypair = KeyPair::try_from(pems[0].contents())
|
||||||
|
.map_err(|e| Error::InvalidPEM(format!("can't decode keypair: {e}")))?;
|
||||||
|
|
||||||
|
let mut rustls_certs = Vec::new();
|
||||||
|
for p in pems.drain(1..) {
|
||||||
|
if p.tag() != "CERTIFICATE" {
|
||||||
|
return Err(Error::InvalidPEM(format!(
|
||||||
|
"invalid tag (expected: 'CERTIFICATE', got: '{}')",
|
||||||
|
p.tag()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
rustls_certs.push(CertificateDer::from(p.contents().to_vec()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Certificate {
|
||||||
|
certificate: rustls_certs,
|
||||||
|
private_key: CryptoPrivateKey::try_from(&keypair)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serializes the certificate (including the private key) in PKCS#8 format in PEM.
|
||||||
|
#[cfg(feature = "pem")]
|
||||||
|
pub fn serialize_pem(&self) -> String {
|
||||||
|
let mut data = vec![pem::Pem::new(
|
||||||
|
"PRIVATE_KEY".to_string(),
|
||||||
|
self.private_key.serialized_der.clone(),
|
||||||
|
)];
|
||||||
|
for rustls_cert in &self.certificate {
|
||||||
|
data.push(pem::Pem::new(
|
||||||
|
"CERTIFICATE".to_string(),
|
||||||
|
rustls_cert.as_ref(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
pem::encode_many(&data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn value_key_message(
|
||||||
|
client_random: &[u8],
|
||||||
|
server_random: &[u8],
|
||||||
|
public_key: &[u8],
|
||||||
|
named_curve: NamedCurve,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut server_ecdh_params = vec![0u8; 4];
|
||||||
|
server_ecdh_params[0] = 3; // named curve
|
||||||
|
server_ecdh_params[1..3].copy_from_slice(&(named_curve as u16).to_be_bytes());
|
||||||
|
server_ecdh_params[3] = public_key.len() as u8;
|
||||||
|
|
||||||
|
let mut plaintext = vec![];
|
||||||
|
plaintext.extend_from_slice(client_random);
|
||||||
|
plaintext.extend_from_slice(server_random);
|
||||||
|
plaintext.extend_from_slice(&server_ecdh_params);
|
||||||
|
plaintext.extend_from_slice(public_key);
|
||||||
|
|
||||||
|
plaintext
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Either ED25519, ECDSA or RSA keypair.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CryptoPrivateKeyKind {
|
||||||
|
Ed25519(Ed25519KeyPair),
|
||||||
|
Ecdsa256(EcdsaKeyPair),
|
||||||
|
Rsa256(ring::rsa::KeyPair),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Private key.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CryptoPrivateKey {
|
||||||
|
/// Keypair.
|
||||||
|
pub kind: CryptoPrivateKeyKind,
|
||||||
|
/// DER-encoded keypair.
|
||||||
|
pub serialized_der: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for CryptoPrivateKey {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
if self.serialized_der != other.serialized_der {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
matches!(
|
||||||
|
(&self.kind, &other.kind),
|
||||||
|
(
|
||||||
|
CryptoPrivateKeyKind::Rsa256(_),
|
||||||
|
CryptoPrivateKeyKind::Rsa256(_)
|
||||||
|
) | (
|
||||||
|
CryptoPrivateKeyKind::Ecdsa256(_),
|
||||||
|
CryptoPrivateKeyKind::Ecdsa256(_)
|
||||||
|
) | (
|
||||||
|
CryptoPrivateKeyKind::Ed25519(_),
|
||||||
|
CryptoPrivateKeyKind::Ed25519(_)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for CryptoPrivateKey {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
match self.kind {
|
||||||
|
CryptoPrivateKeyKind::Ed25519(_) => CryptoPrivateKey {
|
||||||
|
kind: CryptoPrivateKeyKind::Ed25519(
|
||||||
|
Ed25519KeyPair::from_pkcs8_maybe_unchecked(&self.serialized_der).unwrap(),
|
||||||
|
),
|
||||||
|
serialized_der: self.serialized_der.clone(),
|
||||||
|
},
|
||||||
|
CryptoPrivateKeyKind::Ecdsa256(_) => CryptoPrivateKey {
|
||||||
|
kind: CryptoPrivateKeyKind::Ecdsa256(
|
||||||
|
EcdsaKeyPair::from_pkcs8(
|
||||||
|
&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
|
||||||
|
&self.serialized_der,
|
||||||
|
&SystemRandom::new(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
serialized_der: self.serialized_der.clone(),
|
||||||
|
},
|
||||||
|
CryptoPrivateKeyKind::Rsa256(_) => CryptoPrivateKey {
|
||||||
|
kind: CryptoPrivateKeyKind::Rsa256(
|
||||||
|
ring::rsa::KeyPair::from_pkcs8(&self.serialized_der).unwrap(),
|
||||||
|
),
|
||||||
|
serialized_der: self.serialized_der.clone(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&KeyPair> for CryptoPrivateKey {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(key_pair: &KeyPair) -> Result<Self> {
|
||||||
|
Self::from_key_pair(key_pair)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CryptoPrivateKey {
|
||||||
|
pub fn from_key_pair(key_pair: &KeyPair) -> Result<Self> {
|
||||||
|
let serialized_der = key_pair.serialize_der();
|
||||||
|
if key_pair.is_compatible(&rcgen::PKCS_ED25519) {
|
||||||
|
Ok(CryptoPrivateKey {
|
||||||
|
kind: CryptoPrivateKeyKind::Ed25519(
|
||||||
|
Ed25519KeyPair::from_pkcs8_maybe_unchecked(&serialized_der)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?,
|
||||||
|
),
|
||||||
|
serialized_der,
|
||||||
|
})
|
||||||
|
} else if key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) {
|
||||||
|
Ok(CryptoPrivateKey {
|
||||||
|
kind: CryptoPrivateKeyKind::Ecdsa256(
|
||||||
|
EcdsaKeyPair::from_pkcs8(
|
||||||
|
&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
|
||||||
|
&serialized_der,
|
||||||
|
&SystemRandom::new(),
|
||||||
|
)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?,
|
||||||
|
),
|
||||||
|
serialized_der,
|
||||||
|
})
|
||||||
|
} else if key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256) {
|
||||||
|
Ok(CryptoPrivateKey {
|
||||||
|
kind: CryptoPrivateKeyKind::Rsa256(
|
||||||
|
ring::rsa::KeyPair::from_pkcs8(&serialized_der)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?,
|
||||||
|
),
|
||||||
|
serialized_der,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(Error::Other("Unsupported key_pair".to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the client provided a "signature_algorithms" extension, then all
|
||||||
|
// certificates provided by the server MUST be signed by a
|
||||||
|
// hash/signature algorithm pair that appears in that extension
|
||||||
|
//
|
||||||
|
// https://tools.ietf.org/html/rfc5246#section-7.4.2
|
||||||
|
pub(crate) fn generate_key_signature(
|
||||||
|
client_random: &[u8],
|
||||||
|
server_random: &[u8],
|
||||||
|
public_key: &[u8],
|
||||||
|
named_curve: NamedCurve,
|
||||||
|
private_key: &CryptoPrivateKey, /*, hash_algorithm: HashAlgorithm*/
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
let msg = value_key_message(client_random, server_random, public_key, named_curve);
|
||||||
|
let signature = match &private_key.kind {
|
||||||
|
CryptoPrivateKeyKind::Ed25519(kp) => kp.sign(&msg).as_ref().to_vec(),
|
||||||
|
CryptoPrivateKeyKind::Ecdsa256(kp) => {
|
||||||
|
let system_random = SystemRandom::new();
|
||||||
|
kp.sign(&system_random, &msg)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?
|
||||||
|
.as_ref()
|
||||||
|
.to_vec()
|
||||||
|
}
|
||||||
|
CryptoPrivateKeyKind::Rsa256(kp) => {
|
||||||
|
let system_random = SystemRandom::new();
|
||||||
|
let mut signature = vec![0; kp.public().modulus_len()];
|
||||||
|
kp.sign(
|
||||||
|
&ring::signature::RSA_PKCS1_SHA256,
|
||||||
|
&system_random,
|
||||||
|
&msg,
|
||||||
|
&mut signature,
|
||||||
|
)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?;
|
||||||
|
|
||||||
|
signature
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add OID_ED25519 which is not defined in x509_parser
|
||||||
|
pub const OID_ED25519: Oid<'static> = oid!(1.3.101 .112);
|
||||||
|
pub const OID_ECDSA: Oid<'static> = oid!(1.2.840 .10045 .2 .1);
|
||||||
|
|
||||||
|
fn verify_signature(
|
||||||
|
message: &[u8],
|
||||||
|
hash_algorithm: &SignatureHashAlgorithm,
|
||||||
|
remote_key_signature: &[u8],
|
||||||
|
raw_certificates: &[Vec<u8>],
|
||||||
|
insecure_verification: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
if raw_certificates.is_empty() {
|
||||||
|
return Err(Error::ErrLengthMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_, certificate) = x509_parser::parse_x509_certificate(&raw_certificates[0])
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?;
|
||||||
|
|
||||||
|
let verify_alg: &dyn ring::signature::VerificationAlgorithm = match hash_algorithm.signature {
|
||||||
|
SignatureAlgorithm::Ed25519 => &ring::signature::ED25519,
|
||||||
|
SignatureAlgorithm::Ecdsa if hash_algorithm.hash == HashAlgorithm::Sha256 => {
|
||||||
|
&ring::signature::ECDSA_P256_SHA256_ASN1
|
||||||
|
}
|
||||||
|
SignatureAlgorithm::Ecdsa if hash_algorithm.hash == HashAlgorithm::Sha384 => {
|
||||||
|
&ring::signature::ECDSA_P384_SHA384_ASN1
|
||||||
|
}
|
||||||
|
SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha1 => {
|
||||||
|
&ring::signature::RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY
|
||||||
|
}
|
||||||
|
SignatureAlgorithm::Rsa if (hash_algorithm.hash == HashAlgorithm::Sha256) => {
|
||||||
|
if remote_key_signature.len() < 256 && insecure_verification {
|
||||||
|
&ring::signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY
|
||||||
|
} else {
|
||||||
|
&ring::signature::RSA_PKCS1_2048_8192_SHA256
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha384 => {
|
||||||
|
&ring::signature::RSA_PKCS1_2048_8192_SHA384
|
||||||
|
}
|
||||||
|
SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha512 => {
|
||||||
|
if remote_key_signature.len() < 256 && insecure_verification {
|
||||||
|
&ring::signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY
|
||||||
|
} else {
|
||||||
|
&ring::signature::RSA_PKCS1_2048_8192_SHA512
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return Err(Error::ErrKeySignatureVerifyUnimplemented),
|
||||||
|
};
|
||||||
|
|
||||||
|
log::trace!("Picked an algorithm {verify_alg:?}");
|
||||||
|
|
||||||
|
let public_key = ring::signature::UnparsedPublicKey::new(
|
||||||
|
verify_alg,
|
||||||
|
certificate
|
||||||
|
.tbs_certificate
|
||||||
|
.subject_pki
|
||||||
|
.subject_public_key
|
||||||
|
.data,
|
||||||
|
);
|
||||||
|
|
||||||
|
public_key
|
||||||
|
.verify(message, remote_key_signature)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn verify_key_signature(
|
||||||
|
message: &[u8],
|
||||||
|
hash_algorithm: &SignatureHashAlgorithm,
|
||||||
|
remote_key_signature: &[u8],
|
||||||
|
raw_certificates: &[Vec<u8>],
|
||||||
|
insecure_verification: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
verify_signature(
|
||||||
|
message,
|
||||||
|
hash_algorithm,
|
||||||
|
remote_key_signature,
|
||||||
|
raw_certificates,
|
||||||
|
insecure_verification,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the server has sent a CertificateRequest message, the client MUST send the Certificate
|
||||||
|
// message. The ClientKeyExchange message is now sent, and the content
|
||||||
|
// of that message will depend on the public key algorithm selected
|
||||||
|
// between the ClientHello and the ServerHello. If the client has sent
|
||||||
|
// a certificate with signing ability, a digitally-signed
|
||||||
|
// CertificateVerify message is sent to explicitly verify possession of
|
||||||
|
// the private key in the certificate.
|
||||||
|
// https://tools.ietf.org/html/rfc5246#section-7.3
|
||||||
|
pub(crate) fn generate_certificate_verify(
|
||||||
|
handshake_bodies: &[u8],
|
||||||
|
private_key: &CryptoPrivateKey, /*, hashAlgorithm hashAlgorithm*/
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
let signature = match &private_key.kind {
|
||||||
|
CryptoPrivateKeyKind::Ed25519(kp) => kp.sign(handshake_bodies).as_ref().to_vec(),
|
||||||
|
CryptoPrivateKeyKind::Ecdsa256(kp) => {
|
||||||
|
let system_random = SystemRandom::new();
|
||||||
|
kp.sign(&system_random, handshake_bodies)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?
|
||||||
|
.as_ref()
|
||||||
|
.to_vec()
|
||||||
|
}
|
||||||
|
CryptoPrivateKeyKind::Rsa256(kp) => {
|
||||||
|
let system_random = SystemRandom::new();
|
||||||
|
let mut signature = vec![0; kp.public().modulus_len()];
|
||||||
|
kp.sign(
|
||||||
|
&ring::signature::RSA_PKCS1_SHA256,
|
||||||
|
&system_random,
|
||||||
|
handshake_bodies,
|
||||||
|
&mut signature,
|
||||||
|
)
|
||||||
|
.map_err(|e| Error::Other(e.to_string()))?;
|
||||||
|
|
||||||
|
signature
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn verify_certificate_verify(
|
||||||
|
handshake_bodies: &[u8],
|
||||||
|
hash_algorithm: &SignatureHashAlgorithm,
|
||||||
|
remote_key_signature: &[u8],
|
||||||
|
raw_certificates: &[Vec<u8>],
|
||||||
|
insecure_verification: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
verify_signature(
|
||||||
|
handshake_bodies,
|
||||||
|
hash_algorithm,
|
||||||
|
remote_key_signature,
|
||||||
|
raw_certificates,
|
||||||
|
insecure_verification,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn load_certs(raw_certificates: &[Vec<u8>]) -> Result<Vec<CertificateDer<'static>>> {
|
||||||
|
if raw_certificates.is_empty() {
|
||||||
|
return Err(Error::ErrLengthMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut certs = vec![];
|
||||||
|
for raw_cert in raw_certificates {
|
||||||
|
let cert = CertificateDer::from(raw_cert.to_vec());
|
||||||
|
certs.push(cert);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(certs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn verify_client_cert(
|
||||||
|
raw_certificates: &[Vec<u8>],
|
||||||
|
cert_verifier: &Arc<CertificateVerifier>,
|
||||||
|
) -> Result<Vec<CertificateDer<'static>>> {
|
||||||
|
let chains = load_certs(raw_certificates)?;
|
||||||
|
|
||||||
|
let (end_entity, intermediates) = chains
|
||||||
|
.split_first()
|
||||||
|
.ok_or(Error::ErrClientCertificateRequired)?;
|
||||||
|
|
||||||
|
match cert_verifier.verify_client_cert(end_entity, intermediates) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => return Err(Error::Other(err.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(chains)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn verify_server_cert(
|
||||||
|
raw_certificates: &[Vec<u8>],
|
||||||
|
cert_verifier: &Arc<CertificateVerifier>,
|
||||||
|
server_name: &str,
|
||||||
|
) -> Result<Vec<CertificateDer<'static>>> {
|
||||||
|
let chains = load_certs(raw_certificates)?;
|
||||||
|
|
||||||
|
let (end_entity, intermediates) = chains
|
||||||
|
.split_first()
|
||||||
|
.ok_or(Error::ErrServerMustHaveCertificate)?;
|
||||||
|
match cert_verifier.verify_server_cert(end_entity, intermediates, server_name) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => return Err(Error::Other(err.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(chains)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn generate_aead_additional_data(h: &RecordLayerHeader, payload_len: usize) -> Vec<u8> {
|
||||||
|
let mut additional_data = vec![0u8; 13];
|
||||||
|
// SequenceNumber MUST be set first
|
||||||
|
// we only want uint48, clobbering an extra 2 (using uint64, rust doesn't have uint48)
|
||||||
|
additional_data[..8].copy_from_slice(&h.sequence_number.to_be_bytes());
|
||||||
|
additional_data[..2].copy_from_slice(&h.epoch.to_be_bytes());
|
||||||
|
additional_data[8] = h.content_type as u8;
|
||||||
|
additional_data[9] = h.protocol_version.major;
|
||||||
|
additional_data[10] = h.protocol_version.minor;
|
||||||
|
additional_data[11..].copy_from_slice(&(payload_len as u16).to_be_bytes());
|
||||||
|
|
||||||
|
additional_data
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
#[cfg(feature = "pem")]
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "pem")]
|
||||||
|
#[test]
|
||||||
|
fn test_certificate_serialize_pem_and_from_pem() -> crate::error::Result<()> {
|
||||||
|
let cert = Certificate::generate_self_signed(vec!["webrtc.rs".to_owned()])?;
|
||||||
|
|
||||||
|
let pem = cert.serialize_pem();
|
||||||
|
let loaded_cert = Certificate::from_pem(&pem)?;
|
||||||
|
|
||||||
|
assert_eq!(loaded_cert, cert);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
122
libs/dtls/src/crypto/padding.rs
Normal file
122
libs/dtls/src/crypto/padding.rs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
use cbc::cipher::block_padding::{PadType, RawPadding, UnpadError};
|
||||||
|
use core::panic;
|
||||||
|
|
||||||
|
pub enum DtlsPadding {}
|
||||||
|
/// Reference: RFC5246, 6.2.3.2
|
||||||
|
impl RawPadding for DtlsPadding {
|
||||||
|
const TYPE: PadType = PadType::Reversible;
|
||||||
|
|
||||||
|
fn raw_pad(block: &mut [u8], pos: usize) {
|
||||||
|
if pos >= block.len() {
|
||||||
|
panic!("`pos` is bigger or equal to block size");
|
||||||
|
}
|
||||||
|
|
||||||
|
let padding_length = block.len() - pos - 1;
|
||||||
|
if padding_length > 255 {
|
||||||
|
panic!("block size is too big for DTLS");
|
||||||
|
}
|
||||||
|
|
||||||
|
set(&mut block[pos..], padding_length as u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_unpad(data: &[u8]) -> Result<&[u8], UnpadError> {
|
||||||
|
let padding_length = data.last().copied().unwrap_or(1) as usize;
|
||||||
|
if padding_length + 1 > data.len() {
|
||||||
|
return Err(UnpadError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let padding_begin = data.len() - padding_length - 1;
|
||||||
|
|
||||||
|
if data[padding_begin..data.len() - 1]
|
||||||
|
.iter()
|
||||||
|
.any(|&byte| byte as usize != padding_length)
|
||||||
|
{
|
||||||
|
return Err(UnpadError);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(&data[0..padding_begin])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets all bytes in `dst` equal to `value`
|
||||||
|
#[inline(always)]
|
||||||
|
fn set(dst: &mut [u8], value: u8) {
|
||||||
|
// SAFETY: we overwrite valid memory behind `dst`
|
||||||
|
// note: loop is not used here because it produces
|
||||||
|
// unnecessary branch which tests for zero-length slices
|
||||||
|
unsafe {
|
||||||
|
core::ptr::write_bytes(dst.as_mut_ptr(), value, dst.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn padding_length_is_amount_of_bytes_excluding_the_padding_length_itself() -> Result<(), ()> {
|
||||||
|
for original_length in 0..128 {
|
||||||
|
for padding_length in 0..(256 - original_length) {
|
||||||
|
let mut block = vec![0; original_length + padding_length + 1];
|
||||||
|
rand::rng().fill(&mut block[0..original_length]);
|
||||||
|
let original = block[0..original_length].to_vec();
|
||||||
|
DtlsPadding::raw_pad(&mut block, original_length);
|
||||||
|
|
||||||
|
for byte in block[original_length..].iter() {
|
||||||
|
assert_eq!(*byte as usize, padding_length);
|
||||||
|
}
|
||||||
|
assert_eq!(block[0..original_length], original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn full_block_is_padding_error() {
|
||||||
|
for original_length in 0..256 {
|
||||||
|
let mut block = vec![0; original_length];
|
||||||
|
DtlsPadding::raw_pad(&mut block, original_length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn padding_length_bigger_than_255_is_a_pad_error() {
|
||||||
|
let padding_length = 256;
|
||||||
|
for original_length in 0..128 {
|
||||||
|
let mut block = vec![0; original_length + padding_length + 1];
|
||||||
|
DtlsPadding::raw_pad(&mut block, original_length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_block_is_unpadding_error() {
|
||||||
|
let r = DtlsPadding::raw_unpad(&[]);
|
||||||
|
assert!(r.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn padding_too_big_for_block_is_unpadding_error() {
|
||||||
|
let r = DtlsPadding::raw_unpad(&[1]);
|
||||||
|
assert!(r.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn one_of_the_padding_bytes_with_value_different_than_padding_length_is_unpadding_error() {
|
||||||
|
for padding_length in 0..16 {
|
||||||
|
for invalid_byte in 0..padding_length {
|
||||||
|
let mut block = vec![0; padding_length + 1];
|
||||||
|
DtlsPadding::raw_pad(&mut block, 0);
|
||||||
|
|
||||||
|
assert_eq!(DtlsPadding::raw_unpad(&block).ok(), Some(&[][..]));
|
||||||
|
block[invalid_byte] = (padding_length - 1) as u8;
|
||||||
|
let r = DtlsPadding::raw_unpad(&block);
|
||||||
|
assert!(r.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
libs/dtls/src/curve/mod.rs
Normal file
17
libs/dtls/src/curve/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
pub mod named_curve;
|
||||||
|
|
||||||
|
// https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-10
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub enum EllipticCurveType {
|
||||||
|
NamedCurve = 0x03,
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for EllipticCurveType {
|
||||||
|
fn from(val: u8) -> Self {
|
||||||
|
match val {
|
||||||
|
0x03 => EllipticCurveType::NamedCurve,
|
||||||
|
_ => EllipticCurveType::Unsupported,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
83
libs/dtls/src/curve/named_curve.rs
Normal file
83
libs/dtls/src/curve/named_curve.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
use rand_core::OsRng; // requires 'getrandom' feature
|
||||||
|
|
||||||
|
use crate::error::*;
|
||||||
|
|
||||||
|
// https://www.iana.org/assignments/tls-parameters/tls-parameters.xml#tls-parameters-8
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub enum NamedCurve {
|
||||||
|
Unsupported = 0x0000,
|
||||||
|
P256 = 0x0017,
|
||||||
|
P384 = 0x0018,
|
||||||
|
X25519 = 0x001d,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u16> for NamedCurve {
|
||||||
|
fn from(val: u16) -> Self {
|
||||||
|
match val {
|
||||||
|
0x0017 => NamedCurve::P256,
|
||||||
|
0x0018 => NamedCurve::P384,
|
||||||
|
0x001d => NamedCurve::X25519,
|
||||||
|
_ => NamedCurve::Unsupported,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) enum NamedCurvePrivateKey {
|
||||||
|
EphemeralSecretP256(p256::ecdh::EphemeralSecret),
|
||||||
|
EphemeralSecretP384(p384::ecdh::EphemeralSecret),
|
||||||
|
StaticSecretX25519(x25519_dalek::StaticSecret),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NamedCurveKeypair {
|
||||||
|
pub(crate) curve: NamedCurve,
|
||||||
|
pub(crate) public_key: Vec<u8>,
|
||||||
|
pub(crate) private_key: NamedCurvePrivateKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn elliptic_curve_keypair(curve: NamedCurve) -> Result<NamedCurveKeypair> {
|
||||||
|
let (public_key, private_key) = match curve {
|
||||||
|
NamedCurve::P256 => {
|
||||||
|
let secret_key = p256::ecdh::EphemeralSecret::random(&mut OsRng);
|
||||||
|
let public_key = p256::EncodedPoint::from(secret_key.public_key());
|
||||||
|
(
|
||||||
|
public_key.as_bytes().to_vec(),
|
||||||
|
NamedCurvePrivateKey::EphemeralSecretP256(secret_key),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
NamedCurve::P384 => {
|
||||||
|
let secret_key = p384::ecdh::EphemeralSecret::random(&mut OsRng);
|
||||||
|
let public_key = p384::EncodedPoint::from(secret_key.public_key());
|
||||||
|
(
|
||||||
|
public_key.as_bytes().to_vec(),
|
||||||
|
NamedCurvePrivateKey::EphemeralSecretP384(secret_key),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
NamedCurve::X25519 => {
|
||||||
|
let secret_key = x25519_dalek::StaticSecret::random_from_rng(OsRng);
|
||||||
|
let public_key = x25519_dalek::PublicKey::from(&secret_key);
|
||||||
|
(
|
||||||
|
public_key.as_bytes().to_vec(),
|
||||||
|
NamedCurvePrivateKey::StaticSecretX25519(secret_key),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => return Err(Error::ErrInvalidNamedCurve),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(NamedCurveKeypair {
|
||||||
|
curve,
|
||||||
|
public_key,
|
||||||
|
private_key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NamedCurve {
|
||||||
|
pub fn generate_keypair(&self) -> Result<NamedCurveKeypair> {
|
||||||
|
match *self {
|
||||||
|
NamedCurve::X25519 => elliptic_curve_keypair(NamedCurve::X25519),
|
||||||
|
NamedCurve::P256 => elliptic_curve_keypair(NamedCurve::P256),
|
||||||
|
NamedCurve::P384 => elliptic_curve_keypair(NamedCurve::P384),
|
||||||
|
_ => Err(Error::ErrInvalidNamedCurve),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
223
libs/dtls/src/error.rs
Normal file
223
libs/dtls/src/error.rs
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
use std::io;
|
||||||
|
use std::string::FromUtf8Error;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
use tokio::sync::mpsc::error::SendError as MpscSendError;
|
||||||
|
use util::KeyingMaterialExporterError;
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Debug, Error, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("conn is closed")]
|
||||||
|
ErrConnClosed,
|
||||||
|
#[error("read/write timeout")]
|
||||||
|
ErrDeadlineExceeded,
|
||||||
|
#[error("buffer is too small")]
|
||||||
|
ErrBufferTooSmall,
|
||||||
|
#[error("context is not supported for export_keying_material")]
|
||||||
|
ErrContextUnsupported,
|
||||||
|
#[error("packet is too short")]
|
||||||
|
ErrDtlspacketInvalidLength,
|
||||||
|
#[error("handshake is in progress")]
|
||||||
|
ErrHandshakeInProgress,
|
||||||
|
#[error("invalid content type")]
|
||||||
|
ErrInvalidContentType,
|
||||||
|
#[error("invalid mac")]
|
||||||
|
ErrInvalidMac,
|
||||||
|
#[error("packet length and declared length do not match")]
|
||||||
|
ErrInvalidPacketLength,
|
||||||
|
#[error("export_keying_material can not be used with a reserved label")]
|
||||||
|
ErrReservedExportKeyingMaterial,
|
||||||
|
#[error("client sent certificate verify but we have no certificate to verify")]
|
||||||
|
ErrCertificateVerifyNoCertificate,
|
||||||
|
#[error("client+server do not support any shared cipher suites")]
|
||||||
|
ErrCipherSuiteNoIntersection,
|
||||||
|
#[error("server hello can not be created without a cipher suite")]
|
||||||
|
ErrCipherSuiteUnset,
|
||||||
|
#[error("client sent certificate but did not verify it")]
|
||||||
|
ErrClientCertificateNotVerified,
|
||||||
|
#[error("server required client verification, but got none")]
|
||||||
|
ErrClientCertificateRequired,
|
||||||
|
#[error("server responded with SRTP Profile we do not support")]
|
||||||
|
ErrClientNoMatchingSrtpProfile,
|
||||||
|
#[error("client required Extended Master Secret extension, but server does not support it")]
|
||||||
|
ErrClientRequiredButNoServerEms,
|
||||||
|
#[error("server hello can not be created without a compression method")]
|
||||||
|
ErrCompressionMethodUnset,
|
||||||
|
#[error("client+server cookie does not match")]
|
||||||
|
ErrCookieMismatch,
|
||||||
|
#[error("cookie must not be longer then 255 bytes")]
|
||||||
|
ErrCookieTooLong,
|
||||||
|
#[error("PSK Identity Hint provided but PSK is nil")]
|
||||||
|
ErrIdentityNoPsk,
|
||||||
|
#[error("no certificate provided")]
|
||||||
|
ErrInvalidCertificate,
|
||||||
|
#[error("cipher spec invalid")]
|
||||||
|
ErrInvalidCipherSpec,
|
||||||
|
#[error("invalid or unknown cipher suite")]
|
||||||
|
ErrInvalidCipherSuite,
|
||||||
|
#[error("unable to determine if ClientKeyExchange is a public key or PSK Identity")]
|
||||||
|
ErrInvalidClientKeyExchange,
|
||||||
|
#[error("invalid or unknown compression method")]
|
||||||
|
ErrInvalidCompressionMethod,
|
||||||
|
#[error("ECDSA signature contained zero or negative values")]
|
||||||
|
ErrInvalidEcdsasignature,
|
||||||
|
#[error("invalid or unknown elliptic curve type")]
|
||||||
|
ErrInvalidEllipticCurveType,
|
||||||
|
#[error("invalid extension type")]
|
||||||
|
ErrInvalidExtensionType,
|
||||||
|
#[error("invalid hash algorithm")]
|
||||||
|
ErrInvalidHashAlgorithm,
|
||||||
|
#[error("invalid named curve")]
|
||||||
|
ErrInvalidNamedCurve,
|
||||||
|
#[error("invalid private key type")]
|
||||||
|
ErrInvalidPrivateKey,
|
||||||
|
#[error("named curve and private key type does not match")]
|
||||||
|
ErrNamedCurveAndPrivateKeyMismatch,
|
||||||
|
#[error("invalid server name format")]
|
||||||
|
ErrInvalidSniFormat,
|
||||||
|
#[error("invalid signature algorithm")]
|
||||||
|
ErrInvalidSignatureAlgorithm,
|
||||||
|
#[error("expected and actual key signature do not match")]
|
||||||
|
ErrKeySignatureMismatch,
|
||||||
|
#[error("Conn can not be created with a nil nextConn")]
|
||||||
|
ErrNilNextConn,
|
||||||
|
#[error("connection can not be created, no CipherSuites satisfy this Config")]
|
||||||
|
ErrNoAvailableCipherSuites,
|
||||||
|
#[error("connection can not be created, no SignatureScheme satisfy this Config")]
|
||||||
|
ErrNoAvailableSignatureSchemes,
|
||||||
|
#[error("no certificates configured")]
|
||||||
|
ErrNoCertificates,
|
||||||
|
#[error("no config provided")]
|
||||||
|
ErrNoConfigProvided,
|
||||||
|
#[error("client requested zero or more elliptic curves that are not supported by the server")]
|
||||||
|
ErrNoSupportedEllipticCurves,
|
||||||
|
#[error("unsupported protocol version")]
|
||||||
|
ErrUnsupportedProtocolVersion,
|
||||||
|
#[error("Certificate and PSK provided")]
|
||||||
|
ErrPskAndCertificate,
|
||||||
|
#[error("PSK and PSK Identity Hint must both be set for client")]
|
||||||
|
ErrPskAndIdentityMustBeSetForClient,
|
||||||
|
#[error("SRTP support was requested but server did not respond with use_srtp extension")]
|
||||||
|
ErrRequestedButNoSrtpExtension,
|
||||||
|
#[error("Certificate is mandatory for server")]
|
||||||
|
ErrServerMustHaveCertificate,
|
||||||
|
#[error("client requested SRTP but we have no matching profiles")]
|
||||||
|
ErrServerNoMatchingSrtpProfile,
|
||||||
|
#[error(
|
||||||
|
"server requires the Extended Master Secret extension, but the client does not support it"
|
||||||
|
)]
|
||||||
|
ErrServerRequiredButNoClientEms,
|
||||||
|
#[error("expected and actual verify data does not match")]
|
||||||
|
ErrVerifyDataMismatch,
|
||||||
|
#[error("handshake message unset, unable to marshal")]
|
||||||
|
ErrHandshakeMessageUnset,
|
||||||
|
#[error("invalid flight number")]
|
||||||
|
ErrInvalidFlight,
|
||||||
|
#[error("unable to generate key signature, unimplemented")]
|
||||||
|
ErrKeySignatureGenerateUnimplemented,
|
||||||
|
#[error("unable to verify key signature, unimplemented")]
|
||||||
|
ErrKeySignatureVerifyUnimplemented,
|
||||||
|
#[error("data length and declared length do not match")]
|
||||||
|
ErrLengthMismatch,
|
||||||
|
#[error("buffer not long enough to contain nonce")]
|
||||||
|
ErrNotEnoughRoomForNonce,
|
||||||
|
#[error("feature has not been implemented yet")]
|
||||||
|
ErrNotImplemented,
|
||||||
|
#[error("sequence number overflow")]
|
||||||
|
ErrSequenceNumberOverflow,
|
||||||
|
#[error("unable to marshal fragmented handshakes")]
|
||||||
|
ErrUnableToMarshalFragmented,
|
||||||
|
#[error("invalid state machine transition")]
|
||||||
|
ErrInvalidFsmTransition,
|
||||||
|
#[error("ApplicationData with epoch of 0")]
|
||||||
|
ErrApplicationDataEpochZero,
|
||||||
|
#[error("unhandled contentType")]
|
||||||
|
ErrUnhandledContextType,
|
||||||
|
#[error("context canceled")]
|
||||||
|
ErrContextCanceled,
|
||||||
|
#[error("empty fragment")]
|
||||||
|
ErrEmptyFragment,
|
||||||
|
#[error("Alert is Fatal or Close Notify")]
|
||||||
|
ErrAlertFatalOrClose,
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"Fragment buffer overflow. New size {new_size} is greater than specified max {max_size}"
|
||||||
|
)]
|
||||||
|
ErrFragmentBufferOverflow { new_size: usize, max_size: usize },
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
Io(#[source] IoError),
|
||||||
|
#[error("{0}")]
|
||||||
|
Util(#[from] util::Error),
|
||||||
|
#[error("utf8: {0}")]
|
||||||
|
Utf8(#[from] FromUtf8Error),
|
||||||
|
#[error("{0}")]
|
||||||
|
Sec1(#[source] sec1::Error),
|
||||||
|
#[error("{0}")]
|
||||||
|
Aes(#[from] aes::cipher::InvalidLength),
|
||||||
|
#[error("{0}")]
|
||||||
|
P256(#[source] P256Error),
|
||||||
|
#[error("{0}")]
|
||||||
|
RcGen(#[from] rcgen::Error),
|
||||||
|
#[error("mpsc send: {0}")]
|
||||||
|
MpscSend(String),
|
||||||
|
#[error("keying material: {0}")]
|
||||||
|
KeyingMaterial(#[from] KeyingMaterialExporterError),
|
||||||
|
|
||||||
|
/// Error parsing a given PEM string.
|
||||||
|
#[error("invalid PEM: {0}")]
|
||||||
|
InvalidPEM(String),
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[error("{0}")]
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("io error: {0}")]
|
||||||
|
pub struct IoError(#[from] pub io::Error);
|
||||||
|
|
||||||
|
// Workaround for wanting PartialEq for io::Error.
|
||||||
|
impl PartialEq for IoError {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.0.kind() == other.0.kind()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for Error {
|
||||||
|
fn from(e: io::Error) -> Self {
|
||||||
|
Error::Io(IoError(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<sec1::Error> for Error {
|
||||||
|
fn from(e: sec1::Error) -> Self {
|
||||||
|
Error::Sec1(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("{0}")]
|
||||||
|
pub struct P256Error(#[source] p256::elliptic_curve::Error);
|
||||||
|
|
||||||
|
impl PartialEq for P256Error {
|
||||||
|
fn eq(&self, _: &Self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<p256::elliptic_curve::Error> for Error {
|
||||||
|
fn from(e: p256::elliptic_curve::Error) -> Self {
|
||||||
|
Error::P256(P256Error(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because Tokio SendError is parameterized, we sadly lose the backtrace.
|
||||||
|
impl<T> From<MpscSendError<T>> for Error {
|
||||||
|
fn from(e: MpscSendError<T>) -> Self {
|
||||||
|
Error::MpscSend(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
56
libs/dtls/src/extension/extension_server_name.rs
Normal file
56
libs/dtls/src/extension/extension_server_name.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod extension_server_name_test;
|
||||||
|
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const EXTENSION_SERVER_NAME_TYPE_DNSHOST_NAME: u8 = 0;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ExtensionServerName {
|
||||||
|
pub(crate) server_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtensionServerName {
|
||||||
|
pub fn extension_value(&self) -> ExtensionValue {
|
||||||
|
ExtensionValue::ServerName
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> usize {
|
||||||
|
//TODO: check how to do cryptobyte?
|
||||||
|
2 + 2 + 1 + 2 + self.server_name.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn marshal<W: Write>(&self, writer: &mut W) -> Result<()> {
|
||||||
|
//TODO: check how to do cryptobyte?
|
||||||
|
writer.write_u16::<BigEndian>(2 + 1 + 2 + self.server_name.len() as u16)?;
|
||||||
|
writer.write_u16::<BigEndian>(1 + 2 + self.server_name.len() as u16)?;
|
||||||
|
writer.write_u8(EXTENSION_SERVER_NAME_TYPE_DNSHOST_NAME)?;
|
||||||
|
writer.write_u16::<BigEndian>(self.server_name.len() as u16)?;
|
||||||
|
writer.write_all(self.server_name.as_bytes())?;
|
||||||
|
|
||||||
|
Ok(writer.flush()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unmarshal<R: Read>(reader: &mut R) -> Result<Self> {
|
||||||
|
//TODO: check how to do cryptobyte?
|
||||||
|
let _ = reader.read_u16::<BigEndian>()? as usize;
|
||||||
|
let _ = reader.read_u16::<BigEndian>()? as usize;
|
||||||
|
|
||||||
|
let name_type = reader.read_u8()?;
|
||||||
|
if name_type != EXTENSION_SERVER_NAME_TYPE_DNSHOST_NAME {
|
||||||
|
return Err(Error::ErrInvalidSniFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
let buf_len = reader.read_u16::<BigEndian>()? as usize;
|
||||||
|
let mut buf: Vec<u8> = vec![0u8; buf_len];
|
||||||
|
reader.read_exact(&mut buf)?;
|
||||||
|
|
||||||
|
let server_name = String::from_utf8(buf)?;
|
||||||
|
|
||||||
|
Ok(ExtensionServerName { server_name })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use std::io::{BufReader, BufWriter};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extension_server_name() -> Result<()> {
|
||||||
|
let extension = ExtensionServerName {
|
||||||
|
server_name: "test.domain".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut raw = vec![];
|
||||||
|
{
|
||||||
|
let mut writer = BufWriter::<&mut Vec<u8>>::new(raw.as_mut());
|
||||||
|
extension.marshal(&mut writer)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut reader = BufReader::new(raw.as_slice());
|
||||||
|
let new_extension = ExtensionServerName::unmarshal(&mut reader)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
new_extension, extension,
|
||||||
|
"extensionServerName marshal: got {new_extension:?} expected {extension:?}",
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod extension_supported_elliptic_curves_test;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::curve::named_curve::*;
|
||||||
|
|
||||||
|
const EXTENSION_SUPPORTED_GROUPS_HEADER_SIZE: usize = 6;
|
||||||
|
|
||||||
|
/// ## Specifications
|
||||||
|
///
|
||||||
|
/// * [RFC 8422 §5.1.1]
|
||||||
|
///
|
||||||
|
/// [RFC 8422 §5.1.1]: https://tools.ietf.org/html/rfc8422#section-5.1.1
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ExtensionSupportedEllipticCurves {
|
||||||
|
pub elliptic_curves: Vec<NamedCurve>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtensionSupportedEllipticCurves {
|
||||||
|
pub fn extension_value(&self) -> ExtensionValue {
|
||||||
|
ExtensionValue::SupportedEllipticCurves
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> usize {
|
||||||
|
2 + 2 + self.elliptic_curves.len() * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn marshal<W: Write>(&self, writer: &mut W) -> Result<()> {
|
||||||
|
writer.write_u16::<BigEndian>(2 + 2 * self.elliptic_curves.len() as u16)?;
|
||||||
|
writer.write_u16::<BigEndian>(2 * self.elliptic_curves.len() as u16)?;
|
||||||
|
for v in &self.elliptic_curves {
|
||||||
|
writer.write_u16::<BigEndian>(*v as u16)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(writer.flush()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unmarshal<R: Read>(reader: &mut R) -> Result<Self> {
|
||||||
|
let _ = reader.read_u16::<BigEndian>()?;
|
||||||
|
|
||||||
|
let group_count = reader.read_u16::<BigEndian>()? as usize / 2;
|
||||||
|
let mut elliptic_curves = vec![];
|
||||||
|
for _ in 0..group_count {
|
||||||
|
let elliptic_curve = reader.read_u16::<BigEndian>()?.into();
|
||||||
|
elliptic_curves.push(elliptic_curve);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ExtensionSupportedEllipticCurves { elliptic_curves })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use std::io::{BufReader, BufWriter};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extension_supported_groups() -> Result<()> {
|
||||||
|
let raw_supported_groups = vec![0x0, 0x4, 0x0, 0x2, 0x0, 0x1d]; // 0x0, 0xa,
|
||||||
|
let parsed_supported_groups = ExtensionSupportedEllipticCurves {
|
||||||
|
elliptic_curves: vec![NamedCurve::X25519],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut raw = vec![];
|
||||||
|
{
|
||||||
|
let mut writer = BufWriter::<&mut Vec<u8>>::new(raw.as_mut());
|
||||||
|
parsed_supported_groups.marshal(&mut writer)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
raw, raw_supported_groups,
|
||||||
|
"extensionSupportedGroups marshal: got {raw:?}, want {raw_supported_groups:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut reader = BufReader::new(raw.as_slice());
|
||||||
|
let new_supported_groups = ExtensionSupportedEllipticCurves::unmarshal(&mut reader)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
new_supported_groups, parsed_supported_groups,
|
||||||
|
"extensionSupportedGroups unmarshal: got {new_supported_groups:?}, want {parsed_supported_groups:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
53
libs/dtls/src/extension/extension_supported_point_formats.rs
Normal file
53
libs/dtls/src/extension/extension_supported_point_formats.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod extension_supported_point_formats_test;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const EXTENSION_SUPPORTED_POINT_FORMATS_SIZE: usize = 5;
|
||||||
|
|
||||||
|
pub type EllipticCurvePointFormat = u8;
|
||||||
|
|
||||||
|
pub const ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED: EllipticCurvePointFormat = 0;
|
||||||
|
|
||||||
|
/// ## Specifications
|
||||||
|
///
|
||||||
|
/// * [RFC 4492 §5.1.2]
|
||||||
|
///
|
||||||
|
/// [RFC 4492 §5.1.2]: https://tools.ietf.org/html/rfc4492#section-5.1.2
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ExtensionSupportedPointFormats {
|
||||||
|
pub(crate) point_formats: Vec<EllipticCurvePointFormat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtensionSupportedPointFormats {
|
||||||
|
pub fn extension_value(&self) -> ExtensionValue {
|
||||||
|
ExtensionValue::SupportedPointFormats
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> usize {
|
||||||
|
2 + 1 + self.point_formats.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn marshal<W: Write>(&self, writer: &mut W) -> Result<()> {
|
||||||
|
writer.write_u16::<BigEndian>(1 + self.point_formats.len() as u16)?;
|
||||||
|
writer.write_u8(self.point_formats.len() as u8)?;
|
||||||
|
for v in &self.point_formats {
|
||||||
|
writer.write_u8(*v)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(writer.flush()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unmarshal<R: Read>(reader: &mut R) -> Result<Self> {
|
||||||
|
let _ = reader.read_u16::<BigEndian>()?;
|
||||||
|
|
||||||
|
let point_format_count = reader.read_u8()? as usize;
|
||||||
|
let mut point_formats = vec![];
|
||||||
|
for _ in 0..point_format_count {
|
||||||
|
let point_format = reader.read_u8()?;
|
||||||
|
point_formats.push(point_format);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ExtensionSupportedPointFormats { point_formats })
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user