Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ pub mod rfb;
pub mod mqtt;
pub mod pgsql;
pub mod telnet;
pub mod websockets;
pub mod applayertemplate;
pub mod rdp;
pub mod x509;
Expand Down
55 changes: 55 additions & 0 deletions rust/src/websockets/logger.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* Copyright (C) 2023 Open Information Security Foundation
*
* You can copy, redistribute or modify this Program under the terms of
* the GNU General Public License version 2 as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/

use super::websockets::WebSocketsTransaction;
use crate::jsonbuilder::{JsonBuilder, JsonError};
use std;

//TODOws detection on opcode and mask, and payload buffer
//TODOws json schema + SV test

fn ws_opcode_string(p: u8) -> Option<&'static str> {
match p {
0 => Some("continuation"),
1 => Some("text"),
2 => Some("binary"),
8 => Some("connection_close"),
9 => Some("ping"),
0xa => Some("pong"),
_ => None,
}
}

fn log_websockets(tx: &WebSocketsTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> {
js.open_object("websockets")?;
js.set_bool("mask", tx.pdu.mask)?;
if let Some(val) = ws_opcode_string(tx.pdu.opcode) {
js.set_string("opcode", val)?;
} else {
js.set_string("opcode", &format!("unknown-{}", tx.pdu.opcode))?;
}
js.close()?;
Ok(())
}

#[no_mangle]
pub unsafe extern "C" fn rs_websockets_logger_log(
tx: *mut std::os::raw::c_void, js: &mut JsonBuilder,
) -> bool {
let tx = cast_pointer!(tx, WebSocketsTransaction);
log_websockets(tx, js).is_ok()
}
22 changes: 22 additions & 0 deletions rust/src/websockets/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* Copyright (C) 2023 Open Information Security Foundation
*
* You can copy, redistribute or modify this Program under the terms of
* the GNU General Public License version 2 as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/

//! Application layer websockets parser and logger module.

pub mod logger;
mod parser;
pub mod websockets;
63 changes: 63 additions & 0 deletions rust/src/websockets/parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* Copyright (C) 2023 Open Information Security Foundation
*
* You can copy, redistribute or modify this Program under the terms of
* the GNU General Public License version 2 as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/

use nom7::bytes::streaming::take;
use nom7::combinator::cond;
use nom7::number::streaming::{be_u16, be_u64, be_u8};
use nom7::IResult;

#[derive(Clone, Debug, Default)]
pub struct WebSocketsPdu {
pub fin: bool,
pub opcode: u8,
pub mask: bool,
pub payload: Vec<u8>,
}

// cf rfc6455#section-5.2
pub fn parse_message(i: &[u8]) -> IResult<&[u8], WebSocketsPdu> {
let (i, fin_op) = be_u8(i)?;
let fin = (fin_op & 0x80) != 0;
let opcode = fin_op & 0xF;
let (i, mask_plen) = be_u8(i)?;
let mask = (mask_plen & 0x80) != 0;
let (i, payload_len) = match mask_plen & 0x7F {
126 => {
let (i, val) = be_u16(i)?;
Ok((i, val.into()))
}
127 => be_u64(i),
_ => Ok((i, (mask_plen & 0x7F).into())),
}?;
let (i, xormask) = cond(mask, take(4usize))(i)?;
let (i, payload_raw) = take(payload_len)(i)?;
let mut payload = payload_raw.to_vec();
if let Some(xorkey) = xormask {
for i in 0..payload.len() {
payload[i] = payload[i] ^ xorkey[i % 4];
}
}
Ok((
i,
WebSocketsPdu {
fin,
opcode,
mask,
payload,
},
))
}
244 changes: 244 additions & 0 deletions rust/src/websockets/websockets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/* Copyright (C) 2023 Open Information Security Foundation
*
* You can copy, redistribute or modify this Program under the terms of
* the GNU General Public License version 2 as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/

use super::parser;
use crate::applayer::{self, *};
use crate::core::{AppProto, Direction, Flow, ALPROTO_UNKNOWN, IPPROTO_TCP};
use nom7 as nom;
use std;
use std::collections::VecDeque;
use std::ffi::CString;
use std::os::raw::{c_char, c_int, c_void};

static mut ALPROTO_WEBSOCKETS: AppProto = ALPROTO_UNKNOWN;

#[derive(Default)]
pub struct WebSocketsTransaction {
tx_id: u64,
pub pdu: parser::WebSocketsPdu,
tx_data: AppLayerTxData,
}

impl WebSocketsTransaction {
pub fn new(direction: Direction) -> WebSocketsTransaction {
Self {
tx_data: AppLayerTxData::for_direction(direction),
..Default::default()
}
}
}

impl Transaction for WebSocketsTransaction {
fn id(&self) -> u64 {
self.tx_id
}
}

#[derive(Default)]
pub struct WebSocketsState {
state_data: AppLayerStateData,
tx_id: u64,
transactions: VecDeque<WebSocketsTransaction>,
}

impl State<WebSocketsTransaction> for WebSocketsState {
fn get_transaction_count(&self) -> usize {
self.transactions.len()
}

fn get_transaction_by_index(&self, index: usize) -> Option<&WebSocketsTransaction> {
self.transactions.get(index)
}
}

impl WebSocketsState {
pub fn new() -> Self {
Default::default()
}

// Free a transaction by ID.
fn free_tx(&mut self, tx_id: u64) {
let len = self.transactions.len();
let mut found = false;
let mut index = 0;
for i in 0..len {
let tx = &self.transactions[i];
if tx.tx_id == tx_id + 1 {
found = true;
index = i;
break;
}
}
if found {
self.transactions.remove(index);
}
}

pub fn get_tx(&mut self, tx_id: u64) -> Option<&WebSocketsTransaction> {
self.transactions.iter().find(|tx| tx.tx_id == tx_id + 1)
}

fn new_tx(&mut self, direction: Direction) -> WebSocketsTransaction {
let mut tx = WebSocketsTransaction::new(direction);
self.tx_id += 1;
tx.tx_id = self.tx_id;
return tx;
}

fn parse(&mut self, input: &[u8], direction: Direction) -> AppLayerResult {
let mut start = input;
while !start.is_empty() {
match parser::parse_message(start) {
Ok((rem, pdu)) => {
start = rem;
let mut tx = self.new_tx(direction);
tx.pdu = pdu;
//TODOws should we reassemble/stream payload data ?
self.transactions.push_back(tx);
}
Err(nom::Err::Incomplete(_)) => {
// Not enough data. just ask for one more byte.
let consumed = input.len() - start.len();
let needed = start.len() + 1;
return AppLayerResult::incomplete(consumed as u32, needed as u32);
}
Err(_) => {
return AppLayerResult::err();
}
}
}
// Input was fully consumed.
return AppLayerResult::ok();
}
}

// C exports.

extern "C" fn rs_websockets_state_new(
_orig_state: *mut c_void, _orig_proto: AppProto,
) -> *mut c_void {
let state = WebSocketsState::new();
let boxed = Box::new(state);
return Box::into_raw(boxed) as *mut c_void;
}

unsafe extern "C" fn rs_websockets_state_free(state: *mut c_void) {
std::mem::drop(Box::from_raw(state as *mut WebSocketsState));
}

unsafe extern "C" fn rs_websockets_state_tx_free(state: *mut c_void, tx_id: u64) {
let state = cast_pointer!(state, WebSocketsState);
state.free_tx(tx_id);
}

unsafe extern "C" fn rs_websockets_parse_request(
_flow: *const Flow, state: *mut c_void, _pstate: *mut c_void, stream_slice: StreamSlice,
_data: *const c_void,
) -> AppLayerResult {
let state = cast_pointer!(state, WebSocketsState);
let buf = stream_slice.as_slice();
state.parse(buf, Direction::ToServer)
}

unsafe extern "C" fn rs_websockets_parse_response(
_flow: *const Flow, state: *mut c_void, _pstate: *mut c_void, stream_slice: StreamSlice,
_data: *const c_void,
) -> AppLayerResult {
let state = cast_pointer!(state, WebSocketsState);
let buf = stream_slice.as_slice();
state.parse(buf, Direction::ToClient)
}

unsafe extern "C" fn rs_websockets_state_get_tx(state: *mut c_void, tx_id: u64) -> *mut c_void {
let state = cast_pointer!(state, WebSocketsState);
match state.get_tx(tx_id) {
Some(tx) => {
return tx as *const _ as *mut _;
}
None => {
return std::ptr::null_mut();
}
}
}

unsafe extern "C" fn rs_websockets_state_get_tx_count(state: *mut c_void) -> u64 {
let state = cast_pointer!(state, WebSocketsState);
return state.tx_id;
}

unsafe extern "C" fn rs_websockets_tx_get_alstate_progress(
_tx: *mut c_void, _direction: u8,
) -> c_int {
return 1;
}

export_tx_data_get!(rs_websockets_get_tx_data, WebSocketsTransaction);
export_state_data_get!(rs_websockets_get_state_data, WebSocketsState);

// Parser name as a C style string.
const PARSER_NAME: &[u8] = b"websockets\0";

#[no_mangle]
pub unsafe extern "C" fn rs_websockets_register_parser() {
let parser = RustParser {
name: PARSER_NAME.as_ptr() as *const c_char,
default_port: std::ptr::null(),
ipproto: IPPROTO_TCP,
probe_ts: None,
probe_tc: None,
min_depth: 0,
max_depth: 16,
state_new: rs_websockets_state_new,
state_free: rs_websockets_state_free,
tx_free: rs_websockets_state_tx_free,
parse_ts: rs_websockets_parse_request,
parse_tc: rs_websockets_parse_response,
get_tx_count: rs_websockets_state_get_tx_count,
get_tx: rs_websockets_state_get_tx,
tx_comp_st_ts: 1,
tx_comp_st_tc: 1,
tx_get_progress: rs_websockets_tx_get_alstate_progress,
get_eventinfo: None,
get_eventinfo_byid: None,
localstorage_new: None,
localstorage_free: None,
get_tx_files: None,
get_tx_iterator: Some(
applayer::state_get_tx_iterator::<WebSocketsState, WebSocketsTransaction>,
),
get_tx_data: rs_websockets_get_tx_data,
get_state_data: rs_websockets_get_state_data,
apply_tx_config: None,
flags: 0, // do not accept gaps as there is no good way to resync
truncate: None,
get_frame_id_by_name: None, //TODOws
get_frame_name_by_id: None,
};

let ip_proto_str = CString::new("tcp").unwrap();

if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
let alproto = AppLayerRegisterProtocolDetection(&parser, 1);
ALPROTO_WEBSOCKETS = alproto;
if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
let _ = AppLayerRegisterParser(&parser, alproto);
}
SCLogDebug!("Rust websockets parser registered.");
} else {
SCLogDebug!("Protocol detector and parser disabled for WEBSOCKETS.");
}
}
Loading