Compare commits
No commits in common. "master" and "v1.1.11" have entirely different histories.
.gitea/workflows
.gitignore.gitlab-ci.yml.vscode
changelog.mdlicensenpmextra.jsonpackage-lock.jsonpackage.jsonpnpm-lock.yamlreadme.hints.mdreadme.mdtest
ts
00_commitinfo_data.tserrors.ts
tsconfig.jsontslint.jsonhelpers
index.tslogging.tssmartnetwork.classes.cloudflarespeed.tssmartnetwork.classes.smartnetwork.tssmartnetwork.plugins.tstsconfig.json@ -1,66 +0,0 @@
|
|||||||
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
|
|
@ -1,124 +0,0 @@
|
|||||||
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
|
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -3,6 +3,7 @@
|
|||||||
# artifacts
|
# artifacts
|
||||||
coverage/
|
coverage/
|
||||||
public/
|
public/
|
||||||
|
pages/
|
||||||
|
|
||||||
# installs
|
# installs
|
||||||
node_modules/
|
node_modules/
|
||||||
@ -14,6 +15,8 @@ node_modules/
|
|||||||
|
|
||||||
# builds
|
# builds
|
||||||
dist/
|
dist/
|
||||||
dist_*/
|
dist_web/
|
||||||
|
dist_serve/
|
||||||
|
dist_ts_web/
|
||||||
|
|
||||||
#------# custom
|
# custom
|
119
.gitlab-ci.yml
Normal file
119
.gitlab-ci.yml
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
# gitzone ci_default
|
||||||
|
image: registry.gitlab.com/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
|
||||||
|
# ====================
|
||||||
|
|
||||||
|
testStable:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- npmci npm prepare
|
||||||
|
- npmci node install stable
|
||||||
|
- npmci npm install
|
||||||
|
- npmci npm test
|
||||||
|
coverage: /\d+.?\d+?\%\s*coverage/
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
- priv
|
||||||
|
|
||||||
|
testBuild:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- npmci npm prepare
|
||||||
|
- npmci node install lts
|
||||||
|
- npmci npm install
|
||||||
|
- npmci command npm run build
|
||||||
|
coverage: /\d+.?\d+?\%\s*coverage/
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
- notpriv
|
||||||
|
|
||||||
|
release:
|
||||||
|
stage: release
|
||||||
|
script:
|
||||||
|
- npmci node install lts
|
||||||
|
- npmci npm publish
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
- notpriv
|
||||||
|
|
||||||
|
# ====================
|
||||||
|
# metadata stage
|
||||||
|
# ====================
|
||||||
|
codequality:
|
||||||
|
stage: metadata
|
||||||
|
allow_failure: true
|
||||||
|
script:
|
||||||
|
- npmci command npm install -g tslint typescript
|
||||||
|
- npmci npm install
|
||||||
|
- npmci command "tslint -c tslint.json ./ts/**/*.ts"
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
- priv
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
stage: metadata
|
||||||
|
script:
|
||||||
|
- npmci trigger
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
- notpriv
|
||||||
|
|
||||||
|
pages:
|
||||||
|
image: hosttoday/ht-docker-dbase:npmci
|
||||||
|
services:
|
||||||
|
- docker:stable-dind
|
||||||
|
stage: metadata
|
||||||
|
script:
|
||||||
|
- npmci command npm install -g @gitzone/tsdoc
|
||||||
|
- npmci npm prepare
|
||||||
|
- npmci npm install
|
||||||
|
- npmci command tsdoc
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
- notpriv
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
artifacts:
|
||||||
|
expire_in: 1 week
|
||||||
|
paths:
|
||||||
|
- public
|
||||||
|
allow_failure: true
|
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"command": "npm test",
|
|
||||||
"name": "Run npm test",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "node-terminal"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
26
.vscode/settings.json
vendored
26
.vscode/settings.json
vendored
@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
210
changelog.md
210
changelog.md
@ -1,210 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
## 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
23
license
@ -1,23 +0,0 @@
|
|||||||
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.
|
|
||||||
|
|
@ -4,29 +4,13 @@
|
|||||||
"npmAccessLevel": "public"
|
"npmAccessLevel": "public"
|
||||||
},
|
},
|
||||||
"gitzone": {
|
"gitzone": {
|
||||||
"projectType": "npm",
|
|
||||||
"module": {
|
"module": {
|
||||||
"githost": "code.foss.global",
|
"githost": "gitlab.com",
|
||||||
"gitscope": "push.rocks",
|
"gitscope": "pushrocks",
|
||||||
"gitrepo": "smartnetwork",
|
"gitrepo": "smartnetwork",
|
||||||
"description": "A toolkit for network diagnostics including speed tests, port availability checks, and more.",
|
"shortDescription": "network diagnostics",
|
||||||
"npmPackagename": "@push.rocks/smartnetwork",
|
"npmPackagename": "@pushrocks/smartnetwork",
|
||||||
"license": "MIT",
|
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
2063
package-lock.json
generated
Normal file
2063
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
81
package.json
81
package.json
@ -1,72 +1,41 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartnetwork",
|
"name": "@pushrocks/smartnetwork",
|
||||||
"version": "4.0.0",
|
"version": "1.1.11",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A toolkit for network diagnostics including speed tests, port availability checks, and more.",
|
"description": "network diagnostics",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist/index.js",
|
||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist/index.d.ts",
|
||||||
"type": "module",
|
|
||||||
"author": "Lossless GmbH",
|
"author": "Lossless GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/)",
|
"test": "(tstest test/)",
|
||||||
"build": "(tsbuild --web --allowimplicitany)",
|
"build": "(tsbuild)"
|
||||||
"buildDocs": "tsdoc"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.61",
|
"@gitzone/tsbuild": "^2.1.17",
|
||||||
"@git.zone/tsrun": "^1.2.39",
|
"@gitzone/tstest": "^1.0.24",
|
||||||
"@git.zone/tstest": "^1.0.69",
|
"@pushrocks/tapbundle": "^3.0.13",
|
||||||
"@push.rocks/smartenv": "^5.0.0",
|
"tslint": "^5.19.0",
|
||||||
"@push.rocks/tapbundle": "^5.0.3",
|
"tslint-config-prettier": "^1.18.0"
|
||||||
"@types/node": "^22.15.3"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/smartping": "^1.0.7",
|
"@pushrocks/smartpromise": "^3.0.2",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@pushrocks/smartstring": "^3.0.10",
|
||||||
"@push.rocks/smartstring": "^4.0.2",
|
"@types/default-gateway": "^3.0.0",
|
||||||
"@types/default-gateway": "^7.2.2",
|
"@types/portscanner": "^2.1.0",
|
||||||
"isopen": "^1.3.0",
|
"default-gateway": "^5.0.3",
|
||||||
"public-ip": "^7.0.1",
|
"portscanner": "^2.2.0",
|
||||||
"systeminformation": "^5.11.9"
|
"speedtest-net": "^1.5.1"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/*",
|
||||||
"ts_web/**/*",
|
"ts_web/*",
|
||||||
"dist/**/*",
|
"dist/*",
|
||||||
"dist_*/**/*",
|
"dist_web/*",
|
||||||
"dist_ts/**/*",
|
"dist_ts_web/*",
|
||||||
"dist_ts_web/**/*",
|
"assets/*",
|
||||||
"assets/**/*",
|
|
||||||
"cli.js",
|
"cli.js",
|
||||||
"npmextra.json",
|
"npmextra.json",
|
||||||
"readme.md"
|
"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": {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
9958
pnpm-lock.yaml
generated
9958
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
|||||||
|
|
203
readme.md
203
readme.md
@ -1,183 +1,46 @@
|
|||||||
# @push.rocks/smartnetwork
|
# @pushrocks/smartnetwork
|
||||||
|
|
||||||
network diagnostics
|
network diagnostics
|
||||||
|
|
||||||
## Install
|
## 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/)
|
||||||
|
|
||||||
To install `@push.rocks/smartnetwork`, run the following command in your terminal:
|
## Status for master
|
||||||
|
[](https://gitlab.com/pushrocks/smartnetwork/commits/master)
|
||||||
```bash
|
[](https://gitlab.com/pushrocks/smartnetwork/commits/master)
|
||||||
npm install @push.rocks/smartnetwork --save
|
[](https://www.npmjs.com/package/@pushrocks/smartnetwork)
|
||||||
```
|
[](https://snyk.io/test/npm/@pushrocks/smartnetwork)
|
||||||
|
[](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
||||||
### Performing a Traceroute
|
[](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
||||||
|
[](https://prettier.io/)
|
||||||
You can perform a hop-by-hop traceroute to measure latency per hop. Falls back to a single-hop stub if the `traceroute` binary is unavailable.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const hops = await myNetwork.traceroute('google.com', { maxHops: 10, timeout: 5000 });
|
|
||||||
hops.forEach(h =>
|
|
||||||
console.log(`${h.ttl}\t${h.ip}\t${h.rtt === null ? '*' : h.rtt + ' ms'}`),
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
This command will download `@push.rocks/smartnetwork` and add it to your project's `package.json` file.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
In this section, we will dive deep into the capabilities of the `@push.rocks/smartnetwork` package, exploring its various features through TypeScript examples. The package is designed to simplify network diagnostics tasks, including speed tests, port availability checks, ping operations, and more.
|
|
||||||
|
|
||||||
### Basic Setup
|
|
||||||
|
|
||||||
First, import the package into your project:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { SmartNetwork } from '@push.rocks/smartnetwork';
|
import * as smartnetwork from 'smartnetwork';
|
||||||
```
|
const testSmartNetwork = new smartnetwork.SmartNetwork();
|
||||||
|
const run = async () => {
|
||||||
|
// measure average speed over a period of 5 seconds
|
||||||
|
// the structure of speedResult is self explanatory using TypeScript (or the linked TypeDoc above)
|
||||||
|
const speedResult = testSmartNetwork.getSpeed(5000);
|
||||||
|
|
||||||
Then, create an instance of `SmartNetwork`:
|
//
|
||||||
|
const isLocalPortAvailable: boolean = await testSmartNetwork.isLocalPortAvailable(1234);
|
||||||
```typescript
|
const isRemotePortAvailable: boolean = await testSmartNetwork.isRemotePortAvailable(
|
||||||
const myNetwork = new SmartNetwork();
|
'google.com:80'
|
||||||
```
|
);
|
||||||
|
const isRemotePortAvailable: boolean = await testSmartNetwork.isRemotePortAvailable(
|
||||||
### Performing a Speed Test
|
'google.com',
|
||||||
|
80
|
||||||
You can measure the network speed using the `getSpeed` method. It supports optional parameters:
|
);
|
||||||
- `parallelStreams`: number of concurrent streams (default: 1)
|
|
||||||
- `duration`: test duration in seconds (default: fixed segments)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const speedTest = async () => {
|
|
||||||
// Default fixed-segment test
|
|
||||||
let r = await myNetwork.getSpeed();
|
|
||||||
console.log(`Download: ${r.downloadSpeed} Mbps, Upload: ${r.uploadSpeed} Mbps`);
|
|
||||||
|
|
||||||
// Parallel + duration-based test
|
|
||||||
r = await myNetwork.getSpeed({ parallelStreams: 3, duration: 5 });
|
|
||||||
console.log(`Download: ${r.downloadSpeed} Mbps, Upload: ${r.uploadSpeed} Mbps`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
speedTest();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Checking Port Availability Locally
|
For further information read the linked docs at the top of this readme.
|
||||||
|
|
||||||
The `isLocalPortUnused` method allows you to check if a specific port on your local machine is available for use.
|
> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh)
|
||||||
|
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
|
||||||
|
|
||||||
```typescript
|
[](https://maintainedby.lossless.com)
|
||||||
const checkLocalPort = async (port: number) => {
|
|
||||||
const isUnused = await myNetwork.isLocalPortUnused(port);
|
|
||||||
if (isUnused) {
|
|
||||||
console.log(`Port ${port} is available.`);
|
|
||||||
} else {
|
|
||||||
console.log(`Port ${port} is in use.`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkLocalPort(8080); // Example port number
|
|
||||||
```
|
|
||||||
|
|
||||||
### Checking Remote Port Availability
|
|
||||||
|
|
||||||
To verify if a port is available on a remote server, use `isRemotePortAvailable`. You can specify target as `"host:port"` or host plus a numeric port.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Using "host:port"
|
|
||||||
await myNetwork.isRemotePortAvailable('example.com:443');
|
|
||||||
|
|
||||||
// Using host + port
|
|
||||||
await myNetwork.isRemotePortAvailable('example.com', 443);
|
|
||||||
|
|
||||||
// UDP is not supported:
|
|
||||||
try {
|
|
||||||
await myNetwork.isRemotePortAvailable('example.com', { port: 53, protocol: 'udp' });
|
|
||||||
} catch (e) {
|
|
||||||
console.error((e as any).code); // ENOTSUP
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using Ping
|
|
||||||
|
|
||||||
The `ping` method sends ICMP echo requests and optionally repeats them to collect statistics.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Single ping
|
|
||||||
const p1 = await myNetwork.ping('google.com');
|
|
||||||
console.log(`Alive: ${p1.alive}, RTT: ${p1.time} ms`);
|
|
||||||
|
|
||||||
// Multiple pings with statistics
|
|
||||||
const stats = await myNetwork.ping('google.com', { count: 5 });
|
|
||||||
console.log(
|
|
||||||
`min=${stats.min} ms, max=${stats.max} ms, avg=${stats.avg.toFixed(2)} ms, loss=${stats.packetLoss}%`,
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Getting Network Gateways
|
|
||||||
|
|
||||||
You can also retrieve network interfaces (gateways) and determine the default gateway. Caching with TTL is supported via constructor options.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Create with cache TTL of 60 seconds
|
|
||||||
const netCached = new SmartNetwork({ cacheTtl: 60000 });
|
|
||||||
|
|
||||||
// List all interfaces
|
|
||||||
const gateways = await netCached.getGateways();
|
|
||||||
console.log(gateways);
|
|
||||||
|
|
||||||
// Get default gateway
|
|
||||||
const defaultGw = await netCached.getDefaultGateway();
|
|
||||||
console.log(defaultGw);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Discovering Public IP Addresses
|
|
||||||
|
|
||||||
To find out your public IPv4 and IPv6 addresses (with caching):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const publicIps = await netCached.getPublicIps();
|
|
||||||
console.log(`Public IPv4: ${publicIps.v4}`);
|
|
||||||
console.log(`Public IPv6: ${publicIps.v6}`);
|
|
||||||
```
|
|
||||||
|
|
||||||
The `@push.rocks/smartnetwork` package provides an easy-to-use, comprehensive suite of tools for network diagnostics and monitoring, encapsulating complex network operations into simple asynchronous methods. By leveraging TypeScript, developers can benefit from type checking, ensuring that they can work with clear structures and expectations.
|
|
||||||
|
|
||||||
These examples offer a glimpse into the module's utility in real-world scenarios, demonstrating its versatility in handling common network tasks. Whether you're developing a network-sensitive application, diagnosing connectivity issues, or simply curious about your network performance, `@push.rocks/smartnetwork` equips you with the tools you need.
|
|
||||||
### Plugin Architecture
|
|
||||||
|
|
||||||
You can extend `SmartNetwork` with custom plugins by registering them at runtime:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { SmartNetwork } from '@push.rocks/smartnetwork';
|
|
||||||
|
|
||||||
// Define your plugin class or constructor
|
|
||||||
class MyCustomPlugin {
|
|
||||||
// plugin implementation goes here
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register and unregister your plugin by name
|
|
||||||
SmartNetwork.registerPlugin('myPlugin', MyCustomPlugin);
|
|
||||||
// Later, remove it if no longer needed
|
|
||||||
SmartNetwork.unregisterPlugin('myPlugin');
|
|
||||||
```
|
|
||||||
|
|
||||||
Plugins enable you to dynamically augment the core functionality without altering the library's source.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
@ -1,191 +0,0 @@
|
|||||||
import { tap, expect, expectAsync } from '@push.rocks/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).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>((res) => server.close(res));
|
|
||||||
});
|
|
||||||
// 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();
|
|
@ -1,30 +0,0 @@
|
|||||||
import { tap, expect, expectAsync } from '@push.rocks/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 expectAsync(testSmartnetwork.ping('notthere.lossless.com')).property('alive').toBeFalse();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should send a ping to an IP', async () => {
|
|
||||||
await expectAsync(testSmartnetwork.ping('192.168.186.999')).property('alive').toBeFalse();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
51
test/test.ts
51
test/test.ts
@ -1,57 +1,32 @@
|
|||||||
import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@pushrocks/tapbundle';
|
||||||
import * as smartnetwork from '../ts/index.js';
|
import * as smartnetwork from '../ts/index';
|
||||||
|
|
||||||
let testSmartNetwork: smartnetwork.SmartNetwork;
|
let testSmartNetwork: smartnetwork.SmartNetwork;
|
||||||
|
|
||||||
tap.test('should create a valid instance of SmartNetwork', async () => {
|
tap.test('should create a valid instance of SmartNetwork', async () => {
|
||||||
testSmartNetwork = new smartnetwork.SmartNetwork();
|
testSmartNetwork = new smartnetwork.SmartNetwork();
|
||||||
expect(testSmartNetwork).toBeInstanceOf(smartnetwork.SmartNetwork);
|
expect(testSmartNetwork).to.be.instanceOf(smartnetwork.SmartNetwork);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should perform a speedtest', async () => {
|
tap.test('should perform a speedtest', async () => {
|
||||||
const result = await testSmartNetwork.getSpeed();
|
let result = await testSmartNetwork.getSpeed();
|
||||||
console.log(`Download speed for this instance is ${result.downloadSpeed}`);
|
console.log(`Download speed for this instance is ${result.speeds.download}`);
|
||||||
console.log(`Upload speed for this instance is ${result.uploadSpeed}`);
|
console.log(`Upload speed for this instance is ${result.speeds.upload}`);
|
||||||
// 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 () => {
|
tap.test('should determine wether a port is free', async () => {
|
||||||
await expectAsync(testSmartNetwork.isLocalPortUnused(8080)).toBeTrue();
|
await expect(testSmartNetwork.isLocalPortAvailable(8080)).to.eventually.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should scan a port', async () => {
|
tap.test('should scan a port', async () => {
|
||||||
await expectAsync(testSmartNetwork.isRemotePortAvailable('lossless.com:443')).toBeTrue();
|
await expect(testSmartNetwork.isRemotePortAvailable('lossless.com:443')).to.eventually.be.true;
|
||||||
await expectAsync(testSmartNetwork.isRemotePortAvailable('lossless.com', 443)).toBeTrue();
|
await expect(testSmartNetwork.isRemotePortAvailable('lossless.com', 443)).to.be.eventually.true;
|
||||||
await expectAsync(testSmartNetwork.isRemotePortAvailable('lossless.com:444')).toBeFalse();
|
// await expect(testSmartNetwork.isRemotePortAvailable('lossless.com:444')).to.eventually.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should get gateways', async () => {
|
tap.test('should get the default gateways', async () => {
|
||||||
const gateways = await testSmartNetwork.getGateways();
|
const gatewayResult = await testSmartNetwork.getGateways();
|
||||||
console.log(gateways);
|
console.log(gatewayResult);
|
||||||
// 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();
|
tap.start();
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
/**
|
|
||||||
* autocreated commitinfo by @push.rocks/commitinfo
|
|
||||||
*/
|
|
||||||
export const commitinfo = {
|
|
||||||
name: '@push.rocks/smartnetwork',
|
|
||||||
version: '4.0.0',
|
|
||||||
description: 'A toolkit for network diagnostics including speed tests, port availability checks, and more.'
|
|
||||||
}
|
|
20
ts/errors.ts
20
ts/errors.ts
@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
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,4 +1 @@
|
|||||||
export * from './smartnetwork.classes.smartnetwork.js';
|
export * from './smartnetwork.classes.smartnetwork';
|
||||||
export type { SmartNetworkOptions, Hop } from './smartnetwork.classes.smartnetwork.js';
|
|
||||||
export { setLogger, getLogger } from './logging.js';
|
|
||||||
export { NetworkError, TimeoutError } from './errors.js';
|
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
@ -1,307 +0,0 @@
|
|||||||
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,107 +1,55 @@
|
|||||||
import * as plugins from './smartnetwork.plugins.js';
|
import * as plugins from './smartnetwork.plugins';
|
||||||
import { CloudflareSpeed } from './smartnetwork.classes.cloudflarespeed.js';
|
|
||||||
import { getLogger } from './logging.js';
|
export interface ISpeedtestData {
|
||||||
import { NetworkError } from './errors.js';
|
speeds: {
|
||||||
import * as stats from './helpers/stats.js';
|
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
|
* 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;
|
|
||||||
}
|
|
||||||
export class SmartNetwork {
|
export class SmartNetwork {
|
||||||
/** 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
|
* get network speed
|
||||||
* @param opts optional speed test parameters
|
* @param measurementTime
|
||||||
*/
|
*/
|
||||||
public async getSpeed(
|
public async getSpeed(measurementTime = 5000): Promise<ISpeedtestData> {
|
||||||
opts?: { parallelStreams?: number; duration?: number },
|
const done = plugins.smartpromise.defer<ISpeedtestData>();
|
||||||
) {
|
const test = plugins.speedtestNet({ maxTime: measurementTime });
|
||||||
const cloudflareSpeedInstance = new CloudflareSpeed(opts);
|
test.on('data', data => {
|
||||||
return cloudflareSpeedInstance.speedTest();
|
done.resolve(data);
|
||||||
}
|
});
|
||||||
|
test.on('error', err => {
|
||||||
/**
|
done.reject(err);
|
||||||
* Send ICMP pings to a host. Optionally specify count for multiple pings.
|
});
|
||||||
*/
|
return await done.promise;
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -109,17 +57,18 @@ export class SmartNetwork {
|
|||||||
* note: false also resolves with false as argument
|
* note: false also resolves with false as argument
|
||||||
* @param port
|
* @param port
|
||||||
*/
|
*/
|
||||||
/**
|
public 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 doneIpV4 = plugins.smartpromise.defer<boolean>();
|
||||||
const doneIpV6 = 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
|
const net = await import('net'); // creates only one instance of net ;) even on multiple calls
|
||||||
|
|
||||||
// test IPv4 space
|
// test IPv4 space
|
||||||
const ipv4Test = net.createServer();
|
const ipv4Test = net.createServer();
|
||||||
ipv4Test.once('error', () => {
|
ipv4Test.once('error', (err: any) => {
|
||||||
|
if (err.code !== 'EADDRINUSE') {
|
||||||
|
doneIpV4.resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
doneIpV4.resolve(false);
|
doneIpV4.resolve(false);
|
||||||
});
|
});
|
||||||
ipv4Test.once('listening', () => {
|
ipv4Test.once('listening', () => {
|
||||||
@ -134,7 +83,11 @@ export class SmartNetwork {
|
|||||||
|
|
||||||
// test IPv6 space
|
// test IPv6 space
|
||||||
const ipv6Test = net.createServer();
|
const ipv6Test = net.createServer();
|
||||||
ipv6Test.once('error', () => {
|
ipv6Test.once('error', function(err: any) {
|
||||||
|
if (err.code !== 'EADDRINUSE') {
|
||||||
|
doneIpV6.resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
doneIpV6.resolve(false);
|
doneIpV6.resolve(false);
|
||||||
});
|
});
|
||||||
ipv6Test.once('listening', () => {
|
ipv6Test.once('listening', () => {
|
||||||
@ -156,215 +109,32 @@ export class SmartNetwork {
|
|||||||
* checks wether a remote port is available
|
* checks wether a remote port is available
|
||||||
* @param domainArg
|
* @param domainArg
|
||||||
*/
|
*/
|
||||||
/**
|
public async isRemotePortAvailable(domainArg: string, portArg?: number): Promise<boolean> {
|
||||||
* 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>();
|
const done = plugins.smartpromise.defer<boolean>();
|
||||||
plugins.isopen(hostPart, port, (response: Record<string, { isOpen: boolean }>) => {
|
const domainPart = domainArg.split(':')[0];
|
||||||
const info = response[port.toString()];
|
const port = portArg ? portArg : parseInt(domainArg.split(':')[1], 10);
|
||||||
done.resolve(Boolean(info?.isOpen));
|
|
||||||
});
|
|
||||||
last = await done.promise;
|
|
||||||
if (last) return true;
|
|
||||||
}
|
|
||||||
return last;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
plugins.portscanner.checkPortStatus(port, domainPart, (err, status) => {
|
||||||
* List network interfaces (gateways)
|
if (err) {
|
||||||
*/
|
// console.log(err);
|
||||||
public async getGateways(): Promise<Record<string, plugins.os.NetworkInterfaceInfo[]>> {
|
return done.resolve(false);
|
||||||
const fetcher = async () => plugins.os.networkInterfaces();
|
|
||||||
if (this.options.cacheTtl && this.options.cacheTtl > 0) {
|
|
||||||
return this.getCached('gateways', fetcher);
|
|
||||||
}
|
}
|
||||||
return fetcher();
|
if (status === 'open') {
|
||||||
}
|
done.resolve(true);
|
||||||
|
|
||||||
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 () => ({
|
|
||||||
v4: await plugins.publicIp.publicIpv4({ timeout: 1000, onlyHttps: true }).catch(() => null),
|
|
||||||
v6: await plugins.publicIp.publicIpv6({ timeout: 1000, onlyHttps: true }).catch(() => null),
|
|
||||||
});
|
|
||||||
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 dns = await import('dns');
|
|
||||||
const { resolve4, resolve6, resolveMx } = dns.promises;
|
|
||||||
const [A, AAAA, MX] = await Promise.all([
|
|
||||||
resolve4(host).catch(() => []),
|
|
||||||
resolve6(host).catch(() => []),
|
|
||||||
resolveMx(host).catch(() => []),
|
|
||||||
]);
|
|
||||||
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 {
|
} else {
|
||||||
ip = parts[1];
|
done.resolve(false);
|
||||||
const timePart = parts.find((p, i) => i >= 2 && /^\d+(\.\d+)?$/.test(p));
|
|
||||||
rtt = timePart ? parseFloat(timePart) : null;
|
|
||||||
}
|
}
|
||||||
hops.push({ ttl, ip, rtt });
|
});
|
||||||
}
|
const result = await done.promise;
|
||||||
if (hops.length) {
|
return result;
|
||||||
return hops;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// traceroute not available or error: fall through to stub
|
|
||||||
}
|
|
||||||
// fallback stub
|
|
||||||
return [{ ttl: 1, ip: host, rtt: null }];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public async getGateways() {
|
||||||
* Internal caching helper
|
const result = plugins.os.networkInterfaces();
|
||||||
*/
|
return result;
|
||||||
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;
|
public async getDefaultGateWay() {
|
||||||
this.cache.set(key, { value, expiry: now + ttl });
|
// TOTO get default gateways
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,18 @@
|
|||||||
// native scope
|
// native scope
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as https from 'https';
|
|
||||||
import * as perfHooks from 'perf_hooks';
|
|
||||||
|
|
||||||
export { os, https, perfHooks };
|
export {
|
||||||
|
os
|
||||||
|
};
|
||||||
|
|
||||||
// @pushrocks scope
|
// @pushrocks scope
|
||||||
import * as smartping from '@push.rocks/smartping';
|
import * as smartpromise from '@pushrocks/smartpromise';
|
||||||
import * as smartpromise from '@push.rocks/smartpromise';
|
import * as smartstring from '@pushrocks/smartstring';
|
||||||
import * as smartstring from '@push.rocks/smartstring';
|
|
||||||
|
|
||||||
export { smartpromise, smartping, smartstring };
|
export { smartpromise, smartstring };
|
||||||
|
|
||||||
// @third party scope
|
// @third party scope
|
||||||
// @ts-ignore
|
const speedtestNet = require('speedtest-net');
|
||||||
import isopen from 'isopen';
|
import * as portscanner from 'portscanner';
|
||||||
// @ts-ignore
|
|
||||||
import * as publicIp from 'public-ip';
|
|
||||||
import * as systeminformation from 'systeminformation';
|
|
||||||
|
|
||||||
export { isopen, publicIp, systeminformation };
|
export { speedtestNet, portscanner };
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"noImplicitAny": true
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"useDefineForClassFields": false,
|
|
||||||
"target": "ES2022",
|
|
||||||
"module": "NodeNext",
|
|
||||||
"moduleResolution": "NodeNext",
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {}
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"dist_*/**/*.d.ts"
|
|
||||||
]
|
|
||||||
}
|
|
17
tslint.json
Normal file
17
tslint.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user