1# Copyright (c) 2000,2002,2003 Masatoshi SEKI
2#
3# acl.rb is copyrighted free software by Masatoshi SEKI.
4# You can redistribute it and/or modify it under the same terms as Ruby.
5
6require 'ipaddr'
7
8##
9# Simple Access Control Lists.
10#
11# Access control lists are composed of "allow" and "deny" halves to control
12# access.  Use "all" or "*" to match any address.  To match a specific address
13# use any address or address mask that IPAddr can understand.
14#
15# Example:
16#
17#   list = %w[
18#     deny all
19#     allow 192.168.1.1
20#     allow ::ffff:192.168.1.2
21#     allow 192.168.1.3
22#   ]
23#
24#   # From Socket#peeraddr, see also ACL#allow_socket?
25#   addr = ["AF_INET", 10, "lc630", "192.168.1.3"]
26#
27#   acl = ACL.new
28#   p acl.allow_addr?(addr) # => true
29#
30#   acl = ACL.new(list, ACL::DENY_ALLOW)
31#   p acl.allow_addr?(addr) # => true
32
33class ACL
34
35  ##
36  # The current version of ACL
37
38  VERSION=["2.0.0"]
39
40  ##
41  # An entry in an ACL
42
43  class ACLEntry
44
45    ##
46    # Creates a new entry using +str+.
47    #
48    # +str+ may be "*" or "all" to match any address, an IP address string
49    # to match a specific address, an IP address mask per IPAddr, or one
50    # containing "*" to match part of an IPv4 address.
51
52    def initialize(str)
53      if str == '*' or str == 'all'
54        @pat = [:all]
55      elsif str.include?('*')
56        @pat = [:name, dot_pat(str)]
57      else
58        begin
59          @pat = [:ip, IPAddr.new(str)]
60        rescue ArgumentError
61          @pat = [:name, dot_pat(str)]
62        end
63      end
64    end
65
66    private
67
68    ##
69    # Creates a regular expression to match IPv4 addresses
70
71    def dot_pat_str(str)
72      list = str.split('.').collect { |s|
73        (s == '*') ? '.+' : s
74      }
75      list.join("\\.")
76    end
77
78    private
79
80    ##
81    # Creates a Regexp to match an address.
82
83    def dot_pat(str)
84      exp = "^" + dot_pat_str(str) + "$"
85      Regexp.new(exp)
86    end
87
88    public
89
90    ##
91    # Matches +addr+ against this entry.
92
93    def match(addr)
94      case @pat[0]
95      when :all
96        true
97      when :ip
98        begin
99          ipaddr = IPAddr.new(addr[3])
100          ipaddr = ipaddr.ipv4_mapped if @pat[1].ipv6? && ipaddr.ipv4?
101        rescue ArgumentError
102          return false
103        end
104        (@pat[1].include?(ipaddr)) ? true : false
105      when :name
106        (@pat[1] =~ addr[2]) ? true : false
107      else
108        false
109      end
110    end
111  end
112
113  ##
114  # A list of ACLEntry objects.  Used to implement the allow and deny halves
115  # of an ACL
116
117  class ACLList
118
119    ##
120    # Creates an empty ACLList
121
122    def initialize
123      @list = []
124    end
125
126    public
127
128    ##
129    # Matches +addr+ against each ACLEntry in this list.
130
131    def match(addr)
132      @list.each do |e|
133        return true if e.match(addr)
134      end
135      false
136    end
137
138    public
139
140    ##
141    # Adds +str+ as an ACLEntry in this list
142
143    def add(str)
144      @list.push(ACLEntry.new(str))
145    end
146
147  end
148
149  ##
150  # Default to deny
151
152  DENY_ALLOW = 0
153
154  ##
155  # Default to allow
156
157  ALLOW_DENY = 1
158
159  ##
160  # Creates a new ACL from +list+ with an evaluation +order+ of DENY_ALLOW or
161  # ALLOW_DENY.
162  #
163  # An ACL +list+ is an Array of "allow" or "deny" and an address or address
164  # mask or "all" or "*" to match any address:
165  #
166  #   %w[
167  #     deny all
168  #     allow 192.0.2.2
169  #     allow 192.0.2.128/26
170  #   ]
171
172  def initialize(list=nil, order = DENY_ALLOW)
173    @order = order
174    @deny = ACLList.new
175    @allow = ACLList.new
176    install_list(list) if list
177  end
178
179  public
180
181  ##
182  # Allow connections from Socket +soc+?
183
184  def allow_socket?(soc)
185    allow_addr?(soc.peeraddr)
186  end
187
188  public
189
190  ##
191  # Allow connections from addrinfo +addr+?  It must be formatted like
192  # Socket#peeraddr:
193  #
194  #   ["AF_INET", 10, "lc630", "192.0.2.1"]
195
196  def allow_addr?(addr)
197    case @order
198    when DENY_ALLOW
199      return true if @allow.match(addr)
200      return false if @deny.match(addr)
201      return true
202    when ALLOW_DENY
203      return false if @deny.match(addr)
204      return true if @allow.match(addr)
205      return false
206    else
207      false
208    end
209  end
210
211  public
212
213  ##
214  # Adds +list+ of ACL entries to this ACL.
215
216  def install_list(list)
217    i = 0
218    while i < list.size
219      permission, domain = list.slice(i,2)
220      case permission.downcase
221      when 'allow'
222        @allow.add(domain)
223      when 'deny'
224        @deny.add(domain)
225      else
226        raise "Invalid ACL entry #{list.to_s}"
227      end
228      i += 2
229    end
230  end
231
232end
233
234if __FILE__ == $0
235  # example
236  list = %w(deny all
237            allow 192.168.1.1
238            allow ::ffff:192.168.1.2
239            allow 192.168.1.3
240            )
241
242  addr = ["AF_INET", 10, "lc630", "192.168.1.3"]
243
244  acl = ACL.new
245  p acl.allow_addr?(addr)
246
247  acl = ACL.new(list, ACL::DENY_ALLOW)
248  p acl.allow_addr?(addr)
249end
250
251