Technology Consultant
RSS icon Email icon Home icon
  • IPToCountry.NET

    This is my own implementation of an IP To Country static lookup class in VB.NET. It can be used to look up the country code and also full name of an IP address. I am sharing it in the spirit that the underlying data was shared: receive free, give free.

    This all started when I found a downloadable CSV file of IP address ranges to country codes. It is cool that such a thing is available for free, and the file maintainers update that thing daily. But instead of hitting a web service or loading it into a database, querying that 5 MB of data in memory is even better. Plus, this was a fun opportunity to repurpose Array.BinarySearch for something it was not intended, namely matching the Int64 IP address to a private array of Range structures. This also required converting IP address strings (255.255.255.255) into 64-bit integers. I didn’t see anything VB.NET-ish enough for my tastes, so I whipped up a function that uses hex strings. Some hardcore programmer out there can directly convert a four byte array to an Int64 without the hex hack, I’m sure. (Update: I found a better solution here.)
    This class is optimized to run inside of an ASP.NET website, but aside from using Server.MapPath to find the CSV file, it has no dependencies on System.Web. It’s static so that there will be only one copy of the object in memory. You use it by calling two static functions: IPToCountry.GetCountryCode(IP) returns a country code, and you can also look up the full name by calling IPToCountry.GetCountryName(Code).

    I realize that the code formatting is messed up (thanks Wordpress.com) but just create a new class called IPToCountry in your project and copy+paste from here on down; it should work. YMMV Hooray Wordpress!

    Public Class IPToCountry
    Private Shared _Ranges() As Range
    Private Shared _Countries As Hashtable</code>
    
    'The Range structure represents a single line in the IP range file. Structures are less heavy than objects.
    Private Structure Range
    Public FromIP As Long
    Public ToIP As Long
    Public Country As String
    
    'Constructor using the file line, already split
    Public Sub New(ByVal Fields() As String)
    If Fields.Length > 4 Then
    FromIP = CLng(Fields(0))
    ToIP = CLng(Fields(1))
    Country = Fields(4)
    End If
    End Sub
    
    'When loading the file, many of the lines overlap each other. This function is used to avoid loading the extra line
    Public Function Overlaps(ByVal Other As Range) As Boolean
    Return Me.Country = Other.Country AndAlso Me.FromIP = Other.ToIP + 1
    End Function
    
    Public Function Matches(ByVal IP As Long) As Integer
    'Return 1 if the IP is too low, -1 if it's too high
    'I would have thought it was the other way around, but no
    If IP Then
    Return 1
    ElseIf IP > ToIP Then
    Return -1
    Else
    Return 0    'the IP falls within the range
    End If
    End Function
    
    Public Overrides Function ToString() As String
    Return Country & ": " & IPToCountry.IPLongToString(FromIP) & " - " & IPToCountry.IPLongToString(ToIP)
    End Function
    End Structure
    
    'RangeFinder is used by Array.BinarySearch to see if an IP falls within a range
    Private Class RangeFinder
    Implements IComparer
    
    Public Function Compare(ByVal x As Object, ByVal y As Object) As Integer Implements System.Collections.IComparer.Compare
    Dim TheRange As Range = x
    Dim TheIP As Long = y
    
    Return TheRange.Matches(TheIP)
    End Function
    End Class
    
    'RangeSorter is used to sort ranges by FromIP
    Private Class RangeSorter
    Implements IComparer
    
    Public Function Compare(ByVal x As Object, ByVal y As Object) As Integer Implements System.Collections.IComparer.Compare
    Dim RangeX As Range = CType(x, Range)
    Dim RangeY As Range = CType(y, Range)
    
    Return RangeX.FromIP.CompareTo(RangeY.FromIP)
    End Function
    End Class
    
    'Return the full path to the CSV file
    Private Shared Function GetFilePath() As String
    Dim ctx As HttpContext = HttpContext.Current
    Dim Path As String = ctx.Server.MapPath("~/IpToCountry.csv")   'change this to the location of your CSV file
    Return Path
    End Function
    
    'Set up the object for subsequent use; load the array list from the file
    Shared Sub New()
    Dim StartTime As Date = Now     'for debugging
    Dim FilePath As String = GetFilePath()
    Dim RangeList As New ArrayList
    Dim NeedsSort As Boolean = False
    Dim MaxFromRange As Long
    
    _Countries = New Hashtable
    
    If IO.File.Exists(FilePath) Then
    Dim Reader As IO.StreamReader = IO.File.OpenText(FilePath)
    Dim Line As String = Reader.ReadLine()
    Dim Fields() As String
    Dim LastRange As Range
    
    Do While Not Line Is Nothing    'read the file lines one at a time
    'skip lines that start with "#" or blank lines
    If Not Line.StartsWith("#") AndAlso Line.Length > 0 Then
    Fields = Line.Replace("""", "").Split(",")
    
    Dim NewRange As New Range(Fields)
    
    'Check to see if a sort will be needed at the end
    If NeedsSort OrElse (MaxFromRange > 0 AndAlso MaxFromRange < NewRange.FromIP) Then
    NeedsSort = True
    Else
    MaxFromRange = NewRange.FromIP
    End If
    
    'To avoid creating more array elements than necessary, merge adjacent IP ranges here
    If NewRange.Overlaps(LastRange) AndAlso RangeList.Count > 0 Then
    LastRange.ToIP = NewRange.ToIP
    RangeList.Item(RangeList.Count - 1) = LastRange     'must reassign because structs don't do by ref
    Else
    RangeList.Add(NewRange)
    LastRange = NewRange
    'add the full country name to the hashtable
    If Not _Countries.Contains(NewRange.Country) Then
    _Countries.Add(NewRange.Country, Fields(Fields.Length - 1))
    End If
    End If
    End If
    Line = Reader.ReadLine()
    Loop
    Reader.Close()
    ReDim _Ranges(RangeList.Count - 1)
    RangeList.CopyTo(_Ranges)
    
    If NeedsSort Then
    Array.Sort(_Ranges, New RangeSorter)
    End If
    End If
    
    'debugging output here
    Diagnostics.Debug.WriteLine("Loaded IpToCountry.csv in " & Now.Subtract(StartTime).TotalMilliseconds & " ms")
    Diagnostics.Debug.WriteLine(RangeList.Count & " ranges, " & _Countries.Count & " countries; Max IP: " & MaxFromRange.ToString)
    End Sub
    
    Public Shared Function GetCountryCode(ByVal IP As String) As String
    Return GetCountryCode(IPStringToLong(IP))
    End Function
    
    Public Shared Function GetCountryCode(ByVal IP As Long) As String
    Dim Index As Integer = Array.BinarySearch(_Ranges, IP, New RangeFinder)
    If Index > -1 Then
    Return _Ranges(Index).Country
    End If
    End Function
    
    Public Shared Function GetCountryName(ByVal Code As String) As String
    Return _Countries.Item(Code)
    End Function
    
    'Converts an IP string in x.x.x.x format to the numeric representation (Int64)
    Public Shared Function IPStringToLong(ByVal IPString As String) As Long
    Dim s() As String = IPString.Split(".")
    If s.Length = 4 Then
    Try
    s(0) = Hex(CByte(s(0)))
    s(1) = Hex(CByte(s(1)))
    s(2) = Hex(CByte(s(2)))
    s(3) = Hex(CByte(s(3)))
    
    Return Long.Parse(String.Join("", s), Globalization.NumberStyles.AllowHexSpecifier)
    Catch ex As Exception
    Return -1
    End Try
    End If
    End Function
    
    'Converts a numeric IP address into the x.x.x.x format for display
    Public Shared Function IPLongToString(ByVal IPLong As Long) As String
    If IPLong > 16777215 AndAlso IPLong < 4294967295 Then   'valid IP addresses between 1.0.0.0 and 255.255.255.255
    Dim IPHex As String = Hex(IPLong)
    If IPHex.Length < 8 Then IPHex = IPHex.PadLeft(8, "0")
    
    Dim b(3) As Byte
    b(0) = Byte.Parse(IPHex.Substring(0, 2), Globalization.NumberStyles.AllowHexSpecifier)
    b(1) = Byte.Parse(IPHex.Substring(2, 2), Globalization.NumberStyles.AllowHexSpecifier)
    b(2) = Byte.Parse(IPHex.Substring(4, 2), Globalization.NumberStyles.AllowHexSpecifier)
    b(3) = Byte.Parse(IPHex.Substring(6, 2), Globalization.NumberStyles.AllowHexSpecifier)
    
    Return b(0) & "." & b(1) & "." & b(2) & "." & b(3)
    Else
    Return "0.0.0.0"
    End If
    End Function
    End Class