puffy 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.
- 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
|