Skip to content

Commit

Permalink
release
Browse files Browse the repository at this point in the history
  • Loading branch information
delef committed May 14, 2018
1 parent 8c3eea7 commit 29a7ddb
Show file tree
Hide file tree
Showing 14 changed files with 676 additions and 0 deletions.
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/doc/
/lib/
/bin/
/.shards/
/spec/cache/*
!/spec/cache/.keep

# Libraries don't need dependency lock
# Dependencies will be locked in application that uses them
/shard.lock
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
language: crystal
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# MaxMindDB.cr

Pure Crystal [MaxMind DB](http://maxmind.github.io/MaxMind-DB/) reader, including the [GeoIP2](http://dev.maxmind.com/geoip/geoip2/downloadable/), which doesn't require [libmaxminddb](https://github.com/maxmind/libmaxminddb).

## Installation

Add this to your application's `shard.yml`:

```yaml
dependencies:
maxminddb:
github: delef/maxminddb.cr
```
## Usage
```crystal
require "maxminddb"

mmdb = MaxMindDB.new("#{__DIR__}/../data/GeoLite2-Country.mmdb")
result = mmdb.lookup("1.1.1.1")

result["city"]["geoname_id"].as_i # => 2151718
result["city"]["names"]["en"].as_s # => "Research"

result["continent"]["code"].as_s # => "OC"
result["continent"]["geoname_id"].as_i # => 6255151
result["continent"]["names"]["en"].as_s # => "Oceania"

result["country"]["iso_code"].as_s # => "AU"
result["country"]["geoname_id"].as_i # => 2077456
result["country"]["names"]["en"].as_s # => "Australia"

result["location"]["accuracy_radius"].as_i # => 1000
result["location"]["latitude"].as_f # => -37.7
result["location"]["longitude"].as_f # => 145.1833
result["location"]["time_zone"].as_s # => "Australia/Melbourne"

result["postal"]["code"].as_s # => "3095"

result["registered_country"]["iso_code"].as_s # => "AU"
result["registered_country"]["geoname_id"].as_i # => 2077456
result["registered_country"]["names"]["en"].as_s # => "Australia"
```

## Contributing

1. Fork it ( https://github.com/delef/geoip2.cr/fork )
2. Create your feature branch (git checkout -b my-new-feature)
3. Commit your changes (git commit -am 'Add some feature')
4. Push to the branch (git push origin my-new-feature)
5. Create a new Pull Request

## Contributors

- [delef](https://github.com/delef) - creator, maintainer
11 changes: 11 additions & 0 deletions shard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: maxminddb
version: 0.4.1

dependencies:
ipaddress:
github: sija/ipaddress.cr

authors:
- delef <delef@ya.ru>

license: MIT
Empty file added spec/cache/.keep
Empty file.
106 changes: 106 additions & 0 deletions spec/maxminddb_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
require "./spec_helper"

describe MaxMindDB do
city_db = MaxMindDB.new("spec/cache/GeoLite2-City.mmdb")
country_db = MaxMindDB.new("spec/cache/GeoLite2-Country.mmdb")

context "for the ip 77.88.55.88 (IPv4)" do
ip = "74.125.225.224"

it "returns a MaxMindDB::Any" do
city_db.lookup(ip).should be_a(MaxMindDB::Any)
end

it "found?" do
city_db.lookup(ip).found?.should be_true
end

it "returns Mountain View as the English name" do
city_db.lookup(ip)["city"]["names"]["en"].as_s.should eq("Alameda")
end

it "returns -122.0574 as the longitude" do
city_db.lookup(ip)["location"]["longitude"].as_f.should eq(-122.2788)
end

it "returns United States as the English country name" do
country_db.lookup(ip)["country"]["names"]["en"].as_s.should eq("United States")
end

it "returns US as the country iso code" do
country_db.lookup(ip)["country"]["iso_code"].as_s.should eq("US")
end

context "as a Integer" do
integer_ip = IPAddress.new(ip).as(IPAddress::IPv4).to_u32

it "found?" do
city_db.lookup(integer_ip).found?.should be_true
end

it "returns a MaxMindDB::Result" do
city_db.lookup(integer_ip).should be_a(MaxMindDB::Any)
end

it "returns Mountain View as the English name" do
city_db.lookup(integer_ip)["city"]["names"]["en"].as_s.should eq("Alameda")
end

it "returns United States as the English country name" do
country_db.lookup(integer_ip)["country"]["names"]["en"].as_s.should eq("United States")
end
end
end

context "for the ip 2001:708:510:8:9a6:442c:f8e0:7133 (IPv6)" do
ip = "2001:708:510:8:9a6:442c:f8e0:7133"

it "found?" do
city_db.lookup(ip).found?.should be_true
end

it "returns FI as the country iso code" do
country_db.lookup(ip)["country"]["iso_code"].as_s.should eq("FI")
end

context "as an integer" do
integer_ip = IPAddress.new(ip).as(IPAddress::IPv6).to_u128

it "returns FI as the country iso code" do
country_db.lookup(ip)["country"]["iso_code"].as_s.should eq("FI")
end
end
end

context "for the ip 127.0.0.1 (local ip)" do
ip = "127.0.0.1"

it "returns a MaxMindDB::Any" do
city_db.lookup(ip).should be_a(MaxMindDB::Any)
end

it "found?" do
city_db.lookup(ip).found?.should be_false
end
end

context "test ips" do
[
{"185.23.124.1", "SA"},
{"178.72.254.1", "CZ"},
{"95.153.177.210", "RU"},
{"200.148.105.119", "BR"},
{"195.59.71.43", "GB"},
{"179.175.47.87", "BR"},
{"202.67.40.50", "ID"},
].each do |ip, iso|
it "returns a MaxMindDB::Any" do
city_db.lookup(ip).should be_a(MaxMindDB::Any)
end

it "returns #{iso} as the country iso code" do
country_db.lookup(ip)["country"]["iso_code"].as_s.should eq(iso)
end
end
end
end
19 changes: 19 additions & 0 deletions spec/spec_helper.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require "spec"
require "../src/maxminddb"

def get_db(db_link, filename)
return if File.exists?("spec/cache/#{filename.gsub(".gz", "")}")

Process.run "sh", {"-c", "curl #{db_link} -o spec/cache/#{filename}"}
Process.run "sh", {"-c", "gunzip spec/cache/#{filename}"}

File.delete("spec/cache/#{filename}") if File.exists?("spec/cache/#{filename}")
end

links = {
country: "http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.mmdb.gz",
city: "http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz"
}

get_db(links[:country], links[:country].split("/").last)
get_db(links[:city], links[:city].split("/").last)
69 changes: 69 additions & 0 deletions src/maxminddb.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
require "ipaddress"

require "./maxminddb/kmp_bytes"
require "./maxminddb/consts"
require "./maxminddb/types"
require "./maxminddb/any"
require "./maxminddb/decoder"

module MaxMindDB
class Database
def initialize(@db_path : String)
raise ArgumentError.new("Database not found") unless File.exists?(db_path)

size = File.size(@db_path)
@buffer = Bytes.new(size)
File.open(@db_path, "rb") { |file| file.read_fully(@buffer) }

@decoder = Decoder.new(@buffer)
end

def lookup(addr : String)
ip_address = IPAddress.new(addr)
decimal =
if ip_address.ipv4?
ip_address.as(IPAddress::IPv4).to_u32
elsif ip_address.ipv6?
ip_address.as(IPAddress::IPv6).to_u128
else
raise ArgumentError.new("Invalid IP address")
end

lookup(decimal)
end

def lookup(addr : UInt32|UInt128|BigInt)
node = 0

(@decoder.start_index...128).each do |i|
flag = (addr >> (127 - i)) & 1
next_node = @decoder.read(node, flag)

raise ArgumentError.new("Invalid file format") if next_node.zero?

if next_node < @decoder.node_count
node = next_node
else
base = @decoder.search_tree_size + DATA_SEPARATOR_SIZE
position = (next_node - @decoder.node_count) - DATA_SEPARATOR_SIZE

return @decoder.build(position, base).to_any
end
end

raise ArgumentError.new("Invalid file format")
end

def metadata
@decoder.metadata
end

def inspect(io : IO)
io << "#<#{self.class}:0x#{self.object_id.to_s(16)}\n\t@db_path: " << @db_path << ">"
end
end

def self.new(db_path : String)
Database.new(db_path)
end
end
Loading

0 comments on commit 29a7ddb

Please sign in to comment.