Как стать автором
Обновить

Искусственный интеллект лицом

Время на прочтение30 мин
Количество просмотров3.9K

или

Веб морда для ваших поделок

(пет проект)

Если ваши успехи в освоении 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, &param[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;
}
Теги:
Хабы:
Всего голосов 5: ↑3 и ↓2+3
Комментарии1

Публикации

Истории

Работа

Программист С
38 вакансий

Ближайшие события

28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань