├── .gitignore ├── Gemfile ├── config.ru ├── Rakefile ├── geoip_server.gemspec ├── Gemfile.lock ├── README.markdown ├── lib └── geoip_server.rb └── test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/* 2 | pkg/* 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require './lib/geoip_server' 2 | run Sinatra::Application 3 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | task :default => :test 5 | 6 | require 'rake/testtask' 7 | Rake::TestTask.new(:test) do |test| 8 | test.libs << '.' 9 | test.pattern = 'test.rb' 10 | test.verbose = true 11 | end 12 | 13 | namespace :geoip do 14 | desc "Update GeoIP City data file" 15 | task :update_city_lite => :vendor do 16 | %x{wget http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz && gzip -d GeoLiteCity.dat.gz && mv GeoLiteCity.dat ./vendor} 17 | end 18 | 19 | desc "Update GeoIP Country data file" 20 | task :update_country_lite => :vendor do 21 | %x{wget http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz && gzip -d GeoIP.dat.gz && mv GeoIP.dat ./vendor} 22 | end 23 | 24 | task :vendor do 25 | %x{mkdir -p vendor} 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /geoip_server.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'geoip_server' 3 | s.version = '1.3.0' 4 | s.authors = ["Jack Danger Canty", "Erik Michaels-Ober"] 5 | s.description = 'Query the MaxMind GeoIP data records via a web service' 6 | s.email = 'gitcommit@6brand.com' 7 | s.files = %w(Gemfile Gemfile.lock README.markdown Rakefile config.ru geoip_server.gemspec test.rb) 8 | s.files += Dir.glob('lib/**/*.rb') 9 | s.test_files = %w(test.rb) 10 | s.homepage = 'http://github.com/JackDanger/geoip_server' 11 | s.require_paths = ['lib'] 12 | s.summary = s.description 13 | s.add_dependency 'sinatra', '~> 1.1' 14 | s.add_dependency 'geoip', '~> 1.1' 15 | s.add_dependency 'multi_json', '~> 1.3' 16 | s.add_dependency 'newrelic_rpm', '~> 3.4' 17 | s.add_development_dependency 'rake' 18 | s.add_development_dependency 'shoulda' 19 | s.add_development_dependency 'simplecov' 20 | s.add_development_dependency 'rack-test' 21 | end 22 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | geoip_server (1.3.0) 5 | geoip (~> 1.1) 6 | multi_json (~> 1.3) 7 | newrelic_rpm (~> 3.4) 8 | sinatra (~> 1.1) 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | activesupport (3.2.8) 14 | i18n (~> 0.6) 15 | multi_json (~> 1.0) 16 | geoip (1.1.2) 17 | i18n (0.6.1) 18 | multi_json (1.3.6) 19 | newrelic_rpm (3.4.2.1) 20 | rack (1.4.1) 21 | rack-protection (1.2.0) 22 | rack 23 | rack-test (0.6.1) 24 | rack (>= 1.0) 25 | rake (0.9.2.2) 26 | shoulda (3.1.1) 27 | shoulda-context (~> 1.0) 28 | shoulda-matchers (~> 1.2) 29 | shoulda-context (1.0.0) 30 | shoulda-matchers (1.3.0) 31 | activesupport (>= 3.0.0) 32 | simplecov (0.6.4) 33 | multi_json (~> 1.0) 34 | simplecov-html (~> 0.5.3) 35 | simplecov-html (0.5.3) 36 | sinatra (1.3.3) 37 | rack (~> 1.3, >= 1.3.6) 38 | rack-protection (~> 1.2) 39 | tilt (~> 1.3, >= 1.3.3) 40 | tilt (1.3.3) 41 | 42 | PLATFORMS 43 | ruby 44 | 45 | DEPENDENCIES 46 | geoip_server! 47 | rack-test 48 | rake 49 | shoulda 50 | simplecov 51 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # GeoIP Server 2 | 3 | This simple Rack server is useful as a self-hosted service for making lookups to the GeoIP database from [MaxMind][]. 4 | 5 | [maxmind]: http://www.maxmind.com/app/geolite 6 | 7 | 8 | ## Instant installation and deploy 9 | 10 | * Clone this: `git clone git://github.com/JackDanger/geoip_server.git` 11 | * Download the free GeoIP data files: `rake geoip:update_city_lite` 12 | * Commit that data file to your clone: `git add vendor && git commit -m "adding data file"` 13 | * Signup for an account at Heroku ([better details here](http://github.com/sinatra/heroku-sinatra-app)) 14 | * Create and name a new app where you want to host this 15 | * Push it to Heroku.com: `git push heroku master` 16 | 17 | 18 | ## How To 19 | 20 | Once the server is running you can make a GET request to the server and receive lookup results in JSON format. 21 | 22 | ```ruby 23 | require 'json' 24 | require 'open-uri' 25 | # Get the requesting user's IP address 26 | # In a Rails app, you can use: request.remote_ip 27 | # In a Sinatra app, you can use: request.ip 28 | # In a Rack app, you can use: @env['REMOTE_ADDR'] 29 | ip = "207.97.227.239" 30 | data = JSON.load(open("http://my-geoip-service-app.herokuapp.com/#{ip}").read) 31 | "You're in: #{data['city']}" 32 | ``` 33 | 34 | Or, straight from a terminal: 35 | 36 | curl http://my-geoip-service-app.herokuapp.com/207.97.227.239 37 | 38 | Patches welcome, forks celebrated. 39 | 40 | Copyright (c) 2010 [Jack Danger Canty](http://jåck.com). Released under the MIT License. -------------------------------------------------------------------------------- /lib/geoip_server.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'geoip' 3 | require 'multi_json' 4 | 5 | data_file = File.expand_path(File.join(File.dirname(__FILE__), '..', 'vendor', 'GeoLiteCity.dat')) 6 | 7 | configure :production do 8 | begin 9 | require 'newrelic_rpm' 10 | rescue LoadError 11 | end 12 | end 13 | 14 | get '/' do 15 | < 17 | 18 | 19 | 20 | 43 | Detect a computer's location by IP address 44 | 45 | 46 |

47 | Lookup a location by IP address. Example: 48 |

49 | 50 | curl http://#{request.env['HTTP_HOST']}/207.97.227.239 51 | 52 |
53 | 54 | 55 |
56 |

57 | None of this would be possible without MaxMind. 58 |

59 | 60 | 61 | END 62 | end 63 | 64 | get /\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ do |ip| 65 | data = GeoIP.new(data_file).city(ip) 66 | content_type 'application/json;charset=ascii-8bit' 67 | headers['Cache-Control'] = "public; max-age=31536000" # = 365 (days) * 24 (hours) * 60 (minutes) * 60 (seconds) 68 | return "{}" unless data 69 | respond_with(MultiJson.encode(encode(data))) 70 | end 71 | 72 | def respond_with json 73 | # jsonp support 74 | callback, variable = params[:callback], params[:variable] 75 | if callback && variable 76 | "var #{variable} = #{json};\n#{callback}(#{variable});" 77 | elsif variable 78 | "var #{variable} = #{json};" 79 | elsif callback 80 | "#{callback}(#{json});" 81 | else 82 | json 83 | end 84 | end 85 | 86 | def encode data 87 | { 88 | # The host or IP address string as requested 89 | :ip => data.request, 90 | # The IP address string after looking up the host 91 | :ip_lookup => data.ip, 92 | # The ISO3166-1 two-character country code 93 | :country_code => data.country_code2, 94 | # The ISO3166-2 three-character country code 95 | :country_code_long => data.country_code3, 96 | # The ISO3166 English-language name of the country 97 | :country => data.country_name, 98 | # The two-character continent code 99 | :continent => data.continent_code, 100 | # The region name 101 | :region => data.region_name, 102 | # The city name 103 | :city => data.city_name, 104 | # The postal code 105 | :postal_code => data.postal_code, 106 | # The latitude 107 | :lat => data.latitude, 108 | # The longitude 109 | :lng => data.longitude, 110 | # The USA DMA code, if available 111 | :dma_code => data.dma_code, 112 | # The area code, if available 113 | :area_code => data.area_code, 114 | # Timezone, if available 115 | :timezone => data.timezone, 116 | } 117 | end 118 | -------------------------------------------------------------------------------- /test.rb: -------------------------------------------------------------------------------- 1 | ENV['RACK_ENV'] = 'test' 2 | 3 | require 'simplecov' 4 | SimpleCov.start 5 | require 'test/unit' 6 | require 'shoulda' 7 | require 'rack/test' 8 | require 'geoip_server' 9 | 10 | class GeoipServerTest < Test::Unit::TestCase 11 | include Rack::Test::Methods 12 | 13 | def app 14 | Sinatra::Application 15 | end 16 | 17 | context "on GET to /" do 18 | setup { 19 | get '/' 20 | } 21 | should "return ok" do 22 | assert last_response.ok? 23 | end 24 | should "include an example" do 25 | assert last_response.body =~ /curl/ 26 | end 27 | end 28 | 29 | context "on GET to /:ip" do 30 | setup { 31 | get '/67.161.92.71' 32 | } 33 | should "return ok" do 34 | assert last_response.ok? 35 | end 36 | should "return json content-type" do 37 | assert_equal 'application/json;charset=ascii-8bit', last_response.headers['Content-Type'] 38 | end 39 | end 40 | 41 | context "on GET to /:ip?variable=myVariableName" do 42 | setup { 43 | get '/67.161.92.71?variable=myVariableName' 44 | } 45 | should "return ok" do 46 | assert last_response.ok? 47 | end 48 | should "return json content-type" do 49 | assert_equal 'application/json;charset=ascii-8bit', last_response.headers['Content-Type'] 50 | end 51 | should "include a variable" do 52 | assert last_response.body =~ /var myVariableName/ 53 | end 54 | end 55 | 56 | context "on GET to /:ip?callback=myCallbackFunction" do 57 | setup { 58 | get '/67.161.92.71?callback=myCallbackFunction' 59 | } 60 | should "return ok" do 61 | assert last_response.ok? 62 | end 63 | should "return json content-type" do 64 | assert_equal 'application/json;charset=ascii-8bit', last_response.headers['Content-Type'] 65 | end 66 | should "include a function" do 67 | assert last_response.body =~ /myCallbackFunction\(\{.*\}\)/ 68 | end 69 | end 70 | 71 | context "on GET to /:ip?callback=myCallbackFunction&variable=myVariableName" do 72 | setup { 73 | get '/67.161.92.71?callback=myCallbackFunction&variable=myVariableName' 74 | } 75 | should "return ok" do 76 | assert last_response.ok? 77 | end 78 | should "return json content-type" do 79 | assert_equal 'application/json;charset=ascii-8bit', last_response.headers['Content-Type'] 80 | end 81 | should "include a variable" do 82 | assert last_response.body =~ /var myVariableName/ 83 | end 84 | should "include a function" do 85 | assert last_response.body =~ /myCallbackFunction\(myVariableName\);/ 86 | end 87 | end 88 | 89 | context "converting struct" do 90 | setup { 91 | Struct.new( 92 | "City", 93 | :request, 94 | :ip, 95 | :country_code2, 96 | :country_code3, 97 | :country_name, 98 | :continent_code, 99 | :region_name, 100 | :city_name, 101 | :postal_code, 102 | :latitude, 103 | :longitude, 104 | :dma_code, 105 | :area_code, 106 | :timezone 107 | ) unless defined? Struct::City 108 | city = Struct::City.new( 109 | "67.161.92.71", 110 | "67.161.92.71", 111 | "US", 112 | "USA", 113 | "United States", 114 | "NA", 115 | "WA", 116 | "Seattle", 117 | "98117", 118 | 47.6847, 119 | -122.3848, 120 | 819, 121 | 206, 122 | "America/Los_Angeles" 123 | ) 124 | @hash = encode(city) 125 | } 126 | should "find city" do 127 | assert_equal 'Seattle', @hash[:city] 128 | end 129 | should "find country" do 130 | assert_equal 'United States', @hash[:country] 131 | end 132 | should "find lat" do 133 | assert_equal 47.6847, @hash[:lat] 134 | end 135 | should "find lng" do 136 | assert_equal -122.3848, @hash[:lng] 137 | end 138 | end 139 | end 140 | --------------------------------------------------------------------------------