Compare commits

..

No commits in common. "main" and "0.0.0" have entirely different histories.
main ... 0.0.0

138 changed files with 29 additions and 13789 deletions

8
.actrc
View file

@ -1,8 +0,0 @@
# Configuration file for act (run github actions locally using docker)
# https://github.com/nektos/act
# Swap docker image for the one containing the rust toolchain
-P ubuntu-latest=ghcr.io/catthehacker/ubuntu:rust-latest
# Load custom event
-e .github/.act-event.json

View file

@ -1,6 +0,0 @@
{
"act": true,
"repository": {
"default_branch": "main"
}
}

View file

@ -1,23 +0,0 @@
---
name: Bug report
about: Create a report for a found bug
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Steps To Reproduce**
Please provide the steps to reproduce the behavior (if not possible, describe as many details as possible).
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots or Data**
If applicable, add screenshots/data to help explain your problem.
**Additional context**
Add any other context about the problem here.

View file

@ -1,14 +0,0 @@
---
name: Feature request
about: Suggest a feature
title: ''
labels: enhancement
assignees: ''
---
**What is this feature about?**
Shortly explain what your requested feature is about.
**Additional context/references**
Add any other context or references about the feature request here.

View file

@ -1,17 +0,0 @@
---
name: Help regarding code/protocol errors
about: Use this if you can't figure out how to use something.
title: ''
labels: ''
assignees: ''
---
**This issue shall be made only if you have already gone through the docs, have you done it?**
Please state if there is something confusing regarding the docs (eg. location or wording).
**What's you problem?**
State as concise as possible what you want to do and can't do.
**Suggestions to make this clearer**
Mention how could stuff be improved so that someone doesn't have the same problem as you (eg. Error should give more information).

View file

View file

@ -1,20 +0,0 @@
<svg width="181.6" height="20" viewBox="0 0 1816 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Node game coverage: 23.46%">
<title>Node game coverage: 23.46%</title>
<linearGradient id="a" x2="0" y2="100%">
<stop offset="0" stop-opacity=".1" stop-color="#EEE"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<mask id="m"><rect width="1816" height="200" rx="30" fill="#FFF"/></mask>
<g mask="url(#m)">
<rect width="1276" height="200" fill="#555"/>
<rect width="540" height="200" fill="#0f80c1" x="1276"/>
<rect width="1816" height="200" fill="url(#a)"/>
</g>
<g aria-hidden="true" fill="#fff" text-anchor="start" font-family="Verdana,DejaVu Sans,sans-serif" font-size="110">
<text x="60" y="148" textLength="1176" fill="#000" opacity="0.25">Node game coverage</text>
<text x="50" y="138" textLength="1176">Node game coverage</text>
<text x="1331" y="148" textLength="440" fill="#000" opacity="0.25">23.46%</text>
<text x="1321" y="138" textLength="440">23.46%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1,16 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/.github/workflows"
schedule:
interval: "daily"
- package-ecosystem: "cargo"
directory: "/crates/cli"
schedule:
interval: "daily"
- package-ecosystem: "cargo"
directory: "/crates/lib"
schedule:
interval: "daily"

18
.github/labeler.yml vendored
View file

@ -1,18 +0,0 @@
ci:
- .github/workflows/**
- .github/labeler.yml
- .actrc
- .pre-commit-config.yaml
protocol:
- crates/lib/src/protocols/**
game:
- crates/lib/src/games/**
cli:
- crates/cli/**
crate:
- Cargo.toml
- Cargo.lock

View file

@ -1,20 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/softprops/github-actions-schemas/master/workflow.json
name: Security audit
on:
push:
paths:
- "**/Cargo.toml"
- "**/Cargo.lock"
jobs:
security_audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Generate Cargo.lock # https://github.com/rustsec/audit-check/issues/27
run: cargo generate-lockfile
- name: Audit Check
uses: rustsec/audit-check@v2.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,199 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/softprops/github-actions-schemas/master/workflow.json
name: CI
on:
push:
branches: ["main"]
paths:
- "**.rs" # Any rust file
- "**/Cargo.toml" # Any Cargo.toml
- ".rustfmt.toml"
- ".github/workflows/ci.yml" # This action
pull_request:
branches: ["main"]
paths:
- "**.rs" # Any rust file
- "**/Cargo.toml" # Any Cargo.toml
- ".rustfmt.toml"
- ".github/workflows/ci.yml" # This action
env:
CARGO_TERM_COLOR: always
jobs:
# First check that we can build EVERYTHING and that tests pass
build_first:
name: "Build, check, and test with all features"
runs-on: ubuntu-latest
outputs:
cli: ${{ steps.filter.outputs.cli }}
lib: ${{ steps.filter.outputs.lib }}
steps:
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
with:
shared-key: "cargo-deps"
cache-targets: false
- name: Run Build
run: cargo check --verbose --workspace --bins --lib --examples --all-features
- name: Run Tests
run: cargo test --verbose --workspace --bins --lib --examples --tests --all-features
# Check what paths were modified so we only run the required tests
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
cli:
- 'crates/cli/**'
lib:
- 'crates/lib/**'
# If we were able to build then test different feature combinations compile with the library
build_lib:
runs-on: ubuntu-latest
needs: ["build_first"]
# Only run if library files were modified
if: ${{ needs.build_first.outputs.lib == 'true' }}
strategy:
fail-fast: false
matrix:
include:
- build_type: ""
build_name: "Default"
- build_type: "--no-default-features"
build_name: "No features"
- build_type: "--no-default-features --features games"
build_name: "Just games"
- build_type: "--no-default-features --features services"
build_name: "Just Services"
- build_type: "--no-default-features --features game_defs"
build_name: "Just Game definitions"
- build_type: "--no-default-features --features serde"
build_name: "Just serde"
name: "Build library ${{ matrix.build_name }}"
steps:
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
with:
shared-key: "cargo-deps"
cache-targets: false
save-if: false
- name: Run Build
run: cargo check -p gamedig --verbose --lib --examples --tests ${{ matrix.build_type }}
# If we were able to build then test different feature combinations compile with the CLI
build_cli:
runs-on: ubuntu-latest
needs: ["build_first"]
# Only run if CLI files were modified
if: ${{ needs.build_first.outputs.cli == 'true' }}
strategy:
fail-fast: false
matrix:
include:
- build_type: ""
build_name: "Default"
- build_type: "--no-default-features"
build_name: "No features"
name: "Build CLI ${{ matrix.build_name }}"
steps:
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
with:
shared-key: "cargo-deps"
cache-targets: false
save-if: false
- name: Run Build
run: cargo check -p gamedig_cli --verbose --bins --examples --tests ${{ matrix.build_type }}
# If we were able to build then test the MSRV compiles (for the libary as not enforced for CLI)
build_msrv:
name: "Build using MSRV (lib only)"
runs-on: ubuntu-latest
needs: ["build_first"]
# Only run if library files were modified
if: ${{ needs.build_first.outputs.lib == 'true' }}
# Unfortunate hard-coding of rustup directory so that rust-cache caches it
env:
RUSTUP_HOME: /home/runner/.rustup
steps:
# Act's rust runner has rustup in a different place
- if: ${{ env.ACT }}
run: mkdir -p /home/runner && ln -s /usr/share/rust/.rustup /home/runner/.rustup
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
with:
cache-targets: false
cache-directories: ${{ env.RUSTUP_HOME }}/toolchains
- name: Install MSRV
uses: actions-rs/toolchain@v1
with:
toolchain: 1.85.1
override: true
- name: Run MSRV
run: cargo check -p gamedig
# Check the code is formatted properly
formatting:
name: "Check code formatting"
runs-on: ubuntu-latest
# Unfortunate hard-coding of rustup directory so that rust-cache caches it
env:
RUSTUP_HOME: /home/runner/.rustup
steps:
# Act's rust runner has rustup in a different place
- if: ${{ env.ACT }}
run: mkdir -p /home/runner && ln -s /usr/share/rust/.rustup /home/runner/.rustup
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
with:
cache-targets: false
cache-directories: ${{ env.RUSTUP_HOME }}/toolchains
- name: Install Formatting nightly
uses: actions-rs/toolchain@v1
with:
toolchain: nightly-2025-04-19
components: rustfmt
override: true
- name: Run Formatting check
run: cargo fmt --check --verbose
# If we were able to build then lint the codebase with clippy
clippy:
name: "Run clippy tests"
runs-on: ubuntu-latest
needs: ["build_first"]
steps:
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
with:
shared-key: "cargo-deps"
cache-targets: false
save-if: false
# Run github actions version of clippy that adds annotations
- name: Run Clippy
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --workspace --bins --lib --examples --tests --all-features
if: ${{ !env.ACT }} # skip during local actions testing
# Run clippy binary
- name: Run clippy (local)
run: cargo clippy --verbose --workspace --bins --lib --examples --tests --all-features
if: ${{ env.ACT }} # only run during local actions testing
# If we were able to build then test that rustdoc (and rustdoc examples) compile
doc:
name: "Check rustdoc"
runs-on: ubuntu-latest
needs: ["build_first"]
steps:
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
with:
shared-key: "cargo-deps"
cache-targets: false
save-if: false
- name: "Run cargo doc"
run: cargo doc --workspace
env:
RUSTDOCFLAGS: "-D warnings"

View file

@ -1,44 +0,0 @@
name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '0 0 1 * *' # monthly on the 1st at 00:00 UTC
jobs:
analyze:
name: Analyze (rust)
runs-on: ubuntu-latest
permissions:
security-events: write
packages: read
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: rust
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Build
run: cargo build --release
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"

View file

@ -1,16 +0,0 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
triage:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/labeler@v4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
dot: true

View file

@ -1,63 +0,0 @@
# Based on: https://github.com/emibcn/badge-action/blob/master/.github/workflows/test.yml
name: "Generate node comparison badge"
on:
push:
paths:
- "crates/lib/src/games/definitions.rs"
- ".github/workflows/node-badge.yml"
- ".github/workflows/scripts/node-badge.mjs"
branches:
- "main" # Limit badge commits to only happen on the main branch
schedule: # This runs on the default branch only, it could still trigger on PRs but only if they develop on default branch and enable actions.
- cron: "34 3 * * 2" # Update once a week in case node-gamedig has changed
workflow_dispatch:
jobs:
badge:
runs-on: "ubuntu-latest"
name: Create node comparison badge
env:
BADGE_PATH: ".github/badges/node.svg"
steps:
- name: Extract branch name
shell: bash
run: echo "branch=${GITHUB_REF#refs/heads/}" >> "${GITHUB_OUTPUT}"
id: extract_branch
- uses: actions/checkout@v5
- uses: actions/checkout@v5
with:
repository: "gamedig/node-gamedig"
path: "node-gamedig"
sparse-checkout: |
lib/games.js
package.json
- name: Calculate comparison
id: comparison
run: node .github/workflows/scripts/node-badge.mjs
- name: Generate the badge SVG image
uses: emibcn/badge-action@v2.0.3
id: badge
with:
label: "Node game coverage"
status: "${{ steps.comparison.outputs.percent }}%"
color: "0f80c1"
path: ${{ env.BADGE_PATH }}
- name: "Commit badge"
continue-on-error: true
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add "${BADGE_PATH}"
git commit -m "Add/Update badge"
- name: Push badge commit
uses: ad-m/github-push-action@master
if: ${{ success() }}
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ steps.extract_branch.outputs.branch }}

View file

@ -1,63 +0,0 @@
"use strict";
//! Calculate the percentage of games from node that we support
// Expects node-gamedig checkout out in git root /node-gamedig
// Expects the generic example to output a list of game IDs when no arguments are provided
import process from "node:process";
import { closeSync, openSync, writeSync } from "node:fs";
import { spawnSync } from "node:child_process";
const setOutput = (key, value) => {
const file = openSync(process.env.GITHUB_OUTPUT, "a");
writeSync(file, `${key}=${value}\n`);
closeSync(file);
};
// Get node IDs
// NOTE: Here we directly import from games to avoid loading
// unecessary parts of the library that would require us
// to install dependencies.
import { games } from "../../../node-gamedig/lib/games.js";
const node_ids = new Set(Object.keys(games));
const node_total = node_ids.size;
// Get rust IDs
const command = spawnSync("cargo", [
"run",
"-p",
"gamedig",
"--example",
"generic",
]);
if (command.status !== 0) {
console.error(command.stderr.toString("utf8"));
process.exit(1);
}
const rust_ids_pretty = command.stdout.toString("utf8");
const rust_ids = new Set(
rust_ids_pretty
.split("\n")
.map((line) => line.split("\t")[0])
.filter((id) => id.length > 0)
);
// Detect missing node IDs
for (const id of rust_ids) {
if (node_ids.delete(id)) {
rust_ids.delete(id);
}
}
console.log("Node remains", node_ids);
console.log("Rust remains", rust_ids);
const percent = 1 - node_ids.size / node_total;
// Output percent to 2 decimal places
setOutput("percent", Math.round(percent * 10000) / 100);

4
.gitignore vendored
View file

@ -11,7 +11,3 @@ Cargo.lock
# Others
.idea/
.venv/
.vscode/
test_everything.py

View file

@ -1,60 +0,0 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: local
hooks:
- id: clippy
name: Check clippy
language: system
files: '([.]rs|Cargo\.toml)$'
pass_filenames: false
entry: rustup run --install nightly-2025-04-19 cargo-clippy -- --workspace --all-features -- -D warnings
- id: build-no-features
name: Check crate build with no features
language: system
files: '([.]rs|Cargo\.toml)$'
pass_filenames: false
entry: cargo check --workspace --no-default-features
- id: build-all-features
name: Check crate builds with all features
language: system
files: '([.]rs|Cargo\.toml)$'
pass_filenames: false
entry: cargo check --workspace --all-features --lib --bins --examples
- id: test
name: Check tests pass
language: system
files: '([.]rs|Cargo\.toml)$'
pass_filenames: false
entry: cargo test --workspace --bins --lib --examples --tests --all-features
- id: format
name: Check rustfmt
language: system
files: '([.]rs|Cargo\.toml)$'
pass_filenames: false
entry: rustup run --install nightly-2025-04-19 cargo-fmt --check
- id: msrv
name: Check MSRV compiles (lib only)
language: system
files: '([.]rs|Cargo\.toml)$'
pass_filenames: false
entry: rustup run --install 1.85.1 cargo check -p gamedig
- id: docs
name: Check rustdoc compiles
language: system
files: '([.]rs|Cargo\.toml)$'
pass_filenames: false
entry: env RUSTDOCFLAGS="-D warnings" cargo doc
- id: actions
name: Check actions work
language: system
files: '^[.]github/workflows/'
pass_filenames: false
entry: act --rm

View file

@ -1,72 +0,0 @@
attr_fn_like_width = 70
array_width = 60
binop_separator = "Front"
blank_lines_lower_bound = 0
blank_lines_upper_bound = 1
brace_style = "PreferSameLine"
chain_width = 60
color = "Auto"
combine_control_expr = false
comment_width = 80
condense_wildcard_suffixes = true
control_brace_style = "AlwaysSameLine"
disable_all_formatting = false
doc_comment_code_block_width = 100
edition = "2021"
emit_mode = "Files"
empty_item_single_line = true
error_on_line_overflow = false
error_on_unformatted = false
fn_call_width = 60
fn_params_layout = "Tall"
fn_single_line = true
force_explicit_abi = true
force_multiline_blocks = true
format_generated_files = true
format_macro_bodies = true
format_strings = true
group_imports = "Preserve"
hard_tabs = false
show_parse_errors = true
hex_literal_case = "Preserve"
ignore = []
indent_style = "Block"
imports_granularity = "Preserve"
imports_indent = "Block"
imports_layout = "HorizontalVertical"
inline_attribute_width = 0
make_backup = false
match_arm_blocks = true
match_arm_leading_pipes = "Never"
match_block_trailing_comma = false
max_width = 120
merge_derives = true
newline_style = "Auto"
normalize_comments = true
normalize_doc_attributes = false
overflow_delimited_expr = false
reorder_impl_items = false
reorder_imports = true
reorder_modules = true
required_version = "1.8.0"
short_array_element_width_threshold = 10
single_line_if_else_max_width = 50
skip_children = false
space_after_colon = true
space_before_colon = false
spaces_around_ranges = true
struct_field_align_threshold = 0
struct_lit_single_line = true
struct_lit_width = 18
struct_variant_width = 35
tab_spaces = 4
trailing_comma = "Vertical"
trailing_semicolon = true
type_punctuation_density = "Wide"
unstable_features = false
use_field_init_shorthand = false
use_small_heuristics = "Default"
use_try_shorthand = true
style_edition = "2021"
where_single_line = true
wrap_comments = true

View file

@ -1,119 +0,0 @@
# Contributing to rust-GameDig
This project is very open to new suggestions, additions and/or changes, these
can come in the form of *discussions* about the project's state, *proposing a
new feature*, *holding a few points on why we shall do X breaking change* or
*submitting a fix*.
## Communications
GitHub is the place we use to track bugs and discuss new features/changes,
although we have a [Discord](https://discord.gg/NVCMn3tnxH) server for the
community, all bugs, suggestions and changes will be reported on GitHub
alongside with their backing points to ensure the transparency of the project's
development.
## Issues
Before opening an issue, check if there is an existing relevant issue first,
someone might just have had your issue already, or you might find something
related that could be of help.
When opening a new issue, make sure to fill the issue template. They are made
to make the subject to be as understandable as possible, not doing so may result
in your issue not being managed right away, if you don't understand something
(be it regarding your own problem/the issue template/the library), please state
so.
## Development
Note before contributing that everything done here is under the [MIT](https://opensource.org/license/mit/) license.
### Naming
Naming is an important matter, and it shouldn't be changed unless necessary.
Game **names** should be added as they appear on steam (or other storefront
if not listed there) with the release year appended in brackets (except when the
release year is already part of the name).
If there is a mod that needs to be added (or it adds the support for server
queries for the game), its name should be composed of the game name, a separating
**bracket**, the mod name and the release year as specified previously
(e.g. `Grand Theft Auto V - FiveM (2013)`).
A game's **identification** is a lowercase alphanumeric string will and be forged
following these rules:
1. Names composed of a maximum of two words (unless #4 applies) will result in an
id where the words are concatenated (`Dead Cells` -> `deadcells`), acronyms in
the name count as a single word (`S.T.A.L.K.E.R.` -> `stalker`).
2. Names of more than two words shall be made into an acronym made of the
initial
letters (`The Binding of Isaac` -> `tboi`), [hypenation composed words](https://prowritingaid.com/hyphenated-words)
don't count as a single word, but of how many parts they are made of
(`Dino D-Day`, 3 words, so `ddd`).
3. If a game has the exact name as a previously existing id's game
(`Star Wars Battlefront 2`, the 2005 and 2017 one), append the release year to
the newer id (2005 would be `swb2` (suppose we already have this one supported)
and 2017 would be `swb22017`).
4. If a new id (`Day of Dragons` -> `dod`) results in an id that already exists
(`Day of Defeat` -> `dod`), then the new name should ignore rule #2
(`Day of Dragons` -> `dayofdragons`).
5. Roman numbering will be converted to arabic numbering (`XIV` -> `14`).
6. Unless numbers (years included) are at the end of a name, they will be considered
words. If a number is not in the first position, its entire numeric digits will be
used instead of the acronym of that number's digits (`Left 4 Dead` -> `l4d`). If the
number is in the first position the longhand (words: 5 -> five) representation of the
number will be used to create an acronym (`7 Days to Die` -> `sdtd`). Other examples:
`Team Fortress 2` -> `teamfortress2`, `Unreal Tournament 2003` ->
`unrealtournament2003`.
7. If a game supports multiple protocols, multiple entries will be done for said game
where the edition/protocol name (first disposable in this order) will be appended to
the base game id's: `<game_id><protocol_id>` (where the protocol id will follow all
rules except #2) (Minecraft is mainly divided by 2 editions, Java and Bedrock
which will be `minecraftjava` and `minecraftbedrock` respectively, but it also has
legacy versions, which use another protocol, an example would be the one for `1.6`,
so the name would be `Legacy 1.6` which its id will be `legacy16`, resulting in the
entry of `minecraftlegacy16`). One more entry can be added by the base name of the
game, which queries in a group said supported protocols to make generic queries
easier and disposable.
8. If its actually about a mod that adds the ability for queries to be performed,
process only the mod name.
### Making commits
Where possible please format commits as complete atomic changes that don't rely on
any future commits. Also make sure that the commit message is as descriptive as
possible.
To avoid CI failing when you make a PR you can use our pre-commit hooks: tests that
run before you are able to make a commit (you can skip this at any time by adding
the `-n` flag to `git commit`).
To set this up you need the following programs installed
- [pre-commit](https://pre-commit.com/)
- [rustup](https://rustup.rs/)
- [act](https://github.com/nektos/act) (If you want to test changes to github actions workflows)
Once these are installed you can enable the pre-commit hook by running the following in
the root directory of the repository.
```shell
$ pre-commit install
```
### Priorities
Game suggestions will be prioritized by maintainers based on whether the game
uses a protocol already implemented in the library (games that use already
implemented protocols will be added first), except in the case where a
contribution is made with the protocol needed to implement the game.
The same goes for protocols, if 2 were to be requested, the one implemented in
the most games will be prioritized.
### Releases
Currently, there is no release schedule.
Releases are made when the team decides one will be fitting to be done.

View file

@ -1,22 +1,14 @@
[workspace]
members = ["crates/cli", "crates/lib", "crates/id-tests"]
[package]
name = "gamedig"
version = "0.0.0"
edition = "2021"
authors = ["CosminPerRam [cosmin.p@live.com]", "mmorrisontx [https://github.com/mmorrisontx]"]
license-file = "LICENSE.md"
description = "Check out servers with this."
homepage = "https://github.com/CosminPerRam/rust-gamedig"
documentation = "https://github.com/CosminPerRam/rust-gamedig"
repository = "https://github.com/CosminPerRam/rust-gamedig"
readme = "README.md"
keywords = ["server", "valve", "games", "checker", "status"]
# Edition 2021, uses resolver = 2
resolver = "2"
[profile.release]
opt-level = 3
debug = false
rpath = true
lto = 'fat'
codegen-units = 1
[profile.release.package."*"]
opt-level = 3
# When building locally, use the local version of the library
# Comment this out when you want to resolve the library from crates.io
# This is only for crates that use gamedig as a dependency
# https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html
[patch.crates-io]
gamedig = { path = "./crates/lib" }
[dependencies]

104
GAMES.md
View file

@ -1,104 +0,0 @@
A supported game is defined as a game that has been successfully tested, other games that are not present here but use
one of the implemented protocols might work too, but that isn't guaranteed.
Beware of the `Notes` column, as it contains information about query port offsets or other query
requirements/information.
# Supported games:
| Game | Use name | Protocol | Notes |
|------------------------------------|---------------------|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Team Fortress 2 | TEAMFORTRESS2 | Valve | |
| The Ship | THESHIP | Valve (*Altered) | |
| Counter-Strike: Global Offensive | CSGO | Valve | The server must have the cvar `host_players_show` set to `2` to get the full player list. |
| Counter-Strike: Source | CSS | Valve | |
| Day of Defeat: Source | DODS | Valve | |
| Left 4 Dead | LEFT4DEAD | Valve | |
| Left 4 Dead 2 | LEFT4DEAD2 | Valve | |
| Half-Life 2 Deathmatch | HL2D | Valve | |
| Alien Swarm | ALIENSWARM | Valve | |
| Alien Swarm: Reactive Drop | ASRD | Valve | |
| Insurgency | INSURGENCY | Valve | |
| Insurgency: Sandstorm | INSURGENCYSANDSTORM | Valve | Query port offset: 1. |
| Insurgency: Modern Infantry Combat | IMIC | Valve | |
| Counter-Strike: Condition Zero | CSCZ | Valve GoldSrc | |
| Day of Defeat | DOD | Valve GoldSrc | |
| Minecraft | MINECRAFT | Proprietary | Bedrock edition provides a different response compared to the Java edition, query specifically for bedrock to get them, otherwise, only matching fields will be provided. |
| 7 Days To Die | SD2D | Valve | |
| ARK: Survival Evolved | ASE | Valve | |
| Unturned | UNTURNED | Valve | |
| The Forest | THEFOREST | Valve GoldSrc | Query port offset: 1. |
| Team Fortress Classic | TFC | Valve | |
| Sven Co-op | SCO | Valve GoldSrc | |
| Rust | RUST | Valve | |
| Counter-Strike | COUNTERSTRIKE | Valve GoldSrc | |
| Arma 2: Operation Arrowhead | A2OA | Valve | Query port offset: 1. |
| Arma 3 | ARMA3 | Valve | |
| Day of Infamy | DOI | Valve | |
| Half-Life Deathmatch: Source | HLDS | Valve | |
| Risk of Rain 2 | ROR2 | Valve | Query port offset: 1. |
| Battalion 1944 | BATTALION1944 | Valve | Query port offset: 3. It is strongly recommended to also query the rules, as it sends basic server info in them. |
| Black Mesa | BLACKMESA | Valve | |
| Project Zomboid | PROJECTZOMBOID | Valve | |
| Age of Chivalry | AOC | Valve | |
| Don't Starve Together | DST | Valve | Query port is 27016. |
| Colony Survival | COLONYSURVIVAL | Valve | |
| Onset | ONSET | Valve | Query port is 7776. |
| Codename CURE | CODENAMECURE | Valve | |
| Ballistic Overkill | BALLISTICOVERKILL | Valve | Query port is 27016. |
| BrainBread 2 | BRAINBREAD2 | Valve | |
| Avorion | AVORION | Valve | Query port is 27020. |
| Operation: Harsh Doorstop | OHD | Valve | Query port is 27005. |
| V Rising | VRISING | Valve | Query port is 27016. |
| Unreal Tournament | UNREALTOURNAMENT | GameSpy 1 | Query Port offset: 1. |
| Battlefield 1942 | B1942 | GameSpy 1 | Query port is 23000. |
| Serious Sam | SERIOUSSAM | GameSpy 1 | Query Port offset: 1. |
| Frontlines: Fuel of War | FFOW | Valve (*Altered) | Query Port offset: 2. |
| Crysis Wars | CRYSISWARS | GameSpy 3 | |
| Quake 2 | QUAKE2 | Quake 2 | |
| Quake 1 | QUAKE1 | Quake 1 | |
| Quake 3: Arena | QUAKE3 | Quake 3 | |
| Hell Let Loose | HLL | Valve Protocol | Query port is 26420. Note that on this port it might not send players data, as there might be another query port that does send players data. |
| Soldier of Fortune 2 | SOF2 | Quake 3 | |
| Halo: Combat Evolved | HCE | GameSpy 2 | |
| Just Cause 2: Multiplayer | JC2M | GameSpy 3 (*Altered) | |
| Warsow | WARSOW | Quake 3 | |
| Creativerse | CREATIVERSE | Valve | Query Port offset: 1. |
| Garry's Mod | GARRYSMOD | Valve | |
| Barotrauma | BAROTRAUMA | Valve | Query Port offset: 1. |
| Valheim | VALHEIM | Valve | Query Port offset: 1. Does not respond to the A2S rules. |
| The Front | THEFRONT | Valve | Responds with wrong values on `name` (gives out a SteamID instead of the server name) and `players_maximum` (always 200). |
| Conan Exiles | CONANEXILES | Valve | Does not respond to the players query. |
| Darkest Hour: Europe '44-'45 | DARKESTHOUR | Unreal2 | Query port offset: 1 |
| Devastation | DEVASTATION | Unreal2 | Query port offset: 1 |
| Killing Floor | KILLINGFLOOR | Unreal2 | Query port offset: 1 |
| Red Orchestra | REDORCHESTRA | Unreal2 | Query port offset: 1 |
| Unreal Tournament 2003 | UT2003 | Unreal2 | Query port offset: 1 |
| Unreal Tournament 2004 | UT2004 | Unreal2 | Query port offset: 1 |
| Post Scriptum | POSTSCRIPTUM | Valve | |
| Squad | SQUAD | Valve | |
| Savage 2 | SAVAGE2 | Proprietary | |
| Rising World | RISINGWORLD | Valve | Query port offset: -1 |
| ATLAS | ATLAS | Valve | Query port offset: 51800 |
| America's Army: Proving Grounds | AAPG | Valve | Query port: 27020. Does not respond to the rules query. |
| Base Defense | BASEDEFENSE | Valve | Query port: 27015. Does not respond to the rules query. |
| Zombie Panic: Source | ZPS | Valve | Query port: 27015. |
| Call Of Duty: Black Ops 3 | CODBO3 | Valve | Query port: 27017. |
| Counter-Strike 2 | COUNTERSTRIKE2 | Valve | |
| Double Action: Boogaloo | DAB | Valve | |
| Mordhau | MORDHAU | Valve | |
| Enshrouded | ENSHROUDED | Valve | |
| Myth of Empires | MOE | Valve | |
| Pirates, Vikings, and Knights II | PVAK2 | Valve | |
| PixARK | PIXARK | Valve | |
| Ark: Survival Ascended | ASA | Epic | Available on the 'tls' feature |
| Aliens vs. Predator 2010 | AVP | Valve | |
| Arma Reforger | ARMAREFORGER | Valve | |
| Nova-Life: Amboise | NLA | Valve | |
| Abiotic Factor | ABIOTICFACTOR | Valve | |
| Soulmask | SOULMASK | Valve | |
| Starbound | STARBOUND | Valve | |
| Minetest | MINETEST | Proprietary | Available on the 'tls', 'serde' and 'services' feature |
## Planned to add support:
_

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 - 2026 GameDig Organization & Contributors
Copyright (c) 2022 CosminPerRam
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,18 +0,0 @@
A protocol is defined as proprietary if it is being used only for a single scope (or series, like Minecraft).
# Supported protocols:
| Name | For | Proprietary? | Documentation reference | Notes |
|---------------------------|-------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Valve Protocol | Games | No | [Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) | In some cases, the players details query might contain some 0-length named players. Multi-packet decompression not tested. |
| Minecraft | Games | Yes | Java: [List Server Protocol](https://wiki.vg/Server_List_Ping) <br> Bedrock: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js) | |
| GameSpy | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/gamespy3.js) | These protocols are not really standardized, gamedig tries to get the most common fields amongst its supported games, if there are parsing problems, use the `query_vars` function. |
| Quake | Games | No | One: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake1.js) Two: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake2.js) Three: [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/quake3.js) | |
| Just Cause 2: Multiplayer | Games | Yes | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/jc2mp.js) |
| Unreal 2 | Games | No | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/unreal2.js) | Sometimes servers send strings that node-gamedig would treat as latin1 that are UTF-8 encoded, when this happens the remove color code breaks because latin1 decodes the colour sequences differently. Some games provide additional info at the end of the server info packet, this is not currently handled (see the node implementation). Some games use a bot player to denote the team names, this is not currently handled. |
| Savage 2 | Games | Yes | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/savage2.js) | |
| Epic | Games | No | [Node-GameDig Source](https://github.com/gamedig/node-gamedig/blob/master/protocols/epic.js) | Available only on the 'tls' feature. |
## Planned to add support:
_

119
README.md
View file

@ -1,118 +1 @@
<h1 align="center">rust-GameDig</h1>
<h5 align="center">The fast library for querying game servers/services.</h5>
<div align="center">
<a href="https://github.com/gamedig/rust-gamedig/actions">
<img src="https://github.com/gamedig/rust-gamedig/actions/workflows/ci.yml/badge.svg" alt="CI">
</a>
<a href="https://crates.io/crates/gamedig">
<img src="https://img.shields.io/crates/v/gamedig.svg?color=orange" alt="Latest Version">
</a>
<a href="https://crates.io/crates/gamedig">
<img src="https://img.shields.io/crates/d/gamedig?color=purple" alt="Crates.io">
</a>
<a href="https://github.com/gamedig/node-gamedig">
<img src="https://raw.githubusercontent.com/gamedig/rust-gamedig/main/.github/badges/node.svg" alt="Node-GameDig Game Coverage">
</a>
<a href="https://deps.rs/crate/gamedig">
<img src="https://deps.rs/crate/gamedig/latest/status.svg" alt="Rust-GameDig Dependencies">
</a>
</div>
<h5 align="center">
This library brings what
<a href="https://github.com/gamedig/node-gamedig">
node-GameDig
</a>
does (and not only), to pure Rust!
</h5>
**Warning**: This project goes through frequent API breaking changes and hasn't been thoroughly tested.
## Community
Checkout the GameDig Community Discord Server [here](https://discord.gg/NVCMn3tnxH).
Note that it isn't be a replacement for GitHub issues, if you have found a problem
within the library or want to request a feature, it's better to do so here rather than
on Discord.
## Usage
Minimum Supported Rust Version is `1.85.1` and the code is cross-platform.
Pick a game/service/protocol (check the [GAMES](GAMES.md), [SERVICES](SERVICES.md) and [PROTOCOLS](PROTOCOLS.md) files
to see the currently supported ones), provide the ip and the port (be aware that some game servers use a separate port
for the info queries, the port can also be optional if the server is running the default ports) then query on it.
[Team Fortress 2](https://store.steampowered.com/app/440/Team_Fortress_2/) query example:
```rust
use gamedig::games::teamfortress2;
fn main() {
let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None);
// None is the default port (which is 27015), could also be Some(27015)
match response { // Result type, must check what it is...
Err(error) => println!("Couldn't query, error: {}", error),
Ok(r) => println!("{:#?}", r)
}
}
```
Response (note that some games have a different structure):
```json5
{
protocol: 17,
name: "Team Fortress 2 Dedicated Server.",
map: "ctf_turbine",
game: "tf2",
appid: 440,
players_online: 0,
players_details: [],
players_maximum: 69,
players_bots: 0,
server_type: Dedicated,
has_password: false,
vac_secured: true,
version: "7638371",
port: Some(27015),
steam_id: Some(69753253289735296),
tv_port: None,
tv_name: None,
keywords: Some(
"alltalk,nocrits"
),
rules: [
"mp_autoteambalance"
:
"1",
"mp_maxrounds"
:
"5",
//....
]
}
```
Want to see more examples? Checkout the [examples](crates/lib/examples) folder.
## Command Line Interface
The library also has an [official CLI](https://crates.io/crates/gamedig_cli) that you can use, it has
MSRV of `1.85.1`.
## Documentation
The documentation is available at [docs.rs](https://docs.rs/gamedig/latest/gamedig/).
Curious about the history and what changed between versions?
Everything is in the changelogs file: [lib](crates/lib/CHANGELOG.md) and [cli](crates/lib/CHANGELOG.md).
## Contributing
If you want to see your favorite game/service being supported here, open an issue, and I'll prioritize it (or do a pull
request if you want to implement it yourself)!
Before contributing please read [CONTRIBUTING](CONTRIBUTING.md).
# rust-gamedig

View file

@ -1,78 +0,0 @@
Every protocol has its own response type(s), below is a listing of the overlapping fields on these responses.
If a cell is blank it doesn't exist, otherwise it contains the type of that data in the current column's response type.
In the case that a field that performs the same function exists in the current column's response type that name is
annotated in brackets.
# Response table
| Field | Generic | GameSpy(1) | GameSpy(2) | GameSpy(3) | Minecraft(Java) | Minecraft(Bedrock) | Valve | Quake | Unreal2 | Epic | Proprietary: FFOW | Proprietary: TheShip | Proprietary: JC2MP | Proprietary: Savage 2 | Proprietary: Minetest |
|----------------------|----------|------------|------------|------------|-----------------|--------------------|---------------|-----------|------------|----------|-------------------|----------------------|--------------------|-----------------------|-----------------------|
| name | `Option` | `String` | `String` | `String` | | `String` | `String` | `String` | `String` | `String` | `String` | `String` | `String` | `String` | `String` |
| description | `Option` | | | | `String` | | | | | | `String` | | `String` | | `String` |
| game_mode | `Option` | `String` | | `String` | | `Option` | `String` | | `String` | | `String` | `String` | | `String` | |
| game_version | `Option` | `String` | | `String` | `String` | | `String` | `String` | | `String` | `String` | `String` | `String` | | `String` |
| map | `Option` | `String` | `String` | `String` | | `Option` | `String` | `String` | `String` | `String` | `String` | `String` | | `String` | |
| players_maxmimum | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` | `u32` |
| players_online | `u32` | `u32` | `u32` | `u32` | `u32` | `u32` | `u8` | `u8` | `u32` | `u32` | `u8` | `u8` | `u32` | `u8` | `u32` |
| players_bots | `Option` | | | | | | `u8` | | | | | `u8` | | | |
| has_password | `Option` | `bool` | `bool` | `bool` | | | `bool` | | | `bool` | `bool` | `bool` | `bool` | | `Option` |
| players_minimum | | `Option` | `Option` | `Option` | | | | | | | | | | `u8` | |
| players | | `Vec` | `Vec` | `Vec` | `Option>` | | `Option>` | `Vec ` | `Vec` | `Vec` | | `Vec` | `Vec` | | `Vec` |
| tournament | | `bool` | | `bool` | | | | | | | | | | | |
| unused_entries | | `Hashmap` | | `HashMap` | | | | `HashMap` | | | | | | | |
| teams | | | `Vec` | `Vec` | | | | | | | | | | | |
| protocol_version | | | | | `i32` | `String` | `u8` | | | | `u8` | `u8` | | `String` | |
| server_type | | | | | `Server` | `Server` | `Server` | | | | | `Server` | | | |
| rules | | | | | | | `Option>` | | `HashMap>` | | | `HashMap` | | | |
| environment_type | | | | | | | `Environment` | | | | `Environment` | | | | |
| vac_secured | | | | | | | `bool` | | | | `bool` | `bool` | | | |
| map_title | | `Option` | | | | | | | | | | | | | |
| admin_contact | | `Option` | | | | | | | | | | | | | |
| admin_name | | `Option` | | | | | | | | | | | | | |
| favicon | | | | | `Option` | | | | | | | | | | |
| previews_chat | | | | | `Option` | | | | | | | | | | |
| enforces_secure_chat | | | | | `Option` | | | | | | | | | | |
| edition | | | | | | `String` | | | | | | | | | |
| id | | | | | | `String` | | | `String` | | | | | | `String` |
| the_ship | | | | | | | `Option` | | | | | | | | |
| is_mod | | | | | | | `bool` | | | | | | | | |
| extra_data | | | | | | | `Option` | | | | | | | | |
| mod_data | | | | | | | `Option` | | | | | | | | |
| folder | | | | | | | `String` | | | | | | | | |
| appid | | | | | | | `u32` | | | | | | | | |
| active_mod | | | | | | | | | | | `String` | | | | |
| round | | | | | | | | | | | `u8` | | | | |
| rounds_maximum | | | | | | | | | | | `u8` | | | | |
| time_left | | | | | | | | | | | `u16` | | | | |
| port | | | | | | | | | `u32` | | | `Option` | | | `u32` |
| steam_id | | | | | | | | | | | | `Option` | | | |
| tv_port | | | | | | | | | | | | `Option` | | | |
| tv_name | | | | | | | | | | | | `Option` | | | |
| keywords | | | | | | | | | | | | `Option` | | | |
| mode | | | | | | | | | | | | `u8` | | | |
| witnesses | | | | | | | | | | | | `u8` | | | |
| duration | | | | | | | | | | | | `u8` | | | |
| query_port | | | | | | | | | `u32` | | | | | | |
| ip | | | | | | | | | `String` | | | | | | `String` |
| mutators | | | | | | | | | `HashSet` | | | | | | |
| next_map | | | | | | | | | | | | | | `String` | |
| location | | | | | | | | | | | | | | `String` | |
| level_minimum | | | | | | | | | | | | | | `String` | |
| time | | | | | | | | | | | | | | `String` | |
| creative | | | | | | | | | | | | | | | `Option` |
| damage | | | | | | | | | | | | | | | `bool` |
| game_time | | | | | | | | | | | | | | | `u32` |
| lag | | | | | | | | | | | | | | | `Option` |
| proto_max | | | | | | | | | | | | | | | `u16` |
| proto_min | | | | | | | | | | | | | | | `u16` |
| pvp | | | | | | | | | | | | | | | `bool` |
| uptime | | | | | | | | | | | | | | | `u32` |
| url | | | | | | | | | | | | | | | `Option` |
| update_time | | | | | | | | | | | | | | | `u32` |
| start | | | | | | | | | | | | | | | `u32` |
| clients_top | | | | | | | | | | | | | | | `u32` |
| updates | | | | | | | | | | | | | | | `u32` |
| pop_v | | | | | | | | | | | | | | | `f32` |
| geo_continent | | | | | | | | | | | | | | | `Option` |
| ping | | | | | | | | | | | | | | | `f32` |

View file

@ -1,10 +0,0 @@
# Supported services:
| Name | Documentation reference |
|------------------------|-------------------------------------------------------------------------------------------------------|
| Valve Master Server | [Master Server Query Protocol](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol) |
| MineTest Master Server | [Node-GameDig](https://github.com/gamedig/node-gamedig/blob/master/protocols/minetest.js) |
## Planned to add support:
TeamSpeak

View file

@ -1,31 +0,0 @@
# MSRV (Minimum Supported Rust Version)
Current: `1.85.1`
Places to update:
- `Cargo.toml`
- `README.md`
- `crates/lib/README.md`
- `.github/workflows/ci.yml`
- `.pre-commit-config.yaml`
# rustfmt version
Current: `1.8.0`
Places to update:
- `.rustfmt.toml`
- The nightly rust version
# The nightly rust version
The toolchain version used to run rustfmt in CI
Current: `nightly-2025-04-19`
Places to update:
- `.github/workflows/ci.yml`
- `.pre-commit-config.yaml`

View file

@ -1,10 +0,0 @@
[profile.release]
opt-level = 'z'
debug = false
rpath = true
lto = 'fat'
codegen-units = 1
strip = 'debuginfo'
[profile.release.package."*"]
opt-level = 'z'

View file

@ -1,76 +0,0 @@
Who knows what the future holds...
# X.Y.Z - DD/MM/YYYY
Nothing... yet.
# 0.5.0 - 22/02/2026
### Breaking Changes:
- MSRV has been updated to `1.85.1` to match the latest `gamedig` version.
### Changes:
- Updated dependencies
# 0.4.0 - 24/08/2025
### Breaking Changes:
- MSRV has been updated to `1.82.0` to match the latest `gamedig` version.
### Changes:
- Some minor clippy fixes
- Updated dependencies
# 0.3.0 - 23/04/2025
### Changes:
- CLI now uses `gamedig` v0.7.0 (To update, run `cargo install gamedig_cli`).
### Breaking Changes:
- MSRV has been updated to `1.81.0` to match the latest `gamedig` version.
# 0.2.1 - 05/12/2024
Dependencies:
- `gamedig`: `v0.6.0 -> v0.6.1`
# 0.2.0 - 26/11/2024
### Breaking Changes:
- Restructured the release flow to be more consistent (GitHub releases will no longer be available, use cargo instead).
- Changed crate name from `gamedig-cli` to `gamedig_cli` to align with recommended naming conventions.
- The CLI now requires a minimum Rust version of `1.74.1`.
# 0.1.1 - 15/07/2024
### Changes:
- Dependency updates (by @cainthebest)
- `gamedig`: `v0.5.0 -> v0.5.1`
- `clap`: `v4.1.11 -> v4.5.4`
- `quick-xml`: `v0.31.0 -> v0.36.0`
- `webbrowser`: `v0.8.12 -> v1.0.0`
# 0.1.0 - 15/03/2024
### Changes:
- Added the CLI (by @cainthebest).
- Added DNS lookup support (by @Douile).
- Added JSON output option (by @Douile).
- Added BSON output in hex or base64 (by @cainthebest).
- Added XML output option (by @cainthebest).
- Added ExtraRequestSettings as CLI arguments (by @Douile).
- Added TimeoutSettings as CLI argument (by @Douile).
- Added Comprehensive end-user documentation for the CLI interface (by @Douile & @cainthebest).
- Tweaked compile-time flags to allow for a more preformant binary (by @cainthebest).
- Added client for socket capture, dev tools are not included by default (by @Douile).
- Added license information to the CLI (by @cainthebest).
- Added source code information to the CLI (by @cainthebest).

View file

@ -1,52 +0,0 @@
[package]
name = "gamedig_cli"
authors = ["rust-GameDig contributors [https://github.com/gamedig/rust-gamedig/contributors]"]
description = "A command line interface for gamedig"
license = "MIT"
version = "0.5.0"
edition = "2021"
default-run = "gamedig_cli"
homepage = "https://gamedig.github.io/"
repository = "https://github.com/gamedig/rust-gamedig"
readme = "README.md"
keywords = ["server", "query", "game", "check", "status"]
rust-version = "1.85.1"
categories = ["command-line-interface"]
[features]
default = ["json", "bson", "xml", "browser"]
# Tools
packet_capture = ["gamedig/packet_capture"]
# Output formats
bson = ["dep:serde", "dep:bson", "dep:hex", "dep:base64", "gamedig/serde"]
json = ["dep:serde", "dep:serde_json", "gamedig/serde"]
xml = ["dep:serde", "dep:serde_json", "dep:quick-xml", "gamedig/serde"]
# Misc
browser = ["dep:webbrowser"]
[dependencies]
# Core Dependencies
thiserror = "2.0.18"
clap = { version = "4.5.60", default-features = false, features = ["derive"] }
gamedig = { version = "0.9.0", default-features = false, features = ["clap", "games", "game_defs"] }
# Feature Dependencies
# Serialization / Deserialization
serde = { version = "1", optional = true, default-features = false }
# BSON
bson = { version = "2.15", optional = true, default-features = false }
base64 = { version = "0.22", optional = true, default-features = false, features = ["std"] }
hex = { version = "0.4.3", optional = true, default-features = false }
# JSON
serde_json = { version = "1", optional = true, default-features = false }
# XML
quick-xml = { version = "0.39.2", optional = true, default-features = false }
# Browser
webbrowser = { version = "1.1.0", optional = true, default-features = false }

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 - 2026 GameDig Organization & Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,67 +0,0 @@
# Rust GameDig CLI
The official [rust-GameDig](https://crates.io/crates/gamedig) Command Line Interface.
[![CI](https://github.com/gamedig/rust-gamedig/actions/workflows/ci.yml/badge.svg)](https://github.com/gamedig/rust-gamedig/actions) [![License:MIT](https://img.shields.io/github/license/gamedig/rust-gamedig?color=blue)](https://github.com/gamedig/rust-gamedig/blob/main/LICENSE.md) [![node coverage](https://raw.githubusercontent.com/gamedig/rust-gamedig/main/.github/badges/node.svg)](https://github.com/gamedig/node-gamedig)
## Installation
You can install the CLI via `cargo`:
```sh
cargo install gamedig_cli
```
or
```sh
cargo install gamedig_cli --git https://github.com/gamedig/rust-gamedig.git
```
## Usage
Running `gamedig_cli` without any arguments will display the usage information. You can also use the `--help` (or `-h`) flag to see detailed usage instructions.
Here's also a quick rundown of a simple query with the `json-pretty` format:
Pick a game/service/protocol (check
the [GAMES](https://github.com/gamedig/rust-gamedig/blob/main/GAMES.md), [SERVICES](https://github.com/gamedig/rust-gamedig/blob/main/SERVICES.md)
and [PROTOCOLS](https://github.com/gamedig/rust-gamedig/blob/main/PROTOCOLS.md) files to see the currently supported
ones), provide the ip and the port (be aware that some game servers use a separate port for the info queries, the port
can also be optional if the server is running the default ports) then query on it.
[Team Fortress 2](https://store.steampowered.com/app/440/Team_Fortress_2/) query example:
```sh
gamedig_cli query -g teamfortress2 -i 127.0.0.1 -f json-pretty
```
What we are doing here:
- `-g` (or `--game`) specifies the game.
- `-i` (or `--ip`) target ip.
- `-f` (or `--format`) our preferred format.
Note: We haven't specified a port (via `-p` or `--port`), so the default one for the game will be used (`27015` in this
case).
Response (note that some games have a different structure):
```json
{
"name": "A cool server.",
"description": null,
"game_mode": "Team Fortress",
"game_version": "8690085",
"map": "cp_foundry",
"players_maximum": 24,
"players_online": 0,
"players_bots": 0,
"has_password": false,
"players": []
}
```
## Contributing
Please read [CONTRIBUTING](https://github.com/gamedig/rust-gamedig/blob/main/CONTRIBUTING.md).

View file

@ -1,31 +0,0 @@
pub type Result<T> = std::result::Result<T, Error>;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("IO Error: {0}")]
Io(#[from] std::io::Error),
#[error("Clap Error: {0}")]
Clap(#[from] clap::Error),
#[error("Gamedig Error: {0}")]
Gamedig(#[from] gamedig::errors::GDError),
#[cfg(any(feature = "json", feature = "xml"))]
#[error("Serde Error: {0}")]
Serde(#[from] serde_json::Error),
#[cfg(feature = "bson")]
#[error("Bson Error: {0}")]
Bson(#[from] bson::ser::Error),
#[cfg(feature = "xml")]
#[error("Xml Error: {0}")]
Xml(#[from] quick_xml::Error),
#[error("Unknown Game: {0}")]
UnknownGame(String),
#[error("Invalid hostname: {0}")]
InvalidHostname(String),
}

View file

@ -1,487 +0,0 @@
use std::net::{IpAddr, ToSocketAddrs};
use clap::{Parser, Subcommand, ValueEnum};
use gamedig::{
games::*,
protocols::types::{CommonResponse, ExtraRequestSettings, TimeoutSettings},
};
mod error;
use self::error::{Error, Result};
const GAMEDIG_HEADER: &str = r"
_____ _____ _ _____ _ _____
/ ____| | __ \(_) / ____| | |_ _|
| | __ __ _ _ __ ___ ___| | | |_ __ _ | | | | | |
| | |_ |/ _` | '_ ` _ \ / _ \ | | | |/ _` | | | | | | |
| |__| | (_| | | | | | | __/ |__| | | (_| | | |____| |____ _| |_
\_____|\__,_|_| |_| |_|\___|_____/|_|\__, | \_____|______|_____|
__/ |
|___/
A command line interface for querying game servers.
Copyright (C) 2022 - 2024 GameDig Organization & Contributors
Licensed under the MIT license
";
// NOTE: For some reason without setting long_about here the doc comment for
// ExtraRequestSettings gets set as the about for the CLI.
#[derive(Debug, Parser)]
#[command(author, version, about = GAMEDIG_HEADER, long_about = None)]
struct Cli {
#[command(subcommand)]
action: Action,
}
#[derive(Subcommand, Debug)]
enum Action {
/// Query game server information
Query {
/// Unique identifier of the game for which server information is being
/// queried.
#[arg(short, long)]
game: String,
/// Hostname or IP address of the server.
#[arg(short, long)]
ip: String,
/// Optional query port number for the server. If not provided the
/// default port for the game is used.
#[arg(short, long)]
port: Option<u16>,
/// Specifies the output format
#[arg(short, long, default_value = "debug", value_enum)]
format: OutputFormat,
/// Which response variant to use when outputting
#[arg(short, long, default_value = "generic")]
output_mode: OutputMode,
/// Optional file path for packet capture file writer
///
/// When set a PCAP file will be written to the location. This file can
/// be read with a tool like wireshark. The PCAP contains a log of the
/// TCP and UDP data sent/recieved by the gamedig library, it does not
/// contain an accurate representation of the real packets sent on the
/// wire as some information has to be hallucinated in order for it to
/// display nicely.
#[cfg(feature = "packet_capture")]
#[arg(short, long)]
capture: Option<std::path::PathBuf>,
/// Optional timeout settings for the server query
#[command(flatten, next_help_heading = "Timeouts")]
timeout_settings: Option<TimeoutSettings>,
/// Optional extra settings for the server query
#[command(flatten, next_help_heading = "Query options")]
extra_options: Option<ExtraRequestSettings>,
},
/// Check out the source code
Source,
/// Display the MIT License information
License,
}
#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)]
enum OutputMode {
/// A generalised response that maps common fields from all game types to
/// the same name.
Generic,
/// The raw result returned from the protocol query, formatted similarly to
/// how the server returned it.
ProtocolSpecific,
}
#[derive(Clone, Debug, PartialEq, Eq, ValueEnum)]
enum OutputFormat {
/// Human readable structured output
Debug,
/// RFC 8259
#[cfg(feature = "json")]
JsonPretty,
/// RFC 8259
#[cfg(feature = "json")]
Json,
/// Parser tries to be mostly XML 1.1 (RFC 7303) compliant
#[cfg(feature = "xml")]
Xml,
/// RFC 4648 section 8
#[cfg(feature = "bson")]
BsonHex,
/// RFC 4648 section 4
#[cfg(feature = "bson")]
BsonBase64,
}
/// Attempt to find a game from the [library game definitions](GAMES) based on
/// its unique identifier.
///
/// # Arguments
/// * `game_id` - A string slice containing the unique game identifier.
///
/// # Returns
/// * Result<&'static [Game]> - On sucess returns a reference to the game
/// definition; on failure returns a [Error::UnknownGame] error.
fn find_game(game_id: &str) -> Result<&'static Game> {
// Attempt to retrieve the game from the predefined game list
GAMES
.get(game_id)
.ok_or_else(|| Error::UnknownGame(game_id.to_string()))
}
/// Resolve an IP address by either parsing an IP address or doing a DNS lookup.
/// In the case of DNS lookup update extra request options with the hostname.
///
/// # Arguments
/// * `host` - A string slice containing the IP address or hostname of a server
/// to resolve.
/// * `extra_options` - Mutable reference to extra options for the game query.
///
/// # Returns
/// * `Result<IpAddr>` - On sucess returns a resolved IP address; on failure
/// returns an [Error::InvalidHostname] error.
fn resolve_ip_or_domain<T: AsRef<str>>(host: T, extra_options: &mut Option<ExtraRequestSettings>) -> Result<IpAddr> {
let host_str = host.as_ref();
if let Ok(parsed_ip) = host_str.parse() {
Ok(parsed_ip)
} else {
set_hostname_if_missing(host_str, extra_options);
resolve_domain(host_str)
}
}
/// Resolve a domain name to one of its IP addresses (the first one returned).
///
/// # Arguments
/// * `domain` - A string slice containing the domain name to lookup.
///
/// # Returns
/// * `Result<IpAddr>` - On success, returns one of the resolved IP addresses;
/// on failure returns an [Error::InvalidHostname] error.
fn resolve_domain(domain: &str) -> Result<IpAddr> {
// Append a dummy port to perform socket address resolution and then extract the
// IP
Ok(format!("{domain}:0")
.to_socket_addrs()
.map_err(|_| Error::InvalidHostname(domain.to_string()))?
.next()
.ok_or_else(|| Error::InvalidHostname(domain.to_string()))?
.ip())
}
/// Sets the hostname on extra request settings if it is not already set.
///
/// # Arguments
/// * `host` - A string slice containing the hostname.
/// * `extra_options` - A mutable reference to optional [ExtraRequestSettings].
fn set_hostname_if_missing(host: &str, extra_options: &mut Option<ExtraRequestSettings>) {
if let Some(extra_options) = extra_options {
if extra_options.hostname.is_none() {
// If extra_options exists but hostname is None overwrite hostname in place
extra_options.hostname = Some(host.to_string())
}
} else {
// If extra_options is None create default settings with hostname
*extra_options = Some(ExtraRequestSettings::default().set_hostname(host.to_string()));
}
}
/// Output the result of a query to stdout.
///
/// # Arguments
/// * `args` - A reference to the command line options.
/// * `result` - A reference to the result of the query.
fn output_result<T: CommonResponse + ?Sized>(output_mode: OutputMode, format: OutputFormat, result: &T) {
match format {
OutputFormat::Debug => {
match output_mode {
OutputMode::Generic => output_result_debug(result.as_json()),
OutputMode::ProtocolSpecific => output_result_debug(result.as_original()),
};
}
#[cfg(feature = "json")]
OutputFormat::JsonPretty => {
let _ = match output_mode {
OutputMode::Generic => output_result_json_pretty(result.as_json()),
OutputMode::ProtocolSpecific => output_result_json_pretty(result.as_original()),
};
}
#[cfg(feature = "json")]
OutputFormat::Json => {
let _ = match output_mode {
OutputMode::Generic => output_result_json(result.as_json()),
OutputMode::ProtocolSpecific => output_result_json(result.as_original()),
};
}
#[cfg(feature = "xml")]
OutputFormat::Xml => {
let _ = match output_mode {
OutputMode::Generic => output_result_xml(result.as_json()),
OutputMode::ProtocolSpecific => output_result_xml(result.as_original()),
};
}
#[cfg(feature = "bson")]
OutputFormat::BsonHex => {
let _ = match output_mode {
OutputMode::Generic => output_result_bson_hex(result.as_json()),
OutputMode::ProtocolSpecific => output_result_bson_hex(result.as_original()),
};
}
#[cfg(feature = "bson")]
OutputFormat::BsonBase64 => {
let _ = match output_mode {
OutputMode::Generic => output_result_bson_base64(result.as_json()),
OutputMode::ProtocolSpecific => output_result_bson_base64(result.as_original()),
};
}
}
}
/// Output the result using debug formatting.
///
/// # Arguments
/// * `result` - A result that can be output using the debug formatter.
fn output_result_debug<R: std::fmt::Debug>(result: R) {
println!("{result:#?}");
}
/// Output the result as a JSON object.
///
/// # Arguments
/// * `result` - A serde serializable result.
#[cfg(feature = "json")]
fn output_result_json<T: serde::Serialize>(result: T) -> Result<()> {
println!("{}", serde_json::to_string(&result)?);
Ok(())
}
/// Output the result as a pretty printed JSON object.
///
/// # Arguments
/// * `result` - A serde serializable result.
#[cfg(feature = "json")]
fn output_result_json_pretty<T: serde::Serialize>(result: T) -> Result<()> {
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
/// Output the result as an XML object.
/// # Arguments
/// * `result` - A serde serializable result.
#[cfg(feature = "xml")]
fn output_result_xml<T: serde::Serialize>(result: T) -> Result<()> {
use quick_xml::{
events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event},
Writer,
};
use serde_json::Value;
// Serialize the input `result` of generic type `T` into a JSON value.
// This step converts the Rust data structure into a JSON format,
// which will then be used to generate the corresponding XML.
let json = serde_json::to_value(result)?;
// Initialize the XML writer with a new, empty vector to store the XML data.
let mut writer = Writer::new(Vec::new());
// Write the XML 1.1 declaration
writer.write_event(Event::Decl(BytesDecl::new("1.1", Some("utf-8"), None)))?;
// Define a recursive function `json_to_xml` to convert the JSON value into XML
// format. The function takes a mutable reference to the XML writer, an
// optional key as a string slice, and a reference to the JSON value to be
// converted.
fn json_to_xml<W: std::io::Write>(writer: &mut Writer<W>, key: Option<&str>, value: &Value) -> Result<()> {
match value {
// If the JSON value is an object, iterate through its properties,
// creating XML elements with corresponding keys and values.
Value::Object(obj) => {
if let Some(key) = key {
// Start an XML element for the object.
writer.write_event(Event::Start(BytesStart::new(key)))?;
}
for (k, v) in obj {
// Recursively process each property of the object.
json_to_xml(writer, Some(k), v)?;
}
if let Some(key) = key {
// Close the XML element for the object.
writer.write_event(Event::End(BytesEnd::new(key)))?;
}
}
// If the JSON value is an array, iterate through its elements,
// creating XML elements for each item.
Value::Array(arr) => {
for v in arr {
// Use "item" as the default key for array elements without keys.
json_to_xml(writer, key.or(Some("item")), v)?;
}
}
// If the JSON value is null, create an empty XML element.
Value::Null => {
if let Some(key) = key {
writer.write_event(Event::Empty(BytesStart::new(key)))?;
}
}
// For all other JSON value types (String, Number, Bool),
// convert the value to a string and create an XML element with the text content.
// Note: We handle null strings here as well, as they are treated as a string type.
_ => {
if let Some(key) = key {
// Start the XML element with the given key.
writer.write_event(Event::Start(BytesStart::new(key)))?;
}
// Convert the JSON value to a string, trimming quotes for non-string values.
let text_string = match value {
Value::String(s) => s.to_string(),
_ => value.to_string().trim_matches('"').to_string(),
};
// Create a text node with the converted string value.
writer.write_event(Event::Text(BytesText::new(&text_string)))?;
if let Some(key) = key {
// Close the XML element.
writer.write_event(Event::End(BytesEnd::new(key)))?;
}
}
}
Ok(())
}
// Start the root XML element named "data".
writer.write_event(Event::Start(BytesStart::new("data")))?;
// Convert the top-level JSON value to XML.
json_to_xml(&mut writer, None, &json)?;
// Close the root XML element.
writer.write_event(Event::End(BytesEnd::new("data")))?;
// Convert the XML data stored in the writer to a UTF-8 string.
let xml_bytes = writer.into_inner();
let xml_string = String::from_utf8(xml_bytes).expect("Failed to convert XML bytes to UTF-8 string");
println!("{xml_string}");
Ok(())
}
/// Output the result as a BSON object encoded as a hex string.
///
/// # Arguments
/// * `result` - A serde serializable result.
#[cfg(feature = "bson")]
fn output_result_bson_hex<T: serde::Serialize>(result: T) -> Result<()> {
let bson = bson::to_bson(&result)?;
if let bson::Bson::Document(document) = bson {
let bytes = bson::to_vec(&document)?;
println!("{}", hex::encode(bytes));
Ok(())
} else {
panic!("Failed to convert result to BSON Hex (BSON_DOCUMENT_UNAVAILABLE)");
}
}
/// Output the result as a BSON object encoded as a base64 string.
///
/// # Arguments
/// * `result` - A serde serializable result.
#[cfg(feature = "bson")]
fn output_result_bson_base64<T: serde::Serialize>(result: T) -> Result<()> {
use base64::Engine;
let bson = bson::to_bson(&result)?;
if let bson::Bson::Document(document) = bson {
let bytes = bson::to_vec(&document)?;
println!("{}", base64::prelude::BASE64_STANDARD.encode(bytes));
Ok(())
} else {
panic!("Failed to convert result to BSON Base64 (BSON_DOCUMENT_UNAVAILABLE)");
}
}
fn main() -> Result<()> {
let args = Cli::parse();
match args.action {
Action::Query {
game,
ip,
port,
format,
output_mode,
#[cfg(feature = "packet_capture")]
capture,
timeout_settings,
extra_options,
} => {
// Process the query command
let game = find_game(&game)?;
let mut extra_options = extra_options;
let ip = resolve_ip_or_domain(&ip, &mut extra_options)?;
#[cfg(feature = "packet_capture")]
gamedig::capture::setup_capture(capture);
let result = query_with_timeout_and_extra_settings(game, &ip, port, timeout_settings, extra_options)?;
output_result(output_mode, format, result.as_ref());
}
Action::Source => {
println!("{GAMEDIG_HEADER}");
#[cfg(feature = "browser")]
{
// Directly offering to open the URL
println!("\nWould you like to open the GitHub repository in your default browser? [Y/n]");
let mut choice = String::new();
std::io::stdin().read_line(&mut choice).unwrap();
if choice.trim().eq_ignore_ascii_case("Y") {
if webbrowser::open("https://github.com/gamedig/rust-gamedig").is_ok() {
println!("Opening GitHub repository in default browser...");
} else {
println!("Failed to open GitHub repository in default browser.");
println!("Please use the following URL: https://github.com/gamedig/rust-gamedig");
}
} else {
println!("Not to worry, you can always open the repository manually");
println!("by visiting the following URL: https://github.com/gamedig/rust-gamedig");
}
}
#[cfg(not(feature = "browser"))]
{
println!("\nYou can find the source code for this project at the following URL:");
println!("https://github.com/gamedig/rust-gamedig");
}
println!("\nBe sure to leave a star if you like the project :)");
}
Action::License => {
// Bake the license into the binary
// so we don't have to ship it separately
println!("{}", include_str!("../LICENSE.md"));
}
}
Ok(())
}

View file

@ -1,29 +0,0 @@
[package]
name = "gamedig-id-tests"
version = "0.0.1"
edition = "2021"
authors = [
"rust-GameDig contributors [https://github.com/gamedig/rust-gamedig/contributors]",
"node-GameDig contributors [https://github.com/gamedig/node-gamedig/contributors]",
]
license = "MIT"
description = "Test if IDs match the gamedig rules"
homepage = "https://github.com/gamedig/rust-gamedig/CONTRIBUTING.md#naming"
repository = "https://github.com/gamedig/rust-gamedig"
readme = "README.md"
rust-version = "1.65.0"
[features]
cli = ["dep:serde_json", "dep:serde"]
default = ["cli"]
[[bin]]
name = "gamedig-id-tests"
required-features = ["cli"]
[dependencies]
number_to_words = "0.1"
roman_numeral = "0.1"
serde_json = { version = "1", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }

View file

@ -1,451 +0,0 @@
use std::collections::HashMap;
mod utils;
use utils::{extract_bracketed_suffix, split_on_switch_between_alpha_numeric};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IDRule {
IDsMustBeLowerCase,
NumbersAreTheirOwnWord,
IfFirstWordNumberNoDigits,
IfLastWordNumberMustBeAppended,
ConvertRomanNumeralsToArabic,
TwoWordsOrLessUseFullWords,
MoreThanTwoWordsMakeAcronym,
IfIDDuplicateSameGameAppendYearToNewer,
IfIDDuplicateSameGameAppendProtocol,
IfIDDuplicateNoAcronym,
IfModForQueriesProcessOnlyModName,
NoDuplicates,
}
#[derive(Clone, Debug)]
pub struct IDFail {
pub game_id: String,
pub game_name: String,
pub expected_id: String,
pub rule_stack: Vec<IDRule>,
}
impl IDFail {
fn new(game_id: String, game_name: String, expected_id: String, rule_stack: Vec<IDRule>) -> Self {
Self {
game_id,
game_name,
expected_id,
rule_stack,
}
}
}
/// Test a single game against the rules
pub fn test_game_name_rule(
seen_ids: &mut HashMap<String, Vec<String>>,
id: &str,
mut game: GameNameParsed,
is_mod_name: bool,
) -> Vec<IDFail> {
let mut wrong_ids = Vec::new();
let mut rule_stack = Vec::new();
if is_mod_name {
rule_stack.push(IDRule::IfModForQueriesProcessOnlyModName);
}
let mut suffix = String::new();
// A game's identification is a lowercase alphanumeric string will and be forged
// following these rules:
if id.to_lowercase().ne(id) {
wrong_ids.push(IDFail::new(
id.to_owned(),
game.name.to_owned(),
id.to_lowercase(),
vec![IDRule::IDsMustBeLowerCase],
));
}
// 5. Roman numbering will be converted to arabic numbering (XIV -> 14).
game.words = {
let mut is_first = true;
game.words
.into_iter()
.map(|w| {
// First word will never be a numeral
if is_first {
is_first = false;
w
} else if let Ok(number) = roman_numeral::RomanNumeral::from_string(&w) {
rule_stack.push(IDRule::ConvertRomanNumeralsToArabic);
number.get_u32().to_string()
} else {
w
}
})
.collect()
};
// 6. Unless numbers are at the end of a name, they will be considered words,
// but digits will always be used instead of the acronym (counter to #2)
// (Left 4 Dead -> l4d) unless they at the start position (7 Days to Die ->
// sdtd), if they are at the end (such as sequel number or the year), always
// append them (Team Fortress 2 -> teamfortress2, Unreal Tournament 2003 ->
// unrealtournament2003).
game.words = game
.words
.into_iter()
.flat_map(|w| {
let n = split_on_switch_between_alpha_numeric(&w);
if n.len() > 1 {
rule_stack.push(IDRule::NumbersAreTheirOwnWord);
}
n
})
.collect();
// If first word is number make text
if !game.words.is_empty() && game.words[0].chars().next().unwrap().is_ascii_digit() {
game.words[0] = number_to_words::number_to_words(game.words[0].parse::<f64>().unwrap(), false);
rule_stack.push(IDRule::IfFirstWordNumberNoDigits);
}
// If last word is number append full number
if let Some(last_word) = game.words.last() {
if last_word.chars().all(|c| c.is_ascii_digit()) {
suffix += &game.words.pop().unwrap();
rule_stack.push(IDRule::IfLastWordNumberMustBeAppended);
}
}
let main = if game.words.len() <= 2 {
// 1. Names composed of a maximum of two words (unless #4 applies) will result
// in an id where the words are concatenated (Dead Cells -> deadcells),
// acronyms in the name count as a single word (S.T.A.L.K.E.R. -> stalker).
rule_stack.push(IDRule::TwoWordsOrLessUseFullWords);
game.words
.iter()
.map(|w| w.trim_matches('-').to_owned())
.collect::<Vec<_>>()
.join("")
} else {
// 2. Names of more than two words shall be made into an acronym made of the
// initial letters (The Binding of Isaac -> tboi), hypenation composed words
// don't count as a single word, but of how many parts they are made of (Dino
// D-Day, 3 words, so ddd).
rule_stack.push(IDRule::MoreThanTwoWordsMakeAcronym);
game.words
.iter()
.map(|w| w.chars().next().unwrap())
.filter(|c| c.is_alphanumeric())
.collect()
};
let mut expected_id = format!("{main}{suffix}").to_lowercase();
if let Some(other_game_name_words) = seen_ids.get(&expected_id) {
let mut game_names_same = other_game_name_words.len() == game.words.len();
// Check all words in game name are the same
if game_names_same {
for (our_word, their_word) in game.words.iter().zip(other_game_name_words.iter()) {
if our_word.to_lowercase() != their_word.to_lowercase() {
game_names_same = false;
break;
}
}
}
if game_names_same {
if let Some(year) = game.year {
// 3. If a game has the exact name as a previously existing id's game (Star Wars
// Battlefront 2, the 2005 and 2017 one), append the release year to the
// newer id (2005 would be swbf2 (suppose we already have this one supported)
// and 2017 would be swbf22017).
rule_stack.push(IDRule::IfIDDuplicateSameGameAppendYearToNewer);
expected_id = format!("{expected_id}{year}").to_lowercase();
} else if let Some(protocol) = game.optional_parts.first() {
// 7. If a game supports multiple protocols, multiple entries will be done for
// said game where the edition/protocol name (first disposable in this order)
// will be appended to the game name (Minecraft is divided by 2 editions,
// Java and Bedrock which will be minecraftjava and minecraftbedrock
// respectively) and one more entry can be added by the base name of the game
// which queries in a group said supported protocols to make generic queries
// easier and disposable.
rule_stack.push(IDRule::IfIDDuplicateSameGameAppendProtocol);
// Parse the protocol as a game name so we can remove all non-valid characters
let protocol_parsed = extract_game_parts_from_name(protocol);
expected_id = format!("{}{}", expected_id, protocol_parsed.words.concat(),);
}
}
}
// 4. If a new id (Day of Dragons -> dod) results in an id that already exists
// (Day of Defeat -> dod), then the new name should ignore rule #2 (Day of
// Dragons -> dayofdragons).
if seen_ids.contains_key(&expected_id) {
rule_stack.push(IDRule::IfIDDuplicateNoAcronym);
let main = game
.words
.iter()
.map(|w| w.trim_matches('-').to_owned())
.collect::<Vec<_>>()
.join("");
expected_id = format!("{main}{suffix}").to_lowercase();
}
// 8. If its actually about a mod that adds the ability for queries to be
// performed, process only the mod name.
if !is_mod_name && id != expected_id {
if let Some((_, mod_game)) = game.name.split_once('-') {
let mut result = test_game_name_rule(seen_ids, id, extract_game_parts_from_name(mod_game), true);
if result.is_empty() {
return result;
} else {
wrong_ids.append(&mut result);
}
}
}
let duplicate = if seen_ids.insert(expected_id.clone(), game.words).is_some() {
rule_stack.push(IDRule::NoDuplicates);
true
} else {
false
};
// Check ID matches
if id != expected_id || duplicate {
wrong_ids.push(IDFail::new(
id.to_owned(),
game.name.to_owned(),
expected_id,
rule_stack,
));
}
wrong_ids
}
#[derive(Clone, Debug)]
pub struct GameNameParsed<'a> {
name: &'a str,
words: Vec<String>,
optional_parts: Vec<&'a str>,
year: Option<u16>,
}
pub fn extract_game_parts_from_name(game: &str) -> GameNameParsed<'_> {
// Separate game name into words
// NOTE: we have to leave "-" in to prevent hyphenated prefixes being parsed as
// numerals
let mut optional_game_name_parts = Vec::new();
let (game, paren) = extract_bracketed_suffix(game);
if let Some(paren) = paren {
optional_game_name_parts.push(paren);
}
let mut number_accumulator: Option<String> = None;
// Filter map necessary to move out words
#[allow(clippy::unnecessary_filter_map)]
let game_name_words: Vec<_> = game
// First split all text on space or dash
.split_inclusive(&[' ', '-'])
// Remove whitespace surrounding words (leave in dash because it is important information)
.map(str::trim)
// If a word is entirely surrounded in brackets move it to optional parts
.filter_map(|w| {
if w.starts_with('(') && w.ends_with(')') {
optional_game_name_parts.push(w);
None
} else {
Some(w)
}
})
// Remove all characters that aren't alphanumeric or dashses
.map(|w| {
w.replace(
|c: char| !c.is_ascii_digit() && !c.is_alphabetic() && c != '-',
"",
)
})
// Remove words that are empty (discounting strings that are just dashes)
.filter(|w| !w.trim_matches('-').is_empty())
// Combine numbers that are seperated by dashes
// e.g. 44-45 = 4445
// Panics if there is text after number with trailing dash (44-text)
.filter_map(|w| {
if number_accumulator.is_some() {
if let Some(maybe_number) = w.strip_suffix('-') {
if maybe_number.chars().all(|c| c.is_ascii_digit()) {
number_accumulator.as_mut().unwrap().push_str(maybe_number);
return None;
} else {
panic!("Text after number-");
}
} else if w.chars().all(|c| c.is_ascii_digit()) {
let mut accumulator = number_accumulator.as_ref().unwrap().clone();
number_accumulator = None;
accumulator.push_str(&w);
return Some(accumulator);
} else {
panic!("Text after number-");
}
} else if let Some(maybe_number) = w.strip_suffix('-') {
if maybe_number.chars().all(|c| c.is_ascii_digit()) {
number_accumulator = Some(maybe_number.to_string());
return None;
}
}
Some(w)
})
.collect();
let mut game_year: Option<u16> = None;
for optional_part in &optional_game_name_parts {
if let Some(game_year_text) = optional_part
.strip_prefix('(')
.and_then(|s| s.strip_suffix(')'))
{
if let Ok(year) = game_year_text.parse() {
game_year = Some(year);
break;
}
} else if let Ok(year) = optional_part.parse() {
game_year = Some(year);
break;
}
}
GameNameParsed {
name: game,
words: game_name_words,
optional_parts: optional_game_name_parts,
year: game_year,
}
}
/// Iterate game entries and validate the id matches current rules
pub fn test_game_name_rules<'a, I: Iterator<Item = (&'a str, &'a str)>>(games: I) -> Vec<IDFail> {
let mut wrong_ids = Vec::with_capacity(games.size_hint().0);
let mut seen_ids: HashMap<String, Vec<String>> = HashMap::new();
// We must sort games by year so that rule 3 is applied correctly
let mut sorted_games: Vec<_> = games
.map(|(id, game)| {
let game = extract_game_parts_from_name(game);
(id, game)
})
.collect();
sorted_games.sort_by(|(_, a_game), (_, b_game)| {
a_game
.year
.cmp(&b_game.year)
.then(a_game.name.len().cmp(&b_game.name.len()))
});
let game_count = sorted_games.len();
for (id, game) in sorted_games {
wrong_ids.append(&mut test_game_name_rule(&mut seen_ids, id, game, false))
}
if !wrong_ids.is_empty() {
for fail in &wrong_ids {
println!("{fail:#?}");
}
let percentage = (wrong_ids.len() * 100) / game_count;
println!(
"{} ({}%) IDs didn't match naming rules",
wrong_ids.len(),
percentage
);
}
wrong_ids
}
pub fn test_single_game_rule(id: &str, name: &str) -> Vec<IDFail> { test_game_name_rules(std::iter::once((id, name))) }
#[cfg(test)]
mod id_tests {
use super::{test_game_name_rules, test_single_game_rule};
#[test]
fn id_rule_one() {
assert!(test_single_game_rule("testgame", "Test Game").is_empty());
assert!(test_single_game_rule("testgame", "TestGame").is_empty());
assert!(test_single_game_rule("deadcells", "Dead Cells").is_empty());
assert!(test_single_game_rule("stalker", "S.T.A.L.K.E.R").is_empty());
}
#[test]
fn id_rule_two() {
assert!(test_single_game_rule("tgt", "Test Game Three").is_empty());
assert!(test_single_game_rule("tgt", "Test Game-Three").is_empty());
assert!(test_single_game_rule("tboi", "The Binding of Isaac").is_empty());
assert!(test_single_game_rule("ddd", "Dino D-Day").is_empty());
}
#[test]
fn id_rule_three() {
let games = vec![
("swb22017", "Star Wars Battlefront 2 (2017)"),
("swb2", "Star Wars Battlefront 2 (2015)"),
];
assert!(test_game_name_rules(games.into_iter()).is_empty());
}
#[test]
fn id_rule_four() {
let games = vec![("dod", "Day of Defeat"), ("dayofdragons", "Day of Dragons")];
assert!(test_game_name_rules(games.into_iter()).is_empty());
}
#[test]
fn id_rule_five() {
assert!(test_single_game_rule("gta14", "Grand Theft Auto XIV").is_empty());
}
#[test]
fn id_rule_six() {
assert!(test_single_game_rule("l4d", "Left 4 Dead").is_empty());
assert!(test_single_game_rule("sdtd", "7 Days to Die").is_empty());
assert!(test_single_game_rule("teamfortress2", "Team Fortress 2").is_empty());
assert!(test_single_game_rule("unrealtournament2003", "Unreal Tournament 2003").is_empty());
assert!(test_single_game_rule("dhe4445", "Darkest Hour: Europe '44-'45").is_empty());
}
#[test]
fn id_rule_seven() {
let games = vec![
("minecraft", "Minecraft"),
("minecraftjava", "Minecraft (java)"),
("minecraftbedrock", "Minecraft (bedrock)"),
];
assert!(test_game_name_rules(games.into_iter()).is_empty());
}
#[test]
fn id_rule_eight() {
assert!(test_single_game_rule("fivem", "Grand Theft Auto V - FiveM (2013)").is_empty());
assert!(test_single_game_rule("jc3m", "Just Cause 3 - Multiplayer").is_empty());
}
}

View file

@ -1,32 +0,0 @@
#![cfg(feature = "cli")]
use std::collections::HashMap;
/// Format for input games (the same as used in node-gamedig/lib/games.js).
type GamesInput = HashMap<String, Game>;
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
struct Game {
name: String,
}
use gamedig_id_tests::test_game_name_rules;
fn main() {
let games: GamesInput = std::env::args_os().nth(1).map_or_else(
|| serde_json::from_reader(std::io::stdin().lock()).unwrap(),
|file| {
let file = std::fs::OpenOptions::new().read(true).open(file).unwrap();
serde_json::from_reader(file).unwrap()
},
);
let failed = test_game_name_rules(
games
.iter()
.map(|(key, game)| (key.as_str(), game.name.as_str())),
);
assert!(failed.is_empty());
}

View file

@ -1,66 +0,0 @@
/// Split a str when characters swap between being digits and not digits.
pub fn split_on_switch_between_alpha_numeric(text: &str) -> Vec<String> {
if text.is_empty() {
return vec![];
}
let mut parts = Vec::with_capacity(text.len());
let mut current = Vec::with_capacity(text.len());
let mut iter = text.chars();
let c = iter.next().unwrap();
let mut last_was_numeric = c.is_ascii_digit();
current.push(c);
for c in iter {
if c.is_ascii_digit() == last_was_numeric {
current.push(c);
} else {
parts.push(current.iter().collect());
current.clear();
current.push(c);
last_was_numeric = !last_was_numeric;
}
}
parts.push(current.into_iter().collect());
parts
}
#[test]
fn split_correctly() {
assert_eq!(
split_on_switch_between_alpha_numeric("2D45A"),
&["2", "D", "45", "A"]
);
}
#[test]
fn split_symbol_broken_numbers() {
let game_name = super::extract_game_parts_from_name("Darkest Hour: Europe '44-'45");
assert_eq!(game_name.words, &["Darkest", "Hour", "Europe", "4445"]);
}
/// Extract parts at end of string enclosed in brackets.
pub fn extract_bracketed_suffix(text: &str) -> (&str, Option<&str>) {
if let Some(text) = text.strip_suffix(')') {
if let Some((text, extra)) = text.rsplit_once('(') {
return (text, Some(extra));
}
}
(text, None)
}
#[test]
fn extract_brackets_correctly() {
assert_eq!(
extract_bracketed_suffix("no brackets here"),
("no brackets here", None)
);
assert_eq!(
extract_bracketed_suffix("Game name (with protocol here)"),
("Game name ", Some("with protocol here"))
);
}

View file

@ -1,674 +0,0 @@
Who knows what the future holds...
# X.Y.Z - DD/MM/YYYY
# 0.9.0 22/02/2026
Breaking:
- MSRV is now `1.85.1` (was `1.82.0`), this is due to deps we rely on requiring a higher version
Games:
- Fixed minecraft java server side EncoderException error on query (by @paul-hansen).
Crate:
- Some minor clippy fixes
- Updated some dependencies
# 0.8.0 24/08/2025
Breaking:
- MSRV is now `1.82.0` (was `1.81.0`), this is due to deps we rely on requiring a higher version
Crate:
- Some minor clippy fixes
- Updated dependencies
# 0.7.0 - 23/04/2025
Breaking:
- MSRV is now `1.81.0` (was `1.71.1`), this is due to deps we rely on requiring a higher version
Games:
- Added `Arma 3` support (by @Perondas).
Crate:
- Some minor clippy fixes
# 0.6.1 - 05/12/2024
Games:
- Added `Starbound` support (by @Novaenia).
Protocols:
- Fixed enum cast error on valve when parsing uppercase envrionment and server type fields (by @Novaenia).
# 0.6.0 - 26/11/2024
Breaking:
- MSRV is now `1.71.1` (was `1.65.0`), this is due to deps we rely on requiring a higher version on linux builds (`1.65.0` is 2+ years old).
Games:
- [Minetest](https://www.minetest.net/) support (available on the `tls`, `serde` and `services` features) (#218 by
@CosminPerRam).
- Fixed the forest game failing when host has the client steam id (#232 by @paul-hansen).
# 0.5.2 - 20/10/2024
Games:
- [Soulmask](https://store.steampowered.com/app/2646460/Soulmask/) support (by @CosminPerRam).
Protocols:
- Fixed Epic (EOS) protocol to match ports on query (by @cainthebest).
Services:
- MineTest Master Server support (available only on the `tls` and `serde` feature) (by @CosminPerRam).
Crate:
- Performance improvements from clippy suggestions (by @CosminPerRam).
- Feature gate some variables so that they are not unused (by @cainthebest).
- Fixed a OOB panic that could occur when reading strings from the buffer (by @cainthebest).
- Updated `pnet_packet` from `0.34.0` to `0.35.0`.
# 0.5.1 - 12/05/2024
Games:
- [Mordhau](https://store.steampowered.com/app/629760/MORDHAU/) support.
- [Enshrouded](https://store.steampowered.com/app/1203620/Enshrouded/) support.
- [Myth of Empires](https://store.steampowered.com/app/1371580/Myth_of_Empires/) support.
- [Pirates, Vikings, and Knights II](https://store.steampowered.com/app/17570/Pirates_Vikings_and_Knights_II/) support.
- [PixARK](https://store.steampowered.com/app/593600/PixARK/) support.
- [Ark: Survival Ascended](https://store.steampowered.com/app/2399830/ARK_Survival_Ascended/) support, note: not yet in
the games definitions.
- [Aliens vs. Predator 2010](https://store.steampowered.com/app/10680/Aliens_vs_Predator/) support.
- [Arma Reforger](https://store.steampowered.com/app/1874880/Arma_Reforger/) support.
- [Nova-Life: Amboise](https://store.steampowered.com/app/885570/NovaLife_Amboise/) support.
- [Abiotic Factor](https://store.steampowered.com/app/427410/Abiotic_Factor/) support.
Protocols:
- Epic (EOS) support, available only on the `tls` feature.
Crate:
- Updated some dependencies: `crc32fast` to `1.4.0`, `clap` to `4.5.4` and `ureq` to `ureq`.
# 0.5.0 - 15/03/2024
### Changes:
Games:
- [Valheim](https://store.steampowered.com/app/892970/Valheim/) support.
- [The Front](https://store.steampowered.com/app/2285150/The_Front/) support.
- [Conan Exiles](https://store.steampowered.com/app/440900/Conan_Exiles/) support.
- [Post Scriptum](https://store.steampowered.com/app/736220/Post_Scriptum/) support.
- [Squad](https://store.steampowered.com/app/393380/Squad/) support.
- [Savage 2](https://savage2.net/) support.
- [Rising World](https://store.steampowered.com/app/324080/Rising_World/) support.
- [ATLAS](https://store.steampowered.com/app/834910/ATLAS/) support.
- [America's Army: Proving Grounds](https://store.steampowered.com/app/203290/Americas_Army_Proving_Grounds/) support.
- [Base Defense](https://store.steampowered.com/app/632730/Base_Defense/) support.
- [Zombie Panic: Source](https://store.steampowered.com/app/17500/Zombie_Panic_Source/) support.
- Added a valve protocol query example.
- Made all of Just Cause 2: Multiplayer Response and Player fields public.
- [Mindustry](https://mindustrygame.github.io/) support.
- [Eco](https://store.steampowered.com/app/382310/Eco/) support.
- [Call Of Duty: Black Ops 3](https://store.steampowered.com/agecheck/app/311210/) support.
- [Counter-Strike 2](https://store.steampowered.com/app/730/CounterStrike_2/) support.
- [Double Action: Boogaloo](https://store.steampowered.com/app/317360/Double_Action_Boogaloo/) support.
Crate:
- Changed the serde feature to only enable serde derive for some types: serde and serde_json is now a dependecy by
default.
Protocols:
- Added the unreal2 protocol and its associated games: Darkest Hour, Devastation, Killing Floor, Red Orchestra, Unreal
Tournament 2003, Unreal Tournament 2004 (by @Douile).
- Added HTTPClient to allow use of HTTP(S) (and JSON) APIs (by @CosminPerRam & @Douile).
Crate:
- Added a `packet_capture` feature to capture the raw packets sent and received by the socket (by @Douile).
- Added packet emulation and socket retrevial using the `packet_capture` feature (by @Douile).
- Added PCAP writing support to the `packet_capture` feature (by @Douile & @cainthebest).
- Refactored socket to use a custom implementation of socket for packet capture when using the `packet_capture`
feature (by @Douile).
CLI:
- Added a CLI (by @cainthebest).
- Added DNS lookup support (by @Douile).
- Added JSON output option (by @Douile).
- Added BSON output in hex or base64 (by @cainthebest).
- Added XML output option (by @cainthebest).
- Added ExtraRequestSettings as CLI arguments (by @Douile).
- Added TimeoutSettings as CLI argument (by @Douile).
- Added Comprehensive end-user documentation for the CLI interface (by @Douile & @cainthebest).
- Tweaked compile-time flags to allow for a more preformant binary (by @cainthebest).
- Added client for socket capture, dev tools are not included by default (by @Douile).
- Added license information to the CLI (by @cainthebest).
- Added source code information to the CLI (by @cainthebest).
### Breaking:
Game:
- Changed identifications of the following games as they weren't properly expecting the naming rules:
-
- Left 4 Dead: `left4dead` -> `l4d`.
-
- 7 Days to Die: `7d2d` in definitions and `sd2d` in game declaration -> `sdtd`.
-
- Quake 3 Arena: `quake3arena` -> `q3a`.
-
- Unreal tournament 2003: `ut2003` -> `unrealtournament2003`
-
- Unreal tournament 2004: `ut2004` -> `unrealtournament2004`
-
- Darkest Hour: Europe '44-'45: `darkesthour` -> `dhe4445`
- Minecraft:
-
- Legacy 1.5 and 1.3 were renamed to 1.4 and beta 1.8 respectively to show the lowest version they support, this
change includes Structs, Enum and game id renames, also removed the "v" from the game definition name.
-
- Moved the Minecraft protocol implementation in the games folder as its proprietary.
Protocols:
- Valve: Removed `SteamApp` due to it not being really useful at all, replaced all instances with `Engine`.
Query:
- Added a connection timeout to TimeoutSettings (at the moment this only applies to TCP)
- Sockets are now expected to apply timeout settings in new()
# 0.4.1 - 13/10/2023
### Changes:
Game:
- Added [Barotrauma](https://store.steampowered.com/app/602960/Barotrauma/) support.
Crate:
- Added `Send` and `Sync` on `Error::source` to fix some async issues.
Protocols:
- Minecraft Java: Add derives to `RequestSettings` and add `new_just_hostname` that creates new settings just by
specifying
the hostname, `protocol_version` defaults to -1.
Games:
- Organised game modules into protocols (when protocol used by other games),
you can now access a game by its name or by its protocol name:
- `use gamedig::games::teamfortress2;`
- `use gamedig::games::valve::teamfortress2;`
Generics:
- Added standard derives to `ProprietaryProtocol`, `CommonResponseJson`, `CommonPlayerJson`, `TimeoutSettings` and
`ExtraRequestSettings`.
### Breaking...
None, yaay!
# 0.4.0 - 07/10/2023
### Changes:
Games:
- [Creativerse](https://store.steampowered.com/app/280790/Creativerse/) support.
Protocols:
- Quake 2: Fixed a bug where the version tag wouldn't always be present.
- The Ship: Removed instances of using `unwrap` without handling the panics.
Crate:
- Updated [byteorder](https://crates.io/crates/byteorder) dependency from 1.4 to 1.5.
- Rich errors, capturing backtrace is done on `RUST_BACKTRACE=1`. (by @Douile)
- Applied some nursery Clippy lints.
- The `retries` field was added to `TimeoutSettings` that specifies the number of times to retry a failed request (
request being individual send, receive sequence, some protocols can include multiple requests in a single query). (by
@Douile)
- By default `retries` is set to `0`, meaning no retries will be attempted
Generics:
- Added `ExtraRequestSettings` containing all possible extra request settings. (by @Douile)
- Added `query_with_timeout_and_extra_settings()` to allow generic queries with extra settings. (by @Douile)
### Breaking...
Crate:
- The enum used for errors, `GDError` has been renamed to `GDErrorKind`.
- `GDError` is now a struct that holds its kind, the source and a backtrace.
- The `Socket::apply_timeout` method now borrows `TimeoutSettings` (`&Option<TimeoutSettings>`)
- To make this easier to work with a new method was added
to `TimeoutSettings`: `TimeoutSettings::get_read_and_write_or_defaults` this takes a borrowed
optional `TimeoutSettings` and returns the contained read and write durations or the default read and write
durations.
Generics:
- Renamed `CommonResponseJson`'s `game` field (and the function) to `game_mode`.
- Changed `players_maximum` and `players_online` (and their functions) types from `u64` to `u32`.
- Changed `score` type (and the function) of player from `u32` to `i32`.
Games:
- Rename some game definitions and implementations to follow a stable ID naming system.
Protocols:
- Valve:
1. Renamed `protocol` to `protocol_version`.
2. Renamed `version` to `game_version`.
3. Renamed `game` to `game_mode`.
4. Fixed `player`'s `score` field being `u32` when it needed to be `i32`, as specified in the protocol.
5. Added the field `check_app_id` to `GatherSettings` which controls if the app id specified to the request and
reported by the server are the same, errors if not, enabled by default. (by @Douile)
6. Valve: Renamed SteamApp enum variants to match new definition names
- GameSpy (1, 2, 3):
1. Renamed `version` to `game_version`.
2. Changed `players_maximum` and `players_online` (and their functions) types from `usize` to `u32`.
- GameSpy 1:
1. Renamed the player's `frags` to `score` and type from `u32` to `i32`.
2. Made `Option` the following response fields `team`, `face`, `skin`, `mesh` and `secret` to fix missing fields
issues. (by @Douile)
- Quake (1, 2):
1. Renamed `game_type` to `game_mode`.
2. Changed `version` type from `String`to `Option<String>`.
- Minecraft Java
1. Renamed `version_protocol` to `protocol_version`.
2. Renamed `version_name` to `game_version`.
3. Renamed `players_sample` to `players`.
4. Added an optional parameter, `RequestSettings`, which contains fields that are used when creating the handshake
packet (this solves some servers not responding to the query). (by @Douile)
5. Legacy versions naming has been changed to represent up to what version they can query, `LegacyBV1_8` (Beta 1.8 to
1.3) -> `LegacyV1_3` and `LegacyV1_4` (1.4 to 1.5) -> `LegacyV1_5` (and their enums accordingly).
- Minecraft Bedrock
1. Renamed `version_protocol` to `protocol_version`.
- Minecraft:
1. Added an optional parameter, `request_settings` parameter to `query`.
- The Ship:
1. Renamed `protocol` to `protocol_version`.
2. Renamed `max_players` to `players_maximum` and changed its type from `u64` to `u32`.
3. Renamed `bots` to `players_bots`. and changed its type from `u64` to `u32`.
4. Renamed `players` to `players_online`.
5. Renamed `players_details` to `players`.
6. Renamed `game` to `game_mode`.
7. Added field `game_version`.
8. Changed `players_bots` type from `Option<u64>` to `Option<u32>`.
9. Changed `score` type of player from `u32` to `i32`.
- Frontlines: Fuel of War:
1. Renamed `game_mode` to `game`.
2. Renamed `version` to `game_version`.
3. Renamed `protocol` to `protocol_version`.
4. Renamed `game` to `game_mode`.
5. Changed `players_maximum` and `players_minimum` types from `usize` to `u32`.
- Just Cause 2: Multiplayer:
1. Renamed `version` to `game_version`.
2. Changed `players_maximum` and `players_minimum` types from `usize` to `u32`.
# 0.3.0 - 18/07/2023
### Changes:
Protocols:
- GameSpy 2 support.
- Quake 2: Added Optional address field to Player.
Generic query:
- Added generic queries (by [@Douile](https://github.com/Douile)) which come with a common struct for the response
fields.
- The supported games list is available programmatically.
Games:
- [Halo: Combat Evolved](https://en.wikipedia.org/wiki/Halo:_Combat_Evolved) support.
- [Just Cause 2: Multiplayer](https://store.steampowered.com/app/259080/Just_Cause_2_Multiplayer_Mod/) support.
- [Warsow](https://warsow.net/) support.
Internal:
- Buffer reader rewrite, resulting in more data checks and better code quality (
thanks [@cainthebest](https://github.com/cainthebest)).
- Better CI to never break accidentally MSRV again (thanks [@Douile](https://github.com/Douile)).
### Breaking...
Protocols:
- Quake 2: Renamed the players `frags` field to `score` to be more inline with the other protocols.
Crate:
- `no_games` and `no_services` have been changed to `games` and `services`, this better represents that they are present
by default (by [@Douile](https://github.com/Douile)).
- Fixed crate's `rust-version`, it is now `1.60.0` (was `1.56.1`)
# 0.2.3 - 02/06/2023
### Changes:
Protocols:
- Valve:
1. Added standard and serde derives to `GatheringSettings`.
- Quake 1, 2 and 3 support.
Games:
- [Quake 2](https://store.steampowered.com/app/2320/Quake_II/) support.
- [Quake 1](https://store.steampowered.com/app/2310/Quake/) support.
- [Quake 3: Arena](https://store.steampowered.com/app/2200/Quake_III_Arena/) support.
- [Hell Let Loose](https://store.steampowered.com/app/686810/Hell_Let_Loose/) support.
- [Soldier of Fortune 2](https://www.gog.com/en/game/soldier_of_fortune_ii_double_helix_gold_edition) support.
### Breaking:
- Every function that used `&str` for the address has been changed to `&IpAddr` (
thanks [@Douile](https://github.com/Douile) for the re-re-write).
- Protocols now use `&SocketAddr` instead of `address: &str, port: u16`.
Services:
- Valve Master Query:
1. Removed Filter and SearchFilters lifetimes and changed `&'a str` to `String` and `&'a [&'a str]` to `Vec<String>`
# 0.2.2 - 01/05/2023
### Changes:
Crate:
- General optimizations thanks to [cargo clippy](https://github.com/rust-lang/rust-clippy)
and [@cainthebest](https://github.com/cainthebest).
- Added feature `serde` which enables json serialization/deserialization for all types (
by [@cainthebest](https://github.com/cainthebest)).
- Documentation improvements.
Protocols:
- GameSpy 1: Add key `admin` as a possible variable for `admin_name`.
- GameSpy 3 support.
Games:
- [Serious Sam](https://www.gog.com/game/serious_sam_the_first_encounter) support.
- [Frontlines: Fuel of War](https://store.steampowered.com/app/9460/Frontlines_Fuel_of_War/) support.
- [Crysis Wars](https://steamcommunity.com/app/17340) support.
Services:
- [Valve Master Server Query](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol) support.
- Added feature `no_services` which disables the supported services.
### Breaking:
Protocols:
- Valve: Request type enums have been renamed from all caps to starting-only uppercase, ex: `INFO` to `Info`
- GameSpy 1: `players_minimum` is now an `Option<u8>` instead of an `u8`
- GameSpy 1: Is now under `protocols::gamespy::one` instead of `protocols::gamespy`
# 0.2.1 - 03/03/2023
### Changes:
Crate:
- Added feature `no_games` which disables the supported games (useful when only the
protocols/services are needed, also saves storage space).
Games:
- [V Rising](https://store.steampowered.com/app/1604030/V_Rising/) support.
- [Unreal Tournament](https://en.wikipedia.org/wiki/Unreal_Tournament) support.
- [Battlefield 1942](https://www.ea.com/games/battlefield/battlefield-1942) support.
Protocols:
- Valve:
1. Reversed (from `0.1.0`) "Players with no name are no more added to the `players_details` field.", also added a note
in the [protocols](PROTOCOLS.md) file regarding this.
2. Fixed querying while multiple challenge responses might happen.
- GameSpy 1 support.
### Breaking:
None.
# 0.2.0 - 18/02/2023
### Changes:
Games:
- [Don't Starve Together](https://store.steampowered.com/app/322330/Dont_Starve_Together/) support.
- [Colony Survival](https://store.steampowered.com/app/366090/Colony_Survival/) support.
- [Onset](https://store.steampowered.com/app/1105810/Onset/) support.
- [Codename CURE](https://store.steampowered.com/app/355180/Codename_CURE/) support.
- [Ballistic Overkill](https://store.steampowered.com/app/296300/Ballistic_Overkill/) support.
- [BrainBread 2](https://store.steampowered.com/app/346330/BrainBread_2/) support.
- [Avorion](https://store.steampowered.com/app/445220/Avorion/) support.
- [Operation: Harsh Doorstop](https://store.steampowered.com/app/736590/Operation_Harsh_Doorstop/) support.
Protocols:
- Valve:
1. `appid` is now a field in the `Response` struct.
### Breaking:
Protocols:
- Valve:
due to some games being able to host a server from within the game AND from a dedicated server,
if you were to query one of them, the query would fail for the other one, as the `SteamID` enum
for that game could specify only one id.
1. `SteamID` is now `SteamApp`, was an u32 enum, and now it's a simple enum.
2. `App` is now `Engine`, the `Source` enum's structure has been changed from `Option<u32>` to
`Option<u32, Option<u32>>`, where the first parameter is the game app id and the second is
the dedicated server app id (if there is one).
# 0.1.0 - 17/01/2023
### Changes:
Games:
- [Risk of Rain 2](https://store.steampowered.com/app/632360/Risk_of_Rain_2/) support.
- [Battalion 1944](https://store.steampowered.com/app/489940/BATTALION_Legacy/) support.
- [Black Mesa](https://store.steampowered.com/app/362890/Black_Mesa/) support.
- [Project Zomboid](https://store.steampowered.com/app/108600/Project_Zomboid/) support.
- [Age of Chivalry](https://store.steampowered.com/app/17510/Age_of_Chivalry/) support.
Protocols:
- Valve: Players with no name are no more added to the `players_details` field.
- Valve: Split packets are now appending in the correct order.
Crate:
- `MSRV` is now `1.56.1` (was `1.58.1`)
### Breaking:
Protocols:
- Valve: The rules field is now a `HashMap<String, String>` instead of a `Vec<ServerRule>` (where the `ServerRule`
structure had a name and a value fields).
- Valve: Structs that contained the `players`, `max_players` and `bots` fields have been renamed
to `players_online`, `players_maximum` and `players_bots` respectively.
- Minecraft: Structs that contained the `online_players`, `max_players` and `sample_players` fields have been renamed
to `players_online`, `players_maximum` and `players_sample` respectively.
- Minecraft: The Java query response struct named `Response` has been renamed to `JavaResponse`.
Errors:
- Besides the `BadGame` error, now no other errors returns details about what happened (as it was quite pointless).
Crate:
- `package.metadata.msrv` has been replaced with `package.rust-version`
# 0.0.7 - 03/01/2023
### Changes:
[Minecraft](https://www.minecraft.com) bedrock edition support.
Fix Minecraft legacy v1.6 max/online players count being reversed.
Added `query_legacy_specific` method to the Minecraft protocol.
### Breaking:
Removed `query_specific` from the mc protocol in favor of `query_java`, `query_legacy` and `query_legacy_specific`.
Some public functions that are meant to be used only internally were made private.
# 0.0.6 - 28/11/2022
[Minecraft](https://www.minecraft.com) support (bedrock not supported yet).
[7 Days To Die](https://store.steampowered.com/app/251570/7_Days_to_Die/) support.
[ARK: Survival Evolved](https://store.steampowered.com/app/346110/ARK_Survival_Evolved/) support.
[Unturned](https://store.steampowered.com/app/304930/Unturned/) support.
[The Forest](https://store.steampowered.com/app/242760/The_Forest/) support.
[Team Fortress Classic](https://store.steampowered.com/app/20/Team_Fortress_Classic/) support.
[Sven Co-op](https://store.steampowered.com/app/225840/Sven_Coop/) support.
[Rust](https://store.steampowered.com/app/252490/Rust/) support.
[Counter-Strike](https://store.steampowered.com/app/10/CounterStrike/) support.
[Arma 2: Operation Arrowhead](https://store.steampowered.com/app/33930/Arma_2_Operation_Arrowhead/) support.
[Day of Infamy](https://store.steampowered.com/app/447820/Day_of_Infamy/) support.
[Half-Life Deathmatch: Source](https://store.steampowered.com/app/360/HalfLife_Deathmatch_Source/) support.
Successfully tested `Alien Swarm` and `Insurgency: Modern Infantry Combat`.
Restored rules response for `Counter-Strike: Global Offensive` (note: for a full player list response, the
cvar `host_players_show` must be set to `2`).
Increased Valve Protocol `PACKET_SIZE` from 1400 to 6144 (because some games send larger packets than the specified
protocol size).
Removed DNS resolving as it was not needed.
Valve Protocol minor optimizations.
# 0.0.5 - 15/11/2022
Added `SocketBind` error, regarding failing to bind a socket.
Socket custom timeout capability (with an error if provided durations are zero).
Because of this, a parameter similar to GatherSettings has been added on the Valve Protocol Query.
Support for GoldSrc split packets and obsolete A2S_INFO response.
Changed the Valve Protocol app parameter to represent the engine responses.
It is now an enum of:
- `Source(Option<u32>)` - A Source response with optionally, the id (if the id is present and the response id is not the
same, the query fails), if it isn't provided, find it.
- `GoldSrc(bool)` - A GoldSrc response with the option to enforce the obsolete A2S_INFO response.
Fixed Source multi-packet response crash due to when a certain app with a certain protocol doesn't have the Size
field.
Reduced Valve Protocol `PACKET_SIZE` to be as specified from 2048 to 1400.
[Counter-Strike: Condition Zero](https://store.steampowered.com/app/80/CounterStrike_Condition_Zero/) implementation.
[Day of Defeat](https://store.steampowered.com/app/30/Day_of_Defeat/) implementation.
Games besides CSGO and TS now have the same response structure.
# 0.0.4 - 23/10/2022
Queries now support DNS resolve.
Changed uses a bit, example: from `use gamedig::valve::ValveProtocol::query`
to `use gamedig::protocols::valve::query`.
Changed Valve Protocol Query parameters to (ip, port, app, gather_settings), changes include:
- the app is now optional, being None means to anonymously query the server.
- gather_settings is now also an optional, being None means all query settings.
Valve Protocol now supports querying anonymous apps (see previous lines).
Better bad game error.
[Alien Swarm](https://store.steampowered.com/app/630/Alien_Swarm/) implementation (not tested).
[Alien Swarm: Reactive Drop](https://store.steampowered.com/app/563560/Alien_Swarm_Reactive_Drop/) implementation.
[Insurgency](https://store.steampowered.com/app/222880/Insurgency/) implementation.
[Insurgency: Sandstorm](https://store.steampowered.com/app/581320/Insurgency_Sandstorm/) implementation.
[Insurgency: Modern Infantry Combat](https://store.steampowered.com/app/17700/INSURGENCY_Modern_Infantry_Combat/)
implementation (not tested).
# 0.0.3 - 22/10/2022
Valve protocol now properly supports multi-packet responses (compressed ones not tested).
CSGO, TF2 and TS now have independent Responses, if you want a generic one, query the protocol.
[Counter Strike: Source](https://store.steampowered.com/app/240/CounterStrike_Source/) implementation (if protocol is 7,
queries with multi-packet responses will crash).
[Day of Defeat: Source](https://store.steampowered.com/app/300/Day_of_Defeat_Source/) implementation.
[Garry's Mod](https://store.steampowered.com/app/4000/Garrys_Mod/) implementation.
[Half-Life 2 Deathmatch](https://store.steampowered.com/app/320/HalfLife_2_Deathmatch/) implementation.
[Left 4 Dead](https://store.steampowered.com/app/500/Left_4_Dead/) implementation.
[Left 4 Dead 2](https://store.steampowered.com/app/550/Left_4_Dead_2/) implementation.
# 0.0.2 - 20/10/2022
Further implementation of the Valve protocol (PLAYERS and RULES queries).
[Counter Strike: Global Offensive](https://store.steampowered.com/app/730/CounterStrike_Global_Offensive/)
implementation.
[The Ship](https://developer.valvesoftware.com/wiki/The_Ship) implementation.
The library now has error handling.
# 0.0.1 - 16/10/2022
The first usable version of the crate, yay!
It brings:
Initial implementation of the [Valve server query protocol](https://developer.valvesoftware.com/wiki/Server_queries).
Initial [Team Fortress 2](https://en.wikipedia.org/wiki/Team_Fortress_2) support.
# 0.0.0 - 15/10/2022
The first *markdown*, the crate is unusable as it doesn't contain anything helpful.

View file

@ -1,78 +0,0 @@
[package]
name = "gamedig"
version = "0.9.0"
edition = "2021"
authors = [
"rust-GameDig contributors [https://github.com/gamedig/rust-gamedig/contributors]",
"node-GameDig contributors [https://github.com/gamedig/node-gamedig/contributors]",
]
license = "MIT"
description = "Query game servers and not only."
homepage = "https://gamedig.github.io/"
documentation = "https://docs.rs/gamedig/latest/gamedig/"
repository = "https://github.com/gamedig/rust-gamedig"
readme = "README.md"
keywords = ["server", "query", "game", "check", "status"]
rust-version = "1.85.1"
categories = ["parser-implementations", "parsing", "network-programming", "encoding"]
[features]
default = ["games", "services", "game_defs"]
# Enable query functions for specific games
games = []
# Enable game definitions for use with the generic query functions
game_defs = ["dep:phf", "games"]
# Enable service querying
services = []
# Enable serde derivations for our types
serde = []
# Enable clap derivations for our types
clap = ["dep:clap"]
packet_capture = ["dep:pcap-file", "dep:pnet_packet", "dep:lazy_static"]
# Enable TLS for HTTP Client
tls = ["ureq/tls"]
[dependencies]
url = "2.5.8"
byteorder = "1.5.0"
bzip2-rs = "0.1.2"
crc32fast = "1.5.0"
base64 = "0.22.1"
encoding_rs = "0.8.35"
serde_json = { version = "1.0.149" }
serde = { version = "1.0.228", features = ["derive"] }
ureq = { version = "2.12.1", default-features = false, features = ["gzip", "json"] }
phf = { version = "0.13.1", optional = true, features = ["macros"] }
clap = { version = "4.5.60", optional = true, features = ["derive"] }
pcap-file = { version = "2.0.0", optional = true }
pnet_packet = { version = "0.35.0", optional = true }
lazy_static = { version = "1.5.0", optional = true }
[dev-dependencies]
gamedig-id-tests = { path = "../id-tests", default-features = false }
# Examples
[[example]]
name = "minecraft"
required-features = ["games"]
[[example]]
name = "teamfortress2"
required-features = ["games"]
[[example]]
name = "valve_master_server_query"
required-features = ["services"]
[[example]]
name = "test_eco"
required-features = ["games"]
[[example]]
name = "generic"
required-features = ["games", "game_defs"]

View file

@ -1,114 +0,0 @@
<h1 align="center">rust-GameDig</h1>
<h5 align="center">The fast library for querying game servers/services.</h5>
<div align="center">
<a href="https://github.com/gamedig/rust-gamedig/actions">
<img src="https://github.com/gamedig/rust-gamedig/actions/workflows/ci.yml/badge.svg" alt="CI">
</a>
<a href="https://crates.io/crates/gamedig">
<img src="https://img.shields.io/crates/v/gamedig.svg?color=yellow" alt="Latest Version">
</a>
<a href="https://crates.io/crates/gamedig">
<img src="https://img.shields.io/crates/d/gamedig?color=purple" alt="Crates.io">
</a>
<a href="https://github.com/gamedig/node-gamedig">
<img src="https://raw.githubusercontent.com/gamedig/rust-gamedig/main/.github/badges/node.svg" alt="Node-GameDig Game Coverage">
</a>
</div>
<h5 align="center">
This library brings what
<a href="https://github.com/gamedig/node-gamedig">
node-GameDig
</a>
does (and not only), to pure Rust!
</h5>
**Warning**: This project goes through frequent API breaking changes and hasn't been thoroughly tested.
## Community
Checkout the GameDig Community Discord Server [here](https://discord.gg/NVCMn3tnxH).
Note that it isn't be a replacement for GitHub issues, if you have found a problem
within the library or want to request a feature, it's better to do so here rather than
on Discord.
## Usage
Minimum Supported Rust Version is `1.85.1` and the code is cross-platform.
Pick a game/service/protocol (check
the [GAMES](https://github.com/gamedig/rust-gamedig/blob/main/GAMES.md), [SERVICES](https://github.com/gamedig/rust-gamedig/blob/main/SERVICES.md)
and [PROTOCOLS](https://github.com/gamedig/rust-gamedig/blob/main/PROTOCOLS.md) files to see the currently supported
ones), provide the ip and the port (be aware that some game servers use a separate port for the info queries, the port
can also be optional if the server is running the default ports) then query on it.
[Team Fortress 2](https://store.steampowered.com/app/440/Team_Fortress_2/) query example:
```rust
use gamedig::games::teamfortress2;
fn main() {
let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None);
// None is the default port (which is 27015), could also be Some(27015)
match response { // Result type, must check what it is...
Err(error) => println!("Couldn't query, error: {}", error),
Ok(r) => println!("{:#?}", r)
}
}
```
Response (note that some games have a different structure):
```json5
{
protocol: 17,
name: "Team Fortress 2 Dedicated Server.",
map: "ctf_turbine",
game: "tf2",
appid: 440,
players_online: 0,
players_details: [],
players_maximum: 69,
players_bots: 0,
server_type: Dedicated,
has_password: false,
vac_secured: true,
version: "7638371",
port: Some(27015),
steam_id: Some(69753253289735296),
tv_port: None,
tv_name: None,
keywords: Some(
"alltalk,nocrits"
),
rules: [
"mp_autoteambalance"
:
"1",
"mp_maxrounds"
:
"5",
//....
]
}
```
Want to see more examples? Checkout
the [examples](https://github.com/gamedig/rust-gamedig/tree/main/crates/lib/examples) folder.
## Documentation
The documentation is available at [docs.rs](https://docs.rs/gamedig/latest/gamedig/).
Curious about the history and what changed between versions? Everything is in
the [CHANGELOG](https://github.com/gamedig/rust-gamedig/blob/main/crates/lib/CHANGELOG.md) file.
## Contributing
If you want to see your favorite game/service being supported here, open an issue, and I'll prioritize it (or do a pull
request if you want to implement it yourself)!
Before contributing please read [CONTRIBUTING](https://github.com/gamedig/rust-gamedig/blob/main/CONTRIBUTING.md).

View file

@ -1,129 +0,0 @@
use gamedig::{
protocols::types::CommonResponse,
query_with_timeout_and_extra_settings,
ExtraRequestSettings,
GDResult,
Game,
TimeoutSettings,
GAMES,
};
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
/// Make a query given the name of a game
/// The `game` argument is taken from the [GAMES](gamedig::GAMES) map.
fn generic_query(
game: &Game,
addr: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
extra_settings: Option<ExtraRequestSettings>,
) -> GDResult<Box<dyn CommonResponse>> {
println!("Querying {:#?} with game {:#?}.", addr, game);
let response = query_with_timeout_and_extra_settings(game, addr, port, timeout_settings, extra_settings)?;
println!("Response: {:#?}", response.as_json());
let common = response.as_original();
println!("Common response: {:#?}", common);
Ok(response)
}
fn main() {
let mut args = std::env::args().skip(1);
// Handle arguments
if let Some(game_name) = args.next() {
let hostname = args.next().expect("Must provide an address");
// Use to_socket_addrs to resolve hostname to IP
let addr: SocketAddr = format!("{}:0", hostname)
.to_socket_addrs()
.unwrap()
.next()
.expect("Could not lookup host");
let port: Option<u16> = args.next().map(|s| s.parse().unwrap());
let timeout_settings = TimeoutSettings::new(
TimeoutSettings::default().get_read(),
TimeoutSettings::default().get_write(),
TimeoutSettings::default().get_connect(),
2,
)
.unwrap();
let game = GAMES
.get(&game_name)
.expect("Game doesn't exist, run without arguments to see a list of games");
let extra_settings = game
.request_settings
.clone()
.set_hostname(hostname.to_string())
.set_check_app_id(false);
generic_query(
game,
&addr.ip(),
port,
Some(timeout_settings),
Some(extra_settings),
)
.unwrap();
} else {
// Without arguments print a list of games
for (name, game) in GAMES.entries() {
println!("{}\t{}", name, game.name);
}
}
}
#[cfg(test)]
mod test {
use gamedig::{protocols::types::TimeoutSettings, GAMES};
use std::{
net::{IpAddr, Ipv4Addr},
time::Duration,
};
use super::generic_query;
const ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST);
fn test_game(game_name: &str) {
let timeout_settings = Some(
TimeoutSettings::new(
Some(Duration::from_nanos(1)),
Some(Duration::from_nanos(1)),
Some(Duration::from_nanos(1)),
0,
)
.unwrap(),
);
let game = GAMES
.get(game_name)
.expect("Game doesn't exist, run without arguments to see a list of games");
assert!(generic_query(game, &ADDR, None, timeout_settings, None).is_err());
}
#[test]
fn battlefield1942() { test_game("battlefield1942"); }
#[test]
fn minecraft() { test_game("minecraft"); }
#[test]
fn teamfortress2() { test_game("teamfortress2"); }
#[test]
fn quake2() { test_game("quake2"); }
#[test]
fn all_games() {
for game_name in GAMES.keys() {
test_game(game_name);
}
}
}

View file

@ -1,31 +0,0 @@
use gamedig::minecraft;
use gamedig::minecraft::types::RequestSettings;
fn main() {
// or Some(<port>), None is the default protocol port (which is 25565 for java
// and 19132 for bedrock)
let response = minecraft::query(&"127.0.0.1".parse().unwrap(), None);
// This will fail if no server is available locally!
match response {
Err(error) => println!("Couldn't query, error: {}", error),
Ok(r) => println!("{:#?}", r),
}
// This is an example to query a server with a hostname to be specified in the
// packet. Passing -1 on the protocol_version means anything, note that
// an invalid value here might result in server not responding.
let response = minecraft::query_java(
&"209.222.114.62".parse().unwrap(),
Some(25565),
Some(RequestSettings {
hostname: "mc.hypixel.net".to_string(),
protocol_version: -1,
}),
);
match response {
Err(error) => println!("Couldn't query, error: {}", error),
Ok(r) => println!("{:#?}", r),
}
}

View file

@ -1,12 +0,0 @@
use gamedig::games::teamfortress2;
fn main() {
let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None);
// or Some(27015), None is the default protocol port (which is 27015)
match response {
// Result type, must check what it is...
Err(error) => println!("Couldn't query, error: {}", error),
Ok(r) => println!("{:#?}", r),
}
}

View file

@ -1,10 +0,0 @@
use gamedig::games::eco;
use std::net::IpAddr;
use std::str::FromStr;
fn main() {
let ip = IpAddr::from_str("142.132.154.69").unwrap();
let port = 31111;
let r = eco::query(&ip, Some(port));
println!("{:#?}", r);
}

View file

@ -1,14 +0,0 @@
use gamedig::valve_master_server::{query, Filter, Region, SearchFilters};
fn main() {
let search_filters = SearchFilters::new()
.insert(Filter::RunsAppID(440))
.insert(Filter::CanBeEmpty(false))
.insert(Filter::CanBeFull(false))
.insert(Filter::CanHavePassword(false))
.insert(Filter::IsSecured(true))
.insert(Filter::HasTags(vec!["minecraft".to_string()]));
let ips = query(Region::Europe, Some(search_filters)).unwrap();
println!("Servers: {:?} \n Amount: {}", ips, ips.len());
}

View file

@ -1,36 +0,0 @@
use gamedig::protocols::types::GatherToggle;
use gamedig::protocols::valve;
use gamedig::protocols::valve::{Engine, GatheringSettings};
use gamedig::TimeoutSettings;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;
fn main() {
let address = &SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 27015);
let engine = Engine::Source(None); // We don't specify a steam app id, let the query try to find it.
let gather_settings = GatheringSettings {
players: GatherToggle::Enforce, // We want to query for players
rules: GatherToggle::Skip, // We don't want to query for rules
check_app_id: false, // Loosen up the query a bit by not checking app id
};
let read_timeout = Duration::from_secs(2);
let write_timeout = Duration::from_secs(3);
let connect_timeout = Duration::from_secs(4);
let retries = 1; // does another request if the first one fails.
let timeout_settings = TimeoutSettings::new(
Some(read_timeout),
Some(write_timeout),
Some(connect_timeout),
retries,
)
.unwrap();
let response = valve::query(
address,
engine,
Some(gather_settings),
Some(timeout_settings),
);
println!("{response:#?}");
}

View file

@ -1,612 +0,0 @@
use crate::GDErrorKind::PacketBad;
use crate::GDErrorKind::PacketUnderflow;
use crate::GDResult;
use byteorder::{BigEndian, ByteOrder, LittleEndian};
use std::{convert::TryInto, marker::PhantomData};
/// A struct representing a buffer with a specific byte order.
///
/// It's comprised of a byte slice that it reads from, a cursor to keep track of
/// the current position within the byte slice, and a `PhantomData` marker to
/// bind it to a specific byte order (BigEndian or LittleEndian).
///
/// The byte order is defined by the `B: ByteOrder` generic parameter.
pub struct Buffer<'a, B: ByteOrder> {
/// The byte slice that the buffer reads from.
data: &'a [u8],
/// The cursor marking our current position in the buffer.
cursor: usize,
/// A phantom field used to bind the `Buffer` to a specific `ByteOrder`.
_marker: PhantomData<B>,
}
impl<'a, B: ByteOrder> Buffer<'a, B> {
/// Creates and returns a new `Buffer` with the given data.
///
/// The cursor is set to the start of the buffer (position 0) upon
/// initialization.
///
/// # Arguments
///
/// * `data` - A byte slice that the buffer will read from.
pub const fn new(data: &'a [u8]) -> Self {
Self {
data,
cursor: 0,
_marker: PhantomData,
}
}
pub const fn current_position(&self) -> usize { self.cursor }
/// Returns the length of the remaining bytes from the current cursor
/// position.
pub const fn remaining_length(&self) -> usize { self.data.len() - self.cursor }
/// Returns the length of the buffer data.
pub const fn data_length(&self) -> usize { self.data.len() }
// TODO: Look into this to make it take ownership of data, not borrowing it
// There are many instances where we transform this to a vector.
/// Returns the remaining bytes that have not been read.
pub fn remaining_bytes(&self) -> &[u8] { &self.data[self.cursor ..] }
/// Moves the cursor forward or backward by a specified offset.
///
/// # Arguments
///
/// * `offset` - The amount to move the cursor. Use a negative value to move
/// backwards.
///
/// # Errors
///
/// Returns a `BufferError` if the attempted move would position the cursor
/// out of bounds.
pub fn move_cursor(&mut self, offset: isize) -> GDResult<()> {
// Compute the new cursor position by adding the offset to the current cursor
// position. The checked_add method is used for safe addition,
// preventing overflow and underflow.
let new_cursor = (self.cursor as isize).checked_add(offset);
match new_cursor {
// If the addition was not successful (i.e., it resulted in an overflow or underflow),
// return an error indicating that the cursor is out of bounds.
None => Err(PacketBad.into()),
// If the new cursor position is either less than zero (i.e., before the start of the buffer)
// or greater than the remaining length of the buffer (i.e., past the end of the buffer),
// return an error indicating that the cursor is out of bounds.
Some(x) if x < 0 || x as usize > self.data_length() => Err(PacketBad.into()),
// If the new cursor position is within the bounds of the buffer, update the cursor
// position and return Ok.
Some(x) => {
self.cursor = x as usize;
Ok(())
}
}
}
/// Reads a value of type `T` from the buffer, and advances the cursor by
/// the size of `T`.
///
/// # Type Parameters
///
/// * `T` - The type of value to be read from the buffer. This type must
/// implement the `BufferRead` trait with the same byte order as the
/// buffer.
///
/// # Errors
///
/// Returns a `BufferError` if there is not enough data remaining in the
/// buffer to read a value of type `T`.
pub fn read<T: Sized + BufferRead<B>>(&mut self) -> GDResult<T> {
// Get the size of `T` in bytes.
let size = std::mem::size_of::<T>();
// Calculate remaining length of the buffer.
let remaining = self.remaining_length();
// If the size of `T` is larger than the remaining length, return an error
// because we don't have enough data left to read.
if size > remaining {
return Err(PacketUnderflow.context(format!(
"Size requested {size} was larger than remaining bytes {remaining}"
)));
}
// Slice the data array from the current cursor position for `size` amount of
// bytes.
let bytes = &self.data[self.cursor .. self.cursor + size];
// Move the cursor forward by `size`.
self.cursor += size;
// Use the `read_from_buffer` function of the `BufferRead` implementation for
// `T` to convert the bytes into an instance of `T`.
T::read_from_buffer(bytes)
}
/// Reads a string from the buffer using a specified `StringDecoder`, until
/// an optional delimiter.
///
/// # Type Parameters
///
/// * `D` - The type of string decoder to use. This type must implement the
/// `StringDecoder` trait with the same byte order as the buffer.
///
/// # Arguments
///
/// * `until` - An optional delimiter. If provided, the method will read
/// until this delimiter is encountered. If not provided, the method will
/// read until the default delimiter of the decoder.
///
/// # Errors
///
/// Returns a `BufferError` if there is an error decoding the string.
pub fn read_string<D: StringDecoder>(&mut self, until: Option<D::Delimiter>) -> GDResult<String> {
// Check if the cursor is out of bounds.
if self.cursor > self.data_length() {
return Err(PacketUnderflow.context(format!(
"Cursor position {} is out of bounds when reading string. Buffer length: {}",
self.cursor,
self.data_length()
)));
}
// Slice the data array from the current cursor position to the end.
let data_slice = &self.data[self.cursor ..];
// Use the provided delimiter if one was given, or default to the
// delimiter specified by the StringDecoder.
let delimiter = until.unwrap_or(D::DELIMITER);
// Invoke the decode_string function of the provided StringDecoder,
// passing in the remaining data slice, the mutable reference to the
// cursor, and the delimiter.
let result = D::decode_string(data_slice, &mut self.cursor, delimiter)?;
// If decoding was successful, return the decoded string. The cursor
// position has been updated within the decode_string call to reflect
// the new position after reading.
Ok(result)
}
}
/// A trait that provides an interface to switch endianness.
///
/// The trait `SwitchEndian` is used for types that have a specific
/// byte order (endianness) and can switch to another byte order.
/// The type of the switched endianness is determined by the associated
/// type `Output`.
///
/// The associated type `Output` must implement the `ByteOrder` trait.
pub trait SwitchEndian {
type Output: ByteOrder;
}
/// An implementation of `SwitchEndian` for `LittleEndian`.
///
/// The switched endianness type is `BigEndian`.
impl SwitchEndian for LittleEndian {
type Output = BigEndian;
}
/// An implementation of `SwitchEndian` for `BigEndian`.
///
/// The switched endianness type is `LittleEndian`.
impl SwitchEndian for BigEndian {
type Output = LittleEndian;
}
impl<'a, B: SwitchEndian + ByteOrder> Buffer<'a, B> {
/// Switches the byte order of a chunk in the buffer.
///
/// This method consumes the buffer and returns a new buffer
/// with a chunk of the original buffer's data, starting from the
/// original cursor position and of the given size, where the byte
/// order is switched according to the implementation
/// of `SwitchEndian` for `B`.
///
/// Note: The method also advances the cursor of the original buffer
/// by `size`.
///
/// # Parameters
///
/// * `size`: The size of the chunk to be taken from the original buffer.
pub fn switch_endian_chunk(&mut self, size: usize) -> GDResult<Buffer<'a, B::Output>> {
let old_cursor = self.cursor;
self.move_cursor(size as isize)?;
Ok(Buffer {
data: &self.data[old_cursor .. old_cursor + size],
cursor: 0,
_marker: PhantomData,
})
}
}
/// A trait defining a protocol for reading values of a certain type from a
/// buffer.
///
/// Implementors of this trait provide a method for reading their type from a
/// byte buffer with a specific byte order.
pub trait BufferRead<B: ByteOrder>: Sized {
fn read_from_buffer(data: &[u8]) -> GDResult<Self>;
}
/// Macro to implement the `BufferRead` trait for byte types.
///
/// This macro generates an implementation of the `BufferRead` trait for a
/// specified byte type. The implementation will read a single byte from the
/// buffer and convert it to the target type using the provided map function.
///
/// # Arguments
///
/// * `$type` - The target type to implement `BufferRead` for.
/// * `$map_func` - The function to map a byte to the target type.
macro_rules! impl_buffer_read_byte {
($type:ty, $map_func:expr) => {
impl<B: ByteOrder> BufferRead<B> for $type {
fn read_from_buffer(data: &[u8]) -> GDResult<Self> {
// Use the `first` method to get the first byte from the data array.
data.first()
// Apply the $map_func function to convert the raw byte to the $type.
.map($map_func)
// If the data array is empty (and thus `first` returns None),
// `ok_or_else` will return a BufferError.
.ok_or_else(|| PacketBad.into())
}
}
};
}
/// Macro to implement the `BufferRead` trait for multi-byte types.
///
/// This macro generates an implementation of the `BufferRead` trait for a
/// specified multi-byte type. The implementation will read the appropriate
/// number of bytes from the buffer and convert them to the target type using
/// the provided read function.
///
/// # Arguments
///
/// * `$type` - The target type to implement `BufferRead` for.
/// * `$read_func` - The function to read the bytes into the target type.
macro_rules! impl_buffer_read {
($type:ty, $read_func:ident) => {
impl<B: ByteOrder> BufferRead<B> for $type {
fn read_from_buffer(data: &[u8]) -> GDResult<Self> {
// Convert the byte slice into an array of the appropriate type.
let array = data.try_into().map_err(|e| {
// If conversion fails, return an error indicating the required and provided
// lengths.
PacketBad.context(e)
})?;
// Use the provided function to read the data from the array into the given
// type.
Ok(B::$read_func(array))
}
}
};
}
impl_buffer_read_byte!(u8, |&b| b);
impl_buffer_read_byte!(i8, |&b| b as i8);
impl_buffer_read!(u16, read_u16);
impl_buffer_read!(i16, read_i16);
impl_buffer_read!(u32, read_u32);
impl_buffer_read!(i32, read_i32);
impl_buffer_read!(u64, read_u64);
impl_buffer_read!(i64, read_i64);
impl_buffer_read!(f32, read_f32);
impl_buffer_read!(f64, read_f64);
/// A trait defining a protocol for decoding strings from a buffer.
///
/// This trait should be implemented by types that can decode strings from a
/// byte buffer with a specific byte order and delimiter.
pub trait StringDecoder {
/// The type of the delimiter used by the decoder.
type Delimiter: AsRef<[u8]>;
/// The default delimiter used by the decoder.
const DELIMITER: Self::Delimiter;
/// Decodes a string from the provided byte slice, and updates the cursor
/// position accordingly.
///
/// # Arguments
///
/// * `data` - The byte slice to decode the string from.
/// * `cursor` - The current position in the byte slice.
/// * `delimiter` - The delimiter to use for decoding the string.
///
/// # Errors
///
/// Returns a `BufferError` if there is an error decoding the string.
fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult<String>;
}
/// A decoder for UTF-8 encoded strings.
///
/// This decoder uses a single null byte (`0x00`) as the default delimiter.
pub struct Utf8Decoder;
impl StringDecoder for Utf8Decoder {
type Delimiter = [u8; 1];
const DELIMITER: Self::Delimiter = [0x00];
/// Decodes a UTF-8 string from the given data, updating the cursor position
/// accordingly.
fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult<String> {
// Find the position of the delimiter in the data. If the delimiter is not
// found, the length of the data is returned.
let position = data
// Create an iterator over the data.
.iter()
// Find the position of the delimiter
.position(|&b| b == delimiter.as_ref()[0])
// If the delimiter is not found, use the whole data slice.
.unwrap_or(data.len());
// Convert the data until the found position into a UTF-8 string.
let result = std::str::from_utf8(
// Take a slice of data until the position.
&data[.. position]
)
// If the data cannot be converted into a UTF-8 string, return an error
.map_err(|e| PacketBad.context(e))?
// Convert the resulting &str into a String
.to_owned();
// Update the cursor position
// The +1 is to skip the delimiter
*cursor += position + 1;
Ok(result)
}
}
/// A decoder for UTF-8 encoded strings prefixed by a single byte denoting the
/// string's length.
///
/// This decoder uses a single null byte (`0x00`) as the default delimiter.
pub struct Utf8LengthPrefixedDecoder;
impl StringDecoder for Utf8LengthPrefixedDecoder {
type Delimiter = [u8; 1];
const DELIMITER: Self::Delimiter = [0x00];
/// Decodes a UTF-8 string from the given data, updating the cursor position
/// accordingly.
fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult<String> {
// Find the maximum length of the string
let length = *data
.first()
.ok_or_else(|| PacketBad.context("Length of string not found"))?;
// Find the position of the delimiter in the data. If the delimiter is not
// found, the length is returned.
let position = data
// Create an iterator over the data.
.iter()
.skip(1)
.take(length as usize)
// Find the position of the delimiter
.position(|&b| b == delimiter.as_ref()[0])
// If the delimiter is not found, use the whole data slice.
.unwrap_or(length as usize);
// Convert the data until the found position into a UTF-8 string.
let result = std::str::from_utf8(
// Take a slice of data until the position.
&data[1 .. position + 1]
)
// If the data cannot be converted into a UTF-8 string, return an error
.map_err(|e| PacketBad.context(e))?
// Convert the resulting &str into a String
.to_owned();
// Update the cursor position
// The +1 is to skip t length
*cursor += position + 1;
Ok(result)
}
}
/// A decoder for UTF-16 encoded strings.
///
/// This decoder uses a pair of null bytes (`0x00, 0x00`) as the default
/// delimiter.
///
/// # Type Parameters
///
/// * `B` - The byte order to use when decoding the string.
pub struct Utf16Decoder<B: ByteOrder> {
_marker: PhantomData<B>,
}
impl<B: ByteOrder> StringDecoder for Utf16Decoder<B> {
type Delimiter = [u8; 2];
const DELIMITER: Self::Delimiter = [0x00, 0x00];
/// Decodes a UTF-16 string from the given data, updating the cursor
/// position accordingly.
fn decode_string(data: &[u8], cursor: &mut usize, delimiter: Self::Delimiter) -> GDResult<String> {
// Try to find the position of the delimiter in the data
let position = data
// Split the data into 2-byte chunks (as UTF-16 uses 2 bytes per character)
.chunks_exact(2)
// Find the position of the delimiter
.position(|chunk| chunk == delimiter.as_ref())
// If the delimiter is not found, use the whole data, otherwise use the position of the delimiter
.map_or(data.len(), |pos| pos * 2);
// Create a buffer of u16 values to hold the decoded characters
let mut paired_buf: Vec<u16> = vec![0; position / 2];
// Decode the data into the buffer
B::read_u16_into(&data[.. position], &mut paired_buf);
// Convert the buffer of u16 values into a String
let result = String::from_utf16(&paired_buf).map_err(|e| PacketBad.context(e))?;
// Update the cursor position
// The +2 accounts for the delimiter
*cursor += position + 2;
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use byteorder::BigEndian;
#[test]
fn test_new_buffer() {
let data: &[u8] = &[1, 2, 3, 4];
let buffer = Buffer::<LittleEndian>::new(data);
assert_eq!(buffer.data, data);
assert_eq!(buffer.cursor, 0);
}
#[test]
fn test_remaining_length() {
let data: &[u8] = &[1, 2, 3, 4];
let mut buffer = Buffer::<LittleEndian>::new(data);
assert_eq!(buffer.remaining_length(), 4);
buffer.cursor = 2;
assert_eq!(buffer.remaining_length(), 2);
}
#[test]
fn test_move_cursor() {
let data: &[u8] = &[1, 2, 3, 4];
let mut buffer = Buffer::<LittleEndian>::new(data);
// Test moving forward
assert!(buffer.move_cursor(2).is_ok());
assert_eq!(buffer.cursor, 2);
// Test moving backward
assert!(buffer.move_cursor(-1).is_ok());
assert_eq!(buffer.cursor, 1);
// Test moving beyond data limits
assert!(buffer.move_cursor(5).is_err());
assert!(buffer.move_cursor(-2).is_err());
}
#[test]
fn test_switch_endian_chunk_le_be() {
let data = [0x01, 0x02, 0x03, 0x04];
let mut buffer = Buffer::<LittleEndian>::new(&data[..]);
let switched_buffer = buffer.switch_endian_chunk(2).unwrap();
assert_eq!(switched_buffer.data, [0x01, 0x02]);
assert_eq!(switched_buffer.cursor, 0);
assert_eq!(buffer.remaining_bytes(), [0x03, 0x04]);
assert_eq!(buffer.cursor, 2);
}
#[test]
fn test_switch_endian_chunk_be_le() {
let data = [0x01, 0x02, 0x03, 0x04];
let mut buffer = Buffer::<BigEndian>::new(&data[..]);
let switched_buffer = buffer.switch_endian_chunk(2).unwrap();
assert_eq!(switched_buffer.data, [0x01, 0x02]);
assert_eq!(switched_buffer.cursor, 0);
assert_eq!(buffer.remaining_bytes(), [0x03, 0x04]);
assert_eq!(buffer.cursor, 2);
}
#[test]
fn test_buffer_read_u8() {
let data: &[u8] = &[1, 2, 3, 4];
let mut buffer = Buffer::<LittleEndian>::new(data);
let result: Result<u8, _> = buffer.read();
assert_eq!(result.unwrap(), 1);
assert_eq!(buffer.cursor, 1);
}
#[test]
fn test_buffer_read_u16() {
let data: &[u8] = &[1, 2, 3, 4];
let mut buffer = Buffer::<LittleEndian>::new(data);
let result: Result<u16, _> = buffer.read();
assert_eq!(result.unwrap(), 0x0201);
assert_eq!(buffer.cursor, 2);
}
#[test]
fn test_buffer_read_u16_big_endian() {
let data: &[u8] = &[1, 2, 3, 4];
let mut buffer = Buffer::<BigEndian>::new(data);
let result: Result<u16, _> = buffer.read();
assert_eq!(result.unwrap(), 0x0102);
assert_eq!(buffer.cursor, 2);
}
#[test]
fn test_decode_string_utf8() {
let data: &[u8] = b"Hello\0World\0";
let mut cursor = 0;
let delimiter = [0x00];
let result = Utf8Decoder::decode_string(data, &mut cursor, delimiter);
assert_eq!(result.unwrap(), "Hello");
assert_eq!(cursor, 6);
}
#[test]
fn test_decode_string_utf16_le() {
let data: &[u8] = &[0x48, 0x00, 0x65, 0x00, 0x00, 0x00];
let mut cursor = 0;
let delimiter = [0x00, 0x00];
let result = Utf16Decoder::<LittleEndian>::decode_string(data, &mut cursor, delimiter);
assert_eq!(result.unwrap(), "He");
assert_eq!(cursor, 6);
}
#[test]
fn test_decode_string_utf16_be() {
let data: &[u8] = &[0x00, 0x48, 0x00, 0x65, 0x00, 0x00];
let mut cursor = 0;
let delimiter = [0x00, 0x00];
let result = Utf16Decoder::<BigEndian>::decode_string(data, &mut cursor, delimiter);
assert_eq!(result.unwrap(), "He");
assert_eq!(cursor, 6);
}
#[test]
fn test_buffer_underflow_error() {
let data: &[u8] = &[1, 2];
let mut buffer = Buffer::<LittleEndian>::new(data);
let result: Result<u32, _> = buffer.read();
assert_eq!(
result.unwrap_err(),
crate::GDErrorKind::PacketUnderflow.into()
);
}
}

View file

@ -1,39 +0,0 @@
pub(crate) mod packet;
mod pcap;
pub(crate) mod socket;
pub(crate) mod writer;
use self::{pcap::Pcap, writer::Writer};
use pcap_file::pcapng::{blocks::interface_description::InterfaceDescriptionBlock, PcapNgBlock, PcapNgWriter};
use std::path::PathBuf;
pub fn setup_capture(file_path: Option<PathBuf>) {
if let Some(file_path) = file_path {
let file = std::fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(file_path.with_extension("pcap"))
.unwrap();
let mut pcap_writer = PcapNgWriter::new(file).unwrap();
// Write headers
let _ = pcap_writer.write_block(
&InterfaceDescriptionBlock {
linktype: pcap_file::DataLink::ETHERNET,
snaplen: 0xFFFF,
options: vec![],
}
.into_block(),
);
let writer = Box::new(Pcap::new(pcap_writer));
attach(writer)
}
}
/// Attaches a writer to the capture module.
///
/// # Errors
/// Returns an Error if the writer is already set.
fn attach(writer: Box<dyn Writer + Send + Sync>) { crate::capture::socket::set_writer(writer); }

View file

@ -1,203 +0,0 @@
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
/// Size of a standard network packet.
pub(crate) const PACKET_SIZE: usize = 5012;
/// Size of an Ethernet header.
pub(crate) const HEADER_SIZE_ETHERNET: usize = 14;
/// Size of an IPv4 header.
pub(crate) const HEADER_SIZE_IP4: usize = 20;
/// Size of an IPv6 header.
pub(crate) const HEADER_SIZE_IP6: usize = 40;
/// Size of a UDP header.
pub(crate) const HEADER_SIZE_UDP: usize = 4;
/// Represents the direction of a network packet.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Direction {
/// Packet is outgoing (sent by us).
Send,
/// Packet is incoming (received by us).
Receive,
}
/// Defines the protocol of a network packet.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Protocol {
/// Transmission Control Protocol.
Tcp,
/// User Datagram Protocol.
Udp,
}
/// Trait for handling different types of IP addresses (IPv4, IPv6).
pub(crate) trait IpAddress: Sized {
/// Creates an instance from a standard `IpAddr`, returning `None` if the
/// types are incompatible.
fn from_std(ip: IpAddr) -> Option<Self>;
}
/// Represents a captured network packet with metadata.
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct CapturePacket<'a> {
/// Direction of the packet (Send/Receive).
pub(crate) direction: Direction,
/// Protocol of the packet (Tcp/UDP).
pub(crate) protocol: Protocol,
/// Remote socket address.
pub(crate) remote_address: &'a SocketAddr,
/// Local socket address.
pub(crate) local_address: &'a SocketAddr,
}
impl CapturePacket<'_> {
/// Retrieves the local and remote ports based on the packet's direction.
///
/// Returns:
/// - (u16, u16): Tuple of (source port, destination port).
pub(super) fn ports_by_direction(&self) -> (u16, u16) {
let (local, remote) = (self.local_address.port(), self.remote_address.port());
self.direction.order(local, remote)
}
/// Retrieves the local and remote IP addresses.
///
/// Returns:
/// - (IpAddr, IpAddr): Tuple of (local IP, remote IP).
pub(super) fn ip_addr(&self) -> (IpAddr, IpAddr) {
let (local, remote) = (self.local_address.ip(), self.remote_address.ip());
(local, remote)
}
/// Retrieves IP addresses of a specific type (IPv4 or IPv6) based on the
/// packet's direction.
///
/// Panics if the IP type of the addresses does not match the requested
/// type.
///
/// Returns:
/// - (T, T): Tuple of (source IP, destination IP) of the specified type in
/// order.
pub(super) fn ipvt_by_direction<T: IpAddress>(&self) -> (T, T) {
let (local, remote) = (
T::from_std(self.local_address.ip()).expect("Incorrect IP type for local address"),
T::from_std(self.remote_address.ip()).expect("Incorrect IP type for remote address"),
);
self.direction.order(local, remote)
}
}
impl Direction {
/// Orders two elements (source and destination) based on the packet's
/// direction.
///
/// Returns:
/// - (T, T): Ordered tuple (source, destination).
pub(self) const fn order<T>(&self, source: T, remote: T) -> (T, T) {
match self {
Direction::Send => (source, remote),
Direction::Receive => (remote, source),
}
}
}
/// Implements the `IpAddress` trait for `Ipv4Addr`.
impl IpAddress for Ipv4Addr {
/// Creates an `Ipv4Addr` from a standard `IpAddr`, if it's IPv4.
fn from_std(ip: IpAddr) -> Option<Self> {
match ip {
IpAddr::V4(ipv4) => Some(ipv4),
_ => None,
}
}
}
/// Implements the `IpAddress` trait for `Ipv6Addr`.
impl IpAddress for Ipv6Addr {
/// Creates an `Ipv6Addr` from a standard `IpAddr`, if it's IPv6.
fn from_std(ip: IpAddr) -> Option<Self> {
match ip {
IpAddr::V6(ipv6) => Some(ipv6),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
// Helper function to create a SocketAddr from a string
fn socket_addr(addr: &str) -> SocketAddr { SocketAddr::from_str(addr).unwrap() }
#[test]
fn test_ports_by_direction() {
let packet_send = CapturePacket {
direction: Direction::Send,
protocol: Protocol::Tcp,
local_address: &socket_addr("127.0.0.1:8080"),
remote_address: &socket_addr("192.168.1.1:80"),
};
let packet_receive = CapturePacket {
direction: Direction::Receive,
protocol: Protocol::Tcp,
local_address: &socket_addr("127.0.0.1:8080"),
remote_address: &socket_addr("192.168.1.1:80"),
};
assert_eq!(packet_send.ports_by_direction(), (8080, 80));
assert_eq!(packet_receive.ports_by_direction(), (80, 8080));
}
#[test]
fn test_ip_addr() {
let packet_send = CapturePacket {
direction: Direction::Send,
protocol: Protocol::Tcp,
local_address: &socket_addr("127.0.0.1:8080"),
remote_address: &socket_addr("192.168.1.1:80"),
};
let packet_receive = CapturePacket {
direction: Direction::Receive,
protocol: Protocol::Tcp,
local_address: &socket_addr("127.0.0.1:8080"),
remote_address: &socket_addr("192.168.1.1:80"),
};
assert_eq!(
packet_send.ip_addr(),
(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))
)
);
assert_eq!(
packet_receive.ip_addr(),
(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))
)
);
}
#[test]
fn test_ip_by_direction_type_specific() {
let packet = CapturePacket {
direction: Direction::Send,
protocol: Protocol::Tcp,
local_address: &socket_addr("127.0.0.1:8080"),
remote_address: &socket_addr("192.168.1.1:80"),
};
let ipv4_result: Result<(Ipv4Addr, Ipv4Addr), _> =
std::panic::catch_unwind(|| packet.ipvt_by_direction::<Ipv4Addr>());
assert!(ipv4_result.is_ok());
let ipv6_result: Result<(Ipv6Addr, Ipv6Addr), _> =
std::panic::catch_unwind(|| packet.ipvt_by_direction::<Ipv6Addr>());
assert!(ipv6_result.is_err());
}
}

View file

@ -1,383 +0,0 @@
use pcap_file::pcapng::{blocks::enhanced_packet::EnhancedPacketOption, PcapNgBlock, PcapNgWriter};
use pnet_packet::{
ethernet::{EtherType, MutableEthernetPacket},
ip::{IpNextHeaderProtocol, IpNextHeaderProtocols},
ipv4::MutableIpv4Packet,
ipv6::MutableIpv6Packet,
tcp::{MutableTcpPacket, TcpFlags},
udp::MutableUdpPacket,
PacketSize,
};
use std::{io::Write, net::IpAddr, time::Instant};
use super::packet::{
CapturePacket,
Direction,
Protocol,
HEADER_SIZE_ETHERNET,
HEADER_SIZE_IP4,
HEADER_SIZE_IP6,
HEADER_SIZE_UDP,
PACKET_SIZE,
};
const BUFFER_SIZE: usize = PACKET_SIZE - HEADER_SIZE_IP6 - HEADER_SIZE_ETHERNET;
pub(crate) struct Pcap<W: Write> {
writer: PcapNgWriter<W>,
pub(crate) state: State,
}
pub(crate) struct State {
pub(crate) start_time: Instant,
pub(crate) send_seq: u32,
pub(crate) rec_seq: u32,
pub(crate) has_sent_handshake: bool,
pub(crate) stream_count: u32,
}
impl<W: Write> Pcap<W> {
pub(crate) fn new(writer: PcapNgWriter<W>) -> Self {
Self {
writer,
state: State::default(),
}
}
pub(crate) fn write_transport_packet(&mut self, info: &CapturePacket, payload: &[u8]) {
let mut buffer_array: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
let buf: &mut [u8] = &mut buffer_array[..];
let (source_port, dest_port) = info.ports_by_direction();
match info.protocol {
Protocol::Tcp => {
let buf_size = {
let mut tcp = MutableTcpPacket::new(buf).unwrap();
tcp.set_source(source_port);
tcp.set_destination(dest_port);
tcp.set_payload(payload);
tcp.set_data_offset(5);
tcp.set_window(43440);
match info.direction {
Direction::Send => {
tcp.set_sequence(self.state.send_seq);
tcp.set_acknowledgement(self.state.rec_seq);
self.state.send_seq = self.state.send_seq.wrapping_add(payload.len() as u32);
}
Direction::Receive => {
tcp.set_sequence(self.state.rec_seq);
tcp.set_acknowledgement(self.state.send_seq);
self.state.rec_seq = self.state.rec_seq.wrapping_add(payload.len() as u32);
}
}
tcp.set_flags(TcpFlags::PSH | TcpFlags::ACK);
tcp.packet_size()
};
self.write_transport_payload(
info,
IpNextHeaderProtocols::Tcp,
&buf[.. buf_size + payload.len()],
vec![],
);
let mut info = info.clone();
let buf_size = {
let mut tcp = MutableTcpPacket::new(buf).unwrap();
tcp.set_source(dest_port);
tcp.set_destination(source_port);
tcp.set_data_offset(5);
tcp.set_window(43440);
match &info.direction {
Direction::Send => {
tcp.set_sequence(self.state.rec_seq);
tcp.set_acknowledgement(self.state.send_seq);
info.direction = Direction::Receive;
}
Direction::Receive => {
tcp.set_sequence(self.state.send_seq);
tcp.set_acknowledgement(self.state.rec_seq);
info.direction = Direction::Send;
}
}
tcp.set_flags(TcpFlags::ACK);
tcp.packet_size()
};
self.write_transport_payload(
&info,
IpNextHeaderProtocols::Tcp,
&buf[.. buf_size],
vec![EnhancedPacketOption::Comment("Generated TCP ACK".into())],
);
}
Protocol::Udp => {
let buf_size = {
let mut udp = MutableUdpPacket::new(buf).unwrap();
udp.set_source(source_port);
udp.set_destination(dest_port);
udp.set_length((payload.len() + HEADER_SIZE_UDP) as u16);
udp.set_payload(payload);
udp.packet_size()
};
self.write_transport_payload(
info,
IpNextHeaderProtocols::Udp,
&buf[.. buf_size + payload.len()],
vec![],
);
}
}
}
/// Encode a network layer (IP) packet with a payload.
fn encode_ip_packet(
&self,
buf: &mut [u8],
info: &CapturePacket,
protocol: IpNextHeaderProtocol,
payload: &[u8],
) -> (usize, EtherType) {
match info.ip_addr() {
(IpAddr::V4(_), IpAddr::V4(_)) => {
let (source, destination) = info.ipvt_by_direction();
let header_size = HEADER_SIZE_IP4 + (32 / 8);
let mut ip = MutableIpv4Packet::new(buf).unwrap();
ip.set_version(4);
ip.set_total_length((payload.len() + header_size) as u16);
ip.set_next_level_protocol(protocol);
// https://en.wikipedia.org/wiki/Internet_Protocol_version_4#Total_Length
ip.set_header_length((header_size / 4) as u8);
ip.set_source(source);
ip.set_destination(destination);
ip.set_payload(payload);
ip.set_ttl(64);
ip.set_flags(pnet_packet::ipv4::Ipv4Flags::DontFragment);
let mut options_writer =
pnet_packet::ipv4::MutableIpv4OptionPacket::new(ip.get_options_raw_mut()).unwrap();
options_writer.set_copied(1);
options_writer.set_class(0);
options_writer.set_number(pnet_packet::ipv4::Ipv4OptionNumbers::SID);
options_writer.set_length(&[4]);
options_writer.set_data(&(self.state.stream_count as u16).to_be_bytes());
ip.set_checksum(pnet_packet::ipv4::checksum(&ip.to_immutable()));
(ip.packet_size(), pnet_packet::ethernet::EtherTypes::Ipv4)
}
(IpAddr::V6(_), IpAddr::V6(_)) => {
let (source, destination) = info.ipvt_by_direction();
let mut ip = MutableIpv6Packet::new(buf).unwrap();
ip.set_version(6);
ip.set_payload_length(payload.len() as u16);
ip.set_next_header(protocol);
ip.set_source(source);
ip.set_destination(destination);
ip.set_hop_limit(64);
ip.set_payload(payload);
ip.set_flow_label(self.state.stream_count);
(ip.packet_size(), pnet_packet::ethernet::EtherTypes::Ipv6)
}
_ => unreachable!(),
}
}
/// Encode a physical layer (ethernet) packet with a payload.
fn encode_ethernet_packet(
&self,
buf: &mut [u8],
ethertype: pnet_packet::ethernet::EtherType,
payload: &[u8],
) -> usize {
let mut ethernet = MutableEthernetPacket::new(buf).unwrap();
ethernet.set_ethertype(ethertype);
ethernet.set_payload(payload);
ethernet.packet_size()
}
/// Write a TCP handshake.
pub(crate) fn write_tcp_handshake(&mut self, info: &CapturePacket) {
let (source_port, dest_port) = (info.local_address.port(), info.remote_address.port());
let mut info = info.clone();
info.direction = Direction::Send;
let mut buffer_array: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
let buf: &mut [u8] = &mut buffer_array[..];
// Add a generated comment to all packets
let options = vec![
pcap_file::pcapng::blocks::enhanced_packet::EnhancedPacketOption::Comment("Generated TCP handshake".into()),
];
// SYN
let buf_size = {
let mut tcp = MutableTcpPacket::new(buf).unwrap();
self.state.send_seq = 500;
tcp.set_sequence(self.state.send_seq);
tcp.set_flags(TcpFlags::SYN);
tcp.set_source(source_port);
tcp.set_destination(dest_port);
tcp.set_window(43440);
tcp.set_data_offset(5);
tcp.packet_size()
};
self.write_transport_payload(
&info,
IpNextHeaderProtocols::Tcp,
&buf[.. buf_size],
options.clone(),
);
// SYN + ACK
info.direction = Direction::Receive;
let buf_size = {
let mut tcp = MutableTcpPacket::new(buf).unwrap();
self.state.send_seq = self.state.send_seq.wrapping_add(1);
tcp.set_acknowledgement(self.state.send_seq);
self.state.rec_seq = 1000;
tcp.set_sequence(self.state.rec_seq);
tcp.set_flags(TcpFlags::SYN | TcpFlags::ACK);
tcp.set_source(dest_port);
tcp.set_destination(source_port);
tcp.set_window(43440);
tcp.set_data_offset(5);
tcp.packet_size()
};
self.write_transport_payload(
&info,
IpNextHeaderProtocols::Tcp,
&buf[.. buf_size],
options.clone(),
);
// ACK
info.direction = Direction::Send;
let buf_size = {
let mut tcp = MutableTcpPacket::new(buf).unwrap();
tcp.set_sequence(self.state.send_seq);
self.state.rec_seq = self.state.rec_seq.wrapping_add(1);
tcp.set_acknowledgement(self.state.rec_seq);
tcp.set_flags(TcpFlags::ACK);
tcp.set_source(source_port);
tcp.set_destination(dest_port);
tcp.set_window(43440);
tcp.set_data_offset(5);
tcp.packet_size()
};
self.write_transport_payload(
&info,
IpNextHeaderProtocols::Tcp,
&buf[.. buf_size],
options,
);
self.state.has_sent_handshake = true;
}
pub(crate) fn send_tcp_fin(&mut self, info: &CapturePacket) {
let mut buffer_array: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
let buf: &mut [u8] = &mut buffer_array[..];
let (source_port, dest_port) = info.ports_by_direction();
let buf_size = {
let mut tcp = MutableTcpPacket::new(buf).unwrap();
tcp.set_source(source_port);
tcp.set_destination(dest_port);
tcp.set_data_offset(5);
tcp.set_window(43440);
match info.direction {
Direction::Send => {
tcp.set_sequence(self.state.send_seq);
tcp.set_acknowledgement(self.state.rec_seq);
}
Direction::Receive => {
tcp.set_sequence(self.state.rec_seq);
tcp.set_acknowledgement(self.state.send_seq);
}
}
tcp.set_flags(TcpFlags::FIN | TcpFlags::ACK);
tcp.packet_size()
};
self.write_transport_payload(
info,
IpNextHeaderProtocols::Tcp,
&buf[.. buf_size],
vec![EnhancedPacketOption::Comment("Generated TCP FIN".into())],
);
// Update sequence number
match info.direction {
Direction::Send => {
self.state.send_seq = self.state.send_seq.wrapping_add(1);
}
Direction::Receive => {
self.state.rec_seq = self.state.rec_seq.wrapping_add(1);
}
}
}
fn write_transport_payload(
&mut self,
info: &CapturePacket,
protocol: IpNextHeaderProtocol,
payload: &[u8],
options: Vec<pcap_file::pcapng::blocks::enhanced_packet::EnhancedPacketOption>,
) {
let mut network_packet = vec![0; PACKET_SIZE - HEADER_SIZE_ETHERNET];
let (network_size, ethertype) = self.encode_ip_packet(&mut network_packet, info, protocol, payload);
let network_size = network_size + payload.len();
network_packet.truncate(network_size);
let mut physical_packet = vec![0; PACKET_SIZE];
let physical_size =
self.encode_ethernet_packet(&mut physical_packet, ethertype, &network_packet) + network_size;
physical_packet.truncate(physical_size);
self.writer
.write_block(
&pcap_file::pcapng::blocks::enhanced_packet::EnhancedPacketBlock {
original_len: physical_size as u32,
data: physical_packet.into(),
interface_id: 0,
timestamp: self.state.start_time.elapsed(),
options,
}
.into_block(),
)
.unwrap();
}
}
impl Default for State {
fn default() -> Self {
Self {
start_time: Instant::now(),
send_seq: 0,
rec_seq: 0,
has_sent_handshake: false,
stream_count: 0,
}
}
}

View file

@ -1,214 +0,0 @@
use std::{marker::PhantomData, net::SocketAddr};
use crate::{
capture::{
packet::CapturePacket,
packet::{Direction, Protocol},
writer::{Writer, CAPTURE_WRITER},
},
protocols::types::TimeoutSettings,
socket::{Socket, TcpSocketImpl, UdpSocketImpl},
GDResult,
};
/// Sets a global capture writer for handling all packet data.
///
/// # Panics
/// Panics if a capture writer is already set.
///
/// # Arguments
/// * `writer` - A boxed writer that implements the `Writer` trait.
pub(crate) fn set_writer(writer: Box<dyn Writer + Send + Sync>) {
let mut lock = CAPTURE_WRITER.lock().unwrap();
if lock.is_some() {
panic!("Capture writer already set");
}
*lock = Some(writer);
}
/// A trait representing a provider of a network protocol.
pub(crate) trait ProtocolProvider {
/// Returns the protocol used by the provider.
fn protocol() -> Protocol;
}
/// Represents the TCP protocol provider.
pub(crate) struct ProtocolTCP;
impl ProtocolProvider for ProtocolTCP {
fn protocol() -> Protocol { Protocol::Tcp }
}
/// Represents the UDP protocol provider.
pub(crate) struct ProtocolUDP;
impl ProtocolProvider for ProtocolUDP {
fn protocol() -> Protocol { Protocol::Udp }
}
/// A socket wrapper that allows capturing packets.
///
/// # Type parameters
/// * `I` - The inner socket type.
/// * `P` - The protocol provider.
#[derive(Clone, Debug)]
pub(crate) struct WrappedCaptureSocket<I: Socket, P: ProtocolProvider> {
inner: I,
remote_address: SocketAddr,
_protocol: PhantomData<P>,
}
impl<I: Socket, P: ProtocolProvider> Socket for WrappedCaptureSocket<I, P> {
/// Creates a new wrapped socket for capturing packets.
///
/// Initializes a new socket of type `I`, wrapping it to enable packet
/// capturing. Capturing is protocol-specific, as indicated by
/// the `ProtocolProvider`.
///
/// # Arguments
/// * `address` - The address to connect the socket to.
/// * `timeout_settings` - Optional timeout settings for the socket.
///
/// # Returns
/// A `GDResult` containing either the wrapped socket or an error.
fn new(address: &SocketAddr, timeout_settings: &Option<TimeoutSettings>) -> GDResult<Self>
where Self: Sized {
let v = Self {
inner: I::new(address, timeout_settings)?,
remote_address: *address,
_protocol: PhantomData,
};
let info = CapturePacket {
direction: Direction::Send,
protocol: P::protocol(),
remote_address: address,
local_address: &v.local_addr().unwrap(),
};
if let Some(writer) = CAPTURE_WRITER.lock().unwrap().as_mut() {
writer.new_connect(&info)?;
}
Ok(v)
}
/// Sends data over the socket and captures the packet.
///
/// The method sends data using the inner socket and captures the sent
/// packet if a capture writer is set.
///
/// # Arguments
/// * `data` - Data to be sent.
///
/// # Returns
/// A result indicating success or error in sending data.
fn send(&mut self, data: &[u8]) -> GDResult<()> {
let info = CapturePacket {
direction: Direction::Send,
protocol: P::protocol(),
remote_address: &self.remote_address,
local_address: &self.local_addr().unwrap(),
};
if let Some(writer) = CAPTURE_WRITER.lock().unwrap().as_mut() {
writer.write(&info, data)?;
}
self.inner.send(data)
}
/// Receives data from the socket and captures the packet.
///
/// The method receives data using the inner socket and captures the
/// incoming packet if a capture writer is set.
///
/// # Arguments
/// * `size` - Optional size of data to receive.
///
/// # Returns
/// A result containing received data or an error.
fn receive(&mut self, size: Option<usize>) -> crate::GDResult<Vec<u8>> {
let data = self.inner.receive(size)?;
let info = CapturePacket {
direction: Direction::Receive,
protocol: P::protocol(),
remote_address: &self.remote_address,
local_address: &self.local_addr().unwrap(),
};
if let Some(writer) = CAPTURE_WRITER.lock().unwrap().as_mut() {
writer.write(&info, &data)?;
}
Ok(data)
}
/// Applies timeout settings to the wrapped socket.
///
/// Delegates the operation to the inner socket implementation.
///
/// # Arguments
/// * `timeout_settings` - Optional timeout settings to apply.
///
/// # Returns
/// A result indicating success or error in applying timeouts.
fn apply_timeout(
&self,
timeout_settings: &Option<crate::protocols::types::TimeoutSettings>,
) -> crate::GDResult<()> {
self.inner.apply_timeout(timeout_settings)
}
/// Returns the remote port of the wrapped socket.
///
/// Delegates the operation to the inner socket implementation.
///
/// # Returns
/// The remote port number.
fn port(&self) -> u16 { self.inner.port() }
/// Returns the local SocketAddr of the wrapped socket.
///
/// Delegates the operation to the inner socket implementation.
///
/// # Returns
/// The local SocketAddr.
fn local_addr(&self) -> std::io::Result<SocketAddr> { self.inner.local_addr() }
}
// this seems a bad way to do this, but its safe
impl<I: Socket, P: ProtocolProvider> Drop for WrappedCaptureSocket<I, P> {
fn drop(&mut self) {
// Construct the CapturePacket info
let info = CapturePacket {
direction: Direction::Send,
protocol: P::protocol(),
remote_address: &self.remote_address,
local_address: &self
.local_addr()
.unwrap_or_else(|_| SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0)),
};
// If a capture writer is set, close the connection and capture the packet.
if let Some(writer) = CAPTURE_WRITER.lock().unwrap().as_mut() {
let _ = writer.close_connection(&info);
}
}
}
/// A specialized `WrappedCaptureSocket` for UDP, using `UdpSocketImpl` as
/// the inner socket and `ProtocolUDP` as the protocol provider.
///
/// This type captures and processes UDP packets, wrapping around standard
/// UDP socket functionalities with additional packet capture
/// capabilities.
pub(crate) type CapturedUdpSocket = WrappedCaptureSocket<UdpSocketImpl, ProtocolUDP>;
/// A specialized `WrappedCaptureSocket` for TCP, using `TcpSocketImpl` as
/// the inner socket and `ProtocolTCP` as the protocol provider.
///
/// This type captures and processes TCP packets, wrapping around standard
/// TCP socket functionalities with additional packet capture
/// capabilities.
pub(crate) type CapturedTcpSocket = WrappedCaptureSocket<TcpSocketImpl, ProtocolTCP>;

View file

@ -1,86 +0,0 @@
use std::{io::Write, sync::Mutex};
use super::{
packet::{CapturePacket, Protocol},
pcap::Pcap,
};
use crate::GDResult;
use lazy_static::lazy_static;
lazy_static! {
/// A globally accessible, lazily-initialized static writer instance.
/// This writer is intended for capturing and recording network packets.
/// The writer is wrapped in a Mutex to ensure thread-safe access and modification.
pub(crate) static ref CAPTURE_WRITER: Mutex<Option<Box<dyn Writer + Send + Sync>>> = Mutex::new(None);
}
/// Trait defining the functionality for a writer that handles network packet
/// captures. This trait includes methods for writing packet data, handling new
/// connections, and closing connections.
pub(crate) trait Writer {
/// Writes a given packet's data to an underlying storage or stream.
///
/// # Arguments
/// * `packet` - Reference to the packet being captured.
/// * `data` - The raw byte data associated with the packet.
///
/// # Returns
/// A `GDResult` indicating the success or failure of the write operation.
fn write(&mut self, packet: &CapturePacket, data: &[u8]) -> GDResult<()>;
/// Handles the creation of a new connection, potentially logging or
/// initializing resources.
///
/// # Arguments
/// * `packet` - Reference to the packet indicating a new connection.
///
/// # Returns
/// A `GDResult` indicating the success or failure of handling the new
/// connection.
fn new_connect(&mut self, packet: &CapturePacket) -> GDResult<()>;
/// Closes a connection, handling any necessary cleanup or finalization.
///
/// # Arguments
/// * `packet` - Reference to the packet indicating the closure of a
/// connection.
///
/// # Returns
/// A `GDResult` indicating the success or failure of the connection closure
/// operation.
fn close_connection(&mut self, packet: &CapturePacket) -> GDResult<()>;
}
/// Implementation of the `Writer` trait for the `Pcap` struct.
/// This implementation enables writing, connection handling, and closure
/// specific to PCAP (Packet Capture) format.
impl<W: Write> Writer for Pcap<W> {
fn write(&mut self, info: &CapturePacket, data: &[u8]) -> GDResult<()> {
self.write_transport_packet(info, data);
Ok(())
}
fn new_connect(&mut self, packet: &CapturePacket) -> GDResult<()> {
match packet.protocol {
Protocol::Tcp => {
self.write_tcp_handshake(packet);
}
Protocol::Udp => {}
}
self.state.stream_count = self.state.stream_count.wrapping_add(1);
Ok(())
}
fn close_connection(&mut self, packet: &CapturePacket) -> GDResult<()> {
match packet.protocol {
Protocol::Tcp => {
self.send_tcp_fin(packet);
}
Protocol::Udp => {}
}
Ok(())
}
}

View file

@ -1,143 +0,0 @@
use crate::GDErrorKind;
use std::error::Error;
use std::fmt::Formatter;
use std::{backtrace, fmt};
pub(crate) type ErrorSource = Box<dyn Error + 'static + Send + Sync>;
/// The GameDig error type.
///
/// Can be created in three ways (all of which will implicitly generate a
/// backtrace):
///
/// Directly from an [error kind](GDErrorKind) (without a
/// source).
///
/// ```
/// use gamedig::{GDError, GDErrorKind};
/// let _: GDError = GDErrorKind::PacketBad.into();
/// ```
///
/// [From an error kind with a source](GDErrorKind::context) (any
/// type that implements `Into<Box<dyn std::error::Error + 'static>>`).
///
/// ```
/// use gamedig::{GDError, GDErrorKind};
/// let _: GDError = GDErrorKind::PacketBad.context("Reason the packet was bad");
/// ```
///
/// Using the [new helper](GDError::new).
///
/// ```
/// use gamedig::{GDError, GDErrorKind};
/// let _: GDError = GDError::new(GDErrorKind::PacketBad, Some("Reason the packet was bad".into()));
/// ```
pub struct GDError {
pub kind: GDErrorKind,
pub source: Option<ErrorSource>,
pub backtrace: Option<backtrace::Backtrace>,
}
impl From<GDErrorKind> for GDError {
fn from(value: GDErrorKind) -> Self {
let backtrace = Some(backtrace::Backtrace::capture());
Self {
kind: value,
source: None,
backtrace,
}
}
}
impl PartialEq for GDError {
fn eq(&self, other: &Self) -> bool { self.kind == other.kind }
}
impl Error for GDError {
fn source(&self) -> Option<&(dyn Error + 'static)> { self.source.as_ref().map(|err| Box::as_ref(err) as _) }
}
impl fmt::Debug for GDError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
writeln!(f, "GDError{{ kind={:?}", self.kind)?;
if let Some(source) = &self.source {
writeln!(f, " source={source:?}")?;
}
if let Some(backtrace) = &self.backtrace {
let bt = format!("{backtrace:#?}");
writeln!(f, " backtrace={}", bt.replace('\n', "\n "))?;
}
writeln!(f, "}}")?;
Ok(())
}
}
impl fmt::Display for GDError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{self:?}") }
}
impl GDError {
/// Create a new error (with automatic backtrace)
pub fn new(kind: GDErrorKind, source: Option<ErrorSource>) -> Self {
let backtrace = Some(backtrace::Backtrace::capture());
Self {
kind,
source,
backtrace,
}
}
/// Create a new error using any type that can be converted to an error
pub fn from_error<E: Into<ErrorSource>>(kind: GDErrorKind, source: E) -> Self {
Self::new(kind, Some(source.into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
// test error trait GDError
#[test]
fn test_error_trait() {
let source: Result<u32, _> = "nan".parse();
let source_err = source.unwrap_err();
let error_with_context = GDErrorKind::TypeParse.context(source_err.clone());
assert!(error_with_context.source().is_some());
assert_eq!(
format!("{}", error_with_context.source().unwrap()),
format!("{source_err}")
);
let error_without_context: GDError = GDErrorKind::TypeParse.into();
assert!(error_without_context.source().is_none());
}
// Test creating GDError with GDError::new
#[test]
fn test_create_new() {
let error_from_new = GDError::new(GDErrorKind::InvalidInput, None);
assert!(error_from_new.backtrace.is_some());
assert_eq!(error_from_new.kind, GDErrorKind::InvalidInput);
assert!(error_from_new.source.is_none());
}
// Test creating GDError with GDErrorKind::context
#[test]
fn test_create_context() {
let error_from_context = GDErrorKind::InvalidInput.context("test");
assert!(error_from_context.backtrace.is_some());
assert_eq!(error_from_context.kind, GDErrorKind::InvalidInput);
assert!(error_from_context.source.is_some());
}
// Test creating GDError with From<GDErrorKind> for GDError
#[test]
fn test_create_into() {
let error_from_into: GDError = GDErrorKind::InvalidInput.into();
assert!(error_from_into.backtrace.is_some());
assert_eq!(error_from_into.kind, GDErrorKind::InvalidInput);
assert!(error_from_into.source.is_none());
}
}

View file

@ -1,75 +0,0 @@
use crate::error::ErrorSource;
use crate::GDError;
/// All GameDig Error kinds.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GDErrorKind {
/// The received packet was bigger than the buffer size.
PacketOverflow,
/// The received packet was shorter than the expected one.
PacketUnderflow,
/// The received packet is badly formatted.
PacketBad,
/// Couldn't send the packet.
PacketSend,
/// Couldn't receieve data when it was expected.
PacketReceive,
/// Couldn't decompress data.
Decompress,
/// Couldn't create a socket connection.
SocketConnect,
/// Couldn't bind a socket.
SocketBind,
/// Invalid input to the library.
InvalidInput,
/// The server response indicated that it is a different game than the game
/// queried.
BadGame,
/// Couldn't automatically query (none of the attempted protocols were
/// successful).
AutoQuery,
/// A protocol-defined expected format was not met.
ProtocolFormat,
/// Couldn't cast a value to an enum.
UnknownEnumCast,
/// Couldn't parse a json string.
JsonParse,
/// Couldn't parse a value.
TypeParse,
/// Couldn't find the host specified.
HostLookup,
}
impl GDErrorKind {
/// Convert error kind into a full error with a source (and implicit
/// backtrace)
///
/// ```
/// use gamedig::{GDErrorKind, GDResult};
/// let _: GDResult<u32> = "thing".parse().map_err(|e| GDErrorKind::TypeParse.context(e));
/// ```
pub fn context<E: Into<ErrorSource>>(self, source: E) -> GDError { GDError::from_error(self, source) }
}
#[cfg(test)]
mod tests {
use super::*;
// Testing cloning the GDErrorKind type
#[test]
fn test_cloning() {
let error = GDErrorKind::BadGame;
let cloned_error = error.clone();
assert_eq!(error, cloned_error);
}
// test display GDError
#[test]
fn test_display() {
let err = GDErrorKind::BadGame.context("Rust is not a game");
assert_eq!(
format!("{err}"),
"GDError{ kind=BadGame\n source=\"Rust is not a game\"\n backtrace=<disabled>\n}\n"
);
}
}

View file

@ -1,12 +0,0 @@
//! Every GameDig errors.
/// The Error with backtrace.
pub mod error;
/// All defined Error kinds.
pub mod kind;
/// `GDResult`, a shorthand of `Result<T, GDError>`.
pub mod result;
pub use error::*;
pub use kind::*;
pub use result::*;

View file

@ -1,24 +0,0 @@
use crate::GDError;
/// `Result` of `T` and `GDError`.
pub type GDResult<T> = Result<T, GDError>;
#[cfg(test)]
mod tests {
use super::*;
use crate::GDErrorKind;
// Testing Ok variant of the GDResult type
#[test]
fn test_gdresult_ok() {
let result: GDResult<u32> = Ok(42);
assert_eq!(result, Ok(42));
}
// Testing Err variant of the GDResult type
#[test]
fn test_gdresult_err() {
let result: GDResult<u32> = Err(GDErrorKind::InvalidInput.into());
assert!(result.is_err());
}
}

View file

@ -1,47 +0,0 @@
use crate::protocols::valve::Engine;
use crate::{
protocols::valve::{self, game},
GDErrorKind::TypeParse,
GDResult,
};
use std::net::{IpAddr, SocketAddr};
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<game::Response> {
let mut valve_response = valve::query(
&SocketAddr::new(*address, port.unwrap_or(7780)),
Engine::new(489_940),
None,
None,
)?;
if let Some(rules) = &mut valve_response.rules {
if let Some(bat_max_players) = rules.get("bat_max_players_i") {
valve_response.info.players_maximum = bat_max_players.parse().map_err(|e| TypeParse.context(e))?;
rules.remove("bat_max_players_i");
}
if let Some(bat_player_count) = rules.get("bat_player_count_s") {
valve_response.info.players_online = bat_player_count.parse().map_err(|e| TypeParse.context(e))?;
rules.remove("bat_player_count_s");
}
if let Some(bat_has_password) = rules.get("bat_has_password_s") {
valve_response.info.has_password = bat_has_password == "Y";
rules.remove("bat_has_password_s");
}
if let Some(bat_name) = rules.get("bat_name_s") {
valve_response.info.name.clone_from(bat_name);
rules.remove("bat_name_s");
}
if let Some(bat_gamemode) = rules.get("bat_gamemode_s") {
valve_response.info.game_mode.clone_from(bat_gamemode);
rules.remove("bat_gamemode_s");
}
rules.remove("bat_map_s");
}
Ok(game::Response::new_from_valve_response(valve_response))
}

View file

@ -1,160 +0,0 @@
//! Static definitions of currently supported games
use crate::games::minecraft::types::{LegacyGroup, Server};
use crate::protocols::{gamespy::GameSpyVersion, quake::QuakeVersion, valve::Engine, Protocol};
use crate::Game;
use crate::protocols::types::{GatherToggle, ProprietaryProtocol};
use crate::protocols::valve::GatheringSettings;
use phf::{phf_map, Map};
macro_rules! game {
($name: literal, $default_port: expr, $protocol: expr) => {
game!(
$name,
$default_port,
$protocol,
GatheringSettings::default().into_extra()
)
};
($name: literal, $default_port: expr, $protocol: expr, $extra_request_settings: expr) => {
Game {
name: $name,
default_port: $default_port,
protocol: $protocol,
request_settings: $extra_request_settings,
}
};
}
/// Map of all currently supported games
pub static GAMES: Map<&'static str, Game> = phf_map! {
// Query with all minecraft protocols
"minecraft" => game!("Minecraft", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(None))),
// Query with specific minecraft protocols
"minecraftbedrock" => game!("Minecraft (bedrock)", 19132, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Bedrock)))),
"minecraftpocket" => game!("Minecraft (pocket)", 19132, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Bedrock)))),
"minecraftjava" => game!("Minecraft (java)", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Java)))),
"minecraftlegacy16" => game!("Minecraft (legacy 1.6)", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_6))))),
"minecraftlegacy14" => game!("Minecraft (legacy 1.4)", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Legacy(LegacyGroup::V1_4))))),
"minecraftlegacyb18" => game!("Minecraft (legacy b1.8)", 25565, Protocol::PROPRIETARY(ProprietaryProtocol::Minecraft(Some(Server::Legacy(LegacyGroup::VB1_8))))),
"aapg" => game!("America's Army: Proving Grounds", 27020, Protocol::Valve(Engine::new(203_290)), GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Skip,
check_app_id: true,
}.into_extra()),
"abioticfactor" => game!("Abiotic Factor", 27015, Protocol::Valve(Engine::new(427_410))),
"alienswarm" => game!("Alien Swarm", 27015, Protocol::Valve(Engine::new(630))),
"aoc" => game!("Age of Chivalry", 27015, Protocol::Valve(Engine::new(17510))),
"a2oa" => game!("ARMA 2: Operation Arrowhead", 2304, Protocol::Valve(Engine::new(33930))),
"arma3" => game!("ARMA 3", 2303, Protocol::Valve(Engine::new(107_410))),
"ase" => game!("ARK: Survival Evolved", 27015, Protocol::Valve(Engine::new(346_110))),
"asrd" => game!("Alien Swarm: Reactive Drop", 2304, Protocol::Valve(Engine::new(563_560))),
"armareforger" => game!("Arma Reforger", 17777, Protocol::Valve(Engine::new(1_874_880)), GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Enforce,
check_app_id: false,
}.into_extra()),
"atlas" => game!("ATLAS", 57561, Protocol::Valve(Engine::new(834_910))),
"avorion" => game!("Avorion", 27020, Protocol::Valve(Engine::new(445_220))),
"avp2010" => game!("Aliens vs. Predator 2010", 27015, Protocol::Valve(Engine::new(10_680))),
"barotrauma" => game!("Barotrauma", 27016, Protocol::Valve(Engine::new(602_960))),
"basedefense" => game!("Base Defense", 27015, Protocol::Valve(Engine::new(632_730)), GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Skip,
check_app_id: true,
}.into_extra()),
"battalion1944" => game!("Battalion 1944", 7780, Protocol::Valve(Engine::new(489_940))),
"brainbread2" => game!("BrainBread 2", 27015, Protocol::Valve(Engine::new(346_330))),
"battlefield1942" => game!("Battlefield 1942", 23000, Protocol::Gamespy(GameSpyVersion::One)),
"blackmesa" => game!("Black Mesa", 27015, Protocol::Valve(Engine::new(362_890))),
"ballisticoverkill" => game!("Ballistic Overkill", 27016, Protocol::Valve(Engine::new(296_300))),
"codbo3" => game!("Call Of Duty: Black Ops 3", 27017, Protocol::Valve(Engine::new(311_210))),
"codenamecure" => game!("Codename CURE", 27015, Protocol::Valve(Engine::new(355_180))),
"colonysurvival" => game!("Colony Survival", 27004, Protocol::Valve(Engine::new(366_090))),
"conanexiles" => game!("Conan Exiles", 27015, Protocol::Valve(Engine::new(440_900)), GatheringSettings {
players: GatherToggle::Skip,
rules: GatherToggle::Enforce,
check_app_id: true,
}.into_extra()),
"counterstrike" => game!("Counter-Strike", 27015, Protocol::Valve(Engine::new_gold_src(false))),
"counterstrike2" => game!("Counter-Strike 2", 27015, Protocol::Valve(Engine::new(730))),
"cscz" => game!("Counter Strike: Condition Zero", 27015, Protocol::Valve(Engine::new_gold_src(false))),
"csgo" => game!("Counter-Strike: Global Offensive", 27015, Protocol::Valve(Engine::new(730))),
"css" => game!("Counter-Strike: Source", 27015, Protocol::Valve(Engine::new(240))),
"creativerse" => game!("Creativerse", 26901, Protocol::Valve(Engine::new(280_790))),
"crysiswars" => game!("Crysis Wars", 64100, Protocol::Gamespy(GameSpyVersion::Three)),
"dab" => game!("Double Action: Boogaloo", 27015, Protocol::Valve(Engine::new(317_360))),
"dod" => game!("Day of Defeat", 27015, Protocol::Valve(Engine::new_gold_src(false))),
"dods" => game!("Day of Defeat: Source", 27015, Protocol::Valve(Engine::new(300))),
"doi" => game!("Day of Infamy", 27015, Protocol::Valve(Engine::new(447_820))),
"dst" => game!("Don't Starve Together", 27016, Protocol::Valve(Engine::new(322_320))),
"enshrouded" => game!("Enshrouded", 15637, Protocol::Valve(Engine::new(1_203_620))),
"ffow" => game!("Frontlines: Fuel of War", 5478, Protocol::PROPRIETARY(ProprietaryProtocol::FFOW)),
"garrysmod" => game!("Garry's Mod", 27016, Protocol::Valve(Engine::new(4000))),
"hl2d" => game!("Half-Life 2 Deathmatch", 27015, Protocol::Valve(Engine::new(320))),
"hce" => game!("Halo: Combat Evolved", 2302, Protocol::Gamespy(GameSpyVersion::Two)),
"hlds" => game!("Half-Life Deathmatch: Source", 27015, Protocol::Valve(Engine::new(360))),
"hll" => game!("Hell Let Loose", 26420, Protocol::Valve(Engine::new(686_810))),
"insurgency" => game!("Insurgency", 27015, Protocol::Valve(Engine::new(222_880))),
"imic" => game!("Insurgency: Modern Infantry Combat", 27015, Protocol::Valve(Engine::new(17700))),
"insurgencysandstorm" => game!("Insurgency: Sandstorm", 27131, Protocol::Valve(Engine::new(581_320))),
"l4d" => game!("Left 4 Dead", 27015, Protocol::Valve(Engine::new(500))),
"l4d2" => game!("Left 4 Dead 2", 27015, Protocol::Valve(Engine::new(550))),
"ohd" => game!("Operation: Harsh Doorstop", 27005, Protocol::Valve(Engine::new_with_dedicated(736_590, 950_900))),
"onset" => game!("Onset", 7776, Protocol::Valve(Engine::new(1_105_810))),
"pixark" => game!("PixARK", 27015, Protocol::Valve(Engine::new(593_600))),
"postscriptum" => game!("Post Scriptum", 10037, Protocol::Valve(Engine::new(736_220))),
"projectzomboid" => game!("Project Zomboid", 16261, Protocol::Valve(Engine::new(108_600))),
"pvak2" => game!("Pirates, Vikings, and Knights II", 27015, Protocol::Valve(Engine::new(17_570))),
"quake1" => game!("Quake 1", 27500, Protocol::Quake(QuakeVersion::One)),
"quake2" => game!("Quake 2", 27910, Protocol::Quake(QuakeVersion::Two)),
"q3a" => game!("Quake 3 Arena", 27960, Protocol::Quake(QuakeVersion::Three)),
"risingworld" => game!("Rising World", 4254, Protocol::Valve(Engine::new(324_080)), GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Skip,
check_app_id: true,
}.into_extra()),
"ror2" => game!("Risk of Rain 2", 27016, Protocol::Valve(Engine::new(632_360))),
"rust" => game!("Rust", 27015, Protocol::Valve(Engine::new(252_490))),
"savage2" => game!("Savage 2", 11235, Protocol::PROPRIETARY(ProprietaryProtocol::Savage2)),
"sco" => game!("Sven Co-op", 27015, Protocol::Valve(Engine::new_gold_src(false))),
"sdtd" => game!("7 Days to Die", 26900, Protocol::Valve(Engine::new(251_570))),
"sof2" => game!("Soldier of Fortune 2", 20100, Protocol::Quake(QuakeVersion::Three)),
"soulmask" => game!("Soulmask", 27015, Protocol::Valve(Engine::new(2_646_460))),
"serioussam" => game!("Serious Sam", 25601, Protocol::Gamespy(GameSpyVersion::One)),
"squad" => game!("Squad", 27165, Protocol::Valve(Engine::new(393_380))),
"starbound" => game!("Starbound", 21025, Protocol::Valve(Engine::new(211_820)), GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Enforce,
check_app_id: false,
}.into_extra()),
"theforest" => game!("The Forest", 27016, Protocol::Valve(Engine::new_with_dedicated(242_760, 556_450))),
"thefront" => game!("The Front", 27015, Protocol::Valve(Engine::new(2_285_150))),
"teamfortress2" => game!("Team Fortress 2", 27015, Protocol::Valve(Engine::new(440))),
"tfc" => game!("Team Fortress Classic", 27015, Protocol::Valve(Engine::new_gold_src(false))),
"theship" => game!("The Ship", 27015, Protocol::PROPRIETARY(ProprietaryProtocol::TheShip)),
"unturned" => game!("Unturned", 27015, Protocol::Valve(Engine::new(304_930))),
"unrealtournament" => game!("Unreal Tournament", 7778, Protocol::Gamespy(GameSpyVersion::One)),
"valheim" => game!("Valheim", 2457, Protocol::Valve(Engine::new(892_970)), GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Skip,
check_app_id: true,
}.into_extra()),
"vrising" => game!("V Rising", 27016, Protocol::Valve(Engine::new(1_604_030))),
"jc2m" => game!("Just Cause 2: Multiplayer", 7777, Protocol::PROPRIETARY(ProprietaryProtocol::JC2M)),
"warsow" => game!("Warsow", 44400, Protocol::Quake(QuakeVersion::Three)),
"dhe4445" => game!("Darkest Hour: Europe '44-'45 (2008)", 7758, Protocol::Unreal2),
"devastation" => game!("Devastation (2003)", 7778, Protocol::Unreal2),
"killingfloor" => game!("Killing Floor", 7708, Protocol::Unreal2),
"redorchestra" => game!("Red Orchestra", 7759, Protocol::Unreal2),
"unrealtournament2003" => game!("Unreal Tournament 2003", 7758, Protocol::Unreal2),
"unrealtournament2004" => game!("Unreal Tournament 2004", 7778, Protocol::Unreal2),
"eco" => game!("Eco", 3000, Protocol::PROPRIETARY(ProprietaryProtocol::Eco)),
"zps" => game!("Zombie Panic: Source", 27015, Protocol::Valve(Engine::new(17_500))),
"moe" => game!("Myth Of Empires", 12888, Protocol::Valve(Engine::new(1_371_580))),
"mordhau" => game!("Mordhau", 27015, Protocol::Valve(Engine::new(629_760))),
"mindustry" => game!("Mindustry", crate::games::mindustry::DEFAULT_PORT, Protocol::PROPRIETARY(ProprietaryProtocol::Mindustry)),
"nla" => game!("Nova-Life: Amboise", 27015, Protocol::Valve(Engine::new(885_570))),
};

View file

@ -1,8 +0,0 @@
/// The implementation.
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/ffow.js)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -1,37 +0,0 @@
use crate::eco::{EcoRequestSettings, Response, Root};
use crate::http::HttpClient;
use crate::{GDResult, TimeoutSettings};
use std::net::{IpAddr, SocketAddr};
/// Query an eco server.
#[inline]
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, &None) }
/// Query an eco server.
#[inline]
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: &Option<TimeoutSettings>,
) -> GDResult<Response> {
query_with_timeout_and_extra_settings(address, port, timeout_settings, None)
}
/// Query an eco server.
pub fn query_with_timeout_and_extra_settings(
address: &IpAddr,
port: Option<u16>,
timeout_settings: &Option<TimeoutSettings>,
extra_settings: Option<EcoRequestSettings>,
) -> GDResult<Response> {
let address = &SocketAddr::new(*address, port.unwrap_or(3001));
let mut client = HttpClient::new(
address,
timeout_settings,
extra_settings.unwrap_or_default().into(),
)?;
let response = client.get_json::<Root>("/frontpage", None)?;
Ok(response.into())
}

View file

@ -1,241 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::http::{HttpProtocol, HttpSettings};
use crate::protocols::types::{CommonPlayer, CommonResponse};
use crate::ExtraRequestSettings;
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
#[serde(rename = "Info")]
pub info: Info,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Info {
#[serde(rename = "External")]
pub external: bool,
#[serde(rename = "GamePort")]
pub game_port: u32,
#[serde(rename = "WebPort")]
pub web_port: u32,
#[serde(rename = "IsLAN")]
pub is_lan: bool,
#[serde(rename = "Description")]
pub description: String,
#[serde(rename = "DetailedDescription")]
pub detailed_description: String,
#[serde(rename = "Category")]
pub category: String,
#[serde(rename = "OnlinePlayers")]
pub online_players: u32,
#[serde(rename = "TotalPlayers")]
pub total_players: u32,
#[serde(rename = "OnlinePlayersNames")]
pub online_players_names: Vec<String>,
#[serde(rename = "AdminOnline")]
pub admin_online: bool,
#[serde(rename = "TimeSinceStart")]
pub time_since_start: f64,
#[serde(rename = "TimeLeft")]
pub time_left: f64,
#[serde(rename = "Animals")]
pub animals: u32,
#[serde(rename = "Plants")]
pub plants: u32,
#[serde(rename = "Laws")]
pub laws: u32,
#[serde(rename = "WorldSize")]
pub world_size: String,
#[serde(rename = "Version")]
pub version: String,
#[serde(rename = "EconomyDesc")]
pub economy_desc: String,
#[serde(rename = "SkillSpecializationSetting")]
pub skill_specialization_setting: String,
#[serde(rename = "Language")]
pub language: String,
#[serde(rename = "HasPassword")]
pub has_password: bool,
#[serde(rename = "HasMeteor")]
pub has_meteor: bool,
#[serde(rename = "DistributionStationItems")]
pub distribution_station_items: String,
#[serde(rename = "Playtimes")]
pub playtimes: String,
#[serde(rename = "DiscordAddress")]
pub discord_address: String,
#[serde(rename = "IsPaused")]
pub is_paused: bool,
#[serde(rename = "ActiveAndOnlinePlayers")]
pub active_and_online_players: u32,
#[serde(rename = "PeakActivePlayers")]
pub peak_active_players: u32,
#[serde(rename = "MaxActivePlayers")]
pub max_active_players: u32,
#[serde(rename = "ShelfLifeMultiplier")]
pub shelf_life_multiplier: f64,
#[serde(rename = "ExhaustionAfterHours")]
pub exhaustion_after_hours: f64,
#[serde(rename = "IsLimitingHours")]
pub is_limiting_hours: bool,
#[serde(rename = "ServerAchievementsDict")]
pub server_achievements_dict: HashMap<String, String>,
#[serde(rename = "RelayAddress")]
pub relay_address: String,
#[serde(rename = "Access")]
pub access: String,
#[serde(rename = "JoinUrl")]
pub join_url: String,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub name: String,
}
impl CommonPlayer for Player {
fn as_original(&self) -> crate::protocols::types::GenericPlayer<'_> {
crate::protocols::types::GenericPlayer::Eco(self)
}
fn name(&self) -> &str { &self.name }
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct Response {
pub external: bool,
pub port: u32,
pub query_port: u32,
pub is_lan: bool,
pub description: String, // this and other fields require some text filtering
pub description_detailed: String,
pub description_economy: String,
pub category: String,
pub players_online: u32,
pub players_maximum: u32,
pub players: Vec<Player>,
pub admin_online: bool,
pub time_since_start: f64,
pub time_left: f64,
pub animals: u32,
pub plants: u32,
pub laws: u32,
pub world_size: String,
pub game_version: String,
pub skill_specialization_setting: String,
pub language: String,
pub has_password: bool,
pub has_meteor: bool,
pub distribution_station_items: String,
pub playtimes: String,
pub discord_address: String,
pub is_paused: bool,
pub active_and_online_players: u32,
pub peak_active_players: u32,
pub max_active_players: u32,
pub shelf_life_multiplier: f64,
pub exhaustion_after_hours: f64,
pub is_limiting_hours: bool,
pub server_achievements_dict: HashMap<String, String>,
pub relay_address: String,
pub access: String,
pub connect: String,
}
impl From<Root> for Response {
fn from(root: Root) -> Self {
let value = root.info;
Self {
external: value.external,
port: value.game_port,
query_port: value.web_port,
is_lan: value.is_lan,
description: value.description,
description_detailed: value.detailed_description,
description_economy: value.economy_desc,
category: value.category,
players_online: value.online_players,
players_maximum: value.total_players,
players: value
.online_players_names
.iter()
.map(|player| {
Player {
name: player.clone(),
}
})
.collect(),
admin_online: value.admin_online,
time_since_start: value.time_since_start,
time_left: value.time_left,
animals: value.animals,
plants: value.plants,
laws: value.laws,
world_size: value.world_size,
game_version: value.version,
skill_specialization_setting: value.skill_specialization_setting,
language: value.language,
has_password: value.has_password,
has_meteor: value.has_meteor,
distribution_station_items: value.distribution_station_items,
playtimes: value.playtimes,
discord_address: value.discord_address,
is_paused: value.is_paused,
active_and_online_players: value.active_and_online_players,
peak_active_players: value.peak_active_players,
max_active_players: value.max_active_players,
shelf_life_multiplier: value.shelf_life_multiplier,
exhaustion_after_hours: value.exhaustion_after_hours,
is_limiting_hours: value.is_limiting_hours,
server_achievements_dict: value.server_achievements_dict,
relay_address: value.relay_address,
access: value.access,
connect: value.join_url,
}
}
}
impl CommonResponse for Response {
fn as_original(&self) -> crate::protocols::GenericResponse<'_> { crate::protocols::GenericResponse::Eco(self) }
fn players_online(&self) -> u32 { self.players_online }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> { Some(self.players.iter().map(|p| p as _).collect()) }
}
/// Extra request settings for eco queries.
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct EcoRequestSettings {
hostname: Option<String>,
}
impl From<ExtraRequestSettings> for EcoRequestSettings {
fn from(value: ExtraRequestSettings) -> Self {
Self {
hostname: value.hostname,
}
}
}
impl From<EcoRequestSettings> for HttpSettings<String> {
fn from(value: EcoRequestSettings) -> Self {
Self {
protocol: HttpProtocol::Http,
hostname: value.hostname,
headers: Vec::with_capacity(0),
}
}
}

View file

@ -1,15 +0,0 @@
//! Unreal2 game query modules
use crate::protocols::epic::game_query_mod;
game_query_mod!(
asa,
"Ark: Survival Ascended",
7777,
Credentials {
deployment: "ad9a8feffb3b4b2ca315546f038c3ae2",
id: "xyza7891muomRmynIIHaJB9COBKkwj6n",
secret: "PP5UGxysEieNfSrEicaD1N2Bb3TdXuD7xHYcsdUHZ7s",
auth_by_external: false,
}
);

View file

@ -1,8 +0,0 @@
/// The implementation.
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/ffow.js)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -1,66 +0,0 @@
use crate::buffer::{Buffer, Utf8Decoder};
use crate::games::ffow::types::Response;
use crate::protocols::types::TimeoutSettings;
use crate::protocols::valve::{Engine, Environment, Server, ValveProtocol};
use crate::GDResult;
use byteorder::LittleEndian;
use std::net::{IpAddr, SocketAddr};
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let mut client = ValveProtocol::new(
&SocketAddr::new(*address, port.unwrap_or(5478)),
timeout_settings,
)?;
let data = client.get_request_data(
&Engine::GoldSrc(true),
0,
0x46,
String::from("LSQ").into_bytes(),
)?;
let mut buffer = Buffer::<LittleEndian>::new(&data);
let protocol_version = buffer.read::<u8>()?;
let name = buffer.read_string::<Utf8Decoder>(None)?;
let map = buffer.read_string::<Utf8Decoder>(None)?;
let active_mod = buffer.read_string::<Utf8Decoder>(None)?;
let game_mode = buffer.read_string::<Utf8Decoder>(None)?;
let description = buffer.read_string::<Utf8Decoder>(None)?;
let game_version = buffer.read_string::<Utf8Decoder>(None)?;
buffer.move_cursor(2)?;
let players_online = buffer.read::<u8>()?;
let players_maximum = buffer.read::<u8>()?;
let server_type = Server::from_gldsrc(buffer.read::<u8>()?)?;
let environment_type = Environment::from_gldsrc(buffer.read::<u8>()?)?;
let has_password = buffer.read::<u8>()? == 1;
let vac_secured = buffer.read::<u8>()? == 1;
buffer.move_cursor(1)?; // average fps
let round = buffer.read::<u8>()?;
let rounds_maximum = buffer.read::<u8>()?;
let time_left = buffer.read::<u16>()?;
Ok(Response {
protocol_version,
name,
active_mod,
game_mode,
game_version,
description,
map,
players_online,
players_maximum,
server_type,
environment_type,
has_password,
vac_secured,
round,
rounds_maximum,
time_left,
})
}

View file

@ -1,56 +0,0 @@
use crate::protocols::types::CommonResponse;
use crate::protocols::valve::{Environment, Server};
use crate::protocols::GenericResponse;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// The query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Response {
/// Protocol used by the server.
pub protocol_version: u8,
/// Name of the server.
pub name: String,
/// Map name.
pub active_mod: String,
/// Running game mode.
pub game_mode: String,
/// The version that the server is running on.
pub game_version: String,
/// Description of the server.
pub description: String,
/// Current map.
pub map: String,
/// Number of players on the server.
pub players_online: u8,
/// Maximum number of players the server reports it can hold.
pub players_maximum: u8,
/// Dedicated, NonDedicated or SourceTV
pub server_type: Server,
/// The Operating System that the server is on.
pub environment_type: Environment,
/// Indicates whether the server requires a password.
pub has_password: bool,
/// Indicates whether the server uses VAC.
pub vac_secured: bool,
/// Current round index.
pub round: u8,
/// Maximum amount of rounds.
pub rounds_maximum: u8,
/// Time left for the current round in seconds.
pub time_left: u16,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse<'_> { GenericResponse::FFOW(self) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players_maximum(&self) -> u32 { self.players_maximum.into() }
fn players_online(&self) -> u32 { self.players_online.into() }
}

View file

@ -1,9 +0,0 @@
//! Gamespy game query modules
use crate::protocols::gamespy::game_query_mod;
game_query_mod!(battlefield1942, "Battlefield 1942", one, 23000);
game_query_mod!(crysiswars, "Crysis Wars", three, 64100);
game_query_mod!(hce, "Halo: Combat Evolved", two, 2302);
game_query_mod!(serioussam, "Serious Sam", one, 25601);
game_query_mod!(unrealtournament, "Unreal Tournament", one, 7778);

View file

@ -1,8 +0,0 @@
/// The implementation.
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/jc2mp.js)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -1,75 +0,0 @@
use crate::buffer::{Buffer, Utf8Decoder};
use crate::jc2m::{Player, Response};
use crate::protocols::gamespy::common::has_password;
use crate::protocols::gamespy::three::{data_to_map, GameSpy3};
use crate::protocols::types::TimeoutSettings;
use crate::GDErrorKind::{PacketBad, TypeParse};
use crate::GDResult;
use byteorder::BigEndian;
use std::net::{IpAddr, SocketAddr};
fn parse_players_and_teams(packet: &[u8]) -> GDResult<Vec<Player>> {
let mut buf = Buffer::<BigEndian>::new(packet);
let count = buf.read::<u16>()?;
let mut players = Vec::with_capacity(count as usize);
while buf.remaining_length() != 0 {
players.push(Player {
name: buf.read_string::<Utf8Decoder>(None)?,
steam_id: buf.read_string::<Utf8Decoder>(None)?,
ping: buf.read::<u16>()?,
});
}
Ok(players)
}
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let mut client = GameSpy3::new_custom(
&SocketAddr::new(*address, port.unwrap_or(7777)),
timeout_settings,
[0xFF, 0xFF, 0xFF, 0x02],
true,
)?;
let packets = client.get_server_packets()?;
let data = packets
.first()
.ok_or_else(|| PacketBad.context("First packet missing"))?;
let (mut server_vars, remaining_data) = data_to_map(data)?;
let players = parse_players_and_teams(&remaining_data)?;
let players_maximum = server_vars
.remove("maxplayers")
.ok_or_else(|| PacketBad.context("Server variables missing maxplayers"))?
.parse()
.map_err(|e| TypeParse.context(e))?;
let players_online = match server_vars.remove("numplayers") {
None => players.len(),
Some(v) => {
let reported_players = v.parse().map_err(|e| TypeParse.context(e))?;
match reported_players < players.len() {
true => players.len(),
false => reported_players,
}
}
} as u32;
Ok(Response {
game_version: server_vars.remove("version").ok_or(PacketBad)?,
description: server_vars.remove("description").ok_or(PacketBad)?,
name: server_vars.remove("hostname").ok_or(PacketBad)?,
has_password: has_password(&mut server_vars)?,
players,
players_maximum,
players_online,
})
}

View file

@ -1,50 +0,0 @@
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
use crate::protocols::GenericResponse;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub name: String,
pub steam_id: String,
pub ping: u16,
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer<'_> { GenericPlayer::JCMP2(self) }
fn name(&self) -> &str { &self.name }
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Response {
pub game_version: String,
pub description: String,
pub name: String,
pub has_password: bool,
pub players: Vec<Player>,
pub players_maximum: u32,
pub players_online: u32,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse<'_> { GenericResponse::JC2M(self) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}

View file

@ -1,25 +0,0 @@
//! Mindustry game ping (v146)
//!
//! [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/ArcNetProvider.java#L225-L259)
use std::{net::IpAddr, net::SocketAddr};
use crate::{GDResult, TimeoutSettings};
use self::types::ServerData;
pub mod types;
pub mod protocol;
/// Default mindustry server port
///
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/Vars.java#L141-L142)
pub const DEFAULT_PORT: u16 = 6567;
/// Query a mindustry server.
pub fn query(ip: &IpAddr, port: Option<u16>, timeout_settings: &Option<TimeoutSettings>) -> GDResult<ServerData> {
let address = SocketAddr::new(*ip, port.unwrap_or(DEFAULT_PORT));
protocol::query_with_retries(&address, timeout_settings)
}

View file

@ -1,58 +0,0 @@
use std::net::SocketAddr;
use crate::{
buffer::{self, Buffer},
socket::{Socket, UdpSocket},
utils,
GDResult,
TimeoutSettings,
};
use super::types::ServerData;
/// Mindustry max datagram packet size.
pub const MAX_BUFFER_SIZE: usize = 500;
/// Send a ping packet.
///
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/ArcNetProvider.java#L248)
pub(crate) fn send_ping(socket: &mut UdpSocket) -> GDResult<()> { socket.send(&[-2i8 as u8, 1i8 as u8]) }
/// Parse server data.
///
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/NetworkIO.java#L122-L135)
pub fn parse_server_data<B: byteorder::ByteOrder, D: buffer::StringDecoder>(
buffer: &mut Buffer<B>,
) -> GDResult<ServerData> {
Ok(ServerData {
host: buffer.read_string::<D>(None)?,
map: buffer.read_string::<D>(None)?,
players: buffer.read()?,
wave: buffer.read()?,
version: buffer.read()?,
version_type: buffer.read_string::<D>(None)?,
gamemode: buffer.read::<u8>()?.try_into()?,
player_limit: buffer.read()?,
description: buffer.read_string::<D>(None)?,
mode_name: buffer.read_string::<D>(None).ok(),
})
}
/// Query a Mindustry server (without retries).
pub fn query(address: &SocketAddr, timeout_settings: &Option<TimeoutSettings>) -> GDResult<ServerData> {
let mut socket = UdpSocket::new(address, timeout_settings)?;
send_ping(&mut socket)?;
let socket_data = socket.receive(Some(MAX_BUFFER_SIZE))?;
let mut buffer = Buffer::new(&socket_data);
parse_server_data::<byteorder::BigEndian, buffer::Utf8LengthPrefixedDecoder>(&mut buffer)
}
/// Query a Mindustry server.
pub fn query_with_retries(address: &SocketAddr, timeout_settings: &Option<TimeoutSettings>) -> GDResult<ServerData> {
let retries = TimeoutSettings::get_retries_or_default(timeout_settings);
utils::retry_on_timeout(retries, || query(address, timeout_settings))
}

View file

@ -1,107 +0,0 @@
use crate::{
protocols::types::{CommonResponse, GenericResponse},
GDErrorKind,
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// Mindustry sever data
///
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/net/NetworkIO.java#L122-L135)
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ServerData {
pub host: String,
pub map: String,
pub players: i32,
pub wave: i32,
pub version: i32,
pub version_type: String,
pub gamemode: GameMode,
pub player_limit: i32,
pub description: String,
pub mode_name: Option<String>,
}
/// Mindustry game mode
///
/// [Reference](https://github.com/Anuken/Mindustry/blob/a2e5fbdedb2fc1c8d3c157bf344d10ad6d321442/core/src/mindustry/game/Gamemode.java)
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum GameMode {
Survival,
Sandbox,
Attack,
PVP,
Editor,
}
impl TryFrom<u8> for GameMode {
type Error = GDErrorKind;
fn try_from(value: u8) -> Result<Self, Self::Error> {
use GameMode::*;
Ok(match value {
0 => Survival,
1 => Sandbox,
2 => Attack,
3 => PVP,
4 => Editor,
_ => return Err(GDErrorKind::TypeParse),
})
}
}
impl GameMode {
const fn as_str(&self) -> &'static str {
match self {
Self::Survival => "survival",
Self::Sandbox => "sandbox",
Self::Attack => "attack",
Self::PVP => "pvp",
Self::Editor => "editor",
}
}
}
impl CommonResponse for ServerData {
fn as_original(&self) -> GenericResponse<'_> { GenericResponse::Mindustry(self) }
fn players_online(&self) -> u32 { self.players.try_into().unwrap_or(0) }
fn players_maximum(&self) -> u32 { self.player_limit.try_into().unwrap_or(0) }
fn game_mode(&self) -> Option<&str> { Some(self.gamemode.as_str()) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn description(&self) -> Option<&str> { Some(&self.description) }
}
#[cfg(test)]
mod test {
use crate::protocols::types::CommonResponse;
use super::ServerData;
#[test]
fn common_impl() {
let data = ServerData {
host: String::from("host"),
map: String::from("map"),
players: 5,
wave: 2,
version: 142,
version_type: String::from("steam"),
gamemode: super::GameMode::PVP,
player_limit: 20,
description: String::from("description"),
mode_name: Some(String::from("campaign")),
};
let common: &dyn CommonResponse = &data;
assert_eq!(common.players_online(), 5);
assert_eq!(common.players_maximum(), 20);
assert_eq!(common.game_mode(), Some("pvp"));
assert_eq!(common.map(), Some("map"));
assert_eq!(common.description(), Some("description"));
}
}

View file

@ -1,69 +0,0 @@
/// The implementation.
/// Reference: [Server List Ping](https://wiki.vg/Server_List_Ping)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
#[allow(unused_imports)]
pub use protocol::*;
pub use types::*;
use crate::{GDErrorKind, GDResult};
use std::net::{IpAddr, SocketAddr};
/// Query with all the protocol variants one by one (Java -> Bedrock -> Legacy
/// (1.6 -> 1.4 -> Beta 1.8)).
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<JavaResponse> {
if let Ok(response) = query_java(address, port, None) {
return Ok(response);
}
if let Ok(response) = query_bedrock(address, port) {
return Ok(JavaResponse::from_bedrock_response(response));
}
if let Ok(response) = query_legacy(address, port) {
return Ok(response);
}
Err(GDErrorKind::AutoQuery.into())
}
/// Query a Java Server.
pub fn query_java(
address: &IpAddr,
port: Option<u16>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
protocol::query_java(
&SocketAddr::new(*address, port_or_java_default(port)),
None,
request_settings,
)
}
/// Query a (Java) Legacy Server (1.6 -> 1.4 -> Beta 1.8).
pub fn query_legacy(address: &IpAddr, port: Option<u16>) -> GDResult<JavaResponse> {
protocol::query_legacy(&SocketAddr::new(*address, port_or_java_default(port)), None)
}
/// Query a specific (Java) Legacy Server.
pub fn query_legacy_specific(group: LegacyGroup, address: &IpAddr, port: Option<u16>) -> GDResult<JavaResponse> {
protocol::query_legacy_specific(
group,
&SocketAddr::new(*address, port_or_java_default(port)),
None,
)
}
/// Query a Bedrock Server.
pub fn query_bedrock(address: &IpAddr, port: Option<u16>) -> GDResult<BedrockResponse> {
protocol::query_bedrock(
&SocketAddr::new(*address, port_or_bedrock_default(port)),
None,
)
}
fn port_or_java_default(port: Option<u16>) -> u16 { port.unwrap_or(25565) }
fn port_or_bedrock_default(port: Option<u16>) -> u16 { port.unwrap_or(19132) }

View file

@ -1,111 +0,0 @@
// This file has code that has been documented by the NodeJS GameDig library
// (MIT) from https://github.com/gamedig/node-gamedig/blob/master/protocols/minecraftbedrock.js
use crate::{
buffer::{Buffer, Utf8Decoder},
games::minecraft::{BedrockResponse, GameMode, Server},
protocols::types::TimeoutSettings,
socket::{Socket, UdpSocket},
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, TypeParse},
GDResult,
};
use std::net::SocketAddr;
use byteorder::LittleEndian;
pub struct Bedrock {
socket: UdpSocket,
retry_count: usize,
}
impl Bedrock {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = UdpSocket::new(address, &timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
retry_count,
})
}
fn send_status_request(&mut self) -> GDResult<()> {
self.socket.send(&[
0x01, // Message ID: ID_UNCONNECTED_PING
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, // Nonce / timestamp
0x00, 0xff, 0xff, 0x00, 0xfe, 0xfe, 0xfe, 0xfe, 0xfd, 0xfd, 0xfd, 0xfd, 0x12, 0x34, // Magic
0x56, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Client GUID
])?;
Ok(())
}
/// Send a status request, and parse the response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<BedrockResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send a status request, and parse the response (without retry logic).
fn get_info_impl(&mut self) -> GDResult<BedrockResponse> {
self.send_status_request()?;
let received = self.socket.receive(None)?;
let mut buffer = Buffer::<LittleEndian>::new(&received);
if buffer.read::<u8>()? != 0x1c {
return Err(PacketBad.context("Expected 0x1c"));
}
// Checking for our nonce directly from a u64 (as the nonce is 8 bytes).
if buffer.read::<u64>()? != 9_833_440_827_789_222_417 {
return Err(PacketBad.context("Invalid nonce"));
}
// These 8 bytes are identical to the serverId string we receive in decimal
// below
buffer.move_cursor(8)?;
// Verifying the magic value (as we need 16 bytes, cast to two u64 values)
if buffer.read::<u64>()? != 18_374_403_896_610_127_616 {
return Err(PacketBad.context("Invalid magic"));
}
if buffer.read::<u64>()? != 8_671_175_388_723_805_693 {
return Err(PacketBad.context("Invalid magic"));
}
let remaining_length = buffer.switch_endian_chunk(2)?.read::<u16>()? as usize;
error_by_expected_size(remaining_length, buffer.remaining_length())?;
let binding = buffer.read_string::<Utf8Decoder>(None)?;
let status: Vec<&str> = binding.split(';').collect();
// We must have at least 6 values
if status.len() < 6 {
return Err(PacketBad.context("Not enough values"));
}
Ok(BedrockResponse {
edition: status[0].to_string(),
name: status[1].to_string(),
version_name: status[3].to_string(),
protocol_version: status[2].to_string(),
players_maximum: status[5].parse().map_err(|e| TypeParse.context(e))?,
players_online: status[4].parse().map_err(|e| TypeParse.context(e))?,
id: status.get(6).map(std::string::ToString::to_string),
map: status.get(7).map(std::string::ToString::to_string),
game_mode: match status.get(8) {
None => None,
Some(v) => Some(GameMode::from_bedrock(v)?),
},
server_type: Server::Bedrock,
})
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<BedrockResponse> {
Self::new(address, timeout_settings)?.get_info()
}
}

View file

@ -1,178 +0,0 @@
use crate::{
buffer::Buffer,
games::minecraft::{as_string, as_varint, get_string, get_varint, JavaResponse, Player, RequestSettings, Server},
protocols::types::TimeoutSettings,
socket::{Socket, TcpSocket},
utils::retry_on_timeout,
GDErrorKind::{JsonParse, PacketBad},
GDResult,
};
use byteorder::LittleEndian;
use serde_json::Value;
use std::net::SocketAddr;
use std::time::{SystemTime, UNIX_EPOCH};
pub struct Java {
socket: TcpSocket,
request_settings: RequestSettings,
retry_count: usize,
}
impl Java {
fn new(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<Self> {
let socket = TcpSocket::new(address, &timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
request_settings: request_settings.unwrap_or_default(),
retry_count,
})
}
fn send(&mut self, data: Vec<u8>) -> GDResult<()> {
self.socket
.send(&[as_varint(data.len() as i32), data].concat())
}
fn receive(&mut self) -> GDResult<Vec<u8>> {
let data = &self.socket.receive(None)?;
let mut buffer = Buffer::<LittleEndian>::new(data);
let _packet_length = get_varint(&mut buffer)? as usize;
// this declared 'packet length' from within the packet might be wrong (?), not
// checking with it...
Ok(buffer.remaining_bytes().to_vec())
}
fn send_handshake(&mut self) -> GDResult<()> {
let handshake_payload = [
&[
// Packet ID (0)
0x00,
], // Protocol Version (-1 to determine version)
as_varint(self.request_settings.protocol_version).as_slice(),
// Server address (can be anything)
as_string(&self.request_settings.hostname)?.as_slice(),
// Server port (can be anything)
&self.socket.port().to_le_bytes(),
&[
// Next state (1 for status)
0x01,
],
]
.concat();
self.send(handshake_payload)?;
Ok(())
}
fn send_status_request(&mut self) -> GDResult<()> {
self.send(
[0x00] // Packet ID (0)
.to_vec(),
)?;
Ok(())
}
fn send_ping_request(&mut self) -> GDResult<()> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
let mut payload = [0x01].to_vec(); // Packet ID (1)
payload.extend_from_slice(&timestamp.to_be_bytes()); // Timestamp (long, 8 bytes)
self.send(payload)?;
Ok(())
}
/// Send minecraft ping request and parse the response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<JavaResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send minecraft ping request and parse the response (without retry
/// logic).
fn get_info_impl(&mut self) -> GDResult<JavaResponse> {
self.send_handshake()?;
self.send_status_request()?;
self.send_ping_request()?;
let socket_data = self.receive()?;
let mut buffer = Buffer::<LittleEndian>::new(&socket_data);
if get_varint(&mut buffer)? != 0 {
// first var int is the packet id
return Err(PacketBad.context("Expected 0"));
}
let json_response = get_string(&mut buffer)?;
let value_response: Value = serde_json::from_str(&json_response).map_err(|e| JsonParse.context(e))?;
let game_version = value_response["version"]["name"]
.as_str()
.ok_or(PacketBad)?
.to_string();
let protocol_version = value_response["version"]["protocol"]
.as_i64()
.ok_or(PacketBad)? as i32;
let max_players = value_response["players"]["max"].as_u64().ok_or(PacketBad)? as u32;
let online_players = value_response["players"]["online"]
.as_u64()
.ok_or(PacketBad)? as u32;
let players: Option<Vec<Player>> = match value_response["players"]["sample"].is_null() {
true => None,
false => {
Some({
let players_values = value_response["players"]["sample"]
.as_array()
.ok_or(PacketBad)?;
let mut players = Vec::with_capacity(players_values.len());
for player in players_values {
players.push(Player {
name: player["name"].as_str().ok_or(PacketBad)?.to_string(),
id: player["id"].as_str().ok_or(PacketBad)?.to_string(),
});
}
players
})
}
};
Ok(JavaResponse {
game_version,
protocol_version,
players_maximum: max_players,
players_online: online_players,
players,
description: value_response["description"].to_string(),
favicon: value_response["favicon"].as_str().map(str::to_string),
previews_chat: value_response["previewsChat"].as_bool(),
enforces_secure_chat: value_response["enforcesSecureChat"].as_bool(),
server_type: Server::Java,
})
}
pub fn query(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings, request_settings)?.get_info()
}
}

View file

@ -1,83 +0,0 @@
use byteorder::BigEndian;
use crate::minecraft::protocol::legacy_v1_6::LegacyV1_6;
use crate::{
buffer::{Buffer, Utf16Decoder},
games::minecraft::{JavaResponse, LegacyGroup, Server},
protocols::types::TimeoutSettings,
socket::{Socket, TcpSocket},
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, ProtocolFormat},
GDResult,
};
use std::net::SocketAddr;
pub struct LegacyV1_4 {
socket: TcpSocket,
retry_count: usize,
}
impl LegacyV1_4 {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = TcpSocket::new(address, &timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
retry_count,
})
}
fn send_initial_request(&mut self) -> GDResult<()> { self.socket.send(&[0xFE, 0x01]) }
/// Send info request and parse response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<JavaResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send info request and parse response (without retry logic).
fn get_info_impl(&mut self) -> GDResult<JavaResponse> {
self.send_initial_request()?;
let data = self.socket.receive(None)?;
let mut buffer = Buffer::<BigEndian>::new(&data);
if buffer.read::<u8>()? != 0xFF {
return Err(ProtocolFormat.context("Expected 0xFF"));
}
let length = buffer.read::<u16>()? * 2;
error_by_expected_size((length + 3) as usize, data.len())?;
if LegacyV1_6::is_protocol(&mut buffer)? {
return LegacyV1_6::get_response(&mut buffer);
}
let packet_string = buffer.read_string::<Utf16Decoder<BigEndian>>(None)?;
let split: Vec<&str> = packet_string.split('§').collect();
error_by_expected_size(3, split.len())?;
let description = split[0].to_string();
let online_players = split[1].parse().map_err(|e| PacketBad.context(e))?;
let max_players = split[2].parse().map_err(|e| PacketBad.context(e))?;
Ok(JavaResponse {
game_version: "1.4+".to_string(),
protocol_version: -1,
players_maximum: max_players,
players_online: online_players,
players: None,
description,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Legacy(LegacyGroup::V1_4),
})
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings)?.get_info()
}
}

View file

@ -1,116 +0,0 @@
use byteorder::BigEndian;
use crate::{
buffer::{Buffer, Utf16Decoder},
games::minecraft::{JavaResponse, LegacyGroup, Server},
protocols::types::TimeoutSettings,
socket::{Socket, TcpSocket},
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, ProtocolFormat},
GDResult,
};
use std::net::SocketAddr;
pub struct LegacyV1_6 {
socket: TcpSocket,
retry_count: usize,
}
impl LegacyV1_6 {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = TcpSocket::new(address, &timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
retry_count,
})
}
fn send_initial_request(&mut self) -> GDResult<()> {
self.socket.send(&[
0xfe, // Packet ID (FE)
0x01, // Ping payload (01)
0xfa, // Packet identifier for plugin message
0x00, 0x07, // Length of 'GameDig' string (7) as unsigned short
0x00, 0x47, 0x00, 0x61, 0x00, 0x6D, 0x00, 0x65, 0x00, 0x44, 0x00, 0x69, 0x00,
0x67, // 'GameDig' string as UTF-16BE
])?;
Ok(())
}
pub(crate) fn is_protocol(buffer: &mut Buffer<BigEndian>) -> GDResult<bool> {
let state = buffer
.remaining_bytes()
.starts_with(&[0x00, 0xA7, 0x00, 0x31, 0x00, 0x00]);
if state {
buffer.move_cursor(6)?;
}
Ok(state)
}
pub(crate) fn get_response(buffer: &mut Buffer<BigEndian>) -> GDResult<JavaResponse> {
// This is a specific order!
let protocol_version = buffer
.read_string::<Utf16Decoder<BigEndian>>(None)?
.parse()
.map_err(|e| PacketBad.context(e))?;
let game_version = buffer.read_string::<Utf16Decoder<BigEndian>>(None)?;
let description = buffer.read_string::<Utf16Decoder<BigEndian>>(None)?;
let online_players = buffer
.read_string::<Utf16Decoder<BigEndian>>(None)?
.parse()
.map_err(|e| PacketBad.context(e))?;
let max_players = buffer
.read_string::<Utf16Decoder<BigEndian>>(None)?
.parse()
.map_err(|e| PacketBad.context(e))?;
Ok(JavaResponse {
game_version,
protocol_version,
players_maximum: max_players,
players_online: online_players,
players: None,
description,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Legacy(LegacyGroup::V1_6),
})
}
/// Send info request and parse response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<JavaResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send info request and parse response (without retry logic).
fn get_info_impl(&mut self) -> GDResult<JavaResponse> {
self.send_initial_request()?;
let data = self.socket.receive(None)?;
let mut buffer = Buffer::<BigEndian>::new(&data);
if buffer.read::<u8>()? != 0xFF {
return Err(ProtocolFormat.context("Expected 0xFF"));
}
let length = buffer.read::<u16>()? * 2;
error_by_expected_size((length + 3) as usize, data.len())?;
if !Self::is_protocol(&mut buffer)? {
return Err(ProtocolFormat.context("Not legacy 1.6 protocol"));
}
Self::get_response(&mut buffer)
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings)?.get_info()
}
}

View file

@ -1,79 +0,0 @@
use crate::{
buffer::{Buffer, Utf16Decoder},
games::minecraft::{JavaResponse, LegacyGroup, Server},
protocols::types::TimeoutSettings,
socket::{Socket, TcpSocket},
utils::{error_by_expected_size, retry_on_timeout},
GDErrorKind::{PacketBad, ProtocolFormat},
GDResult,
};
use std::net::SocketAddr;
use byteorder::BigEndian;
pub struct LegacyVB1_8 {
socket: TcpSocket,
retry_count: usize,
}
impl LegacyVB1_8 {
fn new(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<Self> {
let socket = TcpSocket::new(address, &timeout_settings)?;
let retry_count = TimeoutSettings::get_retries_or_default(&timeout_settings);
Ok(Self {
socket,
retry_count,
})
}
fn send_initial_request(&mut self) -> GDResult<()> { self.socket.send(&[0xFE]) }
/// Send request for info and parse response.
/// This function will retry fetch on timeouts.
fn get_info(&mut self) -> GDResult<JavaResponse> {
retry_on_timeout(self.retry_count, move || self.get_info_impl())
}
/// Send request for info and parse response (without retry logic).
fn get_info_impl(&mut self) -> GDResult<JavaResponse> {
self.send_initial_request()?;
let data = self.socket.receive(None)?;
let mut buffer = Buffer::<BigEndian>::new(&data);
if buffer.read::<u8>()? != 0xFF {
return Err(ProtocolFormat.context("Expected 0xFF"));
}
let length = buffer.read::<u16>()? * 2;
error_by_expected_size((length + 3) as usize, data.len())?;
let packet_string = buffer.read_string::<Utf16Decoder<BigEndian>>(None)?;
let split: Vec<&str> = packet_string.split('§').collect();
error_by_expected_size(3, split.len())?;
let description = split[0].to_string();
let online_players = split[1].parse().map_err(|e| PacketBad.context(e))?;
let max_players = split[2].parse().map_err(|e| PacketBad.context(e))?;
Ok(JavaResponse {
game_version: "Beta 1.8+".to_string(),
protocol_version: -1,
players_maximum: max_players,
players_online: online_players,
players: None,
description,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Legacy(LegacyGroup::VB1_8),
})
}
pub fn query(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
Self::new(address, timeout_settings)?.get_info()
}
}

View file

@ -1,91 +0,0 @@
use crate::games::minecraft::types::RequestSettings;
use crate::{
games::minecraft::{
protocol::{
bedrock::Bedrock,
java::Java,
legacy_v1_4::LegacyV1_4,
legacy_v1_6::LegacyV1_6,
legacy_vb1_8::LegacyVB1_8,
},
BedrockResponse,
JavaResponse,
LegacyGroup,
},
protocols::types::TimeoutSettings,
GDErrorKind::AutoQuery,
GDResult,
};
use std::net::SocketAddr;
mod bedrock;
mod java;
mod legacy_v1_4;
mod legacy_v1_6;
mod legacy_vb1_8;
/// Queries a Minecraft server with all the protocol variants one by one (Java
/// -> Bedrock -> Legacy (1.6 -> 1.4 -> Beta 1.8)).
pub fn query(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
if let Ok(response) = query_java(address, timeout_settings, request_settings) {
return Ok(response);
}
if let Ok(response) = query_bedrock(address, timeout_settings) {
return Ok(JavaResponse::from_bedrock_response(response));
}
if let Ok(response) = query_legacy(address, timeout_settings) {
return Ok(response);
}
Err(AutoQuery.into())
}
/// Query a Java Server.
pub fn query_java(
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
request_settings: Option<RequestSettings>,
) -> GDResult<JavaResponse> {
Java::query(address, timeout_settings, request_settings)
}
/// Query a (Java) Legacy Server (1.6 -> 1.4 -> Beta 1.8).
pub fn query_legacy(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<JavaResponse> {
if let Ok(response) = query_legacy_specific(LegacyGroup::V1_6, address, timeout_settings) {
return Ok(response);
}
if let Ok(response) = query_legacy_specific(LegacyGroup::V1_4, address, timeout_settings) {
return Ok(response);
}
if let Ok(response) = query_legacy_specific(LegacyGroup::VB1_8, address, timeout_settings) {
return Ok(response);
}
Err(AutoQuery.into())
}
/// Query a specific (Java) Legacy Server.
pub fn query_legacy_specific(
group: LegacyGroup,
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<JavaResponse> {
match group {
LegacyGroup::V1_6 => LegacyV1_6::query(address, timeout_settings),
LegacyGroup::V1_4 => LegacyV1_4::query(address, timeout_settings),
LegacyGroup::VB1_8 => LegacyVB1_8::query(address, timeout_settings),
}
}
/// Query a Bedrock Server.
pub fn query_bedrock(address: &SocketAddr, timeout_settings: Option<TimeoutSettings>) -> GDResult<BedrockResponse> {
Bedrock::query(address, timeout_settings)
}

View file

@ -1,337 +0,0 @@
// Although its a lightly modified version, this file contains code
// by Jaiden Bernard (2021-2022 - MIT) from
// https://github.com/thisjaiden/golden_apple/blob/master/src/lib.rs
use crate::{
buffer::Buffer,
protocols::{
types::{CommonPlayer, CommonResponse, ExtraRequestSettings, GenericPlayer},
GenericResponse,
},
GDErrorKind::{InvalidInput, PacketBad, UnknownEnumCast},
GDResult,
};
use byteorder::ByteOrder;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// The type of Minecraft Server you want to query.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Server {
/// Java Edition.
Java,
/// Legacy Java.
Legacy(LegacyGroup),
/// Bedrock Edition.
Bedrock,
}
/// Legacy Java (Versions) Groups.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum LegacyGroup {
/// 1.6
V1_6,
/// 1.4 - 1.5
V1_4,
/// Beta 1.8 - 1.3
VB1_8,
}
/// Information about a player.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub name: String,
pub id: String,
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer<'_> { GenericPlayer::Minecraft(self) }
fn name(&self) -> &str { &self.name }
}
/// Versioned response type
#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionedResponse<'a> {
Bedrock(&'a BedrockResponse),
Java(&'a JavaResponse),
}
/// A Java query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct JavaResponse {
/// Version name, example: "1.19.2".
pub game_version: String,
/// Protocol version, example: 760 (for 1.19.1 or 1.19.2).
/// Note that for versions below 1.6 this field is always -1.
pub protocol_version: i32,
/// Number of server capacity.
pub players_maximum: u32,
/// Number of online players.
pub players_online: u32,
/// Some online players (can be missing).
pub players: Option<Vec<Player>>,
/// Server's description or MOTD.
pub description: String,
/// The favicon (can be missing).
pub favicon: Option<String>,
/// Tells if the chat preview is enabled (can be missing).
pub previews_chat: Option<bool>,
/// Tells if secure chat is enforced (can be missing).
pub enforces_secure_chat: Option<bool>,
/// Tell's the server type.
pub server_type: Server,
}
/// Java-only additional request settings.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct RequestSettings {
/// Some Minecraft servers do not respond as expected if this
/// isn't a specific value, `mc.hypixel.net` is an example.
pub hostname: String,
/// Specifies the client [protocol version number](https://wiki.vg/Protocol_version_numbers),
/// `-1` means anything.
pub protocol_version: i32,
}
impl Default for RequestSettings {
/// `hostname`: "gamedig"
/// `protocol_version`: -1
fn default() -> Self {
Self {
hostname: "gamedig".to_string(),
protocol_version: -1,
}
}
}
impl RequestSettings {
/// Make a new *RequestSettings* with just the hostname, the protocol
/// version defaults to -1
pub const fn new_just_hostname(hostname: String) -> Self {
Self {
hostname,
protocol_version: -1,
}
}
}
impl From<ExtraRequestSettings> for RequestSettings {
fn from(value: ExtraRequestSettings) -> Self {
let default = Self::default();
Self {
hostname: value.hostname.unwrap_or(default.hostname),
protocol_version: value.protocol_version.unwrap_or(default.protocol_version),
}
}
}
impl CommonResponse for JavaResponse {
fn as_original(&self) -> GenericResponse<'_> { GenericResponse::Minecraft(VersionedResponse::Java(self)) }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
self.players
.as_ref()
.map(|players| players.iter().map(|p| p as &dyn CommonPlayer).collect())
}
}
/// A Bedrock Edition query response.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct BedrockResponse {
/// Server's edition.
pub edition: String,
/// Server's name.
pub name: String,
/// Version name, example: "1.19.40".
pub version_name: String,
/// Protocol version, example: 760 (for 1.19.2).
pub protocol_version: String,
/// Maximum number of players the server reports it can hold.
pub players_maximum: u32,
/// Number of players on the server.
pub players_online: u32,
/// Server id.
pub id: Option<String>,
/// Currently running map's name.
pub map: Option<String>,
/// Current game mode.
pub game_mode: Option<GameMode>,
/// Tells the server type.
pub server_type: Server,
}
impl CommonResponse for BedrockResponse {
fn as_original(&self) -> GenericResponse<'_> { GenericResponse::Minecraft(VersionedResponse::Bedrock(self)) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn map(&self) -> Option<&str> { self.map.as_deref() }
fn game_version(&self) -> Option<&str> { Some(&self.version_name) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
}
impl JavaResponse {
pub fn from_bedrock_response(response: BedrockResponse) -> Self {
Self {
game_version: response.version_name,
protocol_version: 0,
players_maximum: response.players_maximum,
players_online: response.players_online,
players: None,
description: response.name,
favicon: None,
previews_chat: None,
enforces_secure_chat: None,
server_type: Server::Bedrock,
}
}
}
/// A server's game mode (used only by Bedrock servers.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum GameMode {
Survival,
Creative,
Hardcore,
Spectator,
Adventure,
}
impl GameMode {
pub fn from_bedrock(value: &&str) -> GDResult<Self> {
match *value {
"Survival" => Ok(Self::Survival),
"Creative" => Ok(Self::Creative),
"Hardcore" => Ok(Self::Hardcore),
"Spectator" => Ok(Self::Spectator),
"Adventure" => Ok(Self::Adventure),
_ => Err(UnknownEnumCast.context(format!("Unknown gamemode {value:?}"))),
}
}
}
pub(crate) fn get_varint<B: ByteOrder>(buffer: &mut Buffer<B>) -> GDResult<i32> {
let mut result = 0;
let msb: u8 = 0b1000_0000;
let mask: u8 = !msb;
for i in 0 .. 5 {
let current_byte = buffer.read::<u8>()?;
result |= ((current_byte & mask) as i32) << (7 * i);
// The 5th byte is only allowed to have the 4 smallest bits set
if i == 4 && (current_byte & 0xf0 != 0) {
return Err(PacketBad.context("Bad 5th byte"));
}
if (current_byte & msb) == 0 {
break;
}
}
Ok(result)
}
pub(crate) fn as_varint(value: i32) -> Vec<u8> {
let mut bytes = vec![];
let mut reading_value = value;
let msb: u8 = 0b1000_0000;
let mask: i32 = 0b0111_1111;
for _ in 0 .. 5 {
let tmp = (reading_value & mask) as u8;
reading_value &= !mask;
reading_value = reading_value.rotate_right(7);
if reading_value == 0 {
bytes.push(tmp);
break;
}
bytes.push(tmp | msb);
}
bytes
}
pub(crate) fn get_string<B: ByteOrder>(buffer: &mut Buffer<B>) -> GDResult<String> {
let length = get_varint(buffer)? as usize;
let mut text = Vec::with_capacity(length);
for _ in 0 .. length {
text.push(buffer.read::<u8>()?)
}
String::from_utf8(text).map_err(|e| PacketBad.context(e))
}
pub(crate) fn as_string(value: &str) -> GDResult<Vec<u8>> {
let length = value
.len()
.try_into()
.map_err(|e| InvalidInput.context(e))?;
let mut buf = as_varint(length);
buf.extend(value.as_bytes());
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::{as_string, as_varint, get_varint};
use crate::buffer::Buffer;
use crate::minecraft::get_string;
use byteorder::LittleEndian;
#[test]
fn int_as_varint() {
assert_eq!(as_varint(1), [1]);
assert_eq!(as_varint(25565), [221, 199, 1]);
assert_eq!(as_varint(1298923567), [175, 128, 176, 235, 4]);
}
#[test]
fn varint_as_int() {
let mut buffer = Buffer::<LittleEndian>::new(&[1, 127, 221, 199, 1, 0]);
assert_eq!(get_varint(&mut buffer), Ok(1));
assert_eq!(get_varint(&mut buffer), Ok(127));
assert_eq!(get_varint(&mut buffer), Ok(25565));
assert_eq!(buffer.remaining_bytes(), [0]);
}
#[test]
fn string_as_minecraft_string() {
assert_eq!(as_string("A"), Ok(vec![1, 65]));
assert_eq!(
as_string("VarString"),
Ok(vec![9, 86, 97, 114, 83, 116, 114, 105, 110, 103])
);
}
#[test]
fn minecraft_get_string() {
let mut buffer = Buffer::<LittleEndian>::new(&[3, 65, 65, 65, 1, 66]);
assert_eq!(get_string(&mut buffer), Ok("AAA".to_string()));
assert_eq!(get_string(&mut buffer), Ok("B".to_string()));
assert_eq!(buffer.remaining_length(), 0);
}
}

View file

@ -1,8 +0,0 @@
/// The implementation.
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/minetest.js)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -1,23 +0,0 @@
use crate::minetest::Response;
use crate::{minetest_master_server, GDErrorKind, GDResult, TimeoutSettings};
use std::net::IpAddr;
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, &None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: &Option<TimeoutSettings>,
) -> GDResult<Response> {
let address = address.to_string();
let port = port.unwrap_or(30000);
let servers = minetest_master_server::query(timeout_settings.unwrap_or_default())?;
for server in servers.list {
if server.ip == address && server.port == port {
return Ok(server.into());
}
}
Err(GDErrorKind::AutoQuery.context("Server not found in the master query list."))
}

View file

@ -1,108 +0,0 @@
use crate::minetest_master_server::Server;
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
use crate::protocols::GenericResponse;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Player {
pub name: String,
}
impl CommonPlayer for Player {
fn as_original(&self) -> GenericPlayer { GenericPlayer::Minetest(self) }
fn name(&self) -> &str { &self.name }
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Response {
pub name: String,
pub description: String,
pub game_version: String,
pub players_maximum: u32,
pub players_online: u32,
pub has_password: Option<bool>,
pub players: Vec<Player>,
pub id: String,
pub ip: String,
pub port: u16,
pub creative: Option<bool>,
pub damage: bool,
pub game_time: u32,
pub lag: Option<f32>,
pub proto_max: u16,
pub proto_min: u16,
pub pvp: bool,
pub uptime: u32,
pub url: Option<String>,
pub update_time: u32,
pub start: u32,
pub clients_top: u32,
pub updates: u32,
pub pop_v: f32,
pub geo_continent: Option<String>,
pub ping: f32,
}
impl From<Server> for Response {
fn from(server: Server) -> Self {
Self {
name: server.name,
description: server.description,
game_version: server.version,
players_maximum: server.clients_max,
players_online: server.total_clients,
has_password: server.password,
players: server
.clients_list
.unwrap_or_default()
.into_iter()
.map(|name| Player { name })
.collect(),
ip: server.address,
creative: server.creative,
damage: server.damage,
game_time: server.game_time,
id: server.gameid,
lag: server.lag,
port: server.port,
proto_max: server.proto_max,
proto_min: server.proto_min,
pvp: server.pvp,
uptime: server.uptime,
url: server.url,
update_time: server.update_time,
start: server.start,
clients_top: server.clients_top,
updates: server.updates,
pop_v: server.pop_v,
geo_continent: server.geo_continent,
ping: server.ping,
}
}
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse { GenericResponse::Minetest(self) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn description(&self) -> Option<&str> { Some(&self.description) }
fn game_version(&self) -> Option<&str> { Some(&self.game_version) }
fn players_maximum(&self) -> u32 { self.players_maximum }
fn players_online(&self) -> u32 { self.players_online }
fn has_password(&self) -> Option<bool> { self.has_password }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}

View file

@ -1,50 +0,0 @@
//! Currently supported games.
#[cfg(feature = "tls")]
pub mod epic;
pub mod gamespy;
pub mod quake;
pub mod unreal2;
pub mod valve;
#[cfg(all(feature = "tls", feature = "serde", feature = "services"))]
pub mod minetest;
#[cfg(feature = "tls")]
pub use epic::*;
pub use gamespy::*;
pub use quake::*;
pub use unreal2::*;
pub use valve::*;
#[cfg(all(feature = "tls", feature = "serde", feature = "services"))]
pub use minetest::*;
/// Battalion 1944
pub mod battalion1944;
/// Eco
pub mod eco;
/// Frontlines: Fuel of War
pub mod ffow;
/// Just Cause 2: Multiplayer
pub mod jc2m;
/// Mindustry
pub mod mindustry;
/// Minecraft
pub mod minecraft;
/// Savage 2
pub mod savage2;
/// The Ship
pub mod theship;
pub mod types;
pub use types::*;
pub mod query;
pub use query::*;
#[cfg(feature = "game_defs")]
mod definitions;
#[cfg(feature = "game_defs")]
pub use definitions::GAMES;

View file

@ -1,9 +0,0 @@
//! Quake game query modules
use crate::protocols::quake::game_query_mod;
game_query_mod!(quake1, "Quake 1", one, 27500);
game_query_mod!(quake2, "Quake 2", two, 27910);
game_query_mod!(q3a, "Quake 3 Arena", three, 27960);
game_query_mod!(sof2, "Soldier of Fortune 2", three, 20100);
game_query_mod!(warsow, "Warsow", three, 44400);

View file

@ -1,137 +0,0 @@
//! Generic query functions
use std::net::{IpAddr, SocketAddr};
#[cfg(all(feature = "services", feature = "tls", feature = "serde"))]
use crate::games::minetest;
use crate::games::types::Game;
use crate::games::{eco, ffow, jc2m, mindustry, minecraft, savage2, theship};
use crate::protocols;
use crate::protocols::gamespy::GameSpyVersion;
use crate::protocols::quake::QuakeVersion;
use crate::protocols::types::{CommonResponse, ExtraRequestSettings, ProprietaryProtocol, Protocol, TimeoutSettings};
use crate::GDResult;
/// Make a query given a game definition
#[inline]
pub fn query(game: &Game, address: &IpAddr, port: Option<u16>) -> GDResult<Box<dyn CommonResponse>> {
query_with_timeout_and_extra_settings(game, address, port, None, None)
}
/// Make a query given a game definition and timeout settings
#[inline]
pub fn query_with_timeout(
game: &Game,
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Box<dyn CommonResponse>> {
query_with_timeout_and_extra_settings(game, address, port, timeout_settings, None)
}
/// Make a query given a game definition, timeout settings, and extra settings
pub fn query_with_timeout_and_extra_settings(
game: &Game,
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
extra_settings: Option<ExtraRequestSettings>,
) -> GDResult<Box<dyn CommonResponse>> {
let socket_addr = SocketAddr::new(*address, port.unwrap_or(game.default_port));
Ok(match &game.protocol {
Protocol::Valve(engine) => {
protocols::valve::query(
&socket_addr,
*engine,
extra_settings
.or_else(|| Option::from(game.request_settings.clone()))
.map(ExtraRequestSettings::into),
timeout_settings,
)
.map(Box::new)?
}
#[cfg(feature = "tls")]
Protocol::Epic(credentials) => {
protocols::epic::query_with_timeout(credentials.clone(), &socket_addr, timeout_settings).map(Box::new)?
}
Protocol::Gamespy(version) => {
match version {
GameSpyVersion::One => protocols::gamespy::one::query(&socket_addr, timeout_settings).map(Box::new)?,
GameSpyVersion::Two => protocols::gamespy::two::query(&socket_addr, timeout_settings).map(Box::new)?,
GameSpyVersion::Three => {
protocols::gamespy::three::query(&socket_addr, timeout_settings).map(Box::new)?
}
}
}
Protocol::Quake(version) => {
match version {
QuakeVersion::One => protocols::quake::one::query(&socket_addr, timeout_settings).map(Box::new)?,
QuakeVersion::Two => protocols::quake::two::query(&socket_addr, timeout_settings).map(Box::new)?,
QuakeVersion::Three => protocols::quake::three::query(&socket_addr, timeout_settings).map(Box::new)?,
}
}
Protocol::Unreal2 => {
protocols::unreal2::query(
&socket_addr,
&extra_settings
.map(ExtraRequestSettings::into)
.unwrap_or_default(),
timeout_settings,
)
.map(Box::new)?
}
Protocol::PROPRIETARY(protocol) => {
match protocol {
ProprietaryProtocol::Savage2 => {
savage2::query_with_timeout(address, port, timeout_settings).map(Box::new)?
}
ProprietaryProtocol::TheShip => {
theship::query_with_timeout(address, port, timeout_settings).map(Box::new)?
}
ProprietaryProtocol::FFOW => ffow::query_with_timeout(address, port, timeout_settings).map(Box::new)?,
ProprietaryProtocol::JC2M => jc2m::query_with_timeout(address, port, timeout_settings).map(Box::new)?,
ProprietaryProtocol::Mindustry => mindustry::query(address, port, &timeout_settings).map(Box::new)?,
ProprietaryProtocol::Minecraft(version) => {
match version {
Some(minecraft::Server::Java) => {
minecraft::protocol::query_java(
&socket_addr,
timeout_settings,
extra_settings.map(ExtraRequestSettings::into),
)
.map(Box::new)?
}
Some(minecraft::Server::Bedrock) => {
minecraft::protocol::query_bedrock(&socket_addr, timeout_settings).map(Box::new)?
}
Some(minecraft::Server::Legacy(group)) => {
minecraft::protocol::query_legacy_specific(*group, &socket_addr, timeout_settings)
.map(Box::new)?
}
None => {
minecraft::protocol::query(
&socket_addr,
timeout_settings,
extra_settings.map(ExtraRequestSettings::into),
)
.map(Box::new)?
}
}
}
ProprietaryProtocol::Eco => {
eco::query_with_timeout_and_extra_settings(
address,
port,
&timeout_settings,
extra_settings.map(ExtraRequestSettings::into),
)
.map(Box::new)?
}
#[cfg(all(feature = "services", feature = "tls", feature = "serde"))]
ProprietaryProtocol::Minetest => {
minetest::query_with_timeout(address, port, &timeout_settings).map(Box::new)?
}
}
}
})
}

View file

@ -1,8 +0,0 @@
/// The implementation.
/// Reference: [Node-GameGig](https://github.com/gamedig/node-gamedig/blob/master/protocols/savage2.js)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -1,37 +0,0 @@
use crate::buffer::{Buffer, Utf8Decoder};
use crate::games::savage2::types::Response;
use crate::protocols::types::TimeoutSettings;
use crate::socket::{Socket, UdpSocket};
use crate::GDResult;
use byteorder::LittleEndian;
use std::net::{IpAddr, SocketAddr};
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let addr = &SocketAddr::new(*address, port.unwrap_or(11235));
let mut socket = UdpSocket::new(addr, &timeout_settings)?;
socket.send(&[0x01])?;
let data = socket.receive(None)?;
let mut buffer = Buffer::<LittleEndian>::new(&data);
buffer.move_cursor(12)?;
Ok(Response {
name: buffer.read_string::<Utf8Decoder>(None)?,
players_online: buffer.read::<u8>()?,
players_maximum: buffer.read::<u8>()?,
time: buffer.read_string::<Utf8Decoder>(None)?,
map: buffer.read_string::<Utf8Decoder>(None)?,
next_map: buffer.read_string::<Utf8Decoder>(None)?,
location: buffer.read_string::<Utf8Decoder>(None)?,
players_minimum: buffer.read::<u8>()?,
game_mode: buffer.read_string::<Utf8Decoder>(None)?,
protocol_version: buffer.read_string::<Utf8Decoder>(None)?,
level_minimum: buffer.read::<u8>()?,
})
}

View file

@ -1,30 +0,0 @@
use crate::protocols::types::CommonResponse;
use crate::protocols::GenericResponse;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
pub name: String,
pub players_online: u8,
pub players_maximum: u8,
pub players_minimum: u8,
pub time: String,
pub map: String,
pub next_map: String,
pub location: String,
pub game_mode: String,
pub protocol_version: String,
pub level_minimum: u8,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse<'_> { GenericResponse::Savage2(self) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn players_maximum(&self) -> u32 { self.players_maximum.into() }
fn players_online(&self) -> u32 { self.players_online.into() }
}

View file

@ -1,8 +0,0 @@
/// The implementation.
/// Reference: [server queries](https://developer.valvesoftware.com/wiki/Server_queries)
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;

View file

@ -1,23 +0,0 @@
use crate::games::theship::types::Response;
use crate::protocols::types::TimeoutSettings;
use crate::protocols::valve;
use crate::protocols::valve::Engine;
use crate::GDResult;
use std::net::{IpAddr, SocketAddr};
pub fn query(address: &IpAddr, port: Option<u16>) -> GDResult<Response> { query_with_timeout(address, port, None) }
pub fn query_with_timeout(
address: &IpAddr,
port: Option<u16>,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let valve_response = valve::query(
&SocketAddr::new(*address, port.unwrap_or(27015)),
Engine::new(2400),
None,
timeout_settings,
)?;
Response::new_from_valve_response(valve_response)
}

View file

@ -1,122 +0,0 @@
use crate::protocols::types::{CommonPlayer, CommonResponse, GenericPlayer};
use crate::protocols::valve::{get_optional_extracted_data, Server, ServerPlayer};
use crate::protocols::{valve, GenericResponse};
use crate::GDErrorKind::PacketBad;
use crate::GDResult;
use std::collections::HashMap;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct TheShipPlayer {
pub name: String,
pub score: i32,
pub duration: f32,
pub deaths: u32,
pub money: u32,
}
impl TheShipPlayer {
pub fn new_from_valve_player(player: &ServerPlayer) -> GDResult<Self> {
Ok(Self {
name: player.name.clone(),
score: player.score,
duration: player.duration,
deaths: player.deaths.ok_or(PacketBad)?,
money: player.money.ok_or(PacketBad)?,
})
}
}
impl CommonPlayer for TheShipPlayer {
fn as_original(&self) -> GenericPlayer<'_> { GenericPlayer::TheShip(self) }
fn name(&self) -> &str { &self.name }
fn score(&self) -> Option<i32> { Some(self.score) }
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct Response {
pub protocol_version: u8,
pub name: String,
pub map: String,
pub game_mode: String,
pub game_version: String,
pub players: Vec<TheShipPlayer>,
pub players_online: u8,
pub players_maximum: u8,
pub players_bots: u8,
pub server_type: Server,
pub has_password: bool,
pub vac_secured: bool,
pub port: Option<u16>,
pub steam_id: Option<u64>,
pub tv_port: Option<u16>,
pub tv_name: Option<String>,
pub keywords: Option<String>,
pub rules: HashMap<String, String>,
pub mode: u8,
pub witnesses: u8,
pub duration: u8,
}
impl CommonResponse for Response {
fn as_original(&self) -> GenericResponse<'_> { GenericResponse::TheShip(self) }
fn name(&self) -> Option<&str> { Some(&self.name) }
fn map(&self) -> Option<&str> { Some(&self.map) }
fn game_mode(&self) -> Option<&str> { Some(&self.game_mode) }
fn players_maximum(&self) -> u32 { self.players_maximum.into() }
fn players_online(&self) -> u32 { self.players_online.into() }
fn players_bots(&self) -> Option<u32> { Some(self.players_bots.into()) }
fn has_password(&self) -> Option<bool> { Some(self.has_password) }
fn players(&self) -> Option<Vec<&dyn CommonPlayer>> {
Some(
self.players
.iter()
.map(|p| p as &dyn CommonPlayer)
.collect(),
)
}
}
impl Response {
pub fn new_from_valve_response(response: valve::Response) -> GDResult<Self> {
let (port, steam_id, tv_port, tv_name, keywords) = get_optional_extracted_data(response.info.extra_data);
let the_unwrapped_ship = response.info.the_ship.ok_or(PacketBad)?;
Ok(Self {
protocol_version: response.info.protocol_version,
name: response.info.name,
map: response.info.map,
game_mode: response.info.game_mode,
game_version: response.info.game_version,
players_online: response.info.players_online,
players: response
.players
.ok_or(PacketBad)?
.iter()
.map(TheShipPlayer::new_from_valve_player)
.collect::<GDResult<Vec<TheShipPlayer>>>()?,
players_maximum: response.info.players_maximum,
players_bots: response.info.players_bots,
server_type: response.info.server_type,
has_password: response.info.has_password,
vac_secured: response.info.vac_secured,
port,
steam_id,
tv_port,
tv_name,
keywords,
rules: response.rules.ok_or(PacketBad)?,
mode: the_unwrapped_ship.mode,
witnesses: the_unwrapped_ship.witnesses,
duration: the_unwrapped_ship.duration,
})
}
}

View file

@ -1,20 +0,0 @@
//! Game related types
use crate::protocols::types::{ExtraRequestSettings, Protocol};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// Definition of a game
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Game {
/// Full name of the game
pub name: &'static str,
/// Default port used by game
pub default_port: u16,
/// The protocol the game's query uses
pub protocol: Protocol,
/// Request settings.
pub request_settings: ExtraRequestSettings,
}

View file

@ -1,10 +0,0 @@
//! Unreal2 game query modules
use crate::protocols::unreal2::game_query_mod;
game_query_mod!(darkesthour, "Darkest Hour: Europe '44-'45 (2008)", 7758);
game_query_mod!(devastation, "Devastation (2003)", 7778);
game_query_mod!(killingfloor, "Killing Floor", 7708);
game_query_mod!(redorchestra, "Red Orchestra", 7759);
game_query_mod!(ut2003, "Unreal Tournament 2003", 7758);
game_query_mod!(ut2004, "Unreal Tournament 2004", 7778);

View file

@ -1,201 +0,0 @@
//! Valve game query modules
use crate::protocols::valve::game_query_mod;
game_query_mod!(abioticfactor, "Abiotic Factor", Engine::new(427_410), 27015);
game_query_mod!(
a2oa,
"ARMA 2: Operation Arrowhead",
Engine::new(33930),
2304
);
game_query_mod!(arma3, "ARMA 3", Engine::new(107_410), 2303);
game_query_mod!(basedefense, "Base Defense", Engine::new(632_730), 27015);
game_query_mod!(alienswarm, "Alien Swarm", Engine::new(630), 27015);
game_query_mod!(aoc, "Age of Chivalry", Engine::new(17510), 27015);
game_query_mod!(
aapg,
"America's Army: Proving Grounds",
Engine::new(203_290),
27020,
GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Skip,
check_app_id: true,
}
);
game_query_mod!(ase, "ARK: Survival Evolved", Engine::new(346_110), 27015);
game_query_mod!(
asrd,
"Alien Swarm: Reactive Drop",
Engine::new(563_560),
2304
);
game_query_mod!(atlas, "ATLAS", Engine::new(834_910), 57561);
game_query_mod!(avorion, "Avorion", Engine::new(445_220), 27020);
game_query_mod!(
ballisticoverkill,
"Ballistic Overkill",
Engine::new(296_300),
27016
);
game_query_mod!(
armareforger,
"Arma Reforger",
Engine::new(0),
17777,
GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Enforce,
check_app_id: false,
}
);
game_query_mod!(
avp2010,
"Aliens vs. Predator 2010",
Engine::new(10_680),
27015
);
game_query_mod!(barotrauma, "Barotrauma", Engine::new(602_960), 27016);
game_query_mod!(blackmesa, "Black Mesa", Engine::new(362_890), 27015);
game_query_mod!(brainbread2, "BrainBread 2", Engine::new(346_330), 27015);
game_query_mod!(
codbo3,
"Call Of Duty: Black Ops 3",
Engine::new(311_210),
27017
);
game_query_mod!(codenamecure, "Codename CURE", Engine::new(355_180), 27015);
game_query_mod!(
colonysurvival,
"Colony Survival",
Engine::new(366_090),
27004
);
game_query_mod!(
conanexiles,
"Conan Exiles",
Engine::new(440_900),
27015,
GatheringSettings {
players: GatherToggle::Skip,
rules: GatherToggle::Enforce,
check_app_id: true,
}
);
game_query_mod!(
counterstrike,
"Counter-Strike",
Engine::new_gold_src(false),
27015
);
game_query_mod!(counterstrike2, "Counter-Strike 2", Engine::new(730), 27015);
game_query_mod!(creativerse, "Creativerse", Engine::new(280_790), 26901);
game_query_mod!(
cscz,
"Counter Strike: Condition Zero",
Engine::new_gold_src(false),
27015
);
game_query_mod!(
csgo,
"Counter-Strike: Global Offensive",
Engine::new(730),
27015
);
game_query_mod!(css, "Counter-Strike: Source", Engine::new(240), 27015);
game_query_mod!(dab, "Double Action: Boogaloo", Engine::new(317_360), 27015);
game_query_mod!(dod, "Day of Defeat", Engine::new_gold_src(false), 27015);
game_query_mod!(dods, "Day of Defeat: Source", Engine::new(300), 27015);
game_query_mod!(doi, "Day of Infamy", Engine::new(447_820), 27015);
game_query_mod!(dst, "Don't Starve Together", Engine::new(322_320), 27016);
game_query_mod!(enshrouded, "Enshrouded", Engine::new(1_203_620), 15637);
game_query_mod!(garrysmod, "Garry's Mod", Engine::new(4000), 27016);
game_query_mod!(hl2d, "Half-Life 2 Deathmatch", Engine::new(320), 27015);
game_query_mod!(
hlds,
"Half-Life Deathmatch: Source",
Engine::new(360),
27015
);
game_query_mod!(hll, "Hell Let Loose", Engine::new(686_810), 26420);
game_query_mod!(
imic,
"Insurgency: Modern Infantry Combat",
Engine::new(17700),
27015
);
game_query_mod!(insurgency, "Insurgency", Engine::new(222_880), 27015);
game_query_mod!(
insurgencysandstorm,
"Insurgency: Sandstorm",
Engine::new(581_320),
27131
);
game_query_mod!(l4d, "Left 4 Dead", Engine::new(500), 27015);
game_query_mod!(l4d2, "Left 4 Dead 2", Engine::new(550), 27015);
game_query_mod!(
ohd,
"Operation: Harsh Doorstop",
Engine::new_with_dedicated(736_590, 950_900),
27005
);
game_query_mod!(onset, "Onset", Engine::new(1_105_810), 7776);
game_query_mod!(postscriptum, "Post Scriptum", Engine::new(736_220), 10037);
game_query_mod!(
projectzomboid,
"Project Zomboid",
Engine::new(108_600),
16261
);
game_query_mod!(risingworld, "Rising World", Engine::new(324_080), 4254);
game_query_mod!(ror2, "Risk of Rain 2", Engine::new(632_360), 27016);
game_query_mod!(rust, "Rust", Engine::new(252_490), 27015);
game_query_mod!(sco, "Sven Co-op", Engine::new_gold_src(false), 27015);
game_query_mod!(sdtd, "7 Days to Die", Engine::new(251_570), 26900);
game_query_mod!(soulmask, "Soulmask", Engine::new(2_646_460), 27015);
game_query_mod!(squad, "Squad", Engine::new(393_380), 27165);
game_query_mod!(
starbound,
"Starbound",
Engine::new(211_820),
21025,
GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Enforce,
check_app_id: false,
}
);
game_query_mod!(teamfortress2, "Team Fortress 2", Engine::new(440), 27015);
game_query_mod!(
tfc,
"Team Fortress Classic",
Engine::new_gold_src(false),
27015
);
game_query_mod!(theforest, "The Forest", Engine::new(556_450), 27016);
game_query_mod!(thefront, "The Front", Engine::new(2_285_150), 27015);
game_query_mod!(unturned, "Unturned", Engine::new(304_930), 27015);
game_query_mod!(
valheim,
"Valheim",
Engine::new(892_970),
2457,
GatheringSettings {
players: GatherToggle::Enforce,
rules: GatherToggle::Skip,
check_app_id: true,
}
);
game_query_mod!(vrising, "V Rising", Engine::new(1_604_030), 27016);
game_query_mod!(zps, "Zombie Panic: Source", Engine::new(17_500), 27015);
game_query_mod!(moe, "Myth of Empires", Engine::new(1_371_580), 12888);
game_query_mod!(mordhau, "Mordhau", Engine::new(629_760), 27015);
game_query_mod!(
pvak2,
"Pirates, Vikings, and Knights II",
Engine::new(17_570),
27015
);
game_query_mod!(nla, "Nova-Life: Amboise", Engine::new(885_570), 27015);
game_query_mod!(pixark, "PixARK", Engine::new(593_600), 27015);

View file

@ -1,491 +0,0 @@
//! Client for making HTTP requests.
//!
//! This is the first draft implementation: feel free to change things to suit
//! your needs.
// Because this is first draft some functionality is not used yet.
// TODO: When this is used in more places remove this and refine the interface.
#![allow(dead_code)]
use crate::GDErrorKind::{HostLookup, InvalidInput, PacketReceive, PacketSend, ProtocolFormat};
use crate::{GDResult, TimeoutSettings};
use std::io::Read;
use std::net::{SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs};
use ureq::{Agent, AgentBuilder, Request};
use url::{Host, Url};
use serde::{de::DeserializeOwned, Serialize};
/// Max length of HTTP responses in bytes: 1GB
const MAX_RESPONSE_LENGTH: usize = 1024 * 1024 * 1024;
/// HTTP request client. Define parameters host parameters on new, then re-use
/// for each request.
///
/// When making requests directly to the server use [HttpClient::new] as this
/// allows directly specifying the IP to connect to.
///
/// When requests must go through an intermediatary (that we don't know the IP
/// of) use [HttpClient::from_url] which will perform a DNS lookup internally.
///
/// For example usage see [tests].
pub struct HttpClient {
client: Agent,
address: Url,
headers: Vec<(String, String)>,
}
/// HttpHeaders for use with a single request.
pub type HttpHeaders<'a> = Option<&'a [(&'a str, &'a str)]>;
/// HTTP Protocols.
///
/// Note: if the `tls` feature is disabled this will only contain Http.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum HttpProtocol {
#[default]
Http,
#[cfg(feature = "tls")]
Https,
}
impl HttpProtocol {
/// Convert [Protocol] to a static str for use in a [Url].
/// e.g. "http:"
pub const fn as_str(&self) -> &'static str {
use HttpProtocol::*;
match self {
Http => "http:",
#[cfg(feature = "tls")]
Https => "https:",
}
}
}
/// Additional settings for HTTPClients.
///
/// # Can be created using builder functions:
/// ```ignore, We cannot test private functionality
/// use gamedig::http::{HttpSettings, HttpProtocol};
///
/// let _ = HttpSettings::default()
/// .protocol(HttpProtocol::Http)
/// .hostname(String::from("test.com"))
/// .header(String::from("Authorization"), String::from("Bearer Token"));
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct HttpSettings<S: Into<String>> {
/// Choose whether to use HTTP or HTTPS.
pub protocol: HttpProtocol,
/// Choose a hostname override (used to set the [Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) header) and for TLS.
pub hostname: Option<S>,
/// Choose HTTP headers to send with requests.
pub headers: Vec<(S, S)>,
}
impl<S: Into<String>> HttpSettings<S> {
/// Set the HTTP protocol (defaults to HTTP).
pub const fn protocol(mut self, protocol: HttpProtocol) -> HttpSettings<S> {
self.protocol = protocol;
self
}
/// Set the desired HTTP host name: used for the HTTP Host header and for
/// TLS negotiation.
pub fn hostname(mut self, hostname: S) -> HttpSettings<S> {
self.hostname = Some(hostname);
self
}
/// Overwrite all the current HTTP headers with new headers.
pub fn headers(mut self, headers: Vec<(S, S)>) -> HttpSettings<S> {
self.headers = headers;
self
}
/// Set one HTTP header value.
pub fn header(mut self, name: S, value: S) -> HttpSettings<S> {
self.headers.push((name, value));
self
}
}
impl HttpClient {
/// Creates a new HTTPClient that can be used to send requests.
///
/// # Parameters
/// - [address](SocketAddr): The IP and port the HTTP request will connect
/// to.
/// - [timeout_settings](TimeoutSettings): Used to set the connect and
/// socket timeouts for the requests.
/// - [http_settings](HttpSettings): Additional settings for the HTTPClient.
pub fn new<S: Into<String>>(
address: &SocketAddr,
timeout_settings: &Option<TimeoutSettings>,
http_settings: HttpSettings<S>,
) -> GDResult<Self>
where
Self: Sized,
{
let mut client_builder = AgentBuilder::new();
// Set timeout settings
let (read_timeout, write_timeout) = TimeoutSettings::get_read_and_write_or_defaults(timeout_settings);
if let Some(read_timeout) = read_timeout {
client_builder = client_builder.timeout_read(read_timeout);
}
if let Some(write_timeout) = write_timeout {
client_builder = client_builder.timeout_write(write_timeout);
}
if let Some(connect_timeout) = TimeoutSettings::get_connect_or_default(timeout_settings) {
client_builder = client_builder.timeout_connect(connect_timeout);
}
// Every request sent from this client will connect to the address set
{
let address = *address;
client_builder = client_builder.resolver(move |_: &str| Ok(vec![address]));
}
// Set a friendly user-agent string
client_builder = client_builder.user_agent(concat!(
env!("CARGO_PKG_NAME"),
"/",
env!("CARGO_PKG_VERSION")
));
let client = client_builder.build();
let host = http_settings
.hostname
.map(S::into)
.unwrap_or_else(|| address.ip().to_string());
Ok(Self {
client,
// TODO: Use Url from_parts if it gets added
address: Url::parse(&format!(
"{}//{}:{}",
http_settings.protocol.as_str(),
host,
address.port()
))
.map_err(|e| InvalidInput.context(e))?,
headers: http_settings
.headers
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
})
}
/// Create a new HTTP client from a pre-existing URL, performing a DNS
/// lookup on the host when necessary.
///
/// This is aimed to be used when we know the domain of the server but not
/// the IP i.e. when the server is not the service being directly queried
/// for server info.
pub fn from_url<U: TryInto<Url>>(
url: U,
timeout_settings: &Option<TimeoutSettings>,
headers: Option<Vec<(&str, &str)>>,
) -> GDResult<Self>
where
U::Error: std::error::Error + Send + Sync + 'static,
{
let url: Url = url.try_into().map_err(|e| InvalidInput.context(e))?;
let host = url
.host()
.ok_or_else(|| InvalidInput.context("URL used to create a HTTPClient must have a host"))?;
let port = url
.port_or_known_default()
.ok_or_else(|| InvalidInput.context("URL used to create HttpClient must have a port"))?;
let address = match host {
Host::Ipv4(ip) => SocketAddr::V4(SocketAddrV4::new(ip, port)),
Host::Ipv6(ip) => SocketAddr::V6(SocketAddrV6::new(ip, port, 0, 0)),
Host::Domain(domain) => {
format!("{domain}:{port}")
.to_socket_addrs()
.map_err(|e| HostLookup.context(e))?
.next()
.ok_or_else(|| HostLookup.context("No socket addresses found for host"))?
}
};
let http_settings = HttpSettings {
hostname: url.host_str(),
protocol: match url.scheme() {
#[cfg(feature = "tls")]
"https" => HttpProtocol::Https,
_ => HttpProtocol::Http,
},
headers: headers.unwrap_or_default(),
};
Self::new(&address, timeout_settings, http_settings)
}
/// Send a HTTP GET request and return the response data as a buffer.
pub fn get(&mut self, path: &str, headers: HttpHeaders) -> GDResult<Vec<u8>> { self.request("GET", path, headers) }
/// Send a HTTP GET request and parse the JSON resonse.
pub fn get_json<T: DeserializeOwned>(&mut self, path: &str, headers: HttpHeaders) -> GDResult<T> {
self.request_json("GET", path, headers)
}
/// Send a HTTP Post request with JSON data and parse a JSON response.
pub fn post_json<T: DeserializeOwned, S: Serialize>(
&mut self,
path: &str,
headers: HttpHeaders,
data: S,
) -> GDResult<T> {
self.request_with_json_data("POST", path, headers, data)
}
/// Send a HTTP Post request with FORM data and parse a JSON response.
pub fn post_json_with_form<T: DeserializeOwned>(
&mut self,
path: &str,
headers: HttpHeaders,
data: &[(&str, &str)],
) -> GDResult<T> {
self.request_with_form_data("POST", path, headers, data)
}
// NOTE: More methods can be added here as required using the request_json or
// request_with_json methods
fn make_request(&self, method: &str, headers: HttpHeaders) -> Request {
let mut request = self.client.request_url(method, &self.address);
// Set the request headers.
for (key, value) in self.headers.iter() {
request = request.set(key, value);
}
if let Some(headers) = headers {
for (key, value) in headers {
request = request.set(key, value);
}
}
request
}
/// Internal request method, makes a request with an arbitrary HTTP method.
#[inline]
fn request(&mut self, method: &str, path: &str, headers: HttpHeaders) -> GDResult<Vec<u8>> {
// Append the path to the pre-parsed URL and create a request object.
self.address.set_path(path);
let request = self.make_request(method, headers);
// Send the request.
let http_response = request.call().map_err(|e| PacketSend.context(e))?;
let length = if let Some(length) = http_response.header("Content-Length") {
length
.parse::<usize>()
.map_err(|e| ProtocolFormat.context(e))?
.min(MAX_RESPONSE_LENGTH)
} else {
5012 // Sensible default allocation
};
let mut buffer: Vec<u8> = Vec::with_capacity(length);
let _ = http_response
.into_reader()
.take(MAX_RESPONSE_LENGTH as u64)
.read_to_end(&mut buffer)
.map_err(|e| PacketReceive.context(e))?;
Ok(buffer)
}
/// Send a HTTP request without any data and parse the JSON response.
#[inline]
fn request_json<T: DeserializeOwned>(&mut self, method: &str, path: &str, headers: HttpHeaders) -> GDResult<T> {
// Append the path to the pre-parsed URL and create a request object.
self.address.set_path(path);
let request = self.make_request(method, headers);
// Send the request and parse the response as JSON.
request
.call()
.map_err(|e| PacketSend.context(e))?
.into_json::<T>()
.map_err(|e| ProtocolFormat.context(e))
}
/// Send a HTTP request with JSON data and parse the JSON response.
#[inline]
fn request_with_json_data<T: DeserializeOwned, S: Serialize>(
&mut self,
method: &str,
path: &str,
headers: HttpHeaders,
data: S,
) -> GDResult<T> {
self.address.set_path(path);
let request = self.make_request(method, headers);
request
.send_json(data)
.map_err(|e| PacketSend.context(e))?
.into_json::<T>()
.map_err(|e| ProtocolFormat.context(e))
}
/// Send a HTTP request with FORM data and parse the JSON response.
#[inline]
fn request_with_form_data<T: DeserializeOwned>(
&mut self,
method: &str,
path: &str,
headers: HttpHeaders,
data: &[(&str, &str)],
) -> GDResult<T> {
self.address.set_path(path);
let request = self.make_request(method, headers);
request
.send_form(data)
.map_err(|e| PacketSend.context(e))?
.into_json::<T>()
.map_err(|e| ProtocolFormat.context(e))
}
}
#[cfg(test)]
mod tests {
use std::net::{Ipv4Addr, SocketAddrV4, ToSocketAddrs};
use super::*;
#[test]
fn http_settings_builder() {
const HOSTNAME: &str = "example.org";
#[cfg(feature = "tls")]
const PROTOCOL: HttpProtocol = HttpProtocol::Https;
#[cfg(not(feature = "tls"))]
const PROTOCOL: HttpProtocol = HttpProtocol::Http;
let settings = HttpSettings::default()
.hostname(HOSTNAME)
.protocol(PROTOCOL)
.header("Gamedig", "Is Awesome")
.headers(vec![("Foo", "bar")])
.header("Baz", "Buzz");
assert_eq!(settings.hostname, Some(HOSTNAME));
assert_eq!(settings.protocol, PROTOCOL);
assert_eq!(settings.headers, vec![("Foo", "bar"), ("Baz", "Buzz"),]);
}
#[test]
fn http_client_new() {
const PROTOCOL: HttpProtocol = HttpProtocol::Http;
const ADDRESS: SocketAddr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8000));
let settings = HttpSettings {
protocol: PROTOCOL,
hostname: Some("github.com"),
headers: vec![("Authorization", "UUDDLRLRBA")],
};
let client = HttpClient::new(&ADDRESS, &None, settings).unwrap();
assert_eq!(client.address.as_str(), "http://github.com:8000/");
assert_eq!(
client.headers,
vec![(String::from("Authorization"), String::from("UUDDLRLRBA")),]
);
}
#[cfg(feature = "tls")]
#[test]
#[ignore = "HTTP requests won't work without internet"]
fn https_json_get_request() {
let address = "api.github.com:443"
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
let settings = HttpSettings::default()
.protocol(HttpProtocol::Https)
.hostname("api.github.com");
let mut client = HttpClient::new(&address, &None, settings).unwrap();
let response: serde_json::Value = client.get_json("/events", None).unwrap();
println!("{:?}", response);
}
#[test]
#[ignore = "HTTP requests won't work without internet"]
fn http_json_get_request() {
let address = "postman-echo.com:80"
.to_socket_addrs()
.unwrap()
.next()
.unwrap();
let settings = HttpSettings::default().hostname("postman-echo.com");
let mut client = HttpClient::new(&address, &None, settings).unwrap();
let response: serde_json::Value = client.get_json("/get", None).unwrap();
println!("{:?}", response);
}
#[test]
#[ignore = "HTTP requests won't work without internet"]
fn http_get_request() {
let address = "ifconfig.me:80".to_socket_addrs().unwrap().next().unwrap();
let settings = HttpSettings::default()
.hostname("ifconfig.me")
.header("User-Agent", "Curl/8.6.0");
let mut client = HttpClient::new(&address, &None, settings).unwrap();
let response = client.get("/", None).unwrap();
println!("{:?}", std::str::from_utf8(&response));
}
#[test]
#[ignore = "HTTP requests won't work without internet"]
fn http_get_from_url() {
let mut client = HttpClient::from_url("http://postman-echo.com/path-is-ignored", &None, None).unwrap();
let response: serde_json::Value = client.get_json("/get", None).unwrap();
println!("{:?}", response);
}
#[test]
#[ignore = "HTTP requests won't work without internet"]
fn http_get_from_url_parsed() {
let url = Url::parse("http://postman-echo.com/path-is-ignored").unwrap();
let mut client = HttpClient::from_url(url, &None, None).unwrap();
let response: serde_json::Value = client.get_json("/get", None).unwrap();
println!("{:?}", response);
}
}

View file

@ -1,65 +0,0 @@
//! Game Server Query Library.
//!
//! # Usage example:
//!
//! ## For a specific game
//! ```
//! use gamedig::games::teamfortress2;
//!
//! let response = teamfortress2::query(&"127.0.0.1".parse().unwrap(), None); // None is the default port (which is 27015), could also be Some(27015)
//! match response { // Result type, must check what it is...
//! Err(error) => println!("Couldn't query, error: {}", error),
//! Ok(r) => println!("{:#?}", r)
//! }
//! ```
//!
//! ## Using a game definition
//! ```
//! use gamedig::{GAMES, query};
//!
//! let game = GAMES.get("teamfortress2").unwrap(); // Get a game definition, the full list can be found in src/games/mod.rs
//! let response = query(game, &"127.0.0.1".parse().unwrap(), None); // None will use the default port
//! match response {
//! Err(error) => println!("Couldn't query, error: {}", error),
//! Ok(r) => println!("{:#?}", r.as_json()),
//! }
//! ```
//!
//! # Crate features:
//! Enabled by default: `games`, `game_defs`, `services`
//!
//! `serde` - enables serde serialization/deserialization for many gamedig types
//! using serde derive. <br>
//! `games` - include games support. <br>
//! `services` - include services support. <br>
//! `game_defs` - include game definitions for programmatic access (enabled by
//! default). <br>
//! `clap` - enable clap derivations for gamedig settings types. <br>
//! `tls` - enable TLS support for the HTTP client.
pub mod errors;
#[cfg(feature = "games")]
pub mod games;
pub mod protocols;
#[cfg(feature = "services")]
pub mod services;
mod buffer;
mod http;
mod socket;
mod utils;
#[cfg(feature = "packet_capture")]
pub mod capture;
pub use errors::*;
#[cfg(feature = "games")]
pub use games::*;
#[allow(unused_imports)]
#[cfg(feature = "games")]
pub use query::*;
#[cfg(feature = "services")]
pub use services::*;
// Re-export types needed to call games::query::query in the root
pub use protocols::types::{ExtraRequestSettings, TimeoutSettings};

View file

@ -1,58 +0,0 @@
/// The implementation.
pub mod protocol;
/// All types used by the implementation.
pub mod types;
pub use protocol::*;
pub use types::*;
/// Generate a module containing a query function for an epic (EOS) game.
///
/// * `mod_name` - The name to be given to the game module (see ID naming
/// conventions in CONTRIBUTING.md).
/// * `pretty_name` - The full name of the game, will be used as the
/// documentation for the created module.
/// * `steam_app`, `default_port` - Passed through to [game_query_fn].
#[cfg(feature = "games")]
macro_rules! game_query_mod {
($mod_name: ident, $pretty_name: expr, $default_port: literal, $credentials: expr) => {
#[doc = $pretty_name]
pub mod $mod_name {
use crate::protocols::epic::Credentials;
crate::protocols::epic::game_query_fn!($pretty_name, $default_port, $credentials);
}
};
}
#[cfg(feature = "games")]
pub(crate) use game_query_mod;
/// Generate a query function for an epic (EOS) game.
///
/// * `default_port` - The default port the game uses.
/// * `credentials` - Credentials to access EOS.
#[cfg(feature = "games")]
macro_rules! game_query_fn {
($pretty_name: expr, $default_port: literal, $credentials: expr) => {
crate::protocols::epic::game_query_fn! {@gen $default_port, concat!(
"Make a Epic query for ", $pretty_name, ".\n\n",
"If port is `None`, then the default port (", stringify!($default_port), ") will be used."), $credentials}
};
(@gen $default_port: literal, $doc: expr, $credentials: expr) => {
#[doc = $doc]
pub fn query(
address: &std::net::IpAddr,
port: Option<u16>,
) -> crate::GDResult<crate::protocols::epic::Response> {
crate::protocols::epic::query(
$credentials,
&std::net::SocketAddr::new(*address, port.unwrap_or($default_port)),
)
}
};
}
#[cfg(feature = "games")]
pub(crate) use game_query_fn;

View file

@ -1,183 +0,0 @@
use crate::http::HttpClient;
use crate::protocols::epic::Response;
use crate::GDErrorKind::{JsonParse, PacketBad};
use crate::{GDResult, TimeoutSettings};
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use serde::Deserialize;
#[cfg(feature = "serde")]
use serde::Serialize;
use serde_json::Value;
use std::net::SocketAddr;
const EPIC_API_ENDPOINT: &str = "https://api.epicgames.dev";
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Credentials {
#[cfg_attr(feature = "serde", serde(skip_deserializing, skip_serializing))]
pub deployment: &'static str,
#[cfg_attr(feature = "serde", serde(skip_deserializing, skip_serializing))]
pub id: &'static str,
#[cfg_attr(feature = "serde", serde(skip_deserializing, skip_serializing))]
pub secret: &'static str,
pub auth_by_external: bool,
}
pub struct EpicProtocol {
client: HttpClient,
credentials: Credentials,
}
#[derive(Deserialize)]
struct ClientTokenResponse {
access_token: String,
}
#[derive(Deserialize)]
struct QueryResponse {
sessions: Value,
}
macro_rules! extract_optional_field {
($value:expr, $fields:expr, $map_func:expr) => {
$fields
.iter()
.fold(Some(&$value), |acc, &key| acc.and_then(|val| val.get(key)))
.map($map_func)
.flatten()
};
}
macro_rules! extract_field {
($value:expr, $fields:expr, $map_func:expr) => {
extract_optional_field!($value, $fields, $map_func)
.ok_or(PacketBad.context("Field is missing or is not parsable."))?
};
}
impl EpicProtocol {
pub fn new(credentials: Credentials, timeout_settings: TimeoutSettings) -> GDResult<Self> {
Ok(Self {
client: HttpClient::from_url(EPIC_API_ENDPOINT, &Some(timeout_settings), None)?,
credentials,
})
}
pub fn auth_by_external(&self) -> GDResult<String> { Ok(String::new()) }
pub fn auth_by_client(&mut self) -> GDResult<String> {
let body = [
("grant_type", "client_credentials"),
("deployment_id", self.credentials.deployment),
];
let auth_format = format!("{}:{}", self.credentials.id, self.credentials.secret);
let auth_base = BASE64_STANDARD.encode(auth_format);
let auth = format!("Basic {}", auth_base.as_str());
let authorization = auth.as_str();
let headers = [
("Authorization", authorization),
("Content-Type", "application/x-www-form-urlencoded"),
];
let response =
self.client
.post_json_with_form::<ClientTokenResponse>("/auth/v1/oauth/token", Some(&headers), &body)?;
Ok(response.access_token)
}
pub fn query_raw(&mut self, address: &SocketAddr) -> GDResult<Value> {
let port = address.port();
let address = address.ip().to_string();
let body = format!(
"{{\"criteria\":[{{\"key\":\"attributes.ADDRESS_s\",\"op\":\"EQUAL\",\"value\":\"{}\"}}]}}",
address
);
let body = serde_json::from_str::<Value>(body.as_str()).map_err(|e| JsonParse.context(e))?;
let token = if self.credentials.auth_by_external {
self.auth_by_external()?
} else {
self.auth_by_client()?
};
let authorization = format!("Bearer {}", token);
let headers = [
("Content-Type", "application/json"),
("Accept", "application/json"),
("Authorization", authorization.as_str()),
];
let url = format!("/matchmaking/v1/{}/filter", self.credentials.deployment);
let response: QueryResponse = self.client.post_json(url.as_str(), Some(&headers), body)?;
if let Value::Array(sessions) = response.sessions {
if sessions.is_empty() {
return Err(PacketBad.context("No servers provided."));
}
for session in sessions.into_iter() {
let attributes = session
.get("attributes")
.ok_or(PacketBad.context("Expected attributes field missing in sessions."))?;
let address_match = attributes
.get("ADDRESSBOUND_s")
.and_then(Value::as_str)
.map_or(false, |v| v == address || v == format!("0.0.0.0:{}", port))
|| attributes
.get("GAMESERVER_PORT_1")
.and_then(Value::as_u64)
.map_or(false, |v| v == port as u64);
if address_match {
return Ok(session);
}
}
return Err(
PacketBad.context("Servers were provided but the specified one couldn't be found amongst them.")
);
}
Err(PacketBad.context("Expected session field to be an array."))
}
pub fn query(&mut self, address: &SocketAddr) -> GDResult<Response> {
let value = self.query_raw(address)?;
let build_version = extract_optional_field!(value, ["attributes", "BUILDID_s"], Value::as_str);
let minor_version = extract_optional_field!(value, ["attributes", "MINORBUILDID_s"], Value::as_str);
let game_version = match (build_version, minor_version) {
(Some(b), Some(m)) => Some(format!("{b}.{m}")),
_ => None,
};
Ok(Response {
name: extract_field!(value, ["attributes", "CUSTOMSERVERNAME_s"], Value::as_str).to_string(),
map: extract_field!(value, ["attributes", "MAPNAME_s"], Value::as_str).to_string(),
has_password: extract_field!(value, ["attributes", "SERVERPASSWORD_b"], Value::as_bool),
players_online: extract_field!(value, ["totalPlayers"], Value::as_u64) as u32,
players_maxmimum: extract_field!(value, ["settings", "maxPublicPlayers"], Value::as_u64) as u32,
players: vec![],
game_version,
raw: value,
})
}
}
pub fn query(credentials: Credentials, address: &SocketAddr) -> GDResult<Response> {
query_with_timeout(credentials, address, None)
}
pub fn query_with_timeout(
credentials: Credentials,
address: &SocketAddr,
timeout_settings: Option<TimeoutSettings>,
) -> GDResult<Response> {
let mut client = EpicProtocol::new(credentials, timeout_settings.unwrap_or_default())?;
client.query(address)
}

Some files were not shown because too many files have changed in this diff Show more