Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions api/VsoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,25 +183,40 @@ export class VsoClient {
return url.resolve(this.baseUrl, path.join(this.basePath, relativeUrl));
}

private getSerializedObject(object: any): string {
private getSerializedObject(queryValue: any, object: any): string {
let value:string = "";
let first:boolean = true;

for (let property in object) {
if (object.hasOwnProperty(property)) {
let prop = object[property];
let valueString = this.getValueString(property, prop);
if (first && prop !== undefined) {
value += property + "=" + encodeURIComponent(prop);
value += property + "=" + valueString;
first = false;
} else if (prop !== undefined) {
value += "&" + property +"=" + encodeURIComponent(prop);
value += "&" + property +"=" + valueString;
}
}
}

if (value == ""){
value += queryValue + "=" + object.toString();
}

return value;
}

private getValueString(queryValue, value) {
let valueString = null;
if (typeof(value) === 'object') {
valueString = this.getSerializedObject(queryValue, value);
} else {
valueString = queryValue + "=" + encodeURIComponent(value);
}
return valueString;
}

protected getRequestUrl(routeTemplate: string, area: string, resource: string, routeValues: any, queryParams?: any): string {

// Add area/resource route values (based on the location)
Expand All @@ -221,12 +236,7 @@ export class VsoClient {
for (let queryValue in queryParams) {
if (queryParams[queryValue] != null) {
let value = queryParams[queryValue];
let valueString = null;
if (typeof(value) === 'object') {
valueString = this.getSerializedObject(value);
} else {
valueString = queryValue + "=" + encodeURIComponent(queryParams[queryValue]);
}
let valueString = this.getValueString(queryValue, value);
if (first) {
relativeUrl += "?" + valueString;
first = false;
Expand Down Expand Up @@ -314,4 +324,4 @@ export class VsoClient {

return result;
}
}
}
28 changes: 5 additions & 23 deletions api/handlers/basiccreds.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import VsoBaseInterfaces = require('../interfaces/common/VsoBaseInterfaces');

export class BasicCredentialHandler implements VsoBaseInterfaces.IRequestHandler {
username: string;
password: string;
import ifm = require('../interfaces/common/VsoBaseInterfaces');
import * as resthandlers from 'typed-rest-client/Handlers';

export class BasicCredentialHandler extends resthandlers.BasicCredentialHandler implements ifm.IRequestHandler {
constructor(username: string, password: string) {
this.username = username;
this.password = password;
}

// currently implements pre-authorization
// TODO: support preAuth = false where it hooks on 401
prepareRequest(options:any): void {
options.headers['Authorization'] = 'Basic ' + new Buffer(this.username + ':' + this.password).toString('base64');
options.headers['X-TFS-FedAuthRedirect'] = 'Suppress';
}

// This handler cannot handle 401
canHandleAuthentication(res: VsoBaseInterfaces.IHttpResponse): boolean {
return false;
}

handleAuthentication(httpClient, protocol, options, objs, finalCallback): void {
super(username, password);
}
}
}
26 changes: 5 additions & 21 deletions api/handlers/bearertoken.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import VsoBaseInterfaces = require('../interfaces/common/VsoBaseInterfaces');

export class BearerCredentialHandler implements VsoBaseInterfaces.IRequestHandler {
token: string;
import ifm = require('../interfaces/common/VsoBaseInterfaces');
import * as resthandlers from 'typed-rest-client/Handlers';

export class BearerCredentialHandler extends resthandlers.BearerCredentialHandler implements ifm.IRequestHandler {
constructor(token: string) {
this.token = token;
}

// currently implements pre-authorization
// TODO: support preAuth = false where it hooks on 401
prepareRequest(options:any): void {
options.headers['Authorization'] = 'Bearer ' + this.token;
options.headers['X-TFS-FedAuthRedirect'] = 'Suppress';
}

// This handler cannot handle 401
canHandleAuthentication(res: VsoBaseInterfaces.IHttpResponse): boolean {
return false;
}

handleAuthentication(httpClient, protocol, options, objs, finalCallback): void {
super(token);
}
}
}
126 changes: 6 additions & 120 deletions api/handlers/ntlm.ts
Original file line number Diff line number Diff line change
@@ -1,125 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import VsoBaseInterfaces = require('../interfaces/common/VsoBaseInterfaces');
import ifm = require('../interfaces/common/VsoBaseInterfaces');
import * as resthandlers from 'typed-rest-client/Handlers';

import http = require("http");
import https = require("https");
var _ = require("underscore");
var ntlm = require("../opensource/node-http-ntlm/ntlm");

export class NtlmCredentialHandler implements VsoBaseInterfaces.IRequestHandler {
username: string;
password: string;
workstation: string;
domain: string;

constructor(username: string, password: string, workstation?: string, domain?: string) {
this.username = username;
this.password = password;
if (workstation !== undefined) {
this.workstation = workstation;
}
if (domain !== undefined) {
this.domain = domain;
}
}

prepareRequest(options:any): void {
// No headers or options need to be set. We keep the credentials on the handler itself.
// If a (proxy) agent is set, remove it as we don't support proxy for NTLM at this time
if (options.agent) {
delete options.agent;
}
}

canHandleAuthentication(res: VsoBaseInterfaces.IHttpResponse): boolean {
if (res && res.statusCode === 401) {
// Ensure that we're talking NTLM here
// Once we have the www-authenticate header, split it so we can ensure we can talk NTLM
var wwwAuthenticate = res.headers['www-authenticate'];
if (wwwAuthenticate !== undefined) {
var mechanisms = wwwAuthenticate.split(', ');
var idx = mechanisms.indexOf("NTLM");
if (idx >= 0) {
// Check specifically for 'NTLM' since www-authenticate header can also contain
// the Authorization value to use in the form of 'NTLM TlRMTVNT....AAAADw=='
if (mechanisms[idx].length == 4) {
return true;
}
}
}
}
return false;
}

// The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js
handleAuthentication(httpClient, protocol, options, objs, finalCallback): void {
// Set up the headers for NTLM authentication
var ntlmOptions = _.extend(options, {
username: this.username,
password: this.password,
domain: this.domain || '',
workstation: this.workstation || ''
});
var keepaliveAgent;
if (httpClient.isSsl === true) {
keepaliveAgent = new https.Agent({});
} else {
keepaliveAgent = new http.Agent({ keepAlive: true });
}
let self = this;
// The following pattern of sending the type1 message following immediately (in a setImmediate) is
// critical for the NTLM exchange to happen. If we removed setImmediate (or call in a different manner)
// the NTLM exchange will always fail with a 401.
this.sendType1Message(httpClient, protocol, ntlmOptions, objs, keepaliveAgent, function (err, res) {
if (err) {
return finalCallback(err, null, null);
}
setImmediate(function () {
self.sendType3Message(httpClient, protocol, ntlmOptions, objs, keepaliveAgent, res, finalCallback);
});
});
}

// The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js
private sendType1Message(httpClient, protocol, options, objs, keepaliveAgent, callback): void {
var type1msg = ntlm.createType1Message(options);
var type1options = {
headers: {
'Connection': 'keep-alive',
'Authorization': type1msg
},
timeout: options.timeout || 0,
agent: keepaliveAgent,
// don't redirect because http could change to https which means we need to change the keepaliveAgent
allowRedirects: false
};
type1options = _.extend(type1options, _.omit(options, 'headers'));
httpClient.requestInternal(protocol, type1options, objs, callback);
}

// The following method is an adaptation of code found at https://github.com/SamDecrock/node-http-ntlm/blob/master/httpntlm.js
private sendType3Message(httpClient, protocol, options, objs, keepaliveAgent, res, callback): void {
if (!res.headers['www-authenticate']) {
return callback(new Error('www-authenticate not found on response of second request'));
}
// parse type2 message from server:
var type2msg = ntlm.parseType2Message(res.headers['www-authenticate']);
// create type3 message:
var type3msg = ntlm.createType3Message(type2msg, options);
// build type3 request:
var type3options = {
headers: {
'Authorization': type3msg
},
allowRedirects: false,
agent: keepaliveAgent
};
// pass along other options:
type3options.headers = _.extend(type3options.headers, options.headers);
type3options = _.extend(type3options, _.omit(options, 'headers'));
// send type3 message to server:
httpClient.requestInternal(protocol, type3options, objs, callback);
export class NtlmCredentialHandler extends resthandlers.NtlmCredentialHandler implements ifm.IRequestHandler {
constructor(username: string, password: string, workstation?: string, domain?: string) {
super(username, password, workstation, domain);
}
}
}
26 changes: 5 additions & 21 deletions api/handlers/personalaccesstoken.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

import VsoBaseInterfaces = require('../interfaces/common/VsoBaseInterfaces');

export class PersonalAccessTokenCredentialHandler implements VsoBaseInterfaces.IRequestHandler {
token: string;
import ifm = require('../interfaces/common/VsoBaseInterfaces');
import * as resthandlers from 'typed-rest-client/Handlers';

export class PersonalAccessTokenCredentialHandler extends resthandlers.PersonalAccessTokenCredentialHandler implements ifm.IRequestHandler {
constructor(token: string) {
this.token = token;
}

// currently implements pre-authorization
// TODO: support preAuth = false where it hooks on 401
prepareRequest(options:any): void {
options.headers['Authorization'] = 'Basic ' + new Buffer('PAT:' + this.token).toString('base64');
options.headers['X-TFS-FedAuthRedirect'] = 'Suppress';
}

// This handler cannot handle 401
canHandleAuthentication(res: VsoBaseInterfaces.IHttpResponse): boolean {
return false;
}

handleAuthentication(httpClient, protocol, options, objs, finalCallback): void {
super(token);
}
}
}
33 changes: 27 additions & 6 deletions api/interfaces/common/VsoBaseInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
//----------------------------------------------------------------------------

import Serialization = require('../../Serialization');
import http = require("http");
import url = require("url");

/**
* Information about the location of a REST API resource
Expand Down Expand Up @@ -52,15 +54,34 @@ export interface IBasicCredentials {
password: string;
}

export interface IHttpClient {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to export this?

Copy link
Author

@damccorm damccorm Sep 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to export both this and IRequestInfo because we're using them as parameters in other interfaces that are being exported. If I don't export them, an error is thrown Parameter 'httpClient' of method from exported interface has or is using private name 'IHttpClient'.

options(requestUrl: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
get(requestUrl: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
del(requestUrl: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
post(requestUrl: string, data: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
patch(requestUrl: string, data: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
put(requestUrl: string, data: string, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
sendStream(verb: string, requestUrl: string, stream: NodeJS.ReadableStream, additionalHeaders?: IHeaders): Promise<IHttpClientResponse>;
request(verb: string, requestUrl: string, data: string | NodeJS.ReadableStream, headers: IHeaders): Promise<IHttpClientResponse>;
requestRaw(info: IRequestInfo, data: string | NodeJS.ReadableStream): Promise<IHttpClientResponse>;
requestRawWithCallback(info: IRequestInfo, data: string | NodeJS.ReadableStream, onResult: (err: any, res: IHttpClientResponse) => void): void;
}

export interface IRequestInfo {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question.

options: http.RequestOptions;
parsedUrl: url.Url;
httpModule: any;
}

export interface IRequestHandler {
prepareRequest(options: any): void;
canHandleAuthentication(res: IHttpResponse): boolean;
handleAuthentication(httpClient, protocol, options, objs, finalCallback): void;
prepareRequest(options: http.RequestOptions): void;
canHandleAuthentication(response: IHttpClientResponse): boolean;
handleAuthentication(httpClient: IHttpClient, requestInfo: IRequestInfo, objs): Promise<IHttpClientResponse>;
}

export interface IHttpResponse {
statusCode?: number;
headers: any;
export interface IHttpClientResponse {
message: http.IncomingMessage;
readBody(): Promise<string>;
}

export interface IRequestOptions {
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"license": "MIT",
"dependencies": {
"tunnel": "0.0.4",
"typed-rest-client": "^0.12.0",
"typed-rest-client": "1.0.9",
"underscore": "1.8.3"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions samples/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.