Skip to content

Commit

Permalink
feat: Return explicit NetworkError when intercepted by a middlebox
Browse files Browse the repository at this point in the history
We can't do things like look at the certificate in JS to know that the
request was intercepted, but if we detect a non-JSON response,
we know that it must come from an intermediary on the path
(proxy, middlebox, cloud gateway, ...)

In particular some prats are stuck behind ZScaler, and it sometimes
block Tanker decrypt calls. Today this is accounted as a Tanker internal
error and creates a Sentry alert for us.
The new NetworkError message should allow support to identify the issue,
and potentially help the client identity their network as the problem.
  • Loading branch information
tux3 committed Jun 4, 2024
1 parent 089b93a commit aca9bd5
Showing 1 changed file with 30 additions and 8 deletions.
38 changes: 30 additions & 8 deletions packages/core/src/Network/Client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import type { b64string } from '@tanker/crypto';
import { tcrypto, utils } from '@tanker/crypto';
import { TankerError, InternalError, InvalidArgument, InvalidVerification, OperationCanceled, PreconditionFailed } from '@tanker/errors';
import {
TankerError,
InternalError,
InvalidArgument,
InvalidVerification,
NetworkError,
OperationCanceled,
PreconditionFailed,
} from '@tanker/errors';
import { fetch, retry, exponentialDelayGenerator } from '@tanker/http-utils';
import type { DelayGenerator } from '@tanker/http-utils';
import { PromiseWrapper } from '@tanker/types';
Expand Down Expand Up @@ -145,15 +153,21 @@ export class Client {
let error;
try {
({ error } = JSON.parse(responseText));
} catch (_) { } // eslint-disable-line no-empty
} catch (_) {
} // eslint-disable-line no-empty

const apiMethod = init && init.method || 'GET';
if (!error) {
const details = responseText ? `response body: "${responseText}"` : 'empty response body';
const message = `"${response.status} ${response.statusText}" status with ${details}`;
error = { code: '<unknown>', message, status: response.status, trace_id: '<unknown>' };
const message = `"Request may have been intercepted by proxy, received non-JSON response: ${response.status} ${response.statusText}" status with ${details}`;
throw new NetworkError({
apiRoute: url,
apiMethod,
httpStatus: response.status,
message,
});
}

const apiMethod = init && init.method || 'GET';
genericErrorHandler(apiMethod, url, error);
} catch (err) {
const e = err as Error;
Expand Down Expand Up @@ -229,7 +243,11 @@ export class Client {
const { access_token: accessToken } = await this._cancelable(
() => this._baseApiCall(`/devices/${urlize(deviceId)}/sessions`, false, {
method: 'POST',
body: JSON.stringify(b64RequestObject({ signature, challenge, signature_public_key: signaturePublicKey })),
body: JSON.stringify(b64RequestObject({
signature,
challenge,
signature_public_key: signaturePublicKey,
})),
headers: { 'Content-Type': 'application/json' },
}),
)();
Expand Down Expand Up @@ -522,7 +540,9 @@ export class Client {
return sessionToken;
};

getGroupHistories = (query: string): Promise<{ histories: Array<b64string>; }> => this._apiCall(`/user-group-histories?${query}`);
getGroupHistories = (query: string): Promise<{
histories: Array<b64string>;
}> => this._apiCall(`/user-group-histories?${query}`);

getGroupHistoriesByGroupIds = async (groupIds: Array<Uint8Array>): Promise<{ histories: Array<b64string>; }> => {
const result = { histories: [] as Array<b64string> };
Expand Down Expand Up @@ -608,7 +628,9 @@ export class Client {
return provisionalKeys;
};

getTankerProvisionalKeysWithVerif = async (body: { verification: VerificationRequest }): Promise<TankerProvisionalIdentityResponse> => {
getTankerProvisionalKeysWithVerif = async (body: {
verification: VerificationRequest
}): Promise<TankerProvisionalIdentityResponse> => {
const options = {
method: 'POST',
body: JSON.stringify(b64RequestObject(body)),
Expand Down

0 comments on commit aca9bd5

Please sign in to comment.