mirror of
https://github.com/tlsnotary/notary-server.git
synced 2026-01-08 22:18:07 -05:00
Dockerize server. (#9)
* Dockerize server. * Modify tag push condition. * Move build image to cd workflow and change tag syntax.
This commit is contained in:
committed by
GitHub
parent
9a83d96cf3
commit
46acc04f57
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
||||
# exclude everything
|
||||
*
|
||||
|
||||
# include source files and configurations
|
||||
!/src
|
||||
!/config
|
||||
!/fixture
|
||||
!Cargo.lock
|
||||
!Cargo.toml
|
||||
|
||||
# include test files so that we can run test if we want during the build step in Dockerfile
|
||||
!/tests
|
||||
51
.github/workflows/cd.yml
vendored
Normal file
51
.github/workflows/cd.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: cd
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[v]?[0-9]+.[0-9]+.[0-9]+*"
|
||||
|
||||
env:
|
||||
CONTAINER_REGISTRY: ghcr.io
|
||||
CONTAINER_IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build_and_publish_image:
|
||||
name: Build and publish image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Wait for test workflow to succeed
|
||||
uses: lewagon/wait-on-check-action@v1.3.1
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
check-name: 'Build and test'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# How frequent (in seconds) this job will call GitHub API to check the status of the job specified at 'check-name'
|
||||
wait-interval: 60
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.CONTAINER_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker image
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.CONTAINER_REGISTRY }}/${{ env.CONTAINER_IMAGE_NAME }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -1,10 +1,14 @@
|
||||
name: Rust
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "[v]?[0-9]+.[0-9]+.[0-9]+*"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -12,18 +16,20 @@ env:
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
name: Build and test
|
||||
if: ( ! github.event.pull_request.draft )
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Stable
|
||||
- name: Install stable rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2.5.0
|
||||
- name: Use caching
|
||||
uses: Swatinem/rust-cache@v2.5.0
|
||||
|
||||
- name: "Build"
|
||||
run: cargo build
|
||||
@@ -35,9 +41,10 @@ jobs:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Nightly with rustfmt
|
||||
- name: Install nightly rust toolchain with rustfmt
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: nightly
|
||||
@@ -50,9 +57,10 @@ jobs:
|
||||
name: Clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Stable
|
||||
- name: Install stable rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -25,3 +25,6 @@ Cargo.lock
|
||||
|
||||
# vscode project specific settings
|
||||
.vscode/
|
||||
|
||||
# logs
|
||||
*.log
|
||||
|
||||
15
Cargo.toml
15
Cargo.toml
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.67"
|
||||
async-tungstenite = { version = "0.22.2", features = ["tokio-runtime", "tokio-native-tls"] }
|
||||
async-tungstenite = { version = "0.22.2", features = ["tokio-native-tls"] }
|
||||
axum = { version = "0.6.18", features = ["ws"]}
|
||||
axum-core = "0.3.4"
|
||||
axum-macros = "0.3.8"
|
||||
@@ -27,8 +27,8 @@ serde_yaml = "0.9.21"
|
||||
sha1 = "0.10"
|
||||
structopt = "0.3.26"
|
||||
thiserror = "1"
|
||||
tlsn-notary = { git = "https://github.com/tlsnotary/tlsn" }
|
||||
tlsn-tls-core = { git = "https://github.com/tlsnotary/tlsn" }
|
||||
tlsn-notary = { git = "https://github.com/tlsnotary/tlsn", rev = "aaea854" }
|
||||
tlsn-tls-core = { git = "https://github.com/tlsnotary/tlsn", rev = "aaea854" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-rustls = { version = "0.24.1" }
|
||||
tokio-util = { version = "0.7", features = ["compat"] }
|
||||
@@ -40,7 +40,8 @@ uuid = { version = "1.4.1", features = ["v4", "fast-rng"] }
|
||||
ws_stream_tungstenite = { version = "0.10.0", features = ["tokio_io"] }
|
||||
|
||||
[dev-dependencies]
|
||||
hyper-tls = "0.5.0"
|
||||
tls-server-fixture = { git = "https://github.com/tlsnotary/tlsn" }
|
||||
tlsn-prover = { git = "https://github.com/tlsnotary/tlsn" }
|
||||
tokio-native-tls = "0.3.1"
|
||||
# specify vendored feature to use statically linked copy of OpenSSL
|
||||
hyper-tls = { version = "0.5.0", features = ["vendored"] }
|
||||
tls-server-fixture = { git = "https://github.com/tlsnotary/tlsn", rev = "aaea854" }
|
||||
tlsn-prover = { git = "https://github.com/tlsnotary/tlsn", rev = "aaea854" }
|
||||
tokio-native-tls = { version = "0.3.1", features = ["vendored"] }
|
||||
|
||||
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
||||
# Using rust:bookworm so that the builder image has OpenSSL 3.0 which is required by async-tungstenite, because
|
||||
#
|
||||
# (1) async-tungstenite dynamically links to the OS' OpenSSL by using openssl-sys crate (https://docs.rs/openssl/0.10.56/openssl/#automatic)
|
||||
#
|
||||
# (2) async-tungstenite does not utilise the "vendored" feature for its dependency crates, i.e.
|
||||
# tokio-native-tls, tungstenite and native-tls. The "vendored" feature would have statically linked
|
||||
# to a OpenSSL copy instead of dynamically link to the OS' OpenSSL (https://docs.rs/openssl/0.10.56/openssl/#vendored)
|
||||
# — reported an issue here (https://github.com/sdroege/async-tungstenite/issues/119)
|
||||
#
|
||||
# (3) We want to use ubuntu:latest (22.04) as the runner image, which (only) has OpenSSL 3.0, because
|
||||
# OpenSSL 1.1.1 is reaching EOL in Sept 2023 (https://www.openssl.org/blog/blog/2023/03/28/1.1.1-EOL/)
|
||||
#
|
||||
# (4) Therefore, we need the builder image to have the same OpenSSL version, else the built binary will
|
||||
# try to dynamically link to a different (non-existing) version in the runner image
|
||||
#
|
||||
# (5) rust:latest is still using bullseye somehow which only has OpenSSL 1.1.1
|
||||
FROM rust:bookworm AS builder
|
||||
WORKDIR /usr/src/notary-server
|
||||
COPY . .
|
||||
RUN cargo install --path .
|
||||
|
||||
FROM ubuntu:latest
|
||||
WORKDIR /root/.notary-server
|
||||
# Install pkg-config and libssl-dev for async-tungstenite to use (as explained above)
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-recommends \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# Copy default fixture folder for default usage
|
||||
COPY --from=builder /usr/src/notary-server/fixture ./fixture
|
||||
# Copy default config folder for default usage
|
||||
COPY --from=builder /usr/src/notary-server/config ./config
|
||||
COPY --from=builder /usr/local/cargo/bin/notary-server /usr/local/bin/notary-server
|
||||
# Label to link this image with the repository in Github Container Registry (https://docs.github.com/en/packages/learn-github-packages/connecting-a-repository-to-a-package#connecting-a-repository-to-a-container-image-using-the-command-line)
|
||||
LABEL org.opencontainers.image.source=https://github.com/tlsnotary/notary-server
|
||||
CMD [ "notary-server" ]
|
||||
51
README.md
51
README.md
@@ -11,16 +11,59 @@ This project is currently under active development and should not be used in pro
|
||||
|
||||
---
|
||||
## Running the server
|
||||
1. Configure the server setting in this [file](./config.yaml) — refer [here](./src/config.rs) for more information on the definition of the setting parameters.
|
||||
2. Start the server by running following in a terminal at the top level of this project.
|
||||
### Using Cargo
|
||||
1. Configure the server setting in this config [file](./config/config.yaml) — refer [here](./src/config.rs) for more information on the definition of the setting parameters.
|
||||
2. Start the server by running the following in a terminal at the root of this repository.
|
||||
```bash
|
||||
cargo run
|
||||
cargo run --release
|
||||
```
|
||||
3. To use a config file from a different location, run the following command to override the default config file location.
|
||||
```bash
|
||||
cargo run -- --config-file <path-to-new-config-file>
|
||||
cargo run --release -- --config-file <path-of-new-config-file>
|
||||
```
|
||||
|
||||
### Using Docker
|
||||
There are two ways to obtain the notary server's Docker image:
|
||||
- [GitHub](#obtaining-the-image-via-github)
|
||||
- [Building from source](#building-from-source)
|
||||
|
||||
#### GitHub
|
||||
1. Obtain the latest image with:
|
||||
```bash
|
||||
docker pull ghcr.io/tlsnotary/notary-server:latest
|
||||
```
|
||||
2. Run the docker container with:
|
||||
```bash
|
||||
docker run --init -p 127.0.0.1:7047:7047 ghcr.io/tlsnotary/notary-server:latest
|
||||
```
|
||||
3. If you want to change the default configuration, create a `config` folder locally, that contains a `config.yaml`, whose content follows the format of the default config file [here](./config/config.yaml).
|
||||
4. Instead of step 2, run the docker container with the following (remember to change the port mapping if you have changed that in the config):
|
||||
```bash
|
||||
docker run --init -p 127.0.0.1:7047:7047 -v <your config folder path>:/root/.notary-server/config ghcr.io/tlsnotary/notary-server:latest
|
||||
```
|
||||
|
||||
#### Building from source
|
||||
1. Configure the server setting in this config [file](./config/config.yaml).
|
||||
2. Build the docker image by running the following in a terminal at the root of this repository.
|
||||
```bash
|
||||
docker build . -t notary-server:local
|
||||
```
|
||||
3. Run the docker container and specify the port specified in the config file, e.g. for the default port 7047
|
||||
```bash
|
||||
docker run --init -p 127.0.0.1:7047:7047 notary-server:local
|
||||
```
|
||||
|
||||
### Using different key/cert for TLS or/and notarization with Docker
|
||||
1. Instead of changing the key/cert file path(s) in the config file, create a folder containing your key/cert by following the folder structure [here](./fixture/).
|
||||
2. When launching the docker container, mount your folder onto the docker container at the relevant path prefixed by `/root/.notary-server`.
|
||||
- Example 1: Using different key/cert for both TLS and notarization:
|
||||
```bash
|
||||
docker run --init -p 127.0.0.1:7047:7047 -v <your folder path>:/root/.notary-server/fixture notary-server:local
|
||||
```
|
||||
- Example 2: Using different key for notarization (your folder should only contain `notary.key`):
|
||||
```bash
|
||||
docker run --init -p 127.0.0.1:7047:7047 -v <your folder path>:/root/.notary-server/fixture/notary notary-server:local
|
||||
```
|
||||
---
|
||||
## API
|
||||
All APIs are TLS-protected, hence please use `https://` or `wss://`.
|
||||
|
||||
17
config.yaml
17
config.yaml
@@ -1,17 +0,0 @@
|
||||
server:
|
||||
name: "notary-server"
|
||||
domain: "127.0.0.1"
|
||||
port: 7047
|
||||
|
||||
notarization:
|
||||
max-transcript-size: 16384
|
||||
|
||||
tls-signature:
|
||||
private-key-pem-path: "./src/fixture/tls/notary.key"
|
||||
certificate-pem-path: "./src/fixture/tls/notary.crt"
|
||||
|
||||
notary-signature:
|
||||
private-key-pem-path: "./src/fixture/notary/notary.key"
|
||||
|
||||
tracing:
|
||||
default-level: DEBUG
|
||||
17
config/config.yaml
Normal file
17
config/config.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
server:
|
||||
name: "notary-server"
|
||||
host: "0.0.0.0"
|
||||
port: 7047
|
||||
|
||||
notarization:
|
||||
max-transcript-size: 16384
|
||||
|
||||
tls-signature:
|
||||
private-key-pem-path: "./fixture/tls/notary.key"
|
||||
certificate-pem-path: "./fixture/tls/notary.crt"
|
||||
|
||||
notary-signature:
|
||||
private-key-pem-path: "./fixture/notary/notary.key"
|
||||
|
||||
tracing:
|
||||
default-level: DEBUG
|
||||
@@ -9,7 +9,7 @@ pub struct NotaryServerProperties {
|
||||
pub notarization: NotarizationProperties,
|
||||
/// File path of private key and certificate (in PEM format) used for establishing TLS with prover
|
||||
pub tls_signature: TLSSignatureProperties,
|
||||
/// File path of private key (in PEM format) used to sign the notarisation
|
||||
/// File path of private key (in PEM format) used to sign the notarization
|
||||
pub notary_signature: NotarySignatureProperties,
|
||||
/// Setting for logging/tracing
|
||||
pub tracing: TracingProperties,
|
||||
@@ -27,7 +27,7 @@ pub struct NotarizationProperties {
|
||||
pub struct ServerProperties {
|
||||
/// Used for testing purpose
|
||||
pub name: String,
|
||||
pub domain: String,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,6 @@ use structopt::StructOpt;
|
||||
#[structopt(name = "Notary Server")]
|
||||
pub struct CliFields {
|
||||
/// Configuration file location
|
||||
#[structopt(long, default_value = "./config.yaml")]
|
||||
#[structopt(long, default_value = "./config/config.yaml")]
|
||||
pub config_file: String,
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ pub async fn run_server(config: &NotaryServerProperties) -> Result<(), NotarySer
|
||||
let tls_config = Arc::new(server_config);
|
||||
|
||||
let notary_address = SocketAddr::new(
|
||||
IpAddr::V4(config.server.domain.parse().map_err(|err| {
|
||||
IpAddr::V4(config.server.host.parse().map_err(|err| {
|
||||
eyre!("Failed to parse notary host address from server config: {err}")
|
||||
})?),
|
||||
config.server.port,
|
||||
@@ -176,8 +176,8 @@ mod test {
|
||||
#[tokio::test]
|
||||
async fn test_load_notary_key_and_cert() {
|
||||
let config = TLSSignatureProperties {
|
||||
private_key_pem_path: "./src/fixture/tls/notary.key".to_string(),
|
||||
certificate_pem_path: "./src/fixture/tls/notary.crt".to_string(),
|
||||
private_key_pem_path: "./fixture/tls/notary.key".to_string(),
|
||||
certificate_pem_path: "./fixture/tls/notary.crt".to_string(),
|
||||
};
|
||||
let result: Result<(PrivateKey, Vec<Certificate>)> = load_tls_key_and_cert(&config).await;
|
||||
assert!(result.is_ok(), "Could not load tls private key and cert");
|
||||
@@ -186,7 +186,7 @@ mod test {
|
||||
#[tokio::test]
|
||||
async fn test_load_notary_signing_key() {
|
||||
let config = NotarySignatureProperties {
|
||||
private_key_pem_path: "./src/fixture/notary/notary.key".to_string(),
|
||||
private_key_pem_path: "./fixture/notary/notary.key".to_string(),
|
||||
};
|
||||
let result: Result<SigningKey> = load_notary_signing_key(&config).await;
|
||||
assert!(result.is_ok(), "Could not load notary private key");
|
||||
|
||||
@@ -17,7 +17,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_parse_config_file() {
|
||||
let location = "./config.yaml";
|
||||
let location = "./config/config.yaml";
|
||||
let config: Result<NotaryServerProperties> = parse_config_file(location);
|
||||
assert!(
|
||||
config.is_ok(),
|
||||
|
||||
@@ -27,25 +27,25 @@ use notary_server::{
|
||||
ServerProperties, TLSSignatureProperties, TracingProperties,
|
||||
};
|
||||
|
||||
const NOTARY_CA_CERT_PATH: &str = "./src/fixture/tls/rootCA.crt";
|
||||
const NOTARY_CA_CERT_BYTES: &[u8] = include_bytes!("../src/fixture/tls/rootCA.crt");
|
||||
const NOTARY_CA_CERT_PATH: &str = "./fixture/tls/rootCA.crt";
|
||||
const NOTARY_CA_CERT_BYTES: &[u8] = include_bytes!("../fixture/tls/rootCA.crt");
|
||||
|
||||
async fn setup_config_and_server(sleep_ms: u64, port: u16) -> NotaryServerProperties {
|
||||
let notary_config = NotaryServerProperties {
|
||||
server: ServerProperties {
|
||||
name: "tlsnotaryserver.io".to_string(),
|
||||
domain: "127.0.0.1".to_string(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port,
|
||||
},
|
||||
notarization: NotarizationProperties {
|
||||
max_transcript_size: 1 << 14,
|
||||
},
|
||||
tls_signature: TLSSignatureProperties {
|
||||
private_key_pem_path: "./src/fixture/tls/notary.key".to_string(),
|
||||
certificate_pem_path: "./src/fixture/tls/notary.crt".to_string(),
|
||||
private_key_pem_path: "./fixture/tls/notary.key".to_string(),
|
||||
certificate_pem_path: "./fixture/tls/notary.crt".to_string(),
|
||||
},
|
||||
notary_signature: NotarySignatureProperties {
|
||||
private_key_pem_path: "./src/fixture/notary/notary.key".to_string(),
|
||||
private_key_pem_path: "./fixture/notary/notary.key".to_string(),
|
||||
},
|
||||
tracing: TracingProperties {
|
||||
default_level: "DEBUG".to_string(),
|
||||
@@ -90,10 +90,10 @@ async fn test_tcp_prover() {
|
||||
.with_no_client_auth();
|
||||
let notary_connector = TlsConnector::from(Arc::new(client_notary_config));
|
||||
|
||||
let notary_domain = notary_config.server.domain.clone();
|
||||
let notary_host = notary_config.server.host.clone();
|
||||
let notary_port = notary_config.server.port;
|
||||
let notary_socket = tokio::net::TcpStream::connect(SocketAddr::new(
|
||||
IpAddr::V4(notary_domain.parse().unwrap()),
|
||||
IpAddr::V4(notary_host.parse().unwrap()),
|
||||
notary_port,
|
||||
))
|
||||
.await
|
||||
@@ -122,9 +122,9 @@ async fn test_tcp_prover() {
|
||||
})
|
||||
.unwrap();
|
||||
let request = Request::builder()
|
||||
.uri(format!("https://{notary_domain}:{notary_port}/session"))
|
||||
.uri(format!("https://{notary_host}:{notary_port}/session"))
|
||||
.method("POST")
|
||||
.header("Host", notary_domain.clone())
|
||||
.header("Host", notary_host.clone())
|
||||
// Need to specify application/json for axum to parse it as json
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(payload))
|
||||
@@ -150,9 +150,9 @@ async fn test_tcp_prover() {
|
||||
|
||||
// Send notarization request via HTTP, where the underlying TCP connection will be extracted later
|
||||
let request = Request::builder()
|
||||
.uri(format!("https://{notary_domain}:{notary_port}/notarize"))
|
||||
.uri(format!("https://{notary_host}:{notary_port}/notarize"))
|
||||
.method("GET")
|
||||
.header("Host", notary_domain)
|
||||
.header("Host", notary_host)
|
||||
.header("Connection", "Upgrade")
|
||||
// Need to specify this upgrade header for server to extract tcp connection later
|
||||
.header("Upgrade", "TCP")
|
||||
@@ -259,7 +259,7 @@ async fn test_tcp_prover() {
|
||||
async fn test_websocket_prover() {
|
||||
// Notary server configuration setup
|
||||
let notary_config = setup_config_and_server(100, 7049).await;
|
||||
let notary_domain = notary_config.server.domain.clone();
|
||||
let notary_host = notary_config.server.host.clone();
|
||||
let notary_port = notary_config.server.port;
|
||||
|
||||
// Connect to the notary server via TLS-WebSocket
|
||||
@@ -291,9 +291,9 @@ async fn test_websocket_prover() {
|
||||
.unwrap();
|
||||
|
||||
let request = Request::builder()
|
||||
.uri(format!("https://{notary_domain}:{notary_port}/session"))
|
||||
.uri(format!("https://{notary_host}:{notary_port}/session"))
|
||||
.method("POST")
|
||||
.header("Host", notary_domain.clone())
|
||||
.header("Host", notary_host.clone())
|
||||
// Need to specify application/json for axum to parse it as json
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(payload))
|
||||
@@ -324,8 +324,8 @@ async fn test_websocket_prover() {
|
||||
// client while using its high level request function — there does not seem to have a crate that can let you
|
||||
// make a request without establishing TCP connection where you can claim the TCP connection later after making the request
|
||||
let request = http::Request::builder()
|
||||
.uri(format!("wss://{notary_domain}:{notary_port}/notarize"))
|
||||
.header("Host", notary_domain.clone())
|
||||
.uri(format!("wss://{notary_host}:{notary_port}/notarize"))
|
||||
.header("Host", notary_host.clone())
|
||||
.header("Sec-WebSocket-Key", uuid::Uuid::new_v4().to_string())
|
||||
.header("Sec-WebSocket-Version", "13")
|
||||
.header("Connection", "Upgrade")
|
||||
|
||||
Reference in New Issue
Block a user