const builtin = @import("builtin"); const std = @import("std"); const zfetch = @import("zfetch"); const crypt = @import("crypt.zig"); const config = @import("config"); const encryptionconfig = @import("encryptionconfig"); // const tls = @import("iguanaTLS"); // NGINX config isn't allowing ECDHE-RSA-CHACHA20-POLY1305 on TLS 1.2 // I need: // // nginx to allow that // iguanaTLS to support tls 1.3 // iguanaTLS to support something else, like ECDHE-ECDSA-CHACHA20-POLY1305 // In the meantime, I've allowed use of http, since we're encrypting anyway const clipboard_url = "http://clippy.lerch.org/work2"; // const clipboard_url = "https://httpbin.org/post"; const Self = @This(); key: *[crypt.key_size]u8, allocator: std.mem.Allocator, pub fn init(allocator: std.mem.Allocator) !Self { const key = try getKey(allocator); return Self{ .allocator = allocator, .key = key, }; } pub fn deinit(self: *Self) void { self.allocator.free(self.key); } pub fn download(self: *Self) ?[]const u8 { const encrypted = get(self.allocator) catch |e| { std.log.err("Could not download remote clipboard contents: {}", .{e}); if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); } return null; }; defer self.allocator.free(encrypted); return crypt.decryptWithKey(self.allocator, self.key.*, encrypted) catch |e| { std.log.err("Could not decrypt remote clipboard contents: {}", .{e}); if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); } return null; }; } pub fn clipboardChanged(self: *Self, contents: []const u8) !void { var arena_allocator = std.heap.ArenaAllocator.init(self.allocator); defer arena_allocator.deinit(); const aa = arena_allocator.allocator(); const clip_contents = try aa.dupe(u8, contents); defer aa.free(clip_contents); // Ugh - it's the encryption that Crowdstrike doesn't like.. :( var buf: []u8 = try aa.alloc(u8, contents.len); defer aa.free(buf); std.mem.copy(u8, buf, contents); const encrypted = encrypt(aa, self.key.*, buf) catch |e| { std.log.err("Could not encrypt clipboard contents: {}", .{e}); if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); } return; }; defer aa.free(encrypted); put(aa, encrypted) catch |e| { std.log.err("error posting clipboard contents {}", .{e}); if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); } return; }; } fn encrypt(allocator: std.mem.Allocator, key: [crypt.key_size]u8, data: []u8) ![]const u8 { if (encryptionconfig.encryption) |external_encryption| { const result = try std.ChildProcess.exec(.{ .allocator = allocator, .argv = &[_][]const u8{ external_encryption, data, }, }); try std.io.getStdErr().writer().writeAll(result.stderr); switch (result.term) { .Exited => |code| if (code == 0) { return result.stdout; } else return error.NonZeroExit, .Signal => return error.FailedWithSignal, .Stopped => return error.WasStopped, .Unknown => return error.Failed, } } return try crypt.encryptWithKey(allocator, key, data); } fn getKey(allocator: std.mem.Allocator) !*[crypt.key_size]u8 { const passfile = std.fs.cwd().openFile(".clippy", .{}) catch |e| { if (e == error.FileNotFound) { const cwd = std.fs.realpathAlloc(allocator, ".") catch "could not determine"; defer allocator.free(cwd); std.log.err("Could not find '.clippy' file in directory {s}. Please add a password to this file", .{cwd}); } return e; }; defer passfile.close(); const pass = try passfile.readToEndAlloc(allocator, std.math.maxInt(usize)); defer allocator.free(pass); const tmp_key = try crypt.keyFromPassword(allocator, pass, ""); // reuse key - this is slow return tmp_key; } fn get(allocator: std.mem.Allocator) ![]const u8 { if (config.curl) |curl| return getCurl(allocator, curl); // TODO: Windows // var cert_reader = std.io.fixedBufferStream( // @embedFile("/etc/ssl/certs/ca-certificates.crt"), // ).reader(); // const trust = try tls.x509.CertificateChain.from_pem(allocator, cert_reader); try zfetch.init(); defer zfetch.deinit(); var headers = zfetch.Headers.init(allocator); defer headers.deinit(); // try headers.appendValue("Accept", "application/json"); // try headers.appendValue("Content-Type", "text/plain"); var req = try zfetch.Request.init(allocator, clipboard_url, null); defer req.deinit(); try req.do(.GET, headers, null); // Printf debugging // const stdout = std.io.getStdOut().writer(); // try stdout.print("status: {d} {s}\n", .{ req.status.code, req.status.reason }); // try stdout.print("headers:\n", .{}); // for (req.headers.list.items) |header| { // try stdout.print(" {s}: {s}\n", .{ header.name, header.value }); // } // try stdout.print("body:\n", .{}); // const reader = req.reader(); var data = std.ArrayList(u8).init(allocator); defer data.deinit(); const data_writer = data.writer(); var buf: [1024]u8 = undefined; while (true) { const read = try reader.read(&buf); if (read == 0) break; try data_writer.writeAll(buf[0..read]); } return data.toOwnedSlice(); } fn getCurl(allocator: std.mem.Allocator, curl_path: []const u8) ![]const u8 { std.log.debug("curl path: {s}", .{curl_path}); const result = os: { if (builtin.os.tag == .linux) { const curl_cmd = try std.fmt.allocPrint(allocator, "{s} -s {s}", .{ curl_path, clipboard_url }); defer allocator.free(curl_cmd); break :os try execLinux(allocator, curl_cmd); } else if (builtin.os.tag == .windows) { break :os try std.ChildProcess.exec(.{ .allocator = allocator, .argv = &[_][]const u8{ curl_path, // TODO: use Comspec "-s", clipboard_url, }, }); } else { return error.OsUnsupported; } }; try std.io.getStdErr().writer().writeAll(result.stderr); switch (result.term) { .Exited => |code| if (code == 0) { return result.stdout; } else return error.NonZeroExit, .Signal => return error.FailedWithSignal, .Stopped => return error.WasStopped, .Unknown => return error.Failed, } } fn putCurl(allocator: std.mem.Allocator, curl_path: []const u8, data: []const u8) !void { std.log.debug("curl path: {s}", .{curl_path}); std.log.debug("clip url: {s}", .{clipboard_url}); std.log.debug("data (hex): {s}", .{std.fmt.fmtSliceHexLower(data)}); std.log.debug("data (string): {s}", .{data}); const bindata = blk: { if (encryptionconfig.temp_file) |tmp_name| { const tmp = try std.fs.createFileAbsolute(tmp_name, .{}); defer tmp.close(); try tmp.writer().writeAll(data); break :blk "@" ++ tmp_name; } else { break :blk data; } }; std.log.debug("bindata: {s}", .{data}); // binary in args const result = try std.ChildProcess.exec(.{ .allocator = allocator, .argv = &[_][]const u8{ curl_path, // TODO: use Comspec "-s", "-X", "PUT", "--data-binary", bindata, clipboard_url, }, }); try std.io.getStdErr().writer().writeAll(result.stderr); switch (result.term) { .Exited => |code| if (code == 0) { return; } else return error.NonZeroExit, .Signal => return error.FailedWithSignal, .Stopped => return error.WasStopped, .Unknown => return error.Failed, } } fn execLinux(allocator: std.mem.Allocator, cmd: []const u8) !std.ChildProcess.ExecResult { return std.ChildProcess.exec(.{ .allocator = allocator, .argv = &[_][]const u8{ "/usr/bin/env", "sh", "-c", cmd, }, }); } // Potentially useful code, but no longer necessary // fn execWindows(allocator: std.mem.Allocator, cmd: []const u8) !std.ChildProcess.ExecResult { // return std.ChildProcess.exec(.{ // .allocator = allocator, // .argv = &[_][]const u8{ // "c:\\windows\\system32\\cmd.exe", // TODO: use Comspec // "/c", // cmd, // }, // }); // } fn put(allocator: std.mem.Allocator, data: []const u8) !void { if (config.curl) |curl| return putCurl(allocator, curl, data); // TODO: Windows // var cert_reader = std.io.fixedBufferStream( // @embedFile("/etc/ssl/certs/ca-certificates.crt"), // ).reader(); // const trust = try tls.x509.CertificateChain.from_pem(allocator, cert_reader); try zfetch.init(); defer zfetch.deinit(); var headers = zfetch.Headers.init(allocator); defer headers.deinit(); // try headers.appendValue("Accept", "application/json"); // try headers.appendValue("Content-Type", "text/plain"); var req = try zfetch.Request.init(allocator, clipboard_url, null); defer req.deinit(); try req.do(.PUT, headers, data); // Printf debugging // const stdout = std.io.getStdOut().writer(); // try stdout.print("status: {d} {s}\n", .{ req.status.code, req.status.reason }); // try stdout.print("headers:\n", .{}); // for (req.headers.list.items) |header| { // try stdout.print(" {s}: {s}\n", .{ header.name, header.value }); // } // try stdout.print("body:\n", .{}); // // const reader = req.reader(); // // var buf: [1024]u8 = undefined; // while (true) { // const read = try reader.read(&buf); // if (read == 0) break; // // try stdout.writeAll(buf[0..read]); // } } test "full integration" { var allocator = std.testing.allocator; var watcher = init(allocator); defer watcher.deinit(); try watcher.clipboardChanged("hello world"); }