summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/base/src/hostnameUtils.jsm
blob: e80c210b2e45e0d41b98bcf070a7d3fc7baa8e17 (plain)
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
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * Generic shared utility code for checking of IP and hostname validity.
 */

const EXPORTED_SYMBOLS = [
  "isLegalHostNameOrIP",
  "isLegalHostName",
  "isLegalIPv4Address",
  "isLegalIPv6Address",
  "isLegalIPAddress",
  "isLegalLocalIPAddress",
  "cleanUpHostName",
  "kMinPort",
  "kMaxPort",
];

var kMinPort = 1;
var kMaxPort = 65535;

/**
 * Check if aHostName is an IP address or a valid hostname.
 *
 * @param {string} aHostName - The string to check for validity.
 * @param {boolean} aAllowExtendedIPFormats - Allow hex/octal formats in addition to decimal.
 * @returns {?string} Unobscured host name if aHostName is valid.
 *   Returns null if it's not.
 */
function isLegalHostNameOrIP(aHostName, aAllowExtendedIPFormats) {
  /*
   RFC 1123:
   Whenever a user inputs the identity of an Internet host, it SHOULD
   be possible to enter either (1) a host domain name or (2) an IP
   address in dotted-decimal ("#.#.#.#") form.  The host SHOULD check
   the string syntactically for a dotted-decimal number before
   looking it up in the Domain Name System.
  */

  return (
    isLegalIPAddress(aHostName, aAllowExtendedIPFormats) ||
    isLegalHostName(aHostName)
  );
}

/**
 * Check if aHostName is a valid hostname.
 *
 * @returns {?string} The host name if it is valid. Returns null if it's not.
 */
function isLegalHostName(aHostName) {
  /*
   RFC 952:
   A "name" (Net, Host, Gateway, or Domain name) is a text string up
   to 24 characters drawn from the alphabet (A-Z), digits (0-9), minus
   sign (-), and period (.).  Note that periods are only allowed when
   they serve to delimit components of "domain style names". (See
   RFC-921, "Domain Name System Implementation Schedule", for
   background).  No blank or space characters are permitted as part of a
   name. No distinction is made between upper and lower case.  The first
   character must be an alpha character.  The last character must not be
   a minus sign or period.

   RFC 1123:
   The syntax of a legal Internet host name was specified in RFC-952
   [DNS:4].  One aspect of host name syntax is hereby changed: the
   restriction on the first character is relaxed to allow either a
   letter or a digit.  Host software MUST support this more liberal
   syntax.

   Host software MUST handle host names of up to 63 characters and
   SHOULD handle host names of up to 255 characters.

   RFC 1034:
   Relative names are either taken relative to a well known origin, or to a
   list of domains used as a search list.  Relative names appear mostly at
   the user interface, where their interpretation varies from
   implementation to implementation, and in master files, where they are
   relative to a single origin domain name.  The most common interpretation
   uses the root "." as either the single origin or as one of the members
   of the search list, so a multi-label relative name is often one where
   the trailing dot has been omitted to save typing.

   Since a complete domain name ends with the root label, this leads to
   a printed form which ends in a dot.
  */

  const hostPattern =
    /^(([a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])\.?$/i;
  return aHostName.length <= 255 && hostPattern.test(aHostName)
    ? aHostName
    : null;
}

/**
 * Check if aHostName is a valid IPv4 address.
 *
 * @param {string} aHostName - The string to check for validity.
 * @param {boolean} aAllowExtendedIPFormats - If false, only IPv4 addresses
 *   in the common decimal format (4 components, each up to 255)
 *   will be accepted, no hex/octal formats.
 * @returns {string} Unobscured canonicalized address if aHostName is an
 *   IPv4 address. Returns null if it's not.
 */
function isLegalIPv4Address(aHostName, aAllowExtendedIPFormats) {
  // Scammers frequently obscure the IP address by encoding each component as
  // decimal, octal, hex or in some cases a mix match of each. There can even
  // be less than 4 components where the last number covers the missing components.
  // See the test at mailnews/base/test/unit/test_hostnameUtils.js for possible
  // combinations.

  if (!aHostName) {
    return null;
  }

  // Break the IP address down into individual components.
  let ipComponents = aHostName.split(".");
  let componentCount = ipComponents.length;
  if (componentCount > 4 || (componentCount < 4 && !aAllowExtendedIPFormats)) {
    return null;
  }

  /**
   * Checks validity of an IP address component.
   *
   * @param {string} aValue - The component string.
   * @param {integer} aWidth - How many components does this string cover.
   * @returns {integer|null} The value of the component in decimal if it is valid.
   *   Returns null if it's not.
   */
  const kPowersOf256 = [1, 256, 65536, 16777216, 4294967296];
  function isLegalIPv4Component(aValue, aWidth) {
    let component;
    // Is the component decimal?
    if (/^(0|([1-9][0-9]{0,9}))$/.test(aValue)) {
      component = parseInt(aValue, 10);
    } else if (aAllowExtendedIPFormats) {
      // Is the component octal?
      if (/^(0[0-7]{1,12})$/.test(aValue)) {
        component = parseInt(aValue, 8);
      } else if (/^(0x[0-9a-f]{1,8})$/i.test(aValue)) {
        // The component is hex.
        component = parseInt(aValue, 16);
      } else {
        return null;
      }
    } else {
      return null;
    }

    // Make sure the component in not larger than the expected maximum.
    if (component >= kPowersOf256[aWidth]) {
      return null;
    }

    return component;
  }

  for (let i = 0; i < componentCount; i++) {
    // If we are on the last supplied component but we do not have 4,
    // the last one covers the remaining ones.
    let componentWidth = i == componentCount - 1 ? 4 - i : 1;
    let componentValue = isLegalIPv4Component(ipComponents[i], componentWidth);
    if (componentValue == null) {
      return null;
    }

    // If we have a component spanning multiple ones, split it.
    for (let j = 0; j < componentWidth; j++) {
      ipComponents[i + j] =
        (componentValue >> ((componentWidth - 1 - j) * 8)) & 255;
    }
  }

  // First component of zero is not valid.
  if (ipComponents[0] == 0) {
    return null;
  }

  return ipComponents.join(".");
}

/**
 * Check if aHostName is a valid IPv6 address.
 *
 * @param {string} aHostName - The string to check for validity.
 * @returns {string} Unobscured canonicalized address if aHostName is an
 *   IPv6 address. Returns null if it's not.
 */
function isLegalIPv6Address(aHostName) {
  if (!aHostName) {
    return null;
  }

  // Break the IP address down into individual components.
  let ipComponents = aHostName.toLowerCase().split(":");

  // Make sure there are at least 3 components.
  if (ipComponents.length < 3) {
    return null;
  }

  let ipLength = ipComponents.length - 1;

  // Take care if the last part is written in decimal using dots as separators.
  let lastPart = isLegalIPv4Address(ipComponents[ipLength], false);
  if (lastPart) {
    let lastPartComponents = lastPart.split(".");
    // Convert it into standard IPv6 components.
    ipComponents[ipLength] = (
      (lastPartComponents[0] << 8) |
      lastPartComponents[1]
    ).toString(16);
    ipComponents[ipLength + 1] = (
      (lastPartComponents[2] << 8) |
      lastPartComponents[3]
    ).toString(16);
  }

  // Make sure that there is only one empty component.
  let emptyIndex;
  for (let i = 1; i < ipComponents.length - 1; i++) {
    if (ipComponents[i] == "") {
      // If we already found an empty component return null.
      if (emptyIndex) {
        return null;
      }

      emptyIndex = i;
    }
  }

  // If we found an empty component, extend it.
  if (emptyIndex) {
    ipComponents[emptyIndex] = 0;

    // Add components so we have a total of 8.
    for (let count = ipComponents.length; count < 8; count++) {
      ipComponents.splice(emptyIndex, 0, 0);
    }
  }

  // Make sure there are 8 components.
  if (ipComponents.length != 8) {
    return null;
  }

  // Format all components to 4 character hex value.
  for (let i = 0; i < ipComponents.length; i++) {
    if (ipComponents[i] == "") {
      ipComponents[i] = 0;
    }

    // Make sure the component is a number and it isn't larger than 0xffff.
    if (/^[0-9a-f]{1,4}$/.test(ipComponents[i])) {
      ipComponents[i] = parseInt(ipComponents[i], 16);
      if (isNaN(ipComponents[i]) || ipComponents[i] > 0xffff) {
        return null;
      }
    } else {
      return null;
    }

    // Pad the component with 0:s.
    ipComponents[i] = ("0000" + ipComponents[i].toString(16)).substr(-4);
  }

  // TODO: support Zone indices in Link-local addresses? Currently they are rejected.
  // http://en.wikipedia.org/wiki/IPv6_address#Link-local_addresses_and_zone_indices

  let hostName = ipComponents.join(":");
  // Treat 0000:0000:0000:0000:0000:0000:0000:0000 as an invalid IPv6 address.
  return hostName != "0000:0000:0000:0000:0000:0000:0000:0000"
    ? hostName
    : null;
}

/**
 * Check if aHostName is a valid IP address (IPv4 or IPv6).
 *
 * @param {string} aHostName - The string to check for validity.
 * @param {boolean} aAllowExtendedIPFormats - Allow hex/octal formats in
 *   addition to decimal.
 * @returns {?string} Unobscured canonicalized IPv4 or IPv6 address if it is
 *   valid, otherwise null.
 */
function isLegalIPAddress(aHostName, aAllowExtendedIPFormats) {
  return (
    isLegalIPv4Address(aHostName, aAllowExtendedIPFormats) ||
    isLegalIPv6Address(aHostName)
  );
}

/**
 * Check if aIPAddress is a local or private IP address.
 * Note: if the passed in address is not in canonical (unobscured form),
 *       the result may be wrong.
 *
 * @param {string} aIPAddress - A valid IP address literal in canonical
 *   (unobscured) form.
 * @returns {boolean} frue if it is a local/private IPv4 or IPv6 address.
 */
function isLegalLocalIPAddress(aIPAddress) {
  // IPv4 address?
  let ipComponents = aIPAddress.split(".");
  if (ipComponents.length == 4) {
    // Check if it's a local or private IPv4 address.
    return (
      ipComponents[0] == 10 ||
      ipComponents[0] == 127 || // loopback address
      (ipComponents[0] == 192 && ipComponents[1] == 168) ||
      (ipComponents[0] == 169 && ipComponents[1] == 254) ||
      (ipComponents[0] == 172 && ipComponents[1] >= 16 && ipComponents[1] < 32)
    );
  }

  // IPv6 address?
  ipComponents = aIPAddress.split(":");
  if (ipComponents.length == 8) {
    // ::1/128 - localhost
    if (
      ipComponents[0] == "0000" &&
      ipComponents[1] == "0000" &&
      ipComponents[2] == "0000" &&
      ipComponents[3] == "0000" &&
      ipComponents[4] == "0000" &&
      ipComponents[5] == "0000" &&
      ipComponents[6] == "0000" &&
      ipComponents[7] == "0001"
    ) {
      return true;
    }

    // fe80::/10 - link local addresses
    if (ipComponents[0] == "fe80") {
      return true;
    }

    // fc00::/7 - unique local addresses
    if (
      ipComponents[0].startsWith("fc") || // usage has not been defined yet
      ipComponents[0].startsWith("fd")
    ) {
      return true;
    }

    return false;
  }

  return false;
}

/**
 * Clean up the hostname or IP. Usually used to sanitize a value input by the user.
 * It is usually applied before we know if the hostname is even valid.
 *
 * @param {string} aHostName - The hostname or IP string to clean up.
 */
function cleanUpHostName(aHostName) {
  // TODO: Bug 235312: if UTF8 string was input, convert to punycode using convertUTF8toACE()
  // but bug 563172 needs resolving first.
  return aHostName.trim();
}