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.
@@ -0,0 +1,72 @@
1
+ <!-- ============================================================
2
+ ipgeo-config.html
3
+ Configuration node editor template + help panel
4
+ ============================================================ -->
5
+
6
+ <script type="text/javascript">
7
+ RED.nodes.registerType('ipgeo-config', {
8
+ category: 'config',
9
+ defaults: {
10
+ name: { value: 'IPGeolocation.io' }
11
+ },
12
+ credentials: {
13
+ apiKey: { type: 'password' }
14
+ },
15
+ label: function () {
16
+ return this.name || 'IPGeolocation.io';
17
+ },
18
+ oneditprepare: function () {
19
+ // Show a tick/cross indicator based on whether a key is already saved
20
+ const hasKey = this.credentials.has_apiKey;
21
+ $('#node-config-input-apiKey').attr(
22
+ 'placeholder',
23
+ hasKey ? '●●●●●●●● (saved)' : 'Paste your API key here'
24
+ );
25
+ }
26
+ });
27
+ </script>
28
+
29
+ <!-- ── Edit template ── -->
30
+ <script type="text/html" data-template-name="ipgeo-config">
31
+ <div class="form-row">
32
+ <label for="node-config-input-name">
33
+ <i class="fa fa-tag"></i>&nbsp;Name
34
+ </label>
35
+ <input type="text" id="node-config-input-name"
36
+ placeholder="e.g. My IPGeo Key" style="width:70%"/>
37
+ </div>
38
+
39
+ <div class="form-row">
40
+ <label for="node-config-input-apiKey">
41
+ <i class="fa fa-key"></i>&nbsp;API Key
42
+ </label>
43
+ <input type="password" id="node-config-input-apiKey"
44
+ style="width:70%"/>
45
+ <div style="margin-top:6px;font-size:0.85em;color:#888;">
46
+ Get your free key at
47
+ <a href="https://app.ipgeolocation.io/signup" target="_blank">
48
+ ipgeolocation.io
49
+ </a>.
50
+ The key is stored encrypted and never exported in flow JSON.
51
+ </div>
52
+ </div>
53
+ </script>
54
+
55
+ <!-- ── Help panel ── -->
56
+ <script type="text/html" data-help-name="ipgeo-config">
57
+ <p>
58
+ Stores your <strong>IPGeolocation.io API key</strong> securely as a
59
+ Node-RED credential. All IPGeo nodes share this config node so you
60
+ only need to enter the key once.
61
+ </p>
62
+ <h3>How to get an API key</h3>
63
+ <ol>
64
+ <li>Sign up at <a href="https://app.ipgeolocation.io/signup" target="_blank">ipgeolocation.io</a>.</li>
65
+ <li>Log in, open the <strong>Dashboard</strong>, and copy your API key.</li>
66
+ <li>Paste it into the <em>API Key</em> field above and click <strong>Add</strong>.</li>
67
+ </ol>
68
+ <p>
69
+ The key is stored using Node-RED's credential store and is <strong>never
70
+ included</strong> in exported flow JSON.
71
+ </p>
72
+ </script>
@@ -0,0 +1,30 @@
1
+ /**
2
+ * ipgeo-config.js
3
+ *
4
+ * Configuration node — stores the IPGeolocation.io API key as a credential.
5
+ * All functional nodes reference this config node so a single key change
6
+ * propagates everywhere instantly.
7
+ *
8
+ * The config node is intentionally minimal; it is a shared credential
9
+ * container with no input/output behaviour of its own.
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ module.exports = function (RED) {
15
+
16
+ function IPGeoConfigNode(n) {
17
+ RED.nodes.createNode(this, n);
18
+ // 'name' is stored as a plain property (safe to export in flows)
19
+ this.name = n.name;
20
+ // 'apiKey' is stored as a credential (never exported in flow JSON)
21
+ // Access via: this.credentials.apiKey
22
+ }
23
+
24
+ RED.nodes.registerType('ipgeo-config', IPGeoConfigNode, {
25
+ credentials: {
26
+ apiKey: { type: 'password' },
27
+ },
28
+ });
29
+
30
+ };
@@ -0,0 +1,300 @@
1
+ <!-- ============================================================
2
+ ipgeo-lookup.html
3
+ Single-IP geolocation lookup node — editor template + help
4
+ ============================================================ -->
5
+
6
+ <script type="text/javascript">
7
+ (function () {
8
+ 'use strict';
9
+
10
+ // ── Helper: populate a select from an options array ────────────────────────
11
+ function populateSelect(selectId, options, currentVal) {
12
+ const $sel = $(selectId).empty();
13
+ options.forEach(function (opt) {
14
+ $sel.append($('<option>').val(opt.value).text(opt.label));
15
+ });
16
+ if (currentVal !== undefined) $sel.val(currentVal);
17
+ }
18
+
19
+ // ── Include module list ────────────────────────────────────────────────────
20
+ const INCLUDE_MODULES = [
21
+ { id: 'inc-security', val: 'security', label: 'security (+2 credits)' },
22
+ { id: 'inc-abuse', val: 'abuse', label: 'abuse (+1 credit)' },
23
+ { id: 'inc-user_agent', val: 'user_agent', label: 'user_agent' },
24
+ { id: 'inc-geo_acc', val: 'geo_accuracy', label: 'geo_accuracy' },
25
+ { id: 'inc-dma', val: 'dma_code', label: 'dma_code (US only)' },
26
+ { id: 'inc-host', val: 'hostname', label: 'hostname (DB lookup)' },
27
+ { id: 'inc-livehost', val: 'liveHostname', label: 'liveHostname (DNS lookup)' },
28
+ { id: 'inc-fallback', val: 'hostnameFallbackLive', label: 'hostnameFallbackLive' },
29
+ ];
30
+
31
+ RED.nodes.registerType('ipgeo-lookup', {
32
+ category: 'IPGeolocation',
33
+ color: '#5da0d0',
34
+ icon: 'font-awesome/fa-map-marker',
35
+ paletteLabel:'ipgeo lookup',
36
+
37
+ defaults: {
38
+ name: { value: '' },
39
+ server: { value: '', type: 'ipgeo-config', required: true },
40
+ ipType: { value: 'str' },
41
+ ipValue: { value: '' },
42
+ include: { value: '' },
43
+ fields: { value: '' },
44
+ excludes: { value: '' },
45
+ lang: { value: 'en' },
46
+ outputProp: { value: 'payload' },
47
+ outputType: { value: 'msg' },
48
+ uaHeader: { value: false },
49
+ uaProp: { value: 'req.headers.user-agent' },
50
+ twoOutputs: { value: true },
51
+ },
52
+
53
+ inputs: 1,
54
+ outputs: 2, // [success, error]
55
+
56
+ outputLabels: function (idx) {
57
+ return idx === 0 ? 'success' : 'error';
58
+ },
59
+
60
+ label: function () {
61
+ return this.name || 'ipgeo lookup';
62
+ },
63
+
64
+ labelStyle: function () {
65
+ return this.name ? 'node_label_italic' : '';
66
+ },
67
+
68
+ oneditprepare: function () {
69
+ const node = this;
70
+
71
+ // ── IP input — typed input widget ──────────────────────────────────
72
+ $('#node-input-ipValue').typedInput({
73
+ default: 'str',
74
+ typeField: '#node-input-ipType',
75
+ types: ['msg', 'flow', 'global', 'str'],
76
+ });
77
+
78
+ // ── Include checkboxes — encode/decode comma list ──────────────────
79
+ const incList = (node.include || '').split(',').map(s => s.trim()).filter(Boolean);
80
+ INCLUDE_MODULES.forEach(function (mod) {
81
+ const $cb = $('<input type="checkbox" style="margin-right:6px">')
82
+ .prop('id', mod.id)
83
+ .prop('checked', incList.includes(mod.val))
84
+ .data('val', mod.val);
85
+ const $label = $('<label>').attr('for', mod.id).text(mod.label)
86
+ .css({ display: 'inline', fontWeight: 'normal', marginRight: '12px' });
87
+ $('#ipgeo-include-container').append($('<div>').css('display','inline-block').append($cb).append($label));
88
+ });
89
+
90
+ // ── Output property typed input ────────────────────────────────────
91
+ $('#node-input-outputProp').typedInput({
92
+ default: 'msg',
93
+ typeField: '#node-input-outputType',
94
+ types: ['msg', 'flow', 'global'],
95
+ });
96
+
97
+ // ── UA header toggle ───────────────────────────────────────────────
98
+ function toggleUARow() {
99
+ if ($('#node-input-uaHeader').prop('checked')) {
100
+ $('#ipgeo-ua-row').show();
101
+ } else {
102
+ $('#ipgeo-ua-row').hide();
103
+ }
104
+ }
105
+ $('#node-input-uaHeader').prop('checked', node.uaHeader).on('change', toggleUARow);
106
+ toggleUARow();
107
+
108
+ // ── Two outputs checkbox ───────────────────────────────────────────
109
+ $('#node-input-twoOutputs').prop('checked', node.twoOutputs !== false);
110
+ },
111
+
112
+ oneditsave: function () {
113
+ // Collect include checkboxes back into a comma-separated string
114
+ const chosen = [];
115
+ INCLUDE_MODULES.forEach(function (mod) {
116
+ if ($('#' + mod.id).prop('checked')) chosen.push(mod.val);
117
+ });
118
+ this.include = chosen.join(',');
119
+ this.twoOutputs = $('#node-input-twoOutputs').prop('checked');
120
+ },
121
+ });
122
+ }());
123
+ </script>
124
+
125
+ <!-- ── Edit template ── -->
126
+ <script type="text/html" data-template-name="ipgeo-lookup">
127
+ <!-- Name -->
128
+ <div class="form-row">
129
+ <label for="node-input-name"><i class="fa fa-tag"></i>&nbsp;Name</label>
130
+ <input type="text" id="node-input-name" placeholder="ipgeo lookup" style="width:70%"/>
131
+ </div>
132
+
133
+ <!-- Config node -->
134
+ <div class="form-row">
135
+ <label for="node-input-server"><i class="fa fa-key"></i>&nbsp;API Config</label>
136
+ <input type="text" id="node-input-server" style="width:70%"/>
137
+ <input type="hidden" id="node-input-ipType"/>
138
+ </div>
139
+
140
+ <hr/>
141
+
142
+ <!-- IP address input -->
143
+ <div class="form-row">
144
+ <label for="node-input-ipValue"><i class="fa fa-globe"></i>&nbsp;IP / Domain</label>
145
+ <input type="text" id="node-input-ipValue" placeholder="Leave empty to detect caller IP" style="width:70%"/>
146
+ <div style="margin-top:4px;font-size:0.82em;color:#888;">
147
+ IPv4, IPv6, or domain name. Leave empty to auto-detect the caller's IP.
148
+ You can also point to a msg property (e.g. <code>msg.ip</code>).
149
+ </div>
150
+ </div>
151
+
152
+ <hr/>
153
+
154
+ <!-- Include modules (checkboxes) -->
155
+ <div class="form-row">
156
+ <label><i class="fa fa-plus-square"></i>&nbsp;Include</label>
157
+ <div id="ipgeo-include-container" style="display:inline-block;width:70%;vertical-align:top;line-height:2em"></div>
158
+ </div>
159
+ <hr/>
160
+
161
+ <!-- Fields & Excludes -->
162
+ <div class="form-row">
163
+ <label for="node-input-fields"><i class="fa fa-filter"></i>&nbsp;Fields</label>
164
+ <input type="text" id="node-input-fields" placeholder="e.g. location,asn.organization (comma-separated)" style="width:70%"/>
165
+ </div>
166
+ <div class="form-row">
167
+ <label for="node-input-excludes"><i class="fa fa-minus-square"></i>&nbsp;Excludes</label>
168
+ <input type="text" id="node-input-excludes" placeholder="e.g. currency,location.continent_code" style="width:70%"/>
169
+ </div>
170
+
171
+ <!-- Language -->
172
+ <div class="form-row">
173
+ <label for="node-input-lang"><i class="fa fa-language"></i>&nbsp;Language</label>
174
+ <select id="node-input-lang" style="width:70%">
175
+ <option value="en">English (en)</option>
176
+ <option value="de">German (de)</option>
177
+ <option value="ru">Russian (ru)</option>
178
+ <option value="ja">Japanese (ja)</option>
179
+ <option value="fr">French (fr)</option>
180
+ <option value="cn">Chinese Simplified (cn)</option>
181
+ <option value="es">Spanish (es)</option>
182
+ <option value="cs">Czech (cs)</option>
183
+ <option value="it">Italian (it)</option>
184
+ <option value="ko">Korean (ko)</option>
185
+ <option value="fa">Persian (fa)</option>
186
+ <option value="pt">Portuguese (pt)</option>
187
+ </select>
188
+ </div>
189
+
190
+ <hr/>
191
+
192
+ <!-- Forward UA header -->
193
+ <div class="form-row">
194
+ <label><i class="fa fa-desktop"></i>&nbsp;User-Agent</label>
195
+ <input type="checkbox" id="node-input-uaHeader" style="width:auto;margin-right:6px"/>
196
+ <label for="node-input-uaHeader" style="display:inline;font-weight:normal">
197
+ Forward User-Agent header from msg property
198
+ </label>
199
+ </div>
200
+ <div class="form-row" id="ipgeo-ua-row" style="display:none">
201
+ <label for="node-input-uaProp" style="margin-left:20px">msg path</label>
202
+ <input type="text" id="node-input-uaProp" placeholder="req.headers.user-agent" style="width:60%"/>
203
+ </div>
204
+
205
+ <hr/>
206
+
207
+ <!-- Output property -->
208
+ <div class="form-row">
209
+ <label for="node-input-outputProp"><i class="fa fa-sign-out"></i>&nbsp;Output to</label>
210
+ <input type="text" id="node-input-outputProp" style="width:70%"/>
211
+ <input type="hidden" id="node-input-outputType"/>
212
+ </div>
213
+
214
+ <!-- Two outputs toggle -->
215
+ <div class="form-row">
216
+ <label><i class="fa fa-random"></i>&nbsp;Outputs</label>
217
+ <input type="checkbox" id="node-input-twoOutputs" style="width:auto;margin-right:6px"/>
218
+ <label for="node-input-twoOutputs" style="display:inline;font-weight:normal">
219
+ Use second output for errors (instead of throwing to Catch node)
220
+ </label>
221
+ </div>
222
+ </script>
223
+
224
+ <!-- ── Help panel ── -->
225
+ <script type="text/html" data-help-name="ipgeo-lookup">
226
+ <p>
227
+ Looks up geolocation, network, ASN, timezone, currency, security, and
228
+ abuse information for a single IPv4/IPv6 address or domain via
229
+ <strong>IPGeolocation.io v3 API</strong>.
230
+ </p>
231
+
232
+ <h3>Inputs</h3>
233
+ <dl class="message-properties">
234
+ <dt>payload <span class="property-type">any</span></dt>
235
+ <dd>Passed through unchanged.</dd>
236
+ <dt class="optional">ipgeo_include <span class="property-type">string</span></dt>
237
+ <dd>
238
+ Override the node's <em>Include</em> setting per-message.
239
+ Comma-separated list, e.g. <code>"security,abuse"</code>.
240
+ </dd>
241
+ </dl>
242
+
243
+ <h3>Outputs</h3>
244
+ <ol class="node-ports">
245
+ <li>
246
+ <strong>Success</strong>
247
+ <dl class="message-properties">
248
+ <dt>payload <span class="property-type">object</span></dt>
249
+ <dd>
250
+ Full IPGeolocation.io response. Top-level keys include
251
+ <code>ip</code>, <code>location</code>, <code>asn</code>,
252
+ <code>time_zone</code>, <code>currency</code>, <code>company</code>,
253
+ <code>network</code>, and optionally <code>security</code>,
254
+ <code>abuse</code>, <code>user_agent</code>, <code>hostname</code>.
255
+ </dd>
256
+ <dt class="optional">ipgeo_credits <span class="property-type">number</span></dt>
257
+ <dd>Credits consumed by this request (from <code>X-Credits-Charged</code> header).</dd>
258
+ </dl>
259
+ </li>
260
+ <li>
261
+ <strong>Error</strong> (when <em>Use second output</em> is checked)
262
+ <dl class="message-properties">
263
+ <dt>error <span class="property-type">object</span></dt>
264
+ <dd>
265
+ <code>{ code, message, status, body }</code> — structured error object.
266
+ Error codes include <code>AUTH_FAILED</code>, <code>RATE_LIMITED</code>,
267
+ <code>TIMEOUT</code>, <code>BAD_REQUEST</code>, etc.
268
+ </dd>
269
+ </dl>
270
+ </li>
271
+ </ol>
272
+
273
+ <h3>Details</h3>
274
+ <ul>
275
+ <li>Leave <em>IP / Domain</em> empty to look up the caller's IP address.</li>
276
+ <li>
277
+ Use <em>Include</em> checkboxes to add optional data modules.
278
+ <code>security</code> costs an extra 2 credits;
279
+ <code>abuse</code> costs an extra 1 credit.
280
+ </li>
281
+ <li>
282
+ <em>Fields</em> restricts the response to named fields
283
+ (e.g. <code>location.city,asn.organization</code>).
284
+ </li>
285
+ <li>
286
+ <em>Excludes</em> removes fields or whole objects
287
+ (e.g. <code>currency,location.continent_code</code>).
288
+ </li>
289
+ <li>Failed requests are retried up to 3 times with exponential back-off.</li>
290
+ </ul>
291
+
292
+ <h3>References</h3>
293
+ <ul>
294
+ <li>
295
+ <a href="https://ipgeolocation.io/documentation/ip-location-api.html" target="_blank">
296
+ IP Geolocation API documentation
297
+ </a>
298
+ </li>
299
+ </ul>
300
+ </script>
@@ -0,0 +1,165 @@
1
+ /**
2
+ * ipgeo-lookup.js
3
+ *
4
+ * Node-RED node: "ipgeo lookup"
5
+ *
6
+ * Performs a single-IP geolocation lookup via the IPGeolocation.io
7
+ * /v3/ipgeo endpoint (GET).
8
+ *
9
+ * Features
10
+ * ─────────
11
+ * • IPv4, IPv6, domain, or empty (caller IP auto-detect)
12
+ * • Configurable `include` modules: security, abuse, hostname, user_agent,
13
+ * geo_accuracy, dma_code (comma-separated)
14
+ * • `fields` and `excludes` for bandwidth optimisation
15
+ * • Response language selection (12 languages)
16
+ * • Optional custom User-Agent header forwarding
17
+ * • Hostname lookup variants: hostname | liveHostname | hostnameFallbackLive
18
+ * • IP can be sourced from node config OR msg property (dynamic)
19
+ * • Two outputs: [success, error] — error output is optional
20
+ * • Exposes credits consumed on msg.ipgeo_credits
21
+ * • Full retry + exponential backoff via shared api-client
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ const { request, IPGeoError } = require('./lib/api-client');
27
+ const {
28
+ setStatus,
29
+ resolveValue,
30
+ setOutputProperty,
31
+ handleNodeError,
32
+ buildCommonParams,
33
+ } = require('./lib/node-helpers');
34
+
35
+ module.exports = function (RED) {
36
+
37
+ function IPGeoLookupNode(config) {
38
+ RED.nodes.createNode(this, config);
39
+
40
+ // ── Config node (API key) ─────────────────────────────────────────────
41
+ const configNode = RED.nodes.getNode(config.server);
42
+
43
+ // ── Node configuration ────────────────────────────────────────────────
44
+ this.name = config.name;
45
+ this.ipType = config.ipType || 'str'; // 'str' | 'msg'
46
+ this.ipValue = config.ipValue || ''; // value or msg prop path
47
+ this.include = config.include || ''; // comma-sep list of modules
48
+ this.fields = config.fields || '';
49
+ this.excludes = config.excludes || '';
50
+ this.lang = config.lang || 'en';
51
+ this.outputProp = config.outputProp || 'payload'; // output property
52
+ this.outputType = config.outputType || 'msg';
53
+ this.uaHeader = config.uaHeader || false; // forward UA from msg
54
+ this.uaProp = config.uaProp || 'req.headers.user-agent';
55
+ this.twoOutputs = config.twoOutputs !== false; // default true
56
+
57
+ const node = this;
58
+
59
+ // ── Pre-flight: warn if no config node ───────────────────────────────
60
+ if (!configNode) {
61
+ setStatus(node, 'NO_KEY');
62
+ node.warn('No IPGeolocation.io config node selected — configure an API key first.');
63
+ } else {
64
+ setStatus(node, 'IDLE');
65
+ }
66
+
67
+ // ── Input handler ─────────────────────────────────────────────────────
68
+ node.on('input', async function (msg, send, done) {
69
+ // Node-RED 0.x compat
70
+ send = send || function () { node.send.apply(node, arguments); };
71
+
72
+ // ── Validate API key availability ─────────────────────────────────
73
+ const apiKey = configNode && configNode.credentials
74
+ ? configNode.credentials.apiKey
75
+ : null;
76
+
77
+ if (!apiKey) {
78
+ setStatus(node, 'NO_KEY');
79
+ const err = new IPGeoError('No API key configured', 'NO_API_KEY');
80
+ handleNodeError(node, err, msg, done, send, node.twoOutputs);
81
+ return;
82
+ }
83
+
84
+ // ── Resolve IP from msg or static config ──────────────────────────
85
+ let ip = '';
86
+ if (node.ipType === 'msg') {
87
+ ip = resolveValue(RED, node, msg,
88
+ { value: node.ipValue, type: 'msg' }) || '';
89
+ } else if (node.ipType === 'flow') {
90
+ ip = resolveValue(RED, node, msg,
91
+ { value: node.ipValue, type: 'flow' }) || '';
92
+ } else if (node.ipType === 'global') {
93
+ ip = resolveValue(RED, node, msg,
94
+ { value: node.ipValue, type: 'global' }) || '';
95
+ } else {
96
+ // 'str' — static value from node config; can be empty for caller-IP
97
+ ip = (node.ipValue || '').trim();
98
+ }
99
+
100
+ ip = String(ip).trim();
101
+
102
+ // ── Build query parameters ────────────────────────────────────────
103
+ const params = buildCommonParams(apiKey, {
104
+ lang: node.lang,
105
+ fields: node.fields,
106
+ excludes: node.excludes,
107
+ });
108
+
109
+ if (ip) params.ip = ip;
110
+
111
+ // Build include list (may come from node config or msg override)
112
+ const includeList = (msg.ipgeo_include || node.include || '').trim();
113
+ if (includeList) params.include = includeList;
114
+
115
+ // ── Optional User-Agent header forwarding ─────────────────────────
116
+ const extraHeaders = {};
117
+ if (node.uaHeader) {
118
+ const ua = resolveValue(RED, node, msg,
119
+ { value: node.uaProp, type: 'msg' });
120
+ if (ua) extraHeaders['User-Agent'] = ua;
121
+ }
122
+
123
+ // ── Set working status ────────────────────────────────────────────
124
+ setStatus(node, 'WORKING');
125
+
126
+ // ── API call ──────────────────────────────────────────────────────
127
+ try {
128
+ const { data, credits } = await request({
129
+ method: 'GET',
130
+ path: '/ipgeo',
131
+ params,
132
+ headers: extraHeaders,
133
+ });
134
+
135
+ // ── Success ────────────────────────────────────────────────────
136
+ setStatus(node, 'OK',
137
+ `${data.location?.city || data.ip || 'ok'}`);
138
+
139
+ // Write result to configured output property
140
+ setOutputProperty(RED, node, msg,
141
+ { value: node.outputProp, type: node.outputType }, data);
142
+
143
+ // Expose credits consumed (informational)
144
+ if (credits !== null) msg.ipgeo_credits = credits;
145
+
146
+ // Clear error property if previously set
147
+ delete msg.error;
148
+
149
+ send(node.twoOutputs ? [msg, null] : msg);
150
+ if (done) done();
151
+
152
+ } catch (err) {
153
+ handleNodeError(node, err, msg, done, send, node.twoOutputs);
154
+ }
155
+ });
156
+
157
+ // ── Clean up on node removal/redeploy ─────────────────────────────────
158
+ node.on('close', function () {
159
+ setStatus(node, 'IDLE');
160
+ });
161
+ }
162
+
163
+ RED.nodes.registerType('ipgeo-lookup', IPGeoLookupNode);
164
+
165
+ };