597 lines
25 KiB
Ruby
597 lines
25 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Auxiliary
|
|
#
|
|
# this module sends IEC104 commands
|
|
#
|
|
|
|
include Msf::Exploit::Remote::Tcp
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'IEC104 Client Utility',
|
|
'Description' => %q(
|
|
This module allows sending 104 commands.
|
|
),
|
|
'Author' =>
|
|
[
|
|
'Michael John <mjohn.info[at]gmail.com>'
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'Actions' =>
|
|
[
|
|
['SEND_COMMAND', { 'Description' => 'Send command to device' }]
|
|
],
|
|
'DefaultAction' => 'SEND_COMMAND'))
|
|
|
|
register_options(
|
|
[
|
|
Opt::RPORT(2404),
|
|
OptInt.new('ORIGINATOR_ADDRESS', [true, "Originator Address", 0]),
|
|
OptInt.new('ASDU_ADDRESS', [true, "Common Address of ASDU", 1]),
|
|
OptInt.new('COMMAND_ADDRESS', [true, "Command Address / IOA Address", 0]),
|
|
OptInt.new('COMMAND_TYPE', [true, "Command Type", 100]),
|
|
OptInt.new('COMMAND_VALUE', [true, "Command Value", 20])
|
|
]
|
|
)
|
|
end
|
|
|
|
# sends the frame data over tcp connection and returns recieved string
|
|
# using sock.get is causing quite some delay, but scripte needs to process responses from 104 server
|
|
def send_frame(data)
|
|
begin
|
|
sock.put(data)
|
|
sock.get(-1, sock.def_read_timeout)
|
|
rescue StandardError => e
|
|
print_error("Error:" + e.message)
|
|
end
|
|
end
|
|
|
|
# ACPI formats:
|
|
# TESTFR_CON = '\x83\x00\x00\x00'
|
|
# TESTFR_ACT = '\x43\x00\x00\x00'
|
|
# STOPDT_CON = '\x23\x00\x00\x00'
|
|
# STOPDT_ACT = '\x13\x00\x00\x00'
|
|
# STARTDT_CON = '\x0b\x00\x00\x00'
|
|
# STARTDT_ACT = '\x07\x00\x00\x00'
|
|
|
|
# creates and STARTDT Activation frame -> answer should be a STARTDT confirmation
|
|
def startcon
|
|
apci_data = "\x68"
|
|
apci_data << "\x04"
|
|
apci_data << "\x07"
|
|
apci_data << "\x00"
|
|
apci_data << "\x00"
|
|
apci_data << "\x00"
|
|
apci_data
|
|
end
|
|
|
|
# creates and STOPDT Activation frame -> answer should be a STOPDT confirmation
|
|
def stopcon
|
|
apci_data = "\x68"
|
|
apci_data << "\x04"
|
|
apci_data << "\x13"
|
|
apci_data << "\x00"
|
|
apci_data << "\x00"
|
|
apci_data << "\x00"
|
|
apci_data
|
|
end
|
|
|
|
# creates the acpi header of a 104 message
|
|
def make_apci(asdu_data)
|
|
apci_data = "\x68"
|
|
apci_data << [asdu_data.size + 4].pack("c") # size byte
|
|
apci_data << String([$tx].pack('v'))
|
|
apci_data << String([$rx].pack('v'))
|
|
$rx = $rx + 2
|
|
$tx = $tx + 2
|
|
apci_data << asdu_data
|
|
apci_data
|
|
end
|
|
|
|
# parses the header of a 104 message
|
|
def parse_headers(response_data)
|
|
if !response_data[0].eql?("\x04") && !response_data[1].eql?("\x01")
|
|
$rx = + (response_data[2].unpack('H*').first + response_data[1].unpack('H*').first).to_i(16)
|
|
print_good(" TX: " + response_data[4].unpack('H*').first + response_data[3].unpack('H*').first + \
|
|
" RX: " + response_data[2].unpack('H*').first + response_data[1].unpack('H*').first)
|
|
end
|
|
if response_data[7].eql?("\x07")
|
|
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Activation Confirmation)")
|
|
elsif response_data[7].eql?("\x0a")
|
|
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Termination Activation)")
|
|
elsif response_data[7].eql?("\x14")
|
|
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Inrogen)")
|
|
elsif response_data[7].eql?("\x0b")
|
|
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Feedback by distant command / Retrem)")
|
|
elsif response_data[7].eql?("\x03")
|
|
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Spontaneous)")
|
|
elsif response_data[7].eql?("\x04")
|
|
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Initialized)")
|
|
elsif response_data[7].eql?("\x05")
|
|
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Interrogation)")
|
|
elsif response_data[7].eql?("\x06")
|
|
print_good(" CauseTx: " + response_data[7].unpack('H*').first + " (Activiation)")
|
|
|
|
# 104 error messages
|
|
elsif response_data[7].eql?("\x2c")
|
|
print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (Type Identification Unknown)")
|
|
elsif response_data[7].eql?("\x2d")
|
|
print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (Cause Unknown)")
|
|
elsif response_data[7].eql?("\x2e")
|
|
print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (ASDU Address Unknown)")
|
|
elsif response_data[7].eql?("\x2f")
|
|
print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (IOA Address Unknown)")
|
|
elsif response_data[7].eql?("\x6e")
|
|
print_error(" CauseTx: " + response_data[7].unpack('H*').first + " (Unknown Comm Address ASDU)")
|
|
end
|
|
end
|
|
|
|
##############################################################################################################
|
|
# following functions parse different 104 ASDU messages and prints it content, not all messages of the standard are currently implemented
|
|
##############################################################################################################
|
|
def parse_m_sp_na_1(response_data)
|
|
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000 # this bit determines the object addressing structure
|
|
response_data = response_data[11..-1] # cut out acpi data
|
|
if sq_bit.eql?(0b10000000)
|
|
ioa = response_data[0..3] # extract ioa value
|
|
response_data = response_data[3..-1] # cut ioa from message
|
|
i = 0
|
|
while response_data.length >= 1
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
|
|
" SIQ: 0x" + response_data[0].unpack('H*').first)
|
|
response_data = response_data[1..-1]
|
|
i += 1
|
|
end
|
|
else
|
|
while response_data.length >= 4
|
|
ioa = response_data[0..3] # extract ioa
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
|
|
" SIQ: 0x" + response_data[3].unpack('H*').first)
|
|
response_data = response_data[4..-1]
|
|
end
|
|
end
|
|
end
|
|
|
|
def parse_m_me_nb_1(response_data)
|
|
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
|
|
response_data = response_data[11..-1] # cut out acpi data
|
|
if sq_bit.eql?(0b10000000)
|
|
ioa = response_data[0..3]
|
|
response_data = response_data[3..-1]
|
|
i = 0
|
|
while response_data.length >= 3
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
|
|
" Value: 0x" + response_data[0..1].unpack('H*').first + " QDS: 0x" + response_data[2].unpack('H*').first)
|
|
response_data = response_data[3..-1]
|
|
i += 1
|
|
end
|
|
else
|
|
while response_data.length >= 6
|
|
ioa = response_data[0..5]
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
|
|
" Value: 0x" + response_data[3..4].unpack('H*').first + " QDS: 0x" + + response_data[5].unpack('H*').first)
|
|
response_data = response_data[6..-1]
|
|
end
|
|
end
|
|
end
|
|
|
|
def parse_c_sc_na_1(response_data)
|
|
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
|
|
response_data = response_data[11..-1] # cut out acpi data
|
|
if sq_bit.eql?(0b10000000)
|
|
ioa = response_data[0..3]
|
|
response_data = response_data[3..-1]
|
|
i = 0
|
|
while response_data.length >= 1
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
|
|
" DIQ: 0x" + response_data[0].unpack('H*').first)
|
|
response_data = response_data[1..-1]
|
|
i += 1
|
|
end
|
|
else
|
|
while response_data.length >= 4
|
|
ioa = response_data[0..3]
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
|
|
" DIQ: 0x" + response_data[3].unpack('H*').first)
|
|
response_data = response_data[4..-1]
|
|
end
|
|
end
|
|
end
|
|
|
|
def parse_m_dp_na_1(response_data)
|
|
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
|
|
response_data = response_data[11..-1] # cut out acpi data
|
|
if sq_bit.eql?(0b10000000)
|
|
ioa = response_data[0..3]
|
|
response_data = response_data[3..-1]
|
|
i = 0
|
|
while response_data.length >= 1
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
|
|
" SIQ: 0x" + response_data[0].unpack('H*').first)
|
|
response_data = response_data[1..-1]
|
|
i += 1
|
|
end
|
|
else
|
|
while response_data.length >= 4
|
|
ioa = response_data[0..3]
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
|
|
" SIQ: 0x" + response_data[3].unpack('H*').first)
|
|
response_data = response_data[4..-1]
|
|
end
|
|
end
|
|
end
|
|
|
|
def parse_m_st_na_1(response_data)
|
|
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
|
|
response_data = response_data[11..-1] # cut out acpi data
|
|
if sq_bit.eql?(0b10000000)
|
|
ioa = response_data[0..3]
|
|
response_data = response_data[3..-1]
|
|
i = 0
|
|
while response_data.length >= 2
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
|
|
" VTI: 0x" + response_data[0].unpack('H*').first + " QDS: 0x" + response_data[1].unpack('H*').first)
|
|
response_data = response_data[2..-1]
|
|
i += 1
|
|
end
|
|
else
|
|
while response_data.length >= 5
|
|
ioa = response_data[0..4]
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
|
|
" VTI: 0x" + response_data[3].unpack('H*').first + " QDS: 0x" + response_data[4].unpack('H*').first)
|
|
response_data = response_data[5..-1]
|
|
end
|
|
end
|
|
end
|
|
|
|
def parse_m_dp_tb_1(response_data)
|
|
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
|
|
response_data = response_data[11..-1] # cut out acpi data
|
|
if sq_bit.eql?(0b10000000)
|
|
ioa = response_data[0..3]
|
|
response_data = response_data[3..-1]
|
|
i = 0
|
|
while response_data.length >= 8
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
|
|
" DIQ: 0x" + response_data[0].unpack('H*').first)
|
|
print_cp56time2a(response_data[1..7])
|
|
response_data = response_data[8..-1]
|
|
i += 1
|
|
end
|
|
else
|
|
while response_data.length >= 11
|
|
ioa = response_data[0..10]
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
|
|
" DIQ: 0x" + response_data[3].unpack('H*').first)
|
|
print_cp56time2a(response_data[4..10])
|
|
response_data = response_data[11..-1]
|
|
end
|
|
end
|
|
end
|
|
|
|
def parse_m_sp_tb_1(response_data)
|
|
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
|
|
response_data = response_data[11..-1] # cut out acpi data
|
|
if sq_bit.eql?(0b10000000)
|
|
ioa = response_data[0..3]
|
|
response_data = response_data[3..-1]
|
|
i = 0
|
|
while response_data.length >= 8
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
|
|
" SIQ: 0x" + response_data[0].unpack('H*').first)
|
|
print_cp56time2a(response_data[1..7])
|
|
response_data = response_data[8..-1]
|
|
i += 1
|
|
end
|
|
else
|
|
while response_data.length >= 11
|
|
ioa = response_data[0..10]
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
|
|
" SIQ: 0x" + response_data[3].unpack('H*').first)
|
|
print_cp56time2a(response_data[4..10])
|
|
response_data = response_data[11..-1]
|
|
end
|
|
end
|
|
end
|
|
|
|
def parse_c_dc_na_1(response_data)
|
|
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
|
|
response_data = response_data[11..-1] # cut out acpi data
|
|
if sq_bit.eql?(0b10000000)
|
|
ioa = response_data[0..3]
|
|
response_data = response_data[3..-1]
|
|
i = 0
|
|
while response_data.length >= 1
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
|
|
" DCO: 0x" + response_data[0].unpack('H*').first)
|
|
response_data = response_data[1..-1]
|
|
i += 1
|
|
end
|
|
else
|
|
while response_data.length >= 4
|
|
ioa = response_data[0..3]
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
|
|
" DCO: 0x" + response_data[3].unpack('H*').first)
|
|
response_data = response_data[4..-1]
|
|
end
|
|
end
|
|
end
|
|
|
|
def parse_m_me_na_1(response_data)
|
|
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
|
|
response_data = response_data[11..-1] # cut out acpi data
|
|
if sq_bit.eql?(0b10000000)
|
|
ioa = response_data[0..3]
|
|
response_data = response_data[3..-1]
|
|
i = 0
|
|
while response_data.length >= 3
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
|
|
" Value: 0x" + response_data[0..1].unpack('H*').first + " QDS: 0x" + response_data[2].unpack('H*').first)
|
|
response_data = response_data[3..-1]
|
|
i += 1
|
|
end
|
|
else
|
|
while response_data.length >= 6
|
|
ioa = response_data[0..3]
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
|
|
" Value: 0x" + ioa[3..4].unpack('H*').first + " QDS: 0x" + response_data[5].unpack('H*').first)
|
|
response_data = response_data[6..-1]
|
|
end
|
|
end
|
|
end
|
|
|
|
def parse_m_me_nc_1(response_data)
|
|
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
|
|
response_data = response_data[11..-1] # cut out acpi data
|
|
if sq_bit.eql?(0b10000000)
|
|
ioa = response_data[0..3]
|
|
response_data = response_data[3..-1]
|
|
i = 0
|
|
while response_data.length >= 5
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
|
|
" Value: 0x" + response_data[0..3].unpack('H*').first + " QDS: 0x" + response_data[4].unpack('H*').first)
|
|
response_data = response_data[5..-1]
|
|
i += 1
|
|
end
|
|
else
|
|
while response_data.length >= 8
|
|
ioa = response_data[0..3]
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
|
|
" Value: 0x" + response_data[3..6].unpack('H*').first + " QDS: 0x" + response_data[7].unpack('H*').first)
|
|
response_data = response_data[8..-1]
|
|
end
|
|
end
|
|
end
|
|
|
|
def parse_m_it_na_1(response_data)
|
|
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
|
|
response_data = response_data[11..-1] # cut out acpi data
|
|
if sq_bit.eql?(0b10000000)
|
|
response_data = response_data[11..-1]
|
|
ioa = response_data[0..3]
|
|
i = 0
|
|
while response_data.length >= 5
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
|
|
" Value: 0x" + response_data[0..3].unpack('H*').first + " QDS: 0x" + response_data[4].unpack('H*').first)
|
|
response_data = response_data[5..-1]
|
|
i += 1
|
|
end
|
|
else
|
|
while response_data.length >= 8
|
|
ioa = response_data[0..3]
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
|
|
" Value: 0x" + response_data[3..6].unpack('H*').first + " QDS: 0x" + response_data[7].unpack('H*').first)
|
|
response_data = response_data[8..-1]
|
|
end
|
|
end
|
|
end
|
|
|
|
def parse_m_bo_na_1(response_data)
|
|
sq_bit = Integer(response_data[6].unpack('C').first) & 0b10000000
|
|
response_data = response_data[11..-1] # cut out acpi data
|
|
if sq_bit.eql?(0b10000000)
|
|
ioa = response_data[0..3]
|
|
response_data = response_data[3..-1]
|
|
i = 0
|
|
while response_data.length >= 5
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16) + i) + \
|
|
" Value: 0x" + response_data[0..3].unpack('H*').first + " QDS: 0x" + response_data[4].unpack('H*').first)
|
|
response_data = response_data[5..-1]
|
|
i += 1
|
|
end
|
|
else
|
|
while response_data.length >= 8
|
|
ioa = response_data[0..3]
|
|
print_good(" IOA: " + String((ioa[2].unpack('H*').first + ioa[1].unpack('H*').first + ioa[0].unpack('H*').first).to_i(16)) + \
|
|
" Value: 0x" + response_data[3..6].unpack('H*').first + " QDS: 0x" + response_data[7].unpack('H*').first)
|
|
response_data = response_data[8..-1]
|
|
end
|
|
end
|
|
end
|
|
|
|
# function to parses time format used in IEC 104
|
|
# function ported to ruby from: https://github.com/Ebolon/iec104
|
|
def print_cp56time2a(buf)
|
|
us = ((Integer(buf[1].unpack('c').first) & 0xFF) << 8) | (Integer(buf[0].unpack('c').first) & 0xFF)
|
|
second = Integer(us) / 1000
|
|
us = us % 1000
|
|
minute = Integer(buf[2].unpack('c').first) & 0x3F
|
|
hour = Integer(buf[3].unpack('c').first) & 0x1F
|
|
day = Integer(buf[4].unpack('c').first) & 0x1F
|
|
month = (Integer(buf[5].unpack('c').first) & 0x0F) - 1
|
|
year = (Integer(buf[6].unpack('c').first) & 0x7F) + 2000
|
|
print_good(" Timestamp: " + String(year) + "-" + String(format("%02d", month)) + "-" + String(format("%02d", day)) + " " + \
|
|
String(format("%02d", hour)) + ":" + String(format("%02d", minute)) + ":" + String(format("%02d", second)) + "." + String(us))
|
|
end
|
|
|
|
##############################################################################################################
|
|
# END of individual parse functions section
|
|
##############################################################################################################
|
|
|
|
# parses the 104 response string of a message
|
|
def parse_response(response)
|
|
response_elements = response.split("\x68")
|
|
response_elements.shift
|
|
response_elements.each do |response_element|
|
|
if response_element[5].eql?("\x64")
|
|
print_good(" Parsing response: Interrogation command (C_IC_NA_1)")
|
|
parse_headers(response_element)
|
|
elsif response_element[5].eql?("\x01")
|
|
print_good(" Parsing response: Single point information (M_SP_NA_1)")
|
|
parse_headers(response_element)
|
|
parse_m_sp_na_1(response_element)
|
|
elsif response_element[5].eql?("\x0b")
|
|
print_good(" Parsing response: Measured value, scaled value (M_ME_NB_1)")
|
|
parse_headers(response_element)
|
|
parse_m_me_nb_1(response_element)
|
|
elsif response_element[5].eql?("\x2d")
|
|
print_good(" Parsing response: Single command (C_SC_NA_1)")
|
|
parse_headers(response_element)
|
|
parse_c_sc_na_1(response_element)
|
|
elsif response_element[5].eql?("\x03")
|
|
print_good(" Parsing response: Double point information (M_DP_NA_1)")
|
|
parse_headers(response_element)
|
|
parse_m_dp_na_1(response_element)
|
|
elsif response_element[5].eql?("\x05")
|
|
print_good(" Parsing response: Step position information (M_ST_NA_1)")
|
|
parse_headers(response_element)
|
|
parse_m_st_na_1(response_element)
|
|
elsif response_element[5].eql?("\x1f")
|
|
print_good(" Parsing response: Double point information with time (M_DP_TB_1)")
|
|
parse_headers(response_element)
|
|
parse_m_dp_tb_1(response_element)
|
|
elsif response_element[5].eql?("\x2e")
|
|
print_good(" Parsing response: Double command (C_DC_NA_1)")
|
|
parse_headers(response_element)
|
|
parse_c_dc_na_1(response_element)
|
|
elsif response_element[5].eql?("\x1e")
|
|
print_good(" Parsing response: Single point information with time (M_SP_TB_1)")
|
|
parse_headers(response_element)
|
|
parse_m_sp_tb_1(response_element)
|
|
elsif response_element[5].eql?("\x09")
|
|
print_good(" Parsing response: Measured value, normalized value (M_ME_NA_1)")
|
|
parse_headers(response_element)
|
|
parse_m_me_na_1(response_element)
|
|
elsif response_element[5].eql?("\x0d")
|
|
print_good(" Parsing response: Measured value, short floating point value (M_ME_NC_1)")
|
|
parse_headers(response_element)
|
|
parse_m_me_nc_1(response_element)
|
|
elsif response_element[5].eql?("\x0f")
|
|
print_good(" Parsing response: Integrated total without time tag (M_IT_NA_1)")
|
|
parse_headers(response_element)
|
|
parse_m_it_na_1(response_element)
|
|
elsif response_element[5].eql?("\x07")
|
|
print_good(" Parsing response: Bitstring of 32 bits without time tag. (M_BO_NA_1)")
|
|
parse_headers(response_element)
|
|
parse_m_bo_na_1(response_element)
|
|
|
|
elsif response_element[5].eql?("\x46")
|
|
print_good("Recieved end of initialisation confirmation")
|
|
parse_headers(response_element)
|
|
elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x01") && response_element[2].eql?("\x00")
|
|
print_good("Recieved S-Frame")
|
|
parse_headers(response_element)
|
|
elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x0b") && response_element[2].eql?("\x00") && response_element[3].eql?("\x00")
|
|
print_good("Recieved STARTDT_ACT")
|
|
elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x23") && response_element[2].eql?("\x00") && response_element[3].eql?("\x00")
|
|
print_good("Recieved STOPDT_ACT")
|
|
elsif response_element[0].eql?("\x04") && response_element[1].eql?("\x43") && response_element[2].eql?("\x00") && response_element[3].eql?("\x00")
|
|
print_good("Recieved TESTFR_ACT")
|
|
else
|
|
print_status("Recieved unknown message")
|
|
parse_headers(response_element)
|
|
print_status(response_element.unpack('H*').first)
|
|
end
|
|
# Uncomment for print recieved data
|
|
# print_good("DEBUG: " + response_element.unpack('H*').first)
|
|
end
|
|
end
|
|
|
|
# sends 104 command with configure datastore options
|
|
# default values are for a general interrogation command
|
|
# for example a switching command would be:
|
|
# COMMAND_TYPE => 46 // double command without time
|
|
# COMMAND_ADDRESS => 100 // any IOA address that should be switched
|
|
# COMMAND_VALUE => 6 // switching off with short pulse
|
|
# use value 5 to switch on with short pulse
|
|
#
|
|
# Structure of 104 message:
|
|
# 1byte command type
|
|
# 1byte num ix -> 1 (one item send)
|
|
# 1byte cause of transmission -> 6 (activation)
|
|
# 1byte originator address
|
|
# 2byte common adsu address
|
|
# 3byte command address
|
|
# 1byte command value
|
|
def func_send_command
|
|
print_status("Sending 104 command")
|
|
|
|
asdu = [datastore['COMMAND_TYPE']].pack("c") # type of command
|
|
asdu << "\x01" # num ix -> only one item is send
|
|
asdu << "\x06" # cause of transmission = activation, 6
|
|
asdu << [datastore['ORIGINATOR_ADDRESS']].pack("c") # sets originator address of client
|
|
asdu << String([Integer(datastore['ASDU_ADDRESS'])].pack('v')) # sets the common address of ADSU
|
|
asdu << String([Integer(datastore['COMMAND_ADDRESS'])].pack('V'))[0..2] # sets the IOA address, todo: check command address fits in the 3byte address field
|
|
asdu << [datastore['COMMAND_VALUE']].pack("c") # sets the value of the command
|
|
|
|
# Uncomment for debugging
|
|
# print_status("Sending: " + make_apci(asdu).unpack('H*').first)
|
|
response = send_frame(make_apci(asdu))
|
|
|
|
if response.nil?
|
|
print_error("No answer")
|
|
else
|
|
parse_response(response)
|
|
end
|
|
print_status("operation ended")
|
|
end
|
|
|
|
def run
|
|
$rx = 0
|
|
$tx = 0
|
|
begin
|
|
connect
|
|
rescue StandardError => e
|
|
print_error("Error:" + e.message)
|
|
return
|
|
end
|
|
|
|
# send STARTDT_CON to activate connection
|
|
response = send_frame(startcon)
|
|
if response.nil?
|
|
print_error("Could not connect to 104 service")
|
|
return
|
|
else
|
|
parse_response(response)
|
|
end
|
|
|
|
# send the 104 command
|
|
case action.name
|
|
when "SEND_COMMAND"
|
|
func_send_command
|
|
else
|
|
print_error("Invalid ACTION")
|
|
end
|
|
|
|
# send STOPDT_CON to terminate connection
|
|
response = send_frame(stopcon)
|
|
if response.nil?
|
|
print_error("Terminating Connection")
|
|
return
|
|
else
|
|
print_status("Terminating Connection")
|
|
parse_response(response)
|
|
end
|
|
|
|
begin
|
|
disconnect
|
|
rescue StandardError => e
|
|
print_error("Error:" + e.message)
|
|
end
|
|
end
|
|
end
|