node-red-contrib-ipgeolocation 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +423 -0
- package/examples/01 - Single IP Lookup.json +102 -0
- package/examples/02 - Bulk IP Lookup.json +98 -0
- package/examples/03 - IP Security Lookup.json +98 -0
- package/examples/04 - Bulk IP Security.json +96 -0
- package/examples/05 - Abuse Contact Lookup.json +98 -0
- package/examples/06 - ASN Lookup.json +102 -0
- package/examples/07 - Timezone Lookup.json +101 -0
- package/examples/08 - Astronomy (Sun & Moon).json +109 -0
- package/examples/09 - User-Agent Parser.json +96 -0
- package/index.js +22 -0
- package/nodes/ipgeo-abuse.html +203 -0
- package/nodes/ipgeo-abuse.js +148 -0
- package/nodes/ipgeo-asn.html +297 -0
- package/nodes/ipgeo-asn.js +197 -0
- package/nodes/ipgeo-astronomy.html +242 -0
- package/nodes/ipgeo-astronomy.js +195 -0
- package/nodes/ipgeo-bulk-security.html +166 -0
- package/nodes/ipgeo-bulk-security.js +177 -0
- package/nodes/ipgeo-bulk.html +158 -0
- package/nodes/ipgeo-bulk.js +146 -0
- package/nodes/ipgeo-config.html +72 -0
- package/nodes/ipgeo-config.js +30 -0
- package/nodes/ipgeo-lookup.html +300 -0
- package/nodes/ipgeo-lookup.js +165 -0
- package/nodes/ipgeo-security.html +214 -0
- package/nodes/ipgeo-security.js +150 -0
- package/nodes/ipgeo-timezone.html +201 -0
- package/nodes/ipgeo-timezone.js +154 -0
- package/nodes/ipgeo-useragent.html +133 -0
- package/nodes/ipgeo-useragent.js +110 -0
- package/nodes/lib/api-client.js +210 -0
- package/nodes/lib/node-helpers.js +214 -0
- package/package.json +72 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<!-- ============================================================
|
|
2
|
+
ipgeo-bulk-security.html
|
|
3
|
+
Bulk IP security / threat lookup node
|
|
4
|
+
============================================================ -->
|
|
5
|
+
|
|
6
|
+
<script type="text/javascript">
|
|
7
|
+
(function () {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
RED.nodes.registerType('ipgeo-bulk-security', {
|
|
11
|
+
category: 'IPGeolocation',
|
|
12
|
+
color: '#d9534f',
|
|
13
|
+
icon: 'font-awesome/fa-shield',
|
|
14
|
+
paletteLabel: 'ipgeo bulk security',
|
|
15
|
+
|
|
16
|
+
defaults: {
|
|
17
|
+
name: { value: '' },
|
|
18
|
+
server: { value: '', type: 'ipgeo-config', required: true },
|
|
19
|
+
fields: { value: '' },
|
|
20
|
+
excludes: { value: '' },
|
|
21
|
+
outputProp: { value: 'payload' },
|
|
22
|
+
outputType: { value: 'msg' },
|
|
23
|
+
twoOutputs: { value: true },
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
inputs: 1,
|
|
27
|
+
outputs: 2,
|
|
28
|
+
|
|
29
|
+
outputLabels: function (idx) {
|
|
30
|
+
return idx === 0 ? 'success (array)' : 'error';
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
label: function () {
|
|
34
|
+
return this.name || 'ipgeo bulk security';
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
oneditprepare: function () {
|
|
38
|
+
const node = this;
|
|
39
|
+
|
|
40
|
+
$('#node-input-outputProp').typedInput({
|
|
41
|
+
default: 'msg',
|
|
42
|
+
typeField: '#node-input-outputType',
|
|
43
|
+
types: ['msg', 'flow', 'global'],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
$('#node-input-twoOutputs').prop('checked', node.twoOutputs !== false);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
oneditsave: function () {
|
|
50
|
+
this.twoOutputs = $('#node-input-twoOutputs').prop('checked');
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}());
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<!-- ── Edit template ── -->
|
|
57
|
+
<script type="text/html" data-template-name="ipgeo-bulk-security">
|
|
58
|
+
<div class="form-row">
|
|
59
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
60
|
+
<input type="text" id="node-input-name" placeholder="ipgeo bulk security" style="width:70%"/>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="form-row">
|
|
63
|
+
<label for="node-input-server"><i class="fa fa-key"></i> API Config</label>
|
|
64
|
+
<input type="text" id="node-input-server" style="width:70%"/>
|
|
65
|
+
</div>
|
|
66
|
+
<hr/>
|
|
67
|
+
<div class="form-row">
|
|
68
|
+
<label for="node-input-fields"><i class="fa fa-filter"></i> Fields</label>
|
|
69
|
+
<input type="text" id="node-input-fields"
|
|
70
|
+
placeholder="e.g. security.is_vpn,security.threat_score" style="width:70%"/>
|
|
71
|
+
<div style="margin-top:4px;font-size:0.82em;color:#888;">
|
|
72
|
+
Dot-notation, comma-separated. Returns only the listed fields from the
|
|
73
|
+
<code>security</code> object.
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="form-row">
|
|
77
|
+
<label for="node-input-excludes"><i class="fa fa-minus-square"></i> Excludes</label>
|
|
78
|
+
<input type="text" id="node-input-excludes"
|
|
79
|
+
placeholder="e.g. security.proxy_last_seen,security.vpn_last_seen" style="width:70%"/>
|
|
80
|
+
<div style="margin-top:4px;font-size:0.82em;color:#888;">
|
|
81
|
+
Dot-notation, comma-separated. Returns the full <code>security</code>
|
|
82
|
+
object except the listed fields. <code>ip</code> is always included
|
|
83
|
+
and cannot be excluded.
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<hr/>
|
|
87
|
+
<div class="form-row">
|
|
88
|
+
<label for="node-input-outputProp"><i class="fa fa-sign-out"></i> Output to</label>
|
|
89
|
+
<input type="text" id="node-input-outputProp" style="width:70%"/>
|
|
90
|
+
<input type="hidden" id="node-input-outputType"/>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="form-row">
|
|
93
|
+
<label><i class="fa fa-random"></i> Outputs</label>
|
|
94
|
+
<input type="checkbox" id="node-input-twoOutputs" style="width:auto;margin-right:6px"/>
|
|
95
|
+
<label for="node-input-twoOutputs" style="display:inline;font-weight:normal">
|
|
96
|
+
Use second output for errors
|
|
97
|
+
</label>
|
|
98
|
+
</div>
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<!-- ── Help panel ── -->
|
|
102
|
+
<script type="text/html" data-help-name="ipgeo-bulk-security">
|
|
103
|
+
<p>
|
|
104
|
+
Performs a <strong>bulk IP security / threat lookup</strong> for up to
|
|
105
|
+
<strong>50,000</strong> IPv4/IPv6 addresses in a single HTTP POST request,
|
|
106
|
+
via the <code>/v3/security-bulk</code> endpoint.
|
|
107
|
+
</p>
|
|
108
|
+
<p>
|
|
109
|
+
For each IP, the response reports a threat score plus VPN, proxy,
|
|
110
|
+
residential proxy, Tor, relay, bot, spam, known-attacker and
|
|
111
|
+
cloud-provider signals.
|
|
112
|
+
</p>
|
|
113
|
+
|
|
114
|
+
<h3>Input</h3>
|
|
115
|
+
<dl class="message-properties">
|
|
116
|
+
<dt>payload <span class="property-type">array | string</span></dt>
|
|
117
|
+
<dd>
|
|
118
|
+
An array of IPv4/IPv6 address strings, e.g.
|
|
119
|
+
<code>["2.56.188.34","2.56.188.35"]</code>. A comma-separated string is
|
|
120
|
+
also accepted and will be auto-split.
|
|
121
|
+
<strong>Domain names are not supported</strong> by the Security API.
|
|
122
|
+
</dd>
|
|
123
|
+
</dl>
|
|
124
|
+
|
|
125
|
+
<h3>Output</h3>
|
|
126
|
+
<dl class="message-properties">
|
|
127
|
+
<dt>payload <span class="property-type">array</span></dt>
|
|
128
|
+
<dd>
|
|
129
|
+
Array of result objects in the same order as the input. Each entry is
|
|
130
|
+
either <code>{ ip, security: {...} }</code> for a valid IP, or
|
|
131
|
+
<code>{ message: "..." }</code> for a bogon, private, or malformed
|
|
132
|
+
entry — these do not fail the request.
|
|
133
|
+
</dd>
|
|
134
|
+
<dt class="optional">ipgeo_credits <span class="property-type">number</span></dt>
|
|
135
|
+
<dd>
|
|
136
|
+
Total credits consumed. Each valid IP costs 2 credits; bogon, private,
|
|
137
|
+
and malformed entries are not charged.
|
|
138
|
+
</dd>
|
|
139
|
+
<dt class="optional">ipgeo_invalid_count <span class="property-type">number</span></dt>
|
|
140
|
+
<dd>
|
|
141
|
+
Number of entries that came back as <code>{ message }</code> instead
|
|
142
|
+
of a security result. Only present when at least one entry was
|
|
143
|
+
invalid.
|
|
144
|
+
</dd>
|
|
145
|
+
<dt class="optional">ipgeo_invalid_messages <span class="property-type">array</span></dt>
|
|
146
|
+
<dd>
|
|
147
|
+
The <code>message</code> text for each invalid entry. Only present
|
|
148
|
+
when at least one entry was invalid.
|
|
149
|
+
</dd>
|
|
150
|
+
</dl>
|
|
151
|
+
|
|
152
|
+
<h3>Notes</h3>
|
|
153
|
+
<ul>
|
|
154
|
+
<li>Only <code>fields</code> and <code>excludes</code> options are supported — there is no <code>include</code> or language option for the Security API.</li>
|
|
155
|
+
<li>Requires an API key on the configured server; bulk requests cannot be authenticated via Request Origin (CORS-only) setups.</li>
|
|
156
|
+
</ul>
|
|
157
|
+
|
|
158
|
+
<h3>References</h3>
|
|
159
|
+
<ul>
|
|
160
|
+
<li>
|
|
161
|
+
<a href="https://ipgeolocation.io/documentation/ip-security-api.html#bulk-ip-security-lookup-endpoint" target="_blank">
|
|
162
|
+
Bulk IP Security Lookup documentation
|
|
163
|
+
</a>
|
|
164
|
+
</li>
|
|
165
|
+
</ul>
|
|
166
|
+
</script>
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ipgeo-bulk-security.js
|
|
3
|
+
*
|
|
4
|
+
* Node-RED node: "ipgeo bulk security"
|
|
5
|
+
*
|
|
6
|
+
* Performs bulk IP security / threat lookups via the IPGeolocation.io
|
|
7
|
+
* /v3/security-bulk endpoint (POST).
|
|
8
|
+
*
|
|
9
|
+
* Input
|
|
10
|
+
* ─────
|
|
11
|
+
* msg.payload — Array of IPv4/IPv6 address strings (required)
|
|
12
|
+
* OR a comma-separated string which is auto-split
|
|
13
|
+
* (domain names are NOT supported by the Security API)
|
|
14
|
+
*
|
|
15
|
+
* Configuration
|
|
16
|
+
* ─────────────
|
|
17
|
+
* • fields / excludes options (dot-notation, e.g. "security.threat_score")
|
|
18
|
+
* • No "include" or "lang" options — the Security API does not support them
|
|
19
|
+
* • Max 50,000 IPs per request (per API limits)
|
|
20
|
+
* • Each result corresponds to one IP in the input array, in order.
|
|
21
|
+
* Invalid/bogon/private/malformed IPs return { message: '...' } objects
|
|
22
|
+
* instead of failing the whole request (per API spec).
|
|
23
|
+
*
|
|
24
|
+
* Output
|
|
25
|
+
* ──────
|
|
26
|
+
* msg.payload — Array of security result objects
|
|
27
|
+
* msg.ipgeo_credits — Credits consumed (2 per valid IP; from response header)
|
|
28
|
+
* msg.ipgeo_invalid_count — Number of entries that came back as { message }
|
|
29
|
+
* instead of a security result (only set if > 0,
|
|
30
|
+
* mirroring the API's X-Successful-Record header
|
|
31
|
+
* which is likewise only sent when invalid IPs
|
|
32
|
+
* are present)
|
|
33
|
+
* msg.ipgeo_invalid_messages — Array of the invalid entries' messages
|
|
34
|
+
* (only set if ipgeo_invalid_count > 0)
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
'use strict';
|
|
38
|
+
|
|
39
|
+
const { request, IPGeoError } = require('./lib/api-client');
|
|
40
|
+
const {
|
|
41
|
+
setStatus,
|
|
42
|
+
setOutputProperty,
|
|
43
|
+
handleNodeError,
|
|
44
|
+
buildCommonParams,
|
|
45
|
+
} = require('./lib/node-helpers');
|
|
46
|
+
|
|
47
|
+
const MAX_IPS = 50_000;
|
|
48
|
+
|
|
49
|
+
module.exports = function (RED) {
|
|
50
|
+
|
|
51
|
+
function IPGeoBulkSecurityNode(config) {
|
|
52
|
+
RED.nodes.createNode(this, config);
|
|
53
|
+
|
|
54
|
+
const configNode = RED.nodes.getNode(config.server);
|
|
55
|
+
|
|
56
|
+
this.name = config.name;
|
|
57
|
+
this.fields = config.fields || '';
|
|
58
|
+
this.excludes = config.excludes || '';
|
|
59
|
+
this.outputProp = config.outputProp || 'payload';
|
|
60
|
+
this.outputType = config.outputType || 'msg';
|
|
61
|
+
this.twoOutputs = config.twoOutputs !== false;
|
|
62
|
+
|
|
63
|
+
const node = this;
|
|
64
|
+
|
|
65
|
+
if (!configNode) {
|
|
66
|
+
setStatus(node, 'NO_KEY');
|
|
67
|
+
} else {
|
|
68
|
+
setStatus(node, 'IDLE');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
node.on('input', async function (msg, send, done) {
|
|
72
|
+
send = send || function () { node.send.apply(node, arguments); };
|
|
73
|
+
|
|
74
|
+
// ── API key ──────────────────────────────────────────────────────────
|
|
75
|
+
// Note: per the API docs, the bulk security endpoint requires an
|
|
76
|
+
// apiKey and cannot be authenticated via Request Origin (CORS) —
|
|
77
|
+
// unlike the single-IP /security endpoint.
|
|
78
|
+
const apiKey = configNode && configNode.credentials
|
|
79
|
+
? configNode.credentials.apiKey : null;
|
|
80
|
+
|
|
81
|
+
if (!apiKey) {
|
|
82
|
+
setStatus(node, 'NO_KEY');
|
|
83
|
+
const err = new IPGeoError('No API key configured', 'NO_API_KEY');
|
|
84
|
+
handleNodeError(node, err, msg, done, send, node.twoOutputs);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Extract IP list from msg.payload ─────────────────────────────────
|
|
89
|
+
let ips = msg.payload;
|
|
90
|
+
|
|
91
|
+
// Support comma-separated string
|
|
92
|
+
if (typeof ips === 'string') {
|
|
93
|
+
ips = ips.split(',').map(s => s.trim()).filter(Boolean);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!Array.isArray(ips) || ips.length === 0) {
|
|
97
|
+
const err = new IPGeoError(
|
|
98
|
+
'msg.payload must be a non-empty array of IPv4/IPv6 addresses or a comma-separated string',
|
|
99
|
+
'INVALID_INPUT'
|
|
100
|
+
);
|
|
101
|
+
handleNodeError(node, err, msg, done, send, node.twoOutputs);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (ips.length > MAX_IPS) {
|
|
106
|
+
const err = new IPGeoError(
|
|
107
|
+
`Bulk security lookup limit is ${MAX_IPS} IPs per request; received ${ips.length}`,
|
|
108
|
+
'TOO_MANY_IPS'
|
|
109
|
+
);
|
|
110
|
+
handleNodeError(node, err, msg, done, send, node.twoOutputs);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Build params ──────────────────────────────────────────────────────
|
|
115
|
+
// The security-bulk endpoint only supports `fields` and `excludes`
|
|
116
|
+
// (plus `output`, which we don't expose since the response is always
|
|
117
|
+
// parsed as JSON). There is no `include` and no `lang` here.
|
|
118
|
+
const params = buildCommonParams(apiKey, {
|
|
119
|
+
fields: node.fields,
|
|
120
|
+
excludes: node.excludes,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
setStatus(node, 'WORKING');
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const { data, credits } = await request({
|
|
127
|
+
method: 'POST',
|
|
128
|
+
path: '/security-bulk',
|
|
129
|
+
params,
|
|
130
|
+
data: { ips },
|
|
131
|
+
// Bulk requests can be large — give them extra time
|
|
132
|
+
timeout: 30_000,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Each entry is either { ip, security: {...} } for a valid IP, or
|
|
136
|
+
// { message: '...' } for a bogon/private/malformed entry. Count the
|
|
137
|
+
// latter so the user gets the same signal the API otherwise conveys
|
|
138
|
+
// via the (conditionally-present) X-Successful-Record header.
|
|
139
|
+
const invalidEntries = Array.isArray(data)
|
|
140
|
+
? data.filter((entry) => entry && typeof entry === 'object' && !('security' in entry))
|
|
141
|
+
: [];
|
|
142
|
+
|
|
143
|
+
const validCount = Array.isArray(data) ? data.length - invalidEntries.length : ips.length;
|
|
144
|
+
|
|
145
|
+
setStatus(node, 'OK', invalidEntries.length > 0
|
|
146
|
+
? `${validCount}/${ips.length} valid`
|
|
147
|
+
: `${ips.length} IPs`);
|
|
148
|
+
|
|
149
|
+
setOutputProperty(RED, node, msg,
|
|
150
|
+
{ value: node.outputProp, type: node.outputType }, data);
|
|
151
|
+
|
|
152
|
+
if (credits !== null) msg.ipgeo_credits = credits;
|
|
153
|
+
|
|
154
|
+
if (invalidEntries.length > 0) {
|
|
155
|
+
msg.ipgeo_invalid_count = invalidEntries.length;
|
|
156
|
+
msg.ipgeo_invalid_messages = invalidEntries.map((entry) => entry.message);
|
|
157
|
+
} else {
|
|
158
|
+
delete msg.ipgeo_invalid_count;
|
|
159
|
+
delete msg.ipgeo_invalid_messages;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
delete msg.error;
|
|
163
|
+
|
|
164
|
+
send(node.twoOutputs ? [msg, null] : msg);
|
|
165
|
+
if (done) done();
|
|
166
|
+
|
|
167
|
+
} catch (err) {
|
|
168
|
+
handleNodeError(node, err, msg, done, send, node.twoOutputs);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
node.on('close', () => setStatus(node, 'IDLE'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
RED.nodes.registerType('ipgeo-bulk-security', IPGeoBulkSecurityNode);
|
|
176
|
+
|
|
177
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<!-- ============================================================
|
|
2
|
+
ipgeo-bulk.html
|
|
3
|
+
Bulk IP geolocation lookup node
|
|
4
|
+
============================================================ -->
|
|
5
|
+
|
|
6
|
+
<script type="text/javascript">
|
|
7
|
+
(function () {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
RED.nodes.registerType('ipgeo-bulk', {
|
|
11
|
+
category: 'IPGeolocation',
|
|
12
|
+
color: '#5da0d0',
|
|
13
|
+
icon: 'font-awesome/fa-list',
|
|
14
|
+
paletteLabel:'ipgeo bulk',
|
|
15
|
+
|
|
16
|
+
defaults: {
|
|
17
|
+
name: { value: '' },
|
|
18
|
+
server: { value: '', type: 'ipgeo-config', required: true },
|
|
19
|
+
include: { value: '' },
|
|
20
|
+
fields: { value: '' },
|
|
21
|
+
excludes: { value: '' },
|
|
22
|
+
lang: { value: 'en' },
|
|
23
|
+
outputProp: { value: 'payload' },
|
|
24
|
+
outputType: { value: 'msg' },
|
|
25
|
+
twoOutputs: { value: true },
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
inputs: 1,
|
|
29
|
+
outputs: 2,
|
|
30
|
+
|
|
31
|
+
outputLabels: function (idx) {
|
|
32
|
+
return idx === 0 ? 'success (array)' : 'error';
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
label: function () {
|
|
36
|
+
return this.name || 'ipgeo bulk';
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
oneditprepare: function () {
|
|
40
|
+
const node = this;
|
|
41
|
+
|
|
42
|
+
$('#node-input-outputProp').typedInput({
|
|
43
|
+
default: 'msg',
|
|
44
|
+
typeField: '#node-input-outputType',
|
|
45
|
+
types: ['msg', 'flow', 'global'],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
$('#node-input-twoOutputs').prop('checked', node.twoOutputs !== false);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
oneditsave: function () {
|
|
52
|
+
this.twoOutputs = $('#node-input-twoOutputs').prop('checked');
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}());
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<!-- ── Edit template ── -->
|
|
59
|
+
<script type="text/html" data-template-name="ipgeo-bulk">
|
|
60
|
+
<div class="form-row">
|
|
61
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
62
|
+
<input type="text" id="node-input-name" placeholder="ipgeo bulk" style="width:70%"/>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="form-row">
|
|
65
|
+
<label for="node-input-server"><i class="fa fa-key"></i> API Config</label>
|
|
66
|
+
<input type="text" id="node-input-server" style="width:70%"/>
|
|
67
|
+
</div>
|
|
68
|
+
<hr/>
|
|
69
|
+
<div class="form-row">
|
|
70
|
+
<label for="node-input-include"><i class="fa fa-plus-square"></i> Include</label>
|
|
71
|
+
<input type="text" id="node-input-include"
|
|
72
|
+
placeholder="e.g. security,abuse (comma-separated)" style="width:70%"/>
|
|
73
|
+
<div style="margin-top:4px;font-size:0.82em;color:#888;">
|
|
74
|
+
security (+2 credits each), abuse (+1), user_agent, geo_accuracy, dma_code,
|
|
75
|
+
hostname, liveHostname, hostnameFallbackLive
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="form-row">
|
|
79
|
+
<label for="node-input-fields"><i class="fa fa-filter"></i> Fields</label>
|
|
80
|
+
<input type="text" id="node-input-fields"
|
|
81
|
+
placeholder="e.g. location.city,currency" style="width:70%"/>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="form-row">
|
|
84
|
+
<label for="node-input-excludes"><i class="fa fa-minus-square"></i> Excludes</label>
|
|
85
|
+
<input type="text" id="node-input-excludes"
|
|
86
|
+
placeholder="e.g. currency,time_zone" style="width:70%"/>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="form-row">
|
|
89
|
+
<label for="node-input-lang"><i class="fa fa-language"></i> Language</label>
|
|
90
|
+
<select id="node-input-lang" style="width:70%">
|
|
91
|
+
<option value="en">English (en)</option>
|
|
92
|
+
<option value="de">German (de)</option>
|
|
93
|
+
<option value="ru">Russian (ru)</option>
|
|
94
|
+
<option value="ja">Japanese (ja)</option>
|
|
95
|
+
<option value="fr">French (fr)</option>
|
|
96
|
+
<option value="cn">Chinese Simplified (cn)</option>
|
|
97
|
+
<option value="es">Spanish (es)</option>
|
|
98
|
+
<option value="cs">Czech (cs)</option>
|
|
99
|
+
<option value="it">Italian (it)</option>
|
|
100
|
+
<option value="ko">Korean (ko)</option>
|
|
101
|
+
<option value="fa">Persian (fa)</option>
|
|
102
|
+
<option value="pt">Portuguese (pt)</option>
|
|
103
|
+
</select>
|
|
104
|
+
</div>
|
|
105
|
+
<hr/>
|
|
106
|
+
<div class="form-row">
|
|
107
|
+
<label for="node-input-outputProp"><i class="fa fa-sign-out"></i> Output to</label>
|
|
108
|
+
<input type="text" id="node-input-outputProp" style="width:70%"/>
|
|
109
|
+
<input type="hidden" id="node-input-outputType"/>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="form-row">
|
|
112
|
+
<label><i class="fa fa-random"></i> Outputs</label>
|
|
113
|
+
<input type="checkbox" id="node-input-twoOutputs" style="width:auto;margin-right:6px"/>
|
|
114
|
+
<label for="node-input-twoOutputs" style="display:inline;font-weight:normal">
|
|
115
|
+
Use second output for errors
|
|
116
|
+
</label>
|
|
117
|
+
</div>
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<!-- ── Help panel ── -->
|
|
121
|
+
<script type="text/html" data-help-name="ipgeo-bulk">
|
|
122
|
+
<p>
|
|
123
|
+
Performs a <strong>bulk geolocation lookup</strong> for up to <strong>50,000</strong>
|
|
124
|
+
IPv4/IPv6 addresses or domain names in a single HTTP POST request.
|
|
125
|
+
</p>
|
|
126
|
+
|
|
127
|
+
<h3>Input</h3>
|
|
128
|
+
<dl class="message-properties">
|
|
129
|
+
<dt>payload <span class="property-type">array | string</span></dt>
|
|
130
|
+
<dd>
|
|
131
|
+
An array of IP/domain strings, e.g. <code>["1.2.3.4","2.3.4.5"]</code>.
|
|
132
|
+
A comma-separated string is also accepted and will be auto-split.
|
|
133
|
+
</dd>
|
|
134
|
+
<dt class="optional">ipgeo_include <span class="property-type">string</span></dt>
|
|
135
|
+
<dd>Per-message override for the <em>Include</em> setting.</dd>
|
|
136
|
+
</dl>
|
|
137
|
+
|
|
138
|
+
<h3>Output</h3>
|
|
139
|
+
<dl class="message-properties">
|
|
140
|
+
<dt>payload <span class="property-type">array</span></dt>
|
|
141
|
+
<dd>
|
|
142
|
+
Array of result objects in the same order as the input.
|
|
143
|
+
Invalid/private IPs return <code>{ message: "..." }</code>
|
|
144
|
+
instead of geolocation data.
|
|
145
|
+
</dd>
|
|
146
|
+
<dt class="optional">ipgeo_credits <span class="property-type">number</span></dt>
|
|
147
|
+
<dd>Total credits consumed.</dd>
|
|
148
|
+
</dl>
|
|
149
|
+
|
|
150
|
+
<h3>References</h3>
|
|
151
|
+
<ul>
|
|
152
|
+
<li>
|
|
153
|
+
<a href="https://ipgeolocation.io/documentation/ip-location-api.html" target="_blank">
|
|
154
|
+
Bulk IP Lookup documentation
|
|
155
|
+
</a>
|
|
156
|
+
</li>
|
|
157
|
+
</ul>
|
|
158
|
+
</script>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ipgeo-bulk.js
|
|
3
|
+
*
|
|
4
|
+
* Node-RED node: "ipgeo bulk"
|
|
5
|
+
*
|
|
6
|
+
* Performs bulk geolocation lookups via the IPGeolocation.io
|
|
7
|
+
* /v3/ipgeo-bulk endpoint (POST).
|
|
8
|
+
*
|
|
9
|
+
* Input
|
|
10
|
+
* ─────
|
|
11
|
+
* msg.payload — Array of IP strings / domains (required)
|
|
12
|
+
* OR a comma-separated string which is auto-split
|
|
13
|
+
*
|
|
14
|
+
* Configuration
|
|
15
|
+
* ─────────────
|
|
16
|
+
* • Same include/fields/excludes/lang options as the single lookup node
|
|
17
|
+
* • Max 50,000 IPs per request (per API limits)
|
|
18
|
+
* • Each result corresponds to one IP in the input array
|
|
19
|
+
* (invalid IPs return { message: '...' } objects per API spec)
|
|
20
|
+
*
|
|
21
|
+
* Output
|
|
22
|
+
* ──────
|
|
23
|
+
* msg.payload — Array of geolocation result objects
|
|
24
|
+
* msg.ipgeo_credits — Credits consumed (from response header)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const { request, IPGeoError } = require('./lib/api-client');
|
|
30
|
+
const {
|
|
31
|
+
setStatus,
|
|
32
|
+
setOutputProperty,
|
|
33
|
+
handleNodeError,
|
|
34
|
+
buildCommonParams,
|
|
35
|
+
} = require('./lib/node-helpers');
|
|
36
|
+
|
|
37
|
+
const MAX_IPS = 50_000;
|
|
38
|
+
|
|
39
|
+
module.exports = function (RED) {
|
|
40
|
+
|
|
41
|
+
function IPGeoBulkNode(config) {
|
|
42
|
+
RED.nodes.createNode(this, config);
|
|
43
|
+
|
|
44
|
+
const configNode = RED.nodes.getNode(config.server);
|
|
45
|
+
|
|
46
|
+
this.name = config.name;
|
|
47
|
+
this.include = config.include || '';
|
|
48
|
+
this.fields = config.fields || '';
|
|
49
|
+
this.excludes = config.excludes || '';
|
|
50
|
+
this.lang = config.lang || 'en';
|
|
51
|
+
this.outputProp = config.outputProp || 'payload';
|
|
52
|
+
this.outputType = config.outputType || 'msg';
|
|
53
|
+
this.twoOutputs = config.twoOutputs !== false;
|
|
54
|
+
|
|
55
|
+
const node = this;
|
|
56
|
+
|
|
57
|
+
if (!configNode) {
|
|
58
|
+
setStatus(node, 'NO_KEY');
|
|
59
|
+
} else {
|
|
60
|
+
setStatus(node, 'IDLE');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
node.on('input', async function (msg, send, done) {
|
|
64
|
+
send = send || function () { node.send.apply(node, arguments); };
|
|
65
|
+
|
|
66
|
+
// ── API key ──────────────────────────────────────────────────────────
|
|
67
|
+
const apiKey = configNode && configNode.credentials
|
|
68
|
+
? configNode.credentials.apiKey : null;
|
|
69
|
+
|
|
70
|
+
if (!apiKey) {
|
|
71
|
+
setStatus(node, 'NO_KEY');
|
|
72
|
+
const err = new IPGeoError('No API key configured', 'NO_API_KEY');
|
|
73
|
+
handleNodeError(node, err, msg, done, send, node.twoOutputs);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Extract IP list from msg.payload ─────────────────────────────────
|
|
78
|
+
let ips = msg.payload;
|
|
79
|
+
|
|
80
|
+
// Support comma-separated string
|
|
81
|
+
if (typeof ips === 'string') {
|
|
82
|
+
ips = ips.split(',').map(s => s.trim()).filter(Boolean);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!Array.isArray(ips) || ips.length === 0) {
|
|
86
|
+
const err = new IPGeoError(
|
|
87
|
+
'msg.payload must be a non-empty array of IP addresses or a comma-separated string',
|
|
88
|
+
'INVALID_INPUT'
|
|
89
|
+
);
|
|
90
|
+
handleNodeError(node, err, msg, done, send, node.twoOutputs);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (ips.length > MAX_IPS) {
|
|
95
|
+
const err = new IPGeoError(
|
|
96
|
+
`Bulk lookup limit is ${MAX_IPS} IPs per request; received ${ips.length}`,
|
|
97
|
+
'TOO_MANY_IPS'
|
|
98
|
+
);
|
|
99
|
+
handleNodeError(node, err, msg, done, send, node.twoOutputs);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Build params ──────────────────────────────────────────────────────
|
|
104
|
+
const params = buildCommonParams(apiKey, {
|
|
105
|
+
lang: node.lang,
|
|
106
|
+
fields: node.fields,
|
|
107
|
+
excludes: node.excludes,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const includeList = (msg.ipgeo_include || node.include || '').trim();
|
|
111
|
+
if (includeList) params.include = includeList;
|
|
112
|
+
|
|
113
|
+
setStatus(node, 'WORKING');
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const { data, credits } = await request({
|
|
117
|
+
method: 'POST',
|
|
118
|
+
path: '/ipgeo-bulk',
|
|
119
|
+
params,
|
|
120
|
+
data: { ips },
|
|
121
|
+
// Bulk requests can be large — give them extra time
|
|
122
|
+
timeout: 30_000,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
setStatus(node, 'OK', `${ips.length} IPs`);
|
|
126
|
+
|
|
127
|
+
setOutputProperty(RED, node, msg,
|
|
128
|
+
{ value: node.outputProp, type: node.outputType }, data);
|
|
129
|
+
|
|
130
|
+
if (credits !== null) msg.ipgeo_credits = credits;
|
|
131
|
+
delete msg.error;
|
|
132
|
+
|
|
133
|
+
send(node.twoOutputs ? [msg, null] : msg);
|
|
134
|
+
if (done) done();
|
|
135
|
+
|
|
136
|
+
} catch (err) {
|
|
137
|
+
handleNodeError(node, err, msg, done, send, node.twoOutputs);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
node.on('close', () => setStatus(node, 'IDLE'));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
RED.nodes.registerType('ipgeo-bulk', IPGeoBulkNode);
|
|
145
|
+
|
|
146
|
+
};
|