What's new
Runion

This is a sample guest message. Register a free account today to become a member! Once signed in, you'll be able to participate on this site by adding your own topics and posts, as well as connect with other members through your own private inbox!

Пишем приложение на ванильном JS, сервер на Си и деплоим на Onion.

miserylord

Light Weight
Депозит
$0
Автор: miserylord
Эксклюзивно для форума:
Приложение

На первом этапе мы реализуем фронтенд приложения с использованием моковых данных, в качестве базы данных будут выступать JSON-файлы.

Идея приложения — сайт приюта для найденных кошек.

В приложении будет несколько экранов: главный экран с информацией о найденных кошках; экран с информацией о тех, кого уже нашли волонтеры, но кто еще не прибыл в приют; а также экран для входа в аккаунт. В зависимости от роли, администратор попадет в админ-панель, а пользователь — в личный кабинет. Также будет реализован чат для обсуждения выкупа животных из приюта.

Для начала мы скачиваем шрифты и помещаем их в папку fonts, чтобы избежать запросов к сайтам со шрифтами. В папке assets будут находиться SVG-логотипы для меню и вкладки сайта. В папке components будут храниться HTML-файлы, которые будут многократно использоваться в проекте. Например, создадим menu.html.
HTML: Скопировать в буфер обмена
Code:
<div class="menu">
 <a class="logolink" href="index.html">
  <img src="assets/logo.svg" alt="Logotip" class="logo" style="filter: invert(1);" />
 </a>
 <a class="logolink" href="index.html">
  <div class="menu-title">Otcs</div>
 </a>
 <div class="tabs">
  <a href="index.html" class="tab">
   <img src="assets/cats.svg" alt="Cats Icon" class="tab-icon" style="filter: invert(1);" />
   Cats
  </a>
  <a href="timer.html" class="tab">
   <img src="assets/timer.svg" alt="Awaiting Icon" class="tab-icon" style="filter: invert(1);" />
   Awaiting
  </a>
  <a href="login.html" class="tab">
   <img src="assets/login.svg" alt="Login Icon" class="tab-icon" style="filter: invert(1);" />
   Login
  </a>
 </div>
</div>

Этот HTML-код создает горизонтальное меню с логотипом и несколькими вкладками, ведущими на разные страницы сайта.

В папке styles будут находиться CSS-стили, которые я не буду разбирать в статье. В папке mock на этом этапе будут находиться JSON-файлы.

Перейдем к написанию JavaScript-кода. Первый файл — menu.js — отвечает за динамическую подгрузку элементов меню.
JavaScript: Скопировать в буфер обмена
Code:
function loadMenu() {
 fetch('components/menu.html')
  .then(response => response.text())
  .then(data => {
   document.getElementById('menu-container').innerHTML = data;
  })
  .catch(error => console.error('Error loading menu:', error));
}

document.addEventListener('DOMContentLoaded', () => {
 loadMenu();
});


Ожидаем полной загрузки HTML-документа, чтобы он был готов для манипуляций, затем вызываем loadMenu() для загрузки содержимого меню и вставки его в нужный элемент.

Файл cats.js:

JavaScript: Скопировать в буфер обмена
Code:
// 1
let catData = [];
let cattoShow = 8;

// 2
function isValidDate(dateString) {
 const currentDate = new Date();
 const itemDate = new Date(dateString);
 return itemDate <= currentDate;
}

// 3
async function fetchDataPrevie() {
 try {
  const response = await fetch('../mosk/cats.json');
  catData = await response.json();

  const filteredData = catData.filter(element => isValidDate(element.date));

  displayData(filteredData.slice(0, cattoShow));

  if (filteredData.length > cattoShow) {
   document.getElementById('show-more').style.display = 'block';
  }
 } catch (error) {
  console.error("Error fetching:", error);
 }
}

// 4
function displayData(data) {
 const container = document.getElementById('leakdataContainer');
 container.innerHTML = '';

 data.forEach(element => {
  const card = document.createElement('div');
  card.classList.add('cat-card');

  card.innerHTML = `
   <div class="leak-photo"><img id="leak-photo" src="${element.photo}" width="8%" height="70%"></div>
   <div class="cat-date"><div class="cat-date-text">Date of publication:</div>${element.date}</div>
   <div class="cat-name">${element.name}</div>
   <div class="cat-description" style="max-height: 90px; overflow: hidden;">${element.description}</div>
   <div class="cat-category">Category: ${element.category}</div>
   <div class="cat-views">Views: ${element.views}</div>
   <button class="get-more">More Details</button>
  `;

  const button = card.querySelector('.get-more');
  button.addEventListener('click', () => {
   openPopup(element.name, element.description, element.date);
  });

  container.appendChild(card);
 });
}

// 5
document.getElementById('show-more').addEventListener('click', () => {
 const filteredData = catData.filter(element => isValidDate(element.date));
 const nextData = filteredData.slice(cattoShow, cattoShow + 4);
 displayData(filteredData.slice(0, cattoShow + 4));
 cattoShow += 4;

 if (cattoShow >= filteredData.length) {
  document.getElementById('show-more').style.display = 'none';
 }
});

// 6
function openPopup(title, text, date) {
 const popup = document.getElementById('popup');
 const overlay = document.getElementById('overlay');
 const popupTitle = document.getElementById('popup-title');
 const popupText = document.getElementById('popup-text');
 const popupDate = document.getElementById('popup-date');
 const closeBtn = document.querySelector('.close-btn');

 popupTitle.textContent = title;
 popupText.textContent = text;
 popupDate.textContent = date;

 overlay.style.display = 'block';
 popup.style.display = 'block';

 closeBtn.onclick = () => {
  closePopup(overlay, popup);
 };

 window.onclick = (event) => {
  if (event.target === overlay) {
   closePopup(overlay, popup);
  }
 };
}

// 7
function closePopup(overlay, popup) {
 overlay.style.display = 'none';
 popup.style.display = 'none';
}

fetchDataPrevie();

  1. Определяем несколько переменных. Храним данные о котах, полученные из cats.json, и количество карточек для первоначального отображения.
  2. Функция проверяет, чтобы дата публикации не была позже текущей даты. Если дата прошла, элемент считается действительным.
  3. Выполняем запрос к файлу cats.json и сохраняем результат в catData. Фильтруем данные, оставляя только те элементы, у которых дата публикации прошла. Вызываем displayData для отображения первых catToShow элементов. Если доступных элементов больше, чем catToShow, отображается кнопка «Показать еще».
  4. Очищаем контейнер для отображения данных. Создаем и добавляем карточки для каждого элемента: картинка, дата публикации, имя, описание, категория и количество просмотров. Кнопка «Подробнее» с обработчиком открывает всплывающее окно с деталями.
  5. Кнопка «Показать еще»: добавляем следующие 4 элемента к отображаемым данным. Если показаны все доступные элементы, кнопка «Показать еще» скрывается.
  6. Отображаем всплывающее окно с подробной информацией (название, текст, дата). Обработчик закрытия по нажатию на кнопку или клику вне окна вызывает closePopup.
  7. Закрываем всплывающее окно, скрывая его и затемняющий фон.

Реализуем также код для страницы с таймером в файле timer.js.
JavaScript: Скопировать в буфер обмена
Code:
let catData = [];
let cattoShow = 8;


function isValidDate(dateString) {
 const currentDate = new Date();
 const itemDate = new Date(dateString);
 return itemDate >= currentDate;
}

// 1
function formatTimeDifference(dateString) {
 const currentDate = new Date();
 const itemDate = new Date(dateString);
 const difference = itemDate - currentDate;

 if (difference <= 0) return "Published";

 const seconds = Math.floor((difference / 1000) % 60);
 const minutes = Math.floor((difference / (1000 * 60)) % 60);
 const hours = Math.floor((difference / (1000 * 60 * 60)) % 24);
 const days = Math.floor(difference / (1000 * 60 * 60 * 24));

 return `${days}d ${hours}h ${minutes}m ${seconds}s`;
}

// 2
function updateTimerElements() {
 const timerElements = document.querySelectorAll('.cat-timer');
 timerElements.forEach(element => {
  const dateString = element.getAttribute('data-date');
  element.textContent = formatTimeDifference(dateString);
 });
}

async function fetchDataPrevie() {
 try {
  const response = await fetch('../mosk/cats.json');
  catData = await response.json();

  const filteredData = catData.filter(element => isValidDate(element.date));

  displayData(filteredData.slice(0, cattoShow));

  if (filteredData.length > cattoShow) {
   document.getElementById('show-more').style.display = 'block';
  }
 } catch (error) {
  console.error("Error fetching:", error);
 }
}

function displayData(data) {
 const container = document.getElementById('leakdataContainer');
 container.innerHTML = '';

 data.forEach(element => {
  const card = document.createElement('div');
  card.classList.add('cat-card');

  card.innerHTML = `
   <div class="leak-phote"><img id="leak-phote" src="${element.photo}" width="8%" height="70%"></div>
   <div class="cat-date"><div class="cat-date-text">Date of publication:</div>${element.date}</div>
   <div class="cat-name">${element.name}</div>
   <div class="cat-timer" data-date="${element.date}">${formatTimeDifference(element.date)}</div>
  `;

  container.appendChild(card);
 });

 updateTimerElements();
}

document.getElementById('show-more').addEventListener('click', () => {
 const filteredData = catData.filter(element => isValidDate(element.date));
 const nextData = filteredData.slice(cattoShow, cattoShow + 4);
 displayData(filteredData.slice(0, cattoShow + 4));
 cattoShow += 4;

 if (cattoShow >= filteredData.length) {
  document.getElementById('show-more').style.display = 'none';
 }
});


fetchDataPrevie();
// 3
setInterval(updateTimerElements, 1000);


  1. Вычисляем разницу между текущей датой и указанной датой публикации. Если дата уже прошла, возвращаем «Published». Если публикация еще не произошла, выводим время до ее наступления в формате «дни, часы, минуты, секунды».
  2. Ищем все элементы с классом cat-timer на странице и обновляем их текст оставшимся временем до публикации с помощью formatTimeDifference. Используем для обновления счетчика обратного отсчета каждую секунду.
  3. Обновляем каждый таймер на странице каждую секунду.

Наконец, перейдем к HTML-файлам. Вот пример index.html:


HTML: Скопировать в буфер обмена
Code:
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <link rel="stylesheet" href="styles/styles.css">
 <link rel="stylesheet" href="styles/index.css">

 <title>Ode to Cats Shelter</title>
 <link rel="shortcut icon" href="assets/logo.svg" type="image/svg+xml" />
</head>
<body>



 <div id="menu-container"></div>
 <div class="container" id="leakdataContainer"></div>
 <button id="show-more" style="display: none;">Show More</button>
 <script src="scripts/menu.js"></script>
 <script src="scripts/cats.js"></script>
 <div id="overlay" style="display:none;"></div>
 <div id="popup" style="display:none;">
 <h2 id="popup-title"></h2>
 <p id="popup-text"></p>
 <p id="popup-date"></p>
 <button class="close-btn">Close</button>
 </div>

</body>
</html>

Этот HTML-код создает веб-страницу для приюта «Ode to Cats Shelter». Он включает базовую разметку, контейнеры для динамически загружаемого меню и данных, кнопку «Show More», а также модальные элементы для всплывающего окна с информацией.



Перейдем к экрану для аутентификации пользователей. Реализуем простую имитацию процесса входа в систему с использованием JavaScript. Он проверяет введенные пользователем данные по заранее определенному массиву пользователей и в зависимости от их роли перенаправляет на соответствующую страницу.

JavaScript: Скопировать в буфер обмена
Code:
const mockData = [
 { username: 'admin', password: 'admin123', role: 'admin' },
 { username: 'user', password: 'user123', role: 'user' }
];

document.getElementById('loginForm').addEventListener('submit', function(event) {
 event.preventDefault();

 const username = document.getElementById('username').value;
 const password = document.getElementById('password').value;

 const user = mockData.find(user => user.username === username && user.password === password);

 if (user) {
  localStorage.setItem('currentUser', JSON.stringify(user));

  if (user.role === 'admin') {
   window.location.href = 'admin.html';
  } else {
   window.location.href = 'user.html';
  }
 } else {
  document.getElementById('error-message').innerText = 'Invalid login or password';
 }
});

Реализуем проверку по роли пользователя. Этот код выполняет проверку доступа, чтобы убедиться, что только администратор может попасть на защищенную страницу. Он проверяет, авторизован ли текущий пользователь и имеет ли он роль администратора. Если нет, пользователя перенаправляют на главную страницу. Подобную проверку реализуем и для роли пользователя (user).

JavaScript: Скопировать в буфер обмена
Code:
const currentUser = JSON.parse(localStorage.getItem('currentUser'));

if (!currentUser || currentUser.role !== 'admin') {
 window.location.href = 'index.html';
}


Сообщения на данный момент будут храниться в мок-данных. Новые сообщения будут сохраняться в localStorage. Файл chatUser.js:
JavaScript: Скопировать в буфер обмена
Code:
document.addEventListener('DOMContentLoaded', () => {
 const messagesContainer = document.getElementById('messages');
 const messageInput = document.getElementById('messageInput');
 const sendMessageButton = document.getElementById('sendMessage');

 const LOCAL_STORAGE_KEY = 'userChatMessages'; // 1

 // 2
 async function loadMessages() {
  try {
   const response = await fetch('../mosk/user123.json');
   const jsonMessages = await response.json();
   const storedMessages = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || [];

   const allMessages = [...jsonMessages, ...storedMessages];

   messagesContainer.innerHTML = '';
   allMessages.forEach((msg) => {
    const messageDiv = document.createElement('div');
    messageDiv.className = 'message';
    messageDiv.innerHTML = `${msg.sender}: ${msg.message}`;
    messagesContainer.appendChild(messageDiv);
   });
  } catch (error) {
   console.error('Ошибка загрузки сообщений:', error);
  }
 }

 // 3
 function saveMessages(messages) {
  localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(messages));
 }

 // 4
 sendMessageButton.addEventListener('click', () => {
  const message = messageInput.value.trim();
  if (!message) return;

  const messages = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || [];
  const newMessage = {
   sender: 'user',
   message,
   is_read: false,
  };

  messages.push(newMessage);
  saveMessages(messages);
  loadMessages();
  messageInput.value = '';
 });

 // 5
 loadMessages();
});


  1. Ключ, под которым сохраняются сообщения в localStorage.
  2. Загружает сообщения из файла user123.json и из localStorage. Сообщения из JSON-файла объединяются с сообщениями из localStorage. Каждое сообщение добавляется в контейнер messagesContainer как элемент div, отображающий отправителя и сообщение.
  3. Функция saveMessages() для сохранения сообщений в локальное хранилище.
  4. Отправка сообщения. Обработчик события click на sendMessageButton работает следующим образом: сперва извлекает текст сообщения из messageInput. Если сообщение не пустое, создает объект нового сообщения с полями sender, message и is_read. Добавляет новое сообщение в массив сохраненных сообщений и вызывает saveMessages(). Затем вызывает loadMessages() для обновления списка сообщений и очищает поле ввода после отправки сообщения.
  5. Вызывается при загрузке страницы, чтобы отобразить все доступные сообщения.





Сервер

Переходим к реализации серверной части приложения. Сервер предоставляет ресурсы другим клиентам через сеть. В контексте веба сервер часто реализует HTTP-протокол, принимает запросы от клиентов (веб-браузеров), обрабатывает их и отправляет ответы (например, HTML-страницы).

Примерами таких серверов можно привести Apache HTTP Server и Nginx. Это отличные решения, но я предлагаю пойти другим путем и реализовать самописный сервер на C. Мы получим полное управление над ресурсами и меньше накладных расходов на функционал, который не нужен для конкретного проекта. Безусловно, это повышает вероятность ошибок, но, как бы там ни было, это будет интересный опыт. Переходим к коду!

Код ниже разработан в среде Linux и не будет работать на Windows.

Сначала добавим код с мок-данными, а в дальнейшем подключим базу данных.

Это мое первое знакомство с языком программирования C, поэтому на некоторых моментах буду останавливаться подробнее. Возможно, это покажется очевидными вещами.

Итак, код представляет собой простой многопоточный сервер на C, который принимает подключения от клиентов по TCP и обрабатывает их в отдельных потоках, напоминая работу Apache.
C: Скопировать в буфер обмена
Code:
// 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <signal.h>
#include "request_handler.h"

// 2
#define PORT 8082


// 3
void *client_handler(void *arg) {
 int client_socket = *(int*)arg;
 free(arg);
 handle_client(client_socket);
 pthread_exit(NULL);
}

int main() {
 signal(SIGPIPE, SIG_IGN); // 4
 // 5
 int server_socket;
 struct sockaddr_in server_addr, client_addr;
 socklen_t client_len = sizeof(client_addr);

 if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
  perror("Socket failed");
  return 1;
 }

 // 6
 server_addr.sin_family = AF_INET;
 server_addr.sin_addr.s_addr = INADDR_ANY;
 server_addr.sin_port = htons(PORT);

 if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
  perror("Bind failed");
  close(server_socket);
  return 1;
 }

 // 7
 if (listen(server_socket, 10) == -1) {
  perror("Listen failed");
  close(server_socket);
  return 1;
 }

 printf("Server is listening on port %d...\n", PORT);

 // 8
 while (1) {
  int *client_socket = malloc(sizeof(int));
  *client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_len);
  if (*client_socket == -1) {
   perror("Accept failed");
   free(client_socket);
   continue;
  }

 // 9
  pthread_t thread_id;
  if (pthread_create(&thread_id, NULL, client_handler, client_socket) != 0) {
   perror("Could not create thread");
   close(*client_socket);
   free(client_socket);
  } else {
   pthread_detach(thread_id);
  }
 }
 // 10
 close(server_socket);
 return 0;
}

  1. Подключаем заголовочные файлы и определения.
  2. Подключаем макросы. Порт, на котором сервер будет прослушивать соединения.
  3. Функция-обработчик, которая запускается в отдельном потоке для каждого клиента. Получает клиентский сокет через указатель, вызывает handle_client, определенную в request_handler.h, для обработки запроса, освобождает выделенную память для аргумента и завершает поток, вызвав pthread_exit.
  4. Игнорируем сигнал SIGPIPE, который может возникнуть при записи в закрытый сокет (если клиент неожиданно разорвал соединение). Эта строка решила проблему, из-за которой сервер постоянно падал.
  5. server_socket — сокет сервера, server_addr — структура с адресом сервера, client_addr — структура для хранения адреса клиента, client_len — размер структуры client_addr. Создаем сокет IPv4 для TCP-соединения. В случае неудачи выводится сообщение об ошибке, и программа завершает работу.
  6. Настройка адреса сервера и привязка сокета. Указываем, что будет использоваться IPv4, принимаем соединения на любом IP-адресе, который привязан к серверу, и задаем порт для прослушивания, преобразуя его в сетевой порядок байтов. Если привязка не удалась, сервер закрывает сокет и завершает выполнение.
  7. Настройка прослушивания. Переводим сокет в режим прослушивания. 10 — размер очереди подключений. Если настройка завершилась неудачно, сервер закрывает сокет и завершает выполнение. Выводится сообщение, что сервер начал прослушивание на заданном порту.
  8. Бесконечный цикл ожидает новых подключений от клиентов. Выделяется память под новый клиентский сокет. accept принимает подключение от клиента, создавая новый сокет для связи с ним. В случае ошибки память освобождается, и продолжается ожидание новых подключений.
  9. Создаем новый поток, который выполняет функцию client_handler. Аргументом передается указатель на клиентский сокет client_socket. Обрабатываем возможные ошибки. Делаем поток отделяемым, чтобы не нужно было явно ждать его завершения (поток сам освобождает ресурсы по завершении).
  10. После выхода из цикла while (в случае завершения программы) вызываем close(server_socket);, чтобы закрыть серверный сокет.

В языке C файлы с расширением .h (header files) — это заголовочные файлы, которые содержат объявления функций, структур, переменных и макросов. Они упрощают организацию кода и его повторное использование.

Создадим файлы request_handler.h и utils.h (а также с таким же названием файлы с расширением .c).

Код request_handler.h:
C-подобный: Скопировать в буфер обмена
Code:
#ifndef REQUEST_HANDLER_H
#define REQUEST_HANDLER_H

void handle_client(int client_socket);

#endif

Заголовочный файл с защитой от двойного включения работает следующим образом: при первом подключении файла request_handler.h макрос REQUEST_HANDLER_H ещё не определен. Компилятор видит #ifndef REQUEST_HANDLER_H, определяет макрос REQUEST_HANDLER_H и включает содержимое файла. Если файл подключается второй раз (например, косвенно через другие заголовочные файлы), REQUEST_HANDLER_H уже определен, и компилятор игнорирует содержимое заголовочного файла, предотвращая повторное объявление функции handle_client.

Код файла .c:
C-подобный: Скопировать в буфер обмена
Code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include "request_handler.h"
#include "utils.h"

// 1
#define BUFFER_SIZE 1024

// 2
void handle_client(int client_socket) {
 // 3
 char buffer[BUFFER_SIZE];
 read(client_socket, buffer, sizeof(buffer) - 1);

 // 4
 char *file_path = "index.html";
 if (strncmp(buffer, "GET /", 5) == 0) {
  char *path_start = buffer + 5;
  char *path_end = strchr(path_start, ' ');
  if (path_end != NULL) {
   *path_end = '\0';
   if (strlen(path_start) > 0) {
    file_path = path_start;
   }
  }
 }

 // 5
 FILE *file = fopen(file_path, "r");
 if (file == NULL) {
  const char *not_found_response =
   "HTTP/1.1 404 Not Found\r\n"
   "Content-Type: text/html\r\n"
   "\r\n"
   "<!DOCTYPE html>"
   "<html><body><h1>404 Not Found</h1></body></html>";
  write(client_socket, not_found_response, strlen(not_found_response));
  close(client_socket);
  return;
 }


 // 6
 const char *content_type = get_content_type(file_path);

 // 7
 char header[BUFFER_SIZE];
 snprintf(header, sizeof(header),
    "HTTP/1.1 200 OK\r\n"
    "Content-Type: %s\r\n"
    "\r\n", content_type);
 write(client_socket, header, strlen(header));


 // 8
 char file_content[BUFFER_SIZE];
 size_t bytes_read;
 while ((bytes_read = fread(file_content, 1, sizeof(file_content), file)) > 0) {
  write(client_socket, file_content, bytes_read);
 }


 // 9
 fclose(file);
 close(client_socket);
}
  1. Определяем размер буфера для чтения данных.
  2. Эта функция принимает один параметр client_socket, который представляет клиентское соединение. Внутри этой функции происходит обработка запроса клиента.
  3. Чтение данных из сокета. Создается буфер buffer для хранения данных, полученных от клиента. Читаем данные из сокета и записываем их в буфер. Размер данных ограничен BUFFER_SIZE - 1, чтобы оставить место для завершающего нуля.
  4. Определение пути к запрашиваемому файлу. file_path по умолчанию указывает на index.html, чтобы сервер вернул главный файл сайта, если клиент не указал файл. Проверяется, начинается ли запрос с GET. Если это так, начинается разбор пути: path_start указывает на начало запрашиваемого пути, а path_end — на конец. В случае успеха file_path указывает на запрашиваемый путь или остается index.html.
  5. Открытие файла — fopen открывает файл в режиме чтения "r". Если файл не найден, fopen возвращает NULL. В этом случае сервер отправляет клиенту ответ 404 Not Found с короткой HTML-страницей. После отправки ошибки сокет закрывается, и функция завершает работу.
  6. Определение типа содержимого. Это функция из utils.h, которая определяет MIME-тип содержимого файла.
  7. Отправка заголовка HTTP-ответа. Создается HTTP-заголовок с кодом 200 OK и Content-Type, определенным ранее. snprintf записывает форматированный заголовок в буфер header. write отправляет заголовок клиенту.
  8. В цикле читаются порции данных из файла с помощью fread в буфер file_content. Затем write отправляет эти данные клиенту. Цикл продолжается, пока fread возвращает положительное количество байт.
  9. Завершение работы с файлом и закрытие сокета.

Файл utils.c:
C-подобный: Скопировать в буфер обмена
Code:
#include <string.h>
#include "utils.h"

const char* get_content_type(const char* path) {
 if (strstr(path, ".html")) return "text/html";
 if (strstr(path, ".css")) return "text/css";
 if (strstr(path, ".js")) return "application/javascript";
 if (strstr(path, ".svg")) return "image/svg+xml";
 if (strstr(path, ".woff2")) return "font/woff2";
 if (strstr(path, ".json")) return "application/json";

 return "text/plain";
}

Этот код определяет функцию get_content_type, которая возвращает MIME-тип файла, основываясь на его расширении. MIME-типы — это ключевая часть HTTP-протокола, которая позволяет клиентам правильно обрабатывать разные типы файлов. Функция помогает серверу автоматически определять MIME-тип по расширению файла и возвращать соответствующий заголовок клиенту.

Напишем Makefile. Makefile — это специальный файл, который помогает автоматизировать компиляцию и сборку проекта, особенно если проект состоит из нескольких файлов. Он описывает зависимости и команды для сборки, чтобы не прописывать каждый раз все команды компиляции. Команда make читает Makefile и выполняет описанные в нем задачи.
Код: Скопировать в буфер обмена
Code:
CC = gcc
CFLAGS = -Wall
SRC = src/server.c src/request_handler.c src/utils.c
OBJ = $(SRC:.c=.o)
EXEC = server

all: $(EXEC)

$(EXEC): $(OBJ)
 $(CC) $(CFLAGS) -o $(EXEC) $(OBJ)

clean:
 rm -f $(OBJ) $(EXEC)


Компилируем проект с заданными настройками, автоматически отслеживая зависимости и компилируя только измененные файлы. make clean очищает проект, удаляя объектные и исполняемые файлы.

Поскольку я недавно начал изучать язык C, оставлю несколько рекомендаций по роудмапу изучения:

Во-первых, лучшая книга по C — "The C Programming Language" (2-е издание) Ритчи и Кернигана. Для изучения сокетов — "Beej's Guide to Network Programming" (есть на русском) и также полезное видео.

Подключаем базу данных, я буду работать с SQLite. За кулисами добавлю в БД те данные, которые сейчас находятся в .json файле в директории с моками.

В JavaScript в запросе fetch теперь обращаемся по адресу localhost:port/cats.json. Создадим файл data_handler.c (а также одноимённый .h файл), в котором будем обращаться к базе данных.
C: Скопировать в буфер обмена
Code:
#include <stdio.h>
#include <stdlib.h> 
#include <string.h>  
#include <sqlite3.h> 

#include "data_handler.h"

// 1
#define DB_PATH "./cats.db"

char* get_cats_data_as_json() {

 // 2
 sqlite3 *db;
 sqlite3_stmt *stmt;
 char *json_data = malloc(1024 * sizeof(char));
 int buffer_size = 1024, offset = 0;

 // 3
 if (sqlite3_open(DB_PATH, &db) != SQLITE_OK) {
  fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
  return NULL;
 }

 // 4
 const char *sql = "SELECT id, name, date, photo, description, category, views, income FROM cats;";
 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
  fprintf(stderr, "Failed to fetch data: %s\n", sqlite3_errmsg(db));
  sqlite3_close(db);
  return NULL;
 }

 // 5
 offset += snprintf(json_data + offset, buffer_size - offset, "[");

 // 6
 while (sqlite3_step(stmt) == SQLITE_ROW) {
  int id = sqlite3_column_int(stmt, 0);
  const char *name = (const char*)sqlite3_column_text(stmt, 1);
  const char *date = (const char*)sqlite3_column_text(stmt, 2);
  const char *photo = (const char*)sqlite3_column_text(stmt, 3);
  const char *description = (const char*)sqlite3_column_text(stmt, 4);
  const char *category = (const char*)sqlite3_column_text(stmt, 5);
  int views = sqlite3_column_int(stmt, 6);
  int income = sqlite3_column_int(stmt, 7);

 // 7
  if (offset + 256 > buffer_size) {
   buffer_size *= 2;
   json_data = realloc(json_data, buffer_size);
  }

  // 8
  offset += snprintf(json_data + offset, buffer_size - offset,
       "{\"id\":%d,\"name\":\"%s\",\"date\":\"%s\",\"photo\":\"%s\","
       "\"description\":\"%s\",\"category\":\"%s\",\"views\":%d,\"income\":%d},",
       id, name, date, photo, description, category, views, income);
 }

 // 9
 if (offset > 1 && json_data[offset - 1] == ',') {
  offset--;
 }
 snprintf(json_data + offset, buffer_size - offset, "]");

 // 10
 sqlite3_finalize(stmt);
 sqlite3_close(db);

 return json_data;
}
```
  1. Константа DB_PATH указывает расположение файла базы данных.
  2. Переменные для базы данных, запроса и буфера.
  3. sqlite3_open открывает соединение с базой данных. В случае ошибки выводит сообщение и возвращает NULL.
  4. Подготовка SQL-запроса.
  5. Начало JSON-строки. Формирует JSON-строку, добавляя открывающую скобку массива ([).
  6. Извлечение данных и заполнение JSON. Этот блок кода выполняется для каждой строки таблицы cats, извлекая значения каждого столбца.
  7. Проверка, достаточно ли памяти для добавления новой записи. Если нет, удваивает размер buffer_size и перераспределяет память.
  8. Запись данных текущей строки в json_data в формате JSON.
  9. Убираем последнюю запятую и закрываем массив символом ].
  10. Завершаем работу с запросом и закрываем базу данных. Возвращаем указатель на JSON-строку, содержащую все данные.

Добавим обработку маршрута в request_handler.c.
C: Скопировать в буфер обмена
Code:
if (strcmp(file_path, "cats.json") == 0) {
 char *json_data = get_cats_data_as_json();
 if (json_data == NULL) {
  const char *error_response = "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\n\r\nError fetching cat data.";
  write(client_socket, error_response, strlen(error_response));
 } else {
  const char *header = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n";
  write(client_socket, header, strlen(header));
  write(client_socket, json_data, strlen(json_data));
  free(json_data);
 }
 close(client_socket);
 return;
}


Проверяем имя файла, получаем данные о котах в формате JSON, затем происходит проверка успешности получения данных и отправка успешного HTTP-ответа с данными о котах. Закрываем соединение и завершаем работу функции.

Переходим к процессу аутентификации. Саму проверку я менять не стану, хотя и прекрасно вижу.

Код login_handler.c:
C-подобный: Скопировать в буфер обмена
Code:
#include <stdio.h>  
#include <stdlib.h> 
#include <string.h>  
#include <unistd.h> 
#include <sqlite3.h> 

#include "login_handler.h"

#define BUFFER_SIZE 1024
#define DB_PATH "./cats.db"

// 1
void handle_login_request(int client_socket, const char *username, const char *password) {
 sqlite3 *db;
 sqlite3_stmt *stmt;
 char *response = NULL;
 int success = 0;
 char role[10];

 if (sqlite3_open(DB_PATH, &db) != SQLITE_OK) {
  fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
  return;
 }

 // 2
 const char *sql = "SELECT role FROM users WHERE username = ? AND password = ?";
 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
  sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC);
  sqlite3_bind_text(stmt, 2, password, -1, SQLITE_STATIC);

  if (sqlite3_step(stmt) == SQLITE_ROW) {
   success = 1;
   strcpy(role, (const char*)sqlite3_column_text(stmt, 0));
  }
 }

 sqlite3_finalize(stmt);
 sqlite3_close(db);

 // 3
 if (success) {
  response = malloc(BUFFER_SIZE);
  snprintf(response, BUFFER_SIZE,
     "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n"
     "{\"success\": true, \"user\": {\"username\": \"%s\", \"role\": \"%s\"}}",
     username, role);
  write(client_socket, response, strlen(response));
  free(response);
 } else {
  const char *unauthorized_response =
   "HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\n\r\n{\"success\": false}";
  write(client_socket, unauthorized_response, strlen(unauthorized_response));
 }
}

  1. Функция проверяет учётные данные пользователя и отправляет соответствующий ответ через сокет.
  2. SQL-запрос sql извлекает роль пользователя с заданными username и password. sqlite3_prepare_v2 подготавливает запрос, а sqlite3_bind_text подставляет параметры username и password. sqlite3_step выполняет запрос и проверяет, есть ли результат. Если запрос возвращает строку, роль копируется в role, а success устанавливается в 1.
  3. Выделяется память под response и формируется успешный ответ с HTTP-кодом 200 OK и JSON-данными, включающими имя пользователя и его роль. Ответ отправляется клиенту через write, после чего память под response освобождается.

Будьте внимательны при работе с SQL. Насколько я вижу, в этом коде SQL-инъекции исключены благодаря использованию подготовленных выражений и связыванию значений через sqlite3_bind_text. Однако всегда нужно перепроверять всё, что приходит от пользователя на сервер, и включать двойную валидацию — как на фронтенде, так и на бэкенде.

Добавляем код для маршрута.
C: Скопировать в буфер обмена
Code:
if (strncmp(buffer, "POST /login", 11) == 0) {
  char *json_data = strstr(buffer, "\r\n\r\n") + 4;
  char username[50], password[50];

  sscanf(json_data, "{\"username\":\"%49[^\"]\",\"password\":\"%49[^\"]\"}", username, password);

  handle_login_request(client_socket, username, password);
  close(client_socket);
  return;

 }

strncmp сравнивает первые 11 символов строки buffer с "POST /login". Это проверяет, что запрос является POST-запросом, направленным на /login. Далее происходит поиск данных тела запроса, извлечение username и password из JSON, обработка и закрытие соединения.

Осталось внедрить чат. Изменим JavaScript-скрипт, код поддерживает два основных действия: регулярное обновление списка сообщений с сервера и отправку новых сообщений на сервер. Чат реализован с помощью лонг-пулинга.
JavaScript: Скопировать в буфер обмена
Code:
document.addEventListener('DOMContentLoaded', () => {
 const messagesContainer = document.getElementById('messages');
 const messageInput = document.getElementById('messageInput');
 const sendMessageButton = document.getElementById('sendMessage');

 async function loadMessages() {
  try {
   const response = await fetch('http://localhost:8082/chats.json');
   const jsonMessages = await response.json();

   messagesContainer.innerHTML = '';
   jsonMessages.forEach((msg) => {
    const messageDiv = document.createElement('div');
    messageDiv.className = 'message';
    messageDiv.innerHTML = `
     <strong>${msg.sender}</strong>: ${msg.message}
    `;
    messagesContainer.appendChild(messageDiv);
   });

   setTimeout(loadMessages, 1000);

  } catch (error) {
   console.error('Ошибка загрузки сообщений:', error);
   setTimeout(loadMessages, 2000);
  }
 }

 sendMessageButton.addEventListener('click', async () => {
  const message = messageInput.value.trim();
  if (!message) return;

  const newMessage = {
   sender: 'user',
   message,
   user_id: 'user'
  };

  try {
   const response = await fetch('http://localhost:8082/send_message', {
    method: 'POST',
    headers: {
     'Content-Type': 'application/json'
    },
    body: JSON.stringify(newMessage)
   });

   if (response.ok) {
    messageInput.value = '';
    loadMessages();
   } else {
    console.error('Ошибка отправки сообщения');
   }
  } catch (error) {
   console.error('Ошибка при отправке сообщения:', error);
  }
 });

 loadMessages();
});


Маршруты на сервере и запросы к базе данных в файле chat_handler.c.
C: Скопировать в буфер обмена
Code:
if (strcmp(file_path, "chats.json") == 0) {
 while (1) {
  char *json_data = get_chat_data_as_json();
  if (json_data != NULL) {
   const char *header = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n";
   write(client_socket, header, strlen(header));
   write(client_socket, json_data, strlen(json_data));
   free(json_data);
   break;
     }
  sleep(1);
 }
 close(client_socket);
 return;
}


if (strncmp(buffer, "POST /send_message", 18) == 0) {
 char *json_data = strstr(buffer, "\r\n\r\n") + 4;
 char sender[50], message[500], user_id[50];

 sscanf(json_data, "{\"sender\":\"%49[^\"]\",\"message\":\"%499[^\"]\",\"user_id\":\"%49[^\"]\"}",
   sender, message, user_id);
 save_message(sender, message, user_id);

 const char *response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n";
 write(client_socket, response, strlen(response));

 close(client_socket);
 return;
}
C: Скопировать в буфер обмена
Code:
#include <stdio.h>
#include <stdlib.h> 
#include <string.h>  
#include <sqlite3.h> 

#include "chat_handler.h"

#define DB_PATH "./cats.db"
char* get_chat_data_as_json() {
 sqlite3 *db;
 sqlite3_stmt *stmt;
 char *json_data = malloc(1024 * sizeof(char));
 int buffer_size = 1024, offset = 0;

 if (sqlite3_open(DB_PATH, &db) != SQLITE_OK) {
  fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
  return NULL;
 }

 const char *sql = "SELECT sender, message, user_id FROM chats;";
 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
  fprintf(stderr, "Failed to fetch data: %s\n", sqlite3_errmsg(db));
  sqlite3_close(db);
  return NULL;
 }

 offset += snprintf(json_data + offset, buffer_size - offset, "[");

 while (sqlite3_step(stmt) == SQLITE_ROW) {
  const char *sender = (const char*)sqlite3_column_text(stmt, 0);
  const char *message = (const char*)sqlite3_column_text(stmt, 1);
  const char *user_id = (const char*)sqlite3_column_text(stmt, 2);

  if (offset + 256 > buffer_size) {
   buffer_size *= 2;
   json_data = realloc(json_data, buffer_size);
  }

  offset += snprintf(json_data + offset, buffer_size - offset,
       "{\"sender\":\"%s\",\"message\":\"%s\",\"user_id\":\"%s\"},",
       sender, message, user_id);
 }

 if (offset > 1 && json_data[offset - 1] == ',') {
  offset--;
 }
 snprintf(json_data + offset, buffer_size - offset, "]");

 sqlite3_finalize(stmt);
 sqlite3_close(db);

 return json_data;
}


void save_message(const char *sender, const char *message, const char *user_id) {
 sqlite3 *db;
 sqlite3_stmt *stmt;

 if (sqlite3_open(DB_PATH, &db) != SQLITE_OK) {
  fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
  return;
 }

 const char *sql = "INSERT INTO chats (sender, message, user_id) VALUES (?, ?, ?)";
 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
  fprintf(stderr, "Failed to prepare statement: %s\n", sqlite3_errmsg(db));
  sqlite3_close(db);
  return;
 }

 sqlite3_bind_text(stmt, 1, sender, -1, SQLITE_STATIC);
 sqlite3_bind_text(stmt, 2, message, -1, SQLITE_STATIC);
 sqlite3_bind_text(stmt, 3, user_id, -1, SQLITE_STATIC);

 if (sqlite3_step(stmt) != SQLITE_DONE) {
  fprintf(stderr, "Failed to insert data: %s\n", sqlite3_errmsg(db));
 }

 sqlite3_finalize(stmt);
 sqlite3_close(db);
}

Деплой

Tor (The Onion Router) — это сеть, в которой данные передаются через несколько промежуточных узлов (серверов), каждый из которых «оборачивает» их дополнительным уровнем шифрования, наподобие луковой кожуры (отсюда и название "Onion" — "лук"). Подробнее о работе можно узнать в официальной документации.

Onion-домены (.onion) — это сайты, работающие только внутри сети Tor.

Tor-браузер — это браузер со встроенными механизмами, которые направляют трафик через Tor-сеть.

Как же сделать деплой в сеть Tor? Проще простого. Если сайт состоит только из статических страниц, есть способ деплоя через Onionshare — всё максимально просто, и, если у вас есть графический интерфейс, можно просто drag and drop контент. Впрочем, этот проект предназначен скорее для временной передачи файлов.

Для полноценного деплоя установите Tor: sudo apt install tor. Затем откройте файл /etc/tor/torrc и раскомментируйте строки:

HiddenServiceDir /var/lib/tor/hidden_service/
HiddenServicePort 80 127.0.0.1:80

То, что указано после двоеточия, — это порт, на котором запущен сервер. Если это порт 8082, его нужно сменить на 8082! Также обратите внимание: там есть две закомментированные строки HiddenServiceDir. Вы можете задеплоить на сервере количество сайтов, равное количеству свободных портов (то есть чуть больше 65 тысяч).
Далее перезапустите службу: sudo systemctl restart tor. В зависимости от конфигурации системы переключитесь на root и перейдите в директорию (либо туда, где вы сохранили файл, главное — найти hostname) — /var/lib/tor/hidden_service/hostname.

Здесь будет отображён ваш .onion-адрес.

Несколько моментов, насколько я понимаю сеть Tor (если вы решите деплоить что-то самостоятельно, перепроверьте всё это):

  1. В сети Tor не требуется TLS (HTTPS) для защиты передачи данных, поскольку Tor уже предоставляет сквозное шифрование. Трафик зашифрован от пользователя до сервера и остаётся в пределах сети Tor. Если вы решили запустить доступ к серверу как из интернета, так и через Tor, во-первых, это не рекомендуется документацией, во-вторых, вам может понадобиться TLS.
  2. Onion-адрес сети Tor — это производный идентификатор, основанный на публичном ключе, как в криптовалютах и других областях. Когда вы настраиваете Tor Hidden Service, создаётся пара ключей — **закрытый и публичный ключи**. Эти ключи используются для шифрования и дешифрования данных, передаваемых между клиентом и вашим скрытым сервисом. .onion-адрес формируется на основе хеширования публичного ключа. Этот хеш обрезается и преобразуется в строку, которая и становится вашим .onion-адресом. Таким образом, .onion-адрес связан с вашим публичным ключом, и, зная адрес, Tor-клиенты могут проверить подлинность сервиса при подключении. Файл secret_key — это приватный ключ. Если вы хотите сохранить свой .onion-адрес после переноса или переустановки сервера, сделайте резервную копию файла приватного ключа. Это похоже на восстановление доступа к криптокошельку по приватному ключу.
  3. Необходимо предотвратить раскрытие реального IP-адреса, иначе вся защита теряет смысл. Также важно скрыть использование Tor-сервиса от провайдера. Скрывайте все баннеры, которые могут раскрыть информацию о технологиях (в случае самописного сервера это полностью в наших руках). Рекомендуется использовать UNIX-сокеты вместо TCP-сокетов, блокировать весь DNS-трафик, применять нестандартные порты для Tor. Для скрытия от VPS можно использовать VPN перед Tor (впрочем, об этом нужно ещё хорошо подумать)


Трям! Пока!



Вложения​


  • onion.zip
    758.5 КБ · Просмотры: 4
 
прикольная статья. на фоне десяти тысяч js фреймворков, которые вышли за последние пятнадцать минут, это как глоток свежего воздуха :)
правда мне бы не хватало обмена файлами. ведь юзер может попросить доказать, что кошка настоящая, и надо будет отправить ему 5-10 фоток. хотя если что можно отправить ссылкой :) и еще я бы добавил cors для безопасности, да и новичкам, которые будут учиться по статье, тоже полезно.

язык си люблю-не-могу (шучу, могу). но я бы конечно не рискнул писать такой сервер с нуля на этом языке. а то сам знаешь, как бывает. сегодня ты используешь небезопасную технологию, а завтра ты самый желанный жених в воронеже.
но если кому-то прям очень-очень захочется, советую взглянуть на monkey.
 
jacobFor сказал(а):
Ребят есть сервер хочу проверить к нему подключение через постман есть айпишник и есть логин и пороль как будет производиться проверка подключения как нужно будет составлять запрос(какая дополнительная информация нужна пишите отвечу)
Нажмите, чтобы раскрыть...
Почему ты хочешь проверять подключение используя именно постман?
 
Top