ism-wifi 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +66 -0
- package/bin/ism-wifi.js +5 -0
- package/package.json +31 -0
- package/src/cli.js +156 -0
- package/src/config.js +115 -0
- package/src/connectivity.js +22 -0
- package/src/constants.js +24 -0
- package/src/index.js +1 -0
- package/src/logger.js +46 -0
- package/src/login.js +38 -0
- package/src/monitor.js +68 -0
- package/src/utils.js +61 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0 (2026-07-26)
|
|
4
|
+
|
|
5
|
+
- Initial release
|
|
6
|
+
- Interactive setup wizard
|
|
7
|
+
- Connectivity monitoring with configurable interval
|
|
8
|
+
- Automatic login to IIT ISM Dhanbad captive portal
|
|
9
|
+
- Exponential backoff retry logic
|
|
10
|
+
- CLI commands: setup, config, login, reset, version, help
|
|
11
|
+
- Cross-platform support (Windows, Linux, macOS)
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ism-wifi contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# ism-wifi
|
|
2
|
+
|
|
3
|
+
Auto-login to the IIT ISM Dhanbad captive Wi-Fi portal.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g ism-wifi
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
ism-wifi
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
On first run you will be prompted for your credentials. The configuration is saved automatically. The tool monitors your internet connectivity and re-authenticates when the connection drops.
|
|
18
|
+
|
|
19
|
+
## CLI Commands
|
|
20
|
+
|
|
21
|
+
| Command | Description |
|
|
22
|
+
|---------|-------------|
|
|
23
|
+
| `ism-wifi` | Start monitoring |
|
|
24
|
+
| `ism-wifi setup` | Re-run configuration setup |
|
|
25
|
+
| `ism-wifi config` | Display current configuration |
|
|
26
|
+
| `ism-wifi login` | Single login attempt |
|
|
27
|
+
| `ism-wifi reset` | Delete saved configuration |
|
|
28
|
+
| `ism-wifi change-interval` | Change the connectivity check interval |
|
|
29
|
+
| `ism-wifi version` | Print version |
|
|
30
|
+
| `ism-wifi help` | Display usage |
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
Stored automatically at:
|
|
35
|
+
|
|
36
|
+
- **Windows:** `C:\Users\<username>\.ism-wifi\config.json`
|
|
37
|
+
- **Linux/macOS:** `~/.ism-wifi/config.json`
|
|
38
|
+
|
|
39
|
+
## How It Works
|
|
40
|
+
|
|
41
|
+
1. Every N seconds (default 30), the tool checks internet connectivity via `clients3.google.com/generate_204`
|
|
42
|
+
2. If connectivity is lost, it posts your credentials to the ISM captive portal
|
|
43
|
+
3. After a 3-second pause, it verifies the connection was restored
|
|
44
|
+
4. If login fails, it retries with exponential backoff (1s, 2s, 4s, 8s, 16s, 30s)
|
|
45
|
+
|
|
46
|
+
## Troubleshooting
|
|
47
|
+
|
|
48
|
+
**Login keeps failing:** Run `ism-wifi setup` to re-enter your credentials.
|
|
49
|
+
|
|
50
|
+
**SSL errors:** The tool disables SSL verification by default for the captive portal endpoint.
|
|
51
|
+
|
|
52
|
+
## FAQ
|
|
53
|
+
|
|
54
|
+
**Q: Does this run in the background?**
|
|
55
|
+
A: No, it runs in the terminal until you press Ctrl+C.
|
|
56
|
+
|
|
57
|
+
**Q: Is my password stored securely?**
|
|
58
|
+
A: It is stored in plain text in your home directory. Use OS-level disk encryption for security.
|
|
59
|
+
|
|
60
|
+
## Contributing
|
|
61
|
+
|
|
62
|
+
Contributions welcome. Please open an issue or PR.
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
package/bin/ism-wifi.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ism-wifi",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Auto-login to IIT ISM Dhanbad captive Wi-Fi portal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ism-wifi": "./bin/ism-wifi.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"package.json",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"CHANGELOG.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test",
|
|
19
|
+
"start": "node bin/ism-wifi.js"
|
|
20
|
+
},
|
|
21
|
+
"keywords": ["iit-ism", "wifi", "captive-portal", "cli"],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@inquirer/prompts": "^7.0.0",
|
|
25
|
+
"chalk": "^5.4.0",
|
|
26
|
+
"commander": "^12.0.0"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/** @module cli */
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander'
|
|
4
|
+
import { readFileSync } from 'fs'
|
|
5
|
+
import { dirname, join } from 'path'
|
|
6
|
+
import { fileURLToPath } from 'url'
|
|
7
|
+
import { loadConfig, runSetup, showConfig, deleteConfig, updateInterval } from './config.js'
|
|
8
|
+
import { startMonitor } from './monitor.js'
|
|
9
|
+
import { login } from './login.js'
|
|
10
|
+
import { sleep } from './utils.js'
|
|
11
|
+
import { ok, fail } from './logger.js'
|
|
12
|
+
import { BANNER, NO_CONFIG_MSG, SETUP_HEADER, CONFIG_SAVED_MSG, MONITORING_MSG } from './constants.js'
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
15
|
+
const pkgPath = join(__dirname, '..', 'package.json')
|
|
16
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ensure config exists. If not, run setup.
|
|
20
|
+
* @returns {Promise<import('./config.js').Config>}
|
|
21
|
+
*/
|
|
22
|
+
async function ensureConfig() {
|
|
23
|
+
const existing = await loadConfig()
|
|
24
|
+
if (existing) return existing
|
|
25
|
+
return runSetup()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Default action — start monitoring.
|
|
30
|
+
*/
|
|
31
|
+
async function defaultAction() {
|
|
32
|
+
const config = await ensureConfig()
|
|
33
|
+
await startMonitor(config)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Setup command — re-run configuration.
|
|
38
|
+
*/
|
|
39
|
+
async function setupAction() {
|
|
40
|
+
await runSetup()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Config command — display current config.
|
|
45
|
+
*/
|
|
46
|
+
async function configAction() {
|
|
47
|
+
const config = await loadConfig()
|
|
48
|
+
if (!config) {
|
|
49
|
+
console.log('\nNo configuration found. Run: ism-wifi setup\n')
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
showConfig(config)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Login command — single login attempt.
|
|
57
|
+
*/
|
|
58
|
+
async function loginAction() {
|
|
59
|
+
const config = await loadConfig()
|
|
60
|
+
if (!config) {
|
|
61
|
+
console.log('\nNo configuration found. Run: ism-wifi setup\n')
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
console.log('')
|
|
65
|
+
const online = await login(config.username, config.password, config.verifySSL)
|
|
66
|
+
if (online) {
|
|
67
|
+
ok('Access Granted — Internet OK')
|
|
68
|
+
} else {
|
|
69
|
+
fail('Login failed')
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Reset command — delete configuration.
|
|
75
|
+
*/
|
|
76
|
+
async function resetAction() {
|
|
77
|
+
const { confirm } = await import('@inquirer/prompts')
|
|
78
|
+
const answer = await confirm({
|
|
79
|
+
message: 'Are you sure you want to delete the saved configuration?',
|
|
80
|
+
default: false,
|
|
81
|
+
})
|
|
82
|
+
if (!answer) {
|
|
83
|
+
console.log('Aborted.')
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
const deleted = await deleteConfig()
|
|
87
|
+
if (deleted) {
|
|
88
|
+
console.log('Configuration deleted.')
|
|
89
|
+
} else {
|
|
90
|
+
console.log('No configuration found.')
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build and return the CLI program.
|
|
96
|
+
* @returns {Command}
|
|
97
|
+
*/
|
|
98
|
+
export function createProgram() {
|
|
99
|
+
const program = new Command()
|
|
100
|
+
|
|
101
|
+
program
|
|
102
|
+
.name('ism-wifi')
|
|
103
|
+
.description('Auto-login to IIT ISM Dhanbad captive Wi-Fi')
|
|
104
|
+
.version(pkg.version)
|
|
105
|
+
.helpOption('-h, --help', 'Display help')
|
|
106
|
+
|
|
107
|
+
program
|
|
108
|
+
.command('setup')
|
|
109
|
+
.description('Run interactive configuration setup')
|
|
110
|
+
.action(setupAction)
|
|
111
|
+
|
|
112
|
+
program
|
|
113
|
+
.command('config')
|
|
114
|
+
.description('Display current configuration')
|
|
115
|
+
.action(configAction)
|
|
116
|
+
|
|
117
|
+
program
|
|
118
|
+
.command('login')
|
|
119
|
+
.description('Perform a single login attempt and exit')
|
|
120
|
+
.action(loginAction)
|
|
121
|
+
|
|
122
|
+
program
|
|
123
|
+
.command('reset')
|
|
124
|
+
.description('Delete saved configuration')
|
|
125
|
+
.action(resetAction)
|
|
126
|
+
|
|
127
|
+
program
|
|
128
|
+
.command('change-interval')
|
|
129
|
+
.description('Change the connectivity check interval')
|
|
130
|
+
.action(() => updateInterval())
|
|
131
|
+
|
|
132
|
+
program
|
|
133
|
+
.command('version')
|
|
134
|
+
.description('Print package version')
|
|
135
|
+
.action(() => console.log(pkg.version))
|
|
136
|
+
|
|
137
|
+
program
|
|
138
|
+
.command('help')
|
|
139
|
+
.description('Display usage information')
|
|
140
|
+
.action(() => program.help())
|
|
141
|
+
|
|
142
|
+
return program
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Main entry point.
|
|
147
|
+
*/
|
|
148
|
+
export async function run() {
|
|
149
|
+
const program = createProgram()
|
|
150
|
+
|
|
151
|
+
if (process.argv.length <= 2) {
|
|
152
|
+
await defaultAction()
|
|
153
|
+
} else {
|
|
154
|
+
program.parse(process.argv)
|
|
155
|
+
}
|
|
156
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/** @module config */
|
|
2
|
+
|
|
3
|
+
import { readFile, writeFile, unlink, mkdir } from 'fs/promises'
|
|
4
|
+
import { existsSync } from 'fs'
|
|
5
|
+
import { input, password, confirm, number } from '@inquirer/prompts'
|
|
6
|
+
import { getConfigDir, getConfigPath, clampDelay, maskPassword } from './utils.js'
|
|
7
|
+
import { BANNER, NO_CONFIG_MSG, SETUP_HEADER, CONFIG_SAVED_MSG, DEFAULT_CHECK_INTERVAL_S, MAX_RETRY_DELAY_S } from './constants.js'
|
|
8
|
+
import { plain, info, ok } from './logger.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} Config
|
|
12
|
+
* @property {string} username
|
|
13
|
+
* @property {string} password
|
|
14
|
+
* @property {number} checkInterval
|
|
15
|
+
* @property {boolean} verifySSL
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load configuration from disk.
|
|
20
|
+
* @returns {Promise<Config|null>}
|
|
21
|
+
*/
|
|
22
|
+
export async function loadConfig() {
|
|
23
|
+
const configPath = getConfigPath()
|
|
24
|
+
try {
|
|
25
|
+
const data = await readFile(configPath, 'utf-8')
|
|
26
|
+
return JSON.parse(data)
|
|
27
|
+
} catch {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Save configuration to disk.
|
|
34
|
+
* @param {Config} config
|
|
35
|
+
* @returns {Promise<void>}
|
|
36
|
+
*/
|
|
37
|
+
export async function saveConfig(config) {
|
|
38
|
+
const dir = getConfigDir()
|
|
39
|
+
await mkdir(dir, { recursive: true })
|
|
40
|
+
await writeFile(getConfigPath(), JSON.stringify(config, null, 4), 'utf-8')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Delete the configuration file.
|
|
45
|
+
* @returns {Promise<boolean>}
|
|
46
|
+
*/
|
|
47
|
+
export async function deleteConfig() {
|
|
48
|
+
const configPath = getConfigPath()
|
|
49
|
+
if (!existsSync(configPath)) return false
|
|
50
|
+
await unlink(configPath)
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Update the check interval interactively.
|
|
56
|
+
* @returns {Promise<void>}
|
|
57
|
+
*/
|
|
58
|
+
export async function updateInterval() {
|
|
59
|
+
const config = await loadConfig()
|
|
60
|
+
if (!config) {
|
|
61
|
+
console.log('\nNo configuration found. Run: ism-wifi setup first.\n')
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const oldInterval = config.checkInterval
|
|
66
|
+
const intervalStr = await input({
|
|
67
|
+
message: `New check interval in seconds (current: ${oldInterval}):`,
|
|
68
|
+
default: String(oldInterval),
|
|
69
|
+
validate: (val) => {
|
|
70
|
+
const num = Number(val)
|
|
71
|
+
if (Number.isNaN(num) || num < 1) return 'Must be a positive number'
|
|
72
|
+
if (num > MAX_RETRY_DELAY_S) return `Maximum is ${MAX_RETRY_DELAY_S} seconds`
|
|
73
|
+
return true
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
config.checkInterval = clampDelay(Number(intervalStr) || oldInterval)
|
|
78
|
+
await saveConfig(config)
|
|
79
|
+
console.log(`\nInterval updated: ${oldInterval} → ${config.checkInterval} seconds\n`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Run interactive first-time (or re-) configuration.
|
|
84
|
+
* @returns {Promise<Config>}
|
|
85
|
+
*/
|
|
86
|
+
export async function runSetup() {
|
|
87
|
+
plain(BANNER)
|
|
88
|
+
plain(NO_CONFIG_MSG)
|
|
89
|
+
info(SETUP_HEADER)
|
|
90
|
+
|
|
91
|
+
const username = await input({ message: 'Username:', required: true })
|
|
92
|
+
const pwd = await password({ message: 'Password:', mask: true, required: true })
|
|
93
|
+
const intervalStr = await input({
|
|
94
|
+
message: 'Connectivity check interval [30]:',
|
|
95
|
+
default: String(DEFAULT_CHECK_INTERVAL_S),
|
|
96
|
+
})
|
|
97
|
+
const checkInterval = clampDelay(Number(intervalStr) || DEFAULT_CHECK_INTERVAL_S)
|
|
98
|
+
|
|
99
|
+
const config = { username, password: pwd, checkInterval, verifySSL: false }
|
|
100
|
+
await saveConfig(config)
|
|
101
|
+
plain(CONFIG_SAVED_MSG)
|
|
102
|
+
return config
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Display current configuration (password masked).
|
|
107
|
+
* @param {Config} config
|
|
108
|
+
*/
|
|
109
|
+
export function showConfig(config) {
|
|
110
|
+
const { username, password, checkInterval, verifySSL } = config
|
|
111
|
+
console.log(`\nUsername : ${username}`)
|
|
112
|
+
console.log(`\nInterval : ${checkInterval} seconds`)
|
|
113
|
+
console.log(`\nSSL Verify : ${verifySSL}`)
|
|
114
|
+
console.log(`\nPassword : ${maskPassword(password)}\n`)
|
|
115
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** @module connectivity */
|
|
2
|
+
|
|
3
|
+
import { CONNECTIVITY_URL, CONNECTIVITY_TIMEOUT_MS } from './constants.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check whether the internet is reachable.
|
|
7
|
+
* Sends a GET to clients3.google.com/generate_204.
|
|
8
|
+
* Returns true only on HTTP 204.
|
|
9
|
+
* @returns {Promise<boolean>}
|
|
10
|
+
*/
|
|
11
|
+
export async function checkConnectivity() {
|
|
12
|
+
try {
|
|
13
|
+
const response = await fetch(CONNECTIVITY_URL, {
|
|
14
|
+
method: 'GET',
|
|
15
|
+
redirect: 'manual',
|
|
16
|
+
signal: AbortSignal.timeout(CONNECTIVITY_TIMEOUT_MS),
|
|
17
|
+
})
|
|
18
|
+
return response.status === 204
|
|
19
|
+
} catch {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** @module constants */
|
|
2
|
+
|
|
3
|
+
export const PORTAL_URL = 'https://netaccess.iitism.ac.in:6082/?url='
|
|
4
|
+
export const CONNECTIVITY_URL = 'https://clients3.google.com/generate_204'
|
|
5
|
+
export const CONFIG_DIR_NAME = '.ism-wifi'
|
|
6
|
+
export const CONFIG_FILE_NAME = 'config.json'
|
|
7
|
+
export const CONNECTIVITY_TIMEOUT_MS = 10_000
|
|
8
|
+
export const VERIFY_WAIT_MS = 3_000
|
|
9
|
+
export const DEFAULT_CHECK_INTERVAL_S = 30
|
|
10
|
+
export const RETRY_DELAYS_S = [1, 2, 4, 8, 16, 30]
|
|
11
|
+
export const MAX_RETRY_DELAY_S = 30
|
|
12
|
+
|
|
13
|
+
export const BANNER = `
|
|
14
|
+
======================================
|
|
15
|
+
ISM WiFi Auto Login
|
|
16
|
+
======================================
|
|
17
|
+
`
|
|
18
|
+
|
|
19
|
+
export const NO_CONFIG_MSG = '\nNo configuration found.\n'
|
|
20
|
+
export const SETUP_HEADER = "Let's configure ISM WiFi."
|
|
21
|
+
export const CONFIG_SAVED_MSG = '\nConfiguration saved.\n'
|
|
22
|
+
export const MONITORING_MSG = '\nStarting monitoring...\n'
|
|
23
|
+
export const STOPPING_MSG = '\nStopping ISM WiFi...\n'
|
|
24
|
+
export const GOODBYE_MSG = 'Goodbye!'
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { run } from './cli.js'
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** @module logger */
|
|
2
|
+
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import { formatTime } from './utils.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Core log function.
|
|
8
|
+
* @param {string} message
|
|
9
|
+
* @param {'ok'|'fail'|'info'|'warn'|'granted'} type
|
|
10
|
+
*/
|
|
11
|
+
function log(message, type) {
|
|
12
|
+
const timestamp = chalk.gray(`[${formatTime()}]`)
|
|
13
|
+
let symbol
|
|
14
|
+
switch (type) {
|
|
15
|
+
case 'ok':
|
|
16
|
+
symbol = chalk.green('✓')
|
|
17
|
+
break
|
|
18
|
+
case 'fail':
|
|
19
|
+
symbol = chalk.red('✗')
|
|
20
|
+
break
|
|
21
|
+
case 'granted':
|
|
22
|
+
symbol = chalk.green('✓')
|
|
23
|
+
break
|
|
24
|
+
default:
|
|
25
|
+
symbol = ''
|
|
26
|
+
}
|
|
27
|
+
console.log(`\n${timestamp} ${symbol} ${message}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @param {string} msg */
|
|
31
|
+
export function ok(msg) { log(msg, 'ok') }
|
|
32
|
+
|
|
33
|
+
/** @param {string} msg */
|
|
34
|
+
export function fail(msg) { log(msg, 'fail') }
|
|
35
|
+
|
|
36
|
+
/** @param {string} msg */
|
|
37
|
+
export function info(msg) { log(msg, 'info') }
|
|
38
|
+
|
|
39
|
+
/** @param {string} msg */
|
|
40
|
+
export function warn(msg) { log(msg, 'warn') }
|
|
41
|
+
|
|
42
|
+
/** @param {string} msg */
|
|
43
|
+
export function granted(msg) { log(msg, 'granted') }
|
|
44
|
+
|
|
45
|
+
/** @param {string} msg */
|
|
46
|
+
export function plain(msg) { console.log(msg) }
|
package/src/login.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** @module login */
|
|
2
|
+
|
|
3
|
+
import { Agent } from 'https'
|
|
4
|
+
import { PORTAL_URL, VERIFY_WAIT_MS } from './constants.js'
|
|
5
|
+
import { sleep } from './utils.js'
|
|
6
|
+
import { checkConnectivity } from './connectivity.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Attempt to log in to the ISM Wi-Fi portal.
|
|
10
|
+
* Posts credentials, waits, then verifies connectivity.
|
|
11
|
+
* @param {string} username
|
|
12
|
+
* @param {string} password
|
|
13
|
+
* @param {boolean} verifySSL
|
|
14
|
+
* @returns {Promise<boolean>}
|
|
15
|
+
*/
|
|
16
|
+
export async function login(username, password, verifySSL) {
|
|
17
|
+
const body = new URLSearchParams({ username, password })
|
|
18
|
+
|
|
19
|
+
const agent = verifySSL ? undefined : new Agent({ rejectUnauthorized: false })
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(PORTAL_URL, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
25
|
+
body: body.toString(),
|
|
26
|
+
signal: AbortSignal.timeout(15_000),
|
|
27
|
+
...(agent ? { agent } : {}),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
if (!response.ok) return false
|
|
31
|
+
|
|
32
|
+
await sleep(VERIFY_WAIT_MS)
|
|
33
|
+
return checkConnectivity()
|
|
34
|
+
} catch {
|
|
35
|
+
await sleep(VERIFY_WAIT_MS)
|
|
36
|
+
return checkConnectivity()
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/monitor.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** @module monitor */
|
|
2
|
+
|
|
3
|
+
import { checkConnectivity } from './connectivity.js'
|
|
4
|
+
import { login } from './login.js'
|
|
5
|
+
import { sleep } from './utils.js'
|
|
6
|
+
import { RETRY_DELAYS_S, MONITORING_MSG, STOPPING_MSG, GOODBYE_MSG } from './constants.js'
|
|
7
|
+
import { ok, fail, info, granted, plain } from './logger.js'
|
|
8
|
+
|
|
9
|
+
let running = false
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Start the monitoring loop.
|
|
13
|
+
* @param {import('./config.js').Config} config
|
|
14
|
+
*/
|
|
15
|
+
export async function startMonitor(config) {
|
|
16
|
+
running = true
|
|
17
|
+
|
|
18
|
+
process.on('SIGINT', handleSigint)
|
|
19
|
+
|
|
20
|
+
plain(MONITORING_MSG)
|
|
21
|
+
|
|
22
|
+
while (running) {
|
|
23
|
+
const online = await checkConnectivity()
|
|
24
|
+
|
|
25
|
+
if (online) {
|
|
26
|
+
ok('Internet OK')
|
|
27
|
+
} else {
|
|
28
|
+
fail('Internet unavailable')
|
|
29
|
+
await attemptLoginWithBackoff(config)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!running) break
|
|
33
|
+
await sleep(config.checkInterval * 1000)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Attempt login with exponential backoff.
|
|
39
|
+
* @param {import('./config.js').Config} config
|
|
40
|
+
*/
|
|
41
|
+
async function attemptLoginWithBackoff(config) {
|
|
42
|
+
for (const delay of RETRY_DELAYS_S) {
|
|
43
|
+
if (!running) return
|
|
44
|
+
|
|
45
|
+
info('Logging in...')
|
|
46
|
+
|
|
47
|
+
const success = await login(config.username, config.password, config.verifySSL)
|
|
48
|
+
|
|
49
|
+
if (success) {
|
|
50
|
+
granted('Access Granted')
|
|
51
|
+
ok('Internet restored')
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fail('Login failed, retrying...')
|
|
56
|
+
await sleep(delay * 1000)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Handle Ctrl+C gracefully.
|
|
62
|
+
*/
|
|
63
|
+
function handleSigint() {
|
|
64
|
+
running = false
|
|
65
|
+
plain(STOPPING_MSG)
|
|
66
|
+
plain(GOODBYE_MSG)
|
|
67
|
+
process.exit(0)
|
|
68
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** @module utils */
|
|
2
|
+
|
|
3
|
+
import { homedir } from 'os'
|
|
4
|
+
import { join } from 'path'
|
|
5
|
+
import { CONFIG_DIR_NAME, CONFIG_FILE_NAME, MAX_RETRY_DELAY_S } from './constants.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sleep for a given number of milliseconds.
|
|
9
|
+
* @param {number} ms
|
|
10
|
+
* @returns {Promise<void>}
|
|
11
|
+
*/
|
|
12
|
+
export function sleep(ms) {
|
|
13
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Format a Date as HH:MM:SS.
|
|
18
|
+
* @param {Date} [date]
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
export function formatTime(date = new Date()) {
|
|
22
|
+
const h = String(date.getHours()).padStart(2, '0')
|
|
23
|
+
const m = String(date.getMinutes()).padStart(2, '0')
|
|
24
|
+
const s = String(date.getSeconds()).padStart(2, '0')
|
|
25
|
+
return `${h}:${m}:${s}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Mask a string, showing only the first character.
|
|
30
|
+
* @param {string} str
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
export function maskPassword(str) {
|
|
34
|
+
if (!str || str.length === 0) return ''
|
|
35
|
+
return str[0] + '*'.repeat(str.length - 1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the config directory path.
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function getConfigDir() {
|
|
43
|
+
return join(homedir(), CONFIG_DIR_NAME)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get the full config file path.
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
export function getConfigPath() {
|
|
51
|
+
return join(getConfigDir(), CONFIG_FILE_NAME)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Clamp a value to max 30 seconds.
|
|
56
|
+
* @param {number} seconds
|
|
57
|
+
* @returns {number}
|
|
58
|
+
*/
|
|
59
|
+
export function clampDelay(seconds) {
|
|
60
|
+
return Math.min(seconds, MAX_RETRY_DELAY_S)
|
|
61
|
+
}
|