-
Notifications
You must be signed in to change notification settings - Fork 4
/
Mikrotik.lua
309 lines (257 loc) · 7.21 KB
/
Mikrotik.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
-- Compatibility with Lua 5.1 with no built-in bitwise operations.
if not pcall(require, 'bit32') then
-- The BitOp library required.
require('bit')
bit32 = bit
end
local bnot, band, bor = bit32.bnot, bit32.band, bit32.bor
local lrotate, rrotate = bit32.lrotate or bit32.lshift, bit32.rrotate or bit32.rshift
-- openresty?
local has_ngx, ngx = pcall(require, 'ngx')
local socket = (has_ngx and ngx.socket) or require('socket')
local md5 = (has_ngx and ngx.md5) or require('md5')
local DEFAULT_PORT = 8728
local function table_empty(t)
return next(t) == nil
end
local function byte(int, byteIdx)
local shifted = rrotate(int, 8 * byteIdx)
return band(shifted, 0xff)
end
local function hextostring(str)
return (str:gsub('..', function(encodedByte)
return string.char(tonumber(encodedByte, 16))
end))
end
local function md5sumhex(str)
if type(md5) == 'function' then
-- Openresty md5
return md5(str)
elseif md5.sumhexa then
-- MD5
return md5.sumhexa(str)
elseif md5.new and md5.tohex then
-- md5.lua
local sum = md5.new()
sum:update(str)
return md5.tohex(sum:finish())
else
error('Unknown md5 library detected')
end
end
local function parseWord(word)
local _, equalsPos = string.find(word, '.=')
if not equalsPos then
return "type", word
end
local tag = word:sub(1, equalsPos - 1)
local value = word:sub(equalsPos + 1)
return tag, value
end
local function encodeLength(l)
local char = string.char
if l < 0x80 then
return char(l)
elseif l < 0x4000 then
local l = bor(l, 0x8000)
return
char(byte(l, 1)) ..
char(byte(l, 0))
elseif l < 0x200000 then
local l = bor(l, 0xC00000)
return
char(byte(l, 2)) ..
char(byte(l, 1)) ..
char(byte(l, 0))
elseif l < 0x10000000 then
local l = bor(l, 0xE0000000)
return
char(byte(l, 3)) ..
char(byte(l, 2)) ..
char(byte(l, 1)) ..
char(byte(l, 0))
else
return
'\xF0' ..
char(byte(l, 3)) ..
char(byte(l, 2)) ..
char(byte(l, 1)) ..
char(byte(l, 0))
end
end
local function encodeWord(word)
return encodeLength(string.len(word)) .. word
end
-- class Mikrotik
local Mikrotik = {}
Mikrotik.__index = Mikrotik
function Mikrotik:create(address, port, timeout)
local mtk = {}
setmetatable(mtk, Mikrotik)
local client = socket.tcp()
if timeout then
client:settimeout(timeout)
end
if not client:connect(address, port or DEFAULT_PORT) then
return nil
end
mtk.client = client
mtk.nextSentenceTagId = 0
mtk.tagToCallbackTable = {}
mtk.debug = false
return mtk
end
function Mikrotik:readByte()
return self.client:receive(1):byte(1)
end
function Mikrotik:readLen()
local l = self:readByte()
if band(l, 0x80) == 0x00 then
return l
elseif band(l, 0xc0) == 0x80 then
l = band(l, bnot(0xc0))
return
lrotate(l, 8) +
self:readByte()
elseif band(l, 0xe0) == 0xc0 then
l = band(l, bnot(0xc0))
return
lrotate(l, 16) +
lrotate(self:readByte(), 8) +
self:readByte()
elseif band(l, 0xf0) == 0xe0 then
l = band(l, bnot(0xf0))
return
lrotate(l, 24) +
lrotate(self:readByte(), 16) +
lrotate(self:readByte(), 8) +
self:readByte()
elseif band(l, 0xf8) == 0xf0 then
return
lrotate(self:readByte(), 24) +
lrotate(self:readByte(), 16) +
lrotate(self:readByte(), 8) +
self:readByte()
end
end
-- Gets a tag from a message going to be sent
local function getTag(sentence)
local function starts_with(str, start)
return str:sub(1, #start) == start
end
local TAG_PREFIX = '.tag='
for i, word in ipairs(sentence) do
if starts_with(word, TAG_PREFIX) then
return word:sub(#TAG_PREFIX + 1)
end
end
return nil
end
function Mikrotik:sendSentence(sentence, callback)
if self.debug then
for k, v in ipairs(sentence) do
print(">>>>>>", v)
end
end
local message = ""
for i, word in ipairs(sentence) do
message = message .. encodeWord(word)
end
if callback then
local tag = getTag(sentence)
if not tag then
tag = self:nextTag()
message = message .. encodeWord('.tag=' .. tag)
if self.debug then
print(">>>>>> (added by sendSentence) ", '.tag=' .. tag)
end
end
self.tagToCallbackTable[tag] = callback
end
if self.debug then
print(">>>>>>")
end
-- The closing empty word - essentialy a null byte
message = message .. '\0'
return self:send(message)
end
function Mikrotik:readWord()
local len = self:readLen()
if not len or len == 0 then
return nil
end
return self.client:receive(len)
end
function Mikrotik:readSentenceBypassingCallbacks()
local sentence = {}
while true do
local word = self:readWord()
if not word then
if self.debug then
for k, v in pairs(sentence) do
print("<<<<<<", k, v)
end
print("<<<<<<")
end
return sentence
end
local tag, value = parseWord(word)
sentence[tag] = value
end
end
function Mikrotik:handleCallback(sentence)
local tag = sentence['.tag']
if not tag then
return false
end
local callback = self.tagToCallbackTable[tag]
if not callback then
return false
end
callback(sentence)
local type = sentence.type
if type == '!done' or type == '!trap' then
self.tagToCallbackTable[tag] = nil
end
return true
end
function Mikrotik:readSentence()
if self.queuedSentence then
self.queuedSentence = nil
return self.queuedSentence
end
local sentence
repeat
sentence = self:readSentenceBypassingCallbacks()
until not self:handleCallback(sentence) or table_empty(self.tagToCallbackTable)
return sentence
end
function Mikrotik:send(message)
return self.client:send(message)
end
function Mikrotik:wait()
local sentence = self:readSentence()
self.queuedSentence = sentence
return sentence
end
function Mikrotik:nextTag()
self.nextSentenceTagId = self.nextSentenceTagId + 1
return "lua-mtk-" .. self.nextSentenceTagId
end
function Mikrotik:login(user, pass)
self:sendSentence({ "/login" })
local loginResponse = self:readSentence()
if not loginResponse or loginResponse.type ~= '!done' then
return nil
end
local challange = hextostring(loginResponse['=ret'])
local sum = md5sumhex('\0' .. pass .. challange)
self:sendSentence({ "/login", "=name=" .. user, "=response=00" .. sum })
local challangeResponse = self:readSentence()
if not challangeResponse or challangeResponse.type ~= '!done' then
-- Bad credentials?
return nil
end
return true
end
return Mikrotik