Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 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 | 1x 1x 1x 1x 8x 8x 2x 2x 2x 2x 1x 9x 9x 9x 1x 8x 8x 8x 8x 8x 8x 8x 8x 8x 7x 7x 8x 8x 8x 5x 3x 1x 8x 8x 8x 8x 2x 6x 6x 2x 2x 1x 1x 5x 1x | /**
* Device info endpoint — accepts device info from mobile clients,
* enriches with IP geolocation, stores in Firestore, checks bans.
*
* POST /api/device-info → Submit device info
*/
const router = require('express').Router();
const { db } = require('../utils/firebase');
const { now } = require('../utils/helpers');
const log = require('../utils/log');
// ─── Helpers ─────────────────────────────────────────────────────
/**
* Check whether an IPv4 address falls within a CIDR range.
*/
function isIpInSubnet(ip, cidr) {
try {
const [subnet, bits] = cidr.split('/');
const prefixLen = Number.parseInt(bits, 10);
const mask = prefixLen === 0 ? 0 : (~0 << (32 - prefixLen)) >>> 0;
const ipNum =
ip.split('.').reduce((acc, oct) => ((acc << 8) >>> 0) + Number.parseInt(oct, 10), 0) >>> 0;
const subNum =
subnet.split('.').reduce((acc, oct) => ((acc << 8) >>> 0) + Number.parseInt(oct, 10), 0) >>>
0;
return (ipNum & mask) === (subNum & mask);
} catch {
return false;
}
}
/**
* Fetch IP geolocation data from ip-api.com.
* Returns { isp, asn, country, region } or empty object on failure.
*/
async function getIpGeo(ip) {
try {
// Validate IPv4 format to prevent URL injection
if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(ip)) return {};
const resp = await fetch(`http://ip-api.com/json/${ip}?fields=isp,as,country,regionName`);
Iif (!resp.ok) return {};
const data = await resp.json();
return {
isp: data.isp || null,
asn: data.as ? data.as.split(' ')[0] : null,
country: data.country || null,
region: data.regionName || null,
};
} catch {
return {};
}
}
// ─── Route ───────────────────────────────────────────────────────
router.post('/device-info', async (req, res) => {
try {
const body = req.body;
if (!body?.deviceId) {
return res.status(400).json({ error: 'deviceId is required' });
}
const { deviceId } = body;
// Extract client IP
const forwarded = req.headers['x-forwarded-for'];
const ip = forwarded ? forwarded.split(',')[0].trim() : req.ip;
// Enrich with IP geolocation
const geo = await getIpGeo(ip);
// Build device doc
const timestamp = now();
const deviceDoc = {
deviceId,
uniqueId: req.auth.uniqueId,
manufacturer: body.manufacturer || null,
model: body.model || null,
osVersion: body.osVersion || null,
screenResolution: body.screenResolution || null,
screenDensity: body.screenDensity || null,
totalRamMb: body.totalRamMb || null,
appVersion: body.appVersion || null,
buildNumber: body.buildNumber || null,
locale: body.locale || null,
networkType: body.networkType || null,
carrierName: body.carrierName || null,
firebaseInstallationId: body.firebaseInstallationId || null,
lastIp: ip,
isp: geo.isp || null,
asn: geo.asn || null,
country: geo.country || null,
region: geo.region || null,
lastSeenAt: timestamp,
};
// Check if doc already exists to set firstSeen/boundAt
const docRef = db.doc(`deviceBindings/${deviceId}`);
const existing = await docRef.get();
if (!existing.exists) {
deviceDoc.firstSeen = timestamp;
deviceDoc.boundAt = timestamp;
}
// Write to Firestore
await docRef.set(deviceDoc, { merge: true });
// Check bans
const banStatus = await checkBans(deviceId, ip, geo.asn || null);
res.json({ success: true, banStatus });
} catch (err) {
log.error('device-info', 'Error processing device info submission', { error: err.message });
res.status(500).json({ error: 'Internal server error' });
}
});
/** Check if a ban is currently active (not expired). */
function isBanActive(ban) {
return !ban.expiresAt || new Date(ban.expiresAt).getTime() > Date.now();
}
/** Build a ban result object. */
function buildBanResult(banType, ban) {
return { isBanned: true, banType, reason: ban.reason || null, expiresAt: ban.expiresAt || null };
}
/** Check if a network ban matches the given IP/ASN. */
function networkBanMatches(ban, ip, asn) {
Eif (ban.type === 'ip') return ban.value === ip;
if (ban.type === 'subnet') return isIpInSubnet(ip, ban.value);
if (ban.type === 'asn') return ban.value === asn;
return false;
}
/**
* Check device bans and network bans.
* Returns { isBanned, banType, reason, expiresAt }.
*/
async function checkBans(deviceId, ip, asn) {
const noBan = { isBanned: false, banType: null, reason: null, expiresAt: null };
try {
const deviceBanSnap = await db.doc(`deviceBans/${deviceId}`).get();
if (deviceBanSnap.exists && isBanActive(deviceBanSnap.data())) {
return buildBanResult('device', deviceBanSnap.data());
}
const networkBansSnap = await db.collection('networkBans').limit(500).get();
for (const doc of networkBansSnap.docs) {
const ban = doc.data();
if (!isBanActive(ban)) continue;
Eif (networkBanMatches(ban, ip, asn)) {
return buildBanResult(`network_${ban.type}`, ban);
}
}
return noBan;
} catch (err) {
log.error('device-info', 'Error checking bans', { deviceId, error: err.message });
return noBan;
}
}
module.exports = router;
|