@azure/msal-node-extensions 1.0.0-alpha.3 → 1.0.0-alpha.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -5
- package/binding.gyp +11 -1
- package/dist/broker/NativeBrokerPlugin.d.ts +20 -0
- package/dist/error/NativeAuthError.d.ts +6 -0
- package/dist/error/PersistenceError.d.ts +28 -0
- package/dist/index.d.ts +4 -0
- package/dist/msal-node-extensions.cjs.development.js +949 -932
- package/dist/msal-node-extensions.cjs.development.js.map +1 -1
- package/dist/msal-node-extensions.cjs.production.min.js +1 -1
- package/dist/msal-node-extensions.cjs.production.min.js.map +1 -1
- package/dist/msal-node-extensions.esm.js +957 -947
- package/dist/msal-node-extensions.esm.js.map +1 -1
- package/dist/packageMetadata.d.ts +2 -0
- package/dist/persistence/BasePersistence.d.ts +5 -0
- package/dist/persistence/FilePersistence.d.ts +4 -2
- package/dist/persistence/FilePersistenceWithDataProtection.d.ts +3 -1
- package/dist/persistence/IPersistence.d.ts +3 -1
- package/dist/persistence/IPersistenceConfiguration.d.ts +10 -0
- package/dist/persistence/KeychainPersistence.d.ts +3 -1
- package/dist/persistence/LibSecretPersistence.d.ts +3 -1
- package/dist/persistence/PersistenceCachePlugin.d.ts +11 -7
- package/dist/persistence/PersistenceCreator.d.ts +5 -0
- package/dist/utils/Constants.d.ts +30 -0
- package/dist/utils/Environment.d.ts +16 -0
- package/package.json +28 -15
- package/src/{dpapi-addon/Dpapi.ts → Dpapi.ts} +12 -12
- package/src/broker/NativeBrokerPlugin.ts +418 -0
- package/src/dpapi-addon/dpapi_addon.h +2 -1
- package/src/dpapi-addon/dpapi_not_supported.cpp +4 -10
- package/src/dpapi-addon/dpapi_win.cpp +25 -33
- package/src/dpapi-addon/main.cpp +14 -16
- package/src/error/NativeAuthError.ts +19 -0
- package/src/error/PersistenceError.ts +101 -61
- package/src/index.ts +17 -8
- package/src/lock/CrossPlatformLock.ts +89 -89
- package/src/lock/CrossPlatformLockOptions.ts +15 -15
- package/src/packageMetadata.ts +3 -0
- package/src/persistence/BasePersistence.ts +43 -0
- package/src/persistence/FilePersistence.ts +140 -134
- package/src/persistence/FilePersistenceWithDataProtection.ts +92 -84
- package/src/persistence/IPersistence.ts +4 -2
- package/src/persistence/IPersistenceConfiguration.ts +17 -0
- package/src/persistence/KeychainPersistence.ts +89 -78
- package/src/persistence/LibSecretPersistence.ts +90 -79
- package/src/persistence/PersistenceCachePlugin.ts +40 -30
- package/src/persistence/PersistenceCreator.ts +78 -0
- package/src/utils/Constants.ts +67 -30
- package/src/utils/Environment.ts +101 -0
- package/CHANGELOG.json +0 -50
- package/changelog.md +0 -28
- /package/dist/{dpapi-addon/Dpapi.d.ts → Dpapi.d.ts} +0 -0
|
@@ -4,63 +4,58 @@
|
|
|
4
4
|
*/
|
|
5
5
|
// Implementation referenced from https://github.com/bradhugh/node-dpapi
|
|
6
6
|
|
|
7
|
-
#include <
|
|
8
|
-
#include <
|
|
7
|
+
#include <napi.h>
|
|
8
|
+
#include <uv.h>
|
|
9
9
|
#include <Windows.h>
|
|
10
10
|
#include <dpapi.h>
|
|
11
11
|
#include <functional>
|
|
12
12
|
#include <iostream>
|
|
13
13
|
#include <string>
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Napi::Value ProtectDataCommon(bool protect, const Napi::CallbackInfo& info)
|
|
16
16
|
{
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
void ProtectDataCommon(bool protect, Nan::NAN_METHOD_ARGS_TYPE info)
|
|
21
|
-
{
|
|
22
|
-
v8::Isolate* isolate = info.GetIsolate();
|
|
17
|
+
Napi::Env env = info.Env();
|
|
23
18
|
|
|
24
19
|
if (info.Length() != 3) {
|
|
25
|
-
|
|
26
|
-
CreateUtf8String(isolate, "3 arguments are required")));
|
|
20
|
+
throw Napi::RangeError::New(env, "3 arguments are required");
|
|
27
21
|
}
|
|
28
22
|
|
|
29
|
-
if (info[0]
|
|
23
|
+
if (info[0].IsNull() ||
|
|
24
|
+
info[0].IsUndefined() ||
|
|
25
|
+
!info[0].IsTypedArray() ||
|
|
26
|
+
info[0].As<Napi::TypedArray>().TypedArrayType() != napi_uint8_array)
|
|
30
27
|
{
|
|
31
|
-
|
|
32
|
-
CreateUtf8String(isolate, "First argument, data, must be a valid Uint8Array")));
|
|
28
|
+
throw Napi::TypeError::New(env, "First argument, data, must be a valid Uint8Array");
|
|
33
29
|
}
|
|
34
30
|
|
|
35
|
-
if (!info[1]
|
|
31
|
+
if (!info[1].IsNull() &&
|
|
32
|
+
(!info[1].IsTypedArray() || info[1].As<Napi::TypedArray>().TypedArrayType() != napi_uint8_array))
|
|
36
33
|
{
|
|
37
|
-
|
|
38
|
-
CreateUtf8String(isolate, "Second argument, optionalEntropy, must be null or an ArrayBuffer")));
|
|
34
|
+
throw Napi::TypeError::New(env, "Second argument, optionalEntropy, must be null or an ArrayBuffer");
|
|
39
35
|
}
|
|
40
36
|
|
|
41
|
-
if (info[2]
|
|
37
|
+
if (info[2].IsNull() || info[2].IsUndefined() || !info[2].IsString())
|
|
42
38
|
{
|
|
43
|
-
|
|
44
|
-
CreateUtf8String(isolate, "Third argument, scope, must be a string")));
|
|
39
|
+
throw Napi::TypeError::New(env, "Third argument, scope, must be a string");
|
|
45
40
|
}
|
|
46
41
|
|
|
47
42
|
DWORD flags = 0;
|
|
48
|
-
|
|
49
|
-
std::string scope(
|
|
43
|
+
Napi::String strData = info[2].As<Napi::String>();
|
|
44
|
+
std::string scope = strData.Utf8Value();
|
|
50
45
|
if (stricmp(scope.c_str(), "LocalMachine") == 0)
|
|
51
46
|
{
|
|
52
47
|
flags = CRYPTPROTECT_LOCAL_MACHINE;
|
|
53
48
|
}
|
|
54
49
|
|
|
55
|
-
auto buffer =
|
|
56
|
-
auto len =
|
|
50
|
+
auto buffer = info[0].As<Napi::Buffer<char>>().Data();
|
|
51
|
+
auto len = info[0].As<Napi::Buffer<char>>().ElementLength();
|
|
57
52
|
|
|
58
53
|
DATA_BLOB entropyBlob;
|
|
59
54
|
entropyBlob.pbData = nullptr;
|
|
60
|
-
if (!info[1]
|
|
55
|
+
if (!info[1].IsNull())
|
|
61
56
|
{
|
|
62
|
-
entropyBlob.pbData = reinterpret_cast<BYTE*>(
|
|
63
|
-
entropyBlob.cbData =
|
|
57
|
+
entropyBlob.pbData = reinterpret_cast<BYTE*>(info[1].As<Napi::Buffer<char>>().Data());
|
|
58
|
+
entropyBlob.cbData = info[1].As<Napi::Buffer<char>>().ElementLength();
|
|
64
59
|
}
|
|
65
60
|
|
|
66
61
|
DATA_BLOB dataIn;
|
|
@@ -100,15 +95,12 @@ void ProtectDataCommon(bool protect, Nan::NAN_METHOD_ARGS_TYPE info)
|
|
|
100
95
|
{
|
|
101
96
|
DWORD errorCode = GetLastError();
|
|
102
97
|
std::string errorMessage = std::string("Encryption/Decryption failed. Error code: ") + std::to_string(errorCode);
|
|
103
|
-
|
|
104
|
-
CreateUtf8String(isolate, &errorMessage[0])));
|
|
105
|
-
|
|
106
|
-
return;
|
|
98
|
+
throw Napi::Error::New(env, &errorMessage[0]);
|
|
107
99
|
}
|
|
108
100
|
|
|
109
101
|
// Copy and free the buffer
|
|
110
|
-
|
|
102
|
+
Napi::Buffer<char> returnBuffer = Napi::Buffer<char>::Copy(env, reinterpret_cast<char*>(dataOut.pbData), dataOut.cbData);
|
|
111
103
|
LocalFree(dataOut.pbData);
|
|
112
104
|
|
|
113
|
-
|
|
105
|
+
return returnBuffer;
|
|
114
106
|
}
|
package/src/dpapi-addon/main.cpp
CHANGED
|
@@ -3,30 +3,28 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
#include <
|
|
6
|
+
#include <napi.h>
|
|
7
|
+
#include <uv.h>
|
|
7
8
|
#include "dpapi_addon.h"
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
Napi::Value protectData(const Napi::CallbackInfo& info)
|
|
10
11
|
{
|
|
11
|
-
ProtectDataCommon(true, info);
|
|
12
|
+
return ProtectDataCommon(true, info);
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
Napi::Value unprotectData(const Napi::CallbackInfo& info)
|
|
15
16
|
{
|
|
16
|
-
ProtectDataCommon(false, info);
|
|
17
|
+
return ProtectDataCommon(false, info);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
target,
|
|
23
|
-
Nan::New<v8::String>("protectData").ToLocalChecked(),
|
|
24
|
-
Nan::GetFunction(Nan::New<v8::FunctionTemplate>(protectData)).ToLocalChecked());
|
|
20
|
+
Napi::Object init(Napi::Env env, Napi::Object exports) {
|
|
21
|
+
exports.Set(Napi::String::New(env, "protectData"),
|
|
22
|
+
Napi::Function::New(env, protectData));
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
exports.Set(Napi::String::New(env, "unprotectData"),
|
|
25
|
+
Napi::Function::New(env, unprotectData));
|
|
26
|
+
|
|
27
|
+
return exports;
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
NODE_API_MODULE(dpapi, init)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AuthError } from "@azure/msal-common";
|
|
7
|
+
|
|
8
|
+
export class NativeAuthError extends AuthError {
|
|
9
|
+
public statusCode: number;
|
|
10
|
+
public tag: number;
|
|
11
|
+
|
|
12
|
+
constructor(errorStatus: string, errorContext: string, errorCode: number, errorTag: number) {
|
|
13
|
+
super(errorStatus, errorContext);
|
|
14
|
+
this.name = "NativeAuthError";
|
|
15
|
+
this.statusCode = errorCode;
|
|
16
|
+
this.tag = errorTag;
|
|
17
|
+
Object.setPrototypeOf(this, NativeAuthError.prototype);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,61 +1,101 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
-
* Licensed under the MIT License.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Error thrown when trying to write MSAL cache to persistence.
|
|
8
|
-
*/
|
|
9
|
-
export class PersistenceError extends Error {
|
|
10
|
-
|
|
11
|
-
// Short string denoting error
|
|
12
|
-
errorCode: string;
|
|
13
|
-
// Detailed description of error
|
|
14
|
-
errorMessage: string;
|
|
15
|
-
|
|
16
|
-
constructor(errorCode: string, errorMessage: string) {
|
|
17
|
-
const errorString = errorMessage ? `${errorCode}: ${errorMessage}` : errorCode;
|
|
18
|
-
super(errorString);
|
|
19
|
-
Object.setPrototypeOf(this, PersistenceError.prototype);
|
|
20
|
-
|
|
21
|
-
this.errorCode = errorCode;
|
|
22
|
-
this.errorMessage = errorMessage;
|
|
23
|
-
this.name = "PersistenceError";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Error thrown when trying to access the file system.
|
|
28
|
-
*/
|
|
29
|
-
static createFileSystemError(errorCode: string, errorMessage: string): PersistenceError {
|
|
30
|
-
return new PersistenceError(errorCode, errorMessage);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Error thrown when trying to write, load, or delete data from secret service on linux.
|
|
35
|
-
* Libsecret is used to access secret service.
|
|
36
|
-
*/
|
|
37
|
-
static createLibSecretError(errorMessage: string): PersistenceError {
|
|
38
|
-
return new PersistenceError("GnomeKeyringError", errorMessage);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Error thrown when trying to write, load, or delete data from keychain on macOs.
|
|
43
|
-
*/
|
|
44
|
-
static createKeychainPersistenceError(errorMessage: string): PersistenceError {
|
|
45
|
-
return new PersistenceError("KeychainError", errorMessage);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Error thrown when trying to encrypt or decrypt data using DPAPI on Windows.
|
|
50
|
-
*/
|
|
51
|
-
static createFilePersistenceWithDPAPIError(errorMessage: string): PersistenceError {
|
|
52
|
-
return new PersistenceError("DPAPIEncryptedFileError", errorMessage);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Error thrown when using the cross platform lock.
|
|
57
|
-
*/
|
|
58
|
-
static createCrossPlatformLockError(errorMessage: string): PersistenceError {
|
|
59
|
-
return new PersistenceError("CrossPlatformLockError", errorMessage);
|
|
60
|
-
}
|
|
61
|
-
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Error thrown when trying to write MSAL cache to persistence.
|
|
8
|
+
*/
|
|
9
|
+
export class PersistenceError extends Error {
|
|
10
|
+
|
|
11
|
+
// Short string denoting error
|
|
12
|
+
errorCode: string;
|
|
13
|
+
// Detailed description of error
|
|
14
|
+
errorMessage: string;
|
|
15
|
+
|
|
16
|
+
constructor(errorCode: string, errorMessage: string) {
|
|
17
|
+
const errorString = errorMessage ? `${errorCode}: ${errorMessage}` : errorCode;
|
|
18
|
+
super(errorString);
|
|
19
|
+
Object.setPrototypeOf(this, PersistenceError.prototype);
|
|
20
|
+
|
|
21
|
+
this.errorCode = errorCode;
|
|
22
|
+
this.errorMessage = errorMessage;
|
|
23
|
+
this.name = "PersistenceError";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Error thrown when trying to access the file system.
|
|
28
|
+
*/
|
|
29
|
+
static createFileSystemError(errorCode: string, errorMessage: string): PersistenceError {
|
|
30
|
+
return new PersistenceError(errorCode, errorMessage);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Error thrown when trying to write, load, or delete data from secret service on linux.
|
|
35
|
+
* Libsecret is used to access secret service.
|
|
36
|
+
*/
|
|
37
|
+
static createLibSecretError(errorMessage: string): PersistenceError {
|
|
38
|
+
return new PersistenceError("GnomeKeyringError", errorMessage);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Error thrown when trying to write, load, or delete data from keychain on macOs.
|
|
43
|
+
*/
|
|
44
|
+
static createKeychainPersistenceError(errorMessage: string): PersistenceError {
|
|
45
|
+
return new PersistenceError("KeychainError", errorMessage);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Error thrown when trying to encrypt or decrypt data using DPAPI on Windows.
|
|
50
|
+
*/
|
|
51
|
+
static createFilePersistenceWithDPAPIError(errorMessage: string): PersistenceError {
|
|
52
|
+
return new PersistenceError("DPAPIEncryptedFileError", errorMessage);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Error thrown when using the cross platform lock.
|
|
57
|
+
*/
|
|
58
|
+
static createCrossPlatformLockError(errorMessage: string): PersistenceError {
|
|
59
|
+
return new PersistenceError("CrossPlatformLockError", errorMessage);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Throw cache persistence error
|
|
64
|
+
*
|
|
65
|
+
* @param errorMessage string
|
|
66
|
+
* @returns PersistenceError
|
|
67
|
+
*/
|
|
68
|
+
static createCachePersistenceError(errorMessage: string): PersistenceError {
|
|
69
|
+
return new PersistenceError("CachePersistenceError", errorMessage);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Throw unsupported error
|
|
74
|
+
*
|
|
75
|
+
* @param errorMessage string
|
|
76
|
+
* @returns PersistenceError
|
|
77
|
+
*/
|
|
78
|
+
static createNotSupportedError(errorMessage: string): PersistenceError {
|
|
79
|
+
return new PersistenceError("NotSupportedError", errorMessage);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Throw persistence not verified error
|
|
84
|
+
*
|
|
85
|
+
* @param errorMessage string
|
|
86
|
+
* @returns PersistenceError
|
|
87
|
+
*/
|
|
88
|
+
static createPersistenceNotVerifiedError(errorMessage: string): PersistenceError {
|
|
89
|
+
return new PersistenceError("PersistenceNotVerifiedError", errorMessage);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Throw persistence creation validation error
|
|
94
|
+
*
|
|
95
|
+
* @param errorMessage string
|
|
96
|
+
* @returns PersistenceError
|
|
97
|
+
*/
|
|
98
|
+
static createPersistenceNotValidatedError(errorMessage: string): PersistenceError {
|
|
99
|
+
return new PersistenceError("PersistenceNotValidatedError", errorMessage);
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { PersistenceCachePlugin } from "./persistence/PersistenceCachePlugin";
|
|
7
|
+
export { FilePersistence } from "./persistence/FilePersistence";
|
|
8
|
+
export { FilePersistenceWithDataProtection } from "./persistence/FilePersistenceWithDataProtection";
|
|
9
|
+
export { DataProtectionScope } from "./persistence/DataProtectionScope";
|
|
10
|
+
export { KeychainPersistence } from "./persistence/KeychainPersistence";
|
|
11
|
+
export { LibSecretPersistence } from "./persistence/LibSecretPersistence";
|
|
12
|
+
export { IPersistence } from "./persistence/IPersistence";
|
|
13
|
+
export { CrossPlatformLockOptions } from "./lock/CrossPlatformLockOptions";
|
|
14
|
+
export { PersistenceCreator } from "./persistence/PersistenceCreator";
|
|
15
|
+
export { IPersistenceConfiguration } from "./persistence/IPersistenceConfiguration";
|
|
16
|
+
export { Environment } from "./utils/Environment";
|
|
17
|
+
export { NativeBrokerPlugin } from "./broker/NativeBrokerPlugin";
|
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
-
* Licensed under the MIT License.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { promises as fs } from "fs";
|
|
7
|
-
import { pid } from "process";
|
|
8
|
-
import { CrossPlatformLockOptions } from "./CrossPlatformLockOptions";
|
|
9
|
-
import { Constants } from "../utils/Constants";
|
|
10
|
-
import { PersistenceError } from "../error/PersistenceError";
|
|
11
|
-
import { Logger } from "@azure/msal-common";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Cross-process lock that works on all platforms.
|
|
15
|
-
*/
|
|
16
|
-
export class CrossPlatformLock {
|
|
17
|
-
|
|
18
|
-
private readonly lockFilePath: string;
|
|
19
|
-
private lockFileHandle: fs.FileHandle;
|
|
20
|
-
private readonly retryNumber: number;
|
|
21
|
-
private readonly retryDelay: number;
|
|
22
|
-
|
|
23
|
-
private logger: Logger;
|
|
24
|
-
|
|
25
|
-
constructor(lockFilePath: string, logger: Logger, lockOptions?: CrossPlatformLockOptions) {
|
|
26
|
-
this.lockFilePath = lockFilePath;
|
|
27
|
-
this.retryNumber = lockOptions ? lockOptions.retryNumber : 500;
|
|
28
|
-
this.retryDelay = lockOptions ? lockOptions.retryDelay : 100;
|
|
29
|
-
this.logger = logger;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Locks cache from read or writes by creating file with same path and name as
|
|
34
|
-
* cache file but with .lockfile extension. If another process has already created
|
|
35
|
-
* the lockfile, will back off and retry based on configuration settings set by CrossPlatformLockOptions
|
|
36
|
-
*/
|
|
37
|
-
public async lock(): Promise<void> {
|
|
38
|
-
for (let tryCount = 0; tryCount < this.retryNumber; tryCount++) {
|
|
39
|
-
try {
|
|
40
|
-
this.logger.info(`Pid ${pid} trying to acquire lock`);
|
|
41
|
-
this.lockFileHandle = await fs.open(this.lockFilePath, "wx+");
|
|
42
|
-
|
|
43
|
-
this.logger.info(`Pid ${pid} acquired lock`);
|
|
44
|
-
await this.lockFileHandle.write(pid.toString());
|
|
45
|
-
return;
|
|
46
|
-
} catch (err) {
|
|
47
|
-
if (err.code
|
|
48
|
-
this.logger.info(err);
|
|
49
|
-
await this.sleep(this.retryDelay);
|
|
50
|
-
} else {
|
|
51
|
-
this.logger.error(`${pid} was not able to acquire lock. Ran into error: ${err.message}`);
|
|
52
|
-
throw PersistenceError.createCrossPlatformLockError(err.message);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
this.logger.error(`${pid} was not able to acquire lock. Exceeded amount of retries set in the options`);
|
|
57
|
-
throw PersistenceError.createCrossPlatformLockError(
|
|
58
|
-
"Not able to acquire lock. Exceeded amount of retries set in options");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* unlocks cache file by deleting .lockfile.
|
|
63
|
-
*/
|
|
64
|
-
public async unlock(): Promise<void> {
|
|
65
|
-
try {
|
|
66
|
-
if(this.lockFileHandle){
|
|
67
|
-
// if we have a file handle to the .lockfile, delete lock file
|
|
68
|
-
await fs.unlink(this.lockFilePath);
|
|
69
|
-
await this.lockFileHandle.close();
|
|
70
|
-
this.logger.info("lockfile deleted");
|
|
71
|
-
} else {
|
|
72
|
-
this.logger.warning("lockfile handle does not exist, so lockfile could not be deleted");
|
|
73
|
-
}
|
|
74
|
-
} catch (err) {
|
|
75
|
-
if (err.code
|
|
76
|
-
this.logger.
|
|
77
|
-
} else {
|
|
78
|
-
this.logger.error(`${pid} was not able to release lock. Ran into error: ${err.message}`);
|
|
79
|
-
throw PersistenceError.createCrossPlatformLockError(err.message);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private sleep(ms): Promise<void> {
|
|
85
|
-
return new Promise((resolve) => {
|
|
86
|
-
setTimeout(resolve, ms);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { promises as fs } from "fs";
|
|
7
|
+
import { pid } from "process";
|
|
8
|
+
import { CrossPlatformLockOptions } from "./CrossPlatformLockOptions";
|
|
9
|
+
import { Constants } from "../utils/Constants";
|
|
10
|
+
import { PersistenceError } from "../error/PersistenceError";
|
|
11
|
+
import { Logger } from "@azure/msal-common";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Cross-process lock that works on all platforms.
|
|
15
|
+
*/
|
|
16
|
+
export class CrossPlatformLock {
|
|
17
|
+
|
|
18
|
+
private readonly lockFilePath: string;
|
|
19
|
+
private lockFileHandle: fs.FileHandle;
|
|
20
|
+
private readonly retryNumber: number;
|
|
21
|
+
private readonly retryDelay: number;
|
|
22
|
+
|
|
23
|
+
private logger: Logger;
|
|
24
|
+
|
|
25
|
+
constructor(lockFilePath: string, logger: Logger, lockOptions?: CrossPlatformLockOptions) {
|
|
26
|
+
this.lockFilePath = lockFilePath;
|
|
27
|
+
this.retryNumber = lockOptions ? lockOptions.retryNumber : 500;
|
|
28
|
+
this.retryDelay = lockOptions ? lockOptions.retryDelay : 100;
|
|
29
|
+
this.logger = logger;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Locks cache from read or writes by creating file with same path and name as
|
|
34
|
+
* cache file but with .lockfile extension. If another process has already created
|
|
35
|
+
* the lockfile, will back off and retry based on configuration settings set by CrossPlatformLockOptions
|
|
36
|
+
*/
|
|
37
|
+
public async lock(): Promise<void> {
|
|
38
|
+
for (let tryCount = 0; tryCount < this.retryNumber; tryCount++) {
|
|
39
|
+
try {
|
|
40
|
+
this.logger.info(`Pid ${pid} trying to acquire lock`);
|
|
41
|
+
this.lockFileHandle = await fs.open(this.lockFilePath, "wx+");
|
|
42
|
+
|
|
43
|
+
this.logger.info(`Pid ${pid} acquired lock`);
|
|
44
|
+
await this.lockFileHandle.write(pid.toString());
|
|
45
|
+
return;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (err.code === Constants.EEXIST_ERROR || err.code === Constants.EPERM_ERROR) {
|
|
48
|
+
this.logger.info(err);
|
|
49
|
+
await this.sleep(this.retryDelay);
|
|
50
|
+
} else {
|
|
51
|
+
this.logger.error(`${pid} was not able to acquire lock. Ran into error: ${err.message}`);
|
|
52
|
+
throw PersistenceError.createCrossPlatformLockError(err.message);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
this.logger.error(`${pid} was not able to acquire lock. Exceeded amount of retries set in the options`);
|
|
57
|
+
throw PersistenceError.createCrossPlatformLockError(
|
|
58
|
+
"Not able to acquire lock. Exceeded amount of retries set in options");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* unlocks cache file by deleting .lockfile.
|
|
63
|
+
*/
|
|
64
|
+
public async unlock(): Promise<void> {
|
|
65
|
+
try {
|
|
66
|
+
if(this.lockFileHandle){
|
|
67
|
+
// if we have a file handle to the .lockfile, delete lock file
|
|
68
|
+
await fs.unlink(this.lockFilePath);
|
|
69
|
+
await this.lockFileHandle.close();
|
|
70
|
+
this.logger.info("lockfile deleted");
|
|
71
|
+
} else {
|
|
72
|
+
this.logger.warning("lockfile handle does not exist, so lockfile could not be deleted");
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err.code === Constants.ENOENT_ERROR) {
|
|
76
|
+
this.logger.info("Tried to unlock but lockfile does not exist");
|
|
77
|
+
} else {
|
|
78
|
+
this.logger.error(`${pid} was not able to release lock. Ran into error: ${err.message}`);
|
|
79
|
+
throw PersistenceError.createCrossPlatformLockError(err.message);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private sleep(ms): Promise<void> {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
setTimeout(resolve, ms);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
-
* Licensed under the MIT License.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Options for CrossPlatform lock.
|
|
8
|
-
*
|
|
9
|
-
* retryNumber: Numbers of times we should try to acquire a lock. Defaults to 500.
|
|
10
|
-
* retryDelay: Time to wait before trying to retry a lock acquisition. Defaults to 100 ms.
|
|
11
|
-
*/
|
|
12
|
-
export type CrossPlatformLockOptions = {
|
|
13
|
-
retryNumber: number;
|
|
14
|
-
retryDelay: number;
|
|
15
|
-
};
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Options for CrossPlatform lock.
|
|
8
|
+
*
|
|
9
|
+
* retryNumber: Numbers of times we should try to acquire a lock. Defaults to 500.
|
|
10
|
+
* retryDelay: Time to wait before trying to retry a lock acquisition. Defaults to 100 ms.
|
|
11
|
+
*/
|
|
12
|
+
export type CrossPlatformLockOptions = {
|
|
13
|
+
retryNumber: number;
|
|
14
|
+
retryDelay: number;
|
|
15
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { PersistenceError } from "../error/PersistenceError";
|
|
7
|
+
import { Constants } from "../utils/Constants";
|
|
8
|
+
import { IPersistence } from "./IPersistence";
|
|
9
|
+
|
|
10
|
+
export abstract class BasePersistence {
|
|
11
|
+
public abstract createForPersistenceValidation(): Promise<IPersistence>;
|
|
12
|
+
|
|
13
|
+
public async verifyPersistence(): Promise<boolean> {
|
|
14
|
+
// We are using a different location for the test to avoid overriding the functional cache
|
|
15
|
+
const persistenceValidator = await this.createForPersistenceValidation();
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await persistenceValidator.save(Constants.PERSISTENCE_TEST_DATA);
|
|
19
|
+
|
|
20
|
+
const retrievedDummyData = await persistenceValidator.load();
|
|
21
|
+
|
|
22
|
+
if (!retrievedDummyData) {
|
|
23
|
+
throw PersistenceError.createCachePersistenceError(
|
|
24
|
+
"Persistence check failed. Data was written but it could not be read. " +
|
|
25
|
+
"Possible cause: on Linux, LibSecret is installed but D-Bus isn't running \
|
|
26
|
+
because it cannot be started over SSH."
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (retrievedDummyData !== Constants.PERSISTENCE_TEST_DATA) {
|
|
31
|
+
throw PersistenceError.createCachePersistenceError(
|
|
32
|
+
`Persistence check failed. Data written ${Constants.PERSISTENCE_TEST_DATA} is different \
|
|
33
|
+
from data read ${retrievedDummyData}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
await persistenceValidator.delete();
|
|
37
|
+
return true;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
throw PersistenceError.createCachePersistenceError(`Verifing persistence failed with the error: ${e}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
}
|