portfinder 0.0.1 → 0.0.2
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.
- checksums.yaml +4 -4
- data/.codeclimate.yml +18 -0
- data/.gitignore +11 -0
- data/.reek +26 -0
- data/.rubocop.yml +19 -0
- data/.travis.yml +29 -0
- data/Appraisals +7 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +174 -0
- data/Rakefile +32 -0
- data/bin/pf +101 -0
- data/bin/portfinder +101 -0
- data/gemfiles/slop_4.4.0.gemfile +7 -0
- data/gemfiles/slop_4.5.0.gemfile +7 -0
- data/lib/portfinder.rb +67 -3
- data/lib/portfinder/constants.rb +31 -0
- data/lib/portfinder/error.rb +9 -0
- data/lib/portfinder/monitor.rb +48 -0
- data/lib/portfinder/option.rb +23 -0
- data/lib/portfinder/parser.rb +91 -0
- data/lib/portfinder/pool.rb +59 -0
- data/lib/portfinder/scanner.rb +211 -2
- data/lib/portfinder/version.rb +5 -0
- data/portfinder.gemspec +46 -0
- metadata +142 -11
data/bin/portfinder
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "slop"
|
3
|
+
require "portfinder"
|
4
|
+
require "portfinder/option"
|
5
|
+
|
6
|
+
module Portfinder
|
7
|
+
# Portfinder command-line interface base class
|
8
|
+
class CLI
|
9
|
+
def self.start argv
|
10
|
+
new(argv).start
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize argv
|
14
|
+
@host, options = preprocess argv
|
15
|
+
@options, @help_str = prepare_options options, @host
|
16
|
+
end
|
17
|
+
|
18
|
+
def start
|
19
|
+
@host ? scan(@options) : help
|
20
|
+
end
|
21
|
+
|
22
|
+
def scan options
|
23
|
+
scanner =
|
24
|
+
Portfinder::Scanner.new(
|
25
|
+
options[:host], options[:port],
|
26
|
+
randomize: options[:randomize], threaded: true,
|
27
|
+
threads: options[:thread], thread_for: :port
|
28
|
+
)
|
29
|
+
scanner.scan
|
30
|
+
puts scanner.generate_result true
|
31
|
+
end
|
32
|
+
|
33
|
+
def help
|
34
|
+
puts @help_str
|
35
|
+
end
|
36
|
+
|
37
|
+
def version
|
38
|
+
puts "v#{Portfinder::VERSION}"
|
39
|
+
exit
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def preprocess argv
|
45
|
+
default_options = ["-h", "--help"]
|
46
|
+
host = argv[0]
|
47
|
+
if default_options.include? host
|
48
|
+
host = nil
|
49
|
+
options = argv
|
50
|
+
else
|
51
|
+
options = argv[1..-1] || []
|
52
|
+
end
|
53
|
+
|
54
|
+
[host, options]
|
55
|
+
end
|
56
|
+
|
57
|
+
# rubocop:disable Style/MethodLength
|
58
|
+
def option_parser
|
59
|
+
opts = Slop::Options.new
|
60
|
+
opts.banner = "Commands:"
|
61
|
+
opts.separator " portfinder # Display available options"
|
62
|
+
opts.separator(
|
63
|
+
" portfinder <host> # Scans host(s) for provided options"
|
64
|
+
)
|
65
|
+
opts.separator ""
|
66
|
+
opts.separator "Options:"
|
67
|
+
opts.port(
|
68
|
+
"-p", "--port", "# Specify a single port, range or selections",
|
69
|
+
default: 1..1024
|
70
|
+
)
|
71
|
+
opts.int "-t", "--thread", "# Specify threads to spawn", default: 10
|
72
|
+
opts.bool "-r", "--randomize", "# Randomize port scan order"
|
73
|
+
opts.out(
|
74
|
+
"-o", "--out", "# Dump scan result to the specified file",
|
75
|
+
default: "#{@host ? @host.gsub(%r{[./]}, '_') : 'export'}.json"
|
76
|
+
)
|
77
|
+
opts.bool "-v", "--verbose", "# More information during scan"
|
78
|
+
opts.bool "-h", "--help", "# Describe available commands and options"
|
79
|
+
|
80
|
+
Slop::Parser.new opts
|
81
|
+
end
|
82
|
+
|
83
|
+
# Refactor
|
84
|
+
def prepare_options options, host
|
85
|
+
version_opt = ["-V", "--version"]
|
86
|
+
version if version_opt.include?(host)
|
87
|
+
|
88
|
+
parsed = option_parser.parse options
|
89
|
+
parsed_hash = parsed.to_h
|
90
|
+
if host && /^[^-]+/ =~ host
|
91
|
+
parsed_hash.merge!(
|
92
|
+
Slop.parse ["--host", host] { |opt| opt.host "--host" }
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
[parsed_hash, parsed.to_s]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
Portfinder::CLI.start ARGV
|
data/lib/portfinder.rb
CHANGED
@@ -1,5 +1,69 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require "socket"
|
2
|
+
require "ipaddr"
|
3
|
+
require "json"
|
4
|
+
require "yaml"
|
5
|
+
require "portfinder/constants"
|
6
|
+
require "portfinder/error"
|
7
|
+
require "portfinder/parser"
|
8
|
+
require "portfinder/pool"
|
9
|
+
require "portfinder/monitor"
|
10
|
+
require "portfinder/scanner"
|
11
|
+
require "portfinder/version"
|
3
12
|
|
4
|
-
|
13
|
+
# Expectations
|
5
14
|
|
15
|
+
## Mode 1: Blocking mode with monitoring
|
16
|
+
# scanner = Portfinder::Scanner.new(hosts, ports,
|
17
|
+
# randomize: false, threaded: true, threads: 10, thread_for: :port)
|
18
|
+
#
|
19
|
+
# scanner.log do |monitor|
|
20
|
+
# puts "Active threads:\t#{monitor.threads}\nScanning now:\n"
|
21
|
+
# while true
|
22
|
+
# print "\tHost:\t#{monitor.host}\tPort:\t#{monitor.port}\tStatus: #{
|
23
|
+
# monitor.state}\r"
|
24
|
+
# sleep 0.1
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# scanner.scan
|
29
|
+
# puts "\nScan complete!\n\nResult: #{scanner.generate_result}"
|
30
|
+
#
|
31
|
+
# format = "json"
|
32
|
+
# file = open("meau.#{format}", "wb")
|
33
|
+
# file.write scanner.report_as format.to_sym
|
34
|
+
# file.close
|
35
|
+
|
36
|
+
## Mode 2: Partial blocking (join during result invocation)
|
37
|
+
# scanner = Portfinder::Scanner.new("192.168.0.101", 1..65535)
|
38
|
+
# #logger can be placed here...
|
39
|
+
#
|
40
|
+
# scanner.log do |monitor|
|
41
|
+
# puts "Active threads:\t#{monitor.threads}\nScanning now:\n"
|
42
|
+
# while true
|
43
|
+
# print "\tHost:\t#{monitor.host}\tPort:\t#{monitor.port}\r"
|
44
|
+
# sleep 0.1
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
# scanner.scan synchronus = false
|
48
|
+
#
|
49
|
+
# # logger can be placed here
|
50
|
+
# puts "\nScan complete!\n\nResult: #{scanner.generate_result}"
|
51
|
+
|
52
|
+
## Mode 3: Non-blocking (callback invocation upon completion)
|
53
|
+
# (NOTE: Parent thread must be alive to receive callback)
|
54
|
+
#
|
55
|
+
# scanner = Portfinder::Scanner.new("192.168.0.101", 1..65535)
|
56
|
+
#
|
57
|
+
# scanner.scan(false) do
|
58
|
+
# puts "\nScan complete!\n\nResult: #{scanner.generate_result}"
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# scanner.log do |monitor|
|
62
|
+
# puts "Active threads:\t#{monitor.threads}\nScanning now:\n"
|
63
|
+
# while true
|
64
|
+
# print "\tHost:\t#{monitor.host}\tPort:\t#{monitor.port}\r"
|
65
|
+
# sleep 0.1
|
66
|
+
# end
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# sleep 10
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Portfinder
|
2
|
+
IP4_OCTET_ = /\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]/
|
3
|
+
IP4_ = /((#{IP4_OCTET_})\.){3}(#{IP4_OCTET_})/
|
4
|
+
CIDR_ = /\d|[12]\d|3[0-2]/
|
5
|
+
IP4 = /^(?<ip>#{IP4_})$/
|
6
|
+
IP4_RANGE = /^(?<start>#{IP4_})-(?<limit>[2-9]|[1-9]\d|1\d{2}|2[0-4]\d|
|
7
|
+
25[0-4])$/x
|
8
|
+
IP4_SELECTION = /^(#{IP4_},)+#{IP4_}$/
|
9
|
+
IP4_NETWORK = %r{^(?<network>#{IP4_})/(?<net_bits>#{CIDR_})$}
|
10
|
+
|
11
|
+
PORT_ = /[1-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|
|
12
|
+
65[0-4]\d{2}|655[0-2]\d|6553[0-5]/x
|
13
|
+
PORT = /^(?<port>#{PORT_})$/
|
14
|
+
PORT_RANGE = /^(?<start>#{PORT_})-(?<end>#{PORT_})$/
|
15
|
+
PORT_SELECTION = /^(#{PORT_},)+#{PORT_}$/
|
16
|
+
|
17
|
+
# UNIX_FILE_ = /[^;:\|\/\0]+/
|
18
|
+
# UNIX_DIR_ = //
|
19
|
+
|
20
|
+
IP4_TYPES = {
|
21
|
+
ip: Portfinder::IP4,
|
22
|
+
range: Portfinder::IP4_RANGE,
|
23
|
+
selection: Portfinder::IP4_SELECTION,
|
24
|
+
network: Portfinder::IP4_NETWORK
|
25
|
+
}.freeze
|
26
|
+
PORT_TYPES = {
|
27
|
+
port: Portfinder::PORT,
|
28
|
+
range: Portfinder::PORT_RANGE,
|
29
|
+
selection: Portfinder::PORT_SELECTION
|
30
|
+
}.freeze
|
31
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Portfinder
|
2
|
+
# Portfinder scanner monitor
|
3
|
+
class Monitor
|
4
|
+
attr_reader :state
|
5
|
+
attr_accessor :host
|
6
|
+
attr_accessor :port
|
7
|
+
attr_accessor :threads
|
8
|
+
|
9
|
+
def initialize state = :init
|
10
|
+
self.state = state
|
11
|
+
@host = ""
|
12
|
+
@port = nil
|
13
|
+
@threads = 1
|
14
|
+
end
|
15
|
+
|
16
|
+
# Refactor: Enum?
|
17
|
+
def state= value
|
18
|
+
states = %i[init run term]
|
19
|
+
unless states.include?(value)
|
20
|
+
raise TypeError, "state can be any of #{states}"
|
21
|
+
end
|
22
|
+
@state = value
|
23
|
+
end
|
24
|
+
|
25
|
+
def start
|
26
|
+
self.state = :run
|
27
|
+
reset
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop
|
31
|
+
self.state = :term
|
32
|
+
reset
|
33
|
+
end
|
34
|
+
|
35
|
+
def update host, port
|
36
|
+
@host = host
|
37
|
+
@port = port
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def reset
|
43
|
+
@host = ""
|
44
|
+
@port = nil
|
45
|
+
@threads = 1
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# FIXME: Slop doesn't raise an error during option parsing
|
2
|
+
module Slop
|
3
|
+
# Slop parser for IPv4 Hosts
|
4
|
+
class HostOption < Option
|
5
|
+
def call value
|
6
|
+
Portfinder::Parser.new.parse_hosts value
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
# Slop parser for ports
|
11
|
+
class PortOption < Option
|
12
|
+
def call value
|
13
|
+
Portfinder::Parser.new.parse_ports value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Slop parser for File path
|
18
|
+
class OutOption < Option
|
19
|
+
def call value
|
20
|
+
File.basename value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Portfinder
|
2
|
+
# Argument parser
|
3
|
+
class Parser
|
4
|
+
# Parser match type
|
5
|
+
Type = Struct.new :name, :match
|
6
|
+
|
7
|
+
def initialize; end
|
8
|
+
|
9
|
+
# :reek:FeatureEnvy
|
10
|
+
def parse_hosts target
|
11
|
+
type = select_type target, IP4_TYPES
|
12
|
+
return unless type
|
13
|
+
|
14
|
+
case type.name
|
15
|
+
when :ip
|
16
|
+
type.match[:ip]
|
17
|
+
when :range
|
18
|
+
ip4_range type.match[:start], type.match[:limit].to_i
|
19
|
+
when :selection
|
20
|
+
target.split ","
|
21
|
+
when :network
|
22
|
+
ip4_network_hosts type.match[:network], type.match[:net_bits].to_i
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# :reek:FeatureEnvy
|
27
|
+
def parse_ports target
|
28
|
+
type = select_type target, PORT_TYPES
|
29
|
+
return unless type
|
30
|
+
|
31
|
+
case type.name
|
32
|
+
when :port
|
33
|
+
type.match[:port].to_i
|
34
|
+
when :range
|
35
|
+
type.match[:start].to_i..type.match[:end].to_i
|
36
|
+
when :selection
|
37
|
+
target.split(",").map(&:to_i)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_file_path _target, _verify = false
|
42
|
+
raise NotImplementedError, "Implementation pending"
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse_dir_path _target, _verify = false
|
46
|
+
raise NotImplementedError, "Implementation pending"
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def ip4_range start, limit
|
52
|
+
start_offset = start.split(".").last.to_i
|
53
|
+
host_count = (limit - start_offset) + 1
|
54
|
+
host_count_in_range = (1..254).cover? host_count
|
55
|
+
last_host_in_range = (2..254).cover? limit
|
56
|
+
|
57
|
+
return unless host_count_in_range && last_host_in_range
|
58
|
+
|
59
|
+
Enumerator.new host_count do |host|
|
60
|
+
IPAddr.new(
|
61
|
+
start + "/24"
|
62
|
+
).to_range.each_with_index do |addr, index|
|
63
|
+
host << addr.to_s if (start_offset..limit).cover? index
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def ip4_network_hosts network, net_bits
|
69
|
+
ip_count = 2**(32 - net_bits)
|
70
|
+
valid_host_count = ip_count < 2 ? 0 : ip_count - 2
|
71
|
+
range = IPAddr.new("#{network}/#{net_bits}").to_range
|
72
|
+
exceptions = [range.first, range.last]
|
73
|
+
Enumerator.new valid_host_count do |host|
|
74
|
+
range.each do |addr|
|
75
|
+
host << addr.to_s unless exceptions.include?(addr)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def select_type target, types
|
81
|
+
selection_type = nil
|
82
|
+
|
83
|
+
types.each_pair do |type, matcher|
|
84
|
+
match = matcher.match(target)
|
85
|
+
selection_type = Type.new(type, match) if match
|
86
|
+
end
|
87
|
+
|
88
|
+
selection_type
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Portfinder
|
2
|
+
# Portfinder Thread pool
|
3
|
+
class Pool
|
4
|
+
attr_reader :result
|
5
|
+
attr_reader :size
|
6
|
+
|
7
|
+
def initialize size
|
8
|
+
@pool = []
|
9
|
+
@size = size
|
10
|
+
@jobs = Queue.new
|
11
|
+
@result = {}
|
12
|
+
@formatter = nil
|
13
|
+
process
|
14
|
+
end
|
15
|
+
|
16
|
+
def complete_result
|
17
|
+
shutdown
|
18
|
+
@result
|
19
|
+
end
|
20
|
+
|
21
|
+
def result_format &block
|
22
|
+
@formatter = block
|
23
|
+
end
|
24
|
+
|
25
|
+
def schedule *args, &block
|
26
|
+
@jobs << [block, args]
|
27
|
+
end
|
28
|
+
|
29
|
+
def shutdown synchronize = true, &callback
|
30
|
+
@size.times do
|
31
|
+
schedule { throw :shutdown }
|
32
|
+
end
|
33
|
+
|
34
|
+
watcher =
|
35
|
+
Thread.fork do
|
36
|
+
@pool.map(&:join)
|
37
|
+
callback.call if callback
|
38
|
+
end
|
39
|
+
|
40
|
+
watcher.join if synchronize
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def process
|
46
|
+
@pool = Array.new(@size) do
|
47
|
+
Thread.fork do
|
48
|
+
catch :shutdown do
|
49
|
+
loop do
|
50
|
+
job, args = @jobs.pop
|
51
|
+
value = job.call args
|
52
|
+
@result = @formatter.call(@result, args, value) if @formatter
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|