iparty 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ require_relative "max_mind/database"
6
+
7
+ module IParty
8
+ class MaxMind
9
+ class << self
10
+ def db edition
11
+ edition = "GeoLite2-#{edition}" if edition.is_a?(Symbol)
12
+ file = IParty.config.directory.join("#{edition}.mmdb")
13
+ return unless file.exist?
14
+
15
+ if ctn = IParty.config.singletons
16
+ ctn = ctn.call if ctn.is_a?(Proc)
17
+ return ctn.fetch(edition) if ctn.key?(edition)
18
+ end
19
+
20
+ Database.new(file, reader: IParty.config.eager_load ? EagerReader : LazyReader).tap do |dbi|
21
+ ctn[edition] ||= dbi if ctn
22
+ end
23
+ end
24
+
25
+ def lookup edition, *args, close: true, **kw
26
+ return unless mmdb = db(edition)
27
+
28
+ mmdb.lookup(*args, **kw).tap do
29
+ mmdb.close if close && !IParty.config.singletons
30
+ end
31
+ end
32
+
33
+ def with_transactional_update_directory
34
+ temp_dir = IParty.config.directory.join(".updating")
35
+ return yield(temp_dir) if @in_transaction
36
+
37
+ begin
38
+ @in_transaction = true
39
+ temp_dir.mkpath
40
+ yield(temp_dir)
41
+ ensure
42
+ @in_transaction = false
43
+ temp_dir.glob("*.mmdb").each do |file|
44
+ Database.new(file, reader: LazyReader)
45
+ FileUtils.mv(file, IParty.config.directory)
46
+ rescue Database::InvalidFileFormatError
47
+ warn "iparty: ignoring invalid mmdb file: #{file}"
48
+ end
49
+ FileUtils.rm_rf(temp_dir)
50
+ end
51
+ end
52
+
53
+ def fetch_db_files! fetch_when = :always, verbose: false
54
+ with_transactional_update_directory do
55
+ IParty.config.editions.each do |edition|
56
+ fetch_db_file!(edition, fetch_when, verbose: verbose)
57
+ end
58
+ end
59
+ end
60
+
61
+ def fetch_db_file_reason file, fetch_when = :always
62
+ return fetch_when if fetch_when == :always
63
+ return :missing unless file.exist?
64
+
65
+ begin
66
+ Database.new(file, reader: LazyReader)
67
+ rescue Database::InvalidFileFormatError
68
+ return :invalid
69
+ end
70
+
71
+ return unless fetch_when.is_a?(Numeric)
72
+
73
+ ctime = file.ctime
74
+ age = Time.now - ctime
75
+ :expired unless fetch_when > age
76
+ end
77
+
78
+ def fetch_db_file! edition, fetch_when = :always, verbose: false
79
+ target_file = IParty.config.directory.join("#{edition}.mmdb")
80
+ return target_file unless reason = fetch_db_file_reason(target_file, fetch_when)
81
+
82
+ with_transactional_update_directory do |temp_dir|
83
+ warn "iparty: fetching #{reason}: #{target_file.basename}" if verbose
84
+ IParty.config.url_to_mmdb.call(
85
+ IParty.config.mirror.gsub(":edition", edition),
86
+ temp_dir,
87
+ IParty.config,
88
+ )
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IParty
4
+ class Railtie < Rails::Railtie
5
+ railtie_name :iparty
6
+
7
+ rake_tasks do
8
+ require "iparty/rake_task"
9
+ IParty::RakeTask.new
10
+ end
11
+
12
+ config.before_configuration do
13
+ IParty.config.directory = IParty.env_value("IPARTY_DIRECTORY", nil) do |dir|
14
+ if dir && !dir.empty?
15
+ Pathname.new(dir)
16
+ else
17
+ Rails.root.join("vendor", "maxmind")
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+ require "rake/tasklib"
5
+
6
+ module IParty
7
+ class RakeTask < ::Rake::TaskLib
8
+ attr_accessor :name, :verbose
9
+
10
+ def initialize(name = :iparty)
11
+ super()
12
+
13
+ @name = name
14
+ @verbose = true
15
+
16
+ yield self if block_given?
17
+ define_update
18
+ define_fetch
19
+ define_status
20
+ define_config
21
+ end
22
+
23
+ def parse_duration input, default = :missing
24
+ return default unless input.is_a?(String)
25
+
26
+ if input.match?(/\A\d+\z/)
27
+ input.to_i
28
+ elsif asm = input.match(/\A(\d+)\.(second|minute|hour|day|week|month|year)s?\z/)
29
+ ActiveSupport::Duration.send(:"#{asm[2]}s", asm[1].to_i)
30
+ else
31
+ default
32
+ end
33
+ end
34
+
35
+ # iparty:update
36
+ def define_update
37
+ namespace(name) do
38
+ desc "Updates geoip mmdb-files"
39
+ task :update do
40
+ Rake.application.lookup("environment")&.invoke
41
+
42
+ IParty::MaxMind.fetch_db_files!(verbose: @verbose)
43
+ end
44
+ end
45
+ end
46
+
47
+ # iparty:fetch
48
+ # iparty:fetch[86400]
49
+ # iparty:fetch[14.days]
50
+ def define_fetch
51
+ namespace(name) do
52
+ desc "Fetches missing or expired geoip mmdb-files (optional numeric max_age)"
53
+ task :fetch, [:max_age] do |task, args|
54
+ Rake.application.lookup("environment")&.invoke
55
+
56
+ IParty::MaxMind.fetch_db_files!(parse_duration(args[:max_age]), verbose: @verbose)
57
+ end
58
+ end
59
+ end
60
+
61
+ # iparty:status
62
+ def define_status
63
+ namespace(name) do
64
+ desc "Show status of geoip mmdb-files (optional numeric max_age)"
65
+ task :status, [:max_age] do |task, args|
66
+ Rake.application.lookup("environment")&.invoke
67
+
68
+ max_age = parse_duration(args[:max_age])
69
+
70
+ success = IParty.config.editions.map do |edition|
71
+ file = IParty.config.directory.join("#{edition}.mmdb")
72
+ reason = IParty::MaxMind.fetch_db_file_reason(file, max_age)
73
+
74
+ stat_string = if file.exist?
75
+ ctime = file.ctime
76
+ age = Time.now - ctime
77
+ days = (age / 86_400).floor
78
+ hours = ((age / 3_600) % 24).floor
79
+ minutes = ((age / 60) % 60).floor
80
+ age_string = [
81
+ ("#{days}d" if days > 0),
82
+ ("%02d:%02d" % [hours, minutes] if hours > 0 || minutes > 0),
83
+ ].compact.join(" ")
84
+ age_string = "#{age.floor}s" if age_string.empty?
85
+ "[age: #{age_string}, ctime: #{ctime}]"
86
+ end
87
+
88
+ puts [reason&.upcase || "OK", stat_string, file].compact.join(" ")
89
+ !reason
90
+ end.all?
91
+
92
+ exit(1) unless success
93
+ end
94
+ end
95
+ end
96
+
97
+ # iparty:config
98
+ # iparty:config[inspect]
99
+ # iparty:config[json]
100
+ def define_config
101
+ namespace(name) do
102
+ desc "Shows effective IParty config (including license_key, optional format json/inspect)"
103
+ task :config, [:format] do |task, args|
104
+ Rake.application.lookup("environment")&.invoke
105
+
106
+ case args[:format]
107
+ when "json"
108
+ require "json"
109
+ puts JSON.pretty_generate(IParty.config.to_h)
110
+ when "inspect"
111
+ puts IParty.config.inspect
112
+ else
113
+ IParty.config.each_pair do |key, value|
114
+ puts "#{key.to_s.rjust(16)}: #{value.inspect}"
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IParty
4
+ VERSION = "0.1.0"
5
+ end
data/lib/iparty.rb ADDED
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+ require "fileutils"
5
+ require "forwardable"
6
+
7
+ # rubocop:disable Naming/MethodName -- we party!
8
+ def IParty *args, **kw
9
+ IParty.normalize(*args, **kw)
10
+ end
11
+ # rubocop:enable Naming/MethodName
12
+
13
+ module IParty
14
+ class Error < StandardError; end
15
+
16
+ require_relative "iparty/version"
17
+ require_relative "iparty/config"
18
+ require_relative "iparty/address"
19
+ require_relative "iparty/max_mind"
20
+ require_relative "iparty/railtie" if defined?(Rails)
21
+
22
+ @config = default_config
23
+
24
+ def self.fetch_db_files! *args, **kw
25
+ IParty::MaxMind.fetch_db_files!(*args, **kw)
26
+ end
27
+
28
+ def self.normalize input, family = nil, native: false, **kw
29
+ return unless input
30
+ return if input.is_a?(String) && input.match?(/\A[[:space:]]*\z/)
31
+ return if input.respond_to?(:empty?) && input.empty?
32
+
33
+ addr = case input
34
+ when String
35
+ Address.new(input.strip, **kw)
36
+ when IPAddr
37
+ Address.new(input.to_i, input.family)
38
+ when Integer
39
+ family ||= input > (2**32) - 1 ? Socket::AF_INET6 : Socket::AF_INET
40
+ Address.new(input, family, **kw)
41
+ else
42
+ raise IPAddr::InvalidAddressError, "invalid address: #{input}"
43
+ end
44
+
45
+ native ? addr.native : addr
46
+ end
47
+
48
+ def self.classify input
49
+ normalize(input).type
50
+ rescue IPAddr::Error
51
+ :invalid
52
+ end
53
+
54
+ def self.expand_hostnames *ips_or_hosts
55
+ ips_or_hosts.flatten.flat_map do |name|
56
+ next name unless name.is_a?(String) && !name.include?(":") && name.match?(/[a-z]/i)
57
+
58
+ if defined?(Resolv)
59
+ Resolv.getaddresses(name)
60
+ elsif defined?(Addrinfo)
61
+ begin
62
+ Addrinfo.getaddrinfo(name, nil).map(&:ip_address).uniq
63
+ rescue Socket::ResolutionError
64
+ []
65
+ end
66
+ else
67
+ raise "neither resolv nor addrinfo is available (gem install resolv)"
68
+ end
69
+ end
70
+ end
71
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: iparty
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sven Pachnit
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: fileutils
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: forwardable
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: optparse
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: tmpdir
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ description: Makes (geo) IP fun again! Geo, v6 significance and more.
69
+ email:
70
+ - sven@bmonkeys.net
71
+ executables:
72
+ - iparty
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - CHANGELOG.md
77
+ - LICENSE.txt
78
+ - README.md
79
+ - Rakefile
80
+ - exe/iparty
81
+ - lib/iparty.rb
82
+ - lib/iparty/address.rb
83
+ - lib/iparty/cli/application.rb
84
+ - lib/iparty/cli/application/actions.rb
85
+ - lib/iparty/cli/application/appinfo.rb
86
+ - lib/iparty/cli/application/irb_context.rb
87
+ - lib/iparty/cli/application/options.rb
88
+ - lib/iparty/cli/colorize.rb
89
+ - lib/iparty/cli/formatter.rb
90
+ - lib/iparty/config.rb
91
+ - lib/iparty/max_mind.rb
92
+ - lib/iparty/max_mind/database.rb
93
+ - lib/iparty/max_mind/eager_reader.rb
94
+ - lib/iparty/max_mind/lazy_reader.rb
95
+ - lib/iparty/max_mind/result.rb
96
+ - lib/iparty/railtie.rb
97
+ - lib/iparty/rake_task.rb
98
+ - lib/iparty/version.rb
99
+ homepage: https://github.com/2called-chaos/iparty
100
+ licenses:
101
+ - MIT
102
+ metadata:
103
+ allowed_push_host: https://rubygems.org
104
+ homepage_uri: https://github.com/2called-chaos/iparty/blob/master/README.md
105
+ source_code_uri: https://github.com/2called-chaos/iparty
106
+ changelog_uri: https://github.com/2called-chaos/iparty/blob/master/CHANGELOG.md
107
+ rubygems_mfa_required: 'true'
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: 3.2.0
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubygems_version: 4.0.6
123
+ specification_version: 4
124
+ summary: Makes (geo) IP fun again!
125
+ test_files: []