rbroccoli 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +21 -0
- data/README +40 -0
- data/data/bro/callback-typemaps.yml +1010 -0
- data/data/bro/record-typemaps.yml +332 -0
- data/ext/broccoli_ext/autogen.sh +3 -0
- data/ext/broccoli_ext/broccoli.i +463 -0
- data/ext/broccoli_ext/broccoli_wrap.c +4213 -0
- data/ext/broccoli_ext/extconf.rb +15 -0
- data/ext/broccoli_ext/post-clean.rb +3 -0
- data/ext/broccoli_ext/pre-config.rb +5 -0
- data/ext/broccoli_ext/test/broconftest.rb +12 -0
- data/ext/broccoli_ext/test/test.rb +174 -0
- data/lib/Bro/connection.rb +73 -0
- data/lib/Bro/event.rb +34 -0
- data/lib/Bro/record.rb +60 -0
- data/lib/Bro/typemap.rb +158 -0
- data/lib/bro.rb +105 -0
- metadata +64 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'mkmf'
|
2
|
+
|
3
|
+
if ex = find_executable("broccoli-config")
|
4
|
+
$CFLAGS << " " + `#{ex} --cflags`.chomp
|
5
|
+
$LDFLAGS << " " + `#{ex} --libs`.chomp
|
6
|
+
else
|
7
|
+
puts "You need to have broccoli-config in your path!"
|
8
|
+
exit(-1)
|
9
|
+
end
|
10
|
+
|
11
|
+
if have_header("broccoli.h") and
|
12
|
+
have_library("broccoli") and
|
13
|
+
have_library("ssl")
|
14
|
+
create_makefile('broccoli_ext')
|
15
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# Please don't try to read too much into this file. It's mostly my
|
2
|
+
# internal test file while I'm building the C binding.
|
3
|
+
#
|
4
|
+
# Check out the examples directory for better examples that use the ruby
|
5
|
+
# library I've built overtop the C bindings.
|
6
|
+
|
7
|
+
require './broccoli_ext'
|
8
|
+
include Broccoli_ext
|
9
|
+
Broccoli_ext::bro_debug_calltrace=false
|
10
|
+
Broccoli_ext::bro_debug_messages=false
|
11
|
+
|
12
|
+
STDOUT.sync = true
|
13
|
+
|
14
|
+
#a= Broccoli_ext::BroString.new
|
15
|
+
#a.str_val="asdf"
|
16
|
+
|
17
|
+
host_str = "127.0.0.1:12345"
|
18
|
+
|
19
|
+
bc = bro_conn_new_str(host_str, BRO_CFLAG_NONE)
|
20
|
+
puts "Connection? #{bc}"
|
21
|
+
#puts " Connected" unless bro_conn_connect(bc).zero?
|
22
|
+
|
23
|
+
###
|
24
|
+
# Test BroString Creation
|
25
|
+
###
|
26
|
+
|
27
|
+
module SWIG
|
28
|
+
class TYPE_p_bro_conn
|
29
|
+
def method_missing(meth, *args)
|
30
|
+
return bro_conn_data_get(self, meth.id2name)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class TYPE_p_bro_record
|
35
|
+
# I need to build a full record typemapper to deal with this correctly.
|
36
|
+
|
37
|
+
def method_missing(meth, *args)
|
38
|
+
#return bro_record_get_named_val(self, meth.id2name, BRO_TYPE_STRING)
|
39
|
+
end
|
40
|
+
|
41
|
+
def [](position)
|
42
|
+
#return bro_record_get_nth_val(self, position, BRO_TYPE_STRING)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
###
|
50
|
+
# Test Record Creation
|
51
|
+
###
|
52
|
+
#rec = bro_record_new()
|
53
|
+
#puts "Ruby: Inserting data into the record"
|
54
|
+
##time = bro_util_current_time()
|
55
|
+
##puts "Ruby: Current time: #{time}"
|
56
|
+
#bro_record_add_val(rec, "seq", BRO_TYPE_IPADDR, 213054988)
|
57
|
+
#puts "Ruby: Getting the data back out"
|
58
|
+
##puts bro_record_get_named_val(rec, "seq", BRO_TYPE_COUNT);
|
59
|
+
#puts " " + bro_record_get_nth_val(rec, 0, BRO_TYPE_IPADDR).to_s
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
###
|
64
|
+
# Test Callback creation
|
65
|
+
###
|
66
|
+
|
67
|
+
# Ideal :)
|
68
|
+
#bro_ruby_typemap_new("pong", [8,19,0])
|
69
|
+
#build_typemap("pong", [:conn,:record])
|
70
|
+
|
71
|
+
#while 1
|
72
|
+
# ev = bro_event_new("ping")
|
73
|
+
# bro_event_free(ev)
|
74
|
+
# #GC.start
|
75
|
+
#end
|
76
|
+
|
77
|
+
bro_pong_record = Proc.new do |conn, rec|
|
78
|
+
now = bro_util_current_time()
|
79
|
+
puts "Pong_record callback"
|
80
|
+
puts rec
|
81
|
+
|
82
|
+
seq = bro_record_get_nth_val(rec, 0, BRO_TYPE_COUNT)
|
83
|
+
src_time = bro_record_get_nth_val(rec, 1, BRO_TYPE_TIME)
|
84
|
+
dst_time = bro_record_get_nth_val(rec, 2, BRO_TYPE_TIME)
|
85
|
+
|
86
|
+
puts "pong event from #{host_str}: seq=#{seq}, time=#{dst_time-src_time}/#{now-src_time} s"
|
87
|
+
end
|
88
|
+
|
89
|
+
bro_pong = Proc.new do |conn, src_time, dst_time, seq|
|
90
|
+
puts "Pong callback!"
|
91
|
+
now = bro_util_current_time()
|
92
|
+
puts "pong event from #{host_str}: seq=#{seq}, time=#{dst_time-src_time}/#{now-src_time} s"
|
93
|
+
end
|
94
|
+
|
95
|
+
new_connection = Proc.new do |conn|
|
96
|
+
puts "Saw a connection!"
|
97
|
+
end
|
98
|
+
|
99
|
+
dns_request = Proc.new do |conn, msg, query, qtype, qclass|
|
100
|
+
#$count = $count+1
|
101
|
+
#puts "msg: #{msg}"
|
102
|
+
#puts "query: #{query}"
|
103
|
+
#puts "qtype: #{qtype}"
|
104
|
+
#puts "qclass: #{qclass}"
|
105
|
+
#puts "service: #{conn.blah}"
|
106
|
+
#puts "Query output class: #{query.class}"
|
107
|
+
#answers = bro_record_get_nth_val(msg, 11, BRO_TYPE_COUNT).to_s
|
108
|
+
#puts "Number of dns answers: #{answers}"
|
109
|
+
#puts "Query: #{query} - Query type: #{qtype} - Query class: #{qclass}"
|
110
|
+
end
|
111
|
+
|
112
|
+
#puts "Registering callback..."
|
113
|
+
#bro_event_registry_add(bc, "dns_A_reply", dns_reply)
|
114
|
+
|
115
|
+
bro_event_registry_add(bc, "dns_request", ["dns_request", [19, 19, 8, 3, 3], dns_request])
|
116
|
+
|
117
|
+
#bro_event_registry_add(bc, "pong", ["pong", {"pong", [19]}, bro_pong_record])
|
118
|
+
#bro_event_registry_add(bc, "pong", ["pong", {"pong", [6,6,3]}, bro_pong])
|
119
|
+
|
120
|
+
#bro_event_registry_add(bc, "wootback", [[8], wootback])
|
121
|
+
|
122
|
+
#bro_event_registry_add(bc, "new_connection", ["new_connection", {"new_connection", [19]}, new_connection])
|
123
|
+
#bro_event_registry_add(bc, "return_memory", return_memory)
|
124
|
+
|
125
|
+
#puts "Done Registering callback..."
|
126
|
+
puts "Connected" if bro_conn_connect(bc)
|
127
|
+
|
128
|
+
while(1)
|
129
|
+
#puts "Checking input"
|
130
|
+
$count = 0
|
131
|
+
bro_conn_process_input(bc)
|
132
|
+
puts "*" * ($count/2)
|
133
|
+
|
134
|
+
sleep 0.5
|
135
|
+
|
136
|
+
GC.start
|
137
|
+
end
|
138
|
+
exit
|
139
|
+
|
140
|
+
###
|
141
|
+
# Testing record creation and event sending
|
142
|
+
###
|
143
|
+
record = false
|
144
|
+
(1..100).each do |seq|
|
145
|
+
bro_conn_process_input(bc)
|
146
|
+
#puts "Creating event"
|
147
|
+
ev = bro_event_new("ping")
|
148
|
+
timestamp = bro_util_current_time()
|
149
|
+
if(record)
|
150
|
+
rec = bro_record_new()
|
151
|
+
bro_record_add_val(rec, "seq", BRO_TYPE_COUNT, seq)
|
152
|
+
bro_record_add_val(rec, "src_time", BRO_TYPE_TIME, timestamp)
|
153
|
+
bro_event_add_val(ev, BRO_TYPE_RECORD, rec)
|
154
|
+
else
|
155
|
+
bro_event_add_val(ev, BRO_TYPE_TIME, timestamp)
|
156
|
+
bro_event_add_val(ev, BRO_TYPE_COUNT, seq)
|
157
|
+
end
|
158
|
+
|
159
|
+
puts "Sending ping..."
|
160
|
+
bro_event_send(bc, ev)
|
161
|
+
# May not need to call this anymore either
|
162
|
+
#bro_event_free(ev)
|
163
|
+
sleep 1
|
164
|
+
#GC.start
|
165
|
+
end
|
166
|
+
|
167
|
+
#while(1) do
|
168
|
+
# ev = bro_event_new "show_memory"
|
169
|
+
# puts "Sending event"
|
170
|
+
# puts bro_event_send(bc, ev)
|
171
|
+
# sleep 1
|
172
|
+
# puts "Processing input..."
|
173
|
+
# puts bro_conn_process_input(bc)
|
174
|
+
#end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'broccoli_ext'
|
2
|
+
|
3
|
+
module Bro
|
4
|
+
class Connection
|
5
|
+
include Broccoli_ext
|
6
|
+
|
7
|
+
def initialize(hp, flags=nil)
|
8
|
+
flags = (BRO_CFLAG_RECONNECT | BRO_CFLAG_ALWAYS_QUEUE) if flags.nil?
|
9
|
+
@bc = bro_conn_new_str(hp, flags)
|
10
|
+
@io_object = nil
|
11
|
+
@event_blocks = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def delete
|
15
|
+
bro_conn_delete(@bc)
|
16
|
+
end
|
17
|
+
|
18
|
+
def connect
|
19
|
+
bro_conn_connect(@bc)
|
20
|
+
end
|
21
|
+
|
22
|
+
def connected?
|
23
|
+
bro_conn_alive?(@bc)
|
24
|
+
end
|
25
|
+
|
26
|
+
def process_input
|
27
|
+
bro_conn_process_input(@bc)
|
28
|
+
end
|
29
|
+
|
30
|
+
def queue_length
|
31
|
+
bro_event_queue_length(@bc)
|
32
|
+
end
|
33
|
+
|
34
|
+
def flush
|
35
|
+
bro_event_queue_flush(@bc)
|
36
|
+
end
|
37
|
+
|
38
|
+
def send(event)
|
39
|
+
# .ev is the real event pointer
|
40
|
+
bro_event_send(@bc, event.ev)
|
41
|
+
end
|
42
|
+
|
43
|
+
def wait
|
44
|
+
unless @io_object
|
45
|
+
fd = bro_conn_get_fd(@bc)
|
46
|
+
return false if fd < 0
|
47
|
+
@io_object = IO.new(fd)
|
48
|
+
end
|
49
|
+
# block until there is data
|
50
|
+
select([@io_object])
|
51
|
+
end
|
52
|
+
|
53
|
+
def event_handler(event, mapping=nil, &block)
|
54
|
+
# This lets me pass the proc object and the event name into the callback
|
55
|
+
# It needs to be done so that the typemapper can map types before
|
56
|
+
# calling the proc object.
|
57
|
+
# TODO: maybe make this less hacky?
|
58
|
+
Bro::Typemap.define_event(event, mapping) unless mapping.nil?
|
59
|
+
|
60
|
+
typemap = Bro::Typemap.get(:callback, event)
|
61
|
+
|
62
|
+
event_block = [event, typemap, block]
|
63
|
+
@event_blocks << event_block
|
64
|
+
|
65
|
+
bro_event_registry_add(@bc, event, event_block)
|
66
|
+
|
67
|
+
# Re-request all events if we're already connected.
|
68
|
+
bro_event_registry_request(@bc) if connected?
|
69
|
+
end
|
70
|
+
alias :event_handler_for :event_handler
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
data/lib/Bro/event.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Bro
|
2
|
+
|
3
|
+
class Event
|
4
|
+
include Broccoli_ext
|
5
|
+
attr_reader :ev
|
6
|
+
|
7
|
+
def initialize(name)
|
8
|
+
@event_name = name
|
9
|
+
@ev = bro_event_new(name)
|
10
|
+
@current_val = 0
|
11
|
+
ObjectSpace.define_finalizer(self, Event.create_finalizer(@ev))
|
12
|
+
end
|
13
|
+
|
14
|
+
def insert(value)
|
15
|
+
name_type, int_type = Bro::Typemap.get(:callback, @event_name, @current_val)
|
16
|
+
#puts "(#{@ev}, #{int_type}, #{value})"
|
17
|
+
# TODO: Yikes! more hacks to get this thing working
|
18
|
+
value = value.rec if int_type == Broccoli_ext::BRO_TYPE_RECORD
|
19
|
+
bro_event_add_val(@ev, int_type, value)
|
20
|
+
@current_val += 1
|
21
|
+
end
|
22
|
+
|
23
|
+
def free
|
24
|
+
bro_event_free(@ev)
|
25
|
+
end
|
26
|
+
|
27
|
+
# When the garbage collector comes around, make sure the C structure
|
28
|
+
# is freed.
|
29
|
+
def self.create_finalizer(event)
|
30
|
+
proc { bro_event_free(event) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
data/lib/Bro/record.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
module Bro
|
2
|
+
|
3
|
+
class Record
|
4
|
+
include Broccoli_ext
|
5
|
+
attr_accessor :rec
|
6
|
+
|
7
|
+
def initialize(type=nil)
|
8
|
+
@type = type
|
9
|
+
@rec = bro_record_new()
|
10
|
+
if type
|
11
|
+
@rec.record_type = type
|
12
|
+
else
|
13
|
+
# Generate a "unique" name for this record since no type was given
|
14
|
+
# It allows the record to read back it's own values by storing types
|
15
|
+
# as it is created.
|
16
|
+
@rec.record_type = rand().to_s
|
17
|
+
# TODO: abstract into typemapper
|
18
|
+
$bro_global_typemap[:record][@rec.record_type] = {}
|
19
|
+
end
|
20
|
+
@current_val = 0
|
21
|
+
ObjectSpace.define_finalizer(self, Record.create_finalizer(@rec))
|
22
|
+
end
|
23
|
+
|
24
|
+
def method_missing(meth)
|
25
|
+
@rec.send(meth)
|
26
|
+
end
|
27
|
+
|
28
|
+
def insert(name, value, value_type=nil)
|
29
|
+
if value_type.nil? and @rec.record_type.nil?
|
30
|
+
raise "No type inferred.. type must be given"
|
31
|
+
end
|
32
|
+
if not value_type
|
33
|
+
name_type, int_type = Bro::Typemap.get(:record, @type, name)
|
34
|
+
else
|
35
|
+
int_type = Bro::Typemap.get_int_type(value_type)
|
36
|
+
# TODO: needs to be abstracted into the typemapper
|
37
|
+
$bro_global_typemap[:record][@rec.record_type][name.to_sym] = value_type
|
38
|
+
end
|
39
|
+
#puts "(#{@rec}, #{name.to_s}, #{int_type}, #{value})"
|
40
|
+
Broccoli_ext::bro_record_add_val(@rec, name.to_s, int_type, value)
|
41
|
+
@current_val += 1
|
42
|
+
end
|
43
|
+
|
44
|
+
def []=(key, val, type)
|
45
|
+
int_type = Bro::Typemap.get_int_type(type)
|
46
|
+
Broccoli_ext::bro_record_set_nth_val(@rec, key, int_type, val)
|
47
|
+
end
|
48
|
+
|
49
|
+
def free
|
50
|
+
bro_record_free(@rec)
|
51
|
+
end
|
52
|
+
|
53
|
+
# When the garbage collector comes around,
|
54
|
+
# make sure the C structure is freed.
|
55
|
+
def self.create_finalizer(record)
|
56
|
+
proc { bro_record_free(record) }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
data/lib/Bro/typemap.rb
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Bro
|
5
|
+
module Typemap
|
6
|
+
|
7
|
+
DATA_TYPEMAP = {:bool => 1,
|
8
|
+
:int => 2,
|
9
|
+
:count => 3,
|
10
|
+
:counter => 4,
|
11
|
+
:double => 5,
|
12
|
+
:time => 6,
|
13
|
+
:interval => 7,
|
14
|
+
:string => 8,
|
15
|
+
:pattern => 9,
|
16
|
+
:enum => 10,
|
17
|
+
:timer => 11,
|
18
|
+
:port => 12,
|
19
|
+
:addr => 13,
|
20
|
+
:net => 14,
|
21
|
+
:subnet => 15,
|
22
|
+
:record => 19
|
23
|
+
}
|
24
|
+
|
25
|
+
|
26
|
+
def Typemap.get_int_type(type)
|
27
|
+
type_code = DATA_TYPEMAP[type]
|
28
|
+
if type_code.nil?
|
29
|
+
Broccoli_ext::BRO_TYPE_RECORD
|
30
|
+
else
|
31
|
+
type_code
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# TODO: this is an utter mess
|
36
|
+
def Typemap.get(typemap, name, key=nil)
|
37
|
+
if key
|
38
|
+
if typemap == :record and key.class == Fixnum
|
39
|
+
# this is especially bad
|
40
|
+
foo = $bro_global_typemap[typemap][name].to_a[key][1]
|
41
|
+
elsif typemap == :callback and key.class == Fixnum
|
42
|
+
foo = $bro_global_typemap[typemap][name][key]
|
43
|
+
else
|
44
|
+
foo = $bro_global_typemap[typemap][name.to_sym][key.to_sym]
|
45
|
+
end
|
46
|
+
return [foo, get_int_type(foo)]
|
47
|
+
else
|
48
|
+
foo = $bro_global_typemap[typemap][name]
|
49
|
+
return foo.inject([]) { |out, t| out << get_int_type(t) }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def Typemap.insert(typemap_type, name, map)
|
54
|
+
$bro_global_typemap[typemap_type][name] = map
|
55
|
+
end
|
56
|
+
|
57
|
+
def Typemap.define_event(event, mapping)
|
58
|
+
map = mapping.collect { |m| m.to_sym }
|
59
|
+
Bro::Typemap.insert(:callback, event, map)
|
60
|
+
end
|
61
|
+
|
62
|
+
def Typemap.define_record(record_name, mapping)
|
63
|
+
new_mapping = {}
|
64
|
+
mapping.each { |k,v| new_mapping[k.to_sym] = v.to_sym}
|
65
|
+
Bro::Typemap.insert(:record, record_name.to_sym, new_mapping)
|
66
|
+
end
|
67
|
+
|
68
|
+
def Typemap.load
|
69
|
+
$bro_global_typemap = {}
|
70
|
+
if __FILE__.match(File.join("gems", "rbroccoli"))
|
71
|
+
# Gem installed
|
72
|
+
dir = File.join(File.dirname(__FILE__), '..', '..', 'data', 'bro')
|
73
|
+
else
|
74
|
+
# Native install
|
75
|
+
$:.each do |path|
|
76
|
+
if m = path.match(File.join("lib", "ruby"))
|
77
|
+
# Supporting native install
|
78
|
+
tmp_dir = File.join(m.pre_match, "share", "bro")
|
79
|
+
if File.exists?(tmp_dir)
|
80
|
+
dir = tmp_dir
|
81
|
+
break
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
if !dir
|
88
|
+
# Check if it isn't installed
|
89
|
+
dir = File.join(File.dirname(__FILE__), '..', '..', 'data', 'bro')
|
90
|
+
end
|
91
|
+
$bro_global_typemap[:callback] = YAML.load_file("#{dir}/callback-typemaps.yml")
|
92
|
+
$bro_global_typemap[:record] = YAML.load_file("#{dir}/record-typemaps.yml")
|
93
|
+
end
|
94
|
+
|
95
|
+
# This needs the event.bif.bro file to generate its callback
|
96
|
+
# typemappings for the default bro events.
|
97
|
+
# Distributions will have these files prebuilt for the appropriate
|
98
|
+
# version of Bro.
|
99
|
+
def Typemap.generate_callback_file(input_filename, output_filename)
|
100
|
+
f = open(input_filename)
|
101
|
+
|
102
|
+
output = {}
|
103
|
+
f.each_line do |line|
|
104
|
+
match = line.match(/^global\s+(.*?):\s+event\((.*)\)/)
|
105
|
+
if match
|
106
|
+
types = []
|
107
|
+
event = match[1]
|
108
|
+
match[2].split(", ").each { |i| types << i.split(": ")[1].to_sym }
|
109
|
+
output[event] = types
|
110
|
+
end
|
111
|
+
end
|
112
|
+
yaml = open(output_filename, "w")
|
113
|
+
yaml.puts "# This file was autogenerated on #{Time.now}"
|
114
|
+
yaml.puts "# Please do not edit!"
|
115
|
+
yaml.puts output.to_yaml
|
116
|
+
end
|
117
|
+
|
118
|
+
# This will generate a typemapper for records from the
|
119
|
+
# bro.init file.
|
120
|
+
def Typemap.generate_record_file(input_filename, output_filename)
|
121
|
+
f = open(input_filename)
|
122
|
+
|
123
|
+
output = {}
|
124
|
+
fields = {}
|
125
|
+
record_type = ''
|
126
|
+
inside_def = false
|
127
|
+
f.each_line do |line|
|
128
|
+
if line =~ /type (\S+?): record/
|
129
|
+
record_type = $1
|
130
|
+
inside_def = true
|
131
|
+
end
|
132
|
+
|
133
|
+
if inside_def and line =~ /(\S+): (\S+);/
|
134
|
+
fields[$1.to_sym] = $2.to_sym;
|
135
|
+
end
|
136
|
+
|
137
|
+
if line =~ /^\};/
|
138
|
+
if inside_def
|
139
|
+
output[record_type.to_sym] = fields
|
140
|
+
fields = {}
|
141
|
+
inside_def = false
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
yaml = open(output_filename, "w")
|
146
|
+
yaml.puts "# This file was autogenerated on #{Time.now}"
|
147
|
+
yaml.puts "# Please do not edit!"
|
148
|
+
yaml.puts output.to_yaml
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# This is just to get the yaml stuff loaded into the global typemapper hash
|
154
|
+
begin
|
155
|
+
Bro::Typemap.load
|
156
|
+
rescue
|
157
|
+
STDERR.puts "Typemap files not found!"
|
158
|
+
end
|