clipboard/src/clipboard.zig

313 lines
10 KiB
Zig

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");
}