Compare commits
106 Commits
Author | SHA1 | Date | |
---|---|---|---|
6e59f0f5c0 | |||
b219ec2208 | |||
e70e5ac15c | |||
806606c9b9 | |||
ac3b501adf | |||
da02e04edf | |||
1a81adaabd | |||
5ae4187065 | |||
b7d7e405eb | |||
d1ab85cbb3 | |||
9cf4e433bf | |||
7c88ecd82a | |||
771bfe94e7 | |||
def467a27b | |||
7d087e39ef | |||
26e1d5142a | |||
d6be2e27b0 | |||
d6c0af35fa | |||
bc19c21949 | |||
dba1855eb6 | |||
db01586eb9 | |||
172fce2466 | |||
f7f009e0d4 | |||
e5111f0433 | |||
554a7d9647 | |||
76d30b7618 | |||
eee90810da | |||
5bf7d19bf7 | |||
2fa6da38c6 | |||
e11157fe44 | |||
28d99ecb77 | |||
d8f409c191 | |||
bcfa3be58b | |||
15744d3c4e | |||
8b2f541150 | |||
b52bb4b474 | |||
42f5d66fc4 | |||
54bb9549a1 | |||
95c3314400 | |||
695d047200 | |||
c308589d28 | |||
068177b09d | |||
4a299cf3cb | |||
e5c37b1801 | |||
5be0586790 | |||
f5e5297d47 | |||
718fada493 | |||
a42b1b48b5 | |||
5ec50975f3 | |||
ad222abb6a | |||
b29e13b162 | |||
9544823401 | |||
260f000304 | |||
d8044507ed | |||
b9380be999 | |||
1b9c354d69 | |||
a8f4ecf98f | |||
6350088d2a | |||
10ef1d0455 | |||
f709238e50 | |||
49940635d5 | |||
ec4121cbcf | |||
ea9a2572f9 | |||
cc0f1c40a6 | |||
9da04081e4 | |||
4ae0925043 | |||
4e862e784b | |||
cf8abfd4f0 | |||
93c4488b9b | |||
39493465c6 | |||
cab696e45b | |||
67682892ae | |||
5c13987686 | |||
97841e0973 | |||
c8ccde9d84 | |||
654a4c6b54 | |||
12b8793c19 | |||
24e861e5b4 | |||
c7d2b2c031 | |||
f08713bb45 | |||
ad0fa8c65a | |||
88a9bfc20d | |||
0248d6f253 | |||
8d1b302e70 | |||
34bee225d5 | |||
ccece078a2 | |||
a04151e537 | |||
d71346e763 | |||
817894b6ce | |||
bba219ddef | |||
9af53a5b58 | |||
4fcdeb8c3d | |||
5c7d2de902 | |||
f105cdc806 | |||
8f71c68d8c | |||
beb6680856 | |||
311852efe8 | |||
d3a507c3ff | |||
05bc8fb72c | |||
df861590c8 | |||
2ce69f4a8e | |||
7625866ca9 | |||
b88f52ba90 | |||
43c8f63ce6 | |||
b5eac3c54f | |||
fda63e4f95 |
66
.gitea/workflows/default_nottags.yaml
Normal file
66
.gitea/workflows/default_nottags.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Default (not tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm and npmci
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
|
||||
- name: Run npm prepare
|
||||
run: npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
124
.gitea/workflows/default_tags.yaml
Normal file
124
.gitea/workflows/default_tags.yaml
Normal file
@@ -0,0 +1,124 @@
|
||||
name: Default (tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
||||
|
||||
release:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm publish
|
||||
|
||||
metadata:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Code quality
|
||||
run: |
|
||||
npmci command npm install -g typescript
|
||||
npmci npm install
|
||||
|
||||
- name: Trigger
|
||||
run: npmci trigger
|
||||
|
||||
- name: Build docs and upload artifacts
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
pnpm install -g @git.zone/tsdoc
|
||||
npmci command tsdoc
|
||||
continue-on-error: true
|
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,5 +1,19 @@
|
||||
.nogit/
|
||||
node_modules/
|
||||
|
||||
# artifacts
|
||||
coverage/
|
||||
public/
|
||||
pages/
|
||||
|
||||
# installs
|
||||
node_modules/
|
||||
|
||||
# caches
|
||||
.yarn/
|
||||
.cache/
|
||||
.rpt2_cache
|
||||
|
||||
# builds
|
||||
dist/
|
||||
dist_*/
|
||||
|
||||
#------# custom
|
125
.gitlab-ci.yml
125
.gitlab-ci.yml
@@ -1,125 +0,0 @@
|
||||
# gitzone standard
|
||||
image: hosttoday/ht-docker-node:npmci
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- .npmci_cache/
|
||||
key: "$CI_BUILD_STAGE"
|
||||
|
||||
stages:
|
||||
- security
|
||||
- test
|
||||
- release
|
||||
- metadata
|
||||
|
||||
# ====================
|
||||
# security stage
|
||||
# ====================
|
||||
mirror:
|
||||
stage: security
|
||||
script:
|
||||
- npmci git mirror
|
||||
tags:
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
snyk:
|
||||
stage: security
|
||||
script:
|
||||
- npmci npm prepare
|
||||
- npmci command npm install -g snyk
|
||||
- npmci command npm install --ignore-scripts
|
||||
- npmci command snyk test
|
||||
tags:
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
# ====================
|
||||
# test stage
|
||||
# ====================
|
||||
|
||||
testLTS:
|
||||
stage: test
|
||||
script:
|
||||
- npmci npm prepare
|
||||
- npmci node install lts
|
||||
- npmci npm install
|
||||
- npmci npm test
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
testSTABLE:
|
||||
stage: test
|
||||
script:
|
||||
- npmci npm prepare
|
||||
- npmci node install stable
|
||||
- npmci npm install
|
||||
- npmci npm test
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
release:
|
||||
stage: release
|
||||
script:
|
||||
- npmci node install stable
|
||||
- npmci npm publish
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
# ====================
|
||||
# metadata stage
|
||||
# ====================
|
||||
codequality:
|
||||
stage: metadata
|
||||
image: docker:stable
|
||||
allow_failure: true
|
||||
services:
|
||||
- docker:stable-dind
|
||||
script:
|
||||
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
|
||||
- docker run
|
||||
--env SOURCE_CODE="$PWD"
|
||||
--volume "$PWD":/code
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock
|
||||
"registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
|
||||
artifacts:
|
||||
paths: [codeclimate.json]
|
||||
tags:
|
||||
- docker
|
||||
- priv
|
||||
|
||||
trigger:
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci trigger
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
pages:
|
||||
image: hosttoday/ht-docker-node:npmci
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci command npm install -g typedoc typescript
|
||||
- npmci npm prepare
|
||||
- npmci npm install
|
||||
- npmci command typedoc --module "commonjs" --target "ES2016" --out public/ ts/
|
||||
tags:
|
||||
- docker
|
||||
- notpriv
|
||||
only:
|
||||
- tags
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
paths:
|
||||
- public
|
||||
allow_failure: true
|
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "npm test",
|
||||
"name": "Run npm test",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
26
.vscode/settings.json
vendored
Normal file
26
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/npmextra.json"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"npmci": {
|
||||
"type": "object",
|
||||
"description": "settings for npmci"
|
||||
},
|
||||
"gitzone": {
|
||||
"type": "object",
|
||||
"description": "settings for gitzone",
|
||||
"properties": {
|
||||
"projectType": {
|
||||
"type": "string",
|
||||
"enum": ["website", "element", "service", "npm", "wcc"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
254
changelog.md
Normal file
254
changelog.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-09-12 - 4.3.0 - feat(smartnetwork)
|
||||
Add randomizable port search and switch DNS resolution to @push.rocks/smartdns; export smartdns and update docs
|
||||
|
||||
- findFreePort: added options.randomize (IFindFreePortOptions) to return a random available port within a range; retains default sequential search behavior
|
||||
- resolveDns: switched to @push.rocks/smartdns client with 'prefer-system' strategy and DoH fallback; now returns parsed A, AAAA and MX records
|
||||
- Exported smartdns from the internal plugins module and added @push.rocks/smartdns dependency
|
||||
- Documentation updates: readme clarifies random port selection, DNS resolution strategy and adds examples for random findFreePort usage
|
||||
- Bumped @git.zone/tstest dev dependency to ^2.3.7
|
||||
|
||||
## 2025-09-12 - 4.2.0 - feat(PublicIp)
|
||||
Add PublicIp service and refactor SmartNetwork to use it; remove public-ip dependency; update exports, docs and dependencies
|
||||
|
||||
- Add PublicIp class (ts/smartnetwork.classes.publicip.ts) implementing public IPv4/IPv6 lookup with multiple fallback services, timeouts and validation
|
||||
- Refactor SmartNetwork.getPublicIps to use the new PublicIp class and preserve caching behavior
|
||||
- Export PublicIp from package entry (ts/index.ts)
|
||||
- Remove public-ip from plugins/exports and stop using the public-ip package
|
||||
- Bump devDependencies and runtime dependency versions in package.json (@git.zone/tsbuild, @git.zone/tstest, @push.rocks/smartenv, systeminformation)
|
||||
- Improve README: expanded usage, examples, formatting and added emojis for clarity
|
||||
- Add project local settings file (.claude/settings.local.json) for CI/permissions configuration
|
||||
|
||||
## 2025-07-31 - 4.1.0 - feat(port-management)
|
||||
Add findFreePort method for automatic port discovery within a range
|
||||
|
||||
- Added new `findFreePort` method to SmartNetwork class that finds the first available port in a specified range
|
||||
- Added comprehensive tests for the new port finding functionality
|
||||
- Updated README documentation with usage examples for the new feature
|
||||
- Improved port management capabilities for dynamic port allocation scenarios
|
||||
|
||||
## 2025-05-19 - 4.0.2 - fix(tests)
|
||||
Update dev dependencies and refactor test assertions for improved clarity
|
||||
|
||||
- Bumped @git.zone/tsbuild version to ^2.5.1
|
||||
- Bumped @git.zone/tstest version to ^1.9.0
|
||||
- Updated npm test script to include the verbose flag
|
||||
- Replaced expectAsync assertions with resolves based assertions in test files
|
||||
|
||||
## 2025-05-03 - 4.0.1 - fix(formatting)
|
||||
Fix minor formatting issues and newline consistency across project files
|
||||
|
||||
- Ensure newline at end of package.json, errors.ts, logging.ts, and test files
|
||||
- Refine code block formatting in readme.md
|
||||
- Adjust whitespace and code style in smartnetwork classes and cloudflarespeed module
|
||||
- Minor commitinfo data format update
|
||||
|
||||
## 2025-04-28 - 4.0.0 - BREAKING CHANGE(smartnetwork)
|
||||
Enhance documentation and add configurable speed test options with plugin architecture improvements
|
||||
|
||||
- Expanded README with detailed examples for traceroute, speed test, ping, and remote port checks
|
||||
- Added optional parameters for getSpeed (parallelStreams and duration) to allow configurable testing modes
|
||||
- Included plugin architecture usage examples to show runtime registration and unregistration of plugins
|
||||
- Updated test suite to cover DNS resolution, endpoint health-check, caching behavior, and various network diagnostics
|
||||
- Removed legacy planning documentation from readme.plan.md
|
||||
|
||||
## 2025-04-28 - 3.0.5 - fix(core)
|
||||
Improve logging and error handling by introducing custom error classes and a global logging interface while refactoring network diagnostics methods.
|
||||
|
||||
- Added custom error classes (NetworkError, TimeoutError) for network operations.
|
||||
- Introduced a global logging interface to replace direct console logging.
|
||||
- Updated CloudflareSpeed and SmartNetwork classes to use getLogger for improved error reporting.
|
||||
- Disabled connection pooling in HTTP requests to prevent listener accumulation.
|
||||
|
||||
## 2025-04-28 - 3.0.4 - fix(ci/config)
|
||||
Improve CI workflows, update project configuration, and clean up code formatting
|
||||
|
||||
- Added new Gitea workflow files (default_nottags.yaml and default_tags.yaml) to replace GitLab CI
|
||||
- Updated package.json with new buildDocs script, revised homepage URL, bug tracking info, and pnpm overrides
|
||||
- Refined code formatting in TypeScript files, including improved error handling in Cloudflare speed tests and consistent callback structure
|
||||
- Enhanced tsconfig.json by adding baseUrl and paths for better module resolution
|
||||
- Introduced readme.plan.md outlining future improvements and feature enhancements
|
||||
|
||||
## 2025-04-28 - 3.0.3 - fix(deps)
|
||||
Update dependency namespaces and bump package versions in CI configuration and source imports
|
||||
|
||||
- Renamed dependency imports from '@pushrocks/...' to '@push.rocks/...' in package.json, test files, and source files
|
||||
- Updated devDependencies versions and package references (e.g., tsbuild, tsrun, tstest, smartenv, tapbundle, smartpromise, smartstring, public-ip, and @types packages)
|
||||
- Fixed CI command to install '@git.zone/tsdoc' instead of the misspelled '@gitzone/tsdoc'
|
||||
- Updated commit info file to reflect the correct package namespace
|
||||
|
||||
## 2024-05-29 - 3.0.2 - misc
|
||||
Various documentation, configuration, and organizational updates.
|
||||
- Updated project description.
|
||||
- Updated tsconfig configuration.
|
||||
- Updated npmextra.json (githost configuration) across multiple commits.
|
||||
- Switched to a new organizational scheme.
|
||||
|
||||
## 2022-10-22 - 3.0.1 - core
|
||||
Core fixes and maintenance applied.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2022-10-21 - 3.0.0 - core
|
||||
Core updates and fixes.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2022-03-24 - 2.0.14 - core
|
||||
Breaking changes introduced.
|
||||
- BREAKING CHANGE: Switched core to an ECMAScript Modules (ESM) approach.
|
||||
|
||||
## 2022-02-16 - 2.0.13 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2022-02-16 - 2.0.12 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2022-02-16 - 2.0.11 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2022-02-16 - 2.0.10 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2021-04-29 - 2.0.9 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2021-04-28 - 2.0.8 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2021-04-28 - 2.0.7 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2021-04-28 - 2.0.6 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2021-04-28 - 2.0.5 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2021-04-27 - 2.0.4 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2021-04-13 - 2.0.3 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2021-04-13 - 2.0.2 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2021-04-13 - 2.0.1 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2021-04-13 - 2.0.0 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2021-04-13 - 1.1.22 - core
|
||||
Breaking update in core functionality.
|
||||
- BREAKING CHANGE: Updated core functionality.
|
||||
|
||||
## 2020-08-13 - 1.1.21 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2020-08-13 - 1.1.20 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2020-08-12 - 1.1.19 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2020-08-12 - 1.1.18 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2019-11-23 - 1.1.17 - minor
|
||||
Release with no notable changes.
|
||||
- No significant changes.
|
||||
|
||||
## 2019-11-23 - 1.1.16 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2019-11-19 - 1.1.15 - security
|
||||
Security enhancement.
|
||||
- Added Snyk configuration for improved security.
|
||||
|
||||
## 2019-11-19 - 1.1.14 - dependencies
|
||||
Dependency update.
|
||||
- Updated dependencies.
|
||||
|
||||
## 2019-09-08 - 1.1.13 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2019-09-08 - 1.1.12 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2019-09-08 - 1.1.11 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2019-09-08 - 1.1.10 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2019-09-08 - 1.1.9 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2019-09-06 - 1.1.8 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2019-09-06 - 1.1.7 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2019-09-06 - 1.1.6 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2019-04-17 - 1.1.5 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2019-04-17 - 1.1.4 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2019-04-17 - 1.1.3 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2019-04-17 - 1.1.2 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2019-04-16 - 1.1.1 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2019-04-16 - 1.1.0 - core
|
||||
Core maintenance update.
|
||||
- Fixed core issues.
|
||||
|
||||
## 2018-07-17 - 1.0.2 - feature
|
||||
New feature added.
|
||||
- Added port checking functionality.
|
||||
|
||||
## 2017-12-12 - 1.0.1 - initial
|
||||
Initial commit.
|
||||
- Initial project setup.
|
23
license
Normal file
23
license
Normal file
@@ -0,0 +1,23 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Lossless GmbH
|
||||
Copyright (c) 2020 Tomás Arias
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
@@ -1,18 +1,32 @@
|
||||
{
|
||||
"npmci": {
|
||||
"npmGlobalTools": [
|
||||
"npmts"
|
||||
],
|
||||
"npmGlobalTools": [],
|
||||
"npmAccessLevel": "public"
|
||||
},
|
||||
"gitzone": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "gitlab.com",
|
||||
"gitscope": "pushrocks",
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "push.rocks",
|
||||
"gitrepo": "smartnetwork",
|
||||
"shortDescription": "network diagnostics",
|
||||
"npmPackagename": "@pushrocks/smartnetwork",
|
||||
"license": "MIT"
|
||||
"description": "A toolkit for network diagnostics including speed tests, port availability checks, and more.",
|
||||
"npmPackagename": "@push.rocks/smartnetwork",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"network diagnostics",
|
||||
"ping",
|
||||
"port check",
|
||||
"speed test",
|
||||
"network interface",
|
||||
"public IP retrieval",
|
||||
"Cloudflare speed test",
|
||||
"network performance",
|
||||
"network utility",
|
||||
"TypeScript"
|
||||
]
|
||||
}
|
||||
},
|
||||
"tsdoc": {
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
}
|
||||
}
|
1680
package-lock.json
generated
1680
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
74
package.json
74
package.json
@@ -1,25 +1,71 @@
|
||||
{
|
||||
"name": "@pushrocks/smartnetwork",
|
||||
"version": "1.1.1",
|
||||
"name": "@push.rocks/smartnetwork",
|
||||
"version": "4.3.0",
|
||||
"private": false,
|
||||
"description": "network diagnostics",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"description": "A toolkit for network diagnostics including speed tests, port availability checks, and more.",
|
||||
"exports": {
|
||||
".": "./dist_ts/index.js"
|
||||
},
|
||||
"type": "module",
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "(tsrun test/test.ts)",
|
||||
"build": "(npmts)"
|
||||
"test": "(tstest test/ --verbose)",
|
||||
"build": "(tsbuild tsfolders --allowimplicitany)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gitzone/tsrun": "^1.2.5",
|
||||
"@pushrocks/tapbundle": "^3.0.9"
|
||||
"@git.zone/tsbuild": "^2.6.8",
|
||||
"@git.zone/tsrun": "^1.2.39",
|
||||
"@git.zone/tstest": "^2.3.7",
|
||||
"@push.rocks/smartenv": "^5.0.13",
|
||||
"@types/node": "^22.15.19"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pushrocks/smartpromise": "^3.0.2",
|
||||
"@pushrocks/smartstring": "^3.0.10",
|
||||
"@types/portscanner": "^2.1.0",
|
||||
"portscanner": "^2.2.0",
|
||||
"speedtest-net": "^1.5.1"
|
||||
"@push.rocks/smartdns": "^7.6.1",
|
||||
"@push.rocks/smartping": "^1.0.7",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartstring": "^4.0.2",
|
||||
"isopen": "^1.3.0",
|
||||
"systeminformation": "^5.27.8"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
"ts_web/**/*",
|
||||
"dist/**/*",
|
||||
"dist_*/**/*",
|
||||
"dist_ts/**/*",
|
||||
"dist_ts_web/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
"readme.md"
|
||||
],
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
],
|
||||
"keywords": [
|
||||
"network diagnostics",
|
||||
"ping",
|
||||
"port check",
|
||||
"speed test",
|
||||
"network interface",
|
||||
"public IP retrieval",
|
||||
"Cloudflare speed test",
|
||||
"network performance",
|
||||
"network utility",
|
||||
"TypeScript"
|
||||
],
|
||||
"homepage": "https://code.foss.global/push.rocks/smartnetwork#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://code.foss.global/push.rocks/smartnetwork.git"
|
||||
},
|
||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6",
|
||||
"bugs": {
|
||||
"url": "https://code.foss.global/push.rocks/smartnetwork/issues"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {}
|
||||
}
|
||||
}
|
||||
|
9465
pnpm-lock.yaml
generated
Normal file
9465
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
73
readme.hints.md
Normal file
73
readme.hints.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Project Analysis
|
||||
|
||||
## Architecture Overview
|
||||
This is a comprehensive network diagnostics toolkit that provides various network-related utilities. The main entry point is the `SmartNetwork` class which orchestrates all functionality.
|
||||
|
||||
Key features:
|
||||
- Speed testing via Cloudflare (parallelizable with duration support)
|
||||
- Ping operations with statistics
|
||||
- Port availability checks (local and remote)
|
||||
- Network gateway discovery
|
||||
- Public IP retrieval
|
||||
- DNS resolution
|
||||
- HTTP endpoint health checks
|
||||
- Traceroute functionality (with fallback stub)
|
||||
|
||||
## Key Components
|
||||
|
||||
### SmartNetwork Class
|
||||
- Central orchestrator for all network operations
|
||||
- Supports caching via `cacheTtl` option for gateway and public IP lookups
|
||||
- Plugin architecture for extensibility
|
||||
|
||||
### CloudflareSpeed Class
|
||||
- Handles internet speed testing using Cloudflare's infrastructure
|
||||
- Supports parallel streams and customizable test duration
|
||||
- Measures both download and upload speeds using progressive chunk sizes
|
||||
- Includes latency measurements (jitter, median, average)
|
||||
|
||||
### Error Handling
|
||||
- Custom `NetworkError` and `TimeoutError` classes for better error context
|
||||
- Error codes follow Node.js conventions (ENOTSUP, EINVAL, ETIMEOUT)
|
||||
|
||||
### Logging
|
||||
- Global logger interface for consistent logging across the codebase
|
||||
- Replaceable logger implementation (defaults to console)
|
||||
- Used primarily for error reporting in speed tests
|
||||
|
||||
### Statistics Helpers
|
||||
- Utility functions for statistical calculations (average, median, quartile, jitter)
|
||||
- Used extensively by speed testing and ping operations
|
||||
|
||||
## Recent Changes (v4.0.0 - v4.0.1)
|
||||
- Added configurable speed test options (parallelStreams, duration)
|
||||
- Introduced plugin architecture for runtime extensibility
|
||||
- Enhanced error handling with custom error classes
|
||||
- Added global logging interface
|
||||
- Improved connection management by disabling HTTP connection pooling
|
||||
- Fixed memory leaks from listener accumulation
|
||||
- Minor formatting fixes for consistency
|
||||
|
||||
## Testing
|
||||
- Comprehensive test suite covering all major features
|
||||
- Tests run on both browser and node environments
|
||||
- Uses @push.rocks/tapbundle for testing with expectAsync
|
||||
- Performance tests for speed testing functionality
|
||||
- Edge case handling for network errors and timeouts
|
||||
|
||||
## Technical Details
|
||||
- ESM-only package (module type)
|
||||
- TypeScript with strict typing
|
||||
- Depends on external modules for specific functionality:
|
||||
- @push.rocks/smartping for ICMP operations
|
||||
- public-ip for external IP discovery
|
||||
- systeminformation for network interface details
|
||||
- isopen for remote port checking
|
||||
- Uses native Node.js modules for DNS, HTTP/HTTPS, and network operations
|
||||
|
||||
## Design Patterns
|
||||
- Factory pattern for plugin registration
|
||||
- Caching pattern with TTL for expensive operations
|
||||
- Promise-based async/await throughout
|
||||
- Deferred promises for complex async coordination
|
||||
- Error propagation with custom error types
|
431
readme.md
431
readme.md
@@ -1,32 +1,415 @@
|
||||
# @pushrocks/smartnetwork
|
||||
network diagnostics
|
||||
# @push.rocks/smartnetwork 🌐
|
||||
|
||||
## Availabililty and Links
|
||||
* [npmjs.org (npm package)](https://www.npmjs.com/package/@pushrocks/smartnetwork)
|
||||
* [gitlab.com (source)](https://gitlab.com/pushrocks/smartnetwork)
|
||||
* [github.com (source mirror)](https://github.com/pushrocks/smartnetwork)
|
||||
* [docs (typedoc)](https://pushrocks.gitlab.io/smartnetwork/)
|
||||
Comprehensive network diagnostics and utilities for Node.js applications
|
||||
|
||||
## Status for master
|
||||
[](https://gitlab.com/pushrocks/smartnetwork/commits/master)
|
||||
[](https://gitlab.com/pushrocks/smartnetwork/commits/master)
|
||||
[](https://www.npmjs.com/package/@pushrocks/smartnetwork)
|
||||
[](https://snyk.io/test/npm/@pushrocks/smartnetwork)
|
||||
[](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
||||
[](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
||||
[](https://prettier.io/)
|
||||
## 🚀 Install
|
||||
|
||||
## Usage
|
||||
To install `@push.rocks/smartnetwork`, run the following command in your terminal:
|
||||
|
||||
```typescript
|
||||
import * as smartnetwork from 'smartnetwork';
|
||||
testSmartNetwork = new smartnetwork.SmartNetwork();
|
||||
testSmartNetwork.getSpeed
|
||||
```bash
|
||||
pnpm install @push.rocks/smartnetwork --save
|
||||
```
|
||||
|
||||
For further information read the linked docs at the top of this readme.
|
||||
## 🎯 Overview
|
||||
|
||||
> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh)
|
||||
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy.html)
|
||||
**@push.rocks/smartnetwork** is your Swiss Army knife for network diagnostics in Node.js. Whether you're building network monitoring tools, implementing health checks, or just need to debug connectivity issues, this library has you covered with a clean, promise-based API.
|
||||
|
||||
[](https://maintainedby.lossless.com)
|
||||
### ✨ Key Features
|
||||
|
||||
- **🏎️ Speed Testing** - Measure download/upload speeds using Cloudflare's infrastructure
|
||||
- **🔌 Port Management** - Check local/remote port availability, find free ports (sequential or random)
|
||||
- **📡 Connectivity Testing** - Ping hosts, trace routes, check endpoints
|
||||
- **🌍 DNS Operations** - Resolve A, AAAA, and MX records with smart local/remote resolution
|
||||
- **🔍 Network Discovery** - Get network interfaces, gateways, public IPs
|
||||
- **⚡ Performance Caching** - Built-in caching for expensive operations
|
||||
- **🔧 Plugin Architecture** - Extend functionality with custom plugins
|
||||
- **📝 Full TypeScript Support** - Complete type definitions included
|
||||
|
||||
## 💻 Usage
|
||||
|
||||
### Basic Setup
|
||||
|
||||
First, import and initialize SmartNetwork:
|
||||
|
||||
```typescript
|
||||
import { SmartNetwork } from '@push.rocks/smartnetwork';
|
||||
|
||||
// Basic instance
|
||||
const network = new SmartNetwork();
|
||||
|
||||
// With caching enabled (60 seconds TTL)
|
||||
const cachedNetwork = new SmartNetwork({ cacheTtl: 60000 });
|
||||
```
|
||||
|
||||
### 🏎️ Network Speed Testing
|
||||
|
||||
Measure your network performance using Cloudflare's global infrastructure:
|
||||
|
||||
```typescript
|
||||
const speedTest = async () => {
|
||||
// Quick speed test
|
||||
const result = await network.getSpeed();
|
||||
console.log(`Download: ${result.downloadSpeed} Mbps`);
|
||||
console.log(`Upload: ${result.uploadSpeed} Mbps`);
|
||||
|
||||
// Advanced configuration
|
||||
const advancedResult = await network.getSpeed({
|
||||
parallelStreams: 3, // Number of concurrent connections
|
||||
duration: 5 // Test duration in seconds
|
||||
});
|
||||
console.log(`Download: ${advancedResult.downloadSpeed} Mbps`);
|
||||
console.log(`Upload: ${advancedResult.uploadSpeed} Mbps`);
|
||||
};
|
||||
```
|
||||
|
||||
### 🔌 Port Management
|
||||
|
||||
#### Check Local Port Availability
|
||||
|
||||
Verify if a port is available on your local machine (checks both IPv4 and IPv6):
|
||||
|
||||
```typescript
|
||||
const checkLocalPort = async (port: number) => {
|
||||
const isUnused = await network.isLocalPortUnused(port);
|
||||
if (isUnused) {
|
||||
console.log(`✅ Port ${port} is available`);
|
||||
} else {
|
||||
console.log(`❌ Port ${port} is in use`);
|
||||
}
|
||||
};
|
||||
|
||||
await checkLocalPort(8080);
|
||||
```
|
||||
|
||||
#### Find Free Port in Range
|
||||
|
||||
Automatically discover available ports:
|
||||
|
||||
```typescript
|
||||
const findFreePort = async () => {
|
||||
// Find a free port between 3000 and 3100 (sequential - returns first available)
|
||||
const freePort = await network.findFreePort(3000, 3100);
|
||||
|
||||
if (freePort) {
|
||||
console.log(`🎉 Found free port: ${freePort}`);
|
||||
} else {
|
||||
console.log('😢 No free ports available in range');
|
||||
}
|
||||
|
||||
// Find a random free port in range (useful to avoid port conflicts)
|
||||
const randomPort = await network.findFreePort(3000, 3100, { randomize: true });
|
||||
console.log(`🎲 Random free port: ${randomPort}`);
|
||||
};
|
||||
```
|
||||
|
||||
#### Check Remote Port Availability
|
||||
|
||||
Test if services are accessible on remote servers:
|
||||
|
||||
```typescript
|
||||
// Method 1: Using "host:port" syntax
|
||||
const isOpen1 = await network.isRemotePortAvailable('example.com:443');
|
||||
|
||||
// Method 2: Using separate host and port
|
||||
const isOpen2 = await network.isRemotePortAvailable('example.com', 443);
|
||||
|
||||
// Method 3: With advanced options
|
||||
const isOpen3 = await network.isRemotePortAvailable('example.com', {
|
||||
port: 443,
|
||||
protocol: 'tcp', // Only TCP is supported
|
||||
retries: 3, // Number of connection attempts
|
||||
timeout: 5000 // Timeout per attempt in ms
|
||||
});
|
||||
|
||||
// Note: UDP is not supported
|
||||
try {
|
||||
await network.isRemotePortAvailable('example.com', {
|
||||
port: 53,
|
||||
protocol: 'udp'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('UDP not supported:', e.code); // ENOTSUP
|
||||
}
|
||||
```
|
||||
|
||||
### 📡 Network Connectivity
|
||||
|
||||
#### Ping Operations
|
||||
|
||||
Test connectivity and measure latency:
|
||||
|
||||
```typescript
|
||||
// Simple ping
|
||||
const pingResult = await network.ping('google.com');
|
||||
console.log(`Host alive: ${pingResult.alive}`);
|
||||
console.log(`RTT: ${pingResult.time} ms`);
|
||||
|
||||
// Detailed ping statistics
|
||||
const pingStats = await network.ping('google.com', {
|
||||
count: 5, // Number of pings
|
||||
timeout: 1000 // Timeout per ping in ms
|
||||
});
|
||||
|
||||
console.log(`📊 Ping Statistics:`);
|
||||
console.log(` Packet loss: ${pingStats.packetLoss}%`);
|
||||
console.log(` Min: ${pingStats.min} ms`);
|
||||
console.log(` Max: ${pingStats.max} ms`);
|
||||
console.log(` Avg: ${pingStats.avg.toFixed(2)} ms`);
|
||||
console.log(` Stddev: ${pingStats.stddev.toFixed(2)} ms`);
|
||||
```
|
||||
|
||||
#### Traceroute
|
||||
|
||||
Analyze network paths hop-by-hop:
|
||||
|
||||
```typescript
|
||||
const hops = await network.traceroute('google.com', {
|
||||
maxHops: 10, // Maximum number of hops
|
||||
timeout: 5000 // Timeout in ms
|
||||
});
|
||||
|
||||
console.log('🛤️ Route to destination:');
|
||||
hops.forEach(hop => {
|
||||
const rtt = hop.rtt === null ? '*' : `${hop.rtt} ms`;
|
||||
console.log(` ${hop.ttl}\t${hop.ip}\t${rtt}`);
|
||||
});
|
||||
```
|
||||
|
||||
*Note: Falls back to a single-hop stub if the `traceroute` binary is unavailable.*
|
||||
|
||||
### 🌍 DNS Operations
|
||||
|
||||
Resolve various DNS record types using @push.rocks/smartdns with intelligent resolution strategy:
|
||||
|
||||
```typescript
|
||||
const dnsRecords = await network.resolveDns('example.com');
|
||||
|
||||
console.log('🔍 DNS Records:');
|
||||
console.log(' A records:', dnsRecords.A); // IPv4 addresses
|
||||
console.log(' AAAA records:', dnsRecords.AAAA); // IPv6 addresses
|
||||
|
||||
// MX records include priority
|
||||
dnsRecords.MX.forEach(mx => {
|
||||
console.log(` 📧 Mail server: ${mx.exchange} (priority: ${mx.priority})`);
|
||||
});
|
||||
|
||||
// Properly handles local hostnames (localhost, etc.)
|
||||
const localDns = await network.resolveDns('localhost');
|
||||
console.log(' Localhost:', localDns.A); // Returns ['127.0.0.1']
|
||||
```
|
||||
|
||||
*DNS resolution uses a `prefer-system` strategy: tries system resolver first (respects /etc/hosts and local DNS), with automatic fallback to Cloudflare DoH for external domains.*
|
||||
|
||||
### 🏥 HTTP/HTTPS Health Checks
|
||||
|
||||
Monitor endpoint availability and response times:
|
||||
|
||||
```typescript
|
||||
const health = await network.checkEndpoint('https://api.example.com/health', {
|
||||
timeout: 5000 // Request timeout in ms
|
||||
});
|
||||
|
||||
console.log(`🩺 Endpoint Health:`);
|
||||
console.log(` Status: ${health.status}`);
|
||||
console.log(` RTT: ${health.rtt} ms`);
|
||||
console.log(` Headers:`, health.headers);
|
||||
```
|
||||
|
||||
### 🖥️ Network Interface Information
|
||||
|
||||
#### Get All Network Interfaces
|
||||
|
||||
List all network adapters on the system:
|
||||
|
||||
```typescript
|
||||
const gateways = await network.getGateways();
|
||||
|
||||
Object.entries(gateways).forEach(([name, interfaces]) => {
|
||||
console.log(`🔌 Interface: ${name}`);
|
||||
interfaces.forEach(iface => {
|
||||
console.log(` ${iface.family}: ${iface.address}`);
|
||||
console.log(` Netmask: ${iface.netmask}`);
|
||||
console.log(` MAC: ${iface.mac}`);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### Get Default Gateway
|
||||
|
||||
Retrieve the primary network interface:
|
||||
|
||||
```typescript
|
||||
const defaultGateway = await network.getDefaultGateway();
|
||||
|
||||
if (defaultGateway) {
|
||||
console.log('🌐 Default Gateway:');
|
||||
console.log(' IPv4:', defaultGateway.ipv4.address);
|
||||
console.log(' IPv6:', defaultGateway.ipv6.address);
|
||||
}
|
||||
```
|
||||
|
||||
### 🌎 Public IP Discovery
|
||||
|
||||
Discover your public-facing IP addresses:
|
||||
|
||||
```typescript
|
||||
const publicIps = await network.getPublicIps();
|
||||
|
||||
console.log(`🌍 Public IPs:`);
|
||||
console.log(` IPv4: ${publicIps.v4 || 'Not available'}`);
|
||||
console.log(` IPv6: ${publicIps.v6 || 'Not available'}`);
|
||||
```
|
||||
|
||||
### ⚡ Performance Caching
|
||||
|
||||
Reduce network calls with built-in caching:
|
||||
|
||||
```typescript
|
||||
// Create instance with 60-second cache TTL
|
||||
const cachedNetwork = new SmartNetwork({ cacheTtl: 60000 });
|
||||
|
||||
// First call fetches from network
|
||||
const gateways1 = await cachedNetwork.getGateways();
|
||||
const publicIps1 = await cachedNetwork.getPublicIps();
|
||||
|
||||
// Subsequent calls within 60 seconds use cache
|
||||
const gateways2 = await cachedNetwork.getGateways(); // From cache ⚡
|
||||
const publicIps2 = await cachedNetwork.getPublicIps(); // From cache ⚡
|
||||
```
|
||||
|
||||
### 🔧 Plugin Architecture
|
||||
|
||||
Extend SmartNetwork with custom functionality:
|
||||
|
||||
```typescript
|
||||
// Define your plugin
|
||||
class CustomNetworkPlugin {
|
||||
constructor(private smartNetwork: SmartNetwork) {}
|
||||
|
||||
async customMethod() {
|
||||
// Your custom network logic here
|
||||
return 'Custom result';
|
||||
}
|
||||
}
|
||||
|
||||
// Register the plugin
|
||||
SmartNetwork.registerPlugin('customPlugin', CustomNetworkPlugin);
|
||||
|
||||
// Use the plugin
|
||||
const network = new SmartNetwork();
|
||||
const PluginClass = SmartNetwork.pluginsRegistry.get('customPlugin');
|
||||
const plugin = new PluginClass(network);
|
||||
await plugin.customMethod();
|
||||
|
||||
// Clean up when done
|
||||
SmartNetwork.unregisterPlugin('customPlugin');
|
||||
```
|
||||
|
||||
### 🚨 Error Handling
|
||||
|
||||
Handle network errors gracefully with custom error types:
|
||||
|
||||
```typescript
|
||||
import { NetworkError } from '@push.rocks/smartnetwork';
|
||||
|
||||
try {
|
||||
await network.isRemotePortAvailable('example.com', { protocol: 'udp' });
|
||||
} catch (error) {
|
||||
if (error instanceof NetworkError) {
|
||||
console.error(`❌ Network error: ${error.message}`);
|
||||
console.error(` Error code: ${error.code}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 📚 TypeScript Support
|
||||
|
||||
This package is written in TypeScript and provides comprehensive type definitions:
|
||||
|
||||
```typescript
|
||||
interface SmartNetworkOptions {
|
||||
cacheTtl?: number; // Cache TTL in milliseconds
|
||||
}
|
||||
|
||||
interface IFindFreePortOptions {
|
||||
randomize?: boolean; // If true, returns a random free port instead of the first one
|
||||
}
|
||||
|
||||
interface Hop {
|
||||
ttl: number; // Time to live
|
||||
ip: string; // IP address of the hop
|
||||
rtt: number | null; // Round trip time in ms
|
||||
}
|
||||
|
||||
// ... and many more types for complete type safety
|
||||
```
|
||||
|
||||
## 🛠️ Advanced Examples
|
||||
|
||||
### Building a Network Monitor
|
||||
|
||||
```typescript
|
||||
const monitorNetwork = async () => {
|
||||
const network = new SmartNetwork({ cacheTtl: 30000 });
|
||||
|
||||
// Check critical services
|
||||
const services = [
|
||||
{ name: 'Web Server', host: 'example.com', port: 443 },
|
||||
{ name: 'Database', host: 'db.internal', port: 5432 },
|
||||
{ name: 'Cache', host: 'redis.internal', port: 6379 }
|
||||
];
|
||||
|
||||
for (const service of services) {
|
||||
const isUp = await network.isRemotePortAvailable(service.host, service.port);
|
||||
console.log(`${service.name}: ${isUp ? '✅ UP' : '❌ DOWN'}`);
|
||||
}
|
||||
|
||||
// Check internet connectivity
|
||||
const ping = await network.ping('8.8.8.8');
|
||||
console.log(`Internet: ${ping.alive ? '✅ Connected' : '❌ Disconnected'}`);
|
||||
|
||||
// Measure network performance
|
||||
const speed = await network.getSpeed();
|
||||
console.log(`Speed: ⬇️ ${speed.downloadSpeed} Mbps / ⬆️ ${speed.uploadSpeed} Mbps`);
|
||||
};
|
||||
|
||||
// Run monitor every minute
|
||||
setInterval(monitorNetwork, 60000);
|
||||
```
|
||||
|
||||
### Service Discovery
|
||||
|
||||
```typescript
|
||||
const discoverServices = async () => {
|
||||
const network = new SmartNetwork();
|
||||
const commonPorts = [22, 80, 443, 3000, 3306, 5432, 6379, 8080, 9200];
|
||||
|
||||
console.log('🔍 Scanning local services...');
|
||||
|
||||
for (const port of commonPorts) {
|
||||
const isUsed = !(await network.isLocalPortUnused(port));
|
||||
if (isUsed) {
|
||||
console.log(` Port ${port}: In use (possible service running)`);
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
255
test/test.features.ts
Normal file
255
test/test.features.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { SmartNetwork, NetworkError } from '../ts/index.js';
|
||||
import * as net from 'net';
|
||||
import type { AddressInfo } from 'net';
|
||||
|
||||
// DNS resolution
|
||||
tap.test('resolveDns should return A records for localhost', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const res = await sn.resolveDns('localhost');
|
||||
expect(res.A.length).toBeGreaterThan(0);
|
||||
expect(Array.isArray(res.A)).toBeTrue();
|
||||
expect(Array.isArray(res.AAAA)).toBeTrue();
|
||||
expect(Array.isArray(res.MX)).toBeTrue();
|
||||
});
|
||||
|
||||
// DNS resolution edge cases and MX records
|
||||
tap.test('resolveDns should handle non-existent domains', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const res = await sn.resolveDns('no.such.domain.invalid');
|
||||
expect(Array.isArray(res.A)).toBeTrue();
|
||||
expect(Array.isArray(res.AAAA)).toBeTrue();
|
||||
expect(Array.isArray(res.MX)).toBeTrue();
|
||||
expect(res.A.length).toEqual(0);
|
||||
expect(res.AAAA.length).toEqual(0);
|
||||
expect(res.MX.length).toEqual(0);
|
||||
});
|
||||
|
||||
tap.test('resolveDns MX records for google.com', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const res = await sn.resolveDns('google.com');
|
||||
expect(Array.isArray(res.MX)).toBeTrue();
|
||||
if (res.MX.length > 0) {
|
||||
expect(typeof res.MX[0].exchange).toEqual('string');
|
||||
expect(typeof res.MX[0].priority).toEqual('number');
|
||||
}
|
||||
});
|
||||
|
||||
// HTTP endpoint health-check
|
||||
tap.test('checkEndpoint should return status and headers', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const result = await sn.checkEndpoint('https://example.com');
|
||||
expect(result.status).toEqual(200);
|
||||
expect(typeof result.rtt).toEqual('number');
|
||||
expect(typeof result.headers).toEqual('object');
|
||||
expect(result.headers).toHaveProperty('content-type');
|
||||
});
|
||||
|
||||
// Traceroute stub
|
||||
tap.test('traceroute should return at least one hop', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const hops = await sn.traceroute('127.0.0.1');
|
||||
expect(Array.isArray(hops)).toBeTrue();
|
||||
expect(hops.length).toBeGreaterThanOrEqual(1);
|
||||
const hop = hops[0];
|
||||
expect(typeof hop.ttl).toEqual('number');
|
||||
expect(typeof hop.ip).toEqual('string');
|
||||
expect(hop.rtt === null || typeof hop.rtt === 'number').toBeTrue();
|
||||
});
|
||||
// Traceroute fallback stub ensures consistent output when binary missing
|
||||
tap.test('traceroute fallback stub returns a single-hop stub', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const hops = await sn.traceroute('example.com', { maxHops: 5 });
|
||||
expect(Array.isArray(hops)).toBeTrue();
|
||||
expect(hops).array.toHaveLength(1);
|
||||
expect(hops[0]).toEqual({ ttl: 1, ip: 'example.com', rtt: null });
|
||||
});
|
||||
|
||||
// getSpeed options
|
||||
tap.test('getSpeed should accept options and return speeds', async () => {
|
||||
const opts = { parallelStreams: 2, duration: 1 };
|
||||
const sn = new SmartNetwork();
|
||||
const result = await sn.getSpeed(opts);
|
||||
expect(typeof result.downloadSpeed).toEqual('string');
|
||||
expect(typeof result.uploadSpeed).toEqual('string');
|
||||
expect(parseFloat(result.downloadSpeed)).toBeGreaterThan(0);
|
||||
expect(parseFloat(result.uploadSpeed)).toBeGreaterThan(0);
|
||||
expect(parseFloat(result.downloadSpeed)).toBeGreaterThan(0);
|
||||
expect(parseFloat(result.uploadSpeed)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Ping multiple count
|
||||
tap.test('ping with count > 1 should return stats', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const stats = await sn.ping('127.0.0.1', { count: 3 });
|
||||
expect(stats.count).toEqual(3);
|
||||
expect(Array.isArray(stats.times)).toBeTrue();
|
||||
expect(stats.times.length).toEqual(3);
|
||||
expect(typeof stats.min).toEqual('number');
|
||||
expect(typeof stats.max).toEqual('number');
|
||||
expect(typeof stats.avg).toEqual('number');
|
||||
expect(typeof stats.stddev).toEqual('number');
|
||||
expect(typeof stats.packetLoss).toEqual('number');
|
||||
expect(typeof stats.alive).toEqual('boolean');
|
||||
});
|
||||
|
||||
// Remote port UDP not supported
|
||||
// Remote port UDP not supported
|
||||
tap.test('isRemotePortAvailable should throw on UDP', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
// should throw NetworkError with code ENOTSUP when protocol is UDP
|
||||
try {
|
||||
await sn.isRemotePortAvailable('example.com', { protocol: 'udp' });
|
||||
// If no error is thrown, the test should fail
|
||||
throw new Error('Expected isRemotePortAvailable to throw for UDP');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('ENOTSUP');
|
||||
}
|
||||
});
|
||||
|
||||
// Plugin registry
|
||||
tap.test('should register and unregister plugin', async () => {
|
||||
class Dummy {}
|
||||
SmartNetwork.registerPlugin('dummy', Dummy);
|
||||
expect(SmartNetwork.pluginsRegistry.has('dummy')).toBeTrue();
|
||||
SmartNetwork.unregisterPlugin('dummy');
|
||||
expect(SmartNetwork.pluginsRegistry.has('dummy')).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('getGateways should respect cacheTtl', async () => {
|
||||
const sn = new SmartNetwork({ cacheTtl: 1000 });
|
||||
const first = await sn.getGateways();
|
||||
const second = await sn.getGateways();
|
||||
expect(first).toEqual(second);
|
||||
});
|
||||
|
||||
// Remote port checks: missing port should error
|
||||
tap.test('isRemotePortAvailable should require a port', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
try {
|
||||
await sn.isRemotePortAvailable('example.com');
|
||||
throw new Error('Expected error when port is not specified');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('EINVAL');
|
||||
}
|
||||
});
|
||||
|
||||
// Remote port checks: detect open TCP port on example.com
|
||||
tap.test('isRemotePortAvailable should detect open TCP port via string target', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const open = await sn.isRemotePortAvailable('example.com:80');
|
||||
expect(open).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable should detect open TCP port via numeric arg', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const open = await sn.isRemotePortAvailable('example.com', 80);
|
||||
expect(open).toBeTrue();
|
||||
});
|
||||
// Caching public IPs
|
||||
tap.test('getPublicIps should respect cacheTtl', async () => {
|
||||
const sn = new SmartNetwork({ cacheTtl: 1000 });
|
||||
const first = await sn.getPublicIps();
|
||||
const second = await sn.getPublicIps();
|
||||
expect(first).toEqual(second);
|
||||
});
|
||||
|
||||
// Local port usage detection
|
||||
tap.test('isLocalPortUnused should detect used local port', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
// start a server on a random port
|
||||
const server = net.createServer();
|
||||
await new Promise<void>((res) => server.listen(0, res));
|
||||
const addr = server.address() as AddressInfo;
|
||||
// port is now in use
|
||||
const inUse = await sn.isLocalPortUnused(addr.port);
|
||||
expect(inUse).toBeFalse();
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
});
|
||||
|
||||
// findFreePort tests
|
||||
tap.test('findFreePort should find an available port in range', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const freePort = await sn.findFreePort(49152, 49200);
|
||||
expect(freePort).toBeGreaterThanOrEqual(49152);
|
||||
expect(freePort).toBeLessThanOrEqual(49200);
|
||||
|
||||
// Verify the port is actually free
|
||||
const isUnused = await sn.isLocalPortUnused(freePort);
|
||||
expect(isUnused).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('findFreePort should return null when all ports are occupied', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
// Create servers to occupy a small range
|
||||
const servers = [];
|
||||
const startPort = 49300;
|
||||
const endPort = 49302;
|
||||
|
||||
for (let port = startPort; port <= endPort; port++) {
|
||||
const server = net.createServer();
|
||||
await new Promise<void>((res) => server.listen(port, res));
|
||||
servers.push(server);
|
||||
}
|
||||
|
||||
// Now all ports in range should be occupied
|
||||
const freePort = await sn.findFreePort(startPort, endPort);
|
||||
expect(freePort).toBeNull();
|
||||
|
||||
// Clean up servers
|
||||
await Promise.all(servers.map(s => new Promise<void>((res) => s.close(() => res()))));
|
||||
});
|
||||
|
||||
tap.test('findFreePort should validate port range', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Test invalid port numbers
|
||||
try {
|
||||
await sn.findFreePort(0, 100);
|
||||
throw new Error('Expected error for port < 1');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('EINVAL');
|
||||
}
|
||||
|
||||
try {
|
||||
await sn.findFreePort(100, 70000);
|
||||
throw new Error('Expected error for port > 65535');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('EINVAL');
|
||||
}
|
||||
|
||||
// Test startPort > endPort
|
||||
try {
|
||||
await sn.findFreePort(200, 100);
|
||||
throw new Error('Expected error for startPort > endPort');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('EINVAL');
|
||||
}
|
||||
});
|
||||
|
||||
// Real traceroute integration test (skipped if `traceroute` binary is unavailable)
|
||||
tap.test('traceroute real integration against google.com', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
// detect traceroute binary
|
||||
const { spawnSync } = await import('child_process');
|
||||
const probe = spawnSync('traceroute', ['-h']);
|
||||
if (probe.error || probe.status !== 0) {
|
||||
// Skip real integration when traceroute is not installed
|
||||
return;
|
||||
}
|
||||
const hops = await sn.traceroute('google.com', { maxHops: 5, timeout: 5000 });
|
||||
expect(Array.isArray(hops)).toBeTrue();
|
||||
expect(hops.length).toBeGreaterThan(1);
|
||||
for (const hop of hops) {
|
||||
expect(typeof hop.ttl).toEqual('number');
|
||||
expect(typeof hop.ip).toEqual('string');
|
||||
expect(hop.rtt === null || typeof hop.rtt === 'number').toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
tap.start();
|
30
test/test.ping.ts
Normal file
30
test/test.ping.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
|
||||
import * as smartnetwork from '../ts/index.js';
|
||||
|
||||
let testSmartnetwork: smartnetwork.SmartNetwork;
|
||||
|
||||
tap.test('should create a vlid instance of SmartNetwork', async () => {
|
||||
testSmartnetwork = new smartnetwork.SmartNetwork();
|
||||
expect(testSmartnetwork).toBeInstanceOf(smartnetwork.SmartNetwork);
|
||||
});
|
||||
|
||||
tap.test('should send a ping to Google', async () => {
|
||||
const res = await testSmartnetwork.ping('google.com');
|
||||
console.log(res);
|
||||
// verify basic ping response properties
|
||||
expect(res.alive).toBeTrue();
|
||||
expect(res.time).toBeTypeofNumber();
|
||||
expect(res.output).toBeTypeofString();
|
||||
expect(res.output).toMatch(/PING google\.com/);
|
||||
});
|
||||
|
||||
tap.test('should state when a ping is not alive ', async () => {
|
||||
await expect(testSmartnetwork.ping('notthere.lossless.com')).resolves.property('alive').toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('should send a ping to an IP', async () => {
|
||||
await expect(testSmartnetwork.ping('192.168.186.999')).resolves.property('alive').toBeFalse();
|
||||
});
|
||||
|
||||
tap.start();
|
392
test/test.ports.ts
Normal file
392
test/test.ports.ts
Normal file
@@ -0,0 +1,392 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { SmartNetwork, NetworkError } from '../ts/index.js';
|
||||
import * as net from 'net';
|
||||
import type { AddressInfo } from 'net';
|
||||
|
||||
// Helper to create a server on a specific port
|
||||
const createServerOnPort = async (port: number): Promise<net.Server> => {
|
||||
const server = net.createServer();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(port, () => {
|
||||
server.removeListener('error', reject);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return server;
|
||||
};
|
||||
|
||||
// Helper to clean up servers
|
||||
const cleanupServers = async (servers: net.Server[]): Promise<void> => {
|
||||
await Promise.all(servers.map(s => new Promise<void>((res) => s.close(() => res()))));
|
||||
};
|
||||
|
||||
// ========= isLocalPortUnused Tests =========
|
||||
|
||||
tap.test('isLocalPortUnused - should detect free port correctly', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
// Port 0 lets the OS assign a free port, we'll use a high range instead
|
||||
const result = await sn.isLocalPortUnused(54321);
|
||||
expect(typeof result).toEqual('boolean');
|
||||
// Most likely this high port is free, but we can't guarantee it
|
||||
});
|
||||
|
||||
tap.test('isLocalPortUnused - should detect occupied port', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const server = net.createServer();
|
||||
await new Promise<void>((res) => server.listen(0, res));
|
||||
const addr = server.address() as AddressInfo;
|
||||
|
||||
const isUnused = await sn.isLocalPortUnused(addr.port);
|
||||
expect(isUnused).toBeFalse();
|
||||
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
});
|
||||
|
||||
tap.test('isLocalPortUnused - should handle multiple simultaneous checks', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const ports = [55001, 55002, 55003, 55004, 55005];
|
||||
|
||||
// Check all ports simultaneously
|
||||
const results = await Promise.all(
|
||||
ports.map(port => sn.isLocalPortUnused(port))
|
||||
);
|
||||
|
||||
// All should likely be free
|
||||
results.forEach(result => {
|
||||
expect(typeof result).toEqual('boolean');
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('isLocalPortUnused - should work with IPv6 loopback', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const server = net.createServer();
|
||||
|
||||
// Explicitly bind to IPv6
|
||||
await new Promise<void>((res) => server.listen(55100, '::', res));
|
||||
const addr = server.address() as AddressInfo;
|
||||
|
||||
const isUnused = await sn.isLocalPortUnused(addr.port);
|
||||
expect(isUnused).toBeFalse();
|
||||
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
});
|
||||
|
||||
tap.test('isLocalPortUnused - boundary port numbers', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Test port 1 (usually requires root)
|
||||
const port1Result = await sn.isLocalPortUnused(1);
|
||||
expect(typeof port1Result).toEqual('boolean');
|
||||
|
||||
// Test port 65535
|
||||
const port65535Result = await sn.isLocalPortUnused(65535);
|
||||
expect(typeof port65535Result).toEqual('boolean');
|
||||
});
|
||||
|
||||
// ========= findFreePort Tests =========
|
||||
|
||||
tap.test('findFreePort - should find free port in small range', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const freePort = await sn.findFreePort(50000, 50010);
|
||||
|
||||
expect(freePort).not.toBeNull();
|
||||
expect(freePort).toBeGreaterThanOrEqual(50000);
|
||||
expect(freePort).toBeLessThanOrEqual(50010);
|
||||
|
||||
// Verify the port is actually free
|
||||
if (freePort !== null) {
|
||||
const isUnused = await sn.isLocalPortUnused(freePort);
|
||||
expect(isUnused).toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('findFreePort - should find first available port', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const servers = [];
|
||||
|
||||
// Occupy ports 50100 and 50101
|
||||
servers.push(await createServerOnPort(50100));
|
||||
servers.push(await createServerOnPort(50101));
|
||||
|
||||
// Port 50102 should be free
|
||||
const freePort = await sn.findFreePort(50100, 50105);
|
||||
expect(freePort).toEqual(50102);
|
||||
|
||||
await cleanupServers(servers);
|
||||
});
|
||||
|
||||
tap.test('findFreePort - should handle fully occupied range', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const servers = [];
|
||||
const startPort = 50200;
|
||||
const endPort = 50202;
|
||||
|
||||
// Occupy all ports in range
|
||||
for (let port = startPort; port <= endPort; port++) {
|
||||
servers.push(await createServerOnPort(port));
|
||||
}
|
||||
|
||||
const freePort = await sn.findFreePort(startPort, endPort);
|
||||
expect(freePort).toBeNull();
|
||||
|
||||
await cleanupServers(servers);
|
||||
});
|
||||
|
||||
tap.test('findFreePort - should validate port boundaries', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Test port < 1
|
||||
try {
|
||||
await sn.findFreePort(0, 100);
|
||||
throw new Error('Should have thrown for port < 1');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('EINVAL');
|
||||
expect(err.message).toContain('between 1 and 65535');
|
||||
}
|
||||
|
||||
// Test port > 65535
|
||||
try {
|
||||
await sn.findFreePort(100, 70000);
|
||||
throw new Error('Should have thrown for port > 65535');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('EINVAL');
|
||||
}
|
||||
|
||||
// Test negative ports
|
||||
try {
|
||||
await sn.findFreePort(-100, 100);
|
||||
throw new Error('Should have thrown for negative port');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('EINVAL');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('findFreePort - should validate range order', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
try {
|
||||
await sn.findFreePort(200, 100);
|
||||
throw new Error('Should have thrown for startPort > endPort');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('EINVAL');
|
||||
expect(err.message).toContain('less than or equal to end port');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('findFreePort - should handle single port range', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Test when start and end are the same
|
||||
const freePort = await sn.findFreePort(50300, 50300);
|
||||
// Should either be 50300 or null
|
||||
expect(freePort === 50300 || freePort === null).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('findFreePort - should work with large ranges', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Test with a large range
|
||||
const freePort = await sn.findFreePort(40000, 50000);
|
||||
expect(freePort).not.toBeNull();
|
||||
expect(freePort).toBeGreaterThanOrEqual(40000);
|
||||
expect(freePort).toBeLessThanOrEqual(50000);
|
||||
});
|
||||
|
||||
tap.test('findFreePort - should handle intermittent occupied ports', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const servers = [];
|
||||
|
||||
// Occupy every other port
|
||||
servers.push(await createServerOnPort(50400));
|
||||
servers.push(await createServerOnPort(50402));
|
||||
servers.push(await createServerOnPort(50404));
|
||||
|
||||
// Should find 50401, 50403, or 50405
|
||||
const freePort = await sn.findFreePort(50400, 50405);
|
||||
expect([50401, 50403, 50405]).toContain(freePort);
|
||||
|
||||
await cleanupServers(servers);
|
||||
});
|
||||
|
||||
// ========= isRemotePortAvailable Tests =========
|
||||
|
||||
tap.test('isRemotePortAvailable - should detect open HTTP port', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Test with string format
|
||||
const open1 = await sn.isRemotePortAvailable('example.com:80');
|
||||
expect(open1).toBeTrue();
|
||||
|
||||
// Test with separate parameters
|
||||
const open2 = await sn.isRemotePortAvailable('example.com', 80);
|
||||
expect(open2).toBeTrue();
|
||||
|
||||
// Test with options object
|
||||
const open3 = await sn.isRemotePortAvailable('example.com', { port: 80 });
|
||||
expect(open3).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable - should detect closed port', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Port 12345 is likely closed on example.com
|
||||
const closed = await sn.isRemotePortAvailable('example.com', 12345);
|
||||
expect(closed).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable - should handle retries', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Test with retries
|
||||
const result = await sn.isRemotePortAvailable('example.com', {
|
||||
port: 80,
|
||||
retries: 3,
|
||||
timeout: 1000
|
||||
});
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable - should reject UDP protocol', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
try {
|
||||
await sn.isRemotePortAvailable('example.com', {
|
||||
port: 53,
|
||||
protocol: 'udp'
|
||||
});
|
||||
throw new Error('Should have thrown for UDP protocol');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('ENOTSUP');
|
||||
expect(err.message).toContain('UDP port check not supported');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable - should require port specification', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
try {
|
||||
await sn.isRemotePortAvailable('example.com');
|
||||
throw new Error('Should have thrown for missing port');
|
||||
} catch (err: any) {
|
||||
expect(err).toBeInstanceOf(NetworkError);
|
||||
expect(err.code).toEqual('EINVAL');
|
||||
expect(err.message).toContain('Port not specified');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable - should parse port from host:port string', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Valid formats
|
||||
const result1 = await sn.isRemotePortAvailable('example.com:443');
|
||||
expect(result1).toBeTrue();
|
||||
|
||||
// With options overriding the string port
|
||||
const result2 = await sn.isRemotePortAvailable('example.com:8080', { port: 80 });
|
||||
expect(result2).toBeTrue(); // Should use port 80 from options, not 8080
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable - should handle localhost', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const server = net.createServer();
|
||||
|
||||
// Start a local server
|
||||
await new Promise<void>((res) => server.listen(51000, 'localhost', res));
|
||||
|
||||
// Should detect it as open
|
||||
const isOpen = await sn.isRemotePortAvailable('localhost', 51000);
|
||||
expect(isOpen).toBeTrue();
|
||||
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
|
||||
// After closing, might still show as open due to TIME_WAIT, or closed
|
||||
// We won't assert on this as it's OS-dependent
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable - should handle invalid hosts gracefully', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Non-existent domain
|
||||
const result = await sn.isRemotePortAvailable('this-domain-definitely-does-not-exist-12345.com', 80);
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('isRemotePortAvailable - edge case ports', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Test HTTPS port
|
||||
const https = await sn.isRemotePortAvailable('example.com', 443);
|
||||
expect(https).toBeTrue();
|
||||
|
||||
// Test SSH port (likely closed on example.com)
|
||||
const ssh = await sn.isRemotePortAvailable('example.com', 22);
|
||||
expect(ssh).toBeFalse();
|
||||
});
|
||||
|
||||
// ========= Integration Tests =========
|
||||
|
||||
tap.test('Integration - findFreePort and isLocalPortUnused consistency', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
|
||||
// Find a free port
|
||||
const freePort = await sn.findFreePort(52000, 52100);
|
||||
expect(freePort).not.toBeNull();
|
||||
|
||||
if (freePort !== null) {
|
||||
// Verify it's actually free
|
||||
const isUnused1 = await sn.isLocalPortUnused(freePort);
|
||||
expect(isUnused1).toBeTrue();
|
||||
|
||||
// Start a server on it
|
||||
const server = await createServerOnPort(freePort);
|
||||
|
||||
// Now it should be in use
|
||||
const isUnused2 = await sn.isLocalPortUnused(freePort);
|
||||
expect(isUnused2).toBeFalse();
|
||||
|
||||
// findFreePort should skip it
|
||||
const nextFreePort = await sn.findFreePort(freePort, freePort + 10);
|
||||
expect(nextFreePort).not.toEqual(freePort);
|
||||
|
||||
await cleanupServers([server]);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Integration - stress test with many concurrent port checks', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const portRange = Array.from({ length: 20 }, (_, i) => 53000 + i);
|
||||
|
||||
// Check all ports concurrently
|
||||
const results = await Promise.all(
|
||||
portRange.map(async port => ({
|
||||
port,
|
||||
isUnused: await sn.isLocalPortUnused(port)
|
||||
}))
|
||||
);
|
||||
|
||||
// All operations should complete without error
|
||||
results.forEach(result => {
|
||||
expect(typeof result.isUnused).toEqual('boolean');
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('Performance - findFreePort with large range', async () => {
|
||||
const sn = new SmartNetwork();
|
||||
const startTime = Date.now();
|
||||
|
||||
// This should be fast even with a large range
|
||||
const freePort = await sn.findFreePort(30000, 60000);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
expect(freePort).not.toBeNull();
|
||||
// Should complete quickly (within 100ms) as it should find a port early
|
||||
expect(duration).toBeLessThan(100);
|
||||
});
|
||||
|
||||
tap.start();
|
49
test/test.ts
49
test/test.ts
@@ -1,26 +1,57 @@
|
||||
import { expect, tap } from '@pushrocks/tapbundle';
|
||||
import * as smartnetwork from '../ts/index';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartnetwork from '../ts/index.js';
|
||||
|
||||
let testSmartNetwork: smartnetwork.SmartNetwork;
|
||||
|
||||
tap.test('should create a valid instance of SmartNetwork', async () => {
|
||||
testSmartNetwork = new smartnetwork.SmartNetwork();
|
||||
expect(testSmartNetwork).to.be.instanceOf(smartnetwork.SmartNetwork);
|
||||
expect(testSmartNetwork).toBeInstanceOf(smartnetwork.SmartNetwork);
|
||||
});
|
||||
|
||||
tap.test('should perform a speedtest', async () => {
|
||||
let result = await testSmartNetwork.getSpeed();
|
||||
console.log(`Download speed for this instance is ${result.speeds.download}`);
|
||||
console.log(`Upload speed for this instance is ${result.speeds.upload}`);
|
||||
const result = await testSmartNetwork.getSpeed();
|
||||
console.log(`Download speed for this instance is ${result.downloadSpeed}`);
|
||||
console.log(`Upload speed for this instance is ${result.uploadSpeed}`);
|
||||
// verify speeds are returned as strings and parse to positive numbers
|
||||
expect(typeof result.downloadSpeed).toEqual('string');
|
||||
expect(typeof result.uploadSpeed).toEqual('string');
|
||||
expect(parseFloat(result.downloadSpeed)).toBeGreaterThan(0);
|
||||
expect(parseFloat(result.uploadSpeed)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('should determine wether a port is free', async () => {
|
||||
await expect(testSmartNetwork.isLocalPortAvailable(8080)).to.eventually.be.true;
|
||||
await expect(testSmartNetwork.isLocalPortUnused(8080)).resolves.toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should scan a port', async () => {
|
||||
const portscanner = new smartnetwork.PortScanner();
|
||||
expect(portscanner.checkPortStatus('google.com:80')).to.eventually.be.true;
|
||||
await expect(testSmartNetwork.isRemotePortAvailable('lossless.com:443')).resolves.toBeTrue();
|
||||
await expect(testSmartNetwork.isRemotePortAvailable('lossless.com', 443)).resolves.toBeTrue();
|
||||
await expect(testSmartNetwork.isRemotePortAvailable('lossless.com:444')).resolves.toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('should get gateways', async () => {
|
||||
const gateways = await testSmartNetwork.getGateways();
|
||||
console.log(gateways);
|
||||
// verify gateways object has at least one interface
|
||||
expect(typeof gateways).toEqual('object');
|
||||
expect(Object.keys(gateways).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('should get the default gateway', async () => {
|
||||
const defaultGw = await testSmartNetwork.getDefaultGateway();
|
||||
console.log(defaultGw);
|
||||
// verify default gateway contains ipv4 and ipv6 info
|
||||
expect(defaultGw).toBeDefined();
|
||||
expect(defaultGw.ipv4).toBeDefined();
|
||||
expect(defaultGw.ipv6).toBeDefined();
|
||||
});
|
||||
|
||||
tap.test('should get public ips', async () => {
|
||||
const ips = await testSmartNetwork.getPublicIps();
|
||||
console.log(ips);
|
||||
// verify public IPs object contains v4 and v6 properties
|
||||
expect(ips).toHaveProperty('v4');
|
||||
expect(ips).toHaveProperty('v6');
|
||||
});
|
||||
|
||||
tap.start();
|
||||
|
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartnetwork',
|
||||
version: '4.3.0',
|
||||
description: 'A toolkit for network diagnostics including speed tests, port availability checks, and more.'
|
||||
}
|
20
ts/errors.ts
Normal file
20
ts/errors.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Custom error classes for network operations
|
||||
*/
|
||||
export class NetworkError extends Error {
|
||||
public code?: string;
|
||||
constructor(message?: string, code?: string) {
|
||||
super(message);
|
||||
this.name = 'NetworkError';
|
||||
this.code = code;
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class TimeoutError extends NetworkError {
|
||||
constructor(message?: string) {
|
||||
super(message, 'ETIMEOUT');
|
||||
this.name = 'TimeoutError';
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
}
|
||||
}
|
43
ts/helpers/stats.ts
Normal file
43
ts/helpers/stats.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export function average(values: number[]) {
|
||||
let total = 0;
|
||||
|
||||
for (let i = 0; i < values.length; i += 1) {
|
||||
total += values[i];
|
||||
}
|
||||
|
||||
return total / values.length;
|
||||
}
|
||||
|
||||
export function median(values: number[]) {
|
||||
const half = Math.floor(values.length / 2);
|
||||
|
||||
values.sort((a, b) => a - b);
|
||||
|
||||
if (values.length % 2) return values[half];
|
||||
|
||||
return (values[half - 1] + values[half]) / 2;
|
||||
}
|
||||
|
||||
export function quartile(values: number[], percentile: number) {
|
||||
values.sort((a, b) => a - b);
|
||||
const pos = (values.length - 1) * percentile;
|
||||
const base = Math.floor(pos);
|
||||
const rest = pos - base;
|
||||
|
||||
if (values[base + 1] !== undefined) {
|
||||
return values[base] + rest * (values[base + 1] - values[base]);
|
||||
}
|
||||
|
||||
return values[base];
|
||||
}
|
||||
|
||||
export function jitter(values: number[]) {
|
||||
// Average distance between consecutive latency measurements...
|
||||
let jitters = [];
|
||||
|
||||
for (let i = 0; i < values.length - 1; i += 1) {
|
||||
jitters.push(Math.abs(values[i] - values[i + 1]));
|
||||
}
|
||||
|
||||
return average(jitters);
|
||||
}
|
@@ -1,2 +1,5 @@
|
||||
export * from './smartnetwork.classes.portscanner';
|
||||
export * from './smartnetwork.classes.speedtest';
|
||||
export * from './smartnetwork.classes.smartnetwork.js';
|
||||
export type { SmartNetworkOptions, Hop } from './smartnetwork.classes.smartnetwork.js';
|
||||
export { PublicIp } from './smartnetwork.classes.publicip.js';
|
||||
export { setLogger, getLogger } from './logging.js';
|
||||
export { NetworkError, TimeoutError } from './errors.js';
|
||||
|
30
ts/logging.ts
Normal file
30
ts/logging.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Injectable logging interface and global logger
|
||||
*/
|
||||
export interface Logger {
|
||||
/** Debug-level messages */
|
||||
debug?(...args: unknown[]): void;
|
||||
/** Informational messages */
|
||||
info(...args: unknown[]): void;
|
||||
/** Warning messages */
|
||||
warn?(...args: unknown[]): void;
|
||||
/** Error messages */
|
||||
error(...args: unknown[]): void;
|
||||
}
|
||||
|
||||
let globalLogger: Logger = console;
|
||||
|
||||
/**
|
||||
* Replace the global logger implementation
|
||||
* @param logger Custom logger adhering to Logger interface
|
||||
*/
|
||||
export function setLogger(logger: Logger): void {
|
||||
globalLogger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current global logger
|
||||
*/
|
||||
export function getLogger(): Logger {
|
||||
return globalLogger;
|
||||
}
|
311
ts/smartnetwork.classes.cloudflarespeed.ts
Normal file
311
ts/smartnetwork.classes.cloudflarespeed.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
import * as plugins from './smartnetwork.plugins.js';
|
||||
import { getLogger } from './logging.js';
|
||||
import { NetworkError, TimeoutError } from './errors.js';
|
||||
import * as stats from './helpers/stats.js';
|
||||
|
||||
export interface SpeedOptions {
|
||||
parallelStreams?: number;
|
||||
duration?: number;
|
||||
}
|
||||
export class CloudflareSpeed {
|
||||
private opts: SpeedOptions;
|
||||
constructor(opts?: SpeedOptions) {
|
||||
this.opts = opts || {};
|
||||
}
|
||||
|
||||
public async speedTest() {
|
||||
const latency = await this.measureLatency();
|
||||
|
||||
const serverLocations = await this.fetchServerLocations();
|
||||
const cgiData = await this.fetchCfCdnCgiTrace();
|
||||
|
||||
// speed tests: either fixed segments or duration-based mode
|
||||
const parallel = this.opts.parallelStreams ?? 1;
|
||||
const measureDownloadParallel = (bytes: number, iterations: number) => {
|
||||
if (parallel <= 1) {
|
||||
return this.measureDownload(bytes, iterations);
|
||||
}
|
||||
return Promise.all(
|
||||
Array(parallel)
|
||||
.fill(null)
|
||||
.map(() => this.measureDownload(bytes, iterations)),
|
||||
).then((arrays) => arrays.flat());
|
||||
};
|
||||
let downloadTests: number[];
|
||||
if (this.opts.duration && this.opts.duration > 0) {
|
||||
// duration-based download: run for specified seconds
|
||||
downloadTests = [];
|
||||
const durMs = this.opts.duration * 1000;
|
||||
const startMs = Date.now();
|
||||
// use medium chunk size for download
|
||||
const chunkBytes = 25001000;
|
||||
while (Date.now() - startMs < durMs) {
|
||||
const speeds = await measureDownloadParallel(chunkBytes, 1);
|
||||
downloadTests.push(...speeds);
|
||||
}
|
||||
if (downloadTests.length === 0) downloadTests = [0];
|
||||
} else {
|
||||
// fixed download segments
|
||||
const t1 = await measureDownloadParallel(101000, 10);
|
||||
const t2 = await measureDownloadParallel(1001000, 8);
|
||||
const t3 = await measureDownloadParallel(10001000, 6);
|
||||
const t4 = await measureDownloadParallel(25001000, 4);
|
||||
const t5 = await measureDownloadParallel(100001000, 1);
|
||||
downloadTests = [...t1, ...t2, ...t3, ...t4, ...t5];
|
||||
}
|
||||
const speedDownload = stats.quartile(downloadTests, 0.9).toFixed(2);
|
||||
|
||||
// lets test the upload speed with configurable parallel streams
|
||||
const measureUploadParallel = (bytes: number, iterations: number) => {
|
||||
if (parallel <= 1) {
|
||||
return this.measureUpload(bytes, iterations);
|
||||
}
|
||||
return Promise.all(
|
||||
Array(parallel)
|
||||
.fill(null)
|
||||
.map(() => this.measureUpload(bytes, iterations)),
|
||||
).then((arrays) => arrays.flat());
|
||||
};
|
||||
let uploadTests: number[];
|
||||
if (this.opts.duration && this.opts.duration > 0) {
|
||||
// duration-based upload: run for specified seconds
|
||||
uploadTests = [];
|
||||
const durMsUp = this.opts.duration * 1000;
|
||||
const startMsUp = Date.now();
|
||||
const chunkBytesUp = 1001000;
|
||||
while (Date.now() - startMsUp < durMsUp) {
|
||||
const speeds = await measureUploadParallel(chunkBytesUp, 1);
|
||||
uploadTests.push(...speeds);
|
||||
}
|
||||
if (uploadTests.length === 0) uploadTests = [0];
|
||||
} else {
|
||||
const u1 = await measureUploadParallel(11000, 10);
|
||||
const u2 = await measureUploadParallel(101000, 10);
|
||||
const u3 = await measureUploadParallel(1001000, 8);
|
||||
uploadTests = [...u1, ...u2, ...u3];
|
||||
}
|
||||
const speedUpload = stats.quartile(uploadTests, 0.9).toFixed(2);
|
||||
|
||||
return {
|
||||
...latency,
|
||||
ip: cgiData.ip,
|
||||
serverLocation: {
|
||||
shortId: cgiData.colo,
|
||||
name: serverLocations[cgiData.colo],
|
||||
availableLocations: serverLocations,
|
||||
},
|
||||
downloadSpeed: speedDownload,
|
||||
uploadSpeed: speedUpload,
|
||||
};
|
||||
}
|
||||
|
||||
public async measureLatency() {
|
||||
const measurements: number[] = [];
|
||||
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
await this.download(1000).then(
|
||||
(response) => {
|
||||
// TTFB - Server processing time
|
||||
measurements.push(response[4] - response[0] - response[6]);
|
||||
},
|
||||
(error) => {
|
||||
getLogger().error('Error measuring latency:', error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
maxTime: Math.max(...measurements),
|
||||
minTime: Math.min(...measurements),
|
||||
averageTime: stats.average(measurements),
|
||||
medianTime: stats.median(measurements),
|
||||
jitter: stats.jitter(measurements),
|
||||
};
|
||||
}
|
||||
|
||||
public async measureDownload(bytes: number, iterations: number) {
|
||||
const measurements: number[] = [];
|
||||
|
||||
for (let i = 0; i < iterations; i += 1) {
|
||||
await this.download(bytes).then(
|
||||
async (response) => {
|
||||
const transferTime = response[5] - response[4];
|
||||
measurements.push(await this.measureSpeed(bytes, transferTime));
|
||||
},
|
||||
(error) => {
|
||||
getLogger().error('Error measuring download chunk:', error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return measurements;
|
||||
}
|
||||
|
||||
public async measureUpload(bytes: number, iterations: number) {
|
||||
const measurements: number[] = [];
|
||||
|
||||
for (let i = 0; i < iterations; i += 1) {
|
||||
await this.upload(bytes).then(
|
||||
async (response) => {
|
||||
const transferTime = response[6];
|
||||
measurements.push(await this.measureSpeed(bytes, transferTime));
|
||||
},
|
||||
(error) => {
|
||||
getLogger().error('Error measuring upload chunk:', error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return measurements;
|
||||
}
|
||||
|
||||
public async measureSpeed(bytes: number, duration: number) {
|
||||
return (bytes * 8) / (duration / 1000) / 1e6;
|
||||
}
|
||||
|
||||
public async fetchServerLocations(): Promise<{ [key: string]: string }> {
|
||||
const res = JSON.parse(await this.get('speed.cloudflare.com', '/locations')) as Array<{
|
||||
iata: string;
|
||||
city: string;
|
||||
}>;
|
||||
return res.reduce(
|
||||
(data: Record<string, string>, optionsArg) => {
|
||||
data[optionsArg.iata] = optionsArg.city;
|
||||
return data;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
}
|
||||
|
||||
public async get(hostname: string, path: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = plugins.https.request(
|
||||
{
|
||||
hostname,
|
||||
path,
|
||||
method: 'GET',
|
||||
// disable connection pooling to avoid listener accumulation
|
||||
agent: false,
|
||||
},
|
||||
(res) => {
|
||||
const body: Array<Buffer> = [];
|
||||
res.on('data', (chunk) => {
|
||||
body.push(chunk);
|
||||
});
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve(Buffer.concat(body).toString());
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
req.on('error', (err: Error & { code?: string }) => {
|
||||
reject(new NetworkError(err.message, err.code));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
public async download(bytes: number) {
|
||||
const options = {
|
||||
hostname: 'speed.cloudflare.com',
|
||||
path: `/__down?bytes=${bytes}`,
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
return this.request(options);
|
||||
}
|
||||
|
||||
public async upload(bytes: number) {
|
||||
const data = '0'.repeat(bytes);
|
||||
const options = {
|
||||
hostname: 'speed.cloudflare.com',
|
||||
path: '/__up',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Length': Buffer.byteLength(data),
|
||||
},
|
||||
};
|
||||
|
||||
return this.request(options, data);
|
||||
}
|
||||
|
||||
public async request(options: plugins.https.RequestOptions, data = ''): Promise<number[]> {
|
||||
let started: number;
|
||||
let dnsLookup: number;
|
||||
let tcpHandshake: number;
|
||||
let sslHandshake: number;
|
||||
let ttfb: number;
|
||||
let ended: number;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
started = plugins.perfHooks.performance.now();
|
||||
// disable connection pooling to avoid listener accumulation across requests
|
||||
const reqOptions = { ...options, agent: false };
|
||||
const req = plugins.https.request(reqOptions, (res) => {
|
||||
res.once('readable', () => {
|
||||
ttfb = plugins.perfHooks.performance.now();
|
||||
});
|
||||
res.on('data', () => {});
|
||||
res.on('end', () => {
|
||||
ended = plugins.perfHooks.performance.now();
|
||||
resolve([
|
||||
started,
|
||||
dnsLookup,
|
||||
tcpHandshake,
|
||||
sslHandshake,
|
||||
ttfb,
|
||||
ended,
|
||||
parseFloat((res.headers['server-timing'] as string).slice(22)),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for timing events once per new socket
|
||||
req.once('socket', (socket) => {
|
||||
socket.once('lookup', () => {
|
||||
dnsLookup = plugins.perfHooks.performance.now();
|
||||
});
|
||||
socket.once('connect', () => {
|
||||
tcpHandshake = plugins.perfHooks.performance.now();
|
||||
});
|
||||
socket.once('secureConnect', () => {
|
||||
sslHandshake = plugins.perfHooks.performance.now();
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error: Error & { code?: string }) => {
|
||||
reject(new NetworkError(error.message, error.code));
|
||||
});
|
||||
|
||||
req.write(data);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Cloudflare's trace endpoint and parse key=value lines to a record.
|
||||
*/
|
||||
public async fetchCfCdnCgiTrace(): Promise<Record<string, string>> {
|
||||
const parseCfCdnCgiTrace = (text: string) =>
|
||||
text
|
||||
.split('\n')
|
||||
.map((i) => {
|
||||
const parts = i.split('=');
|
||||
return [parts[0], parts[1]];
|
||||
})
|
||||
.reduce(
|
||||
(data: Record<string, string>, [k, v]) => {
|
||||
if (v === undefined) return data;
|
||||
data[k] = v;
|
||||
return data;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
return this.get('speed.cloudflare.com', '/cdn-cgi/trace').then(parseCfCdnCgiTrace);
|
||||
}
|
||||
}
|
@@ -1,22 +0,0 @@
|
||||
import * as plugins from './smartnetwork.plugins';
|
||||
|
||||
export class PortScanner {
|
||||
public async checkPortStatus(domainArg: string): Promise<boolean> {
|
||||
const done = plugins.smartpromise.defer<boolean>();
|
||||
const domainPart = domainArg.split(':')[0];
|
||||
const port = parseInt(domainArg.split(':')[1], 10);
|
||||
|
||||
plugins.portscanner.checkPortStatus(port, domainPart, (err, status ) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
if (status === 'open') {
|
||||
done.resolve(true);
|
||||
} else {
|
||||
done.resolve(false)
|
||||
}
|
||||
})
|
||||
const result = await done.promise;
|
||||
return result;
|
||||
}
|
||||
}
|
163
ts/smartnetwork.classes.publicip.ts
Normal file
163
ts/smartnetwork.classes.publicip.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { getLogger } from './logging.js';
|
||||
|
||||
/**
|
||||
* Service configuration for IP detection
|
||||
*/
|
||||
interface IpService {
|
||||
name: string;
|
||||
v4Url?: string;
|
||||
v6Url?: string;
|
||||
parseResponse?: (text: string) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* PublicIp class for detecting public IPv4 and IPv6 addresses
|
||||
* Uses multiple fallback services for reliability
|
||||
*/
|
||||
export class PublicIp {
|
||||
private readonly services: IpService[] = [
|
||||
{
|
||||
name: 'ipify',
|
||||
v4Url: 'https://api.ipify.org?format=text',
|
||||
v6Url: 'https://api6.ipify.org?format=text',
|
||||
},
|
||||
{
|
||||
name: 'ident.me',
|
||||
v4Url: 'https://v4.ident.me',
|
||||
v6Url: 'https://v6.ident.me',
|
||||
},
|
||||
{
|
||||
name: 'seeip',
|
||||
v4Url: 'https://ipv4.seeip.org',
|
||||
v6Url: 'https://ipv6.seeip.org',
|
||||
},
|
||||
{
|
||||
name: 'icanhazip',
|
||||
v4Url: 'https://ipv4.icanhazip.com',
|
||||
v6Url: 'https://ipv6.icanhazip.com',
|
||||
},
|
||||
];
|
||||
|
||||
private readonly timeout: number;
|
||||
private readonly logger = getLogger();
|
||||
|
||||
constructor(options?: { timeout?: number }) {
|
||||
this.timeout = options?.timeout ?? 2000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public IPv4 address
|
||||
*/
|
||||
public async getPublicIpv4(): Promise<string | null> {
|
||||
for (const service of this.services) {
|
||||
if (!service.v4Url) continue;
|
||||
|
||||
try {
|
||||
const ip = await this.fetchIpFromService(service.v4Url, service.parseResponse);
|
||||
if (this.isValidIpv4(ip)) {
|
||||
this.logger.info?.(`Got IPv4 from ${service.name}: ${ip}`);
|
||||
return ip;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.debug?.(`Failed to get IPv4 from ${service.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.warn?.('Could not determine public IPv4 address from any service');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public IPv6 address
|
||||
*/
|
||||
public async getPublicIpv6(): Promise<string | null> {
|
||||
for (const service of this.services) {
|
||||
if (!service.v6Url) continue;
|
||||
|
||||
try {
|
||||
const ip = await this.fetchIpFromService(service.v6Url, service.parseResponse);
|
||||
if (this.isValidIpv6(ip)) {
|
||||
this.logger.info?.(`Got IPv6 from ${service.name}: ${ip}`);
|
||||
return ip;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.debug?.(`Failed to get IPv6 from ${service.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.warn?.('Could not determine public IPv6 address from any service');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get both IPv4 and IPv6 addresses
|
||||
*/
|
||||
public async getPublicIps(): Promise<{ v4: string | null; v6: string | null }> {
|
||||
const [v4, v6] = await Promise.all([
|
||||
this.getPublicIpv4(),
|
||||
this.getPublicIpv6(),
|
||||
]);
|
||||
|
||||
return { v4, v6 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch IP from a service URL
|
||||
*/
|
||||
private async fetchIpFromService(
|
||||
url: string,
|
||||
parseResponse?: (text: string) => string
|
||||
): Promise<string> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'User-Agent': '@push.rocks/smartnetwork',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
const ip = parseResponse ? parseResponse(text) : text.trim();
|
||||
|
||||
return ip;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate IPv4 address format
|
||||
*/
|
||||
private isValidIpv4(ip: string): boolean {
|
||||
if (!ip) return false;
|
||||
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
if (!ipv4Regex.test(ip)) return false;
|
||||
|
||||
const parts = ip.split('.');
|
||||
return parts.every(part => {
|
||||
const num = parseInt(part, 10);
|
||||
return num >= 0 && num <= 255;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate IPv6 address format
|
||||
*/
|
||||
private isValidIpv6(ip: string): boolean {
|
||||
if (!ip) return false;
|
||||
|
||||
// Simplified IPv6 validation - checks for colon-separated hex values
|
||||
const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
|
||||
const ipv6CompressedRegex = /^::([0-9a-fA-F]{0,4}:){0,6}[0-9a-fA-F]{0,4}$|^([0-9a-fA-F]{0,4}:){1,7}:$/;
|
||||
|
||||
return ipv6Regex.test(ip) || ipv6CompressedRegex.test(ip);
|
||||
}
|
||||
}
|
@@ -1,51 +1,108 @@
|
||||
import * as plugins from './smartnetwork.plugins';
|
||||
|
||||
export class ISpeedtestData {
|
||||
speeds: {
|
||||
download: number;
|
||||
upload: number;
|
||||
originalDownload: number;
|
||||
originalUpload: number;
|
||||
};
|
||||
client: {
|
||||
ip: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
isp: string;
|
||||
isprating: string;
|
||||
rating: number;
|
||||
ispdlavg: number;
|
||||
ispulavg: number;
|
||||
};
|
||||
server: {
|
||||
host: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
location: string;
|
||||
country: string;
|
||||
cc: string;
|
||||
sponsor: string;
|
||||
distance: number;
|
||||
distanceMi: number;
|
||||
ping: number;
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
import * as plugins from './smartnetwork.plugins.js';
|
||||
import { CloudflareSpeed } from './smartnetwork.classes.cloudflarespeed.js';
|
||||
import { PublicIp } from './smartnetwork.classes.publicip.js';
|
||||
import { getLogger } from './logging.js';
|
||||
import { NetworkError } from './errors.js';
|
||||
import * as stats from './helpers/stats.js';
|
||||
|
||||
/**
|
||||
* SmartNetwork simplifies actions within the network
|
||||
*/
|
||||
/**
|
||||
* Configuration options for SmartNetwork
|
||||
*/
|
||||
export interface SmartNetworkOptions {
|
||||
/** Cache time-to-live in milliseconds for gateway and public IP lookups */
|
||||
cacheTtl?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A hop in a traceroute result
|
||||
*/
|
||||
export interface Hop {
|
||||
ttl: number;
|
||||
ip: string;
|
||||
rtt: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the findFreePort method
|
||||
*/
|
||||
export interface IFindFreePortOptions {
|
||||
/** If true, selects a random available port within the range instead of the first one */
|
||||
randomize?: boolean;
|
||||
}
|
||||
|
||||
export class SmartNetwork {
|
||||
async getSpeed(measurementTime = 5000): Promise<ISpeedtestData> {
|
||||
let done = plugins.smartpromise.defer<ISpeedtestData>();
|
||||
const test = plugins.speedtestNet({ maxTime: measurementTime });
|
||||
test.on('data', data => {
|
||||
done.resolve(data);
|
||||
});
|
||||
test.on('error', err => {
|
||||
done.reject(err);
|
||||
});
|
||||
return await done.promise;
|
||||
/** Static registry for external plugins */
|
||||
public static pluginsRegistry: Map<string, any> = new Map();
|
||||
/** Register a plugin by name */
|
||||
public static registerPlugin(name: string, ctor: any): void {
|
||||
SmartNetwork.pluginsRegistry.set(name, ctor);
|
||||
}
|
||||
/** Unregister a plugin by name */
|
||||
public static unregisterPlugin(name: string): void {
|
||||
SmartNetwork.pluginsRegistry.delete(name);
|
||||
}
|
||||
private options: SmartNetworkOptions;
|
||||
private cache: Map<string, { value: any; expiry: number }>;
|
||||
constructor(options?: SmartNetworkOptions) {
|
||||
this.options = options || {};
|
||||
this.cache = new Map();
|
||||
}
|
||||
/**
|
||||
* get network speed
|
||||
* @param opts optional speed test parameters
|
||||
*/
|
||||
public async getSpeed(opts?: { parallelStreams?: number; duration?: number }) {
|
||||
const cloudflareSpeedInstance = new CloudflareSpeed(opts);
|
||||
return cloudflareSpeedInstance.speedTest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send ICMP pings to a host. Optionally specify count for multiple pings.
|
||||
*/
|
||||
public async ping(host: string, opts?: { timeout?: number; count?: number }): Promise<any> {
|
||||
const timeout = opts?.timeout ?? 500;
|
||||
const count = opts?.count && opts.count > 1 ? opts.count : 1;
|
||||
const pinger = new plugins.smartping.Smartping();
|
||||
if (count === 1) {
|
||||
// single ping: normalize time to number
|
||||
const res = await pinger.ping(host, timeout);
|
||||
return {
|
||||
...res,
|
||||
time: typeof res.time === 'number' ? res.time : NaN,
|
||||
};
|
||||
}
|
||||
const times: number[] = [];
|
||||
let aliveCount = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
try {
|
||||
const res = await pinger.ping(host, timeout);
|
||||
const t = typeof res.time === 'number' ? res.time : NaN;
|
||||
if (res.alive) aliveCount++;
|
||||
times.push(t);
|
||||
} catch {
|
||||
times.push(NaN);
|
||||
}
|
||||
}
|
||||
const valid = times.filter((t) => !isNaN(t));
|
||||
const min = valid.length ? Math.min(...valid) : NaN;
|
||||
const max = valid.length ? Math.max(...valid) : NaN;
|
||||
const avg = valid.length ? stats.average(valid) : NaN;
|
||||
const stddev = valid.length ? Math.sqrt(stats.average(valid.map((v) => (v - avg) ** 2))) : NaN;
|
||||
const packetLoss = ((count - aliveCount) / count) * 100;
|
||||
return {
|
||||
host,
|
||||
count,
|
||||
times,
|
||||
min,
|
||||
max,
|
||||
avg,
|
||||
stddev,
|
||||
packetLoss,
|
||||
alive: aliveCount > 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,21 +110,20 @@ export class SmartNetwork {
|
||||
* note: false also resolves with false as argument
|
||||
* @param port
|
||||
*/
|
||||
async isLocalPortAvailable(port: number): Promise<boolean> {
|
||||
/**
|
||||
* Check if a local port is unused (both IPv4 and IPv6)
|
||||
*/
|
||||
public async isLocalPortUnused(port: number): Promise<boolean> {
|
||||
const doneIpV4 = plugins.smartpromise.defer<boolean>();
|
||||
const doneIpV6 = plugins.smartpromise.defer<boolean>();
|
||||
const net = await import('net'); // creates only one instance of net ;) even on multiple calls
|
||||
|
||||
// test IPv4 space
|
||||
const ipv4Test = net.createServer();
|
||||
ipv4Test.once('error', function(err: any) {
|
||||
if (err.code !== 'EADDRINUSE') {
|
||||
doneIpV4.resolve(false);
|
||||
return;
|
||||
}
|
||||
ipv4Test.once('error', () => {
|
||||
doneIpV4.resolve(false);
|
||||
});
|
||||
ipv4Test.once('listening', function() {
|
||||
ipv4Test.once('listening', () => {
|
||||
ipv4Test.once('close', () => {
|
||||
doneIpV4.resolve(true);
|
||||
});
|
||||
@@ -78,21 +134,17 @@ export class SmartNetwork {
|
||||
await doneIpV4.promise;
|
||||
|
||||
// test IPv6 space
|
||||
const test_ipv6 = net.createServer();
|
||||
test_ipv6.once('error', function(err: any) {
|
||||
if (err.code !== 'EADDRINUSE') {
|
||||
doneIpV6.resolve(false);
|
||||
return;
|
||||
}
|
||||
const ipv6Test = net.createServer();
|
||||
ipv6Test.once('error', () => {
|
||||
doneIpV6.resolve(false);
|
||||
});
|
||||
test_ipv6.once('listening', function() {
|
||||
test_ipv6.once('close', () => {
|
||||
ipv6Test.once('listening', () => {
|
||||
ipv6Test.once('close', () => {
|
||||
doneIpV6.resolve(true);
|
||||
});
|
||||
test_ipv6.close();
|
||||
ipv6Test.close();
|
||||
});
|
||||
test_ipv6.listen(port, '::');
|
||||
ipv6Test.listen(port, '::');
|
||||
|
||||
// lets wait for the result
|
||||
const resultIpV4 = await doneIpV4.promise;
|
||||
@@ -101,28 +153,286 @@ export class SmartNetwork {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first available port within a given range
|
||||
* @param startPort The start of the port range (inclusive)
|
||||
* @param endPort The end of the port range (inclusive)
|
||||
* @param options Optional configuration for port selection behavior
|
||||
* @returns The first available port number (or random if options.randomize is true), or null if no ports are available
|
||||
*/
|
||||
public async findFreePort(startPort: number, endPort: number, options?: IFindFreePortOptions): Promise<number | null> {
|
||||
// Validate port range
|
||||
if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) {
|
||||
throw new NetworkError('Port numbers must be between 1 and 65535', 'EINVAL');
|
||||
}
|
||||
if (startPort > endPort) {
|
||||
throw new NetworkError('Start port must be less than or equal to end port', 'EINVAL');
|
||||
}
|
||||
|
||||
// If randomize option is true, collect all available ports and select randomly
|
||||
if (options?.randomize) {
|
||||
const availablePorts: number[] = [];
|
||||
|
||||
// Scan the range to find all available ports
|
||||
for (let port = startPort; port <= endPort; port++) {
|
||||
const isUnused = await this.isLocalPortUnused(port);
|
||||
if (isUnused) {
|
||||
availablePorts.push(port);
|
||||
}
|
||||
}
|
||||
|
||||
// If there are available ports, select one randomly
|
||||
if (availablePorts.length > 0) {
|
||||
const randomIndex = Math.floor(Math.random() * availablePorts.length);
|
||||
return availablePorts[randomIndex];
|
||||
}
|
||||
|
||||
// No free port found in the range
|
||||
return null;
|
||||
} else {
|
||||
// Default behavior: return the first available port (sequential search)
|
||||
for (let port = startPort; port <= endPort; port++) {
|
||||
const isUnused = await this.isLocalPortUnused(port);
|
||||
if (isUnused) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
// No free port found in the range
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* checks wether a remote port is available
|
||||
* @param domainArg
|
||||
*/
|
||||
public async isRemotePortAvailable(domainArg: string, portArg?: number): Promise<boolean> {
|
||||
const done = plugins.smartpromise.defer<boolean>();
|
||||
const domainPart = domainArg.split(':')[0];
|
||||
const port = (() => {
|
||||
return portArg ? portArg : parseInt(domainArg.split(':')[1], 10);
|
||||
})()
|
||||
/**
|
||||
* Check if a remote port is available
|
||||
* @param target host or "host:port"
|
||||
* @param opts options including port, protocol (only tcp), retries and timeout
|
||||
*/
|
||||
/**
|
||||
* Check if a remote port is available
|
||||
* @param target host or "host:port"
|
||||
* @param portOrOpts either a port number (deprecated) or options object
|
||||
*/
|
||||
public async isRemotePortAvailable(
|
||||
target: string,
|
||||
portOrOpts?:
|
||||
| number
|
||||
| { port?: number; protocol?: 'tcp' | 'udp'; timeout?: number; retries?: number },
|
||||
): Promise<boolean> {
|
||||
let hostPart: string;
|
||||
let port: number | undefined;
|
||||
let protocol: string = 'tcp';
|
||||
let retries = 1;
|
||||
let timeout: number | undefined;
|
||||
// preserve old signature (target, port)
|
||||
if (typeof portOrOpts === 'number') {
|
||||
[hostPart] = target.split(':');
|
||||
port = portOrOpts;
|
||||
} else {
|
||||
const opts = portOrOpts || {};
|
||||
protocol = opts.protocol ?? 'tcp';
|
||||
retries = opts.retries ?? 1;
|
||||
timeout = opts.timeout;
|
||||
[hostPart] = target.split(':');
|
||||
const portPart = target.split(':')[1];
|
||||
port = opts.port ?? (portPart ? parseInt(portPart, 10) : undefined);
|
||||
}
|
||||
if (protocol === 'udp') {
|
||||
throw new NetworkError('UDP port check not supported', 'ENOTSUP');
|
||||
}
|
||||
if (!port) {
|
||||
throw new NetworkError('Port not specified', 'EINVAL');
|
||||
}
|
||||
let last: boolean = false;
|
||||
for (let attempt = 0; attempt < retries; attempt++) {
|
||||
const done = plugins.smartpromise.defer<boolean>();
|
||||
plugins.isopen(hostPart, port, (response: Record<string, { isOpen: boolean }>) => {
|
||||
const info = response[port.toString()];
|
||||
done.resolve(Boolean(info?.isOpen));
|
||||
});
|
||||
last = await done.promise;
|
||||
if (last) return true;
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
plugins.portscanner.checkPortStatus(port, domainPart, (err, status ) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
/**
|
||||
* List network interfaces (gateways)
|
||||
*/
|
||||
public async getGateways(): Promise<Record<string, plugins.os.NetworkInterfaceInfo[]>> {
|
||||
const fetcher = async () => plugins.os.networkInterfaces();
|
||||
if (this.options.cacheTtl && this.options.cacheTtl > 0) {
|
||||
return this.getCached('gateways', fetcher);
|
||||
}
|
||||
return fetcher();
|
||||
}
|
||||
|
||||
public async getDefaultGateway(): Promise<{
|
||||
ipv4: plugins.os.NetworkInterfaceInfo;
|
||||
ipv6: plugins.os.NetworkInterfaceInfo;
|
||||
}> {
|
||||
const defaultGatewayName = await plugins.systeminformation.networkInterfaceDefault();
|
||||
if (!defaultGatewayName) {
|
||||
getLogger().warn?.('Cannot determine default gateway');
|
||||
return null;
|
||||
}
|
||||
const gateways = await this.getGateways();
|
||||
const defaultGateway = gateways[defaultGatewayName];
|
||||
return {
|
||||
ipv4: defaultGateway[0],
|
||||
ipv6: defaultGateway[1],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup public IPv4 and IPv6
|
||||
*/
|
||||
public async getPublicIps(): Promise<{ v4: string | null; v6: string | null }> {
|
||||
const fetcher = async () => {
|
||||
const publicIp = new PublicIp({ timeout: 1000 });
|
||||
return publicIp.getPublicIps();
|
||||
};
|
||||
if (this.options.cacheTtl && this.options.cacheTtl > 0) {
|
||||
return this.getCached('publicIps', fetcher);
|
||||
}
|
||||
return fetcher();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve DNS records (A, AAAA, MX)
|
||||
*/
|
||||
public async resolveDns(
|
||||
host: string,
|
||||
): Promise<{ A: string[]; AAAA: string[]; MX: { exchange: string; priority: number }[] }> {
|
||||
try {
|
||||
const dnsClient = new plugins.smartdns.dnsClientMod.Smartdns({
|
||||
strategy: 'prefer-system', // Try system resolver first (handles localhost), fallback to DoH
|
||||
allowDohFallback: true,
|
||||
});
|
||||
|
||||
const [aRecords, aaaaRecords, mxRecords] = await Promise.all([
|
||||
dnsClient.getRecordsA(host).catch((): any[] => []),
|
||||
dnsClient.getRecordsAAAA(host).catch((): any[] => []),
|
||||
dnsClient.getRecords(host, 'MX').catch((): any[] => []),
|
||||
]);
|
||||
|
||||
// Extract values from the record objects
|
||||
const A = aRecords.map((record: any) => record.value);
|
||||
const AAAA = aaaaRecords.map((record: any) => record.value);
|
||||
|
||||
// Parse MX records - the value contains "priority exchange"
|
||||
const MX = mxRecords.map((record: any) => {
|
||||
const parts = record.value.split(' ');
|
||||
return {
|
||||
priority: parseInt(parts[0], 10),
|
||||
exchange: parts[1] || '',
|
||||
};
|
||||
});
|
||||
|
||||
return { A, AAAA, MX };
|
||||
} catch (err: any) {
|
||||
throw new NetworkError(err.message, err.code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a simple HTTP/HTTPS endpoint health check
|
||||
*/
|
||||
public async checkEndpoint(
|
||||
urlString: string,
|
||||
opts?: { timeout?: number },
|
||||
): Promise<{ status: number; headers: Record<string, string>; rtt: number }> {
|
||||
const start = plugins.perfHooks.performance.now();
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
const lib = url.protocol === 'https:' ? plugins.https : await import('http');
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = lib.request(
|
||||
url,
|
||||
{ method: 'GET', timeout: opts?.timeout, agent: false },
|
||||
(res: any) => {
|
||||
res.on('data', () => {});
|
||||
res.once('end', () => {
|
||||
const rtt = plugins.perfHooks.performance.now() - start;
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(res.headers)) {
|
||||
headers[k] = Array.isArray(v) ? v.join(',') : String(v);
|
||||
}
|
||||
resolve({ status: res.statusCode, headers, rtt });
|
||||
});
|
||||
},
|
||||
);
|
||||
req.on('error', (err: any) => reject(new NetworkError(err.message, err.code)));
|
||||
req.end();
|
||||
});
|
||||
} catch (err: any) {
|
||||
throw new NetworkError(err.message, err.code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a traceroute: hop-by-hop latency using the system traceroute tool.
|
||||
* Falls back to a single-hop stub if traceroute is unavailable or errors.
|
||||
*/
|
||||
public async traceroute(
|
||||
host: string,
|
||||
opts?: { maxHops?: number; timeout?: number },
|
||||
): Promise<Hop[]> {
|
||||
const maxHops = opts?.maxHops ?? 30;
|
||||
const timeout = opts?.timeout;
|
||||
try {
|
||||
const { exec } = await import('child_process');
|
||||
const cmd = `traceroute -n -m ${maxHops} ${host}`;
|
||||
const stdout: string = await new Promise((resolve, reject) => {
|
||||
exec(cmd, { encoding: 'utf8', timeout }, (err, stdout) => {
|
||||
if (err) return reject(err);
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
const hops: Hop[] = [];
|
||||
for (const raw of stdout.split('\n')) {
|
||||
const line = raw.trim();
|
||||
if (!line || line.startsWith('traceroute')) continue;
|
||||
const parts = line.split(/\s+/);
|
||||
const ttl = parseInt(parts[0], 10);
|
||||
let ip: string;
|
||||
let rtt: number | null;
|
||||
if (parts[1] === '*' || !parts[1]) {
|
||||
ip = parts[1] || '';
|
||||
rtt = null;
|
||||
} else {
|
||||
ip = parts[1];
|
||||
const timePart = parts.find((p, i) => i >= 2 && /^\d+(\.\d+)?$/.test(p));
|
||||
rtt = timePart ? parseFloat(timePart) : null;
|
||||
}
|
||||
hops.push({ ttl, ip, rtt });
|
||||
}
|
||||
if (status === 'open') {
|
||||
done.resolve(true);
|
||||
} else {
|
||||
done.resolve(false)
|
||||
if (hops.length) {
|
||||
return hops;
|
||||
}
|
||||
})
|
||||
const result = await done.promise;
|
||||
return result;
|
||||
} catch {
|
||||
// traceroute not available or error: fall through to stub
|
||||
}
|
||||
// fallback stub
|
||||
return [{ ttl: 1, ip: host, rtt: null }];
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal caching helper
|
||||
*/
|
||||
private async getCached<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
|
||||
const now = Date.now();
|
||||
const entry = this.cache.get(key);
|
||||
if (entry && entry.expiry > now) {
|
||||
return entry.value;
|
||||
}
|
||||
const value = await fetcher();
|
||||
const ttl = this.options.cacheTtl || 0;
|
||||
this.cache.set(key, { value, expiry: now + ttl });
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
@@ -1,103 +0,0 @@
|
||||
import * as plugins from './smartnetwork.plugins';
|
||||
|
||||
export class ISpeedtestData {
|
||||
speeds: {
|
||||
download: number;
|
||||
upload: number;
|
||||
originalDownload: number;
|
||||
originalUpload: number;
|
||||
};
|
||||
client: {
|
||||
ip: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
isp: string;
|
||||
isprating: string;
|
||||
rating: number;
|
||||
ispdlavg: number;
|
||||
ispulavg: number;
|
||||
};
|
||||
server: {
|
||||
host: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
location: string;
|
||||
country: string;
|
||||
cc: string;
|
||||
sponsor: string;
|
||||
distance: number;
|
||||
distanceMi: number;
|
||||
ping: number;
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SmartNetwork simplifies actions within the network
|
||||
*/
|
||||
export class SmartNetwork {
|
||||
async getSpeed(measurementTime = 5000): Promise<ISpeedtestData> {
|
||||
let done = plugins.smartpromise.defer<ISpeedtestData>();
|
||||
const test = plugins.speedtestNet({ maxTime: measurementTime });
|
||||
test.on('data', data => {
|
||||
done.resolve(data);
|
||||
});
|
||||
test.on('error', err => {
|
||||
done.reject(err);
|
||||
});
|
||||
return await done.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a promise with a boolean answer
|
||||
* note: false also resolves with false as argument
|
||||
* @param port
|
||||
*/
|
||||
async isLocalPortAvailable(port: number): Promise<boolean> {
|
||||
const doneIpV4 = plugins.smartpromise.defer<boolean>();
|
||||
const doneIpV6 = plugins.smartpromise.defer<boolean>();
|
||||
const net = await import('net'); // creates only one instance of net ;) even on multiple calls
|
||||
|
||||
// test IPv4 space
|
||||
const ipv4Test = net.createServer();
|
||||
ipv4Test.once('error', function(err: any) {
|
||||
if (err.code !== 'EADDRINUSE') {
|
||||
doneIpV4.resolve(false);
|
||||
return;
|
||||
}
|
||||
doneIpV4.resolve(false);
|
||||
});
|
||||
ipv4Test.once('listening', function() {
|
||||
ipv4Test.once('close', () => {
|
||||
doneIpV4.resolve(true);
|
||||
});
|
||||
ipv4Test.close();
|
||||
});
|
||||
ipv4Test.listen(port, '0.0.0.0');
|
||||
|
||||
await doneIpV4.promise;
|
||||
|
||||
// test IPv6 space
|
||||
const test_ipv6 = net.createServer();
|
||||
test_ipv6.once('error', function(err: any) {
|
||||
if (err.code !== 'EADDRINUSE') {
|
||||
doneIpV6.resolve(false);
|
||||
return;
|
||||
}
|
||||
doneIpV6.resolve(false);
|
||||
});
|
||||
test_ipv6.once('listening', function() {
|
||||
test_ipv6.once('close', () => {
|
||||
doneIpV6.resolve(true);
|
||||
});
|
||||
test_ipv6.close();
|
||||
});
|
||||
test_ipv6.listen(port, '::');
|
||||
|
||||
// lets wait for the result
|
||||
const resultIpV4 = await doneIpV4.promise;
|
||||
const resultIpV6 = await doneIpV6.promise;
|
||||
const result = resultIpV4 === true && resultIpV6 === true;
|
||||
return result;
|
||||
}
|
||||
}
|
@@ -1,13 +1,21 @@
|
||||
// native scope
|
||||
import * as os from 'os';
|
||||
import * as https from 'https';
|
||||
import * as perfHooks from 'perf_hooks';
|
||||
|
||||
export { os, https, perfHooks };
|
||||
|
||||
// @pushrocks scope
|
||||
import * as smartpromise from '@pushrocks/smartpromise';
|
||||
import * as smartstring from '@pushrocks/smartstring';
|
||||
import * as smartdns from '@push.rocks/smartdns';
|
||||
import * as smartping from '@push.rocks/smartping';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as smartstring from '@push.rocks/smartstring';
|
||||
|
||||
export { smartpromise, smartstring };
|
||||
export { smartdns, smartpromise, smartping, smartstring };
|
||||
|
||||
// @third party scope
|
||||
let speedtestNet = require('speedtest-net');
|
||||
import * as portscanner from 'portscanner';
|
||||
// @ts-ignore
|
||||
import isopen from 'isopen';
|
||||
import * as systeminformation from 'systeminformation';
|
||||
|
||||
export { speedtestNet, portscanner };
|
||||
export { isopen, systeminformation };
|
||||
|
5
ts/tsconfig.json
Normal file
5
ts/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": true
|
||||
}
|
||||
}
|
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {}
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
]
|
||||
}
|
17
tslint.json
17
tslint.json
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"extends": ["tslint:latest", "tslint-config-prettier"],
|
||||
"rules": {
|
||||
"semicolon": [true, "always"],
|
||||
"no-console": false,
|
||||
"ordered-imports": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"member-ordering": {
|
||||
"options":{
|
||||
"order": [
|
||||
"static-method"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultSeverity": "warning"
|
||||
}
|
Reference in New Issue
Block a user