Как спроектировать REST API для выполнения системных команд с помощью Actix Rust


Привет! Сегодня я расскажу, как создать REST API для выполнения системных команд на вашем сервере с помощью метода, которым пользуются известные компании. 

Представьте, что вы переместили свою базу данных на другой сервер, отличный от вашего внутреннего сервера, и хотите теперь управлять сервером баз данных через внутренний API. Для этого нужно создать демона на сервере баз данных.

Шаг первый  —  подбираем зависимости

Начнем с того, что добавим в проект несколько зависимостей:

[dependencies] actix-web = "2.0" actix-rt = "1.0" serde = "1.0.114" simple_logger = "1.6.0" log = "0.4.8" pam = "0.7.0"

Расскажу вкратце об этих зависимостях, а также о том, как они будут использоваться. 

  • Actix  —  это фреймворк для нашего REST API. 
  • serde необходим для сериализации и десериализации. 
  • simple_logger и log нужны для ведения журнала.
  • последний, pam  —  для аутентификации пользователя.

Шаг второй  —  поднимаем и запускаем простой сервер

Буду следовать следующим строкам кода, как советует документация:

Обзор архитектуры

Если все пройдет как надо, вы увидите лог, похожий на мой:

логи терминала

Реализация очень простая, но, полагаю, для базового уровня Actix этого достаточно:

├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── executor │ ├── execute.rs │ └── validate_password.rs └── main.rs └──target

Такое файловое дерево я использую для данного проекта. Надеюсь, вас не смущают моды, которые у меня стоят.

Шаг третий  —  создаем функцию для валидации пользователя

Для этого я применю pam crate. С помощью этого метода вызывающий API может проверить, имеет ли пользователь доступ к серверу или нет:

use actix_web::{post, web, HttpResponse, Responder}; use log::info; use pam::Authenticator; use serde::{Serialize, Deserialize}; #[derive(Deserialize)] pub struct Request { username: String, password: String, } #[derive(Serialize)] pub struct Response { result: bool, } #[post("/validate_password")] pub async fn validate_password(request: web::Json<Request>) -> impl Responder { info!("validating password for {}", request.username); if authenticate(&request.username, &request.password) { HttpResponse::Ok().json(Response { result: true }) } else { HttpResponse::Ok().json(Response { result: false }) } } pub fn authenticate(username: &str, password: &str) -> bool { let mut authenticator = Authenticator::with_password("login").expect("Fail to init with client"); authenticator .get_handler() .set_credentials(username, password); authenticator.authenticate().is_ok() }

Шаг четвертый  —  создаем функцию для исполнения команд

Эта функция получает JSON-запрос с именем пользователя и паролем и возвращать логический результат.

use actix_web::{post, web, HttpResponse, Responder}; use log::info; use serde::{Serialize, Deserialize}; use std::process::Command; use std::io::{self, Write}; #[derive(Deserialize)] pub struct Request { commands: String, } #[derive(Serialize)] pub struct Response { result: bool, } #[post("/execute")] pub async fn execute_command(request: web::Json<Request>) -> impl Responder { info!("validating password for {}", request.commands); let process = Command::new("sh") .arg(&request.commands) .status() .expect("Failed to execute command"); info!("status: {}", &process.to_string()); if process.success() { HttpResponse::Ok().json(Response { result: true }) } else { HttpResponse::Ok().json(Response { result: false }) } }

Оба этих метода будут совместно использовать корневой путь serv.

Шаг пятый  —  создаем основную функцию

Все основные функции реализованы. Теперь нужно установить связь с сервером.

use actix_web::{App, HttpServer, web}; extern crate simple_logger; mod executor { pub mod validate_password; pub mod execute; } #[actix_rt::main] async fn main() -> std::io::Result<()> { simple_logger:: init_with_level(log::Level::Info).unwrap(); HttpServer::new(|| { App::new() .service( web::scope("/serv/") .service(executor::validate_password::validate_password) .service(executor::execute::execute_command) ) }) .workers(10) .keep_alive(15) .bind("127.0.0.1:8088")? .run() .await }

Простое объяснение выполненного

Всё взято из документации.

Метод run() возвращает экземпляр типа Server. Методы этого типа могут применяться для управления http-сервером.

pause()  —  приостанавливает прием входящих соединений.

resume()  —  возобновляет прием входящих соединений.

stop()  —  останавливает обработку входящих соединений, останавливает всех работников и осуществляет выход из системы.

Из-за дерева файлов mod executor должен быть устроен так, как показано ниже. В противном случае созданные нами методы окажутся непригодными.

mod executor { pub mod validate_password; pub mod execute; }

Для каждой из функций созданы две службы. Таким образом, каждая из них будет действовать на адресе 127.0.0.1:8088/serv/.

Здесь определены десять рабочих потоков, вызывающих некоторые системные команды, выполнение которых может занять значительное время. После создания воркеров каждый из них получает отдельный экземпляр приложения для обработки запросов, устраняя время ожидания.

Actix может ждать запросов на поддержание соединения. Поведение соединения keep_alive определяется настройками сервера. Из документации:

  • 75, Some(75), KeepAlive::Timeout(75)  —  таймеру keep alive предоставлено 75 секунд.
  • None или KeepAlive::Disabled  —  keep aliveотключен.
  • KeepAlive::Tcp(75)  —  задействованы параметры сокета SO_KEEPALIVE.

Запустим, наконец, приложение:

cargo run

Заключение

Этот API создавался для удаленного управления серверным API. Поэтому для выполнения Linux-команд не требуется SSH.

Надеюсь, эта статья поможет вам в работе с Rust и Actix. Спасибо, что прочитали!


Перевод статьи Asel Siriwardena, “How to Build a REST API using Actix Rust to Execute System Commands — A Step-by-Step Guide”


Поделиться статьей:


Вернуться к статьям

Комментарии

    Ничего не найдено.