Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f19b870
add .gitignore
precla Nov 8, 2024
891e802
create initial Channel actor
precla Nov 8, 2024
362deb9
cleanup
precla Jan 16, 2025
0bcbc9e
make use of 'fromB_u64()' and code alignment (4 spaces)
precla Jan 16, 2025
a183d4a
initialize variables with 0
precla Jan 16, 2025
ab8f27f
avoid using magic numbers
precla Jan 16, 2025
e41fd92
further cleanup and added more error handling
precla Jan 16, 2025
dcf7423
add wrappers for cleanup functions
precla Jan 16, 2025
956d1d1
added more error handling
precla Jan 16, 2025
954cf91
add capability of sending netconf payload to netconf server
precla Jan 23, 2025
4297dc3
make it clear that 'client' capability ref is taken
precla Jan 23, 2025
d160abb
improve sending of data via ssh
precla Feb 28, 2025
27b2258
add affiinity and return the retreived string to caller
precla Feb 28, 2025
8a135ef
fix misbehavior by using fixed sized char array
precla Feb 28, 2025
830344f
refactoring and minor fixes
precla Mar 4, 2025
4905b47
comment unused buffer realated functions
precla Mar 4, 2025
7d6b10c
set custom disconnect message
precla Mar 4, 2025
95a2701
always send hello message
precla Mar 4, 2025
47fb5a3
add libssh + libuv example
precla Mar 28, 2025
26d4a92
enable non-NETCONF SSH subsystems
jvijtiuk May 8, 2025
72a0fde
further refactoring
precla Jul 8, 2025
64e4c6b
run netconf related code only if netconf subsystem set
precla Jul 8, 2025
9e499b1
add input handling for ssh session
precla Jul 8, 2025
b53d87b
update affinity related code
precla Jul 9, 2025
632ac0a
use non-blocking function for reading data on ssh channel
precla Jul 28, 2025
33c6989
improve print output
precla Jul 28, 2025
a285b2a
run until no more active handles
precla Jul 28, 2025
d927e88
added improved libuv+libssh example
precla Aug 21, 2025
1384e01
refactor
precla Aug 21, 2025
6097e8b
import new libuv+libssh solution into acton
precla Aug 22, 2025
fae5025
adapt and fix new libuv+libssh poll solution in the acton code
precla Aug 22, 2025
61ecd1a
further improvements and fixes
precla Aug 22, 2025
e4ff85b
add ssh subsystem support
precla Aug 25, 2025
ae3c04c
fix 2nd Client failing because same default loop used
precla Aug 25, 2025
401b5db
Redesign SSH Client to async event-driven API using RTS event loop
precla Apr 1, 2026
e7dc704
removed unused build.act.json
precla Apr 1, 2026
52c8da0
updated hash to latest libssh
precla Apr 1, 2026
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.acton.lock
.build
out
zig-cache
zig-out
9 changes: 9 additions & 0 deletions Build.act
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name = "acton_ssh"
fingerprint = 0xb650ac2857701cb1
zig_dependencies = {
"libssh": (
url="https://github.com/precla/libssh/archive/refs/heads/zig-build.tar.gz",
hash="libssh-0.11.0-kUQJaoJiKgDjZpTAyxxo6GamiMzj9LPxlEr2FacwOxIR",
artifacts=[ "ssh" ]
)
}
12 changes: 0 additions & 12 deletions build.act.json

This file was deleted.

35 changes: 35 additions & 0 deletions src/netconf.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// NETCONF <hello> message
#define NETCONF_HELLO_MSG \
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" \
"<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n" \
" <capabilities>\n" \
" <capability>urn:ietf:params:netconf:base:1.0</capability>\n" \
" </capabilities>\n" \
"</hello>]]>]]>"

#define NETCONF_GET_CONFIG_MSG \
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" \
"<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\" message-id=\"10\">\n" \
" <get-config>\n" \
" <source>\n" \
" <running/>\n" \
" </source>\n" \
" </get-config>\n" \
"</rpc>]]>]]>"

#define NETCONF_GET_STATE \
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" \
"<rpc xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\" message-id=\"10\">\n" \
" <get>" \
" <filter type=\"subtree\">" \
" <interfaces xmlns=\"urn:ietf:params:xml:ns:yang:ietf-interfaces\"/>" \
" </filter>" \
" </get>" \
"</rpc>]]>]]>"

// NETCONF <close-session> message
#define NETCONF_CLOSE_SESSION_MSG \
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" \
"<rpc message-id=\"9999\" xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n" \
" <close-session/>\n" \
"</rpc>]]>]]>"
128 changes: 92 additions & 36 deletions src/ssh.act
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,69 +1,125 @@
import net
import testing


def version() -> str:
"""Get the libssh version"""
"""Get the acton-ssh version"""
NotImplemented

def _test_version():
testing.assertEqual("0.11.0", version())

actor Client(cap: net.TCPConnectCap,
host: str,
username: str,
on_connect: action(Client) -> None,
on_close: action(Client, str) -> None,
key: ?str=None,
on_receive: action(Client, bytes) -> None,
on_error: action(Client, str) -> None,
on_remote_close: ?action(Client) -> None,
password: ?str=None,
port: u16=22,
):
"""SSH Client"""
key: ?str=None,
port: int=22,
subsystem: ?str=None):
"""SSH Client

Opens an SSH connection. If subsystem is None, opens a shell channel.
If subsystem is set (e.g. "netconf"), opens a subsystem channel instead.
"""

var _client: u64 = 0

# haha, this is really a pointer :P
var _ssh_session: u64 = 0
proc def _pin_affinity() -> None:
NotImplemented
_pin_affinity()

action def write(data: bytes) -> None:
"""Write data to the SSH channel"""
NotImplemented

action def close(on_close: action(Client) -> None) -> None:
"""Close the connection"""
NotImplemented

def reconnect():
close(_connect)

def _connect(c):
NotImplemented

proc def _init() -> None:
"""Initialize the SSH client"""
NotImplemented
_init()
print("SSH Client connected")

# action def close(on_close: action(TLSConnection) -> None) -> None:
# """Close the connection"""
# NotImplemented
#
# def reconnect():
# close(_connect)
#
# def _connect(c):
# NotImplemented
#

# TODO: implement support for channels
# AFAIK, all things over ssh are done via channels, so need some channel
# primitive, maybe an actor per channel that then multiplexes into the Client
# session? Prolly need some higher level wrappers for common things like
# starting a shell or running a single command. SFTP / SCP would be nice too,
# but for sometime in the future. Custom subsystems need to be supported too.
actor main(env):
var _buf: bytes = b""

NETCONF_HELLO = b"""<?xml version="1.0" encoding="UTF-8"?>
<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<capabilities>
<capability>urn:ietf:params:netconf:base:1.0</capability>
</capabilities>
</hello>]]>]]>"""

NETCONF_GET_CONFIG = b"""<?xml version="1.0" encoding="UTF-8"?>
<rpc xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="10">
<get-config>
<source>
<running/>
</source>
</get-config>
</rpc>]]>]]>"""

NETCONF_CLOSE = b"""<?xml version="1.0" encoding="UTF-8"?>
<rpc message-id="9999" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<close-session/>
</rpc>]]>]]>"""

var _state: int = 0 # 0=wait hello, 1=wait get-config reply, 2=wait close reply

actor main(env):
def on_connect(client: Client):
print("Connected")
print("Connected! Sending NETCONF hello...")
client.write(NETCONF_HELLO)

def on_receive(client: Client, data: bytes):
_buf += data
s = _buf.decode()
if "]]>]]>" in s:
print("=== NETCONF message ===")
print(s)
print("=======================")
_buf = b""

st = _state
if st == 0:
print("Got server hello, sending get-config...")
_state = 1
client.write(NETCONF_GET_CONFIG)
elif st == 1:
print("Got config, sending close-session...")
_state = 2
client.write(NETCONF_CLOSE)
elif st == 2:
print("Got close reply, done!")
def _on_close(c: Client):
env.exit(0)
client.close(_on_close)

def on_error(client: Client, error: str):
print("Error:", error)
env.exit(1)

def on_close(client: Client, error: str):
print("Error", error)
def on_remote_close(client: Client):
print("Remote closed connection")
env.exit(0)

print(version())
c = Client(
net.TCPConnectCap(net.TCPCap(net.NetCap(env.cap))),
"localhost",
"foo",
on_connect,
on_close,
on_receive,
on_error,
on_remote_close,
password="bar",
port=2223,
port=830,
subsystem="netconf",
)
env.exit(0)
Loading