├── joinserver └── deviceId ├── icons └── join.png ├── README.md ├── js ├── util.js ├── web.js ├── ipgetter.js ├── extensions.js ├── gcm.js ├── joinapi.js ├── join.js ├── autoappscommand.js ├── authTokenGetter.js ├── device.js ├── legacy.js ├── sender.js └── encryption.js ├── .gitattributes ├── package.json ├── .gitignore ├── join-config.js ├── join-receive-message.js ├── join-server.html ├── join-message.js ├── join-receive-message.html ├── join-config.html ├── join-message.html └── join-server.js /joinserver/deviceId: -------------------------------------------------------------------------------- 1 | 6f49aa0a31964d89be3d6632a38a3996 -------------------------------------------------------------------------------- /icons/join.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaomgcd/node-red-contrib-join-joaoapps/HEAD/icons/join.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node-Red nodes for [Join by JoaoApps](https://joaoapps.com/join/) 2 | 3 | All info about the Join Node-RED integration [here](https://joaoapps.com/join/node-red/). 4 | 5 | Download Join from Google Play [here](https://play.google.com/store/apps/details?id=com.joaomgcd.join). 6 | -------------------------------------------------------------------------------- /js/util.js: -------------------------------------------------------------------------------- 1 | const fetchPromise = import('node-fetch') 2 | module.exports = { 3 | isString : function(value){ 4 | return typeof value === 'string'; 5 | }, 6 | copyProps : function(source,destination){ 7 | for(var prop in source){ 8 | destination[prop] = source[prop]; 9 | } 10 | }, 11 | fetch: () => fetchPromise.then(({ default: fetch }) => fetch) 12 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /js/web.js: -------------------------------------------------------------------------------- 1 | 2 | const util = require("./util"); 3 | 4 | module.exports = { 5 | post : function(url, obj, node){ 6 | var options = { 7 | method: 'POST', 8 | body: JSON.stringify(obj), 9 | headers: { 10 | 'Content-Type': 'application/json' 11 | } 12 | } 13 | /*if(node){ 14 | node.log(`Posting: ${options.body}`) 15 | }*/ 16 | return util.fetch() 17 | .then(fetch=>fetch(url,options)) 18 | .then(res=>res.json()) 19 | }, 20 | get : function(url, node){ 21 | var options = { 22 | method: 'GET', 23 | headers: { 24 | 'Content-Type': 'application/json' 25 | } 26 | } 27 | return util.fetch() 28 | .then(fetch =>fetch(url)) 29 | .then(res=>res.json()) 30 | } 31 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-contrib-join-joaoapps", 3 | "version": "1.2.4", 4 | "description": "Join by JoaoApps for Node-Red", 5 | "main": "join-message.js", 6 | "scripts": { 7 | "test": "test" 8 | }, 9 | "keywords": [ 10 | "join", 11 | "node-red" 12 | ], 13 | "author": "João Dias", 14 | "license": "ISC", 15 | "node-red": { 16 | "nodes": { 17 | "join-config": "join-config.js", 18 | "join-server": "join-server.js", 19 | "join-receive-message": "join-receive-message.js", 20 | "join-message": "join-message.js" 21 | } 22 | }, 23 | "dependencies": { 24 | "ip": "^1.1.5", 25 | "node-fetch": "^3.3.2", 26 | "node-localstorage": "^1.3.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /js/ipgetter.js: -------------------------------------------------------------------------------- 1 | 2 | const ip = require("ip"); 3 | const web = require("./web"); 4 | module.exports = class IpGetter { 5 | constructor(overrideLocal,overridePublic,logger){ 6 | this.overrideLocal = overrideLocal; 7 | this.overridePublic = overridePublic; 8 | this.logger = logger; 9 | } 10 | getIps(){ 11 | return (this.overridePublic ? Promise.resolve(this.overridePublic) : web.get("http://httpbin.org/ip") 12 | .then(result=>result.origin) 13 | .catch(error=>null)) 14 | .then(publicIp=>{ 15 | var localIp = this.overrideLocal || ip.address(); 16 | const indexOfComma = publicIp.indexOf(","); 17 | this.logger.log("Comma: " + indexOfComma); 18 | if(indexOfComma>0){ 19 | publicIp = publicIp.substring(0,indexOfComma); 20 | } 21 | return {"localIp":localIp,"publicIp":publicIp}; 22 | }); 23 | } 24 | } -------------------------------------------------------------------------------- /js/extensions.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(Array.prototype, "groupBy", { 2 | value: function groupBy(keyGetter) { 3 | if(!this || this.length == 0) return []; 4 | 5 | return this.reduce(function(rv, x) { 6 | if(!rv.groups){ 7 | rv.groups = []; 8 | } 9 | var key = keyGetter(x); 10 | var group = rv.groups.find(existing=>existing.key != null && existing.key == key); 11 | if(!group){ 12 | group = {}; 13 | group.key = key; 14 | group.values = []; 15 | rv.groups.push(group); 16 | } 17 | group.values.push(x); 18 | return rv; 19 | }, {}).groups; 20 | } 21 | }); 22 | Object.defineProperty(Array.prototype, "count", { 23 | value: function count(filter) { 24 | return this.filter(filter).length; 25 | } 26 | }); 27 | Object.defineProperty(Array.prototype, "unique", { 28 | value: function unique(selector) { 29 | var result = [] 30 | for(var item of this){ 31 | var key = selector(item); 32 | if(result.find(existing => selector(existing) == key)) continue; 33 | result.push(item) 34 | } 35 | return result; 36 | } 37 | }); -------------------------------------------------------------------------------- /js/gcm.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | executeGcm : function(node, gcmRaw){ 3 | var gcm = this.getGcm(node,gcmRaw); 4 | if(!gcm) return; 5 | gcm.execute(node); 6 | }, 7 | getGcm : function(node, gcmRaw){ 8 | if(typeof gcmRaw === 'string'){ 9 | try{ 10 | gcmRaw = JSON.parse(gcmRaw); 11 | }catch(e){ 12 | return null; 13 | } 14 | } 15 | try{ 16 | var type = gcmRaw.type; 17 | var json = gcmRaw.json; 18 | if(!type || !json) return null; 19 | var gcm = new classes[type](); 20 | //node.log(`Found class type ${type}...`); 21 | gcm.fromJsonString(json); 22 | return gcm; 23 | }catch(e){ 24 | node.log(`Error processing GCM: ${e}`); 25 | } 26 | return null; 27 | } 28 | } 29 | 30 | class GCM { 31 | constructor(){ 32 | } 33 | execute(node) { 34 | // node.log(this); 35 | } 36 | fromJson(json) { 37 | for (var prop in json) { 38 | this[prop] = json[prop]; 39 | } 40 | } 41 | fromJsonString(str) { 42 | var json = JSON.parse(str); 43 | this.fromJson(json); 44 | 45 | } 46 | } 47 | class GCMPush extends GCM { 48 | constructor(){ 49 | super(); 50 | } 51 | execute(node) { 52 | // node.log(this); 53 | node.reportCommand(this.push.text); 54 | } 55 | } 56 | const classes = { 57 | GCMPush 58 | }; -------------------------------------------------------------------------------- /js/joinapi.js: -------------------------------------------------------------------------------- 1 | 2 | const web = require("./web"); 3 | const USE_LOCAL_SERVER = false; 4 | const JOIN_SERVER_LOCAL = "http://localhost:8080"; 5 | const JOIN_SERVER = "https://joinjoaomgcd.appspot.com"; 6 | const JOIN_BASE_URL = `${USE_LOCAL_SERVER ? JOIN_SERVER_LOCAL : JOIN_SERVER}/_ah/api/`; 7 | module.exports = { 8 | //options: apikey,deviceName,port, 9 | registerInJoinServer : function(node,options){ 10 | var publicIp = options.publicIp; 11 | var localIp = options.localIp; 12 | if(!publicIp){ 13 | publicIp = localIp; 14 | } 15 | var registration = { 16 | "apikey":options.apikey, 17 | "regId": `${publicIp}:${options.port}`, 18 | "regId2": `${localIp}:${options.port}`, 19 | "deviceName":options.deviceName, 20 | "deviceType":13 21 | }; 22 | if(options.deviceId){ 23 | registration.deviceId = options.deviceId; 24 | } 25 | return web.post(`${JOIN_BASE_URL}registration/v1/registerDevice/`,registration,node) 26 | .then(result=>{ 27 | if(!result.success){ 28 | return node.error(result.errorMessage); 29 | } 30 | node.log (`Registered device: ${result.deviceId}: ${result.errorMessage}`); 31 | return result.deviceId; 32 | }) 33 | .catch(error=>{ 34 | node.error(error); 35 | }) 36 | }, 37 | 38 | sendPush : function(push){ 39 | return web.post(`${JOIN_BASE_URL}messaging/v1/sendPush`,push); 40 | }, 41 | 42 | listDevices : function(apikey){ 43 | return web.get(`${JOIN_BASE_URL}registration/v1/listDevices/?apikey=${apikey}`); 44 | } 45 | } -------------------------------------------------------------------------------- /.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 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directories 27 | node_modules 28 | jspm_packages 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | # ========================= 37 | # Operating System Files 38 | # ========================= 39 | 40 | # OSX 41 | # ========================= 42 | 43 | .DS_Store 44 | .AppleDouble 45 | .LSOverride 46 | 47 | # Thumbnails 48 | ._* 49 | 50 | # Files that might appear in the root of a volume 51 | .DocumentRevisions-V100 52 | .fseventsd 53 | .Spotlight-V100 54 | .TemporaryItems 55 | .Trashes 56 | .VolumeIcon.icns 57 | 58 | # Directories potentially created on remote AFP share 59 | .AppleDB 60 | .AppleDesktop 61 | Network Trash Folder 62 | Temporary Items 63 | .apdisk 64 | 65 | # Windows 66 | # ========================= 67 | 68 | # Windows image file caches 69 | Thumbs.db 70 | ehthumbs.db 71 | 72 | # Folder config file 73 | Desktop.ini 74 | 75 | # Recycle Bin used on file shares 76 | $RECYCLE.BIN/ 77 | 78 | # Windows Installer files 79 | *.cab 80 | *.msi 81 | *.msm 82 | *.msp 83 | 84 | # Windows shortcuts 85 | *.lnk 86 | package-lock.json 87 | package-lock.json 88 | -------------------------------------------------------------------------------- /js/join.js: -------------------------------------------------------------------------------- 1 | 2 | const DevicesLib = require("./device"); 3 | const Devices = DevicesLib.Devices; 4 | module.exports = class Join { 5 | constructor(apiKey){ 6 | this.apiKey = apiKey; 7 | } 8 | get devices(){ 9 | return (async () => { 10 | if(!this.devicesInMemory){ 11 | this.devicesInMemory = await Devices.fromServer(this.apiKey); 12 | } 13 | return this.devicesInMemory; 14 | })(); 15 | } 16 | async sendPush(push,deviceFilter,options){ 17 | push.apikey = this.apiKey; 18 | var devices = await this.devices; 19 | var devicesByPushProperties = null; 20 | if(push.deviceIds){ 21 | if(!devicesByPushProperties) devicesByPushProperties = []; 22 | var split = push.deviceIds.split(","); 23 | devicesByPushProperties = devicesByPushProperties.concat(devices.filter(device=>split.indexOf(device.deviceId)>=0)); 24 | } 25 | if(push.deviceNames){ 26 | if(!devicesByPushProperties) devicesByPushProperties = []; 27 | var split = push.deviceNames.split(",").map(name=>name.toLowerCase()); 28 | devicesByPushProperties = devicesByPushProperties.concat(devices.filter(device=>{ 29 | var deviceName = device.deviceName.toLowerCase(); 30 | for(var inputName of split){ 31 | if(deviceName.indexOf(inputName)>=0){ 32 | return true; 33 | } 34 | } 35 | return false; 36 | })); 37 | } 38 | if(devicesByPushProperties){ 39 | devices = Devices.fromArray(devicesByPushProperties); 40 | } 41 | if(deviceFilter){ 42 | if(typeof deviceFilter == "function"){ 43 | devices = devices.filter(deviceFilter); 44 | }else{ 45 | var fromDevice = deviceFilter.asDevices; 46 | devices = fromDevice ? fromDevice : deviceFilter; 47 | } 48 | } 49 | if(!devices || devices.length == 0) throw "No devices to send push to"; 50 | devices = Devices.fromArray(devices.unique(device=>device.deviceId)); 51 | if(!push.senderId){ 52 | push.senderId = options.node.deviceId; 53 | } 54 | //options.node.log(`Sending to ${devices.map(device=>device.deviceName)}`) 55 | return devices.sendPush(push,options); 56 | } 57 | sendCommand(command,deviceFilter,options){ 58 | return this.sendPush({"text":command},deviceFilter,options); 59 | } 60 | notify(title,text,deviceFilter,options){ 61 | return this.sendPush({"title":text,"text":text},deviceFilter,options); 62 | } 63 | } -------------------------------------------------------------------------------- /join-config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(RED) { 3 | function JoinConfigNode(config) { 4 | RED.nodes.createNode(this,config); 5 | var node = this; 6 | if(node.credentials.apikey){ 7 | node.credentials.apikey = node.credentials.apikey.trim(); 8 | } 9 | this.apikey = node.credentials.apikey; 10 | this.deviceName = node.credentials.deviceName; 11 | this.salt = node.credentials.encryptionAccount; 12 | this.encryptionKey = node.credentials.encryptionKey; 13 | this.register = config.register; 14 | var runSetup = async ()=>{ 15 | /*var deviceListResult = await joinapi.listDevices(this.apikey); 16 | if(!deviceListResult.success){ 17 | return node.log(`Couldn't list devices: ${deviceListResult.errorMessage}`); 18 | } 19 | node.devices = deviceListResult.records;*/ 20 | //node.log(`Devices: ${JSON.stringify(node.devices.groupBy(device=>device.deviceType))}`) 21 | /*var devicesToSendTo = node.devices.filter(device=>device.deviceType == 12); 22 | var options = { 23 | "devices": devicesToSendTo, 24 | "gcmPush": { 25 | "push":{"text":"test=:=blabla"} 26 | }, 27 | "node":node 28 | } 29 | var sendResult = await new Sender.SenderIFTTT().send(options); 30 | node.log(`Send Result: ${JSON.stringify(sendResult)}`)*/ 31 | /* var join = new Join(node.apikey); 32 | var sendResult = await join.sendCommand("test=:=blabla",device=>device.deviceType == 12); 33 | node.log(`Send Result: ${JSON.stringify(sendResult)}`)*/ 34 | } 35 | runSetup(); 36 | 37 | } 38 | RED.httpAdmin.get('/js/*', function(req, res){ 39 | var options = { 40 | root: __dirname + '/js/', 41 | dotfiles: 'deny' 42 | }; 43 | res.sendFile(req.params[0], options); 44 | }); 45 | RED.nodes.registerType("join-config",JoinConfigNode,{ 46 | credentials: { 47 | apikey: {type:"text",required:true}, 48 | deviceName: {type:"text",value:"Node-RED",required:true}, 49 | encryptionAccount: {type:"text",required:false}, 50 | encryptionKey: {type:"text",required:false} 51 | } 52 | }); 53 | } -------------------------------------------------------------------------------- /js/autoappscommand.js: -------------------------------------------------------------------------------- 1 | module.exports = class AutoAppsCommand { 2 | constructor(message, variables, options){ 3 | this.message = message; 4 | this.variables = variables; 5 | this.parseNumbers = options ? options.parseNumbers : false; 6 | this.payload = this.getPayload(); 7 | this.command = this.payload["command"] ? this.payload.command : this.payload; 8 | this.values = this.getValues(); 9 | } 10 | getValues(){ 11 | var values = []; 12 | if(this.message.indexOf("=:=")<0) return values; 13 | var commandParts = this.message.split("=:="); 14 | if(commandParts.length<2) return values; 15 | commandParts.shift(); 16 | for(var commandPart of commandParts){ 17 | commandPart = this.parseNumberIfCan(commandPart); 18 | values.push(commandPart); 19 | } 20 | return values; 21 | } 22 | getPayload(){ 23 | var payload = this.message; 24 | var getCommandPayload = () => ({"command":payload}); 25 | if(typeof this.variables == "string"){ 26 | this.variables = this.variables.split(","); 27 | } 28 | if(!this.variables || this.variables.length == 0 || this.message.indexOf("=:=")<0){ 29 | return getCommandPayload(); 30 | } 31 | var commandParts = this.message.split("=:="); 32 | if(commandParts.length == 0){ 33 | return getCommandPayload(); 34 | } 35 | payload = {"command":commandParts[0]}; 36 | this.values = []; 37 | var lastVariable = null; 38 | for (var i = 0; i < commandParts.length- 1 ; i++) { 39 | var variable = this.variables[i]; 40 | if(variable){ 41 | lastVariable = variable; 42 | } 43 | var commandPart = commandParts[i+1]; 44 | if(!commandPart) continue; 45 | 46 | this.values.push(commandPart); 47 | commandPart = this.parseNumberIfCan(commandPart); 48 | if(payload[lastVariable]){ 49 | if(!(payload[lastVariable] instanceof Array)){ 50 | payload[lastVariable] = [payload[lastVariable]]; 51 | } 52 | payload[lastVariable].push(commandPart); 53 | }else{ 54 | payload[lastVariable] = commandPart; 55 | } 56 | } 57 | return payload; 58 | } 59 | isMatch(configuredCommand){ 60 | return this.command == configuredCommand; 61 | } 62 | parseNumberIfCan(numberString){ 63 | if(!this.parseNumbers) return numberString; 64 | 65 | var number = parseInt(numberString); 66 | if(isNaN(number)) return numberString; 67 | 68 | numberString = number; 69 | return numberString; 70 | } 71 | } -------------------------------------------------------------------------------- /js/authTokenGetter.js: -------------------------------------------------------------------------------- 1 | const { createSign } = require('crypto'); 2 | const util = require("./util"); 3 | const fetch = util.fetch; 4 | 5 | module.exports = class AuthTokenGetter { 6 | constructor(accountEmail, privateKey) { 7 | this.accountEmail = accountEmail; 8 | this.privateKey = privateKey; 9 | } 10 | 11 | static accessToken = null; 12 | static tokenExpiry = null; 13 | 14 | async getAccessToken() { 15 | // Check if we have a cached token and it's still valid 16 | if (AuthTokenGetter.accessToken && AuthTokenGetter.tokenExpiry && Date.now() < AuthTokenGetter.tokenExpiry) { 17 | console.log("reused token from static cache"); 18 | return AuthTokenGetter.accessToken; 19 | } 20 | 21 | const accountEmail = this.accountEmail; 22 | const privateKey = this.privateKey; 23 | 24 | const iat = Math.floor(Date.now() / 1000); // Issued at time 25 | const exp = iat + 3600; // Expires in one hour 26 | 27 | const payload = { 28 | iss: accountEmail, 29 | sub: accountEmail, 30 | aud: 'https://oauth2.googleapis.com/token', 31 | iat: iat, 32 | exp: exp, 33 | scope: 'https://www.googleapis.com/auth/firebase.messaging', // Customize the scopes as needed 34 | }; 35 | 36 | const header = { 37 | alg: 'RS256', 38 | typ: 'JWT', 39 | }; 40 | 41 | const base64Encode = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); 42 | 43 | const signingInput = `${base64Encode(header)}.${base64Encode(payload)}`; 44 | 45 | const sign = createSign('RSA-SHA256'); 46 | sign.update(signingInput); 47 | const signature = sign.sign(privateKey, 'base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); 48 | 49 | const jwt = `${signingInput}.${signature}`; 50 | 51 | const fetchProper = await fetch(); 52 | const tokenResponse = await fetchProper('https://oauth2.googleapis.com/token', { 53 | method: 'POST', 54 | headers: { 55 | 'Content-Type': 'application/x-www-form-urlencoded', 56 | }, 57 | body: new URLSearchParams({ 58 | grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', 59 | assertion: jwt, 60 | }).toString(), 61 | }); 62 | 63 | if (!tokenResponse.ok) { 64 | const errorResponse = await tokenResponse.json(); 65 | throw new Error(`Failed to get token: ${errorResponse.error} - ${errorResponse.error_description}`); 66 | } 67 | 68 | const { access_token, expires_in } = await tokenResponse.json(); 69 | 70 | // Cache the new token and set its expiration time 71 | AuthTokenGetter.accessToken = access_token; 72 | AuthTokenGetter.tokenExpiry = Date.now() + (expires_in * 1000); 73 | console.log("new token fetched and cached"); 74 | 75 | return AuthTokenGetter.accessToken; 76 | } 77 | }; -------------------------------------------------------------------------------- /join-receive-message.js: -------------------------------------------------------------------------------- 1 | const AutoAppsCommand = require("./js/autoappscommand"); 2 | const gcm = require("./js/gcm"); 3 | const util = require("./js/util"); 4 | const Encryption = require("./js/encryption.js"); 5 | 6 | module.exports = function(RED) { 7 | function JoinReceiveMessageNode(config) { 8 | RED.nodes.createNode(this,config); 9 | var node = this; 10 | var server = RED.nodes.getNode(config.server); 11 | const joinConfig = RED.nodes.getNode(server.config.joinConfig); 12 | var variables = []; 13 | 14 | var handleIncomingMessage = async command => { 15 | 16 | var gcmMessage = gcm.getGcm(node,command); 17 | var isFullPush = gcmMessage && gcmMessage.push && gcmMessage.push.text; 18 | if(isFullPush){ 19 | //node.log(`Full push: ${command}`); 20 | command = gcmMessage.push.text; 21 | }/*else{ 22 | node.log(`Just command push: ${command}`); 23 | }*/ 24 | // node.log(`Parsing command "${command}" with variables "${config.variables}"`); 25 | if(!util.isString(command)){ 26 | return node.error(`Received command must be a string, was of type ${typeof command}`) 27 | } 28 | const salt = joinConfig.salt; 29 | const password = joinConfig.encryptionKey; 30 | console.log("salt and pass",joinConfig,salt,password); 31 | if(salt && password){ 32 | command = await Encryption.decrypt(command, password) 33 | } 34 | var autoAppsCommand = new AutoAppsCommand(command,config.variables,{ 35 | "parseNumbers": config.parseNumbers 36 | }); 37 | var isMatch = autoAppsCommand.isMatch(config.command); 38 | //node.log(`"${config.command}" matches "${autoAppsCommand.command}": ${isMatch}`); 39 | if(!isMatch) { 40 | node.status({}); 41 | return; 42 | }; 43 | node.status({fill:"green",shape:"dot",text:"Matched Command"}); 44 | var payload = autoAppsCommand.payload; 45 | var msg = {}; 46 | msg.payload = payload; 47 | if(isFullPush){ 48 | msg.push = gcmMessage.push 49 | msg.senderId = gcmMessage.push.senderId; 50 | } 51 | //node.log(`Sending message to flow: ${JSON.stringify(msg)}`); 52 | node.send(msg); 53 | } 54 | node.eventListener = command => { 55 | //node.log(`Received command from server 2: ${command}`); 56 | handleIncomingMessage(command); 57 | }; 58 | //node.log(`Added listener for commands`); 59 | RED.nodes.getNode(config.server).events.on("command", node.eventListener); 60 | node.on('input', function(msg) { 61 | //node.log(`Received command from node input: ${command}`); 62 | handleIncomingMessage(msg.payload); 63 | }); 64 | node.on('close', ()=>{ 65 | //node.log(`Removed listener for commands`); 66 | server.events.removeListener("command",node.eventListener); 67 | }); 68 | } 69 | RED.nodes.registerType("join-receive-message",JoinReceiveMessageNode); 70 | } -------------------------------------------------------------------------------- /join-server.html: -------------------------------------------------------------------------------- 1 | 6 | 49 | 50 | -------------------------------------------------------------------------------- /join-message.js: -------------------------------------------------------------------------------- 1 | 2 | var Join = require("./js/join"); 3 | var joinapi = require("./js/joinapi"); 4 | const util = require("./js/util"); 5 | const Encryption = require("./js/encryption.js"); 6 | module.exports = function(RED) { 7 | function JoinMessageNode(config) { 8 | RED.nodes.createNode(this,config); 9 | var node = this; 10 | var globalContext = this.context().global; 11 | var joinConfig = RED.nodes.getNode(config.joinConfig); 12 | node.join = new Join(joinConfig.credentials.apikey); 13 | node.on('input', async function(msg) { 14 | var push = msg.push || {}; 15 | push.deviceIds = node.credentials.deviceId || msg.senderId || msg.deviceId || msg.deviceIds || push.deviceId || push.deviceIds; 16 | push.deviceNames = node.credentials.deviceName || msg.devices || push.devices; 17 | push.apikey = joinConfig.credentials.apikey || msg.apikey || push.apikey; 18 | push.title = config.title || msg.title || push.title; 19 | push.url = config.url || msg.url || push.url; 20 | push.text = config.text || msg.text || push.text; 21 | push.icon = config.notificationicon || msg.icon || push.icon; 22 | if(!push.text && util.isString(msg.payload)){ 23 | push.text = msg.payload; 24 | } 25 | if(!push.text){ 26 | return node.error("text needs to be set", msg); 27 | } 28 | node.deviceId = globalContext.get("joindeviceid"); 29 | node.status({fill:"yellow",shape:"dot",text:"Sending..."}); 30 | //node.log(`Sending push: ${JSON.stringify(push)}`) 31 | try{ 32 | 33 | const password = joinConfig.encryptionKey; 34 | if(password){ 35 | const e = text => Encryption.encrypt(text,password); 36 | push.text = await e(push.text); 37 | push.url = await e(push.url); 38 | push.smsnumber = await e(push.smsnumber); 39 | push.smstext = await e(push.smstext); 40 | push.clipboard = await e(push.clipboard); 41 | push.file = await e(push.file); 42 | push.files = await e(push.files); 43 | push.wallpaper = await e(push.wallpaper); 44 | } 45 | 46 | var result = await node.join.sendPush(push,null,{"node":node}); 47 | //node.log(`Push results - Sucess: ${result.success}; Failure: ${result.failure}`); 48 | var failure = result.firstFailure; 49 | if(failure){ 50 | var message = failure.message || "Couldn't send push"; 51 | node.status({fill:"red",shape:"dot",text:message}); 52 | return node.error(message, msg); 53 | }else{ 54 | node.status({fill:"green",shape:"dot",text:`Sent to ${result.success} device${result.success>1?"s":""}!`}); 55 | setTimeout(()=>node.status({}),5000); 56 | } 57 | }catch(error){ 58 | node.status({fill:"red",shape:"dot",text:error}); 59 | node.error(error, msg); 60 | } 61 | node.send(msg); 62 | }); 63 | } 64 | RED.nodes.registerType("join-message",JoinMessageNode, { 65 | credentials: { 66 | deviceName: {type:"text",value:"",required:false}, 67 | deviceId: {type:"text",value:"",required:false} 68 | } 69 | }); 70 | } -------------------------------------------------------------------------------- /join-receive-message.html: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | 55 | 56 | -------------------------------------------------------------------------------- /join-config.html: -------------------------------------------------------------------------------- 1 | 2 | 51 | 52 | 60 | 61 | 68 | 69 | 95 | -------------------------------------------------------------------------------- /join-message.html: -------------------------------------------------------------------------------- 1 | 25 | 34 | 73 | 74 | -------------------------------------------------------------------------------- /join-server.js: -------------------------------------------------------------------------------- 1 | 2 | const joinapi = require("./js/joinapi"); 3 | const http = require("http"); 4 | const url = require("url"); 5 | const gcm = require("./js/gcm"); 6 | const IpGetter = require("./js/ipgetter"); 7 | 8 | const EventEmitter = require('events'); 9 | class CommandEmitter extends EventEmitter {} 10 | var LocalStorage = require("node-localstorage").LocalStorage 11 | var localStorage = new LocalStorage('./joinserver'); 12 | const eventEmitter = new CommandEmitter(); 13 | module.exports = function(RED) { 14 | function JoinServerNode(config) { 15 | RED.nodes.createNode(this,config); 16 | var node = this; 17 | node.config = config; 18 | this.events = eventEmitter; 19 | //this.events.id = node.id; 20 | //node.log(`Emitter: ${node.events.id}`) 21 | this.port = config.port; 22 | node.log(`Starting server on port ${this.port}...`); 23 | node.reportCommand = command => { 24 | if(!command) return; 25 | //node.log(`Emitter: ${node.events.id}`) 26 | //node.log(`Reporting command from server: ${command}`); 27 | node.events.emit('command',command); 28 | } 29 | const app = http.createServer((request, response) => { 30 | // Set CORS headers 31 | response.setHeader('Access-Control-Allow-Origin', '*'); 32 | response.setHeader('Access-Control-Request-Method', '*'); 33 | response.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET'); 34 | response.setHeader('Access-Control-Allow-Headers', '*'); 35 | if ( request.method === 'OPTIONS' ) { 36 | response.writeHead(200); 37 | response.end(); 38 | return; 39 | } 40 | //node.log(`Got request: ${request.method} => ${request.method}`); 41 | var query = url.parse(request.url, true).query; 42 | if(query){ 43 | node.reportCommand(query.message); 44 | } 45 | if(request.method == "POST"){ 46 | let body = []; 47 | request.on('data', (chunk) => { 48 | body.push(chunk); 49 | }).on('end', () => { 50 | var gcmRaw = Buffer.concat(body).toString(); 51 | //node.log(`GCM Raw: ${gcmRaw.type} => ${gcmRaw.json}`); 52 | node.reportCommand(gcmRaw) 53 | }); 54 | } 55 | response.writeHead(200, {"Content-Type": "text/html"}); 56 | response.write(`OK`); 57 | response.end(); 58 | }); 59 | app.on('error', e => node.log(`Error starting server: ${e}`)); 60 | app.listen(Number.parseInt(this.port)); 61 | this.on('close', ()=>{ 62 | app.close(); 63 | }); 64 | var joinConfig = RED.nodes.getNode(node.config.joinConfig); 65 | if(joinConfig.register){ 66 | node.log(`Sending Registration ${joinConfig.deviceName}`) 67 | sendRegistration(node); 68 | } 69 | } 70 | 71 | 72 | async function sendRegistration(node){ 73 | var globalContext = node.context().global; 74 | var config = node.config; 75 | var joinConfig = RED.nodes.getNode(config.joinConfig); 76 | if(!joinConfig){ 77 | return node.log (`Can't register device. User has not configured Join yet`); 78 | } 79 | node.log (`Saved device Name: ${joinConfig.credentials.deviceName}`); 80 | var ips = await new IpGetter(node.credentials.localIp,node.credentials.publicIp,node).getIps(); 81 | var lastIps = globalContext.get("lastIps"); 82 | var lastLocalIp = null; 83 | var lastPublicIp = null; 84 | if(lastIps){ 85 | //node.log("Found lastIps: " + lastIps); 86 | //node.log("Found Ips: " + JSON.stringify(ips)); 87 | lastIps = JSON.parse(lastIps); 88 | lastLocalIp = lastIps.localIp; 89 | lastPublicIp = lastIps.publicIp; 90 | } 91 | var existingDeviceId = globalContext.get("joindeviceid"); 92 | node.deviceId = existingDeviceId; 93 | if(lastLocalIp == ips.localIp && lastPublicIp == ips.publicIp){ 94 | node.log("Not registering device because IPs are the same.") 95 | return; 96 | } 97 | 98 | existingDeviceId = localStorage.getItem('deviceId'); 99 | var options = { 100 | "apikey":joinConfig.credentials.apikey, 101 | "deviceName":joinConfig.credentials.deviceName, 102 | "port":config.port 103 | }; 104 | if(existingDeviceId){ 105 | options.deviceId = existingDeviceId; 106 | } 107 | Object.assign(options,ips); 108 | node.log(`Sending registration: ${JSON.stringify(options)}`) 109 | joinapi.registerInJoinServer(node,options) 110 | .then(deviceId=>{ 111 | if(!deviceId) return; 112 | globalContext.set("joindeviceid",deviceId); 113 | globalContext.set("lastIps",JSON.stringify(ips)); 114 | globalContext.set("joindeviceid",deviceId); 115 | localStorage.setItem('deviceId', deviceId); 116 | node.deviceId = deviceId; 117 | //node.log (`Saved device id: ${globalContext.get("joindeviceid")}`); 118 | }); 119 | } 120 | RED.nodes.registerType("join-server",JoinServerNode,{ 121 | credentials: { 122 | publicIp: {type:"text",value:"",required:false}, 123 | localIp: {type:"text",value:"",required:false} 124 | } 125 | }); 126 | } -------------------------------------------------------------------------------- /js/device.js: -------------------------------------------------------------------------------- 1 | 2 | const Sender = require("./sender"); 3 | const SenderGCM = Sender.SenderGCM; 4 | const SenderIP = Sender.SenderIP; 5 | const SenderIFTTT = Sender.SenderIFTTT; 6 | const SenderServer = Sender.SenderServer; 7 | const SendResults = Sender.SendResults; 8 | const joinapi = require("./joinapi"); 9 | const listDevices = joinapi.listDevices; 10 | var extensions = require("./extensions") 11 | 12 | class Device { 13 | isAnyType(...types){ 14 | for(var type of types){ 15 | if(type === this.deviceType) return true; 16 | } 17 | return false; 18 | } 19 | get isAndroidPhone(){ 20 | return this.isAnyType(Devices.TYPE_ANDROID_PHONE); 21 | } 22 | get isAndroidTablet(){ 23 | return this.isAnyType(Devices.TYPE_ANDROID_TABLET); 24 | } 25 | get isAndroid(){ 26 | return this.isAndroidPhone || this.isAndroidTablet; 27 | } 28 | get isChrome(){ 29 | return this.isAnyType(Devices.TYPE_CHROME_BROWSER); 30 | } 31 | get isWindows10(){ 32 | return this.isAnyType(Devices.TYPE_WINDOWS_10); 33 | } 34 | get isGCM(){ 35 | return this.isAndroidPhone || this.isAndroidTablet || this.isChrome; 36 | } 37 | get isIP(){ 38 | return this.isAnyType(Devices.TYPE_IP); 39 | } 40 | get isIFTTT(){ 41 | return this.isAnyType(Devices.TYPE_IFTTT); 42 | } 43 | get onlySendPushes(){ 44 | return this.isIP || this.isIFTTT; 45 | } 46 | 47 | get senderClass(){ 48 | if(this.isGCM) return SenderGCM; 49 | if(this.isIP) return SenderIP; 50 | if(this.isIFTTT) return SenderIFTTT; 51 | return SenderServer; 52 | } 53 | get sender(){ 54 | return new this.senderClass(); 55 | } 56 | get asDevices(){ 57 | var devices = new Devices(); 58 | devices.push(this); 59 | return devices; 60 | } 61 | send(options){ 62 | this.asDevices.send(options); 63 | } 64 | sendPush(push,options){ 65 | this.asDevices.sendPush(push,options); 66 | } 67 | } 68 | 69 | class Devices extends Array { 70 | static get TYPE_ANDROID_PHONE() {return 1;} 71 | static get TYPE_ANDROID_TABLET() {return 2;} 72 | static get TYPE_CHROME_BROWSER() {return 3;} 73 | static get TYPE_WINDOWS_10() {return 4;} 74 | static get TYPE_TASKER() {return 5;} 75 | static get TYPE_FIREFOX() {return 6;} 76 | static get TYPE_GROUP() {return 7;} 77 | static get TYPE_ANDROID_TV() {return 8;} 78 | static get TYPE_GOOGLE_ASSISTANT() {return 9;} 79 | static get TYPE_IOS_PHONE() {return 10;} 80 | static get TYPE_IOS_TABLET() {return 11;} 81 | static get TYPE_IFTTT() {return 12;} 82 | static get TYPE_IP() {return 13;} 83 | static get TYPE_MQTT() {return 14;} 84 | 85 | static fromJson(json){ 86 | var array = JSON.parse(json); 87 | return Devices.fromArray(array); 88 | } 89 | 90 | static fromArray(array){ 91 | var devices = new Devices(); 92 | for(var element of array){ 93 | devices.push(Object.assign(new Device, element)); 94 | } 95 | return devices; 96 | } 97 | 98 | static async fromServer(apiKey,options){ 99 | if(!apiKey) throw "Api Key is missing"; 100 | var devicesResult = null; 101 | if(!options){ 102 | options = {}; 103 | } 104 | /*if(!options.forceRefresh){ 105 | var cachedDevicesJson = localStorage.joinCachedDevices; 106 | if(cachedDevicesJson){ 107 | devicesResult = JSON.parse(cachedDevicesJson); 108 | } 109 | } */ 110 | if(!devicesResult){ 111 | devicesResult = await listDevices(apiKey); 112 | //localStorage.joinCachedDevices = JSON.stringify(devicesResult); 113 | } 114 | if(!devicesResult.success){ 115 | //delete localStorage.joinCachedDevices 116 | throw devicesResult.errorMessage; 117 | } 118 | return Devices.fromArray(devicesResult.records); 119 | } 120 | 121 | sendPush(push,options){ 122 | if(!push.id){ 123 | push.id = Sender.Sender.newMessageId; 124 | } 125 | var gcmPush = { 126 | "push" : push 127 | }; 128 | var gcmRaw = { 129 | "json" : JSON.stringify(gcmPush), 130 | "type" : "GCMPush" 131 | }; 132 | var gcmOptions = { 133 | "gcmRaw" : gcmRaw 134 | } 135 | gcmOptions = Object.assign(gcmOptions, options); 136 | return this.send(gcmOptions); 137 | } 138 | async send(options){ 139 | var groupsBySender = null; 140 | if(options.forceServer){ 141 | groupsBySender = this.groupBy(device=>true); 142 | }else{ 143 | groupsBySender = this.groupBy(device=>device.senderClass); 144 | } 145 | var isPush = options.gcmRaw.type == "GCMPush"; 146 | var results = await Promise.all(groupsBySender.map(group=>{ 147 | //options.node.log(`Sending push with ${group.key}`); 148 | var sender = options.forceServer ? new SenderServer() : new group.key(); 149 | options.devices = group.values; 150 | if(!isPush){ 151 | options.devices = options.devices.filter(device=>!device.onlySendPushes); 152 | }else{ 153 | options.gcmPush = JSON.parse(options.gcmRaw.json); 154 | } 155 | if(options.devices.length == 0) return Promise.resolve(Sender.newSuccessResult) 156 | 157 | options.gcmParams = {}; 158 | return sender.send(options); 159 | })); 160 | return SendResults.fromMany(results) 161 | } 162 | 163 | } 164 | module.exports = { 165 | Devices : Devices, 166 | Device : Device 167 | } -------------------------------------------------------------------------------- /js/legacy.js: -------------------------------------------------------------------------------- 1 | // defineModule(function () { 2 | const util = require("./util"); 3 | const fetch = util.fetch; 4 | 5 | const AuthTokenGetter = require("./authTokenGetter"); 6 | 7 | module.exports = class FCMLegacyToV1Converter { 8 | #authTokenGetter = new AuthTokenGetter( 9 | "fcm-sender@join-external-gcm.iam.gserviceaccount.com", 10 | "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCybvuSZiNWISfi\nBiCMLXMtak93LGyE3faxnKg7TSvx19YJ0Stcofq7jyuehcHMhoksYVwSzdfYm8yV\nVIliNNVAysdI4bSELR8LTNF7wVzLi1UNfpjQGuxiWS0VIev1WuheqvHIbdiJtD38\ntQ89cGlKLiN5DizQD5cg6GGcyFwZv35jOQAIYuQhhJZWl8RRkemcndiZ+semmf6E\nTeSGnmbyFmhXyWySerdvyj+ZzvoPL4olo5deURlgoCg8uiv8ajVCOdOkOQ/E9J+n\n2yIwvjGk/VSeMxXpzQw+5Qj2/gvtz6ufAlIBDb4HpSsE7+Ui7er7BCjSLXdEpS4y\n3PsHKJodAgMBAAECggEAF0eolfCygo2/3Nrsyy0w3keFB6jpnaoyAflM77PBXIPK\n/qvmKudNRcRHrh6Iau1Qn1QyhZeKpk2pcwA9Dm2TNyldt9IO0cHrT3edyzYuq7XJ\nioGuYVRp6+jzm1K6LOBH+fX2pq5CNrEn9z0OOHdenVmIskYZramjD52SArkXXxpn\nelFcAIbAaiqY1OBU0swGadXuhoeC5fqk8axGEF9ZXbf/utXD0mFqhFI3zz9x/gwY\nLzP5Fkd50UQmAb4PE+8q4etjCazvttr9864YlXMTKGwNx8Sh8SehDL4+B56pK1Kr\nano0v+Fj0cHh/UJSJit4RXSJiuxxGGQ5IO7koTWYIQKBgQDjz2BpCZ7OgB2iYsi2\nxZEf8PWWXPpW2aYsn+KcTT4DA1L65aSaWRQVKBUUDHIT7cNzf+wjw7C7Y0ISG2yT\nMfgQbAZMCIzLV3GsM3kV6yqciQczGlp/TqdaJVnGGXPVe5P0sC/Bfwgoi02EkK1K\n+rm/rE5ueT+eHwgxNXeWZcc/8QKBgQDIg3Gltsh8xoMcgbBA/poiCrxSklQ22jq8\nCqzyqrdUDC7pr5hp+DcEjOBiX3j5qp5diSoROrZmYW1go3MG5P6/HR7bitj4feW6\nYl9vblHch9fTaFGsZMJwchjaaN+2RklYUZ6/Nhr4TCnKQgMOyaaCyzCwzDpE2GOX\n1Wktt8Do7QKBgQCKZF+4T6zW3AOks4glaG4aTmKTPtahzkTiFRswQshqQim1264c\nSgMmOxxa+piOvMEguFS3AVmq7MilgV17Kj79kvJcXFFT8kJPD1H+28ceIyxpghf6\nAMkvvUMFUk8JILKoUiQg01AceUvVPaLYyunuo/ldqXDZWRa79jQ4/ImHsQKBgEA1\n75/sr7ldbMElOsclgUBjhbk/iN5j9ikflhDD4J92o1NMWxecWCoJ3xVBk6EIJVy4\nvxLzZVPV4UvwK7bKgFW9QpN1nFO/JWERfZRWlLp1egUGRBlbzvRpZVIUAYgCbBxv\nTtHWxr46zasqhoYmxz7dSMNlM0e2r/YAboUocgtlAoGAZgaKi0hH/JW1cSGTfbMI\n1V4056YtrUgiX5AhKEtfC2sVLC5orwuZmJaa5JjdQT+2PnecMdDmatojDQjklE/e\nvrpopN2oeBDqVA+ofcpVsFxgLTlWRD5uKb027tAcneViRN2CNHlO/Cw4c8ZIG0xe\nQRBL0hYZ7DUaVIdmhvlALMw=\n-----END PRIVATE KEY-----" 11 | ); 12 | #removeUndefinedKeys(obj) { 13 | if (obj && typeof obj === 'object') { 14 | return Object.keys(obj).reduce((acc, key) => { 15 | const value = obj[key]; 16 | if (value !== undefined) { 17 | acc[key] = this.#removeUndefinedKeys(value); 18 | } 19 | return acc; 20 | }, Array.isArray(obj) ? [] : {}); 21 | } 22 | return obj; 23 | }; 24 | convertGcmFromLegacyToV1(legacyGcm) { 25 | const validate_only = legacyGcm.dry_run; 26 | const priority = legacyGcm.priority; 27 | const ttl = legacyGcm.ttl; 28 | const collapse_key = legacyGcm.collapse_key; 29 | const restricted_package_name = legacyGcm.restricted_package_name; 30 | const data = legacyGcm.data; 31 | const registration_ids = legacyGcm.registration_ids; //single devices 32 | const to = legacyGcm.to; //topic 33 | const buildMessage = () => { 34 | const message = { 35 | validate_only, 36 | "message": { 37 | "android": { 38 | priority, 39 | ttl, 40 | collapse_key, 41 | restricted_package_name 42 | }, 43 | data 44 | } 45 | } 46 | return this.#removeUndefinedKeys(message); 47 | } 48 | const buildTopicMessages = () => { 49 | const baseMessage = buildMessage(); 50 | baseMessage.message.to = to; 51 | return [baseMessage]; 52 | } 53 | const buildDeviceMessages = () => { 54 | const messages = registration_ids.map(registration_id => { 55 | const deviceMessage = buildMessage(); 56 | deviceMessage.message.token = registration_id; 57 | return deviceMessage; 58 | }); 59 | return messages; 60 | } 61 | const resultList = to ? buildTopicMessages() : buildDeviceMessages(); 62 | // console.log("v1 messages", resultList); 63 | return resultList; 64 | } 65 | async doV1DirectGCMRequests(requests) { 66 | 67 | const token = await this.#authTokenGetter.getAccessToken(); 68 | const fetches = await requests.map(async request => { 69 | const body = JSON.stringify(request); 70 | const fetchProper = await fetch(); 71 | return fetchProper("https://fcm.googleapis.com/v1/projects/join-external-gcm/messages:send", { 72 | method: 'POST', 73 | headers: { 74 | 'Content-Type': 'application/json', 75 | 'Authorization': `Bearer ${token}` 76 | }, 77 | body 78 | }); 79 | }); 80 | const result = await Promise.allSettled(fetches); 81 | const resultJsons = await Promise.all(result.map(async (r) => { 82 | if (r.status === "fulfilled") { 83 | return r.value.json(); 84 | } else { 85 | console.error("V1 FCM Fetch error:", r.reason); 86 | return null; 87 | } 88 | })); 89 | // console.log(resultJsons); 90 | return resultJsons; 91 | } 92 | 93 | convertGcmResponseV1ToLegacy(v1Responses) { 94 | if (v1Responses.filter(response => response == null).length > 0) throw Error("Couldn't get response from v1 FCM"); 95 | 96 | const multicast_id = Math.floor(Math.random() * 1000000000000000000); 97 | const success = v1Responses.filter(response => response.name).length; 98 | const failure = v1Responses.filter(response => response.error).length; 99 | const canonical_ids = 0; 100 | const results = v1Responses.map(response => { 101 | if (response.name) { 102 | const parts = response.name.split('/'); 103 | return { "message_id": parts[parts.length - 1] }; 104 | } else { 105 | return { "error": response.error.message }; 106 | } 107 | }); 108 | return { 109 | multicast_id, 110 | success, 111 | failure, 112 | canonical_ids, 113 | results 114 | } 115 | } 116 | 117 | async doLegacyRequest(legacyContent) { 118 | const contentv1 = this.convertGcmFromLegacyToV1(legacyContent); 119 | const v1Responses = await this.doV1DirectGCMRequests(contentv1); 120 | const legacyResponses = this.convertGcmResponseV1ToLegacy(v1Responses); 121 | return legacyResponses; 122 | } 123 | } 124 | 125 | // return { 126 | // AuthTokenGetter, 127 | // Base64Implementation, 128 | // FCMLegacyToV1Converter 129 | // }; 130 | // }); -------------------------------------------------------------------------------- /js/sender.js: -------------------------------------------------------------------------------- 1 | const util = require("./util"); 2 | const AutoAppsCommand = require("./autoappscommand"); 3 | const FCMLegacyToV1Converter = require("./legacy"); 4 | var extensions = require("./extensions"); 5 | var joinapi = require("./joinapi"); 6 | class Sender { 7 | static get newMessageId() { 8 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 9 | var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 10 | return v.toString(16); 11 | }); 12 | } 13 | static get newSuccessResult() { 14 | return new SendResult(Sender.newMessageId, true); 15 | } 16 | 17 | static get GCM_PARAM_TIME_TO_LIVE() { return "time_to_live" } 18 | static get GCM_PARAM_PRIORITY() { return "priority" } 19 | static get GCM_MESSAGE_PRIORITY_HIGH() { return "high" } 20 | static get GCM_PARAM_DELAY_WHILE_IDLE() { return "delay_while_idle" } 21 | } 22 | class SendResult { 23 | constructor(messageId, success, message) { 24 | this.messageId = messageId; 25 | this.success = success; 26 | this.message = message; 27 | } 28 | } 29 | class SendResults extends Array { 30 | static fromMany(many) { 31 | var finalResult = new SendResults(); 32 | if (!many || many.length == 0) return finalResult; 33 | for (var results of many) { 34 | finalResult.merge(results); 35 | } 36 | return finalResult; 37 | } 38 | get firstFailure() { 39 | if (this.length == 0) return null; 40 | return this.find(result => !result.success); 41 | } 42 | constructor(results) { 43 | super(); 44 | if (!results || results.length == 0) return; 45 | 46 | this.merge(results) 47 | } 48 | merge(other) { 49 | for (var result of other) { 50 | this.push(new SendResult(result.messageId, result.success, result.message)); 51 | } 52 | this.success = this.count(result => result.success); 53 | this.failure = this.count(result => !result.success); 54 | this.id = Sender.newMessageId; 55 | } 56 | } 57 | class SenderGCM extends Sender { 58 | //gcmRaw, devices, gcmParams 59 | send(options) { 60 | var content = { 61 | "data": options.gcmRaw, 62 | "registration_ids": options.devices.map(device => device.regId2) 63 | } 64 | content = Object.assign(content, options.gcmParams) 65 | 66 | content[Sender.GCM_PARAM_PRIORITY] = Sender.GCM_MESSAGE_PRIORITY_HIGH; 67 | content[Sender.GCM_PARAM_DELAY_WHILE_IDLE] = false; 68 | var postOptions = { 69 | method: 'POST', 70 | body: JSON.stringify(content), 71 | headers: { 72 | 'Content-Type': 'application/json', 73 | 'Authorization': 'key=AIzaSyDvDS_KGPYTBrCG7tppCyq9P3_iVju9UkA', 74 | 'Content-Type': 'application/json; charset=UTF-8' 75 | } 76 | } 77 | // console.log("legacy", content); 78 | const legacy = new FCMLegacyToV1Converter(); 79 | // var v1Content = legacy.convertGcmFromLegacyToV1(content); 80 | // console.log("v1", JSON.stringify(v1Content,null,4)); 81 | // var url = "https://fcm.googleapis.com/fcm/send"; 82 | return legacy.doLegacyRequest(content) 83 | // return util.fetch() 84 | // .then(fetch => fetch(url, postOptions)) 85 | // .then(result => { 86 | // if (result.status == 200) return result.json(); 87 | // return result.text().then(text => Promise.reject(text)); 88 | // }) 89 | .then(results => { 90 | console.log("GCM results:", results); 91 | var finalResults = []; 92 | for (var result of results.results) { 93 | var sendResult = null; 94 | if (result.message_id) { 95 | sendResult = new SendResult(result.message_id, true); 96 | } else { 97 | sendResult = new SendResult(null, false) 98 | } 99 | finalResults.push(sendResult); 100 | } 101 | return new SendResults(finalResults); 102 | }); 103 | 104 | } 105 | } 106 | class SenderIP extends Sender { 107 | send(options) { 108 | return Promise.all(options.devices.map(device => { 109 | 110 | var doForOneDevice = function (options) { 111 | var regId = options.secondTry ? device.regId : device.regId2; 112 | var postOptions = { 113 | method: 'POST', 114 | body: JSON.stringify(options.gcmRaw), 115 | headers: { 116 | 'Content-Type': 'application/json' 117 | } 118 | } 119 | var url = `http://${regId}/push`; 120 | var getSucess = text => Sender.newSuccessResult 121 | var getError = error => { 122 | if (options.secondTry) return { "success": false, "error": typeof error == "string" ? error : error.message }; 123 | options.secondTry = true; 124 | return doForOneDevice(options); 125 | } 126 | return util.fetch() 127 | .then(fetch => fetch(url, postOptions)) 128 | .then(result => result.text()).then(getSucess).catch(getError) 129 | } 130 | return doForOneDevice(options); 131 | })) 132 | .then(allResults => (new SendResults(allResults))) 133 | } 134 | } 135 | class SenderIFTTT extends Sender { 136 | send(options) { 137 | var text = options.gcmPush.push.text; 138 | if (!text) return Promise.reject("Push to IFTTT needs text"); 139 | 140 | return Promise.all(options.devices.map(device => { 141 | var autoAppsCommand = new AutoAppsCommand(text, "value1,value2,value3"); 142 | var valuesForIfttt = {}; 143 | var url = `https://maker.ifttt.com/trigger/${autoAppsCommand.command}/with/key/${device.regId}`; 144 | if (autoAppsCommand.values.length > 0) { 145 | url += "?" 146 | } 147 | for (var i = 0; i < autoAppsCommand.values.length; i++) { 148 | var value = autoAppsCommand.values[i] 149 | var varName = `value${i + 1}`; 150 | valuesForIfttt[varName] = value; 151 | if (i > 0) { 152 | url += "&"; 153 | } 154 | url += `${varName}=${encodeURIComponent(value)}`; 155 | } 156 | //console.log(valuesForIfttt); 157 | var postOptions = { 158 | method: 'GET', 159 | //body: JSON.stringify(valuesForIfttt), 160 | headers: { 161 | 'Content-Type': 'application/json; charset=UTF-8' 162 | } 163 | } 164 | //console.log(url); 165 | return util.fetch() 166 | .then(fetch => fetch(url, postOptions)) 167 | .then(result => Sender.newSuccessResult).catch(error => Sender.newSuccessResult).then(result => (new SendResults([result]))); 168 | })); 169 | } 170 | } 171 | class SenderServer extends Sender { 172 | async send(options) { 173 | var result = null; 174 | var deviceIds = options.devices.map(device => device.deviceId).join(","); 175 | if (options.gcmPush) { 176 | if (options.apiKey) options.gcmPush.push.apikey = options.apiKey; 177 | options.gcmPush.push.deviceIds = deviceIds; 178 | result = joinapi.sendPush(options.gcmPush.push); 179 | } else { 180 | var rawGcmWithOptions = options.gcmRaw; 181 | rawGcmWithOptions.deviceIds = deviceIds; 182 | result = joinapi.sendRawGcm(rawGcmWithOptions); 183 | } 184 | result = await result; 185 | var sendResults = new SendResults(); 186 | for (var device of options.devices) { 187 | if (!result.success) { 188 | sendResults.push(new SendResult(null, false, result.errorMessage)); 189 | } else { 190 | sendResults.push(new SendResult(SendResult.newMessageId, true)); 191 | } 192 | } 193 | return sendResults; 194 | } 195 | } 196 | module.exports = { 197 | SendResult: SendResult, 198 | SendResults: SendResults, 199 | Sender: Sender, 200 | SenderGCM: SenderGCM, 201 | SenderIP: SenderIP, 202 | SenderIFTTT: SenderIFTTT, 203 | SenderServer: SenderServer 204 | } -------------------------------------------------------------------------------- /js/encryption.js: -------------------------------------------------------------------------------- 1 | let CryptoJS = null; 2 | CryptoJS=CryptoJS||function(u,p){var d={},l=d.lib={},s=function(){},t=l.Base={extend:function(a){s.prototype=this;var c=new s;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, 3 | r=l.WordArray=t.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=p?c:4*a.length},toString:function(a){return(a||v).stringify(this)},concat:function(a){var c=this.words,e=a.words,j=this.sigBytes;a=a.sigBytes;this.clamp();if(j%4)for(var k=0;k>>2]|=(e[k>>>2]>>>24-8*(k%4)&255)<<24-8*((j+k)%4);else if(65535>>2]=e[k>>>2];else c.push.apply(c,e);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< 4 | 32-8*(c%4);a.length=u.ceil(c/4)},clone:function(){var a=t.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],e=0;e>>2]>>>24-8*(j%4)&255;e.push((k>>>4).toString(16));e.push((k&15).toString(16))}return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j>>3]|=parseInt(a.substr(j, 5 | 2),16)<<24-4*(j%8);return new r.init(e,c/2)}},b=w.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j>>2]>>>24-8*(j%4)&255));return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j>>2]|=(a.charCodeAt(j)&255)<<24-8*(j%4);return new r.init(e,c)}},x=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape(b.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return b.parse(unescape(encodeURIComponent(a)))}}, 6 | q=l.BufferedBlockAlgorithm=t.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=x.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,e=c.words,j=c.sigBytes,k=this.blockSize,b=j/(4*k),b=a?u.ceil(b):u.max((b|0)-this._minBufferSize,0);a=b*k;j=u.min(4*a,j);if(a){for(var q=0;q>>2]>>>24-8*(r%4)&255)<<16|(l[r+1>>>2]>>>24-8*((r+1)%4)&255)<<8|l[r+2>>>2]>>>24-8*((r+2)%4)&255,v=0;4>v&&r+0.75*v>>6*(3-v)&63));if(l=t.charAt(64))for(;d.length%4;)d.push(l);return d.join("")},parse:function(d){var l=d.length,s=this._map,t=s.charAt(64);t&&(t=d.indexOf(t),-1!=t&&(l=t));for(var t=[],r=0,w=0;w< 10 | l;w++)if(w%4){var v=s.indexOf(d.charAt(w-1))<<2*(w%4),b=s.indexOf(d.charAt(w))>>>6-2*(w%4);t[r>>>2]|=(v|b)<<24-8*(r%4);r++}return p.create(t,r)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})(); 11 | (function(u){function p(b,n,a,c,e,j,k){b=b+(n&a|~n&c)+e+k;return(b<>>32-j)+n}function d(b,n,a,c,e,j,k){b=b+(n&c|a&~c)+e+k;return(b<>>32-j)+n}function l(b,n,a,c,e,j,k){b=b+(n^a^c)+e+k;return(b<>>32-j)+n}function s(b,n,a,c,e,j,k){b=b+(a^(n|~c))+e+k;return(b<>>32-j)+n}for(var t=CryptoJS,r=t.lib,w=r.WordArray,v=r.Hasher,r=t.algo,b=[],x=0;64>x;x++)b[x]=4294967296*u.abs(u.sin(x+1))|0;r=r.MD5=v.extend({_doReset:function(){this._hash=new w.init([1732584193,4023233417,2562383102,271733878])}, 12 | _doProcessBlock:function(q,n){for(var a=0;16>a;a++){var c=n+a,e=q[c];q[c]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360}var a=this._hash.words,c=q[n+0],e=q[n+1],j=q[n+2],k=q[n+3],z=q[n+4],r=q[n+5],t=q[n+6],w=q[n+7],v=q[n+8],A=q[n+9],B=q[n+10],C=q[n+11],u=q[n+12],D=q[n+13],E=q[n+14],x=q[n+15],f=a[0],m=a[1],g=a[2],h=a[3],f=p(f,m,g,h,c,7,b[0]),h=p(h,f,m,g,e,12,b[1]),g=p(g,h,f,m,j,17,b[2]),m=p(m,g,h,f,k,22,b[3]),f=p(f,m,g,h,z,7,b[4]),h=p(h,f,m,g,r,12,b[5]),g=p(g,h,f,m,t,17,b[6]),m=p(m,g,h,f,w,22,b[7]), 13 | f=p(f,m,g,h,v,7,b[8]),h=p(h,f,m,g,A,12,b[9]),g=p(g,h,f,m,B,17,b[10]),m=p(m,g,h,f,C,22,b[11]),f=p(f,m,g,h,u,7,b[12]),h=p(h,f,m,g,D,12,b[13]),g=p(g,h,f,m,E,17,b[14]),m=p(m,g,h,f,x,22,b[15]),f=d(f,m,g,h,e,5,b[16]),h=d(h,f,m,g,t,9,b[17]),g=d(g,h,f,m,C,14,b[18]),m=d(m,g,h,f,c,20,b[19]),f=d(f,m,g,h,r,5,b[20]),h=d(h,f,m,g,B,9,b[21]),g=d(g,h,f,m,x,14,b[22]),m=d(m,g,h,f,z,20,b[23]),f=d(f,m,g,h,A,5,b[24]),h=d(h,f,m,g,E,9,b[25]),g=d(g,h,f,m,k,14,b[26]),m=d(m,g,h,f,v,20,b[27]),f=d(f,m,g,h,D,5,b[28]),h=d(h,f, 14 | m,g,j,9,b[29]),g=d(g,h,f,m,w,14,b[30]),m=d(m,g,h,f,u,20,b[31]),f=l(f,m,g,h,r,4,b[32]),h=l(h,f,m,g,v,11,b[33]),g=l(g,h,f,m,C,16,b[34]),m=l(m,g,h,f,E,23,b[35]),f=l(f,m,g,h,e,4,b[36]),h=l(h,f,m,g,z,11,b[37]),g=l(g,h,f,m,w,16,b[38]),m=l(m,g,h,f,B,23,b[39]),f=l(f,m,g,h,D,4,b[40]),h=l(h,f,m,g,c,11,b[41]),g=l(g,h,f,m,k,16,b[42]),m=l(m,g,h,f,t,23,b[43]),f=l(f,m,g,h,A,4,b[44]),h=l(h,f,m,g,u,11,b[45]),g=l(g,h,f,m,x,16,b[46]),m=l(m,g,h,f,j,23,b[47]),f=s(f,m,g,h,c,6,b[48]),h=s(h,f,m,g,w,10,b[49]),g=s(g,h,f,m, 15 | E,15,b[50]),m=s(m,g,h,f,r,21,b[51]),f=s(f,m,g,h,u,6,b[52]),h=s(h,f,m,g,k,10,b[53]),g=s(g,h,f,m,B,15,b[54]),m=s(m,g,h,f,e,21,b[55]),f=s(f,m,g,h,v,6,b[56]),h=s(h,f,m,g,x,10,b[57]),g=s(g,h,f,m,t,15,b[58]),m=s(m,g,h,f,D,21,b[59]),f=s(f,m,g,h,z,6,b[60]),h=s(h,f,m,g,C,10,b[61]),g=s(g,h,f,m,j,15,b[62]),m=s(m,g,h,f,A,21,b[63]);a[0]=a[0]+f|0;a[1]=a[1]+m|0;a[2]=a[2]+g|0;a[3]=a[3]+h|0},_doFinalize:function(){var b=this._data,n=b.words,a=8*this._nDataBytes,c=8*b.sigBytes;n[c>>>5]|=128<<24-c%32;var e=u.floor(a/ 16 | 4294967296);n[(c+64>>>9<<4)+15]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360;n[(c+64>>>9<<4)+14]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;b.sigBytes=4*(n.length+1);this._process();b=this._hash;n=b.words;for(a=0;4>a;a++)c=n[a],n[a]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;return b},clone:function(){var b=v.clone.call(this);b._hash=this._hash.clone();return b}});t.MD5=v._createHelper(r);t.HmacMD5=v._createHmacHelper(r)})(Math); 17 | (function(){var u=CryptoJS,p=u.lib,d=p.Base,l=p.WordArray,p=u.algo,s=p.EvpKDF=d.extend({cfg:d.extend({keySize:4,hasher:p.MD5,iterations:1}),init:function(d){this.cfg=this.cfg.extend(d)},compute:function(d,r){for(var p=this.cfg,s=p.hasher.create(),b=l.create(),u=b.words,q=p.keySize,p=p.iterations;u.length>>2]&255}};d.BlockCipher=v.extend({cfg:v.cfg.extend({mode:b,padding:q}),reset:function(){v.reset.call(this);var a=this.cfg,b=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var c=a.createEncryptor;else c=a.createDecryptor,this._minBufferSize=1;this._mode=c.call(a, 23 | this,b&&b.words)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var b=this._process(!0)}else b=this._process(!0),a.unpad(b);return b},blockSize:4});var n=d.CipherParams=l.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),b=(p.format={}).OpenSSL={stringify:function(a){var b=a.ciphertext;a=a.salt;return(a?s.create([1398893684, 24 | 1701076831]).concat(a).concat(b):b).toString(r)},parse:function(a){a=r.parse(a);var b=a.words;if(1398893684==b[0]&&1701076831==b[1]){var c=s.create(b.slice(2,4));b.splice(0,4);a.sigBytes-=16}return n.create({ciphertext:a,salt:c})}},a=d.SerializableCipher=l.extend({cfg:l.extend({format:b}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);var l=a.createEncryptor(c,d);b=l.finalize(b);l=l.cfg;return n.create({ciphertext:b,key:c,iv:l.iv,algorithm:a,mode:l.mode,padding:l.padding,blockSize:a.blockSize,formatter:d.format})}, 25 | decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);return a.createDecryptor(c,d).finalize(b.ciphertext)},_parse:function(a,b){return"string"==typeof a?b.parse(a,this):a}}),p=(p.kdf={}).OpenSSL={execute:function(a,b,c,d){d||(d=s.random(8));a=w.create({keySize:b+c}).compute(a,d);c=s.create(a.words.slice(b),4*c);a.sigBytes=4*b;return n.create({key:a,iv:c,salt:d})}},c=d.PasswordBasedCipher=a.extend({cfg:a.cfg.extend({kdf:p}),encrypt:function(b,c,d,l){l=this.cfg.extend(l);d=l.kdf.execute(d, 26 | b.keySize,b.ivSize);l.iv=d.iv;b=a.encrypt.call(this,b,c,d.key,l);b.mixIn(d);return b},decrypt:function(b,c,d,l){l=this.cfg.extend(l);c=this._parse(c,l.format);d=l.kdf.execute(d,b.keySize,b.ivSize,c.salt);l.iv=d.iv;return a.decrypt.call(this,b,c,d.key,l)}})}(); 27 | (function(){for(var u=CryptoJS,p=u.lib.BlockCipher,d=u.algo,l=[],s=[],t=[],r=[],w=[],v=[],b=[],x=[],q=[],n=[],a=[],c=0;256>c;c++)a[c]=128>c?c<<1:c<<1^283;for(var e=0,j=0,c=0;256>c;c++){var k=j^j<<1^j<<2^j<<3^j<<4,k=k>>>8^k&255^99;l[e]=k;s[k]=e;var z=a[e],F=a[z],G=a[F],y=257*a[k]^16843008*k;t[e]=y<<24|y>>>8;r[e]=y<<16|y>>>16;w[e]=y<<8|y>>>24;v[e]=y;y=16843009*G^65537*F^257*z^16843008*e;b[k]=y<<24|y>>>8;x[k]=y<<16|y>>>16;q[k]=y<<8|y>>>24;n[k]=y;e?(e=z^a[a[a[G^z]]],j^=a[a[j]]):e=j=1}var H=[0,1,2,4,8, 28 | 16,32,64,128,27,54],d=d.AES=p.extend({_doReset:function(){for(var a=this._key,c=a.words,d=a.sigBytes/4,a=4*((this._nRounds=d+6)+1),e=this._keySchedule=[],j=0;j>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255]):(k=k<<8|k>>>24,k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255],k^=H[j/d|0]<<24);e[j]=e[j-d]^k}c=this._invKeySchedule=[];for(d=0;dd||4>=j?k:b[l[k>>>24]]^x[l[k>>>16&255]]^q[l[k>>> 29 | 8&255]]^n[l[k&255]]},encryptBlock:function(a,b){this._doCryptBlock(a,b,this._keySchedule,t,r,w,v,l)},decryptBlock:function(a,c){var d=a[c+1];a[c+1]=a[c+3];a[c+3]=d;this._doCryptBlock(a,c,this._invKeySchedule,b,x,q,n,s);d=a[c+1];a[c+1]=a[c+3];a[c+3]=d},_doCryptBlock:function(a,b,c,d,e,j,l,f){for(var m=this._nRounds,g=a[b]^c[0],h=a[b+1]^c[1],k=a[b+2]^c[2],n=a[b+3]^c[3],p=4,r=1;r>>24]^e[h>>>16&255]^j[k>>>8&255]^l[n&255]^c[p++],s=d[h>>>24]^e[k>>>16&255]^j[n>>>8&255]^l[g&255]^c[p++],t= 30 | d[k>>>24]^e[n>>>16&255]^j[g>>>8&255]^l[h&255]^c[p++],n=d[n>>>24]^e[g>>>16&255]^j[h>>>8&255]^l[k&255]^c[p++],g=q,h=s,k=t;q=(f[g>>>24]<<24|f[h>>>16&255]<<16|f[k>>>8&255]<<8|f[n&255])^c[p++];s=(f[h>>>24]<<24|f[k>>>16&255]<<16|f[n>>>8&255]<<8|f[g&255])^c[p++];t=(f[k>>>24]<<24|f[n>>>16&255]<<16|f[g>>>8&255]<<8|f[h&255])^c[p++];n=(f[n>>>24]<<24|f[g>>>16&255]<<16|f[h>>>8&255]<<8|f[k&255])^c[p++];a[b]=q;a[b+1]=s;a[b+2]=t;a[b+3]=n},keySize:8});u.AES=p._createHelper(d)})(); 31 | 32 | CryptoJS=CryptoJS||function(g,j){var e={},d=e.lib={},m=function(){},n=d.Base={extend:function(a){m.prototype=this;var c=new m;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, 33 | q=d.WordArray=n.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=j?c:4*a.length},toString:function(a){return(a||l).stringify(this)},concat:function(a){var c=this.words,p=a.words,f=this.sigBytes;a=a.sigBytes;this.clamp();if(f%4)for(var b=0;b>>2]|=(p[b>>>2]>>>24-8*(b%4)&255)<<24-8*((f+b)%4);else if(65535>>2]=p[b>>>2];else c.push.apply(c,p);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< 34 | 32-8*(c%4);a.length=g.ceil(c/4)},clone:function(){var a=n.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],b=0;b>>2]>>>24-8*(f%4)&255;b.push((d>>>4).toString(16));b.push((d&15).toString(16))}return b.join("")},parse:function(a){for(var c=a.length,b=[],f=0;f>>3]|=parseInt(a.substr(f, 35 | 2),16)<<24-4*(f%8);return new q.init(b,c/2)}},k=b.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var b=[],f=0;f>>2]>>>24-8*(f%4)&255));return b.join("")},parse:function(a){for(var c=a.length,b=[],f=0;f>>2]|=(a.charCodeAt(f)&255)<<24-8*(f%4);return new q.init(b,c)}},h=b.Utf8={stringify:function(a){try{return decodeURIComponent(escape(k.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return k.parse(unescape(encodeURIComponent(a)))}}, 36 | u=d.BufferedBlockAlgorithm=n.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=h.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var b=this._data,d=b.words,f=b.sigBytes,l=this.blockSize,e=f/(4*l),e=a?g.ceil(e):g.max((e|0)-this._minBufferSize,0);a=e*l;f=g.min(4*a,f);if(a){for(var h=0;ha;a++){if(16>a)m[a]=d[e+a]|0;else{var c=m[a-3]^m[a-8]^m[a-14]^m[a-16];m[a]=c<<1|c>>>31}c=(l<<5|l>>>27)+j+m[a];c=20>a?c+((k&h|~k&g)+1518500249):40>a?c+((k^h^g)+1859775393):60>a?c+((k&h|k&g|h&g)-1894007588):c+((k^h^ 40 | g)-899497514);j=g;g=h;h=k<<30|k>>>2;k=l;l=c}b[0]=b[0]+l|0;b[1]=b[1]+k|0;b[2]=b[2]+h|0;b[3]=b[3]+g|0;b[4]=b[4]+j|0},_doFinalize:function(){var d=this._data,e=d.words,b=8*this._nDataBytes,l=8*d.sigBytes;e[l>>>5]|=128<<24-l%32;e[(l+64>>>9<<4)+14]=Math.floor(b/4294967296);e[(l+64>>>9<<4)+15]=b;d.sigBytes=4*e.length;this._process();return this._hash},clone:function(){var e=d.clone.call(this);e._hash=this._hash.clone();return e}});g.SHA1=d._createHelper(j);g.HmacSHA1=d._createHmacHelper(j)})(); 41 | (function(){var g=CryptoJS,j=g.enc.Utf8;g.algo.HMAC=g.lib.Base.extend({init:function(e,d){e=this._hasher=new e.init;"string"==typeof d&&(d=j.parse(d));var g=e.blockSize,n=4*g;d.sigBytes>n&&(d=e.finalize(d));d.clamp();for(var q=this._oKey=d.clone(),b=this._iKey=d.clone(),l=q.words,k=b.words,h=0;h { 48 | if(!password){ 49 | return text; 50 | } 51 | const key = CryptoJS.enc.Base64.parse(password); 52 | const iv = CryptoJS.lib.WordArray.random(16); 53 | const ivBase64 = CryptoJS.enc.Base64.stringify(iv); 54 | const encrypted = CryptoJS.AES.encrypt(text, key, {iv: iv}); 55 | const encryptedText = encrypted.toString(); 56 | const finalString = ivBase64 + "=:=" + encryptedText 57 | return finalString; 58 | } 59 | const encryptArray = (texts, password)=> { 60 | if(!password){ 61 | return texts; 62 | } 63 | return texts.map(text=>Encryption.encrypt(text,password)); 64 | } 65 | const encrypt = (value, password) => { 66 | if(!password) return value; 67 | 68 | if(Util.isString(value)) return encryptString(value,password); 69 | if(Util.isArray(value)) return encryptArray(value,password); 70 | } 71 | const getEncryptedPasswordBytesFromBase64 = (base64EncryptedPassword)=>{ 72 | if(!Util.isString(base64EncryptedPassword)) return base64EncryptedPassword; 73 | 74 | return CryptoJS.enc.Base64.parse(base64EncryptedPassword); 75 | } 76 | const decryptArray = (values, encryptedPassword)=>{ 77 | if(!encryptedPassword) return values; 78 | if(!values || values.length == 0) return values; 79 | encryptedPassword = getEncryptedPasswordBytesFromBase64(encryptedPassword); 80 | 81 | const results = values.map(value => decrypt(value,encryptedPassword)); 82 | return results; 83 | } 84 | const decryptString = (value, encryptedPassword) => { 85 | if(!encryptedPassword) return value; 86 | if(!value || value.length == 0) return value; 87 | encryptedPassword = getEncryptedPasswordBytesFromBase64(encryptedPassword); 88 | 89 | const separatorIndex = value.indexOf("=:="); 90 | if(separatorIndex>0){ 91 | const split = value.split("=:="); 92 | if(split.length==2){ 93 | const iv = CryptoJS.enc.Base64.parse(split[0]); 94 | const encrypted = CryptoJS.enc.Base64.parse(split[1]); 95 | const decrypted = CryptoJS.AES.decrypt({ ciphertext: encrypted },encryptedPassword, { mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7, iv: iv}); 96 | const decryptedString = CryptoJS.enc.Utf8.stringify(decrypted); 97 | if(decryptedString && decryptedString.length>0){ 98 | return decryptedString; 99 | } 100 | } 101 | } 102 | return value; 103 | } 104 | 105 | const decryptFields = (obj, encryptedPassword)=>{ 106 | for(const prop in obj){ 107 | const value = obj[prop]; 108 | if(!value) continue; 109 | 110 | obj[prop] = decrypt(value,encryptedPassword); 111 | } 112 | return obj; 113 | } 114 | const decrypt = (value,encryptedPassword) => { 115 | if(!encryptedPassword) return value; 116 | 117 | if(Util.isString(value)) return decryptString(value,encryptedPassword); 118 | if(Util.isArray(value)) return decryptArray(value,encryptedPassword); 119 | if(Util.isNumber(value)) return value; 120 | return decryptFields(value,encryptedPassword); 121 | } 122 | const encryptionPasswordKey = "encryptionpassword"; 123 | const getPasswordDb = () => { 124 | const db = new Dexie("encryption"); 125 | db.version(1).stores({ 126 | password: 'value' 127 | }); 128 | return db.password; 129 | } 130 | class Encryption{ 131 | static encryptPasswordSync({password, salt, iterations}){ 132 | const encryptedPassword = CryptoJS.PBKDF2(password, salt, { keySize: 256/32, iterations }); 133 | const keyString = CryptoJS.enc.Base64.stringify(encryptedPassword); 134 | return keyString 135 | } 136 | static getEncryptedPasswordInBase64(args = {password, salt, iterations}){ 137 | return new Promise(resolve=>{ 138 | var worker = new Worker("./v2/encryption/workerkey.js"); 139 | // const worker = new Worker("./v2/worker.js", {type: 'module'}); 140 | worker.onmessage = async e => { 141 | worker.terminate(); 142 | const keyString = e.data; 143 | resolve(keyString); 144 | } 145 | // worker.postMessage({ 146 | // file:"./encryption.js", 147 | // clazz:"Encryption", 148 | // func:"encryptPasswordSync", 149 | // args 150 | // }); 151 | worker.postMessage(args); 152 | }) 153 | } 154 | static async encrypt(value,encryptionPassword){ 155 | try{ 156 | return encrypt(value,encryptionPassword); 157 | }catch(error){ 158 | console.log("Couldn't encrypt",value,error); 159 | return value; 160 | } 161 | } 162 | static async decrypt(value,encryptionPassword){ 163 | try{ 164 | return decrypt(value,encryptionPassword); 165 | }catch(error){ 166 | console.log("Couldn't decrypt",value,error); 167 | return value; 168 | } 169 | } 170 | } 171 | try{ 172 | module.exports = Encryption; 173 | }catch{} 174 | 175 | 176 | class Util{ 177 | static toClass(value){ 178 | return {}.toString.call(value); 179 | } 180 | static isSubTypeOf(value,name){ 181 | if(!value) return; 182 | 183 | if(!Util.isString(value)){ 184 | name = name.name; 185 | } 186 | var superType = Object.getPrototypeOf(Object.getPrototypeOf(value)); 187 | 188 | while(superType ? true : false){ 189 | const constructorName = superType.constructor.name; 190 | if(constructorName == "Object") return false; 191 | 192 | if(constructorName == name) return true; 193 | 194 | superType = Object.getPrototypeOf(superType); 195 | } 196 | return false; 197 | } 198 | static getType(value){ 199 | if(!value) return null; 200 | 201 | return value.constructor.name; 202 | } 203 | static isType(value,name){ 204 | if(value === null || value === undefined || !name) return false; 205 | 206 | return value.constructor.name == name; 207 | } 208 | static isString(value){ 209 | return Util.isType(value,"String"); 210 | } 211 | static isFile(value){ 212 | return Util.isType(value,"File"); 213 | } 214 | static isNumber(value){ 215 | return Util.isType(value,"Number"); 216 | } 217 | static isBoolean(value){ 218 | return Util.isType(value,"Boolean"); 219 | } 220 | static isFunction(value){ 221 | return Util.isType(value,"Function"); 222 | } 223 | static isArray(value){ 224 | return Util.isType(value,"Array") || Util.isSubTypeOf("Array"); 225 | } 226 | static isFile(value){ 227 | return Util.isType(value,"File"); 228 | } 229 | static isFormData(value){ 230 | return Util.isType(value,"FormData"); 231 | } 232 | } --------------------------------------------------------------------------------