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 &amp; ": " &amp; IPToCountry.IPLongToString(FromIP) &amp; " - " &amp; 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 " &amp; Now.Subtract(StartTime).TotalMilliseconds &amp; " ms")
Diagnostics.Debug.WriteLine(RangeList.Count &amp; " ranges, " &amp; _Countries.Count &amp; " countries; Max IP: " &amp; 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 &lt; 4294967295 Then   'valid IP addresses between 1.0.0.0 and 255.255.255.255
Dim IPHex As String = Hex(IPLong)
If IPHex.Length &lt; 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) &amp; "." &amp; b(1) &amp; "." &amp; b(2) &amp; "." &amp; b(3)
Else
Return "0.0.0.0"
End If
End Function
End Class

Responses

Hi

I’m a relative newbie in .NET so pardon my stupidity.

I’m trying out your code and I’m getting an error on the following line (***) in this 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

It seems that _Ranges is nothing.

Here is what I’ve done:
—————————–
Dim cc As New IPToCountry
‘Dim ccode As String = cc.GetCountryCode(”216.104.194.173″ ;)
——————————

Here is the error message:
———————————
Value cannot be null. Parameter name: arrayHandleError at System.Array.BinarySearch(Array array, Object value, IComparer comparer) at magrosa.IPToCountry.GetCountryCode(Int64 IP) in c:\inetpub\wwwroot\magrosa\IpToCountry.vb:line 138 at magrosa.DBManager.PumpDbs() in c:\inetpub\wwwroot\magrosa\DBManager.vb:line 69 at magrosa.WebForm1.Page_Load(Object sender, EventArgs e) in c:\inetpub\wwwroot\magrosa\index.aspx.vb:line 61 at System.Web.UI.Control.OnLoad(EventArgs e) at System.Web.UI.Control.LoadRecursive() at System.Web.UI.Page.ProcessRequestMain()
———————–

Can you help. I would like to use this functionality

Regards

George

Leave a response

Your response: