diff --git a/src/implementation/Client/DaprClient.ts b/src/implementation/Client/DaprClient.ts index 8954ff36..3a3b1ad6 100644 --- a/src/implementation/Client/DaprClient.ts +++ b/src/implementation/Client/DaprClient.ts @@ -87,18 +87,22 @@ export default class DaprClient { private readonly logger: Logger; constructor(options: Partial = {}) { - this.options = getClientOptions(options, Settings.getDefaultCommunicationProtocol(), undefined); - this.logger = new Logger("DaprClient", "DaprClient", this.options.logger); - - // Validation on port - if (this.options.daprPort && !/^[0-9]+$/.test(this.options.daprPort)) { + options = getClientOptions(options, Settings.getDefaultCommunicationProtocol(), undefined); + this.logger = new Logger("DaprClient", "DaprClient", options.logger); + + // Legacy validation on port + // URI validation is done later, when we instantiate the HttpEndpoint or GrpcEndpoint + // object in the HttpClient or GrpcClient constructor, but we need to + // keep this additional check for backward compatibility + // TODO: Remove this validation in the next major version + if (options?.daprPort && !/^[0-9]+$/.test(options?.daprPort)) { throw new Error("DAPR_INCORRECT_SIDECAR_PORT"); } // Builder switch (options.communicationProtocol) { case CommunicationProtocolEnum.GRPC: { - const client = new GRPCClient(this.options); + const client = new GRPCClient(options); this.daprClient = client; this.state = new GRPCClientState(client); @@ -119,7 +123,7 @@ export default class DaprClient { } case CommunicationProtocolEnum.HTTP: default: { - const client = new HTTPClient(this.options); + const client = new HTTPClient(options); this.daprClient = client; this.actor = new HTTPClientActor(client); // we use an abstractor here since we interface through a builder with the Actor Runtime @@ -139,6 +143,17 @@ export default class DaprClient { break; } } + + this.options = { + daprHost: this.daprClient.options.daprHost, + daprPort: this.daprClient.options.daprPort, + communicationProtocol: this.daprClient.options.communicationProtocol, + isKeepAlive: options.isKeepAlive, + logger: options.logger, + actor: options.actor, + daprApiToken: options.daprApiToken, + maxBodySizeMb: options.maxBodySizeMb, + }; } static create(client: IClient): DaprClient { diff --git a/src/implementation/Client/GRPCClient/GRPCClient.ts b/src/implementation/Client/GRPCClient/GRPCClient.ts index 397f8f14..a73b51f2 100644 --- a/src/implementation/Client/GRPCClient/GRPCClient.ts +++ b/src/implementation/Client/GRPCClient/GRPCClient.ts @@ -20,6 +20,8 @@ import { Logger } from "../../../logger/Logger"; import GRPCClientSidecar from "./sidecar"; import DaprClient from "../DaprClient"; import { SDK_VERSION } from "../../../version"; +import communicationProtocolEnum from "../../../enum/CommunicationProtocol.enum"; +import { GrpcEndpoint } from "../../../network/GrpcEndpoint"; export default class GRPCClient implements IClient { readonly options: DaprClientOptions; @@ -29,9 +31,22 @@ export default class GRPCClient implements IClient { private readonly clientCredentials: grpc.ChannelCredentials; private readonly logger: Logger; private readonly grpcClientOptions: Partial; + private daprEndpoint: GrpcEndpoint; + + constructor(options: Partial) { + this.daprEndpoint = this.generateEndpoint(options); + + this.options = { + daprHost: this.daprEndpoint.hostname, + daprPort: this.daprEndpoint.port, + communicationProtocol: communicationProtocolEnum.GRPC, + isKeepAlive: options?.isKeepAlive, + logger: options?.logger, + actor: options?.actor, + daprApiToken: options?.daprApiToken, + maxBodySizeMb: options?.maxBodySizeMb, + }; - constructor(options: DaprClientOptions) { - this.options = options; this.clientCredentials = this.generateCredentials(); this.grpcClientOptions = this.generateChannelOptions(); @@ -39,7 +54,11 @@ export default class GRPCClient implements IClient { this.isInitialized = false; this.logger.info(`Opening connection to ${this.options.daprHost}:${this.options.daprPort}`); - this.client = this.generateClient(this.options.daprHost, this.options.daprPort); + this.client = new GrpcDaprClient( + this.daprEndpoint.endpoint, + this.getClientCredentials(), + this.getGrpcClientOptions(), + ); } async getClient(requiresInitialization = true): Promise { @@ -59,8 +78,24 @@ export default class GRPCClient implements IClient { return this.grpcClientOptions; } + private generateEndpoint(options: Partial): GrpcEndpoint { + const host = options?.daprHost ?? Settings.getDefaultHost(); + const port = options?.daprPort ?? Settings.getDefaultGrpcPort(); + let uri = `${host}:${port}`; + + if (!(options?.daprHost || options?.daprPort)) { + // If neither host nor port are specified, check the endpoint environment variable. + const endpoint = Settings.getDefaultGrpcEndpoint(); + if (endpoint != "") { + uri = endpoint; + } + } + + return new GrpcEndpoint(uri); + } + private generateCredentials(): grpc.ChannelCredentials { - if (this.options.daprHost.startsWith("https")) { + if (this.daprEndpoint?.tls) { return grpc.ChannelCredentials.createSsl(); } return grpc.ChannelCredentials.createInsecure(); @@ -93,26 +128,6 @@ export default class GRPCClient implements IClient { return options; } - private generateClient(host: string, port: string): GrpcDaprClient { - return new GrpcDaprClient( - GRPCClient.getEndpoint(host, port), - this.getClientCredentials(), - this.getGrpcClientOptions(), - ); - } - - // The grpc client doesn't allow http:// or https:// for grpc connections, - // so we need to remove it, if it exists - static getEndpoint(host: string, port: string): string { - let endpoint = `${host}:${port}`; - const parts = endpoint.split("://"); - if (parts.length > 1 && parts[0].startsWith("http")) { - endpoint = parts[1]; - } - - return endpoint; - } - private generateInterceptors(): (options: any, nextCall: any) => grpc.InterceptingCall { return (options: any, nextCall: any) => { return new grpc.InterceptingCall(nextCall(options), { diff --git a/src/implementation/Client/HTTPClient/HTTPClient.ts b/src/implementation/Client/HTTPClient/HTTPClient.ts index b2cceb3d..eb08d83f 100644 --- a/src/implementation/Client/HTTPClient/HTTPClient.ts +++ b/src/implementation/Client/HTTPClient/HTTPClient.ts @@ -23,6 +23,8 @@ import { Logger } from "../../../logger/Logger"; import HTTPClientSidecar from "./sidecar"; import { SDK_VERSION } from "../../../version"; import * as SerializerUtil from "../../../utils/Serializer.util"; +import communicationProtocolEnum from "../../../enum/CommunicationProtocol.enum"; +import { HttpEndpoint } from "../../../network/HttpEndpoint"; export default class HTTPClient implements IClient { readonly options: DaprClientOptions; @@ -34,16 +36,25 @@ export default class HTTPClient implements IClient { private static httpAgent: http.Agent; private static httpsAgent: https.Agent; + private daprEndpoint: HttpEndpoint; + + constructor(options: Partial) { + this.daprEndpoint = this.generateEndpoint(options); + + this.options = { + daprHost: this.daprEndpoint.hostname, + daprPort: this.daprEndpoint.port, + communicationProtocol: communicationProtocolEnum.HTTP, + isKeepAlive: options?.isKeepAlive, + logger: options?.logger, + actor: options?.actor, + daprApiToken: options?.daprApiToken, + maxBodySizeMb: options?.maxBodySizeMb, + }; - constructor(options: DaprClientOptions) { - this.options = options; this.logger = new Logger("HTTPClient", "HTTPClient", this.options.logger); this.isInitialized = false; - - this.clientUrl = `${this.options.daprHost}:${this.options.daprPort}/v1.0`; - if (!this.clientUrl.startsWith("http://") && !this.clientUrl.startsWith("https://")) { - this.clientUrl = `http://${this.clientUrl}`; - } + this.clientUrl = `${this.daprEndpoint.endpoint}/v1.0`; if (!HTTPClient.client) { HTTPClient.client = fetch; @@ -63,6 +74,22 @@ export default class HTTPClient implements IClient { } } + private generateEndpoint(options: Partial): HttpEndpoint { + const host = options?.daprHost ?? Settings.getDefaultHost(); + const port = options?.daprPort ?? Settings.getDefaultHttpPort(); + let uri = `${host}:${port}`; + + if (!(options?.daprHost || options?.daprPort)) { + // If neither host nor port are specified, check the endpoint environment variable. + const endpoint = Settings.getDefaultHttpEndpoint(); + if (endpoint != "") { + uri = endpoint; + } + } + + return new HttpEndpoint(uri); + } + async getClient(requiresInitialization = true): Promise { // Ensure the sidecar has been started if (requiresInitialization && !this.isInitialized) { diff --git a/src/implementation/Server/DaprServer.ts b/src/implementation/Server/DaprServer.ts index 8a110cda..14326a6f 100644 --- a/src/implementation/Server/DaprServer.ts +++ b/src/implementation/Server/DaprServer.ts @@ -48,31 +48,38 @@ export default class DaprServer { constructor(serverOptions: Partial = {}) { const communicationProtocol = serverOptions.communicationProtocol ?? Settings.getDefaultCommunicationProtocol(); const clientOptions = getClientOptions(serverOptions.clientOptions, communicationProtocol, serverOptions?.logger); + + // Legacy validation on port + // URI validation is done later, when we instantiate the HttpEndpoint or GrpcEndpoint + // object in the HttpClient or GrpcClient constructor, but we need to + // keep this additional check for backward compatibility + // TODO: Remove this validation in the next major version + if (clientOptions?.daprPort && !/^[0-9]+$/.test(clientOptions?.daprPort)) { + throw new Error("DAPR_INCORRECT_SIDECAR_PORT"); + } + + this.client = new DaprClient(clientOptions); + this.serverOptions = { serverHost: serverOptions.serverHost ?? Settings.getDefaultHost(), serverPort: serverOptions.serverPort ?? Settings.getDefaultAppPort(communicationProtocol), communicationProtocol: communicationProtocol, maxBodySizeMb: serverOptions.maxBodySizeMb, serverHttp: serverOptions.serverHttp, - clientOptions: clientOptions, + clientOptions: this.client.options, logger: serverOptions.logger, }; // Create a client to interface with the sidecar from the server side - this.client = new DaprClient(clientOptions); // If DAPR_SERVER_PORT was not set, we set it process.env.DAPR_SERVER_PORT = this.serverOptions.serverPort; - process.env.DAPR_CLIENT_PORT = clientOptions.daprPort; + process.env.DAPR_CLIENT_PORT = this.client.options.daprPort; // Validation on port if (!/^[0-9]+$/.test(this.serverOptions.serverPort)) { throw new Error("DAPR_INCORRECT_SERVER_PORT"); } - if (!/^[0-9]+$/.test(clientOptions.daprPort)) { - throw new Error("DAPR_INCORRECT_SIDECAR_PORT"); - } - // Builder switch (serverOptions.communicationProtocol) { case CommunicationProtocolEnum.GRPC: { diff --git a/src/network/AbstractEndpoint.ts b/src/network/AbstractEndpoint.ts new file mode 100644 index 00000000..47f905ba --- /dev/null +++ b/src/network/AbstractEndpoint.ts @@ -0,0 +1,45 @@ +/* +Copyright 2023 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export abstract class Endpoint { + protected _scheme = ""; + protected _hostname = ""; + protected _port = 0; + protected _tls = false; + protected _url: string; + protected _endpoint = ""; + + protected constructor(url: string) { + this._url = url; + } + + get tls(): boolean { + return this._tls; + } + + get hostname(): string { + return this._hostname; + } + + get scheme(): string { + return this._scheme; + } + + get port(): string { + return this._port === 0 ? "" : this._port.toString(); + } + + get endpoint(): string { + return this._endpoint; + } +} diff --git a/src/network/GrpcEndpoint.ts b/src/network/GrpcEndpoint.ts new file mode 100644 index 00000000..f153f7bf --- /dev/null +++ b/src/network/GrpcEndpoint.ts @@ -0,0 +1,177 @@ +/* +Copyright 2023 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Endpoint } from "./AbstractEndpoint"; +import { URIParseConfig } from "./Network.consts"; +import { URL, URLSearchParams } from "url"; + +export class GrpcEndpoint extends Endpoint { + private _authority = ""; + + constructor(url: string) { + super(url); + this._authority = URIParseConfig.DEFAULT_AUTHORITY; + + const parsedUrl = new URL(this.preprocessUri(url)); + this.validatePathAndQuery(parsedUrl); + + this.setTls(parsedUrl); + this.setHostname(parsedUrl); + this.setScheme(parsedUrl); + this.setPort(parsedUrl); + this.setEndpoint(); + } + + private preprocessUri(url: string): string { + let urlList = url.split(":"); + + if (urlList.length === 3 && !url.includes("://")) { + // A URI like dns:mydomain:5000 or vsock:mycid:5000 was used + url = url.replace(":", "://"); + } else if ( + urlList.length >= 2 && + !url.includes("://") && + URIParseConfig.ACCEPTED_SCHEMES_GRPC.includes(urlList[0]) + ) { + // A URI like dns:mydomain was used + url = url.replace(":", "://"); + } else { + urlList = url.split("://"); + if (urlList.length === 1) { + // If a scheme was not explicitly specified in the URL + // we need to add a default scheme, + // because of how URL works in JavaScript + + // We also need to check if the provided uri is not of the form :5000 + // if it is, we need to add a default hostname, because the URL class can't parse it + if (url[0] === ":") { + url = `${URIParseConfig.DEFAULT_SCHEME_GRPC}://${URIParseConfig.DEFAULT_HOSTNAME}${url}`; + } else { + url = `${URIParseConfig.DEFAULT_SCHEME_GRPC}://${url}`; + } + } else { + // If a scheme was explicitly specified in the URL + // we need to make sure it is a valid scheme + const scheme = urlList[0]; + if (!URIParseConfig.ACCEPTED_SCHEMES_GRPC.includes(scheme)) { + throw new Error(`Invalid scheme '${scheme}' in URL '${url}'`); + } + + // We should do a special check if the scheme is dns, and it uses + // an authority in the format of dns:[//authority/]host[:port] + if (scheme.toLowerCase() === "dns") { + // A URI like dns://authority/mydomain was used + urlList = url.split("/"); + if (urlList.length < 4) { + throw new Error(`Invalid dns authority '${urlList[2]}' in URL '${url}'`); + } + this._authority = urlList[2]; + url = `dns://${urlList[3]}`; + } + } + } + return url; + } + + private validatePathAndQuery(parsedUrl: URL): void { + if (parsedUrl.pathname && parsedUrl.pathname !== "/") { + throw new Error(`Paths are not supported for gRPC endpoints: '${parsedUrl.pathname}'`); + } + + const params = new URLSearchParams(parsedUrl.search); + if (params.has("tls") && (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:")) { + throw new Error(`The tls query parameter is not supported for http(s) endpoints: '${parsedUrl.search}'`); + } + + params.delete("tls"); + if (Array.from(params.keys()).length > 0) { + throw new Error(`Query parameters are not supported for gRPC endpoints: '${parsedUrl.search}'`); + } + } + + private setTls(parsedUrl: URL): void { + const params = new URLSearchParams(parsedUrl.search); + const tlsStr = params.get("tls") || ""; + this._tls = tlsStr.toLowerCase() === "true"; + + if (parsedUrl.protocol == "https:") { + this._tls = true; + } + } + + private setHostname(parsedUrl: URL): void { + if (!parsedUrl.hostname) { + this._hostname = URIParseConfig.DEFAULT_HOSTNAME; + return; + } + + this._hostname = parsedUrl.hostname; + } + + private setScheme(parsedUrl: URL): void { + if (!parsedUrl.protocol) { + this._scheme = URIParseConfig.DEFAULT_SCHEME_GRPC; + return; + } + + const scheme = parsedUrl.protocol.slice(0, -1); // Remove trailing ':' + if (scheme === "http" || scheme === "https") { + this._scheme = URIParseConfig.DEFAULT_SCHEME_GRPC; + console.warn("http and https schemes are deprecated, use grpc or grpcs instead"); + return; + } + + if (!URIParseConfig.ACCEPTED_SCHEMES_GRPC.includes(scheme)) { + throw new Error(`Invalid scheme '${scheme}' in URL '${this._url}'`); + } + + this._scheme = scheme; + } + + private setPort(parsedUrl: URL): void { + if (this._scheme === "unix" || this._scheme === "unix-abstract") { + this._port = 0; + return; + } + + this._port = parsedUrl.port ? parseInt(parsedUrl.port) : URIParseConfig.DEFAULT_PORT; + } + + private setEndpoint(): void { + const port = this._port ? `:${this.port}` : ""; + + if (this._scheme === "unix") { + const separator = this._url.startsWith("unix://") ? "://" : ":"; + this._endpoint = `${this._scheme}${separator}${this._hostname}`; + return; + } + + if (this._scheme === "vsock") { + this._endpoint = `${this._scheme}:${this._hostname}:${this.port}`; + return; + } + + if (this._scheme === "unix-abstract") { + this._endpoint = `${this._scheme}:${this._hostname}${port}`; + return; + } + + if (this._scheme === "dns") { + const authority = this._authority ? `//${this._authority}/` : ""; + this._endpoint = `${this._scheme}:${authority}${this._hostname}${port}`; + return; + } + + this._endpoint = `${this._scheme}:${this._hostname}${port}`; + } +} diff --git a/src/network/HttpEndpoint.ts b/src/network/HttpEndpoint.ts new file mode 100644 index 00000000..f9f32f0a --- /dev/null +++ b/src/network/HttpEndpoint.ts @@ -0,0 +1,50 @@ +/* +Copyright 2023 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Endpoint } from "./AbstractEndpoint"; +import { URL } from "url"; +import { URIParseConfig } from "./Network.consts"; + +export class HttpEndpoint extends Endpoint { + constructor(url: string) { + super(url); + + try { + const parsedUrl = new URL(HttpEndpoint.preprocessUri(url)); + this._scheme = parsedUrl.protocol.replace(":", ""); + const re = /[\][]/gi; + this._hostname = parsedUrl.hostname.replace(re, ""); + this._port = parseInt(parsedUrl.port) || (this._scheme == "https" ? 443 : 80); + this._tls = this._scheme == "https"; + // Remove brackets if it's a IPv6 addresses + const hostPart = parsedUrl.hostname.includes("[") ? `[${this._hostname}]` : this._hostname; + this._endpoint = `${this._scheme}://${hostPart}:${this._port}`; + } catch (error) { + throw new Error(`Invalid address: ${url}`); + } + } + + // We need to add a default scheme and hostname to the url + // if they are not specified so that the URL class can parse it + // Ex: 127.0.0.1 -> http://127.0.0.1 + // Ex: :5000 -> http://127.0.0.1:5000 + private static preprocessUri(url: string) { + if (url.startsWith(":")) { + return URIParseConfig.DEFAULT_SCHEME_HTTP + "://" + URIParseConfig.DEFAULT_HOSTNAME + url; + } + if (!url.includes("://")) { + return URIParseConfig.DEFAULT_SCHEME_HTTP + "://" + url; + } + return url; + } +} diff --git a/test/unit/protocols/grpc/GRPCClient.test.ts b/src/network/Network.consts.ts similarity index 50% rename from test/unit/protocols/grpc/GRPCClient.test.ts rename to src/network/Network.consts.ts index 529934f6..9114107b 100644 --- a/test/unit/protocols/grpc/GRPCClient.test.ts +++ b/src/network/Network.consts.ts @@ -11,18 +11,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { GRPCClient } from "../../../../src"; - -describe("grpc", () => { - it("getEndpoint should remove http and https from endpoint", () => { - const testCases = [ - { host: "http://localhost", port: "5000", expected: "localhost:5000" }, - { host: "https://localhost", port: "5000", expected: "localhost:5000" }, - { host: "localhost", port: "5000", expected: "localhost:5000" }, - ]; - - testCases.forEach((testCase) => { - expect(GRPCClient.getEndpoint(testCase.host, testCase.port)).toBe(testCase.expected); - }); - }); -}); +export class URIParseConfig { + static readonly DEFAULT_SCHEME_GRPC = "dns"; + static readonly DEFAULT_SCHEME_HTTP = "http"; + static readonly DEFAULT_HOSTNAME = "localhost"; + static readonly DEFAULT_PORT = 443; + static readonly DEFAULT_AUTHORITY = ""; + static readonly ACCEPTED_SCHEMES_GRPC = ["dns", "unix", "unix-abstract", "vsock", "http", "https", "grpc", "grpcs"]; +} diff --git a/src/utils/Client.util.ts b/src/utils/Client.util.ts index 5766c482..fd4fb73f 100644 --- a/src/utils/Client.util.ts +++ b/src/utils/Client.util.ts @@ -26,11 +26,11 @@ import { PubSubBulkPublishMessage } from "../types/pubsub/PubSubBulkPublishMessa import { PubSubBulkPublishApiResponse } from "../types/pubsub/PubSubBulkPublishApiResponse.type"; import { DaprClientOptions } from "../types/DaprClientOptions"; import CommunicationProtocolEnum from "../enum/CommunicationProtocol.enum"; -import { Settings } from "./Settings.util"; import { LoggerOptions } from "../types/logger/LoggerOptions"; import { StateConsistencyEnum } from "../enum/StateConsistency.enum"; import { StateConcurrencyEnum } from "../enum/StateConcurrency.enum"; -import { URL, URLSearchParams } from "url"; +import { Settings } from "./Settings.util"; + /** * Adds metadata to a map. * @param map Input map @@ -262,32 +262,11 @@ export function getClientOptions( clientOptions: Partial | undefined, defaultCommunicationProtocol: CommunicationProtocolEnum, defaultLoggerOptions: LoggerOptions | undefined, -): DaprClientOptions { +): Partial { const clientCommunicationProtocol = clientOptions?.communicationProtocol ?? defaultCommunicationProtocol; - - // We decide the host/port/endpoint here - let daprEndpoint = ""; - if (clientCommunicationProtocol == CommunicationProtocolEnum.HTTP) { - daprEndpoint = Settings.getDefaultHttpEndpoint(); - } else if (clientCommunicationProtocol == CommunicationProtocolEnum.GRPC) { - daprEndpoint = Settings.getDefaultGrpcEndpoint(); - } - - let host = Settings.getDefaultHost(); - let port = Settings.getDefaultPort(clientCommunicationProtocol); - - if (clientOptions?.daprHost || clientOptions?.daprPort) { - host = clientOptions?.daprHost ?? host; - port = clientOptions?.daprPort ?? port; - } else if (daprEndpoint != "") { - const [scheme, fqdn, p] = parseEndpoint(daprEndpoint); - host = `${scheme}://${fqdn}`; - port = p; - } - return { - daprHost: host, - daprPort: port, + daprHost: clientOptions?.daprHost, + daprPort: clientOptions?.daprPort, communicationProtocol: clientCommunicationProtocol, isKeepAlive: clientOptions?.isKeepAlive, logger: clientOptions?.logger ?? defaultLoggerOptions, @@ -296,48 +275,3 @@ export function getClientOptions( maxBodySizeMb: clientOptions?.maxBodySizeMb, }; } - -/** - * Scheme, fqdn and port - */ -type EndpointTuple = [string, string, string]; - -/** - * Parses an endpoint to scheme, fqdn and port - * Examples: - * - http://localhost:3500 -> [http, localhost, 3500] - * - localhost:3500 -> [http, localhost, 3500] - * - :3500 -> [http, localhost, 3500] - * - localhost -> [http, localhost, 80] - * - https://localhost:3500 -> [https, localhost, 3500] - * - [::1]:3500 -> [http, ::1, 3500] - * - [::1] -> [http, ::1, 80] - * - http://[2001:db8:1f70:0:999:de8:7648:6e8]:5000 -> [http, 2001:db8:1f70:0:999:de8:7648:6e8, 5000] - * @throws Error if the address is invalid - * @param address Endpoint address - * @returns EndpointTuple (scheme, fqdn, port) - */ -export function parseEndpoint(address: string): EndpointTuple { - // Prefix with a scheme and host when they're not present, - // because the URL library won't parse it otherwise - if (address.startsWith(":")) { - address = "http://localhost" + address; - } - if (!address.includes("://")) { - address = "http://" + address; - } - - let scheme, fqdn, port: string; - - try { - const myURL = new URL(address); - scheme = myURL.protocol.replace(":", ""); - fqdn = myURL.hostname.replace("[", ""); - fqdn = fqdn.replace("]", ""); - port = myURL.port || (myURL.protocol == "https:" ? "443" : "80"); - } catch (error) { - throw new Error(`Invalid address: ${address}`); - } - - return [scheme, fqdn, port]; -} diff --git a/test/e2e/common/client.test.ts b/test/e2e/common/client.test.ts index e8b48d01..5f1bdb40 100644 --- a/test/e2e/common/client.test.ts +++ b/test/e2e/common/client.test.ts @@ -21,7 +21,6 @@ import { } from "../../../src"; import { sleep } from "../../../src/utils/NodeJS.util"; import { LockStatus } from "../../../src/types/lock/UnlockResponse"; -import { Settings } from "../../../src/utils/Settings.util"; const daprHost = "127.0.0.1"; const daprGrpcPort = "50000"; @@ -558,128 +557,3 @@ describe("common/client", () => { }); }); }); - -describe("http/client with environment variables", () => { - let client: DaprClient; - - // We need to start listening on some endpoints already - // this because Dapr is not dynamic and registers endpoints on boot - // we put a timeout of 10s since it takes around 4s for Dapr to boot up - - afterAll(async () => { - await client.stop(); - }); - - it("should give preference to host and port in constructor arguments over endpoint environment variables ", async () => { - process.env.DAPR_HTTP_ENDPOINT = "https://httpdomain.com"; - process.env.DAPR_GRPC_ENDPOINT = "https://grpcdomain.com"; - - client = new DaprClient({ - daprHost, - daprPort: daprHttpPort, - communicationProtocol: CommunicationProtocolEnum.HTTP, - isKeepAlive: false, - }); - - expect(client.options.daprHost).toEqual(daprHost); - expect(client.options.daprPort).toEqual(daprHttpPort); - - client = new DaprClient({ - daprHost, - daprPort: daprGrpcPort, - communicationProtocol: CommunicationProtocolEnum.GRPC, - isKeepAlive: false, - }); - - expect(client.options.daprHost).toEqual(daprHost); - expect(client.options.daprPort).toEqual(daprGrpcPort); - }); - - it("should give preference to port with no host in constructor arguments over environment variables ", async () => { - process.env.DAPR_HTTP_ENDPOINT = "https://httpdomain.com"; - process.env.DAPR_GRPC_ENDPOINT = "https://grpcdomain.com"; - - client = new DaprClient({ - daprPort: daprHttpPort, - communicationProtocol: CommunicationProtocolEnum.HTTP, - isKeepAlive: false, - }); - - expect(client.options.daprHost).toEqual(Settings.getDefaultHost()); - expect(client.options.daprPort).toEqual(daprHttpPort); - - client = new DaprClient({ - daprPort: daprGrpcPort, - communicationProtocol: CommunicationProtocolEnum.GRPC, - isKeepAlive: false, - }); - - expect(client.options.daprHost).toEqual(Settings.getDefaultHost()); - expect(client.options.daprPort).toEqual(daprGrpcPort); - }); - - it("should give preference to host with no port in constructor arguments over environment variables ", async () => { - process.env.DAPR_HTTP_ENDPOINT = "https://httpdomain.com"; - process.env.DAPR_GRPC_ENDPOINT = "https://grpcdomain.com"; - - client = new DaprClient({ - daprHost: daprHost, - communicationProtocol: CommunicationProtocolEnum.HTTP, - isKeepAlive: false, - }); - - expect(client.options.daprHost).toEqual(daprHost); - expect(client.options.daprPort).toEqual(Settings.getDefaultPort(CommunicationProtocolEnum.HTTP)); - - client = new DaprClient({ - daprHost: daprHost, - communicationProtocol: CommunicationProtocolEnum.GRPC, - isKeepAlive: false, - }); - - expect(client.options.daprHost).toEqual(daprHost); - expect(client.options.daprPort).toEqual(Settings.getDefaultPort(CommunicationProtocolEnum.GRPC)); - }); - - it("should use environment variable endpoint for HTTP", async () => { - process.env.DAPR_HTTP_ENDPOINT = "https://httpdomain.com"; - client = new DaprClient({ - communicationProtocol: CommunicationProtocolEnum.HTTP, - isKeepAlive: false, - }); - - expect(client.options.daprHost).toEqual("https://httpdomain.com"); - expect(client.options.daprPort).toEqual("443"); - }); - - it("should use environment variable endpoint for GRPC", async () => { - process.env.DAPR_GRPC_ENDPOINT = "https://grpcdomain.com"; - client = new DaprClient({ - communicationProtocol: CommunicationProtocolEnum.GRPC, - isKeepAlive: false, - }); - - expect(client.options.daprHost).toEqual("https://grpcdomain.com"); - expect(client.options.daprPort).toEqual("443"); - }); - - it("should use default host and port when no other parameters provided", async () => { - process.env.DAPR_HTTP_ENDPOINT = ""; - process.env.DAPR_GRPC_ENDPOINT = ""; - client = new DaprClient({ - communicationProtocol: CommunicationProtocolEnum.HTTP, - isKeepAlive: false, - }); - - expect(client.options.daprHost).toEqual(Settings.getDefaultHost()); - expect(client.options.daprPort).toEqual(Settings.getDefaultPort(CommunicationProtocolEnum.HTTP)); - - client = new DaprClient({ - communicationProtocol: CommunicationProtocolEnum.GRPC, - isKeepAlive: false, - }); - - expect(client.options.daprHost).toEqual(Settings.getDefaultHost()); - expect(client.options.daprPort).toEqual(Settings.getDefaultPort(CommunicationProtocolEnum.GRPC)); - }); -}); diff --git a/test/unit/utils/Client.util.test.ts b/test/unit/utils/Client.util.test.ts index 2ec1bfdb..94992cca 100644 --- a/test/unit/utils/Client.util.test.ts +++ b/test/unit/utils/Client.util.test.ts @@ -20,12 +20,13 @@ import { getBulkPublishResponse, getClientOptions, createHTTPQueryParam, - parseEndpoint, } from "../../../src/utils/Client.util"; import { Map } from "google-protobuf"; import { PubSubBulkPublishEntry } from "../../../src/types/pubsub/PubSubBulkPublishEntry.type"; import { PubSubBulkPublishApiResponse } from "../../../src/types/pubsub/PubSubBulkPublishApiResponse.type"; import { CommunicationProtocolEnum, DaprClientOptions, LogLevel } from "../../../src"; +import { DaprClient } from "../../../src"; +import { Settings } from "../../../src/utils/Settings.util"; describe("Client.util", () => { describe("addMetadataToMap", () => { @@ -341,10 +342,13 @@ describe("Client.util", () => { daprPort: "50001", communicationProtocol: CommunicationProtocolEnum.GRPC, }; + const options = getClientOptions(inOptions, CommunicationProtocolEnum.HTTP, { level: LogLevel.Error }); - const expectedOptions: Partial = inOptions; - expectedOptions.daprHost = "127.0.0.1"; - expectedOptions.logger = { level: LogLevel.Error }; + const expectedOptions: Partial = { + daprPort: inOptions.daprPort, + communicationProtocol: inOptions.communicationProtocol, + logger: { level: LogLevel.Error }, + }; expect(options).toEqual(expectedOptions); }); @@ -353,9 +357,9 @@ describe("Client.util", () => { communicationProtocol: CommunicationProtocolEnum.GRPC, }; const options = getClientOptions(inOptions, CommunicationProtocolEnum.HTTP, undefined); - const expectedOptions: Partial = inOptions; - expectedOptions.daprHost = "127.0.0.1"; - expectedOptions.daprPort = "50001"; + const expectedOptions: Partial = { + communicationProtocol: CommunicationProtocolEnum.GRPC, + }; expect(options).toEqual(expectedOptions); }); @@ -391,8 +395,6 @@ describe("Client.util", () => { it("returns correct Dapr Client Options when undefined options provided", () => { const options = getClientOptions(undefined, CommunicationProtocolEnum.GRPC, undefined); const expectedOptions: Partial = { - daprHost: "127.0.0.1", - daprPort: "50001", communicationProtocol: CommunicationProtocolEnum.GRPC, }; expect(options).toEqual(expectedOptions); @@ -401,392 +403,148 @@ describe("Client.util", () => { it("returns correct Dapr Client Options when undefined options provided and default HTTP communication", () => { const options = getClientOptions(undefined, CommunicationProtocolEnum.HTTP, undefined); const expectedOptions: Partial = { - daprHost: "127.0.0.1", - daprPort: "3500", communicationProtocol: CommunicationProtocolEnum.HTTP, }; expect(options).toEqual(expectedOptions); }); }); - describe("parseEndpoint", () => { - const testCases = [ - { endpoint: ":5000", scheme: "http", host: "localhost", port: "5000" }, - { - endpoint: ":5000/v1/dapr", - scheme: "http", - host: "localhost", - port: "5000", - }, + describe("test correct client instantiation", () => { + let client: DaprClient; + const daprHost = "127.0.0.1"; + const daprGrpcPort = "50000"; + const daprHttpPort = "3500"; - { endpoint: "localhost", scheme: "http", host: "localhost", port: "80" }, - { - endpoint: "localhost/v1/dapr", - scheme: "http", - host: "localhost", - port: "80", - }, - { - endpoint: "localhost:5000", - scheme: "http", - host: "localhost", - port: "5000", - }, - { - endpoint: "localhost:5000/v1/dapr", - scheme: "http", - host: "localhost", - port: "5000", - }, + // We need to start listening on some endpoints already + // this because Dapr is not dynamic and registers endpoints on boot + // we put a timeout of 10s since it takes around 4s for Dapr to boot up - { - endpoint: "http://localhost", - scheme: "http", - host: "localhost", - port: "80", - }, - { - endpoint: "http://localhost/v1/dapr", - scheme: "http", - host: "localhost", - port: "80", - }, - { - endpoint: "http://localhost:5000", - scheme: "http", - host: "localhost", - port: "5000", - }, - { - endpoint: "http://localhost:5000/v1/dapr", - scheme: "http", - host: "localhost", - port: "5000", - }, + afterAll(async () => { + await client.stop(); + }); - { - endpoint: "https://localhost", - scheme: "https", - host: "localhost", - port: "443", - }, - { - endpoint: "https://localhost/v1/dapr", - scheme: "https", - host: "localhost", - port: "443", - }, - { - endpoint: "https://localhost:5000", - scheme: "https", - host: "localhost", - port: "5000", - }, - { - endpoint: "https://localhost:5000/v1/dapr", - scheme: "https", - host: "localhost", - port: "5000", - }, + it("should give preference to host and port in constructor arguments over endpoint environment variables ", async () => { + process.env.DAPR_HTTP_ENDPOINT = "https://httpdomain.com"; + process.env.DAPR_GRPC_ENDPOINT = "https://grpcdomain.com"; - { endpoint: "127.0.0.1", scheme: "http", host: "127.0.0.1", port: "80" }, - { - endpoint: "127.0.0.1/v1/dapr", - scheme: "http", - host: "127.0.0.1", - port: "80", - }, - { - endpoint: "127.0.0.1:5000", - scheme: "http", - host: "127.0.0.1", - port: "5000", - }, - { - endpoint: "127.0.0.1:5000/v1/dapr", - scheme: "http", - host: "127.0.0.1", - port: "5000", - }, + // HTTP + client = new DaprClient({ + daprHost, + daprPort: daprHttpPort, + communicationProtocol: CommunicationProtocolEnum.HTTP, + isKeepAlive: false, + }); - { - endpoint: "http://127.0.0.1", - scheme: "http", - host: "127.0.0.1", - port: "80", - }, - { - endpoint: "http://127.0.0.1/v1/dapr", - scheme: "http", - host: "127.0.0.1", - port: "80", - }, - { - endpoint: "http://127.0.0.1:5000", - scheme: "http", - host: "127.0.0.1", - port: "5000", - }, - { - endpoint: "http://127.0.0.1:5000/v1/dapr", - scheme: "http", - host: "127.0.0.1", - port: "5000", - }, + expect(client.options.daprHost).toEqual(daprHost); + expect(client.options.daprPort).toEqual(daprHttpPort); - { - endpoint: "https://127.0.0.1", - scheme: "https", - host: "127.0.0.1", - port: "443", - }, - { - endpoint: "https://127.0.0.1/v1/dapr", - scheme: "https", - host: "127.0.0.1", - port: "443", - }, - { - endpoint: "https://127.0.0.1:5000", - scheme: "https", - host: "127.0.0.1", - port: "5000", - }, - { - endpoint: "https://127.0.0.1:5000/v1/dapr", - scheme: "https", - host: "127.0.0.1", - port: "5000", - }, + // GRPC + client = new DaprClient({ + daprHost, + daprPort: daprGrpcPort, + communicationProtocol: CommunicationProtocolEnum.GRPC, + isKeepAlive: false, + }); - { - endpoint: "[2001:db8:1f70:0:999:de8:7648:6e8]", - scheme: "http", - host: "2001:db8:1f70:0:999:de8:7648:6e8", - port: "80", - }, - { - endpoint: "[2001:db8:1f70:0:999:de8:7648:6e8]/v1/dapr", - scheme: "http", - host: "2001:db8:1f70:0:999:de8:7648:6e8", - port: "80", - }, - { - endpoint: "[2001:db8:1f70:0:999:de8:7648:6e8]:5000", - scheme: "http", - host: "2001:db8:1f70:0:999:de8:7648:6e8", - port: "5000", - }, - { - endpoint: "[2001:db8:1f70:0:999:de8:7648:6e8]:5000/v1/dapr", - scheme: "http", - host: "2001:db8:1f70:0:999:de8:7648:6e8", - port: "5000", - }, + expect(client.options.daprHost).toEqual(daprHost); + expect(client.options.daprPort).toEqual(daprGrpcPort); + }); - { - endpoint: "http://[2001:db8:1f70:0:999:de8:7648:6e8]", - scheme: "http", - host: "2001:db8:1f70:0:999:de8:7648:6e8", - port: "80", - }, - { - endpoint: "http://[2001:db8:1f70:0:999:de8:7648:6e8]/v1/dapr", - scheme: "http", - host: "2001:db8:1f70:0:999:de8:7648:6e8", - port: "80", - }, - { - endpoint: "http://[2001:db8:1f70:0:999:de8:7648:6e8]:5000", - scheme: "http", - host: "2001:db8:1f70:0:999:de8:7648:6e8", - port: "5000", - }, - { - endpoint: "http://[2001:db8:1f70:0:999:de8:7648:6e8]:5000/v1/dapr", - scheme: "http", - host: "2001:db8:1f70:0:999:de8:7648:6e8", - port: "5000", - }, + it("should give preference to port with no host in constructor arguments over environment variables ", async () => { + process.env.DAPR_HTTP_ENDPOINT = "https://httpdomain.com"; + process.env.DAPR_GRPC_ENDPOINT = "https://grpcdomain.com"; - { - endpoint: "https://[2001:db8:1f70:0:999:de8:7648:6e8]", - scheme: "https", - host: "2001:db8:1f70:0:999:de8:7648:6e8", - port: "443", - }, - { - endpoint: "https://[2001:db8:1f70:0:999:de8:7648:6e8]/v1/dapr", - scheme: "https", - host: "2001:db8:1f70:0:999:de8:7648:6e8", - port: "443", - }, - { - endpoint: "https://[2001:db8:1f70:0:999:de8:7648:6e8]:5000", - scheme: "https", - host: "2001:db8:1f70:0:999:de8:7648:6e8", - port: "5000", - }, - { - endpoint: "https://[2001:db8:1f70:0:999:de8:7648:6e8]:5000/v1/dapr", - scheme: "https", - host: "2001:db8:1f70:0:999:de8:7648:6e8", - port: "5000", - }, + // HTTP + client = new DaprClient({ + daprPort: daprHttpPort, + communicationProtocol: CommunicationProtocolEnum.HTTP, + isKeepAlive: false, + }); - { endpoint: "domain.com", scheme: "http", host: "domain.com", port: "80" }, - { - endpoint: "domain.com/v1/grpc", - scheme: "http", - host: "domain.com", - port: "80", - }, - { - endpoint: "domain.com:5000", - scheme: "http", - host: "domain.com", - port: "5000", - }, - { - endpoint: "domain.com:5000/v1/dapr", - scheme: "http", - host: "domain.com", - port: "5000", - }, + expect(client.options.daprHost).toEqual(Settings.getDefaultHost()); + expect(client.options.daprPort).toEqual(daprHttpPort); - { - endpoint: "http://domain.com", - scheme: "http", - host: "domain.com", - port: "80", - }, - { - endpoint: "http://domain.com/v1/dapr", - scheme: "http", - host: "domain.com", - port: "80", - }, - { - endpoint: "http://domain.com:5000", - scheme: "http", - host: "domain.com", - port: "5000", - }, - { - endpoint: "http://domain.com:5000/v1/dapr", - scheme: "http", - host: "domain.com", - port: "5000", - }, + // GRPC + client = new DaprClient({ + daprPort: daprGrpcPort, + communicationProtocol: CommunicationProtocolEnum.GRPC, + isKeepAlive: false, + }); - { - endpoint: "https://domain.com", - scheme: "https", - host: "domain.com", - port: "443", - }, - { - endpoint: "https://domain.com/v1/dapr", - scheme: "https", - host: "domain.com", - port: "443", - }, - { - endpoint: "https://domain.com:5000", - scheme: "https", - host: "domain.com", - port: "5000", - }, - { - endpoint: "https://domain.com:5000/v1/dapr", - scheme: "https", - host: "domain.com", - port: "5000", - }, + expect(client.options.daprHost).toEqual(Settings.getDefaultHost()); + expect(client.options.daprPort).toEqual(daprGrpcPort); + }); - { - endpoint: "abc.domain.com", - scheme: "http", - host: "abc.domain.com", - port: "80", - }, - { - endpoint: "abc.domain.com/v1/grpc", - scheme: "http", - host: "abc.domain.com", - port: "80", - }, - { - endpoint: "abc.domain.com:5000", - scheme: "http", - host: "abc.domain.com", - port: "5000", - }, - { - endpoint: "abc.domain.com:5000/v1/dapr", - scheme: "http", - host: "abc.domain.com", - port: "5000", - }, + it("should give preference to host with no port in constructor arguments over environment variables ", async () => { + process.env.DAPR_HTTP_ENDPOINT = "https://httpdomain.com"; + process.env.DAPR_GRPC_ENDPOINT = "https://grpcdomain.com"; - { - endpoint: "http://abc.domain.com/v1/dapr", - scheme: "http", - host: "abc.domain.com", - port: "80", - }, - { - endpoint: "http://abc.domain.com/v1/dapr", - scheme: "http", - host: "abc.domain.com", - port: "80", - }, - { - endpoint: "http://abc.domain.com:5000/v1/dapr", - scheme: "http", - host: "abc.domain.com", - port: "5000", - }, - { - endpoint: "http://abc.domain.com:5000/v1/dapr/v1/dapr", - scheme: "http", - host: "abc.domain.com", - port: "5000", - }, + // HTTP + client = new DaprClient({ + daprHost: daprHost, + communicationProtocol: CommunicationProtocolEnum.HTTP, + isKeepAlive: false, + }); - { - endpoint: "https://abc.domain.com/v1/dapr", - scheme: "https", - host: "abc.domain.com", - port: "443", - }, - { - endpoint: "https://abc.domain.com/v1/dapr", - scheme: "https", - host: "abc.domain.com", - port: "443", - }, - { - endpoint: "https://abc.domain.com:5000/v1/dapr", - scheme: "https", - host: "abc.domain.com", - port: "5000", - }, - { - endpoint: "https://abc.domain.com:5000/v1/dapr/v1/dapr", - scheme: "https", - host: "abc.domain.com", - port: "5000", - }, - ]; + expect(client.options.daprHost).toEqual(daprHost); + expect(client.options.daprPort).toEqual(Settings.getDefaultPort(CommunicationProtocolEnum.HTTP)); + + // GRPC + client = new DaprClient({ + daprHost: daprHost, + communicationProtocol: CommunicationProtocolEnum.GRPC, + isKeepAlive: false, + }); + + expect(client.options.daprHost).toEqual(daprHost); + expect(client.options.daprPort).toEqual(Settings.getDefaultPort(CommunicationProtocolEnum.GRPC)); + }); + + it("should use environment variable endpoint for HTTP", async () => { + process.env.DAPR_HTTP_ENDPOINT = "https://httpdomain.com"; + process.env.DAPR_GRPC_ENDPOINT = "https://grpcdomain.com"; + client = new DaprClient({ + communicationProtocol: CommunicationProtocolEnum.HTTP, + isKeepAlive: false, + }); + + expect(client.options.daprHost).toEqual("httpdomain.com"); + expect(client.options.daprPort).toEqual("443"); + }); + + it("should use environment variable endpoint for GRPC", async () => { + process.env.DAPR_HTTP_ENDPOINT = "https://httpdomain.com"; + process.env.DAPR_GRPC_ENDPOINT = "https://grpcdomain.com"; + client = new DaprClient({ + communicationProtocol: CommunicationProtocolEnum.GRPC, + isKeepAlive: false, + }); + + expect(client.options.daprHost).toEqual("grpcdomain.com"); + expect(client.options.daprPort).toEqual("443"); + }); + + it("should use default host and port when no other parameters provided", async () => { + process.env.DAPR_HTTP_ENDPOINT = ""; + process.env.DAPR_GRPC_ENDPOINT = ""; + + // HTTP + client = new DaprClient({ + communicationProtocol: CommunicationProtocolEnum.HTTP, + isKeepAlive: false, + }); + + expect(client.options.daprHost).toEqual(Settings.getDefaultHost()); + expect(client.options.daprPort).toEqual(Settings.getDefaultPort(CommunicationProtocolEnum.HTTP)); - testCases.forEach(({ endpoint, scheme, host, port }) => { - it(`should correctly parse ${endpoint}`, () => { - const result = parseEndpoint(endpoint); - expect(result[0]).toEqual(scheme); - expect(result[1]).toEqual(host); - expect(result[2]).toEqual(port); + // GRPC + client = new DaprClient({ + communicationProtocol: CommunicationProtocolEnum.GRPC, + isKeepAlive: false, }); + + expect(client.options.daprHost).toEqual(Settings.getDefaultHost()); + expect(client.options.daprPort).toEqual(Settings.getDefaultPort(CommunicationProtocolEnum.GRPC)); }); }); }); diff --git a/test/unit/utils/Network.test.ts b/test/unit/utils/Network.test.ts new file mode 100644 index 00000000..0ec0c3bd --- /dev/null +++ b/test/unit/utils/Network.test.ts @@ -0,0 +1,448 @@ +/* +Copyright 2022 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { GrpcEndpoint } from "../../../src/network/GrpcEndpoint"; +import { HttpEndpoint } from "../../../src/network/HttpEndpoint"; + +describe("Client.util", () => { + describe("parse GRPC Endpoint", () => { + const testCases = [ + // Port only + { + url: ":5000", + error: false, + secure: false, + scheme: "", + host: "localhost", + port: 5000, + endpoint: "dns:localhost:5000", + }, + { + url: ":5000?tls=false", + error: false, + secure: false, + scheme: "", + host: "localhost", + port: 5000, + endpoint: "dns:localhost:5000", + }, + { + url: ":5000?tls=true", + error: false, + secure: true, + scheme: "", + host: "localhost", + port: 5000, + endpoint: "dns:localhost:5000", + }, + // Host only + { + url: "myhost", + error: false, + secure: false, + scheme: "", + host: "myhost", + port: 443, + endpoint: "dns:myhost:443", + }, + { + url: "myhost?tls=false", + error: false, + secure: false, + scheme: "", + host: "myhost", + port: 443, + endpoint: "dns:myhost:443", + }, + { + url: "myhost?tls=true", + error: false, + secure: true, + scheme: "", + host: "myhost", + port: 443, + endpoint: "dns:myhost:443", + }, + // Host and port + { + url: "myhost:443", + error: false, + secure: false, + scheme: "", + host: "myhost", + port: 443, + endpoint: "dns:myhost:443", + }, + { + url: "myhost:443?tls=false", + error: false, + secure: false, + scheme: "", + host: "myhost", + port: 443, + endpoint: "dns:myhost:443", + }, + { + url: "myhost:443?tls=true", + error: false, + secure: true, + scheme: "", + host: "myhost", + port: 443, + endpoint: "dns:myhost:443", + }, + // Scheme, host and port + { + url: "http://myhost", + error: false, + secure: false, + scheme: "", + host: "myhost", + port: 443, + endpoint: "dns:myhost:443", + }, + { url: "http://myhost?tls=false", error: true }, + { url: "http://myhost?tls=true", error: true }, + { + url: "http://myhost:443", + error: false, + secure: false, + scheme: "", + host: "myhost", + port: 443, + endpoint: "dns:myhost:443", + }, + { url: "http://myhost:443?tls=false", error: true }, + { url: "http://myhost:443?tls=true", error: true }, + { + url: "http://myhost:5000", + error: false, + secure: false, + scheme: "", + host: "myhost", + port: 5000, + endpoint: "dns:myhost:5000", + }, + { url: "http://myhost:5000?tls=false", error: true }, + { url: "http://myhost:5000?tls=true", error: true }, + { + url: "https://myhost:443", + error: false, + secure: true, + scheme: "", + host: "myhost", + port: 443, + endpoint: "dns:myhost:443", + }, + { url: "https://myhost:443?tls=false", error: true }, + { url: "https://myhost:443?tls=true", error: true }, + // Scheme = dns + { + url: "dns:myhost", + error: false, + secure: false, + scheme: "dns", + host: "myhost", + port: 443, + endpoint: "dns:myhost:443", + }, + { + url: "dns:myhost?tls=false", + error: false, + secure: false, + scheme: "dns", + host: "myhost", + port: 443, + endpoint: "dns:myhost:443", + }, + { + url: "dns:myhost?tls=true", + error: false, + secure: true, + scheme: "dns", + host: "myhost", + port: 443, + endpoint: "dns:myhost:443", + }, + // Scheme = dns with authority + { + url: "dns://myauthority:53/myhost", + error: false, + secure: false, + scheme: "dns", + host: "myhost", + port: 443, + endpoint: "dns://myauthority:53/myhost:443", + }, + { + url: "dns://myauthority:53/myhost?tls=false", + error: false, + secure: false, + scheme: "dns", + host: "myhost", + port: 443, + endpoint: "dns://myauthority:53/myhost:443", + }, + { + url: "dns://myauthority:53/myhost?tls=true", + error: false, + secure: true, + scheme: "dns", + host: "myhost", + port: 443, + endpoint: "dns://myauthority:53/myhost:443", + }, + { url: "dns://myhost", error: true }, + // Unix sockets + { + url: "unix:my.sock", + error: false, + secure: false, + scheme: "unix", + host: "my.sock", + port: "", + endpoint: "unix:my.sock", + }, + { + url: "unix:my.sock?tls=true", + error: false, + secure: true, + scheme: "unix", + host: "my.sock", + port: "", + endpoint: "unix:my.sock", + }, + // Unix sockets with absolute path + { + url: "unix://my.sock", + error: false, + secure: false, + scheme: "unix", + host: "my.sock", + port: "", + endpoint: "unix://my.sock", + }, + { + url: "unix://my.sock?tls=true", + error: false, + secure: true, + scheme: "unix", + host: "my.sock", + port: "", + endpoint: "unix://my.sock", + }, + // Unix abstract sockets + { + url: "unix-abstract:my.sock", + error: false, + secure: false, + scheme: "unix", + host: "my.sock", + port: "", + endpoint: "unix-abstract:my.sock", + }, + { + url: "unix-abstract:my.sock?tls=true", + error: false, + secure: true, + scheme: "unix", + host: "my.sock", + port: "", + endpoint: "unix-abstract:my.sock", + }, + // Vsock + { + url: "vsock:mycid", + error: false, + secure: false, + scheme: "vsock", + host: "mycid", + port: "443", + endpoint: "vsock:mycid:443", + }, + { + url: "vsock:mycid:5000", + error: false, + secure: false, + scheme: "vsock", + host: "mycid", + port: 5000, + endpoint: "vsock:mycid:5000", + }, + { + url: "vsock:mycid:5000?tls=true", + error: false, + secure: true, + scheme: "vsock", + host: "mycid", + port: 5000, + endpoint: "vsock:mycid:5000", + }, + + // IPv6 addresses + { + url: "[2001:db8:1f70:0:999:de8:7648:6e8]", + error: false, + secure: false, + scheme: "", + host: "[2001:db8:1f70:0:999:de8:7648:6e8]", + port: 443, + endpoint: "dns:[2001:db8:1f70:0:999:de8:7648:6e8]:443", + }, + { + url: "dns:[2001:db8:1f70:0:999:de8:7648:6e8]", + error: false, + secure: false, + scheme: "", + host: "[2001:db8:1f70:0:999:de8:7648:6e8]", + port: 443, + endpoint: "dns:[2001:db8:1f70:0:999:de8:7648:6e8]:443", + }, + { + url: "dns:[2001:db8:1f70:0:999:de8:7648:6e8]:5000", + error: false, + secure: false, + scheme: "", + host: "[2001:db8:1f70:0:999:de8:7648:6e8]", + port: 5000, + endpoint: "dns:[2001:db8:1f70:0:999:de8:7648:6e8]:5000", + }, + { + url: "dns:[2001:db8:1f70:0:999:de8:7648:6e8]:5000?abc=[]", + error: true, + }, + { + url: "https://[2001:db8:1f70:0:999:de8:7648:6e8]", + error: false, + secure: true, + scheme: "", + host: "[2001:db8:1f70:0:999:de8:7648:6e8]", + port: 443, + endpoint: "dns:[2001:db8:1f70:0:999:de8:7648:6e8]:443", + }, + { + url: "https://[2001:db8:1f70:0:999:de8:7648:6e8]:5000", + error: false, + secure: true, + scheme: "", + host: "[2001:db8:1f70:0:999:de8:7648:6e8]", + port: 5000, + endpoint: "dns:[2001:db8:1f70:0:999:de8:7648:6e8]:5000", + }, + + // Invalid addresses (with path and queries) + { url: "host:5000/v1/dapr", error: true }, + { url: "host:5000/?a=1", error: true }, + + // Invalid scheme + { url: "inv-scheme://myhost", error: true }, + { url: "inv-scheme:myhost:5000", error: true }, + ]; + + testCases.forEach((testCase) => { + test(`Testing URL: ${testCase.url}`, () => { + if (testCase.error) { + expect(() => new GrpcEndpoint(testCase.url)).toThrow(Error); + } else { + const url = new GrpcEndpoint(testCase.url); + expect(url.endpoint).toBe(testCase.endpoint); + expect(url.tls).toBe(testCase.secure); + expect(url.hostname).toBe(testCase.host); + expect(url.port).toBe(String(testCase.port)); + } + }); + }); + }); + + describe("parse HTTP Endpoint", () => { + const testCases = [ + { + url: "myhost", + error: false, + secure: false, + scheme: "", + host: "myhost", + port: 80, + endpoint: "http://myhost:80", + }, + { + url: "http://myhost", + error: false, + secure: false, + scheme: "", + host: "myhost", + port: 80, + endpoint: "http://myhost:80", + }, + { + url: "http://myhost:443", + error: false, + secure: false, + scheme: "", + host: "myhost", + port: 443, + endpoint: "http://myhost:443", + }, + { + url: "http://myhost:5000", + error: false, + secure: false, + scheme: "", + host: "myhost", + port: 5000, + endpoint: "http://myhost:5000", + }, + { + url: "https://myhost:443", + error: false, + secure: true, + scheme: "", + host: "myhost", + port: 443, + endpoint: "https://myhost:443", + }, + { + url: "https://myhost:5000", + error: false, + secure: true, + scheme: "", + host: "myhost", + port: 5000, + endpoint: "https://myhost:5000", + }, + { + url: "https://[2001:db8:1f70:0:999:de8:7648:6e8]:5000", + error: false, + secure: true, + scheme: "", + host: "2001:db8:1f70:0:999:de8:7648:6e8", + port: 5000, + endpoint: "https://[2001:db8:1f70:0:999:de8:7648:6e8]:5000", + }, + ]; + + testCases.forEach((testCase) => { + test(`Testing URL: ${testCase.url}`, () => { + if (testCase.error) { + expect(() => new HttpEndpoint(testCase.url)).toThrow(Error); + } else { + const url = new HttpEndpoint(testCase.url); + expect(url.endpoint).toBe(testCase.endpoint); + expect(url.tls).toBe(testCase.secure); + expect(url.hostname).toBe(testCase.host); + expect(url.port).toBe(String(testCase.port)); + } + }); + }); + }); +});