├── dist ├── bin │ ├── main.d.ts │ ├── cli.d.ts │ ├── installer.d.ts │ ├── main.js │ └── cli.js ├── lib │ ├── launch.d.ts │ ├── hostRebootScheduler.d.ts │ ├── dialplan.d.ts │ ├── AmiCredential.d.ts │ ├── Tty0tty.d.ts │ ├── api.d.ts │ ├── atBridge.d.ts │ ├── Astdirs.d.ts │ ├── types.js │ ├── confManager.d.ts │ ├── types.d.ts │ ├── InstallOptions.d.ts │ ├── AmiCredential.js │ ├── db.d.ts │ ├── Tty0tty.js │ ├── Astdirs.js │ ├── InstallOptions.js │ ├── dialplan.js │ ├── hostRebootScheduler.js │ ├── atBridge.js │ ├── db.js │ ├── confManager.js │ └── launch.js ├── chan-dongle-extended-client.d.ts └── chan-dongle-extended-client.js ├── res ├── PUTASSET_TOKEN └── app.db ├── LICENSE ├── .gitmodules ├── .github └── FUNDING.yaml ├── src ├── chan-dongle-extended-client.ts ├── lib │ ├── types.ts │ ├── AmiCredential.ts │ ├── hostRebootScheduler.ts │ ├── Tty0tty.ts │ ├── Astdirs.ts │ ├── InstallOptions.ts │ ├── dialplan.ts │ ├── db.ts │ ├── confManager.ts │ ├── atBridge.ts │ ├── launch.ts │ └── api.ts ├── test │ └── db.ts └── bin │ ├── main.ts │ └── cli.ts ├── tsconfig.json ├── .gitignore ├── package.json └── README.md /dist/bin/main.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /dist/bin/cli.d.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "colors"; 3 | -------------------------------------------------------------------------------- /res/PUTASSET_TOKEN: -------------------------------------------------------------------------------- 1 | f364810b37ecb60195adcd0a8bf477fc58b916cf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (c) Copyright 2019 Joseph Garrone, all rights reserved. 2 | -------------------------------------------------------------------------------- /res/app.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garronej/chan-dongle-extended/HEAD/res/app.db -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "pages"] 2 | path = pages 3 | url = https://github.com/garronej/chan-dongle-extended-pages 4 | -------------------------------------------------------------------------------- /dist/lib/launch.d.ts: -------------------------------------------------------------------------------- 1 | export declare function beforeExit(): Promise; 2 | export declare function launch(): Promise; 3 | -------------------------------------------------------------------------------- /dist/lib/hostRebootScheduler.d.ts: -------------------------------------------------------------------------------- 1 | export declare function schedule(): void; 2 | export declare function rebootIfScheduled(): Promise; 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [garronej] 4 | custom: ['https://www.ringerhq.com/experts/garronej'] 5 | -------------------------------------------------------------------------------- /dist/lib/dialplan.d.ts: -------------------------------------------------------------------------------- 1 | import { Ami } from "ts-ami"; 2 | import * as types from "./types"; 3 | export declare function init(modems: types.Modems, ami: Ami, dialplanContext: string, defaultNumber: string): void; 4 | -------------------------------------------------------------------------------- /dist/lib/AmiCredential.d.ts: -------------------------------------------------------------------------------- 1 | export declare namespace AmiCredential { 2 | const file_path: string; 3 | function set(credential: import("ts-ami").Ami.Credential): void; 4 | function get(): import("ts-ami").Ami.Credential; 5 | } 6 | -------------------------------------------------------------------------------- /dist/lib/Tty0tty.d.ts: -------------------------------------------------------------------------------- 1 | export declare class Tty0tty { 2 | readonly leftEnd: string; 3 | readonly rightEnd: string; 4 | static makeFactory(): (() => Tty0tty); 5 | private available; 6 | private constructor(); 7 | release(): void; 8 | } 9 | -------------------------------------------------------------------------------- /dist/chan-dongle-extended-client.d.ts: -------------------------------------------------------------------------------- 1 | import * as apiDeclaration from "chan-dongle-extended-client/dist/lib/apiDeclaration"; 2 | import * as misc from "chan-dongle-extended-client/dist/lib/misc"; 3 | import { types, DongleController } from "chan-dongle-extended-client"; 4 | export { types, apiDeclaration, misc, DongleController }; 5 | -------------------------------------------------------------------------------- /dist/lib/api.d.ts: -------------------------------------------------------------------------------- 1 | import * as types from "./types"; 2 | import { types as dcTypes } from "../chan-dongle-extended-client"; 3 | export declare function beforeExit(): Promise; 4 | export declare namespace beforeExit { 5 | let impl: () => Promise; 6 | } 7 | export declare function launch(modems: types.Modems, staticModuleConfiguration: dcTypes.StaticModuleConfiguration): Promise; 8 | -------------------------------------------------------------------------------- /src/chan-dongle-extended-client.ts: -------------------------------------------------------------------------------- 1 | 2 | // For dist replace ../../chan-dongle-extended-client by chan-dongle-extended-client 3 | 4 | import * as apiDeclaration from "chan-dongle-extended-client/dist/lib/apiDeclaration"; 5 | import * as misc from "chan-dongle-extended-client/dist/lib/misc"; 6 | import { types, DongleController } from "chan-dongle-extended-client"; 7 | 8 | export { types, apiDeclaration, misc, DongleController }; 9 | -------------------------------------------------------------------------------- /dist/lib/atBridge.d.ts: -------------------------------------------------------------------------------- 1 | import { SerialPortExt } from "ts-gsm-modem"; 2 | import { Api as ConfManagerApi } from "./confManager"; 3 | import * as types from "./types"; 4 | export declare function init(modems: types.Modems, chanDongleConfManagerApi: ConfManagerApi): void; 5 | export declare function waitForTerminate(): Promise; 6 | export declare namespace waitForTerminate { 7 | const ports: Set; 8 | const evtAllClosed: import("evt").VoidEvt; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "lib": [ 6 | "es5", 7 | "es2015" 8 | ], 9 | "declaration": true, 10 | "outDir": "./dist", 11 | "strictNullChecks": true, 12 | "sourceMap": false, 13 | "newLine": "LF", 14 | "downlevelIteration": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": false, 17 | "strictPropertyInitialization": true, 18 | "strictFunctionTypes": true 19 | }, 20 | "filesGlob": [ 21 | "./src/**/*.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /dist/lib/Astdirs.d.ts: -------------------------------------------------------------------------------- 1 | export declare type Astdirs = typeof Astdirs.phony; 2 | export declare namespace Astdirs { 3 | const phony: { 4 | astetcdir: string; 5 | astmoddir: string; 6 | astvarlibdir: string; 7 | astdbdir: string; 8 | astkeydir: string; 9 | astdatadir: string; 10 | astagidir: string; 11 | astspooldir: string; 12 | astrundir: string; 13 | astlogdir: string; 14 | astsbindir: string; 15 | }; 16 | function set(asterisk_main_config_file_path: string): void; 17 | function getStatic(asterisk_main_config_file_path: string): Astdirs; 18 | function get(): Astdirs; 19 | } 20 | -------------------------------------------------------------------------------- /dist/lib/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.matchModem = exports.LockedModem = void 0; 4 | var ts_gsm_modem_1 = require("ts-gsm-modem"); 5 | var LockedModem; 6 | (function (LockedModem) { 7 | function match(modem) { 8 | try { 9 | return !!modem.performUnlock; 10 | } 11 | catch (_a) { 12 | return false; 13 | } 14 | } 15 | LockedModem.match = match; 16 | })(LockedModem = exports.LockedModem || (exports.LockedModem = {})); 17 | function matchModem(modem) { 18 | return modem instanceof ts_gsm_modem_1.Modem; 19 | } 20 | exports.matchModem = matchModem; 21 | -------------------------------------------------------------------------------- /dist/lib/confManager.d.ts: -------------------------------------------------------------------------------- 1 | import { types as dcTypes } from "../chan-dongle-extended-client"; 2 | import { Ami } from "ts-ami"; 3 | export declare type DongleConf = { 4 | dongleName: string; 5 | data: string; 6 | audio: string; 7 | }; 8 | export declare type Api = { 9 | staticModuleConfiguration: dcTypes.StaticModuleConfiguration; 10 | reset(): Promise; 11 | addDongle(dongleConf: DongleConf): Promise; 12 | removeDongle(dongleName: string): Promise; 13 | }; 14 | export declare function beforeExit(): Promise; 15 | export declare namespace beforeExit { 16 | let impl: () => Promise; 17 | } 18 | export declare function getApi(ami: Ami): Promise; 19 | -------------------------------------------------------------------------------- /dist/lib/types.d.ts: -------------------------------------------------------------------------------- 1 | import { Modem, PerformUnlock, AtMessage, AccessPoint } from "ts-gsm-modem"; 2 | import { TrackableMap } from "trackable-map"; 3 | export declare type LockedModem = { 4 | imei: string; 5 | manufacturer: string; 6 | model: string; 7 | firmwareVersion: string; 8 | iccid: string | undefined; 9 | pinState: AtMessage.LockedPinState; 10 | tryLeft: number; 11 | performUnlock: PerformUnlock; 12 | terminate: () => Promise; 13 | }; 14 | export declare namespace LockedModem { 15 | function match(modem: any): modem is LockedModem; 16 | } 17 | export declare type Modems = TrackableMap; 18 | export declare function matchModem(modem: any): modem is Modem; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | /dist/test 40 | /.vscode 41 | /working_directory 42 | /pkg_installed.json 43 | /node 44 | -------------------------------------------------------------------------------- /dist/chan-dongle-extended-client.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // For dist replace ../../chan-dongle-extended-client by chan-dongle-extended-client 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | exports.DongleController = exports.misc = exports.apiDeclaration = exports.types = void 0; 5 | var apiDeclaration = require("chan-dongle-extended-client/dist/lib/apiDeclaration"); 6 | exports.apiDeclaration = apiDeclaration; 7 | var misc = require("chan-dongle-extended-client/dist/lib/misc"); 8 | exports.misc = misc; 9 | var chan_dongle_extended_client_1 = require("chan-dongle-extended-client"); 10 | Object.defineProperty(exports, "types", { enumerable: true, get: function () { return chan_dongle_extended_client_1.types; } }); 11 | Object.defineProperty(exports, "DongleController", { enumerable: true, get: function () { return chan_dongle_extended_client_1.DongleController; } }); 12 | -------------------------------------------------------------------------------- /dist/lib/InstallOptions.d.ts: -------------------------------------------------------------------------------- 1 | export declare type InstallOptions = typeof InstallOptions.defaults; 2 | export declare namespace InstallOptions { 3 | const file_path: string; 4 | const defaults: { 5 | asterisk_main_conf: string; 6 | bind_addr: string; 7 | port: number; 8 | disable_sms_dialplan: boolean; 9 | ast_include_dir_path: string; 10 | enable_ast_ami_on_port: number; 11 | assume_chan_dongle_installed: boolean; 12 | ld_library_path_for_asterisk: string; 13 | do_not_create_systemd_conf: boolean; 14 | unix_user: string; 15 | allow_host_reboot_on_dongle_unrecoverable_crash: boolean; 16 | }; 17 | function set(options: Partial): void; 18 | function get(): InstallOptions; 19 | function getDeduced(): { 20 | assume_asterisk_installed: boolean; 21 | overwrite_ami_port_if_enabled: boolean; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /dist/bin/installer.d.ts: -------------------------------------------------------------------------------- 1 | export declare const unix_user_default = "chan_dongle"; 2 | export declare const srv_name = "chan_dongle"; 3 | export declare const working_directory_path: string; 4 | export declare const node_path: string; 5 | export declare const pidfile_path: string; 6 | export declare const db_path: string; 7 | export declare function getIsProd(): boolean; 8 | export declare namespace getIsProd { 9 | let value: boolean | undefined; 10 | } 11 | export declare namespace tty0tty { 12 | const ko_file_path = "/lib/modules/$(uname -r)/kernel/drivers/misc/tty0tty.ko"; 13 | function install(): Promise; 14 | function remove(): void; 15 | function re_install_if_needed(): Promise; 16 | } 17 | export declare function build_ast_cmdline(): string; 18 | export declare namespace build_ast_cmdline { 19 | function build_from_args(ld_library_path_for_asterisk: string, asterisk_main_conf: string): string; 20 | } 21 | export declare function rebuild_node_modules_if_needed(): Promise; 22 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | Modem, 4 | PerformUnlock, 5 | AtMessage, 6 | AccessPoint 7 | } from "ts-gsm-modem"; 8 | 9 | import { TrackableMap } from "trackable-map"; 10 | 11 | export type LockedModem= { 12 | imei: string; 13 | manufacturer: string; 14 | model: string; 15 | firmwareVersion: string; 16 | iccid: string | undefined; 17 | pinState: AtMessage.LockedPinState; 18 | tryLeft: number; 19 | performUnlock: PerformUnlock; 20 | terminate: ()=> Promise; 21 | }; 22 | 23 | export namespace LockedModem { 24 | 25 | export function match(modem: any): modem is LockedModem { 26 | try { 27 | return !!(modem as LockedModem).performUnlock; 28 | } catch{ 29 | return false; 30 | } 31 | } 32 | } 33 | 34 | export type Modems = TrackableMap; 35 | 36 | export function matchModem(modem: any): modem is Modem { 37 | return modem instanceof Modem; 38 | } 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/lib/AmiCredential.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as path from "path"; 3 | import * as fs from "fs"; 4 | import * as scriptLib from "scripting-tools"; 5 | import { working_directory_path } from "../bin/installer"; 6 | import { InstallOptions } from "./InstallOptions"; 7 | 8 | export namespace AmiCredential { 9 | 10 | export const file_path = path.join(working_directory_path,"asterisk_ami_credentials.json"); 11 | 12 | export function set(credential: import("ts-ami").Ami.Credential): void { 13 | 14 | fs.writeFileSync( 15 | file_path, 16 | Buffer.from(JSON.stringify(credential, null, 2), "utf8") 17 | ); 18 | 19 | scriptLib.execSync(`chmod 640 ${file_path}`); 20 | 21 | const unix_user= InstallOptions.get().unix_user; 22 | 23 | scriptLib.execSync(`chown ${unix_user}:${unix_user} ${file_path}`); 24 | 25 | } 26 | 27 | export function get(): import("ts-ami").Ami.Credential { 28 | return require(file_path); 29 | } 30 | 31 | } 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /dist/lib/AmiCredential.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.AmiCredential = void 0; 4 | var path = require("path"); 5 | var fs = require("fs"); 6 | var scriptLib = require("scripting-tools"); 7 | var installer_1 = require("../bin/installer"); 8 | var InstallOptions_1 = require("./InstallOptions"); 9 | var AmiCredential; 10 | (function (AmiCredential) { 11 | AmiCredential.file_path = path.join(installer_1.working_directory_path, "asterisk_ami_credentials.json"); 12 | function set(credential) { 13 | fs.writeFileSync(AmiCredential.file_path, Buffer.from(JSON.stringify(credential, null, 2), "utf8")); 14 | scriptLib.execSync("chmod 640 " + AmiCredential.file_path); 15 | var unix_user = InstallOptions_1.InstallOptions.get().unix_user; 16 | scriptLib.execSync("chown " + unix_user + ":" + unix_user + " " + AmiCredential.file_path); 17 | } 18 | AmiCredential.set = set; 19 | function get() { 20 | return require(AmiCredential.file_path); 21 | } 22 | AmiCredential.get = get; 23 | })(AmiCredential = exports.AmiCredential || (exports.AmiCredential = {})); 24 | -------------------------------------------------------------------------------- /src/lib/hostRebootScheduler.ts: -------------------------------------------------------------------------------- 1 | import * as i from "../bin/installer"; 2 | import * as path from "path"; 3 | import * as fs from "fs"; 4 | import * as scriptTools from "scripting-tools"; 5 | import * as logger from "logger"; 6 | import { InstallOptions } from "./InstallOptions"; 7 | 8 | const debug = logger.debugFactory(); 9 | 10 | const file_path = path.join(i.working_directory_path, "reboot_scheduled"); 11 | 12 | export function schedule() { 13 | 14 | if (InstallOptions.get().allow_host_reboot_on_dongle_unrecoverable_crash) { 15 | 16 | debug("Scheduling host for reboot"); 17 | 18 | fs.writeFileSync(file_path, Buffer.from("1", "utf8")); 19 | 20 | process.emit("beforeExit", process.exitCode = 1); 21 | 22 | } else { 23 | 24 | debug("Install options does not stipulate that this program have permission to restart the host"); 25 | 26 | } 27 | 28 | } 29 | 30 | export async function rebootIfScheduled() { 31 | 32 | if (!fs.existsSync(file_path)) { 33 | return; 34 | } 35 | 36 | fs.unlinkSync(file_path); 37 | 38 | debug("About to restart host"); 39 | 40 | scriptTools.exec("reboot"); 41 | 42 | await new Promise(_resolve => { }); 43 | 44 | } -------------------------------------------------------------------------------- /src/lib/Tty0tty.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from "child_process"; 2 | 3 | export class Tty0tty { 4 | 5 | public static makeFactory(): (() => Tty0tty) { 6 | 7 | const store: Tty0tty[] = (() => { 8 | 9 | //should return 24 10 | let pairCount = (child_process.execSync("ls /dev") 11 | .toString("utf8") 12 | .match(/(tnt[0-9]+)/g)! 13 | .length) / 2 14 | ; 15 | 16 | let out: Tty0tty[] = []; 17 | 18 | let index = 0; 19 | 20 | while (!!(pairCount--)) { 21 | 22 | out.push(new Tty0tty(`/dev/tnt${index++}`, `/dev/tnt${index++}`)); 23 | 24 | } 25 | 26 | return out; 27 | 28 | })(); 29 | 30 | return () => { 31 | 32 | let tty0tty = store.find(({ available }) => available); 33 | 34 | if (!tty0tty) { 35 | throw new Error("No more void modem available"); 36 | } 37 | 38 | tty0tty.available = false; 39 | 40 | return tty0tty; 41 | 42 | }; 43 | 44 | 45 | } 46 | 47 | private available = true; 48 | 49 | private constructor( 50 | public readonly leftEnd: string, 51 | public readonly rightEnd: string 52 | ) { } 53 | 54 | public release(): void { 55 | this.available = true; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /dist/lib/db.d.ts: -------------------------------------------------------------------------------- 1 | import { apiDeclaration } from "../chan-dongle-extended-client"; 2 | import { Message } from "ts-gsm-modem"; 3 | import * as sqliteCustom from "sqlite-custom"; 4 | export declare let _: sqliteCustom.Api; 5 | export declare function beforeExit(): Promise; 6 | export declare namespace beforeExit { 7 | let impl: () => Promise; 8 | } 9 | /** Must be called and awaited before use */ 10 | export declare function launch(): Promise; 11 | /** Debug only */ 12 | export declare function flush(): Promise; 13 | export declare namespace pin { 14 | type AssociatedTo = AssociatedTo.Iccid | AssociatedTo.Imei; 15 | namespace AssociatedTo { 16 | type Iccid = { 17 | iccid: string; 18 | }; 19 | namespace Iccid { 20 | function match(associatedTo: AssociatedTo): associatedTo is Iccid; 21 | } 22 | type Imei = { 23 | imei: string; 24 | }; 25 | namespace Imei { 26 | function match(associatedTo: AssociatedTo): associatedTo is Imei; 27 | } 28 | } 29 | function save(pin: string | undefined, associatedTo: AssociatedTo): Promise; 30 | function get(associatedTo: AssociatedTo): Promise; 31 | } 32 | export declare namespace messages { 33 | function retrieve(params: apiDeclaration.service.getMessages.Params): Promise; 34 | function save(imsi: string, message: Message): Promise; 35 | } 36 | -------------------------------------------------------------------------------- /dist/lib/Tty0tty.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.Tty0tty = void 0; 4 | var child_process = require("child_process"); 5 | var Tty0tty = /** @class */ (function () { 6 | function Tty0tty(leftEnd, rightEnd) { 7 | this.leftEnd = leftEnd; 8 | this.rightEnd = rightEnd; 9 | this.available = true; 10 | } 11 | Tty0tty.makeFactory = function () { 12 | var store = (function () { 13 | //should return 24 14 | var pairCount = (child_process.execSync("ls /dev") 15 | .toString("utf8") 16 | .match(/(tnt[0-9]+)/g) 17 | .length) / 2; 18 | var out = []; 19 | var index = 0; 20 | while (!!(pairCount--)) { 21 | out.push(new Tty0tty("/dev/tnt" + index++, "/dev/tnt" + index++)); 22 | } 23 | return out; 24 | })(); 25 | return function () { 26 | var tty0tty = store.find(function (_a) { 27 | var available = _a.available; 28 | return available; 29 | }); 30 | if (!tty0tty) { 31 | throw new Error("No more void modem available"); 32 | } 33 | tty0tty.available = false; 34 | return tty0tty; 35 | }; 36 | }; 37 | Tty0tty.prototype.release = function () { 38 | this.available = true; 39 | }; 40 | return Tty0tty; 41 | }()); 42 | exports.Tty0tty = Tty0tty; 43 | -------------------------------------------------------------------------------- /src/lib/Astdirs.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as fs from "fs"; 3 | import { working_directory_path } from "../bin/installer"; 4 | 5 | export type Astdirs = typeof Astdirs.phony; 6 | 7 | export namespace Astdirs { 8 | 9 | export const phony = { 10 | "astetcdir": "", 11 | "astmoddir": "", 12 | "astvarlibdir": "", 13 | "astdbdir": "", 14 | "astkeydir": "", 15 | "astdatadir": "", 16 | "astagidir": "", 17 | "astspooldir": "", 18 | "astrundir": "", 19 | "astlogdir": "", 20 | "astsbindir": "" 21 | }; 22 | 23 | const file_path= path.join(working_directory_path, "astdirs.json"); 24 | 25 | let instance: Astdirs | undefined= undefined; 26 | 27 | export function set(asterisk_main_config_file_path: string): void{ 28 | 29 | const astdirs=getStatic(asterisk_main_config_file_path); 30 | 31 | fs.writeFileSync( 32 | file_path, 33 | Buffer.from(JSON.stringify(astdirs, null, 2), "utf8") 34 | ); 35 | 36 | instance= astdirs; 37 | 38 | } 39 | 40 | export function getStatic(asterisk_main_config_file_path: string): Astdirs { 41 | 42 | const raw = fs.readFileSync(asterisk_main_config_file_path).toString("utf8"); 43 | 44 | const astdirs: Astdirs= {...Astdirs.phony }; 45 | 46 | for (let key in Astdirs.phony ) { 47 | 48 | astdirs[key] = raw.match(new RegExp(`^${key}[^\/]+(\/[^\\s]+)\s*$`, "m"))![1]; 49 | 50 | } 51 | 52 | return astdirs; 53 | 54 | } 55 | 56 | export function get(): Astdirs { 57 | 58 | if (!!instance) { 59 | return instance; 60 | } 61 | 62 | instance = require(file_path); 63 | 64 | return get(); 65 | 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chan-dongle-extended", 3 | "version": "5.6.5", 4 | "description": "feature SMS multipart, phonebook memory access, PIN code management", 5 | "main": "./dist/lib/index.js", 6 | "types": "./dist/lib/index.d.ts", 7 | "scripts": { 8 | "partial_install": "npm install --no-package-lock commander@2.19.0 garronej/scripting-tools garronej/chan-dongle-extended-client", 9 | "preinstall": "npm run partial_install && sudo $(which node) ./dist/bin/installer install_prereq", 10 | "postinstall": "cp $(readlink -e $(which node)) ./node", 11 | "tsc": "node ./node_modules/typescript/bin/tsc -p ./tsconfig.json", 12 | "start": "sudo ./node ./dist/bin/main.js", 13 | "release": "sudo $(which node) ./dist/bin/installer release", 14 | "test": "sudo ./node ./dist/test/db" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/garronej/chan-dongle-extended.git" 19 | }, 20 | "author": "Joseph Garrone", 21 | "license": "SEE LICENSE IN LICENSE", 22 | "dependencies": { 23 | "chan-dongle-extended-client": "github:garronej/chan-dongle-extended-client", 24 | "colors": "^1.3.0", 25 | "commander": "^2.19.0", 26 | "evt": "^1.8.8", 27 | "ini-extended": "github:garronej/ini-extended", 28 | "logger": "github:garronej/logger", 29 | "node-gyp": "^3.7.0", 30 | "node-persist": "^2.1.0", 31 | "run-exclusive": "^2.2.14", 32 | "scripting-tools": "github:garronej/scripting-tools", 33 | "sqlite-custom": "github:garronej/sqlite-custom", 34 | "trackable-map": "github:garronej/trackable-map", 35 | "transfer-tools": "garronej/transfer-tools", 36 | "ts-ami": "github:garronej/ts-ami", 37 | "ts-gsm-modem": "github:garronej/ts-gsm-modem", 38 | "ts-sip": "github:garronej/ts-sip" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "^8.9.1", 42 | "@types/node-persist": "0.0.31", 43 | "typescript": "^3.4.5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /dist/lib/Astdirs.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || function () { 3 | __assign = Object.assign || function(t) { 4 | for (var s, i = 1, n = arguments.length; i < n; i++) { 5 | s = arguments[i]; 6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 7 | t[p] = s[p]; 8 | } 9 | return t; 10 | }; 11 | return __assign.apply(this, arguments); 12 | }; 13 | Object.defineProperty(exports, "__esModule", { value: true }); 14 | exports.Astdirs = void 0; 15 | var path = require("path"); 16 | var fs = require("fs"); 17 | var installer_1 = require("../bin/installer"); 18 | var Astdirs; 19 | (function (Astdirs) { 20 | Astdirs.phony = { 21 | "astetcdir": "", 22 | "astmoddir": "", 23 | "astvarlibdir": "", 24 | "astdbdir": "", 25 | "astkeydir": "", 26 | "astdatadir": "", 27 | "astagidir": "", 28 | "astspooldir": "", 29 | "astrundir": "", 30 | "astlogdir": "", 31 | "astsbindir": "" 32 | }; 33 | var file_path = path.join(installer_1.working_directory_path, "astdirs.json"); 34 | var instance = undefined; 35 | function set(asterisk_main_config_file_path) { 36 | var astdirs = getStatic(asterisk_main_config_file_path); 37 | fs.writeFileSync(file_path, Buffer.from(JSON.stringify(astdirs, null, 2), "utf8")); 38 | instance = astdirs; 39 | } 40 | Astdirs.set = set; 41 | function getStatic(asterisk_main_config_file_path) { 42 | var raw = fs.readFileSync(asterisk_main_config_file_path).toString("utf8"); 43 | var astdirs = __assign({}, Astdirs.phony); 44 | for (var key in Astdirs.phony) { 45 | astdirs[key] = raw.match(new RegExp("^" + key + "[^/]+(/[^\\s]+)s*$", "m"))[1]; 46 | } 47 | return astdirs; 48 | } 49 | Astdirs.getStatic = getStatic; 50 | function get() { 51 | if (!!instance) { 52 | return instance; 53 | } 54 | instance = require(file_path); 55 | return get(); 56 | } 57 | Astdirs.get = get; 58 | })(Astdirs = exports.Astdirs || (exports.Astdirs = {})); 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chan-dongle-extended 2 | 3 | [WEBSITE](https://garronej.github.io/chan-dongle-extended-pages/) 4 | 5 | ## Installing node 6 | 7 | Note for 2021: Save yourself some time, at least for developement, **use Debian 9: Stretch** 8 | 9 | ### On armv6 hosts ( raspberry pi 1 ) 10 | ``` bash 11 | # We can't install it from the repository so we have to download it manually: 12 | # ( The download link is on the download page of the node.js website ) 13 | $ cd ~ && wget https://nodejs.org/dist/v8.12.0/node-v8.12.0-linux-armv6l.tar.xz 14 | $ tar xf node-v8.*-linux-armv6l.tar.xz 15 | # Add the path to node bin dir to the PATH, .bashrc: export PATH=/home/pi/node-v8.12.0-linux-armv6l/bin:$PATH 16 | $ source ~/.bashrc 17 | $ sudo su 18 | $ npm install -g npm@latest-5 19 | ``` 20 | 21 | ### On any other host ( armv7, x32, x64 ) 22 | ``` bash 23 | $ curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - 24 | $ sudo apt-get install -y nodejs 25 | $ sudo npm install -g npm@latest-5 26 | ``` 27 | 28 | ## Publish release 29 | To build, bundle and publish a new release for 30 | a specifics arch there is no need to ``npm install`` just clone 31 | this repo then: 32 | 33 | * run ``npm run partial_install`` ( without sudo, only first time ) 34 | * run ``npm run release`` ( without sudo ) 35 | 36 | ## Run local copy of the code for debugging 37 | ``` bash 38 | $ npm install 39 | $ sudo ./node dist/bin/installer install 40 | $ npm start 41 | ``` 42 | 43 | ## Note regarding dependencies 44 | 45 | At the time of writing these lines libudev-dev ( https://packages.debian.org/fr/jessie/libudev-dev ) 46 | is the development package targeting 'libudev1' for jessie, stretch and buster ( oldstable, stable and testing) 47 | Make sure it is still the case when building a new release. 48 | Indeed 'cheery/node-udev' is not recompiled on client's host so if it happen that libudev1 is not available on a 49 | resent release of debian or ubuntu it will not work. 50 | In short make sure that we does not found ourselves in the situation of libssl-dev ( https://packages.debian.org/fr/jessie/libssl-dev ) 51 | Where the target is the packet 'libssl1.0.0' for jessie and 'libssl1.1' for stretch and buster. 52 | 53 | UPDATE: The following note would be purposeful 54 | only if we decided on the future NOT to recompile 55 | 'cheery/node-udev' on the client host but currently 56 | we do so the note can be ignored. 57 | -------------------------------------------------------------------------------- /src/lib/InstallOptions.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as path from "path"; 3 | import * as fs from "fs"; 4 | import { misc as dcMisc } from "../chan-dongle-extended-client"; 5 | import { working_directory_path, unix_user_default } from "../bin/installer"; 6 | 7 | export type InstallOptions = typeof InstallOptions.defaults; 8 | 9 | export namespace InstallOptions { 10 | 11 | export const file_path = path.join(working_directory_path,"install_options.json"); 12 | 13 | export const defaults = { 14 | "asterisk_main_conf": "/etc/asterisk/asterisk.conf", 15 | "bind_addr": "127.0.0.1", 16 | "port": dcMisc.port, 17 | "disable_sms_dialplan": false, 18 | "ast_include_dir_path": "/usr/include", 19 | "enable_ast_ami_on_port": 5038, 20 | "assume_chan_dongle_installed": false, 21 | "ld_library_path_for_asterisk": "", 22 | "do_not_create_systemd_conf": false, 23 | "unix_user": unix_user_default, 24 | "allow_host_reboot_on_dongle_unrecoverable_crash": false 25 | }; 26 | 27 | let _options: Partial | undefined = undefined; 28 | 29 | export function set(options: Partial): void { 30 | 31 | _options = {}; 32 | 33 | for (let key in defaults) { 34 | _options[key] = options[key]; 35 | } 36 | 37 | fs.writeFileSync( 38 | file_path, 39 | Buffer.from(JSON.stringify(_options, null, 2), "utf8") 40 | ); 41 | 42 | } 43 | 44 | export function get(): InstallOptions { 45 | 46 | if (!_options) { 47 | 48 | _options = require(file_path) as Partial; 49 | 50 | } 51 | 52 | const installOptions: InstallOptions = { ...InstallOptions.defaults }; 53 | 54 | for (const key in InstallOptions.defaults) { 55 | 56 | if (_options[key] !== undefined) { 57 | installOptions[key] = _options[key]; 58 | } 59 | 60 | } 61 | 62 | return installOptions; 63 | 64 | } 65 | 66 | export function getDeduced(): { 67 | assume_asterisk_installed: boolean; 68 | overwrite_ami_port_if_enabled: boolean; 69 | } { 70 | 71 | get(); 72 | 73 | const o = _options!; 74 | 75 | return { 76 | "assume_asterisk_installed": !!o.ast_include_dir_path || !!o.asterisk_main_conf || !!o.ld_library_path_for_asterisk, 77 | "overwrite_ami_port_if_enabled": o.enable_ast_ami_on_port !== undefined 78 | }; 79 | 80 | } 81 | 82 | } 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/lib/dialplan.ts: -------------------------------------------------------------------------------- 1 | import { Ami } from "ts-ami"; 2 | import * as tt from "transfer-tools"; 3 | import * as types from "./types"; 4 | import * as logger from "logger"; 5 | 6 | const debug= logger.debugFactory(); 7 | 8 | export function init( 9 | modems: types.Modems, 10 | ami: Ami, 11 | dialplanContext: string, 12 | defaultNumber: string 13 | ) { 14 | 15 | modems.evtCreate.attach(([modem, accessPoint]) => { 16 | 17 | if (!types.matchModem(modem)) { 18 | return; 19 | } 20 | 21 | 22 | const dongleVariables = { 23 | "DONGLENAME": accessPoint.friendlyId, 24 | "DONGLEPROVIDER": `${modem.serviceProviderName}`, 25 | "DONGLEIMEI": modem.imei, 26 | "DONGLEIMSI": modem.imsi, 27 | "DONGLENUMBER": modem.number || defaultNumber 28 | }; 29 | 30 | modem.evtMessage.attach(message => { 31 | 32 | debug("Notify Message"); 33 | 34 | let textSplit = tt.stringTransform.textSplit( 35 | Ami.headerValueMaxLength, 36 | tt.stringTransform.safeBufferFromTo(message.text, "utf8", "base64") 37 | ); 38 | 39 | let variables = { 40 | ...dongleVariables, 41 | "SMS_NUMBER": message.number, 42 | "SMS_DATE": message.date.toISOString(), 43 | "SMS_TEXT_SPLIT_COUNT": `${textSplit.length}`, 44 | "SMS_BASE64": tt.stringTransformExt.b64crop(Ami.headerValueMaxLength, message.text) 45 | }; 46 | 47 | for (let i = 0; i < textSplit.length; i++){ 48 | variables[`SMS_BASE64_PART_${i}`] = textSplit[i]; 49 | } 50 | 51 | ami.originateLocalChannel(dialplanContext, "reassembled-sms", variables); 52 | 53 | }); 54 | 55 | modem.evtMessageStatusReport.attach(statusReport => { 56 | 57 | debug("Notify status report"); 58 | 59 | let { dischargeDate, isDelivered, sendDate, status, recipient } = statusReport; 60 | 61 | let variable = { 62 | ...dongleVariables, 63 | "STATUS_REPORT_DISCHARGE_TIME": isNaN(dischargeDate.getTime()) ? `${dischargeDate}` : dischargeDate.toISOString(), 64 | "STATUS_REPORT_IS_DELIVERED": `${isDelivered}`, 65 | "STATUS_REPORT_SEND_TIME": `${sendDate.getTime()}`, 66 | "STATUS_REPORT_STATUS": status, 67 | "STATUS_REPORT_RECIPIENT": recipient 68 | }; 69 | 70 | ami.originateLocalChannel(dialplanContext, "sms-status-report", variable); 71 | 72 | }); 73 | 74 | }); 75 | 76 | } 77 | 78 | -------------------------------------------------------------------------------- /dist/lib/InstallOptions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || function () { 3 | __assign = Object.assign || function(t) { 4 | for (var s, i = 1, n = arguments.length; i < n; i++) { 5 | s = arguments[i]; 6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 7 | t[p] = s[p]; 8 | } 9 | return t; 10 | }; 11 | return __assign.apply(this, arguments); 12 | }; 13 | Object.defineProperty(exports, "__esModule", { value: true }); 14 | exports.InstallOptions = void 0; 15 | var path = require("path"); 16 | var fs = require("fs"); 17 | var chan_dongle_extended_client_1 = require("../chan-dongle-extended-client"); 18 | var installer_1 = require("../bin/installer"); 19 | var InstallOptions; 20 | (function (InstallOptions) { 21 | InstallOptions.file_path = path.join(installer_1.working_directory_path, "install_options.json"); 22 | InstallOptions.defaults = { 23 | "asterisk_main_conf": "/etc/asterisk/asterisk.conf", 24 | "bind_addr": "127.0.0.1", 25 | "port": chan_dongle_extended_client_1.misc.port, 26 | "disable_sms_dialplan": false, 27 | "ast_include_dir_path": "/usr/include", 28 | "enable_ast_ami_on_port": 5038, 29 | "assume_chan_dongle_installed": false, 30 | "ld_library_path_for_asterisk": "", 31 | "do_not_create_systemd_conf": false, 32 | "unix_user": installer_1.unix_user_default, 33 | "allow_host_reboot_on_dongle_unrecoverable_crash": false 34 | }; 35 | var _options = undefined; 36 | function set(options) { 37 | _options = {}; 38 | for (var key in InstallOptions.defaults) { 39 | _options[key] = options[key]; 40 | } 41 | fs.writeFileSync(InstallOptions.file_path, Buffer.from(JSON.stringify(_options, null, 2), "utf8")); 42 | } 43 | InstallOptions.set = set; 44 | function get() { 45 | if (!_options) { 46 | _options = require(InstallOptions.file_path); 47 | } 48 | var installOptions = __assign({}, InstallOptions.defaults); 49 | for (var key in InstallOptions.defaults) { 50 | if (_options[key] !== undefined) { 51 | installOptions[key] = _options[key]; 52 | } 53 | } 54 | return installOptions; 55 | } 56 | InstallOptions.get = get; 57 | function getDeduced() { 58 | get(); 59 | var o = _options; 60 | return { 61 | "assume_asterisk_installed": !!o.ast_include_dir_path || !!o.asterisk_main_conf || !!o.ld_library_path_for_asterisk, 62 | "overwrite_ami_port_if_enabled": o.enable_ast_ami_on_port !== undefined 63 | }; 64 | } 65 | InstallOptions.getDeduced = getDeduced; 66 | })(InstallOptions = exports.InstallOptions || (exports.InstallOptions = {})); 67 | -------------------------------------------------------------------------------- /src/test/db.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as db from "../lib/db"; 3 | import * as ttTesting from "transfer-tools/dist/lib/testing"; 4 | import assertSame = ttTesting.assertSame; 5 | import { Message } from "ts-gsm-modem"; 6 | 7 | process.on("unhandledRejection", error => { throw error; }); 8 | 9 | db.launch().then(async ()=> { 10 | 11 | console.log("Start tests"); 12 | 13 | await db.flush(); 14 | 15 | const pin = "1234"; 16 | 17 | await (async () => { 18 | 19 | const iccid = "22222222222222222"; 20 | 21 | await db.pin.save("1234", { iccid }); 22 | 23 | console.assert( 24 | pin === await db.pin.get({ iccid }) 25 | ); 26 | 27 | console.assert( 28 | undefined === await db.pin.get({ "iccid": "00000000" }) 29 | ); 30 | 31 | await db.pin.save(undefined, { iccid }); 32 | 33 | console.assert( 34 | undefined === await db.pin.get({ iccid }) 35 | ); 36 | 37 | 38 | })(); 39 | 40 | await (async () => { 41 | 42 | const imei = "111111111111111"; 43 | 44 | await db.pin.save("1234", { imei }); 45 | 46 | console.assert( 47 | pin === await db.pin.get({ imei }) 48 | ); 49 | 50 | console.assert( 51 | undefined === await db.pin.get({ "imei": "00000000" }) 52 | ); 53 | 54 | await db.pin.save(undefined, { imei }); 55 | 56 | console.assert( 57 | undefined === await db.pin.get({ imei }) 58 | ); 59 | 60 | })(); 61 | 62 | console.log("PASS PIN"); 63 | 64 | const imsi_1= "123456789098765"; 65 | const imsi_2= "111111111111111"; 66 | 67 | const number = "0636786385"; 68 | 69 | let timestamp = Date.now(); 70 | 71 | const messages: Message[]= [ 72 | { "date": new Date(timestamp++), number, "text": "foo bar" }, 73 | { "date": new Date(timestamp++), number, "text": "hello word" }, 74 | { "date": new Date(timestamp++), number, "text": "é^à😡" }, 75 | ]; 76 | 77 | for( let message of messages ){ 78 | 79 | await db.messages.save(imsi_1, message); 80 | await db.messages.save(imsi_2, message ); 81 | 82 | } 83 | 84 | assertSame( 85 | await db.messages.retrieve({ 86 | "imsi": imsi_1, 87 | "fromDate": messages[1].date, 88 | "toDate": messages[2].date 89 | }), 90 | [ messages[1], messages[2] ].map( message => ({ ...message, "imsi": imsi_1 })) 91 | ); 92 | 93 | assertSame( 94 | await db.messages.retrieve({ 95 | "imsi": imsi_1, 96 | "fromDate": messages[0].date, 97 | "toDate": messages[2].date, 98 | "flush": true 99 | }), 100 | messages.map( message => ({ ...message, "imsi": imsi_1 })) 101 | ); 102 | 103 | assertSame( 104 | await db.messages.retrieve({ "imsi": imsi_1 }), 105 | [] 106 | ); 107 | 108 | assertSame( 109 | await db.messages.retrieve({}), 110 | messages.map( message => ({ ...message, "imsi": imsi_2 })) 111 | ); 112 | 113 | console.log("PASS MESSAGES"); 114 | 115 | await db.flush(); 116 | 117 | }); 118 | 119 | -------------------------------------------------------------------------------- /dist/lib/dialplan.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || function () { 3 | __assign = Object.assign || function(t) { 4 | for (var s, i = 1, n = arguments.length; i < n; i++) { 5 | s = arguments[i]; 6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 7 | t[p] = s[p]; 8 | } 9 | return t; 10 | }; 11 | return __assign.apply(this, arguments); 12 | }; 13 | var __read = (this && this.__read) || function (o, n) { 14 | var m = typeof Symbol === "function" && o[Symbol.iterator]; 15 | if (!m) return o; 16 | var i = m.call(o), r, ar = [], e; 17 | try { 18 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); 19 | } 20 | catch (error) { e = { error: error }; } 21 | finally { 22 | try { 23 | if (r && !r.done && (m = i["return"])) m.call(i); 24 | } 25 | finally { if (e) throw e.error; } 26 | } 27 | return ar; 28 | }; 29 | Object.defineProperty(exports, "__esModule", { value: true }); 30 | exports.init = void 0; 31 | var ts_ami_1 = require("ts-ami"); 32 | var tt = require("transfer-tools"); 33 | var types = require("./types"); 34 | var logger = require("logger"); 35 | var debug = logger.debugFactory(); 36 | function init(modems, ami, dialplanContext, defaultNumber) { 37 | modems.evtCreate.attach(function (_a) { 38 | var _b = __read(_a, 2), modem = _b[0], accessPoint = _b[1]; 39 | if (!types.matchModem(modem)) { 40 | return; 41 | } 42 | var dongleVariables = { 43 | "DONGLENAME": accessPoint.friendlyId, 44 | "DONGLEPROVIDER": "" + modem.serviceProviderName, 45 | "DONGLEIMEI": modem.imei, 46 | "DONGLEIMSI": modem.imsi, 47 | "DONGLENUMBER": modem.number || defaultNumber 48 | }; 49 | modem.evtMessage.attach(function (message) { 50 | debug("Notify Message"); 51 | var textSplit = tt.stringTransform.textSplit(ts_ami_1.Ami.headerValueMaxLength, tt.stringTransform.safeBufferFromTo(message.text, "utf8", "base64")); 52 | var variables = __assign(__assign({}, dongleVariables), { "SMS_NUMBER": message.number, "SMS_DATE": message.date.toISOString(), "SMS_TEXT_SPLIT_COUNT": "" + textSplit.length, "SMS_BASE64": tt.stringTransformExt.b64crop(ts_ami_1.Ami.headerValueMaxLength, message.text) }); 53 | for (var i = 0; i < textSplit.length; i++) { 54 | variables["SMS_BASE64_PART_" + i] = textSplit[i]; 55 | } 56 | ami.originateLocalChannel(dialplanContext, "reassembled-sms", variables); 57 | }); 58 | modem.evtMessageStatusReport.attach(function (statusReport) { 59 | debug("Notify status report"); 60 | var dischargeDate = statusReport.dischargeDate, isDelivered = statusReport.isDelivered, sendDate = statusReport.sendDate, status = statusReport.status, recipient = statusReport.recipient; 61 | var variable = __assign(__assign({}, dongleVariables), { "STATUS_REPORT_DISCHARGE_TIME": isNaN(dischargeDate.getTime()) ? "" + dischargeDate : dischargeDate.toISOString(), "STATUS_REPORT_IS_DELIVERED": "" + isDelivered, "STATUS_REPORT_SEND_TIME": "" + sendDate.getTime(), "STATUS_REPORT_STATUS": status, "STATUS_REPORT_RECIPIENT": recipient }); 62 | ami.originateLocalChannel(dialplanContext, "sms-status-report", variable); 63 | }); 64 | }); 65 | } 66 | exports.init = init; 67 | -------------------------------------------------------------------------------- /dist/lib/hostRebootScheduler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | Object.defineProperty(exports, "__esModule", { value: true }); 39 | exports.rebootIfScheduled = exports.schedule = void 0; 40 | var i = require("../bin/installer"); 41 | var path = require("path"); 42 | var fs = require("fs"); 43 | var scriptTools = require("scripting-tools"); 44 | var logger = require("logger"); 45 | var InstallOptions_1 = require("./InstallOptions"); 46 | var debug = logger.debugFactory(); 47 | var file_path = path.join(i.working_directory_path, "reboot_scheduled"); 48 | function schedule() { 49 | if (InstallOptions_1.InstallOptions.get().allow_host_reboot_on_dongle_unrecoverable_crash) { 50 | debug("Scheduling host for reboot"); 51 | fs.writeFileSync(file_path, Buffer.from("1", "utf8")); 52 | process.emit("beforeExit", process.exitCode = 1); 53 | } 54 | else { 55 | debug("Install options does not stipulate that this program have permission to restart the host"); 56 | } 57 | } 58 | exports.schedule = schedule; 59 | function rebootIfScheduled() { 60 | return __awaiter(this, void 0, void 0, function () { 61 | return __generator(this, function (_a) { 62 | switch (_a.label) { 63 | case 0: 64 | if (!fs.existsSync(file_path)) { 65 | return [2 /*return*/]; 66 | } 67 | fs.unlinkSync(file_path); 68 | debug("About to restart host"); 69 | scriptTools.exec("reboot"); 70 | return [4 /*yield*/, new Promise(function (_resolve) { })]; 71 | case 1: 72 | _a.sent(); 73 | return [2 /*return*/]; 74 | } 75 | }); 76 | }); 77 | } 78 | exports.rebootIfScheduled = rebootIfScheduled; 79 | -------------------------------------------------------------------------------- /src/bin/main.ts: -------------------------------------------------------------------------------- 1 | import * as scriptLib from "scripting-tools"; 2 | 3 | scriptLib.createService({ 4 | "rootProcess": async () => { 5 | 6 | const [ 7 | { build_ast_cmdline, node_path, pidfile_path, srv_name, tty0tty, rebuild_node_modules_if_needed }, 8 | { InstallOptions }, 9 | hostRebootScheduler, 10 | child_process, 11 | logger, 12 | os 13 | ]= await Promise.all([ 14 | import("./installer"), 15 | import("../lib/InstallOptions"), 16 | import("../lib/hostRebootScheduler"), 17 | import("child_process"), 18 | import("logger"), 19 | import("os") 20 | ]); 21 | 22 | const debug = logger.debugFactory(); 23 | 24 | const config= { 25 | pidfile_path, 26 | srv_name, 27 | "isQuiet": true, 28 | "daemon_unix_user": InstallOptions.get().unix_user, 29 | "daemon_node_path": node_path, 30 | "daemon_restart_after_crash_delay": 5000, 31 | "preForkTask": async () => { 32 | 33 | await hostRebootScheduler.rebootIfScheduled(); 34 | 35 | await tty0tty.re_install_if_needed(); 36 | 37 | await rebuild_node_modules_if_needed(); 38 | 39 | while (true) { 40 | 41 | debug("Checking whether asterisk is fully booted..."); 42 | 43 | const isAsteriskFullyBooted = await new Promise(resolve => 44 | child_process.exec(`${build_ast_cmdline()} -rx "core waitfullybooted"`) 45 | .once("error", () => resolve(false)) 46 | .once("close", code => (code === 0) ? resolve(true) : resolve(false)) 47 | ); 48 | 49 | if (isAsteriskFullyBooted) { 50 | 51 | break; 52 | 53 | } 54 | 55 | debug("... asterisk not yet running ..."); 56 | 57 | await new Promise(resolve => setTimeout(resolve, 10000)); 58 | 59 | } 60 | 61 | debug("...Asterisk is fully booted!"); 62 | 63 | } 64 | }; 65 | 66 | if( os.userInfo().username === InstallOptions.get().unix_user ){ 67 | 68 | config.daemon_restart_after_crash_delay= -1; 69 | 70 | delete config.preForkTask; 71 | 72 | }else{ 73 | 74 | scriptLib.exit_if_not_root(); 75 | 76 | } 77 | 78 | return config; 79 | 80 | }, 81 | "daemonProcess": async () => { 82 | 83 | const [ 84 | path, 85 | fs, 86 | { working_directory_path }, 87 | logger, 88 | { launch, beforeExit } 89 | ] = await Promise.all([ 90 | import("path"), 91 | import("fs"), 92 | import("./installer"), 93 | import("logger"), 94 | import("../lib/launch") 95 | ]); 96 | 97 | const logfile_path = path.join(working_directory_path, "log"); 98 | 99 | return { 100 | "launch": () => { 101 | 102 | logger.file.enable(logfile_path); 103 | 104 | launch(); 105 | 106 | }, 107 | "beforeExitTask": async error => { 108 | 109 | if (!!error) { 110 | 111 | logger.log(error); 112 | 113 | } 114 | 115 | await Promise.all([ 116 | logger.file.terminate().then(() => { 117 | 118 | if (!!error) { 119 | 120 | scriptLib.execSync([ 121 | "mv", 122 | logfile_path, 123 | path.join(path.dirname(logfile_path), "previous_crash.log") 124 | ].join(" ")); 125 | 126 | } else { 127 | 128 | fs.unlinkSync(logfile_path); 129 | 130 | } 131 | 132 | }), 133 | beforeExit() 134 | ]); 135 | 136 | } 137 | }; 138 | 139 | } 140 | }); 141 | 142 | 143 | -------------------------------------------------------------------------------- /src/lib/db.ts: -------------------------------------------------------------------------------- 1 | import { apiDeclaration } from "../chan-dongle-extended-client"; 2 | import { Message } from "ts-gsm-modem"; 3 | import * as sqliteCustom from "sqlite-custom"; 4 | import { db_path } from "../bin/installer"; 5 | import * as logger from "logger"; 6 | 7 | const debug= logger.debugFactory(); 8 | 9 | export let _: sqliteCustom.Api; 10 | 11 | export function beforeExit() { 12 | return beforeExit.impl(); 13 | } 14 | 15 | export namespace beforeExit { 16 | export let impl= ()=> Promise.resolve(); 17 | } 18 | 19 | /** Must be called and awaited before use */ 20 | export async function launch(): Promise { 21 | 22 | _ = await sqliteCustom.connectAndGetApi( 23 | db_path, "HANDLE STRING ENCODING" 24 | ); 25 | 26 | beforeExit.impl= ()=> { 27 | 28 | debug("Closed"); 29 | 30 | return _.close(); 31 | 32 | }; 33 | 34 | } 35 | 36 | 37 | /** Debug only */ 38 | export async function flush(){ 39 | 40 | await _.query([ 41 | "DELETE FROM pin", 42 | "DELETE FROM message" 43 | ].join(";\n")); 44 | 45 | } 46 | 47 | export namespace pin { 48 | 49 | export type AssociatedTo = AssociatedTo.Iccid | AssociatedTo.Imei; 50 | 51 | export namespace AssociatedTo { 52 | 53 | export type Iccid = { iccid: string; }; 54 | 55 | export namespace Iccid { 56 | 57 | export function match(associatedTo: AssociatedTo): associatedTo is Iccid { 58 | return !!(associatedTo as Iccid).iccid; 59 | } 60 | 61 | 62 | } 63 | 64 | export type Imei = { imei: string; }; 65 | 66 | export namespace Imei { 67 | 68 | export function match(associatedTo: AssociatedTo): associatedTo is Imei { 69 | return !Iccid.match(associatedTo); 70 | } 71 | 72 | } 73 | 74 | } 75 | 76 | export async function save( 77 | pin: string | undefined, 78 | associatedTo: AssociatedTo 79 | ) { 80 | 81 | const sql = (() => { 82 | 83 | if (!!pin) { 84 | 85 | if (AssociatedTo.Iccid.match(associatedTo)) { 86 | 87 | return _.buildInsertOrUpdateQueries( 88 | "pin", 89 | { 90 | "iccid": associatedTo.iccid, 91 | "imei": null, 92 | "value": pin 93 | }, 94 | ["iccid"] 95 | ); 96 | 97 | } else { 98 | 99 | return _.buildInsertOrUpdateQueries( 100 | "pin", 101 | { 102 | "iccid": null, 103 | "imei": associatedTo.imei, 104 | "value": pin 105 | }, 106 | ["imei"] 107 | ); 108 | 109 | } 110 | 111 | } else { 112 | 113 | return [ 114 | "DELETE FROM pin WHERE", 115 | AssociatedTo.Iccid.match(associatedTo) ? 116 | `iccid=${associatedTo.iccid}` : 117 | `imei=${associatedTo.imei}` 118 | ].join(" "); 119 | 120 | } 121 | 122 | })(); 123 | 124 | return _.query(sql); 125 | 126 | } 127 | 128 | export async function get( 129 | associatedTo: AssociatedTo 130 | ): Promise { 131 | 132 | const res = await _.query([ 133 | "SELECT value FROM pin WHERE", 134 | AssociatedTo.Iccid.match(associatedTo) ? 135 | `iccid=${associatedTo.iccid}` : 136 | `imei=${associatedTo.imei}` 137 | ].join(" ")); 138 | 139 | if (res.length === 0) { 140 | return undefined; 141 | } else { 142 | 143 | return res[0]["value"]; 144 | 145 | } 146 | 147 | } 148 | 149 | } 150 | 151 | export namespace messages { 152 | 153 | export async function retrieve( 154 | params: apiDeclaration.service.getMessages.Params 155 | ): Promise { 156 | 157 | const fromDate = params.fromDate ? params.fromDate.getTime() : 0; 158 | const toDate = params.toDate ? params.toDate.getTime() : Date.now(); 159 | 160 | const where_clause = [ 161 | `${_.esc(fromDate)} <= date AND date <= ${_.esc(toDate)}`, 162 | !!params.imsi ? ` AND imsi= ${_.esc(params.imsi)}` : "" 163 | ].join(""); 164 | 165 | let sql = [ 166 | `SELECT imsi, date, number, text`, 167 | `FROM message`, 168 | `WHERE ${where_clause}`, 169 | `ORDER BY date ASC;` 170 | ].join("\n"); 171 | 172 | let entries: any[]; 173 | 174 | if (!!params.flush) { 175 | 176 | sql += "\n" + `DELETE FROM message WHERE ${where_clause}`; 177 | 178 | const res= await _.query(sql); 179 | 180 | entries= res[0]; 181 | 182 | }else{ 183 | 184 | entries= await _.query(sql); 185 | 186 | } 187 | 188 | for (const entry of entries) { 189 | entry["date"] = new Date(entry["date"]); 190 | } 191 | 192 | return entries; 193 | 194 | } 195 | 196 | export async function save(imsi: string, message: Message) { 197 | 198 | const sql = _.buildInsertQuery("message", { 199 | imsi, 200 | "date": message.date.getTime(), 201 | "number": message.number, 202 | "text": message.text 203 | }, "THROW ERROR"); 204 | 205 | await _.query(sql); 206 | 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /src/lib/confManager.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { ini } from "ini-extended"; 3 | import * as runExclusive from "run-exclusive"; 4 | import * as path from "path"; 5 | import { Astdirs } from "./Astdirs"; 6 | import { types as dcTypes } from "../chan-dongle-extended-client"; 7 | import { Ami } from "ts-ami"; 8 | import * as logger from "logger"; 9 | 10 | const debug= logger.debugFactory(); 11 | 12 | export type DongleConf= { 13 | dongleName: string; 14 | data: string; 15 | audio: string; 16 | } 17 | 18 | export type Api = { 19 | staticModuleConfiguration: dcTypes.StaticModuleConfiguration; 20 | reset(): Promise; 21 | addDongle(dongleConf: DongleConf): Promise; 22 | removeDongle(dongleName: string): Promise; 23 | }; 24 | 25 | const default_staticModuleConfiguration: dcTypes.StaticModuleConfiguration = { 26 | "general": { 27 | "interval": "10000000", 28 | "jbenable": "no" 29 | }, 30 | "defaults": { 31 | "context": "from-dongle", 32 | "group": "0", 33 | "rxgain": "0", 34 | "txgain": "0", 35 | "autodeletesms": "no", 36 | "resetdongle": "yes", 37 | "u2diag": "-1", 38 | "usecallingpres": "yes", 39 | "callingpres": "allowed_passed_screen", 40 | "disablesms": "yes", 41 | "language": "en", 42 | "smsaspdu": "yes", 43 | "mindtmfgap": "45", 44 | "mindtmfduration": "80", 45 | "mindtmfinterval": "200", 46 | "callwaiting": "auto", 47 | "disable": "no", 48 | "initstate": "start", 49 | "exten": "+12345678987", 50 | "dtmf": "off" 51 | } 52 | }; 53 | 54 | async function loadChanDongleSo(ami: Ami): Promise { 55 | 56 | debug("Checking if chan_dongle.so is loaded..."); 57 | 58 | try { 59 | 60 | let { response } = await ami.postAction("ModuleCheck", { 61 | "module": "chan_dongle.so" 62 | }); 63 | 64 | if (response !== "Success") { 65 | 66 | throw new Error("not loaded"); 67 | 68 | } 69 | 70 | } catch{ 71 | 72 | debug("chan_dongle.so is not loaded, loading manually"); 73 | 74 | await ami.postAction("ModuleLoad", { 75 | "module": "chan_dongle.so", 76 | "loadtype": "load" 77 | }); 78 | 79 | } 80 | 81 | debug("chan_dongle.so is loaded!"); 82 | 83 | } 84 | 85 | export function beforeExit() { 86 | return beforeExit.impl(); 87 | } 88 | 89 | export namespace beforeExit{ 90 | export let impl= ()=> Promise.resolve(); 91 | } 92 | 93 | export async function getApi(ami: Ami): Promise { 94 | 95 | await loadChanDongleSo(ami); 96 | 97 | ami.evt.attach( 98 | ({ event })=> event === "FullyBooted", 99 | ()=> loadChanDongleSo(ami) 100 | ); 101 | 102 | const dongle_conf_path = path.join(Astdirs.get().astetcdir, "dongle.conf"); 103 | 104 | const staticModuleConfiguration: dcTypes.StaticModuleConfiguration = (() => { 105 | 106 | try { 107 | 108 | let { general, defaults } = ini.parseStripWhitespace( 109 | fs.readFileSync(dongle_conf_path).toString("utf8") 110 | ); 111 | 112 | console.assert(!!general && !!defaults); 113 | 114 | defaults.autodeletesms = default_staticModuleConfiguration.defaults["autodeletesms"]; 115 | general.interval = default_staticModuleConfiguration.general["interval"]; 116 | 117 | for (let key in defaults) { 118 | 119 | if (!defaults[key]) { 120 | defaults[key] = default_staticModuleConfiguration.defaults[key]; 121 | } 122 | 123 | } 124 | 125 | return { general, defaults }; 126 | 127 | } catch { 128 | 129 | return default_staticModuleConfiguration; 130 | 131 | } 132 | 133 | })(); 134 | 135 | const state = { ...staticModuleConfiguration }; 136 | 137 | const update = (): Promise => new Promise( 138 | resolve => fs.writeFile( 139 | dongle_conf_path, 140 | Buffer.from(ini.stringify(state), "utf8"), 141 | async error => { 142 | 143 | if (error) { 144 | throw error; 145 | } 146 | 147 | resolve(); 148 | 149 | ami.postAction("DongleReload", { "when": "gracefully" }) 150 | .catch(() => debug("Dongle reload fail, is asterisk running?")) 151 | ; 152 | 153 | } 154 | ) 155 | ); 156 | 157 | const groupRef = runExclusive.createGroupRef(); 158 | 159 | const api: Api = { 160 | staticModuleConfiguration, 161 | "reset": runExclusive.build(groupRef, 162 | async () => { 163 | 164 | debug("reset"); 165 | 166 | for (let key of Object.keys(state).filter(key => key !== "general" && key !== "defaults")) { 167 | 168 | delete state[key]; 169 | 170 | } 171 | 172 | await update(); 173 | 174 | debug("reset complete"); 175 | 176 | } 177 | ), 178 | "addDongle": runExclusive.build(groupRef, 179 | async ({ dongleName, data, audio }: DongleConf) => { 180 | 181 | debug("addDongle", { dongleName, data, audio }); 182 | 183 | state[dongleName] = { audio, data }; 184 | 185 | await update(); 186 | 187 | } 188 | ), 189 | "removeDongle": runExclusive.build(groupRef, 190 | async (dongleName: string) => { 191 | 192 | debug("removeDongle", { dongleName }); 193 | 194 | delete state[dongleName]; 195 | 196 | await update(); 197 | 198 | } 199 | ) 200 | 201 | }; 202 | 203 | beforeExit.impl= ()=> api.reset(); 204 | 205 | api.reset(); 206 | 207 | return api; 208 | 209 | } 210 | 211 | 212 | 213 | 214 | 215 | -------------------------------------------------------------------------------- /src/lib/atBridge.ts: -------------------------------------------------------------------------------- 1 | /*usb<-->accessPoint.dataIfPathtty0tty.leftEnd<-->tty0tty.rightEnd*/ 2 | /*usb<-->/dev/ttyUSB1/dev/tnt0<-->/dev/tnt1*/ 3 | 4 | import { SerialPortExt, AtMessage, Modem, AccessPoint } from "ts-gsm-modem"; 5 | import { Api as ConfManagerApi } from "./confManager"; 6 | import { Tty0tty } from "./Tty0tty"; 7 | import * as logger from "logger"; 8 | import * as types from "./types"; 9 | import { Evt } from "evt"; 10 | import * as runExclusive from "run-exclusive"; 11 | 12 | const debug = logger.debugFactory(); 13 | 14 | function readableAt(raw: string): string { 15 | return "`" + raw.replace(/\r/g, "\\r").replace(/\n/g, "\\n") + "`"; 16 | } 17 | 18 | export function init( 19 | modems: types.Modems, 20 | chanDongleConfManagerApi: ConfManagerApi 21 | ) { 22 | 23 | atBridge.confManagerApi = chanDongleConfManagerApi; 24 | 25 | const tty0ttyFactory = Tty0tty.makeFactory(); 26 | 27 | modems.evtCreate.attach(async ([modem, accessPoint]) => { 28 | 29 | if (types.LockedModem.match(modem)) { 30 | return; 31 | } 32 | 33 | atBridge(accessPoint, modem, tty0ttyFactory()); 34 | 35 | }); 36 | 37 | } 38 | 39 | 40 | export async function waitForTerminate(): Promise { 41 | 42 | if (waitForTerminate.ports.size === 0) { 43 | return Promise.resolve(); 44 | } 45 | 46 | await waitForTerminate.evtAllClosed.waitFor(); 47 | 48 | debug("All virtual serial ports closed"); 49 | 50 | } 51 | 52 | export namespace waitForTerminate { 53 | 54 | export const ports = new Set(); 55 | 56 | export const evtAllClosed = Evt.create(); 57 | 58 | } 59 | 60 | function atBridge( 61 | accessPoint: AccessPoint, 62 | modem: Modem, 63 | tty0tty: Tty0tty 64 | ) { 65 | 66 | const { confManagerApi } = atBridge; 67 | 68 | ( 69 | modem.isGsmConnectivityOk() ? 70 | Promise.resolve() : 71 | modem.evtGsmConnectivityChange.waitFor() 72 | ).then(async function callee() { 73 | 74 | if (!!modem.terminateState) { 75 | return; 76 | } 77 | 78 | debug("connectivity ok running AT+CCWA"); 79 | 80 | const { final } = await modem.runCommand( 81 | `AT+CCWA=0,0,1\r`, 82 | { "recoverable": true } 83 | ); 84 | 85 | if (!!final.isError) { 86 | 87 | debug("Failed to disable call waiting".red, final.raw); 88 | 89 | modem.evtGsmConnectivityChange.attachOnce( 90 | () => modem.isGsmConnectivityOk(), 91 | () => callee() 92 | ); 93 | 94 | return; 95 | 96 | } 97 | 98 | debug("Call waiting successfully disabled".green); 99 | 100 | }); 101 | 102 | 103 | const runCommand = runExclusive.build( 104 | ((...inputs) => modem.runCommand.apply(modem, inputs) 105 | ) as typeof Modem.prototype.runCommand 106 | ); 107 | 108 | atBridge.confManagerApi.addDongle({ 109 | "dongleName": accessPoint.friendlyId, 110 | "data": tty0tty.rightEnd, 111 | "audio": accessPoint.audioIfPath 112 | }); 113 | 114 | const portVirtual = new SerialPortExt( 115 | tty0tty.leftEnd, 116 | { 117 | "baudRate": 115200, 118 | "parser": SerialPortExt.parsers.readline("\r") 119 | } 120 | ); 121 | 122 | waitForTerminate.ports.add(portVirtual); 123 | 124 | portVirtual.once("close", () => { 125 | 126 | waitForTerminate.ports.delete(portVirtual); 127 | 128 | if (waitForTerminate.ports.size === 0) { 129 | 130 | waitForTerminate.evtAllClosed.post(); 131 | 132 | } 133 | 134 | }); 135 | 136 | modem.evtTerminate.attachOnce( 137 | async () => { 138 | 139 | debug("Modem terminate => closing bridge"); 140 | 141 | await confManagerApi.removeDongle(accessPoint.friendlyId); 142 | 143 | if (portVirtual.isOpen()) { 144 | 145 | await new Promise( 146 | resolve => portVirtual.close(() => resolve()) 147 | ); 148 | 149 | } 150 | 151 | tty0tty.release(); 152 | 153 | } 154 | ); 155 | 156 | portVirtual.evtError.attach(serialPortError => { 157 | debug("uncaught error serialPortVirtual", serialPortError); 158 | modem.terminate(); 159 | }); 160 | 161 | const serviceProviderShort = (modem.serviceProviderName || "Unknown SP").substring(0, 15); 162 | 163 | const forwardResp = (rawResp: string, isRespFromModem: boolean, isPing = false) => { 164 | 165 | if (runExclusive.isRunning(runCommand)) { 166 | debug(`Newer command from chanDongle, dropping response ${readableAt(rawResp)}`.red); 167 | return; 168 | } 169 | 170 | if (!isPing) { 171 | 172 | debug(`(AT) ${!isRespFromModem ? "( fake ) " : ""}modem response: ${readableAt(rawResp)}`); 173 | 174 | } 175 | 176 | portVirtual.writeAndDrain(rawResp); 177 | 178 | }; 179 | 180 | portVirtual.on("data", (buff: Buffer) => { 181 | 182 | if (!!modem.terminateState) { 183 | return; 184 | } 185 | 186 | const command = buff.toString("utf8") + "\r"; 187 | 188 | if (command !== "AT\r") { 189 | 190 | debug(`(AT) command from asterisk-chan-dongle: ${readableAt(command)}`); 191 | 192 | } 193 | 194 | const ok = "\r\nOK\r\n"; 195 | 196 | if ( 197 | command === "ATZ\r" || 198 | command.match(/^AT\+CNMI=/) 199 | ) { 200 | 201 | forwardResp(ok, false); 202 | return; 203 | 204 | } else if (command === "AT\r") { 205 | 206 | forwardResp(ok, false, true); 207 | modem.ping(); 208 | return; 209 | 210 | } else if (command === "AT+COPS?\r") { 211 | 212 | forwardResp(`\r\n+COPS: 0,0,"${serviceProviderShort}",0\r\n${ok}`, false); 213 | return; 214 | 215 | } 216 | 217 | if (runExclusive.getQueuedCallCount(runCommand) !== 0) { 218 | 219 | debug([ 220 | `a command is already running and`, 221 | `${modem.runCommand_queuedCallCount} command in stack`, 222 | `flushing the pending command in stack` 223 | ].join("\n").yellow); 224 | 225 | } 226 | 227 | runExclusive.cancelAllQueuedCalls(runCommand); 228 | 229 | runCommand(command, { 230 | "recoverable": true, 231 | "reportMode": AtMessage.ReportMode.NO_DEBUG_INFO, 232 | "retryOnErrors": false 233 | }).then(({ raw }) => forwardResp(raw, true)); 234 | 235 | }); 236 | 237 | portVirtual.once("data", () => 238 | modem.evtUnsolicitedAtMessage.attach( 239 | urc => { 240 | 241 | const doNotForward = ( 242 | urc.id === "CX_BOOT_URC" || 243 | (urc instanceof AtMessage.P_CMTI_URC) && ( 244 | urc.index < 0 || 245 | confManagerApi.staticModuleConfiguration.defaults["disablesms"] === "yes" 246 | ) 247 | ); 248 | 249 | if (!doNotForward) { 250 | 251 | portVirtual.writeAndDrain(urc.raw); 252 | 253 | } 254 | 255 | debug(`(AT) urc: ${readableAt(urc.raw)} ( ${doNotForward ? "NOT forwarded" : "forwarded"} to asterisk-chan-dongle )`); 256 | 257 | } 258 | ) 259 | ); 260 | 261 | }; 262 | 263 | namespace atBridge { 264 | 265 | export let confManagerApi!: ConfManagerApi; 266 | 267 | } 268 | -------------------------------------------------------------------------------- /src/lib/launch.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Modem, InitializationError, UnlockResult, AccessPoint, 3 | ConnectionMonitor, AtMessage, PerformUnlock 4 | } from "ts-gsm-modem"; 5 | import { TrackableMap } from "trackable-map"; 6 | import { AmiCredential } from "./AmiCredential"; 7 | import { Ami } from "ts-ami"; 8 | import * as dialplan from "./dialplan"; 9 | import * as api from "./api"; 10 | import * as atBridge from "./atBridge"; 11 | import { Evt } from "evt"; 12 | import * as confManager from "./confManager"; 13 | import * as types from "./types"; 14 | import * as logger from "logger"; 15 | import * as db from "./db"; 16 | import { InstallOptions } from "./InstallOptions"; 17 | import * as hostRebootScheduler from "./hostRebootScheduler"; 18 | 19 | import { safePr } from "scripting-tools"; 20 | 21 | const debug = logger.debugFactory(); 22 | 23 | const modems: types.Modems = new TrackableMap(); 24 | 25 | const evtScheduleRetry = new Evt<{ 26 | accessPointId: AccessPoint["id"]; 27 | shouldRebootModem: boolean 28 | }>(); 29 | 30 | export async function beforeExit() { 31 | 32 | debug("Start before exit..."); 33 | 34 | if (ConnectionMonitor.hasInstance) { 35 | ConnectionMonitor.getInstance().stop(); 36 | } 37 | 38 | await Promise.all([ 39 | safePr(db.beforeExit()), 40 | safePr(api.beforeExit()), 41 | safePr(atBridge.waitForTerminate()), 42 | Promise.all( 43 | Array.from(modems.values()) 44 | .map(modem => safePr(modem.terminate())) 45 | ), 46 | (async () => { 47 | 48 | await safePr(confManager.beforeExit(), 1500); 49 | 50 | if (Ami.hasInstance) { 51 | await Ami.getInstance().disconnect(); 52 | } 53 | 54 | })() 55 | ]); 56 | 57 | } 58 | 59 | export async function launch() { 60 | 61 | const installOptions = InstallOptions.get(); 62 | 63 | const ami = Ami.getInstance(AmiCredential.get()); 64 | 65 | 66 | 67 | ami.evtTcpConnectionClosed.attachOnce(() => { 68 | 69 | debug("TCP connection with Asterisk manager closed, reboot"); 70 | 71 | process.emit("beforeExit", process.exitCode = 0); 72 | 73 | }); 74 | 75 | const chanDongleConfManagerApi = await confManager.getApi(ami); 76 | 77 | await db.launch(); 78 | 79 | if (!installOptions.disable_sms_dialplan) { 80 | 81 | const { defaults } = chanDongleConfManagerApi.staticModuleConfiguration; 82 | 83 | dialplan.init(modems, ami, defaults["context"], defaults["exten"]); 84 | 85 | } 86 | 87 | await atBridge.init(modems, chanDongleConfManagerApi); 88 | 89 | await api.launch(modems, chanDongleConfManagerApi.staticModuleConfiguration); 90 | 91 | debug("Started"); 92 | 93 | const monitor = ConnectionMonitor.getInstance(); 94 | 95 | monitor.evtModemConnect.attach(accessPoint => { 96 | 97 | debug(`(Monitor) Connect: ${accessPoint}`); 98 | 99 | createModem(accessPoint) 100 | 101 | }); 102 | 103 | monitor.evtModemDisconnect.attach( 104 | accessPoint=> debug(`(Monitor) Disconnect: ${accessPoint}`) 105 | ); 106 | 107 | evtScheduleRetry.attach(({accessPointId, shouldRebootModem}) => { 108 | 109 | const accessPoint = Array.from(monitor.connectedModems).find(({ id })=> id === accessPointId); 110 | 111 | if( !accessPoint ){ 112 | return; 113 | } 114 | 115 | monitor.evtModemDisconnect 116 | .waitFor(ap => ap === accessPoint, 2000) 117 | .catch(() => createModem(accessPoint, shouldRebootModem?"REBOOT":undefined)) 118 | ; 119 | 120 | }); 121 | 122 | }; 123 | 124 | async function createModem(accessPoint: AccessPoint, reboot?: undefined | "REBOOT" ) { 125 | 126 | debug("Create Modem"); 127 | 128 | let modem: Modem; 129 | 130 | try { 131 | 132 | modem = await Modem.create({ 133 | "dataIfPath": accessPoint.dataIfPath, 134 | "unlock": (modemInfo, iccid, pinState, tryLeft, performUnlock, terminate) => 135 | onLockedModem(accessPoint, modemInfo, iccid, pinState, tryLeft, performUnlock, terminate), 136 | "log": logger.log, 137 | "rebootFirst": !!reboot 138 | }); 139 | 140 | } catch (error) { 141 | 142 | onModemInitializationFailed( 143 | accessPoint, 144 | error as InitializationError 145 | ); 146 | 147 | return; 148 | 149 | } 150 | 151 | onModem(accessPoint, modem); 152 | 153 | } 154 | 155 | function onModemInitializationFailed( 156 | accessPoint: AccessPoint, 157 | initializationError: InitializationError 158 | ) { 159 | 160 | modems.delete(accessPoint); 161 | 162 | if( initializationError instanceof InitializationError.DidNotTurnBackOnAfterReboot ){ 163 | 164 | hostRebootScheduler.schedule(); 165 | 166 | return; 167 | 168 | } 169 | 170 | if( initializationError.modemInfos.hasSim === false ){ 171 | 172 | return; 173 | 174 | } 175 | 176 | /* 177 | NOTE: When we get an initialization error 178 | after a modem have been successfully rebooted 179 | do not attempt to reboot it again to prevent 180 | reboot loop that will conduct to the host being 181 | rebooted 182 | */ 183 | evtScheduleRetry.post({ 184 | "accessPointId": accessPoint.id, 185 | "shouldRebootModem": 186 | !initializationError.modemInfos.successfullyRebooted 187 | }); 188 | 189 | } 190 | 191 | async function onLockedModem( 192 | accessPoint: AccessPoint, 193 | modemInfos: { 194 | imei: string; 195 | manufacturer: string; 196 | model: string; 197 | firmwareVersion: string; 198 | }, 199 | iccid: string | undefined, 200 | pinState: AtMessage.LockedPinState, 201 | tryLeft: number, 202 | performUnlock: PerformUnlock, 203 | terminate: () => Promise 204 | ) { 205 | 206 | debug("onLockedModem", { ...modemInfos, iccid, pinState, tryLeft }); 207 | 208 | const associatedTo: db.pin.AssociatedTo = !!iccid ? ({ iccid }) : ({ "imei": modemInfos.imei }); 209 | 210 | const pin = await db.pin.get(associatedTo); 211 | 212 | if (!!pin) { 213 | 214 | if (pinState === "SIM PIN" && tryLeft === 3) { 215 | 216 | let unlockResult = await performUnlock(pin); 217 | 218 | if (unlockResult.success) { 219 | return; 220 | } 221 | 222 | pinState = unlockResult.pinState; 223 | tryLeft = unlockResult.tryLeft; 224 | 225 | } else { 226 | 227 | await db.pin.save(undefined, associatedTo); 228 | 229 | } 230 | 231 | } 232 | 233 | 234 | const lockedModem: types.LockedModem = { 235 | "imei": modemInfos.imei, 236 | "manufacturer": modemInfos.manufacturer, 237 | "model": modemInfos.model, 238 | "firmwareVersion": modemInfos.firmwareVersion, 239 | iccid, pinState, tryLeft, 240 | "performUnlock": async (...inputs) => { 241 | //NOTE: PerformUnlock throw error if modem disconnect during unlock 242 | 243 | modems.delete(accessPoint); 244 | 245 | let pin: string; 246 | let puk: string | undefined; 247 | let unlockResult: UnlockResult; 248 | 249 | if (!inputs[1]) { 250 | 251 | pin = inputs[0]; 252 | puk = undefined; 253 | 254 | unlockResult = await performUnlock(pin); 255 | 256 | } else { 257 | 258 | pin = inputs[1]; 259 | puk = inputs[0]; 260 | 261 | unlockResult = await performUnlock(puk!, pin); 262 | 263 | } 264 | 265 | if (unlockResult.success) { 266 | 267 | debug(`Persistent storing of pin: ${pin}`); 268 | 269 | await db.pin.save(pin, associatedTo); 270 | 271 | } else { 272 | 273 | await db.pin.save(undefined, associatedTo); 274 | 275 | lockedModem.pinState = unlockResult.pinState; 276 | lockedModem.tryLeft = unlockResult.tryLeft; 277 | 278 | modems.set(accessPoint, lockedModem); 279 | 280 | } 281 | 282 | return unlockResult; 283 | 284 | }, 285 | terminate 286 | }; 287 | 288 | modems.set(accessPoint, lockedModem); 289 | 290 | } 291 | 292 | function onModem( 293 | accessPoint: AccessPoint, 294 | modem: Modem 295 | ) { 296 | 297 | debug("Modem successfully initialized".green); 298 | 299 | const initializationTime = Date.now(); 300 | 301 | modem.evtTerminate.attachOnce( 302 | error => { 303 | 304 | modems.delete(accessPoint); 305 | 306 | debug(`Modem terminate... ${error ? error.message : "No internal error"}`); 307 | 308 | /** 309 | * NOTE: Preventing Modem reboot loop by allowing 310 | * modem to be rebooted at most once every hour. 311 | */ 312 | evtScheduleRetry.post({ 313 | "accessPointId": accessPoint.id, 314 | "shouldRebootModem": ( 315 | !!modem["__api_rebootDongle_called__"] || 316 | ( 317 | !!error && 318 | ( 319 | !modem.successfullyRebooted || 320 | Date.now() - initializationTime > 3600000 321 | ) 322 | ) 323 | ) 324 | }); 325 | 326 | } 327 | ); 328 | 329 | modems.set(accessPoint, modem); 330 | 331 | } 332 | -------------------------------------------------------------------------------- /src/bin/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as program from "commander"; 4 | import { DongleController as Dc, types as dcTypes } from "../chan-dongle-extended-client"; 5 | import * as storage from "node-persist"; 6 | import { InstallOptions } from "../lib/InstallOptions"; 7 | import * as path from "path"; 8 | import * as os from "os"; 9 | import "colors"; 10 | 11 | program 12 | .command("list") 13 | .description("List dongles") 14 | .action(async () => { 15 | 16 | let dc = await getDcInstance(); 17 | 18 | try { 19 | 20 | console.log(JSON.stringify(dc.dongles.valuesAsArray(), null, 2)); 21 | 22 | process.exit(0); 23 | 24 | } catch (error) { 25 | 26 | console.log(error.message.red); 27 | 28 | process.exit(1); 29 | 30 | } 31 | 32 | }); 33 | 34 | program 35 | .command("select [imei]") 36 | .description([ 37 | "Select dongle for subsequent calls", 38 | " ( to avoid having to set --imei on each command)" 39 | ].join("")) 40 | .action(async (imei: string) => { 41 | 42 | if (!imei) { 43 | console.log("Error: command malformed".red); 44 | process.exit(-1); 45 | } 46 | 47 | let dc = await getDcInstance(); 48 | 49 | if (!dc.dongles.has(imei)) { 50 | console.log("Error: no such dongle connected".red); 51 | process.exit(-1); 52 | } 53 | 54 | await selected_dongle.set(imei); 55 | 56 | console.log(`Dongle ${imei} selected`); 57 | 58 | process.exit(0); 59 | 60 | 61 | }); 62 | 63 | program 64 | .command("unlock") 65 | .description("provide SIM PIN or PUK to unlock dongle") 66 | .option("-i, --imei [imei]", "IMEI of the dongle") 67 | .option("-p, --pin [pin]", "SIM PIN ( 4 digits )") 68 | .option("--puk [puk-newPin]", "PUK ( 8 digits ) and new PIN eg. --puk 12345678-0000") 69 | .action(async options => { 70 | 71 | let imei = await selected_dongle.get(options); 72 | 73 | if (!options.pin && !options.puk) { 74 | console.log("Error: command malformed".red); 75 | console.log(options.optionHelp()); 76 | process.exit(-1); 77 | } 78 | 79 | let dc = await getDcInstance(); 80 | 81 | let unlockResult; 82 | 83 | try { 84 | 85 | if (options.pin) { 86 | unlockResult = await dc.unlock(imei, options.pin); 87 | } else { 88 | 89 | let match = (options.puk as string).match(/^([0-9]{8})-([0-9]{4})$/); 90 | 91 | if (!match) { 92 | console.log("Error: puk-newPin malformed".red); 93 | console.log(options.optionHelp()); 94 | process.exit(-1); 95 | return; 96 | } 97 | 98 | let puk = match[1]; 99 | let newPin = match[2]; 100 | 101 | unlockResult = await dc.unlock(imei, puk, newPin); 102 | 103 | } 104 | 105 | } catch (error) { 106 | 107 | console.log(error.message.red); 108 | process.exit(1); 109 | return; 110 | 111 | } 112 | 113 | console.log(JSON.stringify(unlockResult, null, 2)); 114 | 115 | process.exit(0); 116 | 117 | }); 118 | 119 | program 120 | .command("reboot") 121 | .description("Send AT command to reboot a dongle") 122 | .option("-i, --imei [imei]", "IMEI of the dongle") 123 | .action(async options => { 124 | 125 | const imei = await selected_dongle.get(options); 126 | 127 | const dc = await getDcInstance(); 128 | 129 | try { 130 | 131 | await dc.rebootDongle(imei); 132 | 133 | } catch (error) { 134 | 135 | console.log(error.message.red); 136 | process.exit(1); 137 | return; 138 | 139 | } 140 | 141 | console.log("OK".green); 142 | 143 | process.exit(0); 144 | 145 | }); 146 | 147 | 148 | program 149 | .command("send") 150 | .description("Send SMS") 151 | .option("-i, --imei [imei]", "IMEI of the dongle") 152 | .option("-n, --number [number]", "target phone number") 153 | .option("-t, --text [text]", "Text of the message") 154 | .option("-T, --text-base64 [textBase64]", "Text Base64 encoded") 155 | .action(async options => { 156 | 157 | let { number, text, textBase64 } = options; 158 | 159 | if (!number || (!text && !textBase64)) { 160 | console.log("Error: command malformed".red); 161 | console.log(options.optionHelp()); 162 | process.exit(-1); 163 | } 164 | 165 | let imei = await selected_dongle.get(options); 166 | 167 | let dc = await getDcInstance(); 168 | 169 | if (textBase64) { 170 | 171 | const st = await import("transfer-tools/dist/lib/stringTransform"); 172 | 173 | text = st.safeBufferFromTo(textBase64, "base64", "utf8"); 174 | 175 | } else { 176 | 177 | text = JSON.parse(`"${text}"`); 178 | 179 | } 180 | 181 | try { 182 | 183 | let sendMessageResult = await dc.sendMessage(imei, number, text); 184 | 185 | if (sendMessageResult.success) { 186 | console.log(sendMessageResult.sendDate.getTime()); 187 | process.exit(0); 188 | } else { 189 | console.log(0); 190 | process.exit(1); 191 | } 192 | 193 | } catch (error) { 194 | 195 | console.log(error.message.red); 196 | process.exit(1); 197 | return; 198 | } 199 | 200 | 201 | }); 202 | 203 | program 204 | .command("messages") 205 | .description("Get received SMS") 206 | .option("-f, --flush", "Whether or not erasing retrieved messages") 207 | .action(async options => { 208 | 209 | const flush = options.flush === true; 210 | 211 | let dc = await getDcInstance(); 212 | 213 | try { 214 | 215 | const messages = await dc.getMessages({ flush }); 216 | 217 | console.log(JSON.stringify(messages, null, 2)); 218 | 219 | process.exit(0); 220 | 221 | } catch (error) { 222 | 223 | console.log(error.message.red); 224 | process.exit(1); 225 | return; 226 | 227 | } 228 | 229 | 230 | }); 231 | 232 | program 233 | .command("new-contact") 234 | .description("Store new contact in phonebook memory") 235 | .option("-i, --imei [imei]", "IMEI of the dongle") 236 | .option("--name [name]", "Contact's name") 237 | .option("--number [number]", "Contact's number") 238 | .action(async options => { 239 | 240 | let { name, number } = options; 241 | 242 | if (!name || !number) { 243 | console.log("Error: provide at least one of number or name".red); 244 | console.log(options.optionHelp()); 245 | process.exit(-1); 246 | } 247 | 248 | const imei = await selected_dongle.get(options); 249 | 250 | const dc = await getDcInstance(); 251 | 252 | const dongle = dc.usableDongles.get(imei); 253 | 254 | if( !dongle ){ 255 | 256 | console.log("Error: selected dongle is disconnected or locked".red); 257 | process.exit(1); 258 | return; 259 | 260 | } 261 | 262 | let contact: dcTypes.Sim.Contact; 263 | 264 | try{ 265 | 266 | contact= await dc.createContact(dongle.sim.imsi, number, name); 267 | 268 | } catch(error){ 269 | 270 | console.log(error.message.red); 271 | process.exit(1); 272 | return; 273 | 274 | } 275 | 276 | console.log(JSON.stringify(contact, null, 2)); 277 | 278 | process.exit(0); 279 | 280 | }); 281 | 282 | 283 | program 284 | .command("delete-contact") 285 | .description("Delete a contact from phonebook memory") 286 | .option("-i, --imei [imei]", "IMEI of the dongle") 287 | .option("--index [index]", "Contact's index") 288 | .action(async options => { 289 | 290 | const index = parseInt(options["index"]); 291 | 292 | if ( isNaN(index) ) { 293 | console.log("Error: command malformed".red); 294 | console.log(options.optionHelp()); 295 | process.exit(-1); 296 | } 297 | 298 | const imei = await selected_dongle.get(options); 299 | 300 | const dc = await getDcInstance(); 301 | 302 | const dongle = dc.usableDongles.get(imei); 303 | 304 | if( !dongle ){ 305 | 306 | console.log("Error: selected dongle is disconnected or locked".red); 307 | process.exit(1); 308 | return; 309 | 310 | } 311 | 312 | try{ 313 | 314 | await dc.deleteContact(dongle.sim.imsi, index); 315 | 316 | } catch(error){ 317 | 318 | console.log(error.message.red); 319 | process.exit(1); 320 | return; 321 | 322 | } 323 | 324 | console.log(`Contact at index ${index} in SIM memory have been deleted successfully.`); 325 | 326 | process.exit(0); 327 | 328 | }); 329 | 330 | async function getDcInstance(): Promise { 331 | 332 | const { bind_addr, port }= InstallOptions.get(); 333 | 334 | let dc = Dc.getInstance(bind_addr, port); 335 | 336 | try { 337 | 338 | await dc.prInitialization; 339 | 340 | } catch { 341 | 342 | console.log("dongle-extended is not running".red); 343 | process.exit(1); 344 | 345 | } 346 | 347 | return dc; 348 | 349 | } 350 | 351 | namespace selected_dongle { 352 | 353 | const get_storage_user_path = () => path.join("/var/tmp", `${os.userInfo().username}_selected_dongle`); 354 | 355 | export async function get(options: { imei: string | undefined }): Promise { 356 | 357 | if (options.imei) { 358 | return options.imei; 359 | } else { 360 | 361 | await storage.init({ "dir": get_storage_user_path() }); 362 | 363 | let imei = await storage.getItem("cli_imei"); 364 | 365 | if (!imei) { 366 | console.log("Error: No dongle selected"); 367 | process.exit(-1); 368 | } 369 | 370 | return imei; 371 | 372 | } 373 | 374 | } 375 | 376 | export async function set(imei: string) { 377 | 378 | await storage.init({ "dir": get_storage_user_path() }); 379 | 380 | await storage.setItem("cli_imei", imei); 381 | 382 | } 383 | 384 | 385 | } 386 | 387 | if (require.main === module) { 388 | 389 | process.once("unhandledRejection", error => { throw error; }); 390 | 391 | program.parse(process.argv); 392 | 393 | } 394 | -------------------------------------------------------------------------------- /dist/bin/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | var __read = (this && this.__read) || function (o, n) { 39 | var m = typeof Symbol === "function" && o[Symbol.iterator]; 40 | if (!m) return o; 41 | var i = m.call(o), r, ar = [], e; 42 | try { 43 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); 44 | } 45 | catch (error) { e = { error: error }; } 46 | finally { 47 | try { 48 | if (r && !r.done && (m = i["return"])) m.call(i); 49 | } 50 | finally { if (e) throw e.error; } 51 | } 52 | return ar; 53 | }; 54 | Object.defineProperty(exports, "__esModule", { value: true }); 55 | var scriptLib = require("scripting-tools"); 56 | scriptLib.createService({ 57 | "rootProcess": function () { return __awaiter(void 0, void 0, void 0, function () { 58 | var _a, _b, build_ast_cmdline, node_path, pidfile_path, srv_name, tty0tty, rebuild_node_modules_if_needed, InstallOptions, hostRebootScheduler, child_process, logger, os, debug, config; 59 | return __generator(this, function (_c) { 60 | switch (_c.label) { 61 | case 0: return [4 /*yield*/, Promise.all([ 62 | Promise.resolve().then(function () { return require("./installer"); }), 63 | Promise.resolve().then(function () { return require("../lib/InstallOptions"); }), 64 | Promise.resolve().then(function () { return require("../lib/hostRebootScheduler"); }), 65 | Promise.resolve().then(function () { return require("child_process"); }), 66 | Promise.resolve().then(function () { return require("logger"); }), 67 | Promise.resolve().then(function () { return require("os"); }) 68 | ])]; 69 | case 1: 70 | _a = __read.apply(void 0, [_c.sent(), 6]), _b = _a[0], build_ast_cmdline = _b.build_ast_cmdline, node_path = _b.node_path, pidfile_path = _b.pidfile_path, srv_name = _b.srv_name, tty0tty = _b.tty0tty, rebuild_node_modules_if_needed = _b.rebuild_node_modules_if_needed, InstallOptions = _a[1].InstallOptions, hostRebootScheduler = _a[2], child_process = _a[3], logger = _a[4], os = _a[5]; 71 | debug = logger.debugFactory(); 72 | config = { 73 | pidfile_path: pidfile_path, 74 | srv_name: srv_name, 75 | "isQuiet": true, 76 | "daemon_unix_user": InstallOptions.get().unix_user, 77 | "daemon_node_path": node_path, 78 | "daemon_restart_after_crash_delay": 5000, 79 | "preForkTask": function () { return __awaiter(void 0, void 0, void 0, function () { 80 | var isAsteriskFullyBooted; 81 | return __generator(this, function (_a) { 82 | switch (_a.label) { 83 | case 0: return [4 /*yield*/, hostRebootScheduler.rebootIfScheduled()]; 84 | case 1: 85 | _a.sent(); 86 | return [4 /*yield*/, tty0tty.re_install_if_needed()]; 87 | case 2: 88 | _a.sent(); 89 | return [4 /*yield*/, rebuild_node_modules_if_needed()]; 90 | case 3: 91 | _a.sent(); 92 | _a.label = 4; 93 | case 4: 94 | if (!true) return [3 /*break*/, 7]; 95 | debug("Checking whether asterisk is fully booted..."); 96 | return [4 /*yield*/, new Promise(function (resolve) { 97 | return child_process.exec(build_ast_cmdline() + " -rx \"core waitfullybooted\"") 98 | .once("error", function () { return resolve(false); }) 99 | .once("close", function (code) { return (code === 0) ? resolve(true) : resolve(false); }); 100 | })]; 101 | case 5: 102 | isAsteriskFullyBooted = _a.sent(); 103 | if (isAsteriskFullyBooted) { 104 | return [3 /*break*/, 7]; 105 | } 106 | debug("... asterisk not yet running ..."); 107 | return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 10000); })]; 108 | case 6: 109 | _a.sent(); 110 | return [3 /*break*/, 4]; 111 | case 7: 112 | debug("...Asterisk is fully booted!"); 113 | return [2 /*return*/]; 114 | } 115 | }); 116 | }); } 117 | }; 118 | if (os.userInfo().username === InstallOptions.get().unix_user) { 119 | config.daemon_restart_after_crash_delay = -1; 120 | delete config.preForkTask; 121 | } 122 | else { 123 | scriptLib.exit_if_not_root(); 124 | } 125 | return [2 /*return*/, config]; 126 | } 127 | }); 128 | }); }, 129 | "daemonProcess": function () { return __awaiter(void 0, void 0, void 0, function () { 130 | var _a, path, fs, working_directory_path, logger, _b, launch, beforeExit, logfile_path; 131 | return __generator(this, function (_c) { 132 | switch (_c.label) { 133 | case 0: return [4 /*yield*/, Promise.all([ 134 | Promise.resolve().then(function () { return require("path"); }), 135 | Promise.resolve().then(function () { return require("fs"); }), 136 | Promise.resolve().then(function () { return require("./installer"); }), 137 | Promise.resolve().then(function () { return require("logger"); }), 138 | Promise.resolve().then(function () { return require("../lib/launch"); }) 139 | ])]; 140 | case 1: 141 | _a = __read.apply(void 0, [_c.sent(), 5]), path = _a[0], fs = _a[1], working_directory_path = _a[2].working_directory_path, logger = _a[3], _b = _a[4], launch = _b.launch, beforeExit = _b.beforeExit; 142 | logfile_path = path.join(working_directory_path, "log"); 143 | return [2 /*return*/, { 144 | "launch": function () { 145 | logger.file.enable(logfile_path); 146 | launch(); 147 | }, 148 | "beforeExitTask": function (error) { return __awaiter(void 0, void 0, void 0, function () { 149 | return __generator(this, function (_a) { 150 | switch (_a.label) { 151 | case 0: 152 | if (!!error) { 153 | logger.log(error); 154 | } 155 | return [4 /*yield*/, Promise.all([ 156 | logger.file.terminate().then(function () { 157 | if (!!error) { 158 | scriptLib.execSync([ 159 | "mv", 160 | logfile_path, 161 | path.join(path.dirname(logfile_path), "previous_crash.log") 162 | ].join(" ")); 163 | } 164 | else { 165 | fs.unlinkSync(logfile_path); 166 | } 167 | }), 168 | beforeExit() 169 | ])]; 170 | case 1: 171 | _a.sent(); 172 | return [2 /*return*/]; 173 | } 174 | }); 175 | }); } 176 | }]; 177 | } 178 | }); 179 | }); } 180 | }); 181 | -------------------------------------------------------------------------------- /dist/lib/atBridge.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /*usb<-->accessPoint.dataIfPathtty0tty.leftEnd<-->tty0tty.rightEnd*/ 3 | /*usb<-->/dev/ttyUSB1/dev/tnt0<-->/dev/tnt1*/ 4 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 5 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 6 | return new (P || (P = Promise))(function (resolve, reject) { 7 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 8 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 9 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 10 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 11 | }); 12 | }; 13 | var __generator = (this && this.__generator) || function (thisArg, body) { 14 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 15 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 16 | function verb(n) { return function (v) { return step([n, v]); }; } 17 | function step(op) { 18 | if (f) throw new TypeError("Generator is already executing."); 19 | while (_) try { 20 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 21 | if (y = 0, t) op = [op[0] & 2, t.value]; 22 | switch (op[0]) { 23 | case 0: case 1: t = op; break; 24 | case 4: _.label++; return { value: op[1], done: false }; 25 | case 5: _.label++; y = op[1]; op = [0]; continue; 26 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 27 | default: 28 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 29 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 30 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 31 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 32 | if (t[2]) _.ops.pop(); 33 | _.trys.pop(); continue; 34 | } 35 | op = body.call(thisArg, _); 36 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 37 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 38 | } 39 | }; 40 | var __read = (this && this.__read) || function (o, n) { 41 | var m = typeof Symbol === "function" && o[Symbol.iterator]; 42 | if (!m) return o; 43 | var i = m.call(o), r, ar = [], e; 44 | try { 45 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); 46 | } 47 | catch (error) { e = { error: error }; } 48 | finally { 49 | try { 50 | if (r && !r.done && (m = i["return"])) m.call(i); 51 | } 52 | finally { if (e) throw e.error; } 53 | } 54 | return ar; 55 | }; 56 | Object.defineProperty(exports, "__esModule", { value: true }); 57 | exports.waitForTerminate = exports.init = void 0; 58 | var ts_gsm_modem_1 = require("ts-gsm-modem"); 59 | var Tty0tty_1 = require("./Tty0tty"); 60 | var logger = require("logger"); 61 | var types = require("./types"); 62 | var evt_1 = require("evt"); 63 | var runExclusive = require("run-exclusive"); 64 | var debug = logger.debugFactory(); 65 | function readableAt(raw) { 66 | return "`" + raw.replace(/\r/g, "\\r").replace(/\n/g, "\\n") + "`"; 67 | } 68 | function init(modems, chanDongleConfManagerApi) { 69 | var _this = this; 70 | atBridge.confManagerApi = chanDongleConfManagerApi; 71 | var tty0ttyFactory = Tty0tty_1.Tty0tty.makeFactory(); 72 | modems.evtCreate.attach(function (_a) { 73 | var _b = __read(_a, 2), modem = _b[0], accessPoint = _b[1]; 74 | return __awaiter(_this, void 0, void 0, function () { 75 | return __generator(this, function (_c) { 76 | if (types.LockedModem.match(modem)) { 77 | return [2 /*return*/]; 78 | } 79 | atBridge(accessPoint, modem, tty0ttyFactory()); 80 | return [2 /*return*/]; 81 | }); 82 | }); 83 | }); 84 | } 85 | exports.init = init; 86 | function waitForTerminate() { 87 | return __awaiter(this, void 0, void 0, function () { 88 | return __generator(this, function (_a) { 89 | switch (_a.label) { 90 | case 0: 91 | if (waitForTerminate.ports.size === 0) { 92 | return [2 /*return*/, Promise.resolve()]; 93 | } 94 | return [4 /*yield*/, waitForTerminate.evtAllClosed.waitFor()]; 95 | case 1: 96 | _a.sent(); 97 | debug("All virtual serial ports closed"); 98 | return [2 /*return*/]; 99 | } 100 | }); 101 | }); 102 | } 103 | exports.waitForTerminate = waitForTerminate; 104 | (function (waitForTerminate) { 105 | waitForTerminate.ports = new Set(); 106 | waitForTerminate.evtAllClosed = evt_1.Evt.create(); 107 | })(waitForTerminate = exports.waitForTerminate || (exports.waitForTerminate = {})); 108 | function atBridge(accessPoint, modem, tty0tty) { 109 | var _this = this; 110 | var confManagerApi = atBridge.confManagerApi; 111 | (modem.isGsmConnectivityOk() ? 112 | Promise.resolve() : 113 | modem.evtGsmConnectivityChange.waitFor()).then(function callee() { 114 | return __awaiter(this, void 0, void 0, function () { 115 | var final; 116 | return __generator(this, function (_a) { 117 | switch (_a.label) { 118 | case 0: 119 | if (!!modem.terminateState) { 120 | return [2 /*return*/]; 121 | } 122 | debug("connectivity ok running AT+CCWA"); 123 | return [4 /*yield*/, modem.runCommand("AT+CCWA=0,0,1\r", { "recoverable": true })]; 124 | case 1: 125 | final = (_a.sent()).final; 126 | if (!!final.isError) { 127 | debug("Failed to disable call waiting".red, final.raw); 128 | modem.evtGsmConnectivityChange.attachOnce(function () { return modem.isGsmConnectivityOk(); }, function () { return callee(); }); 129 | return [2 /*return*/]; 130 | } 131 | debug("Call waiting successfully disabled".green); 132 | return [2 /*return*/]; 133 | } 134 | }); 135 | }); 136 | }); 137 | var runCommand = runExclusive.build((function () { 138 | var inputs = []; 139 | for (var _i = 0; _i < arguments.length; _i++) { 140 | inputs[_i] = arguments[_i]; 141 | } 142 | return modem.runCommand.apply(modem, inputs); 143 | })); 144 | atBridge.confManagerApi.addDongle({ 145 | "dongleName": accessPoint.friendlyId, 146 | "data": tty0tty.rightEnd, 147 | "audio": accessPoint.audioIfPath 148 | }); 149 | var portVirtual = new ts_gsm_modem_1.SerialPortExt(tty0tty.leftEnd, { 150 | "baudRate": 115200, 151 | "parser": ts_gsm_modem_1.SerialPortExt.parsers.readline("\r") 152 | }); 153 | waitForTerminate.ports.add(portVirtual); 154 | portVirtual.once("close", function () { 155 | waitForTerminate.ports.delete(portVirtual); 156 | if (waitForTerminate.ports.size === 0) { 157 | waitForTerminate.evtAllClosed.post(); 158 | } 159 | }); 160 | modem.evtTerminate.attachOnce(function () { return __awaiter(_this, void 0, void 0, function () { 161 | return __generator(this, function (_a) { 162 | switch (_a.label) { 163 | case 0: 164 | debug("Modem terminate => closing bridge"); 165 | return [4 /*yield*/, confManagerApi.removeDongle(accessPoint.friendlyId)]; 166 | case 1: 167 | _a.sent(); 168 | if (!portVirtual.isOpen()) return [3 /*break*/, 3]; 169 | return [4 /*yield*/, new Promise(function (resolve) { return portVirtual.close(function () { return resolve(); }); })]; 170 | case 2: 171 | _a.sent(); 172 | _a.label = 3; 173 | case 3: 174 | tty0tty.release(); 175 | return [2 /*return*/]; 176 | } 177 | }); 178 | }); }); 179 | portVirtual.evtError.attach(function (serialPortError) { 180 | debug("uncaught error serialPortVirtual", serialPortError); 181 | modem.terminate(); 182 | }); 183 | var serviceProviderShort = (modem.serviceProviderName || "Unknown SP").substring(0, 15); 184 | var forwardResp = function (rawResp, isRespFromModem, isPing) { 185 | if (isPing === void 0) { isPing = false; } 186 | if (runExclusive.isRunning(runCommand)) { 187 | debug(("Newer command from chanDongle, dropping response " + readableAt(rawResp)).red); 188 | return; 189 | } 190 | if (!isPing) { 191 | debug("(AT) " + (!isRespFromModem ? "( fake ) " : "") + "modem response: " + readableAt(rawResp)); 192 | } 193 | portVirtual.writeAndDrain(rawResp); 194 | }; 195 | portVirtual.on("data", function (buff) { 196 | if (!!modem.terminateState) { 197 | return; 198 | } 199 | var command = buff.toString("utf8") + "\r"; 200 | if (command !== "AT\r") { 201 | debug("(AT) command from asterisk-chan-dongle: " + readableAt(command)); 202 | } 203 | var ok = "\r\nOK\r\n"; 204 | if (command === "ATZ\r" || 205 | command.match(/^AT\+CNMI=/)) { 206 | forwardResp(ok, false); 207 | return; 208 | } 209 | else if (command === "AT\r") { 210 | forwardResp(ok, false, true); 211 | modem.ping(); 212 | return; 213 | } 214 | else if (command === "AT+COPS?\r") { 215 | forwardResp("\r\n+COPS: 0,0,\"" + serviceProviderShort + "\",0\r\n" + ok, false); 216 | return; 217 | } 218 | if (runExclusive.getQueuedCallCount(runCommand) !== 0) { 219 | debug([ 220 | "a command is already running and", 221 | modem.runCommand_queuedCallCount + " command in stack", 222 | "flushing the pending command in stack" 223 | ].join("\n").yellow); 224 | } 225 | runExclusive.cancelAllQueuedCalls(runCommand); 226 | runCommand(command, { 227 | "recoverable": true, 228 | "reportMode": ts_gsm_modem_1.AtMessage.ReportMode.NO_DEBUG_INFO, 229 | "retryOnErrors": false 230 | }).then(function (_a) { 231 | var raw = _a.raw; 232 | return forwardResp(raw, true); 233 | }); 234 | }); 235 | portVirtual.once("data", function () { 236 | return modem.evtUnsolicitedAtMessage.attach(function (urc) { 237 | var doNotForward = (urc.id === "CX_BOOT_URC" || 238 | (urc instanceof ts_gsm_modem_1.AtMessage.P_CMTI_URC) && (urc.index < 0 || 239 | confManagerApi.staticModuleConfiguration.defaults["disablesms"] === "yes")); 240 | if (!doNotForward) { 241 | portVirtual.writeAndDrain(urc.raw); 242 | } 243 | debug("(AT) urc: " + readableAt(urc.raw) + " ( " + (doNotForward ? "NOT forwarded" : "forwarded") + " to asterisk-chan-dongle )"); 244 | }); 245 | }); 246 | } 247 | ; 248 | (function (atBridge) { 249 | })(atBridge || (atBridge = {})); 250 | -------------------------------------------------------------------------------- /dist/lib/db.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | var __values = (this && this.__values) || function(o) { 39 | var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; 40 | if (m) return m.call(o); 41 | if (o && typeof o.length === "number") return { 42 | next: function () { 43 | if (o && i >= o.length) o = void 0; 44 | return { value: o && o[i++], done: !o }; 45 | } 46 | }; 47 | throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); 48 | }; 49 | Object.defineProperty(exports, "__esModule", { value: true }); 50 | exports.messages = exports.pin = exports.flush = exports.launch = exports.beforeExit = exports._ = void 0; 51 | var sqliteCustom = require("sqlite-custom"); 52 | var installer_1 = require("../bin/installer"); 53 | var logger = require("logger"); 54 | var debug = logger.debugFactory(); 55 | function beforeExit() { 56 | return beforeExit.impl(); 57 | } 58 | exports.beforeExit = beforeExit; 59 | (function (beforeExit) { 60 | beforeExit.impl = function () { return Promise.resolve(); }; 61 | })(beforeExit = exports.beforeExit || (exports.beforeExit = {})); 62 | /** Must be called and awaited before use */ 63 | function launch() { 64 | return __awaiter(this, void 0, void 0, function () { 65 | return __generator(this, function (_a) { 66 | switch (_a.label) { 67 | case 0: return [4 /*yield*/, sqliteCustom.connectAndGetApi(installer_1.db_path, "HANDLE STRING ENCODING")]; 68 | case 1: 69 | exports._ = _a.sent(); 70 | beforeExit.impl = function () { 71 | debug("Closed"); 72 | return exports._.close(); 73 | }; 74 | return [2 /*return*/]; 75 | } 76 | }); 77 | }); 78 | } 79 | exports.launch = launch; 80 | /** Debug only */ 81 | function flush() { 82 | return __awaiter(this, void 0, void 0, function () { 83 | return __generator(this, function (_a) { 84 | switch (_a.label) { 85 | case 0: return [4 /*yield*/, exports._.query([ 86 | "DELETE FROM pin", 87 | "DELETE FROM message" 88 | ].join(";\n"))]; 89 | case 1: 90 | _a.sent(); 91 | return [2 /*return*/]; 92 | } 93 | }); 94 | }); 95 | } 96 | exports.flush = flush; 97 | var pin; 98 | (function (pin_1) { 99 | var AssociatedTo; 100 | (function (AssociatedTo) { 101 | var Iccid; 102 | (function (Iccid) { 103 | function match(associatedTo) { 104 | return !!associatedTo.iccid; 105 | } 106 | Iccid.match = match; 107 | })(Iccid = AssociatedTo.Iccid || (AssociatedTo.Iccid = {})); 108 | var Imei; 109 | (function (Imei) { 110 | function match(associatedTo) { 111 | return !Iccid.match(associatedTo); 112 | } 113 | Imei.match = match; 114 | })(Imei = AssociatedTo.Imei || (AssociatedTo.Imei = {})); 115 | })(AssociatedTo = pin_1.AssociatedTo || (pin_1.AssociatedTo = {})); 116 | function save(pin, associatedTo) { 117 | return __awaiter(this, void 0, void 0, function () { 118 | var sql; 119 | return __generator(this, function (_a) { 120 | sql = (function () { 121 | if (!!pin) { 122 | if (AssociatedTo.Iccid.match(associatedTo)) { 123 | return exports._.buildInsertOrUpdateQueries("pin", { 124 | "iccid": associatedTo.iccid, 125 | "imei": null, 126 | "value": pin 127 | }, ["iccid"]); 128 | } 129 | else { 130 | return exports._.buildInsertOrUpdateQueries("pin", { 131 | "iccid": null, 132 | "imei": associatedTo.imei, 133 | "value": pin 134 | }, ["imei"]); 135 | } 136 | } 137 | else { 138 | return [ 139 | "DELETE FROM pin WHERE", 140 | AssociatedTo.Iccid.match(associatedTo) ? 141 | "iccid=" + associatedTo.iccid : 142 | "imei=" + associatedTo.imei 143 | ].join(" "); 144 | } 145 | })(); 146 | return [2 /*return*/, exports._.query(sql)]; 147 | }); 148 | }); 149 | } 150 | pin_1.save = save; 151 | function get(associatedTo) { 152 | return __awaiter(this, void 0, void 0, function () { 153 | var res; 154 | return __generator(this, function (_a) { 155 | switch (_a.label) { 156 | case 0: return [4 /*yield*/, exports._.query([ 157 | "SELECT value FROM pin WHERE", 158 | AssociatedTo.Iccid.match(associatedTo) ? 159 | "iccid=" + associatedTo.iccid : 160 | "imei=" + associatedTo.imei 161 | ].join(" "))]; 162 | case 1: 163 | res = _a.sent(); 164 | if (res.length === 0) { 165 | return [2 /*return*/, undefined]; 166 | } 167 | else { 168 | return [2 /*return*/, res[0]["value"]]; 169 | } 170 | return [2 /*return*/]; 171 | } 172 | }); 173 | }); 174 | } 175 | pin_1.get = get; 176 | })(pin = exports.pin || (exports.pin = {})); 177 | var messages; 178 | (function (messages) { 179 | function retrieve(params) { 180 | return __awaiter(this, void 0, void 0, function () { 181 | var fromDate, toDate, where_clause, sql, entries, res, entries_1, entries_1_1, entry; 182 | var e_1, _a; 183 | return __generator(this, function (_b) { 184 | switch (_b.label) { 185 | case 0: 186 | fromDate = params.fromDate ? params.fromDate.getTime() : 0; 187 | toDate = params.toDate ? params.toDate.getTime() : Date.now(); 188 | where_clause = [ 189 | exports._.esc(fromDate) + " <= date AND date <= " + exports._.esc(toDate), 190 | !!params.imsi ? " AND imsi= " + exports._.esc(params.imsi) : "" 191 | ].join(""); 192 | sql = [ 193 | "SELECT imsi, date, number, text", 194 | "FROM message", 195 | "WHERE " + where_clause, 196 | "ORDER BY date ASC;" 197 | ].join("\n"); 198 | if (!!!params.flush) return [3 /*break*/, 2]; 199 | sql += "\n" + ("DELETE FROM message WHERE " + where_clause); 200 | return [4 /*yield*/, exports._.query(sql)]; 201 | case 1: 202 | res = _b.sent(); 203 | entries = res[0]; 204 | return [3 /*break*/, 4]; 205 | case 2: return [4 /*yield*/, exports._.query(sql)]; 206 | case 3: 207 | entries = _b.sent(); 208 | _b.label = 4; 209 | case 4: 210 | try { 211 | for (entries_1 = __values(entries), entries_1_1 = entries_1.next(); !entries_1_1.done; entries_1_1 = entries_1.next()) { 212 | entry = entries_1_1.value; 213 | entry["date"] = new Date(entry["date"]); 214 | } 215 | } 216 | catch (e_1_1) { e_1 = { error: e_1_1 }; } 217 | finally { 218 | try { 219 | if (entries_1_1 && !entries_1_1.done && (_a = entries_1.return)) _a.call(entries_1); 220 | } 221 | finally { if (e_1) throw e_1.error; } 222 | } 223 | return [2 /*return*/, entries]; 224 | } 225 | }); 226 | }); 227 | } 228 | messages.retrieve = retrieve; 229 | function save(imsi, message) { 230 | return __awaiter(this, void 0, void 0, function () { 231 | var sql; 232 | return __generator(this, function (_a) { 233 | switch (_a.label) { 234 | case 0: 235 | sql = exports._.buildInsertQuery("message", { 236 | imsi: imsi, 237 | "date": message.date.getTime(), 238 | "number": message.number, 239 | "text": message.text 240 | }, "THROW ERROR"); 241 | return [4 /*yield*/, exports._.query(sql)]; 242 | case 1: 243 | _a.sent(); 244 | return [2 /*return*/]; 245 | } 246 | }); 247 | }); 248 | } 249 | messages.save = save; 250 | })(messages = exports.messages || (exports.messages = {})); 251 | -------------------------------------------------------------------------------- /dist/lib/confManager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || function () { 3 | __assign = Object.assign || function(t) { 4 | for (var s, i = 1, n = arguments.length; i < n; i++) { 5 | s = arguments[i]; 6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 7 | t[p] = s[p]; 8 | } 9 | return t; 10 | }; 11 | return __assign.apply(this, arguments); 12 | }; 13 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 14 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 15 | return new (P || (P = Promise))(function (resolve, reject) { 16 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 17 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 18 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 19 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 20 | }); 21 | }; 22 | var __generator = (this && this.__generator) || function (thisArg, body) { 23 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 24 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 25 | function verb(n) { return function (v) { return step([n, v]); }; } 26 | function step(op) { 27 | if (f) throw new TypeError("Generator is already executing."); 28 | while (_) try { 29 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 30 | if (y = 0, t) op = [op[0] & 2, t.value]; 31 | switch (op[0]) { 32 | case 0: case 1: t = op; break; 33 | case 4: _.label++; return { value: op[1], done: false }; 34 | case 5: _.label++; y = op[1]; op = [0]; continue; 35 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 36 | default: 37 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 38 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 39 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 40 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 41 | if (t[2]) _.ops.pop(); 42 | _.trys.pop(); continue; 43 | } 44 | op = body.call(thisArg, _); 45 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 46 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 47 | } 48 | }; 49 | var __values = (this && this.__values) || function(o) { 50 | var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; 51 | if (m) return m.call(o); 52 | if (o && typeof o.length === "number") return { 53 | next: function () { 54 | if (o && i >= o.length) o = void 0; 55 | return { value: o && o[i++], done: !o }; 56 | } 57 | }; 58 | throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); 59 | }; 60 | Object.defineProperty(exports, "__esModule", { value: true }); 61 | exports.getApi = exports.beforeExit = void 0; 62 | var fs = require("fs"); 63 | var ini_extended_1 = require("ini-extended"); 64 | var runExclusive = require("run-exclusive"); 65 | var path = require("path"); 66 | var Astdirs_1 = require("./Astdirs"); 67 | var logger = require("logger"); 68 | var debug = logger.debugFactory(); 69 | var default_staticModuleConfiguration = { 70 | "general": { 71 | "interval": "10000000", 72 | "jbenable": "no" 73 | }, 74 | "defaults": { 75 | "context": "from-dongle", 76 | "group": "0", 77 | "rxgain": "0", 78 | "txgain": "0", 79 | "autodeletesms": "no", 80 | "resetdongle": "yes", 81 | "u2diag": "-1", 82 | "usecallingpres": "yes", 83 | "callingpres": "allowed_passed_screen", 84 | "disablesms": "yes", 85 | "language": "en", 86 | "smsaspdu": "yes", 87 | "mindtmfgap": "45", 88 | "mindtmfduration": "80", 89 | "mindtmfinterval": "200", 90 | "callwaiting": "auto", 91 | "disable": "no", 92 | "initstate": "start", 93 | "exten": "+12345678987", 94 | "dtmf": "off" 95 | } 96 | }; 97 | function loadChanDongleSo(ami) { 98 | return __awaiter(this, void 0, void 0, function () { 99 | var response, _a; 100 | return __generator(this, function (_b) { 101 | switch (_b.label) { 102 | case 0: 103 | debug("Checking if chan_dongle.so is loaded..."); 104 | _b.label = 1; 105 | case 1: 106 | _b.trys.push([1, 3, , 5]); 107 | return [4 /*yield*/, ami.postAction("ModuleCheck", { 108 | "module": "chan_dongle.so" 109 | })]; 110 | case 2: 111 | response = (_b.sent()).response; 112 | if (response !== "Success") { 113 | throw new Error("not loaded"); 114 | } 115 | return [3 /*break*/, 5]; 116 | case 3: 117 | _a = _b.sent(); 118 | debug("chan_dongle.so is not loaded, loading manually"); 119 | return [4 /*yield*/, ami.postAction("ModuleLoad", { 120 | "module": "chan_dongle.so", 121 | "loadtype": "load" 122 | })]; 123 | case 4: 124 | _b.sent(); 125 | return [3 /*break*/, 5]; 126 | case 5: 127 | debug("chan_dongle.so is loaded!"); 128 | return [2 /*return*/]; 129 | } 130 | }); 131 | }); 132 | } 133 | function beforeExit() { 134 | return beforeExit.impl(); 135 | } 136 | exports.beforeExit = beforeExit; 137 | (function (beforeExit) { 138 | beforeExit.impl = function () { return Promise.resolve(); }; 139 | })(beforeExit = exports.beforeExit || (exports.beforeExit = {})); 140 | function getApi(ami) { 141 | return __awaiter(this, void 0, void 0, function () { 142 | var dongle_conf_path, staticModuleConfiguration, state, update, groupRef, api; 143 | var _this = this; 144 | return __generator(this, function (_a) { 145 | switch (_a.label) { 146 | case 0: return [4 /*yield*/, loadChanDongleSo(ami)]; 147 | case 1: 148 | _a.sent(); 149 | ami.evt.attach(function (_a) { 150 | var event = _a.event; 151 | return event === "FullyBooted"; 152 | }, function () { return loadChanDongleSo(ami); }); 153 | dongle_conf_path = path.join(Astdirs_1.Astdirs.get().astetcdir, "dongle.conf"); 154 | staticModuleConfiguration = (function () { 155 | try { 156 | var _a = ini_extended_1.ini.parseStripWhitespace(fs.readFileSync(dongle_conf_path).toString("utf8")), general = _a.general, defaults = _a.defaults; 157 | console.assert(!!general && !!defaults); 158 | defaults.autodeletesms = default_staticModuleConfiguration.defaults["autodeletesms"]; 159 | general.interval = default_staticModuleConfiguration.general["interval"]; 160 | for (var key in defaults) { 161 | if (!defaults[key]) { 162 | defaults[key] = default_staticModuleConfiguration.defaults[key]; 163 | } 164 | } 165 | return { general: general, defaults: defaults }; 166 | } 167 | catch (_b) { 168 | return default_staticModuleConfiguration; 169 | } 170 | })(); 171 | state = __assign({}, staticModuleConfiguration); 172 | update = function () { return new Promise(function (resolve) { return fs.writeFile(dongle_conf_path, Buffer.from(ini_extended_1.ini.stringify(state), "utf8"), function (error) { return __awaiter(_this, void 0, void 0, function () { 173 | return __generator(this, function (_a) { 174 | if (error) { 175 | throw error; 176 | } 177 | resolve(); 178 | ami.postAction("DongleReload", { "when": "gracefully" }) 179 | .catch(function () { return debug("Dongle reload fail, is asterisk running?"); }); 180 | return [2 /*return*/]; 181 | }); 182 | }); }); }); }; 183 | groupRef = runExclusive.createGroupRef(); 184 | api = { 185 | staticModuleConfiguration: staticModuleConfiguration, 186 | "reset": runExclusive.build(groupRef, function () { return __awaiter(_this, void 0, void 0, function () { 187 | var _a, _b, key; 188 | var e_1, _c; 189 | return __generator(this, function (_d) { 190 | switch (_d.label) { 191 | case 0: 192 | debug("reset"); 193 | try { 194 | for (_a = __values(Object.keys(state).filter(function (key) { return key !== "general" && key !== "defaults"; })), _b = _a.next(); !_b.done; _b = _a.next()) { 195 | key = _b.value; 196 | delete state[key]; 197 | } 198 | } 199 | catch (e_1_1) { e_1 = { error: e_1_1 }; } 200 | finally { 201 | try { 202 | if (_b && !_b.done && (_c = _a.return)) _c.call(_a); 203 | } 204 | finally { if (e_1) throw e_1.error; } 205 | } 206 | return [4 /*yield*/, update()]; 207 | case 1: 208 | _d.sent(); 209 | debug("reset complete"); 210 | return [2 /*return*/]; 211 | } 212 | }); 213 | }); }), 214 | "addDongle": runExclusive.build(groupRef, function (_a) { 215 | var dongleName = _a.dongleName, data = _a.data, audio = _a.audio; 216 | return __awaiter(_this, void 0, void 0, function () { 217 | return __generator(this, function (_b) { 218 | switch (_b.label) { 219 | case 0: 220 | debug("addDongle", { dongleName: dongleName, data: data, audio: audio }); 221 | state[dongleName] = { audio: audio, data: data }; 222 | return [4 /*yield*/, update()]; 223 | case 1: 224 | _b.sent(); 225 | return [2 /*return*/]; 226 | } 227 | }); 228 | }); 229 | }), 230 | "removeDongle": runExclusive.build(groupRef, function (dongleName) { return __awaiter(_this, void 0, void 0, function () { 231 | return __generator(this, function (_a) { 232 | switch (_a.label) { 233 | case 0: 234 | debug("removeDongle", { dongleName: dongleName }); 235 | delete state[dongleName]; 236 | return [4 /*yield*/, update()]; 237 | case 1: 238 | _a.sent(); 239 | return [2 /*return*/]; 240 | } 241 | }); 242 | }); }) 243 | }; 244 | beforeExit.impl = function () { return api.reset(); }; 245 | api.reset(); 246 | return [2 /*return*/, api]; 247 | } 248 | }); 249 | }); 250 | } 251 | exports.getApi = getApi; 252 | -------------------------------------------------------------------------------- /dist/lib/launch.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __assign = (this && this.__assign) || function () { 3 | __assign = Object.assign || function(t) { 4 | for (var s, i = 1, n = arguments.length; i < n; i++) { 5 | s = arguments[i]; 6 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) 7 | t[p] = s[p]; 8 | } 9 | return t; 10 | }; 11 | return __assign.apply(this, arguments); 12 | }; 13 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 14 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 15 | return new (P || (P = Promise))(function (resolve, reject) { 16 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 17 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 18 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 19 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 20 | }); 21 | }; 22 | var __generator = (this && this.__generator) || function (thisArg, body) { 23 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 24 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 25 | function verb(n) { return function (v) { return step([n, v]); }; } 26 | function step(op) { 27 | if (f) throw new TypeError("Generator is already executing."); 28 | while (_) try { 29 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 30 | if (y = 0, t) op = [op[0] & 2, t.value]; 31 | switch (op[0]) { 32 | case 0: case 1: t = op; break; 33 | case 4: _.label++; return { value: op[1], done: false }; 34 | case 5: _.label++; y = op[1]; op = [0]; continue; 35 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 36 | default: 37 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 38 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 39 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 40 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 41 | if (t[2]) _.ops.pop(); 42 | _.trys.pop(); continue; 43 | } 44 | op = body.call(thisArg, _); 45 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 46 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 47 | } 48 | }; 49 | Object.defineProperty(exports, "__esModule", { value: true }); 50 | exports.launch = exports.beforeExit = void 0; 51 | var ts_gsm_modem_1 = require("ts-gsm-modem"); 52 | var trackable_map_1 = require("trackable-map"); 53 | var AmiCredential_1 = require("./AmiCredential"); 54 | var ts_ami_1 = require("ts-ami"); 55 | var dialplan = require("./dialplan"); 56 | var api = require("./api"); 57 | var atBridge = require("./atBridge"); 58 | var evt_1 = require("evt"); 59 | var confManager = require("./confManager"); 60 | var logger = require("logger"); 61 | var db = require("./db"); 62 | var InstallOptions_1 = require("./InstallOptions"); 63 | var hostRebootScheduler = require("./hostRebootScheduler"); 64 | var scripting_tools_1 = require("scripting-tools"); 65 | var debug = logger.debugFactory(); 66 | var modems = new trackable_map_1.TrackableMap(); 67 | var evtScheduleRetry = new evt_1.Evt(); 68 | function beforeExit() { 69 | return __awaiter(this, void 0, void 0, function () { 70 | var _this = this; 71 | return __generator(this, function (_a) { 72 | switch (_a.label) { 73 | case 0: 74 | debug("Start before exit..."); 75 | if (ts_gsm_modem_1.ConnectionMonitor.hasInstance) { 76 | ts_gsm_modem_1.ConnectionMonitor.getInstance().stop(); 77 | } 78 | return [4 /*yield*/, Promise.all([ 79 | scripting_tools_1.safePr(db.beforeExit()), 80 | scripting_tools_1.safePr(api.beforeExit()), 81 | scripting_tools_1.safePr(atBridge.waitForTerminate()), 82 | Promise.all(Array.from(modems.values()) 83 | .map(function (modem) { return scripting_tools_1.safePr(modem.terminate()); })), 84 | (function () { return __awaiter(_this, void 0, void 0, function () { 85 | return __generator(this, function (_a) { 86 | switch (_a.label) { 87 | case 0: return [4 /*yield*/, scripting_tools_1.safePr(confManager.beforeExit(), 1500)]; 88 | case 1: 89 | _a.sent(); 90 | if (!ts_ami_1.Ami.hasInstance) return [3 /*break*/, 3]; 91 | return [4 /*yield*/, ts_ami_1.Ami.getInstance().disconnect()]; 92 | case 2: 93 | _a.sent(); 94 | _a.label = 3; 95 | case 3: return [2 /*return*/]; 96 | } 97 | }); 98 | }); })() 99 | ])]; 100 | case 1: 101 | _a.sent(); 102 | return [2 /*return*/]; 103 | } 104 | }); 105 | }); 106 | } 107 | exports.beforeExit = beforeExit; 108 | function launch() { 109 | return __awaiter(this, void 0, void 0, function () { 110 | var installOptions, ami, chanDongleConfManagerApi, defaults, monitor; 111 | return __generator(this, function (_a) { 112 | switch (_a.label) { 113 | case 0: 114 | installOptions = InstallOptions_1.InstallOptions.get(); 115 | ami = ts_ami_1.Ami.getInstance(AmiCredential_1.AmiCredential.get()); 116 | ami.evtTcpConnectionClosed.attachOnce(function () { 117 | debug("TCP connection with Asterisk manager closed, reboot"); 118 | process.emit("beforeExit", process.exitCode = 0); 119 | }); 120 | return [4 /*yield*/, confManager.getApi(ami)]; 121 | case 1: 122 | chanDongleConfManagerApi = _a.sent(); 123 | return [4 /*yield*/, db.launch()]; 124 | case 2: 125 | _a.sent(); 126 | if (!installOptions.disable_sms_dialplan) { 127 | defaults = chanDongleConfManagerApi.staticModuleConfiguration.defaults; 128 | dialplan.init(modems, ami, defaults["context"], defaults["exten"]); 129 | } 130 | return [4 /*yield*/, atBridge.init(modems, chanDongleConfManagerApi)]; 131 | case 3: 132 | _a.sent(); 133 | return [4 /*yield*/, api.launch(modems, chanDongleConfManagerApi.staticModuleConfiguration)]; 134 | case 4: 135 | _a.sent(); 136 | debug("Started"); 137 | monitor = ts_gsm_modem_1.ConnectionMonitor.getInstance(); 138 | monitor.evtModemConnect.attach(function (accessPoint) { 139 | debug("(Monitor) Connect: " + accessPoint); 140 | createModem(accessPoint); 141 | }); 142 | monitor.evtModemDisconnect.attach(function (accessPoint) { return debug("(Monitor) Disconnect: " + accessPoint); }); 143 | evtScheduleRetry.attach(function (_a) { 144 | var accessPointId = _a.accessPointId, shouldRebootModem = _a.shouldRebootModem; 145 | var accessPoint = Array.from(monitor.connectedModems).find(function (_a) { 146 | var id = _a.id; 147 | return id === accessPointId; 148 | }); 149 | if (!accessPoint) { 150 | return; 151 | } 152 | monitor.evtModemDisconnect 153 | .waitFor(function (ap) { return ap === accessPoint; }, 2000) 154 | .catch(function () { return createModem(accessPoint, shouldRebootModem ? "REBOOT" : undefined); }); 155 | }); 156 | return [2 /*return*/]; 157 | } 158 | }); 159 | }); 160 | } 161 | exports.launch = launch; 162 | ; 163 | function createModem(accessPoint, reboot) { 164 | return __awaiter(this, void 0, void 0, function () { 165 | var modem, error_1; 166 | return __generator(this, function (_a) { 167 | switch (_a.label) { 168 | case 0: 169 | debug("Create Modem"); 170 | _a.label = 1; 171 | case 1: 172 | _a.trys.push([1, 3, , 4]); 173 | return [4 /*yield*/, ts_gsm_modem_1.Modem.create({ 174 | "dataIfPath": accessPoint.dataIfPath, 175 | "unlock": function (modemInfo, iccid, pinState, tryLeft, performUnlock, terminate) { 176 | return onLockedModem(accessPoint, modemInfo, iccid, pinState, tryLeft, performUnlock, terminate); 177 | }, 178 | "log": logger.log, 179 | "rebootFirst": !!reboot 180 | })]; 181 | case 2: 182 | modem = _a.sent(); 183 | return [3 /*break*/, 4]; 184 | case 3: 185 | error_1 = _a.sent(); 186 | onModemInitializationFailed(accessPoint, error_1); 187 | return [2 /*return*/]; 188 | case 4: 189 | onModem(accessPoint, modem); 190 | return [2 /*return*/]; 191 | } 192 | }); 193 | }); 194 | } 195 | function onModemInitializationFailed(accessPoint, initializationError) { 196 | modems.delete(accessPoint); 197 | if (initializationError instanceof ts_gsm_modem_1.InitializationError.DidNotTurnBackOnAfterReboot) { 198 | hostRebootScheduler.schedule(); 199 | return; 200 | } 201 | if (initializationError.modemInfos.hasSim === false) { 202 | return; 203 | } 204 | /* 205 | NOTE: When we get an initialization error 206 | after a modem have been successfully rebooted 207 | do not attempt to reboot it again to prevent 208 | reboot loop that will conduct to the host being 209 | rebooted 210 | */ 211 | evtScheduleRetry.post({ 212 | "accessPointId": accessPoint.id, 213 | "shouldRebootModem": !initializationError.modemInfos.successfullyRebooted 214 | }); 215 | } 216 | function onLockedModem(accessPoint, modemInfos, iccid, pinState, tryLeft, performUnlock, terminate) { 217 | return __awaiter(this, void 0, void 0, function () { 218 | var associatedTo, pin, unlockResult, lockedModem; 219 | var _this = this; 220 | return __generator(this, function (_a) { 221 | switch (_a.label) { 222 | case 0: 223 | debug("onLockedModem", __assign(__assign({}, modemInfos), { iccid: iccid, pinState: pinState, tryLeft: tryLeft })); 224 | associatedTo = !!iccid ? ({ iccid: iccid }) : ({ "imei": modemInfos.imei }); 225 | return [4 /*yield*/, db.pin.get(associatedTo)]; 226 | case 1: 227 | pin = _a.sent(); 228 | if (!!!pin) return [3 /*break*/, 5]; 229 | if (!(pinState === "SIM PIN" && tryLeft === 3)) return [3 /*break*/, 3]; 230 | return [4 /*yield*/, performUnlock(pin)]; 231 | case 2: 232 | unlockResult = _a.sent(); 233 | if (unlockResult.success) { 234 | return [2 /*return*/]; 235 | } 236 | pinState = unlockResult.pinState; 237 | tryLeft = unlockResult.tryLeft; 238 | return [3 /*break*/, 5]; 239 | case 3: return [4 /*yield*/, db.pin.save(undefined, associatedTo)]; 240 | case 4: 241 | _a.sent(); 242 | _a.label = 5; 243 | case 5: 244 | lockedModem = { 245 | "imei": modemInfos.imei, 246 | "manufacturer": modemInfos.manufacturer, 247 | "model": modemInfos.model, 248 | "firmwareVersion": modemInfos.firmwareVersion, 249 | iccid: iccid, pinState: pinState, tryLeft: tryLeft, 250 | "performUnlock": function () { 251 | var inputs = []; 252 | for (var _i = 0; _i < arguments.length; _i++) { 253 | inputs[_i] = arguments[_i]; 254 | } 255 | return __awaiter(_this, void 0, void 0, function () { 256 | var pin, puk, unlockResult; 257 | return __generator(this, function (_a) { 258 | switch (_a.label) { 259 | case 0: 260 | //NOTE: PerformUnlock throw error if modem disconnect during unlock 261 | modems.delete(accessPoint); 262 | if (!!inputs[1]) return [3 /*break*/, 2]; 263 | pin = inputs[0]; 264 | puk = undefined; 265 | return [4 /*yield*/, performUnlock(pin)]; 266 | case 1: 267 | unlockResult = _a.sent(); 268 | return [3 /*break*/, 4]; 269 | case 2: 270 | pin = inputs[1]; 271 | puk = inputs[0]; 272 | return [4 /*yield*/, performUnlock(puk, pin)]; 273 | case 3: 274 | unlockResult = _a.sent(); 275 | _a.label = 4; 276 | case 4: 277 | if (!unlockResult.success) return [3 /*break*/, 6]; 278 | debug("Persistent storing of pin: " + pin); 279 | return [4 /*yield*/, db.pin.save(pin, associatedTo)]; 280 | case 5: 281 | _a.sent(); 282 | return [3 /*break*/, 8]; 283 | case 6: return [4 /*yield*/, db.pin.save(undefined, associatedTo)]; 284 | case 7: 285 | _a.sent(); 286 | lockedModem.pinState = unlockResult.pinState; 287 | lockedModem.tryLeft = unlockResult.tryLeft; 288 | modems.set(accessPoint, lockedModem); 289 | _a.label = 8; 290 | case 8: return [2 /*return*/, unlockResult]; 291 | } 292 | }); 293 | }); 294 | }, 295 | terminate: terminate 296 | }; 297 | modems.set(accessPoint, lockedModem); 298 | return [2 /*return*/]; 299 | } 300 | }); 301 | }); 302 | } 303 | function onModem(accessPoint, modem) { 304 | debug("Modem successfully initialized".green); 305 | var initializationTime = Date.now(); 306 | modem.evtTerminate.attachOnce(function (error) { 307 | modems.delete(accessPoint); 308 | debug("Modem terminate... " + (error ? error.message : "No internal error")); 309 | /** 310 | * NOTE: Preventing Modem reboot loop by allowing 311 | * modem to be rebooted at most once every hour. 312 | */ 313 | evtScheduleRetry.post({ 314 | "accessPointId": accessPoint.id, 315 | "shouldRebootModem": (!!modem["__api_rebootDongle_called__"] || 316 | (!!error && 317 | (!modem.successfullyRebooted || 318 | Date.now() - initializationTime > 3600000))) 319 | }); 320 | }); 321 | modems.set(accessPoint, modem); 322 | } 323 | -------------------------------------------------------------------------------- /dist/bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 4 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 5 | return new (P || (P = Promise))(function (resolve, reject) { 6 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 7 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 8 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 9 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 10 | }); 11 | }; 12 | var __generator = (this && this.__generator) || function (thisArg, body) { 13 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 14 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 15 | function verb(n) { return function (v) { return step([n, v]); }; } 16 | function step(op) { 17 | if (f) throw new TypeError("Generator is already executing."); 18 | while (_) try { 19 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 20 | if (y = 0, t) op = [op[0] & 2, t.value]; 21 | switch (op[0]) { 22 | case 0: case 1: t = op; break; 23 | case 4: _.label++; return { value: op[1], done: false }; 24 | case 5: _.label++; y = op[1]; op = [0]; continue; 25 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 26 | default: 27 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 28 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 29 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 30 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 31 | if (t[2]) _.ops.pop(); 32 | _.trys.pop(); continue; 33 | } 34 | op = body.call(thisArg, _); 35 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 36 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 37 | } 38 | }; 39 | Object.defineProperty(exports, "__esModule", { value: true }); 40 | var program = require("commander"); 41 | var chan_dongle_extended_client_1 = require("../chan-dongle-extended-client"); 42 | var storage = require("node-persist"); 43 | var InstallOptions_1 = require("../lib/InstallOptions"); 44 | var path = require("path"); 45 | var os = require("os"); 46 | require("colors"); 47 | program 48 | .command("list") 49 | .description("List dongles") 50 | .action(function () { return __awaiter(void 0, void 0, void 0, function () { 51 | var dc; 52 | return __generator(this, function (_a) { 53 | switch (_a.label) { 54 | case 0: return [4 /*yield*/, getDcInstance()]; 55 | case 1: 56 | dc = _a.sent(); 57 | try { 58 | console.log(JSON.stringify(dc.dongles.valuesAsArray(), null, 2)); 59 | process.exit(0); 60 | } 61 | catch (error) { 62 | console.log(error.message.red); 63 | process.exit(1); 64 | } 65 | return [2 /*return*/]; 66 | } 67 | }); 68 | }); }); 69 | program 70 | .command("select [imei]") 71 | .description([ 72 | "Select dongle for subsequent calls", 73 | " ( to avoid having to set --imei on each command)" 74 | ].join("")) 75 | .action(function (imei) { return __awaiter(void 0, void 0, void 0, function () { 76 | var dc; 77 | return __generator(this, function (_a) { 78 | switch (_a.label) { 79 | case 0: 80 | if (!imei) { 81 | console.log("Error: command malformed".red); 82 | process.exit(-1); 83 | } 84 | return [4 /*yield*/, getDcInstance()]; 85 | case 1: 86 | dc = _a.sent(); 87 | if (!dc.dongles.has(imei)) { 88 | console.log("Error: no such dongle connected".red); 89 | process.exit(-1); 90 | } 91 | return [4 /*yield*/, selected_dongle.set(imei)]; 92 | case 2: 93 | _a.sent(); 94 | console.log("Dongle " + imei + " selected"); 95 | process.exit(0); 96 | return [2 /*return*/]; 97 | } 98 | }); 99 | }); }); 100 | program 101 | .command("unlock") 102 | .description("provide SIM PIN or PUK to unlock dongle") 103 | .option("-i, --imei [imei]", "IMEI of the dongle") 104 | .option("-p, --pin [pin]", "SIM PIN ( 4 digits )") 105 | .option("--puk [puk-newPin]", "PUK ( 8 digits ) and new PIN eg. --puk 12345678-0000") 106 | .action(function (options) { return __awaiter(void 0, void 0, void 0, function () { 107 | var imei, dc, unlockResult, match, puk, newPin, error_1; 108 | return __generator(this, function (_a) { 109 | switch (_a.label) { 110 | case 0: return [4 /*yield*/, selected_dongle.get(options)]; 111 | case 1: 112 | imei = _a.sent(); 113 | if (!options.pin && !options.puk) { 114 | console.log("Error: command malformed".red); 115 | console.log(options.optionHelp()); 116 | process.exit(-1); 117 | } 118 | return [4 /*yield*/, getDcInstance()]; 119 | case 2: 120 | dc = _a.sent(); 121 | _a.label = 3; 122 | case 3: 123 | _a.trys.push([3, 8, , 9]); 124 | if (!options.pin) return [3 /*break*/, 5]; 125 | return [4 /*yield*/, dc.unlock(imei, options.pin)]; 126 | case 4: 127 | unlockResult = _a.sent(); 128 | return [3 /*break*/, 7]; 129 | case 5: 130 | match = options.puk.match(/^([0-9]{8})-([0-9]{4})$/); 131 | if (!match) { 132 | console.log("Error: puk-newPin malformed".red); 133 | console.log(options.optionHelp()); 134 | process.exit(-1); 135 | return [2 /*return*/]; 136 | } 137 | puk = match[1]; 138 | newPin = match[2]; 139 | return [4 /*yield*/, dc.unlock(imei, puk, newPin)]; 140 | case 6: 141 | unlockResult = _a.sent(); 142 | _a.label = 7; 143 | case 7: return [3 /*break*/, 9]; 144 | case 8: 145 | error_1 = _a.sent(); 146 | console.log(error_1.message.red); 147 | process.exit(1); 148 | return [2 /*return*/]; 149 | case 9: 150 | console.log(JSON.stringify(unlockResult, null, 2)); 151 | process.exit(0); 152 | return [2 /*return*/]; 153 | } 154 | }); 155 | }); }); 156 | program 157 | .command("reboot") 158 | .description("Send AT command to reboot a dongle") 159 | .option("-i, --imei [imei]", "IMEI of the dongle") 160 | .action(function (options) { return __awaiter(void 0, void 0, void 0, function () { 161 | var imei, dc, error_2; 162 | return __generator(this, function (_a) { 163 | switch (_a.label) { 164 | case 0: return [4 /*yield*/, selected_dongle.get(options)]; 165 | case 1: 166 | imei = _a.sent(); 167 | return [4 /*yield*/, getDcInstance()]; 168 | case 2: 169 | dc = _a.sent(); 170 | _a.label = 3; 171 | case 3: 172 | _a.trys.push([3, 5, , 6]); 173 | return [4 /*yield*/, dc.rebootDongle(imei)]; 174 | case 4: 175 | _a.sent(); 176 | return [3 /*break*/, 6]; 177 | case 5: 178 | error_2 = _a.sent(); 179 | console.log(error_2.message.red); 180 | process.exit(1); 181 | return [2 /*return*/]; 182 | case 6: 183 | console.log("OK".green); 184 | process.exit(0); 185 | return [2 /*return*/]; 186 | } 187 | }); 188 | }); }); 189 | program 190 | .command("send") 191 | .description("Send SMS") 192 | .option("-i, --imei [imei]", "IMEI of the dongle") 193 | .option("-n, --number [number]", "target phone number") 194 | .option("-t, --text [text]", "Text of the message") 195 | .option("-T, --text-base64 [textBase64]", "Text Base64 encoded") 196 | .action(function (options) { return __awaiter(void 0, void 0, void 0, function () { 197 | var number, text, textBase64, imei, dc, st, sendMessageResult, error_3; 198 | return __generator(this, function (_a) { 199 | switch (_a.label) { 200 | case 0: 201 | number = options.number, text = options.text, textBase64 = options.textBase64; 202 | if (!number || (!text && !textBase64)) { 203 | console.log("Error: command malformed".red); 204 | console.log(options.optionHelp()); 205 | process.exit(-1); 206 | } 207 | return [4 /*yield*/, selected_dongle.get(options)]; 208 | case 1: 209 | imei = _a.sent(); 210 | return [4 /*yield*/, getDcInstance()]; 211 | case 2: 212 | dc = _a.sent(); 213 | if (!textBase64) return [3 /*break*/, 4]; 214 | return [4 /*yield*/, Promise.resolve().then(function () { return require("transfer-tools/dist/lib/stringTransform"); })]; 215 | case 3: 216 | st = _a.sent(); 217 | text = st.safeBufferFromTo(textBase64, "base64", "utf8"); 218 | return [3 /*break*/, 5]; 219 | case 4: 220 | text = JSON.parse("\"" + text + "\""); 221 | _a.label = 5; 222 | case 5: 223 | _a.trys.push([5, 7, , 8]); 224 | return [4 /*yield*/, dc.sendMessage(imei, number, text)]; 225 | case 6: 226 | sendMessageResult = _a.sent(); 227 | if (sendMessageResult.success) { 228 | console.log(sendMessageResult.sendDate.getTime()); 229 | process.exit(0); 230 | } 231 | else { 232 | console.log(0); 233 | process.exit(1); 234 | } 235 | return [3 /*break*/, 8]; 236 | case 7: 237 | error_3 = _a.sent(); 238 | console.log(error_3.message.red); 239 | process.exit(1); 240 | return [2 /*return*/]; 241 | case 8: return [2 /*return*/]; 242 | } 243 | }); 244 | }); }); 245 | program 246 | .command("messages") 247 | .description("Get received SMS") 248 | .option("-f, --flush", "Whether or not erasing retrieved messages") 249 | .action(function (options) { return __awaiter(void 0, void 0, void 0, function () { 250 | var flush, dc, messages, error_4; 251 | return __generator(this, function (_a) { 252 | switch (_a.label) { 253 | case 0: 254 | flush = options.flush === true; 255 | return [4 /*yield*/, getDcInstance()]; 256 | case 1: 257 | dc = _a.sent(); 258 | _a.label = 2; 259 | case 2: 260 | _a.trys.push([2, 4, , 5]); 261 | return [4 /*yield*/, dc.getMessages({ flush: flush })]; 262 | case 3: 263 | messages = _a.sent(); 264 | console.log(JSON.stringify(messages, null, 2)); 265 | process.exit(0); 266 | return [3 /*break*/, 5]; 267 | case 4: 268 | error_4 = _a.sent(); 269 | console.log(error_4.message.red); 270 | process.exit(1); 271 | return [2 /*return*/]; 272 | case 5: return [2 /*return*/]; 273 | } 274 | }); 275 | }); }); 276 | program 277 | .command("new-contact") 278 | .description("Store new contact in phonebook memory") 279 | .option("-i, --imei [imei]", "IMEI of the dongle") 280 | .option("--name [name]", "Contact's name") 281 | .option("--number [number]", "Contact's number") 282 | .action(function (options) { return __awaiter(void 0, void 0, void 0, function () { 283 | var name, number, imei, dc, dongle, contact, error_5; 284 | return __generator(this, function (_a) { 285 | switch (_a.label) { 286 | case 0: 287 | name = options.name, number = options.number; 288 | if (!name || !number) { 289 | console.log("Error: provide at least one of number or name".red); 290 | console.log(options.optionHelp()); 291 | process.exit(-1); 292 | } 293 | return [4 /*yield*/, selected_dongle.get(options)]; 294 | case 1: 295 | imei = _a.sent(); 296 | return [4 /*yield*/, getDcInstance()]; 297 | case 2: 298 | dc = _a.sent(); 299 | dongle = dc.usableDongles.get(imei); 300 | if (!dongle) { 301 | console.log("Error: selected dongle is disconnected or locked".red); 302 | process.exit(1); 303 | return [2 /*return*/]; 304 | } 305 | _a.label = 3; 306 | case 3: 307 | _a.trys.push([3, 5, , 6]); 308 | return [4 /*yield*/, dc.createContact(dongle.sim.imsi, number, name)]; 309 | case 4: 310 | contact = _a.sent(); 311 | return [3 /*break*/, 6]; 312 | case 5: 313 | error_5 = _a.sent(); 314 | console.log(error_5.message.red); 315 | process.exit(1); 316 | return [2 /*return*/]; 317 | case 6: 318 | console.log(JSON.stringify(contact, null, 2)); 319 | process.exit(0); 320 | return [2 /*return*/]; 321 | } 322 | }); 323 | }); }); 324 | program 325 | .command("delete-contact") 326 | .description("Delete a contact from phonebook memory") 327 | .option("-i, --imei [imei]", "IMEI of the dongle") 328 | .option("--index [index]", "Contact's index") 329 | .action(function (options) { return __awaiter(void 0, void 0, void 0, function () { 330 | var index, imei, dc, dongle, error_6; 331 | return __generator(this, function (_a) { 332 | switch (_a.label) { 333 | case 0: 334 | index = parseInt(options["index"]); 335 | if (isNaN(index)) { 336 | console.log("Error: command malformed".red); 337 | console.log(options.optionHelp()); 338 | process.exit(-1); 339 | } 340 | return [4 /*yield*/, selected_dongle.get(options)]; 341 | case 1: 342 | imei = _a.sent(); 343 | return [4 /*yield*/, getDcInstance()]; 344 | case 2: 345 | dc = _a.sent(); 346 | dongle = dc.usableDongles.get(imei); 347 | if (!dongle) { 348 | console.log("Error: selected dongle is disconnected or locked".red); 349 | process.exit(1); 350 | return [2 /*return*/]; 351 | } 352 | _a.label = 3; 353 | case 3: 354 | _a.trys.push([3, 5, , 6]); 355 | return [4 /*yield*/, dc.deleteContact(dongle.sim.imsi, index)]; 356 | case 4: 357 | _a.sent(); 358 | return [3 /*break*/, 6]; 359 | case 5: 360 | error_6 = _a.sent(); 361 | console.log(error_6.message.red); 362 | process.exit(1); 363 | return [2 /*return*/]; 364 | case 6: 365 | console.log("Contact at index " + index + " in SIM memory have been deleted successfully."); 366 | process.exit(0); 367 | return [2 /*return*/]; 368 | } 369 | }); 370 | }); }); 371 | function getDcInstance() { 372 | return __awaiter(this, void 0, void 0, function () { 373 | var _a, bind_addr, port, dc, _b; 374 | return __generator(this, function (_c) { 375 | switch (_c.label) { 376 | case 0: 377 | _a = InstallOptions_1.InstallOptions.get(), bind_addr = _a.bind_addr, port = _a.port; 378 | dc = chan_dongle_extended_client_1.DongleController.getInstance(bind_addr, port); 379 | _c.label = 1; 380 | case 1: 381 | _c.trys.push([1, 3, , 4]); 382 | return [4 /*yield*/, dc.prInitialization]; 383 | case 2: 384 | _c.sent(); 385 | return [3 /*break*/, 4]; 386 | case 3: 387 | _b = _c.sent(); 388 | console.log("dongle-extended is not running".red); 389 | process.exit(1); 390 | return [3 /*break*/, 4]; 391 | case 4: return [2 /*return*/, dc]; 392 | } 393 | }); 394 | }); 395 | } 396 | var selected_dongle; 397 | (function (selected_dongle) { 398 | var get_storage_user_path = function () { return path.join("/var/tmp", os.userInfo().username + "_selected_dongle"); }; 399 | function get(options) { 400 | return __awaiter(this, void 0, void 0, function () { 401 | var imei; 402 | return __generator(this, function (_a) { 403 | switch (_a.label) { 404 | case 0: 405 | if (!options.imei) return [3 /*break*/, 1]; 406 | return [2 /*return*/, options.imei]; 407 | case 1: return [4 /*yield*/, storage.init({ "dir": get_storage_user_path() })]; 408 | case 2: 409 | _a.sent(); 410 | return [4 /*yield*/, storage.getItem("cli_imei")]; 411 | case 3: 412 | imei = _a.sent(); 413 | if (!imei) { 414 | console.log("Error: No dongle selected"); 415 | process.exit(-1); 416 | } 417 | return [2 /*return*/, imei]; 418 | } 419 | }); 420 | }); 421 | } 422 | selected_dongle.get = get; 423 | function set(imei) { 424 | return __awaiter(this, void 0, void 0, function () { 425 | return __generator(this, function (_a) { 426 | switch (_a.label) { 427 | case 0: return [4 /*yield*/, storage.init({ "dir": get_storage_user_path() })]; 428 | case 1: 429 | _a.sent(); 430 | return [4 /*yield*/, storage.setItem("cli_imei", imei)]; 431 | case 2: 432 | _a.sent(); 433 | return [2 /*return*/]; 434 | } 435 | }); 436 | }); 437 | } 438 | selected_dongle.set = set; 439 | })(selected_dongle || (selected_dongle = {})); 440 | if (require.main === module) { 441 | process.once("unhandledRejection", function (error) { throw error; }); 442 | program.parse(process.argv); 443 | } 444 | -------------------------------------------------------------------------------- /src/lib/api.ts: -------------------------------------------------------------------------------- 1 | import { Modem, AtMessage } from "ts-gsm-modem"; 2 | import * as types from "./types"; 3 | import { InstallOptions } from "./InstallOptions"; 4 | import * as logger from "logger"; 5 | import { 6 | apiDeclaration, types as dcTypes, misc 7 | } from "../chan-dongle-extended-client"; 8 | import localApiDeclaration = apiDeclaration.service; 9 | import remoteApiDeclaration = apiDeclaration.controller; 10 | import { isVoid, Void } from "trackable-map"; 11 | import * as sipLibrary from "ts-sip"; 12 | import { Evt } from "evt"; 13 | import * as db from "./db"; 14 | import * as net from "net"; 15 | 16 | const debug= logger.debugFactory(); 17 | 18 | const sockets = new Set(); 19 | 20 | export async function beforeExit(){ 21 | return beforeExit.impl(); 22 | } 23 | 24 | export namespace beforeExit { 25 | export let impl= ()=> Promise.resolve(); 26 | } 27 | 28 | export function launch( 29 | modems: types.Modems, 30 | staticModuleConfiguration: dcTypes.StaticModuleConfiguration 31 | ): Promise { 32 | 33 | const { bind_addr, port } = InstallOptions.get(); 34 | 35 | const server = new sipLibrary.api.Server( 36 | makeApiHandlers(modems), 37 | sipLibrary.api.Server.getDefaultLogger({ 38 | "log": logger.log, 39 | "displayOnlyErrors": false, 40 | "hideKeepAlive": true 41 | }) 42 | ); 43 | 44 | const evtListening = Evt.create(); 45 | 46 | const netServer = net.createServer(); 47 | 48 | netServer 49 | .once("error", error => { throw error; }) 50 | .on("connection", async netSocket => { 51 | 52 | let socket = new sipLibrary.Socket(netSocket, true); 53 | 54 | server.startListening(socket); 55 | 56 | sockets.add(socket); 57 | 58 | socket.evtClose.attachOnce(() => sockets.delete(socket)); 59 | 60 | (() => { 61 | 62 | const methodName = remoteApiDeclaration.notifyCurrentState.methodName; 63 | type Params = remoteApiDeclaration.notifyCurrentState.Params; 64 | type Response = remoteApiDeclaration.notifyCurrentState.Response; 65 | 66 | sipLibrary.api.client.sendRequest( 67 | socket, 68 | methodName, 69 | { 70 | "dongles": Array.from(modems.values()).map(modem => buildDongleFromModem(modem)!), 71 | staticModuleConfiguration 72 | } 73 | ).catch(() => { }); 74 | 75 | })(); 76 | 77 | 78 | }) 79 | .once("listening", () => { 80 | 81 | beforeExit.impl = () => new Promise(resolve => { 82 | 83 | netServer.close(() => { 84 | 85 | debug("Terminated!"); 86 | 87 | resolve(); 88 | 89 | }); 90 | 91 | for (const socket of sockets.values()) { 92 | socket.destroy(); 93 | } 94 | 95 | }); 96 | 97 | evtListening.post(); 98 | 99 | }) 100 | .listen(port, bind_addr) 101 | ; 102 | 103 | modems.evt.attach( 104 | ([newModem, _, oldModem]) => { 105 | 106 | const dongleImei: string = (() => { 107 | 108 | if (isVoid(newModem)) { 109 | if (isVoid(oldModem)) throw "( never )"; 110 | return oldModem.imei; 111 | } else { 112 | if (!isVoid(oldModem)) throw "( never )"; 113 | return newModem.imei; 114 | } 115 | 116 | })(); 117 | 118 | { 119 | 120 | const { methodName } = remoteApiDeclaration.updateMap; 121 | type Params = remoteApiDeclaration.updateMap.Params; 122 | type Response = remoteApiDeclaration.updateMap.Response; 123 | 124 | broadcastRequest( 125 | methodName, 126 | { 127 | dongleImei, 128 | "dongle": buildDongleFromModem(newModem) 129 | } 130 | ); 131 | 132 | } 133 | 134 | if (types.matchModem(newModem)) { 135 | 136 | onNewModem(newModem); 137 | 138 | } 139 | 140 | } 141 | ); 142 | 143 | return new Promise( 144 | resolve => evtListening.attachOnce(() => resolve()) 145 | ); 146 | 147 | } 148 | 149 | function broadcastRequest( 150 | methodName: string, 151 | params: Params 152 | ): void { 153 | 154 | for (let socket of sockets) { 155 | 156 | sipLibrary.api.client.sendRequest( 157 | socket, 158 | methodName, 159 | params 160 | ).catch(() => { }); 161 | 162 | } 163 | 164 | } 165 | 166 | function onNewModem(modem: Modem) { 167 | 168 | const dongleImei = modem.imei; 169 | 170 | modem.evtGsmConnectivityChange.attach(() => { 171 | 172 | const { methodName } = remoteApiDeclaration.notifyGsmConnectivityChange; 173 | type Params = remoteApiDeclaration.notifyGsmConnectivityChange.Params; 174 | type Response = remoteApiDeclaration.notifyGsmConnectivityChange.Response; 175 | 176 | for (let socket of sockets) { 177 | 178 | sipLibrary.api.client.sendRequest( 179 | socket, 180 | methodName, 181 | { dongleImei } 182 | ).catch(() => { }); 183 | 184 | } 185 | 186 | }); 187 | 188 | modem.evtCellSignalStrengthTierChange.attach(() => { 189 | 190 | const { methodName } = remoteApiDeclaration.notifyCellSignalStrengthChange; 191 | type Params = remoteApiDeclaration.notifyCellSignalStrengthChange.Params; 192 | type Response = remoteApiDeclaration.notifyCellSignalStrengthChange.Response; 193 | 194 | for (let socket of sockets) { 195 | 196 | sipLibrary.api.client.sendRequest( 197 | socket, 198 | methodName, 199 | { 200 | dongleImei, 201 | "cellSignalStrength": 202 | buildDongleFromModem.modemCellSignalStrengthTierToDongleCellSignalStrength( 203 | modem.getCurrentGsmConnectivityState().cellSignalStrength.tier 204 | ) 205 | } 206 | ).catch(() => { }); 207 | 208 | } 209 | 210 | 211 | }); 212 | 213 | modem.evtMessage.attach(async message => { 214 | 215 | const { methodName } = remoteApiDeclaration.notifyMessage; 216 | type Params = remoteApiDeclaration.notifyMessage.Params; 217 | type Response = remoteApiDeclaration.notifyMessage.Response; 218 | 219 | const response = await new Promise(resolve => { 220 | 221 | let tasks: Promise[] = []; 222 | 223 | for (let socket of sockets) { 224 | 225 | tasks[tasks.length] = (async () => { 226 | 227 | try { 228 | 229 | const response = await sipLibrary.api.client.sendRequest( 230 | socket, 231 | methodName, 232 | { dongleImei, message } 233 | ); 234 | 235 | if (response === "DO NOT SAVE MESSAGE") { 236 | resolve(response); 237 | } 238 | 239 | } catch{ } 240 | 241 | })(); 242 | 243 | } 244 | 245 | Promise.all(tasks).then(() => resolve("SAVE MESSAGE")); 246 | 247 | }); 248 | 249 | if (response === "SAVE MESSAGE") { 250 | 251 | await db.messages.save(modem.imsi, message); 252 | 253 | } 254 | 255 | }); 256 | 257 | modem.evtMessageStatusReport.attach( 258 | statusReport => { 259 | 260 | const methodName = remoteApiDeclaration.notifyStatusReport.methodName; 261 | type Params = remoteApiDeclaration.notifyStatusReport.Params; 262 | type Response = remoteApiDeclaration.notifyStatusReport.Response; 263 | 264 | broadcastRequest(methodName, { dongleImei, statusReport }); 265 | 266 | } 267 | ); 268 | 269 | } 270 | 271 | function makeApiHandlers(modems: types.Modems): sipLibrary.api.Server.Handlers { 272 | 273 | const handlers: sipLibrary.api.Server.Handlers = {}; 274 | 275 | { 276 | 277 | const { methodName } = localApiDeclaration.sendMessage; 278 | type Params = localApiDeclaration.sendMessage.Params; 279 | type Response = localApiDeclaration.sendMessage.Response; 280 | 281 | let handler: sipLibrary.api.Server.Handler = { 282 | "handler": async ({ viaDongleImei, toNumber, text }) => { 283 | 284 | const modem = modems.find(({ imei }) => imei === viaDongleImei); 285 | 286 | if (!types.matchModem(modem)) { 287 | 288 | return { "success": false, "reason": "DISCONNECT" }; 289 | 290 | } 291 | 292 | let sendDate: Date | undefined; 293 | 294 | try { 295 | 296 | sendDate = await performModemAction( 297 | modem, () => modem.sendMessage(toNumber, text) 298 | ); 299 | 300 | } catch { 301 | 302 | return { "success": false, "reason": "DISCONNECT" }; 303 | 304 | } 305 | 306 | if (sendDate === undefined) { 307 | 308 | return { "success": false, "reason": "CANNOT SEND" }; 309 | 310 | } 311 | 312 | return { "success": true, sendDate }; 313 | 314 | 315 | } 316 | }; 317 | 318 | handlers[methodName] = handler; 319 | 320 | } 321 | 322 | { 323 | 324 | const { methodName } = localApiDeclaration.unlock; 325 | type Params = localApiDeclaration.unlock.Params; 326 | type Response = localApiDeclaration.unlock.Response; 327 | 328 | let handler: sipLibrary.api.Server.Handler = { 329 | "handler": async (params) => { 330 | 331 | let dongleImei = params.dongleImei; 332 | 333 | let lockedModem = modems.find(({ imei }) => dongleImei === imei); 334 | 335 | if (!types.LockedModem.match(lockedModem)) { 336 | 337 | return undefined; 338 | 339 | } 340 | 341 | try { 342 | 343 | if (localApiDeclaration.unlock.matchPin(params)) { 344 | 345 | return await lockedModem.performUnlock(params.pin); 346 | 347 | } else { 348 | 349 | return await lockedModem.performUnlock(params.puk, params.newPin); 350 | 351 | } 352 | 353 | } catch{ 354 | 355 | return undefined; 356 | 357 | } 358 | 359 | } 360 | }; 361 | 362 | handlers[methodName] = handler; 363 | 364 | } 365 | 366 | { 367 | 368 | const { methodName } = localApiDeclaration.rebootDongle 369 | type Params = localApiDeclaration.rebootDongle.Params; 370 | type Response = localApiDeclaration.rebootDongle.Response; 371 | 372 | const handler: sipLibrary.api.Server.Handler = { 373 | "handler": async ({ imei }) => { 374 | 375 | const modem = Array.from(modems.values()) 376 | .find(modem => modem.imei === imei); 377 | 378 | if (!modem) { 379 | 380 | return undefined; 381 | 382 | } 383 | 384 | modem["__api_rebootDongle_called__"] = true; 385 | 386 | await modem.terminate(); 387 | 388 | return undefined; 389 | 390 | } 391 | }; 392 | 393 | handlers[methodName] = handler; 394 | 395 | } 396 | 397 | { 398 | 399 | const { methodName } = localApiDeclaration.getMessages; 400 | type Params = localApiDeclaration.getMessages.Params; 401 | type Response = localApiDeclaration.getMessages.Response; 402 | 403 | let handler: sipLibrary.api.Server.Handler = { 404 | "handler": params => db.messages.retrieve(params) 405 | }; 406 | 407 | handlers[methodName] = handler; 408 | 409 | } 410 | 411 | { 412 | 413 | const { methodName } = localApiDeclaration.createContact; 414 | type Params = localApiDeclaration.createContact.Params; 415 | type Response = localApiDeclaration.createContact.Response; 416 | 417 | const handler: sipLibrary.api.Server.Handler = { 418 | "handler": async ({ imsi, number, name }) => { 419 | 420 | const modem = Array.from(modems.values()) 421 | .filter(types.matchModem) 422 | .find(modem => modem.imsi === imsi) 423 | ; 424 | 425 | if (!modem) { 426 | return { "isSuccess": false }; 427 | } 428 | 429 | let contact: dcTypes.Sim.Contact; 430 | 431 | try { 432 | 433 | contact = await performModemAction(modem, 434 | () => modem.createContact(number, name) 435 | ); 436 | 437 | } catch{ 438 | 439 | return { "isSuccess": false }; 440 | 441 | } 442 | 443 | return { "isSuccess": true, contact }; 444 | 445 | } 446 | }; 447 | 448 | handlers[methodName] = handler; 449 | 450 | } 451 | 452 | { 453 | 454 | const { methodName } = localApiDeclaration.updateContact; 455 | type Params = localApiDeclaration.updateContact.Params; 456 | type Response = localApiDeclaration.updateContact.Response; 457 | 458 | const handler: sipLibrary.api.Server.Handler = { 459 | "handler": async ({ imsi, index, new_number, new_name }) => { 460 | 461 | const modem = Array.from(modems.values()) 462 | .filter(types.matchModem) 463 | .find(modem => modem.imsi === imsi) 464 | ; 465 | 466 | if (!modem) { 467 | return { "isSuccess": false }; 468 | } 469 | 470 | let contact: dcTypes.Sim.Contact; 471 | 472 | try { 473 | 474 | contact = await performModemAction(modem, 475 | () => modem.updateContact( 476 | index, { "name": new_name, "number": new_number } 477 | ) 478 | ); 479 | 480 | } catch{ 481 | 482 | return { "isSuccess": false }; 483 | 484 | } 485 | 486 | return { "isSuccess": true, contact }; 487 | 488 | } 489 | }; 490 | 491 | handlers[methodName] = handler; 492 | 493 | } 494 | 495 | { 496 | 497 | const { methodName } = localApiDeclaration.deleteContact; 498 | type Params = localApiDeclaration.deleteContact.Params; 499 | type Response = localApiDeclaration.deleteContact.Response; 500 | 501 | const handler: sipLibrary.api.Server.Handler = { 502 | "handler": async ({ imsi, index }) => { 503 | 504 | const modem = Array.from(modems.values()) 505 | .filter(types.matchModem) 506 | .find(modem => modem.imsi === imsi) 507 | ; 508 | 509 | if (!modem) { 510 | return { "isSuccess": false }; 511 | } 512 | 513 | try { 514 | 515 | await performModemAction(modem, 516 | () => modem.deleteContact(index) 517 | ); 518 | 519 | } catch{ 520 | 521 | return { "isSuccess": false }; 522 | 523 | } 524 | 525 | return { "isSuccess": true }; 526 | 527 | } 528 | }; 529 | 530 | handlers[methodName] = handler; 531 | 532 | } 533 | 534 | return handlers; 535 | 536 | } 537 | 538 | /** 539 | * Perform an action on modem, throw if the Modem disconnect 540 | * before the action is completed. 541 | * */ 542 | async function performModemAction( 543 | modem: Modem, 544 | action: () => Promise 545 | ): Promise { 546 | 547 | const ctx = Evt.newCtx(); 548 | 549 | const response = await Promise.race([ 550 | action(), 551 | new Promise( 552 | (_, reject) => modem.evtTerminate.attachOnce( 553 | ctx, 554 | () => reject( 555 | new Error("Modem disconnect while performing action") 556 | ) 557 | ) 558 | ) 559 | ]); 560 | 561 | modem.evtTerminate.detach(ctx); 562 | 563 | return response; 564 | 565 | } 566 | 567 | function buildDongleFromModem( 568 | modem: Modem | types.LockedModem | Void 569 | ): dcTypes.Dongle | undefined { 570 | 571 | if (types.LockedModem.match(modem)) { 572 | 573 | return buildDongleFromModem.buildLockedDongleFromLockedModem(modem); 574 | 575 | } else if (types.matchModem(modem)) { 576 | 577 | return buildDongleFromModem.buildUsableDongleFromModem(modem); 578 | 579 | } else { 580 | 581 | return undefined; 582 | 583 | } 584 | 585 | } 586 | 587 | namespace buildDongleFromModem { 588 | 589 | export function buildLockedDongleFromLockedModem( 590 | lockedModem: types.LockedModem 591 | ): dcTypes.Dongle.Locked { 592 | 593 | return { 594 | "imei": lockedModem.imei, 595 | "manufacturer": lockedModem.manufacturer, 596 | "model": lockedModem.model, 597 | "firmwareVersion": lockedModem.firmwareVersion, 598 | "sim": { 599 | "iccid": lockedModem.iccid, 600 | "pinState": lockedModem.pinState, 601 | "tryLeft": lockedModem.tryLeft 602 | } 603 | }; 604 | 605 | } 606 | 607 | export function buildUsableDongleFromModem( 608 | modem: Modem 609 | ): dcTypes.Dongle.Usable { 610 | 611 | let number = modem.number; 612 | let storageLeft = modem.storageLeft; 613 | 614 | let contacts: dcTypes.Sim.Contact[] = modem.contacts; 615 | 616 | let imsi = modem.imsi; 617 | 618 | let digest = misc.computeSimStorageDigest(number, storageLeft, contacts); 619 | 620 | let simCountryAndSp = misc.getSimCountryAndSp(imsi); 621 | 622 | return { 623 | "imei": modem.imei, 624 | "manufacturer": modem.manufacturer, 625 | "model": modem.model, 626 | "firmwareVersion": modem.firmwareVersion, 627 | "isVoiceEnabled": modem.isVoiceEnabled, 628 | "sim": { 629 | "iccid": modem.iccid, 630 | imsi, 631 | "country": simCountryAndSp ? ({ 632 | "iso": simCountryAndSp.iso, 633 | "code": simCountryAndSp.code, 634 | "name": simCountryAndSp.name 635 | }) : undefined, 636 | "serviceProvider": { 637 | "fromImsi": simCountryAndSp ? simCountryAndSp.serviceProvider : undefined, 638 | "fromNetwork": modem.serviceProviderName 639 | }, 640 | "storage": { 641 | "number": number || undefined, 642 | "infos": { 643 | "contactNameMaxLength": modem.contactNameMaxLength, 644 | "numberMaxLength": modem.numberMaxLength, 645 | storageLeft 646 | }, 647 | contacts, 648 | digest 649 | } 650 | }, 651 | "cellSignalStrength": modemCellSignalStrengthTierToDongleCellSignalStrength( 652 | modem.getCurrentGsmConnectivityState().cellSignalStrength.tier 653 | ), 654 | "isGsmConnectivityOk": modem.isGsmConnectivityOk() 655 | }; 656 | 657 | } 658 | 659 | export function modemCellSignalStrengthTierToDongleCellSignalStrength( 660 | tier: AtMessage.GsmOrUtranCellSignalStrengthTier 661 | ): dcTypes.Dongle.Usable.CellSignalStrength { 662 | switch (tier) { 663 | case "<=-113 dBm": return "VERY WEAK"; 664 | case "-111 dBm": return "WEAK"; 665 | case "–109 dBm to –53 dBm": return "GOOD"; 666 | case "≥ –51 dBm": return "EXCELLENT"; 667 | case "Unknown or undetectable": return "NULL"; 668 | } 669 | } 670 | 671 | } 672 | --------------------------------------------------------------------------------