puffy 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +36 -0
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/.rubocop.yml +39 -0
- data/.simplecov +8 -0
- data/Gemfile +6 -0
- data/README.md +51 -0
- data/Rakefile +20 -0
- data/bin/puffy +17 -0
- data/lib/core_ext.rb +76 -0
- data/lib/puffy/cli.rb +132 -0
- data/lib/puffy/formatters/base.rb +95 -0
- data/lib/puffy/formatters/netfilter.rb +263 -0
- data/lib/puffy/formatters/netfilter4.rb +23 -0
- data/lib/puffy/formatters/netfilter6.rb +23 -0
- data/lib/puffy/formatters/pf.rb +132 -0
- data/lib/puffy/parser.tab.rb +1217 -0
- data/lib/puffy/puppet.rb +73 -0
- data/lib/puffy/resolver.rb +76 -0
- data/lib/puffy/rule.rb +220 -0
- data/lib/puffy/rule_factory.rb +117 -0
- data/lib/puffy/version.rb +5 -0
- data/lib/puffy.rb +62 -0
- data/puffy.gemspec +47 -0
- metadata +254 -0
data/lib/puffy/puppet.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Puffy
|
6
|
+
# Manage nodes rulesets as a tree of rules to serve via Puppet
|
7
|
+
class Puppet
|
8
|
+
# Setup an environment to store firewall rules to disk
|
9
|
+
#
|
10
|
+
# @param path [String] Root directory of the tree of firewall rules
|
11
|
+
# @param parser [Puffy::Parser] A parser with nodes and rules
|
12
|
+
def initialize(path, parser)
|
13
|
+
@path = path
|
14
|
+
@parser = parser
|
15
|
+
|
16
|
+
@formatters = [
|
17
|
+
Puffy::Formatters::Pf::Ruleset.new,
|
18
|
+
Puffy::Formatters::Netfilter4::Ruleset.new,
|
19
|
+
Puffy::Formatters::Netfilter6::Ruleset.new,
|
20
|
+
]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Saves rules to disk
|
24
|
+
#
|
25
|
+
# @return [void]
|
26
|
+
def save
|
27
|
+
each_fragment do |fragment_name, fragment_content|
|
28
|
+
FileUtils.mkdir_p(File.dirname(fragment_name))
|
29
|
+
|
30
|
+
next unless fragment_changed?(fragment_name, fragment_content)
|
31
|
+
|
32
|
+
File.open(fragment_name, 'w') do |f|
|
33
|
+
f.write(fragment_content)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Show differences between saved and generated rules
|
39
|
+
#
|
40
|
+
# @return [void]
|
41
|
+
def diff
|
42
|
+
each_fragment do |fragment_name, fragment_content|
|
43
|
+
human_fragment_name = fragment_name.delete_prefix(@path).delete_prefix('/')
|
44
|
+
IO.popen("diff -u1 -N --unidirectional-new-file --ignore-matching-lines='^#' --label a/#{human_fragment_name} #{fragment_name} --label b/#{human_fragment_name} -", 'r+') do |io|
|
45
|
+
io.write(fragment_content)
|
46
|
+
io.close_write
|
47
|
+
out = io.read
|
48
|
+
$stdout.write out
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def each_fragment
|
56
|
+
@parser.nodes.each do |hostname|
|
57
|
+
rules = @parser.ruleset_for(hostname)
|
58
|
+
policy = @parser.policy_for(hostname)
|
59
|
+
|
60
|
+
@formatters.each do |formatter|
|
61
|
+
filename = File.join(@path, hostname, formatter.filename_fragment)
|
62
|
+
yield filename, formatter.emit_ruleset(rules, policy)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def fragment_changed?(fragment_name, fragment_content)
|
68
|
+
return true unless File.exist?(fragment_name)
|
69
|
+
|
70
|
+
File.read(fragment_name).split("\n").reject { |l| l =~ /^#/ } != fragment_content.split("\n").reject { |l| l =~ /^#/ }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'resolv'
|
4
|
+
require 'singleton'
|
5
|
+
|
6
|
+
module Puffy
|
7
|
+
# DNS resolution class.
|
8
|
+
class Resolver
|
9
|
+
include Singleton
|
10
|
+
|
11
|
+
# Resolve +hostname+ and return an Array of IPAddr.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# Resolver.instance.resolv('localhost')
|
15
|
+
# #=> [#<IPAddr:[::1]>, #<IPAddr:127.0.0.1>]
|
16
|
+
# Resolver.instance.resolv('localhost', :inet)
|
17
|
+
# #=> [#<IPAddr:127.0.0.1>]
|
18
|
+
# Resolver.instance.resolv('localhost', :inet6)
|
19
|
+
# #=> [#<IPAddr:[::1]>]
|
20
|
+
#
|
21
|
+
# @param hostname [String] The hostname to resolve
|
22
|
+
# @param address_family [Symbol] if set, limit search to +address_family+, +:inet+ or +:inet6+
|
23
|
+
# @return [Array<IPAddr>]
|
24
|
+
def resolv(hostname, address_family = nil)
|
25
|
+
if hostname.is_a?(IPAddr)
|
26
|
+
resolv_ipaddress(hostname, address_family)
|
27
|
+
else
|
28
|
+
resolv_hostname(hostname, address_family)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def resolv_ipaddress(address, address_family)
|
35
|
+
filter_af(address, address_family)
|
36
|
+
end
|
37
|
+
|
38
|
+
def filter_af(address, address_family)
|
39
|
+
return [] if address_family && !match_af?(address, address_family)
|
40
|
+
|
41
|
+
[address]
|
42
|
+
end
|
43
|
+
|
44
|
+
def match_af?(address, address_family)
|
45
|
+
(address.ipv6? && address_family == :inet6) ||
|
46
|
+
(address.ipv4? && address_family == :inet)
|
47
|
+
end
|
48
|
+
|
49
|
+
def resolv_hostname(hostname, address_family)
|
50
|
+
result = []
|
51
|
+
result += resolv_hostname_ipv6(hostname) if address_family.nil? || address_family == :inet6
|
52
|
+
result += resolv_hostname_ipv4(hostname) if address_family.nil? || address_family == :inet
|
53
|
+
raise "\"#{hostname}\" does not resolve to any valid IP#{@af_str[address_family]} address." if result.empty?
|
54
|
+
|
55
|
+
result
|
56
|
+
end
|
57
|
+
|
58
|
+
def resolv_hostname_ipv6(hostname)
|
59
|
+
resolv_hostname_record(hostname, Resolv::DNS::Resource::IN::AAAA)
|
60
|
+
end
|
61
|
+
|
62
|
+
def resolv_hostname_ipv4(hostname)
|
63
|
+
resolv_hostname_record(hostname, Resolv::DNS::Resource::IN::A)
|
64
|
+
end
|
65
|
+
|
66
|
+
def resolv_hostname_record(hostname, record)
|
67
|
+
@dns.getresources(hostname, record).collect { |r| IPAddr.new(r.address.to_s) }.sort
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize # :nodoc:
|
71
|
+
config = nil
|
72
|
+
@dns = Resolv::DNS.open(config)
|
73
|
+
@af_str = { inet: 'v4', inet6: 'v6' }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/lib/puffy/rule.rb
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Puffy
|
4
|
+
class AddressFamilyConflict < RuntimeError
|
5
|
+
end
|
6
|
+
|
7
|
+
# Abstract firewall rule.
|
8
|
+
class Rule
|
9
|
+
# @!attribute action
|
10
|
+
# The action to perform when the rule apply (+:accept+ or +:block+).
|
11
|
+
# @return [Symbol] Action
|
12
|
+
# @!attribute return
|
13
|
+
# Whether blocked packets must be returned to sender instead of being silently dropped.
|
14
|
+
# @return [Boolean] Return flag
|
15
|
+
# @!attribute dir
|
16
|
+
# The direction of the rule (+:in+ or +:out+).
|
17
|
+
# @return [Symbol] Direction
|
18
|
+
# @!attribute proto
|
19
|
+
# The protocol the Puffy::Rule applies to (+:tcp+, +:udp+, etc).
|
20
|
+
# @return [Symbol] Protocol
|
21
|
+
# @!attribute af
|
22
|
+
# The address family of the rule (+:inet6+ or +:inet+)
|
23
|
+
# @return [Symbol] Address family
|
24
|
+
# @!attribute on
|
25
|
+
# The interface the rule applies to.
|
26
|
+
# @return [String] Interface
|
27
|
+
# @!attribute in
|
28
|
+
# The interface packets must arrive on for the rule to apply in a forwarding context.
|
29
|
+
# @return [String] Interface
|
30
|
+
# @!attribute out
|
31
|
+
# The interface packets must be sent to for the rule to apply in a forwarding context.
|
32
|
+
# @return [String] Interface
|
33
|
+
# @!attribute from
|
34
|
+
# The packet source as a Hash for the rule to apply.
|
35
|
+
#
|
36
|
+
# :host:: address of the source host or network the rule apply to
|
37
|
+
# :port:: source port the rule apply to
|
38
|
+
# @return [Hash] Source
|
39
|
+
# @!attribute to
|
40
|
+
# The packet destination as a Hash for the rule to apply.
|
41
|
+
#
|
42
|
+
# :host:: address of the destination host or network the rule apply to
|
43
|
+
# :port:: destination port the rule apply to
|
44
|
+
# @return [Hash] Destination
|
45
|
+
# @!attribute nat_to
|
46
|
+
# The packet destination when peforming NAT.
|
47
|
+
# @return [IPAddr] IP Adress
|
48
|
+
# @!attribute rdr_to
|
49
|
+
# The destination as a Hash for redirections.
|
50
|
+
#
|
51
|
+
# :host:: address of the destination host or network the rule apply to
|
52
|
+
# :port:: destination port the rule apply to
|
53
|
+
# @return [Hash] Destination
|
54
|
+
# @!attribute no_quick
|
55
|
+
# Prevent the rule from being a quick one.
|
56
|
+
# @return [Boolean] Quick flag
|
57
|
+
attr_accessor :action, :return, :dir, :proto, :af, :on, :in, :out, :from, :to, :nat_to, :rdr_to, :no_quick
|
58
|
+
|
59
|
+
# Instanciate a firewall Puffy::Rule.
|
60
|
+
#
|
61
|
+
# +options+ is a Hash of the Puffy::Rule class attributes
|
62
|
+
#
|
63
|
+
# Rule.new({ action: :accept, dir: :in, proto: :tcp, to: { port: 80 } })
|
64
|
+
def initialize(options = {})
|
65
|
+
send_options(options)
|
66
|
+
|
67
|
+
@af = detect_af unless af
|
68
|
+
|
69
|
+
raise "unsupported action `#{options[:action]}'" unless valid_action?
|
70
|
+
raise 'if from_port or to_port is specified, the protocol must also be given' if port_without_protocol?
|
71
|
+
end
|
72
|
+
|
73
|
+
# Instanciate a forward Puffy::Rule.
|
74
|
+
#
|
75
|
+
# @param rule [Puffy::Rule] a NAT rule
|
76
|
+
#
|
77
|
+
# @return [Puffy::Rule]
|
78
|
+
def self.fwd_rule(rule)
|
79
|
+
res = rule.dup
|
80
|
+
res.on_to_in_out!
|
81
|
+
res.to.merge!(res.rdr_to.compact)
|
82
|
+
res.rdr_to = nil
|
83
|
+
res.dir = :fwd
|
84
|
+
res
|
85
|
+
end
|
86
|
+
|
87
|
+
# Return true if the rule is valid in an IPv4 context.
|
88
|
+
def ipv4?
|
89
|
+
af.nil? || af == :inet
|
90
|
+
end
|
91
|
+
|
92
|
+
# Return true if the rule has an IPv4 source or destination.
|
93
|
+
def implicit_ipv4?
|
94
|
+
from_ipv4? || to_ipv4? || rdr_to_ipv4? || (rdr_to && af == :inet)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Return true if the rule is valid in an IPv6 context.
|
98
|
+
def ipv6?
|
99
|
+
af.nil? || af == :inet6
|
100
|
+
end
|
101
|
+
|
102
|
+
# Return true if the rule has an IPv6 source or destination.
|
103
|
+
def implicit_ipv6?
|
104
|
+
from_ipv6? || to_ipv6? || rdr_to_ipv6? || (rdr_to && af == :inet6)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Return true if the rule is a filter rule.
|
108
|
+
def filter?
|
109
|
+
!nat? && !rdr?
|
110
|
+
end
|
111
|
+
|
112
|
+
# Returns whether the rule applies to incomming packets.
|
113
|
+
def in?
|
114
|
+
dir.nil? || dir == :in
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns whether the rule applies to outgoing packets.
|
118
|
+
def out?
|
119
|
+
dir.nil? || dir == :out
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns whether the rule performs Network Address Translation.
|
123
|
+
def nat?
|
124
|
+
nat_to
|
125
|
+
end
|
126
|
+
|
127
|
+
# Returns whether the rule is a redirection.
|
128
|
+
def rdr?
|
129
|
+
rdr_to_host || rdr_to_port
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns whether the rule performs forwarding.
|
133
|
+
def fwd?
|
134
|
+
dir == :fwd
|
135
|
+
end
|
136
|
+
|
137
|
+
# @!method from_host
|
138
|
+
# Returns the source host of the Puffy::Rule.
|
139
|
+
# @!method from_port
|
140
|
+
# Returns the source port of the Puffy::Rule.
|
141
|
+
# @!method to_host
|
142
|
+
# Returns the destination host of the Puffy::Rule.
|
143
|
+
# @!method to_port
|
144
|
+
# Returns the destination port of the Puffy::Rule.
|
145
|
+
# @!method rdr_to_host
|
146
|
+
# Returns the redirect destination host of the Puffy::Rule.
|
147
|
+
# @!method rdr_to_port
|
148
|
+
# Returns the redirect destination port of the Puffy::Rule.
|
149
|
+
%i[from to rdr_to].each do |destination|
|
150
|
+
%i[host port].each do |param|
|
151
|
+
define_method("#{destination}_#{param}") do
|
152
|
+
res = public_send(destination)
|
153
|
+
res && res[param]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Setsthe #in / #out to #on depending on #dir.
|
159
|
+
#
|
160
|
+
# @return [void]
|
161
|
+
def on_to_in_out!
|
162
|
+
if dir == :in
|
163
|
+
self.in ||= on
|
164
|
+
else
|
165
|
+
self.out ||= on
|
166
|
+
end
|
167
|
+
self.on = nil
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def valid_action?
|
173
|
+
[nil, :pass, :block].include?(action)
|
174
|
+
end
|
175
|
+
|
176
|
+
def port_without_protocol?
|
177
|
+
(from_port || to_port) && proto.nil?
|
178
|
+
end
|
179
|
+
|
180
|
+
def send_options(options)
|
181
|
+
options.each do |k, v|
|
182
|
+
send("#{k}=", v)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def detect_af
|
187
|
+
afs = collect_afs
|
188
|
+
return nil if afs.empty?
|
189
|
+
return afs.first if afs.one?
|
190
|
+
|
191
|
+
raise AddressFamilyConflict, "Incompatible address famlilies: #{afs}"
|
192
|
+
end
|
193
|
+
|
194
|
+
def collect_afs
|
195
|
+
%i[from_host to_host rdr_to_host].map do |method|
|
196
|
+
res = send(method)
|
197
|
+
if res.nil? then nil
|
198
|
+
elsif res.ipv4? then :inet
|
199
|
+
elsif res.ipv6? then :inet6
|
200
|
+
else raise 'Fail'
|
201
|
+
end
|
202
|
+
end.uniq.compact
|
203
|
+
end
|
204
|
+
|
205
|
+
# @!method from_ipv4?
|
206
|
+
# @!method from_ipv6?
|
207
|
+
# @!method to_ipv4?
|
208
|
+
# @!method to_ipv6?
|
209
|
+
# @!method rdr_to_ipv4?
|
210
|
+
# @!method rdr_to_ipv6?
|
211
|
+
%i[from to rdr_to].each do |destination|
|
212
|
+
%i[ipv4 ipv6].each do |ip_version|
|
213
|
+
define_method("#{destination}_#{ip_version}?") do
|
214
|
+
res = public_send("#{destination}_host")
|
215
|
+
res&.public_send("#{ip_version}?")
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Puffy
|
4
|
+
# Puffy::Rule factory
|
5
|
+
class RuleFactory
|
6
|
+
# Initialize a Puffy::Rule factory.
|
7
|
+
def initialize
|
8
|
+
@af = nil
|
9
|
+
@resolver = Resolver.instance
|
10
|
+
load_services
|
11
|
+
end
|
12
|
+
|
13
|
+
# Limit the scope of a set of rules to IPv4 only.
|
14
|
+
def ipv4
|
15
|
+
raise 'Address familly already scopped' if @af
|
16
|
+
|
17
|
+
@af = :inet
|
18
|
+
yield
|
19
|
+
@af = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
# Limit the scope of a set of rules to IPv6 only.
|
23
|
+
def ipv6
|
24
|
+
raise 'Address familly already scopped' if @af
|
25
|
+
|
26
|
+
@af = :inet6
|
27
|
+
yield
|
28
|
+
@af = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# Return an Array of Puffy::Rule for the provided +options+.
|
32
|
+
# @param [Hash] options
|
33
|
+
# @return [Array<Puffy::Rule>]
|
34
|
+
def build(options = {})
|
35
|
+
return [] if options == {}
|
36
|
+
|
37
|
+
options = { action: nil, return: false, dir: nil, af: nil, proto: nil, on: nil, from: { host: nil, port: nil }, to: { host: nil, port: nil }, nat_to: nil, rdr_to: { host: nil, port: nil } }.merge(options)
|
38
|
+
|
39
|
+
options = resolv_hostnames_and_ports(options)
|
40
|
+
instanciate_rules(options)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def resolv_hostnames_and_ports(options)
|
46
|
+
%i[from to rdr_to].each do |endpoint|
|
47
|
+
options[endpoint][:host] = host_lookup(options[endpoint][:host])
|
48
|
+
options[endpoint][:port] = port_lookup(options[endpoint][:port])
|
49
|
+
end
|
50
|
+
options[:nat_to] = host_lookup(options[:nat_to])
|
51
|
+
options
|
52
|
+
end
|
53
|
+
|
54
|
+
def instanciate_rules(options)
|
55
|
+
options.expand.map do |hash|
|
56
|
+
rule = Rule.new(hash)
|
57
|
+
rule if af_match_policy?(rule.af)
|
58
|
+
rescue AddressFamilyConflict
|
59
|
+
nil
|
60
|
+
end.compact
|
61
|
+
end
|
62
|
+
|
63
|
+
def load_services
|
64
|
+
@services = {}
|
65
|
+
File.readlines('/etc/services').each do |line|
|
66
|
+
line.sub!(/#.*/, '')
|
67
|
+
pieces = line.split
|
68
|
+
next if pieces.count < 2
|
69
|
+
|
70
|
+
port = pieces.delete_at(1).to_i
|
71
|
+
pieces.each do |piece|
|
72
|
+
@services[piece] = port
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def af_match_policy?(af)
|
78
|
+
@af.nil? || af.nil? || af == @af
|
79
|
+
end
|
80
|
+
|
81
|
+
def host_lookup(host)
|
82
|
+
case host
|
83
|
+
when nil then nil
|
84
|
+
when IPAddr then host
|
85
|
+
when String then @resolver.resolv(host)
|
86
|
+
when Array then host.map { |x| @resolver.resolv(x) }.flatten
|
87
|
+
else
|
88
|
+
raise "Unexpected #{host.class.name}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def port_lookup(port)
|
93
|
+
case port
|
94
|
+
when nil then nil
|
95
|
+
when Integer, Range then port
|
96
|
+
when String then real_port_lookup(port)
|
97
|
+
when Array then port.map { |x| port_lookup(x) }
|
98
|
+
else
|
99
|
+
raise "Unexpected #{port.class.name}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def real_port_lookup(port)
|
104
|
+
res = port_is_a_number(port) || @services[port]
|
105
|
+
|
106
|
+
raise "unknown service \"#{port}\"" unless res
|
107
|
+
|
108
|
+
res
|
109
|
+
end
|
110
|
+
|
111
|
+
def port_is_a_number(port)
|
112
|
+
Integer(port)
|
113
|
+
rescue ArgumentError
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/puffy.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'core_ext'
|
4
|
+
|
5
|
+
require 'puffy/parser.tab'
|
6
|
+
require 'puffy/formatters/base'
|
7
|
+
require 'puffy/formatters/netfilter'
|
8
|
+
require 'puffy/formatters/netfilter4'
|
9
|
+
require 'puffy/formatters/netfilter6'
|
10
|
+
require 'puffy/formatters/pf'
|
11
|
+
require 'puffy/puppet'
|
12
|
+
require 'puffy/resolver'
|
13
|
+
require 'puffy/rule'
|
14
|
+
require 'puffy/rule_factory'
|
15
|
+
require 'puffy/version'
|
16
|
+
|
17
|
+
module Puffy
|
18
|
+
class PuffyError < RuntimeError
|
19
|
+
def initialize(message, token)
|
20
|
+
super(message)
|
21
|
+
@token = token
|
22
|
+
end
|
23
|
+
|
24
|
+
def filename
|
25
|
+
@token[:filename]
|
26
|
+
end
|
27
|
+
|
28
|
+
def lineno
|
29
|
+
@token[:lineno]
|
30
|
+
end
|
31
|
+
|
32
|
+
def line
|
33
|
+
@token[:line]
|
34
|
+
end
|
35
|
+
|
36
|
+
def position
|
37
|
+
@token[:position]
|
38
|
+
end
|
39
|
+
|
40
|
+
def length
|
41
|
+
@token.fetch(:length, 1)
|
42
|
+
end
|
43
|
+
|
44
|
+
def extra
|
45
|
+
'~' * (length - 1)
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s
|
49
|
+
<<~MESSAGE
|
50
|
+
#{filename}:#{lineno}:#{position + 1}: #{super}
|
51
|
+
#{line}
|
52
|
+
#{' ' * position}^#{extra}
|
53
|
+
MESSAGE
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class ParseError < PuffyError
|
58
|
+
end
|
59
|
+
|
60
|
+
class SyntaxError < PuffyError
|
61
|
+
end
|
62
|
+
end
|
data/puffy.gemspec
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/puffy/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'puffy'
|
7
|
+
spec.version = Puffy::VERSION
|
8
|
+
spec.authors = ['Romain Tartière']
|
9
|
+
spec.email = ['romain@blogreen.org']
|
10
|
+
|
11
|
+
spec.summary = 'Network firewall rules made easy!'
|
12
|
+
spec.homepage = 'https://github.com/opus-codium/puffy'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
|
15
|
+
|
16
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org/'
|
17
|
+
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
19
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
20
|
+
spec.metadata['changelog_uri'] = spec.homepage
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
25
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } -
|
26
|
+
['lib/puffy/parser.y'] +
|
27
|
+
['lib/puffy/parser.tab.rb']
|
28
|
+
end
|
29
|
+
spec.bindir = 'bin'
|
30
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ['lib']
|
32
|
+
|
33
|
+
spec.add_runtime_dependency 'cri'
|
34
|
+
spec.add_runtime_dependency 'deep_merge'
|
35
|
+
|
36
|
+
spec.add_development_dependency 'aruba'
|
37
|
+
spec.add_development_dependency 'bundler'
|
38
|
+
spec.add_development_dependency 'cucumber'
|
39
|
+
spec.add_development_dependency 'racc'
|
40
|
+
spec.add_development_dependency 'rake'
|
41
|
+
spec.add_development_dependency 'rspec'
|
42
|
+
spec.add_development_dependency 'rubocop'
|
43
|
+
spec.add_development_dependency 'rubocop-rake'
|
44
|
+
spec.add_development_dependency 'rubocop-rspec'
|
45
|
+
spec.add_development_dependency 'simplecov'
|
46
|
+
spec.add_development_dependency 'timecop'
|
47
|
+
end
|