diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7b67565 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +# Rust build artifacts +/target + +# Git files +.git +.gitignore + +# Docker Compose file +docker-compose.yaml + +# Documentation +README.md +LICENSE diff --git a/.github/workflows/docker-build-push.yaml b/.github/workflows/docker-build-push.yaml new file mode 100644 index 0000000..15eae52 --- /dev/null +++ b/.github/workflows/docker-build-push.yaml @@ -0,0 +1,72 @@ +name: Build and Push Docker Image + +# Define when this workflow will run +on: + push: + branches: + - master # Trigger on pushes to master branch + tags: + - '[0-9]+.[0-9]+.[0-9]+' # Trigger on semantic version tags + paths-ignore: + - 'Cargo.lock' + - 'LICENSE' + - 'README.md' + - 'docker-compose.yml' + workflow_dispatch: # Allow manual triggering of the workflow + +# Define environment variables used throughout the workflow +env: + REGISTRY: quay.io + IMAGE_NAME: invidious/inv-sig-helper + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + # Step 1: Check out the repository code + - name: Checkout code + uses: actions/checkout@v3 + + # Step 2: Set up QEMU for multi-architecture builds + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + # Step 3: Set up Docker Buildx for enhanced build capabilities + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + # Step 4: Authenticate with Quay.io registry + - name: Log in to Quay.io + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} + + # Step 5: Extract metadata for Docker image tagging and labeling + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # Define tagging strategy + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} + type=sha,prefix={{branch}}- + # Define labels + labels: | + quay.expires-after=12w + + # Step 6: Build and push the Docker image + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 # Build for multiple architectures + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Cargo.toml b/Cargo.toml index f51f9e5..fbc64a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,11 @@ lazy-regex = "3.1.0" tub = "0.3.7" tokio-util = { version = "0.7.10", features=["futures-io", "futures-util", "codec"]} futures = "0.3.30" + +# Compilation optimizations for release builds +# Increases compile time but typically produces a faster and smaller binary. Suitable for final releases but not for debug builds. +[profile.release] +lto = true +opt-level = 3 +codegen-units = 1 +panic = 'abort' diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e5f796e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# Use the official Alpine-based Rust image as a parent image +FROM rust:1.80-alpine AS builder + +# Set the working directory in the container +WORKDIR /usr/src/app + +# Install build dependencies +RUN apk add --no-cache \ + musl-dev \ + openssl-dev \ + openssl-libs-static \ + pkgconfig \ + patch + +# Set environment variables for static linking +ENV OPENSSL_STATIC=yes +ENV OPENSSL_DIR=/usr + +# Copy the current directory contents into the container +COPY . . + +# Determine the target architecture and build the application +RUN RUST_TARGET=$(rustc -vV | sed -n 's/host: //p') && \ + rustup target add $RUST_TARGET && \ + RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target $RUST_TARGET + +# Stage for creating the non-privileged user +FROM alpine:3.20 AS user-stage + +RUN adduser -u 10001 -S appuser + +# Stage for a smaller final image +FROM scratch + +# Copy necessary files from the builder stage, using the correct architecture path +COPY --from=builder /usr/src/app/target/*/release/inv_sig_helper_rust /app/inv_sig_helper_rust +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# Copy passwd file for the non-privileged user from the user-stage +COPY --from=user-stage /etc/passwd /etc/passwd + +# Set the working directory +WORKDIR /app + +# Expose port 12999 +EXPOSE 12999 + +# Switch to non-privileged user +USER appuser + +# Set the entrypoint to the binary name +ENTRYPOINT ["/app/inv_sig_helper_rust"] + +# Set default arguments in CMD +CMD ["--tcp", "127.0.0.1:12999"] diff --git a/README.md b/README.md index a8bed3c..d2650b3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,85 @@ -# Protocol Format +# inv_sig_helper + +`inv_sig_helper` is a Rust service that decrypts YouTube signatures and manages player information. It offers a TCP/Unix socket interface for signature decryption and related operations. + +## Features + +- Decrypt YouTube `n` and `s` signatures +- Fetch and update YouTube player data +- Provide signature timestamps and player status +- Efficient signature decryption with multi-threaded JavaScript execution + +## Building and Running + +### Prerequisites + +- Rust 1.77 or later +- Cargo +- Patch +- openssl-devel + +### Building + +1. Clone the repository and navigate to the project directory: + + ``` + git clone https://github.com/iv-org/inv_sig_helper.git + cd inv_sig_helper + ``` + +2. Build the project: + + ``` + cargo build --release + ``` + +### Running + +The service can run in Unix socket mode (default) or TCP mode: + +1. Unix socket mode: + + ``` + ./target/release/inv_sig_helper_rust + ``` + + This creates a Unix socket at `/tmp/inv_sig_helper.sock`. + +2. TCP mode: + + ``` + ./target/release/inv_sig_helper_rust --tcp [IP:PORT] + ``` + + If no IP:PORT is given, it defaults to `127.0.0.1:12999`. + +## Docker + +A Dockerfile is included for containerized deployment. + +1. Build the image: + + ``` + docker build -t inv_sig_helper . + ``` + +2. Run the container: + + ``` + docker run -p 127.0.0.1:12999:12999 inv_sig_helper + ``` + + Or use Docker Compose: + + ``` + docker compose up + ``` + +## Protocol Format All data-types bigger than 1 byte are stored in network endian (big-endian) unless stated otherwise. -## Request Base +### Request Base | Name | Size (bytes) | Description | |-----------|--------------|--------------------------------------| |opcode | 1 | The operation code to perform, A list of operations currently supported (and their data) can be found in the **Operations** chapter | @@ -10,7 +87,7 @@ All data-types bigger than 1 byte are stored in network endian (big-endian) unle The data afterwards depends on the supplied opcode, Please consult the **Operations** chapter for more information. -## Response Base +### Response Base | Name | Size (bytes) | Description | |------------|--------------|---------------------------------------| |request_id | 4 | The ID for the request that this response is meant for | @@ -18,80 +95,84 @@ The data afterwards depends on the supplied opcode, Please consult the **Operati The data afterwards depends on the supplied opcode, Please consult the **Operations** chapter for more information. -## Operations -### `FORCE_UPDATE` (0x00) +### Operations +#### `FORCE_UPDATE` (0x00) Forces the server to re-fetch the YouTube player, and extract the necessary components from it (`nsig` function code, `sig` function code, signature timestamp). -#### Request +##### Request *No additional data required* -#### Response +##### Response | Name | Size (bytes) | Description | |------|--------------|-------------| |status| 2 | The status code of the request: `0xF44F` if successful, `0xFFFF` if no updating is required (YouTube's player ID is equal to the server's current player ID), `0x0000` if an error occurred | -### `DECRYPT_N_SIGNATURE` (0x01) +#### `DECRYPT_N_SIGNATURE` (0x01) Decrypt a provided `n` signature using the server's current `nsig` function code, and return the result (or an error). -#### Request +##### Request | Name | Size (bytes) | Description | |------|--------------|-------------------------------------| |size | 2 | The size of the encrypted signature | |string| *`size`* | The encrypted signature | -#### Response +##### Response | Name | Size (bytes) | Description | |------|--------------|------------------------------------------------------------------| |size | 2 | The size of the decrypted signature, `0x0000` if an error occurred | |string| *`size`* | The decrypted signature | -### `DECRYPT_SIGNATURE` (0x02) +#### `DECRYPT_SIGNATURE` (0x02) Decrypt a provided `s` signature using the server's current `sig` function code, and return the result (or an error). -#### Request +##### Request | Name | Size (bytes) | Description | |------|--------------|-------------------------------------| |size | 2 | The size of the encrypted signature | |string| *`size`* | The encrypted signature | -#### Response +##### Response | Name | Size (bytes) | Description | |------|--------------|------------------------------------------------------------------| |size | 2 | The size of the decrypted signature, `0x0000` if an error occurred | |string| *`size`* | The decrypted signature | -### `GET_SIGNATURE_TIMESTAMP` (0x03) +#### `GET_SIGNATURE_TIMESTAMP` (0x03) Get the signature timestamp from the server's current player, and return it in the form of a 64-bit integer. If there's no player, it will return 0 in the `timestamp` (Please check with `PLAYER_STATUS` if the server has a player) -#### Request +##### Request No additional data required -#### Response +##### Response | Name | Size (bytes) | Description | |---------|--------------|----------------------------------------------------------| |timestamp| 8 | The signature timestamp from the server's current player | -### `PLAYER_STATUS` (0x04) +#### `PLAYER_STATUS` (0x04) Get the server's information about the current player. -#### Request +##### Request No additional data required -#### Response +##### Response | Name | Size (bytes) | Description | |----------|--------------|-------------| |has_player| 1 | If the server has a player, this variable will be `0xFF`. or else, it will be `0x00`| |player_id | 4 | The server's current player ID. If the server has no player, this will always be `0x00000000`| -### `PLAYER_UPDATE_TIMESTAMP` (0x05) +#### `PLAYER_UPDATE_TIMESTAMP` (0x05) Get the time of the last player update, The time is represented as seconds since the last update -#### Request +##### Request No additional data required -#### Response +##### Response | Name | Size (bytes) | Description | |----------|--------------|-------------| |timestamp | 8 | Seconds since the last player update | + +## License + +This project is open source under the AGPL-3.0 license. diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..428dfba --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,19 @@ +version: "3" +services: + inv_sig_helper: + build: + context: . + dockerfile: Dockerfile + # image: quay.io/invidious/inv-sig-helper:latest + command: ["--tcp", "127.0.0.1:12999"] + ports: + - 127.0.0.1:12999:12999 + environment: + - RUST_LOG=info + restart: unless-stopped + cap_drop: + - ALL + read_only: true + user: 10001:10001 + security_opt: + - no-new-privileges:true