d3270d now manages to connect and login with commands from netcat

This commit is contained in:
2023-05-11 18:04:05 +02:00
parent 49c30aad34
commit 962019e310
10 changed files with 155 additions and 50 deletions

67
Cargo.lock generated
View File

@@ -653,6 +653,7 @@ dependencies = [
"tokio-stream",
"tracing",
"tracing-fmt",
"tracing-subscriber 0.3.17",
]
[[package]]
@@ -1150,6 +1151,15 @@ dependencies = [
"regex-automata",
]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata",
]
[[package]]
name = "maybe-uninit"
version = "2.0.0"
@@ -1174,6 +1184,16 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-integer"
version = "0.1.45"
@@ -1215,6 +1235,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "owning_ref"
version = "0.4.1"
@@ -1636,6 +1662,15 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "sharded-slab"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
dependencies = [
"lazy_static",
]
[[package]]
name = "signal-hook"
version = "0.3.15"
@@ -1828,6 +1863,16 @@ dependencies = [
"syn 2.0.15",
]
[[package]]
name = "thread_local"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
dependencies = [
"cfg-if 1.0.0",
"once_cell",
]
[[package]]
name = "tide"
version = "0.16.0"
@@ -2027,7 +2072,7 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "880547feb88739526e322a366be2411c41c797f0dabcddfe99a3216e5a664f71"
dependencies = [
"tracing-subscriber",
"tracing-subscriber 0.1.6",
]
[[package]]
@@ -2050,7 +2095,7 @@ dependencies = [
"ansi_term",
"chrono",
"lazy_static",
"matchers",
"matchers 0.0.1",
"owning_ref",
"regex",
"smallvec 0.6.14",
@@ -2058,6 +2103,24 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
dependencies = [
"matchers 0.1.0",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec 1.10.0",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
name = "tungstenite"
version = "0.13.0"

View File

@@ -11,6 +11,7 @@ pub mod operation;
pub mod types;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all="kebab-case")]
pub enum Indication {
Bell {}, // TODO: make sure this emits/parses {"bell": {}}
/// Indicates that the host connection has changed state.
@@ -61,8 +62,6 @@ pub enum Indication {
Setting(Setting),
/// I/O statistics
Stats(Stats),
/// Reports the terminal name sent to the host during TELNET negotiation
TerminalName(TerminalName),
/// Change in the scrollbar thumb
Thumb(Thumb),
/// Indicates the name of the trace file
@@ -101,6 +100,8 @@ pub enum InitializeIndication {
ScreenMode(ScreenMode),
/// Setting changed
Setting(Setting),
/// Reports the terminal name sent to the host during TELNET negotiation
TerminalName(TerminalName),
/// Scroll thumb position
Thumb(Thumb),
/// Indicates build-time TLS config

View File

@@ -61,12 +61,12 @@ pub enum ConnectionState {
TelnetPending,
ConnectedNvt,
ConnectedNvtCharmode,
#[serde(rename="connected-3270")]
Connected3270,
ConnectedUnbound,
ConnectedENvt,
ConnectedESscp,
#[serde(rename = "connected-e-tn3270e")]
ConnectedETn3270e,
ConnectedSscp,
ConnectedTn3270e,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
@@ -154,7 +154,7 @@ pub enum OiaField {
},
/// Host command timer (minutes:seconds)
Script {
value: String,
value: bool,
},
Timing {
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -200,8 +200,9 @@ impl OiaField {
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct Proxy {
pub name: String,
pub username: String,
pub port: u16,
pub username: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub port: Option<u16>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
@@ -209,14 +210,15 @@ pub struct Setting {
pub name: String,
/// I'd love something other than depending on serde_json for this.
pub value: Option<serde_json::Value>,
pub cause: ActionCause,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cause: Option<ActionCause>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)]
pub struct ScreenMode {
pub model: u8,
pub rows: u8,
pub cols: u8,
pub columns: u8,
pub color: bool,
pub oversize: bool,
pub extended: bool,
@@ -284,7 +286,7 @@ pub enum FileTransferState {
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename = "kebab-case")]
#[serde(rename_all = "kebab-case")]
pub struct Passthru {
pub p_tag: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -304,7 +306,7 @@ pub struct Popup {
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)]
#[serde(rename = "kebab-case")]
#[serde(rename_all = "kebab-case")]
pub enum PopupType {
/// Error message from a connection attempt
ConnectError,
@@ -321,14 +323,14 @@ pub enum PopupType {
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename = "kebab-case")]
#[serde(rename_all = "kebab-case")]
pub struct Row {
pub row: u8,
pub changes: Vec<Change>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename = "kebab-case")]
#[serde(rename_all = "kebab-case")]
pub enum CountOrText {
Count(usize),
Text(String),
@@ -357,7 +359,7 @@ pub struct Screen {
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename = "kebab-case")]
#[serde(rename_all = "kebab-case")]
pub struct RunResult {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub r_tag: Option<String>,
@@ -371,7 +373,7 @@ pub struct RunResult {
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename = "kebab-case")]
#[serde(rename_all = "kebab-case")]
pub struct Scroll {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fg: Option<Color>,
@@ -380,7 +382,7 @@ pub struct Scroll {
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename = "kebab-case")]
#[serde(rename_all = "kebab-case")]
pub struct Stats {
pub bytes_received: usize,
pub bytes_sent: usize,
@@ -435,12 +437,23 @@ mod test {
#[test]
pub fn connection_state_serializes_as_expected() {
assert_eq!(
serde_json::to_string(&ConnectionState::ConnectedETn3270e).unwrap(),
r#""connected-e-tn3270e""#
serde_json::to_string(&ConnectionState::ConnectedTn3270e).unwrap(),
r#""connected-tn3270e""#
);
assert_eq!(
serde_json::to_string(&ConnectionState::ConnectedESscp).unwrap(),
r#""connected-e-sscp""#
serde_json::to_string(&ConnectionState::ConnectedSscp).unwrap(),
r#""connected-sscp""#
);
}
}
fn parse_row() {
let instr = r#"[{"row":1,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","text":"z/OS V1R13 PUT Level 1401"},{"column":26,"fg":"red","gr":"highlight,selectable","count":26},{"column":52,"fg":"red","gr":"highlight,selectable","text":"IP Address = 10.24.74.32 "}]},{"row":2,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":51},{"column":52,"fg":"red","gr":"highlight,selectable","text":"VTAM Terminal = SC0TCP05 "}]},{"row":3,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":80}]},{"row":4,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":23},{"column":24,"fg":"red","gr":"highlight,selectable","text":"Application Developer System"},{"column":52,"fg":"red","gr":"highlight,selectable","count":29}]},{"row":5,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":80}]},{"row":6,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":32},{"column":33,"fg":"red","gr":"highlight,selectable","text":"// OOOOOOO SSSSS"},{"column":52,"fg":"red","gr":"highlight,selectable","count":29}]},{"row":7,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":31},{"column":32,"fg":"red","gr":"highlight,selectable","text":"// OO OO SS"},{"column":47,"fg":"red","gr":"highlight,selectable","count":34}]},{"row":8,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":23},{"column":24,"fg":"red","gr":"highlight,selectable","text":"zzzzzz // OO OO SS"},{"column":46,"fg":"red","gr":"highlight,selectable","count":35}]},{"row":9,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":25},{"column":26,"fg":"red","gr":"highlight,selectable","text":"zz // OO OO SSSS"},{"column":47,"fg":"red","gr":"highlight,selectable","count":34}]},{"row":10,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":23},{"column":24,"fg":"red","gr":"highlight,selectable","text":"zz // OO OO SS"},{"column":49,"fg":"red","gr":"highlight,selectable","count":32}]},{"row":11,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":21},{"column":22,"fg":"red","gr":"highlight,selectable","text":"zz // OO OO SS"},{"column":48,"fg":"red","gr":"highlight,selectable","count":33}]},{"row":12,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":19},{"column":20,"fg":"red","gr":"highlight,selectable","text":"zzzzzz // OOOOOOO SSSS"},{"column":45,"fg":"red","gr":"highlight,selectable","count":36}]},{"row":13,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":80}]},{"row":14,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":80}]},{"row":15,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":19},{"column":20,"fg":"red","gr":"highlight,selectable","text":"System Customization - ADCD.Z113H.*"},{"column":55,"fg":"red","gr":"highlight,selectable","count":26}]},{"row":16,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":80}]},{"row":17,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":80}]},{"row":18,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":80}]},{"row":19,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":80}]},{"row":20,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","text":" ===> Enter \"LOGON\" followed by the TSO userid. Example \"LOGON IBMUSER\" or "}]},{"row":21,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","text":" ===> Enter L followed by the APPLID"},{"column":37,"fg":"red","gr":"highlight,selectable","count":44}]},{"row":22,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","text":" ===> Examples: \"L TSO\", \"L CICSTS41\", \"L CICSTS42\", \"L IMS11\", \"L IMS12\" "}]},{"row":23,"changes":[{"column":1,"fg":"red","gr":"highlight,selectable","count":79},{"column":80,"fg":"green","count":1}]},{"row":24,"changes":[{"column":1,"fg":"green","count":79},{"column":80,"fg":"red","gr":"highlight,selectable","count":1}]}]"#;
if let Err(err) = serde_json::from_slice::<Vec<Row>>(instr.as_bytes()) {
let pos = err.column();
println!("Parse error: {err}");
let (pre, post) = instr.split_at(err.column());
println!("Context: {pre}\x1b[1;31m{post}\x1b[0m");
panic!("{}", err);
}
}
}

View File

@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
// {"run":{"actions":[{"action":"Connect","args":["10.24.74.37:3270"]}]}}
// {"run":{"actions":"Key(a)"}}
// {"run":{"actions":[{"action":"Key","args":["a"]}]}}
// Operations
#[derive(Serialize, Deserialize, Debug, Hash, Eq, PartialEq, Clone)]
#[serde(rename_all = "kebab-case")]

View File

@@ -97,6 +97,9 @@ impl FromStr for GraphicRendition {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "default" {
return Ok(GraphicRendition::empty());
}
s.split(",")
.map(|attr| {
FLAG_NAMES
@@ -136,7 +139,7 @@ mod test {
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)]
#[serde(rename = "camelCase")]
#[serde(rename_all = "camelCase")]
#[repr(u8)]
pub enum Color {
NeutralBlack,

View File

@@ -1,6 +1,6 @@
use crate::b3270::indication::{
Change, Connection, ConnectionState, CountOrText, Cursor, Erase, Oia, OiaFieldName, Row,
RunResult, Screen, ScreenMode, Scroll, Setting, TerminalName, Thumb, Tls, TraceFile,
RunResult, Screen, ScreenMode, Scroll, Setting, Thumb, Tls, TraceFile,
};
use crate::b3270::types::{Color, GraphicRendition, PackedAttr};
use crate::b3270::{Indication, InitializeIndication};
@@ -23,7 +23,6 @@ pub struct Tracker {
cursor: Cursor,
connection: Connection,
formatted: bool,
terminal_name: Option<TerminalName>,
trace_file: Option<String>,
tls: Option<Tls>,
@@ -62,7 +61,7 @@ impl Tracker {
self.erase.bg = erase.bg.or(self.erase.bg);
let rows = self.erase.logical_rows.unwrap_or(self.screen_mode.rows) as usize;
let cols = self.erase.logical_cols.unwrap_or(self.screen_mode.cols) as usize;
let cols = self.erase.logical_cols.unwrap_or(self.screen_mode.columns) as usize;
self.screen = vec![
vec![
@@ -92,6 +91,7 @@ impl Tracker {
| InitializeIndication::Models(_)
| InitializeIndication::Prefixes { .. }
| InitializeIndication::Proxies(_)
| InitializeIndication::TerminalName(_)
| InitializeIndication::TlsHello(_)
| InitializeIndication::Tls(_)
| InitializeIndication::TraceFile(_) => static_init.push(indicator),
@@ -169,7 +169,7 @@ impl Tracker {
self.screen_mode = *mode;
self.handle_indication(&mut Indication::Erase(Erase {
logical_rows: Some(self.screen_mode.rows),
logical_cols: Some(self.screen_mode.cols),
logical_cols: Some(self.screen_mode.columns),
fg: None,
bg: None,
}));
@@ -187,9 +187,6 @@ impl Tracker {
Indication::Setting(setting) => {
self.settings.insert(setting.name.clone(), setting.clone());
}
Indication::TerminalName(term) => {
self.terminal_name = Some(term.clone());
}
Indication::Thumb(thumb) => {
self.thumb = thumb.clone();
}
@@ -239,9 +236,6 @@ impl Tracker {
state: self.formatted,
},
];
if let Some(terminal_name) = self.terminal_name.clone() {
result.push(Indication::TerminalName(terminal_name));
}
if let Some(trace_file) = self.trace_file.clone() {
result.push(Indication::TraceFile(TraceFile {
name: Some(trace_file),
@@ -283,7 +277,7 @@ impl Tracker {
.map(Self::format_row)
.enumerate()
.map(|(row_id, changes)| Row {
row: row_id as u8 - 1,
row: row_id as u8 + 1,
changes,
})
.collect(),
@@ -297,7 +291,7 @@ impl Default for Tracker {
screen: vec![],
oia: Default::default(),
screen_mode: ScreenMode {
cols: 80,
columns: 80,
rows: 43,
color: true,
model: 4,
@@ -329,7 +323,6 @@ impl Default for Tracker {
cause: None,
},
formatted: false,
terminal_name: None,
trace_file: None,
tls: None,
static_init: vec![],

View File

@@ -16,6 +16,7 @@ d3270-common = {path = "../d3270-common"}
bytes = "1.4.0"
tracing = "0.1.37"
tracing-fmt = "0.1.1"
tracing-subscriber = { version = "0.3.17", features = ["registry", "env-filter"] }
futures = "0.3.28"
tokio-stream = { version = "0.1.14", features = ["sync"] }
rand = "0.8.5"

View File

@@ -16,7 +16,7 @@ use tokio_stream::wrappers::{errors::BroadcastStreamRecvError, BroadcastStream};
use tracing::{error, info, info_span, instrument, trace, warn, Instrument};
use d3270_common::b3270::indication::RunResult;
use d3270_common::b3270::operation::Action;
use d3270_common::b3270::operation::{Action, Run};
use d3270_common::b3270::{operation, Indication, Operation};
use d3270_common::tracker::{Disposition, Tracker};
@@ -194,11 +194,16 @@ impl B3270 {
let (ind_chan, _) = broadcast::channel(100);
let mut write_buf = VecDeque::new();
// Queue any initial actions.
for action in initial_actions {
serde_json::to_writer(&mut write_buf, action).unwrap();
write_buf.push_back(b'\n');
}
let act_str = serde_json::to_string(&Operation::Run(Run{
actions: initial_actions.to_vec(),
type_: Some("keybind".to_owned()),
r_tag: None,
})).unwrap();
trace!(json=%act_str, "Writing initialization action");
write_buf.extend(act_str.as_bytes());
write_buf.push_back(b'\n');
let proc = B3270 {
child,
@@ -227,11 +232,11 @@ impl Future for B3270 {
match buf {
Ok(Some(line)) => match serde_json::from_str(&line) {
Ok(ind) => {
trace!(json = line, "Received indication");
trace!(json = %line, "Received indication");
indications.push(ind)
}
Err(error) => {
warn!(%error, msg=line, "Failed to parse indication");
warn!(%error, msg=%line, "Failed to parse indication");
}
},
// EOF on stdin; this is a big problem

View File

@@ -9,7 +9,9 @@ use anyhow::anyhow;
use futures::future::select_all;
use futures::FutureExt;
use tokio::task::JoinHandle;
use tracing::error;
use tracing::{error, info};
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::prelude::*;
use d3270_common::b3270::operation::Action;
@@ -46,10 +48,26 @@ impl JoinHandleTagExt for JoinHandle<anyhow::Error> {
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut subprocess_args = vec![OsString::from_str("-json").unwrap()];
// Configure logging
tracing_subscriber::registry()
.with(tracing_subscriber::filter::EnvFilter::from_default_env())
.with(
tracing_subscriber::fmt::layer()
.with_span_events(FmtSpan::ACTIVE)
).init();
info!("Test");
let mut subprocess_args = vec![
OsString::from_str("-json").unwrap(),
OsString::from_str("-utf8").unwrap(),
];
let mut args_iter = std::env::args_os().peekable();
let mut connect_str = None;
let mut tcp_listen = None;
args_iter.next(); // skip program name.
while let Some(arg) = args_iter.next() {
// we default to one of the ignored args
match arg.to_str().unwrap_or("-json") {
@@ -92,6 +110,7 @@ async fn main() -> anyhow::Result<()> {
let connect_str = connect_str.ok_or_else(|| anyhow!("No connect string given"))?;
info!(args=?subprocess_args, "Starting b3270");
let subproc = tokio::process::Command::new("b3270")
.args(&subprocess_args)
.stdin(Stdio::piped())

View File

@@ -6,16 +6,23 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
use tokio::select;
use tokio::task::JoinHandle;
use tracing::{error, info, info_span, Instrument};
use tracing::{error, info, info_span, Instrument, instrument};
use crate::arbiter::ArbiterHandleRequester;
use crate::gen_connection::GenConnection;
#[instrument(skip(handle_requester))]
pub async fn listener_proc(
socket: SocketAddr,
handle_requester: ArbiterHandleRequester,
) -> anyhow::Result<JoinHandle<anyhow::Error>> {
let listener = tokio::net::TcpListener::bind(socket.clone()).await?;
let listener = match tokio::net::TcpListener::bind(socket.clone()).await {
Err(error) => {
error!(?socket, ?error, "Failed to bind");
return Err(error.into());
}
Ok(listener) => listener
};
let span = info_span!(target: "connection-handling", "tcp_listener", addr=%socket);
Ok(tokio::spawn(
async move {