diff --git a/embedded/mms_dispatcher_hard_profile_smoke.cpp b/embedded/mms_dispatcher_hard_profile_smoke.cpp index 234002136..04b902339 100644 --- a/embedded/mms_dispatcher_hard_profile_smoke.cpp +++ b/embedded/mms_dispatcher_hard_profile_smoke.cpp @@ -68,6 +68,17 @@ constexpr std::array kReadRequest{ 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, 0x1AU, 0x02U, 0x4DU, 0x31U}; +constexpr std::array kReadWithSpecificationRequest{ + 0xA0U, 0x2AU, 0x02U, 0x01U, 0x0DU, + 0xA4U, 0x25U, 0x80U, 0x01U, 0xFFU, + 0xA1U, 0x20U, 0xA0U, 0x1EU, + 0x30U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x52U, 0x31U, + 0x30U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x4DU, 0x31U}; + constexpr std::array kReadResponse{ 0xA1U, 0x0DU, 0x02U, 0x01U, 0x0CU, 0xA4U, 0x08U, 0xA1U, 0x06U, @@ -239,6 +250,14 @@ int main() { return 6; } + dispatched = dispatcher.dispatch( + kReadWithSpecificationRequest, response, workspace); + if (dispatched.status != mms::MmsStaticDispatchStatus::unsupported_request || + dispatched.service != mms::MmsWireConfirmedService::read || + dispatched.invoke_id != 13U) { + return 30; + } + std::array tiny_workspace{}; dispatched = dispatcher.dispatch(kReadRequest, response, tiny_workspace); if (dispatched.status != mms::MmsStaticDispatchStatus::workspace_too_small || diff --git a/embedded/mms_negotiated_limits_hard_profile_smoke.cpp b/embedded/mms_negotiated_limits_hard_profile_smoke.cpp index e6ab2f5bf..c3ba8b7ed 100644 --- a/embedded/mms_negotiated_limits_hard_profile_smoke.cpp +++ b/embedded/mms_negotiated_limits_hard_profile_smoke.cpp @@ -68,6 +68,32 @@ constexpr std::array kReadRequest{ return {wire::EncodeStatus::ok, required, required}; } +[[nodiscard]] mms::MmsStaticWriteResult write_boolean( + void* context, + const std::span encoded_data) noexcept { + if (context == nullptr || encoded_data.size() != 3U || + encoded_data[0] != 0x83U || encoded_data[1] != 0x01U) { + return {false, 10U}; + } + *static_cast(context) = encoded_data[2] != 0U; + return {true, 0U}; +} + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, 3U}; + } + if (destination.size() < 3U) { + return {wire::EncodeStatus::buffer_too_small, 0U, 3U}; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = *static_cast(context) ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, 3U, 3U}; +} + [[nodiscard]] bool set_maximum_mms_pdu( std::array& association, const std::uint32_t value) noexcept { @@ -94,6 +120,82 @@ constexpr std::array kReadRequest{ return false; } +[[nodiscard]] bool set_initiate_negotiation( + std::array& association, + const std::uint8_t outstanding_calling, + const std::uint8_t outstanding_called, + const std::uint8_t nesting_level) noexcept { + bool calling_set = false; + bool called_set = false; + bool nesting_set = false; + for (std::size_t index = 0U; index + 3U <= association.size(); ++index) { + if (association[index + 1U] != 0x01U) { + continue; + } + if (!calling_set && association[index] == 0x81U) { + association[index + 2U] = outstanding_calling; + calling_set = true; + } else if (!called_set && association[index] == 0x82U) { + association[index + 2U] = outstanding_called; + called_set = true; + } else if (!nesting_set && association[index] == 0x83U) { + association[index + 2U] = nesting_level; + nesting_set = true; + } + } + return calling_set && called_set && nesting_set; +} + +[[nodiscard]] bool decode_initiate_response( + const std::span response, + mms::MmsInitiateView& initiate) noexcept { + for (std::size_t index = 0U; index + 2U <= response.size(); ++index) { + if (response[index] != 0xA9U) { + continue; + } + const auto content = static_cast(response[index + 1U]); + if ((response[index + 1U] & 0x80U) != 0U || + content > response.size() - index - 2U) { + continue; + } + if (mms::MmsPduSpanCodec::try_decode_initiate_response_view( + response.subspan(index, content + 2U), initiate)) { + return true; + } + } + return false; +} + +[[nodiscard]] wire::EncodeResult build_ten_variable_write( + std::array& output) noexcept { + constexpr std::array definition{ + 0x30U,0x0DU,0xA0U,0x0BU,0xA1U,0x09U, + 0x1AU,0x03U,0x4CU,0x44U,0x30U, + 0x1AU,0x02U,0x57U,0x31U}; + std::array service{}; + std::size_t offset = 0U; + service[offset++] = 0xA0U; + service[offset++] = 0x81U; + service[offset++] = 0x96U; + for (std::size_t index = 0U; index < 10U; ++index) { + for (const auto byte : definition) { + service[offset++] = byte; + } + } + service[offset++] = 0xA0U; + service[offset++] = 0x1EU; + for (std::size_t index = 0U; index < 10U; ++index) { + service[offset++] = 0x83U; + service[offset++] = 0x01U; + service[offset++] = 0xFFU; + } + if (offset != service.size()) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + return mms::MmsPduSpanCodec::encode_confirmed_request_into( + 20U, 5, true, service, output); +} + template [[nodiscard]] wire::EncodeResult build_data_tpkt( const std::span payload, @@ -164,9 +266,17 @@ template std::span request, std::span response, std::span workspace, - std::span scratch) noexcept { + std::span scratch, + const std::uint8_t outstanding_calling = 10U, + const std::uint8_t outstanding_called = 10U, + const std::uint8_t nesting_level = 5U) noexcept { auto association = kAssociationRequest; - if (!set_maximum_mms_pdu(association, maximum_mms_pdu)) { + if (!set_maximum_mms_pdu(association, maximum_mms_pdu) || + !set_initiate_negotiation( + association, + outstanding_calling, + outstanding_called, + nesting_level)) { return {}; } const auto cotp = osi::CotpSpanCodec::encode_data_into(association, scratch); @@ -189,18 +299,26 @@ int main() { for (std::size_t index = 0U; index < large_value.size(); ++index) { large_value[index] = static_cast(index); } - const std::array objects{ + bool write_state = false; + constexpr std::array boolean_type{0x83U,0x00U}; + const std::array objects{ mms::MmsStaticObjectEntry{ "LD0", "R1", kOctet80Type, read_large_octets, &large_value, false}, mms::MmsStaticObjectEntry{ - "LD0", "M1", kOctet80Type, read_large_octets, &large_value, false}}; + "LD0", "M1", kOctet80Type, read_large_octets, &large_value, false}, + mms::MmsStaticObjectEntry{ + "LD0", "W1", boolean_type, read_boolean, &write_state, false, + write_boolean, &write_state}}; const mms::MmsStaticObjectTable object_table{objects}; const std::array members{ mms::MmsStaticDataSetMember{"LD0", "R1"}}; const std::array data_sets{ mms::MmsStaticDataSetEntry{"LD0", "LLN0$Events", members, false}}; const mms::MmsStaticDataSetTable data_set_table{data_sets}; - const mms::MmsStaticApplicationDispatcher dispatcher{object_table, data_set_table}; + mms::MmsStaticDispatchPolicy dispatch_policy; + dispatch_policy.maximum_write_variables = 10U; + const mms::MmsStaticApplicationDispatcher dispatcher{ + object_table, data_set_table, dispatch_policy}; if (!object_table.valid() || !data_set_table.valid_against(object_table)) { return 1; } @@ -236,14 +354,44 @@ int main() { if (!connect_cotp(runtime, 0x09U, request, response, workspace, scratch)) { return 4; } - const auto association = associate(runtime, 64U, request, response, workspace, scratch); + const auto association = associate( + runtime, 64U, request, response, workspace, scratch, 2U, 3U, 1U); + mms::MmsInitiateView negotiated; if (!association.response_ready() || runtime.state() != mms::MmsStaticConnectionState::established || runtime.negotiated_tpdu_size_bytes() != 512U || - runtime.negotiated_mms_pdu_size() != 64U) { + runtime.negotiated_mms_pdu_size() != 64U || + !decode_initiate_response( + std::span{response}.first(association.bytes_written), + negotiated) || + negotiated.maximum_mms_pdu_size != 64U || + negotiated.maximum_outstanding_calling != 2U || + negotiated.maximum_outstanding_called != 3U || + negotiated.data_structure_nesting_level != 1U) { return 5; } + std::array write_pdu{}; + const auto write_encoded = build_ten_variable_write(write_pdu); + const auto write_tpkt = write_encoded.success() + ? build_mms_tpkt( + std::span{write_pdu}.first(write_encoded.bytes_written), + request, + presentation, + scratch) + : wire::EncodeResult{}; + if (!write_tpkt.success()) { + return 13; + } + const auto write_result = runtime.process_tcp_window( + std::span{request}.first(write_tpkt.bytes_written), + response, + workspace); + if (write_result.status != mms::MmsStaticConnectionStatus::peer_limit_exceeded || + write_result.bytes_written != 0U || write_state) { + return 14; + } + // The two large Read results exceed the peer's 64-byte MMS PDU offer. No // oversized confirmed response may be placed on the wire. const auto read_tpkt = build_mms_tpkt( diff --git a/embedded/mms_pdu_hard_profile_smoke.cpp b/embedded/mms_pdu_hard_profile_smoke.cpp index 270e2c08b..89ebf9079 100644 --- a/embedded/mms_pdu_hard_profile_smoke.cpp +++ b/embedded/mms_pdu_hard_profile_smoke.cpp @@ -106,6 +106,29 @@ int main() { return 3; } + const auto dynamic_default = mms::MmsPduSpanCodec::encode_initiate_response_into( + 65'000U, 10U, 10U, 5U, buffer); + if (!dynamic_default.success() || + dynamic_default.bytes_written != kInitiateResponse.size() || + !matches( + std::span{buffer}.first(dynamic_default.bytes_written), + kInitiateResponse)) { + return 30; + } + + const auto negotiated = mms::MmsPduSpanCodec::encode_initiate_response_into( + 1'024U, 2U, 3U, 1U, buffer); + if (!negotiated.success() || + !mms::MmsPduSpanCodec::try_decode_initiate_response_view( + std::span{buffer}.first(negotiated.bytes_written), + initiate) || + initiate.maximum_mms_pdu_size != 1'024U || + initiate.maximum_outstanding_calling != 2U || + initiate.maximum_outstanding_called != 3U || + initiate.data_structure_nesting_level != 1U) { + return 31; + } + constexpr std::array confirmed_request_golden{ 0xA0U, 0x05U, 0x02U, 0x01U, 0x07U, 0xA4U, 0x00U}; const auto confirmed_request_result = @@ -229,4 +252,4 @@ int main() { } return 0; -} \ No newline at end of file +} diff --git a/include/ariec61850/acse/association_span.hpp b/include/ariec61850/acse/association_span.hpp index d8446b363..7e7337297 100644 --- a/include/ariec61850/acse/association_span.hpp +++ b/include/ariec61850/acse/association_span.hpp @@ -79,6 +79,11 @@ class AcseSpanCodec final { [[nodiscard]] static wire::EncodeResult build_accept_response_into( const AssociationRequestView& request, std::span destination) noexcept; + + [[nodiscard]] static wire::EncodeResult build_accept_response_into( + const AssociationRequestView& request, + std::span mms_initiate_response, + std::span destination) noexcept; }; } // namespace ar::iec61850::acse diff --git a/include/ariec61850/mms/pdu_span.hpp b/include/ariec61850/mms/pdu_span.hpp index 145d1cf25..94bf40ddd 100644 --- a/include/ariec61850/mms/pdu_span.hpp +++ b/include/ariec61850/mms/pdu_span.hpp @@ -76,6 +76,13 @@ class MmsPduSpanCodec final { [[nodiscard]] static wire::EncodeResult encode_default_initiate_response_into( std::span destination) noexcept; + [[nodiscard]] static wire::EncodeResult encode_initiate_response_into( + std::uint32_t maximum_mms_pdu_size, + std::uint32_t maximum_outstanding_calling, + std::uint32_t maximum_outstanding_called, + std::uint32_t data_structure_nesting_level, + std::span destination) noexcept; + [[nodiscard]] static bool try_decode_confirmed_request_view( std::span bytes, MmsConfirmedPduView& request) noexcept; diff --git a/src/acse/association_span.cpp b/src/acse/association_span.cpp index ee976a8f4..4caa894db 100644 --- a/src/acse/association_span.cpp +++ b/src/acse/association_span.cpp @@ -3,6 +3,7 @@ #include "ariec61850/acse/association_span.hpp" #include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/asn1/ber_span_writer.hpp" #include #include @@ -69,6 +70,115 @@ constexpr std::array kDefaultAcceptAare{ 0x82U, 0x0CU, 0x03U, 0x4EU, 0x08U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U}; +[[nodiscard]] std::optional accept_aare_size( + const std::size_t mms_initiate_response_bytes) noexcept { + const auto application_context_oid = asn1::BerSpanWriter::tlv_size( + 6, kMmsApplicationContext.size()); + const auto application_context = application_context_oid + ? asn1::BerSpanWriter::tlv_size(1, *application_context_oid) + : std::nullopt; + const auto result_integer = asn1::BerSpanWriter::tlv_size(2, 1U); + const auto result = result_integer + ? asn1::BerSpanWriter::tlv_size(2, *result_integer) + : std::nullopt; + const auto diagnostic_integer = asn1::BerSpanWriter::tlv_size(2, 1U); + const auto diagnostic_choice = diagnostic_integer + ? asn1::BerSpanWriter::tlv_size(1, *diagnostic_integer) + : std::nullopt; + const auto diagnostic = diagnostic_choice + ? asn1::BerSpanWriter::tlv_size(3, *diagnostic_choice) + : std::nullopt; + const auto direct_reference = asn1::BerSpanWriter::tlv_size(6, 2U); + const auto indirect_reference = asn1::BerSpanWriter::tlv_size(2, 1U); + const auto single_asn1_type = asn1::BerSpanWriter::tlv_size( + 0, mms_initiate_response_bytes); + if (!application_context || !result || !diagnostic || + !direct_reference || !indirect_reference || !single_asn1_type) { + return std::nullopt; + } + const auto external_content = + *direct_reference + *indirect_reference + *single_asn1_type; + const auto external = asn1::BerSpanWriter::tlv_size(8, external_content); + if (!external) { + return std::nullopt; + } + const auto user_information = asn1::BerSpanWriter::tlv_size(30, *external); + if (!user_information) { + return std::nullopt; + } + const auto aare_content = + *application_context + *result + *diagnostic + *user_information; + return asn1::BerSpanWriter::tlv_size(1, aare_content); +} + +[[nodiscard]] wire::EncodeResult encode_accept_aare_into( + const std::span mms_initiate_response, + const std::span destination) noexcept { + const auto required = accept_aare_size(mms_initiate_response.size()); + if (!required) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < *required) { + return {wire::EncodeStatus::buffer_too_small, 0U, *required}; + } + + const auto application_context_oid = asn1::BerSpanWriter::tlv_size( + 6, kMmsApplicationContext.size()).value(); + const auto result_integer = asn1::BerSpanWriter::tlv_size(2, 1U).value(); + const auto diagnostic_integer = asn1::BerSpanWriter::tlv_size(2, 1U).value(); + const auto diagnostic_choice = asn1::BerSpanWriter::tlv_size( + 1, diagnostic_integer).value(); + const auto direct_reference = asn1::BerSpanWriter::tlv_size(6, 2U).value(); + const auto indirect_reference = asn1::BerSpanWriter::tlv_size(2, 1U).value(); + const auto single_asn1_type = asn1::BerSpanWriter::tlv_size( + 0, mms_initiate_response.size()).value(); + const auto external_content = + direct_reference + indirect_reference + single_asn1_type; + const auto external = asn1::BerSpanWriter::tlv_size(8, external_content).value(); + const auto user_information = asn1::BerSpanWriter::tlv_size(30, external).value(); + const auto application_context = asn1::BerSpanWriter::tlv_size( + 1, application_context_oid).value(); + const auto result = asn1::BerSpanWriter::tlv_size(2, result_integer).value(); + const auto diagnostic = asn1::BerSpanWriter::tlv_size( + 3, diagnostic_choice).value(); + const auto aare_content = application_context + result + diagnostic + user_information; + + constexpr std::array direct_reference_oid{0x51U, 0x01U}; + asn1::BerSpanWriter writer{destination.first(*required)}; + if (!writer.write_tlv_header(asn1::BerClass::application, true, 1, aare_content) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 1, application_context_oid) || + !writer.write_tlv( + asn1::BerClass::universal, false, 6, kMmsApplicationContext) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 2, result_integer) || + !writer.write_tlv_header(asn1::BerClass::universal, false, 2, 1U) || + !writer.write_byte(0U) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 3, diagnostic_choice) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 1, diagnostic_integer) || + !writer.write_tlv_header(asn1::BerClass::universal, false, 2, 1U) || + !writer.write_byte(0U) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 30, external) || + !writer.write_tlv_header( + asn1::BerClass::universal, true, 8, external_content) || + !writer.write_tlv( + asn1::BerClass::universal, false, 6, direct_reference_oid) || + !writer.write_tlv_header(asn1::BerClass::universal, false, 2, 1U) || + !writer.write_byte(3U) || + !writer.write_tlv( + asn1::BerClass::context_specific, + true, + 0, + mms_initiate_response) || + writer.size() != *required) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + return {wire::EncodeStatus::ok, *required, *required}; +} + [[nodiscard]] bool valid_oid( const std::span value, const bool allow_empty = false) noexcept { @@ -375,24 +485,41 @@ std::optional AcseSpanCodec::accept_response_size( wire::EncodeResult AcseSpanCodec::build_accept_response_into( const AssociationRequestView& request, const std::span destination) noexcept { - const auto required = accept_response_size(request); - if (!required) { + return build_accept_response_into(request, kDefaultMmsInitiateResponse, destination); +} + +wire::EncodeResult AcseSpanCodec::build_accept_response_into( + const AssociationRequestView& request, + const std::span mms_initiate_response, + const std::span destination) noexcept { + const auto aare_size = accept_aare_size(mms_initiate_response.size()); + if (!aare_size || !request_is_valid_for_response(request)) { return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; } - if (destination.size() < *required) { - return {wire::EncodeStatus::buffer_too_small, 0U, *required}; + const auto cpa_size = osi::PresentationSpanCodec::cpa_accepting_size( + request.presentation, *aare_size); + if (!cpa_size || *cpa_size > std::numeric_limits::max()) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto user_parameter_bytes = 2U + *cpa_size; + if (user_parameter_bytes > std::numeric_limits::max() || + request.session.parameter_bytes.size() > + std::numeric_limits::max() - user_parameter_bytes) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto required = + request.session.parameter_bytes.size() + user_parameter_bytes + 2U; + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; } - std::array aare{}; - const auto aare_result = encode_default_accept_aare_into(aare); - const auto cpa_size = osi::PresentationSpanCodec::cpa_accepting_size( - request.presentation, aare_result.bytes_written); - if (!aare_result.success() || !cpa_size || - *cpa_size > std::numeric_limits::max()) { - return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + std::array aare{}; + const auto aare_result = encode_accept_aare_into(mms_initiate_response, aare); + if (!aare_result.success() || aare_result.bytes_written != *aare_size) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; } - const auto body_bytes = *required - 2U; + const auto body_bytes = required - 2U; destination[0] = osi::SessionSpanCodec::accept_code; destination[1] = static_cast(body_bytes); std::copy( @@ -409,14 +536,14 @@ wire::EncodeResult AcseSpanCodec::build_accept_response_into( std::span{aare}.first(aare_result.bytes_written), destination.subspan(offset, *cpa_size)); if (!cpa_result.success() || cpa_result.bytes_written != *cpa_size) { - return {cpa_result.status, 0U, *required}; + return {cpa_result.status, 0U, required}; } offset += cpa_result.bytes_written; - if (offset != *required) { - return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + if (offset != required) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; } - return {wire::EncodeStatus::ok, *required, *required}; + return {wire::EncodeStatus::ok, required, required}; } } // namespace ar::iec61850::acse diff --git a/src/mms/connection_runtime.cpp b/src/mms/connection_runtime.cpp index 5f8b9cc7b..cd402f460 100644 --- a/src/mms/connection_runtime.cpp +++ b/src/mms/connection_runtime.cpp @@ -22,6 +22,9 @@ namespace { // Must remain aligned with MmsPduSpanCodec::encode_default_initiate_response_into(). constexpr std::uint32_t kServerMaximumMmsPduSize = 65'000U; +constexpr std::uint32_t kServerMaximumOutstandingCalling = 10U; +constexpr std::uint32_t kServerMaximumOutstandingCalled = 10U; +constexpr std::uint32_t kServerMaximumNestingLevel = 5U; [[nodiscard]] MmsStaticConnectionResult make_result( const MmsStaticConnectionStatus status, @@ -171,8 +174,10 @@ constexpr std::uint32_t kServerMaximumMmsPduSize = 65'000U; const MmsConfirmedPduView& confirmed, const MmsStaticApplicationDispatcher& dispatcher, const std::uint32_t presentation_context_id, - std::size_t& required) noexcept { - required = 0U; + std::size_t& mms_required, + std::size_t& frame_required) noexcept { + mms_required = 0U; + frame_required = 0U; if (confirmed.service() != MmsWireConfirmedService::write) { return false; } @@ -198,9 +203,10 @@ constexpr std::uint32_t kServerMaximumMmsPduSize = 65'000U; probe.required_bytes == 0U) { return false; } + mms_required = probe.required_bytes; const auto fully_encoded = osi::PresentationSpanCodec::fully_encoded_data_size( presentation_context_id, probe.required_bytes); - return fully_encoded && add_overhead(*fully_encoded, 11U, required); + return fully_encoded && add_overhead(*fully_encoded, 11U, frame_required); } } // namespace @@ -371,8 +377,32 @@ MmsStaticConnectionResult MmsStaticConnectionRuntime::process_tcp_window( const auto selected_mms_pdu_size = std::min( initiate.maximum_mms_pdu_size, kServerMaximumMmsPduSize); + const auto selected_outstanding_calling = std::min( + initiate.maximum_outstanding_calling, + kServerMaximumOutstandingCalling); + const auto selected_outstanding_called = std::min( + initiate.maximum_outstanding_called, + kServerMaximumOutstandingCalled); + const auto selected_nesting_level = std::min( + initiate.data_structure_nesting_level, + kServerMaximumNestingLevel); + std::array initiate_response{}; + const auto initiate_encoded = MmsPduSpanCodec::encode_initiate_response_into( + selected_mms_pdu_size, + selected_outstanding_calling, + selected_outstanding_called, + selected_nesting_level, + initiate_response); + if (!initiate_encoded.success()) { + state_ = MmsStaticConnectionState::fault; + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + const auto accept = acse::AcseSpanCodec::build_accept_response_into( - association, workspace); + association, + std::span{initiate_response}.first( + initiate_encoded.bytes_written), + workspace); if (!accept.success()) { if (accept.status == wire::EncodeStatus::buffer_too_small) { return make_workspace_capacity(state_, accept.required_bytes); @@ -426,14 +456,29 @@ MmsStaticConnectionResult MmsStaticConnectionRuntime::process_tcp_window( const bool is_confirmed_request = MmsPduSpanCodec::try_decode_confirmed_request_view( pdv.single_asn1_type, confirmed); if (is_confirmed_request) { - std::size_t write_required{}; + std::size_t write_mms_required{}; + std::size_t write_frame_required{}; if (write_outer_capacity( - confirmed, dispatcher_, mms_presentation_context_id_, write_required)) { - if (response.size() < write_required) { - return make_response_capacity(state_, write_required); + confirmed, + dispatcher_, + mms_presentation_context_id_, + write_mms_required, + write_frame_required)) { + if (negotiated_mms_pdu_size_ == 0U || + write_mms_required > negotiated_mms_pdu_size_ || + write_frame_required < osi::TpktSpanCodec::header_length || + write_frame_required - osi::TpktSpanCodec::header_length > + negotiated_tpdu_size_bytes_) { + return make_result( + MmsStaticConnectionStatus::peer_limit_exceeded, + state_, + peek.frame_bytes); + } + if (response.size() < write_frame_required) { + return make_response_capacity(state_, write_frame_required); } - if (workspace.size() < write_required) { - return make_workspace_capacity(state_, write_required); + if (workspace.size() < write_frame_required) { + return make_workspace_capacity(state_, write_frame_required); } } } @@ -548,4 +593,4 @@ MmsStaticConnectionResult MmsStaticConnectionRuntime::process_tcp_window( return wrapped; } -} // namespace ar::iec61850::mms \ No newline at end of file +} // namespace ar::iec61850::mms diff --git a/src/mms/connection_runtime_iedsim.cpp b/src/mms/connection_runtime_iedsim.cpp index 44b4bc836..6da4e90a8 100644 --- a/src/mms/connection_runtime_iedsim.cpp +++ b/src/mms/connection_runtime_iedsim.cpp @@ -22,6 +22,9 @@ namespace { // Must remain aligned with MmsPduSpanCodec::encode_default_initiate_response_into(). constexpr std::uint32_t kServerMaximumMmsPduSize = 65'000U; +constexpr std::uint32_t kServerMaximumOutstandingCalling = 10U; +constexpr std::uint32_t kServerMaximumOutstandingCalled = 10U; +constexpr std::uint32_t kServerMaximumNestingLevel = 5U; constexpr std::array kConservativeStructureType{ 0xA2U, 0x02U, 0xA1U, 0x00U}; @@ -287,8 +290,10 @@ constexpr std::array kEmptyFileDirectoryFields{ const MmsStaticApplicationDispatcher& dispatcher, const std::uint32_t presentation_context_id, const std::size_t negotiated_tpdu_size_bytes, - std::size_t& required) noexcept { - required = 0U; + std::size_t& mms_required, + std::size_t& frame_required) noexcept { + mms_required = 0U; + frame_required = 0U; if (confirmed.service() != MmsWireConfirmedService::write) { return false; } @@ -314,6 +319,7 @@ constexpr std::array kEmptyFileDirectoryFields{ probe.required_bytes == 0U) { return false; } + mms_required = probe.required_bytes; const auto fully_encoded = osi::PresentationSpanCodec::fully_encoded_data_size( presentation_context_id, probe.required_bytes); if (!fully_encoded) { @@ -326,7 +332,7 @@ constexpr std::array kEmptyFileDirectoryFields{ negotiated_tpdu_size_bytes, maximum_user_data, segment_count, - required); + frame_required); } [[nodiscard]] bool compatibility_error_status( @@ -615,8 +621,33 @@ MmsStaticConnectionResult MmsStaticConnectionRuntime::process_tcp_window( const auto selected_mms_pdu_size = std::min( initiate.maximum_mms_pdu_size, kServerMaximumMmsPduSize); + const auto selected_outstanding_calling = std::min( + initiate.maximum_outstanding_calling, + kServerMaximumOutstandingCalling); + const auto selected_outstanding_called = std::min( + initiate.maximum_outstanding_called, + kServerMaximumOutstandingCalled); + const auto selected_nesting_level = std::min( + initiate.data_structure_nesting_level, + kServerMaximumNestingLevel); + std::array initiate_response{}; + const auto initiate_encoded = MmsPduSpanCodec::encode_initiate_response_into( + selected_mms_pdu_size, + selected_outstanding_calling, + selected_outstanding_called, + selected_nesting_level, + initiate_response); + if (!initiate_encoded.success()) { + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + const auto accept = acse::AcseSpanCodec::build_accept_response_into( - association, workspace); + association, + std::span{initiate_response}.first( + initiate_encoded.bytes_written), + workspace); if (!accept.success()) { if (accept.status == wire::EncodeStatus::buffer_too_small) { return make_workspace_capacity(state_, accept.required_bytes); @@ -739,18 +770,27 @@ MmsStaticConnectionResult MmsStaticConnectionRuntime::process_tcp_window( confirmed.invoke_id)); } - std::size_t write_required{}; + std::size_t write_mms_required{}; + std::size_t write_frame_required{}; if (write_outer_capacity( confirmed, dispatcher_, mms_presentation_context_id_, negotiated_tpdu_size_bytes_, - write_required)) { - if (response.size() < write_required) { - return make_response_capacity(state_, write_required); + write_mms_required, + write_frame_required)) { + if (negotiated_mms_pdu_size_ == 0U || + write_mms_required > negotiated_mms_pdu_size_) { + return finish(make_result( + MmsStaticConnectionStatus::peer_limit_exceeded, + state_, + peek.frame_bytes)); + } + if (response.size() < write_frame_required) { + return make_response_capacity(state_, write_frame_required); } - if (workspace.size() < write_required) { - return make_workspace_capacity(state_, write_required); + if (workspace.size() < write_frame_required) { + return make_workspace_capacity(state_, write_frame_required); } } } diff --git a/src/mms/pdu_span.cpp b/src/mms/pdu_span.cpp index d03b83bae..e58584ba8 100644 --- a/src/mms/pdu_span.cpp +++ b/src/mms/pdu_span.cpp @@ -32,6 +32,12 @@ constexpr std::array kDefaultInitiateResponse{ 0x82U, 0x0CU, 0x03U, 0x4EU, 0x08U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U}; +constexpr std::array kInitiateResponseDetail{ + 0x80U, 0x01U, 0x01U, + 0x81U, 0x03U, 0x05U, 0xF1U, 0x00U, + 0x82U, 0x0CU, 0x03U, 0x4EU, 0x08U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U}; + [[nodiscard]] bool read_u32( const asn1::BerTlvView& tlv, std::uint32_t& value) noexcept { @@ -420,6 +426,77 @@ wire::EncodeResult MmsPduSpanCodec::encode_default_initiate_response_into( kDefaultInitiateResponse.size()}; } +wire::EncodeResult MmsPduSpanCodec::encode_initiate_response_into( + const std::uint32_t maximum_mms_pdu_size, + const std::uint32_t maximum_outstanding_calling, + const std::uint32_t maximum_outstanding_called, + const std::uint32_t data_structure_nesting_level, + const std::span destination) noexcept { + if (maximum_mms_pdu_size < 64U || + maximum_mms_pdu_size > maximum_pdu_bytes || + maximum_outstanding_calling == 0U || + maximum_outstanding_called == 0U || + data_structure_nesting_level == 0U) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + const std::array values{ + maximum_mms_pdu_size, + maximum_outstanding_calling, + maximum_outstanding_called, + data_structure_nesting_level}; + std::array value_sizes{}; + std::size_t content_size = 0U; + for (std::size_t index = 0U; index < values.size(); ++index) { + value_sizes[index] = positive_integer_size(values[index]); + const auto field = asn1::BerSpanWriter::tlv_size( + static_cast(index), value_sizes[index]); + if (!field || *field > std::numeric_limits::max() - content_size) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + content_size += *field; + } + const auto detail = asn1::BerSpanWriter::tlv_size(4, kInitiateResponseDetail.size()); + if (!detail || *detail > std::numeric_limits::max() - content_size) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + content_size += *detail; + const auto required = asn1::BerSpanWriter::tlv_size(9, content_size); + if (!required || destination.size() < *required) { + return { + required ? wire::EncodeStatus::buffer_too_small + : wire::EncodeStatus::value_out_of_range, + 0U, + required.value_or(0U)}; + } + + asn1::BerSpanWriter writer{destination.first(*required)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, true, 9, content_size)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + for (std::size_t index = 0U; index < values.size(); ++index) { + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, + false, + static_cast(index), + value_sizes[index]) || + !write_positive_integer(writer, values[index])) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + } + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, + true, + 4, + kInitiateResponseDetail.size()) || + !writer.write_bytes(kInitiateResponseDetail) || + writer.size() != *required) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + return {wire::EncodeStatus::ok, *required, *required}; +} + bool MmsPduSpanCodec::try_decode_confirmed_request_view( const std::span bytes, MmsConfirmedPduView& request) noexcept { diff --git a/src/mms/static_dispatcher.cpp b/src/mms/static_dispatcher.cpp index 919fd964e..63d5a3c1f 100644 --- a/src/mms/static_dispatcher.cpp +++ b/src/mms/static_dispatcher.cpp @@ -297,6 +297,12 @@ namespace { if (!MmsServiceSpanCodec::try_decode_read_request(confirmed, request)) { return make_status(MmsStaticDispatchStatus::malformed_request, confirmed); } + if (request.specification_with_result) { + // This bounded profile does not encode variableAccessSpecification in + // Read responses. Reject the optional request form explicitly before + // invoking any object callbacks instead of returning an incomplete PDU. + return make_status(MmsStaticDispatchStatus::unsupported_request, confirmed); + } std::array results{}; std::size_t workspace_offset = 0U;