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.
@@ -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
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "slop", "~> 4.4.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "slop", "~> 4.5.0"
6
+
7
+ gemspec path: "../"
@@ -1,5 +1,69 @@
1
- module Portfinder
2
- end
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
- require 'portfinder/scanner'
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,9 @@
1
+ module Portfinder
2
+ # Generic Error baseclass for Portfinder specific errors
3
+ class Error < StandardError
4
+ end
5
+
6
+ # Portfinder Error class for invalid hosts
7
+ class InvalidHost < Error
8
+ end
9
+ 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