или
Веб морда для ваших поделок
(пет проект)
Если ваши успехи в освоении data science и других наук дошли до стадии, когда вам есть что показать, то самое время глянуть на эту статью. Эта статья совсем не про искусственный интеллект и про искусственный интеллект далее в статье больше ни слова. Эта статья описывает один из способов получить из сети картинку, обработать её и отдать обратно. Как можно дешевле, надёжней, быстрей (это конечно фантастика) Можно и с AI, можно и без AI, главное то, что есть обработчик картинок и есть что показать человечеству!
Недостатки этой статьи - простовата до невозможности, нет ни одного решения сложней плинтуса. Откровенный примитив. Всё уже описано, расписано и показано в других статьях.
Достоинства этой статьи - применены исключительно простые решения. Все конфиги на пол страницы, все библиотеки многолетней давности и никаких фреймворков. Всё ясно, понятно, обозримо и проверено годами. И работает.
Зачем эта статья. Можно скопипастить и будет работать и можно сосредоточиться на главном своём алгоритме. Можно быстро понять какую часть туториалов прочесть, что бы что то переделать. Тут всё в куче, легко найти, просто поменять. Если нужно добавить новый функционал - ясно понятно куда что и как дописывать.
Таких статей, как прикрутить web интерфейс к питону в десяток строк, полно, но дьявол то как всегда в мелочах. Если сделать вебморду в 10 строк, работать то оно конечно будет, но вам тогда нужен прямой IP и все боты мира будут в него стучать, отнимать ваше и время процессора. А если вдруг ушлый какой и пролезет! Не годится! Нужно защищать свой компьютер и, особенно, свой код, например нейронной выстраданной сети. Код, свой, ещё не опубликованный и незакопирайченый, здесь главная ценность.
Поэтому выносим морду в интернет, защищаем её и строим к ней туннель.
Покупаем/арендуем/берём_в_дар VDS, самый махонький. Я выбрал вариант KVM, чтобы спокойно и уверенно установить там FreeBSD. Их как-то в сети внешней работает много и все сетевые трюки на FreeBSD выглядят просто, достойно и элегантно. (И всё это дело можно уместить в 512, а то и в 128 Мб памяти, что немаловажно по деньгам).
Туннелей тоже много разных и хороших, мой выбор - OpenVPN. Бесплатно и просто подключить свой внутренний вычисляющий сервер на любой ОС к внешнему серверу на FreeBSD.
Вот тут первый вопрос — а где делать сервер и где клиент? Я выбрал вариант когда во внешнем мире на VDS у меня находится сервер VPN. Казалось бы должно быть наоборот, сервер внутри охраняемого периметра, а клиент вне и клиент подключается к серверу. Но, в моем случае, если потерять внешний сервер, если сервер будет скомпрометирован, то я потеряю всего лишь VDS и не потеряю доступ внутрь, к суперсекретному своему алгоритму и к машине, на которой он есть, в охраняемом периметре. Если же вне будет клиент, а сервер внутри, то при компрометации клиента или взломе его, утекут ключи и хакер получит доступ с серверу с секретным кодом. В варианте внешнего сервера нет никаких открытых и пробрасываемых портов, все наглухо закрыто для доступа извне, доступ только через VPN и только по одному порту для FastCGI.
Итак ставим на VSD с FreeBSD обычный web сервер lighttpd, обычный OpenVPN сервер и посредством FastCGI отправляем картинки через openvpn на свой рабочий компьютер для обработки.
Первый архитектурный вопрос прояснился и буду рад и готов получить замечания и предложения.
Но теперь второй вопрос. Картинку то мы получили, обработку, которую собственно и хотим показать миру, сделали и теперь картинку нужно отдать обратно в мир. Но вот вопрос: - а с какого сервера? Если из своего рабочего, то те же проблемы - прямой IP и соблазн для ботов всего мира. Если не прямой, то нужно с сервера обработки отдать картинку куда то во внешний мир безопасно и оттуда показывать. В данном варианте картинка грузится через VPN с помощью scp обратно на внешний сервер на FreeBSD и оттуда отдаётся тем же lighttpd. Картинку покажем и через 10 минут удалим, так что во внешнем мире не хранится ничего, ни картинки, ни алгоритма.
Отдав картинку, lighhtpd показывает страницу опроса с закодированными параметрами картинки - нравится преобразованная картинка или нет или загрузить новую? Ответ вместе с параметрами преобразования картинки и ее именем возвращается по FastCGI на внутренний сервер и записывается в файл. Мало ли, вдруг накопится статистика )) Сделано так, что бы у пользователя было время подумать, отвлекли, пообедать ушёл и т.д. Сессия разорвалась и что бы ответить нужна новая, вот в новом FastCGI запросе и будет новая сессия и вся инфо о картинке будет отправлена.
Ну и опять же конечно буду рад и готов получить замечания и предложения.
Можно скачать все конфиги и исходники с Github
и посмотреть реальную работу: http://107.189.8.250/
Теперь по пунктам и подробно. Ещё раз - буду рад и готов выслушать замечания, правки и критику.
FreeBSD и его конфиги
Ставим FreeBSD как обычно и немного модифицируем /etc/rc.conf
sendmail_enable="NONE" hostname="fun-house-serv.mirror" ifconfig_vtnet0="inet 107.189.8.250 netmask 255.255.255.0" defaultrouter="107.189.8.1" pf_enable="YES" pf_rules="/etc/pf.conf" pflog_enable="YES" gateway_enable="YES" sshd_enable="YES" lighttpd_enable="YES" # Set dumpdev to "AUTO" to enable crash dumps, "NO" to disable dumpdev="AUTO" openvpn_enable="YES" openvpn_configfile="/usr/local/etc/openvpn/server.conf" openvpn_dir="/usr/local/etc/openvpn"
и первым делом настраиваем файрволл
log_opt = "log" ext_if="vtnet0" int_if="tun0" icmp_types="echoreq" #block all set skip on lo set skip on int_if scrub in block in $log_opt on $ext_if pass out keep state # Protect against spoofing antispoof quick for { lo $int_if } # Allow other traffic pass in on $ext_if proto tcp to ($ext_if) port ssh flags S/SA keep state pass in on $ext_if proto tcp to ($ext_if) port http flags S/SA keep state pass in on $ext_if proto udp to any port 1194 pass in inet proto icmp from ($int_if:network) icmp-type $icmp_types keep state pass in inet proto icmp from ($ext_if:network) icmp-type $icmp_types keep state
пропускаем извне только ssh, http и vpn (1194 порт). Разрешаем доступ с tun0, это и есть туннель к нашему внутреннему серверу. Форвардинг нам не нужен совсем.
Создаём юзера под которым будет подключаться клиент и загружать картинки.
Заменяем доступ по ssh на беспарольный доступ по ключу, чтобы scp работал без пароля. Доступ нужен и с рабочего компьютера для настройки и с вычислителя для передачи картинок. Ну и что бы не было каркозябров в названиях файлов, русифицируем FreeBSD, добавляем в /boot/loader.conf:
hw.vga.textmode=1
Далее ставим lighttpd и openvpn любым привычным способом. Добавляем нашего юзера в группу www. Он должен иметь права на добавление картинок в папки lighttpd. Добавляем юзера под которым будет работать openvpn, лучше если это будет не root. По умолчанию конфиги lighttpd не соответствуют создаваемым во FreeBSD по умолчанию папкам. Правим /usr/local/etc/lighttpd/lighttpd.conf
var.log_root = "/var/log/lighttpd" var.server_root = "/usr/local/www/lighttpd" var.state_dir = "/var/run" var.home_dir = "/var/run/lighttpd" var.conf_dir = "/usr/local/etc/lighttpd"
Эта сложная конструкция в lighttpd.conf всего лишь проверяет URL и если что то не нравится, то отправляет на начальную страницу сайта.
$HTTP["host"] =~ "107.189.8.250/*" { url.rewrite-once = ( "^(www\.)?\/upload\.html$" => "", "^(www\.)?\/main\.fcgi$" => "", "^(www\.)?(.*)?\.fcgi\?$" => "", "^(www\.)?\/images\/(.*)?$" => "", "^/(.*)?" => "/upload.html" ) }
Правим там же modules.conf
server.modules = ( "mod_rewrite", "mod_access", # "mod_evasive", # "mod_auth", # "mod_authn_file", # "mod_redirect", # "mod_setenv", # "mod_alias", )
и раскомментируем там же строчку для вызова fastcgi
## ## FastCGI (mod_fastcgi) ## include "conf.d/fastcgi.conf"
Теперь правим fastcgi.conf, дописываем
server.modules += ( "mod_fastcgi" ) fastcgi.server = ( ".fcgi" => ( "main" => ( # Use host / port instead of socket for TCP fastcgi "host" => "192.168.0.66", "port" => 8888, "check-local" => "disable", ) ) )
"192.168.0.66" это адрес внутреннего сервера за периметром. Именно на нем и будут производиться вычисления картинок. Этот адрес может и не принадлежать внутренней сети совсем и к этому серверу вы можете сделать доступ по другому, второму интерфейсу, а этот "192.168.0.66" закрыть для любого обращения, кроме как через VPN через tun0. Ну и порт 8888 выбран не совсем удачно, лучше выбрать другой, если использовать jupyter notebook. Но в данном случае все вычисления на С и порт можно взять любой больше 1024.
Создаём папки для хранения страницы сайта и картинок.
mkdir /usr/local/www/data/ mkdir /usr/local/www/data/images chown <user>:www /usr/local/www/data/images/ chmod 710 /usr/local/www/data/images/
Напомню, что наш <user> должен быть членом группы www и должен иметь права на запись в папку /usr/local/www/data/images и у группы www должны быть права доступа к этой папке.
Далее настраиваем OpenVPN, (вот тут гораздо подробней), создаём папки для openvpn
mkdir /etc/openvpn mkdir /etc/openvpn/keys mkdir /var/log/openvpn pw useradd -n _openvpn -s /usr/bin/nologin
_openvpn - это тот самый юзер, под которым и будет запускаться собственно openvpn и никакой login/shell/home_dir ему не нужен. Можно использовать пользователя nobody.
теперь правим /usr/local/etc/openvpn/server.conf
proto udp port 1194 dev tun0 topology subnet ca /usr/local/etc/openvpn/keys/ca.crt cert /usr/local/etc/openvpn/keys/server.crt key /usr/local/etc/openvpn/keys/server.key dh /usr/local/etc/openvpn/keys/dh2048.pem server 10.0.1.0 255.255.255.0 route 192.168.0.0 255.255.255.0 push "redirect-gateway def1" keepalive 10 120 comp-lzo user _openvpn group _openvpn daemon openvpn persist-key persist-tun tls-server tls-auth /usr/local/etc/openvpn/keys/ta.key 0 verb 3 status /var/log/openvpn/openvpn-status.log log-append /var/log/openvpn/openvpn.log client-config-dir ccd
Теперь создаём ключи и кладем в указанные папки и не забываем правильно установить права
drw------- 2 root wheel 512 24 сент. 20:25 keys -rw-r--r-- 1 root wheel 749 18 окт. 10:17 server.conf
Когда создаём ключи, то нужно обязательно указать правильно CN, оно должно совпадать с именем юзера и именем файла в папке /usr/local/etc/openvpn/ccd/
Точно именно так - все три имени должны совпадать. OpenVPN берет имя юзера из ключей, его настройки из файла /ccd/<user> и вы в системе логинитесь под этим именем при подключении через OpenVPN и получа��те соответствующие права доступа. Всё должно совпадать.
В файле /usr/local/etc/openvpn/ccd/<user> пишем настройки сети
iroute 192.168.0.0 255.255.255.0
Эта запись означает, что сеть 192.168.0.0 находится за VPN, туда нужно и можно через tun0 слать пакеты.
Теперь создаём страницу нашего сайта, которая делает только одно - принимает картинку от посетителя и кладем в файл upload.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="google-site-verification" content="VC7sVZeZ-E94q8E9-W-2DzZcuoLwGt9FWaBzN0n2c9c" /> <title>"send the picture to the house of mirrors"</title> <h2>выберите картинку</h2> <h2>если нажмете SUBMIT</h2> <h1>она будет искажена, может будет смешно</h1> </head> <body> <form enctype="multipart/form-data" action="/main.fcgi" method="post"> <p><input type="file" multiple accept="image/*,image/jpeg" name="f"> <input type="submit" value="SUBMIT"></p> </form> </body> </html>
Можно выкинуть 5 строчку, она для того, что бы google находил и индексировал страницу и годится только для этого конкретного сайта http://107.189.8.250. Подправьте заголовки, если нужно. Если отправляете не только картинки, то правьте accept="image...". Не смог сделать так, что бы браузер выбирал только одну картинку. Буду рад, если кто подскажет. Сейчас можно выбрать несколько картинок и наш обработчик FastCGI обработает только первую. Но это нарушение архитектуры - если обрабатывает одну картинку, то и выбираться должна только одна. Но не смог ...
Теперь перегружаем FreeBSD и смотрим, что же мы там наделали, запустились ли lighttpd, openvpn? Открывается ли страница, можно ли выбрать файл? Можно и нужно проверить порты, всё таки сервер в прямом доступе кулхацкеров и ботов всего мира.
Ну и последний штрих это crontab -e и не забудьте поправить <user> на конкретное свое имя, что указывали ранее. Те картинки, что уже показали, нет нужды хранить на внешнем сервере долго. Каждые 10 минут файлы старше 10 минут будут в папке картинок удалены. Т.е. картинка на внешнем сервере храниться не более 20 минут.
*/10 * * * * /usr/bin/find /usr/local/www/data/images/ -user <user> -type f -mmin +10 -deletelete
Если что то не работает, или работает не так как задумывалось - проверяем IP, в статье указан конкретный рабочий действующий адрес. У вас он может быть и должен быть другим. Проверяем права <user>, проверяем папки. Если опять что не так - пишите мне, может я что и пропустил, гляну в действующем примере.
Настройка Ubuntu
Тут настраивать мало совсем и кратко.
Нужно создать специальную таблицу маршрутизации для VPN в файле /etc/iproute2/rt_tables
# # reserved values # 255 local 254 main 253 default 200 vpn 0 unspec # # local # #1 inr.ruhep
Эта новая таблица 200 vpn нужна для того, что бы ответ на пакеты пришедшие с vpn через tun0 отправлялись обратно также через tun0. Просто в любимом редакторе правим файл и добавляем строку.
Также рутинная процедура создания ключей (укажите правильное имя <user>) с помощью ssh-keygen -t rsaи также рутинно и просто отправим их на наш FreeBSD ssh-copy-id <user>@10.0.1.1. Где 10.0.1.1 это адрес вашего сервера при доступе через VPN на tun0.
Устанавливаем рутинно OpenVPN и создаём два файла в папке /etc/openvpn/client/
это файл конфигурации клиента client_<user>.conf
client remote 107.189.8.250 1194 udp proto udp dev tun0 dev-type tun keepalive 10 120 nobind persist-key persist-tun ns-cert-type server comp-lzo verb 3 script-security 2 route 10.0.1.0 255.255.255.0 10.0.1.5 --route-noexec ca ca.crt cert <user>.crt key <user>.key remote-cert-tls server # If a tls-auth key is used on the server # then every client must also have the key. tls-auth ta.key 1 up "/etc/openvpn/client/special_vpn" key-direction 1 verb 3 status /var/log/openvpn/openvpn-status.log log-append /var/log/openvpn/openvpn.log
и в то же место кладем ключи. Напомню, что <user> должен быть одинаковым везде, там на FreeBSD в трех местах и тут. Именно этот <user> подключается к FreeBSD и именно у него права и возможность положить обработанную картинку на её место.
В той же папке лежит файл /etc/openvpn/client/special_vpn
#!/bin/bash /usr/sbin/ip rule add from 10.0.1.1 table vpn /usr/sbin/ip route add default via 10.0.1.5 dev tun0 table vpn
Название произвольное, но смысл его в правильной настройке маршрутизации. OpenVPN адрес 10.0.1.1 присваивает серверу VPN в сети 10.0.1.0, что мы указали на FreeBSD в файле server.conf. Поскольку вычислитель картинок никуда больше ничего не отдает и не берет, то можно отправлять на адрес 10.0.1.1 и через VPN картинка дойдет до места.
Больше чем уверен, что в этих конфигах можно найти кучу усовершенствований, лишнего и неправильного. Уверен, что с помощью OpenVPN-AS можно сделать проще и быстрее и надёжней. И у меня эта конструкция не сразу заработала, хотя почти точно такая, но для просто VPN, работает рядом много лет.
Если загружать исходники в эту машину и компилировать, то нужно загрузить и все библиотеки и среду. Но можно подготовить бинарник на другой машине и просто запустить два сервиса - OpenVPN и это бинарник. Подготовка бинарника не предмет этой статьи, просто не забудьте ключи для gcc -lopencv_imgcodecs -lopencv_imgproc -lopencv_core -lpthread -lfcgi -lssl -lcrypto
И совсем штрих, (fun_house в данном случае имя юзера под которым картинка обрабатывается) прописываем crontab -e
@reboot /home/fun_house/funhouse.sh
И в файле /home/fun_house/funhouse.sh
#!/bin/sh cd /home/fun_house ./funhouse
funhouse - имя программы, которая собственно создана нами и которая и принимает FastCGI запросы, принимает, обрабатывает картинки и отправляет их обратно. Такой crontab будет при перезагрузке запускать программу обработки.
Обработка
Программа обработки состоит из трех логических частей, но в этом случае все они уложены в один длинный код. По этому поводу есть много мнений и почти все они говорят, что так плохо. Но так наглядно и, главное, последовательно видно что и когда, что за кем и как выполняется в деталях. И в нашем случае наглядность всего алгоритма работы сервиса и детализация рассмотрения перевесили другие аргументы. Переделать для себя всё - пара минут.
Теперь перейдем к описанию алгоритма работы внутреннего сервера.
Главная программа сервиса обработки внутреннего сервера запускает THREAD_COUNT потоков и ждет. Внешний сервер показал посетителю страницу, тот согласился, выбрал картинку и нажал "Отправить". Lighttpd, согласно своим конфигам, отправляет FastCGI запрос на адрес 192.168.0.66 который через OpenVPN будет доставлен на 8888 порт внутреннего сервера. Вся обработка происходит параллельно в запущенных потоках. Каждый поток ждет когда придет его очередь обрабатывать FastCGI запрос от внешнего сервера.
Как только будет открыто соединение со стороны внешнего сервера, программа принимает по частям в размере буфера данные, парсит эти данные, скачивает по необходимости файл со служебной информацией, сохраняет этот файл картинки или скачивает картинку с диска, далее картинку обрабатывает, сохраняет, отправляет на внешний сервер, формирует HTML страницу ответа и отправляет её на внешний сервер. После закрывает соединение, сохраняет журнал в файл журнала и данные в файл результатов.
И так в цикле постоянно.
Директория с программой должна содержать поддиректории data/marks, data/imges и log
Теперь собственно код и комментарии. Очень красиво и подробно про FastCGI в потоках изложено в статье habr.com/ru/post/154187/ и вся идея и почти весь код про fastcgi взят оттуда. Особое спасибо@janagan
Здесь по катом весь source код на почти 1000 строк одним файлом.Наверно это неправильно.
Можно скачать все конфиги и исходники с Github.
Под катом громадный в 1000 строк исходник с комментариями. Комментарии наверняка можно и нужно дополнить.
source funhouse_mirror
//============================================================================ // Name : fun-house_mirror.cpp // Author : Peter Che // Version : // Copyright : Peter Che 7210208@gmail.com // Description : Pet Project in C //============================================================================ /* * программа должна делать следующее * 1. Запускается несколько потоков для обработки * 2. Каждый поток ждет соединения и получает FastCGI данные с помощью библиотеки * 3. Каждый поток парсит заголовки * и либо получает и сохраняет файл из потока FastCGI, * либо забирает сохраненный файл картинки с диска. * 4. Получив файл-картинку поток вызывает для обработки * OpenCV. * 5. Считывает картинку себе в память. Если большая, то resize * 6. если нажата одна из кнопок "нравится"/"отказать", то получает * из запроса старые значения обработки и сохраняет ответ в файл. * 7. Случайным образом (тоже вариант ) генерит новые параметры * 8. поток расчитывает параметры remap (три варианта) * 9. поочередно применяет все три remap и получает выходную картинку * 10. выходную картинку сохраняет на диск и с помощью scp отправляет на сервер 10.0.1.1 (FreeBSD) * 11. создает соответствующую HTML страницу и отправляет её * на внешний сервер * 12. Записывает в журнал параметры обработки, имя файла, время обработки. */ #include <errno.h> #include <unistd.h> #include <pthread.h> #include <sys/types.h> #include <stdio.h> #include <string.h> #include "math.h" #include <fcntl.h> #include <time.h> /* time_t, struct tm, time, localtime */ #include "fcgi_config.h" #include "fcgiapp.h" #include "opencv2/core/utility.hpp" #include "opencv2/imgproc.hpp" #include "opencv2/imgcodecs.hpp" #include "opencv2/core/mat.hpp" #include <openssl/sha.h> using namespace cv; using namespace std; // определяем длину разных буферов // для имени файла, для имени и пути, // для одной записи в журнал. Т.е. вся инфо для журнала заносится в переменную // и только после выполнения всех операций заносится в журнал // // debug и log это разные операции // отладку нужно вкоде испоотзовать с инструкциями препроцессора // #define BUFLEN_F NAME_MAX + 1024 #define BUFLEN_TXT PATH_MAX + NAME_MAX #define LOG_TXT_LEN PATH_MAX + NAME_MAX + 1024 #define THREAD_COUNT 4 // количество потоков. // адрес внешнего сервера. #define FRONT_ADDR "107.189.8.250" // внутренний адрес // лучше конечно вынести их в параметры для этой программы // будет лучше и наглядней #define SOCKET_PATH "192.168.0.66:8888" // условные названия ошибок // лучше избегать констант типа ошибка номер "9" в коде // лучше дефайнить // #define ERROR_OPEN_TEMP_FILE 1 //Error open temporary data file #define ERROR_ACCEPT_NEW_REQUEST 2 //Error accept new request #define ERROR_IMAGE_ZERO_DIMENSION 3 #define ERROR_FILE_NAME 4 #define REQUEST_FORMAT_ERROR_0 5 // REQUEST_FORMAT_ERROR #define REQUEST_CONTENTLENGHT_ERROR 6 // #define REQUEST_GETSTR_ERROR 7 // #define REQUEST_BOUNDARY_ERROR 8 // #define REQUEST_FILENAME_NOTFOUND 9 // #define REQUEST_FILENAME_ERROR 10 // #define REQUEST_FORMAT_ERROR_6 11 // #define REQUEST_CONTENTTYPE_ERROR 12 // #define REQUEST_FILETYPE_ERROR 13 // #define REQUEST_FORMAT_ERROR_RD 14 // #define REQUEST_SERVER_NAME_ERROR 15 // SERVER_NAME_FORMAT_ERROR #define ERROR_RENAME_TMPFILE 101 #define READ_FILE_EXEPTION 102 //read file exception #define WRITE_FILE_EXEPTION 103 #define ERROR_TRANFER_RES_FILE 104 #define ERROR_FASTCGI_SEND 105 #define ERROR_ 105 #define SALT_LEN 3 // количество параметров обработки изображений // в этои варианте все 12 не используются // #define PARAMETERS_NUM 12 #define MAX_IMAGE_ROWS 512 #define MAX_IMAGE_COLS 512 #define MIN_IMAGE_ROWS 16 #define MIN_IMAGE_COLS 16 // условные названия параметров // т.е. в массиве param под номером ANGLE_0 будет 0 // так сделано для наглядности // если поменять количество параметров, // что то же что и размер массива param, то // формирование HTML страницы не изменится, поменяется только обработка #define ANGLE_0 0 #define ANGLE_1 1 #define ANGLE_2 2 #define PERIOD_0 3 #define PERIOD_1 4 #define PERIOD_2 5 #define AMPLITUDE_0 6 #define AMPLITUDE_1 7 #define AMPLITUDE_2 8 #define SIGN_0 9 #define SIGN_1 10 #define SIGN_2 11 //хранит дескриптор открытого сокета static int socketId; // наличие нескольких потоков требует всегда внимания // и мьютексы позволят избежать одновременного использования // общих данных в разных потоках static pthread_mutex_t accept_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER; FILE *log_file; FILE *marks_file; // вспомогательный код //возвращает или 1 или 0 или -1 // с равной вероятностью // для выбора параметров обработки // если всё по нулям, то вернется исходная картинка float rand_101(void) { float ret = float(rand() % 6); if (ret < 2.) return (0.); else if (ret < 4.) return (-1.); return (1.); } // кодирование строки. Следующая программа делает обратное преобразование // кодировка base64 не годится для пребразования в HTML код и обратно // а base58, как часть библитеки биткойн, не смог запустить (( void char2hex(char *hex, char *src, size_t src_len) { size_t i; uchar t; for (i = 0; i < src_len; i++) { t = (uchar) src[i] & 0xff; hex[i * 2] = (uchar) ((uchar) ((t & 0x0f)) + 65); hex[2 * i + 1] = (uchar) ((uchar) (((t >> 4) & 0x0f)) + 65); } } void hex2char(const char *hex, char *src, size_t src_len) { size_t i; for (i = 0; i < src_len; i++) src[i] = (uchar) ((uchar) ((hex[i * 2 + 1] - 65) << 4) + (uchar) (hex[i * 2] - 65)); } static void* do_thread(void *thread_num) { // переменная для хранения кода ошибки // коды ошибок в дефайнах int error_idx; // переменная для хранения кода возврата разных функций int ret; // переменная для хранения номера потока int thread_idx; // FCGX_Request request; // тут хранится название рабочего файла в который будет приниматься картинка char tmp_file_name[22] = "data/tmp/ fcgi.bin"; // буфер для приема данных из сокета fastcgi char buffer[BUFLEN_F + 1]; // переменные для разбора вложения в fastcgi запрос char *buf_start; char *buf_end; char *buf_current; // current point in buffer // переменные из запроса fastcgi char *server_name; char *boundary; char *content_type; char *request_method; char *script_name; char *ch_content_len; // размер содержимого lighttpd присылает в символьном виде // и нужно перекодировать в int int content_len; int file_len; // имя файла из запроса char file_name[NAME_MAX]; // путь к файлу картинки char file_path[PATH_MAX + NAME_MAX]; char file_type[NAME_MAX]; char out_file_path[PATH_MAX]; // буфер для формирования команды scp char exec_txt[BUFLEN_TXT]; // буфер для формирования страниц HTML char html_txt[BUFLEN_TXT]; // буфер для формирования ответа в виде fcgi char fcgi_txt[BUFLEN_TXT]; // буфер для преобразования кодировок char hex_txt[BUFLEN_TXT]; // буфер для формирования записи в журнал // запись формируется и в конце обработки запроса отправляется в журнал // для отладки не годится // если вдруг станете переделывать, что везде где формируется журнал, // сразу пишите в stdout. // если будет ошибка и программа вылетит, то в журнале о последнем запросе ничего не будет char log_txt[LOG_TXT_LEN]; // буфер для формирования хэш unsigned char md_buf[SHA256_DIGEST_LENGTH]; // служебные переменные char *t_pointer; int i, j; float scale; // служебные переменные для преобразования картинок float row, col, row2, col2, row1, col1, f_i, f_j, angle_i, angle_j; // в img_origin может поступить большая картинка и её возможно придется // ресайзить.В img_in находится картинка для дальнейшей обработки Mat img_in, img_origin; int img_width; // массив параметров преобразования картиники // этот массив отправляется в HTML запросе и в ответе кодируется // вместе с именем файла+соль. // это доп функциона��ь, для учета ответов на показ обработанных картинок float param[PARAMETERS_NUM]; unsigned char salt; float tt; // переменная для промежуточных вычислений char *first_point; int batchRW; int done; int tmp_fd; // http://all-ht.ru/inf/prog/c/func/localtime_r.html //Переменная для сохранения текущего системного времени long int s_time; //Структура, в который будет помещен результат преобразования времени struct tm m_time; //Буфер, в который будет записана текстовая строка времени char time_buf[26]; thread_idx = *((int*) thread_num); // инициализируем запрос к сокету if (FCGX_InitRequest(&request, socketId, 0) != 0) { //ошибка при инициализации структуры запроса fprintf(log_file, " thread %d. Can not initializing request\n", thread_idx); return NULL; } // сокет открыт и ждем поступления данных for (;;) { // начальная инициализация переменных // все буферные переменные обнуляются error_idx = 0; bzero(log_txt, LOG_TXT_LEN); bzero(file_name, NAME_MAX); bzero(file_path, PATH_MAX + NAME_MAX); bzero(file_type, NAME_MAX); bzero(out_file_path, PATH_MAX); bzero(exec_txt, BUFLEN_TXT); bzero(html_txt, BUFLEN_TXT); bzero(fcgi_txt, BUFLEN_TXT); bzero(hex_txt, BUFLEN_TXT); bzero(log_txt, LOG_TXT_LEN); bzero(time_buf, 26); do { // фиксирует время начала и записываем в log_txt который // в конце цикла будет отправлен в файл журнала s_time = time(NULL); localtime_r(&s_time, &m_time); sprintf(log_txt + strlen(log_txt), " thread %d. time %s", thread_idx, asctime_r(&m_time, time_buf)); sprintf(tmp_file_name, "%s%03d%s", "data/tmp/", thread_idx, "fcgi.bin"); // открываем временный файл в имени которого номер потока // многопоточность это очень важно и это нужно всегда учитывать // хоть и не всегда есть уверенность в правильности // // временный файл открываем до начала открытия сокета // так задумано, что бы вынести дорогую операцию создания файла вне цикла обработки tmp_fd = open(tmp_file_name, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (tmp_fd < 3) { sprintf(log_txt + strlen(log_txt), "\t%s %s.\n", "Error open temporary data file", tmp_file_name); // если не смогли открыть рабочий файл, значит система больна // и нет возможности принимать данные через сокет // и нет смысла ждать соединение error_idx = ERROR_OPEN_TEMP_FILE; continue; } // заполняем переменную журнала информацией sprintf(log_txt + strlen(log_txt), "\tdata file %s opened\n\tTry to accept new request\n", tmp_file_name); // попробуем получить FastCGI запрос // на обработку. // Другие потоки ждут освобождения // как только этот поток получит соединение // то следующий по очереди поток становится в ожидание соединения pthread_mutex_lock(&accept_mutex); ret = FCGX_Accept_r(&request); pthread_mutex_unlock(&accept_mutex); if (ret < 0) { // ошибка при получении запроса // продолжаем цикл fprintf(log_file, " thread %d. Error accept new request\n", thread_idx); fflush(log_file); error_idx = ERROR_ACCEPT_NEW_REQUEST; continue; } // получили через сокет запрос FastCGI // сохраняем параметры запроса и определяем // содержит ли запрос файл или это только ответ на вопрос server_name = FCGX_GetParam("SERVER_NAME", request.envp); content_type = FCGX_GetParam("CONTENT_TYPE", request.envp); ch_content_len = FCGX_GetParam("CONTENT_LENGTH", request.envp); content_len = atoi(ch_content_len); request_method = FCGX_GetParam("REQUEST_METHOD", request.envp); script_name = FCGX_GetParam("SCRIPT_NAME", request.envp); // проверяем, пришел ли FastCGI запрос с нашего внешнего сервера? // или это чья то шутка if (strncmp(server_name, FRONT_ADDR, strlen(FRONT_ADDR))) { error_idx = REQUEST_SERVER_NAME_ERROR; continue; } // поля должны быть заполнены или это ошибка if (!(server_name and request_method and script_name)) { error_idx = REQUEST_FORMAT_ERROR_0; continue; } // далее обработка запроса содержащего вложение // т.е. запрос содержит не нулевой длины вложение // и его нужно распарсить и извлечь файл if (content_len != 0) { // определяем разделитель. Это достаточно случайный текст, который служит разделителем // в теле запроса boundary = strcasestr(content_type, "boundary=") + strlen("boundary="); if ((content_len <= 100) or (content_len >= 10000000)) { // если содержимое мало или велико, то ничего не делаем и начинаем новый цикл // размер выбран произвольно и вот таких констант в цифровом в коде // быть не должно и если будете адаптировать себе, // то обязательно поменяйте error_idx = REQUEST_CONTENTLENGHT_ERROR; continue; } // считываем с сокета первый буфер и сохраняем длину считаного в batchRW batchRW = FCGX_GetStr(buffer, BUFLEN_F, request.in); if (batchRW <= 0) { error_idx = REQUEST_GETSTR_ERROR; break; } // ищем boundary в запросе. Вся инфо после него if ((buf_start = strcasestr(buffer, boundary)) == NULL) { error_idx = REQUEST_BOUNDARY_ERROR; break; } buf_start += strlen(boundary); // ищем слово "filename=" в запросе if ((buf_current = strcasestr(buf_start, "filename=")) == NULL) { error_idx = REQUEST_FILENAME_NOTFOUND; break; } buf_current += strlen("filename="); // теперь buf_current указывает на первый байт после "filename=" // и надеемся, что это имя файла if (*(buf_current++) != '"') // название файла в кавычках. Это открывающая { error_idx = REQUEST_FILENAME_ERROR; break; } // поиск закрывающей кавычки до конца buffer // то, что в кавычках после "filename=" и есть имя файла во вложении buf_end = (char*) memchr(buf_current, '"', batchRW - 2 * strlen(boundary)); bzero(file_name, NAME_MAX); // проверяем имя файла на длину // т.к. далее к имени файла будем приписывать 3 байта номер потока и соль, // то файлы с очень длинным именем не обрабатываем // можно сохранить имя файла в таблицу и там же сопоставить имя файла внутри // и хранить под этим имененм // но это еще много кода и новая сущность if (buf_end - buf_current >= (int) (NAME_MAX - SALT_LEN - 3) or buf_end - buf_current <= 4) { error_idx = REQUEST_FILENAME_ERROR; break; } // вот тут мы выделили из запроса имя файла t_pointer = file_name; // к имени файла приписывам соль, что бы имена не повторялись. // если поступят на обработку в одном и том же потоке два файла с одинаковым именем, // то отличаться они будут на соль // если совпадут номер потока, имя файла и соль - это катастрофа всего миропорядка for (i = 0; i < SALT_LEN; i++) { salt = rand() % 256; char2hex(t_pointer, (char*) &salt, sizeof(salt)); t_pointer += 2 * sizeof(salt); } memcpy(t_pointer, buf_current, buf_end - buf_current); // выделяем из запроса тип вложения if ((buf_current = strcasestr(buf_start, "Content-Type: ")) == NULL) { error_idx = REQUEST_CONTENTTYPE_ERROR; break; } buf_current += strlen("Content-Type: "); // поиск закрывающего 0d 0a до конца buffer buf_end = (char*) memchr(buf_current, 0x0d, batchRW); // имя файла и служебная информация заведомо поместятся в буфер // ну или должны поместиться. Так выбирали размер буфера для чтения bzero(file_type, NAME_MAX); if (buf_end - buf_current >= NAME_MAX or buf_end - buf_current == 0) { error_idx = REQUEST_FILETYPE_ERROR; break; } memcpy(file_type, buf_current, buf_end - buf_current); if (strncmp(buf_end, "\r\n\r\n", 4) != 0) { error_idx = REQUEST_FORMAT_ERROR_RD; break; } buf_start = buf_end + 4; // 2 is fin boundary '--' 6 if first 2d2d2d2d2d2d // размер вложения включает длину файла и длину служебной информации // размер вложения без служебной информации и есть длина файла. file_len = content_len - (buf_start - buffer) - strlen(boundary) - 2 - 6; done = file_len; // формат заголовка и его парсинг вызывают сомнения. // но библитеку такую не нашел, что бы сразу распарсить и файлы сохранить // почилось кривовато, работает, но наверно есть лучше решение // всё, что осталось в буфере это часть файла // который записываем в служебный, заранее открытый файл batchRW = batchRW - (buf_start - buffer); // и по частям получаем из сокета данные и пишем в файл // длину получили заранее // если браузер прислал два и более файлов, то принимается только один. // если запись файла невозможна,значит нет места или мир рухнул // можно выйти по continue и начать цикл обработки заново и ждать // пока не появится место if (file_len <= batchRW) { ret = write(tmp_fd, buf_start, file_len); done = 0; } else { ret = write(tmp_fd, buf_start, batchRW); done -= batchRW; } if (ret < 0) { error_idx = WRITE_FILE_EXEPTION; break; } while (done > 0) { // вот тут следующий кусок файла получаем batchRW = FCGX_GetStr(buffer, BUFLEN_F, request.in); if (batchRW <= 0) break; if (done <= batchRW) { ret = write(tmp_fd, buffer, done); done = 0; break; } else { ret = write(tmp_fd, buffer, batchRW); done -= batchRW; } if (ret < 0) { error_idx = WRITE_FILE_EXEPTION; break; } } if (ret < 0) { error_idx = WRITE_FILE_EXEPTION; break; } // вот тут файл получили, сохранили в папку data/images/ под рабочим названием // и можно его скормить напрмер OpenCV close(tmp_fd); bzero(file_path, PATH_MAX + NAME_MAX); errno = 0; if (strlen(file_name) > 0) { memcpy(file_path, "data/images/", 12); memmove(file_path + 12, file_name, strlen(file_name)); if ((ret = rename(tmp_file_name, file_path)) < 0) { // переименовываем файл. Рабочее название свободно и файл теперь записан под тем именем // что пришел + соль + номер потока error_idx = ERROR_RENAME_TMPFILE; break; } } } else { // если в fastCGI запросе нет вложений. Значит это ответ на вопрос. // в ответе содержится имя файла, значения параметров и ответ на вопрос // Всё можно извлечь и сохранить в файл marks // и передать имя файла (а он в исходном виде сохранен) на новую обработку // тут можно еще проверок добавить разных и нужных bzero(fcgi_txt, BUFLEN_TXT); first_point = strchr(script_name, '.'); hex2char((char*) (script_name + 1), fcgi_txt, (size_t) (first_point - script_name - 1) / 2); bzero(file_name, NAME_MAX); strcpy(file_name, fcgi_txt + SHA256_DIGEST_LENGTH + PARAMETERS_NUM * sizeof(param[ANGLE_1]) + SALT_LEN * sizeof(salt)); bzero(file_path, PATH_MAX + NAME_MAX); memcpy(file_path, "data/images/", 12); memmove(file_path + 12, file_name, strlen(file_name)); memcpy(param, fcgi_txt + SHA256_DIGEST_LENGTH, PARAMETERS_NUM * sizeof(param[ANGLE_1])); // извлекаем ответ из запроса if (strncmp(first_point + 1, "yes", 3) == 0) fprintf(marks_file, "%s", "yes"); else fprintf(marks_file, "%s", "no"); // сохраняем параметры fprintf(marks_file, "\t%s", file_name); for (i = 0; i < PARAMETERS_NUM; i++) { sprintf(log_txt + strlen(log_txt), "\treceived parameter_%0d=%f\n", i, param[i]); fprintf(marks_file, "\t%f", param[i]); } fprintf(marks_file, "\n"); fflush(marks_file); } } while (0); // записываем в переменную параметры запроса sprintf(log_txt + strlen(log_txt), "\tserver name - %s \n", server_name); sprintf(log_txt + strlen(log_txt), "\tcontent_type %s \n", content_type); sprintf(log_txt + strlen(log_txt), "\tcontent length %d \n", content_len); sprintf(log_txt + strlen(log_txt), "\trequest_method %s \n", request_method); sprintf(log_txt + strlen(log_txt), "\tscript name %s \n", script_name); sprintf(log_txt + strlen(log_txt), "\tContent-Type: %s \n", file_type); sprintf(log_txt + strlen(log_txt), "\tfile_name %s \n", file_name); // если не было ошибок и всё в порядке // то тут есть файл - или только полученный или сохраненный ранее while (error_idx == 0 and strlen(file_name) > 0) { // ... // запускаем обработку OpenCV // если вдруг захотите применить свою обработку. Наверно захотите. // то вот в это место и нужно вставлять свой код // в filename строка с именем скачанного файла // // фиксируем время начала обработки s_time = time(NULL); localtime_r(&s_time, &m_time); sprintf(log_txt + strlen(log_txt), "\topencv start time %s", asctime_r(&m_time, time_buf)); // считываем файл в пямять // можно, конечно для скорости, вновь полученный файл целиком сохранять в памяти // и тут определить тип и использовать библиотеки без OpenCV // или подать на вход imread файл из памяти // а файл сохранить после отправки ответа за запрос. // можно try { img_origin = imread(file_path, IMREAD_COLOR); // исключение OpenCV нужно обрабатывать // файл могут прислать бажный } catch (cv::Exception &e) { const char *err_msg = e.what(); fprintf(log_file, " thread %d. read file exception caught: %s \n ", thread_idx, err_msg); fflush(log_file); error_idx = READ_FILE_EXEPTION; continue; } // маленькие картинки не обрабатываем // к сожалению OpenCV файлы формата .heif // определяет как файлы с нулевой размерностью // поэтому размер файла и размер картинки это разные вещи if (img_origin.rows < MIN_IMAGE_ROWS or img_origin.cols <= MIN_IMAGE_COLS) { error_idx = ERROR_IMAGE_ZERO_DIMENSION; // .heif not supported https://github.com/opencv/opencv/issues/14534 continue; } // большие картинки сжимаем // можно добавить проверку MAX_IMAGE_ROWS и MAX_IMAGE_COLS // есди один из них равен 0, то ничего не делаем if (img_origin.rows >= MAX_IMAGE_ROWS or img_origin.cols >= MAX_IMAGE_COLS) { scale = MIN(float(img_origin.rows)/MAX_IMAGE_ROWS, float(img_origin.cols)/MAX_IMAGE_COLS); resize(img_origin, img_in, Size(int(float(img_origin.cols) / scale), int(float(img_origin.rows) / scale)), INTER_LINEAR); } else img_in = img_origin; // тут делаем несколько предварительных вычислений // эта часть исключительно простого преобразования // и тут как бы оптимизация вычислений // но компилятор скорее всего и сам много чего оптимизирует // и вынесет из циклов // и об этом нужно помнить всегда col1 = float(img_in.cols - 1); row1 = float(img_in.rows - 1); col2 = col1 / 2; row2 = row1 / 2; // это С++ часть. OpenCV делает теперь только так // можно самому выделять очищать память, // может и переделаю попозже Mat img_out(img_in.size(), img_in.type()); // поскольку всё преобразование делается через remap // то тут хранятся вычисляемые параметры Mat map_r(img_in.size(), CV_32FC1); Mat map_c(img_in.size(), CV_32FC1); Mat map_r_s(img_in.size(), CV_32FC1); Mat map_c_s(img_in.size(), CV_32FC1); Mat map_r_ss(img_in.size(), CV_32FC1); Mat map_c_ss(img_in.size(), CV_32FC1); Mat map_r_sss(img_in.size(), CV_32FC1); Mat map_c_sss(img_in.size(), CV_32FC1); // в массиве param хранятся параметры придуманного автором искажения картинок // где то угол поворота, где то сжатие или растяжение по // горизонтали или вертикали // param[PERIOD_0] = 0.5 + 1.5 * (float) rand() / (float) (RAND_MAX); param[AMPLITUDE_0] = (0.125 + 0.125 * (float((rand() % 256) - 128) / 255.)); param[SIGN_0] = rand_101(); param[ANGLE_0] = 0.125 * M_PI; param[PERIOD_1] = 0.5 + 1.5 * (float) rand() / (float) (RAND_MAX); param[AMPLITUDE_1] = float(img_in.rows) / 10.; param[AMPLITUDE_1] = param[AMPLITUDE_1] * (1. + 0.5 * (0.5 - (float) rand() / (float) (RAND_MAX))); param[SIGN_1] = rand_101(); param[ANGLE_1] = 0.125 * M_PI; param[PERIOD_2] = 0.5 + 1.5 * (float) rand() / (float) (RAND_MAX); param[AMPLITUDE_2] = float(img_in.cols) / 10.; param[AMPLITUDE_2] = param[AMPLITUDE_2] * (1. + 0.75 * (0.5 - (float) rand() / (float) (RAND_MAX))); param[SIGN_2] = rand_101(); param[ANGLE_2] = 0.125 * M_PI; // вот тут собственно и вычисляем map для remap for (i = 0; i < img_in.rows; i++) { f_i = float(i); row = f_i - row2; angle_i = param[PERIOD_1] * M_PI * f_i / row1; for (j = 0; j < img_in.cols; j++) { f_j = float(j); col = f_j - col2; tt = (1. + sin(-M_PI / 2 + 4. * M_PI * f_j / col1)) / 2; if (col > 0) tt = -tt; map_c_s.at<float>(i, j) = (f_j - tt * (col1 / 7) * 0.5 * sin(param[SIGN_1] * angle_i)); map_r_s.at<float>(i, j) = f_i; angle_j = param[PERIOD_2] * M_PI * f_j / col1; tt = (1. + sin(-M_PI / 2 + 4. * M_PI * f_i / row1)) / 2; if (row > 0) tt = -tt; map_c_ss.at<float>(i, j) = f_j; map_r_ss.at<float>(i, j) = (f_i - tt * (row1 / 7) * 0.5 * sin(param[SIGN_2] * angle_j)); tt = (1. - abs(col) / col2) * (1. - abs(row) / row2); angle_j = param[SIGN_0] * param[AMPLITUDE_0] * M_PI * sin(M_PI * tt); map_r.at<float>(i, j) = row * cos(angle_j) - col * sin(angle_j) + float(img_in.rows) / 2; map_c.at<float>(i, j) = row * sin(angle_j) + col * cos(angle_j) + float(img_in.cols) / 2; } } img_in.copyTo(img_out); // применяем эти ремапы по очереди // один из них крутить, два других сжимают-растягивают remap(img_out, img_out, map_c_s, map_r_s, INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 0, 0)); remap(img_out, img_out, map_c_ss, map_r_ss, INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 0, 0)); remap(img_out, img_out, map_c, map_r, INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 0, 0)); bzero(out_file_path, PATH_MAX); sprintf(out_file_path, "%s%03d%s", "data/images/", thread_idx, file_name); try { // всё, что получили пишем в рабочий файл imwrite(out_file_path, img_out); } catch (cv::Exception &e) { const char *err_msg = e.what(); fprintf(log_file, " thread %d. write file exception caught: %s \n ", thread_idx, err_msg); fflush(log_file); error_idx = WRITE_FILE_EXEPTION; continue; } // сохраняем время завершения обработки OpenCV s_time = time(NULL); localtime_r(&s_time, &m_time); sprintf(log_txt + strlen(log_txt), "\topencv end time %s", asctime_r(&m_time, time_buf)); // формируем команду ОС для передачи файла по назначению // адрес 10.0.1.1 это адрес VPN сервера при обращении через VPN // по этому адресу наш FreeBSD сервер во внешнем мире // и картинка поедет дважды шифрованная // можно указать FRONT_ADDR и картинка будт отправлена на // сервер во внешнем мире другим путём. // на внешнем FreeBSD сервере можно порт SSH на FRONT_ADDR // совсем закрыть и сервер будет доступен только через VPN bzero(exec_txt, BUFLEN_TXT); sprintf(exec_txt, "scp %s funhouse@10.0.1.1:/usr/local/www/data/images/%03d%.*s", // "cp %s /var/www/html/images/%03d%.*s", // для локального использования или тестирования используем cp // если lighttpd запущен на этом же сервере, то можно и scp использовать // и cp для примера out_file_path, thread_idx, (int) strlen(file_name), file_name); // и отправляем файл через VPN на сервер в сети. if (system(exec_txt) != 0) { fprintf(log_file, " thread %d. File %s not moved successfully\n", thread_idx, out_file_path); error_idx = ERROR_TRANFER_RES_FILE; continue; } bzero(fcgi_txt, BUFLEN_TXT); // начинает готовить HTML страницу // готовим fcgi ответы в виде запросов // в тексте которых имя файла, параметры,соль и хеш // хеш конечно же можно проверить потом t_pointer = fcgi_txt; for (i = 0; i < PARAMETERS_NUM; i++) { memcpy(t_pointer, ¶m[i], sizeof(param[i])); t_pointer += sizeof(param[i]); sprintf(log_txt + strlen(log_txt), "\tparameter_%0d=%f\n", i, param[i]); } for (i = 0; i < SALT_LEN; i++) { salt = rand() % 256; memcpy(t_pointer, &salt, sizeof(salt)); t_pointer += sizeof(salt); } memcpy(t_pointer, file_name, strlen(file_name)); SHA256((const unsigned char*) fcgi_txt, (size_t) (PARAMETERS_NUM * sizeof(param[ANGLE_1]) + SALT_LEN * sizeof(salt) + strlen(file_name)), md_buf); // fcgi запросы-ответы на наши вопросы, готовы и теперь строим HTML страницу bzero(html_txt, BUFLEN_TXT); sprintf(html_txt + strlen(html_txt), "Status: 200 OK\r\n"); sprintf(html_txt + strlen(html_txt), "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\r\n"); sprintf(html_txt + strlen(html_txt), "Content-Type: text/html\r\n\r\n"); sprintf(html_txt + strlen(html_txt), "<html>\r\n<meta charset=\"utf-8\">\r\n"); sprintf(html_txt + strlen(html_txt), "<body>\r\n"); // если картинка большая для показа, то указываем браузеру размер для сжатия img_width = img_out.cols > MAX_IMAGE_ROWS ? MAX_IMAGE_ROWS : img_out.cols; sprintf(html_txt + strlen(html_txt), " <img src=http://%s/images/%03d%s alt=\"Computed image\" style=\"width:%dpx\" >\r\n", FRONT_ADDR, thread_idx, file_name, img_width); sprintf(html_txt + strlen(html_txt), "<form>\n"); // здесь двоичный код с именем файла, параметрами, солью и хешем преобразовываем в код из букв // кодировка base64 не годится,там есть непригодные для HTML символы // кодировку base58 запустить не получилось // то ли лень, то ли не сообразил как bzero(hex_txt, BUFLEN_TXT); char2hex(hex_txt, (char*) md_buf, SHA256_DIGEST_LENGTH); char2hex(hex_txt + 2 * SHA256_DIGEST_LENGTH, fcgi_txt, PARAMETERS_NUM * sizeof(param[ANGLE_1]) + SALT_LEN * sizeof(salt) + strlen(file_name)); // добавляем в страницу строки с опросами в ответе которых прошит FastCGI запрос // с именем файла, параметрами, солью и хеш sprintf(html_txt + strlen(html_txt), "<p><button type=\"submit\" formaction=\"%*s.yes.fcgi\"> НРАВИТСЯ </button></p>\n", (int) strlen(hex_txt), hex_txt); sprintf(html_txt + strlen(html_txt), "<p><button type=\"submit\" formaction=\"%*s.no.fcgi\"> ОТКАЗАТЬ </button></p>\n", (int) strlen(hex_txt), hex_txt); sprintf(html_txt + strlen(html_txt), "<p><button type=\"submit\" formaction=\"upload.html\"> ЕЩЕ КАРТИНКУ </button></p>\n"); // если посетитель нажмет какую либо из кнопок // то fcgi запрос придет на вход этой программы sprintf(html_txt + strlen(html_txt), "</form>"); sprintf(html_txt + strlen(html_txt), "</body>\r\n</html>\r\n"); // тут только отправляем сформированную страницу ввнешний, FreeBSD, сервер // в странице показ обработанной картинки и три ответа на вопрос "ДА" "НЕТ" "ОТЛОЖИТЬ" if ((ret = FCGX_PutS(html_txt, request.out)) < 0) { error_idx = ERROR_FASTCGI_SEND; continue; }; sprintf(log_txt + strlen(log_txt), "\t%s\n", "send page"); break; } if (error_idx != 0) { // если во время парсинга или скачивания или еще когда // возникла ошибка и мы это поняли, // то вот тут формируется страница перехода на начальную страницу нашего сайта bzero(html_txt, BUFLEN_TXT); sprintf(html_txt + strlen(html_txt), "Status: 200 OK\r\nContent-Type: text/html\r\n\r\n"); sprintf(html_txt + strlen(html_txt), "<head>\n"); sprintf(html_txt + strlen(html_txt), "<meta http-equiv=\"refresh\" content=\"0;URL=http://%s/upload.html\"\n", FRONT_ADDR); sprintf(html_txt + strlen(html_txt), "</head>"); printf("%s\n", html_txt); FCGX_PutS(html_txt, request.out); sprintf(log_txt + strlen(log_txt), "\t%s error %d\n", "Moved to upload page", error_idx); fflush(log_file); error_idx = 0; } // все, что смогли или что получилось показали и нужно //закрыть текущее соединение FCGX_Finish_r(&request); //завершающие действия - запись статистики, логгирование ошибок и т.п. s_time = time(NULL); localtime_r(&s_time, &m_time); sprintf(log_txt + strlen(log_txt), "\tend request %s", asctime_r(&m_time, time_buf)); sprintf(log_txt + strlen(log_txt), "\terror_idx %d\n\t +++ FCGX_fin\n\n", error_idx); // так как журнал у нас один для всех потоков, то нельзя допустить, // что бы потоки писали в него в произвольное время - каша получится // поэтому и пишем в журнал монопольно для потока pthread_mutex_lock(&log_mutex); fprintf(log_file, "%s", log_txt); fflush(log_file); pthread_mutex_unlock(&log_mutex); } // ну и если вдруг как то программа решила завершиться // мьютексы нужно вернуть pthread_mutex_destroy(&accept_mutex); pthread_mutex_destroy(&log_mutex); return NULL; } int main(void) { int i; int thread_num[THREAD_COUNT]; pthread_t id[THREAD_COUNT]; // Создание файла журнала и файла записи результатов if ((log_file = fopen("./log/funhouse_mirror.log", "a")) == NULL) { perror("log file open error "); return (44); } if ((marks_file = fopen("./data/marks/funhouse_mirror.marks", "a")) == NULL) { perror("marks file open error "); return (44); } pid_t pid = getpid(); // habr.com/ru/post/154187/ //инициализация библиотеки FCGX_Init(); fprintf(log_file, "\n\n\nfunhouse_mirror started \nLib is inited pid %d \n", pid); fflush(log_file); //открываем новый сокет socketId = FCGX_OpenSocket(SOCKET_PATH, 20); if (socketId < 0) { //ошибка при открытии сокета fprintf(log_file, " socketID < 0 and = %d ", socketId); fflush(log_file); return (11); } fprintf(log_file, "Socket is opened\n"); fflush(log_file); // создаём рабочие потоки // номер потока хранится массиве for (i = 0; i < THREAD_COUNT; i++) { thread_num[i] = i; pthread_create(&(id[i]), NULL, do_thread, &(thread_num[i])); } // ждем завершения рабочих потоков // в данном варианте там бесконечный цикл // и поток может завершиться только по ошибке // ждем всех for (i = 0; i < THREAD_COUNT; i++) { pthread_join(id[i], NULL); fprintf(log_file, "Thread %lu is joined\n", id[i]); fflush(log_file); } return 0; }
