@routr/common 2.0.5-alpha.2 → 2.0.5-alpha.6
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/dist/auth.d.ts +15 -0
- package/dist/auth.js +29 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +15 -24
- package/dist/helper.d.ts +1 -0
- package/dist/helper.js +35 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +8 -2
- package/dist/protos/processor.proto +8 -6
- package/dist/protos/sipmessage.proto +39 -5
- package/dist/service.js +11 -11
- package/dist/types.d.ts +49 -5
- package/package.json +3 -3
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { AuthChallengeRequest, AuthChallengeResponse } from "./types";
|
|
2
|
+
export declare const generateNonce: (algorithm?: string) => string;
|
|
3
|
+
export declare const buildAuthChallenge: (request: AuthChallengeRequest) => {
|
|
4
|
+
realm: string;
|
|
5
|
+
scheme: string;
|
|
6
|
+
algorithm: string;
|
|
7
|
+
qop: string;
|
|
8
|
+
opaque: string;
|
|
9
|
+
stale: boolean;
|
|
10
|
+
nonce: string;
|
|
11
|
+
};
|
|
12
|
+
export declare const calculateAuthResponse: (res: AuthChallengeResponse, credentials?: {
|
|
13
|
+
username: string;
|
|
14
|
+
secret: string;
|
|
15
|
+
}) => string;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.calculateAuthResponse = exports.buildAuthChallenge = exports.generateNonce = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const DEFAULT_ALGORITHM = 'MD5';
|
|
9
|
+
const md5hex = (str, algorithm = DEFAULT_ALGORITHM) => crypto_1.default.createHash(algorithm).update(str).digest('hex');
|
|
10
|
+
const decToHex = (dec) => (dec + Math.pow(16, 8)).toString(16).substr(-8);
|
|
11
|
+
const generateNonce = (algorithm = DEFAULT_ALGORITHM) => md5hex(`${new Date().getTime()}${Math.random()}`, algorithm);
|
|
12
|
+
exports.generateNonce = generateNonce;
|
|
13
|
+
const buildAuthChallenge = (request) => {
|
|
14
|
+
const { realm, scheme, algorithm, qop, opaque, stale } = request;
|
|
15
|
+
return { realm, scheme, algorithm, qop, opaque, stale, nonce: exports.generateNonce(algorithm) };
|
|
16
|
+
};
|
|
17
|
+
exports.buildAuthChallenge = buildAuthChallenge;
|
|
18
|
+
const calculateAuthResponse = (res, credentials) => {
|
|
19
|
+
if (!credentials)
|
|
20
|
+
return null;
|
|
21
|
+
const a1 = `${credentials.username}:${res.realm}:${credentials.secret}`;
|
|
22
|
+
const a2 = `${res.method.toUpperCase()}:${res.uri}`;
|
|
23
|
+
const ha1 = md5hex(a1);
|
|
24
|
+
const ha2 = md5hex(a2);
|
|
25
|
+
return res.qop === 'auth'
|
|
26
|
+
? md5hex(`${ha1}:${res.nonce}:${decToHex(res.nonce_count)}:${res.c_nonce}:${res.qop}:${ha2}`)
|
|
27
|
+
: md5hex(`${ha1}:${res.nonce}:${ha2}`);
|
|
28
|
+
};
|
|
29
|
+
exports.calculateAuthResponse = calculateAuthResponse;
|
package/dist/errors.d.ts
CHANGED
package/dist/errors.js
CHANGED
|
@@ -1,21 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __extends = (this && this.__extends) || (function () {
|
|
3
|
-
var extendStatics = function (d, b) {
|
|
4
|
-
extendStatics = Object.setPrototypeOf ||
|
|
5
|
-
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
|
6
|
-
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
|
7
|
-
return extendStatics(d, b);
|
|
8
|
-
};
|
|
9
|
-
return function (d, b) {
|
|
10
|
-
if (typeof b !== "function" && b !== null)
|
|
11
|
-
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
|
12
|
-
extendStatics(d, b);
|
|
13
|
-
function __() { this.constructor = d; }
|
|
14
|
-
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
|
15
|
-
};
|
|
16
|
-
})();
|
|
17
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.ServiceDefinitionNotFound = void 0;
|
|
3
|
+
exports.ServiceUnavailableError = exports.ServiceDefinitionNotFound = void 0;
|
|
19
4
|
/*
|
|
20
5
|
* Copyright (C) 2022 by Fonoster Inc (https://fonoster.com)
|
|
21
6
|
* http://github.com/fonoster/routr
|
|
@@ -34,14 +19,20 @@ exports.ServiceDefinitionNotFound = void 0;
|
|
|
34
19
|
* See the License for the specific language governing permissions and
|
|
35
20
|
* limitations under the License.
|
|
36
21
|
*/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
22
|
+
const grpc = require("@grpc/grpc-js");
|
|
23
|
+
class ServiceDefinitionNotFound extends Error {
|
|
24
|
+
constructor(name, version) {
|
|
25
|
+
super(`Service definition for ${name}/${version} not found`);
|
|
41
26
|
// Set the prototype explicitly.
|
|
42
|
-
Object.setPrototypeOf(
|
|
43
|
-
return _this;
|
|
27
|
+
Object.setPrototypeOf(this, ServiceDefinitionNotFound.prototype);
|
|
44
28
|
}
|
|
45
|
-
|
|
46
|
-
}(Error));
|
|
29
|
+
}
|
|
47
30
|
exports.ServiceDefinitionNotFound = ServiceDefinitionNotFound;
|
|
31
|
+
class ServiceUnavailableError extends Error {
|
|
32
|
+
constructor(addr) {
|
|
33
|
+
super(`service unavailable [addr = ${addr}]`);
|
|
34
|
+
this.code = grpc.status.UNAVAILABLE;
|
|
35
|
+
Object.setPrototypeOf(this, ServiceUnavailableError.prototype);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.ServiceUnavailableError = ServiceUnavailableError;
|
package/dist/helper.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const deepCopy: <T>(source: T) => T;
|
package/dist/helper.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deepCopy = void 0;
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (C) 2022 by Fonoster Inc (https://fonoster.com)
|
|
6
|
+
* http://github.com/fonoster/routr
|
|
7
|
+
*
|
|
8
|
+
* This file is part of Routr
|
|
9
|
+
*
|
|
10
|
+
* Licensed under the MIT License (the "License")
|
|
11
|
+
* you may not use this file except in compliance with
|
|
12
|
+
* the License. You may obtain a copy of the License at
|
|
13
|
+
*
|
|
14
|
+
* https://opensource.org/licenses/MIT
|
|
15
|
+
*
|
|
16
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
17
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
18
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
19
|
+
* See the License for the specific language governing permissions and
|
|
20
|
+
* limitations under the License.
|
|
21
|
+
*/
|
|
22
|
+
const deepCopy = (source) => {
|
|
23
|
+
return Array.isArray(source)
|
|
24
|
+
? source.map(item => exports.deepCopy(item))
|
|
25
|
+
: source instanceof Date
|
|
26
|
+
? new Date(source.getTime())
|
|
27
|
+
: source && typeof source === 'object'
|
|
28
|
+
? Object.getOwnPropertyNames(source).reduce((o, prop) => {
|
|
29
|
+
Object.defineProperty(o, prop, Object.getOwnPropertyDescriptor(source, prop));
|
|
30
|
+
o[prop] = exports.deepCopy(source[prop]);
|
|
31
|
+
return o;
|
|
32
|
+
}, Object.create(Object.getPrototypeOf(source)))
|
|
33
|
+
: source;
|
|
34
|
+
};
|
|
35
|
+
exports.deepCopy = deepCopy;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import createService, { PROCESSOR_OBJECT_PROTO, LOCATION_OBJECT_PROTO, getObjectProto } from "./service";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { calculateAuthResponse, generateNonce } from "./auth";
|
|
3
|
+
import { ServiceUnavailableError } from "./errors";
|
|
4
|
+
import { Transport, ProcessorConfig, MessageRequest, MessageResponse, NetInterface, Method, ObjectProto, ServiceInfo, Route } from "./types";
|
|
5
|
+
export { createService, getObjectProto, generateNonce, calculateAuthResponse, Route, ServiceUnavailableError, Transport, ProcessorConfig, MessageRequest, MessageResponse, NetInterface, Method, ObjectProto, ServiceInfo, PROCESSOR_OBJECT_PROTO, LOCATION_OBJECT_PROTO };
|
|
6
|
+
export * as Helper from "./helper";
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
19
19
|
return result;
|
|
20
20
|
};
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.LOCATION_OBJECT_PROTO = exports.PROCESSOR_OBJECT_PROTO = exports.getObjectProto = exports.createService = void 0;
|
|
22
|
+
exports.Helper = exports.LOCATION_OBJECT_PROTO = exports.PROCESSOR_OBJECT_PROTO = exports.ServiceUnavailableError = exports.calculateAuthResponse = exports.generateNonce = exports.getObjectProto = exports.createService = void 0;
|
|
23
23
|
/*
|
|
24
24
|
* Copyright (C) 2022 by Fonoster Inc (https://fonoster.com)
|
|
25
25
|
* http://github.com/fonoster/routr
|
|
@@ -38,8 +38,14 @@ exports.LOCATION_OBJECT_PROTO = exports.PROCESSOR_OBJECT_PROTO = exports.getObje
|
|
|
38
38
|
* See the License for the specific language governing permissions and
|
|
39
39
|
* limitations under the License.
|
|
40
40
|
*/
|
|
41
|
-
|
|
41
|
+
const service_1 = __importStar(require("./service"));
|
|
42
42
|
exports.createService = service_1.default;
|
|
43
43
|
Object.defineProperty(exports, "PROCESSOR_OBJECT_PROTO", { enumerable: true, get: function () { return service_1.PROCESSOR_OBJECT_PROTO; } });
|
|
44
44
|
Object.defineProperty(exports, "LOCATION_OBJECT_PROTO", { enumerable: true, get: function () { return service_1.LOCATION_OBJECT_PROTO; } });
|
|
45
45
|
Object.defineProperty(exports, "getObjectProto", { enumerable: true, get: function () { return service_1.getObjectProto; } });
|
|
46
|
+
const auth_1 = require("./auth");
|
|
47
|
+
Object.defineProperty(exports, "calculateAuthResponse", { enumerable: true, get: function () { return auth_1.calculateAuthResponse; } });
|
|
48
|
+
Object.defineProperty(exports, "generateNonce", { enumerable: true, get: function () { return auth_1.generateNonce; } });
|
|
49
|
+
const errors_1 = require("./errors");
|
|
50
|
+
Object.defineProperty(exports, "ServiceUnavailableError", { enumerable: true, get: function () { return errors_1.ServiceUnavailableError; } });
|
|
51
|
+
exports.Helper = __importStar(require("./helper"));
|
|
@@ -33,13 +33,15 @@ message NetInterface {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
message MessageRequest {
|
|
36
|
+
// Same as the Call-Id header
|
|
36
37
|
string ref = 1;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
repeated
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
string edge_port_ref = 2;
|
|
39
|
+
Method method = 3;
|
|
40
|
+
NetInterface sender = 4;
|
|
41
|
+
repeated NetInterface external_addrs = 5;
|
|
42
|
+
repeated string localnets = 6;
|
|
43
|
+
fonoster.routr.sipmessage.v2draft1.SIPMessage message = 7;
|
|
44
|
+
// .google.protobuf.Struct metadata = 8;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
message MessageResponse {
|
|
@@ -68,6 +68,31 @@ enum ResponseType {
|
|
|
68
68
|
SESSION_NOT_ACCEPTABLE = 57;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
message WWWAuthenticate {
|
|
72
|
+
string realm = 1;
|
|
73
|
+
string domain = 2;
|
|
74
|
+
string scheme = 3;
|
|
75
|
+
string nonce = 4;
|
|
76
|
+
string algorithm = 5;
|
|
77
|
+
string qop = 6;
|
|
78
|
+
string opaque = 7;
|
|
79
|
+
bool stale = 8;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
message Authorization {
|
|
83
|
+
string realm = 1;
|
|
84
|
+
string scheme = 2;
|
|
85
|
+
string c_nonce = 3;
|
|
86
|
+
string nonce = 4;
|
|
87
|
+
string algorithm = 5;
|
|
88
|
+
string qop = 6;
|
|
89
|
+
string opaque = 7;
|
|
90
|
+
string response = 8;
|
|
91
|
+
string username = 9;
|
|
92
|
+
string uri = 10;
|
|
93
|
+
int32 nonce_count = 11;
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
message CallID {
|
|
72
97
|
string call_id = 1;
|
|
73
98
|
}
|
|
@@ -118,6 +143,12 @@ message To {
|
|
|
118
143
|
string tag = 2;
|
|
119
144
|
}
|
|
120
145
|
|
|
146
|
+
message Contact {
|
|
147
|
+
Address address = 1;
|
|
148
|
+
int32 expires = 2;
|
|
149
|
+
float q_value = 3;
|
|
150
|
+
}
|
|
151
|
+
|
|
121
152
|
message Expires {
|
|
122
153
|
int32 expires = 1;
|
|
123
154
|
}
|
|
@@ -129,9 +160,12 @@ message SIPMessage {
|
|
|
129
160
|
}
|
|
130
161
|
From from = 3;
|
|
131
162
|
To to = 4;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
repeated
|
|
163
|
+
Contact contact = 5;
|
|
164
|
+
CallID call_id = 6;
|
|
165
|
+
ContentLength content_length = 7;
|
|
166
|
+
Expires expires = 8;
|
|
167
|
+
repeated Via via = 9;
|
|
168
|
+
repeated Extension extensions = 10;
|
|
169
|
+
WWWAuthenticate www_authenticate = 11;
|
|
170
|
+
Authorization authorization = 12;
|
|
137
171
|
}
|
package/dist/service.js
CHANGED
|
@@ -4,11 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getObjectProto = exports.LOCATION_OBJECT_PROTO = exports.PROCESSOR_OBJECT_PROTO = void 0;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
const errors_1 = require("./errors");
|
|
8
|
+
const logger_1 = __importDefault(require("@fonoster/logger"));
|
|
9
|
+
const protoLoader = require('@grpc/proto-loader');
|
|
10
|
+
const grpc = require('@grpc/grpc-js');
|
|
11
|
+
const loadOptions = {
|
|
12
12
|
keepCase: true,
|
|
13
13
|
longs: String,
|
|
14
14
|
enums: String,
|
|
@@ -28,20 +28,20 @@ exports.LOCATION_OBJECT_PROTO = getObjectProto({
|
|
|
28
28
|
});
|
|
29
29
|
function getObjectProto(objectProto) {
|
|
30
30
|
var _a, _b;
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
const definitions = protoLoader.loadSync(objectProto.path, loadOptions);
|
|
32
|
+
const objProto = (_b = (_a = grpc.loadPackageDefinition(definitions)) === null || _a === void 0 ? void 0 : _a.fonoster) === null || _b === void 0 ? void 0 : _b.routr[objectProto.name];
|
|
33
33
|
return objProto && objProto[objectProto.version]
|
|
34
34
|
? objProto[objectProto.version]
|
|
35
35
|
: new errors_1.ServiceDefinitionNotFound(objectProto.name, objectProto.version);
|
|
36
36
|
}
|
|
37
37
|
exports.getObjectProto = getObjectProto;
|
|
38
38
|
function createService(serviceInfo) {
|
|
39
|
-
|
|
40
|
-
logger_1.default.info(
|
|
39
|
+
const cb = () => {
|
|
40
|
+
logger_1.default.info(`starting ${serviceInfo.name} service @ ${serviceInfo.bindAddr}`);
|
|
41
41
|
server.start();
|
|
42
42
|
};
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const credentials = grpc.ServerCredentials.createInsecure();
|
|
44
|
+
const server = new grpc.Server();
|
|
45
45
|
server.addService(serviceInfo.service, serviceInfo.handlers);
|
|
46
46
|
server.bindAsync(serviceInfo.bindAddr, credentials, cb);
|
|
47
47
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export declare type Direction = 'IN' | 'OUT';
|
|
2
1
|
export declare type Method = 'UNKNOWN' | 'REGISTER' | 'INVITE' | 'MESSAGE' | 'PUBLISH' | 'NOTIFY' | 'SUBSCRIBE';
|
|
3
2
|
export declare type Transport = 'TCP' | 'UDP' | 'TLS' | 'SCTP' | 'WS' | 'WSS';
|
|
4
3
|
export interface NetInterface {
|
|
@@ -8,11 +7,15 @@ export interface NetInterface {
|
|
|
8
7
|
}
|
|
9
8
|
export interface MessageRequest {
|
|
10
9
|
ref: string;
|
|
11
|
-
|
|
10
|
+
edge_port_ref: string;
|
|
12
11
|
method: Method;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
sender: NetInterface;
|
|
13
|
+
external_addrs: NetInterface[];
|
|
14
|
+
localnets: string[];
|
|
15
|
+
message: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface MessageResponse {
|
|
18
|
+
message: Record<string, unknown>;
|
|
16
19
|
}
|
|
17
20
|
export interface ProcessorConfig {
|
|
18
21
|
ref: string;
|
|
@@ -21,6 +24,10 @@ export interface ProcessorConfig {
|
|
|
21
24
|
isFallback?: boolean;
|
|
22
25
|
matchFunc?: (request: MessageRequest) => boolean;
|
|
23
26
|
}
|
|
27
|
+
export interface MiddlewareConfig {
|
|
28
|
+
ref: string;
|
|
29
|
+
addr: string;
|
|
30
|
+
}
|
|
24
31
|
export interface ServiceInfo {
|
|
25
32
|
name: string;
|
|
26
33
|
bindAddr: string;
|
|
@@ -32,3 +39,40 @@ export interface ObjectProto {
|
|
|
32
39
|
path: string;
|
|
33
40
|
version: string;
|
|
34
41
|
}
|
|
42
|
+
export interface AuthChallengeRequest {
|
|
43
|
+
realm: string;
|
|
44
|
+
domain: string;
|
|
45
|
+
scheme: string;
|
|
46
|
+
nonce: string;
|
|
47
|
+
algorithm: string;
|
|
48
|
+
qop: string;
|
|
49
|
+
opaque: string;
|
|
50
|
+
stale: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface AuthChallengeResponse {
|
|
53
|
+
realm: string;
|
|
54
|
+
domain: string;
|
|
55
|
+
scheme: string;
|
|
56
|
+
c_nonce: string;
|
|
57
|
+
nonce_count: number;
|
|
58
|
+
response: string;
|
|
59
|
+
username: string;
|
|
60
|
+
nonce: string;
|
|
61
|
+
algorithm: string;
|
|
62
|
+
qop: string;
|
|
63
|
+
opaque: string;
|
|
64
|
+
stale: boolean;
|
|
65
|
+
uri: string;
|
|
66
|
+
method: string;
|
|
67
|
+
}
|
|
68
|
+
export interface Route {
|
|
69
|
+
user: string;
|
|
70
|
+
host: string;
|
|
71
|
+
port: number;
|
|
72
|
+
transport: string;
|
|
73
|
+
registeredOn: number;
|
|
74
|
+
sessionCount?: number;
|
|
75
|
+
expires: number;
|
|
76
|
+
edgePortRef: string;
|
|
77
|
+
labels?: Map<string, string>;
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@routr/common",
|
|
3
|
-
"version": "2.0.5-alpha.
|
|
3
|
+
"version": "2.0.5-alpha.6",
|
|
4
4
|
"description": "Common package",
|
|
5
5
|
"author": "Pedro Sanders <psanders@fonoster.com>",
|
|
6
6
|
"homepage": "https://github.com/fonoster/routr#readme",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@fonoster/logger": "^0.3.3",
|
|
17
|
-
"@grpc/grpc-js": "^1.5.
|
|
17
|
+
"@grpc/grpc-js": "^1.5.9",
|
|
18
18
|
"@grpc/proto-loader": "^0.6.9",
|
|
19
19
|
"fp-ts": "^2.11.8"
|
|
20
20
|
},
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"bugs": {
|
|
36
36
|
"url": "https://github.com/fonoster/routr/issues"
|
|
37
37
|
},
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "ac4963e735fbc4ad3045379463796137e650509c"
|
|
39
39
|
}
|