~sircmpwn/hare-ssh

ref: 0b95f7dc320b11f912dcc23bf249563233c2f794 hare-ssh/net/ssh/client.ha -rw-r--r-- 2.8 KiB
0b95f7dcDrew DeVault net::ssh::client: don't close connection on finish 2 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
use bytes;
use errors;
use io;
use os;
use strings;

// The maximum size of an ordinary SSH packet. Some packets may be larger. This
// is the maximum amount of memory which is dynamically allocated per client by
// the implementation.
export def MAX_PACKETSIZE: size = 35000;

export type client = struct {
	conn: io::handle,
	rbuf: []u8,
	rptr: size,
};

const protoversion: str = "SSH-2.0-hare::ssh-0.0\r\n";

// Creates a new SSH client. When done using the client, the caller should call
// [[client_finish]] to free associated resources.
//
// This function will transmit the initial protocol version string before
// returning. The caller should call [[client_verexch]] to process the server's
// version details, then may use [[client_read]] and [[client_write]] to receive
// or transmit SSH packets.
export fn newclient(conn: io::handle) (client | io::error) = {
	io::writeall(conn, strings::toutf8(protoversion))?;
	return client {
		conn = conn,
		rbuf = alloc([], os::BUFSIZ),
		rptr = 0z,
	};
};

// Frees resources associated with this client. Does not close the underlying
// connection.
export fn client_finish(client: *client) void = {
	free(client.rbuf);
};

// Completes the "Protocol Version Exchange" process by receiving and processing
// verison data from the server. This function returns the version string to the
// caller, which is borrowed from the client state and will be overwritten on
// subsequent calls to [[net::ssh]] functions. If the underlying connection is
// non-blocking, this function may return [[errors::again]], in which case it
// should be called again once the connection is readable.
//
// If the server's protocol version is not supported, [[unsupported]] is
// returned.
export fn client_verexch(
	client: *client,
) (const str | error) = {
	let buf: [os::BUFSIZ]u8 = [0...];
	const z = match (io::read(client.conn, buf)?) {
	case let z: size =>
		yield z;
	case io::EOF =>
		return protoerror;
	};
	append(client.rbuf, buf[..z]...);

	const i = match (bytes::index(client.rbuf, ['\r', '\n'])) {
	case void =>
		if (len(client.rbuf) >= 255) {
			return protoerror;
		} else {
			return errors::again;
		};
	case let i: size =>
		yield i;
	};

	const version = client.rbuf[..i];
	client.rptr = i + 2;

	const version = match (strings::try_fromutf8(version)) {
	case let s: str =>
		yield s;
	case =>
		return protoerror;
	};

	const tok = strings::tokenize(version, "-");
	const ssh = toknext(&tok)?;
	const protover = toknext(&tok)?;
	// XXX: We could validate the rest of the version string matches the
	// spec here if I cared
	if (ssh != "SSH") {
		return protoerror;
	};
	if (protover != "2.0") {
		return unsupported;
	};
	return version;
};

fn toknext(tok: *strings::tokenizer) (str | protoerror) = {
	match (strings::next_token(tok)) {
	case void =>
		return protoerror;
	case let token: str =>
		return token;
	};
};