diff --git a/.github/actions/build-rust-target/action.yml b/.github/actions/build-rust-target/action.yml new file mode 100644 index 000000000..57d53af39 --- /dev/null +++ b/.github/actions/build-rust-target/action.yml @@ -0,0 +1,44 @@ +name: Build Rust target +description: Install a Rust toolchain and build release binaries for one target. + +inputs: + target: + description: Rust target triple. + required: true + toolchain: + description: Rust toolchain version. + required: true + use-cross: + description: Use cross instead of cargo for the build. + required: false + default: "false" + working-directory: + description: Directory where cargo or cross should run. + required: false + default: "." + +runs: + using: composite + steps: + - name: Install toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ inputs.toolchain }} + components: rustfmt + targets: ${{ inputs.target }} + + - name: Install cross + if: inputs.use-cross == 'true' + uses: taiki-e/install-action@cross + + - name: Build with cross + if: inputs.use-cross == 'true' + shell: bash + working-directory: ${{ inputs.working-directory }} + run: cross build --release --all-features --target=${{ inputs.target }} + + - name: Build with cargo + if: inputs.use-cross != 'true' + shell: bash + working-directory: ${{ inputs.working-directory }} + run: cargo build --release --all-features --target=${{ inputs.target }} diff --git a/.github/actions/docker-build-and-push/action.yml b/.github/actions/docker-build-and-push/action.yml new file mode 100644 index 000000000..aab214803 --- /dev/null +++ b/.github/actions/docker-build-and-push/action.yml @@ -0,0 +1,148 @@ +name: Build and push Docker image +description: Build a single-architecture Docker image from downloaded Rust binaries. + +inputs: + artifact-name: + description: GitHub artifact name containing the binaries. + required: true + artifact-path: + description: Directory where the binaries should be downloaded. + required: true + context: + description: Docker build context. + required: true + docker-platform: + description: Docker platform to build. + required: true + docker-hub-image: + description: Docker Hub image name. + required: true + ghcr-image: + description: GHCR image name. + required: true + image-title: + description: Human-readable image title used in OCI metadata. + required: true + image-description: + description: Human-readable image description used in OCI metadata. + required: true + project-url: + description: Project homepage URL used in OCI metadata. + required: true + documentation-url: + description: Documentation URL used in OCI metadata. + required: true + arch-name: + description: Architecture suffix used in tags. + required: true + s6-arch: + description: s6-overlay architecture build argument. Leave empty for classic images. + required: false + default: "" + version-tag: + description: Exact version tag for immutable image tags. + required: true + major-tag: + description: Major version tag without a leading v. + required: true + latest-tag: + description: Floating release tag. + required: true + docker-hub-username: + description: Docker Hub username. + required: true + docker-hub-password: + description: Docker Hub token. + required: true + ghcr-username: + description: GHCR username. + required: true + ghcr-token: + description: GHCR token. + required: true + +runs: + using: composite + steps: + - name: Download binaries + uses: actions/download-artifact@v6 + with: + name: ${{ inputs.artifact-name }} + path: ${{ inputs.artifact-path }} + + - name: Make binaries executable + shell: bash + run: chmod -v a+x "${{ inputs.artifact-path }}"/* + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ inputs.docker-hub-username }} + password: ${{ inputs.docker-hub-password }} + + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ inputs.ghcr-username }} + password: ${{ inputs.ghcr-token }} + + - name: Resolve source revision + id: revision + shell: bash + run: | + source_dir="$(dirname "${{ inputs.context }}")" + echo "sha=$(git -C "${source_dir}" rev-parse HEAD)" >> "${GITHUB_OUTPUT}" + + - name: Resolve created timestamp + id: created + shell: bash + run: | + echo "value=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "${GITHUB_OUTPUT}" + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: registry.hub.docker.com/${{ inputs.docker-hub-image }} + labels: | + org.opencontainers.image.title=${{ inputs.image-title }} + org.opencontainers.image.description=${{ inputs.image-description }} + org.opencontainers.image.url=${{ inputs.project-url }} + org.opencontainers.image.source=${{ inputs.project-url }} + org.opencontainers.image.version=${{ inputs.version-tag }} + org.opencontainers.image.revision=${{ steps.revision.outputs.sha }} + org.opencontainers.image.created=${{ steps.created.outputs.value }} + org.opencontainers.image.licenses=AGPL-3.0-or-later + org.opencontainers.image.documentation=${{ inputs.documentation-url }} + + - name: Resolve build args + id: build-args + shell: bash + run: | + if [ -n "${{ inputs.s6-arch }}" ]; then + echo "value=S6_ARCH=${{ inputs.s6-arch }}" >> "${GITHUB_OUTPUT}" + fi + + - name: Build and push Docker image + uses: docker/build-push-action@v7 + with: + context: ${{ inputs.context }} + platforms: ${{ inputs.docker-platform }} + push: true + provenance: false + build-args: ${{ steps.build-args.outputs.value }} + tags: | + ${{ inputs.docker-hub-image }}:${{ inputs.latest-tag }}-${{ inputs.arch-name }} + ${{ inputs.docker-hub-image }}:${{ inputs.version-tag }}-${{ inputs.arch-name }} + ${{ inputs.docker-hub-image }}:${{ inputs.major-tag }}-${{ inputs.arch-name }} + ${{ inputs.ghcr-image }}:${{ inputs.latest-tag }}-${{ inputs.arch-name }} + ${{ inputs.ghcr-image }}:${{ inputs.version-tag }}-${{ inputs.arch-name }} + ${{ inputs.ghcr-image }}:${{ inputs.major-tag }}-${{ inputs.arch-name }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/actions/docker-manifest/action.yml b/.github/actions/docker-manifest/action.yml new file mode 100644 index 000000000..fc76ae219 --- /dev/null +++ b/.github/actions/docker-manifest/action.yml @@ -0,0 +1,126 @@ +name: Create Docker manifests +description: Create Docker Hub and GHCR multi-architecture manifests. + +inputs: + docker-hub-image: + description: Docker Hub image name. + required: true + ghcr-image: + description: GHCR image name. + required: true + source-path: + description: Checked out repository path used to resolve the image revision. + required: true + image-title: + description: Human-readable image title used in OCI metadata. + required: true + image-description: + description: Human-readable image description used in OCI metadata. + required: true + project-url: + description: Project homepage URL used in OCI metadata. + required: true + documentation-url: + description: Documentation URL used in OCI metadata. + required: true + arch-suffixes: + description: Space-separated architecture suffixes. + required: true + version-tag: + description: Exact version tag for immutable image tags. + required: true + major-tag: + description: Major version tag without a leading v. + required: true + latest-tag: + description: Floating release tag. + required: true + docker-hub-username: + description: Docker Hub username. + required: true + docker-hub-password: + description: Docker Hub token. + required: true + ghcr-username: + description: GHCR username. + required: true + ghcr-token: + description: GHCR token. + required: true + +runs: + using: composite + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ inputs.docker-hub-username }} + password: ${{ inputs.docker-hub-password }} + + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ inputs.ghcr-username }} + password: ${{ inputs.ghcr-token }} + + - name: Resolve source revision + id: revision + shell: bash + run: | + echo "sha=$(git -C "${{ inputs.source-path }}" rev-parse HEAD)" >> "${GITHUB_OUTPUT}" + + - name: Resolve created timestamp + id: created + shell: bash + run: | + echo "value=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "${GITHUB_OUTPUT}" + + - name: Create manifests + shell: bash + env: + DOCKER_HUB_IMAGE: ${{ inputs.docker-hub-image }} + GHCR_IMAGE: ${{ inputs.ghcr-image }} + IMAGE_TITLE: ${{ inputs.image-title }} + IMAGE_DESCRIPTION: ${{ inputs.image-description }} + PROJECT_URL: ${{ inputs.project-url }} + DOCUMENTATION_URL: ${{ inputs.documentation-url }} + ARCH_SUFFIXES: ${{ inputs.arch-suffixes }} + VERSION_TAG: ${{ inputs.version-tag }} + MAJOR_TAG: ${{ inputs.major-tag }} + LATEST_TAG: ${{ inputs.latest-tag }} + REVISION: ${{ steps.revision.outputs.sha }} + CREATED: ${{ steps.created.outputs.value }} + run: | + create_manifest() { + local image="$1" + local tag="$2" + local sources="" + + for arch in ${ARCH_SUFFIXES}; do + sources="${sources} ${image}:${tag}-${arch}" + done + + docker buildx imagetools create \ + --tag "${image}:${tag}" \ + --annotation "index:org.opencontainers.image.title=${IMAGE_TITLE}" \ + --annotation "index:org.opencontainers.image.description=${IMAGE_DESCRIPTION}" \ + --annotation "index:org.opencontainers.image.url=${PROJECT_URL}" \ + --annotation "index:org.opencontainers.image.source=${PROJECT_URL}" \ + --annotation "index:org.opencontainers.image.version=${VERSION_TAG}" \ + --annotation "index:org.opencontainers.image.revision=${REVISION}" \ + --annotation "index:org.opencontainers.image.created=${CREATED}" \ + --annotation "index:org.opencontainers.image.licenses=AGPL-3.0-or-later" \ + --annotation "index:org.opencontainers.image.documentation=${DOCUMENTATION_URL}" \ + ${sources} + } + + create_manifest "${DOCKER_HUB_IMAGE}" "${VERSION_TAG}" + create_manifest "${GHCR_IMAGE}" "${VERSION_TAG}" + create_manifest "${DOCKER_HUB_IMAGE}" "${MAJOR_TAG}" + create_manifest "${GHCR_IMAGE}" "${MAJOR_TAG}" + create_manifest "${DOCKER_HUB_IMAGE}" "${LATEST_TAG}" + create_manifest "${GHCR_IMAGE}" "${LATEST_TAG}" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index 6df15384c..000000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,602 +0,0 @@ -name: build - -# ------------- NOTE -# please setup some secrets before running this workflow: -# DOCKER_IMAGE should be the target image name on docker hub (e.g. "rustdesk/rustdesk-server-s6" ) -# DOCKER_IMAGE_CLASSIC should be the target image name on docker hub for the old build (e.g. "rustdesk/rustdesk-server" ) -# DOCKER_HUB_USERNAME is the username you normally use to login at https://hub.docker.com/ -# DOCKER_HUB_PASSWORD is a token you should create under "account settings / security" with read/write access - -permissions: - contents: read - packages: write - -on: - workflow_dispatch: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' - - '[0-9]+.[0-9]+.[0-9]+' - - 'v[0-9]+.[0-9]+.[0-9]+-[0-9]+' - - '[0-9]+.[0-9]+.[0-9]+-[0-9]+' - -env: - CARGO_TERM_COLOR: always - LATEST_TAG: latest - GHCR_IMAGE: ghcr.io/rustdesk/rustdesk-server-s6 - GHCR_IMAGE_CLASSIC: ghcr.io/rustdesk/rustdesk-server - -jobs: - - # binary build - build: - - name: Build - ${{ matrix.job.name }} - runs-on: ubuntu-22.04 - strategy: - fail-fast: false - matrix: - job: - - { name: "amd64", target: "x86_64-unknown-linux-musl" } - - { name: "arm64v8", target: "aarch64-unknown-linux-musl" } - - { name: "armv7", target: "armv7-unknown-linux-musleabihf" } - - { name: "i386", target: "i686-unknown-linux-musl" } - #- { name: "amd64fb", target: "x86_64-unknown-freebsd" } - - steps: - - - name: Checkout - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Install toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: "stable" - override: true - default: true - components: rustfmt - profile: minimal - target: ${{ matrix.job.target }} - - - name: Build - uses: actions-rs/cargo@v1 - with: - command: build - args: --release --all-features --target=${{ matrix.job.target }} - use-cross: true - - - name: Exec chmod - run: chmod -v a+x target/${{ matrix.job.target }}/release/* - - - name: Publish Artifacts - uses: actions/upload-artifact@v4 - with: - name: binaries-linux-${{ matrix.job.name }} - path: | - target/${{ matrix.job.target }}/release/hbbr - target/${{ matrix.job.target }}/release/hbbs - target/${{ matrix.job.target }}/release/rustdesk-utils - if-no-files-found: error - - build-win: - name: Build - windows - runs-on: windows-2022 - - steps: - - - name: Checkout - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Install toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: "stable" - override: true - default: true - components: rustfmt - profile: minimal - target: x86_64-pc-windows-msvc - - - name: Build - uses: actions-rs/cargo@v1 - with: - command: build - args: --release --all-features --target=x86_64-pc-windows-msvc - use-cross: true - - - name: Install NSIS - run: | - iwr -useb get.scoop.sh -outfile 'install.ps1' - .\install.ps1 -RunAsAdmin - scoop update - scoop bucket add extras - scoop install nsis - - - name: Install Node.js - uses: actions/setup-node@v3 - with: - node-version: 16 - - - name: Sign exe files - uses: GermanBluefox/code-sign-action@v7 - if: false - with: - certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' - password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' - certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' - folder: 'target\x86_64-pc-windows-msvc\release' - recursive: false - - - name: Build UI browser file - run: | - npm i - npm run build - working-directory: ./ui/html - - - name: Build UI setup file - run: | - rustup default nightly - cargo build --release - xcopy /y ..\target\x86_64-pc-windows-msvc\release\*.exe setup\bin\ - xcopy /y target\release\*.exe setup\ - mkdir setup\logs - makensis /V1 setup.nsi - mkdir SignOutput - mv RustDeskServer.Setup.exe SignOutput\ - mv ..\target\x86_64-pc-windows-msvc\release\*.exe SignOutput\ - working-directory: ./ui - - - name: Sign UI setup file - uses: GermanBluefox/code-sign-action@v7 - if: false - with: - certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' - password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' - certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' - folder: './ui/SignOutput' - recursive: false - - - name: Publish Artifacts - uses: actions/upload-artifact@v4 - with: - name: binaries-windows-x86_64 - path: | - ui\SignOutput\hbbr.exe - ui\SignOutput\hbbs.exe - ui\SignOutput\rustdesk-utils.exe - ui\SignOutput\RustDeskServer.Setup.exe - if-no-files-found: error - - # github (draft) release with all binaries - release: - - name: Github release - needs: - - build - - build-win - runs-on: ubuntu-22.04 - strategy: - fail-fast: false - matrix: - job: - - { os: "linux", name: "amd64", suffix: "" } - - { os: "linux", name: "arm64v8", suffix: "" } - - { os: "linux", name: "armv7", suffix: "" } - - { os: "linux", name: "i386", suffix: "" } - #- { os: "linux", name: "amd64fb", suffix: "" } - - { os: "windows", name: "x86_64", suffix: "-unsigned" } - - steps: - - - name: Download binaries (${{ matrix.job.os }} - ${{ matrix.job.name }}) - uses: actions/download-artifact@v4 - with: - name: binaries-${{ matrix.job.os }}-${{ matrix.job.name }} - path: ${{ matrix.job.name }} - - - name: Exec chmod - run: chmod -v a+x ${{ matrix.job.name }}/* - - - name: Pack files (${{ matrix.job.os }} - ${{ matrix.job.name }}) - run: | - sudo apt update - DEBIAN_FRONTEND=noninteractive sudo apt install -y zip - zip ${{ matrix.job.name }}/rustdesk-server-${{ matrix.job.os }}-${{ matrix.job.name }}${{ matrix.job.suffix }}.zip ${{ matrix.job.name }}/* - - - name: Create Release (${{ matrix.job.os }} - (${{ matrix.job.name }}) - uses: softprops/action-gh-release@v1 - with: - draft: true - files: ${{ matrix.job.name }}/rustdesk-server-${{ matrix.job.os }}-${{ matrix.job.name }}${{ matrix.job.suffix }}.zip - - # docker build and push of single-arch images - docker: - - name: Docker push - ${{ matrix.job.name }} - needs: build - runs-on: ubuntu-22.04 - strategy: - fail-fast: false - matrix: - job: - - { name: "amd64", docker_platform: "linux/amd64", s6_platform: "x86_64" } - - { name: "arm64v8", docker_platform: "linux/arm64", s6_platform: "aarch64" } - - { name: "armv7", docker_platform: "linux/arm/v7", s6_platform: "armhf" } - - { name: "i386", docker_platform: "linux/386", s6_platform: "i686" } - - steps: - - - name: Checkout - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Download binaries - uses: actions/download-artifact@v4 - with: - name: binaries-linux-${{ matrix.job.name }} - path: docker/rootfs/usr/bin - - - name: Make binaries executable - run: chmod -v a+x docker/rootfs/usr/bin/* - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Log in to Docker Hub - if: github.event_name != 'pull_request' - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_PASSWORD }} - - - name: Log in to GHCR - if: github.event_name != 'pull_request' - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: rustdesk - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v4 - with: - images: registry.hub.docker.com/${{ secrets.DOCKER_IMAGE }} - - - name: Get git tag - id: vars - run: | - T=${GITHUB_REF#refs/*/} - M=${T%%.*} - echo "GIT_TAG=$T" >> $GITHUB_ENV - echo "MAJOR_TAG=$M" >> $GITHUB_ENV - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: "./docker" - platforms: ${{ matrix.job.docker_platform }} - push: true - provenance: false - build-args: | - S6_ARCH=${{ matrix.job.s6_platform }} - tags: | - ${{ secrets.DOCKER_IMAGE }}:${{ env.LATEST_TAG }}-${{ matrix.job.name }} - ${{ secrets.DOCKER_IMAGE }}:${{ env.GIT_TAG }}-${{ matrix.job.name }} - ${{ secrets.DOCKER_IMAGE }}:${{ env.MAJOR_TAG }}-${{ matrix.job.name }} - ${{ env.GHCR_IMAGE }}:${{ env.LATEST_TAG }}-${{ matrix.job.name }} - ${{ env.GHCR_IMAGE }}:${{ env.GIT_TAG }}-${{ matrix.job.name }} - ${{ env.GHCR_IMAGE }}:${{ env.MAJOR_TAG }}-${{ matrix.job.name }} - labels: ${{ steps.meta.outputs.labels }} - - # docker build and push of multiarch images - docker-manifest: - - name: Docker manifest - needs: docker - runs-on: ubuntu-22.04 - - steps: - - - name: Log in to Docker Hub - if: github.event_name != 'pull_request' - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_PASSWORD }} - - - name: Get git tag - id: vars - run: | - T=${GITHUB_REF#refs/*/} - M=${T%%.*} - echo "GIT_TAG=$T" >> $GITHUB_ENV - echo "MAJOR_TAG=$M" >> $GITHUB_ENV - - - name: Log in to GHCR - if: github.event_name != 'pull_request' - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: rustdesk - password: ${{ secrets.GITHUB_TOKEN }} - - # manifest for :1.2.3 tag - # this has to run only if invoked by a new tag - - name: Create and push manifest (:ve.rs.ion) - uses: Noelware/docker-manifest-action@0.4.3 - if: github.event_name != 'workflow_dispatch' - with: - base-image: ${{ secrets.DOCKER_IMAGE }}:${{ env.GIT_TAG }} - extra-images: ${{ secrets.DOCKER_IMAGE }}:${{ env.GIT_TAG }}-amd64,${{ secrets.DOCKER_IMAGE }}:${{ env.GIT_TAG }}-arm64v8,${{ secrets.DOCKER_IMAGE }}:${{ env.GIT_TAG }}-armv7,${{ secrets.DOCKER_IMAGE }}:${{ env.GIT_TAG }}-i386 - push: true - - # manifest for :1 tag (major release) - - name: Create and push manifest (:major) - uses: Noelware/docker-manifest-action@0.4.3 - with: - base-image: ${{ secrets.DOCKER_IMAGE }}:${{ env.MAJOR_TAG }} - extra-images: ${{ secrets.DOCKER_IMAGE }}:${{ env.MAJOR_TAG }}-amd64,${{ secrets.DOCKER_IMAGE }}:${{ env.MAJOR_TAG }}-arm64v8,${{ secrets.DOCKER_IMAGE }}:${{ env.MAJOR_TAG }}-armv7,${{ secrets.DOCKER_IMAGE }}:${{ env.MAJOR_TAG }}-i386 - push: true - - # manifest for :latest tag - - name: Create and push manifest (:latest) - uses: Noelware/docker-manifest-action@0.4.3 - with: - base-image: ${{ secrets.DOCKER_IMAGE }}:${{ env.LATEST_TAG }} - extra-images: ${{ secrets.DOCKER_IMAGE }}:${{ env.LATEST_TAG }}-amd64,${{ secrets.DOCKER_IMAGE }}:${{ env.LATEST_TAG }}-arm64v8,${{ secrets.DOCKER_IMAGE }}:${{ env.LATEST_TAG }}-armv7,${{ secrets.DOCKER_IMAGE }}:${{ env.LATEST_TAG }}-i386 - push: true - - # GHCR manifests - # manifest for :1.2.3 tag - - name: Create and push GHCR manifest (:ve.rs.ion) - uses: Noelware/docker-manifest-action@0.4.3 - if: github.event_name != 'workflow_dispatch' - with: - base-image: ${{ env.GHCR_IMAGE }}:${{ env.GIT_TAG }} - extra-images: ${{ env.GHCR_IMAGE }}:${{ env.GIT_TAG }}-amd64,${{ env.GHCR_IMAGE }}:${{ env.GIT_TAG }}-arm64v8,${{ env.GHCR_IMAGE }}:${{ env.GIT_TAG }}-armv7,${{ env.GHCR_IMAGE }}:${{ env.GIT_TAG }}-i386 - push: true - - # manifest for :1 tag (major release) - - name: Create and push GHCR manifest (:major) - uses: Noelware/docker-manifest-action@0.4.3 - with: - base-image: ${{ env.GHCR_IMAGE }}:${{ env.MAJOR_TAG }} - extra-images: ${{ env.GHCR_IMAGE }}:${{ env.MAJOR_TAG }}-amd64,${{ env.GHCR_IMAGE }}:${{ env.MAJOR_TAG }}-arm64v8,${{ env.GHCR_IMAGE }}:${{ env.MAJOR_TAG }}-armv7,${{ env.GHCR_IMAGE }}:${{ env.MAJOR_TAG }}-i386 - push: true - - # manifest for :latest tag - - name: Create and push GHCR manifest (:latest) - uses: Noelware/docker-manifest-action@0.4.3 - with: - base-image: ${{ env.GHCR_IMAGE }}:${{ env.LATEST_TAG }} - extra-images: ${{ env.GHCR_IMAGE }}:${{ env.LATEST_TAG }}-amd64,${{ env.GHCR_IMAGE }}:${{ env.LATEST_TAG }}-arm64v8,${{ env.GHCR_IMAGE }}:${{ env.LATEST_TAG }}-armv7,${{ env.GHCR_IMAGE }}:${{ env.LATEST_TAG }}-i386 - push: true - - - # docker build and push of single-arch images - docker-classic: - - name: Docker push - ${{ matrix.job.name }} - needs: build - runs-on: ubuntu-22.04 - strategy: - fail-fast: false - matrix: - job: - - { name: "amd64", docker_platform: "linux/amd64" } - - { name: "arm64v8", docker_platform: "linux/arm64" } - - { name: "armv7", docker_platform: "linux/arm/v7" } - - steps: - - - name: Checkout - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Download binaries - uses: actions/download-artifact@v4 - with: - name: binaries-linux-${{ matrix.job.name }} - path: docker-classic/ - - - name: Make binaries executable - run: chmod -v a+x docker-classic/* - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Log in to Docker Hub - if: github.event_name != 'pull_request' - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_PASSWORD }} - - - name: Log in to GHCR - if: github.event_name != 'pull_request' - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: rustdesk - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v4 - with: - images: registry.hub.docker.com/${{ secrets.DOCKER_IMAGE_CLASSIC }} - - - name: Get git tag - id: vars - run: | - T=${GITHUB_REF#refs/*/} - M=${T%%.*} - echo "GIT_TAG=$T" >> $GITHUB_ENV - echo "MAJOR_TAG=$M" >> $GITHUB_ENV - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: "./docker-classic" - platforms: ${{ matrix.job.docker_platform }} - push: true - provenance: false - tags: | - ${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}-${{ matrix.job.name }} - ${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}-${{ matrix.job.name }} - ${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}-${{ matrix.job.name }} - ${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}-${{ matrix.job.name }} - ${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}-${{ matrix.job.name }} - ${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}-${{ matrix.job.name }} - labels: ${{ steps.meta.outputs.labels }} - - # docker build and push of multiarch images - docker-manifest-classic: - - name: Docker manifest - needs: docker - runs-on: ubuntu-22.04 - - steps: - - - name: Log in to Docker Hub - if: github.event_name != 'pull_request' - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_PASSWORD }} - - - name: Get git tag - id: vars - run: | - T=${GITHUB_REF#refs/*/} - M=${T%%.*} - echo "GIT_TAG=$T" >> $GITHUB_ENV - echo "MAJOR_TAG=$M" >> $GITHUB_ENV - - - name: Log in to GHCR - if: github.event_name != 'pull_request' - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: rustdesk - password: ${{ secrets.GITHUB_TOKEN }} - - # manifest for :1.2.3 tag - # this has to run only if invoked by a new tag - - name: Create and push manifest (:ve.rs.ion) - uses: Noelware/docker-manifest-action@0.4.3 - if: github.event_name != 'workflow_dispatch' - with: - base-image: ${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.GIT_TAG }} - extra-images: ${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}-amd64,${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}-arm64v8,${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}-armv7 - push: true - - # manifest for :1 tag (major release) - - name: Create and push manifest (:major) - uses: Noelware/docker-manifest-action@0.4.3 - with: - base-image: ${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }} - extra-images: ${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}-amd64,${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}-arm64v8,${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}-armv7 - push: true - - # manifest for :latest tag - - name: Create and push manifest (:latest) - uses: Noelware/docker-manifest-action@0.4.3 - with: - base-image: ${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }} - extra-images: ${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}-amd64,${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}-arm64v8,${{ secrets.DOCKER_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}-armv7 - push: true - - # GHCR manifests - # manifest for :1.2.3 tag - - name: Create and push GHCR manifest (:ve.rs.ion) - uses: Noelware/docker-manifest-action@0.4.3 - if: github.event_name != 'workflow_dispatch' - with: - base-image: ${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.GIT_TAG }} - extra-images: ${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}-amd64,${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}-arm64v8,${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.GIT_TAG }}-armv7 - push: true - - # manifest for :1 tag (major release) - - name: Create and push GHCR manifest (:major) - uses: Noelware/docker-manifest-action@0.4.3 - with: - base-image: ${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }} - extra-images: ${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}-amd64,${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}-arm64v8,${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.MAJOR_TAG }}-armv7 - push: true - - # manifest for :latest tag - - name: Create and push GHCR manifest (:latest) - uses: Noelware/docker-manifest-action@0.4.3 - with: - base-image: ${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }} - extra-images: ${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}-amd64,${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}-arm64v8,${{ env.GHCR_IMAGE_CLASSIC }}:${{ env.LATEST_TAG }}-armv7 - push: true - - - deb-package: - - name: debian package - ${{ matrix.job.name }} - needs: build - runs-on: ubuntu-22.04 - strategy: - fail-fast: false - matrix: - job: - - { name: "amd64", debian_platform: "amd64", crossbuild_package: "" } - - { name: "arm64v8", debian_platform: "arm64", crossbuild_package: "crossbuild-essential-arm64" } - - { name: "armv7", debian_platform: "armhf", crossbuild_package: "crossbuild-essential-armhf" } - - { name: "i386", debian_platform: "i386", crossbuild_package: "crossbuild-essential-i386" } - - steps: - - - name: Checkout - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Create packaging env - run: | - sudo apt update - DEBIAN_FRONTEND=noninteractive sudo apt install -y devscripts build-essential debhelper pkg-config ${{ matrix.job.crossbuild_package }} - mkdir -p debian-build/${{ matrix.job.name }}/bin - - - name: Download binaries - uses: actions/download-artifact@v4 - with: - name: binaries-linux-${{ matrix.job.name }} - path: debian-build/${{ matrix.job.name }}/bin - - - name: Build package for ${{ matrix.job.name }} arch - run: | - chmod -v a+x debian-build/${{ matrix.job.name }}/bin/* - cp -vr debian systemd debian-build/${{ matrix.job.name }}/ - cat debian/control.tpl | sed 's/{{ ARCH }}/${{ matrix.job.debian_platform }}/' > debian-build/${{ matrix.job.name }}/debian/control - cd debian-build/${{ matrix.job.name }}/ - debuild -i -us -uc -b -a${{ matrix.job.debian_platform }} - - - name: Create Release - uses: softprops/action-gh-release@v1 - with: - draft: true - files: | - debian-build/rustdesk-server-hbbr_*_${{ matrix.job.debian_platform }}.deb - debian-build/rustdesk-server-hbbs_*_${{ matrix.job.debian_platform }}.deb - debian-build/rustdesk-server-utils_*_${{ matrix.job.debian_platform }}.deb diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..d8dec770e --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,68 @@ +name: ci + +permissions: + contents: read + +on: + pull_request: + push: + branches: + - master + +env: + CARGO_TERM_COLOR: always + RUST_TOOLCHAIN: "1.90" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + rust: + name: Rust checks + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Install toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + + - name: Check Rust workspace + run: cargo check --workspace --all-features + + docker: + name: Docker build checks + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Prepare Docker smoke-test binaries + run: | + touch docker/rootfs/usr/bin/hbbs docker/rootfs/usr/bin/hbbr docker/rootfs/usr/bin/rustdesk-utils + chmod +x docker/rootfs/usr/bin/hbbs docker/rootfs/usr/bin/hbbr docker/rootfs/usr/bin/rustdesk-utils + touch docker-classic/hbbs docker-classic/hbbr + chmod +x docker-classic/hbbs docker-classic/hbbr + + - name: Check S6 Docker image + uses: docker/build-push-action@v7 + with: + context: ./docker + push: false + provenance: false + build-args: | + S6_ARCH=x86_64 + + - name: Check classic Docker image + uses: docker/build-push-action@v7 + with: + context: ./docker-classic + push: false + provenance: false diff --git a/.github/workflows/debian.yaml b/.github/workflows/debian.yaml new file mode 100644 index 000000000..d83370588 --- /dev/null +++ b/.github/workflows/debian.yaml @@ -0,0 +1,152 @@ +name: debian + +permissions: + contents: write + +on: + workflow_dispatch: + inputs: + version: + description: Release tag to build, for example v1.2.3. + required: true + type: string + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+-[0-9]+" + +env: + CARGO_TERM_COLOR: always + RUST_TOOLCHAIN: "1.90" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + prepare: + name: Prepare release metadata + runs-on: ubuntu-22.04 + outputs: + release-tag: ${{ steps.version.outputs.release-tag }} + + steps: + - name: Resolve release version + id: version + shell: bash + run: | + if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then + tag="${{ inputs.version }}" + else + tag="${GITHUB_REF_NAME}" + fi + + if [[ ! "${tag}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then + echo "Invalid release tag: ${tag}" >&2 + exit 1 + fi + + echo "release-tag=${tag}" >> "${GITHUB_OUTPUT}" + + build-linux: + name: Build Linux - ${{ matrix.job.name }} + needs: prepare + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + job: + - { name: "amd64", target: "x86_64-unknown-linux-musl" } + - { name: "arm64v8", target: "aarch64-unknown-linux-musl" } + - { name: "armv7", target: "armv7-unknown-linux-musleabihf" } + - { name: "i386", target: "i686-unknown-linux-musl" } + + steps: + - name: Checkout workflow + uses: actions/checkout@v6 + + - name: Checkout release source + uses: actions/checkout@v6 + with: + ref: ${{ needs.prepare.outputs.release-tag }} + path: source + submodules: recursive + + - name: Build Rust binaries + uses: ./.github/actions/build-rust-target + with: + target: ${{ matrix.job.target }} + toolchain: ${{ env.RUST_TOOLCHAIN }} + use-cross: "true" + working-directory: source + + - name: Make binaries executable + run: chmod -v a+x source/target/${{ matrix.job.target }}/release/* + + - name: Upload binaries + uses: actions/upload-artifact@v6 + with: + name: binaries-linux-${{ matrix.job.name }} + path: | + source/target/${{ matrix.job.target }}/release/hbbr + source/target/${{ matrix.job.target }}/release/hbbs + source/target/${{ matrix.job.target }}/release/rustdesk-utils + if-no-files-found: error + + deb-package: + name: Debian package - ${{ matrix.job.name }} + needs: + - prepare + - build-linux + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + job: + - { name: "amd64", debian_platform: "amd64", crossbuild_package: "" } + - { name: "arm64v8", debian_platform: "arm64", crossbuild_package: "crossbuild-essential-arm64" } + - { name: "armv7", debian_platform: "armhf", crossbuild_package: "crossbuild-essential-armhf" } + - { name: "i386", debian_platform: "i386", crossbuild_package: "crossbuild-essential-i386" } + + steps: + - name: Checkout workflow + uses: actions/checkout@v6 + + - name: Checkout release source + uses: actions/checkout@v6 + with: + ref: ${{ needs.prepare.outputs.release-tag }} + path: source + submodules: recursive + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Create packaging env + run: | + sudo apt update + DEBIAN_FRONTEND=noninteractive sudo apt install -y devscripts build-essential debhelper pkg-config ${{ matrix.job.crossbuild_package }} + mkdir -p source/debian-build/${{ matrix.job.name }}/bin + + - name: Download binaries + uses: actions/download-artifact@v6 + with: + name: binaries-linux-${{ matrix.job.name }} + path: source/debian-build/${{ matrix.job.name }}/bin + + - name: Build package + run: | + chmod -v a+x source/debian-build/${{ matrix.job.name }}/bin/* + cp -vr source/debian source/systemd source/debian-build/${{ matrix.job.name }}/ + sed 's/{{ ARCH }}/${{ matrix.job.debian_platform }}/' source/debian/control.tpl > source/debian-build/${{ matrix.job.name }}/debian/control + cd source/debian-build/${{ matrix.job.name }}/ + debuild -i -us -uc -b -a${{ matrix.job.debian_platform }} + + - name: Create draft release + uses: softprops/action-gh-release@v3 + with: + draft: true + tag_name: ${{ needs.prepare.outputs.release-tag }} + files: | + source/debian-build/rustdesk-server-hbbr_*_${{ matrix.job.debian_platform }}.deb + source/debian-build/rustdesk-server-hbbs_*_${{ matrix.job.debian_platform }}.deb + source/debian-build/rustdesk-server-utils_*_${{ matrix.job.debian_platform }}.deb diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 000000000..57dcd74b6 --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,310 @@ +name: docker + +# Required repository secrets: +# DOCKER_HUB_USERNAME is the Docker Hub username. +# DOCKER_HUB_PASSWORD is a Docker Hub token with read/write access. + +permissions: + contents: read + packages: write + +on: + workflow_dispatch: + inputs: + version: + description: Release tag to build, for example v1.2.3. + required: true + type: string + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+-[0-9]+" + +env: + CARGO_TERM_COLOR: always + LATEST_TAG: latest + RUST_TOOLCHAIN: "1.90" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + PROJECT_URL: https://github.com/${{ github.repository }} + DOCUMENTATION_URL: https://github.com/${{ github.repository }}#installation + DOCKER_IMAGE: hangyvv/rustdesk-server-s6 + DOCKER_IMAGE_CLASSIC: hangyvv/rustdesk-server + GHCR_IMAGE: ghcr.io/hangyvv/rustdesk-server-s6 + GHCR_IMAGE_CLASSIC: ghcr.io/hangyvv/rustdesk-server + +jobs: + prepare: + name: Prepare release metadata + runs-on: ubuntu-22.04 + outputs: + release-tag: ${{ steps.version.outputs.release-tag }} + major-tag: ${{ steps.version.outputs.major-tag }} + + steps: + - name: Resolve release version + id: version + shell: bash + run: | + if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then + tag="${{ inputs.version }}" + else + tag="${GITHUB_REF_NAME}" + fi + + if [[ ! "${tag}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then + echo "Invalid release tag: ${tag}" >&2 + exit 1 + fi + + normalized="${tag#v}" + major="${normalized%%.*}" + + echo "release-tag=${tag}" >> "${GITHUB_OUTPUT}" + echo "major-tag=${major}" >> "${GITHUB_OUTPUT}" + + build-linux: + name: Build Linux - ${{ matrix.job.name }} + needs: prepare + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + job: + - { name: "amd64", target: "x86_64-unknown-linux-musl" } + - { name: "arm64v8", target: "aarch64-unknown-linux-musl" } + - { name: "armv7", target: "armv7-unknown-linux-musleabihf" } + - { name: "i386", target: "i686-unknown-linux-musl" } + + steps: + - name: Checkout workflow + uses: actions/checkout@v6 + + - name: Checkout release source + uses: actions/checkout@v6 + with: + ref: ${{ needs.prepare.outputs.release-tag }} + path: source + submodules: recursive + + - name: Build Rust binaries + uses: ./.github/actions/build-rust-target + with: + target: ${{ matrix.job.target }} + toolchain: ${{ env.RUST_TOOLCHAIN }} + use-cross: "true" + working-directory: source + + - name: Make binaries executable + run: chmod -v a+x source/target/${{ matrix.job.target }}/release/* + + - name: Upload binaries + uses: actions/upload-artifact@v6 + with: + name: binaries-linux-${{ matrix.job.name }} + path: | + source/target/${{ matrix.job.target }}/release/hbbr + source/target/${{ matrix.job.target }}/release/hbbs + source/target/${{ matrix.job.target }}/release/rustdesk-utils + if-no-files-found: error + + docker: + name: Docker S6 - ${{ matrix.job.name }} + needs: + - prepare + - build-linux + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + job: + - { name: "amd64", docker_platform: "linux/amd64", s6_platform: "x86_64" } + - { name: "arm64v8", docker_platform: "linux/arm64", s6_platform: "aarch64" } + - { name: "armv7", docker_platform: "linux/arm/v7", s6_platform: "armhf" } + - { name: "i386", docker_platform: "linux/386", s6_platform: "i686" } + + steps: + - name: Checkout workflow + uses: actions/checkout@v6 + + - name: Checkout release source + uses: actions/checkout@v6 + with: + ref: ${{ needs.prepare.outputs.release-tag }} + path: source + submodules: recursive + + - name: Build and push Docker image + uses: ./.github/actions/docker-build-and-push + with: + artifact-name: binaries-linux-${{ matrix.job.name }} + artifact-path: source/docker/rootfs/usr/bin + context: ./source/docker + docker-platform: ${{ matrix.job.docker_platform }} + docker-hub-image: ${{ env.DOCKER_IMAGE }} + ghcr-image: ${{ env.GHCR_IMAGE }} + image-title: RustDesk Server S6 + image-description: Self-hosted RustDesk container image with s6-overlay and supervised services. + project-url: ${{ env.PROJECT_URL }} + documentation-url: ${{ env.DOCUMENTATION_URL }} + arch-name: ${{ matrix.job.name }} + s6-arch: ${{ matrix.job.s6_platform }} + version-tag: ${{ needs.prepare.outputs.release-tag }} + major-tag: ${{ needs.prepare.outputs.major-tag }} + latest-tag: ${{ env.LATEST_TAG }} + docker-hub-username: ${{ secrets.DOCKER_HUB_USERNAME }} + docker-hub-password: ${{ secrets.DOCKER_HUB_PASSWORD }} + ghcr-username: ${{ github.repository_owner }} + ghcr-token: ${{ secrets.GITHUB_TOKEN }} + + docker-classic: + name: Docker classic - ${{ matrix.job.name }} + needs: + - prepare + - build-linux + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + job: + - { name: "amd64", docker_platform: "linux/amd64" } + - { name: "arm64v8", docker_platform: "linux/arm64" } + - { name: "armv7", docker_platform: "linux/arm/v7" } + + steps: + - name: Checkout workflow + uses: actions/checkout@v6 + + - name: Checkout release source + uses: actions/checkout@v6 + with: + ref: ${{ needs.prepare.outputs.release-tag }} + path: source + submodules: recursive + + - name: Build and push Docker image + uses: ./.github/actions/docker-build-and-push + with: + artifact-name: binaries-linux-${{ matrix.job.name }} + artifact-path: source/docker-classic + context: ./source/docker-classic + docker-platform: ${{ matrix.job.docker_platform }} + docker-hub-image: ${{ env.DOCKER_IMAGE_CLASSIC }} + ghcr-image: ${{ env.GHCR_IMAGE_CLASSIC }} + image-title: RustDesk Server + image-description: Self-hosted RustDesk container image for hbbs and hbbr. + project-url: ${{ env.PROJECT_URL }} + documentation-url: ${{ env.DOCUMENTATION_URL }} + arch-name: ${{ matrix.job.name }} + version-tag: ${{ needs.prepare.outputs.release-tag }} + major-tag: ${{ needs.prepare.outputs.major-tag }} + latest-tag: ${{ env.LATEST_TAG }} + docker-hub-username: ${{ secrets.DOCKER_HUB_USERNAME }} + docker-hub-password: ${{ secrets.DOCKER_HUB_PASSWORD }} + ghcr-username: ${{ github.repository_owner }} + ghcr-token: ${{ secrets.GITHUB_TOKEN }} + + docker-manifest: + name: Docker manifest - s6 + needs: + - prepare + - docker + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Checkout release source + uses: actions/checkout@v6 + with: + path: source + + - name: Create Docker manifests + uses: ./.github/actions/docker-manifest + with: + docker-hub-image: ${{ env.DOCKER_IMAGE }} + ghcr-image: ${{ env.GHCR_IMAGE }} + source-path: source + image-title: RustDesk Server S6 + image-description: Self-hosted RustDesk container image with s6-overlay and supervised services. + project-url: ${{ env.PROJECT_URL }} + documentation-url: ${{ env.DOCUMENTATION_URL }} + arch-suffixes: amd64 arm64v8 armv7 i386 + version-tag: ${{ needs.prepare.outputs.release-tag }} + major-tag: ${{ needs.prepare.outputs.major-tag }} + latest-tag: ${{ env.LATEST_TAG }} + docker-hub-username: ${{ secrets.DOCKER_HUB_USERNAME }} + docker-hub-password: ${{ secrets.DOCKER_HUB_PASSWORD }} + ghcr-username: ${{ github.repository_owner }} + ghcr-token: ${{ secrets.GITHUB_TOKEN }} + + docker-manifest-classic: + name: Docker manifest - classic + needs: + - prepare + - docker-classic + runs-on: ubuntu-22.04 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Checkout release source + uses: actions/checkout@v6 + with: + path: source + + - name: Create Docker manifests + uses: ./.github/actions/docker-manifest + with: + docker-hub-image: ${{ env.DOCKER_IMAGE_CLASSIC }} + ghcr-image: ${{ env.GHCR_IMAGE_CLASSIC }} + source-path: source + image-title: RustDesk Server + image-description: Self-hosted RustDesk container image for hbbs and hbbr. + project-url: ${{ env.PROJECT_URL }} + documentation-url: ${{ env.DOCUMENTATION_URL }} + arch-suffixes: amd64 arm64v8 armv7 + version-tag: ${{ needs.prepare.outputs.release-tag }} + major-tag: ${{ needs.prepare.outputs.major-tag }} + latest-tag: ${{ env.LATEST_TAG }} + docker-hub-username: ${{ secrets.DOCKER_HUB_USERNAME }} + docker-hub-password: ${{ secrets.DOCKER_HUB_PASSWORD }} + ghcr-username: ${{ github.repository_owner }} + ghcr-token: ${{ secrets.GITHUB_TOKEN }} + + dockerhub-description: + name: Docker Hub description + needs: + - prepare + - docker-manifest + - docker-manifest-classic + runs-on: ubuntu-22.04 + + steps: + - name: Checkout release source + uses: actions/checkout@v6 + with: + ref: ${{ needs.prepare.outputs.release-tag }} + path: source + submodules: recursive + + - name: Update Docker Hub description - S6 + uses: peter-evans/dockerhub-description@v5 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} + repository: ${{ env.DOCKER_IMAGE }} + short-description: Self-hosted RustDesk container image with s6-overlay. + readme-filepath: ./source/docker/README.md + + - name: Update Docker Hub description - Classic + uses: peter-evans/dockerhub-description@v5 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} + repository: ${{ env.DOCKER_IMAGE_CLASSIC }} + short-description: Self-hosted RustDesk container image for hbbs and hbbr. + readme-filepath: ./source/docker-classic/README.md diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 000000000..e7dade6a9 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,262 @@ +name: release + +permissions: + contents: write + +on: + workflow_dispatch: + inputs: + version: + description: Release tag to build, for example v1.2.3. + required: true + type: string + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+-[0-9]+" + +env: + CARGO_TERM_COLOR: always + RUST_TOOLCHAIN: "1.90" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + prepare: + name: Prepare release metadata + runs-on: ubuntu-22.04 + outputs: + release-tag: ${{ steps.version.outputs.release-tag }} + + steps: + - name: Resolve release version + id: version + shell: bash + run: | + if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then + tag="${{ inputs.version }}" + else + tag="${GITHUB_REF_NAME}" + fi + + if [[ ! "${tag}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then + echo "Invalid release tag: ${tag}" >&2 + exit 1 + fi + + echo "release-tag=${tag}" >> "${GITHUB_OUTPUT}" + + build-linux: + name: Build Linux - ${{ matrix.job.name }} + needs: prepare + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + job: + - { name: "amd64", target: "x86_64-unknown-linux-musl" } + - { name: "arm64v8", target: "aarch64-unknown-linux-musl" } + - { name: "armv7", target: "armv7-unknown-linux-musleabihf" } + - { name: "i386", target: "i686-unknown-linux-musl" } + + steps: + - name: Checkout workflow + uses: actions/checkout@v6 + + - name: Checkout release source + uses: actions/checkout@v6 + with: + ref: ${{ needs.prepare.outputs.release-tag }} + path: source + submodules: recursive + + - name: Build Rust binaries + uses: ./.github/actions/build-rust-target + with: + target: ${{ matrix.job.target }} + toolchain: ${{ env.RUST_TOOLCHAIN }} + use-cross: "true" + working-directory: source + + - name: Make binaries executable + run: chmod -v a+x source/target/${{ matrix.job.target }}/release/* + + - name: Upload binaries + uses: actions/upload-artifact@v6 + with: + name: binaries-linux-${{ matrix.job.name }} + path: | + source/target/${{ matrix.job.target }}/release/hbbr + source/target/${{ matrix.job.target }}/release/hbbs + source/target/${{ matrix.job.target }}/release/rustdesk-utils + if-no-files-found: error + + build-windows: + name: Build Windows - x86_64 + needs: prepare + runs-on: windows-2022 + env: + WINDOWS_TARGET: x86_64-pc-windows-msvc + + steps: + - name: Checkout workflow + uses: actions/checkout@v6 + + - name: Checkout release source + uses: actions/checkout@v6 + with: + ref: ${{ needs.prepare.outputs.release-tag }} + path: source + submodules: recursive + + - name: Build Rust binaries + uses: ./.github/actions/build-rust-target + with: + target: ${{ env.WINDOWS_TARGET }} + toolchain: ${{ env.RUST_TOOLCHAIN }} + use-cross: "false" + working-directory: source + + - name: Install NSIS + shell: pwsh + run: | + iwr -useb get.scoop.sh -outfile install.ps1 + .\install.ps1 -RunAsAdmin + scoop update + scoop bucket add extras + scoop install nsis + + - name: Install Node.js + uses: actions/setup-node@v6 + with: + node-version: 16 + + - name: Build UI browser file + working-directory: ./source/ui/html + shell: pwsh + run: | + npm i + npm run build + + - name: Build UI setup file + working-directory: ./source/ui + shell: pwsh + run: | + rustup default nightly + cargo build --release + xcopy /y ..\target\x86_64-pc-windows-msvc\release\*.exe setup\bin\ + xcopy /y target\release\*.exe setup\ + mkdir setup\logs + makensis /V1 setup.nsi + mkdir SignOutput + mv RustDeskServer.Setup.exe SignOutput\ + mv ..\target\x86_64-pc-windows-msvc\release\*.exe SignOutput\ + + - name: Upload binaries + uses: actions/upload-artifact@v6 + with: + name: binaries-windows-x86_64 + path: | + source\ui\SignOutput\hbbr.exe + source\ui\SignOutput\hbbs.exe + source\ui\SignOutput\rustdesk-utils.exe + source\ui\SignOutput\RustDeskServer.Setup.exe + if-no-files-found: error + + release-notes: + name: Generate release notes + needs: prepare + runs-on: ubuntu-22.04 + + steps: + - name: Checkout release source + uses: actions/checkout@v6 + with: + ref: ${{ needs.prepare.outputs.release-tag }} + fetch-depth: 0 + submodules: recursive + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + + - name: Generate release notes + run: | + cargo run --locked --quiet --bin release-notes -- \ + --tag "${{ needs.prepare.outputs.release-tag }}" \ + --repo-url "https://github.com/${{ github.repository }}" \ + --cwd "${GITHUB_WORKSPACE}" \ + --output release-notes.md \ + --release-date "$(date -u +%F)" + + - name: Upload release notes + uses: actions/upload-artifact@v6 + with: + name: release-notes + path: release-notes.md + if-no-files-found: error + + create-release: + name: Create GitHub draft release + needs: + - prepare + - release-notes + runs-on: ubuntu-22.04 + + steps: + - name: Download release notes + uses: actions/download-artifact@v6 + with: + name: release-notes + path: . + + - name: Create draft release + uses: softprops/action-gh-release@v3 + with: + draft: true + tag_name: ${{ needs.prepare.outputs.release-tag }} + body_path: release-notes.md + + release: + name: GitHub release - ${{ matrix.job.os }} ${{ matrix.job.name }} + needs: + - prepare + - build-linux + - build-windows + - create-release + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + job: + - { os: "linux", name: "amd64", suffix: "" } + - { os: "linux", name: "arm64v8", suffix: "" } + - { os: "linux", name: "armv7", suffix: "" } + - { os: "linux", name: "i386", suffix: "" } + - { os: "windows", name: "x86_64", suffix: "-unsigned" } + + steps: + - name: Download binaries + uses: actions/download-artifact@v6 + with: + name: binaries-${{ matrix.job.os }}-${{ matrix.job.name }} + path: ${{ matrix.job.name }} + + - name: Pack files + run: | + sudo apt update + DEBIAN_FRONTEND=noninteractive sudo apt install -y zip + chmod -v a+x ${{ matrix.job.name }}/* + zip ${{ matrix.job.name }}/rustdesk-server-${{ matrix.job.os }}-${{ matrix.job.name }}${{ matrix.job.suffix }}.zip ${{ matrix.job.name }}/* + + - name: Upload release asset + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release upload "${{ needs.prepare.outputs.release-tag }}" \ + "${{ matrix.job.name }}/rustdesk-server-${{ matrix.job.os }}-${{ matrix.job.name }}${{ matrix.job.suffix }}.zip" \ + --clobber \ + --repo "${{ github.repository }}" diff --git a/Cargo.lock b/Cargo.lock index 7c76b2b87..95712b313 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1703,6 +1703,7 @@ dependencies = [ "machine-uid 0.2.0", "minreq", "once_cell", + "openssl", "ping", "regex", "reqwest", @@ -2709,6 +2710,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "openssl-src" +version = "300.6.1+3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.104" @@ -2717,6 +2727,7 @@ checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] diff --git a/Cargo.toml b/Cargo.toml index 43963d125..76214b142 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,10 @@ path = "src/hbbr.rs" name = "rustdesk-utils" path = "src/utils.rs" +[[bin]] +name = "release-notes" +path = "src/bin/release-notes.rs" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -59,10 +63,17 @@ reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocki [target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies] reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false } +# hbb_common pulls in native-tls -> openssl-sys on all platforms; build OpenSSL +# from source so static musl cross-builds (cross images have no system OpenSSL). +openssl = { version = "0.10", features = ["vendored"] } [build-dependencies] hbb_common = { path = "libs/hbb_common" } +[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.build-dependencies] +# build.rs depends on hbb_common, which is compiled for the host as well. +openssl = { version = "0.10", features = ["vendored"] } + [workspace] members = ["libs/hbb_common"] exclude = ["ui"] diff --git a/README.md b/README.md index 887cee604..74c0fe354 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,13 @@ # RustDesk Server Program -[![build](https://github.com/rustdesk/rustdesk-server/actions/workflows/build.yaml/badge.svg)](https://github.com/rustdesk/rustdesk-server/actions/workflows/build.yaml) +[![version](https://img.shields.io/github/v/tag/HanGYvv/rustdesk-server?label=version)](https://github.com/HanGYvv/rustdesk-server/releases) +[![license](https://img.shields.io/github/license/HanGYvv/rustdesk-server)](LICENSE) +[![ci](https://github.com/HanGYvv/rustdesk-server/actions/workflows/ci.yaml/badge.svg)](https://github.com/HanGYvv/rustdesk-server/actions/workflows/ci.yaml) +[![docker](https://github.com/HanGYvv/rustdesk-server/actions/workflows/docker.yaml/badge.svg)](https://github.com/HanGYvv/rustdesk-server/actions/workflows/docker.yaml) +[![release](https://github.com/HanGYvv/rustdesk-server/actions/workflows/release.yaml/badge.svg)](https://github.com/HanGYvv/rustdesk-server/actions/workflows/release.yaml) +[![debian](https://github.com/HanGYvv/rustdesk-server/actions/workflows/debian.yaml/badge.svg)](https://github.com/HanGYvv/rustdesk-server/actions/workflows/debian.yaml) -[**Download**](https://github.com/rustdesk/rustdesk-server/releases) +[**Download**](https://github.com/HanGYvv/rustdesk-server/releases) [**Manual**](https://rustdesk.com/docs/en/self-host/) @@ -31,7 +36,7 @@ Three executables will be generated in target/release. - hbbr - RustDesk relay server - rustdesk-utils - RustDesk CLI utilities -You can find updated binaries on the [Releases](https://github.com/rustdesk/rustdesk-server/releases) page. +You can find updated binaries on the [Releases](https://github.com/HanGYvv/rustdesk-server/releases) page. ## Configuration diff --git a/docker-classic/README.md b/docker-classic/README.md new file mode 100644 index 000000000..25e39effc --- /dev/null +++ b/docker-classic/README.md @@ -0,0 +1,88 @@ +# RustDesk Server + +RustDesk Server is a minimal self-hosted RustDesk container image. It bundles +the `hbbs` and `hbbr` binaries in a `scratch`-based runtime without +s6-overlay, leaving service supervision to your own platform or orchestrator. + +- **Source repository:** +- **Docker Hub:** `hangyvv/rustdesk-server` +- **GHCR:** `ghcr.io/hangyvv/rustdesk-server` + +> Looking for a single-container image with built-in supervision and health +> checks? Use the s6-overlay variant: `hangyvv/rustdesk-server-s6`. + +## Highlights + +- Self-hosted RustDesk server (`hbbs` + `hbbr`) +- Minimal `scratch`-based image containing only the server binaries +- No bundled supervisor: run `hbbs` and `hbbr` as you prefer + +## Exposed ports + +| Port | Service | Purpose | +| ----------- | ------- | ----------------------------- | +| `21115/tcp` | hbbs | NAT type test | +| `21116/tcp` | hbbs | TCP hole punching / heartbeat | +| `21116/udp` | hbbs | ID registration / heartbeat | +| `21117/tcp` | hbbr | Relay service | +| `21118/tcp` | hbbs | Web client support | +| `21119/tcp` | hbbr | Web client support | + +## Quick start + +This image has no entrypoint, so you provide the command to run. `hbbs` and +`hbbr` are separate processes and are typically run as two containers: + +```bash +# Relay server +docker run -d --name hbbr \ + -p 21117:21117 -p 21119:21119 \ + -v rustdesk-data:/root \ + hangyvv/rustdesk-server:latest hbbr + +# ID / rendezvous server +docker run -d --name hbbs \ + -p 21115:21115 -p 21116:21116 -p 21116:21116/udp -p 21118:21118 \ + -v rustdesk-data:/root \ + hangyvv/rustdesk-server:latest hbbs -r :21117 +``` + +### docker-compose + +```yaml +services: + hbbs: + image: hangyvv/rustdesk-server:latest + container_name: hbbs + command: hbbs -r :21117 + ports: + - "21115:21115" + - "21116:21116" + - "21116:21116/udp" + - "21118:21118" + volumes: + - rustdesk-data:/root + depends_on: + - hbbr + restart: unless-stopped + + hbbr: + image: hangyvv/rustdesk-server:latest + container_name: hbbr + command: hbbr + ports: + - "21117:21117" + - "21119:21119" + volumes: + - rustdesk-data:/root + restart: unless-stopped + +volumes: + rustdesk-data: +``` + +## Data & keys + +Server state, including the generated `id_ed25519` / `id_ed25519.pub` key +pair, is written to the working directory (`/root`). Mount a shared volume +there for both services and back it up to preserve your server identity. diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..68380655a --- /dev/null +++ b/docker/README.md @@ -0,0 +1,79 @@ +# RustDesk Server S6 + +RustDesk Server S6 is a self-hosted RustDesk container image built on +[s6-overlay](https://github.com/just-containers/s6-overlay). It bundles +`hbbs` and `hbbr` into a single compact runtime with proper service +supervision. + +- **Source repository:** +- **Docker Hub:** `hangyvv/rustdesk-server-s6` +- **GHCR:** `ghcr.io/hangyvv/rustdesk-server-s6` + +## Highlights + +- Self-hosted RustDesk server (`hbbs` + `hbbr`) in one image +- Built with s6-overlay for reliable service supervision and health checks +- Automatic key-pair handling via Docker secrets or environment variables +- Persistent data through a single `/data` volume + +## Exposed ports + +| Port | Service | Purpose | +| ----------- | ------- | -------------------------------- | +| `21115/tcp` | hbbs | NAT type test | +| `21116/tcp` | hbbs | TCP hole punching / heartbeat | +| `21116/udp` | hbbs | ID registration / heartbeat | +| `21117/tcp` | hbbr | Relay service | +| `21118/tcp` | hbbs | Web client support | +| `21119/tcp` | hbbr | Web client support | + +## Environment variables + +| Variable | Default | Description | +| ---------------- | --------------------- | -------------------------------------------------------- | +| `RELAY` | `relay.example.com` | Public address advertised for the relay (`hbbr`) | +| `ENCRYPTED_ONLY` | `0` | Set to `1` to force encrypted connections (`-k _`) | +| `KEY_PUB` | _(unset)_ | Public key content, written to `/data/id_ed25519.pub` | +| `KEY_PRIV` | _(unset)_ | Private key content, written to `/data/id_ed25519` | + +Keys can also be supplied as Docker secrets named `key_pub` and `key_priv`. +If no key pair is provided, `hbbs` generates one on first start. + +## Quick start + +```bash +docker run -d \ + --name rustdesk-server \ + -e RELAY=relay.example.com \ + -p 21115-21119:21115-21119 \ + -p 21116:21116/udp \ + -v rustdesk-data:/data \ + hangyvv/rustdesk-server-s6:latest +``` + +### docker-compose + +```yaml +services: + rustdesk-server: + image: hangyvv/rustdesk-server-s6:latest + container_name: rustdesk-server + environment: + - RELAY=relay.example.com + - ENCRYPTED_ONLY=0 + ports: + - "21115-21119:21115-21119" + - "21116:21116/udp" + volumes: + - rustdesk-data:/data + restart: unless-stopped + +volumes: + rustdesk-data: +``` + +## Data & keys + +All persistent state, including the generated `id_ed25519` / +`id_ed25519.pub` key pair, lives under the `/data` volume. Back this volume +up to preserve your server identity across container recreation. diff --git a/src/bin/release-notes.rs b/src/bin/release-notes.rs new file mode 100644 index 000000000..6ca21b682 --- /dev/null +++ b/src/bin/release-notes.rs @@ -0,0 +1,466 @@ +use std::collections::HashMap; +use std::env; +use std::error::Error; +use std::ffi::OsString; +use std::fmt; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use chrono::Utc; + +const SECTION_ORDER: [&str; 6] = [ + "Added", + "Changed", + "Deprecated", + "Removed", + "Fixed", + "Security", +]; + +#[derive(Debug)] +struct CliError(String); + +impl fmt::Display for CliError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl Error for CliError {} + +#[derive(Debug)] +struct ReleaseNotesArgs { + tag: String, + repo_url: String, + cwd: PathBuf, + output: PathBuf, + release_date: String, +} + +fn main() -> Result<(), Box> { + let args = parse_args(env::args_os().skip(1))?; + let notes = build_release_notes(&args.cwd, &args.tag, &args.repo_url, &args.release_date)?; + + if args.output == Path::new("-") { + print!("{notes}"); + println!(); + } else { + fs::write(&args.output, format!("{notes}\n"))?; + } + + Ok(()) +} + +fn parse_args(mut args: I) -> Result> +where + I: Iterator, +{ + let mut tag = None; + let mut repo_url = None; + let mut cwd = env::current_dir()?; + let mut output = PathBuf::from("-"); + let mut release_date = today_utc(); + + while let Some(arg) = args.next() { + let key = arg + .into_string() + .map_err(|_| CliError("Arguments must be valid UTF-8".into()))?; + + match key.as_str() { + "--tag" => tag = Some(next_string(&mut args, "--tag")?), + "--repo-url" => repo_url = Some(next_string(&mut args, "--repo-url")?), + "--cwd" => cwd = PathBuf::from(next_string(&mut args, "--cwd")?), + "--output" => output = PathBuf::from(next_string(&mut args, "--output")?), + "--release-date" => release_date = next_string(&mut args, "--release-date")?, + "-h" | "--help" => { + print_help(); + std::process::exit(0); + } + other => return Err(Box::new(CliError(format!("Unknown argument: {other}")))), + } + } + + let tag = tag.ok_or_else(|| CliError("Missing required argument: --tag".into()))?; + let repo_url = + repo_url.ok_or_else(|| CliError("Missing required argument: --repo-url".into()))?; + + Ok(ReleaseNotesArgs { + tag, + repo_url, + cwd, + output, + release_date, + }) +} + +fn next_string(args: &mut I, key: &str) -> Result> +where + I: Iterator, +{ + let value = args + .next() + .ok_or_else(|| CliError(format!("Missing value for {key}")))?; + Ok(value + .into_string() + .map_err(|_| CliError(format!("Value for {key} must be valid UTF-8")))?) +} + +fn print_help() { + println!( + "usage: release-notes --tag TAG --repo-url URL [--cwd DIR] [--output FILE] [--release-date YYYY-MM-DD]" + ); +} + +fn today_utc() -> String { + Utc::now().date_naive().format("%F").to_string() +} + +fn build_release_notes( + cwd: &Path, + tag: &str, + repo_url: &str, + release_date: &str, +) -> Result> { + let previous_tag = resolve_previous_tag(cwd, tag)?; + let commits = resolve_commit_subjects(cwd, tag, previous_tag.as_deref())?; + Ok(render_release_notes( + tag, + previous_tag.as_deref(), + release_date, + repo_url, + &commits, + )) +} + +fn resolve_previous_tag(cwd: &Path, current_tag: &str) -> Result, Box> { + let output = git(cwd, ["tag", "--sort=-creatordate"])?; + for line in output.lines() { + let tag = line.trim(); + if !tag.is_empty() && tag != current_tag { + return Ok(Some(tag.to_string())); + } + } + Ok(None) +} + +fn resolve_commit_subjects( + cwd: &Path, + tag: &str, + previous_tag: Option<&str>, +) -> Result, Box> { + let Some(previous_tag) = previous_tag else { + let output = git(cwd, ["log", "--no-merges", "--format=%s", tag])?; + return Ok(output + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(str::to_string) + .collect()); + }; + + let range = format!("{previous_tag}...{tag}"); + let output = git( + cwd, + [ + "log", + "--left-right", + "--no-merges", + "--format=%m%x00%ae%x00%aI%x00%s", + &range, + ], + )?; + deduplicate_rebased_commits(&output) +} + +fn deduplicate_rebased_commits(output: &str) -> Result, Box> { + let mut previous = HashMap::::new(); + let mut current = Vec::new(); + + for line in output.lines().filter(|line| !line.is_empty()) { + let mut fields = line.splitn(4, '\0'); + let side = fields.next(); + let email = fields.next(); + let authored_at = fields.next(); + let subject = fields.next(); + let (Some(side), Some(email), Some(authored_at), Some(subject)) = + (side, email, authored_at, subject) + else { + return Err(Box::new(CliError( + "Unexpected git log output while generating release notes".into(), + ))); + }; + // ponytail: assumes rebase preserves author metadata and subject; + // use Change-Id trailers if that stops holding. + let identity = format!("{email}\0{authored_at}\0{subject}"); + match side { + "<" => *previous.entry(identity).or_default() += 1, + ">" => current.push((identity, subject.to_string())), + _ => { + return Err(Box::new(CliError( + "Unexpected git history side while generating release notes".into(), + ))) + } + } + } + + Ok(current + .into_iter() + .filter_map(|(identity, subject)| match previous.get_mut(&identity) { + Some(count) if *count > 0 => { + *count -= 1; + None + } + _ => Some(subject), + }) + .collect()) +} + +fn git(cwd: &Path, args: [&str; N]) -> Result> { + let output = Command::new("git").args(args).current_dir(cwd).output()?; + if !output.status.success() { + return Err(Box::new(CliError(format!( + "git command failed: {}", + String::from_utf8_lossy(&output.stderr).trim() + )))); + } + Ok(String::from_utf8(output.stdout)?.trim().to_string()) +} + +fn render_release_notes( + tag: &str, + previous_tag: Option<&str>, + release_date: &str, + repo_url: &str, + commits: &[String], +) -> String { + let mut sections: [Vec; 6] = Default::default(); + + for subject in commits { + let section = classify_subject(subject); + let cleaned = normalize_subject(subject); + let index = section_index(section); + if !sections[index].contains(&cleaned) { + sections[index].push(cleaned); + } + } + + let mut lines = vec![ + "# Changelog".to_string(), + String::new(), + format!("## [{}] - {}", tag.trim_start_matches('v'), release_date), + String::new(), + ]; + + for (index, section_name) in SECTION_ORDER.iter().enumerate() { + let items = §ions[index]; + if items.is_empty() { + continue; + } + + lines.push(format!("### {section_name}")); + lines.push(String::new()); + for item in items { + lines.push(format!("- {item}")); + } + lines.push(String::new()); + } + + if let Some(previous_tag) = previous_tag { + lines.extend([ + "### Full Changelog".to_string(), + String::new(), + "
".to_string(), + "Show full changelog".to_string(), + String::new(), + format!( + "[Compare changes]({}/compare/{}...{})", + repo_url.trim_end_matches('/'), + previous_tag, + tag + ), + String::new(), + "
".to_string(), + ]); + } else { + lines.pop(); + } + + lines.join("\n") +} + +fn section_index(section: &'static str) -> usize { + match section { + "Added" => 0, + "Changed" => 1, + "Deprecated" => 2, + "Removed" => 3, + "Fixed" => 4, + "Security" => 5, + _ => 1, + } +} + +fn normalize_subject(subject: &str) -> String { + let subject = strip_pr_suffix(subject); + let subject = strip_conventional_prefix(&subject); + strip_leading_action(&subject) +} + +fn strip_pr_suffix(subject: &str) -> String { + let mut trimmed = subject.trim().to_string(); + if let Some(start) = trimmed.rfind(" (#") { + if trimmed.ends_with(')') + && trimmed[start + 3..trimmed.len() - 1] + .chars() + .all(|c| c.is_ascii_digit()) + { + trimmed.truncate(start); + trimmed = trimmed.trim().to_string(); + } + } + trimmed +} + +fn strip_conventional_prefix(subject: &str) -> String { + let Some(colon) = subject.find(':') else { + return subject.trim().to_string(); + }; + + let prefix = &subject[..colon]; + if prefix + .chars() + .all(|c| c.is_ascii_alphabetic() || c == '(' || c == ')') + { + return subject[colon + 1..].trim().to_string(); + } + + subject.trim().to_string() +} + +fn strip_leading_action(subject: &str) -> String { + for prefix in [ + "add ", + "added ", + "fix ", + "fixed ", + "update ", + "updated ", + "remove ", + "removed ", + "delete ", + "deleted ", + ] { + if subject.len() >= prefix.len() && subject[..prefix.len()].eq_ignore_ascii_case(prefix) { + return subject[prefix.len()..].trim().to_string(); + } + } + subject.trim().to_string() +} + +fn classify_subject(subject: &str) -> &'static str { + let lower = subject.to_ascii_lowercase(); + if lower.contains("security") { + "Security" + } else if lower.starts_with("feat") || lower.starts_with("add") { + "Added" + } else if lower.starts_with("fix") || lower.starts_with("bugfix") { + "Fixed" + } else if lower.starts_with("remove") || lower.starts_with("delete") { + "Removed" + } else if lower.starts_with("deprecate") { + "Deprecated" + } else { + "Changed" + } +} + +#[cfg(test)] +mod tests { + use super::{deduplicate_rebased_commits, normalize_subject, render_release_notes}; + + #[test] + fn excludes_commits_replayed_by_rebase() { + let commits = deduplicate_rebased_commits(concat!( + "<\0dev@example.com\02026-06-01T10:00:00Z\0feat: login\n", + ">\0upstream@example.com\02026-07-01T10:00:00Z\0fix: upstream bug\n", + ">\0dev@example.com\02026-06-01T10:00:00Z\0feat: login\n", + )) + .unwrap(); + + assert_eq!(commits, ["fix: upstream bug"]); + } + + #[test] + fn renders_keep_a_changelog_sections() { + let notes = render_release_notes( + "v1.2.3", + Some("v1.2.2"), + "2026-06-01", + "https://github.com/example/repo", + &[ + "feat: add api login enforcement".to_string(), + "fix(security): update mio from 0.8.5 to 0.8.11 (#633)".to_string(), + "docs: update README.md".to_string(), + "refactor: simplify relay server flow".to_string(), + "remove stale debug logging".to_string(), + "fix: 127.0.0.1 is not loopback (#515)".to_string(), + ], + ); + + let expected = [ + "# Changelog", + "", + "## [1.2.3] - 2026-06-01", + "", + "### Added", + "", + "- api login enforcement", + "", + "### Changed", + "", + "- README.md", + "- simplify relay server flow", + "", + "### Removed", + "", + "- stale debug logging", + "", + "### Fixed", + "", + "- 127.0.0.1 is not loopback", + "", + "### Security", + "", + "- mio from 0.8.5 to 0.8.11", + "", + "### Full Changelog", + "", + "
", + "Show full changelog", + "", + "[Compare changes](https://github.com/example/repo/compare/v1.2.2...v1.2.3)", + "", + "
", + ] + .join("\n"); + + assert_eq!(notes, expected); + } + + #[test] + fn strips_conventional_prefixes_and_actions() { + assert_eq!( + normalize_subject("fix(security): update mio"), + "mio" + ); + assert_eq!( + normalize_subject("chore: Added kubernetes example file (#623)"), + "kubernetes example file" + ); + assert_eq!( + normalize_subject("remove stale debug logging"), + "stale debug logging" + ); + } +} diff --git a/src/jwt.rs b/src/jwt.rs new file mode 100644 index 000000000..c1b05af65 --- /dev/null +++ b/src/jwt.rs @@ -0,0 +1,27 @@ +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub user_id: u32, + pub exp: usize, +} + +pub fn secret() -> String { + std::env::var("RUSTDESK_API_JWT_KEY").unwrap_or_default() +} + +pub fn verify_token(token: &str) -> Result { + let secret = secret(); + if secret.is_empty() { + return Err("JWT secret is not configured".to_owned()); + } + + decode::( + token, + &DecodingKey::from_secret(secret.as_bytes()), + &Validation::new(Algorithm::HS256), + ) + .map(|token_data| token_data.claims) + .map_err(|_| "Invalid token".to_owned()) +} diff --git a/src/lib.rs b/src/lib.rs index 8da29a2d6..66873b09d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ mod rendezvous_server; pub use rendezvous_server::*; pub mod common; +pub mod jwt; mod database; mod peer; mod version; diff --git a/src/main.rs b/src/main.rs index d422c3c06..211355de4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,8 @@ fn main() -> ResultType<()> { -r, --relay-servers=[HOST] 'Sets the default relay servers, separated by comma' -M, --rmem=[NUMBER(default={RMEM})] 'Sets UDP recv buffer size, set system rmem_max first, e.g., sudo sysctl -w net.core.rmem_max=52428800. vi /etc/sysctl.conf, net.core.rmem_max=52428800, sudo sysctl –p' , --mask=[MASK] '[DEPRECATED] Determine if the connection comes from LAN, e.g. 192.168.0.0/16' - -k, --key=[KEY] 'Only allow the client with the same key'", + -k, --key=[KEY] 'Only allow the client with the same key' + , --must-login=[Y|N] 'Only allow logged-in clients to connect'", ); init_args(&args, "hbbs", "RustDesk ID/Rendezvous Server"); let port = get_arg_or("port", RENDEZVOUS_PORT.to_string()).parse::()?; diff --git a/src/peer.rs b/src/peer.rs index 0f218dfdc..4970195e6 100644 --- a/src/peer.rs +++ b/src/peer.rs @@ -102,7 +102,9 @@ impl PeerMap { log::info!("update_pk {} {:?} {:?} {:?}", id, addr, uuid, pk); let (info_str, guid) = { let mut w = peer.write().await; - w.socket_addr = addr; + if addr.port() != 0 || w.socket_addr.port() == 0 { + w.socket_addr = addr; + } w.uuid = uuid.clone(); w.pk = pk.clone(); w.last_reg_time = Instant::now(); diff --git a/src/rendezvous_server.rs b/src/rendezvous_server.rs index eaf7190f9..482ced86a 100644 --- a/src/rendezvous_server.rs +++ b/src/rendezvous_server.rs @@ -1,4 +1,5 @@ use crate::common::*; +use crate::jwt; use crate::peer::*; use hbb_common::{ allow_err, bail, @@ -13,7 +14,7 @@ use hbb_common::{ log, protobuf::{Message as _, MessageField}, rendezvous_proto::{ - register_pk_response::Result::{TOO_FREQUENT, UUID_MISMATCH}, + register_pk_response::Result::{INVALID_ID_FORMAT, TOO_FREQUENT, UUID_MISMATCH}, *, }, tcp::FramedStream, @@ -54,12 +55,28 @@ enum Sink { TcpStream(TcpStreamSink), Ws(WsSink), } + +impl Sink { + async fn send(&mut self, msg: &RendezvousMessage) { + if let Ok(bytes) = msg.write_to_bytes() { + match self { + Sink::TcpStream(s) => { + allow_err!(s.send(Bytes::from(bytes)).await); + } + Sink::Ws(ws) => { + allow_err!(ws.send(tungstenite::Message::Binary(bytes)).await); + } + } + } + } +} type Sender = mpsc::UnboundedSender; type Receiver = mpsc::UnboundedReceiver; static ROTATION_RELAY_SERVER: AtomicUsize = AtomicUsize::new(0); type RelayServers = Vec; const CHECK_RELAY_TIMEOUT: u64 = 3_000; static ALWAYS_USE_RELAY: AtomicBool = AtomicBool::new(false); +static MUST_LOGIN: AtomicBool = AtomicBool::new(false); // Store punch hole requests use once_cell::sync::Lazy; @@ -88,6 +105,7 @@ pub struct RendezvousServer { relay_servers0: Arc, rendezvous_servers: Arc>, inner: Arc, + ws_map: Arc>>, } enum LoopFailure { @@ -150,6 +168,7 @@ impl RendezvousServer { mask, local_ip, }), + ws_map: Arc::new(Mutex::new(HashMap::new())), }; log::info!("mask: {:?}", rs.inner.mask); log::info!("local-ip: {:?}", rs.inner.local_ip); @@ -177,6 +196,24 @@ impl RendezvousServer { "N" } ); + let must_login = get_arg("must-login"); + if must_login.to_uppercase() == "Y" + || (must_login.is_empty() + && std::env::var("MUST_LOGIN") + .unwrap_or_default() + .to_uppercase() + == "Y") + { + MUST_LOGIN.store(true, Ordering::SeqCst); + } + log::info!( + "MUST_LOGIN={}", + if MUST_LOGIN.load(Ordering::SeqCst) { + "Y" + } else { + "N" + } + ); if test_addr.to_lowercase() != "no" { let test_addr = if test_addr.is_empty() { listener.local_addr()? @@ -277,8 +314,7 @@ impl RendezvousServer { match res { Some(Ok((bytes, addr))) => { if let Err(err) = self.handle_udp(&bytes, addr.into(), socket, key).await { - log::error!("udp failure: {}", err); - return LoopFailure::UdpSocket; + log::warn!("udp send/process failure (ignored): {}", err); } } Some(Err(err)) => { @@ -350,13 +386,23 @@ impl RendezvousServer { socket: &mut FramedSocket, key: &str, ) -> ResultType<()> { + if addr.port() == 0 { + log::debug!("drop UDP packet from source port 0: {}", addr); + return Ok(()); + } if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(bytes) { match msg_in.union { Some(rendezvous_message::Union::RegisterPeer(rp)) => { // B registered if !rp.id.is_empty() { log::trace!("New peer registered: {:?} {:?}", &rp.id, &addr); - self.update_addr(rp.id, addr, socket).await?; + let request_pk = self.update_addr(rp.id, addr).await; + let mut msg_out = RendezvousMessage::new(); + msg_out.set_register_peer_response(RegisterPeerResponse { + request_pk, + ..Default::default() + }); + socket.send(&msg_out, addr).await?; if self.inner.serial > rp.serial { let mut msg_out = RendezvousMessage::new(); msg_out.set_configure_update(ConfigUpdate { @@ -511,6 +557,28 @@ impl RendezvousServer { ) -> bool { if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(bytes) { match msg_in.union { + Some(rendezvous_message::Union::RegisterPeer(rp)) => { + if !rp.id.is_empty() { + log::trace!("New peer registered: {:?} {:?}", &rp.id, &addr); + let request_pk = self.update_addr(rp.id, addr).await; + let mut msg_out = RendezvousMessage::new(); + msg_out.set_register_peer_response(RegisterPeerResponse { + request_pk, + ..Default::default() + }); + Self::send_to_sink(sink, msg_out).await; + if self.inner.serial > rp.serial { + let mut msg_out = RendezvousMessage::new(); + msg_out.set_configure_update(ConfigUpdate { + serial: self.inner.serial, + rendezvous_servers: (*self.rendezvous_servers).clone(), + ..Default::default() + }); + Self::send_to_sink(sink, msg_out).await; + } + } + return true; + } Some(rendezvous_message::Union::PunchHoleRequest(ph)) => { // there maybe several attempt, so sink can be none if let Some(sink) = sink.take() { @@ -529,7 +597,7 @@ impl RendezvousServer { rf.socket_addr = AddrMangle::encode(addr).into(); msg_out.set_request_relay(rf); let peer_addr = peer.read().await.socket_addr; - self.tx.send(Data::Msg(msg_out.into(), peer_addr)).ok(); + self.send_to_peer(msg_out, peer_addr).await; } return true; } @@ -574,14 +642,32 @@ impl RendezvousServer { msg_out.set_test_nat_response(res); Self::send_to_sink(sink, msg_out).await; } - Some(rendezvous_message::Union::RegisterPk(_)) => { - let res = register_pk_response::Result::NOT_SUPPORT; + Some(rendezvous_message::Union::RegisterPk(rk)) => { + let res = match self.handle_register_pk(rk, addr, ws).await { + Ok(res) | Err(res) => res, + }; let mut msg_out = RendezvousMessage::new(); msg_out.set_register_pk_response(RegisterPkResponse { result: res.into(), ..Default::default() }); Self::send_to_sink(sink, msg_out).await; + if res == register_pk_response::Result::OK && ws { + if let Some(sink) = sink.take() { + self.ws_map.lock().await.insert(try_into_v4(addr), sink); + } + } + return res == register_pk_response::Result::OK; + } + Some(rendezvous_message::Union::OnlineRequest(or)) => { + let states = self.peers_online_state(or.peers).await; + let mut msg_out = RendezvousMessage::new(); + msg_out.set_online_response(OnlineResponse { + states: states.into(), + ..Default::default() + }); + Self::send_to_sink(sink, msg_out).await; + return true; } _ => {} } @@ -590,12 +676,7 @@ impl RendezvousServer { } #[inline] - async fn update_addr( - &mut self, - id: String, - socket_addr: SocketAddr, - socket: &mut FramedSocket, - ) -> ResultType<()> { + async fn update_addr(&mut self, id: String, socket_addr: SocketAddr) -> bool { let (request_pk, ip_change) = if let Some(old) = self.pm.get_in_memory(&id).await { let mut old = old.write().await; let ip = socket_addr.ip(); @@ -606,7 +687,9 @@ impl RendezvousServer { } && !ip.is_loopback(); let request_pk = old.pk.is_empty() || ip_change; if !request_pk { - old.socket_addr = socket_addr; + if socket_addr.port() != 0 || old.socket_addr.port() == 0 { + old.socket_addr = socket_addr; + } old.last_reg_time = Instant::now(); } let ip_change = if ip_change && old.reg_pk.0 <= 2 { @@ -625,12 +708,93 @@ impl RendezvousServer { if let Some(old) = ip_change { log::info!("IP change of {} from {} to {}", id, old, socket_addr); } - let mut msg_out = RendezvousMessage::new(); - msg_out.set_register_peer_response(RegisterPeerResponse { - request_pk, - ..Default::default() - }); - socket.send(&msg_out, socket_addr).await + request_pk + } + + async fn handle_register_pk( + &mut self, + rk: RegisterPk, + addr: SocketAddr, + refresh_existing: bool, + ) -> Result { + if rk.uuid.is_empty() || rk.pk.is_empty() { + return Err(INVALID_ID_FORMAT); + } + let id = rk.id; + let ip = addr.ip().to_string(); + if id.len() < 6 { + return Err(UUID_MISMATCH); + } else if !self.check_ip_blocker(&ip, &id).await { + return Err(TOO_FREQUENT); + } + let peer = self.pm.get_or(&id).await; + let (changed, ip_changed) = { + let peer = peer.read().await; + if peer.uuid.is_empty() { + (true, false) + } else { + if peer.uuid == rk.uuid { + if peer.info.ip != ip && peer.pk != rk.pk { + log::warn!( + "Peer {} ip/pk mismatch: {}/{:?} vs {}/{:?}", + id, + ip, + rk.pk, + peer.info.ip, + peer.pk, + ); + drop(peer); + return Err(UUID_MISMATCH); + } + } else { + log::warn!( + "Peer {} uuid mismatch: {:?} vs {:?}", + id, + rk.uuid, + peer.uuid + ); + drop(peer); + return Err(UUID_MISMATCH); + } + let ip_changed = peer.info.ip != ip; + ( + peer.uuid != rk.uuid || peer.pk != rk.pk || ip_changed, + ip_changed, + ) + } + }; + let mut req_pk = peer.read().await.reg_pk; + if req_pk.1.elapsed().as_secs() > 6 { + req_pk.0 = 0; + } else if req_pk.0 > 2 { + return Err(TOO_FREQUENT); + } + req_pk.0 += 1; + req_pk.1 = Instant::now(); + peer.write().await.reg_pk = req_pk; + if ip_changed { + let mut lock = IP_CHANGES.lock().await; + if let Some((tm, ips)) = lock.get_mut(&id) { + if tm.elapsed().as_secs() > IP_CHANGE_DUR { + *tm = Instant::now(); + ips.clear(); + ips.insert(ip.clone(), 1); + } else if let Some(v) = ips.get_mut(&ip) { + *v += 1; + } else { + ips.insert(ip.clone(), 1); + } + } else { + lock.insert( + id.clone(), + (Instant::now(), HashMap::from([(ip.clone(), 1)])), + ); + } + } + if changed || refresh_existing { + self.pm.update_pk(id, peer, addr, rk.uuid, rk.pk, ip).await; + } + Ok(register_pk_response::Result::OK) } #[inline] @@ -717,6 +881,14 @@ impl RendezvousServer { }); return Ok((msg_out, None)); } + if let Some(other_failure) = Self::login_failure_message(&ph.token) { + let mut msg_out = RendezvousMessage::new(); + msg_out.set_punch_hole_response(PunchHoleResponse { + other_failure, + ..Default::default() + }); + return Ok((msg_out, None)); + } let id = ph.id; // punch hole request from A, relay to B, // check if in same intranet first, @@ -810,12 +982,39 @@ impl RendezvousServer { } } + fn login_failure_message(token: &str) -> Option { + if !MUST_LOGIN.load(Ordering::SeqCst) { + return None; + } + if jwt::secret().is_empty() { + return Some("JWT secret is not configured".to_owned()); + } + if token.is_empty() { + return Some("Connection failed, please login!".to_owned()); + } + jwt::verify_token(token) + .err() + .map(|_| "Token error, please log out and log back in!".to_owned()) + } + #[inline] async fn handle_online_request( &mut self, stream: &mut FramedStream, peers: Vec, ) -> ResultType<()> { + let states = self.peers_online_state(peers).await; + let mut msg_out = RendezvousMessage::new(); + msg_out.set_online_response(OnlineResponse { + states: states.into(), + ..Default::default() + }); + stream.send(&msg_out).await?; + + Ok(()) + } + + async fn peers_online_state(&mut self, peers: Vec) -> BytesMut { let mut states = BytesMut::zeroed((peers.len() + 7) / 8); for (i, peer_id) in peers.iter().enumerate() { if let Some(peer) = self.pm.get_in_memory(peer_id).await { @@ -828,15 +1027,7 @@ impl RendezvousServer { } } } - - let mut msg_out = RendezvousMessage::new(); - msg_out.set_online_response(OnlineResponse { - states: states.into(), - ..Default::default() - }); - stream.send(&msg_out).await?; - - Ok(()) + states } #[inline] @@ -850,16 +1041,7 @@ impl RendezvousServer { #[inline] async fn send_to_sink(sink: &mut Option, msg: RendezvousMessage) { if let Some(sink) = sink.as_mut() { - if let Ok(bytes) = msg.write_to_bytes() { - match sink { - Sink::TcpStream(s) => { - allow_err!(s.send(Bytes::from(bytes)).await); - } - Sink::Ws(ws) => { - allow_err!(ws.send(tungstenite::Message::Binary(bytes)).await); - } - } - } + sink.send(&msg).await; } } @@ -874,6 +1056,19 @@ impl RendezvousServer { Ok(()) } + async fn send_to_peer(&mut self, msg: RendezvousMessage, peer_addr: SocketAddr) { + let key = try_into_v4(peer_addr); + let mut sink = self.ws_map.lock().await.remove(&key); + if sink.is_none() { + sink = self.tcp_punch.lock().await.remove(&key); + } + if let Some(sink) = sink.as_mut() { + sink.send(&msg).await; + } else { + self.tx.send(Data::Msg(msg.into(), peer_addr)).ok(); + } + } + #[inline] async fn handle_tcp_punch_hole_request( &mut self, @@ -884,7 +1079,7 @@ impl RendezvousServer { ) -> ResultType<()> { let (msg, to_addr) = self.handle_punch_hole_request(addr, ph, key, ws).await?; if let Some(addr) = to_addr { - self.tx.send(Data::Msg(msg.into(), addr))?; + self.send_to_peer(msg, addr).await; } else { self.send_to_tcp_sync(msg, addr).await?; } @@ -963,14 +1158,15 @@ impl RendezvousServer { match fds.next() { Some("h") => { res = format!( - "{}\n{}\n{}\n{}\n{}\n{}\n{}\n", + "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n", "relay-servers(rs) ", "reload-geo(rg)", "ip-blocker(ib) [|] [-]", "ip-changes(ic) [|] [-]", "punch-requests(pr) [] [-]", - "always-use-relay(aur)", - "test-geo(tg) " + "always-use-relay(aur) [Y|N]", + "test-geo(tg) ", + "must-login(ml) [Y|N]" ) } Some("relay-servers" | "rs") => { @@ -1074,7 +1270,7 @@ impl RendezvousServer { let arg = fds.next(); if let Some("-") = arg { lock.clear(); } else { - let mut start = arg.and_then(|x| x.parse::().ok()).unwrap_or(0); + let start = arg.and_then(|x| x.parse::().ok()).unwrap_or(0); let mut page_size = fds.next().and_then(|x| x.parse::().ok()).unwrap_or(10); if page_size == 0 { page_size = 10; } for (_, e) in lock.iter().enumerate().skip(start).take(page_size) { @@ -1115,6 +1311,13 @@ impl RendezvousServer { } } } + Some("must-login" | "ml") => { + if let Some(value) = fds.next() { + MUST_LOGIN.store(value.to_uppercase() == "Y", Ordering::SeqCst); + } else { + let _ = writeln!(res, "MUST_LOGIN: {:?}", MUST_LOGIN.load(Ordering::SeqCst)); + } + } _ => {} } res @@ -1225,6 +1428,7 @@ impl RendezvousServer { } if sink.is_none() { self.tcp_punch.lock().await.remove(&try_into_v4(addr)); + self.ws_map.lock().await.remove(&try_into_v4(addr)); } log::debug!("Tcp connection from {:?} closed", addr); Ok(())