diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6f97c1f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +* +!build.rs +!Cargo.toml +!Cargo.lock +!src +!config diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..071fb8d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,96 @@ +# there is only one editorconfig for our project so it is automatically the root config +root = true + +[*] +# the charset helps with guaranteeing that all chars are encoded in the same way +charset = utf-8 + +# we solely use spaces for our formatting, so the indent needs to be fixed as well +indent_size = 4 +tab_width = 4 +ij_continuation_indent_size = 4 +indent_style = space +ij_smart_tabs = false + +# provide the visual guide and hard wrap, so we don't write overly long lines (but don't wrap automatically) +max_line_length = 120 +ij_visual_guides = 100 +ij_wrap_on_typing = false + +# the final newline helps with old/unix tools so that they can properly print files +insert_final_newline = true + +# trailing whitespaces serve absolutely no value, so we can trim them away +trim_trailing_whitespace = true + +# we do not use the formatter tag at all, since all files need to be compliant +ij_formatter_tags_enabled = false +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on + +[.editorconfig] +# spaces after the comma are in line with our other codestyle and increase the readability +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_comma = false + +# colons are used as regular characters, so we use no spaces at all +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_after_colon = false + +# spaces around the assignment operator increase the readability +ij_editorconfig_spaces_around_assignment_operators = true + +# since there are some very long keys, this is detrimental to the readability +ij_editorconfig_align_group_field_declarations = false + +[{*.yml,*.yaml}] +# yaml structures can get nested very easily, so we reduce the indent to compensate for that +indent_size = 2 +tab_width = 2 + +# some keys can get very long, so we don't want to align all of them together +ij_yaml_align_values_properties = do_not_align + +# the indents for empty lines serve absolutely no value, so we remove them +ij_yaml_block_mapping_on_new_line = false +ij_yaml_keep_indents_on_empty_lines = false + +# sequence values are already kind of indented because of the hyphen, so we don't indent additionally +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_indent_sequence_value = false + +# yaml files are used as configuration so line breaks are crucial for the readability +ij_yaml_keep_line_breaks = true +ij_yaml_sequence_on_new_line = false + +# we don't need spaces before colons +ij_yaml_space_before_colon = false + +# we don't need any spaces within brackets or braces as this is the compressed representation +ij_yaml_spaces_within_braces = false +ij_yaml_spaces_within_brackets = false + +[*.md] +# we want spaces after syntactical elements so we enforce them +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true + +# indents on empty lines serve no real purpose and can therefore be trimmed away +ij_markdown_keep_indents_on_empty_lines = false + +# paragraphs have exactly one +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_between_paragraphs = 1 + +# block elements have exactly one newline around them to increase the readability +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_max_lines_around_block_elements = 1 + +# headers have exactly one newline around them to increase the readability +ij_markdown_min_lines_around_header = 1 +ij_markdown_max_lines_around_header = 1 + +[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] +ij_toml_keep_indents_on_empty_lines = false diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3fcd087 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" +- package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/images/logo.png b/.github/images/logo.png new file mode 100644 index 0000000..0ae06d6 Binary files /dev/null and b/.github/images/logo.png differ diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..d8b3ac7 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,74 @@ +name: Docker + +on: + push: + branches: + - main + tags: + - '*' + pull_request: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + actions: read + security-events: write + id-token: write + steps: + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login into GitHub Container Registry + if: ${{ github.event_name != 'pull_request' }} + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + flavor: | + latest=true + labels: | + org.opencontainers.image.title=mcexport + org.opencontainers.image.description=Minecraft Server Prometheus Exporter + org.opencontainers.image.vendor=Scrayos UG (haftungsbeschränkt) + org.opencontainers.image.authors=Joshua Dean Küpper + org.opencontainers.image.url=https://github.com/scrayosnet/mcexport + org.opencontainers.image.documentation=https://github.com/scrayosnet/mcexport + org.opencontainers.image.source=https://github.com/scrayosnet/mcexport + org.opencontainers.image.licenses=MIT + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ github.ref_type == 'tag' }} + tags: ${{ steps.meta.outputs.tags }} + annotations: ${{ steps.meta.outputs.annotations }} + labels: ${{ steps.meta.outputs.labels }} + provenance: false + sbom: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..257f1e5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,54 @@ +name: Build and Release Binaries + +on: + release: + types: + - published + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - 'ubuntu-latest' + - 'macos-latest' + - 'windows-latest' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Apply caching + uses: swatinem/rust-cache@v2 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Build binary + run: | + cargo build --release + env: + CARGO_BUILD_TARGET: ${{ matrix.os == 'windows-latest' && 'x86_64-pc-windows-msvc' || 'x86_64-unknown-linux-gnu' || 'x86_64-apple-darwin' }} + + - name: Upload binary artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.os }}-binary + path: target/release/${{ github.event.repository.name }}${{ matrix.os == 'windows-latest' && '.exe' || '' }} + + release: + needs: build + runs-on: ubuntu-latest + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: binaries + + - name: Upload release binaries + uses: softprops/action-gh-release@v2 + with: + files: binaries/* diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..d3e745d --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,59 @@ +name: Rust + +on: + push: + branches: + - main + tags: + - v* + pull_request: + branches: + - main + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + permissions: + contents: read + security-events: write + runs-on: ubuntu-latest + steps: + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Apply caching + uses: swatinem/rust-cache@v2 + + - name: Install required cargo + run: cargo install clippy-sarif sarif-fmt + + - name: Check OpenSSL + run: (! cargo tree -i openssl 2> /dev/null) + + - name: Build with cargo + run: cargo build --verbose + + - name: Run tests with cargo + run: cargo test --verbose --all-features + + - name: Check format + run: cargo fmt --check + + - name: Perform linting + run: + cargo clippy + --all-features + --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt + continue-on-error: true + + - name: Upload analysis results to GitHub + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: rust-clippy-results.sarif + wait-for-processing: true + + - name: Perform audit + run: cargo audit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90db511 --- /dev/null +++ b/.gitignore @@ -0,0 +1,98 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +*.iml + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# Block the entire idea folder alltogether +.idea/ + +### Rust template +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Compiled resources +build/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..704b032 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +To view the changes for this project, please look into the [releases][releases-overview]. They contain lists of what was +changed between versions and list all the relevant Pull Requests. + +[releases-overview]: https://github.com/scrayosnet/mcexport/releases diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d16a881 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,136 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org + +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html + +[Mozilla CoC]: https://github.com/mozilla/diversity + +[FAQ]: https://www.contributor-covenant.org/faq + +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..54a25c7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +We're always open to contributions to this project! Below are some small guidelines to follow when submitting any Pull +Requests for this project. Thanks for contributing! + +## Code of Conduct + +Participation in this project comes under the [Contributor Covenant Code of Conduct][code-of-conduct]. + +## Pull Requests + +In order to contribute to this project, please note: + +* We follow the [GitHub Pull Request Model][github-pull-request-model] for all contributions. +* For new features, documentation **must** be included. +* All submissions require review before being merged. +* Once review has occurred, please squash your Pull Request into a single commit and rebase it. +* Write [meaningful commit message][commit-messages] for your contribution. +* See this [blog post][logging-levels] for choosing the right logging levels. +* Please follow the code formatting instructions below. + +### Formatting + +Please note the following rules for formatting your code: + +* Format all Rust code with [rustfmt][rustfmt-docs]. You may also use [clippy][clippy-docs] which performs linting. +* Remove trailing whitespaces in all files. +* Ensure any new or modified files have a [trailing newline][trailing-newline-stackoverflow]. + +This project comes also with an [.editorconfig][editorconfig-docs] that should already handle most of the cases outlined +above will always be extended to match these criteria as close as possible. + +### Continuous Integration + +Automatic checks are performed through [GitHub Actions][github-actions-docs] and run for every submitted Pull Request. +They are all expected to run without any warnings or errors, until the Pull Request will be merged. Please look into the +output of the Workflows that were executed on your Pull Request to check whether everything complies with our checks. + +[code-of-conduct]: CODE_OF_CONDUCT.md + +[github-pull-request-model]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests + +[commit-messages]: https://cbea.ms/git-commit/ + +[logging-levels]: https://medium.com/@tom.hombergs/tip-use-logging-levels-consistently-913b7b8e9782 + +[rustfmt-docs]: https://github.com/rust-lang/rustfmt + +[clippy-docs]: https://github.com/rust-lang/rust-clippy + +[trailing-newline-stackoverflow]: https://stackoverflow.com/questions/5813311/no-newline-at-end-of-file + +[editorconfig-docs]: https://editorconfig.org/ + +[github-actions-docs]: https://github.com/features/actions diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c807154 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1532 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hickory-proto" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07698b8420e2f0d6447a436ba999ec85d8fbf2a398bbd737b82cac4a2e96e512" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.4.0", + "ipnet", + "once_cell", + "rand", + "thiserror 1.0.69", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28757f23aa75c98f254cf0405e6d8c25b831b32921b050a66692427679b1f243" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "lru-cache", + "once_cell", + "parking_lot", + "rand", + "resolv-conf", + "smallvec", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "itoa" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "mcexport" +version = "0.1.0" +dependencies = [ + "axum", + "hickory-resolver", + "prometheus-client", + "serde", + "serde_json", + "serde_test", + "thiserror 2.0.3", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "307e3004becf10f5a6e0d59d20f3cd28231b0e0827a96cd3e0ce6d14bc1e4bb3" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prometheus-client" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504ee9ff529add891127c4827eb481bd69dc0ebc72e9a682e187db4caa60c3ca" +dependencies = [ + "dtoa", + "itoa", + "parking_lot", + "prometheus-client-derive-encode", +] + +[[package]] +name = "prometheus-client-derive-encode" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_test" +version = "1.0.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl 2.0.3", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +dependencies = [ + "form_urlencoded", + "idna 1.0.3", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d521ff7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "mcexport" +description = "Minecraft Server Prometheus Probe Exporter" +version = "0.1.0" +authors = [ + "Joshua Dean Küpper " +] +license = "MIT" +repository = "https://github.com/scrayosnet/mcexport" +readme = "README.md" +documentation = "https://github.com/scrayosnet/mcexport" +homepage = "https://github.com/scrayosnet/mcexport" +keywords = ["minecraft", "prometheus", "monitoring", "exporter", "ping"] +edition = "2021" + +[dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "macros", "net"] } +axum = { version = "0.7", default-features = false, features = ["http1", "tokio", "tower-log", "tracing", "query"] } +tracing = "0.1" +serde = { version = "1.0.214", features = ["derive"] } +serde_json = "1" +hickory-resolver = "0.24.1" +thiserror = "2.0.3" +prometheus-client = "0.22.3" +tracing-subscriber = "0.3.18" +tower-http = { version = "0.6.1", features = ["trace"] } + +[dev-dependencies] +serde_test = "1" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c851a84 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM rust:alpine@sha256:466dc9924d265455aa73e72fd9cdac9db69ce6a988e6f0e6baf852db3485d97d AS builder + +# specify our build directory +WORKDIR /usr/src/mcexport + +# copy the source files into the engine +COPY . . + +# install dev dependencies and perform build process +RUN set -eux \ + && apk add --no-cache musl-dev \ + && cargo build --release + + +FROM scratch + +# declare our metrics port +EXPOSE 8080 + +# copy the raw binary into the new image +COPY --from=builder "/usr/src/mcexport/target/release/mcexport" "/mcexport" + +# copy the users and groups for the nobody user and group +COPY --from=builder "/etc/passwd" "/etc/passwd" +COPY --from=builder "/etc/group" "/etc/group" + +# we run with minimum permissions as the nobody user +USER nobody:nobody + +# just execute the raw binary without any wrapper +ENTRYPOINT ["/mcexport"] diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..fb5dde4 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Scrayos UG (haftungsbeschränkt) + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..eef8db5 --- /dev/null +++ b/README.md @@ -0,0 +1,226 @@ +![The official Logo of mcexport](.github/images/logo.png "mcexport") + +![A visual badge for the latest release](https://img.shields.io/github/v/release/scrayosnet/mcexport "Latest Release") +![A visual badge for the workflow status](https://img.shields.io/github/actions/workflow/status/scrayosnet/mcexport/docker.yml "Workflow Status") +![A visual badge for the dependency status](https://img.shields.io/librariesio/github/scrayosnet/mcexport "Dependencies") +![A visual badge for the Docker image size](https://ghcr-badge.egpl.dev/scrayosnet/mcexport/size "Image Size") +![A visual badge for the license](https://img.shields.io/github/license/scrayosnet/mcexport "License") + +mcexport is a [Prometheus][prometheus-docs] prober exporter to query the publicly available information on arbitrary +Minecraft servers through the official [ping protocol][ping-protocol-docs]. Unlike many other available exporters, this +exporter is explicitly designed to query Minecraft servers from the outside. + +The idea is similar to the official [Blackbox exporter][blackbox-exporter], but mcexport is specialized in probing +Minecraft servers along with their publicly available Server List information. It supports the Minecraft protocol for +all servers that are using Minecraft version 1.7 and above. + +## Motivation + +While there are many Minecraft-related exporters available at GitHub, there's none that use the +[Multi-Target Exporter Pattern][multi-target-exporter-docs] and that can be used without exposing [RCON][rcon-docs]. +Instead of exposing the internal processes and metrics of a single Minecraft server, mcexport probes any requested +Minecraft server through the publicly available data. + +Additionally, mcexport was developed in [Rust][rust-docs], enabling great performance and therefore allowing to scrape +server metrics on a large scale. Combined with its well-tested reliability and hardened security, this makes large-scale +analysis of the Minecraft server ecosystem possible. Therefore, mcexport can be seen as a specialization of the official +[Blackbox exporter][blackbox-exporter] that offers fast and efficient Minecraft metric probing. + +The difference between mcexport and other existing solutions like +[sladkoff/minecraft-prometheus-exporter][sladkoff-exporter], [cpburnz/minecraft-prometheus-exporter][cpburnz-exporter] +and [dirien/minecraft-prometheus-exporter][dirien-exporter] is that those specialize in aggregating detailed metrics on +a single Minecraft server (either as a plugin, mod or external service) while mcexport does aggregate only public data +about dynamically submitted Minecraft servers. + +The only existing solution that also supports dynamic probing is +[Scientistguy/minecraft-prometheus-exporter][scientist-exporter], which requires [RCON][rcon-docs] to be enabled. That +was not an option, as we want to scrape metrics from servers that we have no control over. Therefore, mcexport is the +only solution for this scenario, offering dynamic probing on public Minecraft servers without any configuration or +necessary adjustments. + +Since metrics are bound to a specific instant in time, mcexport needed to be lightweight, scalable and hardened to be +used as a reliable exporter in aggregating data. We offer robust container images and wanted to minimize any +configuration and flags as much as possible. Any configuration is only done within Prometheus, making mcexport +ready for [Kubernetes][kubernetes-docs] from the get-go through [Probe][probe-docs]. + +## Feature Highlights + +* Scrape multiple Minecraft servers through the public [Server List Ping protocol][ping-protocol-docs]. +* Expose information about the supported versions, current and maximum player count, latency and player samples. +* Configure the targets and scrape intervals directly within Prometheus. +* Start with zero configuration, no external resources or dependencies for mcexport. +* Use it without applying any changes to the queried Minecraft servers. + +## Available Modules + +To select a specific protocol version to ping a target, you can use the `module` field of the [Probe Spec][probe-docs] +and set it to the required [protocol version number][pvn-docs]. Any number (negative and positive) is supported and +will be sent as the desired protocol version to the target. mcexport supports the latest iteration of the +[Server List Ping protocol][ping-protocol-docs] that all servers since Minecraft 1.7 use. + +If no module is specified explicitly, the most recent version at the time of the last release is used instead. +Therefore, this may not be the latest version at any time, but at least a somewhat recent version. You can override +the version with the `module` field until a new release is created. + +Ideally, we could use (and fall back to) `-1` as the protocol version, as that is recommended to use, when determining +the appropriate/maximum supported version of a server. However, our practical tests revealed that only very few servers +would support this convention and would just reply with `-1` on their side, meaning unsupported. And servers that +support `-1` also support specifying a recent version. Therefore, the "somewhat recent" version is the best we can do by +default and without further configuration. + +## Getting Started + +> [!WARNING] +> While mcexport is stable, please view the individual releases for any version-specific details that need to be +> considered while deploying. Changes are performed in adherence to [Semantic Versioning][semver-docs]. + +### Setup mcexport + +Before any Minecraft servers can be probed, we first need to set up mcexport on the corresponding machine. This does not +have to be the machine of the probed targets, but instead, it will probably be the same system that Prometheus runs on. + +#### From Binaries + +To run mcexport from a binary file, download the appropriate binary from our [releases][github-releases], make it +executable and run it within the shell of your choice: + +```shell +chmod +x mcexport +./mcexport +``` + +#### Using Docker + +To run mcexport within Docker, we can use the images that we release within our [Container Registry][github-ghcr]. +Those images are hardened and provide the optimal environment to execute mcexport in a containerized environment. + +```shell +docker run --rm \ + -p 10026/tcp \ + --name mcexport \ + ghcr.io/scrayosnet/mcexport:latest +``` + +#### Using Kubernetes + +There's currently no public [Helm Chart][helm-chart-docs] for mcexport. We're open for contributions! In the meantime, +you can create your own deployment using [Kustomize][kustomize-docs] or any other tooling of your choice. + +### Check Setup + +To verify whether everything works as expected, we can invoke the following command on the same machine and observe the +reported result: + +```shell +curl --request GET -sL --url 'http://localhost:10026/probe?target=mc.justchunks.net' +``` + +If the result shows any metrics, mcexport is now setup successfully and can be used to probe Minecraft servers. + +### Configure Prometheus + +Now that mcexport is working as expected, we need to configure Prometheus to probe any Minecraft server targets. +Depending on the individual setup, this can be done in one of those ways: + +#### Normal Configuration + +In a normal (non-Kubernetes) deployment of Prometheus, we can probe any Minecraft server with a scrape +configuration like this: + +```yaml +scrape_configs: +- job_name: "mcexport" + scrape_interval: 60s + scrape_timeout: 30s + static_configs: + - targets: + - 'mc.justchunks.net' + - 'example.com' +``` + +#### CRD-based Configuration + +Assuming, there's a namespace `mcexport` with a deployment of mcexport, and a corresponding service `mcexport` is in +that namespace, that exposes the mcexport instances internally, we can probe metrics with this CRD configuration +using the [Probe][probe-docs] resource: + +```yaml +apiVersion: monitoring.coreos.com/v1 +kind: Probe +metadata: + name: mcexport + namespace: mcexport +spec: + prober: + url: 'mcexport.mcexport:10026' + interval: 60s + scrapeTimeout: 30s + targets: + staticConfig: + static: + - 'mc.justchunks.net' + - 'example.com' +``` + +## Reporting Security Issues + +To report a security issue for this project, please note our [Security Policy][security-policy]. + +## Code of Conduct + +Participation in this project comes under the [Contributor Covenant Code of Conduct][code-of-conduct]. + +## How to contribute + +Thanks for considering contributing to this project! In order to submit a Pull Request, please read +our [contributing][contributing-guide] guide. This project is in active development, and we're always happy to receive +new contributions! + +## License + +This project is developed and distributed under the MIT License. See [this explanation][mit-license-doc] for a rundown +on what that means. + +[prometheus-docs]: https://prometheus.io/ + +[ping-protocol-docs]: https://wiki.vg/Server_List_Ping + +[blackbox-exporter]: https://github.com/prometheus/blackbox_exporter + +[multi-target-exporter-docs]: https://prometheus.io/docs/guides/multi-target-exporter + +[rcon-docs]: https://wiki.vg/RCON + +[rust-docs]: https://www.rust-lang.org/ + +[sladkoff-exporter]: https://github.com/sladkoff/minecraft-prometheus-exporter + +[cpburnz-exporter]: https://github.com/cpburnz/minecraft-prometheus-exporter + +[dirien-exporter]: https://github.com/dirien/minecraft-prometheus-exporter + +[scientist-exporter]: https://github.com/Sciencentistguy/minecraft-prometheus-exporter + +[kubernetes-docs]: https://kubernetes.io/ + +[pvn-docs]: https://wiki.vg/Protocol_version_numbers + +[probe-docs]: https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api.md#monitoring.coreos.com/v1.Probe + +[semver-docs]: https://semver.org/lang/de/ + +[github-releases]: https://github.com/scrayosnet/mcexport/releases + +[github-ghcr]: https://github.com/scrayosnet/mcexport/pkgs/container/mcexport + +[helm-chart-docs]: https://helm.sh/ + +[kustomize-docs]: https://kustomize.io/ + +[security-policy]: SECURITY.md + +[code-of-conduct]: CODE_OF_CONDUCT.md + +[contributing-guide]: CONTRIBUTING.md + +[mit-license-doc]: https://choosealicense.com/licenses/mit/ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..a5a04e3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +We take the security of our projects very seriously. Therefore, we try to react to reports as fast as possible. However, +please allow us some time to implement the necessary changes. + +## Reporting a Vulnerability + +Please report (suspected) security vulnerabilities to **[security@scrayos.net](mailto:security@scrayos.net)** and do not +use the regular GitHub issues. You will receive a response from us within 48 hours. If the issue is confirmed, we will +release a patch as soon as possible, depending on the complexity and severity. If you do not hear from us, please follow +up to confirm, we received your original message. + +Please include as much information as possible with your report. If you have steps to reproduce, that will greatly speed +up the fix. diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0ebe438 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,274 @@ +mod ping; +mod probe; +mod protocol; + +use crate::ping::{get_server_status, PingError, PingStatus}; +use crate::probe::ResolutionResult::{Plain, Srv}; +use crate::probe::{ProbeError, ProbeInfo}; +use crate::protocol::ProtocolError; +use axum::body::Body; +use axum::extract::{Query, State}; +use axum::http::header::CONTENT_TYPE; +use axum::http::StatusCode; +use axum::response::{Html, IntoResponse, Response}; +use axum::routing::get; +use axum::Router; +use hickory_resolver::TokioAsyncResolver; +use prometheus_client::encoding::text::encode; +use prometheus_client::metrics::family::Family; +use prometheus_client::metrics::gauge::Gauge; +use prometheus_client::registry::Registry; +use std::error::Error; +use std::net::SocketAddr; +use std::sync::atomic::{AtomicU32, AtomicU64}; +use std::sync::Arc; +use thiserror::Error; +use tokio::net::TcpListener; +use tower_http::trace::TraceLayer; +use tracing::{info, instrument}; +use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::Layer; + +/// MetricError is the public response error wrapper for all errors that can be relayed to the caller. +/// +/// This wraps all errors of the child modules into their own error type, so that they can be forwarded to the caller +/// of the probe requests. Those errors occur while trying to assemble the response for a specific probe and are then +/// wrapped into a parent type so that they can be more easily traced. +#[derive(Error, Debug)] +pub enum MetricError { + /// An error occurred while reading or writing to the underlying byte stream. + #[error("failed to resolve the intended target address: {0}")] + ProbeError(#[from] ProbeError), + /// An error occurred while encoding, decoding or interpreting the Minecraft protocol. + #[error("failed to interpret or encode protocol messages: {0}")] + ProtocolError(#[from] ProtocolError), + /// An error occurred while reading or writing to the underlying byte stream. + #[error("failed to communicate with the target server: {0}")] + PingError(#[from] PingError), +} + +/// AppState contains various, shared resources for the state of the application. +/// +/// The state of the application can be shared across all requests to benefit from their caching, resource consumption +/// and configuration. The access is handled through [Arc], allowing for multiple threads to use the same resource +/// without any problems regarding thread safety. +#[derive(Debug)] +pub struct AppState { + pub resolver: TokioAsyncResolver, +} + +impl IntoResponse for MetricError { + fn into_response(self) -> Response { + // notify in the log (as we don't see it otherwise) + info!(cause = &self.to_string(), "failed to resolve the target"); + + // respond with the unsuccessful metrics + generate_metrics_response(0, |_| {}) + } +} + +impl IntoResponse for PingStatus { + fn into_response(self) -> Response { + // return the generated metrics + generate_metrics_response(1, |registry| { + // ping duration (gauge) + let ping_duration = Family::<(), Gauge>::default(); + registry.register( + "ping_duration_seconds", + "The duration that's elapsed since the probe request", + ping_duration.clone(), + ); + ping_duration + .get_or_create(&()) + .set(self.ping.as_secs_f64()); + + // srv record (Gauge) + let address_srv = Family::<(), Gauge>::default(); + registry.register( + "address_srv_info", + "Whether there was an SRV record for the hostname", + address_srv.clone(), + ); + address_srv + .get_or_create(&()) + .set(if self.srv { 1 } else { 0 }); + + let players_online = Family::<(), Gauge>::default(); + registry.register( + "players_online_total", + "The number of players that are currently online", + players_online.clone(), + ); + players_online + .get_or_create(&()) + .set(self.status.players.online); + + let players_max = Family::<(), Gauge>::default(); + registry.register( + "players_max_total", + "The number of players that can join at maximum", + players_max.clone(), + ); + players_max.get_or_create(&()).set(self.status.players.max); + + let players_samples_count = Family::<(), Gauge>::default(); + registry.register( + "players_samples_total", + "The number of sample entries that have been sent", + players_samples_count.clone(), + ); + players_samples_count.get_or_create(&()).set( + self.status + .players + .sample + .map_or_else(|| 0usize, |s| s.len()) as i64, + ); + + let protocol_version = Family::<(), Gauge>::default(); + registry.register( + "protocol_version_info", + "The numeric network protocol version", + protocol_version.clone(), + ); + protocol_version + .get_or_create(&()) + .set(self.status.version.protocol); + + let favicon_bytes = Family::<(), Gauge>::default(); + registry.register( + "favicon_bytes", + "The size of the favicon in bytes", + favicon_bytes.clone(), + ); + let size = match self.status.favicon { + Some(icon) => icon.len(), + None => 0, + }; + favicon_bytes.get_or_create(&()).set(size as i64); + }) + } +} + +/// Initializes the application and creates all necessary resources for the operation. +/// +/// This binds the server socket and starts the HTTP server to serve the probe requests of Prometheus. This also +/// configures the corresponding routes for the status and probe endpoint and makes them publicly available. The +/// prometheus registry and metrics are initialized and made ready for the first probe requests. +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::registry() + .with( + tracing_subscriber::fmt::layer() + .compact() + .with_filter(LevelFilter::INFO), + ) + .init(); + + // initialize the application state + let state = AppState { + resolver: TokioAsyncResolver::tokio_from_system_conf().expect("failed to get DNS resolver"), + }; + + // initialize the axum app with all routes + let state = Arc::new(state); + let app = Router::new() + .route("/", get(handle_root)) + .route("/probe", get(handle_probe)) + .layer(TraceLayer::new_for_http()) + .with_state(state); + + // bind the socket address on all interfaces + let addr = SocketAddr::from(([0, 0, 0, 0], 10026)); + info!(addr = addr.to_string(), "binding socket address"); + let listener = TcpListener::bind(addr).await?; + info!(addr = addr.to_string(), "successfully bound server socket"); + + // serve the axum service on the bound socket address + axum::serve(listener, app) + .await + .expect("failed to serve axum server"); + info!("http server stopped successfully"); + + // exit with success + Ok(()) +} + +/// Handles and answers all axum requests on the root path "/". +/// +/// It statically returns "mcexport" so that it can be used as a startup probe in Kubernetes. That means that this +/// endpoint can be checked for its response: Once there is a response, this means that mcexport is successfully +/// initialized and probe requests may be received and handled. It can also be used as a liveness probe to check whether +/// mcexport is still running as expected. Since mcexport is stateless and has no permanent connections, this endpoint +/// can accurately reflect the readiness and liveness of mcexport. +#[instrument] +async fn handle_root() -> Response { + Html("mcexport – Probe").into_response() +} + +/// Handles and answers all axum requests on the probing path "/probe". +/// +/// This endpoint is invoked by Prometheus' probing requests and issues pings to the requested targets. Prometheus will +/// send [info][ProbeInfo] on the corresponding target, and this endpoint will answer with the status and metrics of +/// this ping operation. The ping is only started once the request comes in and Prometheus is responsible for scheduling +/// the requests to this endpoint regularly. +#[instrument] +async fn handle_probe( + Query(info): Query, + State(state): State>, +) -> Result { + // try to resolve the real probe address + let resolve_result = info.target.to_socket_addrs(&state.resolver).await?; + + // issue the status request + let ping_response = match resolve_result { + Srv(addr) => get_server_status(&info, &addr, true).await?, + Plain(addr) => get_server_status(&info, &addr, false).await?, + }; + + Ok(ping_response) +} + +/// Generates a response for the supplied success state and optionally allows adding more metrics. +/// +/// This initializes the [registry][Registry] for this request and registers the success metric with the supplied state +/// within it. Then the closure is called to add more dynamic metrics (if desired). Finally, the metrics are encoded +/// into a buffer and the response is created with the appropriate content type and status code. +fn generate_metrics_response(success_state: i64, add_metrics: F) -> Response +where + F: FnOnce(&mut Registry), +{ + // create a new registry with the appropriate prefix + let mut registry = Registry::with_prefix("mcexport"); + + // create and register the success metric + let success = Family::<(), Gauge>::default(); + registry.register( + "success", + "Whether the probe operation was successful", + success.clone(), + ); + + // set the success status of this metric + success.get_or_create(&()).set(success_state); + + // add dynamic, desired metrics + add_metrics(&mut registry); + + // create a new buffer to store the metrics in + let mut buffer = String::new(); + + // encode the metrics content into the buffer + encode(&mut buffer, ®istry).expect("failed to encode metrics into the buffer"); + + // return a response of the metrics specification + Response::builder() + .status(StatusCode::OK) + .header( + CONTENT_TYPE, + "application/openmetrics-text; version=1.0.0; charset=utf-8", + ) + .body(Body::from(buffer)) + .expect("failed to build success target response") +} diff --git a/src/ping.rs b/src/ping.rs new file mode 100644 index 0000000..bb4e116 --- /dev/null +++ b/src/ping.rs @@ -0,0 +1,204 @@ +//! This module defines and handles the network communication and data layer of Minecraft communication. +//! +//! While the protocol module handles the details of the communication protocol, the ping module handles the network +//! communication in terms of establishing a TCP connection to the server. This includes the initialization of the data +//! stream to the target without any specifics of the Minecraft protocol. The ping packet takes the result of the +//! protocol communication and wraps it into easily usable data structs. + +use crate::probe::ProbeInfo; +use crate::protocol::{execute_ping, retrieve_status, HandshakeInfo, ProtocolError}; +use serde::Deserialize; +use serde_json::from_str; +use std::net::SocketAddr; +use std::time::Duration; +use thiserror::Error; +use tokio::net::TcpStream; +use tracing::info; + +/// The (at the time of the last release) most recent, supported protocol version. +/// +/// As per the [Server List Ping][https://wiki.vg/Server_List_Ping#Handshake] documentation, it is a convention to use +/// `-1` to determine the version of the server. It would therefore be ideal to use this value. However, it turned out +/// that very few servers supported this convention, so we just go with the most recent version, which showed better +/// results in all cases. +const LATEST_PROTOCOL_VERSION: isize = 768; + +/// PingError is the internal error type for all errors related to the network communication. +/// +/// This includes errors with the IO involved in establishing a TCP connection or transferring bytes from mcexport to +/// a target server. Additionally, this also covers the errors that occur while parsing and interpreting the results +/// returned by the corresponding server, that don't have to do with the protocol itself. +#[derive(Error, Debug)] +pub enum PingError { + /// No connection could be initiated to the target server (it is not reachable). + #[error("failed to connect to the server")] + CannotReach, + /// The supplied JSON responses of the server could not be parsed into valid JSON. + #[error("failed to parse illegal JSON response: {0}: \"{1}\"")] + InvalidJson(#[source] serde_json::error::Error, String), + /// An error occurred while trying to perform the server handshake or ping. + #[error("mismatch in communication protocol: {0}")] + ProtocolMismatch(#[from] ProtocolError), + /// An error occurred while parsing the protocol version (module). + #[error("illegal protocol version: {0}")] + IllegalProtocol(String), +} + +/// The information on the protocol version of a server. +#[derive(Debug, Deserialize)] +pub struct ServerVersion { + /// The textual protocol version to display this version visually. + pub name: String, + /// The numeric protocol version (for compatibility checking). + pub protocol: i64, +} + +/// The information on a single, sampled player entry. +#[derive(Debug, Deserialize, PartialEq)] +pub struct ServerPlayer { + /// The visual name to display this player. + pub name: String, + /// The unique identifier to reference this player. + pub id: String, +} + +/// The information on the current, maximum and sampled players. +#[derive(Debug, Deserialize)] +pub struct ServerPlayers { + /// The current number of players that are online at this moment. + pub online: u32, + /// The maximum number of players that can join (slots). + pub max: u32, + /// An optional list of player information samples (version hover). + pub sample: Option>, +} + +/// The MOTD of a pinged server within the different formats. +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum ServerDescription { + /// The MOTD was configured/reported with legacy formatting (plain text). + Plain(String), + /// The MOTD was configured/reported with text components (rich text). + Component { text: String }, +} + +/// The self-reported status of a pinged server with all public metadata. +#[derive(Debug, Deserialize)] +pub struct ServerStatus { + /// The version and protocol information of the server. + pub version: ServerVersion, + /// The current, maximum and sampled players of the server. + pub players: ServerPlayers, + /// The MOTD (Motto Of The Day) of the server. + pub description: ServerDescription, + /// The optional favicon of the server. + pub favicon: Option, +} + +/// The full status of the ping operation along with the resolution information and ping duration. +#[derive(Debug, Deserialize)] +pub struct PingStatus { + /// Whether an SRV record was used to resolve the real address of the server. + pub srv: bool, + /// The latency to get information from one side to the other (RTT/2). + pub ping: Duration, + /// The self-reported status of the server. + pub status: ServerStatus, +} + +/// Requests the status of the supplied server and records the ping duration. +/// +/// This opens a new [TCP stream][TcpStream] and performs a [Server List Ping][https://wiki.vg/Server_List_Ping] +/// exchange, including the general handshake, status request, and ping protocol. If any error with the underlying +/// connection or the communication protocol is encountered, the request is interrupted immediately. The connection is +/// closed after the ping interaction. If the async interaction is stopped, the connection will also be terminated +/// prematurely, accounting for Prometheus' timeouts. +pub async fn get_server_status( + info: &ProbeInfo, + addr: &SocketAddr, + srv: bool, +) -> Result { + // create a new tcp stream to the target + let mut stream = TcpStream::connect(addr) + .await + .map_err(|_| PingError::CannotReach)?; + + // try to use specific, requested version and fall back to "recent" + let protocol_version = match &info.module { + Some(ver) => ver + .parse() + .map_err(|_| PingError::IllegalProtocol(ver.clone()))?, + None => LATEST_PROTOCOL_VERSION, + }; + + // retrieve the server status (general information) + let handshake_info = HandshakeInfo::new( + protocol_version, + info.target.hostname.clone(), + info.target.port, + ); + let status_string = retrieve_status(&mut stream, &handshake_info).await?; + info!("{}", status_string.clone()); + let status: ServerStatus = + from_str(&status_string).map_err(|err| PingError::InvalidJson(err, status_string))?; + + // perform the server ping to measure duration + let ping = execute_ping(&mut stream).await?; + + // wrap everything into a ping response + Ok(PingStatus { srv, ping, status }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ping::ServerDescription::{Component, Plain}; + + #[test] + fn deserialize_component_description() { + let status: ServerStatus = from_str("{\"version\":{\"protocol\":1337,\"name\":\"version 0\"},\"players\":{\"online\":4,\"max\":6,\"sample\":[{\"name\": \"cool\", \"id\": \"test\"}]},\"description\":{\"text\":\"description text\"},\"favicon\":\"favicon content\"}").expect("could not deserialize server status"); + let sample = Some(vec![ServerPlayer { + name: "cool".to_string(), + id: "test".to_string(), + }]); + let favicon = Some("favicon content".to_string()); + + assert_eq!("version 0", status.version.name); + assert_eq!(1337, status.version.protocol); + assert_eq!(4, status.players.online); + assert_eq!(6, status.players.max); + assert_eq!(sample, status.players.sample); + match status.description { + Component { text: des } => assert_eq!("description text", des), + _ => { + panic!("unexpected description type") + } + }; + assert_eq!(favicon, status.favicon); + assert_eq!(favicon, status.favicon); + } + + #[test] + fn deserialize_plain_description() { + let status: ServerStatus = from_str("{\"version\":{\"protocol\":1338,\"name\":\"version 1\"},\"players\":{\"online\":6,\"max\":8,\"sample\":[{\"name\": \"cooler\", \"id\": \"tests\"}]},\"description\":\"description text 2\",\"favicon\":\"favicon content 2\"}").expect("could not deserialize server status"); + let sample = Some(vec![ServerPlayer { + name: "cooler".to_string(), + id: "tests".to_string(), + }]); + let favicon = Some("favicon content 2".to_string()); + + assert_eq!("version 1", status.version.name); + assert_eq!(1338, status.version.protocol); + assert_eq!(6, status.players.online); + assert_eq!(8, status.players.max); + assert_eq!(sample, status.players.sample); + match status.description { + Plain(des) => assert_eq!("description text 2", des), + _ => { + panic!("unexpected description type") + } + }; + assert_eq!(favicon, status.favicon); + } +} diff --git a/src/probe.rs b/src/probe.rs new file mode 100644 index 0000000..c53b588 --- /dev/null +++ b/src/probe.rs @@ -0,0 +1,372 @@ +//! This module defines and handles the Prometheus probe target models and resolution. +//! +//! Therefore, this includes the necessary logic to deserialize, validate and convert the supplied information as well +//! as resolve the real target address. The conversion is based on the defaults of Minecraft, and therefore relevant +//! default values and SRV records are considered while resolving the dynamic target address. It is the responsibility +//! of this module to standardize the desired probing that should be performed for a request. + +use hickory_resolver::error::ResolveError; +use hickory_resolver::proto::rr::RecordType::SRV; +use hickory_resolver::TokioAsyncResolver; +use serde::de::{Unexpected, Visitor}; +use serde::{de, Deserialize, Deserializer}; +use std::fmt; +use std::fmt::{Display, Formatter}; +use std::net::SocketAddr; +use thiserror::Error; +use tracing::log::debug; + +/// ProbeError is the internal error type for all [ProbeInfo] related errors. +/// +/// This includes errors with the resolution of the [ProbeAddress] or any other errors that may occur while trying to +/// make sense of the supplied probe target information. Any [ProbeError] results in mcexport not being able to perform +/// any ping of the desired probe. +#[derive(Error, Debug)] +pub enum ProbeError { + /// The resolution failed because there was a communication error with the responsible DNS name server. + #[error("failed to resolve a DNS record: {0}")] + ResolutionFail(#[from] ResolveError), + /// The resolution failed because no valid A record was specified for the supplied (or configured) hostname. + #[error("could not resolve any \"A\" DNS entries for hostname \"{0}\"")] + CouldNotResolveIp(String), +} + +/// ResolutionResult is the outcome of a DNS resolution for a supplied [ProbeAddress]. +/// +/// The result holds the final resolved [SocketAddr] of a supplied target [ProbeAddress]. It is differentiated between a +/// [plain][Plain] resolution, where no SRV record was found and the supplied hostname was directly resolved to the +/// final IP-Address and an [SRV][Srv] resolution that was performed on the indirect hostname, resolved through the +/// corresponding SRV record. +#[derive(Debug, PartialEq)] +pub enum ResolutionResult { + /// There was an SRV record and the resolved IP address is of the target hostname within this record. + Srv(SocketAddr), + /// There was no SRV record and the resolved IP address is of the original hostname. + Plain(SocketAddr), +} + +/// ProbeInfo is the information supplied by Prometheus for each probe request. +/// +/// This information is supplied for each probe request and needs to be used to ping the right target. We cannot handle +/// requests that come without this query information. While the target address is mandatory, the module is completely +/// optional and not required for the correct operation of mcexport. +#[derive(Debug, Deserialize)] +pub struct ProbeInfo { + /// The target that mcexport should ping for the corresponding probe request. + pub target: ProbeAddress, + /// The module that mcexport should use to ping for the corresponding probe request. + pub module: Option, +} + +/// ProbeAddress is the combination of a hostname or textual IP address with a port. +/// +/// This address should be used to issue a probing ping. The information comes from the request of Prometheus and needs +/// to be validated and parsed before it can be used. To get the real target address, the hostname needs to be resolved +/// and the corresponding SRV records need to be considered. +#[derive(Debug, PartialEq)] +pub struct ProbeAddress { + /// The hostname that should be resolved to the IP address (optionally considering SRV records). + pub hostname: String, + /// The post that should be used to ping the Minecraft server (ignored if SRV exists). + pub port: u16, +} + +impl Display for ProbeInfo { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match &self.module { + Some(module) => write!(f, "{} ({})", self.target, module), + _ => write!(f, "{}", self.target), + } + } +} + +impl ProbeAddress { + /// Converts this hostname and port to the resolved [socket address][SocketAddr]. + /// + /// The [hostname][ProbeAddress::hostname] of this address is resolved through DNS and is used as the IP address + /// of the resulting socket address. To do this, we also check the SRV record for minecraft (`_minecraft._tcp`) and + /// prefer to use this information. If any SRV record was found, the second return of this function will be true. + pub async fn to_socket_addrs( + &self, + resolver: &TokioAsyncResolver, + ) -> Result { + // assemble the SRV record name + let srv_name = format!("_minecraft._tcp.{}", self.hostname); + debug!("trying to resolve SRV record: '{}'", srv_name); + let srv_response = resolver.lookup(&srv_name, SRV).await; + + // check if any SRV record was present (use this data then) + let (hostname, port, srv_used) = match srv_response { + Ok(response) => { + if let Some(record) = response.iter().filter_map(|r| r.as_srv()).next() { + let target = record.target().to_utf8(); + let target_port = record.port(); + debug!( + "found an SRV record for '{}': {}:{}", + srv_name, target, target_port + ); + (target, target_port, true) + } else { + debug!( + "found an SRV record for '{}', but it was of an invalid type", + srv_name + ); + (self.hostname.clone(), self.port, false) + } + } + _ => { + debug!("found no SRV record for '{}'", srv_name); + (self.hostname.clone(), self.port, false) + } + }; + + // resolve the underlying ips for the hostname + let ip_response = resolver.lookup_ip(&hostname).await?; + for ip in ip_response { + debug!("resolved ip address {} for hostname {}", ip, &hostname); + if ip.is_ipv4() { + return match srv_used { + true => Ok(ResolutionResult::Srv(SocketAddr::new(ip, port))), + false => Ok(ResolutionResult::Plain(SocketAddr::new(ip, port))), + }; + } + } + + // no IPv4 could be found, return an error + Err(ProbeError::CouldNotResolveIp(hostname)) + } +} + +impl Display for ProbeAddress { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.hostname, self.port) + } +} + +/// The visitor for the deserialization of [ProbeAddress]. +/// +/// This visitor is responsible for the deserialization and validation of a [ProbeAddress] and returns the appropriate +/// expectations of the format. The address is expected in the format of `hostname:port` and the port is optional, +/// falling back to the default port. +struct ProbeAddressVisitor; + +impl<'de> Visitor<'de> for ProbeAddressVisitor { + type Value = ProbeAddress; + + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str("a string in the form 'hostname' or 'hostname:port'") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + // split the supplied hostname and port into their own parts + let mut parts = value.splitn(2, ':'); + + // get the hostname and port part in their raw form + let hostname = parts + .next() + .expect("splitting a string should result in at least one element"); + let port_str = parts.next().unwrap_or("25565"); + + // parse the port into the expected form + let port: u16 = port_str.parse().map_err(|_| { + de::Error::invalid_value(Unexpected::Str(port_str), &"a valid port number") + })?; + + // check if the hostname is present + if hostname.is_empty() { + Err(de::Error::invalid_value( + Unexpected::Str(hostname), + &"a non-empty hostname", + ))? + } + + // check if the port is valid + if port == 0 { + Err(de::Error::invalid_value( + Unexpected::Unsigned(port.into()), + &"a positive port number", + ))? + } + + // wrap the parsed parts into our ProbeAddress + Ok(ProbeAddress { + hostname: hostname.to_string(), + port, + }) + } +} + +impl<'de> Deserialize<'de> for ProbeAddress { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(ProbeAddressVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_test::{assert_de_tokens, assert_de_tokens_error, Token}; + + #[test] + fn deserialize_without_port() { + let address = ProbeAddress { + hostname: "mc.justchunks.net".to_string(), + port: 25565, + }; + + assert_de_tokens(&address, &[Token::Str("mc.justchunks.net")]) + } + + #[test] + fn deserialize_without_port_ip() { + let address = ProbeAddress { + hostname: "123.123.123.123".to_string(), + port: 25565, + }; + + assert_de_tokens(&address, &[Token::Str("123.123.123.123")]) + } + + #[test] + fn deserialize_with_port() { + let address = ProbeAddress { + hostname: "mc.justchunks.net".to_string(), + port: 25566, + }; + + assert_de_tokens(&address, &[Token::Str("mc.justchunks.net:25566")]) + } + + #[test] + fn deserialize_with_port_ip() { + let address = ProbeAddress { + hostname: "123.123.123.123".to_string(), + port: 25566, + }; + + assert_de_tokens(&address, &[Token::Str("123.123.123.123:25566")]) + } + + #[test] + fn fail_deserialize_with_extra_colons() { + assert_de_tokens_error::( + &[Token::Str("mc.justchunks.net:test:25566")], + "invalid value: string \"test:25566\", expected a valid port number", + ) + } + + #[test] + fn fail_deserialize_with_port_negative() { + assert_de_tokens_error::( + &[Token::Str("mc.justchunks.net:-5")], + "invalid value: string \"-5\", expected a valid port number", + ) + } + + #[test] + fn fail_deserialize_with_port_too_low() { + assert_de_tokens_error::( + &[Token::Str("mc.justchunks.net:0")], + "invalid value: integer `0`, expected a positive port number", + ) + } + + #[test] + fn fail_deserialize_with_port_too_high() { + assert_de_tokens_error::( + &[Token::Str("mc.justchunks.net:100000")], + "invalid value: string \"100000\", expected a valid port number", + ) + } + + #[test] + fn fail_deserialize_with_port_non_numeric() { + assert_de_tokens_error::( + &[Token::Str("mc.justchunks.net:text")], + "invalid value: string \"text\", expected a valid port number", + ) + } + + #[test] + fn fail_deserialize_with_empty() { + assert_de_tokens_error::( + &[Token::Str("")], + "invalid value: string \"\", expected a non-empty hostname", + ) + } + + #[test] + fn fail_deserialize_with_empty_hostname() { + assert_de_tokens_error::( + &[Token::Str(":25566")], + "invalid value: string \"\", expected a non-empty hostname", + ) + } + + #[tokio::test] + async fn resolve_real_address_with_srv() { + let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap(); + let probe_address = ProbeAddress { + hostname: "justchunks.net".to_string(), + port: 1337, + }; + let resolution_result = probe_address.to_socket_addrs(&resolver).await.unwrap(); + let expected_address = SocketAddr::from(([142, 132, 245, 251], 25565)); + + assert_eq!(resolution_result, ResolutionResult::Srv(expected_address)); + } + + #[tokio::test] + async fn resolve_real_address_without_srv() { + let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap(); + let probe_address = ProbeAddress { + hostname: "mc.justchunks.net".to_string(), + port: 25566, + }; + let resolution_result = probe_address.to_socket_addrs(&resolver).await.unwrap(); + let expected_address = SocketAddr::from(([142, 132, 245, 251], 25566)); + + assert_eq!(resolution_result, ResolutionResult::Plain(expected_address)); + } + + #[tokio::test] + async fn resolve_real_ip_address() { + let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap(); + let probe_address = ProbeAddress { + hostname: "142.132.245.251".to_string(), + port: 25566, + }; + let resolution_result = probe_address.to_socket_addrs(&resolver).await.unwrap(); + let expected_address = SocketAddr::from(([142, 132, 245, 251], 25566)); + + assert_eq!(resolution_result, ResolutionResult::Plain(expected_address)); + } + + #[tokio::test] + #[should_panic] + async fn fail_resolve_illegal_address() { + let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap(); + let probe_address = ProbeAddress { + hostname: "illegal_address".to_string(), + port: 25566, + }; + probe_address.to_socket_addrs(&resolver).await.unwrap(); + } + + #[tokio::test] + #[should_panic] + async fn fail_resolve_illegal_ip_address() { + let resolver = TokioAsyncResolver::tokio_from_system_conf().unwrap(); + let probe_address = ProbeAddress { + hostname: "500.132.245.251".to_string(), + port: 25566, + }; + probe_address.to_socket_addrs(&resolver).await.unwrap(); + } +} diff --git a/src/protocol.rs b/src/protocol.rs new file mode 100644 index 0000000..480ca7e --- /dev/null +++ b/src/protocol.rs @@ -0,0 +1,449 @@ +//! This module defines and handles the Minecraft protocol and communication. +//! +//! This is necessary to exchange data with the target servers that should be probed. We only care about the packets +//! related to the [ServerListPing](https://wiki.vg/Server_List_Ping) and therefore only implement that part of the +//! Minecraft protocol. The implementations may differ from the official Minecraft client implementation if the +//! observed outcome is the same and the result is reliable. + +use std::io::Cursor; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use thiserror::Error; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tokio::net::TcpStream; +use tokio::time::Instant; + +/// ProtocolError is the internal error type for all errors related to the protocol communication +/// +/// This includes errors with the expected packets, packet contents or encoding of the exchanged fields. Errors of the +/// underlying data layer (for Byte exchange) are wrapped from the underlying IO errors. Additionally, the internal +/// timeout limits also are covered as errors. +#[derive(Error, Debug)] +pub enum ProtocolError { + /// An error occurred while reading or writing to the underlying byte stream. + #[error("error reading or writing data")] + Io(#[from] std::io::Error), + /// The received packet is of an invalid length that we cannot process. + #[error("illegal packet length")] + IllegalPacketLength, + /// The received VarInt cannot be correctly decoded (was formed incorrectly). + #[error("invalid VarInt data")] + InvalidVarInt, + /// The received packet ID is not mapped to an expected packet. + #[error("illegal packet ID: {actual} (expected {expected})")] + IllegalPacketId { expected: usize, actual: usize }, + /// The JSON response of the status packet is incorrectly encoded (not UTF-8). + #[error("invalid ServerListPing response body (invalid encoding)")] + InvalidEncoding, + /// An error occurred with the payload of a ping. + #[error("mismatched payload value: {actual} (expected {expected})")] + PayloadMismatch { expected: u64, actual: u64 }, +} + +/// State is the desired state that the connection should be in after the initial handshake. +#[derive(Clone, Copy)] +pub enum State { + Status, + Login, +} + +impl From for usize { + fn from(state: State) -> Self { + match state { + State::Status => 1, + State::Login => 2, + } + } +} + +/// OutboundPacket are packets that are written and therefore have a fixed, specific packet ID. +pub trait OutboundPacket { + /// Returns the specified + fn get_packet_id(&self) -> usize; + + async fn to_buffer(&self) -> Result, ProtocolError>; +} + +/// InboundPacket are packets that are read and therefore are expected to be of a specific packet ID. +pub trait InboundPacket: Sized { + fn get_packet_id() -> usize; + + async fn new_from_buffer(buffer: Vec) -> Result; +} + +/// This packet initiates the status request attempt and tells the server the details of the client. +/// +/// The data in this packet can differ from the actual data that was used but will be considered by the server when +/// assembling the response. Therefore, this data should mirror what a normal client would send. +pub struct HandshakePacket { + pub packet_id: usize, + pub protocol_version: isize, + pub server_address: String, + pub server_port: u16, + pub next_state: State, +} + +impl HandshakePacket { + pub fn new(protocol_version: isize, server_address: String, server_port: u16) -> Self { + Self { + packet_id: 0, + protocol_version, + server_address, + server_port, + next_state: State::Status, + } + } +} + +impl OutboundPacket for HandshakePacket { + fn get_packet_id(&self) -> usize { + self.packet_id + } + + async fn to_buffer(&self) -> Result, ProtocolError> { + let mut buffer = Cursor::new(Vec::::new()); + + buffer.write_varint(self.protocol_version as usize).await?; + buffer.write_string(&self.server_address).await?; + buffer.write_u16(self.server_port).await?; + buffer.write_varint(self.next_state.into()).await?; + + Ok(buffer.into_inner()) + } +} + +/// This packet will be sent after the [HandshakePacket] and requests the server metadata. +/// +/// The packet can only be sent after the [HandshakePacket] and must be written before any status information can be +/// read, as this is the differentiator between the status and the ping sequence. +pub struct RequestPacket { + pub packet_id: usize, +} + +impl RequestPacket { + pub fn new() -> Self { + Self { packet_id: 0 } + } +} + +impl OutboundPacket for RequestPacket { + fn get_packet_id(&self) -> usize { + 0 + } + + async fn to_buffer(&self) -> Result, ProtocolError> { + Ok(Vec::new()) + } +} + +/// This is the response for a specific [StatusRequestPacket] that contains all self-reported metadata. +/// +/// This packet can be received only after a [StatusRequestPacket] and will not close the connection, allowing for a +/// ping sequence to be exchanged afterward. +pub struct ResponsePacket { + pub packet_id: usize, + pub body: String, +} + +impl InboundPacket for ResponsePacket { + fn get_packet_id() -> usize { + 0 + } + + async fn new_from_buffer(buffer: Vec) -> Result { + let mut reader = Cursor::new(buffer); + + let body = reader.read_string().await?; + + Ok(ResponsePacket { packet_id: 0, body }) + } +} + +/// This is the request for a specific [PongPacket] that can be used to measure the server ping. +/// +/// This packet can be sent after a connection was established or the [StatusResponsePacket] was received. Initiating +/// the ping sequence will consume the connection after the [PongPacket] was received. +pub struct PingPacket { + pub packet_id: usize, + pub payload: u64, +} + +impl PingPacket { + pub fn new(payload: u64) -> Self { + Self { + packet_id: 1, + payload, + } + } +} + +impl OutboundPacket for PingPacket { + fn get_packet_id(&self) -> usize { + self.packet_id + } + + async fn to_buffer(&self) -> Result, ProtocolError> { + let mut buffer = Cursor::new(Vec::::new()); + + buffer.write_u64(self.payload).await?; + + Ok(buffer.into_inner()) + } +} + +/// This is the response to a specific [PingPacket] that can be used to measure the server ping. +/// +/// This packet can be received after a corresponding [PingPacket] and will have the same payload as the request. This +/// also consumes the connection, ending the Server List Ping sequence. +pub struct PongPacket { + pub packet_id: usize, + pub payload: u64, +} + +impl InboundPacket for PongPacket { + fn get_packet_id() -> usize { + 1 + } + + async fn new_from_buffer(buffer: Vec) -> Result { + let mut reader = Cursor::new(buffer); + + let payload = reader.read_u64().await?; + + Ok(PongPacket { + packet_id: 0, + payload, + }) + } +} + +/// AsyncReadPacket allows reading a specific [InboundPacket] from an [AsyncWrite]. +/// +/// Only [InboundPackets][InboundPacket] can be read as only those packets are received. There are additional +/// methods to read the data that is encoded in a Minecraft-specific manner. Their implementation is analogous to the +/// [write implementation][AsyncWritePacket]. +pub trait AsyncReadPacket { + /// Reads the supplied [InboundPacket] type from this object as described in the official + /// [protocol documentation](https://wiki.vg/Protocol#Packet_format). + async fn read_packet(&mut self) -> Result; + + /// Reads a VarInt from this object as described in the official + /// [protocol documentation](https://wiki.vg/Protocol#VarInt_and_VarLong). + async fn read_varint(&mut self) -> Result; + + /// Reads a String from this object as described in the official + /// [protocol documentation](https://wiki.vg/Protocol#Type:String). + async fn read_string(&mut self) -> Result; +} + +impl AsyncReadPacket for R { + async fn read_packet(&mut self) -> Result { + // extract the length of the packet and check for any following content + let length = self.read_varint().await?; + if length == 0 { + return Err(ProtocolError::IllegalPacketLength); + } + + // extract the encoded packet id and validate if it is expected + let packet_id = self.read_varint().await?; + let expected_packet_id = T::get_packet_id(); + if packet_id != expected_packet_id { + return Err(ProtocolError::IllegalPacketId { + expected: expected_packet_id, + actual: packet_id, + }); + } + + // read the remaining content of the packet into a new buffer + let mut buffer = vec![0; length - 1]; + self.read_exact(&mut buffer).await?; + + // convert the received buffer into our expected packet + T::new_from_buffer(buffer).await + } + + async fn read_varint(&mut self) -> Result { + let mut read = 0; + let mut result = 0; + loop { + let read_value = self.read_u8().await?; + let value = read_value & 0b0111_1111; + result |= (value as usize) << (7 * read); + read += 1; + if read > 5 { + return Err(ProtocolError::InvalidVarInt); + } + if (read_value & 0b1000_0000) == 0 { + return Ok(result); + } + } + } + + async fn read_string(&mut self) -> Result { + let length = self.read_varint().await?; + + let mut buffer = vec![0; length]; + self.read_exact(&mut buffer).await?; + + String::from_utf8(buffer).map_err(|_| ProtocolError::InvalidEncoding) + } +} + +/// AsyncWritePacket allows writing a specific [OutboundPacket] to an [AsyncWrite]. +/// +/// Only [OutboundPackets][OutboundPacket] can be written as only those packets are sent. There are additional +/// methods to write the data that is encoded in a Minecraft-specific manner. Their implementation is analogous to the +/// [read implementation][AsyncReadPacket]. +pub trait AsyncWritePacket { + /// Writes the supplied [OutboundPacket] onto this object as described in the official + /// [protocol documentation](https://wiki.vg/Protocol#Packet_format). + async fn write_packet( + &mut self, + packet: T, + ) -> Result<(), ProtocolError>; + + /// Writes a VarInt onto this object as described in the official + /// [protocol documentation](https://wiki.vg/Protocol#VarInt_and_VarLong). + async fn write_varint(&mut self, int: usize) -> Result<(), ProtocolError>; + + /// Writes a String onto this object as described in the official + /// [protocol documentation](https://wiki.vg/Protocol#Type:String). + async fn write_string(&mut self, string: &str) -> Result<(), ProtocolError>; +} + +impl AsyncWritePacket for W { + async fn write_packet( + &mut self, + packet: T, + ) -> Result<(), ProtocolError> { + // write the packet into a buffer and box it as a slice (sized) + let packet_buffer = packet.to_buffer().await?; + let raw_packet = packet_buffer.into_boxed_slice(); + + // create a new buffer and write the packet onto it (to get the size) + let mut buffer: Cursor> = Cursor::new(Vec::new()); + buffer.write_varint(packet.get_packet_id()).await?; + buffer.write_all(&raw_packet).await?; + + // write the length of the content (length frame encoder) and then the packet + let inner = buffer.into_inner(); + self.write_varint(inner.len()).await?; + self.write_all(&inner).await?; + + Ok(()) + } + + async fn write_varint(&mut self, int: usize) -> Result<(), ProtocolError> { + let mut int = (int as u64) & 0xFFFF_FFFF; + let mut written = 0; + let mut buffer = [0; 5]; + loop { + let temp = (int & 0b0111_1111) as u8; + int >>= 7; + if int != 0 { + buffer[written] = temp | 0b1000_0000; + } else { + buffer[written] = temp; + } + written += 1; + if int == 0 { + break; + } + } + self.write_all(&buffer[0..written]).await?; + + Ok(()) + } + + async fn write_string(&mut self, string: &str) -> Result<(), ProtocolError> { + self.write_varint(string.len()).await?; + self.write_all(string.as_bytes()).await?; + + Ok(()) + } +} + +/// The necessary information that will be reported to the server on a handshake of the Server List Ping protocol. +pub struct HandshakeInfo { + /// The intended protocol version. + pub protocol_version: isize, + /// The hostname to connect. + pub hostname: String, + /// The server port to connect. + pub server_port: u16, +} + +impl HandshakeInfo { + pub fn new(protocol_version: isize, server_address: String, server_port: u16) -> Self { + Self { + protocol_version, + hostname: server_address, + server_port, + } + } +} + +/// Performs the status protocol exchange and returns the self-reported server status. +/// +/// This sends the [Handshake][HandshakePacket] and the [StatusRequest][StatusRequestPacket] packet and awaits the +/// [StatusResponse][StatusResponsePacket] from the server. This response is in JSON and will not be interpreted by this +/// function. The connection is not consumed by this operation, and the protocol allows for pings to be exchanged after +/// the status has been returned. +pub async fn retrieve_status( + stream: &mut TcpStream, + info: &HandshakeInfo, +) -> Result { + // create a new handshake packet and send it + let handshake = HandshakePacket::new( + info.protocol_version, + info.hostname.clone(), + info.server_port, + ); + stream.write_packet(handshake).await?; + + // create a new status request packet and send it + let request = RequestPacket::new(); + stream.write_packet(request).await?; + + // await the response for the status request and read it + let response: ResponsePacket = stream.read_packet().await?; + + Ok(response.body) +} + +/// Performs the ping protocol exchange and records the duration it took. +/// +/// This sends the [Ping][PingPacket] and awaits the response of the [Pong][PongPacket], while recording the time it +/// takes to get a response. From this recorded RTT (Round-Trip-Time) the latency is calculated by dividing this value +/// by two. This is the most accurate way to measure the ping we can use. +pub async fn execute_ping(stream: &mut TcpStream) -> Result { + // create a new value for the payload (to distinguish it) + let payload = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time since epoch could not be retrieved") + .as_secs(); + + // record the current time to get the round trip time + let start = Instant::now(); + + // create and send a new ping packet + let ping = PingPacket::new(payload); + stream.write_packet(ping).await?; + + // await the retrieval of the corresponding pong packet + let pong: PongPacket = stream.read_packet().await?; + + // take the time for the response and divide it to get the latency + let mut duration = start.elapsed(); + duration = duration.div_f32(2.0); + + // if the pong packet did not match, something unexpected happened with the server + if pong.payload != payload { + return Err(ProtocolError::PayloadMismatch { + expected: payload, + actual: pong.payload, + }); + } + + Ok(duration) +}