-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfmd-api.js
More file actions
281 lines (240 loc) · 9.42 KB
/
fmd-api.js
File metadata and controls
281 lines (240 loc) · 9.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
const axios = require('axios');
const { hashPasswordForLogin, unwrapPrivateKey, decryptPacketModern, signString } = require('./crypto');
// This whole file is vodo magic
const defaultServer = "https://fmd.nulide.de/"
/**
* @typedef {Object} apiConfig
* @property {String} apiConfig.url URL of server (Defaults to: https://fmd.nulide.de/)
*/
/**
* @typedef {Object} loginData
* @property {Object} accessToken Used for server auth
* @property {CryptoKey} privateKey Uses to decrypt data returned
*/
/**
* @typedef {Object} LocationData
* @property {'fused' | 'gps' | 'network' | 'opencell'} provider What provider provided the lat and lon data
* @property {Date} date Date the loction was saved (Number)
* @property {Number} bat Battery level of device
* @property {Number} lon Device Longitude
* @property {Number} lat Device Latitude
* @property {String} time Date the loction was saved (String)
* @property {Number} [speed] Current speed (kph)
* @property {Number} [accuracy] Accuracy of location
* @property {Number} [altitude] Altitude of location
* @property {Number} [bearing] Bearing of location
*/
class FMD_API {
/**
* FMD API!
* @param {String} deviceID The Device ID
* @param {String} password Your password
* @param {apiConfig} config Config
*/
constructor(deviceID, password, config = {}) {
this.deviceID = deviceID;
this.password = password;
this.lastCheck = 0;
config ??= {}
config.url ??= defaultServer
this.config = config
this.url = config.url
this.commands = {
locate: () => this.sendToPhone("locate"),
locate_gps: () => this.sendToPhone("locate gps"),
locate_cell: () => this.sendToPhone("locate cell"),
locate_last: () => this.sendToPhone("locate last"),
ring: () => this.sendToPhone("ring"),
lock: () => this.sendToPhone("lock"),
camera_front: () => this.sendToPhone("camera front"),
camera_back: () => this.sendToPhone("camera back")
}
}
/**
* Login to FMD server
* @return {Promise<loginData>} The login data containing accessToken and privateKey
*/
async login() {
try {
let saltResponse = await axios.put(`${this.url}/salt`, { IDT: this.deviceID, Data: "unused" })
let saltJSON = saltResponse.data
let salt = saltJSON.Data
let res = await hashPasswordForLogin(this.password, salt)
// console.log("res", res)
let accessResponse = await axios.put(`${this.url}/requestAccess`, { IDT: this.deviceID, Data: res })
let AccessData = accessResponse.data
this.accessToken = AccessData.Data
let keyResponse = await axios.put(`${this.url}/key`, { IDT: this.accessToken, Data: "unused" })
let keyData = keyResponse.data
// console.log("keyData",keyData)
const { decryptKey, signKey } = await unwrapPrivateKey(this.password, keyData.Data);
this.privateKeyForDecrypt = decryptKey;
this.privateKeyForSign = signKey;
return {
accessToken: this.accessToken,
privateKeyForDecrypt: this.privateKeyForDecrypt,
privateKeyForSign: this.privateKeyForSign
}
} catch (error) {
console.error("Login failed:", error);
throw new Error("Unable to login to FMD server.");
}
}
/**
* Checks if valid login- if none is present then a relogin will be made, will return false if a valid login can't be made
* @returns {Promise<boolean>}
*/
async checkLogin() {
const FIVE_MINUTES = 5 * 60 * 1000;
const now = Date.now();
const timeSinceLastCheck = now - this.lastCheck;
// If we already have a token and last check was recent, skip validation
if (this.accessToken && timeSinceLastCheck < FIVE_MINUTES) return true;
// If no access token yet, try to log in
if (!this.accessToken) {
try {
await this.login();
} catch (err) {
console.error("Initial login failed:", err);
return false;
}
}
// Try a lightweight validation request
try {
const res = await axios.put(`${this.url}/locationDataSize`, { IDT: this.accessToken, Data: "unused" });
const count = parseInt(res.data?.Data, 10);
if (!isNaN(count)) {
this.lastCheck = now;
return true; // token still valid
}
} catch (err) {
// console.warn("Access token likely invalid, retrying login...");
try {
await this.login();
this.lastCheck = Date.now();
return true;
} catch (e) {
console.error("Re-login failed:", e);
return false;
}
}
return false;
}
/**
*
* @returns {Promise<Number>}
*/
async getLocationCount() {
await this.checkLogin();
try {
let responseDataSize = await axios.put(`${this.url}/locationDataSize`, { IDT: this.accessToken, Data: "unused" })
let newestLocationDataIndex = parseInt(responseDataSize.data.Data, 10) - 1;
return newestLocationDataIndex;
} catch (error) {
console.error("getLocationCount failed:", error);
throw new Error("Unable to get location count.");
}
}
/**
* Get location (1:1 of FMD.locate)
* @property {Number} requestedIndex Location index (Default: -1)
* @returns {Promise<LocationData>}
*/
async getLocation(index = -1) {
return await this.locate(index);
}
/**
* Get location
* @property {Number} requestedIndex Location index (Default: -1)
* @returns {Promise<LocationData>}
*/
async locate(requestedIndex = -1) {
await this.checkLogin();
try {
let newestLocationDataIndex = await this.getLocationCount()
if (requestedIndex == -1) {
requestedIndex = newestLocationDataIndex;
}
let responseLocation = await axios.put(`${this.url}/location`, {
IDT: this.accessToken,
Data: requestedIndex.toString()
});
// console.log("responseLocation", responseLocation.data)
let rawLocation = await decryptPacketModern(this.privateKeyForDecrypt, responseLocation.data.Data)
let location = JSON.parse(rawLocation);
let tDate = new Date(location?.date);
if (tDate) location.date = tDate;
return location
} catch (error) {
console.error("Location get failed:", error);
throw new Error("Unable to get location.");
}
}
/**
* Send command to device
* @property {String} command Command to send
* @returns {Promise<Boolean>} Whether the command was successfully sent
*/
async sendToPhone(command) {
await this.checkLogin();
try {
const unixTime = Date.now(); // Current time in milliseconds
const toSign = `${unixTime}:${command}`;
// Sign the command using the private key
const signature = await signString(this.privateKeyForSign, toSign);
const commandData = {
IDT: this.accessToken,
Data: command,
UnixTime: unixTime,
CmdSig: signature
};
let res = await axios.post(`${this.url}/command`, commandData);
return res.status == 200;
} catch (error) {
console.error("SendToPhone failed:", error);
throw new Error("Unable to send command to FMD server.");
}
}
/**
* Get old commands!
* @deprecated Doesn't work currently - Not impemnted fully FMD side
* @returns {null} Doesn't do anything for now!!
*/
async getOldCommands() {
return null
let res = await axios.post(`${this.url}/commandLogs`, { IDT: this.accessToken, Data: "" })
// console.log(res)
}
/**
* Get amount of pictures the server has
* @returns {Promise<Number>} Count of picturs on the server
*/
async getPictureCount() {
await this.checkLogin();
try {
let resPictureSize = await axios.put(`${this.url}/pictureSize`, { IDT: this.accessToken, Data: "" })
return resPictureSize.data.Data
} catch (error) {
console.error("getPictureCount failed:", error);
throw new Error("Unable to get picture count.");
}
}
/**
* Get picture the server has at the given index
* @property {Number} pictureIndex Index of picture
* @returns {Promise<Buffer>} Picture buffer
*/
async getPicture(pictureIndex) {
await this.checkLogin();
try {
const resPicture = await axios.put(`${this.url}/picture`, { IDT: this.accessToken, Data: pictureIndex.toString() })
let pictureDataRaw = resPicture.data
let pictureData = await decryptPacketModern(this.privateKeyForDecrypt, pictureDataRaw)
return Buffer.from(pictureData, "base64")
} catch (error) {
console.error("getPicture failed:", error);
throw new Error("Unable to get picture from FMD server.");
}
}
}
module.exports = FMD_API