Add user agent header to TS client and normalized the other clients (#14484)
This commit is contained in:
parent
5df73373b5
commit
d35b33f294
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
|
@ -22,22 +23,68 @@ namespace Microsoft.AspNetCore.Http.Connections.Client.Internal
|
|||
|
||||
Debug.Assert(assemblyVersion != null);
|
||||
|
||||
var majorVersion = typeof(Constants).Assembly.GetName().Version.Major;
|
||||
var minorVersion = typeof(Constants).Assembly.GetName().Version.Minor;
|
||||
var os = RuntimeInformation.OSDescription;
|
||||
var runtime = ".NET";
|
||||
var runtimeVersion = RuntimeInformation.FrameworkDescription;
|
||||
|
||||
// assembly version attribute should always be present
|
||||
// but in case it isn't then don't include version in user-agent
|
||||
if (assemblyVersion != null)
|
||||
UserAgentHeader = ConstructUserAgent(typeof(Constants).Assembly.GetName().Version, assemblyVersion?.InformationalVersion, GetOS(), runtime, runtimeVersion);
|
||||
}
|
||||
|
||||
private static string GetOS()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
UserAgentHeader = $"Microsoft SignalR/{majorVersion}.{minorVersion} ({assemblyVersion.InformationalVersion}; {os}; {runtime}; {runtimeVersion})";
|
||||
return "Windows NT";
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return "macOS";
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return "Linux";
|
||||
}
|
||||
else
|
||||
{
|
||||
UserAgentHeader = $"Microsoft SignalR/{majorVersion}.{minorVersion} ({os}; {runtime}; {runtimeVersion})";
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static string ConstructUserAgent(Version version, string detailedVersion, string os, string runtime, string runtimeVersion)
|
||||
{
|
||||
var userAgent = $"Microsoft SignalR/{version.Major}.{version.Minor} (";
|
||||
|
||||
if (!string.IsNullOrEmpty(detailedVersion))
|
||||
{
|
||||
userAgent += $"{detailedVersion}";
|
||||
}
|
||||
else
|
||||
{
|
||||
userAgent += "Unknown Version";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(os))
|
||||
{
|
||||
userAgent += $"; {os}";
|
||||
}
|
||||
else
|
||||
{
|
||||
userAgent += "; Unknown OS";
|
||||
}
|
||||
|
||||
userAgent += $"; {runtime}";
|
||||
|
||||
if (!string.IsNullOrEmpty(runtimeVersion))
|
||||
{
|
||||
userAgent += $"; {runtimeVersion}";
|
||||
}
|
||||
else
|
||||
{
|
||||
userAgent += "; Unknown Runtime Version";
|
||||
}
|
||||
|
||||
userAgent += ")";
|
||||
|
||||
return userAgent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,23 +12,40 @@ public class UserAgentHelper {
|
|||
}
|
||||
|
||||
public static String createUserAgentString() {
|
||||
return constructUserAgentString(Version.getDetailedVersion(), getOS(), "Java", getJavaVersion(), getJavaVendor());
|
||||
}
|
||||
|
||||
public static String constructUserAgentString(String detailedVersion, String os, String runtime, String runtimeVersion, String vendor) {
|
||||
StringBuilder agentBuilder = new StringBuilder("Microsoft SignalR/");
|
||||
|
||||
// Parsing version numbers
|
||||
String detailedVersion = Version.getDetailedVersion();
|
||||
agentBuilder.append(getVersion(detailedVersion));
|
||||
agentBuilder.append(" (");
|
||||
agentBuilder.append(detailedVersion);
|
||||
agentBuilder.append("; ");
|
||||
|
||||
// Getting the OS name
|
||||
agentBuilder.append(getOS());
|
||||
agentBuilder.append("; Java; ");
|
||||
|
||||
// Vendor and Version
|
||||
agentBuilder.append(getJavaVersion());
|
||||
agentBuilder.append("; ");
|
||||
agentBuilder.append(getJavaVendor());
|
||||
if (!os.isEmpty()) {
|
||||
agentBuilder.append(os);
|
||||
} else {
|
||||
agentBuilder.append("Unknown OS");
|
||||
}
|
||||
|
||||
agentBuilder.append("; ");
|
||||
agentBuilder.append(runtime);
|
||||
|
||||
agentBuilder.append("; ");
|
||||
if (!runtimeVersion.isEmpty()) {
|
||||
agentBuilder.append(runtimeVersion);
|
||||
} else {
|
||||
agentBuilder.append("Unknown Runtime Version");
|
||||
}
|
||||
|
||||
agentBuilder.append("; ");
|
||||
if (!vendor.isEmpty()) {
|
||||
agentBuilder.append(vendor);
|
||||
} else {
|
||||
agentBuilder.append("Unknown Vendor");
|
||||
}
|
||||
|
||||
agentBuilder.append(")");
|
||||
|
||||
return agentBuilder.toString();
|
||||
|
|
@ -49,6 +66,16 @@ public class UserAgentHelper {
|
|||
}
|
||||
|
||||
static String getOS() {
|
||||
return System.getProperty("os.name");
|
||||
String osName = System.getProperty("os.name").toLowerCase();
|
||||
|
||||
if (osName.indexOf("win") >= 0) {
|
||||
return "Windows NT";
|
||||
} else if (osName.contains("mac") || osName.contains("darwin")) {
|
||||
return "macOS";
|
||||
} else if (osName.contains("linux")) {
|
||||
return "Linux";
|
||||
} else {
|
||||
return osName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,4 +51,17 @@ public class UserAgentTest {
|
|||
|
||||
assertEquals(handMadeUserAgent, userAgent);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("UserAgents")
|
||||
public void UserAgentHeaderIsCorrect(String detailedVersion, String os, String runtime, String runtimeVersion, String vendor, String expected) {
|
||||
assertEquals(expected, UserAgentHelper.constructUserAgentString(detailedVersion, os, runtime, runtimeVersion, vendor));
|
||||
}
|
||||
|
||||
private static Stream<Arguments> UserAgents() {
|
||||
return Stream.of(
|
||||
Arguments.of("1.4.5-dev", "Windows NT", "Java", "7.0.1", "Oracle", "Microsoft SignalR/1.4 (1.4.5-dev; Windows NT; Java; 7.0.1; Oracle)"),
|
||||
Arguments.of("3.1.0", "", "Java", "7.0.1", "", "Microsoft SignalR/3.1 (3.1.0; Unknown OS; Java; 7.0.1; Unknown Vendor)"),
|
||||
Arguments.of("5.0.2", "macOS", "Java", "", "Android", "Microsoft SignalR/5.0 (5.0.2; macOS; Java; Unknown Runtime Version; Android)"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Microsoft.AspNetCore.Http.Connections.Features;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace FunctionalTests
|
||||
{
|
||||
|
|
@ -136,6 +137,11 @@ namespace FunctionalTests
|
|||
return Context.GetHttpContext().Request.Headers["Content-Type"];
|
||||
}
|
||||
|
||||
public string GetHeader(string headerName)
|
||||
{
|
||||
return Context.GetHttpContext().Request.Headers[headerName];
|
||||
}
|
||||
|
||||
public string GetCookie(string cookieName)
|
||||
{
|
||||
var cookies = Context.GetHttpContext().Request.Cookies;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { AbortError, DefaultHttpClient, HttpClient, HttpRequest, HttpResponse, HttpTransportType, HubConnectionBuilder, IHttpConnectionOptions, JsonHubProtocol, NullLogger } from "@microsoft/signalr";
|
||||
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
|
||||
import { getUserAgentHeader, Platform } from "@microsoft/signalr/dist/esm/Utils";
|
||||
|
||||
import { eachTransport, eachTransportAndProtocolAndHttpClient, ENDPOINT_BASE_HTTPS_URL, ENDPOINT_BASE_URL } from "./Common";
|
||||
import "./LogBannerReporter";
|
||||
|
|
@ -1092,6 +1093,33 @@ describe("hubConnection", () => {
|
|||
}
|
||||
});
|
||||
|
||||
eachTransport((t) => {
|
||||
it("sets the user agent header", async (done) => {
|
||||
const hubConnection = getConnectionBuilder(t, TESTHUBENDPOINT_URL)
|
||||
.withHubProtocol(new JsonHubProtocol())
|
||||
.build();
|
||||
|
||||
try {
|
||||
await hubConnection.start();
|
||||
|
||||
// Check to see that the Content-Type header is set the expected value
|
||||
const [name, value] = getUserAgentHeader();
|
||||
const headerValue = await hubConnection.invoke("GetHeader", name);
|
||||
|
||||
if ((t === HttpTransportType.ServerSentEvents || t === HttpTransportType.WebSockets) && !Platform.isNode) {
|
||||
expect(headerValue).toBeNull();
|
||||
} else {
|
||||
expect(headerValue).toEqual(value);
|
||||
}
|
||||
|
||||
await hubConnection.stop();
|
||||
done();
|
||||
} catch (e) {
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getJwtToken(url: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const httpClient = new DefaultHttpClient(NullLogger.instance);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { ILogger, LogLevel } from "./ILogger";
|
|||
import { HttpTransportType, ITransport, TransferFormat } from "./ITransport";
|
||||
import { LongPollingTransport } from "./LongPollingTransport";
|
||||
import { ServerSentEventsTransport } from "./ServerSentEventsTransport";
|
||||
import { Arg, createLogger, Platform } from "./Utils";
|
||||
import { Arg, createLogger, getUserAgentHeader, Platform } from "./Utils";
|
||||
import { WebSocketTransport } from "./WebSocketTransport";
|
||||
|
||||
/** @private */
|
||||
|
|
@ -302,16 +302,17 @@ export class HttpConnection implements IConnection {
|
|||
}
|
||||
|
||||
private async getNegotiationResponse(url: string): Promise<INegotiateResponse> {
|
||||
let headers;
|
||||
const headers = {};
|
||||
if (this.accessTokenFactory) {
|
||||
const token = await this.accessTokenFactory();
|
||||
if (token) {
|
||||
headers = {
|
||||
["Authorization"]: `Bearer ${token}`,
|
||||
};
|
||||
headers[`Authorization`] = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
|
||||
const [name, value] = getUserAgentHeader();
|
||||
headers[name] = value;
|
||||
|
||||
const negotiateUrl = this.resolveNegotiateUrl(url);
|
||||
this.logger.log(LogLevel.Debug, `Sending negotiation request: ${negotiateUrl}.`);
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { HttpError, TimeoutError } from "./Errors";
|
|||
import { HttpClient, HttpRequest } from "./HttpClient";
|
||||
import { ILogger, LogLevel } from "./ILogger";
|
||||
import { ITransport, TransferFormat } from "./ITransport";
|
||||
import { Arg, getDataDetail, sendMessage } from "./Utils";
|
||||
import { Arg, getDataDetail, getUserAgentHeader, sendMessage } from "./Utils";
|
||||
|
||||
// Not exported from 'index', this type is internal.
|
||||
/** @private */
|
||||
|
|
@ -58,9 +58,13 @@ export class LongPollingTransport implements ITransport {
|
|||
throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");
|
||||
}
|
||||
|
||||
const headers = {};
|
||||
const [name, value] = getUserAgentHeader();
|
||||
headers[name] = value;
|
||||
|
||||
const pollOptions: HttpRequest = {
|
||||
abortSignal: this.pollAbort.signal,
|
||||
headers: {},
|
||||
headers,
|
||||
timeout: 100000,
|
||||
};
|
||||
|
||||
|
|
@ -194,8 +198,12 @@ export class LongPollingTransport implements ITransport {
|
|||
// Send DELETE to clean up long polling on the server
|
||||
this.logger.log(LogLevel.Trace, `(LongPolling transport) sending DELETE request to ${this.url}.`);
|
||||
|
||||
const headers = {};
|
||||
const [name, value] = getUserAgentHeader();
|
||||
headers[name] = value;
|
||||
|
||||
const deleteOptions: HttpRequest = {
|
||||
headers: {},
|
||||
headers,
|
||||
};
|
||||
const token = await this.getAccessToken();
|
||||
this.updateHeaderToken(deleteOptions, token);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { HttpClient } from "./HttpClient";
|
|||
import { ILogger, LogLevel } from "./ILogger";
|
||||
import { ITransport, TransferFormat } from "./ITransport";
|
||||
import { EventSourceConstructor } from "./Polyfills";
|
||||
import { Arg, getDataDetail, Platform, sendMessage } from "./Utils";
|
||||
import { Arg, getDataDetail, getUserAgentHeader, Platform, sendMessage } from "./Utils";
|
||||
|
||||
/** @private */
|
||||
export class ServerSentEventsTransport implements ITransport {
|
||||
|
|
@ -62,7 +62,13 @@ export class ServerSentEventsTransport implements ITransport {
|
|||
} else {
|
||||
// Non-browser passes cookies via the dictionary
|
||||
const cookies = this.httpClient.getCookieString(url);
|
||||
eventSource = new this.eventSourceConstructor(url, { withCredentials: true, headers: { Cookie: cookies } } as EventSourceInit);
|
||||
const headers = {
|
||||
Cookie: cookies,
|
||||
};
|
||||
const [name, value] = getUserAgentHeader();
|
||||
headers[name] = value;
|
||||
|
||||
eventSource = new this.eventSourceConstructor(url, { withCredentials: true, headers } as EventSourceInit);
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ import { NullLogger } from "./Loggers";
|
|||
import { IStreamSubscriber, ISubscription } from "./Stream";
|
||||
import { Subject } from "./Subject";
|
||||
|
||||
// Version token that will be replaced by the prepack command
|
||||
/** The version of the SignalR client. */
|
||||
export const VERSION: string = "0.0.0-DEV_BUILD";
|
||||
|
||||
/** @private */
|
||||
export class Arg {
|
||||
public static isRequired(val: any, name: string): void {
|
||||
|
|
@ -25,7 +29,6 @@ export class Arg {
|
|||
|
||||
/** @private */
|
||||
export class Platform {
|
||||
|
||||
public static get isBrowser(): boolean {
|
||||
return typeof window === "object";
|
||||
}
|
||||
|
|
@ -82,7 +85,7 @@ export function isArrayBuffer(val: any): val is ArrayBuffer {
|
|||
|
||||
/** @private */
|
||||
export async function sendMessage(logger: ILogger, transportName: string, httpClient: HttpClient, url: string, accessTokenFactory: (() => string | Promise<string>) | undefined, content: string | ArrayBuffer, logMessageContent: boolean): Promise<void> {
|
||||
let headers;
|
||||
let headers = {};
|
||||
if (accessTokenFactory) {
|
||||
const token = await accessTokenFactory();
|
||||
if (token) {
|
||||
|
|
@ -92,6 +95,9 @@ export async function sendMessage(logger: ILogger, transportName: string, httpCl
|
|||
}
|
||||
}
|
||||
|
||||
const [name, value] = getUserAgentHeader();
|
||||
headers[name] = value;
|
||||
|
||||
logger.log(LogLevel.Trace, `(${transportName} transport) sending data. ${getDataDetail(content, logMessageContent)}.`);
|
||||
|
||||
const responseType = isArrayBuffer(content) ? "arraybuffer" : "text";
|
||||
|
|
@ -181,3 +187,71 @@ export class ConsoleLogger implements ILogger {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
export function getUserAgentHeader(): [string, string] {
|
||||
let userAgentHeaderName = "X-SignalR-User-Agent";
|
||||
if (Platform.isNode) {
|
||||
userAgentHeaderName = "User-Agent";
|
||||
}
|
||||
return [ userAgentHeaderName, constructUserAgent(VERSION, getOsName(), getRuntime(), getRuntimeVersion()) ];
|
||||
}
|
||||
|
||||
/** @private */
|
||||
export function constructUserAgent(version: string, os: string, runtime: string, runtimeVersion: string | undefined): string {
|
||||
// Microsoft SignalR/[Version] ([Detailed Version]; [Operating System]; [Runtime]; [Runtime Version])
|
||||
let userAgent: string = "Microsoft SignalR/";
|
||||
|
||||
const majorAndMinor = version.split(".");
|
||||
userAgent += `${majorAndMinor[0]}.${majorAndMinor[1]}`;
|
||||
userAgent += ` (${version}; `;
|
||||
|
||||
if (os && os !== "") {
|
||||
userAgent += `${os}; `;
|
||||
} else {
|
||||
userAgent += "Unknown OS; ";
|
||||
}
|
||||
|
||||
userAgent += `${runtime}`;
|
||||
|
||||
if (runtimeVersion) {
|
||||
userAgent += `; ${runtimeVersion}`;
|
||||
} else {
|
||||
userAgent += "; Unknown Runtime Version";
|
||||
}
|
||||
|
||||
userAgent += ")";
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
function getOsName(): string {
|
||||
if (Platform.isNode) {
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
return "Windows NT";
|
||||
case "darwin":
|
||||
return "macOS";
|
||||
case "linux":
|
||||
return "Linux";
|
||||
default:
|
||||
return process.platform;
|
||||
}
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function getRuntimeVersion(): string | undefined {
|
||||
if (Platform.isNode) {
|
||||
return process.versions.node;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getRuntime(): string {
|
||||
if (Platform.isNode) {
|
||||
return "NodeJS";
|
||||
} else {
|
||||
return "Browser";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { HttpClient } from "./HttpClient";
|
|||
import { ILogger, LogLevel } from "./ILogger";
|
||||
import { ITransport, TransferFormat } from "./ITransport";
|
||||
import { WebSocketConstructor } from "./Polyfills";
|
||||
import { Arg, getDataDetail, Platform } from "./Utils";
|
||||
import { Arg, getDataDetail, getUserAgentHeader, Platform } from "./Utils";
|
||||
|
||||
/** @private */
|
||||
export class WebSocketTransport implements ITransport {
|
||||
|
|
@ -50,12 +50,18 @@ export class WebSocketTransport implements ITransport {
|
|||
let webSocket: WebSocket | undefined;
|
||||
const cookies = this.httpClient.getCookieString(url);
|
||||
|
||||
if (Platform.isNode && cookies) {
|
||||
if (Platform.isNode) {
|
||||
const headers = {};
|
||||
const [name, value] = getUserAgentHeader();
|
||||
headers[name] = value;
|
||||
|
||||
if (cookies) {
|
||||
headers[`Cookie`] = `${cookies}`;
|
||||
}
|
||||
|
||||
// Only pass cookies when in non-browser environments
|
||||
webSocket = new this.webSocketConstructor(url, undefined, {
|
||||
headers: {
|
||||
Cookie: `${cookies}`,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
// Version token that will be replaced by the prepack command
|
||||
/** The version of the SignalR client. */
|
||||
export const VERSION: string = "0.0.0-DEV_BUILD";
|
||||
|
||||
// Everything that users need to access must be exported here. Including interfaces.
|
||||
export { AbortSignal } from "./AbortController";
|
||||
export { AbortError, HttpError, TimeoutError } from "./Errors";
|
||||
|
|
@ -22,3 +18,4 @@ export { NullLogger } from "./Loggers";
|
|||
export { JsonHubProtocol } from "./JsonHubProtocol";
|
||||
export { Subject } from "./Subject";
|
||||
export { IRetryPolicy, RetryContext } from "./IRetryPolicy";
|
||||
export { VERSION } from "./Utils";
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { HttpResponse } from "../src/HttpClient";
|
|||
import { HttpConnection, INegotiateResponse, TransportSendQueue } from "../src/HttpConnection";
|
||||
import { IHttpConnectionOptions } from "../src/IHttpConnectionOptions";
|
||||
import { HttpTransportType, ITransport, TransferFormat } from "../src/ITransport";
|
||||
import { getUserAgentHeader } from "../src/Utils";
|
||||
|
||||
import { HttpError } from "../src/Errors";
|
||||
import { NullLogger } from "../src/Loggers";
|
||||
|
|
@ -1124,6 +1125,32 @@ describe("HttpConnection", () => {
|
|||
"Failed to start the transport 'WebSockets': Error: There was an error with the transport.");
|
||||
});
|
||||
|
||||
it("user agent header set on negotiate", async () => {
|
||||
await VerifyLogger.run(async (logger) => {
|
||||
let userAgentValue: string = "";
|
||||
const options: IHttpConnectionOptions = {
|
||||
...commonOptions,
|
||||
httpClient: new TestHttpClient()
|
||||
.on("POST", (r) => {
|
||||
userAgentValue = r.headers![`User-Agent`];
|
||||
return new HttpResponse(200, "", "{\"error\":\"nope\"}");
|
||||
}),
|
||||
logger,
|
||||
} as IHttpConnectionOptions;
|
||||
|
||||
const connection = new HttpConnection("http://tempuri.org", options);
|
||||
try {
|
||||
await connection.start(TransferFormat.Text);
|
||||
} catch {
|
||||
} finally {
|
||||
await connection.stop();
|
||||
}
|
||||
|
||||
const [, value] = getUserAgentHeader();
|
||||
expect(userAgentValue).toEqual(value);
|
||||
}, "Failed to start the connection: Error: nope");
|
||||
});
|
||||
|
||||
describe(".constructor", () => {
|
||||
it("throws if no Url is provided", async () => {
|
||||
// Force TypeScript to let us call the constructor incorrectly :)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { HttpResponse } from "../src/HttpClient";
|
||||
import { TransferFormat } from "../src/ITransport";
|
||||
import { LongPollingTransport } from "../src/LongPollingTransport";
|
||||
import { getUserAgentHeader } from "../src/Utils";
|
||||
|
||||
import { VerifyLogger } from "./Common";
|
||||
import { TestHttpClient } from "./TestHttpClient";
|
||||
|
|
@ -120,6 +121,50 @@ describe("LongPollingTransport", () => {
|
|||
await stopPromise;
|
||||
});
|
||||
});
|
||||
|
||||
it("user agent header set on sends and polls", async () => {
|
||||
await VerifyLogger.run(async (logger) => {
|
||||
let firstPoll = true;
|
||||
let firstPollUserAgent = "";
|
||||
let secondPollUserAgent = "";
|
||||
let deleteUserAgent = "";
|
||||
const pollingPromiseSource = new PromiseSource();
|
||||
const httpClient = new TestHttpClient()
|
||||
.on("GET", async (r) => {
|
||||
if (firstPoll) {
|
||||
firstPoll = false;
|
||||
firstPollUserAgent = r.headers![`User-Agent`];
|
||||
return new HttpResponse(200);
|
||||
} else {
|
||||
secondPollUserAgent = r.headers![`User-Agent`];
|
||||
await pollingPromiseSource.promise;
|
||||
return new HttpResponse(204);
|
||||
}
|
||||
})
|
||||
.on("DELETE", async (r) => {
|
||||
deleteUserAgent = r.headers![`User-Agent`];
|
||||
return new HttpResponse(202);
|
||||
});
|
||||
|
||||
const transport = new LongPollingTransport(httpClient, undefined, logger, false);
|
||||
|
||||
await transport.connect("http://tempuri.org", TransferFormat.Text);
|
||||
|
||||
// Begin stopping transport
|
||||
const stopPromise = transport.stop();
|
||||
|
||||
// Allow polling to complete
|
||||
pollingPromiseSource.resolve();
|
||||
|
||||
// Wait for stop to complete
|
||||
await stopPromise;
|
||||
|
||||
const [, value] = getUserAgentHeader();
|
||||
expect(firstPollUserAgent).toEqual(value);
|
||||
expect(deleteUserAgent).toEqual(value);
|
||||
expect(secondPollUserAgent).toEqual(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function makeClosedPromise(transport: LongPollingTransport): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { TransferFormat } from "../src/ITransport";
|
|||
import { HttpClient, HttpRequest } from "../src/HttpClient";
|
||||
import { ILogger } from "../src/ILogger";
|
||||
import { ServerSentEventsTransport } from "../src/ServerSentEventsTransport";
|
||||
import { getUserAgentHeader } from "../src/Utils";
|
||||
import { VerifyLogger } from "./Common";
|
||||
import { TestEventSource, TestMessageEvent } from "./TestEventSource";
|
||||
import { TestHttpClient } from "./TestHttpClient";
|
||||
|
|
@ -179,6 +180,26 @@ describe("ServerSentEventsTransport", () => {
|
|||
expect(error).toEqual(new Error("error parsing"));
|
||||
});
|
||||
});
|
||||
|
||||
it("sets user agent header on connect and sends", async () => {
|
||||
await VerifyLogger.run(async (logger) => {
|
||||
let request: HttpRequest;
|
||||
const httpClient = new TestHttpClient().on((r) => {
|
||||
request = r;
|
||||
return "";
|
||||
});
|
||||
|
||||
const sse = await createAndStartSSE(logger, "http://example.com", undefined, httpClient);
|
||||
|
||||
let [, value] = getUserAgentHeader();
|
||||
expect((TestEventSource.eventSource.eventSourceInitDict as any).headers[`User-Agent`]).toEqual(value);
|
||||
await sse.send("");
|
||||
|
||||
[, value] = getUserAgentHeader();
|
||||
expect(request!.headers![`User-Agent`]).toBe(value);
|
||||
expect(request!.url).toBe("http://example.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createAndStartSSE(logger: ILogger, url?: string, accessTokenFactory?: (() => string | Promise<string>), httpClient?: HttpClient): Promise<ServerSentEventsTransport> {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export class TestEventSource {
|
|||
public onmessage!: (evt: MessageEvent) => any;
|
||||
public readyState: number = 0;
|
||||
public url: string = "";
|
||||
public eventSourceInitDict?: EventSourceInit;
|
||||
public withCredentials: boolean = false;
|
||||
|
||||
// tslint:disable-next-line:variable-name
|
||||
|
|
@ -31,6 +32,7 @@ export class TestEventSource {
|
|||
|
||||
constructor(url: string, eventSourceInitDict?: EventSourceInit) {
|
||||
this.url = url;
|
||||
this.eventSourceInitDict = eventSourceInitDict;
|
||||
|
||||
TestEventSource.eventSource = this;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export class TestWebSocket {
|
|||
public protocol: string;
|
||||
public readyState: number = 1;
|
||||
public url: string;
|
||||
public options?: any;
|
||||
|
||||
public static webSocketSet: PromiseSource;
|
||||
public static webSocket: TestWebSocket;
|
||||
|
|
@ -67,10 +68,11 @@ export class TestWebSocket {
|
|||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
constructor(url: string, protocols?: string | string[]) {
|
||||
constructor(url: string, protocols?: string | string[], options?: any) {
|
||||
this.url = url;
|
||||
this.protocol = protocols ? (typeof protocols === "string" ? protocols : protocols[0]) : "";
|
||||
this.receivedData = [];
|
||||
this.options = options;
|
||||
|
||||
TestWebSocket.webSocket = this;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
import { constructUserAgent } from "../src/Utils";
|
||||
|
||||
describe("User Agent", () => {
|
||||
[["1.0.4-build.10", "Linux", "NodeJS", "10", "Microsoft SignalR/1.0 (1.0.4-build.10; Linux; NodeJS; 10)"],
|
||||
["1.4.7-build.10", "", "Browser", "", "Microsoft SignalR/1.4 (1.4.7-build.10; Unknown OS; Browser; Unknown Runtime Version)"],
|
||||
["3.1.1-build.10", "macOS", "Browser", "", "Microsoft SignalR/3.1 (3.1.1-build.10; macOS; Browser; Unknown Runtime Version)"],
|
||||
["3.1.3-build.10", "", "Browser", "4", "Microsoft SignalR/3.1 (3.1.3-build.10; Unknown OS; Browser; 4)"]]
|
||||
.forEach(([version, os, runtime, runtimeVersion, expected]) => {
|
||||
it(`is in correct format`, async () => {
|
||||
const userAgent = constructUserAgent(version, os, runtime, runtimeVersion);
|
||||
expect(userAgent).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import { ILogger } from "../src/ILogger";
|
||||
import { TransferFormat } from "../src/ITransport";
|
||||
import { getUserAgentHeader } from "../src/Utils";
|
||||
import { WebSocketTransport } from "../src/WebSocketTransport";
|
||||
import { VerifyLogger } from "./Common";
|
||||
import { TestMessageEvent } from "./TestEventSource";
|
||||
|
|
@ -202,6 +203,32 @@ describe("WebSocketTransport", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("sets user agent header on connect", async () => {
|
||||
await VerifyLogger.run(async (logger) => {
|
||||
(global as any).ErrorEvent = TestEvent;
|
||||
const webSocket = await createAndStartWebSocket(logger);
|
||||
|
||||
let closeCalled: boolean = false;
|
||||
let error: Error;
|
||||
webSocket.onclose = (e) => {
|
||||
closeCalled = true;
|
||||
error = e!;
|
||||
};
|
||||
|
||||
const [, value] = getUserAgentHeader();
|
||||
expect(TestWebSocket.webSocket.options!.headers[`User-Agent`]).toEqual(value);
|
||||
|
||||
await webSocket.stop();
|
||||
|
||||
expect(closeCalled).toBe(true);
|
||||
expect(error!).toBeUndefined();
|
||||
|
||||
await expect(webSocket.send(""))
|
||||
.rejects
|
||||
.toBe("WebSocket is not in the OPEN state");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createAndStartWebSocket(logger: ILogger, url?: string, accessTokenFactory?: (() => string | Promise<string>), format?: TransferFormat): Promise<WebSocketTransport> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
|
@ -12,23 +14,19 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
|||
{
|
||||
public class UserAgentHeaderTest
|
||||
{
|
||||
[Fact]
|
||||
public void UserAgentHeaderIsAccurate()
|
||||
[Theory]
|
||||
[MemberData(nameof(UserAgents))]
|
||||
public void UserAgentHeaderIsCorrect(Version version, string detailedVersion, string os, string runtime, string runtimeVersion, string expected)
|
||||
{
|
||||
var majorVersion = typeof(HttpConnection).Assembly.GetName().Version.Major;
|
||||
var minorVersion = typeof(HttpConnection).Assembly.GetName().Version.Minor;
|
||||
var version = typeof(HttpConnection).Assembly.GetName().Version;
|
||||
var os = RuntimeInformation.OSDescription;
|
||||
var runtime = ".NET";
|
||||
var runtimeVersion = RuntimeInformation.FrameworkDescription;
|
||||
var assemblyVersion = typeof(Constants)
|
||||
.Assembly
|
||||
.GetCustomAttributes<AssemblyInformationalVersionAttribute>()
|
||||
.FirstOrDefault();
|
||||
var userAgent = Constants.UserAgentHeader;
|
||||
var expectedUserAgent = $"Microsoft SignalR/{majorVersion}.{minorVersion} ({assemblyVersion.InformationalVersion}; {os}; {runtime}; {runtimeVersion})";
|
||||
Assert.Equal(expected, Constants.ConstructUserAgent(version, detailedVersion, os, runtime, runtimeVersion));
|
||||
}
|
||||
|
||||
Assert.Equal(expectedUserAgent, userAgent);
|
||||
public static IEnumerable<object[]> UserAgents()
|
||||
{
|
||||
yield return new object[] { new Version(1, 4), "1.4.3-preview9", "Windows NT", ".NET", ".NET 4.8.7", "Microsoft SignalR/1.4 (1.4.3-preview9; Windows NT; .NET; .NET 4.8.7)" };
|
||||
yield return new object[] { new Version(3, 1), "3.1.0", "", ".NET", ".NET 4.8.9", "Microsoft SignalR/3.1 (3.1.0; Unknown OS; .NET; .NET 4.8.9)" };
|
||||
yield return new object[] { new Version(3, 1), "3.1.0", "", ".NET", "", "Microsoft SignalR/3.1 (3.1.0; Unknown OS; .NET; Unknown Runtime Version)" };
|
||||
yield return new object[] { new Version(3, 1), "", "Linux", ".NET", ".NET 4.5.1", "Microsoft SignalR/3.1 (Unknown Version; Linux; .NET; .NET 4.5.1)" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue