// Copyright (C) 2013-2023 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. /// @file dhcp4_test_utils.h /// /// @brief This file contains utility classes used for DHCPv4 server testing #ifndef DHCP4_TEST_UTILS_H #define DHCP4_TEST_UTILS_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace isc { namespace dhcp { namespace test { /// @brief Dummy Packet Filtering class. /// /// This class reports capability to respond directly to the client which /// doesn't have address configured yet. /// /// All packet and socket handling functions do nothing because they are not /// used in unit tests. class PktFilterTest : public PktFilter { public: /// @brief Constructor. /// /// Sets the 'direct response' capability to true. PktFilterTest() : direct_resp_supported_(true) { } /// @brief Reports 'direct response' capability. /// /// @return always true. virtual bool isDirectResponseSupported() const { return (direct_resp_supported_); } /// Does nothing. virtual SocketInfo openSocket(Iface&, const isc::asiolink::IOAddress& addr, const uint16_t port, const bool, const bool) { return (SocketInfo(addr, port, 0)); } /// Does nothing. virtual Pkt4Ptr receive(Iface&, const SocketInfo&) { return Pkt4Ptr(); } /// Does nothing. virtual int send(const Iface&, uint16_t, const Pkt4Ptr&) { return (0); } /// @brief Holds a boolean value which indicates whether direct response /// capability is supported (true) or not (false). bool direct_resp_supported_; }; typedef boost::shared_ptr PktFilterTestPtr; /// Forward definition for Dhcp4Client defined in dhcp4_client.h /// dhcp4_client.h includes dhcp_test_utils.h (this file), so to avoid /// circular dependencies, we need a forward class declaration. class Dhcp4Client; /// @brief "Naked" DHCPv4 server, exposes internal fields class NakedDhcpv4Srv: public Dhcpv4Srv { public: /// @brief Constructor. /// /// This constructor disables default modes of operation used by the /// Dhcpv4Srv class: /// - Send/receive broadcast messages through sockets on interfaces /// which support broadcast traffic. /// - Direct DHCPv4 traffic - communication with clients which do not /// have IP address assigned yet. /// /// Enabling these modes requires root privileges so they must be /// disabled for unit testing. /// /// Note, that disabling broadcast options on sockets does not impact /// the operation of these tests because they use local loopback /// interface which doesn't have broadcast capability anyway. It rather /// prevents setting broadcast options on other (broadcast capable) /// sockets which are opened on other interfaces in Dhcpv4Srv constructor. /// /// The Direct DHCPv4 Traffic capability can be disabled here because /// it is tested with PktFilterLPFTest unittest. The tests which belong /// to PktFilterLPFTest can be enabled on demand when root privileges can /// be guaranteed. /// /// @param port port number to listen on; the default value 0 indicates /// that sockets should not be opened. NakedDhcpv4Srv(uint16_t port = 0) : Dhcpv4Srv(port, false, false) { // Create a default lease database backend. std::string dbconfig = "type=memfile universe=4 persist=false"; isc::dhcp::LeaseMgrFactory::create(dbconfig); // Create fixed server id. server_id_.reset(new Option4AddrLst(DHO_DHCP_SERVER_IDENTIFIER, asiolink::IOAddress("192.0.3.1"))); LeaseMgr::setIOService(getIOService()); } /// @brief Returns fixed server identifier assigned to the naked server /// instance. OptionPtr getServerID() const { return (server_id_); } /// @brief fakes packet reception /// @param timeout ignored /// /// The method receives all packets queued in receive queue, one after /// another. Once the queue is empty, it initiates the shutdown procedure. /// /// See fake_received_ field for description virtual Pkt4Ptr receivePacket(int /*timeout*/) { // If there is anything prepared as fake incoming traffic, use it if (!fake_received_.empty()) { Pkt4Ptr pkt = fake_received_.front(); fake_received_.pop_front(); return (pkt); } // Make sure the server processed all packets in MT. isc::util::MultiThreadingMgr::instance().getThreadPool().wait(3); // If not, just trigger shutdown and return immediately shutdown(); return (Pkt4Ptr()); } /// @brief fake packet sending /// /// Pretend to send a packet, but instead just store it in fake_send_ list /// where test can later inspect server's response. virtual void sendPacket(const Pkt4Ptr& pkt) { isc::util::MultiThreadingLock lock(mutex_); fake_sent_.push_back(pkt); } /// @brief fake receive packet from server /// /// The client uses this packet as a reply from the server. /// /// @return The received packet. Pkt4Ptr receiveOneMsg() { // Make sure the server processed all packets. isc::util::MultiThreadingMgr::instance().getThreadPool().wait(2); // Lock the mutex for the rest of the function. isc::util::MultiThreadingLock lock(mutex_); // Return empty pointer if server hasn't responded. if (fake_sent_.empty()) { return (Pkt4Ptr()); } Pkt4Ptr msg = fake_sent_.front(); fake_sent_.pop_front(); return (msg); } /// @brief adds a packet to fake receive queue /// /// See fake_received_ field for description void fakeReceive(const Pkt4Ptr& pkt) { fake_received_.push_back(pkt); } virtual ~NakedDhcpv4Srv() { } /// @brief Runs processing DHCPDISCOVER. /// /// @param discover a message received from client /// @return DHCPOFFER message or null Pkt4Ptr processDiscover(Pkt4Ptr& discover) { AllocEngine::ClientContext4Ptr context(new AllocEngine::ClientContext4()); earlyGHRLookup(discover, context); return (processDiscover(discover, context)); } /// @brief Runs processing DHCPREQUEST. /// /// @param request a message received from client /// @return DHCPACK or DHCPNAK message Pkt4Ptr processRequest(Pkt4Ptr& request) { AllocEngine::ClientContext4Ptr context(new AllocEngine::ClientContext4()); earlyGHRLookup(request, context); return (processRequest(request, context)); } /// @brief Runs processing DHCPRELEASE. /// /// @param release message received from client void processRelease(Pkt4Ptr& release) { AllocEngine::ClientContext4Ptr context(new AllocEngine::ClientContext4()); earlyGHRLookup(release, context); processRelease(release, context); } /// @brief Runs processing DHCPDECLINE. /// /// @param decline message received from client void processDecline(Pkt4Ptr& decline) { AllocEngine::ClientContext4Ptr context(new AllocEngine::ClientContext4()); earlyGHRLookup(decline, context); processDecline(decline, context); } /// @brief Runs processing DHCPINFORM. /// /// @param inform a message received from client /// @return DHCPACK message Pkt4Ptr processInform(Pkt4Ptr& inform) { AllocEngine::ClientContext4Ptr context(new AllocEngine::ClientContext4()); earlyGHRLookup(inform, context); return (processInform(inform, context)); } /// @brief Dummy server identifier option used by various tests. OptionPtr server_id_; /// @brief packets we pretend to receive. /// /// Instead of setting up sockets on interfaces that change between OSes, it /// is much easier to fake packet reception. This is a list of packets that /// we pretend to have received. You can schedule new packets to be received /// using fakeReceive() and NakedDhcpv4Srv::receivePacket() methods. std::list fake_received_; /// @brief packets we pretend to send. std::list fake_sent_; using Dhcpv4Srv::adjustIfaceData; using Dhcpv4Srv::appendServerID; using Dhcpv4Srv::processDiscover; using Dhcpv4Srv::processRequest; using Dhcpv4Srv::processRelease; using Dhcpv4Srv::processDecline; using Dhcpv4Srv::processInform; using Dhcpv4Srv::processClientName; using Dhcpv4Srv::createNameChangeRequests; using Dhcpv4Srv::acceptServerId; using Dhcpv4Srv::sanityCheck; using Dhcpv4Srv::srvidToString; using Dhcpv4Srv::classifyPacket; using Dhcpv4Srv::deferredUnpack; using Dhcpv4Srv::accept; using Dhcpv4Srv::acceptMessageType; using Dhcpv4Srv::selectSubnet; using Dhcpv4Srv::setSendResponsesToSource; using Dhcpv4Srv::VENDOR_CLASS_PREFIX; using Dhcpv4Srv::shutdown_; using Dhcpv4Srv::alloc_engine_; using Dhcpv4Srv::server_port_; using Dhcpv4Srv::client_port_; /// @brief Mutex to protect the packet buffers. std::mutex mutex_; }; // We need to pass one reference to the Dhcp4Client, which is defined in // dhcp4_client.h. That header includes this file. To avoid circular // dependencies, we use forward declaration here. class Dhcp4Client; /// @brief Base class for DHCPv4 server testing. /// /// Currently it configures the test data path directory in /// the @c CfgMgr. When the object is destroyed, the original /// path is reverted. class BaseServerTest : public ::testing::Test { public: /// @brief Constructor. BaseServerTest(); /// @brief Destructor. virtual ~BaseServerTest(); private: /// @brief Holds the original data directory. std::string original_datadir_; }; class Dhcpv4SrvTest : public BaseServerTest { public: /// @brief Specifies expected outcome enum ExpectedResult { SHOULD_PASS, // pass = accept decline, move lease to declined state. SHOULD_FAIL // fail = reject the decline }; /// @brief Specifies if lease affinity is enabled or disabled enum LeaseAffinity { LEASE_AFFINITY_ENABLED, LEASE_AFFINITY_DISABLED }; class Dhcpv4SrvMTTestGuard { public: Dhcpv4SrvMTTestGuard(Dhcpv4SrvTest& test, bool mt_enabled) : test_(test) { test_.setMultiThreading(mt_enabled); } ~Dhcpv4SrvMTTestGuard() { test_.setMultiThreading(false); } Dhcpv4SrvTest& test_; }; /// @brief Constructor /// /// Initializes common objects used in many tests. /// Also sets up initial configuration in CfgMgr. Dhcpv4SrvTest(); /// @brief Destructor /// /// Removes existing configuration. virtual ~Dhcpv4SrvTest(); /// @brief Add 'Parameter Request List' option to the packet. /// /// This function adds PRL option comprising the following option codes: /// - 5 - Name Server /// - 15 - Domain Name /// - 7 - Log Server /// - 8 - Quotes Server /// - 9 - LPR Server /// /// @param pkt packet to add PRL option to. void addPrlOption(Pkt4Ptr& pkt); /// @brief Used to configure a server for tests. /// /// A wrapper over configureDhcp4Server() to which any other /// simulations of production code are added. /// /// @brief server the server being tested /// @brief config the configuration the server is configured with /// /// @return a JSON-formatted status of the reconfiguration static ConstElementPtr configure(Dhcpv4Srv& server, isc::data::ConstElementPtr config); /// @brief Configures options being requested in the PRL option. /// /// The lpr-servers option is NOT configured here although it is /// added to the 'Parameter Request List' option in the /// \ref addPrlOption. When requested option is not configured /// the server should not return it in its response. The goal /// of not configuring the requested option is to verify that /// the server will not return it. void configureRequestedOptions(); /// @brief Configures server identifier at different levels. void configureServerIdentifier(); /// @brief checks that the response matches request /// /// @param q query (client's message) /// @param a answer (server's message) void messageCheck(const Pkt4Ptr& q, const Pkt4Ptr& a); /// @brief Check that certain basic (always added when lease is acquired) /// are present in a message. /// /// @param pkt A message to be checked. /// @return Assertion result which indicates whether test passed or failed. ::testing::AssertionResult basicOptionsPresent(const Pkt4Ptr& pkt); /// @brief Check that certain basic (always added when lease is acquired) /// are not present. /// /// @param pkt A packet to be checked. /// @return Assertion result which indicates whether test passed or failed. ::testing::AssertionResult noBasicOptions(const Pkt4Ptr& pkt); /// @brief Check that certain requested options are present in the message. /// /// @param pkt A message to be checked. /// @return Assertion result which indicates whether test passed or failed. ::testing::AssertionResult requestedOptionsPresent(const Pkt4Ptr& pkt); /// @brief Check that certain options (requested with PRL option) /// are not present. /// /// @param pkt A packet to be checked. /// @return Assertion result which indicates whether test passed or failed. ::testing::AssertionResult noRequestedOptions(const Pkt4Ptr& pkt); /// @brief generates client-id option /// /// Generate client-id option of specified length /// Ids with different lengths are sufficient to generate /// unique ids. If more fine grained control is required, /// tests generate client-ids on their own. /// Sets client_id_ field. /// @param size size of the client-id to be generated OptionPtr generateClientId(size_t size = 4); /// @brief generate hardware address /// /// @param size size of the generated MAC address /// @param pointer to Hardware Address object HWAddrPtr generateHWAddr(size_t size = 6); /// @brief Convenience method for making a server identifier option instance. /// /// @param address IP address to add to the option /// /// @return Pointer to the newly constructed option. OptionCustomPtr makeServerIdOption(const isc::asiolink::IOAddress& address); /// @brief Convenience method for making a fqdn list option instance. /// /// @return Pointer to the newly constructed option. OptionPtr makeFqdnListOption(); /// Check that address was returned from proper range, that its lease /// lifetime is correct, that T1 and T2 are returned properly /// @param rsp response to be checked /// @param subnet subnet that should be used to verify assigned address /// and options /// @param t1_present check that t1 must be present (true) or must not be /// present (false) /// @param t2_present check that t2 must be present (true) or must not be /// present (false) /// @param expected_valid check that lease lifetime has the not-zero /// expected value (zero value means that do not check). void checkAddressParams(const Pkt4Ptr& rsp, const Subnet4Ptr subnet, bool t1_present = false, bool t2_present = false, uint32_t expected_valid = 0); /// @brief Basic checks for generated response (message type and trans-id). /// /// @param rsp response packet to be validated /// @param expected_message_type expected message type /// @param expected_transid expected transaction-id void checkResponse(const Pkt4Ptr& rsp, int expected_message_type, uint32_t expected_transid); /// @brief Checks if the lease sent to client is present in the database /// /// @param rsp response packet to be validated /// @param client_id expected client-identifier (or NULL) /// @param HWAddr expected hardware address (not used now) /// @param expected_addr expected address Lease4Ptr checkLease(const Pkt4Ptr& rsp, const OptionPtr& client_id, const HWAddrPtr&, const isc::asiolink::IOAddress& expected_addr); /// @brief Checks if server response (OFFER, ACK, NAK) includes proper server-id /// /// @param rsp response packet to be validated /// @param expected_srvid expected value of server-id void checkServerId(const Pkt4Ptr& rsp, const OptionPtr& expected_srvid); /// @brief Checks if server response (OFFER, ACK, NAK) includes proper client-id /// /// This method follows values reported by CfgMgr in echoClientId() method. /// Depending on its configuration, the client-id is either mandatory or /// forbidden to appear in the response. /// /// @param rsp response packet to be validated /// @param expected_clientid expected value of client-id void checkClientId(const Pkt4Ptr& rsp, const OptionPtr& expected_clientid); /// @brief Checks the value of the dhcp-server-identifier option in a packet /// /// @param packet packet to test /// @param expected_address IP address the packet's option should contain void checkServerIdOption(const Pkt4Ptr& packet, const isc::asiolink::IOAddress& expected_address); /// @brief Create packet from output buffer of another packet. /// /// This function creates a packet using an output buffer from another /// packet. This imitates reception of a packet from the wire. The /// unpack function is then called to parse packet contents and to /// create a collection of the options carried by this packet. /// /// This function is useful for unit tests which verify that the received /// packet is parsed correctly. In those cases it is usually inappropriate /// to create an instance of the packet, add options, set packet /// fields and use such packet as an input to processDiscover or /// processRequest function. This is because, such a packet has certain /// options already initialized and there is no way to verify that they /// have been initialized when packet instance was created or wire buffer /// processing. By creating a packet from the buffer we guarantee that the /// new packet is entirely initialized during wire data parsing. /// /// @param src_pkt A source packet, to be copied. /// @param [out] dst_pkt A destination packet. /// /// @return assertion result indicating if a function completed with /// success or failure. static ::testing::AssertionResult createPacketFromBuffer(const isc::dhcp::Pkt4Ptr& src_pkt, isc::dhcp::Pkt4Ptr& dst_pkt); /// @brief Tests if Discover or Request message is processed correctly /// /// This test verifies that the Parameter Request List option is handled /// correctly, i.e. it checks that certain options are present in the /// server's response when they are requested and that they are not present /// when they are not requested or NAK occurs. /// /// @todo We need an additional test for PRL option using real traffic /// capture. /// /// @param msg_type DHCPDISCOVER or DHCPREQUEST void testDiscoverRequest(const uint8_t msg_type); /// @brief Create test which verifies server identifier. /// /// @param expected_server_id expected server identifier /// @param query the query used to get associated client classes /// @param requested the requested address /// @param server_id server identifier void buildCfgOptionTest(isc::asiolink::IOAddress expected_server_id, Pkt4Ptr& query, isc::asiolink::IOAddress requested, isc::asiolink::IOAddress server_id); /// @brief Runs DHCPv4 configuration from the JSON string. /// /// @param config String holding server configuration in JSON format. /// @param commit A boolean flag indicating if the new configuration /// should be committed (if true), or not (if false). /// @param open_sockets A boolean flag indicating if sockets should /// be opened (if true), or not (if false). /// @param create_managers A boolean flag indicating if managers should be /// recreated. /// @param test A boolean flag which indicates if only testing config. /// @param lease_affinity A flag which indicates if lease affinity should /// be enabled or disabled. void configure(const std::string& config, const bool commit = true, const bool open_sockets = true, const bool create_managers = true, const bool test = false, const LeaseAffinity lease_affinity = LEASE_AFFINITY_DISABLED); /// @brief Configure the DHCPv4 server using the JSON string. /// /// @param config String holding server configuration in JSON format. /// @param srv Instance of the server to be configured. /// @param commit A boolean flag indicating if the new configuration /// should be committed (if true), or not (if false). /// @param open_sockets A boolean flag indicating if sockets should /// be opened (if true), or not (if false). /// @param create_managers A boolean flag indicating if managers should be /// recreated. /// @param test A boolean flag which indicates if only testing config. /// @param lease_affinity A flag which indicates if lease affinity should /// be enabled or disabled. void configure(const std::string& config, NakedDhcpv4Srv& srv, const bool commit = true, const bool open_sockets = true, const bool create_managers = true, const bool test = false, const LeaseAffinity lease_affinity = LEASE_AFFINITY_DISABLED); /// @brief Configure specified DHCP server using JSON string. /// /// @param config String holding server configuration in JSON format. /// @param srv Instance of the server to be configured. /// @param commit A boolean flag indicating if the new configuration /// should be committed (if true), or not (if false). /// @param exp_rcode expected status code (default = 0 (success)) /// @return (a pair of status code and a string with result) std::pair configureWithStatus(const std::string& config, NakedDhcpv4Srv& srv, const bool commit = true, const int exp_rcode = 0); /// @brief Pretends a packet of specified type was received. /// /// Instantiates fake network interfaces, configures passed Dhcpv4Srv, /// then creates a message of specified type and sends it to the /// server and then checks whether expected statistics were set /// appropriately. /// /// @param srv the DHCPv4 server to be used /// @param config JSON configuration to be used /// @param pkt_type type of the packet to be faked /// @param stat_name name of the expected statistic void pretendReceivingPkt(NakedDhcpv4Srv& srv, const std::string& config, uint8_t pkt_type, const std::string& stat_name); /// @brief Create @c Dhcpv4Exchange from client's query. Dhcpv4Exchange createExchange(const Pkt4Ptr& query); /// @brief Performs 4-way exchange to obtain new lease. /// /// This is used as a preparatory step for Decline operation. /// /// @param client Client to be used to obtain a lease. void acquireLease(Dhcp4Client& client); /// @brief Tests if the acquired lease is or is not declined. /// /// @param client Dhcp4Client instance /// @param hw_address_1 HW Address to be used to acquire the lease. /// @param client_id_1 Client id to be used to acquire the lease. /// @param hw_address_2 HW Address to be used to decline the lease. /// @param client_id_2 Client id to be used to decline the lease. /// @param expected_result SHOULD_PASS if the lease is expected to /// be successfully declined, or SHOULD_FAIL if the lease is expected /// to not be declined. /// @param config_index specifies which config index should be used: /// 0 - memfile, 1 - mysql, 2 - pgsql void acquireAndDecline(Dhcp4Client& client, const std::string& hw_address_1, const std::string& client_id_1, const std::string& hw_address_2, const std::string& client_id_2, ExpectedResult expected_result, uint8_t config_index = 0); /// @brief Checks if received relay agent info option is echoed back to the /// client. void relayAgentInfoEcho(); /// @brief Checks if received bad relay agent info option is not echoed back /// to the client. void badRelayAgentInfoEcho(); /// @brief Checks if client port can be overridden in packets being sent. void portsClientPort(); /// @brief Checks if server port can be overridden in packets being sent. void portsServerPort(); /// @brief Check if example files contain valid configuration. void checkConfigFiles(); /// @brief Check if the server configuration stored in file is valid. /// /// @param path The path to the configuration file. void loadConfigFile(const std::string& path); /// @brief This function cleans up after the test. virtual void TearDown(); /// @brief Set multi-threading mode. void setMultiThreading(bool enabled) { multi_threading_ = enabled; } /// @brief A subnet used in most tests. Subnet4Ptr subnet_; /// @brief A pool used in most tests. Pool4Ptr pool_; /// @brief A client-id used in most tests. ClientIdPtr client_id_; /// @brief Return code int rcode_; /// @brief Comment received from configuration. isc::data::ConstElementPtr comment_; /// @brief Server object under test. NakedDhcpv4Srv srv_; /// @brief The multi-threading flag. bool multi_threading_; }; /// @brief Patch the server config to add interface-config/re-detect=false /// @param json the server config inline void disableIfacesReDetect(isc::data::ConstElementPtr json) { isc::data::ConstElementPtr ifaces_cfg = json->get("interfaces-config"); if (ifaces_cfg) { isc::data::ElementPtr mutable_cfg = boost::const_pointer_cast(ifaces_cfg); mutable_cfg->set("re-detect", isc::data::Element::create(false)); } } /// @brief Patch the server config to add multi-threading/enable-multi-threading /// @param json the server config inline void configureMultiThreading(bool enabled, isc::data::ConstElementPtr json) { isc::data::ConstElementPtr multi_threading = json->get("multi-threading"); if (!multi_threading) { isc::data::ElementPtr mutable_cfg = boost::const_pointer_cast(json); multi_threading = isc::data::Element::createMap(); mutable_cfg->set("multi-threading", multi_threading); } isc::data::ElementPtr mutable_cfg = boost::const_pointer_cast(multi_threading); if (enabled) { mutable_cfg->set("enable-multi-threading", isc::data::Element::create(true)); mutable_cfg->set("thread-pool-size", isc::data::Element::create(4)); mutable_cfg->set("packet-queue-size", isc::data::Element::create(4)); } else { mutable_cfg->set("enable-multi-threading", isc::data::Element::create(false)); } } /// @brief Runs parser in JSON mode, useful for parser testing /// /// @param in string to be parsed /// @return ElementPtr structure representing parsed JSON inline isc::data::ElementPtr parseJSON(const std::string& in) { isc::dhcp::Parser4Context ctx; return (ctx.parseString(in, isc::dhcp::Parser4Context::PARSER_JSON)); } /// @brief Runs parser in Dhcp4 mode /// /// This is a simplified Dhcp4 mode, so no outer { } and "Dhcp4" is /// needed. This format is used by most of the tests. /// /// @param in string to be parsed /// @param verbose display the exception message when it fails /// @return ElementPtr structure representing parsed JSON inline isc::data::ElementPtr parseDHCP4(const std::string& in, bool verbose = false) { try { isc::dhcp::Parser4Context ctx; isc::data::ElementPtr json; json = ctx.parseString(in, isc::dhcp::Parser4Context::SUBPARSER_DHCP4); disableIfacesReDetect(json); return (json); } catch (const std::exception& ex) { if (verbose) { std::cout << "EXCEPTION: " << ex.what() << std::endl; } throw; } } /// @brief Runs parser in option definition mode /// /// This function parses specified text as JSON that defines option definitions. /// /// @param in string to be parsed /// @param verbose display the exception message when it fails /// @return ElementPtr structure representing parsed JSON inline isc::data::ElementPtr parseOPTION_DEFS(const std::string& in, bool verbose = false) { try { isc::dhcp::Parser4Context ctx; return (ctx.parseString(in, isc::dhcp::Parser4Context::PARSER_OPTION_DEFS)); } catch (const std::exception& ex) { if (verbose) { std::cout << "EXCEPTION: " << ex.what() << std::endl; } throw; } } } // end of isc::dhcp::test namespace } // end of isc::dhcp namespace } // end of isc namespace #endif // DHCP4_TEST_UTILS_H