grable
This commit is contained in:
commit
420e85a64d
|
@ -0,0 +1,6 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"api",
|
||||
"common",
|
||||
"websocket-worker"
|
||||
]
|
|
@ -0,0 +1,13 @@
|
|||
[server]
|
||||
bind = "0.0.0.0"
|
||||
port = 8171
|
||||
base_url = "http://localhost:8171/"
|
||||
|
||||
[auth]
|
||||
tokens = [
|
||||
{ "token" = "test-ro-token", can_write = false },
|
||||
{ "token" = "test-rw-token", can_write = true }
|
||||
]
|
||||
|
||||
[log]
|
||||
log_file = "chat_log.log"
|
|
@ -0,0 +1,34 @@
|
|||
use std::net::IpAddr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub server: ServerConfig,
|
||||
pub auth: AuthConfig,
|
||||
pub log: LogConfig
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ServerConfig {
|
||||
pub bind: IpAddr,
|
||||
pub port: u16,
|
||||
pub base_url: Url
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct AuthConfig {
|
||||
pub tokens: Vec<Token>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Token {
|
||||
pub token: String,
|
||||
pub can_write: bool
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct LogConfig {
|
||||
pub log_file: String
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, StreamHandler};
|
||||
use actix_web::{Error, HttpRequest, HttpResponse, web};
|
||||
use actix_web::web::Data;
|
||||
use actix_web_actors::ws;
|
||||
use actix_web_actors::ws::{CloseCode, CloseReason, Message, ProtocolError};
|
||||
use log::{debug, error, info, warn};
|
||||
use common::message::{DisconnectReason, GatewayChatMessage, GatewayPacketC2S, GatewayPacketS2C};
|
||||
use crate::config::{Config, Token};
|
||||
use crate::gateway::WsState::Handshaking;
|
||||
|
||||
struct GatewayInterthreadMessage(GatewayChatMessage);
|
||||
|
||||
pub enum WsState {
|
||||
Handshaking,
|
||||
Relay
|
||||
}
|
||||
|
||||
pub struct WsClient {
|
||||
remote_addr: SocketAddr,
|
||||
addr: Addr<WsActor>
|
||||
}
|
||||
|
||||
pub struct WsActor {
|
||||
config: Config,
|
||||
clients: Arc<RwLock<Vec<WsClient>>>,
|
||||
remote_addr: SocketAddr,
|
||||
state: WsState,
|
||||
write_perms: bool,
|
||||
log: Arc<RwLock<File>>
|
||||
}
|
||||
|
||||
impl Actor for WsActor {
|
||||
type Context = ws::WebsocketContext<Self>;
|
||||
}
|
||||
|
||||
impl actix::Message for GatewayInterthreadMessage {
|
||||
type Result = Result<(), ()>;
|
||||
}
|
||||
|
||||
impl Handler<GatewayInterthreadMessage> for WsActor {
|
||||
type Result = Result<(), ()>;
|
||||
|
||||
fn handle(&mut self, msg: GatewayInterthreadMessage, ctx: &mut Self::Context) -> Self::Result {
|
||||
info!("received message from another thread, relaying it");
|
||||
ctx.text(serde_json::to_string(&GatewayPacketS2C::Relayed {
|
||||
msg: msg.0
|
||||
}).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// {"Authenticate":{"token":"test-rw-token","request_write_perms":true}}
|
||||
// {"Relay":{"msg":{"source":"Discord","username":"@realcore","message":"ello","timestamp":"2023-07-20T01:58:36+00:00"}}}
|
||||
|
||||
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsActor {
|
||||
fn handle(&mut self, msg: Result<Message, ProtocolError>, ctx: &mut Self::Context) {
|
||||
match msg {
|
||||
Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
|
||||
Ok(ws::Message::Text(text)) => {
|
||||
// attempt to decode the packet
|
||||
let packet: GatewayPacketC2S = match serde_json::from_str(&text) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
error!("packet decode error: {}", e);
|
||||
info!("disconnecting client for an illegal packet");
|
||||
ctx.text(serde_json::to_string(&GatewayPacketS2C::Disconnect {
|
||||
reason: DisconnectReason::IllegalPacket
|
||||
}).unwrap());
|
||||
ctx.close(Some(CloseReason::from(CloseCode::Abnormal)));
|
||||
ctx.stop();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match self.state {
|
||||
Handshaking => {
|
||||
match packet {
|
||||
GatewayPacketC2S::Authenticate { token, request_write_perms } => {
|
||||
info!("client authenticating with {}", token);
|
||||
|
||||
if !self.config.auth.tokens.iter().any(|u| u.token == token) {
|
||||
warn!("client authenticated with invalid token {}", token);
|
||||
info!("disconnecting client for failed authentication");
|
||||
ctx.text(serde_json::to_string(&GatewayPacketS2C::Disconnect {
|
||||
reason: DisconnectReason::AuthenticationRejected
|
||||
}).unwrap());
|
||||
ctx.close(Some(CloseReason::from(CloseCode::Abnormal)));
|
||||
ctx.stop();
|
||||
}
|
||||
|
||||
let can_write = self.config.auth.tokens.iter().find(|u| u.token == token).unwrap().can_write;
|
||||
|
||||
if request_write_perms && !can_write {
|
||||
warn!("client requested write perms on token {} that is read-only, ignoring", token);
|
||||
}
|
||||
|
||||
self.write_perms = request_write_perms && can_write;
|
||||
self.state = WsState::Relay;
|
||||
|
||||
ctx.text(serde_json::to_string(&GatewayPacketS2C::AuthenticationAccepted {
|
||||
write_perms: self.write_perms,
|
||||
}).unwrap());
|
||||
}
|
||||
_ => {
|
||||
error!("recieved non-Authenticate packet during Handshaking stage");
|
||||
info!("disconnecting client for an illegal packet");
|
||||
ctx.text(serde_json::to_string(&GatewayPacketS2C::Disconnect {
|
||||
reason: DisconnectReason::IllegalPacket
|
||||
}).unwrap());
|
||||
ctx.close(Some(CloseReason::from(CloseCode::Abnormal)));
|
||||
ctx.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
WsState::Relay => {
|
||||
match packet {
|
||||
GatewayPacketC2S::Relay { msg } => {
|
||||
info!("relaying message {:?}", msg);
|
||||
{
|
||||
let mut buf = serde_json::to_vec(&msg).unwrap();
|
||||
buf.push(b'\n');
|
||||
self.log.write().unwrap().write_all(&buf).unwrap();
|
||||
}
|
||||
{
|
||||
for client in self.clients.read().unwrap().iter() {
|
||||
if client.remote_addr == self.remote_addr { continue; }
|
||||
client.addr.try_send(GatewayInterthreadMessage(msg.clone())).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
GatewayPacketC2S::Disconnect { .. } => {
|
||||
info!("client disconnecting");
|
||||
ctx.close(Some(CloseReason::from(CloseCode::Normal)));
|
||||
ctx.stop();
|
||||
},
|
||||
_ => {
|
||||
error!("received non-Relay/Disconnect packet during Handshaking stage");
|
||||
info!("disconnecting client for an illegal packet");
|
||||
ctx.text(serde_json::to_string(&GatewayPacketS2C::Disconnect {
|
||||
reason: DisconnectReason::IllegalPacket
|
||||
}).unwrap());
|
||||
ctx.close(Some(CloseReason::from(CloseCode::Abnormal)));
|
||||
ctx.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Ok(ws::Message::Close(_)) => {
|
||||
ctx.stop();
|
||||
},
|
||||
_ => {
|
||||
warn!("recv unknown websocket type: {:?}", msg);
|
||||
info!("disconnecting client for an illegal packet");
|
||||
ctx.text(serde_json::to_string(&GatewayPacketS2C::Disconnect {
|
||||
reason: DisconnectReason::IllegalPacket
|
||||
}).unwrap());
|
||||
ctx.close(Some(CloseReason::from(CloseCode::Abnormal)));
|
||||
ctx.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
self.clients.write().unwrap().push(WsClient {
|
||||
remote_addr: self.remote_addr,
|
||||
addr: ctx.address(),
|
||||
})
|
||||
}
|
||||
|
||||
fn finished(&mut self, ctx: &mut Self::Context) {
|
||||
let pos = { self.clients.write().unwrap().iter().position(|u| u.remote_addr == self.remote_addr) }.unwrap();
|
||||
self.clients.write().unwrap().remove(pos);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn gateway(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
|
||||
let config: &Data<Config> = req.app_data().unwrap();
|
||||
let clients: &Data<Arc<RwLock<Vec<WsClient>>>> = req.app_data().unwrap();
|
||||
let log: &Data<Arc<RwLock<File>>> = req.app_data().unwrap();
|
||||
|
||||
let resp = ws::start(WsActor {
|
||||
config: config.as_ref().clone(),
|
||||
clients: clients.get_ref().clone(),
|
||||
remote_addr: req.peer_addr().unwrap(),
|
||||
state: Handshaking,
|
||||
write_perms: false,
|
||||
log: log.as_ref().clone()
|
||||
}, &req, stream);
|
||||
|
||||
debug!("{:?}", resp);
|
||||
|
||||
resp
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
use actix_web::get;
|
||||
use actix_web::web::{Data, Json};
|
||||
use common::status::{DATA_API_VERSION, Status};
|
||||
use crate::config::Config;
|
||||
|
||||
#[get("/status")]
|
||||
pub async fn status_endpoint(config: Data<Config>) -> Json<Status> {
|
||||
Json(Status {
|
||||
data_api_version: DATA_API_VERSION,
|
||||
gateway_url: config.server.base_url.join("/gateway").unwrap(),
|
||||
status_url: config.server.base_url.join("/status").unwrap()
|
||||
})
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct GatewayChatMessage {
|
||||
pub source: GatewayChatSource,
|
||||
pub username: String,
|
||||
pub message: String,
|
||||
pub timestamp: DateTime<Utc>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum GatewayChatSource {
|
||||
Discord,
|
||||
Minecraft
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum GatewayPacketC2S {
|
||||
Authenticate { token: String, request_write_perms: bool },
|
||||
Relay { msg: GatewayChatMessage },
|
||||
Disconnect { reason: DisconnectReason }
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum GatewayPacketS2C {
|
||||
AuthenticationAccepted { write_perms: bool },
|
||||
Disconnect { reason: DisconnectReason },
|
||||
Relayed { msg: GatewayChatMessage }
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum DisconnectReason {
|
||||
AuthenticationRejected,
|
||||
IllegalPacket,
|
||||
AllDone
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
pub const DATA_API_VERSION: u64 = 1;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Status {
|
||||
pub data_api_version: u64,
|
||||
pub status_url: Url,
|
||||
pub gateway_url: Url
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
websockets = [
|
||||
"https://discord.com/api/webhooks/1130340476797583380/0RBp48jK4x3qjKYcmypjw4ydm3GoK2r_D-yzz95b5cnBHtq8lsFx66kJnmniIVAe8H4u"
|
||||
]
|
||||
api_status_url = "http://localhost:8171/status"
|
||||
workers = 8
|
||||
token = "test-ro-token"
|
|
@ -0,0 +1,10 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub websockets: Vec<String>,
|
||||
pub api_status_url: Url,
|
||||
pub workers: usize,
|
||||
pub token: String
|
||||
}
|
Loading…
Reference in New Issue