ipaddr.coffee 14.1 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 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 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460
# Define the main object
ipaddr = {}

root = this

# Export for both the CommonJS and browser-like environment
if module? && module.exports
  module.exports = ipaddr
else
  root['ipaddr'] = ipaddr

# A generic CIDR (Classless Inter-Domain Routing) RFC1518 range matcher.
matchCIDR = (first, second, partSize, cidrBits) ->
  if first.length != second.length
    throw new Error "ipaddr: cannot match CIDR for objects with different lengths"

  part = 0
  while cidrBits > 0
    shift = partSize - cidrBits
    shift = 0 if shift < 0

    if first[part] >> shift != second[part] >> shift
      return false

    cidrBits -= partSize
    part     += 1

  return true

# An utility function to ease named range matching. See examples below.
ipaddr.subnetMatch = (address, rangeList, defaultName='unicast') ->
  for rangeName, rangeSubnets of rangeList
    # ECMA5 Array.isArray isn't available everywhere
    if rangeSubnets[0] && !(rangeSubnets[0] instanceof Array)
      rangeSubnets = [ rangeSubnets ]

    for subnet in rangeSubnets
      return rangeName if address.match.apply(address, subnet)

  return defaultName

# An IPv4 address (RFC791).
class ipaddr.IPv4
  # Constructs a new IPv4 address from an array of four octets
  # in network order (MSB first)
  # Verifies the input.
  constructor: (octets) ->
    if octets.length != 4
      throw new Error "ipaddr: ipv4 octet count should be 4"

    for octet in octets
      if !(0 <= octet <= 255)
        throw new Error "ipaddr: ipv4 octet should fit in 8 bits"

    @octets = octets

  # The 'kind' method exists on both IPv4 and IPv6 classes.
  kind: ->
    return 'ipv4'

  # Returns the address in convenient, decimal-dotted format.
  toString: ->
    return @octets.join "."

  # Returns an array of byte-sized values in network order (MSB first)
  toByteArray: ->
    return @octets.slice(0) # octets.clone

  # Checks if this address matches other one within given CIDR range.
  match: (other, cidrRange) ->
    if cidrRange == undefined
      [other, cidrRange] = other

    if other.kind() != 'ipv4'
      throw new Error "ipaddr: cannot match ipv4 address with non-ipv4 one"

    return matchCIDR(this.octets, other.octets, 8, cidrRange)

  # Special IPv4 address ranges.
  # See also https://en.wikipedia.org/wiki/Reserved_IP_addresses
  SpecialRanges:
    unspecified: [
      [ new IPv4([0,     0,    0,   0]),  8 ]
    ]
    broadcast: [
      [ new IPv4([255, 255,  255, 255]), 32 ]
    ]
    multicast: [ # RFC3171
      [ new IPv4([224,   0,    0,   0]), 4  ]
    ]
    linkLocal: [ # RFC3927
      [ new IPv4([169,   254,  0,   0]), 16 ]
    ]
    loopback: [ # RFC5735
      [ new IPv4([127,   0,    0,   0]), 8  ]
    ]
    carrierGradeNat: [ # RFC6598
      [ new IPv4([100,   64,   0,   0]), 10 ]
    ]
    private: [ # RFC1918
      [ new IPv4([10,    0,    0,   0]), 8  ]
      [ new IPv4([172,   16,   0,   0]), 12 ]
      [ new IPv4([192,   168,  0,   0]), 16 ]
    ]
    reserved: [ # Reserved and testing-only ranges; RFCs 5735, 5737, 2544, 1700
      [ new IPv4([192,   0,    0,   0]), 24 ]
      [ new IPv4([192,   0,    2,   0]), 24 ]
      [ new IPv4([192,  88,   99,   0]), 24 ]
      [ new IPv4([198,  51,  100,   0]), 24 ]
      [ new IPv4([203,   0,  113,   0]), 24 ]
      [ new IPv4([240,   0,    0,   0]), 4  ]
    ]

  # Checks if the address corresponds to one of the special ranges.
  range: ->
    return ipaddr.subnetMatch(this, @SpecialRanges)

  # Convrets this IPv4 address to an IPv4-mapped IPv6 address.
  toIPv4MappedAddress: ->
    return ipaddr.IPv6.parse "::ffff:#{@toString()}"

  # returns a number of leading ones in IPv4 address, making sure that
  # the rest is a solid sequence of 0's (valid netmask)
  # returns either the CIDR length or null if mask is not valid
  prefixLengthFromSubnetMask: ->
    # number of zeroes in octet
    zerotable =
      0:   8
      128: 7
      192: 6
      224: 5
      240: 4
      248: 3
      252: 2
      254: 1
      255: 0

    cidr = 0
    # non-zero encountered stop scanning for zeroes
    stop = false
    for i in [3..0] by -1
      octet = @octets[i]
      if octet of zerotable
        zeros = zerotable[octet]
        if stop and zeros != 0
          return null
        unless zeros == 8
          stop = true
        cidr += zeros
      else
        return null
    return 32 - cidr

# A list of regular expressions that match arbitrary IPv4 addresses,
# for which a number of weird notations exist.
# Note that an address like 0010.0xa5.1.1 is considered legal.
ipv4Part = "(0?\\d+|0x[a-f0-9]+)"
ipv4Regexes =
  fourOctet: new RegExp "^#{ipv4Part}\\.#{ipv4Part}\\.#{ipv4Part}\\.#{ipv4Part}$", 'i'
  longValue: new RegExp "^#{ipv4Part}$", 'i'

# Classful variants (like a.b, where a is an octet, and b is a 24-bit
# value representing last three octets; this corresponds to a class C
# address) are omitted due to classless nature of modern Internet.
ipaddr.IPv4.parser = (string) ->
  parseIntAuto = (string) ->
    if string[0] == "0" && string[1] != "x"
      parseInt(string, 8)
    else
      parseInt(string)

  # parseInt recognizes all that octal & hexadecimal weirdness for us
  if match = string.match(ipv4Regexes.fourOctet)
    return (parseIntAuto(part) for part in match[1..5])
  else if match = string.match(ipv4Regexes.longValue)
    value = parseIntAuto(match[1])
    if value > 0xffffffff || value < 0
      throw new Error "ipaddr: address outside defined range"
    return ((value >> shift) & 0xff for shift in [0..24] by 8).reverse()
  else
    return null

# An IPv6 address (RFC2460)
class ipaddr.IPv6
  # Constructs an IPv6 address from an array of eight 16-bit parts
  # or sixteen 8-bit parts in network order (MSB first).
  # Throws an error if the input is invalid.
  constructor: (parts) ->
    if parts.length == 16
      @parts = []
      for i in [0..14] by 2
        @parts.push((parts[i] << 8) | parts[i + 1])
    else if parts.length == 8
      @parts = parts
    else
      throw new Error "ipaddr: ipv6 part count should be 8 or 16"

    for part in @parts
      if !(0 <= part <= 0xffff)
        throw new Error "ipaddr: ipv6 part should fit in 16 bits"

  # The 'kind' method exists on both IPv4 and IPv6 classes.
  kind: ->
    return 'ipv6'

  # Returns the address in compact, human-readable format like
  # 2001:db8:8:66::1
  toString: ->
    stringParts = (part.toString(16) for part in @parts)

    compactStringParts = []
    pushPart = (part) -> compactStringParts.push part

    state = 0
    for part in stringParts
      switch state
        when 0
          if part == '0'
            pushPart('')
          else
            pushPart(part)

          state = 1
        when 1
          if part == '0'
            state = 2
          else
            pushPart(part)
        when 2
          unless part == '0'
            pushPart('')
            pushPart(part)
            state = 3
        when 3
          pushPart(part)

    if state == 2
      pushPart('')
      pushPart('')

    return compactStringParts.join ":"

  # Returns an array of byte-sized values in network order (MSB first)
  toByteArray: ->
    bytes = []
    for part in @parts
      bytes.push(part >> 8)
      bytes.push(part & 0xff)

    return bytes

  # Returns the address in expanded format with all zeroes included, like
  # 2001:db8:8:66:0:0:0:1
  toNormalizedString: ->
    return (part.toString(16) for part in @parts).join ":"

  # Checks if this address matches other one within given CIDR range.
  match: (other, cidrRange) ->
    if cidrRange == undefined
      [other, cidrRange] = other

    if other.kind() != 'ipv6'
      throw new Error "ipaddr: cannot match ipv6 address with non-ipv6 one"

    return matchCIDR(this.parts, other.parts, 16, cidrRange)

  # Special IPv6 ranges
  SpecialRanges:
    unspecified: [ new IPv6([0,      0,      0, 0, 0,      0,      0, 0]), 128 ] # RFC4291, here and after
    linkLocal:   [ new IPv6([0xfe80, 0,      0, 0, 0,      0,      0, 0]), 10  ]
    multicast:   [ new IPv6([0xff00, 0,      0, 0, 0,      0,      0, 0]), 8   ]
    loopback:    [ new IPv6([0,      0,      0, 0, 0,      0,      0, 1]), 128 ]
    uniqueLocal: [ new IPv6([0xfc00, 0,      0, 0, 0,      0,      0, 0]), 7   ]
    ipv4Mapped:  [ new IPv6([0,      0,      0, 0, 0,      0xffff, 0, 0]), 96  ]
    rfc6145:     [ new IPv6([0,      0,      0, 0, 0xffff, 0,      0, 0]), 96  ] # RFC6145
    rfc6052:     [ new IPv6([0x64,   0xff9b, 0, 0, 0,      0,      0, 0]), 96  ] # RFC6052
    '6to4':      [ new IPv6([0x2002, 0,      0, 0, 0,      0,      0, 0]), 16  ] # RFC3056
    teredo:      [ new IPv6([0x2001, 0,      0, 0, 0,      0,      0, 0]), 32  ] # RFC6052, RFC6146
    reserved: [
      [ new IPv6([ 0x2001, 0xdb8, 0, 0, 0, 0, 0, 0]), 32 ] # RFC4291
    ]

  # Checks if the address corresponds to one of the special ranges.
  range: ->
    return ipaddr.subnetMatch(this, @SpecialRanges)

  # Checks if this address is an IPv4-mapped IPv6 address.
  isIPv4MappedAddress: ->
    return @range() == 'ipv4Mapped'

  # Converts this address to IPv4 address if it is an IPv4-mapped IPv6 address.
  # Throws an error otherwise.
  toIPv4Address: ->
    unless @isIPv4MappedAddress()
      throw new Error "ipaddr: trying to convert a generic ipv6 address to ipv4"

    [high, low] = @parts[-2..-1]

    return new ipaddr.IPv4([high >> 8, high & 0xff, low >> 8, low & 0xff])

# IPv6-matching regular expressions.
# For IPv6, the task is simpler: it is enough to match the colon-delimited
# hexadecimal IPv6 and a transitional variant with dotted-decimal IPv4 at
# the end.
ipv6Part = "(?:[0-9a-f]+::?)+"
ipv6Regexes =
  native:       new RegExp "^(::)?(#{ipv6Part})?([0-9a-f]+)?(::)?$", 'i'
  transitional: new RegExp "^((?:#{ipv6Part})|(?:::)(?:#{ipv6Part})?)" +
                           "#{ipv4Part}\\.#{ipv4Part}\\.#{ipv4Part}\\.#{ipv4Part}$", 'i'

# Expand :: in an IPv6 address or address part consisting of `parts` groups.
expandIPv6 = (string, parts) ->
  # More than one '::' means invalid adddress
  if string.indexOf('::') != string.lastIndexOf('::')
    return null

  # How many parts do we already have?
  colonCount = 0
  lastColon = -1
  while (lastColon = string.indexOf(':', lastColon + 1)) >= 0
    colonCount++

  # 0::0 is two parts more than ::
  colonCount-- if string.substr(0, 2) == '::'
  colonCount-- if string.substr(-2, 2) == '::'

  # The following loop would hang if colonCount > parts
  if colonCount > parts
    return null

  # replacement = ':' + '0:' * (parts - colonCount)
  replacementCount = parts - colonCount
  replacement = ':'
  while replacementCount--
    replacement += '0:'

  # Insert the missing zeroes
  string = string.replace('::', replacement)

  # Trim any garbage which may be hanging around if :: was at the edge in
  # the source string
  string = string[1..-1] if string[0] == ':'
  string = string[0..-2] if string[string.length-1] == ':'

  return (parseInt(part, 16) for part in string.split(":"))

# Parse an IPv6 address.
ipaddr.IPv6.parser = (string) ->
  if string.match(ipv6Regexes['native'])
    return expandIPv6(string, 8)

  else if match = string.match(ipv6Regexes['transitional'])
    parts = expandIPv6(match[1][0..-2], 6)
    if parts
      octets = [parseInt(match[2]), parseInt(match[3]),
                parseInt(match[4]), parseInt(match[5])]
      for octet in octets
        if !(0 <= octet <= 255)
          return null

      parts.push(octets[0] << 8 | octets[1])
      parts.push(octets[2] << 8 | octets[3])
      return parts

  return null

# Checks if a given string is formatted like IPv4/IPv6 address.
ipaddr.IPv4.isIPv4 = ipaddr.IPv6.isIPv6 = (string) ->
  return @parser(string) != null

# Checks if a given string is a valid IPv4/IPv6 address.
ipaddr.IPv4.isValid = (string) ->
  try
    new this(@parser(string))
    return true
  catch e
    return false

ipaddr.IPv4.isValidFourPartDecimal = (string) ->
  if ipaddr.IPv4.isValid(string) and string.match(/^\d+(\.\d+){3}$/)
    return true
  else
    return false

ipaddr.IPv6.isValid = (string) ->
  # Since IPv6.isValid is always called first, this shortcut
  # provides a substantial performance gain.
  if typeof string == "string" and string.indexOf(":") == -1
    return false

  try
    new this(@parser(string))
    return true
  catch e
    return false

# Tries to parse and validate a string with IPv4/IPv6 address.
# Throws an error if it fails.
ipaddr.IPv4.parse = ipaddr.IPv6.parse = (string) ->
  parts = @parser(string)
  if parts == null
    throw new Error "ipaddr: string is not formatted like ip address"

  return new this(parts)

ipaddr.IPv4.parseCIDR = (string) ->
  if match = string.match(/^(.+)\/(\d+)$/)
    maskLength = parseInt(match[2])
    if maskLength >= 0 and maskLength <= 32
      return [@parse(match[1]), maskLength]

  throw new Error "ipaddr: string is not formatted like an IPv4 CIDR range"

ipaddr.IPv6.parseCIDR = (string) ->
  if match = string.match(/^(.+)\/(\d+)$/)
    maskLength = parseInt(match[2])
    if maskLength >= 0 and maskLength <= 128
      return [@parse(match[1]), maskLength]

  throw new Error "ipaddr: string is not formatted like an IPv6 CIDR range"

# Checks if the address is valid IP address
ipaddr.isValid = (string) ->
  return ipaddr.IPv6.isValid(string) || ipaddr.IPv4.isValid(string)

# Try to parse an address and throw an error if it is impossible
ipaddr.parse = (string) ->
  if ipaddr.IPv6.isValid(string)
    return ipaddr.IPv6.parse(string)
  else if ipaddr.IPv4.isValid(string)
    return ipaddr.IPv4.parse(string)
  else
    throw new Error "ipaddr: the address has neither IPv6 nor IPv4 format"

ipaddr.parseCIDR = (string) ->
  try
    return ipaddr.IPv6.parseCIDR(string)
  catch e
    try
      return ipaddr.IPv4.parseCIDR(string)
    catch e
      throw new Error "ipaddr: the address has neither IPv6 nor IPv4 CIDR format"

# Try to parse an array in network order (MSB first) for IPv4 and IPv6
ipaddr.fromByteArray = (bytes) ->
  length = bytes.length
  if length == 4
    return new ipaddr.IPv4(bytes)
  else if length == 16
    return new ipaddr.IPv6(bytes)
  else
    throw new Error "ipaddr: the binary input is neither an IPv6 nor IPv4 address"

# Parse an address and return plain IPv4 address if it is an IPv4-mapped address
ipaddr.process = (string) ->
  addr = @parse(string)
  if addr.kind() == 'ipv6' && addr.isIPv4MappedAddress()
    return addr.toIPv4Address()
  else
    return addr