catflap 0.0.2 → 1.0.1

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,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'catflap'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require 'pry'
11
+ Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'catflap/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'catflap'
8
+ s.version = Catflap::VERSION
9
+ s.summary = 'Manage NetFilter-based rules to grant port access on-demand ' \
10
+ 'commandline or REST API requests.'
11
+ s.description = 'A simple solution to provide on-demand service access (e.g.
12
+ port 80 on webserver), where a more robust and secure VPN solution is not
13
+ available. Essentially, it is a more user-friendly form of "port knocking".
14
+ The original proof-of-concept implementation was run for almost three years
15
+ by Demotix, to protect development and staging servers from search engine
16
+ crawlers and other unwanted traffic.'
17
+ s.authors = ['Nyk Cowham']
18
+ s.email = 'nykcowham@gmail.com'
19
+ s.homepage = 'https://github.com/nyk/catflap'
20
+ s.files = `git ls-files -z`
21
+ .split("\x0")
22
+ .reject { |f| f.match(%r{^(test|spec|tasks|features)/}) }
23
+ s.executables = ['catflap']
24
+ s.licenses = ['MIT']
25
+ s.requirements = 'NetFilters (iptables) installed and working.'
26
+ s.require_paths = ['lib']
27
+ s.add_dependency 'json', '>= 1.8.3'
28
+ s.add_development_dependency 'bundler', '~> 1.11'
29
+ s.add_development_dependency 'rake', '~> 10.0'
30
+ s.add_development_dependency 'rspec', '~> 3.0'
31
+ s.bindir = 'bin'
32
+ end
@@ -0,0 +1,30 @@
1
+ # This is an example of the main configuration file for catflap.
2
+ # By default catflap looks for this file in /usr/local/etc/catflap/config.yaml
3
+ # but you can change the location by using the --config-file <filepath> option
4
+ # of the bin/catflap command line interface.
5
+
6
+ server:
7
+ listen_addr: '0.0.0.0' # what ip address the catflap server should listen on.
8
+ port: 8778 # the TCP port that the catflap server listens to.
9
+ docroot: './ui' # you can override the ui location.
10
+ endpoint: '/catflap' # the endpoint for the REST API.
11
+ passfile: './etc/passfile.yaml' # pass phrases are stored here.
12
+ token_ttl: 15 # expire tokens after 15 seconds.
13
+ pid_path: '/var/run' # The path where the pid file should be written.
14
+
15
+ https:
16
+ port: 4773 # TCP oport that catflap https server listens to.
17
+ force: true # Force HTTP requests to redirect to HTTPS.
18
+ certificate: '' # Path to your SSL certificate file.
19
+ private_key: '' # Private key for your SSL certificate.
20
+
21
+ firewall:
22
+ plugin: 'netfilter' # options are netfilter or iptables
23
+ dports: '80,443' # lock multiple ports separating them by commas.
24
+ options: # options are specific to each firewall plugin driver.
25
+ chain: 'CATFLAP' # Namespace for the chains (e.g. CATFLAP-ALLOW, CATFLAP-DENY).
26
+ forward:
27
+ 80: 8778
28
+ 443: 4773
29
+ log_rejected: true
30
+ accept_local: true # this is set to false only when devs are testing catflap.
@@ -0,0 +1,89 @@
1
+ ### BEGIN INIT INFO
2
+ # Provides: catflap
3
+ # Default-Start: 2 3 4 5
4
+ # Default-Stop: 0 1 6
5
+ # Short-Description: Catflap daemon to provide API web service.
6
+ # Description: Provides a means to request and gain access
7
+ # to certain restricted ports like 80 and 443
8
+ ### END INIT INFO
9
+
10
+ # Using the lsb functions to perform the operations.
11
+ . /lib/lsb/init-functions
12
+ # Process name ( For display )
13
+ NAME=catflap
14
+ # Daemon name, where is the actual executable
15
+ DAEMON=/usr/local/bin/catflap
16
+
17
+ # pid file for the daemon
18
+ PIDFILE=/var/run/catflap.pid
19
+
20
+ # Include catflap defaults if available
21
+ if [ -f /etc/default/catflap ] ; then
22
+ . /etc/default/catflap
23
+ fi
24
+
25
+ # If the daemon is not there, then exit.
26
+ test -x $DAEMON || exit 5
27
+
28
+ case $1 in
29
+ start)
30
+ # Checked the PID file exists and check the actual status of process
31
+ if [ -e $PIDFILE ]; then
32
+ status_of_proc -p $PIDFILE $DAEMON "$NAME process" && status="0" || status="$?"
33
+ # If the status is SUCCESS then don't need to start again.
34
+ if [ $status = "0" ]; then
35
+ exit # Exit
36
+ fi
37
+ fi
38
+ # Start the daemon.
39
+ log_daemon_msg "Starting the process" "$NAME"
40
+ # Start the daemon with the help of start-stop-daemon
41
+ # Log the message appropriately
42
+ if start-stop-daemon --start --quiet --oknodo --background --make-pidfile --pidfile $PIDFILE --exec $DAEMON -- $DAEMON_OPTS " start" 2>&1; then
43
+ log_end_msg 0
44
+ else
45
+ log_end_msg 1
46
+ fi
47
+ ;;
48
+ stop)
49
+ # Stop the daemon.
50
+ if [ -e $PIDFILE ]; then
51
+ status_of_proc -p $PIDFILE $DAEMON "Stoppping the $NAME process" && status="0" || status="$?"
52
+ if [ "$status" = 0 ]; then
53
+ start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE
54
+ /bin/rm -rf $PIDFILE
55
+ fi
56
+ else
57
+ log_daemon_msg "$NAME process is not running"
58
+ log_end_msg 0
59
+ fi
60
+ ;;
61
+ restart)
62
+ # Restart the daemon.
63
+ $0 stop && sleep 2 && $0 start
64
+ ;;
65
+ status)
66
+ # Check the status of the process.
67
+ if [ -e $PIDFILE ]; then
68
+ status_of_proc -p $PIDFILE $DAEMON "$NAME process" && exit 0 || exit $?
69
+ else
70
+ log_daemon_msg "$NAME Process is not running"
71
+ log_end_msg 0
72
+ fi
73
+ ;;
74
+ reload)
75
+ # Reload the process. Basically sending some signal to a daemon to reload
76
+ # it configurations.
77
+ if [ -e $PIDFILE ]; then
78
+ start-stop-daemon --stop --signal USR1 --quiet --pidfile $PIDFILE --name $NAME
79
+ log_success_msg "$NAME process reloaded successfully"
80
+ else
81
+ log_failure_msg "$PIDFILE does not exists"
82
+ fi
83
+ ;;
84
+ *)
85
+ # For invalid arguments, print the usage message.
86
+ echo "Usage: $0 {start|stop|restart|reload|status}"
87
+ exit 2
88
+ ;;
89
+ esac
@@ -0,0 +1,7 @@
1
+ # This is a sample passfile. It is recommened that you use your own
2
+ # pass phrases. A pass phrase must consist of at least two words separated by
3
+ # a space. The first word of the phrase is used as a unique key for efficient
4
+ # lookup of multiple pass phrases.
5
+ passphrases:
6
+ frisky: 'frisky kitten'
7
+ peeky: 'peeky blinders'
@@ -1,87 +1,131 @@
1
+ require 'catflap/version'
1
2
  require 'yaml'
2
- require 'ipaddr'
3
+ require 'digest'
3
4
 
5
+ ##
6
+ # Catflap controller class
7
+ #
8
+ # This class controls the initialization of the configuration file, setting
9
+ # options, initializing the firewall plugin driver and loading pass phrases.
10
+ #
11
+ # @author Nyk Cowham <nykcowham@gmail.com>
4
12
  class Catflap
13
+ # @!attribute noop
14
+ # @return [Boolean] true if no operation is to be performed.
15
+ # @!attribute verbose
16
+ # @return [Boolean] true if command output should be written to stdout.
17
+ # @!attribute [r] fwplugin
18
+ # @return [String] the name of the firewall driver plugin file and class.
19
+ # @!attribute [r] bind_addr
20
+ # @return [String] the IP address the Catflap server should listen to.
21
+ # @!attribute [r] port
22
+ # @return [Integer] the port number the Catflap server should listen to.
23
+ # @!attribute [r] docroot
24
+ # @return [String] file path location that HTML/CSS/JS files are located.
25
+ # @!attribute [r] endpoint
26
+ # @return [String] the name of the REST service endpoint (e.g. /catflap).
27
+ # @!attribute [r] https
28
+ # @return [Hash<Symbol, String>] the HTTPS server configuration options.
29
+ # @!attribute [r] daemonize
30
+ # @return [Boolean] true if the web server should run in the background.
31
+ # @!attribute [r] dports
32
+ # @return [String] comma-separated list of ports to guard. (e.g. '80,443').
33
+ # @!attribute [r] passphrases
34
+ # @return [Hash<String, String>] associative array of pass phrases. The
35
+ # key is the first word of the pass phrase where words are separated by
36
+ # a space or non-alphanumeric character (except for the underscore).
37
+ # @!attribute [r] passfile
38
+ # @return [String] file path of the YAML file containg pass phrases.
39
+ # @!attribute [r] token_ttl
40
+ # @return [Integer] the time-to-live in seconds before tokens expire.
41
+ # @!attribute [r] pid_path
42
+ # @return [String] path/directory where pid file should be written.
43
+ # @!attribute [r] redirect_url
44
+ # @return [String] URL browser should be redirect to after authentication.
45
+ # @!attribute [r] firewall
46
+ # @return [FirewallPlugin]
47
+ # a firewall object that is implemented by the firewall driver plugin.
5
48
 
6
- attr_accessor :config, :chain, :port, :dports, :print, :noop, :log_rejected
49
+ attr_accessor :verbose, :noop, :daemonize
7
50
 
8
- def initialize config_file
9
- config_file = config_file || '/usr/local/etc/catflap.yaml'
10
- @config = {}
11
- @config = YAML.load_file config_file if File.readable? config_file
12
- @port = @config['server']['port'] || 4777
13
- @chain = @config['rules']['chain'] || 'catflap-accept'
14
- @dports = @config['rules']['dports'] || '80,443'
15
- @print = false
16
- @noop = false
17
- @log_rejected = true
18
- end
51
+ attr_reader :fwplugin, :listen_addr, :port, :docroot, :endpoint, :dports,
52
+ :passfile, :passphrases, :token_ttl, :pid_path, :redirect_url,
53
+ :firewall, :https
19
54
 
20
- def install_rules!
21
- output = "iptables -N #{@chain}\n" # Create a new user-defined chain as a container for our catflap netfilter rules
22
- output << "iptables -A #{@chain} -s 127.0.0.1 -p tcp -m multiport --dports #{@dports} -j ACCEPT\n" # Accept packets to localhost
23
- output << "iptables -A INPUT -p tcp -m multiport --dports #{@dports} -j #{@chain}\n" # Jump from INPUT chain to the catflap chain
24
- output << "iptables -A INPUT -p tcp -m multiport --dports #{@dports} -j LOG\n" if @log_rejected # Log any rejected packets to /var/log/messages
25
- output << "iptables -A INPUT -p tcp -m multiport --dports #{@dports} -j DROP\n" # Drop any other packets to the ports on the INPUT chain
26
- execute! output
55
+ # @param [String, nil] file_path file path of the YAML configuration file.
56
+ # @param [Boolean] noop set to true to suppress destructive operations.
57
+ # @param [Boolean] verbose set to true to print operations to stdout stream.
58
+ # @return [Catflap]
59
+ def initialize(file_path = nil, noop = false, verbose = false)
60
+ @noop = noop
61
+ @verbose = verbose
62
+ initialize_config(file_path)
63
+ initialize_firewall_plugin
64
+ load_passphrases
27
65
  end
28
66
 
29
- def uninstall_rules!
30
- output = "iptables -D INPUT -p tcp -m multiport --dports #{@dports} -j #{@chain}\n" # Remove user-defined chain from INPUT chain
31
- output << "iptables -F #{@chain}\n" # Flush the catflap user-defined chain
32
- output << "iptables -X #{@chain}\n" # Remove the catflap chain
33
- output << "iptables -D INPUT -p tcp -m multiport --dports #{@dports} -j LOG\n" # Remove the logging rule
34
- output << "iptables -D INPUT -p tcp -m multiport --dports #{@dports} -j DROP\n" # Remove the packet dropping rule
35
- execute! output
36
- end
67
+ # Initialize the configuration options from the YAML configuration file.
68
+ # @param [String, nil] file_path file path of the YAML configuration file.
69
+ # @return void
70
+ def initialize_config(file_path = nil)
71
+ @config = read_config_file(file_path)
72
+ alias_server_attributes
37
73
 
38
- def purge_rules!
39
- output = "iptables -F #{@chain}"
40
- execute! output
74
+ @https ||= {}
41
75
  end
42
76
 
43
- def list_rules
44
- system "iptables -S #{@chain}"
77
+ # Alias the server configuration attributes to instance variables.
78
+ def alias_server_attributes
79
+ @config['server'].each do |key, value|
80
+ instance_variable_set('@' + key, value)
81
+ end
45
82
  end
46
83
 
47
- def check_address ip
48
- check_user_input ip
49
- return system "iptables -C #{@chain} -s #{ip} -p tcp -m multiport --dports #{@dports} -j ACCEPT\n"
50
- end
84
+ # Find YAML configuration file, load and read it.
85
+ # @param [String, nil] file_path if one was passed on command line.
86
+ # @return [Hash] the parsed YAML file as a nested hash.
87
+ def read_config_file(file_path)
88
+ # Look for config file in order of precedence.
89
+ unless file_path
90
+ ['~/.catflap', '/usr/local/etc/catflap', '/etc/catflap'].each do |path|
91
+ file = File.expand_path(path + '/config.yaml')
92
+ if File.readable? file
93
+ file_path = file
94
+ break
95
+ end
96
+ end
97
+ end
51
98
 
52
- def add_address! ip
53
- check_user_input ip
54
- output = "iptables -I #{@chain} 1 -s #{ip} -p tcp -m multiport --dports #{@dports} -j ACCEPT\n"
55
- execute! output
99
+ YAML.load_file(file_path || './etc/config.yaml')
56
100
  end
57
101
 
58
- def delete_address! ip
59
- check_user_input ip
60
- output = "iptables -D #{@chain} -s #{ip} -p tcp -m multiport --dports #{@dports} -j ACCEPT\n"
61
- execute! output
62
- end
102
+ # Initialize the firewall plugin driver.
103
+ # @return [FirewallPlugin] an object of a class inheriting from FirewallPlugin
104
+ def initialize_firewall_plugin
105
+ plugin = @config['firewall']['plugin']
106
+ driver = plugin.capitalize + 'Driver'
107
+ require_relative "catflap/plugins/firewall/#{plugin}.rb"
108
+ @firewall =
109
+ Object.const_get(driver).new(@config, @noop, @verbose)
110
+ end
63
111
 
64
- def add_addresses_from_file! filepath
65
- if File.readable? filepath
66
- output = ""
67
- File.open(filepath, "r").each_line do |ip|
68
- check_user_input ip
69
- output << "iptables -I #{@chain} 1 -s #{ip.chomp} -p tcp -m multiport --dports #{@dports} -j ACCEPT\n"
70
- end
71
- execute! output
112
+ # Load the pass phrase YAML file.
113
+ # @raise [IOError] if the file is missing or not readable.
114
+ # @return void
115
+ def load_passphrases
116
+ if @passfile && File.readable?(@passfile)
117
+ phrases = YAML.load_file(@passfile)
118
+ @passphrases = phrases['passphrases']
72
119
  else
73
- puts "The file #{filepath} is not readable!"
74
- exit 1
120
+ raise IOError, "Cannot read the passfile: #{@passfile}!"
75
121
  end
76
122
  end
77
123
 
78
- def execute! output
79
- if @print then puts output end
80
- system output unless @noop
81
- end
82
-
83
- def check_user_input suspect
84
- return IPAddr.new(suspect)
124
+ # Generates a SHA256 encrypted token based on a passphrase and a timestamp
125
+ # @param [String] pass the passphrase stored in passfile.
126
+ # @param [String] timestamp to salt the digest.
127
+ # @return [String] a token in the form of a SHA256 digest of a string.
128
+ def generate_token(pass, salt)
129
+ Digest::SHA256.hexdigest(pass + salt)
85
130
  end
86
-
87
131
  end
@@ -0,0 +1,102 @@
1
+ require 'catflap'
2
+ require 'catflap/http'
3
+
4
+ ##
5
+ # Command controller class.
6
+ #
7
+ # This class separates the implementation details of the command line
8
+ # interface from the actual interface. This allows for creating custom
9
+ # command tools by directly implementing this class.
10
+ # @author Nyk Cowham <nykcowham@gmail.com>
11
+ class CfCommand
12
+ include CfWebserver
13
+
14
+ # Initialize a new CatflapCli object
15
+ # @param [Hash<String, String>] options an associative array of options
16
+ # read from the configuration file built by Catflap::initialize_config().
17
+ # @return CatflapCli
18
+ # @see
19
+ # Catflap - options are generated from file: Catflap::initialize_config()
20
+ def initialize(options)
21
+ @options = options
22
+ @cf = Catflap.new(
23
+ options[:config_file], options[:noop], options[:verbose]
24
+ )
25
+ @cf.daemonize = @options[:daemonize]
26
+ end
27
+
28
+ # A handler function to dispatch commands received from the front-end to the
29
+ # firewall driver class or the Catflap web service.
30
+ # @param [String] command the command that is to be executed.
31
+ # @param [String] arg and argument for the command, (e.g. an IP address).
32
+ # @return void
33
+ # @raise ArgumentError when a required command argument is missing.
34
+ # @raise NameError when the command is not recognized.
35
+ def dispatch_commands(command, arg)
36
+ # handle commands and options.
37
+ case command
38
+ when 'version'
39
+ "Catflap version #{Catflap::VERSION}"
40
+ when 'start'
41
+ server_start(@cf, @options[:https])
42
+ when 'stop'
43
+ server_stop(@cf, @options[:https])
44
+ when 'status'
45
+ server_status(@cf, @options[:https])
46
+ when 'restart'
47
+ begin
48
+ server_stop(@cf, @options[:https])
49
+ server_start(@cf, @options[:https])
50
+ end
51
+ when 'reload'
52
+ @cf.load_passphrases
53
+ when 'purge'
54
+ @cf.firewall.purge_rules
55
+ when 'install'
56
+ @cf.firewall.install_rules
57
+ when 'uninstall'
58
+ @cf.firewall.uninstall_rules
59
+ when 'list'
60
+ puts @cf.firewall.list_rules
61
+ when 'grant'
62
+ raise ArgumentError, 'You must provide a valid IP address' if arg.nil?
63
+ @cf.firewall.add_address(arg) unless @cf.firewall.check_address(arg)
64
+ when 'revoke'
65
+ raise ArgumentError, 'You must provide a valid IP address' if arg.nil?
66
+ @cf.firewall.delete_address(arg) if @cf.firewall.check_address(arg)
67
+ when 'check'
68
+ raise ArgumentError, 'You must provide a valid IP address' unless arg
69
+ return @cf.firewall.check_address(arg)
70
+ when 'bulkload'
71
+ raise ArgumentError, 'You must provide a file path' unless arg
72
+ add_addresses_from_file(arg)
73
+ when nil # catflap --version can be run with no command, so that's ok.
74
+ else
75
+ raise NameError, "there is no command '#{command}'"
76
+ end
77
+ end
78
+ # rubocop:enable Metrics/CyclomaticComplexity,Metrics/MethodLength
79
+ # rubocop:enable Metrics/PerceivedComplexity,Metrics/AbcSize
80
+
81
+ # Handler function to bulkload IP's to the firewall.
82
+ #
83
+ # Checking that the file path points to readable file ensures that we can
84
+ # safely accept the user-submitted parameter without any additional data
85
+ # sanitization.
86
+ # @param [String] filepath path to the bulkload file of IP addresses to add.
87
+ # @return void
88
+ # @raise IOError if the file cannot be found or is not readable.
89
+ #
90
+ # @note Every IP address in the file is validated to ensure that it resolves
91
+ # to a valid IP address.
92
+ # @see Firewall Firewall::assert_valid_ipaddr(ip)
93
+ def add_addresses_from_file(filepath)
94
+ if File.readable? filepath
95
+ File.open(filepath, 'r').each_line do |ip|
96
+ @cf.firewall.add_address(ip.chomp)
97
+ end
98
+ else
99
+ raise IOError, "The file #{filepath} is not readable!"
100
+ end
101
+ end
102
+ end