mikrotik 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +9 -0
- data/.yardopts +1 -0
- data/Gemfile +11 -0
- data/Guardfile +8 -0
- data/README.markdown +4 -0
- data/Rakefile +47 -0
- data/examples/bandwidth_monitor.rb +34 -0
- data/examples/dhcp.rb +8 -0
- data/examples/uptime.rb +8 -0
- data/lib/extensions/events.rb +41 -0
- data/lib/extensions/string.rb +27 -0
- data/lib/mikrotik.rb +42 -0
- data/lib/mikrotik/client.rb +124 -0
- data/lib/mikrotik/command.rb +81 -0
- data/lib/mikrotik/connection.rb +84 -0
- data/lib/mikrotik/errors.rb +11 -0
- data/lib/mikrotik/protocol/parser.rb +126 -0
- data/lib/mikrotik/protocol/sentence.rb +70 -0
- data/lib/mikrotik/utilities.rb +22 -0
- data/lib/mikrotik/version.rb +3 -0
- data/mikrotik.gemspec +17 -0
- data/spec/helper.rb +1 -0
- data/spec/parser.rb +52 -0
- metadata +70 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private -e ./doc/setup.rb
|
data/Gemfile
ADDED
data/Guardfile
ADDED
data/README.markdown
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
|
4
|
+
begin
|
5
|
+
Bundler.setup(:default, :development)
|
6
|
+
rescue Bundler::BundlerError => e
|
7
|
+
$stderr.puts e.message
|
8
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
9
|
+
exit e.status_code
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'rubygems'
|
13
|
+
require 'rake'
|
14
|
+
|
15
|
+
begin
|
16
|
+
require 'jeweler'
|
17
|
+
Jeweler::Tasks.new do |gem|
|
18
|
+
gem.name = "mikrotik"
|
19
|
+
gem.summary = %Q{Mikrotik API protocol client in Ruby}
|
20
|
+
gem.description = %Q{An implementation of the Mikrotik API protocol in Ruby/EventMachine}
|
21
|
+
gem.email = "iain@ominiom.com"
|
22
|
+
gem.homepage = "http://github.com/ominiom/mikrotik"
|
23
|
+
gem.authors = ["Iain Wilson"]
|
24
|
+
gem.files = FileList["lib/*.rb", "lib/*/*.rb", "lib/*/*/*.rb"]
|
25
|
+
end
|
26
|
+
Jeweler::GemcutterTasks.new
|
27
|
+
rescue LoadError
|
28
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
29
|
+
end
|
30
|
+
|
31
|
+
require 'rake/testtask'
|
32
|
+
Rake::TestTask.new(:test) do |test|
|
33
|
+
test.libs << 'lib' << 'test'
|
34
|
+
test.pattern = 'test/**/test_*.rb'
|
35
|
+
test.verbose = true
|
36
|
+
end
|
37
|
+
|
38
|
+
task :default => :test
|
39
|
+
|
40
|
+
begin
|
41
|
+
require 'yard'
|
42
|
+
YARD::Rake::YardocTask.new
|
43
|
+
rescue LoadError
|
44
|
+
task :yardoc do
|
45
|
+
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Add lib to load path
|
2
|
+
$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
|
4
|
+
require 'mikrotik'
|
5
|
+
|
6
|
+
EventMachine.run do
|
7
|
+
client = Mikrotik.connect :host => "192.168.1.254", :port => 8728, :username => "admin", :password => "debUwu8A"
|
8
|
+
|
9
|
+
puts "Connecting..."
|
10
|
+
|
11
|
+
client.on_login_success do |client|
|
12
|
+
puts "Login successful"
|
13
|
+
|
14
|
+
# Run a command every 5 seconds
|
15
|
+
EM.add_timer(EM::PeriodicTimer.new(5) do
|
16
|
+
client.execute(client.command(:interface, :print).returning(:name, :bytes).where(:name => :ether1).on_reply do |reply|
|
17
|
+
# Sum up the in and out counts
|
18
|
+
bytes = reply.result(:bytes).split('/').map(&:to_i).inject(0) { |i, n| i = i + n }
|
19
|
+
# Format nicely
|
20
|
+
total = sprintf "%.2f", (bytes / 1024.0 / 1024.0)
|
21
|
+
# Output
|
22
|
+
puts "Interface '#{reply.result :name}' has transferred #{total} MB"
|
23
|
+
end)
|
24
|
+
end)
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
trap 'SIGINT' do
|
29
|
+
client.disconnect
|
30
|
+
puts "Disconnected from #{client.options[:host]}"
|
31
|
+
exit
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
data/examples/dhcp.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
client.execute(client.command(:ip, :dhcp_server, :lease, :getall).on_done do |replies|
|
2
|
+
puts replies.collect { |reply| "IP '#{reply.result :address}' assigned to MAC #{reply.result('mac-address')}" }
|
3
|
+
end)
|
4
|
+
|
5
|
+
client.on_pending_complete do |client|
|
6
|
+
puts "All commands completed, closing session..."
|
7
|
+
client.disconnect
|
8
|
+
end
|
data/examples/uptime.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
client.execute(client.command(:system, :resource, :print).returning(:uptime).on_reply do |reply|
|
2
|
+
puts "System uptime is #{reply.result :uptime}"
|
3
|
+
end)
|
4
|
+
|
5
|
+
client.on_pending_complete do |client|
|
6
|
+
puts "All commands completed, closing session..."
|
7
|
+
client.disconnect
|
8
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# @private
|
2
|
+
module Events
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
# @return [Boolean] If the event has a handler registered
|
9
|
+
def has_event_handler?(event)
|
10
|
+
@events ||= {}
|
11
|
+
!@events["on_#{event}".to_sym].nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
# @param [Array<String, Symbol>] events Array of event names
|
17
|
+
# @example
|
18
|
+
# has_events :delete, :create
|
19
|
+
def has_events(*events)
|
20
|
+
events.map { |event| event.to_s }.each do |event|
|
21
|
+
self.class_eval <<-"END"
|
22
|
+
def on_#{event}(*args, &block)
|
23
|
+
@events ||= {}
|
24
|
+
if block_given? then
|
25
|
+
@events[:on_#{event}] = block.to_proc
|
26
|
+
else
|
27
|
+
@events[:on_#{event}].call(*args) unless @events[:on_#{event}].nil?
|
28
|
+
end
|
29
|
+
return self
|
30
|
+
end
|
31
|
+
END
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def has_event(event)
|
36
|
+
has_events(*[event])
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# @private
|
2
|
+
class String
|
3
|
+
|
4
|
+
def to_mikrotik_word
|
5
|
+
str = self.dup
|
6
|
+
if RUBY_VERSION >= '1.9.0'
|
7
|
+
str.force_encoding(Encoding::BINARY)
|
8
|
+
end
|
9
|
+
if str.length < 0x80
|
10
|
+
return str.length.chr + str
|
11
|
+
elsif str.length < 0x4000
|
12
|
+
return Microtik::Utilities.bytepack(str.length | 0x8000, 2) + str
|
13
|
+
elsif str.length < 0x200000
|
14
|
+
return Microtik::Utilities.bytepack(str.length | 0xc00000, 3) + str
|
15
|
+
elsif str.length < 0x10000000
|
16
|
+
return Microtik::Utilities.bytepack(str.length | 0xe0000000, 4) + str
|
17
|
+
elsif str.length < 0x0100000000
|
18
|
+
return 0xf0.chr + Microtik::Utilities.bytepack(str.length, 5) + str
|
19
|
+
else
|
20
|
+
raise RuntimeError.new(
|
21
|
+
"String is too long to be encoded for " +
|
22
|
+
"the MikroTik API using a 4-byte length!"
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
data/lib/mikrotik.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'digest/md5'
|
3
|
+
|
4
|
+
# TODO : Allow underscores to be used instead of dashes in command paths and properties - e.g. in dhcp-server and mac-address
|
5
|
+
|
6
|
+
module Mikrotik
|
7
|
+
|
8
|
+
module Protocol; end;
|
9
|
+
|
10
|
+
# @return [Boolean] Whether debugging prints are enabled or not
|
11
|
+
def self.debugging
|
12
|
+
false
|
13
|
+
end
|
14
|
+
|
15
|
+
# Prints message if debugging is enabled
|
16
|
+
# @param [Array<String>] message Parts of the message
|
17
|
+
def self.debug(message)
|
18
|
+
puts message.join(' ') if debugging
|
19
|
+
end
|
20
|
+
|
21
|
+
# Shorthand for Mikrotik::Client.connect
|
22
|
+
# @return [Mikrotik::Client]
|
23
|
+
def self.connect(*args)
|
24
|
+
Client.connect(*args)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
# Add ourselves to the load path
|
30
|
+
$: << File.dirname(__FILE__)
|
31
|
+
|
32
|
+
# Load up the code
|
33
|
+
require 'extensions/string'
|
34
|
+
require 'extensions/events'
|
35
|
+
require 'mikrotik/version'
|
36
|
+
require 'mikrotik/errors'
|
37
|
+
require 'mikrotik/client'
|
38
|
+
require 'mikrotik/connection'
|
39
|
+
require 'mikrotik/command'
|
40
|
+
require 'mikrotik/utilities'
|
41
|
+
require 'mikrotik/protocol/parser'
|
42
|
+
require 'mikrotik/protocol/sentence'
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# Contains all methods for interacting with a device
|
2
|
+
# such as logging in and executing commands
|
3
|
+
#
|
4
|
+
# @see Mikrotik::Client.connect
|
5
|
+
module Mikrotik::Client
|
6
|
+
include Events
|
7
|
+
|
8
|
+
# Fired when the API login command has returned successfully.
|
9
|
+
# After this event has fired it is safe to start sending commands
|
10
|
+
has_event :login_success
|
11
|
+
|
12
|
+
# Fired when the API login command returns an error -
|
13
|
+
# for example in cases where the credentials were wrong
|
14
|
+
has_event :login_failure
|
15
|
+
|
16
|
+
# Fired when the queue of commands awaiting completion
|
17
|
+
# becomes empty
|
18
|
+
has_event :pending_complete
|
19
|
+
|
20
|
+
# @return [Hash] Options used for the connection
|
21
|
+
# @see Client#connect
|
22
|
+
attr_reader :options
|
23
|
+
|
24
|
+
# Connects to a RouterOS device. Required options are +:host+
|
25
|
+
# and +:password+ - +:username+ and +:port+ default
|
26
|
+
# to +admin+ and +8728+ respectively
|
27
|
+
#
|
28
|
+
# @param [Hash] options +:test+ Hash of connection options
|
29
|
+
# @option options [String] :host Device's IP address
|
30
|
+
# @option options [Integer] :port (8728) Port of the API service on the device
|
31
|
+
# @option options [String] :username ('admin') API username to authenticate with
|
32
|
+
# @option options [String] :password Password for the given username
|
33
|
+
# @return [Client]
|
34
|
+
# @raise [ArgumentError] If not all required options are given
|
35
|
+
# @example Connection with minimum options set
|
36
|
+
# Mikrotik::Client.connect({
|
37
|
+
# :host => '192.168.1.1',
|
38
|
+
# :password => 'topsecret'
|
39
|
+
# })
|
40
|
+
# @example Connection with all options set to override defaults
|
41
|
+
# Mikrotik::Client.connect({
|
42
|
+
# :host => '10.200.0.21',
|
43
|
+
# :port => 3450,
|
44
|
+
# :username => 'apiuser',
|
45
|
+
# :password => 'topsecret'
|
46
|
+
# })
|
47
|
+
#
|
48
|
+
def self.connect(options = { :username => 'admin', :port => 8728 })
|
49
|
+
defaults = { :username => 'admin', :port => 8728 }
|
50
|
+
options = defaults.merge(options)
|
51
|
+
# Check required options have been given
|
52
|
+
all_required_options = !([:host, :port, :username, :password].map { |option|
|
53
|
+
options.key?(option) && !options[option].nil?
|
54
|
+
}.include?(false))
|
55
|
+
raise ArgumentError, "Options for host and password must be given" unless all_required_options
|
56
|
+
|
57
|
+
c = EM.connect options[:host], options[:port], self, options
|
58
|
+
c.pending_connect_timeout = 10.0
|
59
|
+
c
|
60
|
+
end
|
61
|
+
|
62
|
+
# Logs in to the device. This is called automatically when
|
63
|
+
# the TCP connection succeeds. To be sure the device is ready
|
64
|
+
# to +execute+ commands before sending any register an event handler
|
65
|
+
# on +on_login_success+ and send commands from there
|
66
|
+
def login
|
67
|
+
execute command(:login).on_reply { |reply|
|
68
|
+
|
69
|
+
challenge = Mikrotik::Utilities.hex_to_bin(reply.result :ret)
|
70
|
+
response = "00" + Digest::MD5.hexdigest(0.chr + @options[:password] + challenge)
|
71
|
+
|
72
|
+
execute command(:login).with(
|
73
|
+
:name => @options[:username],
|
74
|
+
:response => response
|
75
|
+
).on_done { |replies|
|
76
|
+
Mikrotik.debug [:client, :on_login_success]
|
77
|
+
@logged_in = true
|
78
|
+
on_login_success(self)
|
79
|
+
}.on_trap { |trap|
|
80
|
+
Mikrotik.debug [:client, :on_login_failure]
|
81
|
+
if has_event_handler? :on_login_failure then
|
82
|
+
on_login_failure(self)
|
83
|
+
else
|
84
|
+
raise Mikrotik::Errors::UnhandledTrap, 'Login failed'
|
85
|
+
end
|
86
|
+
}
|
87
|
+
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
# Sends a command to the device
|
92
|
+
# @param command [Mikrotik::Command] The command to be executed
|
93
|
+
# @return [Mikrotik::Command] The command executed
|
94
|
+
def execute(command)
|
95
|
+
send command
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [Boolean] Status of the session login
|
99
|
+
def logged_in?
|
100
|
+
@logged_in
|
101
|
+
end
|
102
|
+
|
103
|
+
# Shortcut for creating a Mikrotik::Command
|
104
|
+
# @return Mikrotik::Command
|
105
|
+
# @see Mikrotik::Command
|
106
|
+
def command(*path)
|
107
|
+
Mikrotik::Command.new(*path)
|
108
|
+
end
|
109
|
+
|
110
|
+
def inspect
|
111
|
+
"#<#{self.class.name}:0x#{self.object_id} host=#{@options[:host]}>"
|
112
|
+
end
|
113
|
+
|
114
|
+
def initialize(opts = {})
|
115
|
+
@closing = false
|
116
|
+
@logged_in = false
|
117
|
+
@options = opts
|
118
|
+
@events = {}
|
119
|
+
@buffer = ""
|
120
|
+
@next_tag = 1
|
121
|
+
extend Mikrotik::Connection
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# Class encapsulating a API command to be executed on the device
|
2
|
+
class Mikrotik::Command
|
3
|
+
include Events
|
4
|
+
|
5
|
+
# Fired when the command completes
|
6
|
+
# @yield [replies]
|
7
|
+
# @yieldparam [false, Array<Mikrotik::Protocol::Sentence>] replies All replies received from the device for this command
|
8
|
+
has_events :done
|
9
|
+
|
10
|
+
# Fired on each reply received from the command
|
11
|
+
# @yield [reply]
|
12
|
+
# @yieldparam [false, Mikrotik::Protocol::Sentence] reply The sentence received in reply
|
13
|
+
has_event :reply
|
14
|
+
|
15
|
+
# Fired when an error response is received from the command
|
16
|
+
# @yield [error_message]
|
17
|
+
# @yieldparam [false, String] error_message Error description
|
18
|
+
has_event :trap
|
19
|
+
|
20
|
+
# @return [Array<Mikrotik::Protocol::Sentence>] All sentences sent in response to this command so far
|
21
|
+
attr_reader :replies
|
22
|
+
|
23
|
+
# @return [Hash] All option name/value pairs set on the command
|
24
|
+
attr_reader :options
|
25
|
+
|
26
|
+
# @return [String] Path to the command
|
27
|
+
attr_reader :command
|
28
|
+
|
29
|
+
# Creates a new command object
|
30
|
+
# @param [Array<String, Symbol>] path Path of the API command
|
31
|
+
def initialize(*command_path)
|
32
|
+
@command = command_path.collect { |part| "#{part}".gsub('_', '-') }.join('/')
|
33
|
+
@command = "/#{@command}" unless @command.start_with?('/')
|
34
|
+
@options = {}
|
35
|
+
@replies = []
|
36
|
+
end
|
37
|
+
|
38
|
+
# Adds property names and values to the command
|
39
|
+
# @param [Hash] options
|
40
|
+
def with(options = {})
|
41
|
+
options.each_pair do |option, value|
|
42
|
+
@options["=#{option}"] = value
|
43
|
+
end
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
# Adds querying conditions to the command
|
48
|
+
# @param [Hash] conditions
|
49
|
+
def where(conditions = {})
|
50
|
+
conditions.each_pair do |property, value|
|
51
|
+
@options["?#{property}"] = value
|
52
|
+
end
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
# Specifies what properties should be returned by the device in response to this command
|
57
|
+
#
|
58
|
+
# @param [Array<String, Symbol>] properties List of the names of properties to request in response
|
59
|
+
# @return [self]
|
60
|
+
def returning(*properties)
|
61
|
+
@options['=.proplist'] ||= []
|
62
|
+
properties.each do |property|
|
63
|
+
@options['=.proplist'] << "#{property}"
|
64
|
+
end
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
# Encodes the command for transmission
|
69
|
+
# @return [String] Command encoded as an API sentence in binary string format
|
70
|
+
def encoded
|
71
|
+
@command.to_mikrotik_word + @options.collect { |key, value|
|
72
|
+
case value.class.name
|
73
|
+
when 'Array'
|
74
|
+
[key, value.collect { |item| "#{item}" }.join(',')].join '='
|
75
|
+
else
|
76
|
+
"#{key}=#{value}"
|
77
|
+
end
|
78
|
+
}.map { |option| option.to_mikrotik_word }.join + 0x00.chr
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# Contains logic for sending and receiving data to and from the device
|
2
|
+
module Mikrotik::Connection
|
3
|
+
|
4
|
+
# @private
|
5
|
+
def connection_completed
|
6
|
+
@commands = {}
|
7
|
+
@sentences = []
|
8
|
+
@parser = Mikrotik::Protocol::Parser.new
|
9
|
+
@parser.on_sentence do |sentence|
|
10
|
+
handle_reply(sentence)
|
11
|
+
end
|
12
|
+
login
|
13
|
+
end
|
14
|
+
|
15
|
+
# Disconnects from the device
|
16
|
+
def disconnect
|
17
|
+
Mikrotik.debug [:connection, :disconnect]
|
18
|
+
@closing = true
|
19
|
+
close_connection(true)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @private
|
23
|
+
def send(command)
|
24
|
+
command.options['.tag'] = @next_tag
|
25
|
+
Mikrotik.debug [:connection, :send, command.encoded]
|
26
|
+
@commands[@next_tag] = command
|
27
|
+
send_data(command.encoded)
|
28
|
+
@next_tag = @next_tag + 1
|
29
|
+
command
|
30
|
+
end
|
31
|
+
|
32
|
+
# @private
|
33
|
+
def receive_data(data)
|
34
|
+
Mikrotik.debug [:connection, :receive, data.size, data]
|
35
|
+
@parser << data
|
36
|
+
end
|
37
|
+
|
38
|
+
# @private
|
39
|
+
def unbind
|
40
|
+
raise Mikrotik::Errors::ConnectionDropped unless @closing
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# @private
|
46
|
+
def handle_reply(reply)
|
47
|
+
unless reply.tagged? then
|
48
|
+
Mikrotik.debug [:connection, :handle_reply, :untagged]
|
49
|
+
return
|
50
|
+
end
|
51
|
+
Mikrotik.debug [:connection, :handle_reply, :tagged, reply.tag]
|
52
|
+
# Retreive the command this reply is intended for by its tag
|
53
|
+
command = @commands[reply.tag]
|
54
|
+
command.replies << reply unless reply.completely_done?
|
55
|
+
# Catch those nasty errors
|
56
|
+
if reply.trap? then
|
57
|
+
Mikrotik.debug [:command, :on_trap]
|
58
|
+
# If there's an error handler, fire it...
|
59
|
+
if command.has_event_handler?(:on_trap) then
|
60
|
+
command.on_trap(reply.param(:message))
|
61
|
+
else
|
62
|
+
# ...if not, raise an error
|
63
|
+
raise Mikrotik::Errors::UnhandledTrap, reply.result(:message) || "Unknown error"
|
64
|
+
end
|
65
|
+
return
|
66
|
+
end
|
67
|
+
# Still fire on_reply events for done messages that contain more data
|
68
|
+
unless reply.completely_done? then
|
69
|
+
Mikrotik.debug [:command, :on_reply]
|
70
|
+
command.on_reply(reply)
|
71
|
+
end
|
72
|
+
# If this was the last reply, fire the on_done event handler
|
73
|
+
if reply.done? then
|
74
|
+
Mikrotik.debug [:command, :on_done, reply.tag]
|
75
|
+
command.on_done(command.replies)
|
76
|
+
# As the command has completed remove it from the list of active commands
|
77
|
+
@commands.delete(reply.tag)
|
78
|
+
Mikrotik.debug [:client, :commands, :size, @commands.size]
|
79
|
+
end
|
80
|
+
# If there are no more active commands fire the event
|
81
|
+
on_pending_complete(self) if @commands.empty?
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Mikrotik::Errors
|
2
|
+
|
3
|
+
# Raised when an error response is received to a
|
4
|
+
# command with no on_trap event handler
|
5
|
+
class UnhandledTrap < RuntimeError; end;
|
6
|
+
|
7
|
+
# Raised when the connection to the device
|
8
|
+
# disconnects unexpectedly
|
9
|
+
class ConnectionDropped < RuntimeError; end;
|
10
|
+
|
11
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# Responsible for extracting replies from API sentence data
|
2
|
+
class Mikrotik::Protocol::Parser
|
3
|
+
include Events
|
4
|
+
|
5
|
+
# Fired when the parser extracts a complete API sentence
|
6
|
+
# @yield [Mikrotik::Protocol::Sentence] sentence
|
7
|
+
has_event :sentence
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@data = ''
|
11
|
+
@data.force_encoding(Encoding::BINARY) if RUBY_VERSION > '1.9'
|
12
|
+
reset!
|
13
|
+
end
|
14
|
+
|
15
|
+
# Discards the sentence currently being parsed
|
16
|
+
def reset!
|
17
|
+
@sentence = Mikrotik::Protocol::Sentence.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Adds data to the parser buffer and runs the parser
|
21
|
+
def <<(data)
|
22
|
+
@data << data
|
23
|
+
success = true
|
24
|
+
success = get_sentence while success
|
25
|
+
end
|
26
|
+
|
27
|
+
PAIR = /^(.+)=(.+)$/
|
28
|
+
|
29
|
+
# Indentifies the size of the length field in bits
|
30
|
+
# @return boolean, integer
|
31
|
+
def get_length_size
|
32
|
+
return false if @data.empty?
|
33
|
+
|
34
|
+
# High bits used for length type
|
35
|
+
# Low bits used as first bits of length
|
36
|
+
|
37
|
+
# 0xxxxxxx = 7 bit length
|
38
|
+
# 10xxxxxx = 14 bit length
|
39
|
+
# 110xxxxx = 21 bit length
|
40
|
+
# 1110xxxx = 28 bit length
|
41
|
+
# 11110000 = 32 bit length follows
|
42
|
+
|
43
|
+
t = @data.unpack('C').first
|
44
|
+
|
45
|
+
return 7 if t & 0b10000000 == 0
|
46
|
+
return 14 if t & 0b01000000 == 0
|
47
|
+
return 21 if t & 0b00100000 == 0
|
48
|
+
return 28 if t & 0b00010000 == 0
|
49
|
+
return 32 if t == 0b11110000
|
50
|
+
|
51
|
+
raise ArgumentError, "Invalid length type encoding"
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_length
|
55
|
+
size = get_length_size
|
56
|
+
|
57
|
+
return false unless size
|
58
|
+
|
59
|
+
need = bytes(size + 1)
|
60
|
+
return false unless @data.size >= need
|
61
|
+
|
62
|
+
length = nil
|
63
|
+
field = @data[0, need].unpack('C*')
|
64
|
+
|
65
|
+
case size
|
66
|
+
when 7
|
67
|
+
length = field[0] & 0x7f
|
68
|
+
when 14
|
69
|
+
length = ((field[0] & 0x3f) << 8) | field[1]
|
70
|
+
when 21
|
71
|
+
length = ((field[0] & 0x1f) << 16) | field[1] << 8 | field[2]
|
72
|
+
when 28
|
73
|
+
length = ((field[0] & 0x0f) << 24) | field[1] << 16 | field[2] << 8 | field[3]
|
74
|
+
when 32
|
75
|
+
length = field[1..4].pack('C4').unpack('N').first
|
76
|
+
end
|
77
|
+
|
78
|
+
if length
|
79
|
+
return length
|
80
|
+
else
|
81
|
+
raise ArgumentError, "Invalid word length"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def get_word
|
86
|
+
return false if @data.empty?
|
87
|
+
length_size = bytes(get_length_size + 1)
|
88
|
+
length = get_length
|
89
|
+
|
90
|
+
total_size = length_size + length
|
91
|
+
|
92
|
+
if length && @data.size >= total_size
|
93
|
+
@data.slice!(0, length_size)
|
94
|
+
word = @data.slice!(0, length)
|
95
|
+
Mikrotik.debug [:parser, :got_word, word]
|
96
|
+
return word
|
97
|
+
end
|
98
|
+
|
99
|
+
return false
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_sentence
|
103
|
+
while word = get_word
|
104
|
+
if word.empty?
|
105
|
+
Mikrotik.debug [:parser, :got_sentence, @sentence]
|
106
|
+
on_sentence(@sentence)
|
107
|
+
reset!
|
108
|
+
return true
|
109
|
+
else
|
110
|
+
if word =~ PAIR
|
111
|
+
key, value = word.scan(PAIR).flatten
|
112
|
+
@sentence[key] = value
|
113
|
+
else
|
114
|
+
@sentence[word] = nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
return false
|
120
|
+
end
|
121
|
+
|
122
|
+
def bytes(bits)
|
123
|
+
(bits / 8.0).ceil
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class Mikrotik::Protocol::Sentence < Hash
|
2
|
+
|
3
|
+
# Sentence only contains a zero length word
|
4
|
+
# @return [Boolean]
|
5
|
+
def empty?
|
6
|
+
super || false
|
7
|
+
end
|
8
|
+
|
9
|
+
# Is this the last reply for a command?
|
10
|
+
# @return [Boolean]
|
11
|
+
def done?
|
12
|
+
key?('!done')
|
13
|
+
end
|
14
|
+
|
15
|
+
# Is this reply only informing that a command is done, and contains no other data?
|
16
|
+
# @return [Boolean]
|
17
|
+
def completely_done?
|
18
|
+
size == 2 and done? and tagged?
|
19
|
+
end
|
20
|
+
|
21
|
+
# Is this informing of an error that should be trapped?
|
22
|
+
# @return [Boolean]
|
23
|
+
def trap?
|
24
|
+
key?('!trap')
|
25
|
+
end
|
26
|
+
|
27
|
+
# Does the reply have a command tag associated with it?
|
28
|
+
# @return [Boolean]
|
29
|
+
def tagged?
|
30
|
+
key?('.tag')
|
31
|
+
end
|
32
|
+
|
33
|
+
# Retreives the reply's command tag
|
34
|
+
#
|
35
|
+
# @return [nil] If the command is not tagged
|
36
|
+
# @return [Integer] tag If the command is tagged
|
37
|
+
def tag
|
38
|
+
tagged? ? self['.tag'] : nil
|
39
|
+
end
|
40
|
+
|
41
|
+
# Fetches a returned property
|
42
|
+
# @return value
|
43
|
+
def result(key)
|
44
|
+
self["=#{key}"]
|
45
|
+
end
|
46
|
+
|
47
|
+
# Fetches any returned value in this reply, converting it to a ruby type first
|
48
|
+
def [](key)
|
49
|
+
return nil unless key?(key)
|
50
|
+
case key
|
51
|
+
when '.tag'
|
52
|
+
fetch(key).to_i
|
53
|
+
else
|
54
|
+
value = fetch(key)
|
55
|
+
#case value
|
56
|
+
#when /\d+/
|
57
|
+
# value.to_i
|
58
|
+
#when /\d+.\d+/
|
59
|
+
# value.to_f
|
60
|
+
#when 'true'
|
61
|
+
# true
|
62
|
+
#when 'false'
|
63
|
+
# false
|
64
|
+
#else
|
65
|
+
# value
|
66
|
+
#end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# @private
|
2
|
+
class Mikrotik::Utilities
|
3
|
+
|
4
|
+
def self.hex_to_bin(input)
|
5
|
+
padding = input.size.even? ? '' : '0'
|
6
|
+
[padding + input].pack('H*')
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.bytepack(num, size)
|
10
|
+
s = String.new
|
11
|
+
s.force_encoding(Encoding::BINARY) if RUBY_VERSION >= '1.9.0'
|
12
|
+
x = num < 0 ? -num : num # Treat as unsigned
|
13
|
+
while size > 0
|
14
|
+
size -= 1
|
15
|
+
s = (x & 0xff).chr + s
|
16
|
+
x >>= 8
|
17
|
+
end
|
18
|
+
raise RuntimeError, "Number #{num} is too large to fit in #{size} bytes." if x > 0
|
19
|
+
return s
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
data/mikrotik.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "mikrotik/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "mikrotik"
|
7
|
+
s.version = Mikrotik::VERSION
|
8
|
+
s.authors = ["Iain Wilson"]
|
9
|
+
s.homepage = "http://github.com/ominiom/mikrotik"
|
10
|
+
s.summary = %q{A Mikrotik RouterOS API client using EventMachine}
|
11
|
+
s.description = %q{Provides an interface to run commands and collect results from a RouterOS device}
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require './lib/mikrotik.rb'
|
data/spec/parser.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe Mikrotik::Protocol::Parser do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
@parser = Mikrotik::Protocol::Parser.new
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '#get_length_size' do
|
10
|
+
|
11
|
+
it "returns 7 for 1 byte lengths" do
|
12
|
+
@parser.instance_variable_set(:@data, 0b01111111.chr)
|
13
|
+
@parser.send(:get_length_size).should eq(7)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "returns 14 for 2 byte lengths" do
|
17
|
+
@parser.instance_variable_set(:@data, 0b10111111.chr)
|
18
|
+
@parser.send(:get_length_size).should eq(14)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns 21 for 3 byte lengths" do
|
22
|
+
@parser.instance_variable_set(:@data, 0b11011111.chr)
|
23
|
+
@parser.send(:get_length_size).should eq(21)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "returns 28 for 28 bit lengths" do
|
27
|
+
@parser.instance_variable_set(:@data, 0b11101111.chr)
|
28
|
+
@parser.send(:get_length_size).should eq(28)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "returns 32 for 32 bit lengths" do
|
32
|
+
@parser.instance_variable_set(:@data, 0b11110000.chr)
|
33
|
+
@parser.send(:get_length_size).should eq(32)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#get_length' do
|
39
|
+
|
40
|
+
it "handles 7 bit lengths" do
|
41
|
+
@parser.instance_variable_set(:@data, 0b01111101.chr)
|
42
|
+
@parser.send(:get_length).should eq(0b1111101)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "handles 14 bit lengths" do
|
46
|
+
@parser.instance_variable_set(:@data, 0b10111111.chr + 0b11111111.chr)
|
47
|
+
@parser.send(:get_length).should eq(63 * 256 + 255)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mikrotik
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Iain Wilson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-18 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Provides an interface to run commands and collect results from a RouterOS
|
15
|
+
device
|
16
|
+
email:
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- .gitignore
|
22
|
+
- .yardopts
|
23
|
+
- Gemfile
|
24
|
+
- Guardfile
|
25
|
+
- README.markdown
|
26
|
+
- Rakefile
|
27
|
+
- doc/setup.rb
|
28
|
+
- examples/bandwidth_monitor.rb
|
29
|
+
- examples/dhcp.rb
|
30
|
+
- examples/uptime.rb
|
31
|
+
- lib/extensions/events.rb
|
32
|
+
- lib/extensions/string.rb
|
33
|
+
- lib/mikrotik.rb
|
34
|
+
- lib/mikrotik/client.rb
|
35
|
+
- lib/mikrotik/command.rb
|
36
|
+
- lib/mikrotik/connection.rb
|
37
|
+
- lib/mikrotik/errors.rb
|
38
|
+
- lib/mikrotik/protocol/parser.rb
|
39
|
+
- lib/mikrotik/protocol/sentence.rb
|
40
|
+
- lib/mikrotik/utilities.rb
|
41
|
+
- lib/mikrotik/version.rb
|
42
|
+
- mikrotik.gemspec
|
43
|
+
- spec/helper.rb
|
44
|
+
- spec/parser.rb
|
45
|
+
homepage: http://github.com/ominiom/mikrotik
|
46
|
+
licenses: []
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ! '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
requirements: []
|
64
|
+
rubyforge_project:
|
65
|
+
rubygems_version: 1.8.15
|
66
|
+
signing_key:
|
67
|
+
specification_version: 3
|
68
|
+
summary: A Mikrotik RouterOS API client using EventMachine
|
69
|
+
test_files: []
|
70
|
+
has_rdoc:
|