cloudpub_sdk/lib.rs
1//! # CloudPub Rust SDK
2//!
3//! CloudPub Rust SDK предоставляет простой интерфейс для взаимодействия с
4//! платформой CloudPub. CloudPub обеспечивает безопасное туннелирование и публикацию сервисов,
5//! позволяя безопасно открывать локальные сервисы в интернет.
6//!
7//! ## Возможности
8//!
9//! - **Публикация сервисов**: Открытие локальных сервисов (HTTP, HTTPS, TCP, UDP, WebSocket, RTSP) в интернет
10//! - **Безопасное туннелирование**: Все соединения шифруются и аутентифицируются
11//! - **Несколько протоколов**: Поддержка различных протоколов, включая HTTP/HTTPS, TCP/UDP, WebSocket и RTSP
12//! - **Аутентификация**: Встроенная поддержка аутентификации на основе токенов и учетных данных
13//! - **Управление сервисами**: Запуск, остановка, список и управление опубликованными сервисами
14//! - **Событийно-ориентированная архитектура**: Поддержка async/await с управлением состоянием на основе событий
15//!
16//! ## Быстрый старт
17//!
18//! Добавьте SDK в ваш `Cargo.toml`:
19//!
20//! ```toml
21//! [dependencies]
22//! cloudpub-sdk = "2.4.2"
23//! ```
24//!
25//! ## Базовое использование
26//!
27//! ### Простая публикация сервиса
28//!
29//! ```no_run
30//! use cloudpub_sdk::Connection;
31//! use cloudpub_sdk::protocol::{Protocol, Auth, Endpoint};
32//!
33//! #[tokio::main]
34//! async fn main() -> anyhow::Result<()> {
35//! // Создание соединения с учетными данными
36//! let mut conn = Connection::builder()
37//! .credentials("user@example.com", "password")
38//! .build()
39//! .await?;
40//!
41//! // Публикация локального веб-сервиса
42//! let endpoint = conn.publish(
43//! Protocol::Http,
44//! "localhost:3000".to_string(),
45//! Some("My Web App".to_string()),
46//! Some(Auth::None),
47//! None, // acl
48//! None, // headers
49//! None, // rules
50//! ).await?;
51//!
52//! println!("Service published at: {}", endpoint.as_url());
53//!
54//! // Ожидание сигнала Ctrl+C для остановки
55//! tokio::signal::ctrl_c().await?;
56//!
57//! // Очистка
58//! conn.unpublish(endpoint.guid).await?;
59//! Ok(())
60//! }
61//! ```
62//!
63//! ### Расширенная публикация сервиса с ACL, заголовками и правилами фильтрации
64//!
65//! ```no_run
66//! use cloudpub_sdk::Connection;
67//! use cloudpub_sdk::protocol::{
68//! Protocol, Auth, Endpoint, Acl, Header, FilterRule,
69//! Role, FilterAction
70//! };
71//!
72//! #[tokio::main]
73//! async fn main() -> anyhow::Result<()> {
74//! let mut conn = Connection::builder()
75//! .credentials("admin@example.com", "password")
76//! .build()
77//! .await?;
78//!
79//! // Настройка списка контроля доступа
80//! let acl = vec![
81//! Acl { user: "admin@example.com".to_string(), role: Role::Admin as i32 },
82//! Acl { user: "user@example.com".to_string(), role: Role::Reader as i32 },
83//! Acl { user: "writer@example.com".to_string(), role: Role::Writer as i32 },
84//! ];
85//!
86//! // Добавление пользовательских HTTP заголовков
87//! let headers = vec![
88//! Header { name: "X-API-Version".to_string(), value: "2.0".to_string() },
89//! Header { name: "Access-Control-Allow-Origin".to_string(), value: "*".to_string() },
90//! Header { name: "X-Custom-Header".to_string(), value: "CloudPub".to_string() },
91//! ];
92//!
93//! // Определение правил фильтрации для обработки запросов
94//! let rules = vec![
95//! FilterRule {
96//! order: 0,
97//! action_type: FilterAction::FilterAllow as i32,
98//! action_value: None,
99//! data: "http.path starts_with \"/api/\" and http.headers[\"X-API-Key\"] contains \"valid\"".to_string(),
100//! ..Default::default()
101//! },
102//! FilterRule {
103//! order: 1,
104//! action_type: FilterAction::FilterDeny as i32,
105//! action_value: None,
106//! data: "http.path matches \"^/admin.*\" and not (ip.src >= 192.168.1.0 and ip.src <= 192.168.1.255)".to_string(),
107//! ..Default::default()
108//! },
109//! FilterRule {
110//! order: 2,
111//! action_type: FilterAction::FilterRedirect as i32,
112//! action_value: Some("https://api.example.com/v2/".to_string()), // URL перенаправления
113//! data: "http.path starts_with \"/api/v1/\"".to_string(),
114//! ..Default::default()
115//! },
116//! ];
117//!
118//! // Публикация сервиса со всеми расширенными возможностями
119//! let endpoint = conn.publish(
120//! Protocol::Https,
121//! "localhost:8443".to_string(),
122//! Some("Secured API Server".to_string()),
123//! Some(Auth::Basic),
124//! Some(acl),
125//! Some(headers),
126//! Some(rules),
127//! ).await?;
128//!
129//! println!("Advanced service published at: {}", endpoint.as_url());
130//!
131//! Ok(())
132//! }
133//! ```
134//!
135//! ### Расширенная конфигурация
136//!
137//! ```no_run
138//! # async fn example() -> anyhow::Result<()> {
139//! use cloudpub_sdk::Connection;
140//! use std::time::Duration;
141//!
142//! // Создание соединения с расширенной конфигурацией
143//! let conn = Connection::builder()
144//! .config_path("/custom/path/config.toml") // Пользовательский файл конфигурации
145//! .log_level("debug") // Включить отладочное логирование
146//! .verbose(true) // Вывод логов в консоль
147//! .timeout(Duration::from_secs(60)) // Установить таймаут операции
148//! .token("existing-auth-token") // Использовать существующий токен
149//! .build()
150//! .await?;
151//! # Ok(())
152//! # }
153//! ```
154//!
155//! ## Расширенные возможности
156//!
157//! ### Списки контроля доступа (ACL)
158//!
159//! Контроль доступа к вашим сервисам на основе ролей пользователей:
160//!
161//! ```no_run
162//! # use cloudpub_sdk::protocol::{Acl, Role};
163//! let acl = vec![
164//! Acl {
165//! user: "admin@example.com".to_string(),
166//! role: Role::Admin as i32 // Полный доступ
167//! },
168//! Acl {
169//! user: "reader@example.com".to_string(),
170//! role: Role::Reader as i32 // Доступ только на чтение
171//! },
172//! Acl {
173//! user: "writer@example.com".to_string(),
174//! role: Role::Writer as i32 // Доступ на чтение-запись
175//! },
176//! Acl {
177//! user: "guest@example.com".to_string(),
178//! role: Role::Nobody as i32 // Нет доступа
179//! },
180//! ];
181//! ```
182//!
183//! ### Пользовательские HTTP заголовки
184//!
185//! Добавление пользовательских заголовков в HTTP/HTTPS ответы:
186//!
187//! ```no_run
188//! # use cloudpub_sdk::protocol::Header;
189//! let headers = vec![
190//! Header {
191//! name: "X-API-Version".to_string(),
192//! value: "2.0".to_string()
193//! },
194//! Header {
195//! name: "Access-Control-Allow-Origin".to_string(),
196//! value: "*".to_string()
197//! },
198//! Header {
199//! name: "Cache-Control".to_string(),
200//! value: "no-cache, no-store, must-revalidate".to_string()
201//! },
202//! ];
203//! ```
204//!
205//! ### Правила фильтрации
206//!
207//! Определение правил для управления маршрутизацией запросов и доступом на основе различных параметров соединения.
208//! Правила обрабатываются по порядку (по полю `order`), и первое подходящее правило применяется.
209//!
210//! ```no_run
211//! # use cloudpub_sdk::protocol::{FilterRule, FilterAction};
212//! let rules = vec![
213//! // Разрешить доступ к API только из локальной сети
214//! FilterRule {
215//! order: 0,
216//! action_type: FilterAction::FilterAllow as i32,
217//! action_value: None,
218//! data: "ip.src >= 192.168.1.0 and ip.src <= 192.168.1.255 and http.path starts_with \"/api/\"".to_string(),
219//! ..Default::default()
220//! },
221//!
222//! // Блокировать доступ к админ-панели снаружи
223//! FilterRule {
224//! order: 1,
225//! action_type: FilterAction::FilterDeny as i32,
226//! action_value: None,
227//! data: "http.path matches \"^/admin.*\"".to_string(),
228//! ..Default::default()
229//! },
230//!
231//! // Перенаправление старых API эндпоинтов на новую версию
232//! // action_value содержит URL перенаправления для FilterRedirect
233//! FilterRule {
234//! order: 2,
235//! action_type: FilterAction::FilterRedirect as i32,
236//! action_value: Some("https://api.example.com/v2/".to_string()), // Redirect URL
237//! data: "http.path starts_with \"/api/v1/\"".to_string(),
238//! ..Default::default()
239//! },
240//!
241//! // Блокировать запросы из определенных стран
242//! FilterRule {
243//! order: 3,
244//! action_type: FilterAction::FilterDeny as i32,
245//! action_value: None,
246//! data: "geo.country != \"Россия\"".to_string(),
247//! ..Default::default()
248//! },
249//!
250//! // Разрешить только аутентифицированные API запросы
251//! FilterRule {
252//! order: 4,
253//! action_type: FilterAction::FilterAllow as i32,
254//! action_value: None,
255//! data: "http.path starts_with \"/api/\" and http.headers[\"Authorization\"] contains \"Bearer\"".to_string(),
256//! ..Default::default()
257//! },
258//! ];
259//! ```
260//!
261//! **Доступные переменные для фильтр выражений:**
262//! - `ip.src`, `ip.dst` - IP адреса источника и назначения
263//! - `port.src`, `port.dst` - Порты источника и назначения
264//! - `protocol` - Протокол соединения ("tcp", "udp", "http")
265//! - `http.host`, `http.path`, `http.method` - HTTP-специфичные поля
266//! - `http.headers["Header-Name"]` - Доступ к HTTP заголовкам
267//! - `geo.country`, `geo.region`, `geo.city`, `geo.code` - Данные геолокации
268//!
269//! **Операторы:**
270//! - Сравнение: `==`, `!=`, `>`, `<`, `>=`, `<=`
271//! - Строковые: `matches` (regex), `contains`, `starts_with`, `ends_with`
272//! - Логические: `and`, `or`, `not`
273//!
274//! ## Типы сервисов
275//!
276//! CloudPub поддерживает несколько протоколов сервисов:
277//!
278//! ### HTTP/HTTPS сервисы
279//!
280//! ```no_run
281//! # async fn http_example(conn: &mut cloudpub_sdk::Connection) -> anyhow::Result<()> {
282//! use cloudpub_sdk::protocol::{Protocol, Auth, Endpoint};
283//!
284//! // Публикация HTTP сервиса с базовой аутентификацией
285//! let http_endpoint = conn.publish(
286//! Protocol::Http,
287//! "localhost:8080".to_string(),
288//! Some("API Server".to_string()),
289//! Some(Auth::Basic), // Требовать пароль для доступа
290//! None, // acl
291//! None, // headers
292//! None, // rules
293//! ).await?;
294//!
295//! // Публикация HTTPS сервиса
296//! let https_endpoint = conn.publish(
297//! Protocol::Https,
298//! "localhost:8443".to_string(),
299//! Some("Secure API".to_string()),
300//! Some(Auth::Basic), // Требовать токен для доступа
301//! None, // acl
302//! None, // headers
303//! None, // rules
304//! ).await?;
305//! # Ok(())
306//! # }
307//! ```
308//!
309//! ### TCP/UDP сервисы
310//!
311//! ```no_run
312//! # async fn tcp_example(conn: &mut cloudpub_sdk::Connection) -> anyhow::Result<()> {
313//! use cloudpub_sdk::protocol::{Protocol, Auth};
314//!
315//! // Публикация TCP сервиса (например, SSH)
316//! let tcp_endpoint = conn.publish(
317//! Protocol::Tcp,
318//! "localhost:22".to_string(),
319//! Some("SSH Server".to_string()),
320//! Some(Auth::None),
321//! None, // acl
322//! None, // headers
323//! None, // rules
324//! ).await?;
325//!
326//! // Публикация UDP сервиса (например, DNS)
327//! let udp_endpoint = conn.publish(
328//! Protocol::Udp,
329//! "localhost:53".to_string(),
330//! Some("DNS Server".to_string()),
331//! Some(Auth::None),
332//! None, // acl
333//! None, // headers
334//! None, // rules
335//! ).await?;
336//! # Ok(())
337//! # }
338//! ```
339//!
340//! ### WebSocket сервисы
341//!
342//! ```no_run
343//! # async fn ws_example(conn: &mut cloudpub_sdk::Connection) -> anyhow::Result<()> {
344//! use cloudpub_sdk::protocol::{Protocol, Auth};
345//!
346//! // Публикация TCP сервиса
347//! let tcp_endpoint = conn.publish(
348//! Protocol::Tcp,
349//! "localhost:8080".to_string(),
350//! Some("TCP Server".to_string()),
351//! Some(Auth::None),
352//! None, // acl
353//! None, // headers
354//! None, // rules
355//! ).await?;
356//!
357//! // Публикация UDP сервиса
358//! let udp_endpoint = conn.publish(
359//! Protocol::Udp,
360//! "localhost:8443".to_string(),
361//! Some("UDP Server".to_string()),
362//! Some(Auth::Basic),
363//! None, // acl
364//! None, // headers
365//! None, // rules
366//! ).await?;
367//! # Ok(())
368//! # }
369//! ```
370//!
371//! ### RTSP стриминговые сервисы
372//!
373//! ```no_run
374//! # async fn rtsp_example(conn: &mut cloudpub_sdk::Connection) -> anyhow::Result<()> {
375//! use cloudpub_sdk::protocol::{Protocol, Auth};
376//!
377//! // Публикация RTSP потока с учетными данными в URL
378//! // Формат: rtsp://username:password@host:port/path
379//! let rtsp_endpoint = conn.publish(
380//! Protocol::Rtsp,
381//! "rtsp://camera:secret123@192.168.1.100:554/live/stream1".to_string(),
382//! Some("Security Camera".to_string()),
383//! Some(Auth::Basic), // Аутентификация доступа в CloudPub
384//! None, // acl
385//! None, // headers
386//! None, // rules
387//! ).await?;
388//!
389//! // RTSP без учетных данных (публичный поток)
390//! let public_rtsp = conn.publish(
391//! Protocol::Rtsp,
392//! "rtsp://localhost:554/public".to_string(),
393//! Some("Public Stream".to_string()),
394//! Some(Auth::None),
395//! None, // acl
396//! None, // headers
397//! None, // rules
398//! ).await?;
399//! # Ok(())
400//! # }
401//! ```
402//!
403//! ## Управление сервисами
404//!
405//! ### Список сервисов
406//!
407//! ```no_run
408//! # async fn list_example(conn: &mut cloudpub_sdk::Connection) -> anyhow::Result<()> {
409//! use cloudpub_sdk::protocol::Endpoint;
410//! // Получить все зарегистрированные сервисы
411//! let services = conn.ls().await?;
412//!
413//! for service in services {
414//! let name = service.client.as_ref()
415//! .and_then(|c| c.description.clone())
416//! .unwrap_or_else(|| "Без имени".to_string());
417//! println!("Service: {}", name);
418//! println!(" GUID: {}", service.guid);
419//! println!(" URL: {}", service.as_url());
420//! println!(" Status: {}", service.status.unwrap_or_else(|| "Unknown".to_string()));
421//! println!(" Protocol: {:?}", service.remote_proto);
422//! }
423//! # Ok(())
424//! # }
425//! ```
426//!
427//! ### Запуск и остановка сервисов
428//!
429//! ```no_run
430//! # async fn control_example(conn: &mut cloudpub_sdk::Connection) -> anyhow::Result<()> {
431//! // Временно остановить сервис
432//! conn.stop("service-guid-123".to_string()).await?;
433//!
434//! // Перезапустить сервис
435//! conn.start("service-guid-123".to_string()).await?;
436//!
437//! // Постоянно удалить сервис
438//! conn.unpublish("service-guid-123".to_string()).await?;
439//!
440//! // Удалить все сервисы
441//! conn.clean().await?;
442//! # Ok(())
443//! # }
444//! ```
445//!
446//! ## Управление конфигурацией
447//!
448//! ### Чтение и запись опций
449//!
450//! ```no_run
451//! # fn config_example(conn: &cloudpub_sdk::Connection) -> anyhow::Result<()> {
452//! // Установка значений конфигурации
453//! conn.set("server", "api.cloudpub.com")?;
454//! conn.set("port", "443")?;
455//! conn.set("ssl", "true")?;
456//!
457//! // Получение значений конфигурации
458//! let server = conn.get("server")?;
459//! println!("Server: {}", server);
460//!
461//! // Получение всех опций конфигурации
462//! let options = conn.options();
463//! for (key, value) in options {
464//! println!("{}: {}", key, value);
465//! }
466//! # Ok(())
467//! # }
468//! ```
469//!
470//! ### Управление аутентификацией
471//!
472//! ```no_run
473//! # async fn auth_example() -> anyhow::Result<()> {
474//! use cloudpub_sdk::Connection;
475//!
476//! // Аутентификация с учетными данными
477//! let mut conn = Connection::builder()
478//! .credentials("user@example.com", "password")
479//! .build()
480//! .await?;
481//!
482//! // Позже, выход для очистки токена
483//! conn.logout()?;
484//!
485//! // Повторная аутентификация с сохраненным токеном
486//! let conn = Connection::builder()
487//! .token("saved-auth-token")
488//! .build()
489//! .await?;
490//! # Ok(())
491//! # }
492//! ```
493//!
494//! ## Вспомогательные функции
495//!
496//! ### Проверка состояния сервера
497//!
498//! ```no_run
499//! # async fn ping_example(conn: &mut cloudpub_sdk::Connection) -> anyhow::Result<()> {
500//! // Измерение задержки сервера (возвращает микросекунды)
501//! let latency_us = conn.ping().await?;
502//! let latency_ms = latency_us as f64 / 1000.0;
503//! println!("Server latency: {}μs ({:.2}ms)", latency_us, latency_ms);
504//!
505//! if latency_ms > 100.0 {
506//! println!("Warning: High latency detected!");
507//! }
508//! # Ok(())
509//! # }
510//! ```
511//!
512//! ### Управление кэшем
513//!
514//! ```no_run
515//! # fn cache_example(conn: &cloudpub_sdk::Connection) -> anyhow::Result<()> {
516//! // Очистка локального кэша
517//! conn.purge()?;
518//! println!("Cache cleared");
519//! # Ok(())
520//! # }
521//! ```
522//!
523//! ## Обработка ошибок
524//!
525//! Все методы SDK возвращают `Result<T>` для правильной обработки ошибок:
526//!
527//! ```no_run
528//! # async fn error_example() -> anyhow::Result<()> {
529//! use cloudpub_sdk::Connection;
530//!
531//! match Connection::builder()
532//! .credentials("user@example.com", "wrong-password")
533//! .build()
534//! .await
535//! {
536//! Ok(conn) => {
537//! println!("Connected successfully");
538//! },
539//! Err(e) => {
540//! eprintln!("Connection failed: {}", e);
541//! // Обработка специфичных типов ошибок
542//! if e.to_string().contains("authentication") {
543//! eprintln!("Authentication failed. Please check your credentials.");
544//! }
545//! }
546//! }
547//! # Ok(())
548//! # }
549//! ```
550//!
551//! ## Полный пример
552//!
553//! Вот полный пример, демонстрирующий полный жизненный цикл с расширенными возможностями:
554//!
555//! ```no_run
556//! use cloudpub_sdk::Connection;
557//! use cloudpub_sdk::protocol::{
558//! Protocol, Auth, Endpoint, Acl, Header, FilterRule,
559//! Role, FilterAction
560//! };
561//! use std::time::Duration;
562//! use tokio::time::sleep;
563//!
564//! #[tokio::main]
565//! async fn main() -> anyhow::Result<()> {
566//! // Инициализация соединения с конфигурацией
567//! println!("Connecting to CloudPub...");
568//! let mut conn = Connection::builder()
569//! .credentials("admin@example.com", "secure-password")
570//! .log_level("info")
571//! .verbose(true)
572//! .timeout_secs(30)
573//! .build()
574//! .await?;
575//!
576//! println!("Connected successfully!");
577//!
578//! // Публикация нескольких сервисов
579//! println!("\nPublishing services...");
580//!
581//! // Простой веб-сервис без расширенных возможностей
582//! let web_service = conn.publish(
583//! Protocol::Http,
584//! "localhost:3000".to_string(),
585//! Some("Web Application".to_string()),
586//! Some(Auth::None),
587//! None, // Нет ACL ограничений
588//! None, // Нет пользовательских заголовков
589//! None, // Нет правил фильтрации
590//! ).await?;
591//! println!("✓ Web app: {}", web_service.as_url());
592//!
593//! // API сервер с ACL и пользовательскими заголовками
594//! let api_acl = vec![
595//! Acl { user: "api_admin@example.com".to_string(), role: Role::Admin as i32 },
596//! Acl { user: "api_user@example.com".to_string(), role: Role::Reader as i32 },
597//! ];
598//! let api_headers = vec![
599//! Header { name: "X-API-Version".to_string(), value: "2.0".to_string() },
600//! Header { name: "Access-Control-Allow-Origin".to_string(), value: "*".to_string() },
601//! ];
602//! let api_service = conn.publish(
603//! Protocol::Https,
604//! "localhost:8443".to_string(),
605//! Some("API Server".to_string()),
606//! Some(Auth::Basic),
607//! Some(api_acl),
608//! Some(api_headers),
609//! None, // Нет правил фильтрации
610//! ).await?;
611//! println!("✓ API server: {}", api_service.as_url());
612//!
613//! // SSH сервис с правилами фильтрации для безопасности
614//! let ssh_rules = vec![
615//! FilterRule {
616//! order: 0,
617//! action_type: FilterAction::FilterAllow as i32,
618//! action_value: None,
619//! data: "ip.src == 192.168.1.10 or ip.src == 10.0.0.5".to_string(), // Разрешить только определенные IP
620//! ..Default::default()
621//! },
622//! FilterRule {
623//! order: 1,
624//! action_type: FilterAction::FilterDeny as i32,
625//! action_value: None,
626//! data: "ip.src matches \".*\"".to_string(), // Заблокировать все остальные IP
627//! ..Default::default()
628//! },
629//! ];
630//! let ssh_service = conn.publish(
631//! Protocol::Tcp,
632//! "localhost:22".to_string(),
633//! Some("SSH Access".to_string()),
634//! Some(Auth::Basic),
635//! None, // Нет ACL
636//! None, // Нет заголовков (TCP не использует HTTP заголовки)
637//! Some(ssh_rules),
638//! ).await?;
639//! println!("✓ SSH: {}", ssh_service.as_url());
640//!
641//! // Проверка состояния сервера
642//! println!("\nChecking server health...");
643//! let latency_us = conn.ping().await?;
644//! println!("Server latency: {}μs ({:.2}ms)", latency_us, latency_us as f64 / 1000.0);
645//!
646//! // Список всех сервисов
647//! println!("\nActive services:");
648//! let services = conn.ls().await?;
649//! for (i, service) in services.iter().enumerate() {
650//! let name = service.client.as_ref()
651//! .and_then(|c| c.description.clone())
652//! .unwrap_or_else(|| "Без имени".to_string());
653//! println!("{}. {} - {}",
654//! i + 1,
655//! name,
656//! service.as_url()
657//! );
658//! }
659//!
660//! // Работать некоторое время
661//! println!("\nServices are running. Press Ctrl+C to stop...");
662//! tokio::select! {
663//! _ = tokio::signal::ctrl_c() => {
664//! println!("\nShutting down...");
665//! }
666//! _ = sleep(Duration::from_secs(3600)) => {
667//! println!("\nTimeout reached");
668//! }
669//! }
670//!
671//! // Очистка
672//! println!("Очистка сервисов...");
673//! for service in services {
674//! conn.unpublish(service.guid).await?;
675//! let name = service.client.as_ref()
676//! .and_then(|c| c.description.clone())
677//! .unwrap_or_else(|| "Без имени".to_string());
678//! println!("✓ Removed: {}", name);
679//! }
680//!
681//! println!("All services stopped. Goodbye!");
682//! Ok(())
683//! }
684//! ```
685//! ## Потокобезопасность
686//!
687//! Структура `Connection` использует внутреннюю синхронизацию и может безопасно использоваться
688//! между потоками через `Arc<Mutex<Connection>>` при необходимости.
689//!
690//! ## Лицензия
691//!
692//! Этот SDK лицензирован под лицензией Apache 2.0.
693
694pub use cloudpub_client::config::ClientOpts;
695pub use cloudpub_common::protocol;
696
697mod builder;
698mod connection;
699
700pub use builder::ConnectionBuilder;
701pub use connection::{CheckSignalFn, Connection, ConnectionEvent};