├── .cargo └── config.toml ├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── docker.yml │ ├── publish.yml │ └── release.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── demo.tape ├── docs └── images │ └── demo.gif ├── scripts └── release-version.sh └── src ├── main.rs ├── parse_arg.rs ├── port_descriptions ├── mod.rs └── tcp.txt ├── scanner ├── host_info.rs └── mod.rs └── terminal ├── color.rs ├── mod.rs └── spinner.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-Ctarget-cpu=native"] 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 4 10 | max_line_length = off 11 | 12 | [*.rs] 13 | indent_size = 4 14 | max_line_length = 100 15 | 16 | [*.yml, *.yaml] 17 | indent_size = 2 18 | indent_style = space 19 | 20 | [*.toml] 21 | indent_size = 4 22 | 23 | [*.sh] 24 | indent_size = 2 25 | indent_style = space 26 | 27 | [*.md] 28 | trim_trailing_whitespace = false 29 | max_line_length = off 30 | indent_size = 2 31 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | env: 8 | DOCKER_BASE_NAME: ghcr.io/${{ github.repository }} 9 | DOCKER_HUB_BASE_NAME: ${{ github.repository }} 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - name: Unshallow 18 | run: git fetch --prune --unshallow 19 | push: 20 | runs-on: ubuntu-22.04 21 | needs: lint 22 | strategy: 23 | matrix: 24 | baseimage: 25 | - "alpine:3.17" 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - name: Set env 30 | run: | 31 | if [ "${{ github.event_name }}" = 'release' ]; then 32 | export TAG_NAME="${{ github.event.release.tag_name }}" 33 | else 34 | export TAG_NAME="latest" 35 | fi 36 | echo "PKG_TAG=${DOCKER_BASE_NAME}:${TAG_NAME}" >> $GITHUB_ENV 37 | echo "HUB_TAG=${DOCKER_HUB_BASE_NAME}:${TAG_NAME}" >> $GITHUB_ENV 38 | echo "LATEST_PKG_TAG=${DOCKER_BASE_NAME}:latest" >> $GITHUB_ENV 39 | echo "LATEST_HUB_TAG=${DOCKER_HUB_BASE_NAME}:latest" >> $GITHUB_ENV 40 | - name: Build ${{ matrix.baseimage }} base image 41 | run: | 42 | docker build . -t "${PKG_TAG}" --build-arg BASE_IMAGE="${{ matrix.baseimage }}" 43 | docker tag "${PKG_TAG}" "${HUB_TAG}" 44 | docker tag "${PKG_TAG}" "${LATEST_PKG_TAG}" 45 | docker tag "${PKG_TAG}" "${LATEST_HUB_TAG}" 46 | - name: Login to Registries 47 | if: github.event_name != 'pull_request' 48 | env: 49 | PERSONAL_GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }} 50 | DCKR_PAT: ${{ secrets.DCKR_PAT }} 51 | run: | 52 | echo "${PERSONAL_GITHUB_TOKEN}" | docker login ghcr.io -u trinhminhtriet --password-stdin 53 | echo "${DCKR_PAT}" | docker login -u trinhminhtriet --password-stdin 54 | - name: Push to GitHub Packages 55 | if: github.event_name != 'pull_request' 56 | run: | 57 | docker push "${PKG_TAG}" 58 | docker push "${LATEST_PKG_TAG}" 59 | - name: Push to Docker Hub 60 | if: github.event_name != 'pull_request' 61 | run: | 62 | docker push "${HUB_TAG}" 63 | docker push "${LATEST_HUB_TAG}" 64 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to crates.io 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | publish: 13 | name: Publish to crates.io 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Rust toolchain 21 | uses: actions-rs/toolchain@v1 22 | with: 23 | profile: minimal 24 | toolchain: stable 25 | override: true 26 | 27 | - name: Publish scanr to crates.io 28 | run: cargo publish --manifest-path Cargo.toml --token ${{ secrets.CARGO_REGISTRY_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | release: 13 | name: "Release" 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | include: 18 | - os: ubuntu-latest 19 | artifact_name: scanr 20 | asset_name: scanr-linux-gnu-amd64 21 | - os: windows-latest 22 | artifact_name: scanr.exe 23 | asset_name: scanr-windows-amd64.exe 24 | - os: macos-latest 25 | artifact_name: scanr 26 | asset_name: scanr-darwin-amd64 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | - name: Set up Rust toolchain 31 | uses: actions-rs/toolchain@v1 32 | with: 33 | profile: minimal 34 | toolchain: stable 35 | override: true 36 | - name: Build release 37 | run: cargo build --release --locked 38 | - name: Set prerelease flag (non-Windows) 39 | if: runner.os != 'Windows' 40 | run: | 41 | if [ $(echo ${{ github.ref }} | grep "rc") ]; then 42 | echo "PRERELEASE=true" >> $GITHUB_ENV 43 | echo "PRERELEASE=true" 44 | else 45 | echo "PRERELEASE=false" >> $GITHUB_ENV 46 | echo "PRERELEASE=false" 47 | fi 48 | echo $PRERELEASE 49 | VERSION=$(echo ${{ github.ref }} | sed 's/refs\/tags\///g') 50 | echo "VERSION=$VERSION" >> $GITHUB_ENV 51 | echo "VERSION=$VERSION" 52 | - name: Set prerelease flag (Windows) 53 | if: runner.os == 'Windows' 54 | shell: powershell 55 | run: | 56 | $full = "${{ github.ref }}" 57 | 58 | if ( $full -like '*rc*' ) { 59 | echo "PRERELEASE=true" >> $env:GITHUB_ENV 60 | echo "PRERELEASE=true" 61 | } else { 62 | echo "PRERELEASE=false" >> $env:GITHUB_ENV 63 | echo "PRERELEASE=false" 64 | } 65 | 66 | $trimmed = $full -replace 'refs/tags/','' 67 | echo "VERSION=$trimmed" >> $env:GITHUB_ENV 68 | echo "VERSION=$trimmed" 69 | - name: Upload release assets 70 | uses: svenstaro/upload-release-action@v2 71 | with: 72 | repo_token: ${{ secrets.PERSONAL_GITHUB_TOKEN }} 73 | file: target/release/${{ matrix.artifact_name }} 74 | asset_name: ${{ matrix.asset_name }} 75 | tag: ${{ github.ref }} 76 | prerelease: ${{ env.PRERELEASE }} 77 | release_name: "scanr ${{ env.VERSION }}" 78 | body: "Please refer to **[CHANGELOG.md](https://github.com/trinhminhtriet/scanr/blob/master/CHANGELOG.md)** for information on this release." 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | target 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2024-10-25 2 | 3 | - Initialize 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.15" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.8" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.5" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.1" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 55 | dependencies = [ 56 | "windows-sys", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.4" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 64 | dependencies = [ 65 | "anstyle", 66 | "windows-sys", 67 | ] 68 | 69 | [[package]] 70 | name = "autocfg" 71 | version = "1.4.0" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 74 | 75 | [[package]] 76 | name = "backtrace" 77 | version = "0.3.74" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 80 | dependencies = [ 81 | "addr2line", 82 | "cfg-if", 83 | "libc", 84 | "miniz_oxide", 85 | "object", 86 | "rustc-demangle", 87 | "windows-targets", 88 | ] 89 | 90 | [[package]] 91 | name = "base64" 92 | version = "0.21.7" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 95 | 96 | [[package]] 97 | name = "bitflags" 98 | version = "2.6.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 101 | 102 | [[package]] 103 | name = "block-buffer" 104 | version = "0.10.4" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 107 | dependencies = [ 108 | "generic-array", 109 | ] 110 | 111 | [[package]] 112 | name = "byteorder" 113 | version = "1.5.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 116 | 117 | [[package]] 118 | name = "bytes" 119 | version = "1.7.2" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" 122 | 123 | [[package]] 124 | name = "cfg-if" 125 | version = "1.0.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 128 | 129 | [[package]] 130 | name = "clap" 131 | version = "4.5.39" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" 134 | dependencies = [ 135 | "clap_builder", 136 | "clap_derive", 137 | ] 138 | 139 | [[package]] 140 | name = "clap_builder" 141 | version = "4.5.39" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" 144 | dependencies = [ 145 | "anstream", 146 | "anstyle", 147 | "clap_lex", 148 | "strsim", 149 | "unicase", 150 | "unicode-width", 151 | ] 152 | 153 | [[package]] 154 | name = "clap_derive" 155 | version = "4.5.32" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 158 | dependencies = [ 159 | "heck", 160 | "proc-macro2", 161 | "quote", 162 | "syn", 163 | ] 164 | 165 | [[package]] 166 | name = "clap_lex" 167 | version = "0.7.4" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 170 | 171 | [[package]] 172 | name = "colorchoice" 173 | version = "1.0.2" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 176 | 177 | [[package]] 178 | name = "cpufeatures" 179 | version = "0.2.14" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" 182 | dependencies = [ 183 | "libc", 184 | ] 185 | 186 | [[package]] 187 | name = "crypto-common" 188 | version = "0.1.6" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 191 | dependencies = [ 192 | "generic-array", 193 | "typenum", 194 | ] 195 | 196 | [[package]] 197 | name = "data-encoding" 198 | version = "2.6.0" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" 201 | 202 | [[package]] 203 | name = "digest" 204 | version = "0.10.7" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 207 | dependencies = [ 208 | "block-buffer", 209 | "crypto-common", 210 | ] 211 | 212 | [[package]] 213 | name = "encoding_rs" 214 | version = "0.8.34" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" 217 | dependencies = [ 218 | "cfg-if", 219 | ] 220 | 221 | [[package]] 222 | name = "equivalent" 223 | version = "1.0.1" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 226 | 227 | [[package]] 228 | name = "fnv" 229 | version = "1.0.7" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 232 | 233 | [[package]] 234 | name = "form_urlencoded" 235 | version = "1.2.1" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 238 | dependencies = [ 239 | "percent-encoding", 240 | ] 241 | 242 | [[package]] 243 | name = "futures-channel" 244 | version = "0.3.31" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 247 | dependencies = [ 248 | "futures-core", 249 | "futures-sink", 250 | ] 251 | 252 | [[package]] 253 | name = "futures-core" 254 | version = "0.3.31" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 257 | 258 | [[package]] 259 | name = "futures-sink" 260 | version = "0.3.31" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 263 | 264 | [[package]] 265 | name = "futures-task" 266 | version = "0.3.31" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 269 | 270 | [[package]] 271 | name = "futures-util" 272 | version = "0.3.31" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 275 | dependencies = [ 276 | "futures-core", 277 | "futures-sink", 278 | "futures-task", 279 | "pin-project-lite", 280 | "pin-utils", 281 | "slab", 282 | ] 283 | 284 | [[package]] 285 | name = "generic-array" 286 | version = "0.14.7" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 289 | dependencies = [ 290 | "typenum", 291 | "version_check", 292 | ] 293 | 294 | [[package]] 295 | name = "getrandom" 296 | version = "0.2.15" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 299 | dependencies = [ 300 | "cfg-if", 301 | "libc", 302 | "wasi", 303 | ] 304 | 305 | [[package]] 306 | name = "gimli" 307 | version = "0.31.1" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 310 | 311 | [[package]] 312 | name = "h2" 313 | version = "0.3.26" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" 316 | dependencies = [ 317 | "bytes", 318 | "fnv", 319 | "futures-core", 320 | "futures-sink", 321 | "futures-util", 322 | "http 0.2.12", 323 | "indexmap", 324 | "slab", 325 | "tokio", 326 | "tokio-util", 327 | "tracing", 328 | ] 329 | 330 | [[package]] 331 | name = "hashbrown" 332 | version = "0.15.0" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" 335 | 336 | [[package]] 337 | name = "headers" 338 | version = "0.3.9" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" 341 | dependencies = [ 342 | "base64", 343 | "bytes", 344 | "headers-core", 345 | "http 0.2.12", 346 | "httpdate", 347 | "mime", 348 | "sha1", 349 | ] 350 | 351 | [[package]] 352 | name = "headers-core" 353 | version = "0.2.0" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" 356 | dependencies = [ 357 | "http 0.2.12", 358 | ] 359 | 360 | [[package]] 361 | name = "heck" 362 | version = "0.5.0" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 365 | 366 | [[package]] 367 | name = "hermit-abi" 368 | version = "0.3.9" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 371 | 372 | [[package]] 373 | name = "http" 374 | version = "0.2.12" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 377 | dependencies = [ 378 | "bytes", 379 | "fnv", 380 | "itoa", 381 | ] 382 | 383 | [[package]] 384 | name = "http" 385 | version = "1.1.0" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 388 | dependencies = [ 389 | "bytes", 390 | "fnv", 391 | "itoa", 392 | ] 393 | 394 | [[package]] 395 | name = "http-body" 396 | version = "0.4.6" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" 399 | dependencies = [ 400 | "bytes", 401 | "http 0.2.12", 402 | "pin-project-lite", 403 | ] 404 | 405 | [[package]] 406 | name = "httparse" 407 | version = "1.9.5" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" 410 | 411 | [[package]] 412 | name = "httpdate" 413 | version = "1.0.3" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 416 | 417 | [[package]] 418 | name = "hyper" 419 | version = "0.14.31" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" 422 | dependencies = [ 423 | "bytes", 424 | "futures-channel", 425 | "futures-core", 426 | "futures-util", 427 | "h2", 428 | "http 0.2.12", 429 | "http-body", 430 | "httparse", 431 | "httpdate", 432 | "itoa", 433 | "pin-project-lite", 434 | "socket2", 435 | "tokio", 436 | "tower-service", 437 | "tracing", 438 | "want", 439 | ] 440 | 441 | [[package]] 442 | name = "idna" 443 | version = "0.5.0" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 446 | dependencies = [ 447 | "unicode-bidi", 448 | "unicode-normalization", 449 | ] 450 | 451 | [[package]] 452 | name = "indexmap" 453 | version = "2.6.0" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" 456 | dependencies = [ 457 | "equivalent", 458 | "hashbrown", 459 | ] 460 | 461 | [[package]] 462 | name = "is_terminal_polyfill" 463 | version = "1.70.1" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 466 | 467 | [[package]] 468 | name = "itoa" 469 | version = "1.0.11" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 472 | 473 | [[package]] 474 | name = "libc" 475 | version = "0.2.172" 476 | source = "registry+https://github.com/rust-lang/crates.io-index" 477 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 478 | 479 | [[package]] 480 | name = "lock_api" 481 | version = "0.4.12" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 484 | dependencies = [ 485 | "autocfg", 486 | "scopeguard", 487 | ] 488 | 489 | [[package]] 490 | name = "log" 491 | version = "0.4.22" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 494 | 495 | [[package]] 496 | name = "memchr" 497 | version = "2.7.4" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 500 | 501 | [[package]] 502 | name = "mime" 503 | version = "0.3.17" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 506 | 507 | [[package]] 508 | name = "mime_guess" 509 | version = "2.0.5" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 512 | dependencies = [ 513 | "mime", 514 | "unicase", 515 | ] 516 | 517 | [[package]] 518 | name = "miniz_oxide" 519 | version = "0.8.0" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 522 | dependencies = [ 523 | "adler2", 524 | ] 525 | 526 | [[package]] 527 | name = "mio" 528 | version = "1.0.2" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 531 | dependencies = [ 532 | "hermit-abi", 533 | "libc", 534 | "wasi", 535 | "windows-sys", 536 | ] 537 | 538 | [[package]] 539 | name = "multer" 540 | version = "2.1.0" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" 543 | dependencies = [ 544 | "bytes", 545 | "encoding_rs", 546 | "futures-util", 547 | "http 0.2.12", 548 | "httparse", 549 | "log", 550 | "memchr", 551 | "mime", 552 | "spin", 553 | "version_check", 554 | ] 555 | 556 | [[package]] 557 | name = "object" 558 | version = "0.36.5" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" 561 | dependencies = [ 562 | "memchr", 563 | ] 564 | 565 | [[package]] 566 | name = "once_cell" 567 | version = "1.20.2" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 570 | 571 | [[package]] 572 | name = "os_info" 573 | version = "3.11.0" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "41fc863e2ca13dc2d5c34fb22ea4a588248ac14db929616ba65c45f21744b1e9" 576 | dependencies = [ 577 | "log", 578 | "serde", 579 | "windows-sys", 580 | ] 581 | 582 | [[package]] 583 | name = "parking_lot" 584 | version = "0.12.3" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 587 | dependencies = [ 588 | "lock_api", 589 | "parking_lot_core", 590 | ] 591 | 592 | [[package]] 593 | name = "parking_lot_core" 594 | version = "0.9.10" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 597 | dependencies = [ 598 | "cfg-if", 599 | "libc", 600 | "redox_syscall", 601 | "smallvec", 602 | "windows-targets", 603 | ] 604 | 605 | [[package]] 606 | name = "percent-encoding" 607 | version = "2.3.1" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 610 | 611 | [[package]] 612 | name = "pin-project" 613 | version = "1.1.6" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec" 616 | dependencies = [ 617 | "pin-project-internal", 618 | ] 619 | 620 | [[package]] 621 | name = "pin-project-internal" 622 | version = "1.1.6" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" 625 | dependencies = [ 626 | "proc-macro2", 627 | "quote", 628 | "syn", 629 | ] 630 | 631 | [[package]] 632 | name = "pin-project-lite" 633 | version = "0.2.14" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 636 | 637 | [[package]] 638 | name = "pin-utils" 639 | version = "0.1.0" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 642 | 643 | [[package]] 644 | name = "ppv-lite86" 645 | version = "0.2.20" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 648 | dependencies = [ 649 | "zerocopy", 650 | ] 651 | 652 | [[package]] 653 | name = "proc-macro2" 654 | version = "1.0.88" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" 657 | dependencies = [ 658 | "unicode-ident", 659 | ] 660 | 661 | [[package]] 662 | name = "quote" 663 | version = "1.0.37" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 666 | dependencies = [ 667 | "proc-macro2", 668 | ] 669 | 670 | [[package]] 671 | name = "rand" 672 | version = "0.8.5" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 675 | dependencies = [ 676 | "libc", 677 | "rand_chacha", 678 | "rand_core", 679 | ] 680 | 681 | [[package]] 682 | name = "rand_chacha" 683 | version = "0.3.1" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 686 | dependencies = [ 687 | "ppv-lite86", 688 | "rand_core", 689 | ] 690 | 691 | [[package]] 692 | name = "rand_core" 693 | version = "0.6.4" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 696 | dependencies = [ 697 | "getrandom", 698 | ] 699 | 700 | [[package]] 701 | name = "redox_syscall" 702 | version = "0.5.7" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 705 | dependencies = [ 706 | "bitflags", 707 | ] 708 | 709 | [[package]] 710 | name = "rustc-demangle" 711 | version = "0.1.24" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 714 | 715 | [[package]] 716 | name = "ryu" 717 | version = "1.0.18" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 720 | 721 | [[package]] 722 | name = "scanr" 723 | version = "0.1.15" 724 | dependencies = [ 725 | "clap", 726 | "os_info", 727 | "tokio", 728 | "warp", 729 | ] 730 | 731 | [[package]] 732 | name = "scoped-tls" 733 | version = "1.0.1" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 736 | 737 | [[package]] 738 | name = "scopeguard" 739 | version = "1.2.0" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 742 | 743 | [[package]] 744 | name = "serde" 745 | version = "1.0.210" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 748 | dependencies = [ 749 | "serde_derive", 750 | ] 751 | 752 | [[package]] 753 | name = "serde_derive" 754 | version = "1.0.210" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 757 | dependencies = [ 758 | "proc-macro2", 759 | "quote", 760 | "syn", 761 | ] 762 | 763 | [[package]] 764 | name = "serde_json" 765 | version = "1.0.132" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" 768 | dependencies = [ 769 | "itoa", 770 | "memchr", 771 | "ryu", 772 | "serde", 773 | ] 774 | 775 | [[package]] 776 | name = "serde_urlencoded" 777 | version = "0.7.1" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 780 | dependencies = [ 781 | "form_urlencoded", 782 | "itoa", 783 | "ryu", 784 | "serde", 785 | ] 786 | 787 | [[package]] 788 | name = "sha1" 789 | version = "0.10.6" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 792 | dependencies = [ 793 | "cfg-if", 794 | "cpufeatures", 795 | "digest", 796 | ] 797 | 798 | [[package]] 799 | name = "signal-hook-registry" 800 | version = "1.4.2" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 803 | dependencies = [ 804 | "libc", 805 | ] 806 | 807 | [[package]] 808 | name = "slab" 809 | version = "0.4.9" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 812 | dependencies = [ 813 | "autocfg", 814 | ] 815 | 816 | [[package]] 817 | name = "smallvec" 818 | version = "1.13.2" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 821 | 822 | [[package]] 823 | name = "socket2" 824 | version = "0.5.7" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 827 | dependencies = [ 828 | "libc", 829 | "windows-sys", 830 | ] 831 | 832 | [[package]] 833 | name = "spin" 834 | version = "0.9.8" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 837 | 838 | [[package]] 839 | name = "strsim" 840 | version = "0.11.1" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 843 | 844 | [[package]] 845 | name = "syn" 846 | version = "2.0.81" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "198514704ca887dd5a1e408c6c6cdcba43672f9b4062e1b24aa34e74e6d7faae" 849 | dependencies = [ 850 | "proc-macro2", 851 | "quote", 852 | "unicode-ident", 853 | ] 854 | 855 | [[package]] 856 | name = "thiserror" 857 | version = "1.0.64" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" 860 | dependencies = [ 861 | "thiserror-impl", 862 | ] 863 | 864 | [[package]] 865 | name = "thiserror-impl" 866 | version = "1.0.64" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" 869 | dependencies = [ 870 | "proc-macro2", 871 | "quote", 872 | "syn", 873 | ] 874 | 875 | [[package]] 876 | name = "tinyvec" 877 | version = "1.8.0" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 880 | dependencies = [ 881 | "tinyvec_macros", 882 | ] 883 | 884 | [[package]] 885 | name = "tinyvec_macros" 886 | version = "0.1.1" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 889 | 890 | [[package]] 891 | name = "tokio" 892 | version = "1.45.1" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" 895 | dependencies = [ 896 | "backtrace", 897 | "bytes", 898 | "libc", 899 | "mio", 900 | "parking_lot", 901 | "pin-project-lite", 902 | "signal-hook-registry", 903 | "socket2", 904 | "tokio-macros", 905 | "windows-sys", 906 | ] 907 | 908 | [[package]] 909 | name = "tokio-macros" 910 | version = "2.5.0" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 913 | dependencies = [ 914 | "proc-macro2", 915 | "quote", 916 | "syn", 917 | ] 918 | 919 | [[package]] 920 | name = "tokio-tungstenite" 921 | version = "0.21.0" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" 924 | dependencies = [ 925 | "futures-util", 926 | "log", 927 | "tokio", 928 | "tungstenite", 929 | ] 930 | 931 | [[package]] 932 | name = "tokio-util" 933 | version = "0.7.12" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" 936 | dependencies = [ 937 | "bytes", 938 | "futures-core", 939 | "futures-sink", 940 | "pin-project-lite", 941 | "tokio", 942 | ] 943 | 944 | [[package]] 945 | name = "tower-service" 946 | version = "0.3.3" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 949 | 950 | [[package]] 951 | name = "tracing" 952 | version = "0.1.40" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 955 | dependencies = [ 956 | "log", 957 | "pin-project-lite", 958 | "tracing-core", 959 | ] 960 | 961 | [[package]] 962 | name = "tracing-core" 963 | version = "0.1.32" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 966 | dependencies = [ 967 | "once_cell", 968 | ] 969 | 970 | [[package]] 971 | name = "try-lock" 972 | version = "0.2.5" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 975 | 976 | [[package]] 977 | name = "tungstenite" 978 | version = "0.21.0" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" 981 | dependencies = [ 982 | "byteorder", 983 | "bytes", 984 | "data-encoding", 985 | "http 1.1.0", 986 | "httparse", 987 | "log", 988 | "rand", 989 | "sha1", 990 | "thiserror", 991 | "url", 992 | "utf-8", 993 | ] 994 | 995 | [[package]] 996 | name = "typenum" 997 | version = "1.17.0" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1000 | 1001 | [[package]] 1002 | name = "unicase" 1003 | version = "2.8.0" 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" 1005 | checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" 1006 | 1007 | [[package]] 1008 | name = "unicode-bidi" 1009 | version = "0.3.17" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" 1012 | 1013 | [[package]] 1014 | name = "unicode-ident" 1015 | version = "1.0.13" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 1018 | 1019 | [[package]] 1020 | name = "unicode-normalization" 1021 | version = "0.1.24" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 1024 | dependencies = [ 1025 | "tinyvec", 1026 | ] 1027 | 1028 | [[package]] 1029 | name = "unicode-width" 1030 | version = "0.2.0" 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" 1032 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1033 | 1034 | [[package]] 1035 | name = "url" 1036 | version = "2.5.2" 1037 | source = "registry+https://github.com/rust-lang/crates.io-index" 1038 | checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" 1039 | dependencies = [ 1040 | "form_urlencoded", 1041 | "idna", 1042 | "percent-encoding", 1043 | ] 1044 | 1045 | [[package]] 1046 | name = "utf-8" 1047 | version = "0.7.6" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1050 | 1051 | [[package]] 1052 | name = "utf8parse" 1053 | version = "0.2.2" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1056 | 1057 | [[package]] 1058 | name = "version_check" 1059 | version = "0.9.5" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1062 | 1063 | [[package]] 1064 | name = "want" 1065 | version = "0.3.1" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1068 | dependencies = [ 1069 | "try-lock", 1070 | ] 1071 | 1072 | [[package]] 1073 | name = "warp" 1074 | version = "0.3.7" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" 1077 | dependencies = [ 1078 | "bytes", 1079 | "futures-channel", 1080 | "futures-util", 1081 | "headers", 1082 | "http 0.2.12", 1083 | "hyper", 1084 | "log", 1085 | "mime", 1086 | "mime_guess", 1087 | "multer", 1088 | "percent-encoding", 1089 | "pin-project", 1090 | "scoped-tls", 1091 | "serde", 1092 | "serde_json", 1093 | "serde_urlencoded", 1094 | "tokio", 1095 | "tokio-tungstenite", 1096 | "tokio-util", 1097 | "tower-service", 1098 | "tracing", 1099 | ] 1100 | 1101 | [[package]] 1102 | name = "wasi" 1103 | version = "0.11.0+wasi-snapshot-preview1" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1106 | 1107 | [[package]] 1108 | name = "windows-sys" 1109 | version = "0.52.0" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1112 | dependencies = [ 1113 | "windows-targets", 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "windows-targets" 1118 | version = "0.52.6" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1121 | dependencies = [ 1122 | "windows_aarch64_gnullvm", 1123 | "windows_aarch64_msvc", 1124 | "windows_i686_gnu", 1125 | "windows_i686_gnullvm", 1126 | "windows_i686_msvc", 1127 | "windows_x86_64_gnu", 1128 | "windows_x86_64_gnullvm", 1129 | "windows_x86_64_msvc", 1130 | ] 1131 | 1132 | [[package]] 1133 | name = "windows_aarch64_gnullvm" 1134 | version = "0.52.6" 1135 | source = "registry+https://github.com/rust-lang/crates.io-index" 1136 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1137 | 1138 | [[package]] 1139 | name = "windows_aarch64_msvc" 1140 | version = "0.52.6" 1141 | source = "registry+https://github.com/rust-lang/crates.io-index" 1142 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1143 | 1144 | [[package]] 1145 | name = "windows_i686_gnu" 1146 | version = "0.52.6" 1147 | source = "registry+https://github.com/rust-lang/crates.io-index" 1148 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1149 | 1150 | [[package]] 1151 | name = "windows_i686_gnullvm" 1152 | version = "0.52.6" 1153 | source = "registry+https://github.com/rust-lang/crates.io-index" 1154 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1155 | 1156 | [[package]] 1157 | name = "windows_i686_msvc" 1158 | version = "0.52.6" 1159 | source = "registry+https://github.com/rust-lang/crates.io-index" 1160 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1161 | 1162 | [[package]] 1163 | name = "windows_x86_64_gnu" 1164 | version = "0.52.6" 1165 | source = "registry+https://github.com/rust-lang/crates.io-index" 1166 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1167 | 1168 | [[package]] 1169 | name = "windows_x86_64_gnullvm" 1170 | version = "0.52.6" 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" 1172 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1173 | 1174 | [[package]] 1175 | name = "windows_x86_64_msvc" 1176 | version = "0.52.6" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1179 | 1180 | [[package]] 1181 | name = "zerocopy" 1182 | version = "0.7.35" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1185 | dependencies = [ 1186 | "byteorder", 1187 | "zerocopy-derive", 1188 | ] 1189 | 1190 | [[package]] 1191 | name = "zerocopy-derive" 1192 | version = "0.7.35" 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" 1194 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1195 | dependencies = [ 1196 | "proc-macro2", 1197 | "quote", 1198 | "syn", 1199 | ] 1200 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scanr" 3 | version = "0.1.15" 4 | edition = "2021" 5 | authors = ["Triet Trinh "] 6 | description = "ScanR: A lightweight, fast, and configurable port scanner built in Rust for reliable multi-platform network scanning." 7 | repository = "https://github.com/trinhminhtriet/scanr" 8 | homepage = "https://trinhminhtriet.com" 9 | license = "MIT" 10 | readme = "README.md" 11 | keywords = ["docker", "scanr", "port", "scan", "tokio"] 12 | categories = ["command-line-utilities"] 13 | 14 | [lints.rust] 15 | unsafe_code = "forbid" 16 | 17 | [lints.clippy] 18 | nursery = { level = "warn", priority = -1 } 19 | pedantic = { level = "warn", priority = -1 } 20 | unused_async = "warn" 21 | unwrap_used = "warn" 22 | expect_used = "warn" 23 | todo = "warn" 24 | module_name_repetitions = "allow" 25 | doc_markdown = "allow" 26 | similar_names = "allow" 27 | 28 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 29 | 30 | [dependencies] 31 | clap = { version = "4.5", features = ["color", "derive", "unicode"] } 32 | tokio = { version = "1.45", features = [ 33 | "macros", 34 | "net", 35 | "parking_lot", 36 | "rt", 37 | "rt-multi-thread", 38 | "signal", 39 | "sync", 40 | "time", 41 | ] } 42 | 43 | [target.'cfg(windows)'.dependencies] 44 | os_info = "3.11" 45 | 46 | [dev-dependencies] 47 | warp = "0.3" 48 | 49 | [profile.release] 50 | lto = true 51 | codegen-units = 1 52 | panic = "abort" 53 | strip = "none" 54 | debug = false 55 | opt-level = 3 56 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.86.0-bookworm AS builder 2 | 3 | # Install dependencies including LLVM 14 first 4 | RUN apt-get update && apt-get install -y \ 5 | clang \ 6 | cmake \ 7 | libssl-dev \ 8 | pkg-config \ 9 | llvm-14 \ 10 | libclang-14-dev 11 | 12 | # Set LIBCLANG_PATH after LLVM 14 is installed 13 | ENV LIBCLANG_PATH=/usr/lib/llvm-14/lib 14 | 15 | WORKDIR /app 16 | COPY . /app 17 | 18 | RUN cargo build --release 19 | 20 | # Prod stage 21 | FROM debian:bookworm-slim 22 | 23 | ARG APPLICATION="scanr" 24 | ARG DESCRIPTION="ScanR: A lightweight, fast, and configurable port scanner built in Rust for reliable multi-platform network scanning." 25 | ARG PACKAGE="trinhminhtriet/scanr" 26 | 27 | LABEL org.opencontainers.image.ref.name="${PACKAGE}" \ 28 | org.opencontainers.image.authors="Triet Trinh " \ 29 | org.opencontainers.image.documentation="https://github.com/${PACKAGE}/README.md" \ 30 | org.opencontainers.image.description="${DESCRIPTION}" \ 31 | org.opencontainers.image.licenses="MIT" \ 32 | org.opencontainers.image.source="https://github.com/${PACKAGE}" 33 | 34 | COPY --from=builder /app/target/release/scanr /bin/scanr 35 | WORKDIR /workdir 36 | 37 | # ENTRYPOINT ["scanr"] 38 | 39 | # 86400 seconds = 24 hours; # 3600 seconds = 1 hour 40 | CMD ["bash", "-c", "while true; do sleep 3600; done"] 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 trinhminhtriet.com 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := scanr 2 | AUTHOR := trinhminhtriet 3 | DATE := $(shell date +%FT%T%Z) 4 | GIT := $(shell [ -d .git ] && git rev-parse --short HEAD) 5 | VERSION := $(shell git describe --tags) 6 | 7 | default: build 8 | 9 | clean: 10 | @echo "Cleaning build dir" 11 | @$(RM) -r target 12 | @echo "Cleaning using cargo" 13 | @cargo clean 14 | 15 | check: 16 | @echo "Checking $(NAME)" 17 | @cargo check 18 | 19 | test: 20 | @echo "Running tests" 21 | @cargo test 22 | @echo "Running tests with coverage and open report in browser" 23 | @cargo tarpaulin --out Html --open 24 | 25 | build: 26 | @echo "Building release: $(VERSION)" 27 | @cargo build --release 28 | ln -sf $(PWD)/target/release/$(NAME) /usr/local/bin/$(NAME) 29 | which $(NAME) 30 | $(NAME) --version 31 | $(NAME) --help 32 | 33 | build_debug: 34 | @echo "Building debug" 35 | @cargo build 36 | 37 | run: 38 | @echo "Running debug" 39 | @cargo run 40 | 41 | release: 42 | ./scripts/release-version.sh 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🕵🏻 ScanR 2 | 3 | ```text 4 | ____ ____ 5 | / ___| ___ __ _ _ __ | _ \ 6 | \___ \ / __| / _` || '_ \ | |_) | 7 | ___) || (__ | (_| || | | || _ < 8 | |____/ \___| \__,_||_| |_||_| \_\ 9 | 10 | ``` 11 | 12 | 🕵🏻 ScanR: A lightweight, fast, and configurable port scanner built in Rust for reliable multi-platform network scanning. 13 | 14 | ![ScanR](docs/images/demo.gif) 15 | 16 | ## 🚀 Installation 17 | 18 | To install **scanr**, simply clone the repository and follow the instructions below: 19 | 20 | ### Build from source 21 | 22 | ```sh 23 | git clone git@github.com:trinhminhtriet/scanr.git 24 | cd scanr 25 | 26 | cargo build --release 27 | rm -rf /usr/local/bin/scanr \ 28 | && ln -s ${PWD}/target/release/scanr /usr/local/bin/scanr \ 29 | && which scanr && scanr --version 30 | ``` 31 | 32 | ### Build with Docker 33 | 34 | ```sh 35 | docker build -t trinhminhtriet/scanr . 36 | ``` 37 | 38 | ### Run from Docker 39 | 40 | ```sh 41 | docker run -it --rm trinhminhtriet/scanr 42 | docker run -it --rm trinhminhtriet/scanr https://github.com 43 | ``` 44 | 45 | Running the below command will globally install the `scanr` binary. 46 | 47 | ```sh 48 | cargo install scanr 49 | ``` 50 | 51 | Optionally, you can add `~/.cargo/bin` to your PATH if it's not already there 52 | 53 | ```sh 54 | echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc 55 | source ~/.bashrc 56 | ``` 57 | 58 | ## 💡 Usage 59 | 60 | Available command line arguments 61 | | argument|result| 62 | |--|--| 63 | | `[string]` | The address or IP to scan. [default: `127.0.0.1`] | 64 | |`-a` | Scan every port, from `1` to `65535`, conflicts with `-p` | 65 | |`-c [number]` | How many concurrent request should be made. [default: `1000`] | 66 | |`-m` | Monochrome mode - won't colourize the output [default: `false`] | 67 | |`-p [number / string]` | Inclusive port range to scan, accepts either a range: `-300`, `101-200`, or a single port `80`, conflicts with `-a` [default: `-1000`] | 68 | |`-r [number]` | Retry attempts per port. [default: `1`] | 69 | |`-t [number]` | Timeout for each request in milliseconds. [default: `2000`] | 70 | |`-6` | Scan the IPv6 address instead of IPv4, [default: `false`] | 71 | 72 | ### Examples 73 | 74 | ```shell 75 | # Scan github.com using the default settings 76 | scanr github.com 77 | 78 | # Scan default address [127.0.0.1], all ports [1-65535], 79 | # 2048 concurrent requests, 500ms timeout, 0 retries, IPv4 80 | scanr -a -c 2048 -t 500 -r 0 81 | 82 | # Scan www.google.com, ports 10-600, 83 | # 500 concurrent requests, 3000ms timeout, default retries [1], IPv4 84 | scanr www.google.com -p 10-600 -c 500 -t 3000 85 | 86 | # Scan www.digitalocean.com, ports 1-100 87 | # default concurrent requests[1000], 1000ms timeout, and use IPv6 address 88 | scanr www.digitalocean.com -p -100 -t 1000 -6 89 | 90 | # Scan www.bbc.com, port 443 only 91 | # default concurrent requests[1000], default timeout[2000ms], 6 retries, IPv4 92 | scanr www.bbc.com -p 443 -r 6 93 | ``` 94 | 95 | ## 🗑️ Uninstallation 96 | 97 | Running the below command will globally uninstall the `scanr` binary. 98 | 99 | ```sh 100 | cargo uninstall scanr 101 | ``` 102 | 103 | Remove the project repo 104 | 105 | ```sh 106 | rm -rf /path/to/git/clone/scanr 107 | ``` 108 | 109 | ## 🤝 How to contribute 110 | 111 | We welcome contributions! 112 | 113 | - Fork this repository; 114 | - Create a branch with your feature: `git checkout -b my-feature`; 115 | - Commit your changes: `git commit -m "feat: my new feature"`; 116 | - Push to your branch: `git push origin my-feature`. 117 | 118 | Once your pull request has been merged, you can delete your branch. 119 | 120 | ## 📝 License 121 | 122 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 123 | -------------------------------------------------------------------------------- /demo.tape: -------------------------------------------------------------------------------- 1 | Output docs/images/demo.gif 2 | 3 | Set FontSize 16 4 | Set Width 1440 5 | Set Height 768 6 | Set TypingSpeed 400ms 7 | 8 | Type@100ms "echo 'https://trinhminhtriet.com'" 9 | Sleep 50ms 10 | Enter 11 | 12 | Type@150ms "scanr --version" 13 | Sleep 50ms 14 | Enter 15 | Sleep 100ms 16 | 17 | Type@150ms "scanr --help" 18 | Sleep 50ms 19 | Enter 20 | Sleep 100ms 21 | 22 | Type@100ms "scanr github.com" 23 | Sleep 100ms 24 | Enter 25 | Sleep 5s 26 | 27 | Type@100ms "scanr www.google.com -p 10-600 -c 500 -t 3000" 28 | Sleep 100ms 29 | Enter 30 | Sleep 5s 31 | 32 | Type@100ms "scanr www.digitalocean.com -p -100 -t 1000 -6" 33 | Sleep 100ms 34 | Enter 35 | Sleep 5s 36 | 37 | Type@100ms "scanr www.bbc.com -p 443 -r 6" 38 | Sleep 100ms 39 | Enter 40 | Sleep 5s -------------------------------------------------------------------------------- /docs/images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trinhminhtriet/scanr/ef09500ac02f6d6f3afd0c8e6262c8d1d01ec104/docs/images/demo.gif -------------------------------------------------------------------------------- /scripts/release-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | [ -z "$(git status --porcelain)" ] || (echo "dirty working directory" && exit 1) 5 | 6 | current_version="$(grep '^version = ' Cargo.toml | head -1 | cut -d '"' -f2)" 7 | IFS='.' read -r major minor patch <<<"$current_version" 8 | new_patch=$((patch + 1)) 9 | new_version="$major.$minor.$new_patch" 10 | tag_name="v$new_version" 11 | 12 | if [ -z "$new_version" ]; then 13 | echo "New version required as argument" 14 | exit 1 15 | fi 16 | 17 | echo ">>> Bumping version" 18 | sed -i.bak "s/version = \"$current_version\"/version = \"$new_version\"/" Cargo.toml 19 | rm Cargo.toml.bak 20 | 21 | sed -i.bak "s/version = \"$tag_name\"/version = \"$tag_name\"/" src/main.rs 22 | rm src/main.rs.bak 23 | 24 | sleep 10 25 | 26 | echo ">>> Commit" 27 | git add Cargo.toml Cargo.lock 28 | git commit -am "version $new_version" 29 | git tag $tag_name 30 | 31 | echo ">>> Publish" 32 | git push 33 | git push origin $tag_name 34 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use scanner::{host_info, AllPortStatus}; 2 | use terminal::{print, spinner::Spinner}; 3 | 4 | mod parse_arg; 5 | mod port_descriptions; 6 | mod scanner; 7 | mod terminal; 8 | 9 | fn exit(code: i32) { 10 | Spinner::show_cursor(); 11 | std::process::exit(code); 12 | } 13 | 14 | fn tokio_signal() { 15 | tokio::spawn(async move { 16 | tokio::signal::ctrl_c().await.ok(); 17 | exit(1); 18 | }); 19 | } 20 | 21 | #[tokio::main] 22 | async fn main() { 23 | let cli_args = parse_arg::CliArgs::new(); 24 | terminal::text_color(&cli_args); 25 | let exit_error = || print::address_error(&cli_args); 26 | 27 | tokio_signal(); 28 | 29 | let Ok(host_info) = host_info::HostInfo::try_from(&cli_args.address).await else { 30 | return exit_error(); 31 | }; 32 | let Some(ip) = host_info.get_ip(&cli_args) else { 33 | return exit_error(); 34 | }; 35 | 36 | print::name_and_target(&cli_args, ip); 37 | print::extra_ips(&host_info, ip); 38 | 39 | let spinner = Spinner::new(); 40 | spinner.start(); 41 | let now = std::time::Instant::now(); 42 | let scan_output = AllPortStatus::scan_ports(&cli_args, ip).await; 43 | spinner.stop(); 44 | let done = now.elapsed(); 45 | 46 | print::scan_time(&scan_output, done); 47 | print::result_table(&scan_output); 48 | } 49 | -------------------------------------------------------------------------------- /src/parse_arg.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::ops::RangeInclusive; 3 | 4 | pub const PORT_UPPER_DEFAULT: u16 = 1000; 5 | 6 | #[derive(Parser, Debug, Clone)] 7 | #[command(version, about)] 8 | pub struct Cli { 9 | #[clap(default_value = "127.0.0.1", default_value_if("ip_v6", "true", "::1"))] 10 | address: String, 11 | 12 | /// Scan all ports, conflicts with "-p". 13 | #[clap(short = 'a', default_value_t = false, conflicts_with = "ports")] 14 | all_ports: bool, 15 | 16 | /// Maximum number of concurrent requests. 17 | #[clap(short = 'c', value_name = "concurrent", default_value_t = 1000)] 18 | concurrent: u16, 19 | 20 | /// Ports to scan, accepts a range, or single port, conflicts with "-p". 21 | #[clap( 22 | short = 'p', 23 | value_name = "ports", 24 | default_value = "-1000", 25 | conflicts_with = "all_ports", 26 | allow_hyphen_values = true 27 | )] 28 | ports: String, 29 | 30 | /// Monochrome mode - remove text colouring 31 | #[clap(short = 'm', default_value_t = false)] 32 | monochrome: bool, 33 | 34 | /// Maximum number of retry attempts per port. 35 | #[clap(short = 'r', value_name = "retries", default_value_t = 1)] 36 | retry: u8, 37 | 38 | /// Timeout for port scanning in milliseconds. 39 | #[clap(short = 't', value_name = "ms", default_value_t = 2000)] 40 | timeout: u32, 41 | 42 | /// Enable IPv6 scanning. Defaults to IPv4. 43 | #[clap(short = '6', default_value_t = false)] 44 | ip_v6: bool, 45 | } 46 | 47 | #[derive(Debug, Clone)] 48 | pub struct PortRange { 49 | pub min: u16, 50 | pub max: u16, 51 | pub range_size: u16, 52 | } 53 | 54 | impl PortRange { 55 | pub const fn get_range(&self) -> RangeInclusive { 56 | self.min..=self.max 57 | } 58 | } 59 | 60 | impl From<&Cli> for PortRange { 61 | fn from(cli: &Cli) -> Self { 62 | if cli.all_ports { 63 | Self { 64 | min: 1, 65 | max: u16::MAX, 66 | range_size: u16::MAX, 67 | } 68 | } else if cli.ports.contains('-') { 69 | let (start, end) = cli.ports.split_once('-').unwrap_or_default(); 70 | let start = start.parse::().unwrap_or(1); 71 | let end = end.parse::().unwrap_or(PORT_UPPER_DEFAULT); 72 | let range_size = start.abs_diff(end) + 1; 73 | 74 | if start <= end { 75 | Self { 76 | min: start, 77 | max: end, 78 | range_size, 79 | } 80 | } else { 81 | Self { 82 | min: end, 83 | max: start, 84 | range_size, 85 | } 86 | } 87 | } else { 88 | cli.ports.parse::().map_or( 89 | Self { 90 | min: 1, 91 | max: PORT_UPPER_DEFAULT, 92 | range_size: PORT_UPPER_DEFAULT, 93 | }, 94 | |single_port| Self { 95 | min: single_port, 96 | max: single_port, 97 | range_size: 1, 98 | }, 99 | ) 100 | } 101 | } 102 | } 103 | 104 | #[derive(Debug, Clone)] 105 | pub struct CliArgs { 106 | pub address: String, 107 | pub concurrent: u16, 108 | pub ip6: bool, 109 | pub monochrome: bool, 110 | pub ports: PortRange, 111 | pub retry: u8, 112 | pub timeout: u32, 113 | } 114 | 115 | impl CliArgs { 116 | pub fn new() -> Self { 117 | let cli = Cli::parse(); 118 | 119 | let port_range = PortRange::from(&cli); 120 | 121 | Self { 122 | address: cli.address, 123 | concurrent: cli.concurrent, 124 | ip6: cli.ip_v6, 125 | monochrome: cli.monochrome, 126 | ports: port_range, 127 | retry: cli.retry, 128 | timeout: cli.timeout, 129 | } 130 | } 131 | } 132 | 133 | #[cfg(test)] 134 | impl CliArgs { 135 | pub fn test_new(ports: String, concurrent: u16, address: Option<&str>, ip_v6: bool) -> Self { 136 | let adr = if ip_v6 && address.is_none() { 137 | "::1" 138 | } else { 139 | "127.0.0.1" 140 | }; 141 | 142 | let cli = Cli { 143 | address: address.unwrap_or(adr).to_owned(), 144 | all_ports: false, 145 | monochrome: false, 146 | concurrent, 147 | ports, 148 | retry: 1, 149 | timeout: 1250, 150 | ip_v6, 151 | }; 152 | let port_range = PortRange::from(&cli); 153 | 154 | Self { 155 | address: cli.address, 156 | concurrent: cli.concurrent, 157 | monochrome: cli.monochrome, 158 | ip6: cli.ip_v6, 159 | ports: port_range, 160 | retry: cli.retry, 161 | timeout: cli.timeout, 162 | } 163 | } 164 | } 165 | 166 | #[cfg(test)] 167 | mod tests { 168 | use super::{Cli, PortRange}; 169 | 170 | /// Re-useable test to make sure ports get parsed correctly 171 | fn test(ports: &str, min: u16, max: u16, range: u16) { 172 | let result = PortRange::from(&Cli { 173 | monochrome: false, 174 | address: "127.0.0.1".to_owned(), 175 | all_ports: false, 176 | concurrent: 1024, 177 | ip_v6: false, 178 | ports: ports.to_owned(), 179 | retry: 1, 180 | timeout: 1000, 181 | }); 182 | assert_eq!(result.min, min); 183 | assert_eq!(result.max, max); 184 | assert_eq!(result.range_size, range); 185 | assert_eq!(result.get_range(), (min..=max)); 186 | } 187 | 188 | #[test] 189 | fn test_cli_port_range() { 190 | test("1-1000", 1, 1000, 1000); 191 | test("1000-1", 1, 1000, 1000); 192 | test("100-200", 100, 200, 101); 193 | test("100-", 100, 1000, 901); 194 | test("80", 80, 80, 1); 195 | test("1-1000000", 1, 1000, 1000); 196 | test("65536-1000000", 1, 1000, 1000); 197 | test("random", 1, 1000, 1000); 198 | 199 | let result = PortRange::from(&Cli { 200 | monochrome: false, 201 | address: "127.0.0.1".to_owned(), 202 | all_ports: true, 203 | concurrent: 1024, 204 | ip_v6: false, 205 | ports: String::new(), 206 | retry: 100, 207 | timeout: 1000, 208 | }); 209 | assert_eq!(result.min, 1); 210 | assert_eq!(result.max, 65535); 211 | assert_eq!(result.range_size, 65535); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/port_descriptions/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | // https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml 4 | 5 | pub struct PortDescriptions { 6 | tcp: HashMap, 7 | } 8 | 9 | impl PortDescriptions { 10 | /// Generate the HashMap based on the "tcp.txt" file 11 | pub fn new() -> Self { 12 | let txt = include_str!("tcp.txt"); 13 | let txt_len = txt.lines().count(); 14 | let mut tcp = HashMap::with_capacity(txt_len); 15 | 16 | for line in txt.lines() { 17 | let (name, port) = line.split_once(',').unwrap_or_default(); 18 | if let Ok(port) = port.parse::() { 19 | tcp.insert(port, name); 20 | } 21 | } 22 | Self { tcp } 23 | } 24 | 25 | /// Get the description of a given port, with always return a &str, "unknown" for items with no descriptions 26 | /// At the moment it just gets TCP ports, need to implement UDP functionality later 27 | pub fn get<'a>(&self, port: u16) -> &'a str { 28 | self.tcp.get(&port).unwrap_or(&"unknown") 29 | } 30 | } 31 | 32 | #[cfg(test)] 33 | pub mod tests { 34 | 35 | use crate::port_descriptions::PortDescriptions; 36 | 37 | #[test] 38 | /// Check that known and unknown ports return the correct &str 39 | fn test_port_descriptions() { 40 | let values = PortDescriptions::new(); 41 | 42 | let response = values.get(43); 43 | assert_eq!(response, "whois"); 44 | 45 | let response = values.get(80); 46 | assert_eq!(response, "http"); 47 | 48 | let response = values.get(443); 49 | assert_eq!(response, "https"); 50 | 51 | let response = values.get(6379); 52 | assert_eq!(response, "redis"); 53 | 54 | let response = values.get(5432); 55 | assert_eq!(response, "postgresql"); 56 | 57 | let response = values.get(50001); 58 | assert_eq!(response, "unknown"); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/scanner/host_info.rs: -------------------------------------------------------------------------------- 1 | use crate::parse_arg::CliArgs; 2 | use std::{net::IpAddr, slice::Iter}; 3 | 4 | #[derive(Debug, Hash, Clone)] 5 | pub struct HostInfo { 6 | elapsed: std::time::Duration, 7 | ips: Vec, 8 | } 9 | 10 | impl HostInfo { 11 | pub async fn try_from(address: &str) -> Result { 12 | let now = std::time::Instant::now(); 13 | (tokio::time::timeout( 14 | std::time::Duration::from_millis(5000), 15 | tokio::net::lookup_host(format!("{address}:80")), 16 | ) 17 | .await) 18 | .map_or(Err(()), |lookup| { 19 | lookup.map_or(Err(()), |mut addr| { 20 | let mut all_ips = vec![]; 21 | for socket_addr in addr.by_ref() { 22 | all_ips.push(socket_addr.ip()); 23 | } 24 | if all_ips.is_empty() { 25 | Err(()) 26 | } else { 27 | all_ips.sort(); 28 | Ok(Self { 29 | elapsed: now.elapsed(), 30 | ips: all_ips, 31 | }) 32 | } 33 | }) 34 | }) 35 | } 36 | 37 | pub fn get_ip(&self, cli_args: &CliArgs) -> Option<&IpAddr> { 38 | self.ips.iter().find(|x| { 39 | if cli_args.ip6 { 40 | x.is_ipv6() 41 | } else { 42 | x.is_ipv4() 43 | } 44 | }) 45 | } 46 | 47 | pub fn iter_ip(&self) -> Iter { 48 | self.ips.iter() 49 | } 50 | 51 | pub fn ip_len(&self) -> usize { 52 | self.ips.len() 53 | } 54 | 55 | #[cfg(test)] 56 | pub const fn test_get(ips: Vec) -> Self { 57 | Self { 58 | elapsed: std::time::Duration::from_millis(0), 59 | ips, 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/scanner/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod host_info; 2 | 3 | use crate::{exit, parse_arg::CliArgs, port_descriptions::PortDescriptions}; 4 | use std::{collections::HashSet, future::Future, net::IpAddr, pin::Pin}; 5 | use tokio::{net::TcpStream, sync::mpsc::Sender}; 6 | 7 | #[derive(Debug, Clone, Hash, Copy)] 8 | struct PortMessage { 9 | port: u16, 10 | open: bool, 11 | } 12 | 13 | impl PortMessage { 14 | /// Check if the next port in the sequence can be spawned 15 | /// Checks that port + chunk won't overflow, and then if not, check that the output is smaller or equal to the port_max 16 | fn can_spawn(self, chunk: u16, port_max: u16) -> Option { 17 | self.port.checked_add(chunk).filter(|&p| p <= port_max) 18 | } 19 | } 20 | 21 | #[derive(Debug, Clone)] 22 | pub struct AllPortStatus { 23 | open: HashSet, 24 | number_ports: u16, 25 | port_max: u16, 26 | pub closed: u16, 27 | } 28 | 29 | /// This is used on the second pass, to fill the second pass Struct based on the first 30 | impl From<&Self> for AllPortStatus { 31 | fn from(value: &Self) -> Self { 32 | Self { 33 | open: HashSet::with_capacity(value.open_len().into()), 34 | closed: value.closed, 35 | number_ports: value.number_ports, 36 | port_max: value.port_max, 37 | } 38 | } 39 | } 40 | 41 | impl AllPortStatus { 42 | fn new(cli_args: &CliArgs) -> Self { 43 | Self { 44 | open: HashSet::with_capacity(64), 45 | closed: 0, 46 | number_ports: cli_args.ports.range_size, 47 | port_max: cli_args.ports.max, 48 | } 49 | } 50 | 51 | /// Create the actual output with descriptions 52 | /// Will only generate the port_descriptions hashset if there are any open ports and all ports have been scanned 53 | pub fn get_all_open<'a>(&self) -> Option> { 54 | if !self.open.is_empty() && self.complete() { 55 | let port_details = PortDescriptions::new(); 56 | let mut output = self 57 | .open 58 | .iter() 59 | .map(|i| (*i, port_details.get(*i))) 60 | .collect::>(); 61 | output.sort_by(|a, b| a.0.cmp(&b.0)); 62 | Some(output) 63 | } else { 64 | None 65 | } 66 | } 67 | 68 | /// Get total number of open ports 69 | pub fn open_len(&self) -> u16 { 70 | u16::try_from(self.open.len()).unwrap_or_default() 71 | } 72 | 73 | /// Return true if all ports have been scanned 74 | fn complete(&self) -> bool { 75 | self.open_len() + self.closed == self.number_ports 76 | } 77 | 78 | /// Insert new port details, if true store port, if false just increase a counter 79 | fn insert(&mut self, message: PortMessage) { 80 | if message.open { 81 | self.open.insert(message.port); 82 | } else { 83 | self.closed += 1; 84 | } 85 | } 86 | 87 | /// Scan the given port, will be recursive if counter > 1 88 | /// Should this be changed to an async fn that just uses a while loop, instead of Box::pin recursion? 89 | fn scan_port( 90 | port: u16, 91 | ip: IpAddr, 92 | timeout: u32, 93 | sx: Sender, 94 | counter: u8, 95 | ) -> Pin + Send>> { 96 | Box::pin(async move { 97 | if let Ok(Ok(_)) = tokio::time::timeout( 98 | std::time::Duration::from_millis(u64::from(timeout)), 99 | TcpStream::connect(format!("{ip}:{port}")), 100 | ) 101 | .await 102 | { 103 | // Should one try to actually write to the port? 104 | // let open = stream.try_write(&[0]).is_ok(); 105 | if sx.send(PortMessage { port, open: true }).await.is_err() { 106 | exit(1); 107 | }; 108 | } else if counter > 1 { 109 | Self::scan_port(port, ip, timeout, sx, counter - 1).await; 110 | } else if sx.send(PortMessage { port, open: false }).await.is_err() { 111 | exit(1); 112 | }; 113 | }) 114 | } 115 | 116 | /// Spawn a port scan into its own thread 117 | fn spawn_scan_port(port: u16, ip: IpAddr, timeout: u32, sx: Sender, counter: u8) { 118 | tokio::spawn(Self::scan_port(port, ip, timeout, sx, counter)); 119 | } 120 | 121 | /// Scan the entire range of selected ports by initiating multiple concurrent requests simultaneously 122 | async fn first_pass(cli_args: &CliArgs, ip: &IpAddr) -> Self { 123 | let mut first_pass = Self::new(cli_args); 124 | let retry = cli_args.retry + 1; 125 | 126 | let (sx, mut rx) = tokio::sync::mpsc::channel(usize::from(cli_args.concurrent)); 127 | 128 | for port in cli_args.ports.get_range().take(cli_args.concurrent.into()) { 129 | Self::spawn_scan_port(port, *ip, cli_args.timeout, sx.clone(), retry); 130 | } 131 | 132 | while let Some(message) = rx.recv().await { 133 | first_pass.insert(message); 134 | 135 | // Need to manually close the receiver here, as we don't drop sx, and its being continually cloned 136 | if first_pass.complete() { 137 | rx.close(); 138 | } 139 | 140 | if let Some(new_port) = message.can_spawn(cli_args.concurrent, first_pass.port_max) { 141 | Self::spawn_scan_port(new_port, *ip, cli_args.timeout, sx.clone(), retry); 142 | } 143 | } 144 | first_pass 145 | } 146 | 147 | /// Verify the discovered open ports through sequential scans instead of concurrent spawning. 148 | async fn second_pass(first_pass: Self, cli_args: &CliArgs, ip: &IpAddr) -> Self { 149 | let mut validated_result = Self::from(&first_pass); 150 | let (sx, mut rx) = tokio::sync::mpsc::channel(first_pass.open.len()); 151 | for port in &first_pass.open { 152 | Self::scan_port(*port, *ip, cli_args.timeout, sx.clone(), cli_args.retry + 1).await; 153 | } 154 | drop(sx); 155 | while let Some(message) = rx.recv().await { 156 | validated_result.insert(message); 157 | } 158 | validated_result 159 | } 160 | 161 | /// Scan the ports, first pass spawns the request, second pass runs in series 162 | pub async fn scan_ports(cli_args: &CliArgs, ip: &IpAddr) -> Self { 163 | let first_pass = Self::first_pass(cli_args, ip).await; 164 | if first_pass.open_len() > 0 { 165 | Self::second_pass(first_pass, cli_args, ip).await 166 | } else { 167 | first_pass 168 | } 169 | } 170 | } 171 | 172 | #[cfg(test)] 173 | impl AllPortStatus { 174 | pub const fn test_new( 175 | open: HashSet, 176 | number_ports: u16, 177 | port_max: u16, 178 | closed: u16, 179 | ) -> Self { 180 | Self { 181 | open, 182 | number_ports, 183 | port_max, 184 | closed, 185 | } 186 | } 187 | } 188 | 189 | #[cfg(test)] 190 | #[allow(clippy::unwrap_used)] 191 | mod tests { 192 | use std::{ 193 | collections::HashSet, 194 | net::{IpAddr, Ipv4Addr, Ipv6Addr}, 195 | }; 196 | 197 | use warp::Filter; 198 | 199 | use crate::{ 200 | parse_arg, 201 | scanner::{AllPortStatus, PortMessage}, 202 | }; 203 | 204 | const V4_LOCAL: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); 205 | const V6_LOCAL: IpAddr = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)); 206 | 207 | /// Start a server, on a given port, in own thread, on both IPv4 and IPv6 interfaces 208 | async fn start_server(port: u16) { 209 | let routes = warp::any().map(|| "Test Sever"); 210 | 211 | tokio::spawn(warp::serve(routes).run((V4_LOCAL, port))); 212 | tokio::spawn(warp::serve(routes).run((V6_LOCAL, port))); 213 | tokio::time::sleep(std::time::Duration::from_millis(1)).await; 214 | } 215 | 216 | #[test] 217 | /// Check that a port can be spawned off. based on current port, chunk size, and upper limit 218 | fn test_scanner_can_portmessage_spawn() { 219 | let message = PortMessage { 220 | port: 80, 221 | open: true, 222 | }; 223 | 224 | assert!(message.can_spawn(10, 81).is_none()); 225 | assert!(message.can_spawn(10, 90).is_some()); 226 | assert!(message.can_spawn(10, 91).is_some()); 227 | } 228 | 229 | #[test] 230 | /// Depending on message open status, port gets inserted into HashSet, or closed total is increased 231 | fn test_scanner_get_insert() { 232 | let mut result = AllPortStatus { 233 | open: HashSet::new(), 234 | closed: 8, 235 | number_ports: 10, 236 | port_max: 10, 237 | }; 238 | 239 | result.insert(PortMessage { 240 | port: 1, 241 | open: false, 242 | }); 243 | assert_eq!(result.closed, 9); 244 | 245 | result.insert(PortMessage { 246 | port: 80, 247 | open: true, 248 | }); 249 | assert_eq!(result.closed, 9); 250 | assert_eq!(result.open_len(), 1); 251 | assert!(result.open.contains(&80)); 252 | } 253 | 254 | #[test] 255 | /// Scan all ports, at least 2 should be open 256 | fn test_scanner_get_open_none() { 257 | let mut result = AllPortStatus { 258 | open: HashSet::new(), 259 | closed: 8, 260 | number_ports: 10, 261 | port_max: 10, 262 | }; 263 | 264 | // 8 of 10 ports scanned, so is_complete is false and get_all_open is none 265 | assert!(!result.complete()); 266 | assert!(result.get_all_open().is_none()); 267 | 268 | result.closed = 9; 269 | // 9 of 10 ports scanned, so is_complete is false and get_all_open is none 270 | assert!(!result.complete()); 271 | assert!(result.get_all_open().is_none()); 272 | 273 | // All ports now "scanned", so complete() and get_all_open is Some() 274 | result.closed = 10; 275 | assert!(result.complete()); 276 | assert!(result.get_all_open().is_none()); 277 | } 278 | 279 | #[test] 280 | /// Extract port descriptions Vec<(u16, &str)> when all scans complete, retrieve valid descriptions for given ports 281 | fn test_scanner_get_open_some() { 282 | let mut result = AllPortStatus { 283 | open: HashSet::new(), 284 | closed: 8, 285 | number_ports: 10, 286 | port_max: 10, 287 | }; 288 | 289 | // 8 of 10 ports scanned, so is_complete is false 290 | assert!(!result.complete()); 291 | assert!(result.get_all_open().is_none()); 292 | 293 | result.open.insert(6379); 294 | // 9 of 10 ports scanned, so is_complete is false 295 | assert!(!result.complete()); 296 | assert!(result.get_all_open().is_none()); 297 | 298 | result.open.insert(443); 299 | assert!(result.complete()); 300 | let result = result.get_all_open(); 301 | assert!(result.is_some()); 302 | let result = result.unwrap(); 303 | 304 | assert_eq!(result.len(), 2); 305 | // First element is 443, as has been pre-sorted 306 | assert_eq!(result[0], (443, "https")); 307 | assert_eq!(result[1], (6379, "redis")); 308 | } 309 | 310 | #[tokio::test] 311 | /// Zero ports of 1-1000 open 312 | async fn test_scanner_1000_empty() { 313 | let cli_args = parse_arg::CliArgs::test_new("1-1000".to_owned(), 2048, None, false); 314 | let result = AllPortStatus::first_pass(&cli_args, &V4_LOCAL).await; 315 | assert_eq!(result.closed, 1000); 316 | assert_eq!(result.open.len(), 0); 317 | } 318 | 319 | #[tokio::test] 320 | /// Scan all ports, due to VSCode some ports might be open 321 | async fn test_scanner_all() { 322 | let cli_args = parse_arg::CliArgs::test_new("1-65535".to_owned(), 2048, None, false); 323 | let result = AllPortStatus::first_pass(&cli_args, &V4_LOCAL).await; 324 | println!("{result:#?}"); 325 | assert_eq!( 326 | usize::from(result.closed) + usize::from(result.open_len()), 327 | 65535 328 | ); 329 | } 330 | 331 | #[tokio::test] 332 | /// Port 80 open, but only scan first 10, so zero response, on both IPv4 and IPv6 interfaces 333 | async fn test_scanner_10_port_80_empty() { 334 | start_server(80).await; 335 | let cli_args = parse_arg::CliArgs::test_new("1-10".to_owned(), 2048, None, false); 336 | let result = AllPortStatus::first_pass(&cli_args, &V4_LOCAL).await; 337 | assert_eq!(result.closed, 10); 338 | assert_eq!(result.open.len(), 0); 339 | 340 | let cli_args = parse_arg::CliArgs::test_new("1-10".to_owned(), 2048, Some("::1"), true); 341 | let result = AllPortStatus::first_pass(&cli_args, &V6_LOCAL).await; 342 | assert_eq!(result.closed, 10); 343 | assert_eq!(result.open.len(), 0); 344 | } 345 | 346 | #[tokio::test] 347 | /// Port 80 open of first 1000, on both IPv4 and IPv6 interfaces 348 | async fn test_scanner_port_80() { 349 | start_server(80).await; 350 | let cli_args = parse_arg::CliArgs::test_new("1-1000".to_owned(), 2048, None, false); 351 | let result = AllPortStatus::first_pass(&cli_args, &V4_LOCAL).await; 352 | assert_eq!(result.closed, 999); 353 | assert_eq!(result.open.len(), 1); 354 | assert_eq!( 355 | usize::from(result.closed) + usize::from(result.open_len()), 356 | 1000 357 | ); 358 | 359 | let cli_args = parse_arg::CliArgs::test_new("1-1000".to_owned(), 2048, Some("::1"), true); 360 | let result = AllPortStatus::first_pass(&cli_args, &V6_LOCAL).await; 361 | assert_eq!(result.closed, 999); 362 | assert_eq!(result.open.len(), 1); 363 | assert_eq!( 364 | usize::from(result.closed) + usize::from(result.open_len()), 365 | 1000 366 | ); 367 | } 368 | 369 | #[tokio::test] 370 | /// Port 80 and 443 open of first 1000, on both IPv4 and IPv6 interfaces 371 | async fn test_scanner_1000_80_443() { 372 | start_server(80).await; 373 | start_server(443).await; 374 | let cli_args = parse_arg::CliArgs::test_new("1-1000".to_owned(), 2048, None, false); 375 | let result = AllPortStatus::first_pass(&cli_args, &V4_LOCAL).await; 376 | assert_eq!(result.closed, 998); 377 | assert_eq!(result.open.len(), 2); 378 | assert_eq!( 379 | usize::from(result.closed) + usize::from(result.open_len()), 380 | 1000 381 | ); 382 | 383 | let cli_args = parse_arg::CliArgs::test_new("1-1000".to_owned(), 2048, Some("::1"), false); 384 | let result = AllPortStatus::first_pass(&cli_args, &V6_LOCAL).await; 385 | assert_eq!(result.closed, 998); 386 | assert_eq!(result.open.len(), 2); 387 | assert_eq!( 388 | usize::from(result.closed) + usize::from(result.open_len()), 389 | 1000 390 | ); 391 | } 392 | 393 | #[tokio::test] 394 | /// Scan all ports, at least 2 should be open, on both IPv4 and IPv6 interfaces 395 | async fn test_scanner_all_80() { 396 | start_server(80).await; 397 | let cli_args = parse_arg::CliArgs::test_new("1-65535".to_owned(), 2048, None, false); 398 | let result = AllPortStatus::first_pass(&cli_args, &V4_LOCAL).await; 399 | assert_eq!( 400 | usize::from(result.closed) + usize::from(result.open_len()), 401 | 65535 402 | ); 403 | // Random ports can be open when developing in VSCode Dev Container, so just check that open ports is minimum 2 404 | assert!(result.open.len() >= 2); 405 | let result = result.get_all_open(); 406 | assert!(result.is_some()); 407 | let result = result.unwrap(); 408 | assert!(result.contains(&(80, "http"))); 409 | 410 | let cli_args = parse_arg::CliArgs::test_new("1-65535".to_owned(), 2048, Some("::1"), false); 411 | let result = AllPortStatus::first_pass(&cli_args, &V6_LOCAL).await; 412 | assert_eq!( 413 | usize::from(result.closed) + usize::from(result.open_len()), 414 | 65535 415 | ); 416 | // Random ports can be open when developing in VSCode Dev Container, so just check that open ports is minimum 2 417 | assert!(result.open.len() >= 2); 418 | let result = result.get_all_open(); 419 | assert!(result.is_some()); 420 | let result = result.unwrap(); 421 | assert!(result.contains(&(80, "http"))); 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /src/terminal/color.rs: -------------------------------------------------------------------------------- 1 | use crate::parse_arg::CliArgs; 2 | use std::fmt; 3 | use std::sync::atomic::AtomicBool; 4 | 5 | pub static MONOCHROME: AtomicBool = AtomicBool::new(false); 6 | pub static WIN_10: AtomicBool = AtomicBool::new(false); 7 | 8 | #[cfg(windows)] 9 | /// Set the MONOCHROME & WIN_10 static atomic bools, MONOCHROME based on Windows version OR cli_args 10 | pub fn text_color(cli_args: &CliArgs) { 11 | let win_10 = os_info::get() 12 | .edition() 13 | .is_some_and(|i| !i.contains("Windows 11")); 14 | MONOCHROME.store( 15 | win_10 || cli_args.monochrome, 16 | std::sync::atomic::Ordering::SeqCst, 17 | ); 18 | WIN_10.store(win_10, std::sync::atomic::Ordering::SeqCst); 19 | } 20 | 21 | #[cfg(not(windows))] 22 | /// Set the MONOCHROME static variable, based on cli_args 23 | pub fn text_color(cli_args: &CliArgs) { 24 | MONOCHROME.store(cli_args.monochrome, std::sync::atomic::Ordering::SeqCst); 25 | } 26 | 27 | /// Colorize the text using escape codes 28 | /// Will be ignored if MONOCHROME is true 29 | pub enum Color { 30 | Green, 31 | Magenta, 32 | Red, 33 | Reset, 34 | Underline, 35 | Yellow, 36 | } 37 | 38 | impl fmt::Display for Color { 39 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 40 | if MONOCHROME.load(std::sync::atomic::Ordering::SeqCst) { 41 | Ok(()) 42 | } else { 43 | let disp = match self { 44 | Self::Green => "32", 45 | Self::Magenta => "35", 46 | Self::Red => "31", 47 | Self::Reset => "0", 48 | Self::Underline => "4", 49 | Self::Yellow => "33", 50 | }; 51 | write!(f, "\x1b[{disp}m") 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/terminal/mod.rs: -------------------------------------------------------------------------------- 1 | mod color; 2 | pub mod spinner; 3 | 4 | pub use color::text_color; 5 | 6 | pub mod print { 7 | use std::net::IpAddr; 8 | 9 | use crate::{ 10 | exit, 11 | parse_arg::CliArgs, 12 | scanner::{host_info::HostInfo, AllPortStatus}, 13 | terminal::color::Color, 14 | }; 15 | 16 | /// Generate a string from a Duration, in format "x.xxxs" 17 | fn ms_to_string(dur: std::time::Duration) -> String { 18 | let as_ms = dur.as_millis(); 19 | let as_sec = dur.as_secs(); 20 | format!( 21 | "{}.{:<03}s", 22 | as_sec, 23 | as_ms.saturating_sub(u128::from(as_sec * 1000)) 24 | ) 25 | } 26 | 27 | /// Generate an Optional of any additional IP addresses 28 | fn get_extra_ips(host_info: &HostInfo, ip: &IpAddr) -> Option { 29 | let ip_len = host_info.ip_len(); 30 | if ip_len > 1 { 31 | let mut output = "Other IPs: ".to_owned(); 32 | host_info 33 | .iter_ip() 34 | .filter(|z| z != &ip) 35 | .enumerate() 36 | .for_each(|(index, i)| { 37 | let prefix = if index > 0 { ", " } else { "" }; 38 | output.push_str(&format!("{prefix}{i}")); 39 | }); 40 | Some(output) 41 | } else { 42 | None 43 | } 44 | } 45 | 46 | /// Generate information about the host/address/IP that will be scanned, to be shown on two lines along with the application name 47 | fn get_host_ip(cli_args: &CliArgs, ip: &IpAddr) -> (String, String, String) { 48 | let ports = if cli_args.ports.min == cli_args.ports.max { 49 | format!("{}", cli_args.ports.min) 50 | } else { 51 | format!("{}-{}", cli_args.ports.min, cli_args.ports.max) 52 | }; 53 | if cli_args.address == ip.to_string() { 54 | (format!("{}:", cli_args.address), ports, String::new()) 55 | } else { 56 | (format!("{ip}:"), ports, cli_args.address.to_string()) 57 | } 58 | } 59 | 60 | /// Generate the string for scan_time function 61 | fn get_scan_time(result: &AllPortStatus, done: std::time::Duration) -> String { 62 | format!( 63 | "{g}{t} open{r}, {re}{cl} closed{r}, took {ms}", 64 | g = Color::Green, 65 | t = result.open_len(), 66 | r = Color::Reset, 67 | re = Color::Red, 68 | cl = result.closed, 69 | ms = ms_to_string(done) 70 | ) 71 | } 72 | 73 | /// Generate the results table, assuming there are open ports 74 | fn get_table(result: &AllPortStatus) -> Option { 75 | result.get_all_open().map(|ports| { 76 | let mut output = format!( 77 | "{u}PORT{r} {u}DESCRIPTION{r}", 78 | u = Color::Underline, 79 | r = Color::Reset 80 | ); 81 | 82 | for (index, (port, desc)) in ports.iter().enumerate() { 83 | let color = if index % 2 == 0 { 84 | Color::Yellow 85 | } else { 86 | Color::Reset 87 | }; 88 | output.push_str(&format!("\n{}{port:<5} {desc}{}", color, Color::Reset)); 89 | } 90 | output 91 | }) 92 | } 93 | 94 | /// Print invalid address to screen, then quit with error code 1 95 | pub fn address_error(cli_args: &CliArgs) { 96 | println!( 97 | "{c}Error with address: {r}{a}", 98 | a = cli_args.address, 99 | c = Color::Red, 100 | r = Color::Reset, 101 | ); 102 | exit(1); 103 | } 104 | 105 | /// Print any additional IP's, ignoring the IP that will be scanned 106 | pub fn extra_ips(host_info: &HostInfo, ip: &IpAddr) { 107 | if let Some(extra_ips) = get_extra_ips(host_info, ip) { 108 | println!("{extra_ips}"); 109 | } 110 | } 111 | 112 | /// Print the name of the application, using a small figlet font 113 | pub fn name_and_target(cli_args: &CliArgs, ip: &IpAddr) { 114 | let (ip, ports, address) = get_host_ip(cli_args, ip); 115 | let bar = (0..=address 116 | .chars() 117 | .count() 118 | .max(ip.chars().count() + ports.chars().count()) 119 | + 19) 120 | .map(|_| '═') 121 | .collect::(); 122 | println!( 123 | "{m}{bar}\n{r} {ip}{y}{ports}{r}\n{m}{r} {address}\n{m}{bar}{r}", 124 | m = Color::Magenta, 125 | y = Color::Yellow, 126 | r = Color::Reset 127 | ); 128 | } 129 | 130 | /// If any open ports found, print the results into a table 131 | pub fn result_table(result: &AllPortStatus) { 132 | if let Some(result) = get_table(result) { 133 | println!("{result}"); 134 | } 135 | } 136 | 137 | /// Print totals of open & closed ports, and the time it took to scan them 138 | pub fn scan_time(result: &AllPortStatus, done: std::time::Duration) { 139 | println!("{}", get_scan_time(result, done)); 140 | } 141 | 142 | #[cfg(test)] 143 | #[allow(clippy::unwrap_used)] 144 | mod tests { 145 | use std::{ 146 | collections::HashSet, 147 | net::{Ipv4Addr, Ipv6Addr}, 148 | }; 149 | 150 | use crate::{parse_arg, terminal::color::MONOCHROME}; 151 | 152 | use super::*; 153 | 154 | #[test] 155 | /// A duration is formatted into a "X.YYYs" string 156 | fn test_terminal_ms_to_string() { 157 | assert_eq!( 158 | ms_to_string(std::time::Duration::from_millis(250)), 159 | "0.250s" 160 | ); 161 | assert_eq!( 162 | ms_to_string(std::time::Duration::from_millis(1000)), 163 | "1.000s" 164 | ); 165 | assert_eq!( 166 | ms_to_string(std::time::Duration::from_millis(1250)), 167 | "1.250s" 168 | ); 169 | assert_eq!( 170 | ms_to_string(std::time::Duration::from_millis(12_8250)), 171 | "128.250s" 172 | ); 173 | } 174 | 175 | #[test] 176 | /// Generate extra ip function will either return None, or a String in format "Other IPs: xx" 177 | fn test_terminal_generate_extra_ips() { 178 | let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); 179 | let ips = Vec::from([ip]); 180 | let host_info = HostInfo::test_get(ips); 181 | assert!(get_extra_ips(&host_info, &ip).is_none()); 182 | 183 | let ip_2 = IpAddr::V4(Ipv4Addr::new(255, 255, 255, 255)); 184 | let ips = Vec::from([ip, ip_2]); 185 | let host_info = HostInfo::test_get(ips); 186 | let result = get_extra_ips(&host_info, &ip); 187 | assert!(result.is_some()); 188 | assert_eq!(result.unwrap(), "Other IPs: 255.255.255.255"); 189 | 190 | let ip_3 = IpAddr::V6(Ipv6Addr::new(127, 126, 125, 124, 123, 122, 121, 120)); 191 | let ips = Vec::from([ip, ip_2, ip_3]); 192 | let host_info = HostInfo::test_get(ips); 193 | let result = get_extra_ips(&host_info, &ip); 194 | assert!(result.is_some()); 195 | assert_eq!( 196 | get_extra_ips(&host_info, &ip).unwrap(), 197 | "Other IPs: 255.255.255.255, 7f:7e:7d:7c:7b:7a:79:78" 198 | ); 199 | } 200 | 201 | #[test] 202 | /// Get the IP and Address or just IP and a blank string, as well as the colorised port range 203 | fn test_terminal_host_ip() { 204 | let cli_args = parse_arg::CliArgs::test_new("1".to_owned(), 512, None, false); 205 | let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); 206 | assert_eq!( 207 | get_host_ip(&cli_args, &ip), 208 | ("127.0.0.1:".into(), "1".into(), String::new()) 209 | ); 210 | 211 | let cli_args = parse_arg::CliArgs::test_new("1-10000".to_owned(), 512, None, false); 212 | let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); 213 | assert_eq!( 214 | get_host_ip(&cli_args, &ip), 215 | ("127.0.0.1:".into(), "1-10000".into(), String::new()) 216 | ); 217 | 218 | let cli_args = parse_arg::CliArgs::test_new("1-65535".to_owned(), 512, None, false); 219 | let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); 220 | assert_eq!( 221 | get_host_ip(&cli_args, &ip), 222 | ("127.0.0.1:".into(), "1-65535".into(), String::new()) 223 | ); 224 | 225 | let cli_args = 226 | parse_arg::CliArgs::test_new("1".to_owned(), 512, Some("www.google.com"), false); 227 | let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); 228 | assert_eq!( 229 | get_host_ip(&cli_args, &ip), 230 | ( 231 | "127.0.0.1:".into(), 232 | "1".into(), 233 | String::from("www.google.com") 234 | ) 235 | ); 236 | 237 | let cli_args = parse_arg::CliArgs::test_new( 238 | "1-10000".to_owned(), 239 | 512, 240 | Some("www.google.com"), 241 | false, 242 | ); 243 | let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); 244 | assert_eq!( 245 | get_host_ip(&cli_args, &ip), 246 | ( 247 | "127.0.0.1:".into(), 248 | "1-10000".into(), 249 | String::from("www.google.com") 250 | ) 251 | ); 252 | 253 | let cli_args = parse_arg::CliArgs::test_new( 254 | "1-65535".to_owned(), 255 | 512, 256 | Some("www.google.com"), 257 | false, 258 | ); 259 | let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); 260 | assert_eq!( 261 | get_host_ip(&cli_args, &ip), 262 | ( 263 | "127.0.0.1:".into(), 264 | "1-65535".into(), 265 | String::from("www.google.com") 266 | ) 267 | ); 268 | } 269 | 270 | #[test] 271 | /// Generate extra ip function will either return None, or a String in format "Other IPs: xx" 272 | fn test_terminal_table() { 273 | let input = AllPortStatus::test_new(HashSet::new(), 10, 1, 0); 274 | assert!(get_table(&input).is_none()); 275 | 276 | let input = AllPortStatus::test_new(HashSet::from([80]), 1, 80, 0); 277 | let result = get_table(&input); 278 | assert!(result.is_some()); 279 | let result = result.unwrap(); 280 | assert!(result.contains("PORT")); 281 | assert!(result.contains("DESCRIPTION")); 282 | assert!(result.contains("80 ")); 283 | assert!(result.contains("http")); 284 | 285 | let input = AllPortStatus::test_new(HashSet::from([443, 6379, 32825]), 3, 32825, 0); 286 | let result = get_table(&input); 287 | assert!(result.is_some()); 288 | let result = result.unwrap(); 289 | assert!(result.contains("PORT")); 290 | assert!(result.contains("DESCRIPTION")); 291 | assert!(result.contains("443 ")); 292 | assert!(result.contains("https")); 293 | assert!(result.contains("6379 ")); 294 | assert!(result.contains("redis")); 295 | assert!(result.contains("32825 ")); 296 | assert!(result.contains("unknown")); 297 | } 298 | 299 | #[test] 300 | /// Generate extra ip function will either return None, or a String in format "Other IPs: xx" 301 | fn test_terminal_scan_time() { 302 | let input = ( 303 | AllPortStatus::test_new(HashSet::new(), 10, 1, 10), 304 | std::time::Duration::from_millis(100), 305 | ); 306 | let result = get_scan_time(&input.0, input.1); 307 | assert!(result.contains("0 open")); 308 | assert!(result.contains("10 closed")); 309 | assert!(result.contains(", took 0.100s")); 310 | 311 | let input = ( 312 | AllPortStatus::test_new(HashSet::from([1, 2, 3]), 10, 1, 6), 313 | std::time::Duration::from_millis(2500), 314 | ); 315 | let result = get_scan_time(&input.0, input.1); 316 | assert!(result.contains("3 open")); 317 | assert!(result.contains("6 closed")); 318 | assert!(result.contains(", took 2.500s")); 319 | } 320 | 321 | #[test] 322 | /// Test that escape codes are printed to output 323 | fn test_terminal_monochrome_false() { 324 | MONOCHROME.store(false, std::sync::atomic::Ordering::SeqCst); 325 | let input = ( 326 | AllPortStatus::test_new(HashSet::new(), 10, 1, 10), 327 | std::time::Duration::from_millis(100), 328 | ); 329 | let result = get_scan_time(&input.0, input.1); 330 | 331 | assert!(result.contains("\x1b[32m0 open\x1b[0m")); 332 | assert!(result.contains("\x1b[31m10 closed\x1b[0m")); 333 | 334 | let input = AllPortStatus::test_new(HashSet::from([443, 6379, 32825]), 3, 32825, 0); 335 | let result = get_table(&input); 336 | assert!(result.is_some()); 337 | let result = result.unwrap(); 338 | 339 | assert!(result.contains("\x1b[33m443")); 340 | assert!(result.contains("https\x1b[0m")); 341 | 342 | assert!(result.contains("6379")); 343 | assert!(result.contains("redis")); 344 | 345 | assert!(result.contains("\x1b[33m32825")); 346 | assert!(result.contains("unknown\x1b[0m")); 347 | } 348 | 349 | #[test] 350 | /// Test that escape codes are not printed to output 351 | fn test_terminal_monochrome_true() { 352 | MONOCHROME.store(true, std::sync::atomic::Ordering::SeqCst); 353 | let input = ( 354 | AllPortStatus::test_new(HashSet::new(), 10, 1, 10), 355 | std::time::Duration::from_millis(100), 356 | ); 357 | let result = get_scan_time(&input.0, input.1); 358 | 359 | assert!(!result.contains("\x1b[32m0 open\x1b[0m")); 360 | assert!(!result.contains("\x1b[31m10 closed\x1b[0m")); 361 | 362 | let input = AllPortStatus::test_new(HashSet::from([443, 6379, 32825]), 3, 32825, 0); 363 | let result = get_table(&input); 364 | assert!(result.is_some()); 365 | let result = result.unwrap(); 366 | 367 | assert!(!result.contains("\x1b[33m443")); 368 | assert!(!result.contains("https\x1b[0m")); 369 | 370 | assert!(result.contains("6379")); 371 | assert!(result.contains("redis")); 372 | 373 | assert!(!result.contains("\x1b[33m32825")); 374 | assert!(!result.contains("unknown\x1b[0m")); 375 | } 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /src/terminal/spinner.rs: -------------------------------------------------------------------------------- 1 | use crate::terminal::color::Color; 2 | use std::{ 3 | fmt, 4 | io::Write, 5 | sync::{atomic::AtomicBool, Arc}, 6 | }; 7 | 8 | use super::color::WIN_10; 9 | 10 | const FRAMES: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; 11 | 12 | const WIN_10_FRAMES: [char; 10] = ['|', '/', '-', '\\', '|', '/', '-', '\\', '|', '|']; 13 | 14 | #[derive(Debug, Default)] 15 | pub struct Spinner(Arc); 16 | 17 | enum Cursor { 18 | Show, 19 | Hide, 20 | } 21 | 22 | impl fmt::Display for Cursor { 23 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 24 | if WIN_10.load(std::sync::atomic::Ordering::SeqCst) { 25 | Ok(()) 26 | } else { 27 | let disp = match self { 28 | Self::Show => "h", 29 | Self::Hide => "l", 30 | }; 31 | write!(f, "\x1b[?25{disp}") 32 | } 33 | } 34 | } 35 | 36 | impl Spinner { 37 | /// Show the cursor on the terminal again 38 | pub fn show_cursor() { 39 | print!("{}", Cursor::Show); 40 | } 41 | 42 | /// Hide the cursor, so spinner line looks nicer 43 | fn hide_cursor() { 44 | print!("{}", Cursor::Hide); 45 | } 46 | 47 | /// Animate the loading icon until `run` is false 48 | /// Should this be converted to non-async, and spawned using std::thread? 49 | async fn spin(run: Arc) { 50 | while run.load(std::sync::atomic::Ordering::SeqCst) { 51 | let frames = if WIN_10.load(std::sync::atomic::Ordering::SeqCst) { 52 | WIN_10_FRAMES 53 | } else { 54 | FRAMES 55 | }; 56 | for i in frames { 57 | print!("{c}{i}{r} scanning ", c = Color::Red, r = Color::Reset); 58 | std::io::stdout().flush().ok(); 59 | print!("\r"); 60 | tokio::time::sleep(std::time::Duration::from_millis(75)).await; 61 | } 62 | } 63 | } 64 | 65 | pub fn new() -> Self { 66 | Self(Arc::new(AtomicBool::new(false))) 67 | } 68 | 69 | /// Print to stdout a spinner, with the text "scanning" 70 | /// Spawns into own thread 71 | pub fn start(&self) { 72 | if !self.0.swap(true, std::sync::atomic::Ordering::SeqCst) { 73 | Self::hide_cursor(); 74 | tokio::spawn(Self::spin(Arc::clone(&self.0))); 75 | } 76 | } 77 | 78 | /// Stop the spinner, and re-show the cursor 79 | pub fn stop(&self) { 80 | self.0.store(false, std::sync::atomic::Ordering::SeqCst); 81 | Self::show_cursor(); 82 | } 83 | } 84 | 85 | // todo test spinners somehow 86 | #[cfg(test)] 87 | mod tests { 88 | use super::Cursor; 89 | use crate::terminal::color::WIN_10; 90 | 91 | #[test] 92 | /// Cursor shown, hidden, but ignored when WIN_10 is true 93 | fn test_spinner_cursor() { 94 | assert_eq!( 95 | [27, 91, 63, 50, 53, 104], 96 | Cursor::Show.to_string().as_bytes() 97 | ); 98 | assert_eq!( 99 | [27, 91, 63, 50, 53, 108], 100 | Cursor::Hide.to_string().as_bytes() 101 | ); 102 | 103 | WIN_10.store(true, std::sync::atomic::Ordering::SeqCst); 104 | 105 | assert!(Cursor::Show.to_string().as_bytes().is_empty()); 106 | assert!(Cursor::Hide.to_string().as_bytes().is_empty()); 107 | } 108 | } 109 | --------------------------------------------------------------------------------