From ea4c1ba4c36e269ad3ac16c56288b9746b7bc29b Mon Sep 17 00:00:00 2001 From: liuwentan Date: Sat, 3 Dec 2022 22:23:41 +0800 Subject: [PATCH] add json/redis rule parser --- CMakeLists.txt | 1 + include/maat/maat.h | 4 + include/utils.h | 9 + scanner/IPMatcher.h | 117 -- scanner/adapter_hs.cpp | 11 +- scanner/adapter_hs.h | 8 +- scanner/adapter_hs_gtest.cpp | 20 +- src/CMakeLists.txt | 7 +- src/inc_internal/json2iris.h | 29 + src/inc_internal/maat_command.h | 39 + src/inc_internal/maat_common.h | 9 +- src/inc_internal/maat_config_monitor.h | 2 + src/inc_internal/maat_redis_monitor.h | 33 + src/inc_internal/maat_rule.h | 110 +- src/inc_internal/maat_table_schema.h | 45 +- src/inc_internal/maat_utils.h | 15 + src/json2iris.cpp | 1207 ++++++++++++ src/maat_api.cpp | 92 +- src/maat_command.cpp | 348 ++++ src/maat_config_monitor.cpp | 84 +- src/maat_redis_monitor.cpp | 1403 ++++++++++++++ src/maat_rule.cpp | 66 +- src/maat_table_runtime.cpp | 2 +- src/maat_table_schema.cpp | 215 ++- src/maat_utils.cpp | 195 +- test/CMakeLists.txt | 3 +- test/maat_framework_gtest.cpp | 83 +- test/maat_json.json | 2339 ++++++++++++++++++++++++ tools/CMakeLists.txt | 8 + tools/maat_redis_tool.cpp | 394 ++++ vendor/CMakeLists.txt | 13 + vendor/hiredis-1.1.0.tar.gz | Bin 0 -> 122002 bytes 32 files changed, 6734 insertions(+), 177 deletions(-) delete mode 100644 scanner/IPMatcher.h create mode 100644 src/inc_internal/json2iris.h create mode 100644 src/inc_internal/maat_command.h create mode 100644 src/inc_internal/maat_redis_monitor.h create mode 100644 src/json2iris.cpp create mode 100644 src/maat_command.cpp create mode 100644 src/maat_redis_monitor.cpp create mode 100644 test/maat_json.json create mode 100644 tools/CMakeLists.txt create mode 100644 tools/maat_redis_tool.cpp create mode 100644 vendor/hiredis-1.1.0.tar.gz diff --git a/CMakeLists.txt b/CMakeLists.txt index 3966239..a449375 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,4 +14,5 @@ enable_testing() add_subdirectory(vendor) add_subdirectory(src) add_subdirectory(test) +add_subdirectory(tools) add_subdirectory(scanner) \ No newline at end of file diff --git a/include/maat/maat.h b/include/maat/maat.h index 6ad2167..c8682e3 100644 --- a/include/maat/maat.h +++ b/include/maat/maat.h @@ -66,6 +66,10 @@ int maat_options_set_instance_name(struct maat_options *opts, const char *instan int maat_options_set_deferred_load_on(struct maat_options *opts); int maat_options_set_iris_full_dir(struct maat_options *opts, const char *full_dir); int maat_options_set_iris_inc_dir(struct maat_options *opts, const char *inc_dir); +int maat_options_set_json_file(struct maat_options *opts, const char *json_filename); +int maat_options_set_redis_ip(struct maat_options *opts, const char *redis_ip); +int maat_options_set_redis_port(struct maat_options *opts, uint16_t redis_port); +int maat_options_set_redis_db_index(struct maat_options *opts, int db_index); /* maat_instance API */ struct maat *maat_new(struct maat_options *opts, const char *table_info_path); diff --git a/include/utils.h b/include/utils.h index 295ce5d..9981a8f 100644 --- a/include/utils.h +++ b/include/utils.h @@ -20,6 +20,15 @@ extern "C" #define ALLOC(type, number) ((type *)calloc(sizeof(type), number)) +#define FREE(ptr) \ + { \ + if (ptr) \ + { \ + free(ptr); \ + ptr = NULL; \ + } \ + } + #ifdef __cpluscplus } #endif diff --git a/scanner/IPMatcher.h b/scanner/IPMatcher.h deleted file mode 100644 index 0e387f3..0000000 --- a/scanner/IPMatcher.h +++ /dev/null @@ -1,117 +0,0 @@ -/* - * - * Copyright (c) 2020 - * String Algorithms Research Group - * Institute of Information Engineering, Chinese Academy of Sciences (IIE-CAS) - * National Engineering Laboratory for Information Security Technologies (NELIST) - * All rights reserved - * - * Written by: LU YUHAI (luyuhai@iie.ac.cn) - * Last modification: 2020-04-20 - * - * This code is the exclusive and proprietary property of IIE-CAS and NELIST. - * Usage for direct or indirect commercial advantage is not allowed without - * written permission from the authors. - * - */ - -#ifndef H_IP_MATCHER_H -#define H_IP_MATCHER_H -#include - -#ifdef __cplusplus -extern "C" -{ -#endif - - enum IP_TYPE - { - IPv4, - IPv6 - }; - - /* 带掩码的单点IPv4规则 */ - struct ipv4_range - { - unsigned int start_ip; /* IP范围下界 */ - unsigned int end_ip; /* IP范围上界 */ - }; - - /* 带掩码的单点IPv6规则 */ - struct ipv6_range - { - unsigned int start_ip[4]; /* IP范围下界 */ - unsigned int end_ip[4]; /* IP范围上界 */ - }; - - /* 通用的ip规则类型 */ - struct ip_rule - { - enum IP_TYPE type; /* 规则类型,ipv4或ipv6 */ - unsigned int rule_id; /* 规则ID */ - void* user_tag; /* 用户自定义数据,命中时随匹配结果返回 */ - union - { - struct ipv4_range ipv4_rule; /*带掩码的单点IPv4规则*/ - struct ipv6_range ipv6_rule; /*带掩码的单点IPv6规则*/ - }; - }; - - /* 通用的待扫描数据类型 */ - struct ip_data - { - enum IP_TYPE type; /* 规则类型,ipv4或ipv6 */ - union /* 根据rule_type决定数据负载是ipv4还是ipv6 */ - { - unsigned int ipv4; /* ipv4数据*/ - unsigned int ipv6[4]; /* ipv6数据*/ - }; - }; - - - /* 布尔表达式的扫描结果类型 */ - struct scan_result - { - unsigned int rule_id; /* 规则的ID */ - void * tag; /* 用户自定义数据,命中时随匹配结果返回 */ - }; - - - struct ip_matcher; - - /* - 功能:根据输入的规则生成扫描器 - 参数: - rules[in]:一组ip规则 - rule_num[in]:输入的规则数量 - mem_use[out]:内存消耗量 - 返回值: - ip扫描器,返回空指针生成扫描器失败 - */ - struct ip_matcher* ip_matcher_new(struct ip_rule * rules, size_t rule_num, size_t * mem_use); - - /* - 功能:调用ip扫描器对输入的ip数据进行扫描 - 参数: - matcher[in]:ip扫描器 - data[in]:输入的待扫描ip数据 - result[in]:返回结果存储数组 - size[in]:结果数组的大小 - 返回值: - 命中结果的数量(<=size);返回值为-1表示出错。 - - */ - int ip_matcher_match(struct ip_matcher* matcher, struct ip_data * data, struct scan_result* result, size_t size); - - /* - 功能:销毁一个ip扫描器 - 参数: - matcher[in]:待销毁的ip扫描器指针 - */ - void ip_matcher_free(struct ip_matcher* matcher); - -#ifdef __cplusplus -} -#endif - -#endif /* !defined(H_IP_MATCHER_H) */ diff --git a/scanner/adapter_hs.cpp b/scanner/adapter_hs.cpp index 2ff4a5f..c852325 100644 --- a/scanner/adapter_hs.cpp +++ b/scanner/adapter_hs.cpp @@ -184,7 +184,7 @@ void adpt_hs_compile_data_free(struct adpt_hs_compile_data *hs_cd, size_t n_patt struct adapter_hs *adapter_hs_initialize(int scan_mode, size_t nr_worker_threads, and_expr_t *expr_array, size_t n_expr_array) { - if ((scan_mode != SCAN_MODE_BLOCK && scan_mode != SCAN_MODE_STREAM) || + if ((scan_mode != HS_SCAN_MODE_BLOCK && scan_mode != HS_SCAN_MODE_STREAM) || 0 == nr_worker_threads || NULL == expr_array || 0 == n_expr_array) { fprintf(stderr, "%s input parameters illegal!", __func__); return NULL; @@ -407,11 +407,12 @@ int adapter_hs_scan(struct adapter_hs *hs_instance, int thread_id, const char *d utarray_new(pattern_id_set, &ut_pattern_id_icd); utarray_reserve(pattern_id_set, hs_instance->n_patterns); + int err_count = 0; if (hs_rt->literal_db != NULL) { err = hs_scan(hs_rt->literal_db, data, data_len, 0, scratch, matched_event_cb, pattern_id_set); if (err != HS_SUCCESS) { //log_error() - return -1; + err_count++; } } @@ -419,10 +420,14 @@ int adapter_hs_scan(struct adapter_hs *hs_instance, int thread_id, const char *d err = hs_scan(hs_rt->regex_db, data, data_len, 0, scratch, matched_event_cb, pattern_id_set); if (err != HS_SUCCESS) { //log_error() - return -1; + err_count++; } } + if (2 == err_count) { + return -1; + } + size_t pattern_set_size = utarray_len(pattern_id_set); unsigned long long items[pattern_set_size]; memset(items, 0, sizeof(unsigned long long) * pattern_set_size); diff --git a/scanner/adapter_hs.h b/scanner/adapter_hs.h index 3d6f9f3..1679b61 100644 --- a/scanner/adapter_hs.h +++ b/scanner/adapter_hs.h @@ -24,10 +24,10 @@ extern "C" struct adapter_hs; /* scan mode */ -enum scan_mode { - SCAN_MODE_BLOCK = 1, - SCAN_MODE_STREAM, - SCAN_MODE_MAX +enum hs_scan_mode { + HS_SCAN_MODE_BLOCK = 1, + HS_SCAN_MODE_STREAM, + HS_SCAN_MODE_MAX }; /* pattern type: PATTERN_TYPE_STR(pure literal string) or PATTERN_TYPE_REG(regex expression) */ diff --git a/scanner/adapter_hs_gtest.cpp b/scanner/adapter_hs_gtest.cpp index 1031f53..ef93983 100644 --- a/scanner/adapter_hs_gtest.cpp +++ b/scanner/adapter_hs_gtest.cpp @@ -66,11 +66,11 @@ TEST(block_mode_initialize, invalid_input_parameter) EXPECT_EQ(hs_instance, nullptr); /* case2: invalid expr parameter */ - hs_instance = adapter_hs_initialize(SCAN_MODE_BLOCK, 1, nullptr, 1); + hs_instance = adapter_hs_initialize(HS_SCAN_MODE_BLOCK, 1, nullptr, 1); EXPECT_EQ(hs_instance, nullptr); /* case3: invalid expr num */ - hs_instance = adapter_hs_initialize(SCAN_MODE_BLOCK, 1, exprs, 0); + hs_instance = adapter_hs_initialize(HS_SCAN_MODE_BLOCK, 1, exprs, 0); EXPECT_EQ(hs_instance, nullptr); } @@ -79,7 +79,7 @@ TEST(block_mode_scan, invalid_input_parameter) and_expr_t expr_array[64]; size_t n_expr_array = 0; - struct adapter_hs *hs_instance = adapter_hs_initialize(SCAN_MODE_BLOCK, 1, nullptr, 0); + struct adapter_hs *hs_instance = adapter_hs_initialize(HS_SCAN_MODE_BLOCK, 1, nullptr, 0); EXPECT_EQ(hs_instance, nullptr); hs_instance = adapter_hs_initialize(0, 1, expr_array, n_expr_array); @@ -88,7 +88,7 @@ TEST(block_mode_scan, invalid_input_parameter) n_expr_array = 1; expr_array[0].expr_id = 101; expr_array[0].n_patterns = 10; - hs_instance = adapter_hs_initialize(SCAN_MODE_BLOCK, 1, expr_array, n_expr_array); + hs_instance = adapter_hs_initialize(HS_SCAN_MODE_BLOCK, 1, expr_array, n_expr_array); EXPECT_EQ(hs_instance, nullptr); memset(expr_array, 0, sizeof(expr_array)); @@ -96,7 +96,7 @@ TEST(block_mode_scan, invalid_input_parameter) expr_array[0].expr_id = 101; expr_array[0].n_patterns = 1; expr_array[0].patterns[0].type = 0; - hs_instance = adapter_hs_initialize(SCAN_MODE_BLOCK, 1, expr_array, n_expr_array); + hs_instance = adapter_hs_initialize(HS_SCAN_MODE_BLOCK, 1, expr_array, n_expr_array); EXPECT_EQ(hs_instance, nullptr); } @@ -109,7 +109,7 @@ TEST(block_mode_scan, hit_one_expr) EXPECT_EQ(ret, 0); EXPECT_EQ(n_expr_array, 6); - struct adapter_hs *hs_instance = adapter_hs_initialize(SCAN_MODE_BLOCK, 1, expr_array, n_expr_array); + struct adapter_hs *hs_instance = adapter_hs_initialize(HS_SCAN_MODE_BLOCK, 1, expr_array, n_expr_array); EXPECT_NE(hs_instance, nullptr); expr_array_free(expr_array, n_expr_array); @@ -149,7 +149,7 @@ TEST(block_mode_scan, hit_two_expr) EXPECT_EQ(ret, 0); EXPECT_EQ(n_expr_array, 6); - struct adapter_hs *hs_instance = adapter_hs_initialize(SCAN_MODE_BLOCK, 1, expr_array, n_expr_array); + struct adapter_hs *hs_instance = adapter_hs_initialize(HS_SCAN_MODE_BLOCK, 1, expr_array, n_expr_array); EXPECT_NE(hs_instance, nullptr); expr_array_free(expr_array, n_expr_array); @@ -184,7 +184,7 @@ TEST(block_mode_scan, hit_three_expr) EXPECT_EQ(ret, 0); EXPECT_EQ(n_expr_array, 6); - struct adapter_hs *hs_instance = adapter_hs_initialize(SCAN_MODE_BLOCK, 1, expr_array, n_expr_array); + struct adapter_hs *hs_instance = adapter_hs_initialize(HS_SCAN_MODE_BLOCK, 1, expr_array, n_expr_array); EXPECT_NE(hs_instance, nullptr); expr_array_free(expr_array, n_expr_array); @@ -221,7 +221,7 @@ TEST(block_mode_scan, hit_four_expr) EXPECT_EQ(ret, 0); EXPECT_EQ(n_expr_array, 6); - struct adapter_hs *hs_instance = adapter_hs_initialize(SCAN_MODE_BLOCK, 1, expr_array, n_expr_array); + struct adapter_hs *hs_instance = adapter_hs_initialize(HS_SCAN_MODE_BLOCK, 1, expr_array, n_expr_array); EXPECT_NE(hs_instance, nullptr); expr_array_free(expr_array, n_expr_array); @@ -260,7 +260,7 @@ TEST(block_mode_scan, hit_five_expr) EXPECT_EQ(ret, 0); EXPECT_EQ(n_expr_array, 6); - struct adapter_hs *hs_instance = adapter_hs_initialize(SCAN_MODE_BLOCK, 1, expr_array, n_expr_array); + struct adapter_hs *hs_instance = adapter_hs_initialize(HS_SCAN_MODE_BLOCK, 1, expr_array, n_expr_array); EXPECT_NE(hs_instance, nullptr); expr_array_free(expr_array, n_expr_array); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1e1b481..a6c25d3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,8 +8,9 @@ set(MAAT_FRAME_VERSION ${MAAT_FRAME_MAJOR_VERSION}.${MAAT_FRAME_MINOR_VERSION}.$ message(STATUS "Maat Frame, Version: ${MAAT_FRAME_VERSION}") add_definitions(-fPIC) -set(MAAT_SRC maat_api.cpp rcu_hash.cpp maat_garbage_collection.cpp maat_config_monitor.cpp maat_rule.cpp - maat_kv.cpp maat_ex_data.cpp maat_table_schema.cpp maat_table_runtime.cpp maat_utils.cpp) +set(MAAT_SRC json2iris.cpp maat_api.cpp rcu_hash.cpp maat_garbage_collection.cpp maat_config_monitor.cpp + maat_rule.cpp maat_kv.cpp maat_ex_data.cpp maat_table_schema.cpp maat_table_runtime.cpp maat_utils.cpp + maat_command.cpp maat_redis_monitor.cpp) set(LIB_SOURCE_FILES ${PROJECT_SOURCE_DIR}/deps/cJSON/cJSON.c) @@ -25,7 +26,7 @@ set_target_properties(maat_frame_static PROPERTIES LINKER_LANGUAGE CXX) set_target_properties(maat_frame_static PROPERTIES OUTPUT_NAME maatframe) set_target_properties(maat_frame_static PROPERTIES CLEAN_DIRECT_OUTPUT 1) -target_link_libraries(maat_frame_static adapter-static pthread) +target_link_libraries(maat_frame_static adapter-static hiredis-static pthread crypto z) # Shared Library Output #add_library(maat_frame_shared SHARED ${MAAT_SRC} ${LIB_SOURCE_FILES}) diff --git a/src/inc_internal/json2iris.h b/src/inc_internal/json2iris.h new file mode 100644 index 0000000..bfeca07 --- /dev/null +++ b/src/inc_internal/json2iris.h @@ -0,0 +1,29 @@ +/* +********************************************************************************************** +* File: json2iris.h +* Description: rule for transform json2iris +* Authors: Liu WenTan +* Date: 2022-10-31 +* Copyright: (c) 2018-2022 Geedge Networks, Inc. All rights reserved. +*********************************************************************************************** +*/ + +#ifndef _JSON2IRIS_H_ +#define _JSON2IRIS_H_ + +#ifdef __cpluscplus +extern "C" +{ +#endif + +#include "hiredis/hiredis.h" + +int json2iris(const char* json_buff, const char* json_filename, const char*compile_tn, + const char* group2compile_tn, const char* group2group_tn, redisContext *redis_write_ctx, + char* iris_dir_buf, int buf_len, char* encrypt_key, char* encrypt_algo); + +#ifdef __cpluscplus +} +#endif + +#endif \ No newline at end of file diff --git a/src/inc_internal/maat_command.h b/src/inc_internal/maat_command.h new file mode 100644 index 0000000..66a0636 --- /dev/null +++ b/src/inc_internal/maat_command.h @@ -0,0 +1,39 @@ +/* +********************************************************************************************** +* File: maat_command.h +* Description: +* Authors: Liu WenTan +* Date: 2022-10-31 +* Copyright: (c) 2018-2022 Geedge Networks, Inc. All rights reserved. +*********************************************************************************************** +*/ + +#ifndef _MAAT_COMMAND_H_ +#define _MAAT_COMMAND_H_ + +#ifdef __cpluscplus +extern "C" +{ +#endif + +enum maat_operation { + MAAT_OP_DEL = 0, + MAAT_OP_ADD, + MAAT_OP_RENEW_TIMEOUT //Rule expire time is changed to now+cmd->expire_after +}; + +struct maat_cmd_line +{ + const char *table_name; + const char *table_line; + int rule_id; // for MAAT_OP_DEL, only rule_id and table_name are necessary. + int expire_after; //expired after $timeout$ seconds, set to 0 for never timeout. +}; + +int maat_cmd_set_line(struct maat *maat_instance, const struct maat_cmd_line *line_rule); + +#ifdef __cpluscplus +} +#endif + +#endif \ No newline at end of file diff --git a/src/inc_internal/maat_common.h b/src/inc_internal/maat_common.h index 872120d..61aa499 100644 --- a/src/inc_internal/maat_common.h +++ b/src/inc_internal/maat_common.h @@ -18,6 +18,8 @@ extern "C" #include +#include "maat_rule.h" + struct maat_options { size_t nr_worker_threads; int rule_effect_interval_ms; @@ -25,8 +27,11 @@ struct maat_options { int gc_timeout_ms; int deferred_load_on; enum data_source input_mode; - char iris_full_dir[NAME_MAX]; - char iris_inc_dir[NAME_MAX]; + union { + struct source_iris_ctx iris_ctx; + struct source_json_ctx json_ctx; + struct source_redis_ctx redis_ctx; + }; }; #ifdef __cpluscplus diff --git a/src/inc_internal/maat_config_monitor.h b/src/inc_internal/maat_config_monitor.h index 46878ac..eb3de29 100644 --- a/src/inc_internal/maat_config_monitor.h +++ b/src/inc_internal/maat_config_monitor.h @@ -28,6 +28,8 @@ void config_monitor_traverse(long long version, const char *idx_dir, void (*finish_fn)(void *), void *u_param); +int load_maat_json_file(struct maat *maat_instance, const char *json_filename, char *err_str, size_t err_str_sz); + #ifdef __cpluscplus } #endif diff --git a/src/inc_internal/maat_redis_monitor.h b/src/inc_internal/maat_redis_monitor.h new file mode 100644 index 0000000..7bb4800 --- /dev/null +++ b/src/inc_internal/maat_redis_monitor.h @@ -0,0 +1,33 @@ +/* +********************************************************************************************** +* File: maat_redis_monitor.h +* Description: maat redis monitor api +* Authors: Liu WenTan +* Date: 2022-11-29 +* Copyright: (c) 2018-2022 Geedge Networks, Inc. All rights reserved. +*********************************************************************************************** +*/ + +#ifndef _MAAT_REDIS_MONITOR_H_ +#define _MAAT_REDIS_MONITOR_H_ + +#ifdef __cpluscplus +extern "C" +{ +#endif + +#include "maat_rule.h" + +#include + +void redis_monitor_traverse(long long version, struct source_redis_ctx* mr_ctx, + void (*start_fn)(long long, int, void *), + int (*update_fn)(const char *, const char *, void *), + void (*finish_fn)(void *), + void *u_param); + +#ifdef __cpluscplus +} +#endif + +#endif \ No newline at end of file diff --git a/src/inc_internal/maat_rule.h b/src/inc_internal/maat_rule.h index 808d95d..6e7cec6 100644 --- a/src/inc_internal/maat_rule.h +++ b/src/inc_internal/maat_rule.h @@ -21,6 +21,13 @@ extern "C" #include #include #include +#include +#include + +#include "hiredis/hiredis.h" +#include "uthash/uthash.h" +#include "maat_table_schema.h" +#include "maat_command.h" struct maat_runtime { /* maat_runtime can be created and destroy dynamic, so need version info */ @@ -37,7 +44,9 @@ struct maat_runtime { enum data_source { DATA_SOURCE_NONE = 0, - DATA_SOURCE_IRIS_FILE + DATA_SOURCE_REDIS, + DATA_SOURCE_IRIS_FILE, + DATA_SOURCE_JSON_FILE }; struct source_iris_ctx { @@ -45,6 +54,52 @@ struct source_iris_ctx { char full_dir[NAME_MAX]; }; +struct source_json_ctx +{ + char json_file[NAME_MAX]; + char iris_file[NAME_MAX]; + char effective_json_md5[MD5_DIGEST_LENGTH*2+1]; + struct timespec last_md5_time; +}; + +struct source_redis_ctx +{ + redisContext *read_ctx; + redisContext *write_ctx; + char redis_ip[64]; + uint16_t redis_port; + int redis_db; + time_t last_reconnect_time; +}; + +struct foreign_key { + int column; + char *key; + size_t key_len; + char *filename; +}; + +//rm= Redis Maat +struct serial_rule { + enum maat_operation op;//0: delete, 1: add. + unsigned long rule_id; + int label_id; + long long timeout; // absolute unix time. + char table_name[NAME_MAX]; + char *table_line; + int n_foreign; + struct foreign_key *f_keys; + TAILQ_ENTRY(serial_rule) entries; + UT_hash_handle hh; +}; + +#define POSSIBLE_REDIS_REPLY_SIZE 2 +struct expected_reply { + int s_rule_seq; + int possible_reply_num; + redisReply possible_replies[POSSIBLE_REDIS_REPLY_SIZE]; +}; + struct maat { char instance_name[NAME_MAX]; @@ -56,6 +111,8 @@ struct maat { enum data_source input_mode; union { struct source_iris_ctx iris_ctx; + struct source_json_ctx json_ctx; + struct source_redis_ctx mr_ctx; }; int deferred_load; @@ -72,7 +129,24 @@ struct maat { int rule_update_checking_interval_ms; int gc_timeout_ms; //garbage collection timeout_ms; + int cumulative_update_off; //Default: cumulative update on + struct maat_garbage_bin *garbage_bin; + + char compile_tn[NAME_MAX]; + char group_tn[NAME_MAX]; + char group2compile_tn[NAME_MAX]; + char group2group_tn[NAME_MAX]; + + char decrypt_key[NAME_MAX]; + char decrypt_algo[NAME_MAX]; + int maat_json_is_gzipped; + + long long load_specific_version; //Default: Load the Latest. Only valid in redis mode, and maybe failed for too old + char foreign_cont_dir[NAME_MAX]; + + /* statistics */ + long long line_cmd_acc_num; }; void maat_start_cb(long long new_version, int update_type, void *u_para); @@ -85,6 +159,40 @@ void *rule_monitor_loop(void *arg); void maat_read_full_config(struct maat *maat_instance); +/* maat command API for internal */ +redisContext *maat_cmd_connect_redis(const char *redis_ip, int redis_port, int redis_db); + +redisReply *maat_cmd_wrap_redis_command(redisContext *c, const char *format, ...); + +int maat_cmd_wrap_redis_get_reply(redisContext *c, redisReply **reply); + +long long maat_cmd_redis_server_time_s(redisContext *c); + +long long maat_cmd_read_redis_integer(const redisReply *reply); + +int maat_cmd_get_valid_flag_offset(const char *line, enum table_type table_type, int valid_column_seq); + +const char *maat_cmd_find_Nth_column(const char *line, int Nth, int *column_len); + +int maat_cmd_exec_serial_rule(redisContext *c, struct serial_rule *s_rule, size_t serial_rule_num, long long server_time); + +void maat_cmd_empty_serial_rule(struct serial_rule *s_rule); + +int maat_cmd_get_rm_key_list(redisContext *c, long long instance_version, long long desired_version, + long long *new_version, struct table_schema_manager* table_schema_mgr, + struct serial_rule **list, int *update_type, int cumulative_off); + +int maat_cmd_get_redis_value(redisContext *c, struct serial_rule *rule_list, int rule_num, int print_process); + +int maat_cmd_get_foreign_keys_by_prefix(redisContext *ctx, struct serial_rule *rule_list, int rule_num, const char* dir); + +void maat_cmd_get_foreign_conts(redisContext *ctx, struct serial_rule *rule_list, int rule_num, int print_fn); + +void maat_cmd_rewrite_table_line_with_foreign(struct serial_rule *s_rule); + +void maat_cmd_set_serial_rule(struct serial_rule *rule, enum maat_operation op, unsigned long rule_id, + const char *table_name, const char *line, long long timeout); + #ifdef __cpluscplus } #endif diff --git a/src/inc_internal/maat_table_schema.h b/src/inc_internal/maat_table_schema.h index 570b591..739cef6 100644 --- a/src/inc_internal/maat_table_schema.h +++ b/src/inc_internal/maat_table_schema.h @@ -25,13 +25,28 @@ extern "C" #define MAX_DISTRICT_STR 128 #define MAX_IP_STR 128 #define MAX_KEYWORDS_STR 1024 +#define MAX_FOREIGN_CLMN_NUM 8 enum table_type { TABLE_TYPE_EXPR = 0, TABLE_TYPE_EXPR_PLUS, TABLE_TYPE_IP, + TABLE_TYPE_IP_PLUS, + TABLE_TYPE_INTERVAL, + TABLE_TYPE_INTERVAL_PLUS, + TABLE_TYPE_DIGEST, + TABLE_TYPE_SIMILARITY, TABLE_TYPE_PLUGIN, TABLE_TYPE_IP_PLUGIN, + TABLE_TYPE_FQDN_PLUGIN, + TABLE_TYPE_BOOL_PLUGIN, + //above are physical table + TABLE_TYPE_VIRTUAL, + TABLE_TYPE_COMPOSITION, + TABLE_TYPE_COMPILE, + TABLE_TYPE_GROUP, + TABLE_TYPE_GROUP2GROUP, + TABLE_TYPE_GROUP2COMPILE, TABLE_TYPE_MAX }; @@ -42,8 +57,21 @@ enum expr_type { EXPR_TYPE_MAX }; +enum scan_type { + SCAN_TYPE_INVALID = -1, + SCAN_TYPE_NONE = 0, + SCAN_TYPE_PLUGIN, + SCAN_TYPE_IP_PLUGIN, + SCAN_TYPE_FQDN_PLUGIN, + SCAN_TYPE_BOOL_PLUGIN, + SCAN_TYPE_IP, + SCAN_TYPE_INTERVAL, + SCAN_TYPE_STRING, + SCAN_TYPE_MAX +}; + enum match_method { - MATCH_METHOD_SUB=0, + MATCH_METHOD_SUB = 0, MATCH_METHOD_RIGHT, MATCH_METHOD_LEFT, MATCH_METHOD_COMPLETE, @@ -60,10 +88,6 @@ struct expr_item { int is_hexbin; int is_case_sensitive; int is_valid; - - //rule_tag; 鍙瓨鍦╯chema閲 - //int have_exdata; - //struct ex_data *ex_data; //hash琛 }; struct plugin_item { @@ -129,14 +153,21 @@ void table_schema_manager_all_plugin_cb_finish(struct table_schema_manager* tabl /* table schema generic API */ struct table_schema *table_schema_get(struct table_schema_manager *table_schema_mgr, int table_id); +struct table_schema *table_schema_get_by_scan_type(struct table_schema_manager *table_schema_mgr, + int table_id, enum scan_type type, int *virtual_table_id); + enum table_type table_schema_get_table_type(struct table_schema *table_schema); int table_schema_get_table_id(struct table_schema *table_schema); +enum scan_type table_schema_get_scan_type(struct table_schema *table_schema); + struct table_item *table_schema_line_to_item(const char *line, struct table_schema *table_schema); +int table_schema_get_valid_flag_column(struct table_schema *table_schema); + /* expr table schema API */ -enum scan_mode expr_table_schema_get_scan_mode(struct table_schema *table_schema); +enum hs_scan_mode expr_table_schema_get_scan_mode(struct table_schema *table_schema); /* plugin table schema API */ int plugin_table_schema_set_ex_data_schema(struct table_schema *table_schema, @@ -165,6 +196,8 @@ size_t plugin_table_schema_callback_count(struct table_schema *table_schema); void plugin_table_schema_all_cb_update(struct table_schema *table_schema, const char *row); +int plugin_table_schema_get_foreign_column(struct table_schema *table_schema, int *foreign_columns); + #ifdef __cpluscplus } #endif diff --git a/src/inc_internal/maat_utils.h b/src/inc_internal/maat_utils.h index 3d26770..42f0824 100644 --- a/src/inc_internal/maat_utils.h +++ b/src/inc_internal/maat_utils.h @@ -30,6 +30,8 @@ extern "C" #define MIN(a, b) (((a) < (b)) ? (a) : (b)) #endif +#define UNUSED __attribute__((unused)) + char *maat_strdup(const char *s); int get_column_pos(const char *line, int column_seq, size_t *offset, size_t *len); @@ -42,6 +44,19 @@ char *str_unescape_and(char *s); char *str_unescape(char *s); +char *md5_file(const char *filename, char *md5string); + +int decrypt_open(const char* file_name, const char* key, const char* algorithm, + unsigned char**pp_out, size_t *out_sz, char* err_str, size_t err_str_sz); + +int crypt_memory(const unsigned char *inbuf, size_t inlen, unsigned char **pp_out, size_t *out_sz, + const char *key, const char *algorithm, int do_encrypt, char *err_str, size_t err_str_sz); + +int gzip_uncompress(const unsigned char *in_compressed_data, size_t in_compressed_sz, + unsigned char **out_uncompressed_data, size_t *out_uncompressed_sz); + +size_t memcat(void **dest, size_t offset, size_t *n_dest, const void *src, size_t n_src); + /* system cmd wrapper */ int system_cmd_mkdir(const char* path); diff --git a/src/json2iris.cpp b/src/json2iris.cpp new file mode 100644 index 0000000..46cc7a6 --- /dev/null +++ b/src/json2iris.cpp @@ -0,0 +1,1207 @@ +/* +********************************************************************************************** +* File: json2iris.h +* Description: rule for transform json2iris +* Authors: Liu WenTan +* Date: 2022-10-31 +* Copyright: (c) 2018-2022 Geedge Networks, Inc. All rights reserved. +*********************************************************************************************** +*/ + +#include +#include +#include +#include +#include +#include + +#include "utils.h" +#include "json2iris.h" +#include "cJSON/cJSON.h" +#include "maat_kv.h" +#include "maat_utils.h" +#include "maat_table_schema.h" +#include "maat_rule.h" +#include "uthash/uthash.h" + +#define MAX_COLUMN_NUM 32 + +#define mr_region_id_var "SEQUENCE_REGION" +#define mr_group_id_var "SEQUENCE_GROUP" + +const int json_version = 1; +const char *untitled_group_name="Untitled"; + +enum maat_group_relation { + PARENT_TYPE_COMPILE = 0, + PARENT_TYPE_GROUP +}; + +struct group_info { + int group_id; + char group_name[PATH_MAX]; + UT_hash_handle hh; +}; + +struct iris_table { + char table_name[PATH_MAX]; + char table_path[PATH_MAX]; + int line_count; + enum table_type table_type; + void *buff; + size_t write_pos; + size_t buff_sz; + UT_hash_handle hh; +}; + +struct iris_description { + int group_cnt; + int region_cnt; + + char tmp_iris_dir[PATH_MAX]; + char tmp_iris_index_dir[PATH_MAX]; + char index_path[PATH_MAX]; + + struct iris_table *group_table; + struct iris_table *group2group_table; + struct iris_table *group2compile_table; + struct iris_table *compile_table; + + struct group_info *group_name_map; + struct iris_table *iris_table_map; + + struct maat_kv_store *str2int_map; + redisContext *redis_write_ctx; + + char *encrypt_key; + char *encrypt_algo; + FILE *idx_fp; +}; + +struct translate_command { + const char *json_string; + char *json_value; + int json_type; + int str2int_flag; + int empty_allowed; + const char *default_string; + int default_int; +}; + +struct iris_table *query_table_info(struct iris_description *p_iris, const char *table_name, enum table_type table_type) +{ + struct iris_table *table_info = NULL; + HASH_FIND(hh, p_iris->iris_table_map, table_name, strlen(table_name), table_info); + if (NULL == table_info) { + table_info = ALLOC(struct iris_table, 1); + table_info->line_count = 0; + table_info->table_type = table_type; + memcpy(table_info->table_name, table_name, MIN(sizeof(table_info->table_name) - 1, strlen(table_name))); + snprintf(table_info->table_path, sizeof(table_info->table_path), "%s/%s.local", p_iris->tmp_iris_dir, table_info->table_name); + HASH_ADD_KEYPTR(hh, p_iris->iris_table_map, table_name, strlen(table_name), table_info); + } + + return table_info; +} + +void free_iris_table_info(void *p) +{ + struct iris_table *table = (struct iris_table *)p; + if (table != NULL) { + FREE(table->buff); + } + FREE(table); +} + +int set_iris_descriptor(const char *json_file, cJSON *json, const char *encrypt_key, const char *encrypt_algo, + const char *compile_tn, const char *group2compile_tn, const char* group2group_tn, + redisContext *redis_write_ctx, struct iris_description *iris_cfg) +{ + memset(iris_cfg, 0, sizeof(struct iris_description)); + snprintf(iris_cfg->tmp_iris_dir, sizeof(iris_cfg->tmp_iris_dir), "%s_iris_tmp", json_file); + snprintf(iris_cfg->tmp_iris_index_dir, sizeof(iris_cfg->tmp_iris_index_dir), "%s_iris_tmp/index", json_file); + snprintf(iris_cfg->index_path, sizeof(iris_cfg->index_path), "%s/full_config_index.%010d", iris_cfg->tmp_iris_index_dir, json_version); + + iris_cfg->redis_write_ctx = redis_write_ctx; + iris_cfg->str2int_map = maat_kv_store_new(); + + maat_kv_register(iris_cfg->str2int_map, "yes", 1); + maat_kv_register(iris_cfg->str2int_map, "no", 0); + + maat_kv_register(iris_cfg->str2int_map, "ip", TABLE_TYPE_IP); + maat_kv_register(iris_cfg->str2int_map, "ip_plus", TABLE_TYPE_IP_PLUS); + maat_kv_register(iris_cfg->str2int_map, "string", TABLE_TYPE_EXPR); + maat_kv_register(iris_cfg->str2int_map, "expr", TABLE_TYPE_EXPR); + maat_kv_register(iris_cfg->str2int_map, "expr_plus", TABLE_TYPE_EXPR_PLUS); + maat_kv_register(iris_cfg->str2int_map, "intval", TABLE_TYPE_INTERVAL); + maat_kv_register(iris_cfg->str2int_map, "interval", TABLE_TYPE_INTERVAL); + maat_kv_register(iris_cfg->str2int_map, "intval_plus", TABLE_TYPE_INTERVAL_PLUS); + maat_kv_register(iris_cfg->str2int_map, "interval_plus", TABLE_TYPE_INTERVAL_PLUS); + maat_kv_register(iris_cfg->str2int_map, "digest", TABLE_TYPE_DIGEST); + maat_kv_register(iris_cfg->str2int_map, "similar", TABLE_TYPE_SIMILARITY); + + maat_kv_register(iris_cfg->str2int_map, "ipv4", 4); + maat_kv_register(iris_cfg->str2int_map, "ipv6", 6); + + maat_kv_register(iris_cfg->str2int_map, "double", 0); + maat_kv_register(iris_cfg->str2int_map, "single", 1); + + maat_kv_register(iris_cfg->str2int_map, "none", 0); + maat_kv_register(iris_cfg->str2int_map, "and", 1); + maat_kv_register(iris_cfg->str2int_map, "regex", 2); + maat_kv_register(iris_cfg->str2int_map, "offset", 3); + + maat_kv_register(iris_cfg->str2int_map, "sub", 0); + maat_kv_register(iris_cfg->str2int_map, "right", 1); + maat_kv_register(iris_cfg->str2int_map, "suffix", 1); + maat_kv_register(iris_cfg->str2int_map, "left", 2); + maat_kv_register(iris_cfg->str2int_map, "prefix", 2); + maat_kv_register(iris_cfg->str2int_map, "complete", 3); + maat_kv_register(iris_cfg->str2int_map, "exact", 3); + + maat_kv_register(iris_cfg->str2int_map, "uncase plain", 0); + maat_kv_register(iris_cfg->str2int_map, "hexbin", 1); + maat_kv_register(iris_cfg->str2int_map, "case plain", 2); + + iris_cfg->compile_table = query_table_info(iris_cfg, compile_tn, TABLE_TYPE_COMPILE); + iris_cfg->group2compile_table = query_table_info(iris_cfg, group2compile_tn, TABLE_TYPE_GROUP2COMPILE); + iris_cfg->group2group_table = query_table_info(iris_cfg, group2group_tn, TABLE_TYPE_GROUP2GROUP); + + if (encrypt_key && encrypt_algo) { + iris_cfg->encrypt_key = maat_strdup(encrypt_key); + iris_cfg->encrypt_algo = maat_strdup(encrypt_algo); + } + + return 0; +} + +void clear_iris_descriptor(struct iris_description *iris_cfg) +{ + if (iris_cfg->group_name_map != NULL) { + struct group_info *node = NULL; + struct group_info *tmp = NULL; + HASH_ITER(hh, iris_cfg->group_name_map, node, tmp) { + HASH_DELETE(hh, iris_cfg->group_name_map, node); + FREE(node); + } + } + + if(iris_cfg->iris_table_map != NULL) { + struct iris_table *node = NULL; + struct iris_table *tmp = NULL; + HASH_ITER(hh, iris_cfg->iris_table_map, node, tmp) { + HASH_DELETE(hh, iris_cfg->iris_table_map, node); + free_iris_table_info(node); + } + } + + maat_kv_store_free(iris_cfg->str2int_map); + FREE(iris_cfg->encrypt_algo); + FREE(iris_cfg->encrypt_key); + + return; +} + +int create_tmp_dir(struct iris_description *p) +{ + if ((access(p->tmp_iris_dir,F_OK)) < 0) { + if ((mkdir(p->tmp_iris_dir, 0777)) < 0) { + return -1; + } + } + + if ((access(p->tmp_iris_index_dir,F_OK)) < 0) { + if ((mkdir(p->tmp_iris_index_dir, 0777)) < 0) { + return -1; + } + } + + return 0; +} + +int write_plugin_line(cJSON *plug_table_json, int sequence, struct iris_description *p_iris) +{ + cJSON *item = cJSON_GetObjectItem(plug_table_json, "table_name"); + if (NULL == item || item->type != cJSON_String) { + fprintf(stderr, "The %d plugin_table's table_name not defined or format error.", sequence); + return -1; + } + const char *table_name = item->valuestring; + + cJSON *table_content = cJSON_GetObjectItem(plug_table_json, "table_content"); + if (NULL == table_content || table_content->type != cJSON_Array) { + fprintf(stderr, "%d plugin_table's table_content not defined or format error.", sequence); + return -1; + } + int line_cnt = cJSON_GetArraySize(table_content); + + struct iris_table *table_info = query_table_info(p_iris, table_name, TABLE_TYPE_PLUGIN); + + cJSON *each_line = NULL; + const char *line_content = NULL; + for (int i = 0; i < line_cnt; i++) { + each_line = cJSON_GetArrayItem(table_content, i); + if (NULL == each_line || each_line->type != cJSON_String) { + fprintf(stderr, "plugin_table %s's line %d format error.", table_info->table_name, i+1); + continue; + } + + line_content = each_line->valuestring; + table_info->write_pos += memcat(&(table_info->buff), table_info->write_pos, &(table_info->buff_sz), line_content, strlen(line_content)); + table_info->write_pos += memcat(&(table_info->buff), table_info->write_pos, &(table_info->buff_sz), "\n", 1); + table_info->line_count++; + } + + return 0; +} + +static struct group_info *group_info_read(struct group_info *group_name_map, const char *group_name) +{ + struct group_info *node = NULL; + HASH_FIND(hh, group_name_map, group_name, strlen(group_name), node); + + return node; +} + +static int get_group_seq(struct iris_description *iris_cfg) +{ + redisReply *data_reply=NULL; + int sequence = 0; + + if (NULL == iris_cfg->redis_write_ctx) { + sequence = iris_cfg->group_cnt; + } else { + data_reply = maat_cmd_wrap_redis_command(iris_cfg->redis_write_ctx, "INCRBY %s 1", mr_group_id_var); + sequence = (int)data_reply->integer - 1; + freeReplyObject(data_reply); + data_reply = NULL; + } + iris_cfg->group_cnt++; + + return sequence; +} + +static struct group_info *group_info_add_unsafe(struct iris_description *p_iris, const char *group_name) +{ + static struct group_info untitled_group; + struct group_info *group_info = NULL; + + if (0 == strncasecmp(group_name, untitled_group_name, strlen(untitled_group_name))) { + group_info = &untitled_group; + group_info->group_id = get_group_seq(p_iris); + } else { + group_info = ALLOC(struct group_info, 1); + group_info->group_id = get_group_seq(p_iris); + strncpy(group_info->group_name, group_name, sizeof(group_info->group_name)); + HASH_ADD_KEYPTR(hh, p_iris->group_name_map, group_name, strlen(group_name), group_info); + } + return group_info; +} + +static int get_region_seq(struct iris_description *iris_cfg) +{ + int sequence = 0; + + if (NULL == iris_cfg->redis_write_ctx) { + sequence = iris_cfg->region_cnt; + } else { + redisReply *data_reply = maat_cmd_wrap_redis_command(iris_cfg->redis_write_ctx, "INCRBY %s 1", mr_region_id_var); + sequence = (int)data_reply->integer - 1; + freeReplyObject(data_reply); + } + iris_cfg->region_cnt++; + + return sequence; +} + +int direct_write_rule(cJSON *json, struct maat_kv_store *str2int, struct translate_command *cmd, int cmd_cnt, struct iris_table *table) +{ + int i = 0; + int ret = -1; + + for (i = 0; i < cmd_cnt; i++) { + cJSON *item = cJSON_GetObjectItem(json, cmd[i].json_string); + if (NULL == item && (1 == cmd[i].empty_allowed)) { + cJSON dummy; + dummy.valuestring = (char *)cmd[i].default_string; + dummy.valueint = cmd[i].default_int; + dummy.valuedouble = cmd[i].default_int; + dummy.type = cmd[i].json_type; + item = &dummy; + } + + if (NULL == item || item->type != cmd[i].json_type) { + fprintf(stderr, "%s not defined or wrong format.", cmd[i].json_string); + ret = -1; + goto error_out; + } + + if (1 == cmd[i].str2int_flag) { + int int_value = 0; + char *p = item->valuestring; + ret = maat_kv_read(str2int, p, &int_value); + if (ret < 0) { + fprintf(stderr, "%s's value %s is not valid format.", cmd[i].json_string, p); + FREE(p); + ret = -1; + goto error_out; + } + cmd[i].json_value = (char *)malloc(21);/* 2^64+1 can be represented in 21 chars. */ + snprintf(cmd[i].json_value, 21, "%d", int_value); + } else { + switch (item->type) { + case cJSON_Number: + cmd[i].json_value = cJSON_Print(item); + break; + case cJSON_String: + cmd[i].json_value = ALLOC(char, strlen(item->valuestring) + 1); + memcpy(cmd[i].json_value, item->valuestring, strlen(item->valuestring)); + break; + default://impossible ,already checked + assert(0); + break; + } + } + } + + for (i = 0; i < cmd_cnt; i++) { + table->write_pos += memcat(&(table->buff), table->write_pos, &table->buff_sz, cmd[i].json_value, strlen(cmd[i].json_value)); + table->write_pos += memcat(&(table->buff), table->write_pos, &table->buff_sz, "\t", 1); + } + table->write_pos += memcat(&(table->buff), table->write_pos, &table->buff_sz, "\n", 1); + table->line_count++; + ret = 0; +error_out: + for (i = 0; i < cmd_cnt; i++) { + if (cmd[i].json_value != NULL) { + FREE(cmd[i].json_value); + } + } + return ret; +} + +int write_expr_line(cJSON *region_json, struct iris_description *p_iris, struct iris_table *table) +{ + struct translate_command json_cmd[MAX_COLUMN_NUM]; + int cmd_cnt = 0; + memset(json_cmd, 0, sizeof(json_cmd)); + + json_cmd[cmd_cnt].json_string = "region_id"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "group_id"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + if (table->table_type==TABLE_TYPE_EXPR_PLUS) { + json_cmd[cmd_cnt].json_string = "district"; + json_cmd[cmd_cnt].json_type = cJSON_String; + cmd_cnt++; + } + + json_cmd[cmd_cnt].json_string = "keywords"; + json_cmd[cmd_cnt].json_type = cJSON_String; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "expr_type"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].str2int_flag = 1; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "match_method"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].str2int_flag = 1; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "format"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].str2int_flag = 1; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "is_valid"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + return direct_write_rule(region_json, p_iris->str2int_map, json_cmd, cmd_cnt, table); +} + +int write_ip_line(cJSON *region_json, struct iris_description *p_iris, struct iris_table *table) +{ + struct translate_command json_cmd[MAX_COLUMN_NUM]; + int cmd_cnt = 0; + memset(json_cmd, 0, sizeof(json_cmd)); + + json_cmd[cmd_cnt].json_string = "region_id"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "group_id"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "addr_type"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].str2int_flag = 1; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "src_ip"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "0.0.0.0"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "mask_src_ip"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "255.255.255.255"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "src_port"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "0"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "mask_src_port"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "65535"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "dst_ip"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "0.0.0.0"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "mask_dst_ip"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "255.255.255.255"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "dst_port"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "0"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "mask_dst_port"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "65535"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "protocol"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_int = 0; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "direction"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].str2int_flag = 1; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "double"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "is_valid"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + return direct_write_rule(region_json, p_iris->str2int_map, json_cmd, cmd_cnt, table); +} + +int write_ip_plus_line(cJSON *region_json, struct iris_description *p_iris, struct iris_table *table) +{ + struct translate_command json_cmd[MAX_COLUMN_NUM]; + int cmd_cnt = 0; + memset(json_cmd, 0, sizeof(json_cmd)); + + json_cmd[cmd_cnt].json_string = "region_id"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "group_id"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "addr_type"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].str2int_flag = 1; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "saddr_format"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "mask"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "src_ip1"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "0.0.0.0"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "src_ip2"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "255.255.255.255"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "sport_format"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "mask"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "src_port1"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "0"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "src_port2"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "65535"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "daddr_format"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "mask"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "dst_ip1"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "0.0.0.0"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "dst_ip2"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "255.255.255.255"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "dport_format"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "mask"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "dst_port1"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "0"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "dst_port2"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "65535"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "protocol"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_int = 0; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "direction"; + json_cmd[cmd_cnt].json_type = cJSON_String; + json_cmd[cmd_cnt].str2int_flag = 1; + json_cmd[cmd_cnt].empty_allowed = 1; + json_cmd[cmd_cnt].default_string = "double"; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "is_valid"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + return direct_write_rule(region_json, p_iris->str2int_map, json_cmd, cmd_cnt, table); +} + +int write_intval_line(cJSON *region_json, struct iris_description *p_iris, struct iris_table *table) +{ + struct translate_command json_cmd[MAX_COLUMN_NUM]; + int cmd_cnt = 0; + memset(json_cmd, 0, sizeof(json_cmd)); + + json_cmd[cmd_cnt].json_string = "region_id"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "group_id"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + if (table->table_type == TABLE_TYPE_INTERVAL_PLUS) { + json_cmd[cmd_cnt].json_string = "district"; + json_cmd[cmd_cnt].json_type = cJSON_String; + cmd_cnt++; + } + + json_cmd[cmd_cnt].json_string = "low_boundary"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "up_boundary"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "is_valid"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + return direct_write_rule(region_json, p_iris->str2int_map, json_cmd, cmd_cnt, table); +} + +int write_digest_line(cJSON *region_json, struct iris_description *p_iris, struct iris_table *table) +{ + struct translate_command json_cmd[MAX_COLUMN_NUM]; + int cmd_cnt = 0; + memset(json_cmd, 0, sizeof(json_cmd)); + + json_cmd[cmd_cnt].json_string = "region_id"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "group_id"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "raw_len"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "digest"; + json_cmd[cmd_cnt].json_type = cJSON_String; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "cfds_level"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "is_valid"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + return direct_write_rule(region_json, p_iris->str2int_map, json_cmd, cmd_cnt, table); +} + +int write_similar_line(cJSON *region_json, struct iris_description *p_iris, struct iris_table *table) +{ + struct translate_command json_cmd[MAX_COLUMN_NUM]; + int cmd_cnt = 0; + memset(json_cmd, 0, sizeof(json_cmd)); + + json_cmd[cmd_cnt].json_string = "region_id"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "group_id"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "target"; + json_cmd[cmd_cnt].json_type = cJSON_String; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "threshold"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + json_cmd[cmd_cnt].json_string = "is_valid"; + json_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + return direct_write_rule(region_json, p_iris->str2int_map, json_cmd, cmd_cnt, table); +} + +int write_region_rule(cJSON *region_json, int compile_id, int group_id, struct iris_description *p_iris) +{ + cJSON *item = cJSON_GetObjectItem(region_json, "table_name"); + if (NULL == item || item->type != cJSON_String) { + fprintf(stderr, "compile rule %d's table_name not defined or format error.", compile_id); + return -1; + } + const char *table_name = item->valuestring; + + item = cJSON_GetObjectItem(region_json, "table_type"); + if (NULL == item || item->type != cJSON_String) { + fprintf(stderr, "compile rule %d's table name %s's table_type not defined or format error.", + compile_id, table_name); + return -1; + } + + const char *table_type_str = item->valuestring; + enum table_type table_type = TABLE_TYPE_EXPR; + int ret = maat_kv_read(p_iris->str2int_map, table_type_str, (int*)&(table_type)); + if (ret != 1) { + fprintf(stderr, "compile rule %d table name %s's table_type %s invalid.", + compile_id, table_name, table_type_str); + return -1; + } + + cJSON *table_content = cJSON_GetObjectItem(region_json, "table_content"); + if (NULL == table_content || table_content->type != cJSON_Object) { + fprintf(stderr, "compile rule %d table name %s's table_content not defined or format error.", + compile_id, table_name); + return -1; + } + + int region_id = get_region_seq(p_iris); + cJSON_AddNumberToObject(table_content, "region_id", region_id); + cJSON_AddNumberToObject(table_content, "group_id", group_id); + cJSON_AddNumberToObject(table_content, "is_valid", 1); + + struct iris_table *table_info = query_table_info(p_iris, table_name, table_type); + switch(table_type) + { + case TABLE_TYPE_EXPR: + case TABLE_TYPE_EXPR_PLUS: + ret = write_expr_line(table_content, p_iris, table_info); + break; + case TABLE_TYPE_IP: + ret = write_ip_line(table_content, p_iris, table_info); + break; + case TABLE_TYPE_IP_PLUS: + ret = write_ip_plus_line(table_content, p_iris, table_info); + break; + case TABLE_TYPE_INTERVAL: + case TABLE_TYPE_INTERVAL_PLUS: + ret = write_intval_line(table_content, p_iris, table_info); + break; + case TABLE_TYPE_DIGEST: + ret = write_digest_line(table_content, p_iris, table_info); + break; + case TABLE_TYPE_SIMILARITY: + ret = write_similar_line(table_content, p_iris, table_info); + break; + default: + assert(0); + break; + } + return ret; +} + +int write_group2compile_line(int group_id, int compile_id, int group_not_flag, int clause_index, + const char *virtual_table, struct iris_description *p_iris) +{ + char buff[4096] = {0}; + struct iris_table *table = p_iris->group2compile_table; + + snprintf(buff, sizeof(buff), "%d\t%d\t1\t%d\t%s\t%d\n", group_id, compile_id, group_not_flag, virtual_table, clause_index); + table->write_pos += memcat(&(table->buff), table->write_pos, &(table->buff_sz), buff, strlen(buff)); + table->line_count++; + + return 0; +} + +int write_group2group_line(int group_id, int superior_group_id, struct iris_description *p_iris) +{ + char buff[4096] = {0}; + struct iris_table *table = p_iris->group2group_table; + + snprintf(buff, sizeof(buff), "%d\t%d\t1\n", group_id, superior_group_id); + table->write_pos += memcat(&(table->buff), table->write_pos, &(table->buff_sz), buff, strlen(buff)); + table->line_count++; + + return 0; +} + +int write_group_rule(cJSON *group_json, int parent_id, int parent_type, int tracking_compile_id, + int Nth_group, struct iris_description *p_iris) +{ + int ret = 0; + int group_not_flag = 0; + int clause_index = 0; + const char *str_parent_type[2] = {"compile", "group"}; + const char *group_name = NULL; + const char *virtual_table = NULL; + + cJSON *item = cJSON_GetObjectItem(group_json, "group_name"); + if (NULL == item || item->type != cJSON_String) { + group_name = untitled_group_name; + } else { + group_name = item->valuestring; + } + + if (parent_type == PARENT_TYPE_COMPILE) { + item = cJSON_GetObjectItem(group_json, "virtual_table"); + if (NULL == item || item->type != cJSON_String) { + virtual_table = "null"; + } else { + virtual_table = item->valuestring; + } + + item = cJSON_GetObjectItem(group_json, "not_flag"); + if (NULL == item || item->type != cJSON_Number) { + group_not_flag = 0; + } else { + group_not_flag = item->valueint; + } + + item = cJSON_GetObjectItem(group_json, "clause_index"); + if (NULL == item || item->type != cJSON_Number) { + clause_index = Nth_group; + } else { + clause_index = item->valueint; + } + } else { + group_not_flag = 0; + } + + struct group_info *group_info = group_info_read(p_iris->group_name_map, group_name); + //exist group name, regions and sub groups will be ommit. + if (NULL == group_info) { + group_info = group_info_add_unsafe(p_iris, group_name); + cJSON *region_json = cJSON_GetObjectItem(group_json, "regions"); + if (region_json != NULL) { + cJSON *region_rule = NULL; + cJSON_ArrayForEach(region_rule, region_json) { + ret = write_region_rule(region_rule, tracking_compile_id, group_info->group_id, p_iris); + if (ret < 0) { + fprintf(stderr, "compile rule %d write region error.", tracking_compile_id); + return -1; + } + } + } + + cJSON *sub_groups = cJSON_GetObjectItem(group_json,"sub_groups"); + if (sub_groups != NULL) { + //recursively + int i = 0; + cJSON_ArrayForEach(item, sub_groups) { + i++; + ret = write_group_rule(item, group_info->group_id, PARENT_TYPE_GROUP, tracking_compile_id, i, p_iris); + if (ret < 0) { + return -1; + } + } + } + + if (NULL == region_json && NULL == sub_groups) { + fprintf(stdout, "A group of compile rule %d has neither regions, sub groups, nor refered another exisited group.", + tracking_compile_id); + } + } + + if (parent_type == PARENT_TYPE_COMPILE) { + ret = write_group2compile_line(group_info->group_id, parent_id, group_not_flag, clause_index, virtual_table, p_iris); + } else { + ret = write_group2group_line(group_info->group_id, parent_id, p_iris); + } + + if (ret < 0) { + fprintf(stderr, "%s rule %d write group error.", str_parent_type[parent_type], parent_id); + return -1; + } + + return 0; +} + +int write_compile_line(cJSON *compile, struct iris_description *p_iris) +{ + cJSON *item=cJSON_GetObjectItem(compile, "compile_id"); + if (item->type != cJSON_Number) { + fprintf(stderr, "compile_id format not number."); + return -1; + } + int compile_id = item->valueint; + + cJSON *group_array = cJSON_GetObjectItem(compile, "groups"); + int group_num = cJSON_GetArraySize(group_array); + int *clause_ids = ALLOC(int, group_num); + int clause_num = 0; + cJSON *group_obj = NULL; + + cJSON_ArrayForEach(group_obj, group_array) { + item = cJSON_GetObjectItem(group_obj, "clause_index"); + if (item) { + int i = 0; + int clause_index = item->valueint; + for (i = 0; i < clause_num; i++) { + if (clause_ids[i] == clause_index) { + break; + } + } + + if (i == clause_num) { + clause_ids[clause_num] = clause_index; + clause_num++; + } + } + } + FREE(clause_ids); + + if (clause_num == 0) { + clause_num = group_num; + } + + cJSON_AddNumberToObject(compile, "clause_num", clause_num); + + struct translate_command compile_cmd[MAX_COLUMN_NUM]; + memset(compile_cmd, 0, sizeof(compile_cmd)); + + int cmd_cnt = 0; + compile_cmd[cmd_cnt].json_string = "compile_id"; + compile_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + compile_cmd[cmd_cnt].json_string = "service"; + compile_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + compile_cmd[cmd_cnt].json_string = "action"; + compile_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + compile_cmd[cmd_cnt].json_string = "do_blacklist"; + compile_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + compile_cmd[cmd_cnt].json_string = "do_log"; + compile_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + + compile_cmd[cmd_cnt].json_string = "tags"; + compile_cmd[cmd_cnt].json_type = cJSON_String; + compile_cmd[cmd_cnt].empty_allowed = 1; + compile_cmd[cmd_cnt].default_string = "0"; + cmd_cnt++; + + compile_cmd[cmd_cnt].json_string = "user_region"; + compile_cmd[cmd_cnt].json_type = cJSON_String; + cmd_cnt++; + + compile_cmd[cmd_cnt].json_string = "is_valid"; + compile_cmd[cmd_cnt].json_type = cJSON_String; + compile_cmd[cmd_cnt].str2int_flag = 1; + cmd_cnt++; + + compile_cmd[cmd_cnt].json_string = "clause_num"; + compile_cmd[cmd_cnt].json_type = cJSON_Number; + cmd_cnt++; + + compile_cmd[cmd_cnt].json_string = "evaluation_order"; + compile_cmd[cmd_cnt].json_type = cJSON_String; + compile_cmd[cmd_cnt].empty_allowed = 1; + compile_cmd[cmd_cnt].default_string = "0.0"; + cmd_cnt++; + + struct iris_table *table_info = NULL; + item = cJSON_GetObjectItem(compile,"table_name"); + if (NULL == item || item->type != cJSON_String) { + table_info = p_iris->compile_table; + } else { + table_info = query_table_info(p_iris, item->valuestring, TABLE_TYPE_COMPILE); + } + + int ret = direct_write_rule(compile, p_iris->str2int_map, compile_cmd, cmd_cnt, table_info); + if (ret < 0) { + return -1; + } + + return compile_id; +} + +void write_table_idx(struct iris_description *p_iris, struct iris_table *table) +{ + char line_cnt_str[32] = {0}; + char err_str[256] = {0}; + + snprintf(line_cnt_str, sizeof(line_cnt_str), "%010d\n", table->line_count); + + size_t table_file_sz = strlen(line_cnt_str) + table->write_pos; + unsigned char *buff = ALLOC(unsigned char, table_file_sz); + memcpy(buff, line_cnt_str, strlen(line_cnt_str)); + memcpy(buff + strlen(line_cnt_str), table->buff, table->write_pos); + + FILE *table_fp = fopen(table->table_path, "w"); + if (p_iris->encrypt_key) { + unsigned char *encrypt_buff = NULL; + size_t encrypt_buff_sz = 0; + int ret = crypt_memory(buff, table_file_sz, &encrypt_buff, &encrypt_buff_sz, p_iris->encrypt_key, + p_iris->encrypt_algo, 1, err_str, sizeof(err_str)); + assert(ret == 0); + fwrite(encrypt_buff, encrypt_buff_sz, 1, table_fp); + fprintf(p_iris->idx_fp, "%s\t%d\t%s\t%s\n", table->table_name, table->line_count, table->table_path, p_iris->encrypt_algo); + FREE(encrypt_buff); + } else { + fwrite(buff, table_file_sz, 1, table_fp); + fprintf(p_iris->idx_fp, "%s\t%d\t%s\n", table->table_name, table->line_count, table->table_path); + } + + fclose(table_fp); + FREE(buff); +} + +int write_index_file(struct iris_description *p_iris) +{ + p_iris->idx_fp = fopen(p_iris->index_path, "w"); + if (NULL == p_iris->idx_fp) { + fprintf(stderr, "index file %s fopen error %s.",p_iris->index_path, strerror(errno)); + return -1; + } + + struct iris_table *node = NULL; + struct iris_table *tmp = NULL; + HASH_ITER(hh, p_iris->iris_table_map, node, tmp) { + write_table_idx(p_iris, node); + } + + fclose(p_iris->idx_fp); + p_iris->idx_fp = NULL; + + return 0; +} + +int write_iris(cJSON *json, struct iris_description *p_iris) +{ + int i=0; + int ret=0; + static struct group_info *parent_group = NULL; //TODO + + cJSON *plug_tables = cJSON_GetObjectItem(json, "plugin_table"); + if (plug_tables != NULL) { + cJSON *each_plug_table = NULL; + cJSON_ArrayForEach(each_plug_table, plug_tables) { + write_plugin_line(each_plug_table, i, p_iris); + i++; + } + } + + cJSON *group_array = cJSON_GetObjectItem(json, "groups");//sub-group to group + if (group_array != NULL) { + cJSON *group_obj = NULL; + cJSON_ArrayForEach(group_obj, group_array) { + const char *parent_group_name = NULL; + cJSON *item = cJSON_GetObjectItem(group_obj, "parent_group"); + if (NULL == item || item->type!=cJSON_String) { + parent_group_name = untitled_group_name; + } else { + parent_group_name = item->string; + } + + parent_group = group_info_read(p_iris->group_name_map, parent_group_name); + if(NULL == parent_group) { + parent_group = group_info_add_unsafe(p_iris, parent_group_name); + } + + ret = write_group_rule(group_obj, parent_group->group_id, PARENT_TYPE_GROUP, 0, 0, p_iris); + if (ret < 0) { + return -1; + } + } + } + + int compile_cnt = 0; + cJSON *compile_array = cJSON_GetObjectItem(json, "rules"); + if (compile_array != NULL) { + compile_cnt = cJSON_GetArraySize(compile_array); + } + + if (compile_cnt > 0) { + cJSON *compile_obj = NULL; + cJSON_ArrayForEach(compile_obj, compile_array) { + int compile_id = write_compile_line(compile_obj,p_iris); + if (compile_id < 0) { + fprintf(stderr, "In %d compile rule.", i); + return -1; + } + + group_array = cJSON_GetObjectItem(compile_obj, "groups"); + if (NULL == group_array) { + fprintf(stderr, "compile rule %d have no group.",compile_id); + return -1; + } + + int group_cnt = cJSON_GetArraySize(group_array); + if (group_cnt <= 0) { + fprintf(stderr, "compile rule %d have no groups.", compile_id); + return -1; + } + + i = 0; + cJSON *group_obj = NULL; + cJSON_ArrayForEach(group_obj, group_array) { + ret = write_group_rule(group_obj, compile_id, PARENT_TYPE_COMPILE, compile_id, i, p_iris); + if (ret < 0) { + return -1; + } + i++; + } + } + } + + ret = write_index_file(p_iris); + if (ret < 0) { + return -1; + } + return 0; +} + +int json2iris(const char *json_buff, const char *json_filename, const char *compile_tn, + const char *group2compile_tn, const char *group2group_tn, redisContext *redis_write_ctx, + char *iris_dir_buf, int buf_len, char *encrypt_key, char *encrypt_algo) +{ + int ret = -1; + cJSON *tmp_obj = NULL; + struct iris_description iris_cfg; + + memset(&iris_cfg, 0, sizeof(iris_cfg)); + + cJSON *json = cJSON_Parse(json_buff); + if (!json) { + fprintf(stderr, "Error before: %-200.200s", cJSON_GetErrorPtr()); + goto error_out; + } + + tmp_obj = cJSON_GetObjectItem(json, "compile_table"); + if (tmp_obj) { + compile_tn = tmp_obj->valuestring; + } + + tmp_obj = cJSON_GetObjectItem(json, "group2compile_table"); + if (tmp_obj) { + group2compile_tn = tmp_obj->valuestring; + } + + tmp_obj = cJSON_GetObjectItem(json, "group2group_table"); + if (tmp_obj) { + group2group_tn = tmp_obj->valuestring; + } + + ret = set_iris_descriptor(json_filename, json, encrypt_key, encrypt_algo, + compile_tn, group2compile_tn, group2group_tn, + redis_write_ctx, &iris_cfg); + if (ret < 0) { + goto error_out; + } + + ret = create_tmp_dir(&iris_cfg); + if (ret < 0) { + fprintf(stderr, "create tmp folder %s error", iris_cfg.tmp_iris_dir); + goto error_out; + } + + ret = write_iris(json, &iris_cfg); + if (ret < 0) { + goto error_out; + } + memcpy(iris_dir_buf, iris_cfg.tmp_iris_index_dir, MIN(strlen(iris_cfg.tmp_iris_index_dir) + 1, (unsigned int)buf_len)); + + cJSON_Delete(json); + clear_iris_descriptor(&iris_cfg); + return 0; + +error_out: + cJSON_Delete(json); + clear_iris_descriptor(&iris_cfg); + return -1; +} \ No newline at end of file diff --git a/src/maat_api.cpp b/src/maat_api.cpp index 544bebe..bb9a47f 100644 --- a/src/maat_api.cpp +++ b/src/maat_api.cpp @@ -12,13 +12,16 @@ #include #include "utils.h" +#include "json2iris.h" #include "maat/maat.h" #include "maat_rule.h" #include "maat_common.h" #include "maat_kv.h" +#include "maat_command.h" #include "maat_table_schema.h" #include "maat_table_runtime.h" #include "maat_config_monitor.h" +#include "maat_redis_monitor.h" struct maat_options* maat_options_new(void) { @@ -65,6 +68,7 @@ int maat_options_set_gc_timeout_ms(struct maat_options *opts, int interval_ms) int maat_options_set_deferred_load_on(struct maat_options *opts) { opts->deferred_load_on = 1; + return 0; } @@ -74,8 +78,9 @@ int maat_options_set_iris_full_dir(struct maat_options *opts, const char *full_d return -1; } - memcpy(opts->iris_full_dir, full_dir, strlen(full_dir)); + memcpy(opts->iris_ctx.full_dir, full_dir, strlen(full_dir)); opts->input_mode = DATA_SOURCE_IRIS_FILE; + return 0; } @@ -85,26 +90,91 @@ int maat_options_set_iris_inc_dir(struct maat_options *opts, const char *inc_dir return -1; } - memcpy(opts->iris_inc_dir, inc_dir, strlen(inc_dir)); + memcpy(opts->iris_ctx.inc_dir, inc_dir, strlen(inc_dir)); opts->input_mode = DATA_SOURCE_IRIS_FILE; + + return 0; +} + +int maat_options_set_json_file(struct maat_options *opts, const char *json_filename) +{ + strncpy(opts->json_ctx.json_file, json_filename, sizeof(opts->json_ctx.json_file)); + opts->input_mode = DATA_SOURCE_JSON_FILE; + + return 0; +} + +int maat_options_set_redis_ip(struct maat_options *opts, const char *redis_ip) +{ + memcpy(opts->redis_ctx.redis_ip, redis_ip, strlen(redis_ip)); + opts->input_mode = DATA_SOURCE_REDIS; + + return 0; +} + +int maat_options_set_redis_port(struct maat_options *opts, uint16_t redis_port) +{ + opts->redis_ctx.redis_port = redis_port; + + return 0; +} + +int maat_options_set_redis_db_index(struct maat_options *opts, int db_index) +{ + opts->redis_ctx.redis_db = db_index; + return 0; } void maat_read_full_config(struct maat *maat_instance) { + int ret = -1; + char err_str[NAME_MAX] = {0}; + struct source_redis_ctx *mr_ctx = NULL; + switch (maat_instance->input_mode) { + case DATA_SOURCE_REDIS: + mr_ctx = &(maat_instance->mr_ctx); + fprintf(stdout, "Maat initiate from Redis %s:%hu db%d.", + mr_ctx->redis_ip, mr_ctx->redis_port, mr_ctx->redis_db); + mr_ctx->read_ctx = maat_cmd_connect_redis(mr_ctx->redis_ip, mr_ctx->redis_port, mr_ctx->redis_db); + if (mr_ctx->read_ctx != NULL) { + redis_monitor_traverse(maat_instance->maat_version, mr_ctx, + maat_start_cb, maat_update_cb, maat_finish_cb, + maat_instance); + } + + if (NULL == maat_instance->creating_maat_rt) { + fprintf(stderr, "At initiation: NO effective rule in %s.", + maat_instance->iris_ctx.full_dir); + } + break; case DATA_SOURCE_IRIS_FILE: config_monitor_traverse(maat_instance->maat_version, maat_instance->iris_ctx.full_dir, - maat_start_cb, - maat_update_cb, - maat_finish_cb, + maat_start_cb, maat_update_cb, maat_finish_cb, maat_instance); if (NULL == maat_instance->creating_maat_rt) { fprintf(stderr, "At initiation: NO effective rule in %s.", maat_instance->iris_ctx.full_dir); } break; + case DATA_SOURCE_JSON_FILE: + ret = load_maat_json_file(maat_instance, maat_instance->json_ctx.json_file, err_str, sizeof(err_str)); + if (ret < 0) { + fprintf(stderr, "Maat re-initiate with JSON file %s failed: %s", maat_instance->json_ctx.json_file, err_str); + return; + } + + config_monitor_traverse(maat_instance->maat_version, + maat_instance->json_ctx.iris_file, + maat_start_cb, maat_update_cb, maat_finish_cb, + maat_instance); + if (NULL == maat_instance->creating_maat_rt) { + fprintf(stderr, "At initiation: NO effective rule in %s.", + maat_instance->json_ctx.iris_file); + } + break; default: break; } @@ -134,9 +204,17 @@ struct maat *maat_new(struct maat_options *opts, const char *table_info_path) maat_instance->input_mode = opts->input_mode; switch (maat_instance->input_mode) { + case DATA_SOURCE_REDIS: + memcpy(maat_instance->mr_ctx.redis_ip, opts->redis_ctx.redis_ip, strlen(opts->redis_ctx.redis_ip)); + maat_instance->mr_ctx.redis_port = opts->redis_ctx.redis_port; + maat_instance->mr_ctx.redis_db = opts->redis_ctx.redis_db; + break; case DATA_SOURCE_IRIS_FILE: - memcpy(maat_instance->iris_ctx.full_dir, opts->iris_full_dir, strlen(opts->iris_full_dir)); - memcpy(maat_instance->iris_ctx.inc_dir, opts->iris_inc_dir, strlen(opts->iris_inc_dir)); + memcpy(maat_instance->iris_ctx.full_dir, opts->iris_ctx.full_dir, strlen(opts->iris_ctx.full_dir)); + memcpy(maat_instance->iris_ctx.inc_dir, opts->iris_ctx.inc_dir, strlen(opts->iris_ctx.inc_dir)); + break; + case DATA_SOURCE_JSON_FILE: + memcpy(maat_instance->json_ctx.json_file, opts->json_ctx.json_file, strlen(opts->json_ctx.json_file)); break; default: fprintf(stderr, "data source unsupported:%d\n", maat_instance->input_mode); diff --git a/src/maat_command.cpp b/src/maat_command.cpp new file mode 100644 index 0000000..7e30b1b --- /dev/null +++ b/src/maat_command.cpp @@ -0,0 +1,348 @@ +/* +********************************************************************************************** +* File: maat_command.cpp +* Description: +* Authors: Liu WenTan +* Date: 2022-10-31 +* Copyright: (c) 2018-2022 Geedge Networks, Inc. All rights reserved. +*********************************************************************************************** +*/ + +#include +#include +#include +#include + +#include "utils.h" +#include "maat_utils.h" +#include "maat_command.h" +#include "maat_rule.h" +#include "hiredis/hiredis.h" +#include "maat_config_monitor.h" +#include "maat_table_schema.h" + +extern const char *foreign_source_prefix; +extern const char *mr_key_prefix; + +extern const char *mr_expire_lock; +extern const long mr_expire_lock_time; + +extern const char *mr_status_sset; +extern const char *mr_version_sset; +extern const char *mr_label_sset; + +redisReply *maat_cmd_wrap_redis_command(redisContext *c, const char *format, ...) +{ + va_list ap; + void *reply = NULL; + int ret = REDIS_ERR; + int retry = 0; + + while (reply == NULL && retry < 2 && ret != REDIS_OK) { + va_start(ap,format); + reply = redisvCommand(c,format,ap); + va_end(ap); + if (NULL == reply) { + ret = redisReconnect(c); + retry++; + } + } + + return (redisReply *)reply; +} + +redisContext *maat_cmd_connect_redis(const char *redis_ip, int redis_port, int redis_db) +{ + struct timeval connect_timeout; + connect_timeout.tv_sec = 0; + connect_timeout.tv_usec = 100 * 1000; // 100 ms + + redisContext *c = redisConnectWithTimeout(redis_ip, redis_port, connect_timeout); + if (NULL == c || c->err) { + fprintf(stderr, "Unable to connect redis server %s:%d db%d, error: %s", + redis_ip, redis_port, redis_db, c == NULL ? "Unknown" : c->errstr); + + if (c != NULL) { + redisFree(c); + } + return NULL; + } + + redisEnableKeepAlive(c); + redisReply *reply = maat_cmd_wrap_redis_command(c, "select %d", redis_db); + freeReplyObject(reply); + reply = NULL; + + return c; +} + +struct s_rule_array +{ + int cnt; + int size; + struct serial_rule *array; +}; + +void save_serial_rule(void *data, void *user) +{ + struct s_rule_array *array = (struct s_rule_array *)user; + int i = array->cnt; + memcpy(&(array->array[i]), data, sizeof(struct serial_rule)); + array->array[i].op = MAAT_OP_ADD; +} + +void maat_cmd_empty_serial_rule(struct serial_rule *s_rule) +{ + if (s_rule->table_line != NULL) { + FREE(s_rule->table_line); + } + + if (s_rule->n_foreign > 0) { + for (int i = 0; i < s_rule->n_foreign; i++) { + FREE(s_rule->f_keys[i].filename); + FREE(s_rule->f_keys[i].key); + } + + FREE(s_rule->f_keys); + } + + memset(s_rule, 0, sizeof(struct serial_rule)); +} + +int connect_redis_for_write(struct source_redis_ctx *mr_ctx) +{ + assert(mr_ctx->write_ctx == NULL); + mr_ctx->write_ctx = maat_cmd_connect_redis(mr_ctx->redis_ip, mr_ctx->redis_port, mr_ctx->redis_db); + if (NULL == mr_ctx->write_ctx) { + return -1; + } else { + return 0; + } +} + +redisContext *get_redis_ctx_for_write(struct maat *maat_instance) +{ + if (NULL == maat_instance->mr_ctx.write_ctx) { + int ret = connect_redis_for_write(&(maat_instance->mr_ctx)); + if(ret!=0) + { + return NULL; + } + } + return maat_instance->mr_ctx.write_ctx; +} + +void maat_cmd_set_serial_rule(struct serial_rule *rule, enum maat_operation op, unsigned long rule_id, + const char *table_name, const char *line, long long timeout) +{ + memset(rule, 0, sizeof(struct serial_rule)); + rule->op = op; + rule->rule_id = rule_id; + rule->timeout = timeout; + assert(strlen(table_name) < sizeof(rule->table_name)); + strncpy(rule->table_name, table_name, sizeof(rule->table_name)); + if (line != NULL) { + rule->table_line = maat_strdup(line); + } +} + +int maat_cmd_get_valid_flag_offset(const char *line, enum table_type table_type, int valid_column_seq) +{ + int column_seq = 0; + + switch (table_type) { + case TABLE_TYPE_EXPR: + column_seq = 7; + break; + case TABLE_TYPE_IP: + column_seq = 14; + break; + case TABLE_TYPE_IP_PLUS: + column_seq = 18; + break; + case TABLE_TYPE_COMPILE: + column_seq = 8; + break; + case TABLE_TYPE_PLUGIN: + case TABLE_TYPE_IP_PLUGIN: + case TABLE_TYPE_FQDN_PLUGIN: + case TABLE_TYPE_BOOL_PLUGIN: + if (valid_column_seq < 0) { + return -1; + } + + column_seq = valid_column_seq; + break; + case TABLE_TYPE_INTERVAL: + column_seq = 5; + break; + case TABLE_TYPE_INTERVAL_PLUS: + column_seq = 6; + break; + case TABLE_TYPE_DIGEST: + column_seq = 6; + break; + case TABLE_TYPE_SIMILARITY: + column_seq = 5; + break; + case TABLE_TYPE_EXPR_PLUS: + column_seq = 8; + break; + case TABLE_TYPE_GROUP2COMPILE: + case TABLE_TYPE_GROUP2GROUP: + column_seq = 3; + break; + default: + assert(0); + } + + size_t offset = 0; + size_t len = 0; + int ret = get_column_pos(line, column_seq, &offset, &len); + // 0 is also a valid value for some non-MAAT producer. + if (ret < 0 || offset >= strlen(line) || (line[offset] != '1' && line[offset] != '0')) { + return -1; + } + + return offset; +} + +long long maat_cmd_redis_server_time_s(redisContext *c) +{ + long long server_time = 0; + + redisReply *data_reply = maat_cmd_wrap_redis_command(c, "TIME"); + if (data_reply->type == REDIS_REPLY_ARRAY) { + server_time = atoll(data_reply->element[0]->str); + freeReplyObject(data_reply); + data_reply = NULL; + } + + return server_time; +} + +const char *maat_cmd_find_Nth_column(const char *line, int Nth, int *column_len) +{ + size_t i = 0; + int j = 0; + size_t start=0, end=0; + size_t line_len = strlen(line); + + for (i = 0; i < line_len; i++) { + if (line[i] != ' ' && line[i] != '\t') { + continue; + } + + j++; + if (j == Nth - 1) { + start = i + 1; + } + + if(j == Nth) { + end = i; + break; + } + } + + if (start == end) { + return NULL; + } + + if (end == 0) { + end = i; + } + + *column_len = end - start; + + return line + start; +} + +long long maat_cmd_read_redis_integer(const redisReply *reply) +{ + switch (reply->type) { + case REDIS_REPLY_INTEGER: + return reply->integer; + break; + case REDIS_REPLY_ARRAY: + assert(reply->element[0]->type == REDIS_REPLY_INTEGER); + return reply->element[0]->integer; + break; + case REDIS_REPLY_STRING: + return atoll(reply->str); + break; + default: + return -1; + break; + } + return 0; +} + +int maat_cmd_wrap_redis_get_reply(redisContext *c, redisReply **reply) +{ + return redisGetReply(c, (void **)reply); +} + +int maat_cmd_set_line(struct maat *maat_instance, const struct maat_cmd_line *line_rule) +{ + int i = 0; + int ret = 0; + long long absolute_expire_time = 0; + + redisContext *write_ctx = get_redis_ctx_for_write(maat_instance); + if (NULL == write_ctx) { + return -1; + } + + long long server_time = maat_cmd_redis_server_time_s(write_ctx); + if(!server_time) { + return -1; + } + + struct serial_rule *s_rule = ALLOC(struct serial_rule, 1); + + int table_id = table_schema_manager_get_table_id(maat_instance->table_schema_mgr, line_rule->table_name); + if (table_id < 0) { + fprintf(stderr, "Command set line id %d failed: unknown table %s.", line_rule->rule_id, line_rule->table_name); + FREE(s_rule); + return -1; + } + + struct table_schema *table_schema = table_schema_get(maat_instance->table_schema_mgr, table_id); + if (!table_schema) { + FREE(s_rule); + return -1; + } + + int valid_flag_column = table_schema_get_valid_flag_column(table_schema); + if (valid_flag_column < 0) { + fprintf(stderr, "Command set line id %d failed: table %s is not a plugin or ip_plugin table.", + line_rule->rule_id, line_rule->table_name); + FREE(s_rule); + return -1; + } + + enum table_type table_type = table_schema_get_table_type(table_schema); + int is_valid = maat_cmd_get_valid_flag_offset(line_rule->table_line, table_type, valid_flag_column); + + if (line_rule->expire_after > 0) { + absolute_expire_time = server_time + line_rule->expire_after; + } + + maat_cmd_set_serial_rule(s_rule + i, (enum maat_operation)is_valid, line_rule->rule_id, line_rule->table_name, + line_rule->table_line, absolute_expire_time); + + int success_cnt = maat_cmd_exec_serial_rule(write_ctx, s_rule, 1, server_time); + if (success_cnt != 1) { + ret = -1; + goto error_out; + } + + ret = success_cnt; + maat_instance->line_cmd_acc_num += success_cnt; + +error_out: + maat_cmd_empty_serial_rule(s_rule); + FREE(s_rule); + + return ret; +} \ No newline at end of file diff --git a/src/maat_config_monitor.cpp b/src/maat_config_monitor.cpp index 9a9ab5d..3e2575b 100644 --- a/src/maat_config_monitor.cpp +++ b/src/maat_config_monitor.cpp @@ -14,10 +14,13 @@ #include #include #include +#include -#include "maat_config_monitor.h" -#include "maat_utils.h" #include "utils.h" +#include "maat_utils.h" +#include "maat_rule.h" +#include "json2iris.h" +#include "maat_config_monitor.h" #define CM_MAX_TABLE_NUM 256 #define MAX_CONFIG_LINE (1024 * 16) @@ -345,4 +348,81 @@ void config_monitor_traverse(long long current_version, const char *idx_dir, } free(idx_path_array); +} + +int load_maat_json_file(struct maat *maat_instance, const char *json_filename, char *err_str, size_t err_str_sz) +{ + int ret = 0; + struct stat fstat_buf; + unsigned char *json_buff = NULL; + unsigned char *decrypted_buff = NULL; + unsigned char *uncompressed_buff = NULL; + size_t json_buff_sz = 0; + size_t decrypted_buff_sz = 0; + size_t uncompressed_buff_sz = 0; + + fprintf(stdout, "Maat initial with JSON file %s, formating..", json_filename); + + if (strlen(maat_instance->decrypt_key) && strlen(maat_instance->decrypt_algo)) { + ret = decrypt_open(json_filename, maat_instance->decrypt_key, maat_instance->decrypt_algo, + (unsigned char **)&decrypted_buff, &decrypted_buff_sz, err_str, err_str_sz); + if (ret < 0) { + fprintf(stderr, "Decrypt Maat JSON file %s failed.", json_filename); + return -1; + } + + json_buff=decrypted_buff; + json_buff_sz=decrypted_buff_sz; + } + + if (maat_instance->maat_json_is_gzipped) { + ret = gzip_uncompress(json_buff, json_buff_sz, &uncompressed_buff, &uncompressed_buff_sz); + free(json_buff); + if (ret < 0) { + fprintf(stderr, "Uncompress Maat JSON file %s failed.", json_filename); + return -1; + } + + json_buff = uncompressed_buff; + json_buff_sz = uncompressed_buff_sz; + } + + //decryption failed or no decryption + if (NULL == json_buff) { + ret = load_file_to_memory(json_filename, &json_buff, &json_buff_sz); + if (ret < 0) { + fprintf(stderr, "Read Maat JSON file %s failed.", json_filename); + return -1; + } + } + + ret = json2iris((const char*)json_buff, json_filename, + maat_instance->compile_tn, maat_instance->group2compile_tn, maat_instance->group2group_tn, + NULL, + maat_instance->json_ctx.iris_file, + sizeof(maat_instance->json_ctx.iris_file), + strlen(maat_instance->decrypt_key) ? maat_instance->decrypt_key : NULL, + strlen(maat_instance->decrypt_algo) ? maat_instance->decrypt_algo : NULL); + + free(json_buff); + json_buff = NULL; + if (ret < 0) { + return -1; + } + + if (!maat_instance->is_running) { + strncpy(maat_instance->json_ctx.json_file, json_filename, sizeof(maat_instance->json_ctx.json_file)); + } + + ret=stat(json_filename, &fstat_buf); + maat_instance->json_ctx.last_md5_time = fstat_buf.st_ctim; + + md5_file(maat_instance->json_ctx.json_file, maat_instance->json_ctx.effective_json_md5); + fprintf(stdout, "JSON file %s md5: %s, generate index file %s OK.", + maat_instance->json_ctx.json_file, + maat_instance->json_ctx.effective_json_md5, + maat_instance->json_ctx.iris_file); + maat_instance->input_mode = DATA_SOURCE_JSON_FILE; + + return 0; } \ No newline at end of file diff --git a/src/maat_redis_monitor.cpp b/src/maat_redis_monitor.cpp new file mode 100644 index 0000000..bdc2815 --- /dev/null +++ b/src/maat_redis_monitor.cpp @@ -0,0 +1,1403 @@ +/********************************************************************************************** +* File: maat_redis_monitor.cpp +* Description: +* Authors: Liu WenTan +* Date: 2022-11-29 +* Copyright: (c) 2018-2022 Geedge Networks, Inc. All rights reserved. +*********************************************************************************************** +*/ + +#include +#include +#include +#include + +#include "utils.h" +#include "maat_utils.h" +#include "maat_command.h" +#include "maat_config_monitor.h" +#include "maat_redis_monitor.h" +#include "maat_table_schema.h" + +const time_t MAAT_REDIS_RECONNECT_INTERVAL_S = 5; +const static int MAAT_REDIS_SYNC_TIME = 30 * 60; + +const char *mr_expire_lock = "EXPIRE_OP_LOCK"; +const long mr_expire_lock_timeout_ms = 300 * 1000; + +const char *mr_version_sset = "MAAT_VERSION_TIMER"; +const char *mr_status_sset = "MAAT_UPDATE_STATUS"; +const char *mr_expire_sset = "MAAT_EXPIRE_TIMER"; +const char *mr_label_sset = "MAAT_LABEL_INDEX"; + +const char *mr_key_prefix[2] = {"OBSOLETE_RULE", "EFFECTIVE_RULE"}; +const char *foreign_source_prefix = "redis://"; +const char *foreign_key_prefix = "__FILE_"; + +const char *mr_op_str[] = {"DEL", "ADD", "RENEW_TIMEOUT"}; + +char *get_foreign_cont_filename(const char *table_name, int rule_id, const char *foreign_key, const char *dir) +{ + char buffer[512] = {0}; + + snprintf(buffer, sizeof(buffer),"%s/%s-%d-%s", dir, table_name, rule_id, foreign_key); + char *filename = ALLOC(char, strlen(buffer) + 1); + memcpy(filename, buffer, strlen(buffer)); + + return filename; +} + +void _get_foregin_keys(struct serial_rule *p_rule, int *foreign_columns, int n_foreign, const char *dir) +{ + int foreign_key_size = 0; + p_rule->f_keys = ALLOC(struct foreign_key, n_foreign); + + for (int i = 0; i < n_foreign; i++) { + const char *p_foreign = maat_cmd_find_Nth_column(p_rule->table_line, foreign_columns[i], &foreign_key_size); + if (NULL == p_foreign) { + fprintf(stderr, "Get %s,%lu foreign keys failed: No %dth column.", + p_rule->table_name, p_rule->rule_id, foreign_columns[i]); + continue; + } + + //emtpy file + if (0 == strncasecmp(p_foreign, "null", strlen("null"))) { + continue; + } + + if (0 != strncmp(p_foreign, foreign_source_prefix, strlen(foreign_source_prefix))) { + fprintf(stderr, "Get %s,%lu foreign key failed: Invalid source prefix %s.", + p_rule->table_name, p_rule->rule_id, p_foreign); + continue; + } + + p_rule->f_keys[p_rule->n_foreign].column = foreign_columns[i]; + foreign_key_size = foreign_key_size - strlen(foreign_source_prefix); + p_foreign += strlen(foreign_source_prefix); + + if (0 != strncmp(p_foreign, foreign_key_prefix, strlen(foreign_key_prefix))) { + fprintf(stdout, "%s, %lu foreign key prefix %s is not recommended.", + p_rule->table_name, p_rule->rule_id, p_foreign); + } + + p_rule->f_keys[p_rule->n_foreign].key = ALLOC(char, foreign_key_size+1); + memcpy(p_rule->f_keys[p_rule->n_foreign].key, p_foreign, foreign_key_size); + p_rule->f_keys[p_rule->n_foreign].filename = get_foreign_cont_filename(p_rule->table_name, p_rule->rule_id, p_rule->f_keys[p_rule->n_foreign].key, dir); + p_rule->n_foreign++; + } + + if (0 == p_rule->n_foreign) { + FREE(p_rule->f_keys); + } +} + +int get_foreign_keys_define(redisContext *ctx, struct serial_rule *rule_list, int rule_num, struct maat *maat_instance, const char *dir) +{ + int rule_with_foreign_key = 0; + struct table_schema *table_schema = NULL; + + for (int i = 0; i < rule_num; i++) { + if (NULL == rule_list[i].table_line) { + continue; + } + + int table_id = table_schema_manager_get_table_id(maat_instance->table_schema_mgr, rule_list[i].table_name); + table_schema = table_schema_get(maat_instance->table_schema_mgr, table_id); + enum table_type table_type = table_schema_get_table_type(table_schema); + if (!table_schema || table_type != TABLE_TYPE_PLUGIN) { + continue; + } + + int foreign_columns[8]; + int n_foreign_column = plugin_table_schema_get_foreign_column(table_schema, foreign_columns); + if (0 == n_foreign_column) { + continue; + } + + _get_foregin_keys(rule_list+i, foreign_columns, n_foreign_column, dir); + rule_with_foreign_key++; + } + + return rule_with_foreign_key; +} + +int maat_cmd_get_foreign_keys_by_prefix(redisContext *ctx, struct serial_rule *rule_list, + int rule_num, const char* dir) +{ + int j = 0; + int foreign_key_size = 0; + int rule_with_foreign_key = 0; + const char *p_foreign = NULL; + + int n_foreign = 0; + int foreign_columns[MAX_FOREIGN_CLMN_NUM]; + + for (int i = 0; i < rule_num; i++) { + j = 1; + n_foreign = 0; + do { + p_foreign = maat_cmd_find_Nth_column(rule_list[i].table_line, j, &foreign_key_size); + if (p_foreign != NULL && foreign_key_size > (int)strlen(foreign_source_prefix) && + 0 == strncmp(p_foreign, foreign_source_prefix, strlen(foreign_source_prefix))) { + foreign_columns[n_foreign] = j; + n_foreign++; + } + j++; + } while (p_foreign != NULL && n_foreign < MAX_FOREIGN_CLMN_NUM); + + if (n_foreign > 0) { + _get_foregin_keys(rule_list+i, foreign_columns, n_foreign, dir); + rule_with_foreign_key++; + } + } + + return rule_with_foreign_key; +} + +struct foreign_conts_track +{ + int rule_idx; + int foreign_idx; +}; + +int _get_maat_redis_value(redisContext *c, struct serial_rule *rule_list, int rule_num) +{ + int i = 0; + int failed_cnt = 0; + UNUSED int ret = 0; + int error_happened = 0; + int *retry_ids = ALLOC(int, rule_num); + char redis_cmd[256] = {0}; + redisReply* reply = NULL; + + for (i = 0; i < rule_num; i++) { + snprintf(redis_cmd, sizeof(redis_cmd), "GET %s:%s,%lu", mr_key_prefix[rule_list[i].op], + rule_list[i].table_name, + rule_list[i].rule_id); + ret = redisAppendCommand(c, redis_cmd); + assert(ret == REDIS_OK); + } + + for (i = 0; i < rule_num; i++) { + ret = maat_cmd_wrap_redis_get_reply(c, &reply); + if (ret == REDIS_ERR) { + fprintf(stderr, "Redis GET %s:%s,%lu failed, redis server error.", mr_key_prefix[rule_list[i].op], + rule_list[i].table_name, rule_list[i].rule_id); + error_happened = 1; + break; + } + + if (reply->type == REDIS_REPLY_STRING) { + rule_list[i].table_line = maat_strdup(reply->str); + } else { + if (reply->type == REDIS_REPLY_NIL) { + retry_ids[failed_cnt] = i; + failed_cnt++; + } else { + fprintf(stderr, "Redis GET %s:%s,%lu failed",mr_key_prefix[rule_list[i].op], + rule_list[i].table_name, rule_list[i].rule_id); + error_happened = 1; + } + } + + freeReplyObject(reply); + reply = NULL; + } + + if (1 == error_happened) { + FREE(retry_ids); + return -1; + } + + int idx = 0; + for (i = 0; i < failed_cnt; i++) { + idx = retry_ids[i]; + snprintf(redis_cmd, sizeof(redis_cmd), "GET %s:%s,%lu", mr_key_prefix[MAAT_OP_DEL], + rule_list[idx].table_name, + rule_list[idx].rule_id); + ret = redisAppendCommand(c, redis_cmd); + } + + for (i = 0; i < failed_cnt; i++) { + idx = retry_ids[i]; + ret = maat_cmd_wrap_redis_get_reply(c, &reply); + if (ret == REDIS_ERR) { + fprintf(stderr, "redis command %s failed, redis server error.", redis_cmd); + free(retry_ids); + return -1; + } + + if (reply->type == REDIS_REPLY_STRING) { + rule_list[idx].table_line = maat_strdup(reply->str); + } else if(reply->type==REDIS_REPLY_ERROR) { + //Deal with Redis response: "Loading Redis is loading the database in memory" + fprintf(stderr, "redis command %s error, reply type=%d, error str=%s", redis_cmd, reply->type, reply->str); + } else { + //Handle type "nil" + fprintf(stderr, "redis command %s failed, reply type=%d", redis_cmd, reply->type); + } + + freeReplyObject(reply); + reply = NULL; + + } + + FREE(retry_ids); + return 0; +} + +int maat_cmd_get_redis_value(redisContext *c, struct serial_rule *rule_list, int rule_num, int print_process) +{ + int max_redis_batch = 4096; + int success_cnt = 0; + int next_print = 10; + + while (success_cnt < rule_num) { + int batch_cnt = MIN(rule_num-success_cnt, max_redis_batch); + int ret = _get_maat_redis_value(c, rule_list+success_cnt, batch_cnt); + if (ret < 0) { + return -1; + } else { + success_cnt += batch_cnt; + } + + if (print_process == 1) { + if ((success_cnt * 100) / rule_num > next_print) { + printf(" >%d%%",next_print); + next_print += 10; + } + } + } + + if (print_process == 1) { + printf(" >100%%\n"); + } + + return 0; +} + +int get_inc_key_list(long long instance_version, long long target_version, + redisContext *c, struct serial_rule **list) +{ + //Returns all the elements in the sorted set at key with a score that instance_version < score <= redis_version. + //The elements are considered to be ordered from low to high scores(instance_version). + redisReply *reply = (redisReply *)redisCommand(c, "ZRANGEBYSCORE %s (%lld %lld", mr_status_sset, + instance_version,target_version); + if (NULL == reply) { + fprintf(stderr, "GET %s failed with a NULL reply, error: %s.", mr_status_sset, c->errstr); + return -1; + } + + assert(reply->type == REDIS_REPLY_ARRAY); + + int rule_num = reply->elements; + if (0 == reply->elements) { + freeReplyObject(reply); + reply = NULL; + return 0; + } + + redisReply *tmp_reply= maat_cmd_wrap_redis_command(c, "ZSCORE %s %s", mr_status_sset, reply->element[0]->str); + if (tmp_reply->type != REDIS_REPLY_STRING) { + fprintf(stderr, "ZSCORE %s %s failed Version: %lld->%lld", mr_status_sset, + reply->element[0]->str, instance_version, target_version); + freeReplyObject(tmp_reply); + tmp_reply = NULL; + freeReplyObject(reply); + reply = NULL; + return -1; + } + + long long nearest_rule_version = maat_cmd_read_redis_integer(tmp_reply); + freeReplyObject(tmp_reply); + tmp_reply = NULL; + + if (nearest_rule_version < 0) { + return -1; + } + + if (nearest_rule_version != instance_version + 1) { + fprintf(stdout, "Noncontinuous VERSION Redis: %lld MAAT: %lld.", + nearest_rule_version, instance_version); + } + + int i = 0; + int j = 0; + char op_str[4] = {0}; + struct serial_rule *s_rule = ALLOC(struct serial_rule, reply->elements); + + for (i = 0, j = 0; i < reply->elements; i++) { + assert(reply->element[i]->type == REDIS_REPLY_STRING); + int ret = sscanf(reply->element[i]->str, "%[^,],%[^,],%lu", op_str, + s_rule[j].table_name, &(s_rule[j].rule_id)); + if (ret != 3 || s_rule[i].rule_id < 0) { + fprintf(stderr, "Invalid Redis Key: %s", reply->element[i]->str); + continue; + } + + if (strncmp(op_str, "ADD", strlen("ADD")) == 0) { + s_rule[j].op = MAAT_OP_ADD; + } else if(strncmp(op_str, "DEL", strlen("DEL")) == 0) { + s_rule[j].op = MAAT_OP_DEL; + } else { + fprintf(stderr, "Invalid Redis Key: %s", reply->element[i]->str); + continue; + } + j++; + } + + rule_num = j; + *list = s_rule; + freeReplyObject(reply); + reply = NULL; + + return rule_num; +} + +void serial_rule_free(struct serial_rule *s_rule) +{ + if (s_rule->table_line != NULL) { + FREE(s_rule->table_line); + } + + if (s_rule->n_foreign > 0) { + for (int i = 0; i < s_rule->n_foreign; i++) { + FREE(s_rule->f_keys[i].filename); + FREE(s_rule->f_keys[i].key); + } + FREE(s_rule->f_keys); + } + + FREE(s_rule); +} + +struct serial_rule *serial_rule_clone(const struct serial_rule *s_rule) +{ + struct serial_rule *new_rule = ALLOC(struct serial_rule, 1); + + new_rule->op = s_rule->op; + new_rule->rule_id = s_rule->rule_id; + new_rule->label_id = s_rule->label_id; + new_rule->timeout = s_rule->timeout; + memcpy(new_rule->table_name, s_rule->table_name, strlen(s_rule->table_name)); + new_rule->n_foreign = s_rule->n_foreign; + new_rule->table_line = ALLOC(char, strlen(s_rule->table_line)); + memcpy(new_rule->table_line, s_rule->table_line, strlen(s_rule->table_line)); + + new_rule->f_keys = ALLOC(struct foreign_key, new_rule->n_foreign); + for (int j = 0; j < new_rule->n_foreign; j++) { + new_rule->f_keys[j].key = ALLOC(char, s_rule->f_keys[j].key_len); + memcpy(new_rule->f_keys[j].key, s_rule->f_keys[j].key, s_rule->f_keys[j].key_len); + new_rule->f_keys[j].filename = ALLOC(char, strlen(s_rule->f_keys[j].filename)); + memcpy(new_rule->f_keys[j].filename, s_rule->f_keys[j].filename, strlen(s_rule->f_keys[j].filename)); + } + + return new_rule; +} + +int recovery_history_version(const struct serial_rule *current, int current_num, + const struct serial_rule *changed, int changed_num, + struct serial_rule **history_result) +{ + int i = 0; + int ret = 0; + unsigned int history_num = 0; + int hash_slot_size = 1; + char hkey[256+20] = {0}; + int tmp = current_num + changed_num; + for (; tmp > 0; tmp = tmp/2) { + hash_slot_size *= 2; + } + + struct serial_rule *s_rule_map = NULL; + struct serial_rule *rule_node = NULL; + for (i = 0; i < current_num; i++) { + snprintf(hkey, sizeof(hkey), "%ld,%s", current[i].rule_id, current[i].table_name); + rule_node = serial_rule_clone(current + i); + HASH_ADD_KEYPTR(hh, s_rule_map, hkey, strlen(hkey), rule_node); + } + + for (i = changed_num - 1; i >= 0; i--) { + snprintf(hkey, sizeof(hkey), "%ld,%s", changed[i].rule_id, changed[i].table_name); + //newly added rule is need to delete from current, so that history version can be recovered. + if (changed[i].op == MAAT_OP_ADD) { + rule_node = NULL; + HASH_FIND(hh, s_rule_map, hkey, strlen(hkey), rule_node); + if (rule_node != NULL) { + HASH_DELETE(hh, s_rule_map, rule_node); + } + serial_rule_free(rule_node); + } else { + rule_node = serial_rule_clone(changed + i); + HASH_ADD_KEYPTR(hh, s_rule_map, hkey, strlen(hkey), rule_node); + } + } + + history_num = HASH_CNT(hh, s_rule_map); + struct serial_rule *array = ALLOC(struct serial_rule, history_num); + struct serial_rule *elem_node = NULL; + struct serial_rule *tmp_node = NULL; + i = 0; + HASH_ITER(hh, s_rule_map, elem_node, tmp_node) { + memcpy(&array[i], elem_node, sizeof(struct serial_rule)); + array[i].op = MAAT_OP_ADD; + i++; + } + elem_node = NULL; + tmp_node = NULL; + + *history_result = array; + ret = history_num; + + HASH_ITER(hh, s_rule_map, elem_node, tmp_node) { + HASH_DELETE(hh, s_rule_map, elem_node); + serial_rule_free(elem_node); + } + + return ret; +} + +int maat_cmd_get_rm_key_list(redisContext *c, long long instance_version, long long desired_version, + long long *new_version, struct table_schema_manager* table_schema_mgr, + struct serial_rule **list, int *update_type, int cumulative_off) +{ + int rule_num = 0; + long long target_version = 0; + struct serial_rule *s_rule_array = NULL; + + redisReply *reply = (redisReply *)redisCommand(c, "GET MAAT_VERSION"); + if (reply != NULL) { + if (reply->type == REDIS_REPLY_NIL || reply->type == REDIS_REPLY_ERROR) { + fprintf(stderr, "GET MAAT_VERSION failed, maybe Redis is busy."); + freeReplyObject(reply); + reply = NULL; + return -1; + } + } else { + fprintf(stderr, "GET MAAT_VERSION failed with NULL reply, error: %s.", c->errstr); + return -1; + } + + long long redis_version = maat_cmd_read_redis_integer(reply); + if (redis_version < 0) { + if (reply->type == REDIS_REPLY_ERROR) { + fprintf(stderr, "Redis Communication error: %s.", reply->str); + } + return -1; + } + + freeReplyObject(reply); + reply = NULL; + + if (redis_version == instance_version) { + return 0; + } + + if (0 == instance_version || desired_version != 0) { + goto FULL_UPDATE; + } + + if (redis_version < instance_version) { + fprintf(stderr, "VERSION roll back MAAT: %lld -> Redis: %lld.", instance_version, redis_version); + goto FULL_UPDATE; + } + + if (redis_version > instance_version && 1 == cumulative_off) { + target_version = instance_version; + } else { + target_version = redis_version - 1; + } + + do { + target_version++; + rule_num = get_inc_key_list(instance_version, target_version, c, &s_rule_array); + if (rule_num > 0) { + break; + } else if (rule_num < 0) { + goto FULL_UPDATE; + } else { + //ret=0, nothing to do. + } + + } while (0 == rule_num && target_version <= redis_version && 1 == cumulative_off); + + if (0 == rule_num) { + fprintf(stdout, "Got nothing after ZRANGEBYSCORE %s (%lld %lld, cumulative %s", mr_status_sset, + instance_version, target_version-1, cumulative_off == 1 ? "OFF" : "ON"); + return 0; + } + + fprintf(stdout, "Inc Update from instance_version %lld to %lld (%d entries).", + instance_version, target_version, rule_num); + + *list = s_rule_array; + *update_type = CM_UPDATE_TYPE_INC; + *new_version = target_version; + return rule_num; + +FULL_UPDATE: + fprintf(stdout, "Initiate full udpate from instance_version %lld to %lld.", instance_version, + desired_version == 0 ? redis_version : desired_version); + size_t append_cmd_cnt = 0; + int ret = redisAppendCommand(c, "MULTI"); + append_cmd_cnt++; + + ret = redisAppendCommand(c, "GET MAAT_VERSION"); + append_cmd_cnt++; + + ret = redisAppendCommand(c, "KEYS EFFECTIVE_RULE:*"); + append_cmd_cnt++; + + size_t i = 0; + //consume reply "OK" and "QUEUED". + for (i = 0; i < append_cmd_cnt; i++) { + maat_cmd_wrap_redis_get_reply(c, &reply); + freeReplyObject(reply); + reply = NULL; + } + + reply = maat_cmd_wrap_redis_command(c, "EXEC"); + if (NULL == reply) { + fprintf(stderr, "Redis Communication error: %s.", c->errstr); + return -1; + } + + if (reply->type != REDIS_REPLY_ARRAY) { + fprintf(stderr, "Invalid Redis Key List type %d", reply->type); + freeReplyObject(reply); + reply = NULL; + return -1; + } + + *new_version = maat_cmd_read_redis_integer(reply->element[0]); + redisReply *sub_reply = reply->element[1]; + if (sub_reply->type != REDIS_REPLY_ARRAY) { + fprintf(stderr, "Invalid Redis Key List type %d", sub_reply->type); + freeReplyObject(reply); + reply = NULL; + return -1; + } + + size_t full_idx = 0; + s_rule_array = ALLOC(struct serial_rule, sub_reply->elements); + for (i = 0, full_idx = 0; i < sub_reply->elements; i++) { + if (sub_reply->element[i]->type != REDIS_REPLY_STRING) { + fprintf(stderr, "Invalid Redis Key Type: %d", sub_reply->element[i]->type); + continue; + } + + ret = sscanf(sub_reply->element[i]->str, "%*[^:]:%[^,],%ld", + s_rule_array[full_idx].table_name, + &(s_rule_array[full_idx].rule_id)); + s_rule_array[full_idx].op = MAAT_OP_ADD; + + if (ret != 2 || s_rule_array[full_idx].rule_id < 0 || strlen(s_rule_array[full_idx].table_name) == 0) { + fprintf(stderr, "Invalid Redis Key Format: %s", sub_reply->element[i]->str); + continue; + } + + if (table_schema_mgr) { + int table_id = table_schema_manager_get_table_id(table_schema_mgr, s_rule_array[full_idx].table_name); + //Unrecognized table. + if (table_id < 0) { + continue; + } + } + full_idx++; + } + + rule_num = full_idx; + freeReplyObject(reply); + reply = NULL; + + if (desired_version != 0) { + struct serial_rule *changed_rule_array = NULL; + int changed_rule_num = get_inc_key_list(desired_version, redis_version, c, &changed_rule_array); + if (changed_rule_num < 0) { + fprintf(stderr, "Recover history version %lld faild where as redis version is %lld.", desired_version, redis_version); + } else if(0 == changed_rule_num) { + fprintf(stderr, "Nothing to recover from history version %lld to redis version is %lld.", desired_version, redis_version); + } else { + struct serial_rule *history_rule_array = NULL; + ret = recovery_history_version(s_rule_array, full_idx, changed_rule_array, changed_rule_num, &history_rule_array); + if (ret > 0) { + FREE(s_rule_array); + s_rule_array = history_rule_array; + rule_num = ret; + *new_version = desired_version; + fprintf(stdout, "Successfully recovered from history version %lld to redis version is %lld.", + desired_version, redis_version); + } + } + FREE(changed_rule_array); + } + + *list = s_rule_array; + *update_type = CM_UPDATE_TYPE_FULL; + fprintf(stdout, "Full update %d keys of version %lld.", rule_num, *new_version); + + return rule_num ; +} + +void _get_foreign_conts(redisContext *c, struct serial_rule *rule_list, int rule_num, int print_fn) +{ + int i = 0; + int j = 0; + UNUSED int ret = 0; + int key_num = 0; + struct serial_rule *s_rule = NULL; + struct foreign_conts_track *track = ALLOC(struct foreign_conts_track, rule_num * MAX_FOREIGN_CLMN_NUM); + + for (i = 0; i < rule_num; i++) { + s_rule = rule_list + i; + if (s_rule->n_foreign == 0) { + continue; + } + + if (s_rule->op == MAAT_OP_DEL) { + for (j = 0; j < rule_list[i].n_foreign; j++) { + if (NULL == rule_list[i].f_keys[j].filename) { + continue; + } + + ret = remove(rule_list[i].f_keys[j].filename); + if (ret == -1) { + fprintf(stderr, "Foreign content file %s remove failed.", + rule_list[i].f_keys[j].filename); + } + } + } else { + for (j = 0; j < s_rule->n_foreign; j++) { + if (NULL == rule_list[i].f_keys[j].filename) { + continue; + } + + struct stat file_info; + ret = stat(s_rule->f_keys[j].filename, &file_info); + if (0 == ret) { + continue; + } + + char redis_cmd[256] = {0}; + snprintf(redis_cmd, sizeof(redis_cmd), "GET %s", s_rule->f_keys[j].key); + ret = redisAppendCommand(c, redis_cmd); + track[key_num].rule_idx = i; + track[key_num].foreign_idx = j; + key_num++; + assert(ret == REDIS_OK); + } + } + } + + redisReply *reply = NULL; + for (i = 0; i < key_num; i++) { + ret = maat_cmd_wrap_redis_get_reply(c, &reply); + if (ret == REDIS_ERR) { + fprintf(stderr, "Get %s,%lu foreign key %s content failed, redis server error.", + rule_list[track[i].rule_idx].table_name, + rule_list[track[i].rule_idx].rule_id, + rule_list[track[i].rule_idx].f_keys[track[i].foreign_idx].key); + break; + } + + if (reply->type != REDIS_REPLY_STRING) { + fprintf(stderr, "Get %s,%lu foreign key %s content failed.", + rule_list[track[i].rule_idx].table_name, + rule_list[track[i].rule_idx].rule_id, + rule_list[track[i].rule_idx].f_keys[track[i].foreign_idx].key); + continue; + } else { + s_rule = rule_list+track[i].rule_idx; + FILE *fp = fopen(s_rule->f_keys[track[i].foreign_idx].filename, "w"); + if (NULL == fp) { + fprintf(stderr, "Write foreign content failed: fopen %s error.", + s_rule->f_keys[track[i].foreign_idx].filename); + } else { + fwrite(reply->str, 1, reply->len, fp); + fclose(fp); + fp = NULL; + if (1 == print_fn) { + printf("Written foreign content %s\n", s_rule->f_keys[track[i].foreign_idx].filename); + } + } + } + + freeReplyObject(reply); + reply = NULL; + } + + FREE(track); + return; +} + +void maat_cmd_get_foreign_conts(redisContext *c, struct serial_rule *rule_list, int rule_num, int print_fn) +{ + int max_redis_batch = 4096; + int success_cnt = 0; + + while (success_cnt < rule_num) { + int batch_cnt = MIN(rule_num - success_cnt, max_redis_batch); + _get_foreign_conts(c, rule_list + success_cnt, batch_cnt, print_fn); + success_cnt += batch_cnt; + } +} + +int invalidate_line(char *line, enum table_type table_type, int valid_column_seq) +{ + int i = maat_cmd_get_valid_flag_offset(line, table_type, valid_column_seq); + if (i < 0) { + return -1; + } + + line[i] = '0'; + + return 0; +} + +void maat_cmd_rewrite_table_line_with_foreign(struct serial_rule *s_rule) +{ + int i = 0; + size_t fn_size = 0; + + for (i = 0; i < s_rule->n_foreign; i++) { + fn_size += strlen(s_rule->f_keys[i].filename); + } + + char *rewrite_line = ALLOC(char, strlen(s_rule->table_line) + fn_size); + char *pos_rewrite_line = rewrite_line; + const char *pos_origin_line = s_rule->table_line; + + for (i = 0; i < s_rule->n_foreign; i++) { + int origin_column_size = 0; + const char *origin_column = maat_cmd_find_Nth_column(s_rule->table_line, + s_rule->f_keys[i].column, + &origin_column_size); + strncat(pos_rewrite_line, pos_origin_line, origin_column - pos_origin_line); + pos_rewrite_line += origin_column - pos_origin_line; + pos_origin_line = origin_column+origin_column_size; + strncat(pos_rewrite_line, s_rule->f_keys[i].filename, strlen(s_rule->f_keys[i].filename)); + pos_rewrite_line += strlen(s_rule->f_keys[i].filename); + } + + strncat(pos_rewrite_line, pos_origin_line, strlen(s_rule->table_line) - (pos_origin_line - s_rule->table_line)); + FREE(s_rule->table_line); + s_rule->table_line = rewrite_line; +} + +void expected_reply_add(struct expected_reply* expected, int s_rule_seq, int type, long long integer) +{ + int i = expected->possible_reply_num; + assert(i < POSSIBLE_REDIS_REPLY_SIZE); + expected->s_rule_seq = s_rule_seq; + expected->possible_replies[i].type = type; + expected->possible_replies[i].integer = integer; + expected->possible_reply_num++; +} + +int redlock_try_lock(redisContext *c, const char *lock_name, long long expire) +{ + int ret = 0; + + redisReply *reply = maat_cmd_wrap_redis_command(c, "SET %s locked NX PX %lld", lock_name, expire); + if (reply->type == REDIS_REPLY_NIL) { + ret = 0; + } else { + ret = 1; + } + + freeReplyObject(reply); + reply = NULL; + + return ret; +} + +long long _exec_serial_rule_begin(redisContext* c, size_t rule_num, size_t renew_rule_num, + int *renew_allowed, long long *transaction_version) +{ + int ret = -1; + redisReply *data_reply = NULL; + + if (renew_rule_num > 0) { + while (0 == redlock_try_lock(c, mr_expire_lock, mr_expire_lock_timeout_ms)) { + usleep(1000); + } + *renew_allowed = 1; + } + + if (rule_num > renew_rule_num) { + data_reply = maat_cmd_wrap_redis_command(c, "INCRBY MAAT_PRE_VER 1"); + *transaction_version = maat_cmd_read_redis_integer(data_reply); + freeReplyObject(data_reply); + data_reply = NULL; + if (*transaction_version < 0) { + return -1; + } + } + + if (*renew_allowed == 1 || rule_num > renew_rule_num) { + data_reply = maat_cmd_wrap_redis_command(c, "MULTI"); + freeReplyObject(data_reply); + data_reply = NULL; + ret = 0; + } + + return ret; +} + +void redlock_unlock(redisContext *c, const char *lock_name) +{ + redisReply *reply = maat_cmd_wrap_redis_command(c, "DEL %s", lock_name); + freeReplyObject(reply); + reply = NULL; +} + +const char* lua_exec_done= +"local maat_version=redis.call(\'incrby\', KEYS[1], 1);" +"local transaction=redis.call(\'lrange\', KEYS[4], 0, -1);" +"for k,v in pairs(transaction) do" +" redis.call(\'zadd\', KEYS[2], maat_version, v);" +"end;" +"redis.call(\'del\', KEYS[4]);" +"redis.call(\'zadd\', KEYS[3], ARGV[1], maat_version);" +"return maat_version;"; +redisReply* _exec_serial_rule_end(redisContext *c, const char *transaction_list, long long server_time, + int renew_allowed, struct expected_reply *expect_reply, size_t *cnt) +{ + redisReply *data_reply = NULL; + + if (1 == renew_allowed) { + redlock_unlock(c, mr_expire_lock); + expect_reply[*cnt].s_rule_seq = -1; + (*cnt)++; + } + + if (strlen(transaction_list) > 0) { + data_reply = maat_cmd_wrap_redis_command(c, "eval %s 4 MAAT_VERSION %s %s %s %lld", + lua_exec_done, + mr_status_sset, + mr_version_sset, + transaction_list, + server_time); + freeReplyObject(data_reply); + data_reply = NULL; + expected_reply_add(expect_reply + *cnt, -1, REDIS_REPLY_INTEGER, 0); + (*cnt)++; + } + + data_reply = maat_cmd_wrap_redis_command(c, "EXEC"); + + return data_reply; +} + +void _exec_serial_rule(redisContext *c, const char *transaction_list, struct serial_rule *s_rule, size_t rule_num, + struct expected_reply *expect_reply, size_t *cnt, size_t offset, int renew_allowed) +{ + size_t i = 0; + size_t append_cmd_cnt = 0; + redisReply *data_reply = NULL; + + for (i = 0; i < rule_num; i++) { + switch (s_rule[i].op) { + case MAAT_OP_ADD: + redisAppendCommand(c, "SET %s:%s,%lu %s", + mr_key_prefix[MAAT_OP_ADD], + s_rule[i].table_name, + s_rule[i].rule_id, + s_rule[i].table_line); + expected_reply_add(expect_reply+*cnt, i+offset, REDIS_REPLY_STATUS, 0); + (*cnt)++; + append_cmd_cnt++; + //Allowing add duplicated members for rule id recycling. + redisAppendCommand(c, "RPUSH %s ADD,%s,%lu", + transaction_list, + s_rule[i].table_name, + s_rule[i].rule_id); + expected_reply_add(expect_reply+*cnt, -1, REDIS_REPLY_INTEGER, 0); + (*cnt)++; + append_cmd_cnt++; + if (s_rule[i].timeout > 0) { + redisAppendCommand(c, "ZADD %s %lld %s,%lu", + mr_expire_sset, + s_rule[i].timeout, + s_rule[i].table_name, + s_rule[i].rule_id); + expected_reply_add(expect_reply+*cnt, i+offset, REDIS_REPLY_INTEGER, 1); + expected_reply_add(expect_reply+*cnt, i+offset, REDIS_REPLY_INTEGER, 0); + (*cnt)++; + append_cmd_cnt++; + } + + if (s_rule[i].label_id > 0) + { + redisAppendCommand(c, "ZADD %s %d %s,%lu", + mr_label_sset, + s_rule[i].label_id, + s_rule[i].table_name, + s_rule[i].rule_id); + expected_reply_add(expect_reply+*cnt, i+offset, REDIS_REPLY_INTEGER, 1); + expected_reply_add(expect_reply+*cnt, i+offset, REDIS_REPLY_INTEGER, 0); + + (*cnt)++; + + append_cmd_cnt++; + } + break; + case MAAT_OP_DEL: + redisAppendCommand(c, "RENAME %s:%s,%lu %s:%s,%lu", + mr_key_prefix[MAAT_OP_ADD], + s_rule[i].table_name, + s_rule[i].rule_id, + mr_key_prefix[MAAT_OP_DEL], + s_rule[i].table_name, + s_rule[i].rule_id); + expected_reply_add(expect_reply+*cnt, i+offset, REDIS_REPLY_STATUS, 0); + (*cnt)++; + append_cmd_cnt++; + + redisAppendCommand(c, "EXPIRE %s:%s,%lu %d", + mr_key_prefix[MAAT_OP_DEL], + s_rule[i].table_name, + s_rule[i].rule_id, + MAAT_REDIS_SYNC_TIME); + expected_reply_add(expect_reply+*cnt, i+offset, REDIS_REPLY_INTEGER, 1); + (*cnt)++; + append_cmd_cnt++; + + //NX: Don't update already exisiting elements. Always add new elements. + redisAppendCommand(c, "RPUSH %s DEL,%s,%lu", + transaction_list, + s_rule[i].table_name, + s_rule[i].rule_id); + expected_reply_add(expect_reply+*cnt, -1, REDIS_REPLY_INTEGER, 0); + (*cnt)++; + append_cmd_cnt++; + + // Try to remove from expiration sorted set, no matter wheather it exists or not. + redisAppendCommand(c, "ZREM %s %s,%lu", + mr_expire_sset, + s_rule[i].table_name, + s_rule[i].rule_id); + expected_reply_add(expect_reply+*cnt, -1, REDIS_REPLY_INTEGER, 0); + (*cnt)++; + append_cmd_cnt++; + + // Try to remove from label sorted set, no matter wheather it exists or not. + redisAppendCommand(c, "ZREM %s %s,%lu", + mr_label_sset, + s_rule[i].table_name, + s_rule[i].rule_id); + expected_reply_add(expect_reply+*cnt, -1, REDIS_REPLY_INTEGER, 0); + (*cnt)++; + append_cmd_cnt++; + break; + case MAAT_OP_RENEW_TIMEOUT: + if (renew_allowed != 1) { + continue; + } + //s_rule[i].timeout>0 was checked by caller. + redisAppendCommand(c, "ZADD %s %lld %s,%lu", + mr_expire_sset, + s_rule[i].timeout, + s_rule[i].table_name, + s_rule[i].rule_id); + expected_reply_add(expect_reply+*cnt, -1, REDIS_REPLY_INTEGER, 0); + (*cnt)++; + append_cmd_cnt++; + + break; + default: + assert(0); + break; + } + } + + for (i = 0; i < append_cmd_cnt; i++) { + maat_cmd_wrap_redis_get_reply(c, &data_reply); + freeReplyObject(data_reply); + data_reply=NULL; + } +} + +int mr_transaction_success(redisReply *data_reply) +{ + if (data_reply->type == REDIS_REPLY_NIL) { + return 0; + } else { + return 1; + } +} + +int mr_operation_success(redisReply *actual_reply, struct expected_reply *expected) +{ + if (expected->possible_replies[0].type != actual_reply->type) { + return 0; + } + + for (int i = 0; i < expected->possible_reply_num; i++) { + if (expected->possible_replies[i].type == REDIS_REPLY_INTEGER && + expected->possible_replies[i].type == actual_reply->type && + expected->possible_replies[i].integer == actual_reply->integer) { + return 1; + } + + if (expected->possible_replies[i].type == REDIS_REPLY_STATUS && + expected->possible_replies[i].type == actual_reply->type && + 0 == strcasecmp(actual_reply->str, "OK")) { + return 1; + } + } + + return 0; +} + +int maat_cmd_exec_serial_rule(redisContext *c, struct serial_rule *s_rule, size_t serial_rule_num, long long server_time) +{ + size_t i = 0; + size_t rule_seq = 0; + size_t multi_cmd_cnt = 0; + size_t success_cnt = 0; + size_t renew_num = 0; + size_t max_redis_batch = 1024; + int renew_allowed = 0; + int last_failed = -1; + redisReply *p = NULL; + redisReply *transaction_reply = NULL; + const int MAX_REDIS_OP_PER_SRULE = 8; + char transaction_list[NAME_MAX * 2] = {0}; + long long transaction_version = 0; + long long transaction_finished_version = 0; + size_t max_multi_cmd_num = MAX_REDIS_OP_PER_SRULE * serial_rule_num + 2;// 2 for operation in _exec_serial_rule_end() + struct expected_reply *expected_reply = ALLOC(struct expected_reply, max_multi_cmd_num); + + for (i = 0; i < serial_rule_num; i++) { + if (s_rule[i].op == MAAT_OP_RENEW_TIMEOUT) { + renew_num++; + } + } + + int ret = _exec_serial_rule_begin(c, serial_rule_num, renew_num, &renew_allowed, &transaction_version); + //Preconditions for transaction are not satisfied. + if (ret != 0) { + success_cnt = -1; + goto error_out; + } + + if (transaction_version > 0) { + snprintf(transaction_list, sizeof(transaction_list), "MAAT_TRANSACTION_%lld", transaction_version); + } + + while (success_cnt < serial_rule_num) { + size_t batch_cnt = MIN(serial_rule_num - success_cnt, max_redis_batch); + _exec_serial_rule(c, transaction_list, s_rule + success_cnt, batch_cnt, expected_reply, &multi_cmd_cnt, + success_cnt, renew_allowed); + assert(multi_cmd_cntelements == multi_cmd_cnt); + for (i = 0; i < multi_cmd_cnt; i++) { + p = transaction_reply->element[i]; + //failed is acceptable + //or transaciton is success + //or continuation of last failed + if (expected_reply[i].s_rule_seq == -1 || 1 == mr_operation_success(p, expected_reply+i) || last_failed == expected_reply[i].s_rule_seq) { + continue; + } + rule_seq = expected_reply[i].s_rule_seq; + fprintf(stderr, "%s %s %lu failed, rule id maybe conflict or not exist.", + mr_op_str[s_rule[rule_seq].op], s_rule[rule_seq].table_name, + s_rule[rule_seq].rule_id); + success_cnt--; + last_failed = rule_seq; + } + } else { + success_cnt = -1; + } + + if (transaction_version > 0) { + transaction_finished_version = maat_cmd_read_redis_integer(transaction_reply->element[multi_cmd_cnt-1]); + fprintf(stdout, "Redis transaction MAAT_PRE_VER = %lld , MAAT_VERSION = %lld ", + transaction_version, transaction_finished_version); + } + + freeReplyObject(transaction_reply); + transaction_reply = NULL; + +error_out: + if (renew_num > 0 && renew_allowed != 1) { + for (i = 0; i < (unsigned int)serial_rule_num; i++) { + if (s_rule[i].op == MAAT_OP_RENEW_TIMEOUT) { + fprintf(stdout, "%s %s %lu is not allowed due to lock contention.", + mr_op_str[MAAT_OP_RENEW_TIMEOUT], s_rule[i].table_name, + s_rule[i].rule_id); + } + } + + if (success_cnt > 0) { + success_cnt -= renew_num; + } + } + + FREE(expected_reply); + + return success_cnt; +} + +void cleanup_update_status(redisContext *c) +{ + long long version_upper_bound = 0; + long long version_lower_bound = 0; + long long version_num = 0; + long long entry_num = 0; + + long long server_time = maat_cmd_redis_server_time_s(c); + if (!server_time) { + return; + } + + redisReply *reply = maat_cmd_wrap_redis_command(c, "MULTI"); + freeReplyObject(reply); + reply = NULL; + + int append_cmd_cnt = 0; + redisAppendCommand(c, "ZRANGEBYSCORE %s -inf %lld", + mr_version_sset, server_time - MAAT_REDIS_SYNC_TIME); + append_cmd_cnt++; + + redisAppendCommand(c, "ZREMRANGEBYSCORE %s -inf %lld", + mr_version_sset,server_time - MAAT_REDIS_SYNC_TIME); + append_cmd_cnt++; + + //consume reply "OK" and "QUEUED". + for(int i = 0; i < append_cmd_cnt; i++) { + maat_cmd_wrap_redis_get_reply(c, &reply); + freeReplyObject(reply); + reply = NULL; + } + + redisReply *sub_reply = NULL; + reply = maat_cmd_wrap_redis_command(c, "EXEC"); + if (reply->type != REDIS_REPLY_ARRAY) { + goto error_out; + } + + sub_reply = reply->element[0]; + if (sub_reply->type != REDIS_REPLY_ARRAY) { + goto error_out; + } + + version_num = sub_reply->elements; + if (version_num == 0) { + goto error_out; + } + + version_lower_bound = maat_cmd_read_redis_integer(sub_reply->element[0]); + version_upper_bound = maat_cmd_read_redis_integer(sub_reply->element[sub_reply->elements-1]); + freeReplyObject(reply); + reply = NULL; + + //To deal with maat_version reset to 0, do NOT use -inf as lower bound intentionally. + reply = maat_cmd_wrap_redis_command(c, "ZREMRANGEBYSCORE %s %lld %lld", mr_status_sset, + version_lower_bound, version_upper_bound); + entry_num = maat_cmd_read_redis_integer(reply); + freeReplyObject(reply); + reply = NULL; + + fprintf(stdout, "Clean up update status from version %lld to %lld (%lld versions, %lld entries).", + version_lower_bound, version_upper_bound, version_num, entry_num); + return; + +error_out: + freeReplyObject(reply); + reply = NULL; + + return; +} + +void check_maat_expiration(redisContext *c) +{ + UNUSED int ret = 0; + + long long server_time = maat_cmd_redis_server_time_s(c); + if (!server_time) { + return; + } + + redisReply *data_reply= maat_cmd_wrap_redis_command(c, "ZRANGEBYSCORE %s -inf %lld", mr_expire_sset, server_time); + if (data_reply->type != REDIS_REPLY_ARRAY || 0 == data_reply->elements) { + freeReplyObject(data_reply); + data_reply = NULL; + return; + } + + size_t s_rule_num = data_reply->elements; + struct serial_rule *s_rule = ALLOC(struct serial_rule, s_rule_num); + + for (size_t i = 0; i < s_rule_num; i++) { + s_rule[i].op = MAAT_OP_DEL; + ret = sscanf(data_reply->element[i]->str, "%[^,],%ld", s_rule[i].table_name, &(s_rule[i].rule_id)); + assert(ret == 2); + } + freeReplyObject(data_reply); + data_reply = NULL; + + int success_cnt = maat_cmd_exec_serial_rule(c, s_rule, s_rule_num, server_time); + if (success_cnt < 0) { + fprintf(stderr, "maat_cmd_exec_serial_rule failed.\n"); + } else if (success_cnt == (int)s_rule_num) { + fprintf(stdout, "Succesfully expired %zu rules in Redis.", s_rule_num); + } else { + fprintf(stderr, "Failed to expired %d of %zu rules in Redis, try later.", + s_rule_num - success_cnt, s_rule_num); + } + + FREE(s_rule); +} + +void redis_monitor_traverse(long long version, struct source_redis_ctx *mr_ctx, + void (*start_fn)(long long, int, void *), + int (*update_fn)(const char *, const char *, void *), + void (*finish_fn)(void *), + void *u_param) +{ + int i = 0; + int ret = 0; + int table_id = 0; + int empty_value_num = 0; + int no_table_num = 0; + int call_update_num = 0; + int valid_column = -1; + enum table_type table_type; + enum scan_type scan_type; + struct table_schema *table_schema = NULL; + + //authorized to write + if (mr_ctx->write_ctx != NULL && mr_ctx->write_ctx->err == 0) { + //For thread safe, deliberately use redis_read_ctx but not redis_write_ctx. + if (1 == redlock_try_lock(mr_ctx->read_ctx, mr_expire_lock, mr_expire_lock_timeout_ms)) { + check_maat_expiration(mr_ctx->read_ctx); + cleanup_update_status(mr_ctx->read_ctx); + redlock_unlock(mr_ctx->read_ctx, mr_expire_lock); + } + } + + if (NULL == mr_ctx->read_ctx || mr_ctx->read_ctx->err) { + if (time(NULL) - mr_ctx->last_reconnect_time < MAAT_REDIS_RECONNECT_INTERVAL_S) { + return; + } + + mr_ctx->last_reconnect_time = time(NULL); + if (mr_ctx->read_ctx != NULL) { + redisFree(mr_ctx->read_ctx); + } + fprintf(stdout, "Reconnecting..."); + + mr_ctx->read_ctx = maat_cmd_connect_redis(mr_ctx->redis_ip, mr_ctx->redis_port, mr_ctx->redis_db); + if (NULL == mr_ctx->read_ctx) { + return; + } else { + version = 0; //Trigger full update when reconnect to redis. + } + } + + struct maat *maat_instance = (struct maat *)u_param; + struct serial_rule *rule_list = NULL; + long long new_version = 0; + int update_type = CM_UPDATE_TYPE_INC; + + int rule_num = maat_cmd_get_rm_key_list(mr_ctx->read_ctx, version, maat_instance->load_specific_version, + &new_version, maat_instance->table_schema_mgr, &rule_list, + &update_type, maat_instance->cumulative_update_off); + //redis communication error + if (rule_num < 0) { + redisFree(mr_ctx->read_ctx); + mr_ctx->read_ctx = NULL; + return; + } + + maat_instance->load_specific_version = 0;//only valid for one time. + //error or nothing changed + if (0 == rule_num && update_type == CM_UPDATE_TYPE_INC) { + return; + } + + if (rule_num > 0) { + ret = maat_cmd_get_redis_value(mr_ctx->read_ctx, rule_list, rule_num, 0); + //redis communication error + if (ret < 0) { + redisFree(mr_ctx->read_ctx); + mr_ctx->read_ctx = NULL; + fprintf(stderr, "Get Redis value failed, abandon update and close connection."); + goto clean_up; + } + + for (i = 0; i < rule_num; i++) { + if (NULL == rule_list[i].table_line) { + empty_value_num++; + } + } + + if (empty_value_num == rule_num) { + fprintf(stdout, "All %d rules are empty, abandon update.", empty_value_num); + goto clean_up; + } + + ret = get_foreign_keys_define(mr_ctx->read_ctx, rule_list, rule_num, maat_instance, maat_instance->foreign_cont_dir); + if (ret > 0) { + maat_cmd_get_foreign_conts(mr_ctx->read_ctx, rule_list, rule_num, 0); + } + } + + start_fn(new_version, update_type, u_param); + fprintf(stdout, "Start %s update: %lld -> %lld (%d entries).", + update_type==CM_UPDATE_TYPE_INC?"INC":"FULL", version, new_version, rule_num); + + for (i = 0; i < rule_num; i++) { + if (NULL == rule_list[i].table_line) { + continue; + } + + table_id = table_schema_manager_get_table_id(maat_instance->table_schema_mgr, rule_list[i].table_name); + //Unrecognized table. + if (table_id < 0) { + no_table_num++; + continue; + } + + table_schema = table_schema_get(maat_instance->table_schema_mgr, table_id); + if (rule_list[i].op == MAAT_OP_DEL) { + scan_type = table_schema_get_scan_type(table_schema); + table_type = table_schema_get_table_type(table_schema); + table_schema = table_schema_get_by_scan_type(maat_instance->table_schema_mgr, table_id, scan_type, NULL); + valid_column = table_schema_get_valid_flag_column(table_schema); + ret = invalidate_line(rule_list[i].table_line, table_type, valid_column); + if (ret < 0) { + fprintf(stdout, "Invalidate line failed, invaid format %s .", rule_list[i].table_line); + continue; + } + } + + if (rule_list[i].n_foreign > 0) { + maat_cmd_rewrite_table_line_with_foreign(rule_list+i); + } + + update_fn(rule_list[i].table_name, rule_list[i].table_line, u_param); + call_update_num++; + } + + finish_fn(u_param); + + if (call_update_num < rule_num) { + fprintf(stdout, "Load %d entries to match engine, no tablle: %d, empty value: %d.", + call_update_num, no_table_num, empty_value_num); + } + +clean_up: + for (i = 0; i < rule_num; i++) { + maat_cmd_empty_serial_rule(rule_list + i); + } + + FREE(rule_list); +} \ No newline at end of file diff --git a/src/maat_rule.cpp b/src/maat_rule.cpp index 45e57b9..8764212 100644 --- a/src/maat_rule.cpp +++ b/src/maat_rule.cpp @@ -14,12 +14,16 @@ #include #include #include +#include #include +#include "utils.h" +#include "json2iris.h" +#include "maat_utils.h" #include "maat_rule.h" #include "maat_config_monitor.h" -#include "utils.h" -#include "maat_utils.h" +#include "maat_redis_monitor.h" + #include "maat_table_runtime.h" #include "maat_table_schema.h" @@ -100,7 +104,16 @@ int maat_update_cb(const char *table_name, const char *line, void *u_param) struct maat *maat_instance =(struct maat *)u_param; struct maat_runtime* maat_rt = NULL; int table_id = table_schema_manager_get_table_id(maat_instance->table_schema_mgr, table_name); + if (table_id < 0) { + fprintf(stderr, "update warning, unknown table name %s\n", table_name); + return -1; + } + struct table_schema* table_schema = table_schema_get(maat_instance->table_schema_mgr, table_id); + if (NULL == table_schema) { + fprintf(stderr, "update warning, table name %s doesn't have table schema\n", table_name); + return -1; + } if (maat_instance->creating_maat_rt != NULL) { maat_rt = maat_instance->creating_maat_rt; @@ -177,10 +190,21 @@ void *rule_monitor_loop(void *arg) } pthread_mutex_unlock(&(maat_instance->background_update_mutex)); + char md5_tmp[MD5_DIGEST_LENGTH * 2 + 1] = {0}; + char err_str[NAME_MAX] = {0}; + struct stat attrib; while (maat_instance->is_running) { usleep(maat_instance->rule_update_checking_interval_ms * 1000); if( 0 == pthread_mutex_trylock(&(maat_instance->background_update_mutex))) { switch (maat_instance->input_mode) { + case DATA_SOURCE_REDIS: + redis_monitor_traverse(maat_instance->maat_version, + &(maat_instance->mr_ctx), + maat_start_cb, + maat_update_cb, + maat_finish_cb, + maat_instance); + break; case DATA_SOURCE_IRIS_FILE: config_monitor_traverse(maat_instance->maat_version, maat_instance->iris_ctx.inc_dir, @@ -189,6 +213,29 @@ void *rule_monitor_loop(void *arg) maat_finish_cb, maat_instance); break; + case DATA_SOURCE_JSON_FILE: + memset(md5_tmp, 0, sizeof(md5_tmp)); + stat(maat_instance->json_ctx.json_file, &attrib); + if (memcmp(&attrib.st_ctim, &(maat_instance->json_ctx.last_md5_time), sizeof(attrib.st_ctim))) { + maat_instance->json_ctx.last_md5_time = attrib.st_ctim; + md5_file(maat_instance->json_ctx.json_file, md5_tmp); + if (0 != strcmp(md5_tmp, maat_instance->json_ctx.effective_json_md5)) { + ret = load_maat_json_file(maat_instance, maat_instance->json_ctx.json_file, err_str, sizeof(err_str)); + if (ret < 0) { + fprintf(stdout, "Maat re-initiate with JSON file %s (md5=%s)failed: %s", + maat_instance->json_ctx.json_file, md5_tmp, err_str); + } else { + config_monitor_traverse(0, maat_instance->json_ctx.iris_file, + maat_start_cb, + maat_update_cb, + maat_finish_cb, + maat_instance); + fprintf(stdout, "Maat re-initiate with JSON file %s success, md5: %s", + maat_instance->json_ctx.json_file, md5_tmp); + } + } + } + break; default: break; } @@ -231,7 +278,20 @@ void *rule_monitor_loop(void *arg) maat_runtime_destroy(maat_instance->maat_rt); maat_garbage_bin_free(maat_instance->garbage_bin); table_schema_manager_destroy(maat_instance->table_schema_mgr); - free(maat_instance); + + if (maat_instance->input_mode == DATA_SOURCE_REDIS) { + if (maat_instance->mr_ctx.read_ctx != NULL) { + redisFree(maat_instance->mr_ctx.read_ctx); + maat_instance->mr_ctx.read_ctx = NULL; + } + + if (maat_instance->mr_ctx.write_ctx != NULL) { + redisFree(maat_instance->mr_ctx.write_ctx); + maat_instance->mr_ctx.write_ctx = NULL; + } + } + + FREE(maat_instance); return NULL; } \ No newline at end of file diff --git a/src/maat_table_runtime.cpp b/src/maat_table_runtime.cpp index a9eaa98..c935c86 100644 --- a/src/maat_table_runtime.cpp +++ b/src/maat_table_runtime.cpp @@ -36,7 +36,7 @@ struct plugin_user_ctx { }; struct expr_runtime { - enum scan_mode scan_mode; + enum hs_scan_mode scan_mode; struct adapter_hs *hs; struct adapter_hs_stream *hs_stream; struct rcu_hash_table *htable; diff --git a/src/maat_table_schema.cpp b/src/maat_table_schema.cpp index 48d44eb..32b5345 100644 --- a/src/maat_table_schema.cpp +++ b/src/maat_table_schema.cpp @@ -33,7 +33,7 @@ struct expr_table_schema { int match_method_column; int is_hexbin_column; int is_valid_column; /* valid means add, invalid means delete */ - enum scan_mode scan_mode; /* adapter_hs scan mode */ + enum hs_scan_mode scan_mode; /* adapter_hs scan mode */ }; #define MAX_PLUGIN_PER_TABLE 32 @@ -60,6 +60,10 @@ struct ip_plugin_table_schema { struct ex_data_schema ex_schema; }; +struct virtual_table_schema { + int physical_table_id[SCAN_TYPE_MAX]; +}; + struct table_schema { int table_id; char table_name[NAME_MAX]; @@ -68,6 +72,7 @@ struct table_schema { struct expr_table_schema expr; struct plugin_table_schema plugin; struct ip_plugin_table_schema ip_plugin; + struct virtual_table_schema virtual_table; }; }; @@ -388,6 +393,10 @@ struct table_schema_manager *table_schema_manager_create(const char *table_info_ unsigned char *json_buff = NULL; size_t json_buff_sz = 0; + if (NULL == table_info_path) { + return NULL; + } + int ret = load_file_to_memory(table_info_path, &json_buff, &json_buff_sz); if (ret < 0) { fprintf(stderr, "Maat read table info %s error.\n", table_info_path); @@ -417,8 +426,8 @@ struct table_schema_manager *table_schema_manager_create(const char *table_info_ maat_kv_register(reserved_word_map, "plugin", TABLE_TYPE_PLUGIN); maat_kv_register(reserved_word_map, "ip_plugin", TABLE_TYPE_IP_PLUGIN); - maat_kv_register(reserved_word_map, "block", SCAN_MODE_BLOCK); - maat_kv_register(reserved_word_map, "stream", SCAN_MODE_STREAM); + maat_kv_register(reserved_word_map, "block", HS_SCAN_MODE_BLOCK); + maat_kv_register(reserved_word_map, "stream", HS_SCAN_MODE_STREAM); struct table_schema_manager *table_schema_mgr = ALLOC(struct table_schema_manager, 1); struct table_schema **pptable = table_schema_mgr->schema_table; @@ -471,6 +480,10 @@ struct table_schema_manager *table_schema_manager_create(const char *table_info_ void table_schema_manager_destroy(struct table_schema_manager *table_schema_mgr) { + if (NULL == table_schema_mgr) { + return; + } + for (size_t i = 0; i < MAX_TABLE_NUM; i++) { if (NULL == table_schema_mgr->schema_table[i]) { continue; @@ -480,11 +493,15 @@ void table_schema_manager_destroy(struct table_schema_manager *table_schema_mgr) } maat_kv_store_free(table_schema_mgr->tablename2id_map); - free(table_schema_mgr); + FREE(table_schema_mgr); } int table_schema_manager_get_table_id(struct table_schema_manager* table_schema_mgr, const char *table_name) { + if (NULL == table_schema_mgr || NULL == table_name) { + return -1; + } + int table_id = -1; int ret = maat_kv_read(table_schema_mgr->tablename2id_map, table_name, &table_id); @@ -497,6 +514,10 @@ int table_schema_manager_get_table_id(struct table_schema_manager* table_schema_ enum table_type table_schema_manager_get_table_type(struct table_schema_manager *table_schema_mgr, int id) { + if (NULL == table_schema_mgr) { + return TABLE_TYPE_MAX; + } + if (table_schema_mgr->schema_table[id] == NULL) { return TABLE_TYPE_MAX; } @@ -511,6 +532,10 @@ size_t table_schema_manager_get_size(struct table_schema_manager* table_schema_m void table_schema_manager_all_plugin_cb_start(struct table_schema_manager *table_schema_mgr, int update_type) { + if (NULL == table_schema_mgr) { + return; + } + struct table_schema *ptable = NULL; struct plugin_table_schema *plugin_schema = NULL; @@ -536,6 +561,10 @@ void table_schema_manager_all_plugin_cb_start(struct table_schema_manager *table void table_schema_manager_all_plugin_cb_finish(struct table_schema_manager* table_schema_mgr) { + if (NULL == table_schema_mgr) { + return; + } + struct table_schema *ptable = NULL; struct plugin_table_schema *plugin_schema = NULL; @@ -606,19 +635,149 @@ enum match_method int_to_match_method_type(int match_method_type) struct table_schema *table_schema_get(struct table_schema_manager *table_schema_mgr, int table_id) { + if ((NULL == table_schema_mgr) || (table_id < 0)) { + return NULL; + } + return table_schema_mgr->schema_table[table_id]; } +struct table_schema *table_schema_get_by_scan_type(struct table_schema_manager *table_schema_mgr, + int table_id, enum scan_type scan_type, int *virtual_table_id) +{ + enum scan_type table_scan_type; + struct table_schema **pptable = table_schema_mgr->schema_table; + size_t n_table = MAX_TABLE_NUM; + + if ((unsigned int)table_id > n_table) { + return NULL; + } + + if (NULL == pptable[table_id]) { + return NULL; + } + + struct table_schema *ptable = pptable[table_id]; + if (NULL == ptable) { + return NULL; + } + + struct table_schema *p_physical_table = NULL; + if (ptable->table_type == TABLE_TYPE_VIRTUAL) { + p_physical_table = pptable[ptable->virtual_table.physical_table_id[scan_type]]; + *virtual_table_id = table_id; + } else { + p_physical_table = ptable; + if(virtual_table_id) { + *virtual_table_id = 0; + } + } + + table_scan_type = table_schema_get_scan_type(p_physical_table); + if (table_scan_type != scan_type) { + return NULL; + } + + return p_physical_table; +} + enum table_type table_schema_get_table_type(struct table_schema *table_schema) { + if (NULL == table_schema) { + return TABLE_TYPE_MAX; + } + return table_schema->table_type; } int table_schema_get_table_id(struct table_schema *table_schema) { + if (NULL == table_schema) { + return -1; + } + return table_schema->table_id; } +enum scan_type table_schema_get_scan_type(struct table_schema *table_schema) +{ + enum scan_type ret = SCAN_TYPE_INVALID; + + if (NULL == table_schema) { + return ret; + } + + switch (table_schema->table_type) { + case TABLE_TYPE_EXPR: + case TABLE_TYPE_EXPR_PLUS: + case TABLE_TYPE_SIMILARITY: + case TABLE_TYPE_DIGEST: + ret = SCAN_TYPE_STRING; + break; + case TABLE_TYPE_INTERVAL: + case TABLE_TYPE_INTERVAL_PLUS: + ret = SCAN_TYPE_INTERVAL; + break; + case TABLE_TYPE_IP: + case TABLE_TYPE_IP_PLUS: + case TABLE_TYPE_COMPOSITION: + ret = SCAN_TYPE_IP; + break; + case TABLE_TYPE_PLUGIN: + ret = SCAN_TYPE_PLUGIN; + break; + case TABLE_TYPE_IP_PLUGIN: + ret = SCAN_TYPE_IP; + break; + case TABLE_TYPE_FQDN_PLUGIN: + ret = SCAN_TYPE_FQDN_PLUGIN; + break; + case TABLE_TYPE_BOOL_PLUGIN: + ret = SCAN_TYPE_BOOL_PLUGIN; + break; + case TABLE_TYPE_COMPILE: + ret = SCAN_TYPE_NONE; + break; + default: + break; + } + + return ret; +} + +int table_schema_get_valid_flag_column(struct table_schema *table_schema) +{ + int valid_flag_column = -1; + + if (NULL == table_schema) { + return valid_flag_column; + } + + switch (table_schema->table_type) { + case TABLE_TYPE_EXPR: + valid_flag_column = table_schema->expr.is_valid_column; + break; + case TABLE_TYPE_PLUGIN: + valid_flag_column = table_schema->plugin.is_valid_column; + break; + case TABLE_TYPE_IP_PLUGIN: + valid_flag_column = table_schema->ip_plugin.is_valid_column; + break; + /* + case TABLE_TYPE_FQDN_PLUGIN: + valid_flag_column = table_schema->fqdn_plugin.valid_flag_column; + break; + case TABLE_TYPE_BOOL_PLUGIN: + valid_flag_column = table_schema->bool_plugin.valid_flag_column; + break;*/ + default: + valid_flag_column = -1; + break; + } + + return valid_flag_column; +} + int populate_expr_table_item(const char *line, struct expr_table_schema *expr_schema, struct expr_item *expr_item) { size_t column_offset = 0; @@ -778,6 +937,10 @@ int populate_ip_plugin_table_item(const char *line, struct ip_plugin_table_schem struct table_item * table_schema_line_to_item(const char *line, struct table_schema *table_schema) { + if (NULL == line || NULL == table_schema) { + return NULL; + } + int ret = -1; struct table_item *table_item = ALLOC(struct table_item, 1); @@ -812,10 +975,14 @@ error: return NULL; } -enum scan_mode expr_table_schema_get_scan_mode(struct table_schema *table_schema) +enum hs_scan_mode expr_table_schema_get_scan_mode(struct table_schema *table_schema) { + if (NULL == table_schema) { + return HS_SCAN_MODE_MAX; + } + if (table_schema->table_type != TABLE_TYPE_EXPR) { - return SCAN_MODE_MAX; + return HS_SCAN_MODE_MAX; } return table_schema->expr.scan_mode; @@ -827,7 +994,7 @@ int plugin_table_schema_set_ex_data_schema(struct table_schema *table_schema, maat_plugin_ex_dup_func_t *dup_func, long argl, void *argp) { - if (NULL == new_func || NULL == free_func || NULL == dup_func) { + if (NULL == table_schema || NULL == new_func || NULL == free_func || NULL == dup_func) { assert(0); fprintf(stderr, "%s failed: invalid parameter", __FUNCTION__); return -1; @@ -857,6 +1024,10 @@ int plugin_table_schema_set_ex_data_schema(struct table_schema *table_schema, struct ex_data_schema *plugin_table_schema_get_ex_data_schema(struct table_schema *table_schema) { + if (NULL == table_schema) { + return NULL; + } + struct ex_data_schema *ex_schema = NULL; switch (table_schema->table_type) { @@ -875,6 +1046,10 @@ struct ex_data_schema *plugin_table_schema_get_ex_data_schema(struct table_schem int plugin_table_schema_ex_data_schema_flag(struct table_schema *table_schema) { + if (NULL == table_schema) { + return -1; + } + struct ex_data_schema *ex_schema = NULL; switch (table_schema->table_type) { @@ -897,6 +1072,10 @@ int plugin_table_schema_add_callback(struct table_schema_manager* table_schema_m maat_finish_callback_t *finish, void *u_para) { + if ((NULL == table_schema_mgr) || (table_id < 0)) { + return -1; + } + struct table_schema *ptable = table_schema_get(table_schema_mgr, table_id); if (NULL == ptable) { fprintf(stderr, "table_id:%d unregistered, can't add callback func", table_id); @@ -945,6 +1124,10 @@ size_t plugin_table_schema_callback_count(struct table_schema *table_schema) void plugin_table_schema_all_cb_update(struct table_schema* table_schema, const char *row) { + if (NULL == table_schema || NULL == row) { + return; + } + struct plugin_table_schema *plugin_schema = NULL; switch (table_schema->table_type) { @@ -963,4 +1146,22 @@ void plugin_table_schema_all_cb_update(struct table_schema* table_schema, const default: break; } +} + +int plugin_table_schema_get_foreign_column(struct table_schema *table_schema, int *foreign_columns) +{ + if (NULL == table_schema) { + return -1; + } + + if (table_schema->table_type != TABLE_TYPE_PLUGIN) { + return 0; + } + + int n_foreign = table_schema->plugin.n_foreign; + for (int i = 0; i < n_foreign; i++) { + foreign_columns[i] = table_schema->plugin.foreign_columns[i]; + } + + return n_foreign; } \ No newline at end of file diff --git a/src/maat_utils.cpp b/src/maat_utils.cpp index 444704f..967e621 100644 --- a/src/maat_utils.cpp +++ b/src/maat_utils.cpp @@ -11,6 +11,9 @@ #include #include #include +#include +#include +#include #include "maat_utils.h" @@ -19,6 +22,7 @@ char *maat_strdup(const char *s) if (NULL == s) { return NULL; } + char *d = (char *)malloc(strlen(s) + 1); memcpy(d, s, strlen(s) + 1); @@ -27,10 +31,14 @@ char *maat_strdup(const char *s) int get_column_pos(const char *line, int column_seq, size_t *offset, size_t *len) { - const char *seps=" \t"; - char *saveptr=NULL, *subtoken=NULL, *str=NULL; + int i = 0; + int ret = -1; + char *str = NULL; + char *saveptr = NULL; + char *subtoken = NULL; + const char *seps = " \t"; char *dup_line = maat_strdup(line); - int i = 0, ret = -1; + for (str = dup_line; ; str = NULL) { subtoken = strtok_r(str, seps, &saveptr); if (subtoken == NULL) @@ -53,7 +61,7 @@ int load_file_to_memory(const char *file_name, unsigned char **pp_out, size_t *o int ret = 0; FILE *fp = NULL; struct stat fstat_buf; - size_t read_size=0; + size_t read_size = 0; ret = stat(file_name, &fstat_buf); if (ret != 0) { @@ -186,4 +194,181 @@ int system_cmd_mkdir(const char *path) char cmd[MAX_SYSTEM_CMD_LEN] = {0}; snprintf(cmd, sizeof(cmd), "mkdir -p %s", path); return system(cmd); -} \ No newline at end of file +} + +char *md5_file(const char *filename, char *md5string) +{ + unsigned char md5[MD5_DIGEST_LENGTH] = {0}; + struct stat file_info; + stat(filename, &file_info); + size_t file_size = file_info.st_size; + + FILE *fp = fopen(filename,"r"); + if (NULL == fp) { + return NULL; + } + + char *file_buff = (char *)malloc(file_size); + fread(file_buff, 1, file_size, fp); + fclose(fp); + + MD5((const unsigned char *)(file_buff), (unsigned long)(file_size), md5); + for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) { + sprintf(&md5string[i*2], "%02x", (unsigned int)md5[i]); + } + + free(file_buff); + return md5string; +} + +int crypt_memory(const unsigned char *inbuf, size_t inlen, unsigned char **pp_out, size_t *out_sz, + const char *key, const char *algorithm, int do_encrypt, + char *err_str, size_t err_str_sz) +{ + OpenSSL_add_all_algorithms(); + const EVP_CIPHER *cipher = EVP_get_cipherbyname(algorithm); + if (NULL == cipher) { + snprintf(err_str, err_str_sz, "Cipher %s is not supported.", algorithm); + return 0; + } + + const EVP_MD *dgst = EVP_get_digestbyname("md5"); + if (NULL == dgst) { + snprintf(err_str, err_str_sz, "Get MD5 object failed."); + return 0; + } + + const unsigned char *salt = NULL; + unsigned char cipher_key[EVP_MAX_KEY_LENGTH]; + unsigned char cipher_iv[EVP_MAX_IV_LENGTH]; + + memset(cipher_key,0,sizeof(cipher_key)); + memset(cipher_iv,0,sizeof(cipher_iv)); + + int ret = EVP_BytesToKey(cipher, dgst, salt, (unsigned char *)key, + strlen((const char *)key), 1, cipher_key, cipher_iv); + if(0 == ret) { + snprintf(err_str, err_str_sz, "Key and IV generatioin failed."); + return 0; + } + + /* Don't set key or IV right away; we want to check lengths */ + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + EVP_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, do_encrypt); + OPENSSL_assert(EVP_CIPHER_CTX_key_length(ctx) % 16 == 0); + OPENSSL_assert(EVP_CIPHER_CTX_iv_length(ctx) == 16); + + /* Now we can set key and IV */ + //It should be set to 1 for encryption, 0 for decryption and -1 to leave the value unchanged (the actual value of 'enc' being supplied in a previous call). + EVP_CipherInit_ex(ctx, NULL, NULL, cipher_key, cipher_iv, -1); + int out_blk_len = 0; + int out_buff_offset = 0; + int out_buff_len = inlen + EVP_CIPHER_block_size(cipher) - 1; + *pp_out = (unsigned char *)malloc(out_buff_len * sizeof(unsigned char)); + if (!EVP_CipherUpdate(ctx, *pp_out + out_buff_offset, &out_blk_len, inbuf, inlen)) { + snprintf(err_str, err_str_sz, "EVP_CipherUpdate failed."); + EVP_CIPHER_CTX_free(ctx); + goto error_out; + } + + out_buff_offset += out_blk_len; + if (!EVP_CipherFinal_ex(ctx, *pp_out+out_buff_offset, &out_blk_len)) { + snprintf(err_str, err_str_sz, "EVP_CipherFinal_ex failed. Maybe password is wrong?"); + EVP_CIPHER_CTX_free(ctx); + goto error_out; + } + + out_buff_offset += out_blk_len; + EVP_CIPHER_CTX_free(ctx); + *out_sz = out_buff_offset; + return 0; + +error_out: + free(*pp_out); + *pp_out = NULL; + return -1; +} + +int decrypt_open(const char* file_name, const char* key, const char* algorithm, + unsigned char**pp_out, size_t *out_sz, char* err_str, size_t err_str_sz) +{ + size_t file_sz = 0; + unsigned char *file_buff = NULL; + int ret = load_file_to_memory(file_name, &file_buff, &file_sz); + if (ret < 0) { + return -1; + } + + ret = crypt_memory(file_buff, file_sz, pp_out, out_sz, key, algorithm, 0, err_str, err_str_sz); + free(file_buff); + file_buff = NULL; + + return ret; +} + +int gzip_uncompress_one_try(const unsigned char *in_compressed_data, size_t in_compressed_sz, + unsigned char **out_uncompressed_data, size_t *out_uncompressed_sz) +{ + z_stream strm; + strm.zalloc = NULL; + strm.zfree = NULL; + strm.opaque = NULL; + + strm.avail_in = in_compressed_sz; + strm.avail_out = *out_uncompressed_sz; + strm.next_in = (Bytef *) in_compressed_data; + strm.next_out = *out_uncompressed_data; + + int ret = -1; + ret = inflateInit2(&strm, MAX_WBITS+16); + if (ret == Z_OK) { + ret = inflate(&strm, Z_FINISH); + if (ret == Z_STREAM_END) { + *out_uncompressed_sz = strm.total_out; + ret = inflateEnd(&strm); + return ret; + } + } + inflateEnd(&strm); + return ret; +} + +int gzip_uncompress(const unsigned char *in_compressed_data, size_t in_compressed_sz, + unsigned char **out_uncompressed_data, size_t *out_uncompressed_sz) +{ + int z_result; + int ret = -1; + size_t buffer_sz = in_compressed_sz * 2; + *out_uncompressed_data = (unsigned char *)malloc(buffer_sz); + + do { + *out_uncompressed_sz=buffer_sz; + z_result = gzip_uncompress_one_try(in_compressed_data, in_compressed_sz, + out_uncompressed_data, out_uncompressed_sz); + switch (z_result) { + case Z_OK: + ret = 0; + break; + case Z_BUF_ERROR: + buffer_sz *= 2; + *out_uncompressed_data = (unsigned char *)realloc(*out_uncompressed_data, buffer_sz); + break; + default: + ret = -1; + break; + } + } while (z_result == Z_BUF_ERROR); + + return ret; +} + +size_t memcat(void **dest, size_t offset, size_t *n_dest, const void *src, size_t n_src) +{ + if (*n_dest < offset + n_src) { + *n_dest = (offset + n_src) * 2; + *dest = realloc(*dest, sizeof(char) * (*n_dest)); + } + memcpy((char *) * dest + offset, src, n_src); + + return n_src; +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 835b206..b34a608 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -13,4 +13,5 @@ target_link_libraries(maat_framework_gtest maat_frame_static gtest_static) file(COPY rule DESTINATION ./) file(COPY table_info.conf DESTINATION ./) -file(COPY and_expr.conf DESTINATION ./) \ No newline at end of file +file(COPY and_expr.conf DESTINATION ./) +file(COPY maat_json.json DESTINATION ./) \ No newline at end of file diff --git a/test/maat_framework_gtest.cpp b/test/maat_framework_gtest.cpp index 714f1dc..b9750b5 100644 --- a/test/maat_framework_gtest.cpp +++ b/test/maat_framework_gtest.cpp @@ -5,11 +5,14 @@ #include "maat_utils.h" #include "maat_table_schema.h" #include "maat_table_runtime.h" +#include "maat_command.h" struct maat *g_maat_instance = NULL; const char *table_info_path = "./table_info.conf"; const char *rule_full_path = "./rule/full/index"; const char *rule_inc_path = "./rule/inc/index"; +const char *json_path="./maat_json.json"; +const char *iris_file = "./HTTP_URL.000001"; TEST(maat_scan_string, hit_one_expr) { struct table_schema_manager *table_schema_mgr = g_maat_instance->table_schema_mgr; @@ -31,20 +34,22 @@ TEST(maat_scan_string, hit_one_expr) { EXPECT_EQ(n_result_array, 1); EXPECT_EQ(result_array[0], 101); +/* memset(result_array, 0, sizeof(result_array)); char data3[64] = "maat"; ret = maat_scan_string(g_maat_instance, table_id, 0, data3, strlen(data3), result_array, &n_result_array, NULL); EXPECT_EQ(ret, 0); EXPECT_EQ(n_result_array, 1); - EXPECT_EQ(result_array[0], 102); + EXPECT_EQ(result_array[0], 102); */ +/* memset(result_array, 0, sizeof(result_array)); char data4[64] = "world"; ret = maat_scan_string(g_maat_instance, table_id, 0, data4, strlen(data4), result_array, &n_result_array, NULL); EXPECT_EQ(n_result_array, 1); - EXPECT_EQ(result_array[0], 103); + EXPECT_EQ(result_array[0], 103); */ } - +#if 0 TEST(maat_scan_string, hit_two_expr) { struct table_schema_manager *table_schema_mgr = g_maat_instance->table_schema_mgr; int table_id = table_schema_manager_get_table_id(table_schema_mgr, "HTTP_URL"); @@ -159,14 +164,82 @@ TEST(maat_scan_string, config_dynamic_update) { EXPECT_EQ(result_array[4], 101); } + +TEST(maat_scan_string, hit_one_expr) { + struct table_schema_manager *table_schema_mgr = g_maat_instance->table_schema_mgr; + int table_id = table_schema_manager_get_table_id(table_schema_mgr, "HTTP_URL"); + + char data[128] = "i.ytimg.com"; + int result_array[5] = {0}; + size_t n_result_array = 0; + int ret = maat_scan_string(g_maat_instance, table_id, 0, data, strlen(data), result_array, &n_result_array, NULL); + EXPECT_EQ(ret, 0); + EXPECT_EQ(n_result_array, 1); + EXPECT_EQ(result_array[0], 30); +} + +TEST(maat_scan_string, hit_two_expr) { + struct table_schema_manager *table_schema_mgr = g_maat_instance->table_schema_mgr; + int table_id = table_schema_manager_get_table_id(table_schema_mgr, "HTTP_URL"); + + char data[128] = "should hit aaa bbb"; + int result_array[5] = {0}; + size_t n_result_array = 0; + int ret = maat_scan_string(g_maat_instance, table_id, 0, data, strlen(data), result_array, &n_result_array, NULL); + EXPECT_EQ(ret, 0); + EXPECT_EQ(n_result_array, 2); + EXPECT_EQ(result_array[0], 28); + EXPECT_EQ(result_array[1], 27); +} + +TEST(maat_scan_string, hit_three_expr) { + struct table_schema_manager *table_schema_mgr = g_maat_instance->table_schema_mgr; + int table_id = table_schema_manager_get_table_id(table_schema_mgr, "HTTP_URL"); + + char data[128] = "should hit aaa bbb C#涓浗"; + int result_array[5] = {0}; + size_t n_result_array = 0; + int ret = maat_scan_string(g_maat_instance, table_id, 0, data, strlen(data), result_array, &n_result_array, NULL); + EXPECT_EQ(ret, 0); + EXPECT_EQ(n_result_array, 3); + EXPECT_EQ(result_array[0], 28); + EXPECT_EQ(result_array[1], 27); + EXPECT_EQ(result_array[2], 18); +} +#endif +TEST(maat_redis, write) { + char line[1024] = {0}; + char table_name[64] = "HTTP_URL"; + struct maat_cmd_line rule_line; + rule_line.table_name = (char *)malloc(sizeof(char) * 64); + + memcpy((void *)rule_line.table_name, table_name, strlen(table_name)); + FILE *fp = fopen(iris_file, "r"); + EXPECT_NE(fp, nullptr); + + int rule_num = 0; + fscanf(fp, "%d\n", &rule_num); + + while (NULL != fgets(line, sizeof(line), fp)) { + rule_line.table_line = (char *)malloc(sizeof(line)); + memcpy((void *)rule_line.table_line, line, strlen(line)); + maat_cmd_set_line(g_maat_instance, &rule_line); + break; + } + +} + int main(int argc, char ** argv) { int ret=0; ::testing::InitGoogleTest(&argc, argv); struct maat_options *opts = maat_options_new(); - maat_options_set_iris_full_dir(opts, rule_full_path); - maat_options_set_iris_inc_dir(opts, rule_inc_path); + //maat_options_set_iris_full_dir(opts, rule_full_path); + //maat_options_set_iris_inc_dir(opts, rule_inc_path); + maat_options_set_redis_ip(opts, "127.0.0.1"); + maat_options_set_redis_port(opts, 6379); + //maat_options_set_json_file(opts, json_path); g_maat_instance = maat_new(opts, table_info_path); EXPECT_NE(g_maat_instance, nullptr); diff --git a/test/maat_json.json b/test/maat_json.json new file mode 100644 index 0000000..42cf591 --- /dev/null +++ b/test/maat_json.json @@ -0,0 +1,2339 @@ +{ + "compile_table": "COMPILE", + "group2compile_table": "GROUP2COMPILE", + "group2group_table": "GROUP2GROUP", + "groups": [ + { + "group_name": "ASN1234", + "regions": [ + { + "table_name": "AS_NUMBER", + "table_type": "expr", + "table_content": { + "keywords": "AS1234", + "expr_type": "none", + "match_method": "exact", + "format": "uncase plain" + } + } + ] + }, + { + "group_name": "ASN2345", + "regions": [ + { + "table_name": "AS_NUMBER", + "table_type": "expr", + "table_content": { + "keywords": "AS2345", + "expr_type": "none", + "match_method": "exact", + "format": "uncase plain" + } + } + ] + }, + { + "group_name": "financial-department-ip", + "regions": [ + { + "table_name": "IP_CONFIG", + "table_type": "ip", + "table_content": { + "addr_type": "ipv4", + "src_ip": "192.168.40.88", + "mask_src_ip": "255.255.255.255", + "src_port": "0", + "mask_src_port": "65535", + "dst_ip": "0.0.0.0", + "mask_dst_ip": "255.255.255.255", + "dst_port": "0", + "mask_dst_port": "65535", + "protocol": 6, + "direction": "double" + } + } + ] + }, + { + "group_name": "Country-Sparta-IP", + "regions": [ + { + "table_name": "GeoLocation", + "table_type": "expr", + "table_content": { + "keywords": "Greece.Sparta", + "expr_type": "none", + "match_method": "exact", + "format": "uncase plain" + } + } + ] + }, + { + "group_name": "IPv4-composition-source-only", + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv4", + "saddr_format": "range", + "src_ip1": "192.168.50.24", + "src_ip2": "192.168.50.24", + "sport_format": "range", + "src_port1": "1", + "src_port2": "40000", + "daddr_format": "mask", + "dst_ip1": "0.0.0.0", + "dst_ip2": "255.255.255.0", + "dport_format": "range", + "dst_port1": "0", + "dst_port2": "65535", + "protocol": 6, + "direction": "double" + } + } + ] + }, + { + "group_name": "FQDN_OBJ1", + "regions": [ + { + "table_name": "KEYWORDS_TABLE", + "table_type": "expr", + "table_content": { + "keywords": "sports.example.com", + "expr_type": "none", + "match_method": "exact", + "format": "uncase plain" + } + } + ] + }, + { + "group_name": "FQDN_CAT1", + "regions": [ + { + "table_name": "INTERGER_PLUS", + "table_type": "interval_plus", + "table_content": { + "district": "fqdn_cat_id", + "low_boundary": 1724, + "up_boundary": 1724 + } + } + ] + }, + { + "group_name": "IPv4-composition-NOT-client-ip", + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv4", + "saddr_format": "range", + "src_ip1": "192.168.58.19", + "src_ip2": "192.168.58.19", + "sport_format": "range", + "src_port1": "20000", + "src_port2": "20000", + "daddr_format": "mask", + "dst_ip1": "0.0.0.0", + "dst_ip2": "255.255.255.0", + "dport_format": "range", + "dst_port1": "0", + "dst_port2": "65535", + "protocol": 6, + "direction": "double" + } + } + ] + }, + { + "group_name": "IPv4-composition-NOT-server-ip", + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv4", + "saddr_format": "range", + "src_ip1": "10.0.1.20", + "src_ip2": "10.0.1.25", + "sport_format": "range", + "src_port1": "1", + "src_port2": "443", + "daddr_format": "mask", + "dst_ip1": "0.0.0.0", + "dst_ip2": "255.255.255.0", + "dport_format": "range", + "dst_port1": "0", + "dst_port2": "65535", + "protocol": 6, + "direction": "double" + } + } + ] + } + ], + "rules": [ + { + "compile_id": 123, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "escaped\\bdata:have\\ba\\bspace\\band\\ba\\b\\&\\bsymbol.", + "is_valid": "yes", + "groups": [ + { + "group_name": "123_IP_group", + "regions": [ + { + "table_name": "IP_CONFIG", + "table_type": "ip", + "table_content": { + "addr_type": "ipv4", + "src_ip": "10.0.6.201", + "mask_src_ip": "255.255.0.0", + "src_port": "0", + "mask_src_port": "65535", + "dst_ip": "0.0.0.0", + "mask_dst_ip": "255.255.255.255", + "dst_port": "0", + "mask_dst_port": "65535", + "protocol": 6, + "direction": "double" + } + }, + { + "table_name": "IP_CONFIG", + "table_type": "ip", + "table_content": { + "addr_type": "ipv6", + "src_ip": "2001:da8:205:1::101", + "mask_src_ip": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:0000", + "src_port": "0", + "mask_src_port": "65535", + "dst_ip": "0::0", + "mask_dst_ip": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + "dst_port": "0", + "mask_dst_port": "65535", + "protocol": 6, + "direction": "double" + } + } + ] + }, + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "abckkk&123", + "expr_type": "and", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 124, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "anything", + "is_valid": "yes", + "groups": [ + { + "group_name": "123_IP_group" + }, + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "CONTENT_SIZE", + "table_type": "interval", + "table_content": { + "low_boundary": 100, + "up_boundary": 500 + } + } + ] + } + ] + }, + { + "compile_id": 125, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "anything", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "action=search\\&query=(.*)", + "expr_type": "regex", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 126, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "anything", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "should_not_hit_any_rule", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + }, + { + "group_name": "126_interval_group", + "regions": [ + { + "table_name": "CONTENT_SIZE", + "table_type": "interval", + "table_content": { + "low_boundary": 2014, + "up_boundary": 2016 + } + } + ] + } + ] + }, + { + "compile_id": 127, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "anything", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "FILE_DIGEST", + "table_type": "digest", + "table_content": { + "raw_len": 1160164, + "digest": "3072:Xk/maCm4yLYtRIFDFnVfHH+CAQI6VD5mekDmaa/4qCuFnqak1s3/+Gn1IJHa/AvybUsbGWcIAy9grTp2s5bbj/TaKxONfb[0:1160163]#12288:UChtbFS6pypdTy4m2[0:1160163]", + "cfds_level": 70 + } + } + ] + } + ] + }, + { + "compile_id": 128, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "StringScan.ExprPlus", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_SIGNATURE", + "table_type": "expr_plus", + "table_content": { + "district": "HtTP\\bUrL", + "keywords": "abckkk&123", + "expr_type": "and", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 129, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "utf8_涓枃", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "C#涓浗", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 130, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "utf8_缁磋", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "KEYWORDS_TABLE", + "table_type": "expr", + "table_content": { + "keywords": "2010&賷賶賱賶丿賶賰賶", + "expr_type": "and", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 131, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "utf8_缁磋2", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "KEYWORDS_TABLE", + "table_type": "expr", + "table_content": { + "keywords": "爻賶賷丕爻賶賷", + "expr_type": "and", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 132, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "string\\bunescape", + "is_valid": "yes", + "groups": [ + { + "group_name": "TakeMeHome", + "regions": [ + { + "table_name": "KEYWORDS_TABLE", + "table_type": "expr", + "table_content": { + "keywords": "Take\\bme\\bHome&Batman\\", + "expr_type": "and", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 133, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "13018_table_conjunction_test_part1\bnow_its_very_very_long0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz0123456789abcdefghijklmnopkrstuvwxyz", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_HOST", + "table_type": "expr", + "table_content": { + "keywords": "www.3300av.com", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 134, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "table_conjunction_test_part2", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "novel&27122.txt", + "expr_type": "and", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 135, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "anything", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "SIM_URL", + "table_type": "similar", + "table_content": { + "target": "mwss.xiu.youku.com/live/hls/v1/0000000000000000000000001526a0a8/709.ts?&token=98765", + "threshold": 90 + } + } + ] + } + ] + }, + { + "compile_id": 136, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "offset_string", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "IMAGE_FP", + "table_type": "expr", + "table_content": { + "keywords": "4362-4458:323031333A30333A30372032333A35363A313000323031333A30333A30372032333A35363A3130000000FFE20C584943435F50524F46494C4500010100000C484C696E6F021000006D6E74725247422058595A2007CE00020009000600310000", + "expr_type": "offset", + "match_method": "none", + "format": "hexbin" + } + } + ] + } + ] + }, + { + "compile_id": 137, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "offset_string", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "IMAGE_FP", + "table_type": "expr", + "table_content": { + "keywords": "19339-19467:6CB2CB2F2028474C994991CCFC65CCA5E3B6FF001673985D157358610CACC674EE64CC27B5721CCDABD9CCA7C8E9F7BB1F54A930A6034D50F92711F5B2DACCB0715D2E6873CE5CE431DC701A194C260E9DB78CC89F2C84745869AB88349A3AE0412AB59D9ABA84EDEFFF0057FA4DA66D333698B5AD6F844DA2226D1CADAD5E44", + "expr_type": "offset", + "match_method": "none", + "format": "hexbin" + } + } + ] + } + ] + }, + { + "compile_id": 138, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "effective_range": 0, + "tags":"{\"tag_sets\":[[{\"tag\":\"location\",\"value\":[\"鍖椾含/鏈濋槼/鍗庝弗鍖楅噷\",\"涓婃捣/娴︿笢/闄嗗鍢碶"]},{\"tag\":\"isp\",\"value\":[\"鐢典俊\",\"鑱旈歕"]}],[{\"tag\":\"location\",\"value\":[\"鍖椾含\"]},{\"tag\":\"isp\",\"value\":[\"鑱旈歕"]}]]}", + "user_region": "Not\\baccepted\\btags", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "should&hit&aaa", + "expr_type": "and", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 139, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "effective_range": 0, + "tags":"{\"tag_sets\":[[{\"tag\":\"location\",\"value\":[\"鍖椾含/鏈濋槼/鍗庝弗鍖楅噷\"]},{\"tag\":\"isp\",\"value\":[\"鐢典俊\",\"绉诲姩\"]}]]}", + "user_region": "Accepted\\btags", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "should&hit&bbb", + "expr_type": "and", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 140, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "file_streams", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "KEYWORDS_TABLE", + "table_type": "expr", + "table_content": { + "keywords": "2018-10-05", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 141, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "Something:I\\bhave\\ba\\bname,7799", + "table_name":"COMPILE_ALIAS", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "i.ytimg.com", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 142, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "StringScan.UTF8EncodedURL", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": ",IgpwcjA0LnN2bzAzKgkxMjcuMC4wLjE", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 143, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "NOTLogic.OneRegion", + "is_valid": "yes", + "groups": [ + { + "not_flag":0, + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "must-contained-string-of-rule-143", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + }, + { + "not_flag":1, + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "must-not-contained-string-of-rule-143", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 144, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "NOTLogic.ScanNotAtLast", + "is_valid": "yes", + "groups": [ + { + "not_flag":0, + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "must-contained-string-of-rule-144", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + }, + { + "not_flag":1, + "regions": [ + { + "table_name": "KEYWORDS_TABLE", + "table_type": "expr", + "table_content": { + "keywords": "must-not-contained-string-of-rule-144", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 145, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "NOTLogic.ScanNotIP", + "is_valid": "yes", + "groups": [ + { + "not_flag":0, + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "must-contained-string-of-rule-145", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + }, + { + "not_flag":1, + "group_name": "123_IP_group" + } + ] + }, + { + "compile_id": 146, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "StringScan.Regex", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "Cookie:\\s&head", + "expr_type": "regex", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 147, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "StringScan.UTF8EncodedURL", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "googlevideo.com/videoplayback&mn=sn-35153iuxa-5a56%2Csn-n8v7znz7", + "expr_type": "and", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "is_valid": "yes", + "do_log": 0, + "effective_rage": 0, + "action": 0, + "compile_id": 148, + "service": 0, + "do_blacklist": 0, + "user_region": "StringScan.ExprPlusWithOffset", + "groups": [ + { + "regions": [ + { + "table_name": "APP_PAYLOAD", + "table_content": { + "format": "hexbin", + "match_method": "sub", + "district": "Payload", + "keywords": "1-1:03&9-10:2d&14-16:2d34&19-21:2d&24-25:2d", + "expr_type": "offset" + }, + "table_type": "expr_plus" + } + ], + "group_name": "Untitled" + } + ] + }, + { + "compile_id": 150, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "StringScan.BugReport20190325", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_type": "expr", + "table_name": "TROJAN_PAYLOAD", + "table_content": { + "keywords": "0-4:01000000", + "expr_type": "offset", + "format": "hexbin", + "match_method": "sub" + } + } + ], + "group_name": "billgates_regist1" + }, + { + "regions": [ + { + "table_type": "expr", + "table_name": "TROJAN_PAYLOAD", + "table_content": { + "keywords": "1:G2.40", + "expr_type": "none", + "format": "uncase plain", + "match_method": "sub" + } + } + ], + "group_name": "billgates_regist2" + } + ] + }, + { + "compile_id": 151, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "StringScan.PrefixAndSuffix", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_type": "expr", + "table_name": "MAIL_ADDR", + "table_content": { + "keywords": "ceshi3@mailhost.cn", + "expr_type": "none", + "format": "uncase plain", + "match_method": "suffix" + } + } + ], + "group_name": "Untitled" + } + ] + }, + { + "compile_id": 152, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "StringScan.PrefixAndSuffix", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_type": "expr", + "table_name": "MAIL_ADDR", + "table_content": { + "keywords": "ceshi3@mailhost.cn", + "expr_type": "none", + "format": "uncase plain", + "match_method": "prefix" + } + }, + { + "table_type": "expr", + "table_name": "MAIL_ADDR", + "table_content": { + "keywords": "ceshi6@mailhost.cn", + "expr_type": "none", + "format": "uncase plain", + "match_method": "prefix" + } + } + ], + "group_name": "152_mail_addr" + }, + { + "group_name": "interval_group_refered", + "sub_groups": [ + {"group_name": "126_interval_group"} + ] + } + ] + }, + { + "compile_id": 153, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "Policy.SubGroup", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_type": "expr", + "table_name": "MAIL_ADDR", + "table_content": { + "keywords": "ceshi4@mailhost.cn", + "expr_type": "none", + "format": "uncase plain", + "match_method": "prefix" + } + } + ], + "group_name": "Untitled", + "sub_groups": [ + {"group_name": "152_mail_addr"} + ], + "not_flag" : 0 + }, + { + "group_name": "IP_group_refered", + "sub_groups": [ + {"group_name": "123_IP_group"} + ] + } + ] + }, + { + "compile_id": 154, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "ipv4_plus", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv4", + "saddr_format": "range", + "src_ip1": "10.0.7.100", + "src_ip2": "10.0.7.106", + "sport_format": "range", + "src_port1": "5000", + "src_port2": "5001", + "daddr_format": "mask", + "dst_ip1": "123.56.104.218", + "dst_ip2": "255.255.255.0", + "dport_format": "range", + "dst_port1": "7400", + "dst_port2": "7400", + "protocol": 6, + "direction": "double" + } + } + ], + "not_flag" : 0 + } + ] + }, + { + "compile_id": 155, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "ipv6_plus", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv6", + "saddr_format": "range", + "src_ip1": "1001:da8:205:1::101", + "src_ip2": "1001:da8:205:1::201", + "sport_format": "mask", + "src_port1": "5210", + "src_port2": "65520", + "daddr_format": "mask", + "dst_ip1": "3001:da8:205:1::401", + "dst_ip2": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:0000", + "dport_format": "mask", + "dst_port1": "0", + "dst_port2": "65535", + "protocol": 6, + "direction": "double" + } + } + ], + "not_flag" : 0 + } + ] + }, + { + "compile_id": 156, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "ExprPlusWithHex", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_SIGNATURE", + "table_type": "expr_plus", + "table_content": { + "district": "Content-Type", + "keywords": "2f68746d6c", + "expr_type": "none", + "match_method": "sub", + "format": "hexbin" + } + } + ] + } + ] + }, + { + "compile_id": 157, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "StringScan.StreamScanUTF8", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_type": "expr", + "table_name": "TROJAN_PAYLOAD", + "table_content": { + "keywords": "鎴戠殑璁㈠崟", + "expr_type": "none", + "format": "none", + "match_method": "sub" + } + } + ] + } + ] + }, + { + "compile_id": 158, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "IPScan.IPv4_CIDR", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv4", + "saddr_format": "CIDR", + "src_ip1": "192.168.0.1", + "src_ip2": "32", + "sport_format": "range", + "src_port1": "5210", + "src_port2": "5211", + "daddr_format": "CIDR", + "dst_ip1": "10.0.6.1", + "dst_ip2": "24", + "dport_format": "mask", + "dst_port1": "0", + "dst_port2": "65535", + "protocol": 6, + "direction": "double" + } + } + ], + "not_flag" : 0 + } + ] + }, + { + "compile_id": 159, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "IPScan.IPv6_CIDR", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv6", + "saddr_format": "CIDR", + "src_ip1": "2001:db8::", + "src_ip2": "120", + "sport_format": "mask", + "src_port1": "5210", + "src_port2": "65520", + "daddr_format": "CIDR", + "dst_ip1": "2001:4860:4860::8888", + "dst_ip2": "65", + "dport_format": "mask", + "dst_port1": "0", + "dst_port2": "65535", + "protocol": 6, + "direction": "double" + } + } + ], + "not_flag" : 0 + } + ] + }, + { + "compile_id": 160, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "VirtualWithOnePhysical", + "is_valid": "yes", + "groups": [ + { + "group_name":"TakeMeHome", + "virtual_table":"HTTP_RESPONSE_KEYWORDS", + "not_flag" : 0 + }, + { + "not_flag":0, + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "https://blog.csdn.net/littlefang/article/details/8213058", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 161, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "virtual_table_test_temp", + "is_valid": "yes", + "groups": [ + { + "group_name":"vt_grp_http_sig1", + "not_flag":0, + "regions": [ + { + "table_name": "HTTP_SIGNATURE", + "table_type": "expr_plus", + "table_content": { + "district": "User-Agent", + "keywords": "Chrome/78.0.3904.108", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + }, + { + "group_name":"vt_grp_http_sig2", + "not_flag":0, + "regions": [ + { + "table_name": "HTTP_SIGNATURE", + "table_type": "expr_plus", + "table_content": { + "district": "Cookie", + "keywords": "uid=12345678", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + }, + { + "table_name": "HTTP_SIGNATURE", + "table_type": "expr_plus", + "table_content": { + "district": "Cookie", + "keywords": "sessionid=888888", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 162, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "VirtualWithVirtual", + "is_valid": "yes", + "groups": [ + { + "group_name":"vt_grp_http_sig1", + "virtual_table":"HTTP_REQUEST_HEADER", + "not_flag":0 + }, + { + "group_name":"vt_grp_http_sig2", + "virtual_table":"HTTP_RESPONSE_HEADER", + "not_flag":0 + } + ] + }, + { + "compile_id": 163, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "OneGroupInTwoVirtual", + "is_valid": "yes", + "groups": [ + { + "group_name":"vt_grp_http_sig2", + "virtual_table":"HTTP_REQUEST_HEADER", + "not_flag":0 + }, + { + "group_name":"vt_grp_http_sig2", + "virtual_table":"HTTP_RESPONSE_HEADER", + "not_flag":0 + } + ] + }, + { + "compile_id": 164, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "CharsetWindows1251", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "KEYWORDS_TABLE", + "table_type": "expr", + "table_content": { + "keywords": ">袟袗袨\\b芦小械胁械褉谐邪蟹胁褌芯屑邪褌懈泻邪\\b袗泄小禄<", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 165, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "EvaluationOrder", + "is_valid": "yes", + "evaluation_order":"2.111", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "cavemancircus.com/", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + }, + { + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv4", + "saddr_format": "CIDR", + "src_ip1": "192.168.23.1", + "src_ip2": "24" + } + } + ], + "not_flag" : 0 + } + ] + }, + { + "compile_id": 166, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "EvaluationOrder", + "is_valid": "yes", + "evaluation_order":"100.233", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "2019/12/27/pretty-girls-6", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 167, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "EvaluationOrder", + "is_valid": "yes", + "evaluation_order":"300.999", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "2019/12/27", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 168, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "EvaluationOrder", + "is_valid": "yes", + "evaluation_order":"0", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "2019/12/27", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + }, + { + "compile_id": 169, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "IPScan.IPv4_Any", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv4", + "saddr_format": "CIDR", + "src_ip1": "0.0.0.0", + "src_ip2": "0", + "sport_format": "mask", + "src_port1": "20304", + "src_port2": "65535", + "daddr_format": "CIDR", + "dst_ip1": "0.0.0.0", + "dst_ip2": "0", + "dport_format": "range", + "dst_port1": "0", + "dst_port2": "0", + "protocol": 6, + "direction": "single" + } + } + ], + "not_flag" : 0 + } + ] + }, + { + "compile_id": 170, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "IPScan.IPv4_virtual.source", + "is_valid": "no", + "groups": [ + { + "group_name": "ipv4_virtual.source", + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv4", + "saddr_format": "CIDR", + "src_ip1": "192.168.40.10", + "src_ip2": "32", + "sport_format": "mask", + "src_port1": "443", + "src_port2": "65535", + "daddr_format": "CIDR", + "dst_ip1": "0.0.0.0", + "dst_ip2": "0", + "dport_format": "range", + "dst_port1": "0", + "dst_port2": "0", + "protocol": 6, + "direction": "double" + } + } + ], + "not_flag" : 0 + } + ] + }, + { + "compile_id": 171, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "IPScan.IPv4_virtual.destination", + "is_valid": "no", + "groups": [ + { + "group_name": "ipv4_virtual.destination", + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv4", + "saddr_format": "CIDR", + "src_ip1": "192.168.231.46", + "src_ip2": "32", + "sport_format": "mask", + "src_port1": "25705", + "src_port2": "65535", + "daddr_format": "CIDR", + "dst_ip1": "0.0.0.0", + "dst_ip2": "0", + "dport_format": "range", + "dst_port1": "0", + "dst_port2": "0", + "protocol": 6, + "direction": "double" + } + } + ], + "not_flag" : 0 + } + ] + }, + { + "compile_id": 172, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "ipv4_virtual.match", + "is_valid": "yes", + "groups": [ + { + "group_name":"ipv4_virtual.source", + "virtual_table":"VIRTUAL_IP_PLUS_SOURCE", + "not_flag":0 + }, + { + "group_name":"ipv4_virtual.destination", + "virtual_table":"VIRTUAL_IP_PLUS_DESTINATION", + "not_flag":0 + } + ] + }, + { + "compile_id": 173, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "IPScan.IPv4_composition.source", + "is_valid": "no", + "groups": [ + { + "group_name": "ipv4_composition.source", + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv4", + "saddr_format": "CIDR", + "src_ip1": "192.168.40.11", + "src_ip2": "32", + "sport_format": "mask", + "src_port1": "443", + "src_port2": "65535", + "daddr_format": "CIDR", + "dst_ip1": "0.0.0.0", + "dst_ip2": "0", + "dport_format": "range", + "dst_port1": "0", + "dst_port2": "0", + "protocol": 6, + "direction": "double" + } + } + ], + "not_flag" : 0 + } + ] + }, + { + "compile_id": 174, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "IPScan.IPv4_composition.destination", + "is_valid": "no", + "groups": [ + { + "group_name": "ipv4_composition.destination", + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv4", + "saddr_format": "CIDR", + "src_ip1": "192.168.231.47", + "src_ip2": "32", + "sport_format": "mask", + "src_port1": "25715", + "src_port2": "65535", + "daddr_format": "CIDR", + "dst_ip1": "0.0.0.0", + "dst_ip2": "0", + "dport_format": "range", + "dst_port1": "0", + "dst_port2": "0", + "protocol": 6, + "direction": "double" + } + } + ], + "not_flag" : 0 + } + ] + }, + { + "compile_id": 175, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "ipv4_composition.match", + "is_valid": "yes", + "groups": [ + { + "group_name":"ipv4_composition.source", + "virtual_table":"COMPOSITION_IP_SOURCE", + "not_flag":0 + }, + { + "group_name":"ipv4_composition.destination", + "virtual_table":"COMPOSITION_IP_DESTINATION", + "not_flag":0 + } + ] + }, + { + "compile_id": 176, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "IPScan.IPv4_composition.session", + "is_valid": "no", + "groups": [ + { + "group_name": "ipv4_composition.session", + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv4", + "saddr_format": "CIDR", + "src_ip1": "192.168.40.11", + "src_ip2": "2", + "sport_format": "mask", + "src_port1": "443", + "src_port2": "65535", + "daddr_format": "CIDR", + "dst_ip1": "192.168.231.47", + "dst_ip2": "32", + "dport_format": "range", + "dst_port1": "25715", + "dst_port2": "25715", + "protocol": 6, + "direction": "single" + } + } + ], + "not_flag" : 0 + } + ] + }, + { + "compile_id": 177, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "ipv4_composition.session.match", + "is_valid": "yes", + "groups": [ + { + "group_name":"ipv4_composition.session", + "virtual_table":"COMPOSITION_IP_SESSION", + "not_flag":0, + "clause_index":1 + + } + ] + }, + { + "compile_id": 178, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "Hierarchy.TwoVirtualInOneClause", + "is_valid": "yes", + "groups": [ + { + "group_name":"ASN1234", + "virtual_table":"SOURCE_IP_ASN", + "not_flag":0, + "clause_index":0 + }, + { + "group_name":"financial-department-ip", + "virtual_table":"null", + "not_flag":0, + "clause_index":0 + }, + { + "group_name":"Country-Sparta-IP", + "virtual_table":"SOURCE_IP_GEO", + "not_flag":0, + "clause_index":0 + }, + { + "group_name":"ASN2345", + "virtual_table":"DESTINATION_IP_ASN", + "not_flag":0, + "clause_index":1 + } + ] + }, + { + "compile_id": 179, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "anything", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "INTERGER_PLUS", + "table_type": "interval_plus", + "table_content": { + "district": "interval.plus", + "low_boundary": 2020, + "up_boundary": 2020 + } + } + ] + } + ] + }, + { + "compile_id": 180, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "Hierarchy_VirtualWithTwoPhysical", + "is_valid": "yes", + "groups": [ + { + "group_name":"FQDN_OBJ1", + "virtual_table":"VIRTUAL_SSL_SNI", + "not_flag" : 0, + "clause_index":0 + }, + { + "group_name":"FQDN_CAT1", + "virtual_table":"VIRTUAL_SSL_SNI", + "not_flag" : 0, + "clause_index":0 + } + ] + }, + { + "compile_id": 181, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "ipv4_composition.match", + "is_valid": "yes", + "groups": [ + { + "group_name":"IPv4-composition-source-only", + "virtual_table":"COMPOSITION_IP_SOURCE", + "not_flag":0 + } + ] + }, + { + "compile_id": 182, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "8-expr", + "is_valid": "yes", + "groups": [ + { + "regions": [ + { + "table_name": "KEYWORDS_TABLE", + "table_type": "expr", + "table_content": { + "keywords": "string1&string2&string3&string4&string5&string6&string7&string8", + "expr_type": "and", + "match_method": "expr", + "format": "uncase plain" + } + } + ] + } + ] + }, + + { + "compile_id": 184, + "user_region": "APP_ID=6006740;Liumengyan-Bugreport-20210515", + "description": "Hulu", + "is_valid": "yes", + "do_blacklist": 0, + "do_log": 0, + "action": 0, + "service": 0, + "groups": [ + { + "group_name": "Untitled", + "regions": [ + + { + "table_name": "IP_CONFIG", + "table_type": "ip", + "table_content": { + "protocol": 0, + "addr_type": "ipv6", + "direction": "double", + "src_ip": "::", + "dst_ip": "2620:100:3000::", + "src_port": "0", + "dst_port": "0", + "mask_src_port": "65535", + "mask_src_ip": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + "mask_dst_port": "65535", + "mask_dst_ip": "ffff:ffff:ff00:0000:0000:0000:0000:0000" + } + } + ] + } + ] + }, + { + "compile_id": 185, + "service": 0, + "action": 0, + "do_blacklist": 0, + "do_log": 0, + "effective_rage": 0, + "user_region": "ipv4_composition.NOT_match", + "is_valid": "yes", + "groups": [ + { + "group_name":"IPv4-composition-NOT-client-ip", + "virtual_table":"COMPOSITION_IP_SOURCE", + "not_flag":0 + }, + { + "group_name":"IPv4-composition-NOT-server-ip", + "virtual_table":"COMPOSITION_IP_DESTINATION", + "not_flag":1 + } + ] + }, + { + "compile_id": 186, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "NOTLogic.ScanHitAtLast", + "is_valid": "yes", + "groups": [ + { + "not_flag": 1, + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "must-not-contained-string-of-rule-186", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + }, + { + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv4", + "saddr_format": "range", + "src_ip1": "10.0.8.186", + "src_ip2": "10.0.8.186", + "sport_format": "range", + "src_port1": "18611", + "src_port2": "18611", + "daddr_format": "range", + "dst_ip1": "10.0.8.20", + "dst_ip2": "10.0.8.20", + "dport_format": "range", + "dst_port1": "80", + "dst_port2": "80", + "protocol": 6, + "direction": "single" + } + } + ], + "not_flag": 0 + } + ] + }, + { + "compile_id": 187, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "NOTLogic.ScanHitAtLast", + "is_valid": "yes", + "groups": [ + { + "not_flag": 1, + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "must-not-contained-string-of-rule-187", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + }, + { + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv4", + "saddr_format": "range", + "src_ip1": "10.0.8.187", + "src_ip2": "10.0.8.187", + "sport_format": "range", + "src_port1": "18611", + "src_port2": "18611", + "daddr_format": "range", + "dst_ip1": "10.0.8.20", + "dst_ip2": "10.0.8.20", + "dport_format": "range", + "dst_port1": "80", + "dst_port2": "80", + "protocol": 6, + "direction": "single" + } + } + ], + "not_flag": 0 + } + ] + }, + { + "compile_id": 188, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "NOTLogic.ScanHitAtLast", + "is_valid": "yes", + "groups": [ + { + "not_flag": 1, + "regions": [ + { + "table_name": "HTTP_URL", + "table_type": "expr", + "table_content": { + "keywords": "must-not-contained-string-of-rule-188", + "expr_type": "none", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + }, + { + "regions": [ + { + "table_type": "ip_plus", + "table_name": "IP_PLUS_CONFIG", + "table_content": { + "addr_type": "ipv4", + "saddr_format": "range", + "src_ip1": "10.0.8.188", + "src_ip2": "10.0.8.188", + "sport_format": "range", + "src_port1": "18611", + "src_port2": "18611", + "daddr_format": "range", + "dst_ip1": "10.0.8.20", + "dst_ip2": "10.0.8.20", + "dport_format": "range", + "dst_port1": "80", + "dst_port2": "80", + "protocol": 6, + "direction": "single" + } + } + ], + "not_flag": 0 + } + ] + }, + { + "is_valid": "yes", + "do_log": 0, + "effective_rage": 0, + "action": 0, + "compile_id": 189, + "service": 0, + "do_blacklist": 0, + "user_region": "StringScan.ShouldNotHitExprPlus", + "groups": [ + { + "regions": [ + { + "table_name": "APP_PAYLOAD", + "table_content": { + "format": "hexbin", + "match_method": "sub", + "district": "tcp.payload.c2s_first_data", + "keywords": "ab00", + "expr_type": "none" + }, + "table_type": "expr_plus" + } + ], + "group_name": "Untitled" + } + ] + }, + { + "compile_id": 190, + "service": 1, + "action": 1, + "do_blacklist": 1, + "do_log": 1, + "user_region": "StringScan.ExprPlus", + "is_valid": "yes", + "groups": [ + { + "group_name": "Untitled", + "regions": [ + { + "table_name": "HTTP_SIGNATURE", + "table_type": "expr_plus", + "table_content": { + "district": "鎴戠殑DistrIct", + "keywords": "addis&sapphire", + "expr_type": "and", + "match_method": "sub", + "format": "uncase plain" + } + } + ] + } + ] + } + ], + "plugin_table": [ + { + "table_name": "QD_ENTRY_INFO", + "table_content": [ + "1\t192.168.0.1\t101\t1", + "2\t192.168.0.2\t101\t1", + "3\t192.168.1.1\t102\t1" + ] + }, + { + "table_name": "TEST_PLUGIN_TABLE", + "table_content": [ + "1\t3388\t99\t1", + "2\t3355\t66\t1", + "3\tcccc\t11\t1" + ] + }, + { + "table_name": "TEST_EFFECTIVE_RANGE_TABLE", + "table_content": [ + "1\tSUCCESS\t99\t1\t{\"tag_sets\":[[{\"tag\":\"location\",\"value\":[\"鍖椾含/鏈濋槼/鍗庝弗鍖楅噷\"]},{\"tag\":\"isp\",\"value\":[\"鐢典俊\",\"绉诲姩\"]}]]}\t1111", + "2\tSUCCESS\t66\t1\t0\t222", + "3\tFAILED\t11\t1\t{\"tag_sets\":[[{\"tag\":\"location\",\"value\":[\"鍖椾含/鏈濋槼/鍗庝弗鍖楅噷\",\"涓婃捣/娴︿笢/闄嗗鍢碶"]},{\"tag\":\"isp\",\"value\":[\"鐢典俊\",\"鑱旈歕"]}],[{\"tag\":\"location\",\"value\":[\"鍖椾含\"]},{\"tag\":\"isp\",\"value\":[\"鑱旈歕"]}]]}\t333", + "4\tSUCCESS\t66\t1\t{}\t444", + "5\tSUCCESS\t66\t1\t{\"tag_sets\":[[{\"tag\":\"location\",\"value\":[\"鍖椾含\"]}]]}\t444", + "6\tSUCCESS\t66\t1\t{\"tag_sets\":[[{\"tag\":\"weather\",\"value\":[\"hot\"]}]]}\t444" + ] + }, + { + "table_name": "IR_INTERCEPT_IP", + "table_content": [ + "1000000130\t1000000130\t4\t192.168.10.99\t255.255.255.255\t0\t65535\t0.0.0.0\t255.255.255.255\t0\t65535\t0\t1\t1\t96\t1\tuser_region\t{}\t2019/1/24/18:0:34", + "161\t161\t4\t0.0.0.0\t255.255.255.255\t0\t65535\t61.135.169.121\t255.255.255.255\t0\t65535\t0\t0\t1\t96\t832\t0\t0\t2019/1/24/18:48:42" + ] + }, + { + "table_name": "TEST_IP_PLUGIN_WITH_EXDATA", + "table_content": [ + "101\t4\t192.168.30.99\t192.168.30.101\tSomething-like-json\t1", + "102\t4\t192.168.30.90\t192.168.30.128\tBigger-range-should-in-the-back\t1", + "103\t6\t2001:db8:1234::\t2001:db8:1235::\tBigger-range-should-in-the-back\t1", + "104\t6\t2001:db8:1234::1\t2001:db8:1234::5210\tSomething-like-json\t1", + "105\t6\t2620:100:3000::\t2620:0100:30ff:ffff:ffff:ffff:ffff:ffff\tBugreport-liumengyan-20210517\t1" + ] + }, + { + "table_name": "TEST_FQDN_PLUGIN_WITH_EXDATA", + "table_content": [ + "201\t0\twww.example1.com\tcatid=1\t1", + "202\t1\t.example1.com\tcatid=1\t1", + "203\t0\tnews.example1.com\tcatid=2\t1", + "204\t0\tr3---sn-i3belne6.example2.com\tcatid=3\t1", + "205\t0\tr3---sn-i3belne6.example2.com\tcatid=3\t1" + ] + }, + { + "table_name":"TEST_BOOL_PLUGIN_WITH_EXDATA", + "table_content": [ + "301\t1&2&1000\ttunnel1\t1", + "302\t101&102\ttunnel2\t1", + "303\t102\ttunnel3\t1", + "304\t101\ttunnel4\t1", + "305\t0&1&2&3&4&5&6&7\ttunnel5\t1", + "306\t101&101\tinvalid\t1" + ] + } + ] +} diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt new file mode 100644 index 0000000..e1df602 --- /dev/null +++ b/tools/CMakeLists.txt @@ -0,0 +1,8 @@ +include_directories(${PROJECT_SOURCE_DIR}/src/inc_internal) +include_directories(${PROJECT_SOURCE_DIR}/deps) +include_directories(${PROJECT_SOURCE_DIR}/scanner) + +add_executable(maat_redis_tool maat_redis_tool.cpp) +target_link_libraries(maat_redis_tool maat_frame_static) + +install(TARGETS maat_redis_tool DESTINATION /usr/local/bin/ COMPONENT TOOLS) \ No newline at end of file diff --git a/tools/maat_redis_tool.cpp b/tools/maat_redis_tool.cpp new file mode 100644 index 0000000..7b969fa --- /dev/null +++ b/tools/maat_redis_tool.cpp @@ -0,0 +1,394 @@ +#include +#include +#include +#include +#include +#include + +#include "utils.h" +#include "maat_rule.h" +#include "maat_utils.h" +#include "maat_command.h" +#include "cJSON/cJSON.h" +#include "maat_config_monitor.h" +#include "json2iris.h" +#include "hiredis/hiredis.h" + +const char *redis_dump_dir = "./redis_dump"; +const char *default_table_info = "./table_info.conf"; + +void maat_tool_print_usage(void) +{ + printf("maat_redis_tool manipulate rules from redis.\n"); + printf("Usage:\n"); + printf("\t-h [host], redis IP, 127.0.0.1 as default.\n"); + printf("\t-p [port], redis port, 6379 as default.\n"); + printf("\t-n [db], redis db, 0 as default.\n"); + printf("\t-d [dir], dump rules from redis to [dir], %s as default.\n",redis_dump_dir); + printf("\t-v [version], dump specific [version] from redis, dump latest version as default.\n"); + printf("\t-j [payload.json], add or delete rules as maat json. Must have field compile_table field, and plugin table's valid flag must be in the last column.\n"); + printf("\t-t [timeout], timeout config after t seconds, default is 0 which means never timeout.\n"); + printf("example: ./maat_redis_tool -h 127.0.0.1 -p 6379 -d %s\n",redis_dump_dir); + printf(" ./maat_redis_tool -h 127.0.0.1 -p 6379 -j payload.json -t 300\n"); +} + +static int compare_serial_rule(const void *a, const void *b) +{ + struct serial_rule *ra=(struct serial_rule *)a; + struct serial_rule *rb=(struct serial_rule *)b; + + int ret = strcmp(ra->table_name, rb->table_name); + if (0 == ret) { + ret = ra->rule_id - rb->rule_id; + } + + return ret; +} + +int set_file_rulenum(const char *path, int rule_num) +{ + FILE* fp=NULL; + if (0 == rule_num) { + fp = fopen(path, "w"); + } else { + fp = fopen(path, "r+"); + } + + if (NULL == fp) { + fprintf(stderr, "fopen %s failed %s at set rule num.", path, strerror(errno)); + return -1; + } + + fprintf(fp, "%010d\n", rule_num); + fclose(fp); + + return 0; +} + +void read_rule_from_redis(redisContext *c, long long desire_version, const char* output_path) +{ + struct serial_rule *rule_list; + int line_count=0; + int i=0,ret=0; + int update_type = CM_UPDATE_TYPE_INC; + long long version=0; + const char* cur_table=NULL; + char foreign_files_dir[256] = {0}; + char table_path[256],index_path[256]; + FILE *table_fp=NULL, *index_fp=NULL; + + int rule_num = maat_cmd_get_rm_key_list(c, 0, desire_version, &version, NULL, &rule_list, &update_type, 0); + if (0 == rule_num) { + if (desire_version != 0) { + printf("Read desired version %lld failed.\n", desire_version); + } else { + printf("No Effective Rules.\n"); + } + return; + } + + if (rule_num < 0) { + printf("Read Redis Error.\n"); + return; + } + + assert(update_type == CM_UPDATE_TYPE_FULL); + printf("MAAT Version: %lld, key number: %d\n", version, rule_num); + if (0 == rule_num) { + goto clean_up; + } + + printf("Reading value: \n"); + ret = maat_cmd_get_redis_value(c, rule_list, rule_num, 1); + if (ret < 0) { + goto clean_up; + } + + printf("Sorting.\n"); + qsort(rule_list, rule_num, sizeof(struct serial_rule), compare_serial_rule); + if ((access(output_path, F_OK)) <0) { + if ((mkdir(output_path, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)) < 0) { + printf("mkdir %s error\n", output_path); + } + } + + snprintf(foreign_files_dir, sizeof(foreign_files_dir), "%s/foreign_files/",output_path); + + if ((access(foreign_files_dir, F_OK)) <0) { + if((mkdir(foreign_files_dir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)) < 0) { + printf("mkdir %s error\n", foreign_files_dir); + } + } + + ret = maat_cmd_get_foreign_keys_by_prefix(c, rule_list, rule_num, foreign_files_dir); + if (ret > 0) { + printf("%d lines has foreign content.\n", ret); + maat_cmd_get_foreign_conts(c, rule_list, rule_num, 1); + } + + snprintf(index_path,sizeof(index_path),"%s/full_config_index.%020lld",output_path,version); + index_fp = fopen(index_path, "w"); + if (NULL == index_fp) { + printf("Open %s failed.\n",index_path); + goto clean_up; + } + + for (i = 0; i < rule_num; i++) { + if (rule_list[i].n_foreign > 0) { + maat_cmd_rewrite_table_line_with_foreign(rule_list+i); + } + + if (NULL == cur_table || 0 != strcmp(cur_table,rule_list[i].table_name)) { + if (table_fp != NULL) { + fprintf(index_fp, "%s\t%d\t%s\n", cur_table, line_count, table_path); + fclose(table_fp); + table_fp = NULL; + set_file_rulenum(table_path, line_count); + line_count = 0; + printf("Written table %s\n",table_path); + } + + snprintf(table_path, sizeof(table_path), "%s/%s.%020lld", output_path, + rule_list[i].table_name, version); + set_file_rulenum(table_path, 0); + + table_fp = fopen(table_path, "a"); + if (NULL == table_fp) { + printf("Open %s failed.\n", table_path); + goto clean_up; + } + + cur_table = rule_list[i].table_name; + } + fprintf(table_fp, "%s\tkey=%ld\n", rule_list[i].table_line, rule_list[i].rule_id); + line_count++; + } + + fclose(table_fp); + table_fp = NULL; + + fprintf(index_fp, "%s\t%d\t%s\n", cur_table, line_count, table_path); + set_file_rulenum(table_path, line_count); + printf("Written table %s\n", table_path); + printf("Written complete: %s\n", index_path); + +clean_up: + for (i = 0; i < rule_num; i++) { + maat_cmd_empty_serial_rule(rule_list+i); + } + + free(rule_list); + rule_list = NULL; + + if (c != NULL) { + redisFree(c); + } + + if (index_fp != NULL) { + fclose(index_fp); + } + + if (table_fp != NULL) { + fclose(table_fp); + } +} + +int count_line_num(const char *table_name,const char *line, void *u_para) +{ + (*((unsigned int *)u_para))++; + return 0; +} + +int line_idx = 0; +long long absolute_expire_time=0; +int make_serial_rule(const char *table_name, const char *line,void *u_para) +{ + struct serial_rule *s_rule=(struct serial_rule *)u_para; + int rule_id=0,is_valid=0; + char *buff = ALLOC(char, strlen(line) + 1); + + memcpy(buff, line, strlen(line) + 1); + + while (buff[strlen(buff) - 1] == '\n' || buff[strlen(buff) - 1] == '\t') { + buff[strlen(buff) - 1] = '\0'; + } + + int j = 0; + char *str1 = NULL; + char *token = NULL; + char *saveptr1 = NULL; + char *last1 =NULL; + for (j = 0,str1 = buff; ; j++, str1 = NULL) { + token = strtok_r(str1, "\t ", &saveptr1); + if (token == NULL) + break; + if(j==0) + { + sscanf(token,"%d",&rule_id); + } + last1=token; + } + sscanf(last1,"%d",&is_valid); + + memcpy(buff,line, strlen(line)+1); + while(buff[strlen(buff)-1]=='\n'||buff[strlen(buff)-1]=='\t') + { + buff[strlen(buff)-1]='\0'; + } + //printf("Writing table %s, rule_id=%d, timeout=%lld, is_valid=%d\n", table_name, rule_id,absolute_expire_time,is_valid); + maat_cmd_set_serial_rule(s_rule + line_idx, (enum maat_operation)is_valid, rule_id, table_name, buff, absolute_expire_time); + line_idx++; + free(str1); + + return 0; +} + +#define WORK_MODE_DUMP 0 +#define WORK_MODE_JSON 1 +int main(int argc, char * argv[]) +{ + int oc=0,ret=0; + int model=0; + char redis_ip[64]; + int redis_port=6379; + int redis_db=0; + strncpy(redis_ip, "127.0.0.1", sizeof(redis_ip)); + char dump_dir[128], json_file[128], tmp_iris_path[128]; + strncpy(dump_dir,redis_dump_dir,sizeof(dump_dir)); + redisContext * ctx=NULL; + unsigned int total_line_cnt=0, success_cnt=0, i=0; + int timeout=0; + FILE* json_fp=NULL; + cJSON *json=NULL, *tmp_obj=NULL; + struct stat fstat_buf; + unsigned long json_file_size=0,read_size=0; + long long desired_version=0; + char* json_buff=NULL; + size_t json_buff_sz=0; + char compile_table_name[128] = {0}; + + while ((oc = getopt(argc,argv,"h:p:n:d:v:f:j:t:")) != -1) { + switch (oc) { + case 'h': + strncpy(redis_ip, optarg, sizeof(redis_ip)); + break; + case 'p': + sscanf(optarg, "%d", &redis_port); + break; + case 'n': + sscanf(optarg, "%d", &redis_db); + break; + case 'd': + model = WORK_MODE_DUMP; + strncpy(dump_dir, optarg, sizeof(dump_dir)); + if (dump_dir[strlen(dump_dir)-1] == '/') { + dump_dir[strlen(dump_dir)-1] = '\0'; + } + break; + case 'v': + sscanf(optarg, "%lld", &desired_version); + break; + case 'j': + strncpy(json_file, optarg, sizeof(json_file)); + model = WORK_MODE_JSON; + ret = stat(json_file, &fstat_buf); + if (ret != 0) { + printf("fstat file %s error.\n", json_file); + return -1; + } + + json_fp = fopen(json_file, "r"); + if (NULL == json_fp) { + printf("fopen file %s error %s.\n", json_file, strerror(errno)); + return -1; + } + + json_file_size = fstat_buf.st_size; + json_buff = ALLOC(char, json_file_size); + read_size = fread(json_buff, 1, json_file_size, json_fp); + if (read_size != json_file_size) { + printf("fread file %s error.\n", json_file); + return -1; + } + + json = cJSON_Parse(json_buff); + if (!json) { + printf("%s format is error before: %-200.200s\n", json_file, cJSON_GetErrorPtr()); + return -1; + } + + tmp_obj=cJSON_GetObjectItem(json, "compile_table"); + if (NULL == tmp_obj) { + printf("No \"compile_table\" in %s.\n",json_file); + return -1; + } + strncpy(compile_table_name, tmp_obj->valuestring, sizeof(compile_table_name)); + free(json_buff); + cJSON_Delete(json); + fclose(json_fp); + break; + case 't': + sscanf(optarg,"%d",&timeout); + break; + case '?': + default: + maat_tool_print_usage(); + return 0; + break; + } + } + + ctx = maat_cmd_connect_redis(redis_ip, redis_port, redis_db); + if (NULL == ctx) { + return -1; + } + + if (model == WORK_MODE_DUMP) { + printf("Reading key list from %s:%d db%d.\n", redis_ip, redis_port, redis_db); + read_rule_from_redis(ctx, desired_version, dump_dir); + } else if(model == WORK_MODE_JSON) { + ret = load_file_to_memory(json_file, (unsigned char**)&json_buff, &json_buff_sz); + if (ret < 0) { + printf("open %s failed.\n", json_file); + } + + ret = json2iris(json_buff, json_file, NULL, NULL, NULL, ctx, tmp_iris_path, sizeof(tmp_iris_path), NULL, NULL); + if (ret < 0) { + printf("Invalid json format.\n"); + } + + total_line_cnt = 0; + config_monitor_traverse(0, tmp_iris_path, NULL, count_line_num, NULL, &total_line_cnt); + printf("Serialize %s to %d lines, write temp file to %s .\n",json_file, total_line_cnt, tmp_iris_path); + + struct serial_rule *s_rule = ALLOC(struct serial_rule, total_line_cnt); + long long server_time = maat_cmd_redis_server_time_s(ctx); + if (!server_time) { + printf("Get Redis Time failed.\n"); + } + + if (timeout > 0) { + absolute_expire_time = server_time + timeout; + } + + config_monitor_traverse(0, tmp_iris_path, NULL, make_serial_rule, NULL, s_rule); + printf("Timeout = %lld\n", absolute_expire_time); + + ret = 0; + do { + success_cnt = maat_cmd_exec_serial_rule(ctx, s_rule, total_line_cnt, server_time); + } while(success_cnt < 0); + + if (success_cnt != total_line_cnt) { + printf("Only Add %d of %d, rule id maybe conflicts.\n", success_cnt, total_line_cnt); + } + + for (i = 0; i < total_line_cnt; i++) { + maat_cmd_empty_serial_rule(s_rule+i); + } + free(s_rule); + redisFree(ctx); + } + + return 0; +} \ No newline at end of file diff --git a/vendor/CMakeLists.txt b/vendor/CMakeLists.txt index 8daca01..aa549f0 100644 --- a/vendor/CMakeLists.txt +++ b/vendor/CMakeLists.txt @@ -70,3 +70,16 @@ add_library(ipmatcher-static STATIC IMPORTED GLOBAL) add_dependencies(ipmatcher-static ipmatcher) set_property(TARGET ipmatcher-static PROPERTY IMPORTED_LOCATION ${INSTALL_DIR}/lib/ipmatcher.a) set_property(TARGET ipmatcher-static PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${INSTALL_DIR}/include) + +# hiredis-1.1.0 +ExternalProject_Add(hiredis PREFIX hiredis + URL ${CMAKE_CURRENT_SOURCE_DIR}/hiredis-1.1.0.tar.gz + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${VENDOR_BUILD} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_C_FLAGS="-fPIC") + +ExternalProject_Get_Property(hiredis INSTALL_DIR) +file(MAKE_DIRECTORY ${VENDOR_BUILD}/include) + +add_library(hiredis-static STATIC IMPORTED GLOBAL) +add_dependencies(hiredis-static hiredis) +set_property(TARGET hiredis-static PROPERTY IMPORTED_LOCATION ${VENDOR_BUILD}/lib64/libhiredisd.a) +set_property(TARGET hiredis-static PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${VENDOR_BUILD}/include) \ No newline at end of file diff --git a/vendor/hiredis-1.1.0.tar.gz b/vendor/hiredis-1.1.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..edafe2c39413329826203d640a2fd71f0a13dc0b GIT binary patch literal 122002 zcmV(rK<>XEiwFP!000001MFN|bKAI*o@f0E9N9IF6I0a9vaK0US(cT=JF=yf>?Bk1 ztOAh`#fT(0yvX*hHsW~0&F_`1;m zW#gNHX3!xv24jL%;(Yh`R}rpSt;YX{EB!w-R=+D%E0t=kRH>91U8%ZT+WSuX3bM>y zzUyHJx^yV@meH+hPY>=-a_yFpLQRuc-l0;e?(OXktE9RQwjjlFt*ji3O11s{{k`42 z{ZSQHYWu@oHTSJ$e!d>>8tV>Gbyq4CN`<2Qb^2iQ0X%E*{D;q}`CkD9_-^;>h?Bhj z;`2Xg|AMA_V}JO_esB-_S9gog_J4{i+x`wQ2zH62I(Xy}M^dOY(zU{^Y22p|5&x@; z>|d(Y()KTxcB_@|oFqpi*J)`yG1S>sTQ3Wyx;6V6I^oN$ z1e>9+38J9`tj4kDWoVKLueO%=I{#RVWbW}1!Jh9dd^Bk0k13!PjXJs*lY~6oGnkr= z9fBvirJ?2+TN-f%FWy0X^uvHm;Z$3>1dspw?|=LcYTekx(OHg$4fI;{yoei#(Qc(! z{pY`Vv9F=m`psf~`b~@%m}>4u?A!6>n{*fd_+NCE zwq>S~g#zP_DxuAb(~EWs9c?x)FWz=~=*7n7`Q_PhtGB(CBR7iSt3;Yf3<9_l-IfF= zgMwr;Zhr@Yjg8Ql%uJijw~g~Rt$r?Op_b?9Lzo7R3wPd=EfpF1gdmS151yJJY#8u~ zK*l|zMAu`AuBkI2DxwxN>n;l5_iv(;&iO^}^!W0k(}SKKd;VwRKa*!<&};WmGJ$)< z|JrV~{2c$E;96z>p|2b2eFJ}REsy`DGNk+I_+PBlO3(5C2`(1@>ww{4WrQsidBpW{ z)T*P8w(pL2knJ1Bz#+f+aR1j_?lT>7r1Xcr<@o~T1&d35hu?5~%as@pK_xN#1g@;f z^@tq{4!#RP6Jf>EspZPbm?#tKdq>ksE+P@!Pc1MLpr=Y~VyVPZAle2bygvcY;idSF zfuxb!Z=&WgHJ4Bw4 zTL+@ED~ox89c8Rfi4@!}z@xE_i2`7rSIH24bal-lsx-Vkyc4rumq^U`ZoV+w;UEYQ zNK)Kw#DZWx9EwK>l8L9t*!Ez03m_U`~S4tvaCkJa~|5N=d!#Ads1 zYYtW+Z2=q^IRpkbM-oRQ_fC+spMs=-p-ENWAW0mm^9O5%;yk(LP~VQI^Ct{fsLe_r z=z>zi%`X-?1FD{pD&`Q6lpH++)pBXRB~ZbnCPP=PClSfS3U#GYIR;`1p$P=h~R4bIzgb->bvsUn}5rq|LwjqO3)S`KP zuc>^$>3R+Gghfw5=1w!Y2M1F zJ5?Bwq?qc_w6wl8;xa7o_PCUx(@SsAITuhm5uxk&^0a+2xcH^pI_!m|jC34JMjbIg zX|Aj=?*Vgy7DN$K4ojWf(Bx*$nL-mZZ#6J1YH*{aFh${QviMI53?K>n9!+QNKZh+r zF{dtOzovU)xkH@=lK}(}ohR=ahfKZ3q|Sznt9iLi2x?EnB+ zuq^D`viSkB0S1HE*nucE!oJ~gVjN_UA%j_PG_lJ& zi#8Q_^*JnV54g*vQppuN3L9bsF1n?@gDi66MMJYFJc6)`x{%VsUB06MHyi?C)6ocn zT#z;|gvzG^XKEUxr^V59cOmp|pckD$G26(siK35gkvLGp5ufP@I@w`;U{9a&8{tGO zpcA3WIT6@Qj39 zg2dDC_@XMEMA$A zgfp~-#mX&j2v>cASftb^o#xN2UOlK4V#C(0AWM%0oAj!|b_mml?&@B&a^qQd#s(`( zxfkuM@qpC0oN=kyvME;35cN5>Bw$j6>YsbUw{$PQ z*qS}Y`Phx*#s_Pqtd!ENqlQx>7*B}8;X-eT8lA^>0f@&NBrb-eSw6u-JwXOSLI2dLqu=18l;jfeW(PoO)dl=T zSUh#(ZDNhNWFGpNC8| z`Cw4g_*(AYZ1U_k-KPYyL^O?FHVt+jd7`ACan@&OurFVb)E1|eG6a%W&9{s?X9^0b z6&5*Bt;*5x)bfaNzcQ$)D=?$!$df@l-zs<9FRh|!2R2`-F z7Ukiw$-cIAFhRk)@lq_7lseD)iN?ZIxv?Gk8Oe6N$yZZ8Knh5BctVE0wv6up9NN>B zrR)Qm2lOIEx?j|XD15Hf!v-@#SWey%uF2jLw1IR8SS${H?I0I`lOXU4XodynEnB(< z6Sx3#30qrU#4*}47>#sA$4108*qnC7@n;e_9E`ytPHM~{(3}a`0wzM9lpmyPW zLeRKgNtx^CYOaX%U^DJooKq}U-g#dq3{8-Kem#CTm4_o*qcJ`pM6UFrLNTchdg--!R~H2?Zw_JOE|L-X-16!JpH3)l3E?8*=ukVSBS3(LS$>nJ9hE%v3mJoGsayjrF zSgQ;nfq)Dr7q9VC%eV~}A)sSTx3u7!=iJV|1e*Zji~Mgv<@!o4yxf#?xgbJ^eDo3e zi1M2ky~aDpoSU7q?rFQ#%cH*?qCBkHwd*|k^#_(j%(hLh*`gvmO%`mC!a&7g_~37F z4qvW89$G7aKNPgze~381cO>2XJ{_U#c2+D?gA?(bf-T~M9qx9YpQ#Cte2^QPIv!$s zwuhVD_b0OJDD_SDm9^T`RztQU{RdDdfwdGFxQe6OUbYT8{lR(Ttd&QHpaQVSyaL3_ z5S>T|%wqfv%z}v(CXQR6jwYXBrOUoVAEYvjwJep2g;GU&t83%FP8{7$Cv?{&ShUrQ56^EZcDoHC=PqrG~SzBMcL z&E1GIiwbPHV2vrEk zd|I4A1X0aOIXrBbpLs!e5OEl-=pJ&e==|4a;8GLXs+q zGy=?;#4xZU2e5Pmsh9k6JD)o0w#0F=2nCrGKb0lNk7dasmWL;@HvdTL>B~$}?^?a% zPQP_n%EjW%vQdYp>_DRAX;rwP@H&$8Zf+DSC^_&JP51r}OoA-znl|*%d zm5Hf^+EKxtB`Nh-0UsfeuiOwz$Kkp^N_!AbA%eTJ&=7nN1BAGeVIsUFaZXK2pM;Ch zo^>Ey*Q(OZeoabMegpK+CW+?&f_T;WI|((LhrnY1tQb4axKd`7Og4iun_j)T>!H1P z)BDqlw}aF3{zaqR9`tx1j~$?sQfK&LX6(OfqH?jc7qi)Q?7U^dMD8@Kq$O&p5;|*K z^iDqvTJIX|!#oUKF<5rl}?@zsebyr<{5l=ID!Zu?)+*l2qE=&gB}GMSV+QZ|J-g^hl7RHx02-J0u=E6#{tzEBa3% z1QSakJIEr}2)nnI0x=h&EZYfS`%f5Y4#6IzB-Sl}p9UGh4J=x7xtA}`I~T2&FY74k zNx>Gf=ryeHJm0aUS$hr9Rsx`FI2iZQUjc z1t&YmR&j!28vzEs_;|HZf|;;}Mc6SMnbB@<2Vy3FEhiKdWD5$p?3ZXf(3fGjVm;iX zCTLh~INB8CHFaf-i6LnYe>o{rgcQwoByx2fCkjNa7C~f**%XZotbr^e;xK;zA#Cj7 zVm2?)v>ocGb_X5Rc!zAikiVuROn!dZZVNp$mEA7SlwU3S5u-5}^e~?x%|L z)J+Y!jvEf2_{dShP1Kx3k7V~>CS=|>s=w6vSVv|7W*?zT#y{4bhp^Ro=C)pVr^M`VB|Y@jSxG_Lj;Gy)X$xTSZKp)Q6GZI zJ-%ISbL2s%-Dz!y{jDMZbKepNwRLdsk4L}z?6CXf8oOSmCSvUo7#0uK$0nfVGoK@M zFhJSRhhv7w@F=+^dM2}=O-%oTVs+gffi&vS(@jEsFN$TL647-a$-Oa9KvMAzkW@V2 zQn|#7PJ%L5m=Xspq3}}*uhxty3v;DK0J7AF|c+kRS+FDi?_*ejF{9h0l24ELQi{AAfak;oM<90h7m%CJ#t( zCgw%oCzb@$wM$uYmaURF?Tj>(v2+JODb=EcFrBwvigTt+)U5Ius7&{YBj3tanrI6r zcX@ra0d1J@0j4&)-JrI?>%Id0lh9XS)7j!GzqE6>bcr||oZ+?Np*S;W@%J6aqp=@0^#L8rSzBAmYraVfZF>#c683JlP3io=IGny^W;mqpp>0)!=?t{G=FInsZ_ci>-@Kq(AA*Tz8W;! zt;YFfmt~fqmu&pvEukoD>kCM^D18qL6sV(qkW{g`&+HlpFZLA$k~PYKhaYHR>rrT4lcnY^*SmtHmqW%WQ9bpSf>Dc`99?Jfa?UCF{QK`vj4pesG*yaCXs*A#Uf?;C)%Myg9N zHcMcvZ9amfYv04JI{tX9?p_uKx+p9G3T7HGY`zQbKrk#JBwja!#KbKi8h~w5s{Tx8 zzcC%w8iHC)e=@Nwj^*EiP8Tp|G7h6x42?eCX@Sd>Qim~5Aa{s*?4YAhNJXWF#S-%C zbt%6t^QxdI1kbJn7fD#$yM=CkG=yIj&l-aPMdpQtGwaSVDrd!OV%eTF7Dup| zOFx7q6X^>JxK{0@N-tuPPqpJ;f~P zF))->L-RPUjIxgdUIJIhAK!{A5AZPtIT(GolHp&LfYTC#*?6kOQLUCUVGW_%YB9pq zdxS`uGP)AnX}fGJA>rF#I_9-EBoY0kgAlpj>TE25H#XKoG1P}E33@>ID2Kvhqdh)@ ze){>ZFR^pi_e{F7>vyJ<4oKNSxF1J-M^TcJJ$egGa*(3no+6fK>GeHOmIgY#mUIO9 zK%=H*I32X{?!JU~tr76`=pH*(W(&Xk+tB|AROsImdy?<;c$T1So;{W9@D(gqK;R;- z-N&I^7TUK3e+V?NGp0DS%*DeBPOol~heB`kK$hSPo^6I`7lA_TdDrVq0MC(sYq*o9Eb`000gDCe zJ7XuQmpr26+KK*c+&y0%jf7>Y`}kbOX>gAvDqmEO^)@S~mj%fB_IA3iv56IHX)IW8CwmOx#!$^gXBe~S6UFe#Bx35=~tN@*4Ho&1vQ@D&_hAL_jg8c zL_e1fUssBQ?ot}iC8+49p@^UR5|i7JzXVtQbP4w1X~+Z1km7p91K^X!1q=?KtWKo& z_*$R)CHmc2*Y`S}?*PS-B$GtIbF>0t!YQ#0(MV$lbKor{zFC5AkYeWw+zddFqc1+J zHS=?F-L)=^2Umn42}zjUQO>rr#HQHUHn(Y4O8xBH_wE9}B?hv$tryM+wE+9mBf?N4 zXywANtb}U`Ny{z^LErYgiHm86w2%wO!&*Td=k|VN$LsaCwUP7 zjoRrUiDV>KxYww*myqvl?}m524p*{|#&>WTS!6U`sgA&aZBl8ERLJF}jH8j*qkhXH zdzxNW`&5c*6_>Lwh!{c4%i%r)j+^O-b|0W7W%8)-gd3{wE^QI>EWPIE*)xmKwIRp6 zIT9>*aY>~D45l*nI*0D?YGZwdp2RGm)-k;v1IzN|z#-|Sl>_ss7-8>z8)|cj4Yg?) zl_4(bjr}2tZZ%CSg@l|{VOgQm64dMl@1W+`yFzIj%6J?Pkekaa;v41pNQy&_$jajG z^rKk6>8vW%HtB(}M7NSxRYGNe z1m5+$XQhYsMMsL8E@;zmey1)WfYd`;!U%9(8UY@^QdBty@~}r3rGS3_Gk#HdyZ#`LnIZ)Z-Fj^>j*Xr z#_5z1yHLJ!R=@jT{3h9KweAJLmrJd6r(V@l47!a6C%T6|U9;jwtnI z@Jt#QH#CLwki$r?BRxhq%=UBa{n{BXX||T$&iJX8=5j#5_vnuOp8Urv_4lpw)5ck)d{5!>64pg@w7A0MGmWT{8uOnrN zx1cLk?4BNOil<-X-KNUjuRD)@jTUq!OWm(4Hi2Hy6BoK)N0zK@L4Q`+e_AI&sU2I; zr7U&NS)zx=qPbz2`*p(-P#2BXNRQeS=1R7x@SjdBetj?EOWnZuG7*~*AGNbN#15bZ?e?=x{}(41zpKf z_v^~=f>_XDM?i6!C2Hl&i zb+_H!%Z>GC&id~5?z8ny*WKNQ@pN`~;KkmvOV`=xZtZM8WdnZ6ONrwp80cIG+hx4q zp|lO#ZD{C+VB`!%FuCo!2P^1kG2Vw-D=(S`mrPbAL)V`M{aXSCFw!AJF-;DY!szUG zTJRs%a1bwMFZ4!UIGFltI!1ko8WpTM$y}{v&qy6OiiLl%U!@q8>H`?RYcKFob-Z@` zs!=NRE(gyFThB(L!p_cU6PNB6Uhee!VsF>kYolCnr?a=-cG|AHwX^5!b=NO9+ivG_ z^Qqg}bGsWZ?~8YcOvJH2ne;t3+4xuKKw%YIO_Y9+Tir4>2}-0$SFlBmJ{i(LLt&h* zhI;*G+DAJJsko}Ufs0XU!#-|qZi+gg0(IpM3)5h202l=mypFj`<>SY4{i|Y2?86Qi ze?XU-Ru9^o?1x)?nIJ&xVGtnL-%ktcTXWFIx@ko(1qA8s! z3eoJL-U-#MIDAr9OS8(rZbJ*aP|H6NSRY-xu3Zc?i2hx$n!KDy}+edq_!USgeVRpttd&^$aCCnkEl@`q$skyx)AzQ?X=gRv(X z6Bjd0B$mel@;!FafE$0{`$O?AS#AB-fio<8!1RjT{-0r+dd}Z_$%W-hXcOD6!HE>F zrv1s)nDn188Q9UU{P7I0ahR}omWPA%J9}dwic6jP_ zK8U)9dc4HaJp?T^{*x!(!6=iL+rM_(u0w0^!M*l|8|yO+h4ouN3BMd+zRfbu-R=9y zEys{J=3!sEhTCYnxKS(?CAz`TUoY#G(vRr2z@h3ItE+H+R>cKXl_sng6br29!12HF zwV~pr=k|dI6rwhCrSA;mo8c8>7GFPSCm$$Uy~IqS-;K%hDS&+lw=!#tFQoND;e~Af z0!sp7!7z}nOK81Alg3aToZ{1{B>Qfu%0-cFxNxUS>$db)-PY6gj^plZZ(Z(mo;`Eh z+ntWnZo8X%@L9j??6x~kH#RqSFTX}N)56o;wzKuDyS3*yPdA@+o_3yY?ruUAcl&aE z``KmtguILZo-e0;w&kE;)P*+wgmZW`=>_mN2-s()uz}uk6oW^((OjII)J~6EuS-8) zkR~5j!j7{5a3zX|bH4VQGSAq^`;BW|o+E8%XHl1M#@GKSv^bcLP**y-% z<^yVB=wCFd|6ZYi6vG`-N53_8|38xP5rQIz1}LFt4sV@_^8wKIMd1D2ZB1D34VX>> z01behfs0~zibdz%h)Wq_Xn%DzVk}A58J)RpQg7xfug$BMrv&( z6xFr1Q|pPK=+eYfUfhucw?P^q6(n%eJ32y9HR0X#N?j8%~1s9us} zdJg%Bkd7<5(=}&0>G|We!J5)b+Y~#r4R1%C1 zN6oWS=*If?W> z7EttF0*ckuG6p8o26R~(pf|ks`?xbPcd8^sf*jsZhBc??`cPr$hiMm^Ck?}vn~zhP z<>0>IKs`gaDu6|Ni^A|xk z7$bx)crM>X+8luynZ%}PCVG)GfOWLVKs(z4XVg;@erP?#*XBnIkO z5qID#X6+%|k}5`yY!nJh_@{CpS3}u)Dr;>oGl6ZI0MDKx^2h)xK^UumsB@szmAAeQ zPJnBY<~-^(*&*y;T4J6x&R;gl_3F!t-nO_lLGse1@>f*7ZBTDWNFr^6CZDvrY3u{_ z>TXMrZlt*ybjMrlnLh3o5KEwi5AYNyq8$PPbh{kp0kGi+gD%41OckwA9nNRl>c}+9 zXRXuPVdbRs4o{2Rglo0N&$Q)IIV7nqxA%-^x|=HUl&bY?oonqa)7PHrYcExNL{;l4 z(wB6%xQyC32p7dj2%~E9XqGrOP*{#8y~Q0AWzT8LM+#ndV9lBTnuxzm;T3SlV;<5H z=N}4V^Y4Fod4Bx7 z&qM7}9VDM4jq=)(x2oG8QRAsNP1X21ujg5Ht@N_0=tLlbo&IgWJ8macbV}VGt>Vj^O+sO!n_m;(DiBptxHvS@ zRur=Ga)O&4hz#`M6XfQk*`LwayGFkw#WyTZ^aB}s7o%S7zM`T&VEkV*#vS|~s2Gc+ zZsf70=ZbgM?=v=KO06`RV&YtJxNeH2(Qz^G4MyI^5L`NBFp`7miTLej_ZcCo-fj#^ z*z4;M6_g5zN0VA!14VH(;{jf=p0hoJNYbxaq=msj6**2_5YfvEq~?lVyIe%m=1 zZ3*9R^M+NTkmcp2_uQ~VhVJ8<0I~s!`n^d~v$yf2GcrY^%$cR%I9s%F3`N*NMHz)_ zm#lBd5aVd6>+~3B>s}ZK_bi4r@j4V1o$)coSH@#avKi?t8?wB_5~c^uCV|G8j@@aH zyy)_s43@!O-$^n`B`yP`H*trkO1UJV0>}F7@W9gV?ah1bZ3NtO=)uiLzWp56DI7El z!H@uJBf&vKOD%eYqW3!X20Kh4j7NP3=zZ#dj3nH$-s$hN?{mdF2)X*}O`94Pk!AmvD}#5C#;}l5z8xI?zwyvhidGg^zUQWg9;|P?o5M;KVYkeu^AB@N11+;ZO|qQ zb;^rW&Wi6J0KJ7r5Z95jP=+aaRj?r(NqTSejm@PO%^QBeHACu_GOJ2bExkGhir)YJ z0NmIH>@&bK#S#C)Yh*bU-#v!HTpNq7jWZPS&WO2|C!K81kZ+Qx2!pbq*o20c`YwT@_Z^-g^FSHM8VA#X1w<)i=+Z5n?UC3@ zjm4qpeX+Q*k!7B~Bf}i(FnKtf)aV+D-fO#)+>)UoL&T^&4kYxZ1Rk=(_Y}o0I@0vZ z`t)AQD&wOy+=)FNTYM(tB{9-SA9)FiE_2lY0Dv*#-D9e>BrUwRmmW3j-3v9G0psX( zNrWSBS*On_ucxC>BSLf@M<{Y84#Q?Qy@A3m-H2>9HqWAr)C)dbIGC1%h;hZg?WIqt z0Y&cv5Jhs(Emvec#!mXx*rCYhs?e=dcF^6 zyxL5xr{C&WGb9*eb&ZPcc$x%V*E2yf2xQJ|qH4tEFyP0;Zt$tIQhZ5SN?c7&;# zWtc_TX?hrOQ1Ktp@Uif7@9L|RnXMb32V1nfLEzGChU~PE%X>ZcBM{R#H z_LGCJ$#-VZBpJVsL|;gyOR|)wpFV&M6kP^cQZgzM4in`99O%Tjc+$Gmg2gNDHGlM zmJ~!GZ>|kLm$#lC=Md6y}7|WVtmvNie<*PVu%`^PS1twMuG%W0P|2%(;>S|SO2Yl z>rZEx$jf&*t#_=`%=Jx7`mh1(#>ny^{bKC#B`j_5lX09f&{wpGB0|;8dlORWyL+jT zI~2vPO^=7fs+LOx4gpd6J`DuUe=xyV`IAmh$z%spAk~52>99#-di&j7Li$&jBq^~D zy-`Ft&Uas$U^4M(8e%rb&NcAvgeawib4lY$T$9jzGI?1HHyH|qM_ynPRN2AZPvf}? zf1M4i$>QGh)>^uF!?c9s+L>$LE z%8=qMY07(teS;TU!{~51lXvWJn8qYqLAi>dF1)UA##d6AA+sWN!(4)R!JiMA#obM> zk=tdB9HOOOa_Dq^6^T4bwtdiyGy*EssoV5PW1%QtXq_3s*y)WGzB7#9NqwbH9}Gq5 z$fZi$ra&$;Hllg#2uLdDiW#GkBCm){ClOJx$HYW>P0=oEiqaAj7Jopow6VAJ*A;75 z%5Z9e-i)KAUh3&n{g;$Z5bw=s@MIi7`b&#BlAhCD<#Zz z$G^$wC_Rv+rljW&oS^R}J5=bmCZG);B=Va_=^OeVy>HQB-$_q3?l9G8`dTJqR?`_% zhZy$<10UrFY)~g<7}|Qnpra_ZE{-g{NNtBjYJgD3!L2hhPCUI-vMkhFv56D&7FwgL zQvGo#SvD(5WlqUM*?YPSM$Eil6|3a6rX3l@jRUmAbmC(Ga+Isxs>t~4`uhvF)as}T zE&19{UJm|R(8UY7_?%a{Rl|9^FzzfDqkw7{PTYG1bf?I)PW=gBTclGkMz2d=eA+S$ zZT^s)Y`c>i*By$mCE6wY6O#-EKw!&Xv|t(zIOMMdJ9nzCw+d*BoLQCbCeEeSO5xGJ z89V6a7l^D3Gq7IR+gRVpi?8>#_wZl3P$TXWIisI->}TU?Vf|SFO}l5hk@D)AzJf*I zDKE$gM0x|}1+2S=8R|Lf0m|LTuOD8c%GHq?zD!1-WPi-il8z4@c5P+XbE8p?jS*ne zH*UX=J|xvk>;*U3G?4D3sI$@l zN29(+*@=-aB3DTg{qrm~;k?Uu`da-pyzF~$^5`-mzE)(LK&hS}L)F>Zc>1*c7XUG- zH45F^p#vloYJCi~bU{k!abKYNC0oCRK{h(Qf!76e=j`t6Z0+er!jXimw76Mw*uI+N zH_^2hzwE($*0=Lwcl~mAbCcTAPA>)O)YRiKXAl|2)2ADkopxTVyU(1%JqA!%rm+f_MKk?RGxjWKgV!gzPP&$g+H25NqRwm+*isy|08 zXC$m5mD|90wqdf{&)RLr$&2nrXQRCvg?l-vr9)Kgjy>_lANy_3y%xW0ZEij7T;|2b zrt_@35p9Gl3vi2iVL~uPuVw$;>GVXS=Z!x2u#q<1%guJQewS>noge_>LVT00o;I4L zlam}dB#%V4dIi8c1~v%eCc#G9+;N`mN~lmmIy~!Lx8MSuLg_Kt_d^+1@FqoJ`(gmtAIHaC*}`{mzZqipPK zZt{jM$qH2sayesz~#e$PmzFynQpqBy*-}ckFGmnGYrH7aIp_b=&q*La3{oQ z9svw1_%fTjK%^K9N3drnm$2^a;sdBL`)8>56ZZGZ=1!;kln12L@sZOFy9^a!z(DtI zVF7>H-e|Ayb>*4%$A~h}Pz2roB)bzt5|~LtRYZ2M^Dmw8pg`d}#4qc+uG{S3=0+9NQj-$JMNcfUAMi*Yw`wG1nq#QMC!7UnUuU>b%ZY6 z0!`FU-NLpOzg#+7uw?)P>^j>!&tUqUb=TS9D}f1Sy)m=p9xnLL7_NWo=`MQ3<-)5x z=my1}ovskW`+xsEa#g4whQXKmhQvi*ra%*f{|Lyh$P-OOI z>$;dOQ@c%K`ZRYd*7oMcB{f78A)5aBVbmY8q2y5S@)GfHV5)i8ubrK( zF2HoRv*m8D^Z3dCz!99Nel|FeinCI)L*43t#4UY?L`~BX&e|E!uHq;bz(1iDi>N4- zNSgd_ky0a+0-V#8gI5)2B=2kE34sAlwj`caX|VG=EJj}{Qc>Y85KJ&}MVqMxWGN$I zMQDnVr$`p%YGFb~R1X}}0|?XiPz%iDpgIY63vD)U|q-5XAO1(OCA;a5?E;qSyUjD(X|(R`6mQEN=16|ENu6@h{{8++Gi)nZq) zUL^O(abXgA3|}NyWZ`&1#qb4gRuueS99CYQA75ze29-Zi)2GOjja}Swyx*yOi-b`Z znar6?z>b#zuzF)3IW_boq1%nG){*N|o?660fHriO=y6Z3LKr+_4Cii)93s(|`Am08 zj3p&jyP{)3EG7n*gM$Tp;mcbd7V(YH0A-1v4k>-Jv@)V|(&;-v&kf*oHoU>8FKdMp zkIo4JaEl3b-Ql%2_J_*TiYfdFWfP#w3Xy10^YmNAE#m2Skf-0+%`q~dP?9T1l#ay( zWm9OCYp17`ar0W9MKCb)8k z9`u|6ULDtPMvv5kiwti*gT>hH${!wvzJZ}#mO?}_kQDkSwwZr(yG2H0$Z-G`1N;m7 zXFx&&@`?+agF;-DWoF3TW8*R&C))p-fZSAod`iL*gK}VBNf?dts7EryyPE>a%QYmC5rvnpan(`L9&?Nc=5Ga$Lk2E;U2Q%h& zsf6vWp+YY5fs~ZQXzUi0{)9a~IMaHe{OlPmW-(J5^3m^5fHA}<*gn4xc`B41M~`Wq z0>Pr&0i5SRO1V7NXLQ}b8xz8#bh^_C^3a+FFJ@}g=g=SOk&xhO$oA@?^DBzOQ0pks zLBk)*oQAlw(P4WCJJ&W#|;wWhPP4X1zYP=ecrGXzBJjYd9mhlIT> z(2_wj;lB{$h2ZU|@dIDseiSbqeC=NYJ=XC@PR`f_>uM8hGX}E)!BV752mKMf0n9f! za7W*{#j}iCgy`yVUgjn5qmnrA&_f=n%uS5Iso>dmL-I1D7c*HhTkTDyz!8Z_-Uo2v zhZ7?xj1*E5rkFD>-Nf zKKNsxzdv|NGQn7y@CZv=vNvat7 zSDq}jqvYgeY@a-DX)Oxxsf~fj>@ct~ZBiX5C|h#iLW41M$HuyEhKGNnu*o+U&ga6E zX>s}iDHy&&vJu(Gu&FsL4DIbp23 z+rkX8XAFAX81$wR)rS_wDERBwAGut7q4{ zW-F4$XEf?5KUrd8h!ZAZ26R9vYA?I085KGgqbi`lf^%i#xx(z;SV6A2s-PARpB;5! zH*qdYWblCmzzO10wOm*3L`hPisDWds0Mf6;JeLap$8Nv>BPJQESyJ=Z#9~@UX6iNzGlegrcnC*03N$CNfto9iMn@ zf$MB6M_uLU0tSIY>aZmdIS)DAVn=nul04>~na?JXC1QS9pLSbm@o43brJhW=jHr}; zv49QKN_N;r2VP9VJx$$U47!rIvVKov-E1WaajGiRzk0zmJb2|h$p zTrOT8Q!T>Q4Mg_Ef!J8z+|Gr>0{mYoWe6Lh{G#Vf`$Z|7aF||Ztt@3^r=X=&Zj6^K zUIj84DsbA0@d#whB9rV6-$D6n&oP9dl6G7{^sxfJ{62#P11gBGw|Dm02goOG0Pul% z>5{VnLs%FPrW&Jx1IpDN#Av0~v10O46z0QU~ueN3pdpMd(bp;$=Uy z^J%Iy^>l5pCP@XZSJE&T2Y1C%RUH zl9wW3m6&-~wv>5YD%WdSxc1+$L)l9>p-BH@?8Zi=sXzuWJg)pKR{q=9GJ3{0|G-k8 zM7H>S?wb`XeN;WEWE;(T_4F9V@y$PY4a(>BdgZh!@4FU|exH=hMzOb&K`D~z;$^id zxSMbOVRie7216}pxj?;gc2a`tGg?rrynny3q6fDUzK6y?!7wYQhY2$VI9R{4ud2-7 zzb^f`R?p3PSv{To@~qS>znb-u^IqgK27qy_RpVG4VKHC)oXvQl$%+_a-NhRZq(&=ZBT-@#%S$De*&Z z93aiF^B_oyJwQ&HrXyFq)o7MZ4@>n!@$6a7c!8>S+`Fyn>0#vzRs;7&xpr6)wNtYk z%hI59^Qii>=t7_PPHy88c6X+Fl%*InIcfzc zT>!ulU@rt!BU?CZmFsYXDsY%8tp{7qtjZG ze1I5C0NqxzR6mA4u)EJH^=7rwh>Sa<#PMMXQ9Ey*oi|&QnMV&ygKTcAnMhLr{lnVZ z2JFbQ8mv(1I1^NNYy%mdEnpN`m3_Wlzp*Iu6i@@3JpSo`80NX@=664r`5N2Euj+%a2CY(;HA$mdF=xOu*Z< zq%ObtL0b7h;bx_N1UKTojGp!aUcNl9o*Wvl=3L1yaw)ov4E#ZR&!Bv#dW`su=m>>% zN1Y<@JK-(fc-L-F7}ild1m?0Hu`;5@J0K8qAD3l62OMv@L?QDp9;WY_OAP;oHr^r#vN z%%~zMzDWRj?z8Gbph1HHD$synd>z!c3=xhYnp6O-ur6CoX$@cdj29FHGD zdsYiTR91mbQVlg;0d58=v_Z6ISgd~bT9+xE0}ibG1efg_DwUBK4vNBYK&pgsV3yIR zCR;?pt<6M&dQ+I7cx=KXMg9D=S%vEdrQ_Ap5@9HXFcp@43A6?vODZo(sJZ2Fa;ki( zUVc@5GrxUV9_M%|45~!U5b<&Y5WAvK4qpCjH$$bp3 z4?n8IOFx%Eba9y_zT*f@fTzdqL_qCd(Q^tDn%|^06T0KoBJMh7Ir1K#F`F*1RVkT z2TYZnvzc)SaRETW5LB%qMJ-}s08(L%Gvw_xdq6wVhAZkv zhV5qXARUwjre+;|%#uHP@CjQW3Zzd9+Bz#WU$x$p>J5fl zMLn^Z+~u?+gEeh;$cBZYK8aS31o5-L#hs%3uU@M)8CH6EphW5H?4**jV2|4`T1l*| zO@r|oYDN2N{%8XKxx*mnuY^?BdsW%Bb?mmH{kiOtZ)f+e{-TFa{U4qsx5?@ko-0XSw$EC6~Ht zL>dqk*G17xGJlT^**v~{qWOH9Vls_Q6PUAQ?5hS##IBoV>2CZiOMByIIr43f#(go!V{tmPg`)ol`qOLI#ia})QF zSc2j;8!)L}`AH^zc{`W{Fn-b!!QrK35W@=^NJS7E4achkRHwG)P0=%0kOS?eyd$lyERz ze0(6-6SkQ(sadLF2x?xxE}b66wv4IQr)3Fx#+rXkHh4V{z*H<8E*21>%QMXQ+R~8zN5& zY6*=dC`T76NJwZu8GG%i&Z0qb{h>DOg(s{K5Ym&7ME98Z4F3AlNoL@8wfkY7eK_3l zz?-02M|*RSuEh2b1f<^e*m4gm4cy5-8^@X>3dp*XheArB0jjjdE@fdNl|UO!Ry!I@ zLmx0sm)WV@TC)GzEVvG$S zwN5x6AE_-L{ZP;kVeAk5326}l786v3jlJto5e6M7S2wj?w7Y5seO*1=M`PS8L*6w( zz+-N{sy0NUcGP@Zs#gSjM-507$aC@XooK$Q2*j!N>hY_lcvU+&1pZ&(?SQ(Odi5nL zO&VgQ1T<1(1z%)Jr|*y-t5+HgtBKl?cwMPuyc?hsswdUvI~vGQwRwtt9YJR$pxNrpYWe)6R0npoepYK# zfFrA9;3_Sjlz{&`EJE+lqo}-rI}L?ZCnu3v*5000>Ns?3iZ3fLM!cM`Bbp@NVmO=d zTe*s6PNfq-KWCM46~6#w19JglD=*t@RDL>#Lhw=?mR_S_RwlauBCsTA5cC>{3}C=a zuFY!m9Ju-8TJ4bF+^E#wRLhmd^UO)DLBKk1RPxY6vqT+369B2kbNv4Dyip~Xkl9Hf)|2 z5=A4W!#7nNwk!-w->AxMLLe)@lHmUACdrSVFZi+PKjmcbuzk#@|F$-EcD7>l-|oit zBmMUUKB0lCT6(mMHw1qf2mFUhO2=6>Q6ZbOE|(%jA-Iiam!9 zB|k&;GMU=TKOeAW+9&f=U#qcw(qH>he=VJxtgM=TMln?f>d2^2ERo*@9hoNaMeOMn z21koNq@-f8hd_d5i1j_NI8R_&fJ{58{`{wdP^YL&!ExBH{&XM}+)752ks@*V78XpclzgIv ztC@(jm);+UZ?e)YH1|!GJI}3slQqlaGKMTt_cg1gSthJ$j_Du_k!H1swasz_Io^~i zud)gyJG)zZ&*TFn#47azv!#smq%cG}EP2oTYu9lH{t#X8 zZrvdBuOqKZC}M3g7_FISq#%!g*M&l8R`^Xe>_3a367dqm*$?eF6 zjrosq`P*;B>L>96V+OBL6xL1S`;#Za?eu(@R2i`BufOsy=neLB`RAW!H~aI?Y1{qz zXQScozyFS1YhG9Xoy3}dIw;h(GRoNgpzs#mX$o&Ka1rolok@Z0^03eW=8UA$mCF8%A zL`M=u=Zer}SQURpJzZpFM8fGSOYa+cJHJv!cYvbF*M|qZXJS*=F*-??9{9+1=VepI z)N(xac%KGDWEr!|QavZeoq&8FV;^$m3ehdnXA_ZBFoSYwywp*PE)fuXV(*Fq(n9ABr8a_Ju-xY`;H<>lZSpw`P*; z`zTFTFcjV~x~#yq92!OvJ{n#*`I6>zV9d{aDR+~6ZR{;oK<+15z}Q`EYiA9>>M+tl zq&c>&OFTExgM(p26U(7Tyf~8~9tuj#r5(nD%oJC))e!r5`CB&!y@7pTT@&hFfa{D2 zsDxX-WTQVq zvj>G^bM0eq83kv;ZDP_W-vfcv@8X&bY2}!Ng+enBe57@l3=(@GvJ-`#NVxQcK?)hD zD{cEWLghXZFA<|3SwwfsvyUy>p)?87=qP552aH(zCofWFtHxHEcPS3`x}X+3(wEiwiGk(exA14YCk! zU;2+HOvY+T)gnROK6{$WSMaZfwOkfcvbS$JquV|!M6^H&NxUoxqQq%s%uS3t5jdD! z=3g~FkcT@78(>M2T$GFhm@#*}*`CdH<)SBpip^%gbZCI_bO9F@MHc(=IO%c35LZrL zDvUK|;T+XUbk1|oQ6@^v$3_!}idEh$o(aG2v+e$(b&nj1*a1xo?pd@uqNbOq;`iKO zKQoKN+1G|dYtdIQ1?4}rrl0k!y@61od&Li%`R3o$8YnqRp;{E*`~Wlt%eo_*E3!rB zv0`%uTVR+k8gOh4A7TcvT@l@Z_O?r$;jF2}=5*f`SRVYn8H4LZ2iLpj;Cjmr4)q}k zW5dVh(CGI)My6lUwy;s;g~ zV^Hb9oA~3~7i+<|lUsqxmku(l#I+{09-qGBnv;iLcbR&0DG(GW_~1e37%cJ!_Td{p z|2|Nf;P6?oxCV57u=ZD%Un2U$|J3iIBMPHQkD@7u@Ky(qEGWCWaFaP5UG#hy3;* zZkE>bBBFTNm9yO3ZGjQ0gTKlC)NH-3m2-LeW_Ym4CyMjNC%Jqs2AjW4W*$LqIWO#X z$-K;Nc)>aRTM}9LHy{W{qY(6eOCkp4+#;nEtRwSHdbak{1o6K3RlLvqD@wJbLb32s zbiV%{*Yt0bf^5Aopi^1sh{CMZP2ZzA`lPu({4De}`oOc`p6-MWmzWw#f+M>%zBI8} zA_7Umi*T0Y1muy4Y{~e3jNIc7cmpOG*d$%c@c8o$Le)o}H1yFRlOBY5LmbQzUvVjJ ziwl}`N3u4=s)NE|xl9r9X-o~1P?Nc&=t^>mcb1X}*$~degxU;X{kH^C%Iwuxv~Tyt zx72=oRiZ6(cRyHTi7x~X+cKj1*P>F?Ajk9uGued>IpMMt4C>Fk+Qphp4DnQ}5Tv?q3zF0M6CoRj2FFpCSM9 zuQM5-fc8V+CRtDvUcZLY9UU9uPY0Mcu`oKTqrk)*Kqy`8>Dx^P|3*;IJ;U*T2m|@wKtRJVq3~A8VuMSoJq%rA(vMy?S>yNNeCtZBr8z%(hKfUL#? z4#H|TbOHp@K3oY1VFu9UWnKgO}B2<6w7N zHkpa5)b~yAz{kBbb1#vf|LpyQa>j-cR!<$y&$NH)GUDxwtv+{q#b<)Wi&BufA5l%K zog)=raXpcVDiahxDg|ujUk4vx9DvJ2X!K~W#Ej=_IyKM?Ye__@I@4?@L{{?R#afXW z9b4^c0)2IM1wB%$W&~if3_jy#KL6V3&MJ@QgZ&{~gZgrd=ynaG8hb3(dd&c5!b;iZ zsCJW)RZp&xF$}-=wH2 zb-~+uy1Qcb@J$xAOgU+jbFfkvY;CS6(-`?vj;wI)i~}OO+-s+=YG8?+eyA;m=xOE( zhIF}~(97t`>;!&6S)lA!jxF6WJZd) zR0kL}_p(pfy3wsmreo$jY+dDrK5zo`DFg0k*y$B1k#lL>oDsyD z`bG(_<>Iaj$bqPwec4}1XVtaR%PL&p0V+tsj-9qw=sRtHjQ(@CK*;)Qf4cVEn*)qp z-S>v>lfE)y&gF`$tCTu|iR;W=;&1;#tYO%{M0);O?(+oYZcitfhYHEEq`9EkUC=jtuhQ>g z8lZQr!`k`FlgdTW4Bl0WWv=hKaEu7v7nrtv>jIG9lwEN+WfC3I0CQeqBGeA$-;*eT zCXfKa_2MRg@P}Xo6A31_efMAmHitjnhxx6%!1=;#3H@$R7Pg_wNv_+|!3ZGBCb?>Z zdzfnNR%TTUK6oQ6f_tI(!y5MRB9oCq3_(3pF8S8D$tPG^}?z`4EwGh$Xoz$27M zTV^lK3*65ATa9(Gadf{xKTSl-m@g0$d{ZbH+*N_wzbqgUb%uP2VOLHc&UYKwrM^sv zIvsV%U-lyC1{dlO=PBDE=G~o+82ZzZ#BA6yi!n18vYa#j$^@&*jr*4}J?Q&af5~D1 zljF)s?HH(3%%0ae^-&-sN{CTW*ww~60m|Owaf#gH(8U@X0n$ zLYS6}01d;R(lWUc6A?_U!OFXUjz8kd7A3cuuj1d7ee~;u44#GBU)g{UXme0DpHTA1`u6-CAwLITP`6_NTH{ z0*0A!-YP0ns-62>6((GE(6VSE6PnM5HhJDo^Vo6>4~4wb$uBpitAvSYVr?kWyF5g%Mkb}H|H4=%|S5Xn#u z`C%sP!jqS@7O5Y|;Y&i>LK~|Wx;eDVaWPLc$INmldwYg6Si>9I+2s4q%@c`-*umkL z8(pbK@QF$%OFrDU4B&=MVFCH((*~ao+?g3tVCCsj#C{k}tCX&=#QnaUr6Q}^u9540 zKa;t*xaefenIbXQ)!Cb~*gNruW3lRuaM}}r)N1Z|hVr(n(uAwBc0--KFspO2r zi>fxFTLYVARg>@pion=7r>h(c9UWX_J;4ietQV9Tnh%9ceTzrVFCZ8+dR6xT?Uszn zVoQV&f>P0mJ-^=#ctvH_-7qC4Z#28PjLd6KD?LOoxmR!$F92NuszqOX&8`gnVd3xY z*blSZ8d-7ad}LGxT^<1U$Bkfv(A5Rg%s709s8g9DThe3QP+0{hk2WW@R}`<27D0&} zt_8i8T?GmuTrwu)k*%&)=;i7vkPtliOo_Akj&J25QwiTEzT{Z=xJK3nITt>;Y!5)q zTj24DO7VcbB$~_%_!QglLDUpXJBY_Fr~Tv&+z|&>3i9lVjz_VQ7Z@P}HWvoFhJpv; z^!(%m9dA2@7ciQf_=k>#-BCaA!_0Peh2XI-z75_Fp)sxmz=sx9$+b1u1xUu}EXYE9 zB0t20=E@FcStdOdZZ*lI@Jyfw+3csxCq50ep-byT#3C|*7L+kswj>`eB!$mZfMe4Z z7djnvhBl>Kc^yYt#uWq}edObTH}{LYiSX|LQ$Te+Ox}&xFKMnblt7!V${rFSv4g@J z*XpfkuA&^G@JAC^#(%7TdY;LwtqL{qz-6vakBvi94(WpRr(@t=Fx`n@_ad>nMhiXZ zjMQ2(7suIbF-A(W)hwT7pFnqcu|lrEJs@GjePXR#yz=>J^=DO^;+upaO>pr((cnu% z42#J`r2@7jPZX@3A>Z9{_%QBxUl=$a!|2KW2DfL><{`9cem{hpCC4y3@_@L7TLB%} z@y75aXk~au1SO^>r#14`&R|-YNSO#1fP;c!g%_%wUk^fnkh^oG-m0Fx*|sq^N3LNW z%UECDEt#gizyhQJfEZ`T0Yn!#P8_7fMUgEm5pKBORC@Ea3#^HU$WFYI;u2*6N^Bps zuP1Ics;8#F06X(naRe2_uciTj5Wm9nLZKk!zwnD7@g70(zI9qVeR)zV{|K*}E?&Wk z#!wuhLzh4sw%Z387FAy@(YET9^G2m~cvw$rOr3^b8nv)s$aTX3-7EUztF=C=-|%N) zg!!xJ#TvUb7x#*&^~uSkkaCm6hQZ7Gp4Rt=_V`8o9lqL4eudTE!%@LdlrW7dp%ZH& zGFT~^jKH7j+4VM(sPK1}Z%J=>?GvbexpF7KEsYT+rJLy1+LhzAkVmE)aEqr4o8uy3 z^U6W0>y_lUCcNc7x+op#l5%$%<086CiC6}%1G~Kqr<OT#VZ1{xy`=-s#hvW8;Ij3vEGfs$jUQB z;wpjlA^8UL2|IB>=rj6rb$)6A^0Sf~`e z$JKPm43-4e#Z^Z7jfikR3CrI&x3mGhAeg$L8bHyIisT)Wkk^^$GZHc{P&K25M71xZ zP(X7BiuPgR1y7aNcgDQgkoqbDCXx&~8F!NCb&$`nIFvXEQmr6UzOP6W+y_omWV^dx{yLR`V(_H_C7(V<~IMbWubz zz!+|%HDlS0&&$s3EH~0`10Z!sU?9i)7SSypOvN#d95|IS7&fUTRJbwXJM?I21Ap7T zFy_jXP6h!9OXCBEL>wudY>7$)r|8Z%kAg*H*_L_ zP{pAU@s!sbVF0H{Drw}T5dtCktH#DC5%(){m$2I>I4L3?-X()uuWRoWI_>9y zp|RubE|n`(lmw2}F_pZvQsx<4FhdnsyiCaAK?%mhxQS?UYUbGQbf#k|#k0DE)w)2o zj&wMgD5I&*8MBMw?K^?};JS*@MbsOzzd2kIij6NjC-*(<)9qT#!AeNU6e<+qB~B@1 zeyN-3DC3~s$|U?GqJUe!>x{b!o{an=@FQ^#9{`F!Xvid`SmOepN9JpmGKBJKBa-_4 z27CK}!ZK)|mM4P@H!EgIUW6--#K9&?)>l_8r5L8jJ8xXTwMWd-D&cUosyrdZ?T}z& z8gL(s?Bx@Q3?-N3&^EyEv`VG`=@NsnwF*E2eQWTGSe{!%Og`fxuMi+|AoWvL)?^Ah9Zb{MkFiGvEOfGF6B zWEdFfORETy4k@Iw6us#7Xs#dhqe!|_$YzyK{Cy==}fV`xv&V#5h)M@+E ziG5Kk&8cem7hA8CsN$A!yw|A88nF58H8!bQ!^)^`P1q*Vq#)HvVl#pQFoPYB6fc*3 zpS?F&D8^gK+|c@Bl>5es6P_0&SJv{MaJ?lNqq@g(6TeFggQTgGw*AXm?ZiH@p%4 zxD%&93(WY>vCn0z6okoxc8lx9T}IW6%n2q^7C^KlK$eQOl5j%x`1Jg>g%ufe0JhVi{BBV>=IN7XQ)7rBs*x8Wy^vLrv~h9Yy;{YgdD7bENffkv?P(Ogfl8Ll9@=$oPZpp-UTs)7{k@H4(gOf3STojAZIDJ z>*S*}0vS6uf>;;awu3G|L^G+be>2Qc!Z7AVLZ$&lRt6avlc8mE7aH{yGblh(2-TF& z(DF>k-$Al8*bJG6!nxq>#k5F}k>`*nC5Z-{wnat|J3KL{N|IqHY)XKi_!%v4gI+YB zm1Uui11ED&Rof+nI5i`DZiICG`I25m;ek8PgWBp~Q^?~)eoKDcjQ9>k8gt;_nlLTHF zhl7g`+%eicbhT(w%a|=jdt#+hGi)A)imjDm4l*jB$)QX$YNH@U8Mfrm8w{WnT5|y7 z(Dwr=-Igj6iOh$F+*>MeX?9)IMgm<BCIpmHM|^jEp)XWrP{vy-W4CclESX?yNT6Yd9^3>#(CBxjDildF zEi|$LO~O3VwzDKQyOkr!yfkByePRotcs4YxD~eiMl5u%H?5H=FXFv})V_4`I($>7x zb|XU(0z=I@oJfe9RRE!4f-FugJ~lmXuvO z15YA5vH=RExlG+L=f`I6iq%3h?6EQ;p7f$-_);@L74hcLvfzMm!f2{lZMfj^S9^*g zVkNRxh=6grRfK#QILsB@WFC{gC@!T*hestm(b5>l6iNp($B>usId&(K)z^@T>Q3ZN z$%JSEspN1$Mh@)1{Y+*<$(!tHpnBUKj3&35nH}GrG@U)l0n9r9ek-cx~bJ9RyS8Oo5@47hV>Q`s7OmEK4d7LvKB=tC{zG& ztcLDP;jrbkFopX4=-eWsOtfFKmV@K=GqFmpGjiXkjf-@F!J8ANXQUzpw%i6BIV6^c z#sEN9is&KjXf)KdT6P%B<1QKF!XVqtJ6i zLs4}ylqDg|TG?hx(GNYI)f-!IB3;EgMMcfn=Gsb>5-LFR=o~Nk1&Pmf4RvUlZnY0A zqHLw1lord@*$M#vCMrU;eKNZ$5nU?{ENzN_kQ|VZ!M-IZfzDginii6Ulk6BraknFJu=mTH*Aq>_$cLLD)g(q z-v;`VYhLI~KykDKFviAifN8%f#-LEC9hqPdDxF{74??Z6wrnOn1*0A*<`^=u!ZB@P zt)JCv&05(IvRjZgp{yJ;O~ppMJxYJ98!I7|O}k6#ILj>8z#ExmC4?AF`Xe-HMH5=e zZxot`s}(f$XLxsX^@*mDTw-6X!p?2 zMHaDBR3O8qNIrTHx=!FJT^17O9Qs^~G%D(nK}v_c`XuRtw~iE@sa~m(UnqV#>y!4^ z)>IPpV$kUUF)%82-L=t&D;4D&Z5T6nKz~u8+;z=?Km6d{k~!pmAT#KP;ojGB#Z(>@ z$_9-M!y*^2HqW1J#)@J#X~$Jn6k8BC`Wd=6NFtM~MmN#?jfL4U^py$OU2-Nsa+#?r z!YAy;E4>M%5{L92($OIq=@c_BrN@5cWLgGjYP6EcQIqFQ`vLe?SL1MrGB_3zo*d23 zTv~=Yd|n9MQcM7pLrQFes@BoO_CCSW(viNZG$VB&mB`K=e-|j#hsPf6gt?*;G}BK* z+x%s77crYZdrguJ|4{!BqEzEjQ@}RoAYIZhkBwh+ydc)rtLsT^OszP zZ{XPA4BroP3nw~&U<2gkVflUF=YFmp_P_|F9is{k5U=a!$$lYnhJ_bukq$)XdE#4n zexbmI(R$$p=>QH?Q6SD%Ssh?4k2;7-Hm)SSgo{;F(ZRZqdN|CA0_(*G;L-715O3|z6~f7y2@ z$ZbiQ%CbQ7#}SJklwr02FSeM1Rf7p-)=P{hW4nKaZ9ti<9F2-4f?`t5Z;b`IijZ0Z zZ6*OP=ZfMbTAGK;DP69}S$nbASGxNpohxQ|*+@&7|J8=(3gne$3X6m;Mx>8&O_et? zm)>O*%c`Xyz`H2G6l|XhG*(C!7!G@ger^pUsfA+7N!9?hjKQF3<4FSS)d0|}I+B!s@Z%0WX`QCANl!D?}DZ5-LMiih{re@Y1DI!D5o6 zs$|GzFKHL3ARmSWh&W2{AW#!pVObrK_Db~UquuL>(~z_18fATkq)E9MyBTNXF`=Zw ztAy~Da&wIfL+7Qi>l9cDz3KX-9+-I7sGc)fw|N)VX<2euijYIQc3ci3;|T9%l1PeG zWV+E+MBbdNJ$Kajh&v5J^KR+n$EtQf$LN4KoXo#U6VR;#_}297RAt5LC5jPaO(tBk z^IDA;mklFYzi=cKq19uG@+VgSx^I-ZX4|@L0Kt^d5P=L%v?zzUFoWX5SSl=Qj=EPU z(od5WNwcu8B1sCyiA11*LegD;|?lS?uvnc!LvsYhwGoFE=b zst#b(!K00W$NZuSP@s^>$c;W40Z z)t$3?iqd96JJA|JD!{#Qc(h4I6BUVQUy-mT@);JbqNM6w(Ve=Q@ps4|L{hUXIr05b zksZY_;7DFfhka#=I3w(Zl0j+V`;BwU7t634QiGbSw+0zVg*_{G%~_FvD+s2FXH^Ec z+9@eU*k@)j8rsxGIZzZu-SQ*UHdSfq^W-CDgw0St)J>#??k>kMV!W=2vxsO16*#xr z_U=}rdGneM1+LNoU#o=-4Kx#Sxc)#QOjVdYr9Cd%LMQ3vGS_xRc`zmMgs`3xM)tj^ zGy|DLNkMqU%84Bf@e+&zFb}n7YYY=RBz@t%JH4}hkvPB*wiIGU9VIx@hPZA@eu&O^ zlB6XJ$z@Hj8;hK=199Lf>~=#VM_YwXQM3#KUI%b1ii%2Llf<2SfwYvhcw)u$79`V~ zXG_1EaIYu^C;f;FU5N+-=a_0~bAgp^8YS1JkBJHtBmLOeR$nS-OfnrgS0%JFP0ffw zki@@v#8YbM)iWfM&V01<6ZJ^H4`RmS{L(Km`CXtvO~hb2+xvRju(`<(QPK zt7pceyp6s{oIrzgKRO+h$|8dUq-zDO`aR1^NAqx9dkZ4(*f=9cY3OC*m*RSeso0M|7gYL)3lMp>ylV?Zu$po9Qwe5}tWpMK5bL>FAdCC=8E zGVI&KW(XMP3XQj{wU^Q7r1l}CT1Wv=isN;~)5|-7jp|4+Ktk)d$%pOCp3MB19?miQ z2W21@kxIdv)zzUtfu%9eR5&064$Uo(I4ENg8Gx72YYy=ZiKwJEXuJzHzX>PF3N`bP zDOdV1>LPNU6L)B9Me`ELqx2t}zt$lcjChTtHVtXKuxuv6ic{&(`&QYSdD3Xe7QdJX zPSw#THe*~Rl0G&d5fPU#8q~s+$X)>S)N%RBvCbrIjKqBrn*Gk$5mL-3wT~Eyhr;Ih zOk>Ufw6hcrlr{@$(z8LAAh86X<)zq226c*oEn|R(QXX$!b~=3qH(K;P@9XI*RU$9pgfBAVCb>oS6~2S=-Gah#Br|16Se>MjHmL}tPF|6SOdUp=(MB(cO)*8f zYMbt8(_ykp42ewKMAG|5MNTlC3Ycfsn!wBuiRiwL6)w3*1gg{auQ`}G1)AY7x`7cv zfy7Tt5An9jO!0avs$c|YhLfbyHdo+6ZdcRu72jTH=PBv0A|n-KB-P0Ac86@r$;=Vi z$ml46rh$cwG)~qHuwRLc#TtbI2gyZ}Tp7PQ%BTfiN;Eg>JDt#lM}bw1L~cQQ@TNXD z+$gcID0K647%I4~M%tKKHqCMlKK-}i0-mx*&;{9lpVB#z)H%J%N&F(?(D%K8iYmhR2#TXm6QLEf0}b7^r85}e%{?-6 z!i&`?pz3i$7{roe)KgI1@YPB2-w=Eo0e($d;4|7AqfAKz@o^%Y2M<^wcp65@IZ9t)FzC(@_r0o|tea zg)ZIpl2}1haqSG@og>Q9(8!2fYfevIp@0%I+6bf1_(7vUHGv>{l1&5Oj$8^-Xk`LJ zhjVc`)WaVpV1m$W#xvdaP@R)8$yn29Mm23Vq_{C=((~dMmE>mJPIP=9dFcN#Tnmd? z-tU0r)vFeM=c}gJ$z>?ckuhKE+#7(^TFUygt(^l^gaE$42F^$2a>1eyj#jM=;AAnp zV)z1)`u(sJQZz&T4iU_}8_T>r!|Ou^5ms&oVPcL9umV#!v^WVuprQRDSqBy~=K;GQ z5=m-!hQR3_`?rcaV6uzt1Eo-a{AA^E-`LIk=)JTug0L|AMg@hI5(Aj@kaeci6C8HA zsks}J9_hfc#OZRlWr>R9pcHK7Xen?b&_)^bAVql%3C(D`6A=Xi!W3Z2$px zeuU@~%lc+qOBp>IW8!*fX(RifppU9wE8fQi)Z$x9M=|NKIl8$*%IN?TfuI0_eTdmi z6ZH$Q*t*qqc?TFl0ocD3dh!O-K_;}Tw@0ZQQiUcU0JjVYx~SJs6uXJiPy&HEO=7@M zIT_z~TIZ=WsB#$8b0&0KAtT-_%F*GDal1Jtscn*sVfirz&Q~wWQIs69aicXh`sgl{OE$Se`HYp=yO0? zy?e}IVekYpB-gH)xRkKfDMB}I|2XDg7dsUS05FISu}F|s1g({a28JA1>kv9>8KLiW zn=rP`|F*R$3W7r4Z|66kZRelu?!n(3ipoG<8_JAXhhdZP5senr^~`w`d?s2YD@(C? zbsU-KhLPTYOdj}pv*c-?BO9$Z&7$q1yU=@v;+Qv1)|w{`l_Qm*Ov$lG;GdjG&gnR=gOI4AJFyKf}sIBxGd<>4S|7Z&_Wb1{^vjYwaRQ;fyHv*$deExz>uD1Y76!qaEBdF+9HPm6pLp3 zHV+o*2z?^S82&p#Q@ruQ95sejJq-l`YXD8zZ+@-H{GgJ&=q#--X20YNoiBI`VccFB zvLo>INc!IJgHHVufg%v@tE)yHA9Tj|O;9s23a`b5o${6<`v(C2;F9xSnIJQAHEDZ> zKrcSov5fGXV?Bwgl^2@|;5p7D7}4s$eey<#TUHz5A@)^OUgsHQB!wqJjRdVwgC49D zFY4FYv|Js4w_^wMTp&gx{i>Z1$-VMpO`6^qo-{yG=_AOfnKXQe_f;R{mZ*%#w6Tzi z!QAzw0pyT;1JNRi{YT%Tf=ZKHTE?$hC4r7FjQ$L(x9GZgzvI6oUgaiu2a2{G^fOv*c^vgAZS%3=%H*j2h{PV9K8x^$6^r)+Jf@L7B>?3 zZ~!T|zMNWKLkqj%9^i4fb$BSszoDa|DzL#IM+XgS8e_vb$yP8DhlbunpWE1`(Z*Fn zy)zIXdKIfm7$ToWMxv96aL7Z9y&h!5Dqlky*cmjFh)h$g!eMO5BF|;GbZOyg`rRN! z&QUVS8lUpLAP=68s%$vzEbLI|KHM~keOEYSAN0@tDT@ucX)um=g@;rPcc0vEYI|~q z2524odlv@hU`N2767G``-HnW`O4-be!37+`MTYRjhH(18^B~jys;PUrVtw#(4Q;5063T z`z~dOBEow-rSL4}(ptxnZZLFLSy@Au^0kS-hAjuOL2B~Mk)ilxYgmAz z<{;!^4RIdF7M3cG+<|H-IyRZ^(P(Ve$Yg`)RAaO^t3jDr9Yd!kw8a)qlJ<+=sSJb=AT5A_(wFo!&S4q2D`!cSISB4`4-<543 zY>LIA>JrKz#YEqgiv(^U*L0K6)<@ZeJ^#r}o+lzl^VsFn88(v>`(ovM_+f}VB|TBu zG1k^Z3CGIL`D-%ig52UZ=Y;bLZ&~Axg=+D%Ayztw?d`3_X+svlk%xAw?^}h6WuA(8 zh#X@=gln7&p=x-f+P!m1<9L6!#e7H7?21@Y(WMCz;sAkY6fjm>8u2$v!cexEXN57XnjUf zxT%@EP%Z(h7XaEFmC;Iup|pgxcAS1~Hx{r0Rp}PP=Q3zDz-)5kx@n=JC`+@jp>7@A zwno#S*OJ4?Mhu!v8@2d$4NXyCZ>o3fgZ9?dOQ#J*Z@P(y@dis}!8y0;`_e4qa zDW+$F*UV?Yxh&CM1=I7hlXSOK=;`IGE)2ni+lCZvVc7nsU!BQqD_^Vtc9uw-7$ z@=f$rP8vBgXftxgo`?O0)}RscEjDc3v+M?!3fLG?4!Ilv@fmcZOcbM_h`%0&{3?4j zaj3I6coqzErk`fgu);|75wI8&qc4=#bk@`&$S(Gt92}%wB=IuJ3tlrp=)TJx=SdnR zH3(#NfRPh`s2gLb1syd`wx;Y7`%3oQlGfOqV+NZU`uea`EvYbvY(@UBKz3PSIu&Lb zD5;8(<|Uf=BzBLPcUV3YOsM26dalNh=)$Iyl+B)}L;T3#clt;d@yybpr9&B}+&QgL z&{v(E=7Qr0dc)|+#W9lnD`JJL)RD?ApY3eE)S*nwffb$8gB{SV1qYN-nT!I0oTfgF zU+xrQ?}1}^And?&p@i2 zze)pUO^r%*AUaSLCcW|0&=eYWG-@v+8F{IKo&G`!jAAcEGHueeUhZ@^cb@L7ugQ8} zpKNdBq=mhVd`t%^36UAP5OsRK1fwlj@FyO0Ny^+Z@oc^^#;RhNHY_q;M8u=nC1LWfsay4+gelGt$b4IoUK)?FqZf}aw zaz~w^{-I00&%6eP%yIkTcudhFMRp(`bb(yGk`HzHOs1t-LZo>q%{y?DR0<*SvT+DA z?zqDMui$^V^GDe{Y!o+&aQKw<WHUc}Jb4@u~aa2QlRiWXs`-;M^|&<9opu1;!AFp<}X z208R?*s+S!tVe}r^?k=1Q0|lDp+J`gP}ERHGMah9Xw)d=pzbspDU?u6b^Xp%d&XnM zH4NuMmZZR?xns}i2O;#eu+^N1p5Li7L7ntMO$191hkp1pP)cef0jUkV`3?7e4Qn4$ zD*%%~Y`^;$rPn7};sBXv1`p}RQC6#i?ji9l?L9hC(C!KXIRh-* zd{u3TM(wEiwp6bO_hY_lcvU+&tkfH#bb1I+Pn$rA z1E&QKR!R-1xk4uf-jz<@iOSDs^-7~5YIRY4jR`^z#aqm>a@wp`8hKGYEuWkpR!@)f z0@@I#wWc_!zOFW*ShJSLezCE5O;I}%uPgQPEBIM@Sv{#X-%*!G)#fR7cLe>F#967{ ztd`GDN_F6@>u0qFwTANpR_V1>%(^~!4;cL`Uo|aZrk3{M4O%(^vg|Ri*5{;_dKJc`0UVbH^ z&wAU;*WM*isF$MEdR1isL#fhw)oNwFX4Wv_g|@5-Zi6)v^xFnENQoG~SBk~ZFlWVn zP9jx&n!$E(98!MD{D+IKl7J0nM`2Uye0=Y4@e^dKSCO@r(K9e*d^h|q;|1rHn+~R4 zx4Hv9ip`{i;os$=?H;|)TK%~FyzC+7TTRagPVhlV)U)i73dD39z|56bGooX+wKd#J>6H7RvEMRuBd>cNiQKZTg?z#g9d2ndaXP(h44V0* zJIv`=vk31tn8cGT#>nc}!_u>SZYpC*aUYtuJ2V94#1T4`k#d$J8tNiOOd;&fy)Kf& z$X9H9lG95m6-wHPE$m;~z(~2TbeuG8Va97W>=~>h7$vO{0G8#ZlrP)A%&xjQBZ8`{ z>|quT$u*-aIWn-R+{o}G+|RTPIZ%_L5iE=q-PLNsb@$2(CPqF5)+tY%@nkJPTU}(_ z+@bQWv!^bB?C=ex1Iel@>$zE|%R4kIB)9DINK2ex+n$vOUbim zx5d%4Tz#(_>rnd<&&Aj1vwnFGLFeNq@iRmIch&dW4<-SeFaKNL+uqoX%KvuPw|5`q ze_!Ae=h%Rmd^v+>)2|HvjCab5%>7t28DsCb!SXg1yG3bs}p@Nc#_j;c`0x@FC~GfkRb46C5x5+CGIZv@aa9mwY64~G z5Baml89|xkK!pItHd?iz?+v_|U zyCW3Phh6fE&{p66Tp`DVvqV#De>!O;ZV6VY$99By-{=O6;Yxz^tFM^Hh#vqEdCp$E zfwh;D=Si7BVza#K;6LL);p@nmbb8@fc~{L}7Imeg;fD~~5N>91eX8k$VRKqD&-Vg& z=-nQL#aIJFA+4Ob^OJ>yvJJ<~iZQF*7&`ovdN9?~xnm*u!d-@C}@>atj~eqNc6ttqG;I(t3D2G=8xOg1g{k~>vAvFp!<;!KpKCu7eR!+FjK*(RrJ z*Jo=e1uDw`3_nGM0~PZe!HwY|Dl!(qPoyZlFbe8Z94V)s8%6cnMT)BDT=Y|-xhP7T zNZ9mz*0mhHfUljtzJ#5Q5*KL$O@sW1Z;&fv0kTg_cycH)47X`r$l)D0QwYS_ZY-+D z=@{~##Ouuv{LCPgv@|pNejF`uWfHW}+gESp`29OaC=}&>=E;VlRL@)yQH=JUD=CUm zOY;Oq5xOr)Y-DXHxqLBL5uRcx5{lg7OfAw!zc)Aj822V~zLq>lyipY83< znElu8_M`mgi+tkppX!@;Kf<4636S|xi-5fA2U5{jY1b{KrkB;dYe0U2ML@Bpl2q{g zK2*cACde6IF%=bv*tOwgtoPztrTwPuE{%zPfzm(gmq z0(YXEpIY89`-GKviOVy0kSWyD4?ajqmCS|QLS5>^j0#xQB-n*}X%X;#hjA_hUkU~7 zvZFGFH64zn#!ZeWohnwYEURPhuX~M5;6I7})dv6P)4_g5-KmB9=F!L&{+UZ4+el$9 zb!?x@d8Dy*ROabZ?NBfjcac4u$$V{%#m6>es&86Hhpp;qwP_y_`W|C;_-TkI?Gb7; zo>Ulv%^oBpuB76e2`NlP%5OUuH{4|ndl-}_Jn-@3{-|f>&kXus=4*bKJ{I_YZ0>FC z#OVLct<6XJ{|kH`{XZW4KOX%*9{oQa{XZW4KR$>5$4T{N<;@IXlJPF|0-1EBLD53b z5ci?PPA5q0+(qi-Ubp%Q{vmOn5TaDCTOKO-uTfQht)EQ@)6&0Mcx4PINp?lAL(vt$ z@T?`9U%f#kjyulFfv|EHW(6w)3)^~qe$uQ0b|}LV(g>ti1-vQ&RV=Ar<3*?+4gq7r z%V4~X$E!Ix4w({lwpeVT?*+AL2*&B<_J!0BAs+Cf*BwC@~5I1j0%5a}*A8N1p%=No%J%`6cDJe7-1WPVLH z8w)QezS+r~>~q{uOF@kWG4Fa(qfd81HQb4+5OwF2TM0Y7cKTv+{rhcK18Kn_3|bDQ zi_C_*)jGdWWav%8xB2+m8kRz;lp$*6)=TjAyT5kAUa9{i$_ZmTsFkm#BuwR^w|VLDc^lG$JNdI!_^DV&b{_5R1r}s}8Yn_dIQgdC6Gi z0^E61MsZ`Sbcn{%YfolsO!%VLI0PBJ1a2))T9Z61KKdl*o+nkkv8oylyt*#BxmNUP zjVUpf&|=J!|3jA#q5@bb|KEwr|M#{Z<3D|g&!hbRQU3oZ|9_PKKg$0f<^P{e{tpnJ zrvlKg4beW}nawpxf-EiMSN%l#IINsha5JobT;G^grh?1JWj5z%3t(J#Qy8H4-l`J+ zn@?M942iyq|5`6YMp{1ZVd6fFIv$u0D4~00GhUD~82|%FE zE14ItB9VDgUUpMmDD;eB8*B8CwWO{oY^+6W|GuGD{$d{#?8_Wd!65#v9@ZGJ{P(ujq;< zo;*o4f`f?5Q!0s$Djb=0+)K?E7%?h1Duc;=pfSc(KzA7+b3>nJgb8Y!G(!w~J`n#g zYah}!oD#yc+M0XB&PTwWMup;e11+$q5v}e9lxjXIeM@nkL{!WQlMg0QjI=trW}#as z+xqp_@Hfe~{=}lIYq4)SRN(?f2N*6aFu91`VA1eze2I(`rJsGG6mpS5VN0+G_{}Jv zNvtIgzuo_RdIl_%*sqff`4riqRxwmq+9mg?W&F^*9%a7T3uzhb;C_7kxL%dcYY^n)q!(6*;qW2bPA^# zy$~8oHJ69zX3=cYs#{WBuMyjR5~;`Ch7|@}w6&R(FdyT0E%-DQZ8OlfM4=J=+pLRU zqSfcgXMWc8y+}O=rSGQ(xeFpTs6WQN1|g^sf!04V>OIi3W76INoIRFro6FN$X&orq8N)_}p565alma1cZotu`a;Czl+rY1{PhY5eQLXN~M5x zt{>Q|LLoXnCg`osw>k^<)m6nexy19y*R~sCPC9G{B4iww@@kkZ|L=Vv`~UUbo%MM9 zx6Q5XNBRF3_#{efUd^%Cl|OH#;zC(!cU5@)Ods1-5hB<4%?FsA4>CqcLFAByrb07NdGrrx{QSc%Ft$pJ zV?{7Vi^~zqWhh=Zm@LXn!+Es$NRFN43bSA%wHB zcM-Y?0Zd#Cy)C@({QQhvk=JmXNniI;nkU|MphPtr15GiYl)TFq0G(1z&x76P9BKma z77i&Lue9ulLTXM9hb?&YVUc*u9I&$S(X;9F{lGQmqYTm*@1T3N#y#ew&7e72cB>4> zW`aTSG77=2vRQi0M8k+KapL?Vh5*$Bl8}LFJ|!ZZH`4nsUeRUb!A=1oZ*9V*GVuC9 z8A~wZwT5$Mv$BwdO|=`jCCm0}mCU`8Ou&roqUuG&UO;YAXQ)s`?%^g7e|N|JcU@kk zfD%h0o5C7S2QxQzDOft{L{Dp?cBaVC*8_<9QZcrGQBB7HHZ4&Emu*SW)7Jyd4lx%-5wFzKvz6qdJ zav65Nx5x%iu@kYC_)iipU^f3h{Zjt_>$_WahJawYz+F@G?UJ~=rT~hqiO?U8C!${~k%Ib2$pWG5p;3)L3M+hy4G1tr zBFQ?v#!$>zJ&;v709MvwjcJfKqy|xrKzL!R!C|O?GQW5~%{`s3Qhtfj%!YHaO|CF{5oxW$WP$8j{Pcwm^POz{L$O*h% z2vQqJGnHfXC#?1Bfmuy9thNdJ%{_tAYPaZ$`N-B{ZfVB!(}^pVoxU;k1Mwg6Sy zumx&!d6Y2An)k=Xqlmv)$R?9ET@SKoE&_hm)($M>&TClhgor zS)G4J5E;9IAOxj|E*2bAyelsFX1x$2XY35fQz8H=#ueA=#f^>P<`z~7Tz4NB{K+WT zUt7EKCcSC9*zpHUV#|NqeZRe?Dy>OEXbq_-awN;bIeLSJJ+;%;>(ZZVbrKHY&1t@< zp2CYC#KuMr?XcL^aEY4mVq+un=B(5#zk)V5w{kPkh!VpYG?eM_$^gXD zr$e&hXD7HNJ0LIG&_^yt8rnt`;XZs&SJ>&jM3KuPR}@n2Zc8C0$_~@X7K`gk9V#z? zb@8-jJ3KpBUoQiBtA!_+MIBE@TA27%8mm41Es7pTk<&RP30gp&Au>M)4oCeQ15ym_ zJ|sR+b$y8;!qPU?AKo|-zu&1X9Rz^RweFgx)Fa&CN)5G;gd%ZP9_;)Mipj{$Nfj7K zPbAUzhgSd=5;>X|Z}Qp$EEv(7n0_^9z{Ey}7V?}^SzY0g`@|+pOgZLsfgE$nF5k;Y zG9N@L8I_9h=OE5vB+JI)#pg=M5?h}mCrfO8jXg|6qWG;9NFBAWNZ#%*NcYE_O{{NTwtgRu9Dpf9B(Y_VG{a?TRzrX#@|MkEB zumA10|M}bhEq?nyfBWzM>;L%efBg2}fBS!a`~QCXAArln##V8Cueh;O@} z@FwG1@zVLw1Iow0E+)GKuO+&Sy)Kn&jh|;oAdQ!X#IWp--6MZG?2>Qln*HeC^nZsx2cTta@+X)9%s=hn@L+=}@&YyMc}G09&FeR-Ez>$w)S z*}fKbp~4G2pv3YCv#1VXaXqpkp3zc7R}A%CxM+ejW1XqLD&T`C~v(<@6@3ve%y*WY2-g+ zo&6;~iRe%KIs<3jxtkex%$?hNqQR{XURHcH3zQlMEV>yJ7esLe(FL*i0dq}~sDt%+ z<5WE?H%^U{;gHEeNX#4`hwOo<6#4j&Z;jV}*GTz2Go0E#v6q7Mf>R4HX zYli(9kB2^qYv3|^G$0gi@y$d2Q~!z`U1o>UImml8hFU_VwN zat?t)oAV0E*eqh3MphaMc~Ee4WQ#8baPc~Qf#(M4HJ^1;1RzbVw{HcdV6LDqmQq2V+T2lR&RL)HZcK7*E)fL00ej(>B4!=>oLFut(vzLK&JYYwR1+ z#y0&!dXbF>H3`LNB8%?xGpJXsR+Yae*XV7pL;TW@gc!e{W{&LGot*kNtRv?L(JUB! z-6*3%p4qwNpio!tgtVfUUFSCYWOALC4nW56W&+>|h4={L&>mG!D!AXprCdA=2zOj* zVSuyaN?r6lehkBH+y)bOfDM*{Babyj*(aBZSL=JlSKd|c40W#u&|I;5gTH|-rjokQ ziGO33fuD8}mC~63mV+8UO`R@PSrr>@VNVCk{Y2*zXGsVKE#XNYgIgXw1V&hlwm;Ke zu*XHyP$@g(D++Na&j+5Rkso+SaN<$d)*%j@_mAFSIuKXzYDo3zMYWbEuN*9mxPWX@ zhVB*7tk|5<3fZo}5sPxA3cJk;wIKJ69JAi7GZT-mT@2X6fMglq?}l(3g&6p9;v_}v6IG@kq674W&;>DXGckR?

4BKgl6-biITbi-_iql9$s%=B_*h|sFREQl9M_4GMB z))iu%8x?-#8gT0v8cnrNUmPI%vBdf66|4y?UQQQXl_nswjOC1qg$+x|ImA&@;`t{d zj7|Vtzbb5S`cUeJn4_9^HT3y*5*5H3bpbcKKaRPd;dPHf3Mvyq!B_^hAN6?FO{qHl zBloZs^+etuLg%93kw#-p85<#spbx#9d9iYF1C|6SnA3^KrqqaZB$^milB7cbu=zvr z)*E*H8x|1`T?*VGE*-BwoGOMxQAXylY)o(9h?dOa5d&RQs!B*BR~uoSSh=U{YNha{b<6yU|S|<0y8R&F<=?SPSG}GmtY@tiT{T{bsog12V)Mj=b_Mx1GB+|RnvW;5!J{EV2$8j z?6TB9yc>sIJqNwC5#XBxKL{zLS%6oDb4#8eNsUJjQ3&d;&%UqDE$%icDFbkf-M>wd z-!(yRY!%&}8Sav%rD!8L#}K`&+foC4pC+jnaFuN3Ud%dd@%0+stI@1Wv%0jkau3(# z`rKKX8masCX=N6gGtJM_=bU-k%BwqfskSX_#~W$aJsppsME@3JSa;kYAQVX4TOKLW z$;cl{%!!EK-p+D&=FAd{Od`3 zh^{FGw9gPW&A|JK)}aS6T%BwcdJspj$>yL3ajZDUu=8NX8Ba7FJ%}McdkA)fw3)_@ z2x(igh#>sEWugBSKQrY2e|wnBFZ@~L|Gl}f5taY%>};<;%KyK>XKl3*NdQwO8_sP? z{WA7N*&RB$X!#p zJX|K12TJ6!FbzuSy!nbUeMIt}TwGA%lkdL6SDgLipCa=~Lh2Ja#dx+8nd$_axlgte zJJkuccDHONu~aA6)E%>(*r`sip*v?giKRMGNn)1Ic9M|lgywj+Y$uUaC!8#1aki76 zX5~5gDVpKrC$^#h9sy9FJAN_C5Z&a(2$TF2iMS5bb3z%L=}#^b;nHU|w78JX2O~WX zcqa(Lj#yc`FGj$XLEWEf?Z3KYR!?N&Pp#U^Kf{6GUzcMKXhixtd)7(;Yf7^sy+Iiu zMR~NXjuT9Nh!%K}Rjq`gB&YB~lJAj>4l{;j1Nw6gOR@{;h80=c&kQc^DlNRe)W8%! zlx|@59{!x;lDt%8SbQ}0XY`g;M`jg$%~3;%?rN6IbYC5-ESJdT#h}%he9W%XH)NUR zS2E?xqD)zJj-UUC^cWQsQ)ohLFwwOaKlIk*1MOIVJlXJDD5yq9acxDIB#;rIM8+J> z0*_AhNtQ;$hot9V$RB3T4au-sMM439jl5K-%!=0+E0HdQDGngol*r5dee5r^ zKvW;4MdF5m2&o>QmQJ#iC>phFq)N>(%?)o)YE9A^W^=h*Y^r*88c`?A?yGszlhHc4 z$c&k<&+a!w#ED>~Q$Gu?H*#|<4_NAQ9%OI0Knl<@URng(*>n7A?j$vIZ<4mX0>B*j zYP=okI&v`^kFD#+Xi}aE5%m2@5IGxTxpk6t&9oM|=LXWEN;e87Gjy9_wd7NogmS4) zIP}QEQvrLQCY6w|Mmz6N^7mDgE`AjL{cfL#{12u4#m)owvB3X(XJ>akD*xNw-q?7Q z|9yc^a{lMd^}KlPb$X857sq3=OBFv1GO=1E(|gBSAKpz?c#3Hz}rYMiM9`tavmXb^>8NA)`>H$BS&Fa5RJ`{FPnnikZYUcsw(84{!wenKB0gLN6p-Emna&In1*C4#R^VeR z&!usF9di_z69mkQ_cyFpM|okBK?E@-vJ0wY%tiS)B#$CKRg>kKwaoLDdj4|HTk@o3 zCr*wX1HDU90KBc9Zf$0G%B!}yOlv}>E}=mY8w%Q!)^nmNY_g;q>sXBq2kxNF<};C| z#NLR(+G(a@uT@3vcVg>c4gZ!$)DLa(K1bJVE*d!_Zd^s`u_KEw~)am>WW z$Q&?ZYHCTc#>gWxl&hhFKupGo^PADWUe>Ix$jy`A5H`>2ndP7h{1hH7Xjbh9bPzIz zt4!teP-kpoxo2=pIn#&q_onxwpCtb8;VeJ+vw;8GUEhnv{|EkW=aK*W0-s0z?~(s| zk^g%b{?8640K21BPKYOQ4jwFXMg$EHo4=<&`ivI5I5_a#27Zx{(jn$pPP|^kEJ>2CC;45zM7`HAU|rgSmme zBM$I!9_t^WEx~GbRZ;ReNw7=pk9ysSyv`?<(ll+32+dWy6qB&oRYjua4AOShY$?0S zRH$qP3DNlYpU>g37jmFI2O3?-={+}Wr6j9_2H=WYP6EBQzvmSTlfc1>$W>k_888EB zlCA%P@@F%e{!h!T&6w}!;oa=c9-5O&n94o)I`pKP%lbcGf|q}unk#<3kimUEi)DQt zhBQh*qMyQ-F=q{lwjUL?|5-i}{(m&Oc5nT0@pjOExIPx}{~KG|dkOv@n~(he7x;WF zN^;HhHO$-{MIb^dtXviCsn_o!Cnk4`5vV|9SrAaZy#n4bmvM*J-q;@^m&oQNz!;9b zkD;kaL6qMew_cVSul7ZG|9yw*?QXvZ=JtKt33`SNO}V@;t~#ChRnt_&IB_SXDv@{5y0^LeJz_lIt)>y9S9eX#+EQIvoy9E>K+cLqpV-TuGbjNVq84{KQ;I=kJxqfoQH@=5Df_|TaBS=s~F%0^U!OR5=x`Md!CUS{Hp>Wmr z+YZ$6$8OO9rnoci1D`w`0Ury4tIEKQL)S${oZE!a!zkQg$8&>>4DG1e{_a}^*>6Rm z-w`VSTP8{h-Hu>DuY8xuP)jY2N~P<%ou1EqpdP1K{BCBWUGYun?CeeDU9H}Fd0ss^ zY#kA2{9L#n-HurQVXf<4uMMaD{&O+-(DlZ0=ue)APS^N8a6Y)ASQLe0@qXoe=zSEg z;R){8;Qb1gpaQ^sy#x;SU;{?}dmpnu>?eZ%$?xEc+W(;Y-%bqwZ*Ols=70SXpJe&( z`m@63`o>0HG@Sl5G8-;@1Ju*^Mk8PR-~bCfcK@Cz{f(!CSeI>~Lm}K0r@lY#_NIXL zei(Iz`p2Ur`B9SmC`o>lBtJ@$A0^3mwj_#YA~&szBJnh%g#s$YqL1($hQk_9>_$5% z6Sg1e(6z=0r#z<3+j+l2CJ#HCy`cs(>2gLB1~4?V$) z4(}-)J`$PQ(X_o5Oxr~x{^oQTcvnNWOI@kbuYj@48reElKe)FMaU*tf><+P{8bnrB z(M|Gbb5MC$Wf-=*s>c?ICNPh;*Vp1i0#!G%MK$Y(QsDnvzh(jE?d;!wbNhF zpB6;RSqk1jIZny6K-bkXWV;X zmFNMz@=*)w4^*t%u*bA*hgBA|?Q|H8D5Zuz{4vOwXa#F6XgDF+D_jQ{)LF$mm#&D$ z?M3o7r+?$z2IzRqOiSOnl76f}Go1nj2?Q#4N>`&1&%Jb3&7-*Q#u-j@K<6p&#C>kb z8v_zB@}&qbPz{aUj!WzV;LEm)L7;H}tdK@t0!IP&qffjphF}&L;9Uo5#VBYPCRsc= z5dRSwtOj)91rQ^lE2De>o7(co#>X)?ZS}CMoqvt3XUF)mD_Ks@u~(sgb5bqkn4DB? z=DSX0gvP6Ac(ce9{9##ch-h8?=Cqvf?obDvjkgKxbKi4;a7&t{dO1PqM)V~TA#-b+ z1!lHjUVUzK;_%Uzp3Aav45iqiu$Ytv0O->W#>zyYHiFPskzQd!6d7trag)a~oH8^p z>A7}jM@IQ3IcSol54dQfG#7J^PhMy5C$PA)BC!S<@dihJj5!T-cP;>7kxt%d< zQjJ1H2a-aeBJz<~_4$i+SE-kSTlMWg7kr*H72{$2o>OQU@yeJC8gNC2u0IW6-4N4p zq6|fVq9lnBGRfVvW!!E*UcCDo#41 zqR}m~P~or74L@w5?oI>uAprS1Lo^Zn)GOzWO6l;h{zrp8>S`^Z0~ye}JA}?pt3Mkc zqQ>qz{7}-6vGA6xs8CmRFzU}r%wppBP?mBzOtG7issa@Xm0QD(il>bk8ZU*^cA&yR z1&xLaRBB~Vkv6omZKyy=A>I4yuTgS$VK`}QX^{K&t_ZhV)w3onS9GwE$CBctMZzNT?JUUSMX{Q3OIVH^v0yB}z4m&=-O% z2&SkZVzRJL`8BAlYN+qE$@ns4AtT_|#73)f(t?x6sUi5f%EaI*(+cWncTPAesFftSM= z(PT%$nv?t;jVj&#C>m)Tsi3u>=UqPSl@+KJ*}pyNz>w!Jt+e5+P_Z6F(|EKi8)CS}N- zcAm@R<9+hL3NMBnOFog1C8GvCL2t+kZ3AcG@4^e_$)kfe-w6bpU5WVgSfu{&ogSI0 zYE%m!CZUY@HfZ2pS$ZE#obi=AnYj;O6yX%Dbxc^02rzhZ;X;HPAe^NgxzL)uAz7Kd z6=?@@Tz)$V4JFB$32v|d6b-**pa)Q@B~3ZeL75s+>NzRiENR{MUo2s zq|Hvjpf9|*944)s7A9)iv3lAnmrhPzmdZcc2kCN%Xqo!XJBz#yIqT%{#*T`U0`RI10X#5sUo5~h> zgn0-(YWC-*hi(JeMB3zup$y4CfvXi!l)6nrG?j2w0=S{Pse4@5kD)fXPhScWP(q+$ zge>22IcI_*%66OT87qq)uEWsNYmt58P4ck{;qH<%&URg?o;}4_$xsQ-S|X{FJU6yh z;l+OA^r5+&HCHt>rKg!OqEDIFBttz<%1K64K!1QqMn+_C3*Obx>8qsLUYqm2 zT!Zs3pHxbx=Vu1rZ=9lT$U3UsQxtD86IkTb=Y&T>d=x`;(Hgt z*wM>R_T6NU?6b2@Aca#*s=1`>WM-e!04gX1ut%i%06VZ_LwLP>hsKVg>0=7az|TWp zq(<}D&cPp=*AJeyP7mLWyv_${lW?0MD}}@^i=u|2#~Uwj^HxJBcpMC%5^Pz(fHaV9 zQ3nBA=2Dww^^sbO%2J6KYa{ny$QOMT-=`J<6WxvB_?>VQBwX_3dL(Vt)p74RXdp>i zO@%c==$P#rB*vYuezlR&B}$NI8jFKb&KWAr0R*^~E=!E{tu;sMW}PyH5F&Y>(N2bE z7|uS(%bvnKh(kXq6T3-Ylf(}tB#S8tgDRkd&PJ|O`yfP7v(jI~phKYR!%At2+qYVl zp66vHXp%}=4nM?bs|H2TfveJIBu@d=J8T$hPcgnl(J-E|vEHa5T2l5d`U)a>v~L2s zXx$KaLQ3b$R( z(-}-LQ+0caUY@dx%RZ{U@x=Sn$q3%GrKXjli5OYSCC5|1|*ky-YW2Vz#*LS7M?reiR zbJM22Zj=DWHU*1bf-|lPbs@!t!$1T#m}_afOH0>cvdC^QK~5^&LP)b#wdP4I>1xCj zC&GoQi(qu^j6G*4Syg5De>;*z0-%}#^82!c$8-ZufWBd_OuLY|A4dQ)JWl?2;E)TO zLs}Pj1c)(ku3U{sQ4xt9(NP*s2W>b#P#(C30i}J-ZsFP4&e2Yn?p|mSBYmTxh2CT` z3ij95*n`6J*8s(MonUPQt1@kW&0{F|mjxa{L5{#m4xgnN6kgCs^qp3(oSnRDp;y*B z+&l^Ut8spIR;xD~WWYo_Og5y)|KdPw<;1VQ8dTQIp16^J#P3#mBI)6Tk1@xZ^CeIr2W-g(@FVL6dq^058k#zOCdgysO0`Gm*p zcw@FkDmQLX$<8Bo)@I3&KR+UKBn6s0HDU1T3CWn~%qKAD&toN(vj}1xQ&q!gC{iIY zu@oCj7={snLWUA6bluJvB>>?aCwp_2i)Se9y*uO>}b$@u}GX<@D zJ&16h!B(nIvSi%++)uuoFhTqz8E(Xc;_YjHWd|-SuB7FBk7qsauoMzcY>(%ry?ZtwslBetkkANu))pXk8f9G^XV*wZQZO#Y?b7#_La@U z)GtHo7X2d(iEqz=>k_(IPc{Q*cjv%odz9<;0CTH{&)E|Zy`4~2KczNf^ zE!u=4d=hFX(4{SJ5(s(edaZ)mCv*Sz;=|b^l&LI}#|u>p5+;OcvstYUP<$(&l1}~! zAC8#_R`1&+n>}?@q)Z}WG0H-nkikdRjYF8Q@FGrFSOS*NF6r8i z@oJWk0&$D?Ya>k0J?V}8=~b_2HpTYP*isriW1R5&*};35@?zHFyFWu4gLdaNhV89EbU{A z=&3tvD^;#x-?IkA)PaSBQ(n_mk}yqH6wv*R8{U!psMP7n=oI0KIb#euWPvS8!=k4I zn@#h->-2y8{>fI)rnJrYYBoKKF~iKyswjydTsk=ypd=kc1%)_0Npj@28@hHz+v65U z(?Y7t;e`1qM@r{4wsAjpSZxX|4V;hOU^<9lZ7sK>ZEg7t1WF}JfyFT5c-JjP8i=;n zRN5662W0qL81AQzW=Pb!vM`w9m6B#f@bZV($*{F2WaH2!Li@l3LS0l1P{eFj_1-oOSUgfW@o_jNEF0X00i z%xd>wB`?zta`}~SgOz;pusD)VLjOykP5eAKjd7}pF3I6*>`6j-pLcIy8zih^P`gz` zF(LAqB?L;4n=8RXf{=7x=~=)?)WT?cYMbf*WQ(vn?P9X=w$Xfo^;**Eq|mX_cC1z! zxm@B_Tw*JHiiygIy->0Il&K11^|(6OQA*Er)Uz@$T0ryKx?=M)e<#ke96j}ISqfn# zsn~8Zh8ij=2tyXIm7rn#V}3~d$E?SV%Zk%*s}t>Y1ve@}uP7?qCh+}v_Kc4<<+PEU zWqrFgVL+|5B*DF_A)YC3V%|)EJ3a057Pf^tPjtTjF!XO=y=;_C6ihIJhI|ywT+1+w zH{LidbWOj5Vj_GGS^S}gCaXw45{;*{?z6Njv0d`Ic3N%Ll5{Cc3g(oduM$WYK4FIT z`^KaVaN-=~MX4SPv5Vw=znWvAx7`57Lb|5w)^U{)r*xV3;ni_1`6c~zK(A8c%d16gYiKLsI(W!q3eDrM$7|B>9da z*FY|EW~5cZU5R!CmDC3zF1YsrXLT`I8=&7bbf zQG5z5)G=pS;RQ!O!kwYXr^xzK5WM>>J<=OpR0@WH-6H3fI|23)i#_b|ElY7Kk#U;q zL+D~(c~S`IcfK+>jlr7+X5YB+U>oXH%zg{Y9>Raoz#KM;iQpH$;fVT7fozfxEbr9^ zSi#4``^P=Jv_WQ_UY;-pqXZWDb9i)q=ADstV<^{OG`CktIlT?E4-Low*t3Ah99#_Orc1O@WV3$ zOT)T2b#7Gmq}zd+=9N@t;iQbI(X?2?n}DUYMUBb9x0==0mD+hTFIMEHT$#bJCGd2g z?tn3tlPr8P%mUIVtHeC(t7Hi&R>_;Dm|B!0H5Y(qYKOIbaY#`o(LG>*3_of116K*T z3%*8n4R) zsWPr9%TS#06(v|9Y7@07eRp^@>7jgMO!q80F%?CfHW&H1kbYh3%G$RR))WUHypht& zs~&WI7uAa?73mn^-B4d&l4T<4DPb6PEdSNO$Z)wzMlI4H%8VC>7SCAsx!#APU)KME zcgT0&d}l5_mmGI`3mEILeV<&yxU16Gz48LsMl6Wasdr4VnI4ihDN6`s5EY79Ss@mLyEz(CxIB>@ z*FQOd#J#RgnNwK$sQmNSxo47P=@bHUi6$GjEiJVi1z)e6Ses3kjb9| z|HkW#WJ%@v5_Sk1D>M<5_zUQ=(Zup=v*;#6#p|^3Mq*13R`<^G9?vG(E&Uo3fXoOd z$jECtijgO*?+PQU!Edz<@c78D;XBLvTp6mv3&Yy2Mdkw`--FJL9&C7=DSJSxvq8!X zRMb#96Jzdp$Zb>xoA3!O+>=h|Org$v#L!=^Jfaf9RR@f#B>qV7Z|T^^h=zL$&5o=O z529{SWNTotDn_`)PR8Syskau$o( zUPkzdajJ=R6I_k(6LG|$TFJf@l%*vxpq?HGBd@gXIq~Bs8;CfCd>{26iRgye&zr+> zjQUQ;wWc9eZ?UOJ|8Cf*NyhKo4gG6*&qOxAfhFYj-->VlHInU7(p$J|fxZV0S9?`w z&vNId`Zy%9qv6O`P0M=RPeaNuN7fBGfIph#%WaZ|X*H}5W+GcveTF;S;$0f|g-fwJ z4a}3k4oT}r%g=?V3Y|1hHMyq2Vei0I@zVD{d~n^7(yJN{n##vh$zUTa)qQabe$wL? z1&swb20t_K41VUG^rY_QA9nmKdEiyU`k2Qr*+l=ZNOwu~q9{UFNy3flLsB~xfp<8` zE2}}GUYPOfMpxea%y3cqK5;aOHXz1GPW_2}yq6lzazlyl#Th8jED;~LfDK-PjK*`V( zMIQdmP)l9*ZNgOemPxiIQ>DeRu|kq9kcpdM+A2r@D-1Dw2(VMG*_zZH;#ioKFy*ij zzMf)v5&NNmHkV|PW>8G7JWHfjFh_^7mLkgmm6gO;@Lh-Osg_R?Xq%;Z2 zKc$3+ymE#GcQBfSv7XX~s{cJ5HMIlf^=FYLv49N9I4$B$9UU}IOPfO3iWbF~PHgyF zIRN4?kElg8&L_BVvJ+>Nv%EL0oJQ=CQ`kTTps-1Cg{Bh>H)W(8Kw&S7aV@C~yHu`6 zW2MFT0PSLzB%TB40jxBT!u^IdbvW!n*Ip|HgjkUN&$Uj)Vo@znxMazT0`EyuoOMQW z1#-`AI3(<=2pG(jlWn?KYP1{}fWNrD7bvh*XM7dOrMn7`uGJ$YILBv(TogrwdK343 zprpNXjHF@va5-(;wry+Lwr$%zZQHhO+qP{R)6+(M{l5A3x5@5ilg%def2C5n^IXrB zbDsnHnun^L;qFv~lirjMCo5bqt7<=&wh{BP6&<-Q+lLjGjQR;PQCM@IAw9>ypH&ak z?crc>kYr(235C}Mv2*bRgj5;#Y#)T#i`a($cjXsY|C$h>RI6T<4vV}GSd3jw6QsYE0Uxx4E9Kz zkteo{#_xa+jj*}fTlTLI`Qy9M=hM^oKAZnjVDSyZzqPyhnf3&MFMenqDFW=m_53x_ z?G&{uPVplDS4dw^u3@lYa^A(=+2>2s(Afus(R}DapC_5R>;RVGXN<>wJA@$AG~D{c zvxTe$Pj4-e9#sp%khHa}o@$-t#h5{r^YPohkqsbuGhD{HB#^W zKS`7qy1E%#r>iow6+sZvrH2t~y?>gx0 z`={ftoC0TdyR6#K76$*(u(aZw04CT^C@hg*9`iICc*?-`jddgvpx-CQ;BEd572yi1 zjmm`oyjzJy`>Z#0g$T|4L$c~B;TFpA*!EH{(SY{mE?YQOh9(O<-r)7rL^|A_YxVei zS5WJ$D8f>_gp=hE4lXR4^n5&bs|SKolt8R%=OY5#OP1UV|2}H=ELwzX>8j`#>7NyN zL_HA8XRSz)FjSVEBs~sYUO4Ir;v6=wqW(|aeVrkrWy~t_zjgOC{eii~sY}V0x7beJ zN_P2%E|T*)(pjk|vlee(rOK$L$9K&dp_+rwd6WHO6(3zL+Cj{GeML`YF5Ae_s>KOO zlg^g5vJ`H){>&?BtE*xyFHfa|TZ7iH6|a)*Vat%c$zLLv$58tXO;R0^`qTpI24W@e zUA(N)=ap`o<)1F_C;L5|v_15AL%2;a`5-hZ{B`PjNB!2~>+q44nJcHBO~kR*EyBQm zij@46>D003MkH&5{CWNpdzWkq-Z7^6#pVIGr=QDF2`K>lZi!;i>pba_5ya*Pg9wb7 zoc~NT(CH@S3~sP;c~`Fs3Qi}g*X^S!gyCo*B3Gx$K*Y(&tK4Ck8mtGDD{I_%*zDx3 z#8;c0O7oiPJ+r5VlY+~j*Aie_x&5|<{LedfsZ2De;X&P);mp!f^hqKaeow{mUI$Y< zh;E^L@&-g6=j=os`TZIs;cc@E*KJ2SKV4rnBbb(LHz!rQ&gH%~m}9qoDG%6JH^D`= zq3t)Fqi~L*ANIZfq_42h}Y$iauT@Zz(QOM<@CQFmUE8Hzpl1Q#%jm z8Z-Y06mnH&j6*}mb+u&;vvbw=FNi{HW^0Qhaw>TVFHtvNR>{8%ZklWkFz&;MkI?BE zPskoNLt2ydIFNZ_D`ZNj#t$d`+Uv3c6qvc?(v#4=bayZ;A`n(3h+9+rG~fzJV&oku+Ij|Ge}B*h#BLhN#2D)E}PU%xEtRX)(m)GF4_Z4M_Ty9*2GYc4+g`&F%Ud z={av>LnDkHoO>)J9N>zRv2ej@S!D)RBMcqmLhm|svDh*iN7Oug2}@e*7l;JMFsf!t zX84iaFGTUX-2F}%at{TcZj`=H6eDb+AKr`)$KRRiNK;H=m$&5#UZwW zzeQ?|&a_DB6EqT1%_2hAnty1l$)r+lbS$5Aqft8rY8)@Br|ty^xf2p$MM6*G%i^uj zREny!u*80>SLkUt~-96V};UFF+|Z z3d<=EFWk9wgGAC+J~AqY*tqql%Z6c`DZh+ zBq0YLvn#FCh9l6tKtEaer5VC_)R53SH%`D#!*6@oBk@x(ONEWmZ-c>g=nunr$Z~=j z7mX$GDp%5bSs|1;ryp5<^$!MMBaxZ2Wx3{`MJuB<#8kGJY8+m*H)Kdc+|SfKQ+RXu zGm$d9O5a&Uy0PJE&ql|Y1Fa_7S!X<#fhxs0^Mqh!P;E@D10>Ml!MSDIr>n&mOdh@K zl#o&mec7f3@=DpJ{04CzRw4{xlvn*E-Hcf_>z}Y`D=ZH5Gz%TG?RG+TxVqBP*}Wgm ze2=&Zya;Lz`a`dTb|BPUPe**kiwL6F)!S&Xj4$qCG9~<{wKQhlZ`DC)h^I zvhi3VVl`N5gZJt2UmJEYKE7tlnfDY~)hsiZW(eSv=e+qsL{ySvQzdU9b!3pzeMQID z=iKO|o@ooUU#7A=McgmWnl;iF*6s|^5%DH5mSP0tlZU()2Ql7WURJ|C)jvqj0b1?( zf-3^)_zu0__CD@^{xDPmwswYJ5S9Vkb4A~Gih!4Lz@KFR-z5P5$uI7`&ky)(PqXK} zbh&zZ0VI@ckl3uhboSVozU4DO`IoEDL)?ieh8xZ+R#%!s)Dvn_7>Q@E4GwNHgcZ78 z`R7*Q(f6gxkae8p2X7(sL_+F+O&Zj&X zRWjdDowXed>3mdczH#|XxPP<8W(sr4iZhujVa(vPHmH5kUY6{K4M7$_yhNtqao%V8 z48YOm-o|eH+?Tbb{a;Kl9v7G7!kAE7Xq6|EVd)b}bo6QZWmvQ^3}mz|;R)n)xtnNo zKr@KP+;*CXZ}pd`+gg1Mjk^*i$mTr+SO%5+D@mGcMPMNldIla;P#w|H-PaCW)4O6j zGktJHH=_l-Z^riP@Taln@mPG7aDfSdhw4mOlNVwQNqveO0;fYGet$4K?k>miB7>?2 zy#4s04yaTE<+C#~GgHR|{UrD59Vv$=s!O=I&{$Fak>dOV<@swJ`MHAF)PIrP2@5W7 z3G$HDB7#84w!4<79*j{Icz$z%t3Up`AXgjE2_6<`a2$Syr{WTL@s_FFU98Dt5$Q;T zRBDG~NQB*&Csewdnys%FL^%x7a$y1PAr%V~6dff-t=WpGydPwGmxiJ{BXPI3uIsJ> zm!m1jiKj6HH148d3z&RSQiXEXJP0Z{x=9mWP|~f2 zDv&S;(wXq-5k^XwgS|8hQZ~mP!`LLa@CY1*UM{gOKE9DY&Fk}zJ7*I?IWo18RrK!jV0N8jiO4&Z#ptgsn^_$KGj}d< z{AglcYUE2km(dcUjE>|7Dx@xM-gqx&hd_j9WoGPR3fMm8X`92z;7lSg`{;-udlMWuiHr9s^raaYQiXTDl}K zI-9zsL|Cjc&E7hlc3F;9k!RPoJ(7;>4R9^w4NF7{aVgxzy|qRdJ$?Ha(z~NPBGDD> zjfUZ*kw#fQLLA|#SkcbOy6OD*mj(pfo zSef+-E(oUZh2;6Us)T7BWq(k~cpN3BaEeb$*g@RmR1`po_We2XA&t3PP_)FGRST6) zIOWRChuE^x^;x|+RMN6bgf``$yK^M#l6ZqKi&d-yI!7bsQ3yxJu zpI*HH5$7GN1W&ySsZ-%7E_OR5flFVLt9{7NwEX1WY>3gqt)94toNEczZ$BD&J_l89 z|2B=};L4g4fy=nr*u^7WIesaV(-<~I;IVT6EFcp+a<#bf9eRBcmQ2|CL+8k*W5d!o zltno!kR#9!Ylp}anzjX~OejYOMAw2=ng|+%rc)(Vn>T!Zi_>9S95tY}J^@%h1rdgu z`79+7k9ZMMK@Q!(x>BFtMRFOWhv}h&HWOZ3$*`v*MmS3OJ<9P9jyzLs1KedOXiESA zJzv`%+(Br3pwekakrQm=*#a}B^&(}^ls$^wo?o6X!KozrNwjOH;`TCf7fCpY^vt2C zmD>=l*oJhME*Sud+t_^Z;$) z1w=Ww(}+9JI?XmbMS?R^v)_hd2PzUjquZgBfP^MeGi3fx#TJ{qC=$z@lt;O6QIHJt z>#}94rvnp+5~)cq_wC>A`^!V$lqk%t#>Bw=@}*~^6D}x-*mw$)JajxS8*pmW1sX*~Y!P5!_tQ9vSSgNf>c>w9D9^3; zBYHAr!{~vUBHW}_d7S+F1)(#EQns9cs!S&pc$k;X$1)%ULGu{(+^lrH^B}^TK{A^& zBFSndu+MubWsbIQ6^Qf4P{H;2=UHG*Cr>gk{6da)uOO7PI)}7#m&fy3kDJ(_czC5Q zM0B}>zs(i_=-Awg0agvmC@c9h2sL37M%j*K6gLkLl+Wz9wXZ}x#o2kz!_*Of{nzP2 zIx^KAi8>=Em5wn{Rv@4f4u2QRaxw;lCxAvz+)U=$8o^GWd(D#6r<;?{cA`6$!rN>R zL}xe9bivlk^LFV<@{kR)2#9wDBdq3BQaLPr?OaDdpv56h&ONcM$QFa3kQisNv{`A9 z)!dK>VZ*p)H;);GpjM#yF$IA}>ls{nVs>lPsjTIH9)qC3=NVxT-(o^9?-#wixJVS^ z-hBzDcFf08plt(N{^AynGyaX4h4oA^!fd=S~mSrFHJx+YZQ8h&uZ}h*A*kn>* z9Z8KyTRisML!Nw^=5tGycbt70R`m=mEB2vP0+53ObRJ<0o^2585m`^?E<$Fky|`Xe@G3b+WnLQ2kAFz{H$mHrmzB^i!$E>q9{WkYvY~7;3#5 zV8W0oCV+;YY!`U$YDDOIa!@|iT}4hPghN=aIp8&BoZQAE7>)E9=7Sj&ePW>-1$%K{ zrZb&*ULqf8w-c8XGDy_`(u;R>&oFCGvGX7A;~DfPCdxqQC^b@>SfeZc4jfL?JwB;& z3MC!V7R)GT(tOAEtXPG~F!s*#<1)RLVY(Vj=`y{N6Xrm`<}$^FS8j`{xvD4k^D zg_B<{`nm+Iypgf&tF)ZPnr=t-^9N0Tfy@Pv&Yea#W{Th(xfR?2FEM#1i5{Ew$`Wgv-&b|7u6@^&Ki)tq%{C!>H$9I_kB@^0K4it(hUIrJHYqHy!3VV zfB)QFpHELG9IVk2oKqyGpZ8ayHW+u-0zC>c-SdLU1W0GEQpf+~$=dwJM?g$nM0<;AQSUI=8#8x)^I=^_N_ z26ZFv+GLSYR~^?{+ULL(MbE<>9Wo=&fXZqy_y=SCcGE|lnj;2QBe82>XphrCk#+jZ zW0`(vP#*FygVP58X;6#zS{{3NqCXDCUp?XwT61Ea|EC$bs^qUZCyYPqi=h=X62zm7 zc{7|~6;NvHja@s9`+-%VmXLLyw1SW0gK~2P#w3Y!!d4QU*m}InEr-Mfp_{rDHIoJd z_K=ASgpQqfGs@f$IG+DgRZyEGwKqQt16*};Eslaaa^uJZyZCWnXnTW zNH>+kYpJ3<<*qf!%*4GfWMgw~jE{x-Qm~yT`OQ<+P+xvwp9}fZNi;{7xGCZRDOM}k zARIx-gJSJpsC5=}icod?>fr*+0zJMi~1pn%)2+fF*6FN|T zDBEyIF*Q2k$lBwmI{Habm|mEg6f6-O_NoAIMov@H-H zy&G1aJp-NSS+}%GFaBYV74x4_fez(EqM`)cH2{{05Wl7UY~JcV7KEVm>!0&|@_AkC z?akFb#ozH$0Jk@GUsJO|u|}h3eeIv_qwi_@JOtY)jflD4**tfsd0-C$951VFMOS@o zL4E`#p)mY_k+nQB;uP9lL34uINmL~Q324RJj`SM*+mIuax}1sZFOO~NzRF}jo1R*{ z+&mTRjUS`xl&s^AyE0^k7|}z*X|ZWl@#gbi{E9*27y4AC)f2VK=P7~Cw8z@(@Q8e6 znE-ieh~H(&J5LksD16nY4pkn9B7bVipEiFGW-IQx)Me3w_MPt6;M)=LK6q2v}m-(iaql_@F-pND7_eCfg59C8w=t~o1K-T-iC*h&uy}KlSuw*_!Au3 zT-=q;(;2Yz5EM7~onhzQXyx&DQaq2?xH{X&yhiNEU?W&In6c``GNv7=P9jRP5px9d?;rP=Y!0NU; zs8qP6@DbqyfIbA@6Lpy1!#HR#nWaRoniXI(nAxbZeUBypMP!d(|jIm?|s`7 zF*H$|t51J7ps^`=AUR+nS4Y9aabp)c&{HJqz$j}_r3u`69Kr|^J@871QI5c8g)dzf zN}5JTi!!xiYJMb(vS8miD%I7&yG*(-`AW%VFz;Tyxj{2<2IL&T?YF~e4Aau5JS>Cm zl|_SzX7G&Npht;XtfZ@OTs~+% zRVQNHm#FgbtMbr5QKIe;W=2|>8avmu!vnVbD{k#>_*iVzJx|Gl8Ij7QZJ%_*JR=MI zP#%ip5yZ9>l>(y?M}U5XipIYWo4dEQaM8{V(#ev`m5JvEoz}RsC<(d#UFgn&N#XY(1-NcOa2xaysrwN@K&%Id4> z7`6jUzdOm~j(S_YRv$CXXT^p5y}Pz@bQT~0b1rb2cegP{{BCc|Lk`&5+Wiv^cqikR z9NzT_Z>L!aCGCwx%3-_+c}XJegLM@X6Aid+9=Yz?2uRf$D?2+`eWTJUU(W!IvF1s7 z-t4tnhyiJnc02juWv<&vAd2n?C@z3ItqxSQIMY`5N_R=C%-CtbTQ}6R)ef|$-DswQ z)yI5(Ih(I5psS-W9(W+KOU7sLTxBH7PfMj@| z#?=%N7&RtL!B+fnjInfa-#!r9!;DY{8rZ}eaXxG*;@mXN^XFE4oggKzYGzfj!pljo zZhbzvbj(SRE`!nKe3wz8A$(IuVSL*|Lu@RRlRAf0WlFsgEsRS1J4HQ!^sw0(80x|y zlHW61c`;67;x`CO1h1W5P6Q@gM?*(TPos@#5dE|W!i5Jn>s+}c`Qjq$?9_==QGoXA zOYa`Oz~g`Zi@Wz{5FNlv(Jzb(fSd6@0&Yd#cYt@q9D&3)#S3=pU&`DMkf<}8fE3M_ zJP~+~KfnU?5wKhe%(ed*)Lt`}Ot`_ORs z&mBtf8$Wgqc7itm!{4{Tmp((IHc~^u=aRVQ0TSDL{c!epz1ilsALAohrxXAmGfu+M z@KQ}dR0Jz^@J)YGvfnclYfL5B=!Ex&iw2E;1){IUaFF{l0QO@P~N=&~rKVrplY76L&9_tYl=;MxlMk3p@3-S z$HX!A?4VLIaiXDxPgz>EtWl-rsIsS~i_BVNlq^LVj?834O*JkRI%dmY9wpYynwa4-D~tLWH}f=)L)^Ek(bz6pK6>`5-(w`)m>#c9Yf%z*zEInv(_G zRND_9`Q6mm|Ibe=1^;@vM~&0a&VuIx8C*OraP?yih-8uPb;(P_l70sf=>I3oF>5PW zLGW03St7XwyAp`DC>4Ec|iWk>Lo=&BQh6osnU&*WqK z>AiFKs;d2wCw|rHV@4y<4waSsG>Qz8#CL4CYdfn88t~TpZMA4Nv#H3fY)0-V?eI z*A+pj+&<#8{VnZ+UM^!&c`)^__JjSE-YN$3*sP1j6=7Dsa3+R#ond1UzL5`>*Y)7< zJPcm;=^bX2z3xBYGG z^Rn>$A?>}o%4}`@yS{?ZOceG1n;32|EX$>;y;psEoyuuP0(m&*KV~?_g?{NXVFWum zKxcaPj)N^-lB9v)-H^y6+()^nM{n5jJJ*_*LZ2>_v_G? zpUpn53|_{c^cY3F#LZdFTXf{~S8=rEF}X*jNLUpNA!E7gmc)}Udyk)j+d<1x1)EXh zE5j_~=R(D-!;x%QgxpPB@+rY-lm9o)+gI1eSH;}VliueK2@zoI=j}1=mAC#=z?&V< zOM&nA>`c+NMHLoMzq#la`2)p{vSpy(pJc;9;|{b)N&Fx`Km(Mpa|8RSR;V{nAd?|u zUR>yBVDW(f50M*q`ytcdJnSRbWII)({>99cr^2^kziMFp#R4uEZJirM)S^}23)s`^ zTXml3KZ25?4s(LXi@uneoF343`wu3-R@!TRqXirJ;Fiu@)EfRiRvSZXz`z}F%EOQ# z|9RmrL4HuBYb$?jH18kNB11l#4Wr`rSlwm&o`1NFyrQD;+qA+Eh{GoHrCx7P8k%BlPEVD3oIcw~)#ij@$u45alCX!@X0`ec zRA7B?P_pcwN#kO)uY2(?r~K1ft&SDDPz_^c&74Bj0)e|cP42&}#Us`KzpRL(o1Q=w zTRB@cL43zalG2mwNj;>ozb_cYw{H(Op3sPL<-> zYOgi0g26(-P%E5O$yKis2muRHjddi?QjiVF(Zs$D)gJ4H?nWcGnU z0>MgQ`sEnt@|19a)LT8duhRnur4lpj_Ez>gYdk7*^_cYp?_2!(HTC}(6=AbEw`~cq z{Bn4xgrikHncYjxKXcCJr^Akp>BIiRB`-}=DPHLES{fO2Hn@&$6CtK*N8qu^$6$an#lO8kTU( zhYTcI?TvHox3vRcA3uE_iOk=uSyg;0UbH%BL(x$EcH_cxd)>-Xs!|cKYvyd~nW!^^ z!Fk`H5cM;j{KI0~hSJX`aWQ5uJwz{tXV4)$9#C=q$TR`mRGKcmbKGtVB|NSlBB(jQ zp+qQ{9%pyhFm4r9yd4Q|V$W#chLL@Z-=XUaDk%z&DlAihpyhyV_2gg>an(;LfXz!v z{S-c1_194(y8+qF;A{_gg%QkYIInzf9`!7bW7HySMo`%X*rn!6-Ncil1_Bb32O6J56=x$oHBQp*K=4m+)D*E{X0t1)%DQH+%%6g?&jo0Aa({4hN<)Av8$@vRx= z@uugpH_hgzpW!4Yvb0dsk|r}s)SAksc*|XCSUsbo;a`6uyBHR|Pk+3J>t{G;pIhaU z*GPiAcy3fTTa#GRGOs-*GwboMid~xXlDpWZR#ufL$)#{nL;5QGQI7}CGTQg#ux|^i zzi{j2N!kkqpv(Nqn`U%eeo)b1+7H*=572W#w1>~Sqz001ZBTP82_va@ejBgSPRJ*7&64rO&4RjVF^mZ~g4W_IY0aD_u)XB+QQI>$^r&;>H1MUu$+LqBue|rb z84v?n2vtHFu+3L0J(X#|`kPb8{gE=3qAD1zpB}xh>)40-kHlUUYjB#T9uYGRR5=9w zbTma}w#O>3&2X@JW{|nU$3(_m71UWOq7YtNpVIKuh4GuoPFIxZmWS%cqO_Umqf+z) zwV2mrp8GCc#+-TMb>w+M`!^xwoOp7G2-@&;@)fjrxA=J+L~VInGgmhfr&T0^2g?b! zm?KwCtfCvGR-F;J^#3*?8k-1S$7$Vt2-7w7gS&EV?Put=U-;Ym(eA`E2UBRe=X3DG zDOSxq?9zss7MP*6e`ZwOtOjuNr%RmUMhl{AyT5MIl2^=d$T^ZVunga?1>zMn)qx|m^1d~-Um5Q_P3G{B{3i6u;27IoNx=~!!mhv3X2LrV*6`Aj)Tr!hWSs8+ zFvy={o6C5G z*z+3*rYmu6SfBL+(rB@sZFUn&X)p1EQT3j;W1B#tu2m55*e2P)g1yre2lgzGb(OX7 zx>pYHj0XHA<1bjA0hxqw7~dB)!zOehdiWT>$fuBh>98DmRNf%yDnZiPnI~d2br&dj zTwG^;*JOHDvM0-ViaDeXY3)TKN5d;(VS_g6kghrcO(K{EW@wZOR6JPt1nucxh*e?I z>m9$uM;WFavg{KG{DVX9XCop23FkvP0|PsbAP|l=9B2gAhfNIfdIJ1FG}TEE;2kJU zdu|H%tQ}|JN5JR#?;4`!hofnn5Zi+QM!r~|HxZWiuN)B(#(R|=N)E)rw$NC{whsTN zIu^WK-c0q3`D<@v$+1fl=L24AGK7{;87{J`5q?j6|%*B=BAU)x1pzSlRB*0YE)mInYlsmp?(&?}#Uc&m>^$ie9Jlw)pS$0?b^a4mZ0%SiJf7`nH`n z^5S>5>qFZP;9z$%B zg;pX~tgo~i_b}3QjY*aYoP;9uFr8wGNQ7$a)$RX$K9`&gfkt8kZrzKD0uK?GDA9|h znr`|#TtqLbGp)*PpRu(pd5r(T9D`bNHI#(f@^42V=$V+7&FkOZvRM9xfqz3v?7SZp zzuJB;gfj31bu`iV`+z6TQpJKDzg#E&hjmY=?aO!V8gsH;{kCwHj5M1j84^3!rf{$?%F*Uj&E&&_F$uM0AEdRcpqI`LlP*oT*=XxuT-{ptDCRq(-gzr z^NSNx3bDWH^VZGxWa~nTKLp(IBzWbGRfNbk4B92UfNTlVVYXga#r06G7p7QuBZd_~ z)I~rj&_k~+tRN55@213%^9{KE`tZg6*$lzn`RVJ6hGA_Hl371mbI?-MzF4y=J)P_OHoBt)4|;RQcfyjFU`Or$ zYq`2h<)_ph=ft&QWBb@+eUNv|qW#_1a~+q*_3hWP*<`Ny26$^avwr#eW3t=s85-S@ zc#8$%BQeG)8En-EH-|f~;*O{;OiNYD$Cd#v%3D+4acTNjNdRY_MVHt^g`Ee}C7+tt z4;7;8mLFtjU1Zccew3c{nm3Yeq%U{|u?*c{Cs{{}9aWOwjm;a7Kx1r+pVr6O?OKBgmZyoMNE41tARaX97%f)6^`a6J&{%Eetkf z%WKC@H|@dYmq7`a$oVZsg6ooKb|MORkCamxYfeF9G1I@sP9Q%$@Drk67A?R6+axM6 zl%+^iqoDUim7@@QH2vnr1Ew?1pMN4#1sHc#x16BL=HO&UcP_#!%{pR7O_wvQw59d1 z&|z(QjZiYRpg>pV$$o2M3KJ-g(z~b}*QP;&sBWWBGQ-y?G4Dh{;wr9lEujUR9Pnwc z)aG6{MvxszR%U|cXUol+F}HI{ph`hCV}|!D9nh+JluZchA^L_>Y9}j?GCX2sQ} zU=F;^&^S3e-@9-}Gt}8b$eFyIQcJ2wNj>k?W?D$1CbyTymBk^oWs8r+HBC3?Mxa@V zsgb3=PUJAgXkdl1SDOUj$u+BEs*V}cISsySSv-Ph%~po0Fok`WJ+s z&X>MDqT8sX7X*9Wvw7^7yzPyr9H8y{sp;djPX92e4Y2Lm*WucJ*YdSoCu3JIa~fB= zANK(f)8l3hcy*{k;;mbm&I#OS0+Jcw?(`nYWYvVwex2o5L!slzu z{k?bh(@a0Y@oaXT|E^u&T;I0!-6K3HxU~E8cpr9}dpq}U$PZwV4~XE;_!9Vf=?n1j z^=k9k_kDf8G7tG6=rex-JU+bxyxiJ`?)|>Q?)(7n#^D_fg|89p{IR1Py|9_WfVDNN zs*O*Er8>>j2g{tnD-n8#G4K6*$=z}5xUN|Gv$Q@Mc`V~SJls2F>vUoy8 z2FI5eSqn6f)LFu~MAo=K!a@gEC}i%Ux=O-Ck~(@kGxrL~Fi7B6&gXXPrC1=cU95D< z7dWA!%GYSCSDW=ccgVh@^qZ(8pm4A9bF@e4Qci##Q0EJj`+T^RGE4aRLnRDpKQqY# zKnC?@0oGp|J%FE$*5{*v^A%6N_syx^<9Qwee(^$a3^9S__0(iYRD}yjY+Js`N`BaV z8yLin9nS7|lZxm4FAF;?gqorm;cmY~cD?R+h7<@u+%hfTtF7iGYmaTyAs27Zadp?W zHMTxO9vjKfR<1~LGF`)S-XF!S4A!R^TinZ`Ylb7tL9l-z<8G3^Y!n8sIj|3qnzuz^ zk$$=}iaY#@O}4MdJp3o1(e6R> zdA6w4?L{Ex>kYN8-ie?n__@_VwXcD0TLa&+0lj4U|B>xaTLw-3x8h-`$1D>!=`XC) zIam1Lk^azjo(~j-qcyNfn;te~amX*xm4TdSx#SBv>|mS1in>6i&_;xnvRT?ShFYXd zi;*H^HaqwGx{D%yMC zSIAmaUHNItw()7_YyAZ}>-a7X_V(D^nj<6szVJEPsUkNL-#NPca`@f7`h~VfIG}K`tfn6UVTjl{^)Vf;{~bPG~l8ErKWQ_Uu(tJ<}l`aogeTu zsHgbRp$GX<0OYh{?jWu`$$n@eJtRUL?n%z)-1XL|`wMmU&@D{0_NnskCbl&L-C{WU zrOu54#bjy%x3f#GaS9mF%s<|xeL-U^W8sMfA@VRVk~SSy6N1mq@$!@=ppr?dsrD9Z z9vPB6=NTBlR?n&3Ko-ZxM;AHiCKT<|7~C`FkqtsUnZx8{G?RK>v>?o;Z=a{aJviOs zm;JoRYINAI;u}H5q8Y9{U7W@29n00zWvZJ2gQ~bZzZMX{zWBwAELNQ`P|&OZm6Vk- zC^U4B?kn<9I!5>Qt^hcE$3B@uvVCM~_nX_@YY$K(o6y?$Y9{iWa+(Q78IJ(E4^BBY zt)u?{+|~D~$)@rLRw|#oOg#z(&C1}vfH%o1AiqFj9z{6s$f(D^dPSV{uY)6wDM2F; zLp1hVI}}AU4~ECeTeDv*UD|(82Avo~Ql*(RrcPYD(MTSz;vOh95aepfX^Zp3k5Kxv zfXl8hU1S zmClw5nJYI-8u4z4q2UV`R&Y`@r7hY+eS()A;Fper!$@t)b9$)CX@Rhm7PbvK5_4 zUHi($vRfi$#ZaC>E=CF-kGz4G;NtlU|xWWq_m37)RIiTFAPl!A~Tr` zazk(2(q@j8H-_CQT2Bc5g8w~xC%Vf_WYj#X4Y@CI1YgQu*m05ZyR2U14GHYP1Nmie zmUKm@{=g1v?15ki@NmTYan|VjLVCrLP%Y|`;_mE!!{%;3ShBw&Up!^`hC?P-(IEN^vcwX;&wEZ zuSc>JO3s~zgA|x%?^}!=E+W6=cy{4}{G!gy(B81(+o#iThcbWbY1_hg z-~qeETFA`r-3yOEwi!L%^$8;9Jd{ z`@8DxcdnAP&LywMdt?+YO+hA1x6#fNg=U7kqMrymvBT@o##YAjPz2{;p3DaU8K}Ag zHb@#oHFzkk_!h1v4vk>iFO_wvY8Zwoe)^5->nP9XclY_D)A9 zbtuJlgI8n0pF#^+ZYA^=K129%FEOUTO{cRV;mUzF!$EuYz!Pq*NAdo3-3#$5?C#I; z-2FN+AE^inq@)EwDrh4!Nl#VFtE_od8Q%+0Br^S0;OCx&wRkI0P}Lzhh@11!GKX40 zgv=>5LDF->^=>KM*LU3qp@oKaZU%5#CQ^kLR4v7BFBhB_kjDP1=x+8i&fkanmT?n*!wkBPO2w! z^deXn`P_ZWLHW-TFwMMfW|h*cLxS4kI`$@lUBC8te|r*{A^u~`bi$AkiLX$Lzp4E| zq~w+~8~2vC9(?16K|0``=pfm$uufq7T=LyaefL9zXNqov5djJ<$~Aw$ZBpN`gI4kp zsnci-%n~Ea&CL#SfRtNwl2Xl;%reN1Q>Xhil^gTJnDQ%u%YJGS69)??YFa~bM1?$% zy;`Gq**iWimam*XpTrQ5b_jA5uS)RZs=I$Y{S#==LhgpqgoB?M&{F>7%+RN13EDk8 z2ux{Dervp>-g?W~2@ohN0H=OEG^`{QEnWm{)mx_56NNvHYoR`zEQNkTBWmSuU`a2T)R& zJZ#z|+2)nM1OqwH-M;;R!u3VPS-Xv{jf8rKkL?SWS#pGhpo$iSH-X&)#;I~ChpVRaSJTqU4ws=EQ05)ld(22zgI!krXY2sNnPT+eVcs6Mn1z4XafiMd3j zl`aj+p~OrK9EvCNIiC86;fP(KT1QBL8IxSf{Gn+in6H*HEhi15czLEL>+TyCVyuzD%x>4J66r4FphHgD%NpvVfEKRiEr zuPI2*bEJ876JAupfSU)^eobsdNmN<3She$lM!pgMdIr&`P4ZoQ)980OgfRYaAGH}< zfg;Cnr1*^n{{AjLgt{dzYDx4GNT71jDn14FQDl%bI|dgQJ#{l<@wJ&*oQ*!=sa^j2 z(qsfh+CH-Tfj3}B=69RPG#fkFjkJIa-3iY>|5OykwFUVzwuZK)UOo9EZs93z{iaTT z_LTj^l?5TH*vO%g#`z&oV&j%t3PlQ1gnv*wR^hj*a5z5x2UM-k>lTNsQW`JKjnpacQ&gGoiEd{hZv)B`M+s*;kh{{?bDjlbOwV~piZqvO`HP8#?s7ZDuWdg3(p1RhmO ze}DMzDYxjX>N$FASokJnR#h{FW>-_a=E`goph@N>^!GEg!HD_z!}xLGtKnn&dRbkm zBLH8?3QMz6FBA7khj8LiNZBQIuM|))D|{q3I@}~wHise~e9ooVvM_(NydBJ#GaW%4 zv(Uaeb#EQ{_QK$gjMQ3VzForQUtV@eTNcPz#)h+h`YMyNZzFqRUI}Q-Y_!)GYcNBz zZ`4(r%1Fb8Vrnmm8b$d4;+_5^{1~Annu?C;Z+)xmg^-TaN#fwPQtF(L15+xO8hXy1aD}EDd=5VUM+E?l!3ND#M=VQ zWd`GsKp;|>J%3QI*T2B7P9oUfD>i3<(|Kv%X7K#(ucD5V-n&m?loY)RRV5|_kS?CA2Cw~ceWu}0TqgyTGe_B&`L9zW38 z+@P;!PZ+BS?iq{}!ngixx4;))P!~3~gdo9}$O=o&I@(x@Aycgq;u?Y9Lf=-`Nfw7- z(l~787_$?{5tlYo;M3=nm0C`xZ>&7{O_A1sLJNk|OU726w#)8(v!pGmd89+E!|a~%GPDb&o}W`-fiGo$8Y zH@xL>iXn`G)^ zB;)Xut{~`mP-!Tqm)s*>QXD490C16bJeOUe)NxoeuanT{G9AI5p;TS^$zMaq_>S(WXRepZ$< zS_ejDCoKCdnc^ae-_<#GJLwM$1Jj4K$4v> zwu{*NNdt?U{Ap9isz!$|N6yV+JJPW__cLStOOW}`Cy*C{^e#p6n}KG&An($Ia9A|c6-!mUcAILpq6y{X&4+MOAyQm&L9w2H%j{hlG^gEk zf}O|>vG4kTms-};4FzGG-d_ePX|~DfxlFlb@$C!$+x$YSw;tbi{Vwm~lvCfq{@Ne* zuf*KHad_Bn?o-siK6-$KK{rY}<9LJ=?<-sDtin7v-U3|R*7{7wtS1m4r<|Gnx1V=CkdsRV=opA zzQxhSJ;7>tD5{9ig@S~7pS(fBWPb9iIgBTn>18q)Gh${U`Y32Fbt*QoTg2-H7N95L z$Q~7iDl6XS9Neiv4dm#A-Y2c0E-wsd5{WKd)dG>HK@i)7W^FAJ%G_13L+DjJOJToPEn`uWX#hA}OkG*K?DafbO;3^cIOEi|!7U7d zw$m-bFjbtk25pgfyTy;C7zE#$Zf9PzPKXUWkmMGqs>}6RUz)f=m@9)0{S$BU*!TI>hPSIBbh%=^vC|UAJNuFGGQJ z`8-(@K4+mpAMZ7PTxisoRbGbd(#oo5a7Fi}XbzJMA`T9Ris-X&G>T9aNG?$1sdSc1 z`&~@Qj>fDg+p}{NJh6ci4U_39-X*XqTx3!^b?yY!*)lH2kTj1z7WQCJ+iGbqnfQ9J zo6I0p9%tMgnmHa61C4u@HZYlRKaw`y?Cx)US5f=9i-ogvFvkYSYQd~j5Z4YSs3|&_ zT%G{_7~-Ad%y7EMPwtPHo3#9x>xH>`_YtMR@4&Euo}ox?_BcI$il zZ7kTvk~i!C9h;kt*5duNrN*`TJ2-6YG(aXg__=&;L%dV45FF8| z$`x>dF;I}i(d}$9jGXH-J?9ZfW9`m~7Pj3D4C<5Dz9DD# zw#LKxJ?5lFi!ad3?JH|wj;NSrZ0(ZHi)`kqS%I-_IAo6{LrM3)v%>()4T$YF>Jn7D z(K}{SI=Q1nk75x9WESh88;XWqy18FLW+EOfbmQl*Oq@$0@Nd!ZOcrMrMDpBCAzzd4 zS+hCO0%I>kx$BL$)i`v|b|nYphu5>LWAh{rn`>+xHMR>lvFoW0oN@m_tW4qlD^_yw z4InND*(b=oQ4HKmIP3t?-4{z9PE10cZTwqUNp6g@e<-$6UG2!VPtFh`;rk(ON8L%S zXj7VDq5_9=_$w1W97aX~CaRpw#ELR*-zL#XQOu^5ZQU+xW(n9Zo;Zw|;@Z?9@GAJN z_KCr$PlM+EUJIk8AXOEztqik2RbC)OB-;*%|kSV{}C*^kd7 zcOqO+znpa|N3TptEn9xqXdD1v{e6Q17#{+U?aHTf6yeU({qO9h30X&?^X_?cB&ffY z7Eq^x9x2I?SY^YuqOxFTC@7lMDCCyUsnTP)za_CGCYN(l^ z8Rf(^A4_CDl*A?EfH{PQ%$KuCKr&tln!=h-##hSBohqQ&v{)cXBEU%Smdf zZKOs~rk74?0T$6sUlo<2y1lYo>}g&Ci-^r6 z9}3AR8cQ9&+Q+Ax8DS735Si?@CA1XS#60$uUzkDlQ5)W{WVmJ^eQ`JM^QvO zLKV&Iq+%Q;JT;my-N5U71J3(buVDNVF)K3bV$!GJj|r#V_1+{n6xA}RbChI zKp1vsj0VGJR;<;r?9RHJP;xX~&~1w0FDMZ+P!aSnzyp*59pMSxC1+Z);U?x}Su-ac z;VA5=ta^{WPc#j+}i0 zW)8rcoPVMnKaLDAJ2OG67va@P zphIP)5uA}|p^iOZJ4HNOn&4F;j1J>60;dHOg7Q+IL}%f7EYv@X&wF&G{sxA5qvh-- znFy^#$kFky>jH%TlZv$F-+!pymp!vlyo@=#T@YKL+Jz89G4kSK?49I#)=P9k;`K%# z)=KV)ejKItxjP5!o$4cKya-GMhctV>Fe9X-UD>wPpw|arWtcZDZ!q#&!?T>J)Xj_U zQpT~%VHOe4IL6!)!^pOnaWKSzfz9r~tS^|kDMpWC6mihxD{jaNfkBKC%#{b2lV+|l zSdwiz*B_+6&6^^@HGpBe#`(+_B-7}aQ*Fi+p5p?2@c=|4G*$01COG(uRXj%gymgct z9SSU3tcc zT;c2zEnc7?O^sq3@B=T?s*XcDMIUqs488`Tyx(J*Fd4_uISC_%S1BiBO(5W`9ZgT3 zrqh#(kqYFfbLLm1rqZq+zRX1EIX8?hOa?yw;X3*WGl@;O{*|imBG!~Hqjy7FO2cBS zK^NhMrt1u&9ks4E1(u$(7xY*k>rmkW0sYz>hKbaFjdh+8t6xpcs8)w%; zXaFl`6-pMLaovqzr}|r6xIZ?#Y;puI9RY*~EE6Q|El#yL66I|02nm+m5!x~lqvL85 zE#oETP@IHSf~#~H3cJfIV`++HG$a`hq}h^2PQFAwS>!eEJ(Y#XZgy5pplArZ5pso< zryE~4o!|0Y)`OoxO=sS=nHk6fFUUKU?7;8p+$cOLtmV`v#tqI2S_<#b@A{P@``W74u&woKa1cj$ zB<>~2xO+B*bLQo!Gt@s8YwKAxcn_>V&_c`Fe%$K?FOfB%+R+g8q^f!q>-#?c6%Lmh z0*Tbm%oO?2p>Qry&*<`Ikg8tx8x|X<2dkeOrtR#r}{jG4FgB)$&{EJJkTln z=1?rg=!t~AL;99h`6dAaadNWg207VT2rrMoK*tg7R)F@<7HyVwwDXOV%(wUmvhEVY--p(RZ z6aZ2IN?<-^4Sf}-C$J}?=eHNdRFO3O;ehP3*$jM`n5VP^sAoq;iP}iUDH&J@f2d)d z1aR8GOUzZQBEkm@9;n`h!JVr#j0A}^=}j&W1uUc@Ze5&%Ft#J2oZNI0cBQGz01F?z zYqo;c{?5@4n}>}6z8@U!f8X40YzJ?Cf|re8YyaSGEz zf2u;WMCbPRT8;mG4CUZ`u)X0T?17u(1=X$=x`Ik(Ez_+Bc`JvfrRCw{4EiBXI&VSg1o{w?*v}iCk{%YZJm6kSW?gX3L z-#2maTo_velxQ^tec);9c`VtY{pNqc>eutnn|t2p2b)K*dbQm1*8bk+ z`$j+vuI;KrHh5@uDtYU)oC{;L1^YnB=+VSFKl)KfLc{5h&TUMq98Z)}4$G$O=o7mZ zopiCPhG@yILG7}h9i5yDcz*Fip!kyH=_H~^j1t8gL$IdkFpiwY#FEFIf$$V^mOpVC z=;l46(+*xR!c`g}_7o&_HoBnU0Z^)|6SRz?ot*~|9wiJTq)WBSbq+3wBh!bk=yMFzF3#f4nT%Ysb!tV- zzzB#q*69fE>(d1JloUgZrLi)pbVq~!v^gfy*u`M`f8Rcg6HAsf>gqrR0EiJ6= zd6aMQ{r+AP4jEaWSuI{f3i|k{wQsyObb2*_7)6zcF>_eHFH-@QU|~S1Esvhz$rzIW z;f@g;^up-i08Y&oIa1S-P*=psS=|9e@8}fMWLDixB2LE10lS?z7PKv4P{5U9APR9P zALF&=ZfW$ub+(XGe=wCuIQ_s@B; zY7a6c^7iyW{TU^OBD&5;9)B4O*4El_2{SC*!yDiqDzX#_b!W;i<|I5qLV2T&%$Xz5#};5Wsl@j=X1U*;+x zAyWyXz(2WiZd;gMsd3ulVKW3{$V8DC$r#20$&xbdFx;c0ZQM|S;8hw&rf>+>ICa`U z?0yS^fwaUK{DQ7xTd@s!R@lV${xQm2xk#|oz%IxBPkJg^wUf6Uo^41_YYx|Hw+qtF z{$bU5gyPEH+ng~?;1mM3{Y}+3!ZNZRT~pG#WUcMAVCjfxgx@p4W9l;$6&agHIv7a&jTI9GsUpmaDbO4my^;;2Ix_rk|=S^ zUPGDG<7#ezD8K)t8_D6{(vnoA230e`Ip8#2HWfAY$!cR`v8a|zR(33395QOk?j&IU zSR<}Yh+#*G%RB*t_HEWu$+qfsJE9Y*H#hFIi8yVW(PpeL7Dy0b-7Ia~GS9@umk1kO z(8&#Q3IwyenHK>!Tf!4q92PbLl-GxqaBxRRu$Ydz5mTz!XXv|`e@I@B4?b-?a1O&( z+TX}>47MXDFcr1B=e^C0)>b_QT*jrgKC=k&F12rF6;Z5G+26zO1woZG>$*+Rit_xQ z#VpGaN8Xwisp36l4JvBC|4WHbv8Yhyg!mLMbaj^o{huc2M`XDiDrh5lOs8j=DILVkEjS*1mjx()i+4FUA>4Y7)^XbNTJ!CgTpRVajSaMf}2bE#(N#@pU-M2@6!(8<1>_I0M5l(G^56)XbXC6?`cZV_;^ltiqbv znR5DI<)s4Jlwyie?EW!Yun+h?D9E_4KQ%IldAKrf{7y6^9{+TRYiir~FVXEMn?chG zkkwab^fVq>XrP@OEI!aRrZ0&9u~Aqk9=Y#S=n>n{9mcUSZ>$)N3q+7XNH@6XIqD47 z?$Qd}MYU z)2@&MNbo@~29?mtgbeq$5xAdxwM=+CXaV}VW;GlbRHX53_5uPl^ zCRuozSaV~Mp<4R}M+?1dTz8ZQht2Qt22)1LPL5r`WErogdn@=OA$L&x$yd&XkHoGD zTF6hsr*IM755@hrO&!Zi98kOLDNOs#D(Qs%HapTP!JCJI1k6xB`Rb7A>6}Vx>!W!6 zl#O)>JR-#!zjiv(VAbW?4--~|gR02XF?%wqBP^R+M@MRzL~#5f$4KU<94n*9$KGN> z(#VQ^m!9#}4u=krGxuds^EIs6$Tdy|uvhAyKsn7C-H3>r<)u8_EsY-^A^dNK1Z z7HP1ZFlytgK~3L$uvZ)QBKb!k5*dOdG;9k%N7?qtE`jacgsIA-qhopVO z)d-Z6fFO4ND%&fL5f&U+C^h(2Yu9F@J;Q>tz+%dV!u&w5o;XS~*)f;IY}`fA_Kh1w z6rEM;b^$$SwLCvq=B(OZCIIE4Blwa58fT5kAFOa*jhlrFoY!h`G`?9i-5_^H{LuMd zJRV4#GngU~h3BNd(wQE#@#Ps^F)YIp@wH8K86t^9LsFxJ5Chj#UGR#CV-W1A&cJOva&=`sbGj5ZdEo|Y>$ zH%8QkQu_LRz3j+vy?9J}CF*=kO|&;cmmvacI$#rK%br8W=~JvS8w15?ioO!;D+H*b z0+{T?WC!Fd#hhqf+HNeBNDVRD8HpR*3Qx!8LM+$4Du?qil7Jb!9=RR=0p|y-DjF~O z1KZ3A5VCQLP&gY_T(!kLAvxNhEBm0uy;@#0n4qo_^VTBU!q=Xw`T z^X_E6_!MsGG{13cFYBTf?#yXTwsM+IGXOqhCbnRc^W6*Z$V0k7Qb8(Y*W|}r74Hhm z!bBl98>4E1VeK+(iaksf-JtkeRsg9{G)8Et({hZapm>z&Q$p=q9u=oGRX6sYEx+gl zlVShLGfR#Eh7$<;7vYt%0ia2k%l}(?d~V#z{nA(aE|JM&jPyxg0LkA!+Jo)>bDNLf z|8vw?iHCRA$87&kcvq`u{Xd^QTV1{P|NH_UG>?rhU;T6I{pNRQd)hkM+}$PPjOLI3 zd_XBo`hB2QC*63QEw$Txv)w%W=L0kxZ%ade|Bb(VdLVWF{+sps)6>8I=C=9ifjEkA zSyfCPJ{WyGb>5pFpA@n_c(4az(hH?Cd9ZDgDZdEbgN!r?k=VwaU@PbVZ-+r{;*+ts z9?Hr1;QMHtQjLEe9Pa-Y8oPeqIBYfd_x|}{H$F*U1k1aB|4oJ*EcaDsTO=^S+a`?N z9_uH1#FpQ`k1r->$UoNcK<)GKVQ4w z|6kzq|NDRcznA~G9Q1>8-T<#kfCnWCHHnJVSEcEsxBN}%AFm(0JQycrdUcc}{q%cP zd>xBJ1H<%H>1;9?y?FXG1p#porYnOOFJqJ5WCb|Ur`_m0>f?R)(?NPNjr-lFtF_u% z>Ggvkc-fppgKfO)BO|T1<77H|O&Y0}TYX?_MsYuSEgGtqo7$qb$-8SCkQOz+Ahx~! zd-;9q4K|J%l^v+2US3r`tgPVwWlL^6EYE7F^784-et2+CGoyRC8xKEv2D;1i=6VAK zV&e=l--t6vPaiT!HK>&HmX9;P`@) zRf`^KmJs>DQsu#e=1x#XclD#q!?%qi#hbl&VKrEJ@c&5d^51`B#&YZU@UXFW1dL_t z2%GxkAfzxQjFFWGjlJ#WP8s?FZps-bu%(BamcnY~%2#{axdy_8|}j#II_m*)7)QcA_!c z1AL6G_z0yS4j6t`+xcTmf{!t*Z?~YYP3~@Ye{(xO5oZehB2~ zi%bA55wTfZ5^sb1EgEeLs9vMJf6&--T67vjhZylilp)iIQwmEttMT^cwi;+5LWW&h zObZmiotz*us2B_BS*#Sq0F&=kwd=Xrf3!_DlqEJMfS<^A9mKSQ1oa=ITl+ZfU6CO} zx)Qw6j^)H^nhC*q)7<%~eb8teYELuc)Ro`=#Ei8t@)2`hyyz)1Y-Aukih#&b-^w1} zG2|H%C3WDTw~^0J#u_4#k=kW0`bl;c9%p5%2rgK=xkFyBP?}cK=e}%Jk!`S#XQjuB z+3tV8b<_rSX&YFa?~EgDuy0(1ghYiyO{J8fn}?3u#`0qG5+dx{=(UBs?uMs;v7dIH z&#oprstr48Mk$iY#%=Qw@BJCk+K6Zy7RKg$s1A)u1>0n3|1HPXT8jXNymE0Ar{u`7 z$CTM2Z&mbr-_GHdDmlL{J8P=t*@dG}t0b3tij!B5MO{NyXUpUf8M{ZS#C0ME@S&1G zRsu98L_>0R^HAq{#vYC=M=)eql@1{K3A;)4EnJFX4y=(=gE}fs#`+P@d}+dpR&4?8jZl z`jvM?whwb6(OV851iLOpZGhV?^+&Ql#dX7jfrHm;cr2r`4mw2w$Xj>HU{}P>OclrK z+iXM~9SEch0y$OFm<`hGaanEAQOM(D7iRX*nF0!vQ4+fsAE9TFBnpynT*S;coJ_(h z5jA{k&u3w%Etqur0W^{13cQHoeJvsJ%B{U-B{c%r?M#jNlm3kjSgM%oC`h37m@rYR74<{!E_?%{zp z8PtZ}7R3ECpi2n!?#v2=vfe6!iCE-2eoVG&0jC&=F3Q3`FxDN}z|X3}F_E*pLTS>>@FPp?&(mJsxG`xS|dO zb4LHy1?f{He#7WA!6$sg)AEY?(-K@I63vX!<%i@b9)NJ9{ROQoNovWcS+L(*>;H@I zf9}o(aPIx@`tzLq&wBmd{^v`4{^RWb5O-Z0K-#YySCx1!W#l%?=I>CX24hw@!wQhP z#P=)0GeEgMJcEBbx4{qK6cesBN#B|hk) ze>tJhkD~7i2?_AHzevUuT6Kgj#(l|^$F*6uSnjE70KnB<(v&I|5q`6|i;o>uE)W{RqBbKBu_JgGJk(I3Ob0D2-tN4f_qe^r*pr48h?`XA1Hao7@ zhmoUFJKDDi>0`72E;WuR6r%VG*?lkQp!sjJbHu~$$MRs z2-3vYwDXfvlrPF-YA4?1s`;8K@93uo4^)UKH6~vW02N&dQ?x6$PWaw28AeqrL(08^ znK(P=Jeq1iwYYdSsM)#Qxo|eCF}^Aj4&uAU5BtZv+Z>e`fbmsPuGUKM`|p8O4ame{ zrILvPiS{9Iw1=n2TA-rP34h31Iu5^Ohk2Cz;8x>DNTG3m!-$Fplz zHal^()p28g2j3i}tu`!9qs4<6WyrQwV#Wu$HucqTZNxg0SckK(B8kK!<+?RQL`YLM9@+!di*ssKVSqlXh&NVKP*0tf;_wdnosk;)9s# z@@j>2!8`4@jiVjPiUi+V^t}^eKz$Qbn=ZS?)6s;(orDzdKJ4q*dxREDg@F-D;QU;c5`;Dso7@3jXiD&MLvxDEPcv?DtoJT}n5bnLL5B%60An1g$#& z!Qb?BrfmwKPr2h>nV>lTfP>+MO%dayV@(Q#*f)4?KEis}n^flHEpfxr;;pAew}f~} zNIS)ZcFb=`y%P(gmhnC$yvKw^)^UO2kv=aJc_i9H0NQxq-LdYu;yF}w_A>x1bfHfb z>|FtQ7B^}jOHzKdn5&t&HF$Pv-aHu<(Y)MlZyp@%Hrnk9a(3qF`^~L)@UUp6&4Yu( z{iA(K0TaMKbLRST9^-xcsp>2h)+FBUHs5T?)V?y0(MaJUGYi3i^8xnFHj9z$Vdq>~z{t2eGcD-S5?H&DbNXMy1)E`BKnHZ_F9PKn;1*9@+ zXGj!IO06Uu%9umZlFqQ=SJx3*c^*dmfd;NIjk%;1qe^y0d&zdx4|CxGZGw75GoHzS z=azDBf4i}}`4cQ9)YeORg)_LtDQk6Xb#lvdsR&TUbLjo%kL~v@%o$ZK@9yut#Rn+* zeEaRUmEb9_vxkpoV2WPveHwN0XBZvtOR%uh{-pB2w$v`r_wgy#@kQchRsp&RUXe7c zI8RC@MRBI32?|G!MpQaVoLX2_LWz?Zj$B?Zzn+}8r`XYJnmc?@Rbap6mj-fKR(-C_ zUusG!CN(5&U7A%)3G`snICrmZz$+2xuL81icKot|LF_H(Xa2-l{EY_=oTx=0SO}E? zg90-g5!SeHkiO+@fd<1j!-WJ%F)0jV(!hE`fNGS{S}Df5QPS@N#}ot?z3$KTUu1gt zqxQ=Ry$B3DWxb-z1V}6n_IG#pkB=lrNCb)T=yR z4;As?)hyPQ+FY&V^_)fYV-7+Y{UnV!L+J4%ATX-ljYY>kOX$AUpj~J)eqaO%2_?Oq z3?l^HBRqP{b;163h}lKcG_9}>Lw5L029?8d>-`PDuQLa#EHZ@@^6gRNy@&86ah^Bp0x`hfWUIrc-k~&w!d572Cxq<)pnq@p0h> zg_DFV7bwDrG(tXmg!C$Oc8|Q)R)XNItM71fXsD53Vc-ptcElcO>K)dB*|#a6xM(t& zfly^FWWq+E!09}tprp=yq?$n8Ahm9_MG69H&x6;*`AB7N0f7=@J=!x1P@?WvS=IQg z<(s*TW|L%kH8&Xy>y2!eiu&ZiEpG`a@^5Y3S$YvqI%k3_YWido-)Ocl!P1MHAtyJn zxqH~y-2Ul>^~hXR6trBM)3W$0zVMf_xWL?=w~5yb<`_W1Zpj4SHiyr5HkZL>c<5j= z{H|C>b&+G&Shy@K_txN@VS+a5MIV`Awo-Yl_ z>fR*OzT;uZ!Hy?Mdo)2fJer(4H#qPj{kOpY3e;~&h;wWM@F>O(t&@0hX>$KPa6S7| zG9nv9gJgWQ$XcthwCIKuJX*QG|xK{^{nO>LeB+<+}hjzoI_U4FFfQ$ z&{JUr(fS)&jOa9|K?{xd)O#*ol|U4oc-)!B6I(dU%bf@-)oL*rR=sWZ`w4j*YIiZ| z85+NcltRYx~g=SdR0P${`1UO%5ZHJJpks8jGR4@X? zc2;|*Mj0^b2_!VtQ0e;esa#dTOvY8S0e>)?Y!ZG%FV_@?r95s?a2tgwT1kz1R=&~zwd2j_AetE5Q!QX4v=6xy9|9QCGXrp5v!?D8&Ug{M65lrrxk zQVE8Gc?keQKY}~~UkQtndUCdSt+O>%$0h)HFqq1ZqQ^m3!d8y86 zf-z^3L5{V~&~^2AE(_7o6e(k3F{|6GF_#5^?6J0CgCT!f8|41v=Z0-1Ix)f4hACu+ zX}Fd&Yqf2WAKx*OSxPr|TxLO?rtFy*g`lV8SFZvO3A|n32B-3sWbWD^tj$>mpMyy7 zuv@CCY=Kw?@EE;KUP8*YB-2tPirHd%xmO_g+f_2NQMk;@3nve zCLgDxmEed&PIRZlfP|foRSF12u_c(UQliWV6K&DM=E3?*?VrRROw9uoE@H*jdgZ54&ONfM&;Fr761;HH z`POs(l%4r??|@H3+S_sue7?vvF#e19UmlSyF^^{>7^%xW{0dyb@@r^%2qYGVgp-5z z##csPmNh8k3Jks~|4^G;evTyhaDkbjbD|*o9}lOIdrsPhf*NpHySz@d=2HIrQI*kT z`blw4&5JxBg;#X6i-Lp46a~V<14@{QAm12U9Y(o8CeV+K+CwfhP3!h|wZJTqn-C)z8c=9-($CQ}n>5w>bovt?D9@3)!qn4=he>_E=&W9ILu^7XqNNKC>~pjX&)vvp(1R~@ks_lk(#b>bate~Ts4oHJ7Q1*N zZ-5uMYo-3}6#8?O`8NWDtGFtL$MG<}oDB?$r#XMo3Y@nVz68kFakZzwhNa;Gs(nMV zZFj#;m6RXNA6cxJBjjIGF3vYXujbt+;BZWTPOM(3)qAF2&eu2H3!!tlSUW$P6> z2-K~?X6-;Tqgbk>p}7rCL!X6nkyJq*QAHwzD+Ad4x0m0%BVKa8FRsG4!w6*xF%yNg z!*J4y`i4CWr=cLXT5%NZH{=nEso-o5F?S7b9EA8jwKD}ae~6)Hy9v2hq!Q2(4=b5q zN&?ak9Hm3g2A61md%rxWya+HqZTjLVrlN%jo@O)Dij6=&9!5{YUK{Bl#to0s$E z(LbK5cD2DdIp^%{w~h}E_79Id^C39+95ipmDEhnkblp&s`S_1_m-kP7=EQ$}wpL%w z$A7FnyO00)1wQxjAMfKo-p7BukN} z5Pfjtwh;mI4ftcLZCuBRXX8$sKk}{IQ1>`D&f{NRHCQ->yV2%tTt8~e#x49jHx>@1 zu%R+!{E1`i|Cv9o{D*g7cb@=Yw*2?(*|YV${I|OH^}YP}1wQ`t?{_b^)o&uV-HS~3 zBGbLdbT2a9i%kC`MW)#y^F4v^P3U?pZ@q7AVOA%r&~ykyr0a{S&w=nILYaPtu3F>C z+=%-fyxg51T_331SthnV-O9N^^NA0e9dF->t1nsNNwPya2PaK1j3`qoQ1<>6`+N|A zK1(iu^0@Ta821%K@UE6Vls{47WM0jaT+Liy7>c8tQf*!+_fmUC^WMd99IEE7=jFrN>Ls>Po(?f| zGZ00@tV;0;yVi33;=uMU(K`_5oHtiWeP$8B95T~`4w^H5^9vxpv1^pKyfrMF#ts=Z zTBFwUO|4QXmC~~+sX+x2nCTS6tA{B%{xaz!n>~Wr+ydyuCmS>@;;h8d$g=w)61^-W`}Hfg zIDSS<$YJJBxRi6KV67)oE9Xo$vTa@E86Oj$=q%qs*N9O^fRqm^wFIf^x}A@cfujsPyjE2d;khpz374@sBjpp^0;kl5I!wdb9Np)Y;!7w zV`y(WqRj75WjUzR0Pu2otQ$ngE}L4rN%D~lH&9Ufpu?D5oM8%ij8RCj8R5J+8>52} zQqL;Quu>FB6RP}N{{_u>kVtz>c6J=UQqTT3>mxO`#2+C@X^zNv+SQ@QCr?aOnmgvy znE(hZ1fbJDns_a8@iDJI^mPslC+FElAsR3tfnQ|ti2tkryMUEa$6%M@!EuF7GVOOM zfc+AN7)TlCv5hij;Re}$++zW~Vp_FKPFN7Xh|`D?(qpXQDK)0jcBgDj#E}fBeJa9y zWH>^TfidPlRsybZgdl-gCq3^*Xm67m?TSnQ|4^Q3R7(JBG@MYzcnbzE&|x?og&maZ zmFB?0wUelaN-K<;KxR&+acoF8i=B~zU^+N~QYgfSolb_)E6HYnW0xBeUw!p5Zlp7s23SlpjWa=4+!9x!5fI*0t zf~#aoIpI6=aE5bpCnuzEIzd^$g@|Y~G$cfo0ky86eyXEHNb`D@q`$GL6B1W0wA$~N z9bVsXcd$XKW^BO{VUHW?OK7t3Rh7CU&HT}Jhu((KDfE0EnWvi4n=a1nv@X=dD;mZk zW}7We0!94L3(KXQT-UqtDX&@0v7bw&k2(q}qn~R`j%tsyw2JqQCS(kW1w!vx)~dSo zWv6uuESBTo2H384z= z%imTF)p1Axm-!Wylu*!xGEE(h=g0NO8qP1_=eG_d(2W9LEk8yf#0v=;qw$uq77W|f zi|GUiU3jSqGFz8E{D=X;TnoI4UtzwUGVX!B=cf zdLAsntZNx3!E6`H3@`+5Xi!qcSB&zq72LD1!$!Pw`-!P-+k|@^9Q*m+~8S;8Q+bE zd0~jId)qla6{AI25vq(XsDRz3&4<`D%~Hz+jL z$9P0sd6p{jglJx@V!0mgZ%a=DVA76~J#;__(~ku*lYWY2f~C%Mj1jlSvPMEllm_W_ zCX|8MK;cTd60VF^n0*sI2U!ffK)DE_!Dw=oI{~y}$`_=X-yxTu$2gnA)?Z=hyuzpg z`&sH+E`B5Oh0DBOu3IHoOIwD5P)KrLDFx=i?oVyJzW6El{dZ?X+_JyI`^|&v6mK<- zu2cNrxb-gUfis192}r44)Q|9rEX{jcdjRb9H!zw-SX37Dh4my>4^1fINi-l9@&vC- zuO?@f`$-12dVnw8@n!sTshD^=E!REoRK242(1k5c(S&oT!~eR`0GmK$zxqJ#0C^xq ziqnK?ExcgsuBP=)^B+)f#neyQSFm+Z(h4bEKJ?fv67yN&GVwOjMr>hoWKf`j;(@#O9% zQw#&x^(=gu<)R9nIj>*Y=_5bef^YL^m?elYa##}d!}00u7hbLvP@6fjt`pP;VdPvW z3diKtE7!K!jytlbWT|&vCPW%<2t4Y!=OHd!|?Gd7_2LbR5 zYP1g4M6g85kTjshs0Iuh)=ltK8LQAx2f3F#4lb#lE2=40{hWuWaAHfFp#t2M2Z@)0 zXwq3hC1`Xx>c<@@6`Y~dS45YllZ%8KCY_m#34zMiEqmdfuSv3(K?iFyI+0sx(vo5( z9=emvK^#Bid1V35Am!)6MEJWHu)#x2u73b9^}Y@L-)rt>C!qFlQKYduJEDhi|Fa*@ zdvQPSt?F4GD2G7G3?z4}XKH&^17MlserE#z&HjEK`gj$@sUEX4ltro2PD9FiHZGSY zN4*^k=n32N^PR7_S|>?zSIc$6sRFf5YeE!7|2%rUvEb^yfiW$#yjYO--U-`*+r_W9 z$hcAbMcs# zf$4?0ML%MYkl5K`ycOE>*Hny$ZDvK zLPl$P-CF^?jnJ>?UUb^Qky^HE3+uIAwe}pX_6*|`8Iw=xE!u5mV5$_{&O$qCCe_w* zuv!fsso0Q(3a8r=3_d1cl@*zQ9#1D{`5coACS>IMpk-q4%H}rfb$~{$^c6@1sa2q? zzU5IbbL`*krrkg&DWI2`7cE7zK*H)baVuZc)7;C)Sc;IMxYUZDYn#Ldea%KQoW zfBcx;Whs@5j#SFqtJooP@gY{e_#%vlO!y*lc4Ev!ZF$~sNcMqqNtE429st6od!p*T zRCd8dM|3gA`+ei^jY8Fz!Ft7W4MCszEu=p``vvLA;}?%B`E~rvY{2)?_ylPBKy86_ zxy@x4qe&$8o;cRd?mCI1Bt-pP#_bY@Q;0vg+X~ z@Z;SfrolXSp_e@0mUp|jlXN+Q13YKZ84EWHc8Dn&d^Ca%3U1w}?U=#M;Adfu20zyl zgk8f-H~6v4@!H&vn$%((5PoKGLAe>(ky4$|Uh{Y&3xsdS$;Dzk(NZ|QG|b8ncn=LC z01dW7DpI~2CQlOq1Q#{@6u`L1d4$`CjyNJ6U=M|)lZqxrn>?t1q|L2cJP*~L6Pixy zLoYNbH3^*>4H^x2s4@oNf0UMc|SN3Qr}v5xfi7oJdu=TkQ`(NoD>_j(#wIN)Lq zNwSf|ig%9}bJXrm8QH_}IJ~L`gK$JjZ@NGR8j@!1{Dkw~JUt!bmBkmhbTvG;x)B=8& z5@syN^v(ZVzFU_xk8_L-b~Okut)jI|Rkwh})7&u>-)A9sH=MgMlqd?h|0iL?|2${> zjMKF+B4;6A{C@MGV$f;nRj}%rs(%SKk%TU^2vm~zz5;uiYpIhghVRd_C~&;I#~=$= zmWQL3xyH=W571nn{7-5cShC64PL@uDq zkMn6UY%4#TW>TX2JSKUP6mi>6uF*D#5l6?ZYc&4i8jX9+yJogty+-Rk+ku&lx3AH7 zP8Bw@<-cDG;)l(>x7TQV=^BlbN$xe;et3#valL4z9;*>-rj7>=X?IFi@8b%3P_Dw>|@Lm=IUN7P> zju9|9ID!gb$oI-_h85T>7Q%@CMm@96+fh0hCs!O*cNq574YlNw#E}!lBSk&@_}3Ke z%gGzVXQd;y05!o(*|RELH~=m@597XCB&|WE9~r69x1JVe`w4PpI6{~~eD!2Hm4-NI zR?UlXyvp1U;5`Pq;#NI2-|W+SVV?!QUiHuw23aq}aHgA@j=bsh?(AMBdzFW=y@5RB zHU(dqEHUkng(Bo3J|UPOI!x@CGB|w~xorYwaD2%PkM@V`rXMoK84BgAqCF(T$YFF2 zuh55)RYlpp6yg_jh-;<#Lv<4ec_0=Wprsh^ynn3g8=JXpa#i+m@>sk=rBc$!HWzV`p}{C8!AZ46$)V7O;8?r9KQ3KG z6eRUvv6Sf}v5U|;G^roN6Sd)NeLiMG8|JpVe_P~Pttk0<{7ohJaB9sj*vUw{5QAOC&5ejoq+3w-Y5zu(7yzmNZZ zAOHP6{`-CW_di$scPnSsyQa|;I?1!LFYHM0F$QQyHWd|Kq9vlZ@=kbj40pL)&ph1! zE~t@Jfba%(JB5HDPxWiq^=j&{1MU5@Tf6+=oLX zWUm9HCLvPPp!sxPDk6EkN~YM7L>Ewzd`c9LzQj=w&}kJ0(8DmYXohxy{lUO4#tA3t z40bfFRnZnyBS2kCP&4XxS*)g6gh2Ds&Nv?FSdTdn;@ImX63|FV<_o)lP`$`srvb~p z#;&O7H(l`Q%)j#v2qr;%?hYH?`1$94E_;N6)`=LVKEcJET zze)o(2rohC##3G?sVt4GH{XF=Tm)s5wwLP9D{gzn;8T<}0VR8w*_?a15tPgI-QAZj z*H$^1Ing^Ncb!pxn&N*CfTlzsPL{SxAhAkjiU)N63k4-tn`><(R~Or3 zE9?%faDt6XQ=^%Q7gVHUGSj6&_fq_uy3r+;7hO`BjK(X-Zk$(Z5H%59NNbuu? zckUZ;Qv_}#$(A9X6;np+(cLn9r9;Ku17%b;>pevy-Odg9ux@X47VoqIkWCr3lSJ?284VxrH zXuE)n2W$Zl=Qa92))%L_78bi{+A!RXd%JZYP%O8h zN_$kH`{qiyY&W-5fwz^4bJI|UvK3#OFdk9NLY<(XAs$pgb@u6)QCm{nc#q*v`t1FW z%Qfoif9cxc&ik0D|E<@*uGhZK>3{3>d;Ra1_~423tY*Y8VbM zO%ta@1K#!oiL0oqa?0Z8sNj8eH_BpYW`7J~@8;<^EGodtz1{Exj_KyGbFU@7*Am}r ziSM<<_gdorfm-6ZSwdM4T#zJmRx(jfLQh{RP*0N2e6miotW#M&jV~F-cH+QL-Sk~| z+yaK2b9vb|{9@aWj~>BE-9Gy10NtTm-`PeCWK^!-K6Bk5#3n)Ihhs??x;*QSpIP<| zZX4e$+{QPBZPcGH+(`X-VJmB^3%9biTG-0-^@Uq`zFzT|bktlwKw3LmEU0uC{R?CC zl~sNt!lpL;cNJvRA?Nw?S~ z*<9*biG;{1k+S5%%!FB>+*}l9NFTCJ1&M_=%a7B!C9ek)sCNmlx-@2GaR)TlQ1=&%$U=ucwl9D0xCE*xyrPvNMY6Vmk zPIVA>yQnVYkthulvOy>xbags0fW;Zf;T&58L8B+n8VlAq;L>T8g{*$X(Y>Z;jD^&O zV6MVtyaU^YFPs>#^IDq6;#+u*SFK&P~co;o|MD3VU5(_pDLA~hMQ zHq~ZY(b_Q_4!8G2`hQZ#h-4bhwi5*zxKrXtSq+s=Q0o}aa8&I9JECOUwPP2a8B1)( zO+yv<3TqjNhS_kc{qcv-u zNX^nh{T?XP3SPc6Ro(#amQ7?ZE=V<*J%cly;hT5chwbk-56j$_G{szM}$)ecB$UkMWsyU!kL4xgt3O;NZBK^F=U?ozB!Lv22~7>Cemc zU(EPgk~+&;LE&U1`>p%}6ynsXim?=kSE|AZb|kbrwh*S8-KFdxoG+O)V&rwAal)`w zhC8)$!7{(woAxco6f@8mQM>aIMl=YA%*~L>bHJH=F~D0t{un7v%xL4H%MgQcE50<< z3~9O4OOlgtO!2JX#m}qOR$anE(@{61$cp8Z%LqqWdf5FiEV&q9`0Zpm8KIq$1UN6P zLdlcqM1r7nk^$LQph*si3dDRTZg-@4n0VTHUQ#TmVwB-0>vHLyliTbkr-0^tSx(Ln zJfHxu&qX}2kfs-lIIwq{gru1;k?3x{!in~>U z6aCjT=8OtSM_y`N#)gc22}g1rjid88nWmTq2;B_v3fuu~ryqsm3{Y*L)$~zOGnmKl zYYfTfkx^MsN9Ap{MIDP6Cq`k9SjF-s$|%#Zq^suS(fE?nIosP0#USK;*}{!y9-`Dk zQV~ztB%jVnTh#!s7SU0LV_#8h2ozmm|MU4{CC`4reRnYzHHDNoLuVgqZHcj9$uq$) z6}rc2YY?er16Fv05&6*&6GUOa!Nxla&m)`CzXuFFOp?J)B5S1OF`ji+9IFOPKs2;i zCPi`KnWC_(soov>Ho7X2H*u5vOEV=}ma7IyzpI2gB-g>Cz|o6kWhB3c7-vjp5jh$r z)6+AVc|Yn+96^nT{Q8xireR6gJi_*r($hJ!2B%VTzZf)v`K9UL|`cX#)<40k3)ILaM9aN^F%+b6GbEeI6yVz!Rks=&XU3?EO} zW|c(b7leCgn?-sGqBOM45}RPtb&(ROm84Axf5p%hNOeSnhDjr>BIn|Aakj#DSSPaC zwM!msbEKSC)(AAYn8!4SYN@aSFf_tEA2p%$5p6Z+?biJ z^Ip<3MRUS$gWgo!k+gFmagdc_-3Y8J>w7qWFw?QLszaTj2c*6FKoBqByt3kME-4+9 z;F8SCc6K+Dc9jsfn6Jn#o+($65N4Yd7BfAoEC@0_){vTIB`)U#G*sb}bv(+#R^3p^ zS;^@zQsE(r4nklrP#PL0{v&h+xi#Tth1pG&y&H2&oQE3@uc*r>4Dk?99D5y#F`Q;4 zhz~_>LUR&oF~f;<@42bZP~5j{t3JY;m~MvMg>j>m8k@Dl8H9Ku>=3hbaJ)r;g{dY4 z>GTAcq{(z*-yWmLrtHVm@u}3&I0E}jJd6-wQH*IJuw357e-Pg0AfqMM2jz`UodRNQ z&}IS3bu()7Wk^lY-v7_u__KCB{U%S81S&Ay6<*P7HqAZK1&p(`$h?GaGT|4Bg&3ve z$kE=zX6kVO*GQ|EvEXXOx>2DgYy8CKI4vd}RwW!>QNeXC=+Vp2#jm<*>!uXbouzk= zIHojQ>IPQt6{DeWh5sj1>`C1NKt&Z4^g0uet8vfTfGiJ_@qol_d?A+#q;DEWh6ZVh zLK-1gG7z{_jBK=|rg<>Q$VV~@v4#%Hv$Pq_VQ0*~3X1+K^#wq_8_7#`Vbh!BPXX_> z5x}97j1}kh7%*l0cqQ2EBL^n>6TNnfN}9~(ub^fqI^0{BUVLefLCr*+71#)ImKbA4MKz^SP&UX)YLi^Gw8F@>Q0xXh(da^k;!1BEsYmA&gc``Ypn6cJ z`c-RVO_EM6G_6{Oz6TyK9jD=GlvUUqQ|=R(F|PM6N_!l|Kg$j0$23T$L%q&MA(VE` z;C!c87J%a-*>;kF)??zpK1Og(_7(3g3mXnaF=Ss;q9O;4##qCFLi3Qr8x5jvjNA#M zznTslHuKhY)Gc#91FMH1Fj@}!VP-Lq6Zg;-z71eYQDtK zNC-a35ez_sVlpcQVC1~8G&N(?8TvH~%j7)+kJQ$6uju${PB?+=N$*@xQOU(kK{9s+ zxr@Qsmty{EdQD6}?cK&64=r;GA+ZU|ICT}GpX)!NM`Q^>xd7y+RRX1MGd8jzUnUS% zGM(@|tgBRLu59&EnRg?{)3IuNi3>XyG%5&}x|Ug1_cyO!3EpIZWEEHlN}^#<)t*SZ zU*@*&uRPgnUllGsS$8gfzNugP*6})1hdDf7Z!a`$RrIq=Sp`&;W@@B#`7aa_Rs1GA z??!Q-Sv-lJnp)vg@!#}k=k}7BE=rrE_$~Ac50!#Ln>q=@5Kydkmc%bmGl|l)#GKOC z8^8`A?_s%YskYUXhxni41%YA{;-0rhQgshV@f}d+1)JUBB2^TswZty!aeX6aOTwox zPwX?J8n*Pp@&r1_o#$Gz6eTB@u{EvKfbYnMa?4WO2&lP>zfAl(Y=X7?VYf>@!Kb*c zs9s04lIS;r({Ox(43@02vOjsk{Z(t#vgsY9zzU&Fs2SgJ7}K@Ehnf@kxj?SV3T~Tx z^WrWXn_?}KDVDHQER>KufvLR#BOXJekA+P$w=p)at=qF$v&ZS zn;=~gPAz~;f)ZrXpim;1*h}iHw4jOg_Gj~Gw#|bV6^!XXIJ8X%wEFz7xU}xNGK)IN z+Y~4yAb86)>%<7UUU*^4xIvVJqV7lO`D_5``{F+Xh~QrrN_4QygAt>>1tSV9JMEzG z34-iE((~l-#tCJTr9&hJ%_K!~&{!17na$`bx0hwjeS2Br94a*q=QX!QpTYyeu{)5@ zysiDo+2Glo{c~>9E=bmQ(dNR;y+UeogqFgQFlTP;xvuVOLj_kA6BikxWWc`YK9dD1 z*k|veKJWq@UxyJ6wgdhL)8asvmYPhg0hV7|CRot@c{C;`Cqg!Y0bvm_UXF3|7*>?S zL`Zy1a;E`aba%IP#A1Bjlv}HicutNnvOvxcZQj6+7r$68L-$B7>_{J(QLNSIEm?!l~`;E36M%vsXNq*O3Pkct zm{fIppX$0T-XPmRc@9q|&TfNeecOBxx~nX$TH(+E3&&S< z=|ERUQ&RVb(xSRkqceFFkrzB~y=IRU3QFI95%E^!Xmt_h`ZZVYnM4Jog)zQzGW3d# z<}xjH>&lff?3NRIKhJPwuW}6}vK_1KpkQ(Y03jxUXAj5x1u#o7x^thzr^unRg%3W$ ziTBW=3-4DLG`-mIrHWlmh_Z`=LAWu@%k>NZe&!J`uezL}U%qWLU03HbI0)>GHQ1u-yx3Z+4{BQC z;nceIHcB~5TW3~!M{eHOri683C$2Gy2zci9mFT_~jtR5_HTVDF*kSwuLsTLAjs|2m_pI9oDku{2~#aq(>4CqCm?8P;_6>WucrTgR| zUe7)3ma1X(q#8assi>Csdl&}+eJ7Q1nDU`?4!N{5ChIsoavP?6k%?ekQF$ax`cQ1P z8vKe-z}r}J-YJuD$(zBwWoGC-Cxa5+A;OTy(kosTmM~lhUyC_M%$ySjYG#j?aAch< z>ezsnbP$xo5s?i+PlMh1D63+-n)nDg(Z93!oV$Wg<3ZdH$6|Z0C%_D;%~6Dy`hk?G z55!b+j5-Dsq4dGr3!$+q-F`}T9&`b2(5xSybV%e?))NU~1X1#|pIod2??EC%bI70@ zkE6~68l-DlC3!lg&@>i6Y6>+Krg5>AdDwBUguY4j+DN9Hn!9yQ_7^L04% zCe|Aa2^bj8jBcXoE`_R0p^AN}L=TTmJN7$nDKA8TVBxg+5UPJ4jZXjogP`=VcXIDU8!h`r>UQTZiTZrXJ<(ShaTP}H?FR;f_JE`r^u3Sq{%OW{bPoPd_L4!U$$9 z?_y|!pKGCxZhvv~!A~BA@MED73F-xX&O;>{Qvo{VJ1#`9sTaYH=ONg%0KukN6mhIF zu7PUB4SgY`n_5^my+=D|Ot(QhZBc^PBy2heToKA*9Qhh$*oQBtLrmg1eC?dgdW=8E zPoDfjzG0V>*+e*dJ{Anh9eJjJH-okw!&1*}YF6)sW;XiR{*;qIlzhJ_>RGyx(H0Y! z-qe3V{y<~Ni&;{QPX9`}TlB@O{^tn;>7J9s=s3Smg^>|O3)7lDQKt4&gZ7sxPN(-=|VKbsOA`B)D&ma|1?42 z%Gmdlj|y%d*oGWqtka*SG=Nn3u{y&vbV8vuQX%Uqe;lRQIYkR`eIo2@eoYoskN^|z zmR5@HPRLmYAW$X>SwW=G*ouvi&%7A_4Z_QK0Nlukgkd0%scZ$lIl`6z(@=>s?O^W8 z1t^2PQ-uhqG?j2_U~Vh< z&_dn_9htRuT4xtmg*XP63IC_J6)h;;O&bAB|jPV}Oj0pbLnTW442ZYkXU{tOY(M}?I@X<68 z31}V9>D3kW&bV;VyO;L6nmm*~aYWH&NQ!EL6^5Z!(G?w9-|X?w)v(XJRY$<(#g{6U zhxY2knrE!=4!az68_p(k@GxH~+@m>BRE#3NYk_m=l&)L&cu)rD>VQ}IeT7sb6s;&w zb=x*EUCpjFyzZ}717L#t%C`yIfksqFjk<*lilgbTS`SsAS8jviW+3eQt4J2$>kIfQ zmn8#a)?}q&Wa(;^D_KVZm9eRPzrNgeqngs-Br4RXACnQ7nAk;tY8dvOSbq40-jBn8 z@*&BwZlhpqg0cVvJLGa_+3r8!KrNboR~L#Vn9d~m7!9pP6t6s_5J@`MSv9a?1Zf7x zdPe858hcfTP|dcB4vZ|p(A-Fl5r4sy0HLqr=t`5YPLjeH;-LFkq2zPou_zn?HABIr z+RJT>FI6qU7vLCFbO64Xy5N|qD#r|#FVW5_VI9YFRE!GMhOlq3Yzz7mj>0bC2aG%H z_>`i~rz>W3#4eHip*Tj|fVyF2lZD{T8xy8ROj%Mb_H}?$ZJZ@$Fs|?dkmEP-gfI+Bc4-uXh{`8|CVv@-D3pSJ>&RFwmD4A`R`4gh___8AJA5g^ z{N&dvqB_^9Fg9c%$V2*Bf%q3KYS57&Oxmx)4CXFG?Yi*F!b$I5 zoiEHjO!XF!HduFO2_5JvZ9q!9JLcUTTYC(C5hEz^A7#ApFZp;J{K|ShgSyx*09Kev zBbqykGMkp~#IG}4QJX}EJ$0#ric=z*(ud)P@rQ5)Eo%_mr4N@c&!Wp^W{_T&{=~Xe*Yp7a z=u=mYynbxjoQI{$(uYz>E8ZCea0@RxDhau3ALZ}bKYXyRzW7loa~b9{*ZT0`VW(7G z8qHStOBch(FFeCW4KFb71$D;$I^$V&CjL5;S#?5xop4s26Mvl(ZyoP#3Pnd2gkQo; zcR}XkHIef{+&;bHXa8vjf<{Vkm8NSJ&5QW8p5Ia z0L2M*{+Zyl2<+dW8=qoC5)KGLp2lKtM0YSXq)YW@O5}tJZK}|oBy9vI8xLi2rs&_i zf;Ct`SDVMr>*jgge!gm+uiDSo%=0z-`MP<&Za;r!o3H8HeulM1yR-@cy>15jLl`O5kk?r-U zom3exoh5kF;TwoBuUqhl?MG)SsI5vC-*dSbvWmoZvI+`3&qOkJ_>)RrBVZk56QoPde@zq1u~Fp$A&FWnB3*i6HBEU|=eqc*NQYg;5}SLADyL;pA+wp% z;4P!!2gsb4csW*zuoi(!$vsvYOou07AFV_wwl2*m#VVaX-4e?K#wz9mhGYSY=6Qx2 zB6uK!^MVx`h)c-S!1u>_r2&D%<`od-L?wU-;xm^-?H3l4t;RQ^;@a@aVs`n>(s|TD z+2oZiqEp+3SN1qRC;{gLAo4M_B^xo4qrnDnPN{Zb<;SSd0<_~c9{bwpCc_BYaD{nNDJl>v znaRN17>v6|FoWtVY|>|3!?_jyRb+R=ASAOT;Ok9S4LOD8xEV0pAF|ec6_j&~fsB5s{=D*J?)bg6*Rru@?2ogm z!V$@{s>FPJgk$rX=%~*KEsfUv@2x8|-|Eh?!eX9i2GB-Xt%m*aN)Nd%=)6Wwze7d@ z!^b&J*k5I=tBe<|GSO8gi&i<&RZbSI66z}9{3@C&>99xVyz+jVHR^l@9zE(5Vox4B zN;%Ij`vas7&nG?Ap;KSpY zm@$(U%$i5(ap@Z9IcF62`1x1HPk%>q4-zK4fyWrzyhUx&tk>h&uP3u#*9uYh`>CtaFx>2E%ofth?zI!{!3H%ByAxK*{mePKz%0uJ8|i~~~l2F6)mOKLQCTI#`u zPZ0MQx4*Xnd5K-Ex~ibVP%dYZ4maH4g!gAg*dZGCRIu&U`Ls36Z^A zCocMyR#|c|g-pCZ zea~K|kes0|&k55Gt!0;cUCsn)aKK)$U!ix2qlCNgoV9MOmDQFyR@SlrD|nPWSgXjk z5nIcxk7)-ZRp2j*CveW@j`q_0ipIOx#xgLno@zKi&s7e-%f`^*ZqTX;zoib?t+WPN z0HqR}Fb|=uMU%$nk&Vrt3v4*9n?VjYlLDSHPMHJ4AI6wSYOG?fDqVCkmMjJ*lo?{h zQlle8iL!C1ygsS+bzBO%xDGqxBn>#;oLmW{5(;tU%uP>C#s#D)zfxA47$f0K!X8=q z>-v3I!FPB$auG?}SRDGiP;($(jb1WVEy>+TN@GS3f_V6mwCq(;lV1fFA&2jqBvUaF zqOwREhp-drUSATUQ+dvW0LK>HvQG@W-24~zp^caWe_*{em9Sq|Hehv3Ma{L*lhI^s z&@bDFDvE{Nmari{vsEitQJEcxJXFezE2GKJ7?w2s>EH=BDEctt5xqTppZ>Bh;* z*=zHtgeDPiUX51HO4ieUJcuVLKGUHFkN1vSjqUPfg;Rc1;G43+J%Toz6t%zEtVnQa zIcDHZL!}yAS-L?SK^wMdw_MuFgjvB$CyrZVg--FJ_V%Gx zs;-Mw*8_W41injaJDOo)doTi7<71(4OT8KFPFR%!>BBmRpn~=FlFbg;9;}+ zunBeWP5c%7+1=gWd)t2B+^fpB%^zpY9Gkt(w4c|CS9p1u2MYs}(Jp_xy1KUZ^=fVH z`8UtjzyA8!H??o7CBI&xlrQ@=PV{+A!Bmx0zN$YzOn*UJrb~O5-WJ{ur`3nY1mNT5 z9v&P&!nYfPmgu3rMPnm_bS7Fon+4zX)o<6oeg5_8x6i8eZ`RkJf4#n5`+Du`+PBZ1 z)t}d&;jq`9T^j}AhYgcd+<2kK;If!vP?-wNH07&&lx#_&0$yp-nMwBVXn5BeT_9J< z!~#pY#JLpslJXqW_3br=e0kZ4!vv^X!N4!C=8l{MdiFJf2exzjtTC^v+JwG{i0F$GUMtl}agTwZL5%d^M0 zmKkF>hgZ7ZD3@0k8zz^VHO^IcoM8d4bhBZuF0|aMbC$d24iN`fzq8e@*J_JlB$WH( z)*E_Xbfi-9vjB7+i6Z$UH!pcLi@Ak?K;0@7s+6kA@$j+=!+m}0L1h^GtKwJrK|z5k z{Ffcn9e^sl`%%5T<)BC~`w)H@eyE{nH!fAb&X3SCa97OnWmO*+vsf9b1}KVw8ZiW5 z216};M2qbYFbuS;#F2a$lO=Ngz;ShycCaQ)`G!+<6_R2(=RPDcdqSZ<>gY{ZMHiLq z=E;Gr2*lZSu9Y$`UASPG3Yl;|MV~5(b6nDyJuI#vRdJdw^I{}^Fa*w2{Dm;2VHotH z3yGF&E0WG6DsnF#qub*};-VD<^3^`+#-%$LuqTyX?Bm8;Vn-vK<{eEj6sMY>N-x=inSewwj2 zT#%uGZZw=^s0Mz7a+GSDZ??7@J8$1L|LgO81IZB1*Sg+sD@mt_LW-kMG`=6CYphtU{IC1ZU-NSo zkE3p!F4tG;E48Pvv@2(K-bW2S&!4Z;U-)GHuCCTrpZ~A=`r6w1^ZMEve6Q8&>-Bp5 zf7R}MB%aR{-Cm)~agt1Cm7DiIJGsBe=P8}zyYWe|x>5_4mxIkQzP7NLeUMUu&C_vMRNxRXHrNeM(kdo3CvQ&-6oJLi6DEzu=f)F;O2b4> zrgZIvw;6cl6r&-@bd0&phbo~Nxu2lN5Ngig$dh!{*b2Ljb|l9QItYx=P;X*I5AT}% zT*jFQ$$ZQq&au)UnwY>c*|Cu=h778cbjA7+sIIzt2~RL`VMl{xm`vhMR8`?-C?+|D zz=n40j#cmiJu2w6(+`2=8?O`%1-i6=qJ|;~eSD+Q0daUxHyVxQkAl3j>CRxpsyWq?TP65?L{$FBu`y*HG)wwA`KJsl$Ic#&?OGHk&sI93XM?l z`c}g_!5~x#CIy1(FopqM=Q4OEcvCx$0S8rnboAy*dXo!80Sl?<5a$c44r-PW<;I9p zL&mN&l^KxH%DZMOXzlMD{jho12;lp{;r{o{?Z$TS<|laB2)6bQemZQveRmYR+uz-8 z9JYeZy={29cXZf%b9}T94@#RYs9B;N1MfEXehL~t9vn7WtziE!Xudz#Z9;p{!QtlK zQM1v4{k6BXd%WG;dy5IvkAl7ZqhPoBzIg=2j`pkAFU2d$)eQD`g7=NXt#|Nq^G$QN zdGr%?xzjw_!|ryVzs=xa^YEy-b-cTI7#tiQ9_+WMHJsmev$eIm*?iyFUV$+{zd_^s z#@p zy`#qA;qk#ybAPV_YyJa32w<>@H^c<%{XLp90Bis7Cu|piK}%5$et6e_=dk8`GQFe2 zO$0{^0JwEzmxNvco=4VXgT2Pv-R9fI-c|$O?qfqgG+T`dFJ7~SWt!ai51Y{QF;1Tr z4aUY_EnKeB(gw|)U~~KXCJvqpV{3pCt)`$4JZ&9sy%XqjOdaH2hseIR+mx4DUzDrg zPLjU&n?JSz+W~|2w}LWeWLm<1DvAegzi(}|0f-M!dcy}M2EW9u+c52^0D?hT-_QiF zUkCMyHKq>wkG9*^sKN=8m8jjuyL(}u4KJ>Z1g_GjELGcv0%$uBj!$#XfL+IDdfZ}Z z)5RDYPuXLWjO@<3<7aG;hB+*do>kHIgKSMga*t%v+z;W@C!Z>nS%c`PN`3(?kWVdU zMGz7#@ing`1{I0F&+@iHlylStLr^Za+oQ1aG3vJ4m5LrF%N(v37!8a(_SS-adZKz_ z$Jl4VfFFrHfP`}rrGyTT{R@aK8`l`gH)s?Ri_Px& zx-~mYy;3+kdikfAokNUSD;5N`{L^oOpq_sU*t(j33ah-9e+tl9&p$=IU+{JQ`5QQ1 zGp)SaK5T!#d00NGrWC^%SA_xpx4#_$X+7g`q}k_{G6sUyA<;7LE2#L+^uj%qZ_-x9 z1JCwuW3Sx9278s)ubmlI9z2jFf$@-H6(}n>Rx();8!N@c7d4BhOcgtJ_U)lJu6i{W zbH)tO^(qmiQg#N|qNUbyoA{=%2?Xdj0MO;vjvmvkUVlEbe*O8p2G&+*Hn6rjuYu?5 zGaGom?l$1qnsN}?qLpNw@0VN4jQ-aR&tb;yTfM zUBPFkLZh2@HW%lVS*(h9EOEN+DBNF?j^ytl++LS@ zc*Rk6;~*_nOxS+=BZb|VNoDKQ`dtUie)-ZpN3xDYGm~!2Bx^Z(Mnhswv%$jh!iFsnE+DC5d`t4^)W}a56_9+kDoc2EzkDjyC-y#=)w+tF zm5L`xxnkcNSAr*sHLS4gUchls)&DT=k;SE3@sBf`Sd4?5*~DTzq}9ZAPIPgm`Ge1N zR$ZNmt7$ipO9fIcorM&*bCSSVt<>c(mtK;yM7cSoVJ_)29=uKmpXKOvdiX4duhYZg z$1gh$ibWX{5caN$hw;QZdMUatY-dx!34(?N%v-iLBeMnl3@s*Y0=3~p;4Q0%Qu=h9 zTwoyg4Ej4`P{A*4?_D1Iq_Bd|*LUKSNV^$_VEkbh8C~Uim39hx#awv)UafA==cJ-G ziFHwZA*%BB+r8thcKdn>t!eRHC3u-b{*_fW>D6km#(EM8a(b=iH?$(%RGXJ!cw?dT zVH?*?N@P%MI_id#NUxjq2BTk(_0z>tX>8nm8i9>##iQOR$Un1Dkmp6i7q3F`Dh2;D z*{C_$in9wA3yi#R&CrM&cjbD&aXpvtjk|D==4+hYCq);B@}1lX&Q}# z&4Z@pnC)cYbSPTb6$3`Ka>@ixigA#{&e+U>b0=av^cICk(o3wl!A(30TJY0FizW5y zU}6vh=hV1F^EB5{+V+!H6BEZ{arH+BYPwFvP#|QvYM06TCgTNu*3Z}kyf7ZxlFU9d zUd$?zDBki*1XVJLaU8cfN;ux+Vi}WQP<)?asvBH@N%YG@?jsBw5&AZomhFK(E6TnRY@> z`HuBa!XP^GUq}Q^pd_1w(L;*)bb*J!RWj8g1oXKA2NcF2>vmNjC1t$_%JlU1GEnL_-OiCwIG)!*XA^j9|TJ(V|i z4;!1?Kly9QOHVbx-d5pg<)x?EcB6H)xxIaO*!bW6s#XC{_4lo}=*#4+9FPwcVN1xmCbd z9oC_VQCtfutJeipVO*QvZ#H*}YpW7IxHpcrfm9a&onQK^9q$3oZoMn4DewK2VUsk9 zR$AVADgs5^+k68+Y;61MS_KNK&*)gc_M+#TQs&aD5h&3b|ny~RUPYKFmSGz9Ll6EvUh<9WoYnhWH_G0`UKYT!id zLcwv=kIq9rwIramZz#?Eyj3#Y>bBW<#GHPT#8Mr15xzjPJANGeBd7&0^5-T1<83tA z4burdE>|k9^&cFyXhiixJjA29tj|~TKBWS{?Z^KHo?2(1prl65J0}4(*N5JblL}x| z7}LwZTaI&R4vMtUhGse{zu){04(7w{@@jRpQVkwStxUvrto9W~*2$PkD}dxcfINcN z6~gPPlAUhdV?kLb7~&j2gj!o|pMsRw?T*p@&@n_Af*hGO@ED(yfF_nb?AfI%2TNx$ z<*m& z45fv2k-`SGK5v@s9evbD&Kbp(+3;W(=y}ToaXLB1t@a}Dee$cNw)xTFPwmaOo6Wr! zxzad3dgbNszTez^;p+oyz1iH}+1-5WlIw_Vy}AhoajWW3i2Q9dP9{kw>8m!S zb}HfJ{uDQ8ghP3l79#KJX!;70d$ut*aCEzh)?rZKa#`BvckRQi?@>(v zbN~JKAoH%Zx6SVyx=e{Tc?cAPmZDPV5Ey780prc(BX?g1OOtaLFwf1E#U@`T8 z(>4>K?4o@7+{avcZQ@~jqUA}*A4@@;(|I;U$WjAqH}b{`o;VvXyD@oM=^`kvkcGu@*nEFW?8J5<=`d|UgrhYCCPI<`jpvF<>Jd4@4<;PlxWaA+X)>-_y9@ zIdIDEi*P&&pW>@r`R zZlyljxVidf!<;m89TS9#E@rUiAt1$t71g%)yy?$p7!r7hgnFlI=8QmmM1%WR&bl)# zBwP|wP)N7MGy{R|S}`~zCI4F2pJ6y)Qxks)-OB8{`V1YJ)7AoaV;Qx5o~kTZ(X7Z# z`@DWd12du_sQ$NU3jEe39A!G~c1yu*Q<|bO*8HqBENE$F8Qs#TKw)f?70+u+n>dF8 z5L4t)J=zZ)F7`i2FPYK*rTx2f{Fcvb{oi`Mw!Z4g|G0Lq|N8A(~;2=XEF&pXKcI-rJf$`wycsV4n9N*evW>Mq-Ru`kPJjLD3+9Ev;~JC zIr$ZJ(vA%@KF4Y5De9UC1UxSyfD6jm@tC8J8OMz96j=v-Fp^0*2;y|8!Vy0SE{tjU zXdIo#$u#X>soGReSPzgchjfyML+TgN_Ng8~kj9fKozl+Gm6j+>C%c#lKusX{;VjaJ zG0v|aUcoOt2W&hjj}kclPWo5My(FZ=F{;?c;TT`-5K8BPqq>k@XBGnj-QVZ@_;h&&GniZbdbXU9Im4L&^m z0UHOb(-^_%x08%7@QR}w0V_0Sh6gI!yKiSY?4*JH1ZvYmYJ!*qg-oaYIO!^4kZ#bUS@}dbqDqDq?0__#m)E>ZFux!Bo_glQ< z%YCv|^0cd2SOMS3+P}wz}HQ@SWX*!E2F41|mZG}UqDTo3DpK;{DZ<;rgXyHsDOvac( zQqG5D2Jx(%1O-D@$6l7-Jd64uKx+n$P-dzl!aj9f%QKbVI-IY0P7m2jPtDYFH&626 zr$pn-oGSWB`@{W|_L6|wE^W-JzjfHyJZg}OaY5jg(oPunnMGwrI?H0u>7a3V)c&sV zQ~MAEU0f%BCtLj(Il&G&?g2mXF}h;q&`AQ^BCwvK(13wpB;Jql2o9k+J#TX7yZf8l z3r?_~0PD528EnBYST9F<7PlgsT}QTOH=9^BU5H+#}bH>YJ-!XI{kJ# zvrcY!yZqV`C~Yzzagp<7EZ}Bd1$=@y2e;{lgA2Ts2v?_}2s5iQynpW8N!P%lTYqmAEz^RBp>Y@Rl39#g{Ghz0y=cE(KemmH$6 zitLX>XD@$WS*x;=nG!lzWhQBt#Til&cd9G7HVRtsO%{sJhQW%9bv77G8F&=;uG#|* zhhc~S2zbq%pV|kF#vzyfly&#;Ecy@eJeek5S4%85#Nw;1l&&14s2NjE!dC1s^% z;2`@WVoS@fGp{y0b!7pqD)_`xvapZYJrpdkuI_uEZ^d13UD?rwWrS*M;+;NR@V328 z$Ad#g4z}npKqYeHN3un8D2>a*<%uox+#Tk*>)P=$Q?z1n+*B#cYM&-}Cxh#&+^$@* zrzj<+j)d>$t7~mO&D(=$a+Y+fraxlQkK(-4buxOhbCXtxfIvYLWA=i=!g7q4%#J36)g$`|r-7T=tf> zTFYz&Y37=s&a$j?!A;o1+x7R~vosky&!fMpR>MJp96`w$vnLj6-tkc>7yZEAGRUnG zPfn~(3!j$RW3F_VBv9FPKPN=g^|K8x2BIR~a3j}N7Gmn-=Ij3PS?$|4wYC(fX{o+Y z@F%#?iz04>fO3X8HOhukAY>XKms|Wi$d?Qu4S}j6Ejb{oku3$6@UKIh?2o+cuEuRF zEwC3}#Lc6=RCy$w0d+m$;UP=e)q?<{Z9lK%yTjGNE`6g_@5^=E6b7noV3Y}nWtlQH zklSANhnrA~K=qlfZEa>K0Jl}~w&Wk88s`^2w1a9vieOOZ#J~PQh-4@PKa?^Wv9;4_dNGY^eKnq z3y{OS9PQ4_2m4QcRiZNuJW@pTQnSG!x-+@SfLRjX=6EMO_-l^aBI{XF?w!gsjoOwF z+J?;ztBcjL%i=V5HPvjmV~RkJtwP+9At+hnu>e23FM-jOOre5h~gZMGz@&$d=r z-*GREVSsbkdYjwk;`#f5*)!OG598mT(yjC$8YJVZg+~ei>(OoMtKgCULY10;VbT6V z@mOEL1ZP5ya|D?F0k^B@l13lYjy#;?cP^Vnrn?p7*>U5?S-hh zVPR0I?vMxM!B|rppIe=l5nai=J9s3GNdUK36szgcBi*A65Zk4=9_=4Cfuv$kHss#D zFA3b0JD8E|D+v+j#E>Fs8eaVTtiJk-Y0W^?jH&fgYirY%Qt^`xC67I{QI&0h!Kaq% z)snSjL65YjFM@|@scJid43mn5=$=4guBR3fRK%1~6n$)?EryFNw;7d}SHr%5hNQ`m zJ8HAxR5`Lo{b{jH-rOezk$aJ;^p#_S&H3GKNbqKGF%B^seGrb5>9Culu@$8CgovVw%7H~zj^>#A26N`_BX!(wp(zuBLU0&*qR!(XI$3uj z`vje#Ldg!!fBm8PS3JrlK&CDy?Xv{v4;C!*$xz%-mQP)1e&)gn4GFB~p0@UyS^dgF zENj7TM3Gya8}>*GfR;OH6yvJ8!8Uax2s5H^a|oQpHx9z9lW6NK>U{hGHbmKF#j3&* zn8X3vOs>g()sn#?4a^CPuITziWy3ftP%ArVkxv6aTD3;D{o^U6S>+K&-IZV`q=2jD z5!>)z#A%esePmd56nkvnDdNB8i0(LfMwF|u8X~S={@~#23fs{bzIVcrIVdxNh9R|T zl7R2SF(ZdESG74(YFipL0N@(0U2ad7l-}i#O8Uq!h%-r5V}Az-Rf|XydtUKtLcRLV8Hv}M46EC1;Rz7EM(a(R*uCOljLZ#U^JT@ zN7=?M$n6$Qikbe1YYZOCClLj{hDJKjM4t-)FL>FBMw4tiVxvswGiTse}F5!;-cM ziWynZdGZ7K7HlpwKm)jY=skZI(x?*$9#2 zvMQM~4FuC@!zKoti6ka}&yx_{LMR`ba_?aO6x$xy$H>{?q0cF9FwEi2wAYKqytO(Q z3Ld)7APV!2dZ`LT6N2n^oGO)D*zqo94o8S1qh7P^Hg@Swjkr{|IY&;(&V?4clT-A0 z$3JiEIXqT_LZ;ju6E$=&PB0DKmhvjGTAGQV7HOM}p;f{SvzT{=tA1Qr2v2Sa<1B7i z;%5#o%=S@xE6=qBdl3u4$oVa^1^qJblWg|EIpU65@NM|dhlM^ON4bN5Q=5$0MJlE6 zTzGR%J9a2zNqZ6BII;e81tq1nrK7gwJ1N0VoFdQU=(pxWp>Du)*)gl->g9aPP)jzHulZgcYZT8|* zm8{s;%Y+;q%3QtjKgWXQHfH$<!`hm5iKe}VKv#k93M_odAdZY&h%=U9!#KL%+jQtiS=1vcahot6VXPT zMiZg{y>56_el$6+T3QlOf0WB*-BD$7zQT#3Doc`ht@4DQ;SlWMlVM!*eIw|}ZhIR| z*f{$LI^0Zm;(mnR%8z=J86EZ}E8UBC@#)!iIHB^v%a_60Y9;tRpjW%eg?dw=JXJQF z1n>Dog9W7>6`Vw{3xc!o9Aj$$rH(-M>`YRX;E{wVRRyn?kmgGUdc2dv^E#&R4oaua zh0~zCyuG*Gcys)=B7vZ3-|{0Bs+7xWjFWy~?7V#CsABKhckC555t$DbkmG8d18HHK zH;f?025&*Id6G~(XN*R1Md&@1#IUM6rXVVW#7?uZyDfHVZE5mFiQV~7%jdlCN2^&H zguf=^(PXTC#zXq)C{7QfQU3}}6yOi9G}GIN>Ma4}mm)(Nsl{qQEorkvY~YoW0|$c; zr~{0k)ZE+IFWHI$E-0=L=!U>%MrhK6ua;lasi-fQ4;u%&KeaJ1)85;RyGwYYj~CKs zQX<^R82(?DrcxZe2cnfqfd`E=$@?*QLhYd**w=<*h`}fs^x~)w@;!zx8)DQzx)Tmj z=^Vlt_V{w;YUSULgYvTqr%b~88alJAJK%C;rU7s-eHbJCs@NcdBFh1g?w7%{8RN%D z;)0SkGOE645xJ{L(#NhI@oLv=)^ZUB;A%Wxd0feXoiM<(fn}f6RURa2M~&nFPF(I@ z#W#RWgF2klp1VTTiosfFoxYkO83fk2jD;*@M3n+z^gfxeFg7?83<}aTUd%1ulLH2r zdo&FTN~F7f0o-0VM2nej#H$^-`@YIx0vhx|-)Kgc@uXaLZiL+(%R%|uh!H0k!eZ7{ zhr44Hm|N_V5398OCis@F)P-%R216jJDr7wR3gchARDzv8(YdJ$_Hn7z*llbb1>dT< z2vy|bD!9eQOlYbxzDJ%W1-eJ50OJIxMT5!cYTg*O-?WDE8GB7Qp5`uXD#@Lvy$&O4}urY6AZ!lQ!#>A zYpyUu^)v^2g;z;VPfz=iiiCjFm5c#=fD}I@RmNlJk%=HTXL}a{k)169jA8g41|LM* z)PO_MT>}p8al3hR+?o#&OYrC#&Au0RFr8l8!wh}6h#tegu=Ke1z66Py)%?!x zaqHdon>!$YsM=OWFKmmN(xpDyuo~!HRoF#? z-Tv8m2%?dDL=5YA|Qa^{X);k|O2f}_QZAC0!Ua9Gkl=w*Fwvp|R4@02OHJQO-+kuTc?#OIfk9b+<7 zTND_S#{l*|IeUu9y&`h6m=$aF3n@Xqx7!kgncFk7adI@V<7Kq647_Xpsi^BXDm*FY zl9@wYw^NxzVHTf4W{9L?W32_yKRN*2IC!F^9dr%DXUvgEu~^!n>VZ@@wvDKA9VLCz zhr9?BN{CGstr}HS=k6ou1W)!O5M`3!Bpj<6Mg<8V8&XsPEV;CX;_~0=U#Mk%Ky~v2 z+99dBN=Ze;+(`q92J_%w0ma!E#SaXv~g+{9>tP0K+M?~xRD8nuv(9a31W!vr0Yn6Ey6XOo}?&>q8f-s zp+l#$4otOk5?T9Iq*iseA&R&556$hPcO*2HdxHt*S*rkp68sii^Yek13fZ3Xusozm1?Ls7D&UCp8Jl`q1d6Y}R-)JnQ3gO+nw&$Sa$LQ+C!`e5c0Du&faCg5T z)egT&dl~O#UqRxU$w;7#H1fFNBW- zuUhSY5KtF8J{F?;{266*p?M{zB`ag@ zyeT+^=gq|_JaAADZmA0$oh()iTgp;C|-DG;wkLGhK(;=mz;G`U|ebtd$R)M6) z!Q;_my7oz@BUH&n-O9pDjqKPF@pvS{FI8ZFt!MeY9LpoSX}Las2JsMRKs;mol*tRg zxbUbSF6Vi$+<@%M6W~V9e_oSdB=n>93p)D&e^#;{*6AVgrM1h;OZ&|#y+MhZ9~xJ& z6xu~Q_F%0Vd|eIJFw`rSK%ECvMWx%?ED)Vs1*4GWPsN{NcUl_)?VnE07Le>Vp%w!N z8^M1ZGoZhY{9g!;+ra`C>iV$wTEPO*;WhyIdLe+Mz^lW5ZPJ^mC-}>8Z}ox=uq~nb zw?wB_H+7&W16UNRRPir~S>jW3)p zM(KNvTQBRLvB;RLKw*qD)VgVtUK}d2Nh4Heiwuv~Yk>nL$SROC%f#+!)Wwd1fpx!5 zO{i7SQBR~5eGLpvyp1L_b*vL-djljhI99q&5Ec)`B48AbVX4hrxhsWsJOI9z zELS$NBio50gb9AFmMO$;rSwm&RDEvlNZS`qI9 z!+4kmgK2*fj{uqCNrZWap{Pox6473G+*K+jvX|u{uAs+^|{;0Z&Q^4RT z#Y@ODB5K-Zf|vF^#Yqe>XyA~8bbO|zWvRAQU01gxKSvjL$HfzZ0lX!W#lDB5a*}7eGQjRGBn}f}q z8_1hB5*Roy-Y;9Y+tB=K5Djp@`P(4V_7Y-Y{2CU(D99{u%m+N-|)um-(Zl8o{w|6TR=8k~*Ub6|J z$O#gTiarj9r=J(H)aji8_U{Z>3$FD)6iIP%VuAD70lUS1G`;u~%@qdWWjvS;EDkVG zOyu<-%`<}L3=bOAYpd(?P#nrU6ZZb=KX46#FN)k&UmVLafq#1aEZ69yB#+;6K-EimBGI*)v6S zR)x&-3NGQpet(*wQ$|klT=KaiUt9+DBN<=#gILB{o5vCG8_7^*@C+ylhw*3eqhWrw z?2DS6?cF2jOq9&zoVYy(qOZMXZGp{I7&GrgGbQilTq_VwECgEp_A?WMKk+el$I$+J z0kkb(fAhxy`}3cDc3MbLuqWe>DPKsD)&f(sa9U5)R#BrjIsAG<=Zwm8UMXkgt2B8xJWadbSk^GY{raB}kYr%v(yLuB?mXV`b{`{+Uc~emNRt%$H z-u=lu4+Cxo;xgG8MW@82RPORZ#B=fOUV8i;cIGmyn!Ai#Ym@7@VE76$iBXfC-0^wnM*astZt$LBlK0ON2<>c zNsuEx;x2Kjmc3;AYpvPsxiws~M(bb=LnNF4%m>1=q(PaNN-f_vwbhEdYUbv}-@+ZP z)2liy)Z}vV!nydmQ_1?K>-K%)@QuS5&cAh8$8$eY5^tF!Ams zmW-ZmaeUm~KYp{@$hXi7PtpSH=wisf<-k8fjPzAok+{0`MMow!f~%{3mE)Y7W&cP% zm!5%Qld&DSjW>6SHXkt7vKMt0HlF!hmq9$+FLK>!gEwgI?KJnAM?c*Soa15dn#M?f z+*XWJ810O`SXs1F+PJU3(0c8KcXKmZ9frfd&{Dx@XD!udI=Y5&|4xA2W=MbL+yCo8 zdat=yAgo(a@H+z^H1rv8^(JH*M)Fx`bO!NGlB6G$F_Yw7s9=K9-zJ}3P`)k(zS-a3 zU9`Tish<_tJxtPhN6_y5MWFrFmjL$+0)ddfBZw!7c9;3wS9EiU1R>NHpb~>{bSKbz z$mTq~7^Xn7y$S|E+;NKWrn>k|QIWMjg}x0Ve_l)CU%v~9AMZ7;*NzBb&m29$Oz|tH zC!m!n{Rxx=fEbI{U0qmhGQS-G;`8eRaa944@%TDWTq~GBmYB|O~TyBS5!ZcPTRO-$D^#I#mhyzcs44PxCL1Z?8Z z#a73s!|C7-2ve!Nw*KwvH{aIQR=-)VeY?8$&9mCKYpd({XMIinU44dG)V>j$&GpZy z)VK!I-F&-u{C<92<;uOb_>Q7obPceSK9`C_wEL9qT+H(B#uks9>t^cv*ZveYPd9Oa zyZynF*Obk;S+Ed5nJjwud=`3y!I4K&AL?R23fwlrJLmHpxjf*jp8I+#pI{D^?gJ^J44GJ5l_ZzqA zO=WnygC4!f;+{Nlh9lNYH)+TFTf1BHu9Y0O5XyKxxkkU)@k8PVMa9MW< zP7V(R&3KYV{T|sHVk);FJjEaon8JRT440LwNzgfqIHL+9fot@l+LCt{V+`^z&bO&r zGc!C3{Wzxf=*GMy#@C!^N>?ry__l~^OEvQ>3p;Afc&K}+g*y5ajvn&%HEW0#&EiyI z&)^X4Jgqq}y;fl_Ro0F-o7=!jyx(l@1*N!4NEoM+uRNw3`%(laRRO43aLmj zn8=hgylFlxOEx@^gro_(-7)0?JB!&}=C_qFhMhc!>2TfL0l}tm^vPdm35rezqfC|! zjOl{8>QAGIDp}6Y6SxfAMX;G3QP=R7Ta(q1O!i-8(?C59Hla~;f5P}8!O}39ECmED zU4}7~7bvy8Cvf*-=o~byJ;7ioY0^KpZP2BM9aQrLX+((-B$mkl@HrTUg9v8bnM`q2 zmQ)M0_Ip3_#B@VB65<1bMM+$obTr+Pn9o**xA%|UdHjZj(bJh20L*(hXaGSPjnCsw zL;(yj>DW;Cx#0KTUA4NWjjd2@mT|fOFAw4_-WUytR&SW-Mz1lLVbYCgC5AF9`T?Zp z9L|hx73X@BIbyTC!ZxtDpX;}_m(V^zL0kyP3DbyZi0xjZwWDY-O2*+>C#nn- zHWK)zxb?5moEpKML$Gkp*PJgvFP(N6RLC{cNLB(Rfml1n@a#H?Mo~_;7UrtO15IsJ z+HNbsPBI;KgXkQ%_GatgD>pp~GANpiITY&CgKQ#kd|F6+Fb)g`4OQvnl2K$u(9aPI zW%X&Z9rZlV;UNsoeUId4fNQ638Am-}NxGg;KpLX+C#AFT1o#h#@|cuIM}G=CooF<% z@&cvBlcpR$YT{1;(ofA-s9b5~YY9H&n`UpF5v z+TN2rFGn>RV;wqw@nKsYDmg&1URRZXs?IH6rt75yByhcK#QD)6g* z5~^^OPH>&s3FO2gR~2zgZ`Jxr<0^`gttd99u@ssWtH<7Sx=CFxAE9Tlwk6}N-8ch} z)D=pMvr-q$Yz-}Ld`%7GzHVxE?o!5$ZOV70TmyBsD{|LJF;O%|wBW7mY z-&4dxXQzLKe3iTVtOxM=(MUyP)H{)ZWfDHVZ5#y@EB&^~2!+Jb>}ktt2Lz;FDl)aW zzMe<`Uy(O#@v&f>Ej3P5IhoOBl}dM*lhTGpc@g+kt^+Q{Hzgw?7H-K-!)9#+w`QtC zXQJyJ>`mCR$sn*`E>&(0PdfuO9P(#yV(56wQ744MR&s?PB1^Uql~fhh^$@$M+UY@{ zk+>I&7uO)9q#oS}LwJs{4{{vj{FT(pV+r%854XF`ycv`ZfD+#Y@?fD+2X#a$7Ttn7 z^3(X!Eg%{xB@enbcI8S6^^kIbfcp43( zF?pX34eT_3gc)l9M->{9{wUVGtA`wAToHdDraw_Ts>=Hd#}`K!LvVl+%<_qj9jwm` z&u+HX;B(NEh1xayU0{`OA9(_MZ_{QrNyslE+P~wd6T$8cqL`=yt&6kCld%RmoZeHt zq2B;G3DDY&PXro-lZbXc!M50*S-@Brp|c?t=WwCd*yMseHc`f`=^B9$0(}sQ4D!s5 z{#+FP`AeLE`Fp+Qhk(6|@^v+7#EB7!`Z7$oq@W`|5yZ4r1qj09#LyuV&6}5zUfnz2$U6 zJzv%64JIAJtUD`e-kzWk)HY2%%*=j&IySi6-rFZy+1@-p+TS^BG=g3~JPj@|#>x%n zDGygvrQcu8=2P1x4e%-|z`VOl| zP=Vw-KUBT;S(1F5eRgEKmY!X$OYW@j9xaQbjq2@&IFUbz4-T8(Z*LxLTF3Dt){Q#g zzx#tnGcm8Y!#t@qkc}ADusM1$obasCzTkuhB2;H~O8&CJ^p0(=*vOy+H6KEFZf@@^ z!wX)(4#Hm(Wf!tQQkN!h)WjAhO1YpE3h7)CdX|X0sIj)=$3yk#rec%~Pu1PTqM`#% zMNbCVQauwLSY`f{W@R~FK+sxXYRSjzP7G6Zw>(*xv%JBqIk~C2iC@R+{q7BprUrK zAI+t7wj+a8xl)ZE8(XEk#oy<`Bo7ANxDTp* zbIiRp7Tb+vF?#OUi~ZcP3=)6xY*Makn;Tr14`^8&pf~TNWsQql{ZSB zuo2owx?X3!DbR$o822^-g?>8I^y!Bc>iw}3~7XiM0F~; zRF1_7el>dP6dU?UMa_Z^wx>a_PbLkj8JHQgB~{v8Eh4^{G)eFl9pXy>$8Y z#TYnbeH2(H8u49a*-w?Uo8pk5UILxjR>&&5vwCIT$P;ve>ZTNexol6wgPpT5bRL#2 zim4vMWfyr<45XKfa*hw?n;e^u$Fln{N|Wih0|Y=!+R@QuIt(x#{Tiq!s8o9opVK;?6Htlu&Oun91#3O{L{#1Q(8Fo6=e)kvlYXeWXzMOP#Jg2Rx;*Q zWA!K%L$go;#%y{ZC%PDA=(cIf7qN=UD-NOkeZ_HJLsalUfc*863rtT0l5;qS&k}?6-|QE_#E@xe}yslJ?Pbm7||` zp$t&&K{7UUIaoqqqJIueryHf6aXgw(zJdTKTVsDm!!$$3{Wl}~-zh(#49U*Qh>zM? zlP;6zV;0sH(?_p&y--Tei3vd5A0ia13^+=H&SFnyWdT9}!w#%2z%U*RU?$-N6OjR| zl^&0I6af2b#pPM_!chClpp(%E8b$!CHefifGvg6PjueZq$hf%5GFkODB+i=$salft z#w4{bgcwU*h|p5Y6c-1%LT9*HD`-wnNui1;fCWOHF8Gw!o;Z9Ruyr1-2|3tC z>8OMLimwW{g_p^*(Ok8VNo!Nc-{EYB3BG7-zTKqEvxY^xVZ*|m%~}@UX(BebN^-u@ ziQ*^;-JEtNJf@OTi4&(_9mJg>es!m$V;4-}CASZLd7;LbWAlrP*=&EUR<~3zis6tm zeQSSj4~-s48RL1?YB{b>KJyBVY;%kys|DkA>1o{a4)J)e`QxmCpx>WoFfQw~*a%D~ zva4Jw?xJcjs(S>3W(FnX-eqC$$@xfjhd5R)mvsvCV3~r81y2d1Fd#yGK@6+|EcS??N#^+FGL}8 ze8v=J!`Y z8E08BPFGO=6Z}&!IZ}Jhm}2hw=&v(t{Ad#_KYCtOeDHCB&{k4vdzkPAbJ?0R`KhBy zUWbX`AZ$BmOq?cE&Z(yuER`-oSoJ3LQR4T3a>E%>E1C$Iwfyum z5UdJ0tY_+a7(8q2tU)SR4E8y*8`BygKLiY+F+fOU3AgpTn*qSopAUo8O!aMx0A6eU z*@kW~e_VU=0?a`K8~gIGfa=)s{EF+vb6*sE}s^FVS@O!Xc@qmpQenZAqKcL-| zj`hn4=E;^yjIpi|M|N zG6v6*?o4W$PdXV7je0m4S4#x%7nHkNbiua+gIu59@!S{M(tiT59E=k*VAUI+5^B&@ ze0+L_S2X{cM#IicNPxfwFB|gv*yUaHvET>e4O&{gvzFHEmMqqRktmr?M$?Hh#Wb9Q za}M*9v$!9Xzmi@3o4uq{HfLkO&U#)<6?Cyr?w~MadN3VM;y#+9^rH~HqlOk)M-y`5 z;sE2zxXf!>{z4-K-TM+|RKc)Km|i77H;O(wRN$pQ8=I3)37LN=ykVOU-FiH<8#7I$ zX=TL*6oI9XIM*_uvc)Wh3{Dv^J0nzOp(tps7Cp81>BM%jj`>4@6YHzlF+S_^{ zcUaLmMs7L@(w-&%DRqKOAX9BVbZt0F;Gyp=dDHk_ z7?um9TGRUm-B`Yh`Xjz(v^Xm#Y3t!dkBS#{!0djIFXY(*(|kQ!+xR%0{JgQbs8B(6s#wZ(*q~ zC4+`zeZ45L`+MCD^_5EMUr^LlHykNfiXp>7In7v+tO27KT<7UJw5}&{Jd7?!%;O-^ z&7H$km&a*%;j5$5`lM=WT46^|Wb95OGbr@xK|P7YgU*Bu$xKL)Mn~gCSJ&xDKTgkJ zHY%jyc^slKuLP7`u>$Z?$@U;UEqhVFYPrxX;lF@2efJ^MExsNuifN)knmA@)9beR} z*`8aagX1^5&DJ{=-$9KK2bD1~yQ$mxY(rpoQ@f6D4O&#~Ns{ye>R$Dy7D0NuIevNJ z#6vUmZ7HQc*+@RVsGoCes9g;sHM{)U&_fkU8nrg^(g3KEr&*yf*=S8q(1!NJVpDqJ z3D*WxO{XX9yhtp^u>i!nApY-5L_J4sLb#9C*HE+h~nP{^`8U;3rCR+VeOYbnq$X7Zp zs0qbBXSRZlpL4GM7IZdZHFDdKs&Gf^w;Wqn?*n#^;T=FOIuvm?v*%XW2iVzd>a&E5 zf9FgY+~dY)17<}5#?fh+Js z9Cl{<2k+U!+$R=u#YcsLyJEAl?aTD6rPC}Ibu{~Q=KvR)L&t2Sf0>irzLmbqb6tbV z2(=nQnEvuESZim_8Qho)EAM2z;S617W2UjB@(Y(8QdOa5@+?W{5=Y}iK{R7BRl*-Y zXcSI5XX5LNUWpl*Opytad0*K=-7vZ+>!mf)aj>LnVrl$7rlMGK?jX-F)!KRIB9O-t3Gw`N+x(M0)h% z=fZ(K9k_)Zdq0N8Alfl!-UUp+722Fk!p<4eaFNC}W=ukmrE`a&L3V}fzpa>=su6w4 z&q|*a_F-3jB*%s6$D-K2U@MqO(G=Dp2<==R_F`l^R}@ostkg>GwA~F6lL;|vM%310 z1(US{RAJU?*zo2;WY*x?w_CO8ZDs|&kH&FNLQfgmk!C!)XdWx(z2b{=3JHq$EHvRd ziV2AeJBNhcR2!jUw-dXdxUvET%RG9?!rAAcngOe#DRlNd1-+-B3sBHK3crH5&KiX) zXA8;&7zp^Z!E;0_n~MPVT7FmBl#6o{V5Zollk{hjL7W@^Q+n6f-Q5q?@-*;tB~u_OlS%()i+=gkJwDgBLisa; zb-BwbRfY}nl*~TkF5q7Ec_*sRnTz&6LN6-nJ%=u2xcCf{SxE3jE24CDPPn>~gXHro z7adn$R%4bE4Q7(=1w@3c40#ph@WJ%tDV!L88VSJ9um2hO|Nn99{}v4onbpuftSuDC zfHi}%E^JqGytEb31Uk=D{+wdSf2I_Y;fod(NfzOT1+V@{R@jaXmFI?^UFVLRf|!r` zAh$6zQs2EaziNknt{dvPw-bLhzH2kjC(L4>Hvg<_)8-$0jf{~Y1zD@;k=LKHO(Z`H zeheN3|1B5A7dmC0UHePi1f!WeX+&M4u*I)~gEugfP59q`XD)^_pcr9r*fG*bj1_I> z{ds3Ry0a;d{=!$_SpCc1fO|;DJc97$Zn^LHxHP@kIVKRg=1xAM0nQG`wHTbbA&HXnvhb|D& zII<2sdPOR8q{F%u3{JT+OGz2_F}iZW5UlfMC1}MM)^wO~|MHw{AMMU0A}6X&yA%+Z ziwW4-?gi@XHFsCCW|U+@nWgSgu`aae1~Eruu5*cqQ^>9Hy@udkL-55f2xm*?jyz4) zl(6+K3~T&ZOgt<>&?8cB&b%D+AMl=L-u%Z&(jXbelVn`3XpTsp{BhSh zmIa<=xz4`-Z+>d{3G-q%Hg|9-bFC=uvkVn29xU^gojTlr;D_o~us2Qy!TbHa=F$FP zPQY5ovBeemt(h12xD>8w#?OUvzrY}j1&n!IWE{>A- zDJHw?);LdlVQHT2nE!<=JaVWqA2l0c;v+OWQF|H&5e+FvvJ{5uD|SHyfJvJihwvEl zUrZwLom1Y;8g@=?HBoZdG(8X_pCP9?Mpqpc^ZD9Fb_RbMR`%Z9?Vg);xjeNe?~HRb z9F%4=<$P4Z<-~kN2UsMzhsAZnVE$tNWi) zJWK2q+BypzMhpkp1V?XU~5DoZL{SS7z?~N5Dr=GU$>y z^1^fr4a=q@zI_Nt#u2p0w7P^xO*#9pVzj%H<<~@6kwE6m&^wt(WYW$I>1!!yTS4;R zm*s$;Y1)mi;c2r|C15hV)hWK%{D8{moVkcBJmMmmqZK$mm9oqtS`#@cBn62+Jv*bT z^2Eg1*CF>!HPxEZ(wawtJ=G6;)-dm{tD>>cNX(+0N<#m3R|i!R)rou&#DY z3=G)hXCb%UfFsN^5|vhb^2|Po6ma0s-#o2bfuS>a#q3TdM;UUfHo|m0F(wxULgBTiF$)*kVccTY`iLF9Fy?T zvK1@(5}4?ZLLY}4b7_;IUo%MC@~U;Nb^IxV?B$f9OBsQ@D!7p=rus9&_oI$CGmoh% zi+OO=+}~@pTa6=Zxoqt3I&-oQ_+asAWwR$0&kIZDCk8?r z$#Ev1QiQYX_>wFwG@zaB1&7mW9PVs8I=HQIgtB%Rbf<&Cm7SgO*J(;&oiQC~I1GAp z*&7efKxECU=*;HDX4;*}WsuJ(t^cb7L5Atb@K#G4=}^1-At#`arr0`U_?UH?44pCoW~+J z5jB$Umb&Bs9sU92^&X&u6?rll-UHhT0-nK8CpogK=btxKlC&$o*X`5CII2rLi zW(-ivNd2L>39A-pI}gK)+TWQK!a#uCs22|-U;s8-fc{72s1kIO;J4s|rpRBxvx@v? zu$ysIHspo6bfgELI3i_Gu4UyuI+!@#g{W6%Ds9c7U8orBINYYR0+R}-{fYciw8Ke{ z#n9^5fvO2p0!qxKPgr%Py80GBK5`h&Jadlxi%Sm>)sA9DgxX5oD|^_2hbp00so<2& zu^|hIGe(=|8+*Otfg8oSX4{k;2;DwIZ)iHh0Cg4_sUJeP@tvk~6Y+2x4v6M)9SbHZ zM7CH?;SQVy$q*P|Gz5(Y(|(9xq#J}`1ibV3V>YjG1{L$^UkuCCOdf3vds zY#skAZO}MQ`ans}64-q4$nMF{8srj(T0PF+2v#CiLmQkOVL!@g-WT37bGA+4*5QVh z?Q&0-Osh+T#S|6GSnQsViF5>n9nOcb^^$b9XCY=np_?n+N&!Ce%bavngu1i#VGeiQ zl+~KbIaE)Hdk+Lb>Dn(O(d{Y9PwwtTj>gjME1X2h=P8O|3lm5sLEaHboFTC-D6q{F z)SMB5G`7g7atp<>uuUu%f;hQ3cH=IZCd8r{{$)Dc#XJ`L%W$J(d@HYqXDs;Lh+n$m{mr z-EWiNWu$naBrnnK`4Zh&P19&0XWAJ=W>H?gt=2YHlJ)JnA?sXHkFEORsHLPp?Jg^K zun6`9BjihKv^1!mqcAca)yjUJl-6Zk!3Dlfz9vfb(4W#FU97IFs`~-`!9%XkWFzgS z@JDzOYBz0nr_DPmHvlRp@#jRXQ3~o;u?OF!Lzi-Z7h9eg-53}JxT}Y&ZZXO7#2PjW zP;Eu&3ao=OtdT#A=$dhV@)30su3B#3<>I&2vYVWvU7!v~Z3VieMgk=&KQ(>8$wa`2 z5+F3U_x96(L5h>cL9%6IUm@24uU4_>cz+rXWntN< z`4ZJ953k;gtu5L5dVcjTiievDR+bt==xVFiA>`A&2y*Jk2=M9^C9a>iB+@c&JgQ8k#-w)ulH{S$Bo_i#n6g zhfXsxa-M(mh;Qci7y)%yef-U z<&_Dn2vn6IFz?~ICU6$_amrB5vFdv5mI&&)B(2g2Us(NQE>SMgSr6^7L zd&-uLlhG30>Qr?bAQ7xMtC(r2>a0EWi{%l>S*QiH#q*2I%6XoIL4jJue-Q~=T0J~a z4=54ASi08pvaj-`h3{VFqAKmn{1?vhIzg%9oQ}tdG!PrbHwioHc7C8eP?O7%|0$km z%Voxjc@TRlG@|&No~pPFCkY8mmHkZR81k>}3=j`4rCQv+q2E&SbB~i-c=9xkX$XiE zboiQfn0d}k*Es^W_})8BtKGlz|67H?I3l3JD<~2VlY^WEam#=%Fky?0)yl2FVz_}- zZJ4_!Wg-1D$81JAP0csNwyrg{qTFxsmtmP%p}nimW~ja!NVWEU?VLNo$g`}RNfxX4 z1pDh(R<`EcxJ?pMDtX-7*x0~*A1LO1c5>7?Z67m8qaLKsEhh1j=5WR+tpBRh?P{g* zFY_ikV(UuX=zX<4<@8h(*u_l@$Izhx6a1)T3Km7tw&q6*K}4h7`euEzwnahF zFW+w5Uj&{!-uf_%wvtQc-h__BwSB+c`$xNXxb{W=dEjzM0q+ltJ;&wFnaBK|mV zcQGa{^T7)-abyDG?d)vN9O(@MhWzuut<4-9IDN1=9uCJo^=;`)S*wTWY=5 z>*GTQi|59w_p*N|3+cT~zwf;~a3yue=dD*-L-|k4cAl`eA&WwO3ynCri)XO-g}5?2 zjWGqCD`=r9UN6djee~;j&jaT5JLhMIrM0qw?7G6+`0C$XG52Ks-M?$@{`;rvZ2b!# z_-w7gX6u!b13Kp&E3M^}T4L_UDJwNg?EN#kxQrA)hA}1H1~&*l+fz>&arm_x)2z|vuASk%0*4V1!8kN44gi2ZV6PhD2{B5 z?Clb*bC~pFH@1$vR+}V8mN@V?2a~Ce@T&b!Gau$HX})u0fZ2Pdk#)A z_=eFW@tKQj*LGEGr?%fTuqWYvw9S6*AG@`E=0@Qi{1XRZ!rbK2Z|pX894rTYkDI1YoFNqR_pBW7tUk8M%g-A*ReRk8YgE9+9lXAV~yI*-xjrq+Ont- zb_?ba3zNuo_Ep1_6%x<>Wmr}hiOAMUS*;_hm05&E92O`rB!~XB*}+qoGh`44G_4ls zR<7(UJ6-Du>INl%sixAR=S;2nu{5&JZ2dW7M!4JNK^lYBYi24ktaTP2{{wL@JZ@6J$)Pkp?iCK4wed30K;$t+~g*T&<7tzk~e;U7qWxE z9B{q&cn(_ySE&UHn-KUR&Jq6`vd>}StbMUn{mcP_9;8A<6k-n(kwHwm@{;RnO4%|B z`F^^l*U217r7R(*u3*2RmMGuOR5>86dIpNDYEwA4j5S%|LX{=aSyG+{m!mjf{?ofZ zv_kcaiB|lEJ_MlSVf@4RLiu$ge-vg1gsv6Grozw!|kKM%6HK>Oe}CGkN0r6E`x z&~gmtB$th9Je#iJEFT*>2;3-tIAu4{ba+h{=!R3;`$mr%tI#y>#yh}7;Y6oOXb`@t zPlKk9eU}bJlFlF&`_1alXx}Ny_#4#Ed@_WO-iX)B5|VweKe)5RL18z&Nfh}y$O23f zQT+?hB3Yt$Nz%>1vhyHm{n{sb_WQN3TkX>zuVte>-lg^DT37 zd63w9-EZM+1f#}ufp*BAgCOD123dN6VMH3z=G8eEMwFEgBi!esYU4kd=gIUmC{5_N z+zm#5FdC~*(j@$INLkshqMXkn?Yk)V!&~1Q#N*HEBk{W&RFeuTmxBr&&8}3kHAXyo zlU64Z*kFI(Q@M_8D8CT{vTkKw8F!OyFgnnShe^FD{YM9<=vEiZJv8_Q&$b zs=C_)kBREnA7CN4aHH+Glz3_79$j@>6*YQqdcU2uj^4DZszRtJjkJpI>BK-@p##w( z<6}KoMYwdTD#MSJ*s-~}nc)K`H)WKd01{9OOw0aQwoWcIhz6LwqOm2z3n(kZ4Tf`@ zC!{2evCHuzhlIAPL6cKjNb)9S_&7>3Mim}0tzUgfC36Apnw7jD4>7>3Mim}0v-zg1IG&}J^(lg E072nNhyVZp literal 0 HcmV?d00001