├── .gitignore ├── fakezonegen ├── .gitignore ├── Makefile ├── config.php.example ├── deb │ ├── .gitignore │ └── DEBIAN │ │ ├── conffiles │ │ └── control ├── fakezonegen.php └── fakezonegen2.php └── slave ├── .gitignore ├── Makefile ├── checkurl.py ├── config.py.example ├── extra ├── DEBIAN │ ├── conffiles │ └── control ├── checkurl ├── roskomcheck └── viewlinks.php └── roskomcheck.py /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orgtechservice/roskombox/63577503ebcb2e098e89ca876416d6243002bf84/.gitignore -------------------------------------------------------------------------------- /fakezonegen/.gitignore: -------------------------------------------------------------------------------- 1 | *.deb 2 | -------------------------------------------------------------------------------- /fakezonegen/Makefile: -------------------------------------------------------------------------------- 1 | 2 | ############################ AUTOCONF VARS ################################### 3 | 4 | # архитектура для которой будет собираться deb-пакет. 5 | # сборщик пакетов и компилятор имеют разную терминлогию, 6 | # в скрипте configure мы транслируем HOST_CPU в имя архитектуры которую 7 | # ожидает менеджер пакетов 8 | DEB_ARCH:=all 9 | 10 | ############################### VARS ######################################### 11 | 12 | # Имя файла deb-пакета 13 | DEB_FILENAME=roskombox-fakezonegen-$(DEB_ARCH).deb 14 | 15 | ############################# GENERIC RULES ################################## 16 | 17 | # .PHONY указывает цели которые не создают файлов. 18 | .PHONY: all deb clean distclean 19 | 20 | all: deb 21 | 22 | # сборка deb-пакетов, просто короткая цель чтобы не вспоминать имя пакета 23 | deb: deb_clean $(DEB_FILENAME) 24 | echo $(DEB_FILENAME) 25 | 26 | # сборка пакета 27 | $(DEB_FILENAME): deb_clean deb_install 28 | fakeroot dpkg-deb --build deb $(DEB_FILENAME) 29 | 30 | .PHONY: deb_install 31 | deb_install: $(PROG_FILENAME) 32 | mkdir -p deb/etc/maycloud 33 | mkdir -p deb/var/lib/maycloud/php-exec 34 | mkdir -p deb/var/lib/maycloud/php-modules 35 | 36 | install -m 0755 fakezonegen.php deb/var/lib/maycloud/php-exec 37 | install -m 0755 fakezonegen2.php deb/var/lib/maycloud/php-exec 38 | install -m 0644 config.php.example deb/etc/maycloud/fakezonegen.php 39 | 40 | ########################### CLEAN RULES ###################################### 41 | 42 | # полная очистка 43 | distclean: deb_clean 44 | rm -rf $(DEB_FILENAME) 45 | 46 | # простая очистка, промежуточные файлы, но оставляет целевые 47 | clean: deb_clean 48 | 49 | # зачистка в каталоге deb-пакета 50 | .PHONY: deb_clean 51 | deb_clean: 52 | rm -rf deb/etc 53 | rm -rf deb/var 54 | -------------------------------------------------------------------------------- /fakezonegen/config.php.example: -------------------------------------------------------------------------------- 1 | 'roskom_db', 6 | 'DB_USER' => 'roskom', 7 | 'DB_PASSWD' => '******', 8 | 'DB_HOST' => 'localhost', 9 | 10 | // БД админки 11 | 'LANBILL_DB_NAME' => 'lan_test', 12 | 'LANBILL_DB_USER' => 'lan', 13 | 'LANBILL_DB_PASSWD' => '******', 14 | 'LANBILL_DB_HOST' => 'localhost', 15 | 16 | // Файлы 17 | 'FILENAME' => '/tmp/fake.zones', 18 | 'TRAP' => '/etc/bind/zone.rkn-trap', 19 | 20 | // Команда перезагрузки 21 | 'RELOAD_CMD' => 'rndc reload', 22 | 23 | 'EXCEPT_DOMAINS' => ['vk.com', 'vkontakte.ru', 'ya.ru', 'yandex.ru'], 24 | ]; 25 | -------------------------------------------------------------------------------- /fakezonegen/deb/.gitignore: -------------------------------------------------------------------------------- 1 | etc 2 | var 3 | -------------------------------------------------------------------------------- /fakezonegen/deb/DEBIAN/conffiles: -------------------------------------------------------------------------------- 1 | /etc/maycloud/fakezonegen.php 2 | -------------------------------------------------------------------------------- /fakezonegen/deb/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: roskombox-fakezonegen 2 | Version: 1.0-2 3 | Section: misc 4 | Architecture: all 5 | Depends: maycloud-phpcore, bind9 6 | Maintainer: Alice 7 | Priority: extra 8 | Description: Fake zone generator 9 | -------------------------------------------------------------------------------- /fakezonegen/fakezonegen.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | beginTransaction(); 65 | 66 | try { 67 | $message = $lanbill->quote($message); 68 | $event_id = $lanbill->quote($event_id); 69 | $module_name = $lanbill->quote('roskom'); 70 | 71 | // Вдруг событие уже существует 72 | $statement = $lanbill->prepare("SELECT * FROM eventmon_events WHERE event_module = $module_name AND event_id = $event_id FOR UPDATE"); 73 | $statement->execute(); 74 | $row = $statement->fetch(PDO::FETCH_ASSOC); 75 | $statement->closeCursor(); 76 | 77 | // Сохраним или обновим событие 78 | if($row) { 79 | $lanbill->exec("UPDATE eventmon_events SET event_updated = $time, event_message = $message WHERE event_id = $event_id"); 80 | } else { 81 | $data = ['event_id' => $event_id, 'event_module' => $module_name, 'event_when' => $time, 'event_updated' => $time, 'event_message' => $message]; 82 | $fields = implode(', ', array_keys($data)); 83 | $values = implode(', ', array_values($data)); 84 | $lanbill->exec("INSERT INTO eventmon_events ($fields) VALUES ($values)"); 85 | } 86 | 87 | $lanbill->commit(); 88 | return true; 89 | } catch(Exception $e) { 90 | $lanbill->rollback(); 91 | return false; 92 | } 93 | } 94 | 95 | function getLastRegistryUpdate() { 96 | global $lanbill; 97 | $statement = $lanbill->prepare("SELECT config_value FROM config WHERE config_name = 'roskom:lastDumpDate'"); 98 | $statement->execute(); 99 | $row = $statement->fetch(PDO::FETCH_ASSOC); 100 | $statement->closeCursor(); 101 | return $row ? $row['config_value'] : false; 102 | } 103 | 104 | function getOurLastDate() { 105 | global $config; 106 | $m = []; 107 | $line = getLastLine($config['FILENAME']); 108 | if(!preg_match('/^#DUMP COMPLETE <(\d+)>$/', $line, $m)) { 109 | return false; 110 | } else { 111 | return @ (int) $m[1]; 112 | } 113 | } 114 | 115 | /* Далее непосредственно логика скрипта */ 116 | 117 | // Сначала подключимся к БД админки 118 | $lanbill = NULL; 119 | try { 120 | $lanbill = new PDO ( 121 | "mysql:dbname={$config['LANBILL_DB_NAME']};host={$config['LANBILL_DB_HOST']}", 122 | $config['LANBILL_DB_USER'], 123 | $config['LANBILL_DB_PASSWD'], 124 | [PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES UTF8"] 125 | ); 126 | } catch(Exception $e) { 127 | // Если не удалось, то максимум, что мы можем — поругаться в стандартный вывод 128 | echo "MySQL connection failed\n{$e->getMessage()}\n"; 129 | die(-1); 130 | } 131 | 132 | // Получим дату последнего обновления реестра 133 | $last_dump_date = getLastRegistryUpdate(); 134 | 135 | // Проверим, существует ли файл зон 136 | if(file_exists($config['FILENAME'])) { 137 | // Запросим дату обновления файла зон 138 | $our_last_date = getOurLastDate(); 139 | 140 | // Если даты совпадают, нам ничего не нужно делать 141 | if($last_dump_date === $our_last_date) { 142 | die(0); 143 | } 144 | } 145 | 146 | // Теперь установим соединение с основной БД Роскома 147 | $db = NULL; 148 | try { 149 | $db = new PDO ( 150 | "mysql:dbname={$config['DB_NAME']};host={$config['DB_HOST']}", 151 | $config['DB_USER'], 152 | $config['DB_PASSWD'], 153 | [PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES UTF8"] 154 | ); 155 | } catch(Exception $e) { 156 | // В случае неудачи можно ещё попробовать создать аварийное событие, вдруг получится 157 | $message = "MySQL connection failed\n{$e->getMessage()}\n"; 158 | echo $message; 159 | fireEvent('fzg_roskom', $message); 160 | die(-1); 161 | } 162 | 163 | $IDN = new idna_convert(); 164 | 165 | $ips = []; 166 | $domains = []; 167 | 168 | try { 169 | // Сначала запросим HTTPS-ресурсы из реестра 170 | $statement = $db->prepare("SELECT url_text FROM roskom_url WHERE LOWER(url_text) LIKE 'https://%'"); 171 | $statement->execute(); 172 | while($row = $statement->fetch(PDO::FETCH_ASSOC)) { 173 | $match = []; 174 | if(validateDomain($row['url_text'], $match)) { 175 | if(isIpAddress($match[2])) { 176 | $ips[$match[2]] = $match[2]; 177 | } else { 178 | //$domains[$match[2]] = isCyrillic($match[4]) ? $IDN->encode($match[2]) : $match[2]; 179 | $domains[$match[2]] = $IDN->encode($match[2]); 180 | } 181 | } 182 | } 183 | $statement->closeCursor(); 184 | 185 | // Теперь запросим ресурсы с левым портом из реестра 186 | $statement = $db->prepare("SELECT url_text FROM roskom_url WHERE LOWER(url_text) REGEXP '^http://[^/]+:[0-9]+'"); 187 | $statement->execute(); 188 | while($row = $statement->fetch(PDO::FETCH_ASSOC)) { 189 | $match = []; 190 | if(validateDomain($row['url_text'], $match)) { 191 | if(usingWrongPort($match)) { 192 | if(isIpAddress($match[2])) { 193 | $ips[$match[2]] = $match[2]; 194 | } else { 195 | //$domains[$match[2]] = isCyrillic($match[4]) ? $IDN->encode($match[2]) : $match[2]; 196 | $domains[$match[2]] = $IDN->encode($match[2]); 197 | } 198 | } 199 | } 200 | } 201 | $statement->closeCursor(); 202 | 203 | // Теперь запросим HTTPS-ресурсы локальных решений 204 | $statement = $db->prepare("SELECT url_text FROM roskom_order_url WHERE LOWER(url_text) LIKE 'https://%'"); 205 | $statement->execute(); 206 | while($row = $statement->fetch(PDO::FETCH_ASSOC)) { 207 | $match = []; 208 | if(validateDomain($row['url_text'], $match)) { 209 | if(isIpAddress($match[2])) { 210 | $ips[$match[2]] = $match[2]; 211 | } else { 212 | //$domains[$match[2]] = isCyrillic($match[4]) ? $IDN->encode($match[2]) : $match[2]; 213 | $domains[$match[2]] = $IDN->encode($match[2]); 214 | } 215 | } 216 | } 217 | $statement->closeCursor(); 218 | 219 | // Теперь запросим ресурсы с левым портом из локальных решений 220 | $statement = $db->prepare("SELECT url_text FROM roskom_order_url WHERE lower(url_text) REGEXP '^http://[^/]+:[0-9]+'"); 221 | $statement->execute(); 222 | while($row = $statement->fetch(PDO::FETCH_ASSOC)) { 223 | $match = []; 224 | if(validateDomain($row['url_text'], $match)) { 225 | if(usingWrongPort($match)) { 226 | if(isIpAddress($match[2])) { 227 | $ips[$match[2]] = $match[2]; 228 | } else { 229 | //$domains[$match[2]] = isCyrillic($match[4]) ? $IDN->encode($match[2]) : $match[2]; 230 | $domains[$match[2]] = $IDN->encode($match[2]); 231 | } 232 | } 233 | } 234 | } 235 | $statement->closeCursor(); 236 | } catch(Exception $e) { 237 | $report = fireEvent('fzg_roskom_fetch', "Fake zone generator failed to fetch data from roskom database"); 238 | if(!$report) { 239 | echo "MySQL error\n{$e->getMessage()}\n"; 240 | } 241 | die(-1); 242 | } 243 | 244 | // Теперь от БД можно отключиться 245 | unset($db); 246 | 247 | $zones = []; 248 | foreach($domains as & $value) { 249 | if($value[strlen($value) - 1] === '.') $value = substr($value, 0, strlen($value) - 1); 250 | $zones[$value] = renderZone($value); 251 | } 252 | 253 | $data = implode("\n", $zones) . "\n\n#DUMP COMPLETE <$last_dump_date>\n"; 254 | file_put_contents($config['FILENAME'], $data); 255 | 256 | // Снова проверим, присутствует ли метка времени 257 | // Если строки с датой в файле не оказалось, необходимо громко заматериться вслух 258 | $our_last_date = getOurLastDate(); 259 | if($our_last_date === false) { 260 | fireEvent('fzg_broken_file', "Fake zone file is broken: no last dump date present"); 261 | } else { 262 | exec($config['RELOAD_CMD']); 263 | } 264 | 265 | // Отключаемся от второй базы 266 | unset($lanbill); 267 | -------------------------------------------------------------------------------- /fakezonegen/fakezonegen2.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | beginTransaction(); 67 | 68 | try { 69 | $message = $lanbill->quote($message); 70 | $event_id = $lanbill->quote($event_id); 71 | $module_name = $lanbill->quote('roskom'); 72 | 73 | // Вдруг событие уже существует 74 | $statement = $lanbill->prepare("SELECT * FROM eventmon_events WHERE event_module = $module_name AND event_id = $event_id FOR UPDATE"); 75 | $statement->execute(); 76 | $row = $statement->fetch(PDO::FETCH_ASSOC); 77 | $statement->closeCursor(); 78 | 79 | // Сохраним или обновим событие 80 | if($row) { 81 | $lanbill->exec("UPDATE eventmon_events SET event_updated = $time, event_message = $message WHERE event_id = $event_id"); 82 | } else { 83 | $data = ['event_id' => $event_id, 'event_module' => $module_name, 'event_when' => $time, 'event_updated' => $time, 'event_message' => $message]; 84 | $fields = implode(', ', array_keys($data)); 85 | $values = implode(', ', array_values($data)); 86 | $lanbill->exec("INSERT INTO eventmon_events ($fields) VALUES ($values)"); 87 | } 88 | 89 | $lanbill->commit(); 90 | return true; 91 | } catch(Exception $e) { 92 | $lanbill->rollback(); 93 | return false; 94 | } 95 | } 96 | 97 | function getLastRegistryUpdate() { 98 | global $lanbill; 99 | $statement = $lanbill->prepare("SELECT config_value FROM config WHERE config_name = 'roskom:lastDumpDate'"); 100 | $statement->execute(); 101 | $row = $statement->fetch(PDO::FETCH_ASSOC); 102 | $statement->closeCursor(); 103 | return $row ? $row['config_value'] : false; 104 | } 105 | 106 | function getOurLastDate() { 107 | global $config; 108 | $m = []; 109 | $line = getLastLine($config['FILENAME']); 110 | if(!preg_match('/^#DUMP COMPLETE <(\d+)>$/', $line, $m)) { 111 | return false; 112 | } else { 113 | return @ (int) $m[1]; 114 | } 115 | } 116 | 117 | function endsWith($haystack, $needle) { 118 | $length = strlen($needle); 119 | if($length == 0) return false; 120 | return (substr($haystack, - $length) === $needle); 121 | } 122 | 123 | /* Далее непосредственно логика скрипта */ 124 | 125 | // Сначала подключимся к БД админки 126 | $lanbill = NULL; 127 | try { 128 | $lanbill = new PDO ( 129 | "mysql:dbname={$config['LANBILL_DB_NAME']};host={$config['LANBILL_DB_HOST']}", 130 | $config['LANBILL_DB_USER'], 131 | $config['LANBILL_DB_PASSWD'], 132 | [PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES UTF8"] 133 | ); 134 | } catch(Exception $e) { 135 | // Если не удалось, то максимум, что мы можем — поругаться в стандартный вывод 136 | echo "MySQL connection failed\n{$e->getMessage()}\n"; 137 | die(-1); 138 | } 139 | 140 | // Получим дату последнего обновления реестра 141 | $last_dump_date = getLastRegistryUpdate(); 142 | 143 | // Проверим, существует ли файл зон 144 | if(file_exists($config['FILENAME'])) { 145 | // Запросим дату обновления файла зон 146 | $our_last_date = getOurLastDate(); 147 | 148 | // Если даты совпадают, нам ничего не нужно делать 149 | if($last_dump_date === $our_last_date) { 150 | die(0); 151 | } 152 | } 153 | 154 | // Теперь установим соединение с основной БД Роскома 155 | $db = NULL; 156 | try { 157 | $db = new PDO ( 158 | "mysql:dbname={$config['DB_NAME']};host={$config['DB_HOST']}", 159 | $config['DB_USER'], 160 | $config['DB_PASSWD'], 161 | [PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES UTF8"] 162 | ); 163 | } catch(Exception $e) { 164 | // В случае неудачи можно ещё попробовать создать аварийное событие, вдруг получится 165 | $message = "MySQL connection failed\n{$e->getMessage()}\n"; 166 | echo $message; 167 | fireEvent('fzg_roskom', $message); 168 | die(-1); 169 | } 170 | 171 | $IDN = new idna_convert(); 172 | 173 | $ips = []; 174 | $domains = []; 175 | 176 | try { 177 | // Сначала запросим HTTPS-ресурсы из реестра 178 | $statement = $db->prepare("SELECT url_text FROM roskom_url WHERE LOWER(url_text) LIKE 'https://%'"); 179 | $statement->execute(); 180 | while($row = $statement->fetch(PDO::FETCH_ASSOC)) { 181 | $match = []; 182 | if(validateDomain($row['url_text'], $match)) { 183 | if(isIpAddress($match[2])) { 184 | $ips[$match[2]] = $match[2]; 185 | } else { 186 | //$domains[$match[2]] = isCyrillic($match[4]) ? $IDN->encode($match[2]) : $match[2]; 187 | $domains[$match[2]] = $IDN->encode($match[2]); 188 | } 189 | } 190 | } 191 | $statement->closeCursor(); 192 | 193 | // Теперь запросим ресурсы с левым портом из реестра 194 | $statement = $db->prepare("SELECT url_text FROM roskom_url WHERE LOWER(url_text) REGEXP '^http://[^/]+:[0-9]+'"); 195 | $statement->execute(); 196 | while($row = $statement->fetch(PDO::FETCH_ASSOC)) { 197 | $match = []; 198 | if(validateDomain($row['url_text'], $match)) { 199 | if(usingWrongPort($match)) { 200 | if(isIpAddress($match[2])) { 201 | $ips[$match[2]] = $match[2]; 202 | } else { 203 | //$domains[$match[2]] = isCyrillic($match[4]) ? $IDN->encode($match[2]) : $match[2]; 204 | $domains[$match[2]] = $IDN->encode($match[2]); 205 | } 206 | } 207 | } 208 | } 209 | $statement->closeCursor(); 210 | 211 | // Теперь запросим HTTPS-ресурсы локальных решений 212 | $statement = $db->prepare("SELECT url_text FROM roskom_order_url WHERE LOWER(url_text) LIKE 'https://%'"); 213 | $statement->execute(); 214 | while($row = $statement->fetch(PDO::FETCH_ASSOC)) { 215 | $match = []; 216 | if(validateDomain($row['url_text'], $match)) { 217 | if(isIpAddress($match[2])) { 218 | $ips[$match[2]] = $match[2]; 219 | } else { 220 | //$domains[$match[2]] = isCyrillic($match[4]) ? $IDN->encode($match[2]) : $match[2]; 221 | $domains[$match[2]] = $IDN->encode($match[2]); 222 | } 223 | } 224 | } 225 | $statement->closeCursor(); 226 | 227 | // Теперь запросим ресурсы с левым портом из локальных решений 228 | $statement = $db->prepare("SELECT url_text FROM roskom_order_url WHERE lower(url_text) REGEXP '^http://[^/]+:[0-9]+'"); 229 | $statement->execute(); 230 | while($row = $statement->fetch(PDO::FETCH_ASSOC)) { 231 | $match = []; 232 | if(validateDomain($row['url_text'], $match)) { 233 | if(usingWrongPort($match)) { 234 | if(isIpAddress($match[2])) { 235 | $ips[$match[2]] = $match[2]; 236 | } else { 237 | //$domains[$match[2]] = isCyrillic($match[4]) ? $IDN->encode($match[2]) : $match[2]; 238 | $domains[$match[2]] = $IDN->encode($match[2]); 239 | } 240 | } 241 | } 242 | } 243 | $statement->closeCursor(); 244 | 245 | // Теперь запросим ресурсы, которые нужно блокировать вместе со всеми поддоменами (domain-mask) 246 | $in_extra = $block_domains ? ', \'domain\'' : ''; 247 | $statement = $db->prepare("SELECT domain_text FROM roskom_domain WHERE domain_content_id IN (SELECT content_id FROM roskom_content WHERE content_block_type IN ('domain-mask'$in_extra))"); 248 | $statement->execute(); 249 | while($row = $statement->fetch(PDO::FETCH_ASSOC)) { 250 | $domain = str_replace('*.', '', $row['domain_text']); 251 | $domains[$domain] = $IDN->encode($domain); 252 | } 253 | $statement->closeCursor(); 254 | 255 | } catch(Exception $e) { 256 | $report = fireEvent('fzg_roskom_fetch', "Fake zone generator failed to fetch data from roskom database"); 257 | if(!$report) { 258 | echo "MySQL error\n{$e->getMessage()}\n"; 259 | } 260 | die(-1); 261 | } 262 | 263 | // Теперь от БД можно отключиться 264 | unset($db); 265 | 266 | $zones = []; 267 | foreach($domains as & $value) { 268 | if($value[strlen($value) - 1] === '.') $value = substr($value, 0, strlen($value) - 1); 269 | if(in_array($value, $config['EXCEPT_DOMAINS'])) continue; 270 | 271 | foreach($config['EXCEPT_DOMAINS_MASK'] as $exception) { 272 | if(endsWith($value, $exception)) 273 | continue; 274 | } 275 | 276 | $zones[$value] = renderZone($value); 277 | } 278 | 279 | $data = implode("\n", $zones) . "\n\n#DUMP COMPLETE <$last_dump_date>\n"; 280 | file_put_contents($config['FILENAME'], $data); 281 | 282 | // Снова проверим, присутствует ли метка времени 283 | // Если строки с датой в файле не оказалось, необходимо громко заматериться вслух 284 | $our_last_date = getOurLastDate(); 285 | if($our_last_date === false) { 286 | fireEvent('fzg_broken_file', "Fake zone file is broken: no last dump date present"); 287 | } else { 288 | exec($config['RELOAD_CMD']); 289 | } 290 | 291 | // Отключаемся от второй базы 292 | unset($lanbill); 293 | -------------------------------------------------------------------------------- /slave/.gitignore: -------------------------------------------------------------------------------- 1 | config.py 2 | output 3 | roskombox-slave-all.deb 4 | -------------------------------------------------------------------------------- /slave/Makefile: -------------------------------------------------------------------------------- 1 | 2 | ############################ AUTOCONF VARS ################################### 3 | 4 | # архитектура для которой будет собираться deb-пакет. 5 | # сборщик пакетов и компилятор имеют разную терминлогию, 6 | # в скрипте configure мы транслируем HOST_CPU в имя архитектуры которую 7 | # ожидает менеджер пакетов 8 | DEB_ARCH:=all 9 | 10 | ############################### VARS ######################################### 11 | 12 | # Имя файла deb-пакета 13 | DEB_FILENAME=roskombox-slave-$(DEB_ARCH).deb 14 | 15 | ############################# GENERIC RULES ################################## 16 | 17 | # .PHONY указывает цели которые не создают файлов. 18 | .PHONY: all deb clean distclean 19 | 20 | all: deb 21 | 22 | # сборка deb-пакетов, просто короткая цель чтобы не вспоминать имя пакета 23 | deb: deb_clean $(DEB_FILENAME) 24 | echo $(DEB_FILENAME) 25 | 26 | # сборка пакета 27 | $(DEB_FILENAME): deb_clean deb_install 28 | fakeroot dpkg-deb --build output/roskombox-slave $(DEB_FILENAME) 29 | 30 | .PHONY: deb_install 31 | deb_install: $(PROG_FILENAME) 32 | mkdir -p output/roskombox-slave/opt/roskomcheck 33 | mkdir -p output/roskombox-slave/usr/bin 34 | cp -r extra/DEBIAN output/roskombox-slave 35 | cp config.py.example output/roskombox-slave/opt/roskomcheck/config.py 36 | install -m 0755 roskomcheck.py output/roskombox-slave/opt/roskomcheck/roskomcheck.py 37 | install -m 0755 checkurl.py output/roskombox-slave/opt/roskomcheck/checkurl.py 38 | install -m 0755 extra/roskomcheck output/roskombox-slave/usr/bin/roskomcheck 39 | install -m 0755 extra/checkurl output/roskombox-slave/usr/bin/checkurl 40 | 41 | ########################### CLEAN RULES ###################################### 42 | 43 | # полная очистка 44 | distclean: deb_clean 45 | rm -rf $(DEB_FILENAME) 46 | 47 | # простая очистка, промежуточные файлы, но оставляет целевые 48 | clean: deb_clean 49 | 50 | # зачистка в каталоге deb-пакета 51 | .PHONY: deb_clean 52 | deb_clean: 53 | rm -rf output 54 | -------------------------------------------------------------------------------- /slave/checkurl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # Рекомендуется Python 3.x 4 | 5 | # Импортируем важные пакеты 6 | import sys, requests 7 | 8 | # Отключим ругань на невалидный сертификат 9 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 10 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 11 | 12 | # Прикинемся браузером 13 | request_headers = { 14 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/49.0.2623.108 Chrome/49.0.2623.108 Safari/537.36', 15 | } 16 | 17 | # http://stackoverflow.com/questions/22492484/how-do-i-get-the-ip-address-from-a-http-request-using-the-requests-library 18 | try: 19 | from requests.packages.urllib3.connectionpool import HTTPConnectionPool 20 | except: 21 | print("Sadly, your version of Requests is too old.\nTry using the version from OTS repo: http://doc.mkpnet.ru/admin/deb/index.html") 22 | sys.exit(-1) 23 | 24 | # Новый метод, который мы обезьянним методом воткнём вместо старого 25 | def _make_request(self, conn, method, url, **kwargs): 26 | response = self._old_make_request(conn, method, url, **kwargs) 27 | sock = getattr(conn, 'sock', False) 28 | if sock: 29 | setattr(response, 'peer', sock.getpeername()) 30 | else: 31 | setattr(response, 'peer', None) 32 | return response 33 | 34 | # Осуществляем подмену 35 | HTTPConnectionPool._old_make_request = HTTPConnectionPool._make_request 36 | HTTPConnectionPool._make_request = _make_request 37 | 38 | try: 39 | url = sys.argv[1] 40 | except: 41 | print('Usage: checkurl ') 42 | sys.exit(-1) 43 | 44 | try: 45 | response = requests.get(url, timeout = 3, stream = True, headers = request_headers) 46 | content = response.raw.read(100000, decode_content = True) 47 | 48 | if b'eais.rkn.gov.ru' in content: 49 | print('Blocked') 50 | else: 51 | peer = response.raw._original_response.peer 52 | if peer is not None: 53 | address = peer[0] 54 | if address.startswith('127') or address.startswith('192.168') or address.startswith('10.10') or address == '::1' or address is None: 55 | print('Local IP address') 56 | else: 57 | print('Available') 58 | else: 59 | print('Available') 60 | except: 61 | print('Connection failed (probably blocked)') 62 | -------------------------------------------------------------------------------- /slave/config.py.example: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Число потоков 4 | THREADS = 100 5 | 6 | # Параметры для подключения к БД 7 | DATABASE = { 8 | 'host': 'localhost', 9 | 'user': 'lan', 10 | 'passwd': '******', 11 | 'db': 'lan_test', 12 | } 13 | 14 | # Таймаут подключения в секундах 15 | HTTP_TIMEOUT = 2 16 | 17 | # Журнал доступных URL 18 | ENABLE_LOG = True 19 | LOG = '/tmp/available-links.txt' 20 | -------------------------------------------------------------------------------- /slave/extra/DEBIAN/conffiles: -------------------------------------------------------------------------------- 1 | /opt/roskomcheck/config.py 2 | -------------------------------------------------------------------------------- /slave/extra/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: roskombox-slave 2 | Version: 1.0-22 3 | Section: misc 4 | Architecture: all 5 | Depends: python3, python3-requests, python3-mysql.connector 6 | Maintainer: Alice 7 | Priority: extra 8 | Description: OTS package for RSOC checker utility 9 | Conflicts: ots-roskomcheck 10 | Replaces: ots-roskomcheck 11 | -------------------------------------------------------------------------------- /slave/extra/checkurl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /opt/roskomcheck/checkurl.py $1 4 | -------------------------------------------------------------------------------- /slave/extra/roskomcheck: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /opt/roskomcheck/roskomcheck.py $1 4 | -------------------------------------------------------------------------------- /slave/extra/viewlinks.php: -------------------------------------------------------------------------------- 1 | Ссылки, открывшиеся при последней проверке

Ссылки, открывшиеся при последней проверке

$content
"; 12 | } 13 | 14 | function renderLinks(& $links) { 15 | return "
    " . implode ( 16 | '', 17 | array_map ( 18 | function($link) { 19 | $link = htmlspecialchars($link); 20 | return "
  • $link
  • "; 21 | }, 22 | $links 23 | ) 24 | ) . "
"; 25 | } 26 | 27 | $content = renderLinks($links); 28 | echo htmlPage($content); 29 | -------------------------------------------------------------------------------- /slave/roskomcheck.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # Рекомендуется Python 3.x 4 | 5 | """ 6 | Я старался как следует прокомментировать основные моменты, чтобы в случае необходимости 7 | в моём коде мог разобраться сотрудник, незнакомый с Python 8 | © Илья 9 | """ 10 | 11 | # Импортируем важные пакеты 12 | import time, sys, threading, requests, signal 13 | 14 | # Время начала работы скрипта 15 | execution_start = time.time() 16 | 17 | # Расставим затычки-мьютексы 18 | in_mutex = threading.Lock() 19 | out_mutex = threading.Lock() 20 | 21 | # Прикинемся браузером 22 | request_headers = { 23 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/49.0.2623.108 Chrome/49.0.2623.108 Safari/537.36', 24 | } 25 | 26 | # Счётчик обработанных ссылок (для отображения прогресса) 27 | counter = 0 28 | 29 | # http://stackoverflow.com/questions/22492484/how-do-i-get-the-ip-address-from-a-http-request-using-the-requests-library 30 | try: 31 | from requests.packages.urllib3.connectionpool import HTTPConnectionPool 32 | 33 | # Отключим ругань на невалидный сертификат 34 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 35 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 36 | except: 37 | print("Sadly, your version of Requests is too old.\nTry using the version from OTS repo: http://doc.mkpnet.ru/admin/deb/index.html") 38 | sys.exit(-1) 39 | 40 | # Новый метод, который мы обезьянним методом воткнём вместо старого 41 | def _make_request(self, conn, method, url, **kwargs): 42 | response = self._old_make_request(conn, method, url, **kwargs) 43 | sock = getattr(conn, 'sock', False) 44 | if sock: 45 | setattr(response, 'peer', sock.getpeername()) 46 | else: 47 | setattr(response, 'peer', None) 48 | return response 49 | 50 | # Осуществляем подмену 51 | HTTPConnectionPool._old_make_request = HTTPConnectionPool._make_request 52 | HTTPConnectionPool._make_request = _make_request 53 | 54 | # Наш воркер 55 | class Worker(threading.Thread): 56 | def __init__(self, thread_id, in_data, out_data, trace): 57 | threading.Thread.__init__(self), 58 | self.thread_id = thread_id 59 | self.in_data = in_data 60 | self.out_data = out_data 61 | self.timeout = 3 62 | self.total_count = len(in_data) 63 | self.trace = trace 64 | 65 | def select_unprocessed(self): 66 | with in_mutex: 67 | try: 68 | result = self.in_data.pop() 69 | except: 70 | result = None 71 | return result 72 | 73 | def report_progress(self, item): 74 | global counter 75 | counter += 1 76 | print(u"(%d of %d) [%s] %s" % (counter, self.total_count, item[2], item[1])) 77 | 78 | def process_item(self, item): 79 | global request_headers 80 | item[4] = int(time.time()) 81 | 82 | try: 83 | response = requests.get(item[1], timeout = self.timeout, stream = True, headers = request_headers) 84 | content = response.raw.read(100000, decode_content = True) 85 | 86 | if b'eais.rkn.gov.ru' in content: 87 | item[2] = 'blocked' 88 | else: 89 | peer = response.raw._original_response.peer 90 | if peer is not None: 91 | address = peer[0] 92 | if address.startswith('127') or address.startswith('192.168') or address.startswith('10.10') or address == '::1' or address is None: 93 | item[2] = 'local-ip' 94 | item[3] = '' 95 | else: 96 | item[2] = 'available' 97 | #item[3] = content 98 | item[3] = '' 99 | else: 100 | item[2] = 'available' 101 | #item[3] = content 102 | item[3] = '' 103 | except Exception as e: 104 | item[2] = 'failure' 105 | 106 | with out_mutex: 107 | if self.trace: 108 | self.report_progress(item) 109 | self.out_data.append(item) 110 | 111 | def set_timeout(self, new_timeout): 112 | self.timeout = new_timeout 113 | 114 | def run(self): 115 | while True: 116 | item = self.select_unprocessed() 117 | if item is None: 118 | break 119 | else: 120 | self.process_item(item) 121 | 122 | try: 123 | import mysql.connector as MySQLdb 124 | except: 125 | print("mysql.connector is required, other drivers are not supported") 126 | sys.exit(-1) 127 | 128 | # Импортируем конфигурацию 129 | import config 130 | 131 | # Профилирование 132 | import resource 133 | 134 | # Получение ID проверялки из базы данных 135 | def get_instance_id(api = False): 136 | timestamp = int(time.time()) 137 | instance_id = 0 138 | cursor.execute("SELECT checker_id, checker_force_scan, checker_state, checker_last_scan_time, checker_enabled, checker_threads FROM roskom_checkers WHERE REPLACE(checker_ip, '127.0.0.1', 'localhost') = (SELECT SUBSTRING_INDEX(host, ':', 1) FROM information_schema.processlist WHERE ID = connection_id()) FOR UPDATE") 139 | rows = cursor.fetchall() 140 | if len(rows) != 1: 141 | print("This checker instance is not registered within lanbill database. Register this checker first.") 142 | sys.exit(-1) 143 | else: 144 | force_scan = int(rows[0][1]) 145 | checker_state = rows[0][2] 146 | checker_last_scan_time = int(rows[0][3]) 147 | checker_enabled = rows[0][4] 148 | checker_threads = int(rows[0][5]) 149 | 150 | if checker_threads != 0: 151 | config.THREADS = checker_threads 152 | 153 | if checker_enabled != 'yes': 154 | print("Checker is turned off in the settings section") 155 | sys.exit(0) 156 | 157 | if api: 158 | if force_scan == 0: 159 | sys.exit(0) 160 | else: 161 | if force_scan == 1: 162 | print("There is an unfinished scheduled scan") 163 | sys.exit(0) 164 | 165 | if checker_state == 'scanning': 166 | # В этом месте существует вероятность, что по факту проверка не выполняется, т.е скрипт упал или что-то такое, возможно, стоит ругаться об этом в лог, если последняя проверка была слишком давно 167 | # Форсируем проверку, если есть подозрение на такую ситуацию 168 | if checker_last_scan_time > (timestamp - 3600): 169 | print("Already scanning") 170 | sys.exit(0) 171 | 172 | instance_id = int(rows[0][0]) 173 | cursor.execute("UPDATE roskom_checkers SET checker_state = 'scanning', checker_force_scan = 0, checker_last_scan_time = %s WHERE checker_id = %s", (timestamp, instance_id)) 174 | 175 | return instance_id 176 | 177 | # Соединение с БД 178 | def connect_db(): 179 | try: 180 | db = MySQLdb.connect(charset = 'utf8', use_unicode = True, **config.DATABASE) 181 | return db 182 | except Exception as e: 183 | print("MySQL connection failure\n%s" % str(e)) 184 | print("Edit /opt/roskomcheck/config.py to configure access to lanbill database") 185 | sys.exit(-1) 186 | 187 | def log_message(message, level = 'info'): 188 | timestamp = int(time.time()) 189 | cursor.execute("INSERT INTO roskom_log (log_time, log_message, log_type, log_script_name) VALUES (%s, %s, %s, 'roskomcheck.py')", (timestamp, message, level)) 190 | db.commit() 191 | 192 | trace = True 193 | api = False 194 | if len(sys.argv) >= 2: 195 | if sys.argv[1] == 'cron': 196 | trace = False 197 | 198 | if sys.argv[1] == 'api': 199 | api = True 200 | 201 | def signal_handler(signal, frame): 202 | timestamp = int(time.time()) 203 | message = "instance #%d aborted by signal" % (instance_id,) 204 | db = connect_db() 205 | cursor = db.cursor() 206 | cursor.execute("UPDATE roskom_checkers SET checker_state = 'idle', checker_force_scan = 0 WHERE checker_id = %s", (instance_id,)) 207 | cursor.execute("INSERT INTO roskom_log (log_time, log_message, log_type, log_script_name) VALUES (%s, %s, %s, 'roskomcheck.py')", (timestamp, message, 'info')) 208 | db.commit() 209 | print('Exitting requested') 210 | db.close() 211 | exit(0) 212 | 213 | signal.signal(signal.SIGINT, signal_handler) 214 | signal.signal(signal.SIGTERM, signal_handler) 215 | signal.signal(signal.SIGQUIT, signal_handler) 216 | 217 | # Установим соединение с БД 218 | db = connect_db() 219 | #db.begin() 220 | cursor = db.cursor(buffered = True) 221 | # Проверим, зарегистрирован ли наш экземпляр в админке. Если нет, нам незачем работать 222 | instance_id = get_instance_id(api) 223 | db.commit() 224 | 225 | log_message("instance #%d started using %d threads" % (instance_id, config.THREADS)) 226 | 227 | # Получим список URL-ок, доступность которых нам требуется проверить 228 | cursor.execute("SELECT url_text FROM roskom_url") 229 | in_data = [[0, i[0], 'unknown', '', 0] for i in cursor.fetchall()] 230 | out_data = [] 231 | 232 | # Можно отсоединиться от БД на время анализа 233 | cursor.close() 234 | db.close() 235 | 236 | # Инициализируем наши рабочие потоки 237 | threads = {} 238 | for i in range(config.THREADS): 239 | threads[i] = Worker(i, in_data, out_data, trace) 240 | threads[i].set_timeout(config.HTTP_TIMEOUT) 241 | threads[i].setDaemon(True) 242 | 243 | # Разветвляемся 244 | for index, thread in threads.items(): 245 | thread.start() 246 | 247 | # Соединяемся 248 | for index, thread in threads.items(): 249 | thread.join() 250 | 251 | # На этом этапе у нас сформирована статистика в массиве out_data, получим данные для внесения в БД 252 | timestamp = int(time.time()) 253 | total = len(out_data) 254 | available = [i for i in out_data if i[2] == 'available'] 255 | unavailable = [i for i in out_data if i[2] in ['blocked', 'failure', 'local-ip']] 256 | 257 | # Предварительная оценка ресурсов для записи в лог 258 | stat = resource.getrusage(resource.RUSAGE_SELF) 259 | 260 | # Время окончания работы скрипта, не считая финальную транзакцию 261 | execution_end = time.time() 262 | execution_time = execution_end - execution_start 263 | execution_minutes = int(execution_time / 60) 264 | execution_seconds = (execution_time - (execution_minutes * 60)) 265 | 266 | # Установим соединение с БД для сохранения результатов 267 | db = connect_db() 268 | cursor = db.cursor() 269 | cursor.execute("INSERT INTO roskom_checker_scans (scan_checker_id, scan_when, scan_urls_total, scan_urls_available, scan_urls_unavailable, scan_time) VALUES (%s, %s, %s, %s, %s, %s)", (instance_id, timestamp, total, len(available), len(unavailable), int(execution_time))) 270 | cursor.execute("UPDATE roskom_checkers SET checker_last_scan_time = %s, checker_last_scan_id = %s, checker_state = 'idle', checker_force_scan = 0 WHERE checker_id = %s", (timestamp, cursor.lastrowid, instance_id)) 271 | cursor.execute("DELETE FROM roskom_available_links WHERE link_checker_id = %s", (instance_id,)) 272 | 273 | for link in available: 274 | # Внесём ссылку в список доступных при последней проверке 275 | cursor.execute("INSERT INTO roskom_available_links (link_url, link_body, link_checker_id) VALUES (%s, %s, %s)", (link[1], link[3], instance_id)) 276 | 277 | # Обновим статистику в roskom_url_stat 278 | cursor.execute("UPDATE roskom_url_stat SET us_ptime = %s, us_checker_id = %s, us_pcount = us_pcount + 1 WHERE us_hash = MD5(%s)", (link[4], instance_id, link[1])) 279 | 280 | log_message("instance #%d finished, taking %d kb RES and %dm %.2fs" % (instance_id, stat.ru_maxrss, execution_minutes, execution_seconds)) 281 | db.commit() 282 | cursor.close() 283 | db.close() 284 | 285 | if trace: 286 | # Выведем статистику проверки 287 | print("\n Scan report") 288 | print(" Total: %d\n Unavailable: %d\n Available: %d\n" % (total, len(unavailable), len(available))) 289 | 290 | if config.ENABLE_LOG: 291 | with open(config.LOG, "w") as handle: 292 | handle.write("\n".join([i[1] for i in available])) 293 | handle.write("\n") 294 | --------------------------------------------------------------------------------