Skip to content

Commit 1311000

Browse files
taneltmmrts
authored andcommitted
Refact native application connection closing
Signed-off-by: Tanel Metsar <[email protected]>
1 parent 14dad5a commit 1311000

File tree

9 files changed

+82
-113
lines changed

9 files changed

+82
-113
lines changed

src/background-safari/services/NativeAppService.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,14 @@ export default class NativeAppService {
5252

5353
try {
5454
const message = await new Promise<{ version: string }>((resolve, reject) => {
55-
setTimeout(reject, libraryConfig.NATIVE_APP_HANDSHAKE_TIMEOUT);
55+
setTimeout(
56+
() => {
57+
reject(new NativeUnavailableError(
58+
`native application handshake timeout, ${libraryConfig.NATIVE_APP_HANDSHAKE_TIMEOUT}ms`
59+
));
60+
},
61+
libraryConfig.NATIVE_APP_HANDSHAKE_TIMEOUT,
62+
);
5663

5764
browser.runtime.sendNativeMessage(
5865
"application.id",
@@ -91,7 +98,7 @@ export default class NativeAppService {
9198
this.pending = null;
9299
}
93100

94-
async send<T>(message: NativeRequest): Promise<T> {
101+
async send<T>(message: NativeRequest, timeout: number, throwAfterTimeout: Error): Promise<T> {
95102
if (message.command == "quit") return {} as T;
96103

97104
switch (this.state) {
@@ -101,6 +108,8 @@ export default class NativeAppService {
101108
const sendPromise = new Promise<T>((resolve, reject) => {
102109
this.pending = { resolve, reject };
103110

111+
setTimeout(() => { reject(throwAfterTimeout); }, timeout );
112+
104113
const onResponse = (message: any): void => {
105114
this.pending = null;
106115

src/background/actions/TokenSigning/getCertificate.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import NativeAppService from "../../services/NativeAppService";
3434
import config from "../../../config";
3535
import errorToResponse from "./errorToResponse";
3636
import threeLetterLanguageCodes from "./threeLetterLanguageCodes";
37-
import { throwAfterTimeout } from "../../../shared/utils/timing";
3837
import tokenSigningResponse from "../../../shared/tokenSigningResponse";
3938

4039
export default async function getCertificate(
@@ -74,10 +73,11 @@ export default async function getCertificate(
7473
},
7574
};
7675

77-
const response = await Promise.race([
78-
nativeAppService.send<NativeGetSigningCertificateResponse>(message),
79-
throwAfterTimeout(config.TOKEN_SIGNING_USER_INTERACTION_TIMEOUT, new UserTimeoutError()),
80-
]);
76+
const response = await nativeAppService.send<NativeGetSigningCertificateResponse>(
77+
message,
78+
config.TOKEN_SIGNING_USER_INTERACTION_TIMEOUT,
79+
new UserTimeoutError(),
80+
);
8181

8282
if (!response?.certificate) {
8383
return tokenSigningResponse<TokenSigningErrorResponse>("no_certificates", nonce);

src/background/actions/TokenSigning/sign.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import NativeAppService from "../../services/NativeAppService";
3434
import config from "../../../config";
3535
import errorToResponse from "./errorToResponse";
3636
import threeLetterLanguageCodes from "./threeLetterLanguageCodes";
37-
import { throwAfterTimeout } from "../../../shared/utils/timing";
3837
import tokenSigningResponse from "../../../shared/tokenSigningResponse";
3938

4039

@@ -128,10 +127,11 @@ export default async function sign(
128127
},
129128
};
130129

131-
const response = await Promise.race([
132-
nativeAppService.send<NativeSignResponse>(message),
133-
throwAfterTimeout(config.TOKEN_SIGNING_USER_INTERACTION_TIMEOUT, new UserTimeoutError()),
134-
]);
130+
const response = await nativeAppService.send<NativeSignResponse>(
131+
message,
132+
config.TOKEN_SIGNING_USER_INTERACTION_TIMEOUT,
133+
new UserTimeoutError(),
134+
);
135135

136136
if (!response?.signature) {
137137
return tokenSigningResponse<TokenSigningErrorResponse>("technical_error", nonce);

src/background/actions/TokenSigning/status.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,19 @@
2020
* SOFTWARE.
2121
*/
2222

23-
import { NativeQuitRequest } from "@web-eid.js/models/message/NativeRequest";
23+
import UnknownError from "@web-eid.js/errors/UnknownError";
2424

2525
import {
2626
TokenSigningErrorResponse,
2727
TokenSigningStatusResponse,
2828
} from "../../../models/TokenSigning/TokenSigningResponse";
2929

3030
import NativeAppService from "../../services/NativeAppService";
31+
import config from "../../../config";
3132
import errorToResponse from "./errorToResponse";
3233
import tokenSigningResponse from "../../../shared/tokenSigningResponse";
3334

35+
3436
export default async function status(
3537
nonce: string,
3638
): Promise<TokenSigningStatusResponse | TokenSigningErrorResponse> {
@@ -46,12 +48,11 @@ export default async function status(
4648
throw new Error("missing native application version");
4749
}
4850

49-
const message: NativeQuitRequest = {
50-
command: "quit",
51-
arguments: {},
52-
};
53-
54-
await nativeAppService.send(message);
51+
await nativeAppService.send(
52+
{ command: "quit", arguments: {} },
53+
config.NATIVE_GRACEFUL_DISCONNECT_TIMEOUT,
54+
new UnknownError("native application failed to close gracefully"),
55+
);
5556

5657
return tokenSigningResponse<TokenSigningStatusResponse>("ok", nonce, { version });
5758
} catch (error) {

src/background/actions/authenticate.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import UnknownError from "@web-eid.js/errors/UnknownError";
3232
import actionErrorHandler from "../../shared/actionErrorHandler";
3333
import config from "../../config";
3434
import { getSenderUrl } from "../../shared/utils/sender";
35-
import { throwAfterTimeout } from "../../shared/utils/timing";
3635

3736
export default async function authenticate(
3837
challengeNonce: string,
@@ -62,11 +61,11 @@ export default async function authenticate(
6261
},
6362
};
6463

65-
const response = await Promise.race([
66-
nativeAppService.send<NativeAuthenticateResponse>(message),
67-
68-
throwAfterTimeout(userInteractionTimeout, new UserTimeoutError()),
69-
]);
64+
const response = await nativeAppService.send<NativeAuthenticateResponse>(
65+
message,
66+
userInteractionTimeout,
67+
new UserTimeoutError(),
68+
);
7069

7170
config.DEBUG && console.log("Authenticate: authentication token received");
7271

src/background/actions/getSigningCertificate.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import NativeAppService from "../services/NativeAppService";
3636
import actionErrorHandler from "../../shared/actionErrorHandler";
3737
import config from "../../config";
3838
import { getSenderUrl } from "../../shared/utils/sender";
39-
import { throwAfterTimeout } from "../../shared/utils/timing";
4039

4140
export default async function getSigningCertificate(
4241
sender: MessageSender,
@@ -63,10 +62,11 @@ export default async function getSigningCertificate(
6362
},
6463
};
6564

66-
const response = await Promise.race([
67-
nativeAppService.send<NativeGetSigningCertificateResponse>(message),
68-
throwAfterTimeout(userInteractionTimeout, new UserTimeoutError()),
69-
]);
65+
const response = await nativeAppService.send<NativeGetSigningCertificateResponse>(
66+
message,
67+
userInteractionTimeout,
68+
new UserTimeoutError(),
69+
);
7070

7171
const isResponseValid = (
7272
response?.certificate &&

src/background/actions/sign.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import NativeAppService from "../services/NativeAppService";
3636
import actionErrorHandler from "../../shared/actionErrorHandler";
3737
import config from "../../config";
3838
import { getSenderUrl } from "../../shared/utils/sender";
39-
import { throwAfterTimeout } from "../../shared/utils/timing";
4039

4140

4241
export default async function sign(
@@ -71,10 +70,11 @@ export default async function sign(
7170
},
7271
};
7372

74-
const response = await Promise.race([
75-
nativeAppService.send<NativeSignResponse>(message),
76-
throwAfterTimeout(userInteractionTimeout, new UserTimeoutError()),
77-
]);
73+
const response = await nativeAppService.send<NativeSignResponse>(
74+
message,
75+
userInteractionTimeout,
76+
new UserTimeoutError(),
77+
);
7878

7979
const isResponseValid = (
8080
response?.signature &&

src/background/actions/status.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
} from "@web-eid.js/models/message/ExtensionResponse";
2828

2929
import Action from "@web-eid.js/models/Action";
30+
import UnknownError from "@web-eid.js/errors/UnknownError";
3031
import VersionMismatchError from "@web-eid.js/errors/VersionMismatchError";
3132
import { serializeError } from "@web-eid.js/utils/errorSerializer";
3233

@@ -48,10 +49,11 @@ export default async function status(libraryVersion: string): Promise<ExtensionS
4849
: status.version
4950
);
5051

51-
await nativeAppService.send({
52-
command: "quit",
53-
arguments: {},
54-
});
52+
await nativeAppService.send(
53+
{ command: "quit", arguments: {} },
54+
config.NATIVE_GRACEFUL_DISCONNECT_TIMEOUT,
55+
new UnknownError("native application failed to close gracefully"),
56+
);
5557

5658
const componentVersions = {
5759
library: libraryVersion,

src/background/services/NativeAppService.ts

Lines changed: 32 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,13 @@
2121
*/
2222

2323
import NativeUnavailableError from "@web-eid.js/errors/NativeUnavailableError";
24-
import UnknownError from "@web-eid.js/errors/UnknownError";
2524
import { deserializeError } from "@web-eid.js/utils/errorSerializer";
2625
import libraryConfig from "@web-eid.js/config";
2726

28-
import { NativeFailureResponse } from "@web-eid.js/models/message/NativeResponse";
2927
import { NativeRequest } from "@web-eid.js/models/message/NativeRequest";
3028
import { Port } from "../../models/Browser/Runtime";
3129
import calculateJsonSize from "../../shared/utils/calculateJsonSize";
3230
import config from "../../config";
33-
import { throwAfterTimeout } from "../../shared/utils/timing";
34-
35-
type UnwrappedPromise
36-
= { resolve: (value?: any) => void; reject: (reason?: any) => void }
37-
| null;
3831

3932
export enum NativeAppState {
4033
UNINITIALIZED,
@@ -47,21 +40,23 @@ export default class NativeAppService {
4740
public state: NativeAppState = NativeAppState.UNINITIALIZED;
4841

4942
private port: Port | null = null;
50-
private pending: UnwrappedPromise = null;
51-
private activeConnection: UnwrappedPromise = null;
5243

5344
async connect(): Promise<{ version: string }> {
5445
this.state = NativeAppState.CONNECTING;
46+
this.port = browser.runtime.connectNative(config.NATIVE_APP_NAME);
5547

56-
this.port = browser.runtime.connectNative(config.NATIVE_APP_NAME);
5748
this.port.onDisconnect.addListener(this.disconnectListener.bind(this));
5849

5950
try {
60-
const message = await this.nextMessage(libraryConfig.NATIVE_APP_HANDSHAKE_TIMEOUT);
51+
const message = await this.nextMessage(
52+
libraryConfig.NATIVE_APP_HANDSHAKE_TIMEOUT,
53+
new NativeUnavailableError(
54+
`native application handshake timeout, ${libraryConfig.NATIVE_APP_HANDSHAKE_TIMEOUT}ms`
55+
),
56+
);
6157

6258
if (message.version) {
6359
this.state = NativeAppState.CONNECTED;
64-
new Promise((resolve, reject) => this.activeConnection = { resolve, reject });
6560

6661
return message;
6762
}
@@ -90,85 +85,50 @@ export default class NativeAppService {
9085
}
9186
}
9287

93-
async disconnectListener(): Promise<void> {
94-
config.DEBUG && console.log("Native app disconnected");
88+
disconnectListener(): void {
89+
config.DEBUG && console.log("Native app disconnected.");
90+
9591
// Accessing lastError when it exists stops chrome from throwing it unnecessarily.
9692
chrome?.runtime?.lastError;
9793

98-
// Defer active connection cleanup for Edge
99-
await new Promise((resolve) => setTimeout(resolve));
100-
101-
this.activeConnection?.resolve();
10294
this.state = NativeAppState.DISCONNECTED;
103-
104-
this.pending?.reject?.(new UnknownError("native application closed the connection before a response"));
105-
this.pending = null;
106-
}
107-
108-
disconnectForcefully(): void {
109-
this.state = NativeAppState.DISCONNECTED;
110-
111-
// At this point, requests should already be resolved.
112-
// Rejecting a resolved promise is a NOOP.
113-
this.pending?.reject?.(new UnknownError("extension closed connection to native app prematurely"));
114-
this.pending = null;
115-
116-
this.port?.disconnect();
11795
}
11896

11997
close(): void {
12098
if (this.state == NativeAppState.DISCONNECTED) return;
12199

122-
this.disconnectForcefully();
100+
this.state = NativeAppState.DISCONNECTED;
101+
this.port?.disconnect();
102+
103+
config.DEBUG && console.log("Native app port closed by extension.");
123104
}
124105

125-
send<T>(message: NativeRequest): Promise<T> {
106+
async send<T>(message: NativeRequest, timeout: number, throwAfterTimeout: Error): Promise<T> {
126107
switch (this.state) {
127108
case NativeAppState.CONNECTED: {
128-
return new Promise((resolve, reject) => {
129-
this.pending = { resolve, reject };
130-
131-
const onResponse = async (message: T): Promise<void> => {
132-
this.port?.onMessage.removeListener(onResponse);
109+
config.DEBUG && console.log("Sending message to native app", JSON.stringify(message));
133110

134-
try {
135-
await Promise.race([
136-
this.activeConnection,
137-
throwAfterTimeout(
138-
config.NATIVE_GRACEFUL_DISCONNECT_TIMEOUT,
139-
new Error("Native application did not disconnect after response")
140-
),
141-
]);
111+
const messageSize = calculateJsonSize(message);
142112

143-
} catch (error) {
144-
console.error(error);
145-
this.disconnectForcefully();
146-
147-
} finally {
148-
const error = (message as unknown as NativeFailureResponse)?.error;
149-
150-
if (error) {
151-
reject(deserializeError(error));
152-
} else {
153-
resolve(message);
154-
}
113+
if (messageSize > config.NATIVE_MESSAGE_MAX_BYTES) {
114+
throw new Error(`native application message exceeded ${config.NATIVE_MESSAGE_MAX_BYTES} bytes`);
115+
}
155116

156-
this.pending = null;
157-
}
158-
};
117+
this.port?.postMessage(message);
159118

160-
this.port?.onMessage.addListener(onResponse);
119+
try {
120+
const response = await this.nextMessage(timeout, throwAfterTimeout);
161121

162-
config.DEBUG && console.log("Sending message to native app", JSON.stringify(message));
122+
this.close();
163123

164-
const messageSize = calculateJsonSize(message);
124+
return response;
125+
} catch (error) {
126+
console.error(error);
165127

166-
if (messageSize > config.NATIVE_MESSAGE_MAX_BYTES) {
167-
throw new Error(`native application message exceeded ${config.NATIVE_MESSAGE_MAX_BYTES} bytes`);
168-
}
128+
this.close();
169129

170-
this.port?.postMessage(message);
171-
});
130+
throw error;
131+
}
172132
}
173133

174134
case NativeAppState.UNINITIALIZED: {
@@ -197,7 +157,7 @@ export default class NativeAppService {
197157
}
198158
}
199159

200-
nextMessage(timeout: number): Promise<any> {
160+
nextMessage(timeout: number, throwAfterTimeout: Error): Promise<any> {
201161
return new Promise((resolve, reject) => {
202162
let cleanup: (() => void) | null = null;
203163
let timer: ReturnType<typeof setTimeout> | null = null;
@@ -227,9 +187,7 @@ export default class NativeAppService {
227187
timer = setTimeout(
228188
() => {
229189
cleanup?.();
230-
reject(new NativeUnavailableError(
231-
`a message from native application was expected, but message wasn't received in ${timeout}ms`
232-
));
190+
reject(throwAfterTimeout);
233191
},
234192
timeout,
235193
);

0 commit comments

Comments
 (0)