Skip to content

Commit

Permalink
Merge pull request #27 from wdhongtw/feat-remove-expect-dependency
Browse files Browse the repository at this point in the history
feat: remove expect dependency
  • Loading branch information
wdhongtw authored Mar 9, 2022
2 parents cf4f0ae + ab9120a commit d62adb1
Show file tree
Hide file tree
Showing 6 changed files with 443 additions and 61 deletions.
5 changes: 0 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,6 @@ When you click the indicator, you will be prompted for passphrase to unlock the

- Linux environment (It's not been tested on other platform)
- GPG tool chain (`gpg`, `gpg-agent`, `gpg-connect-agent`) above 2.1
- `expect` tool by Don Libes: [Links](https://core.tcl-lang.org/expect/index)
- You can get this tool on most Linux distribution.

Current implementation require a pty between this extension and GPG tools to send passphrase,
so we use `expect` to handle this. I wish I can remove this dependency in the future.

## Issues & Reviews

Expand Down
5 changes: 0 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,5 @@
"url": "https://github.com/wdhongtw/vscode-gpg-indicator"
},
"preview": true,
"dependencies": {
"lookpath": "^1.2.0"
}
"dependencies": {}
}
2 changes: 1 addition & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ class KeyStatusManager {
}

this.#logger.log(`Try to unlock current key: ${theKey.fingerprint}`);
await gpg.unlockByKeyId(theKey.fingerprint, passphrase);
await gpg.unlockByKey(this.#logger, theKey.keygrip, passphrase);
await this.syncStatus();
}

Expand Down
357 changes: 357 additions & 0 deletions src/indicator/assuan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,357 @@
import * as net from 'net';

class RequestCommand {
command: string;
parameters: string | undefined = undefined;

constructor(command: string, parameters?: string) {
this.command = command;
this.parameters = parameters;
}
}

class RequestRawData {
bytes: Buffer;

constructor(bytes: Buffer) {
this.bytes = bytes;
}
}

enum RequestType {
command = "",
rawData = "D",
}

/**
* The Request object for the Assuan protocol.
*
* @see {@link https://www.gnupg.org/documentation/manuals/assuan/Client-requests.html#Client-requests}
*/
class Request {
#bytes: Buffer = Buffer.from([]);

static fromCommand(command: RequestCommand): Request {
const request = new Request;
if (command.parameters) {
request.#bytes = Buffer.from(`${command.command} ${command.parameters}`, 'utf8');
} else {
request.#bytes = Buffer.from(command.command, 'utf8');
}

return request;
}

static fromRawData(rawData: RequestRawData): Request {
const request = new Request;
request.#bytes = Buffer.concat([Buffer.from('D ', 'utf8'), rawData.bytes]);

return request;
}

toBytes(): Buffer {
return this.#bytes;
}
}

class ResponseOk {
message: string | undefined;

constructor(message?: string) {
this.message = message;
}
}

class ResponseError {
code: number;
description: string | undefined;

constructor(code: number, description?: string) {
this.code = code;
this.description = description;
}
}


class ResponseRawData {
bytes: Buffer;

constructor(bytes: Buffer) {
this.bytes = bytes;
}
}


class ResponseInformation {
keyword: string;
information: string;

constructor(keyword: string, information: string) {
this.keyword = keyword;
this.information = information;
}
}

class ResponseComment {
comment: string;

constructor(comment: string) {
this.comment = comment;
}
}

class ResponseInquire {
keyword: string;
parameters: string;

constructor(keyword: string, parameters: string) {
this.keyword = keyword;
this.parameters = parameters;
}
}

enum ResponseType {
ok = "OK",
error = "ERR",
information = "S",
comment = "#",
rawData = "D",
inquire = "INQUIRE",
}

/**
* The Response object for the Assuan protocol.
*
* @see {@link https://www.gnupg.org/documentation/manuals/assuan/Server-responses.html#Server-responses}
*/
class Response {
#bytes: Buffer = Buffer.from([]);

static fromBytes(bytes: Buffer): Response {
const response = new Response();
response.#bytes = bytes;
return response;
}

getType(): ResponseType {
const types = [
ResponseType.ok,
ResponseType.error,
ResponseType.information,
ResponseType.comment,
ResponseType.rawData,
ResponseType.inquire,
];
for (const type of types) {
if (this.#bytes.indexOf(type, 0, 'utf8') !== 0) {
continue;
}
return type;
}
throw new Error("Unknown server response type");
}

checkType(type: ResponseType): void {
if (this.getType() !== type) {
throw new Error("The response is not of given type");
}
}

toOk(): ResponseOk {
this.checkType(ResponseType.ok);

if (this.#bytes.length === 2) {
return new ResponseOk();
}
return new ResponseOk(this.#bytes.subarray(3).toString('utf8'));
}

toError(): ResponseError {
this.checkType(ResponseType.error);

const regex = /^ERR\s(?<code>\d+)(?:\s(?<description>.*))?$/;
const payload = this.#bytes.toString('utf8');
const match = regex.exec(payload);
if (!match || !match.groups) {
throw new Error("fail to parse error response");
}
return new ResponseError(parseInt(match.groups["code"], 10), match.groups["description"]);
}

toRawData(): ResponseRawData {
this.checkType(ResponseType.rawData);

return new ResponseRawData(this.#bytes.subarray(2));
}

toInformation(): ResponseInformation {
this.checkType(ResponseType.information);

const regex = /^S\s(?<keyword>\w+)\s(?<information>.*)$/;
const payload = this.#bytes.toString('utf8');
const match = regex.exec(payload);
if (!match || !match.groups) {
throw new Error("fail to parse information response");
}
return new ResponseInformation(match.groups["keyword"], match.groups["information"]);
}

toComment(): ResponseComment {
this.checkType(ResponseType.comment);

return new ResponseComment(this.#bytes.subarray(2).toString('utf-8'));
}

toInquire(): ResponseInquire {
this.checkType(ResponseType.inquire);

const regex = /^S\s(?<keyword>\w+)\s(?<parameters>.*)$/;
const payload = this.#bytes.toString('utf8');
const match = regex.exec(payload);
if (!match || !match.groups) {
throw new Error("fail to parse inquire response");
}
return new ResponseInquire(match.groups["keyword"], match.groups["parameters"]);
}
}

function splitLines(data: Buffer): Array<Buffer> {
const result: Buffer[] = [];
while (data.length > 0) {
const index = data.indexOf('\n', 0, 'utf8');
if (index === -1) {
throw new Error("the input data is contains no \\n character");
}
result.push(data.subarray(0, index));
data = data.subarray(index + 1, data.length);
}
return result;
}

function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

interface Logger {
log(message: string): void;
}

/**
* The AssuanClient class is a helper client for Assuan Protocol.
*/
class AssuanClient {
#logger: Logger;
#socket: net.Socket;

#responseLines: Buffer[] = [];

#socketErrorBuffer: Error[] = [];
#isConnected = false;

/**
* Construct a client for Assuan Protocol
*
* @remarks User should wait initialize() to complete before sending any command.
*
* @param logger - An object which implement Console interface for debug message.
* @param socketPath - The file path to GnuPG unix socket.
*/
constructor(logger: Logger, socketPath: string) {
this.#logger = logger;

this.#socket = net.createConnection(socketPath, () => {
this.#isConnected = true;
});

this.#socket.on('data', (data: Buffer) => {
const lines = splitLines(data);
for (const line of lines) {
this.#responseLines.push(line);
this.#logger.log('Assuan data receive: ' + line.toString('utf8'));
}
});

this.#socket.on('error', (error: Error) => {
this.#socketErrorBuffer.push(error);
});
}

/**
* Wait fo for the underline connection to be established.
*/
async initialize(): Promise<void> {
while (true) {
if (!this.#isConnected) {
await sleep(0);
}
return;
}
}

/**
* Close the underline connection.
*/
async dispose(): Promise<void> {
this.#socket.destroy();
}

async sendRequest(request: Request): Promise<void> {
this.checkError();

const line = request.toBytes();
this.#logger.log('Assuan data send: ' + line.toString('utf8'));
await this.handleSend(Buffer.concat([line, Buffer.from('\n', 'utf8')]));
}

handleSend(payload: Buffer): Promise<void> {
return new Promise((resolve, reject) => {
this.#socket.write(payload, (err: Error | undefined) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}

/**
* Throws if encounter socket error or receive a error response.
*/
checkError(): void {
const socketError = this.#socketErrorBuffer.shift();
if (socketError) {
throw socketError;
}
}

async receiveResponse(): Promise<Response> {
while (true) {
this.checkError();

const line = this.#responseLines.shift();
if (!line) {
await sleep(0);
continue;
}

return Response.fromBytes(line);
}
}
}


export {
Logger,

AssuanClient,
Request,
RequestType,
RequestCommand,
RequestRawData,
Response,
ResponseType,
ResponseOk,
ResponseError,
ResponseRawData,
};
Loading

0 comments on commit d62adb1

Please sign in to comment.