node-hue-api
Version:
Philips Hue API Library for Node.js
286 lines (284 loc) • 12.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RemoteApi = void 0;
const httpClient = __importStar(require("./HttpClientFetch"));
const ApiError_1 = require("../../ApiError");
const OAuthTokens_1 = require("./OAuthTokens");
const util_1 = require("../../util");
const crypto_1 = require("crypto");
// This class is a bit different to the other endpoints currently as they operate in a digest challenge for the most
// part and also operate off a different base url compared with the local/remote endpoints that make up the rest of the
// bridge API commands.
class RemoteApi {
constructor(clientId, clientSecret) {
this._config = {
clientId: clientId,
clientSecret: clientSecret,
baseUrl: new URL('https://api.meethue.com'),
};
this._tokens = new OAuthTokens_1.OAuthTokens();
}
get clientId() {
return this._config.clientId;
}
get clientSecret() {
return this._config.clientSecret;
}
get baseUrl() {
return this._config.baseUrl.href;
}
get accessToken() {
return this._tokens.accessTokenValue;
}
get accessTokenExpiry() {
return this._tokens.accessTokenExpiresAt;
}
get refreshToken() {
return this._tokens.refreshTokenValue;
}
get refreshTokenExpiry() {
return this._tokens.refreshTokenExpiresAt;
}
setAccessToken(token, expiry) {
this._tokens._setAccessToken(token, expiry);
return this;
}
setRefreshToken(token, expiry) {
this._tokens._setRefreshToken(token, expiry);
return this;
}
/**
* Builds the digest response to pass to the remote API for the provided request details.
*/
getDigestResponse(realm, nonce, method, path) {
const clientId = this.clientId, clientSecret = this.clientSecret, hashOne = (0, crypto_1.createHash)('md5').update(`${clientId}:${realm}:${clientSecret}`).digest('hex'), hashTwo = (0, crypto_1.createHash)('md5').update(`${method.toUpperCase()}:${path}`).digest('hex'), hash = (0, crypto_1.createHash)('md5').update(`${hashOne}:${nonce}:${hashTwo}`).digest('hex');
if (!clientId) {
throw new ApiError_1.ApiError('clientId has not been provided, unable to build a digest response');
}
if (!clientSecret) {
throw new ApiError_1.ApiError('clientSecret has not been provided, unable to build a digest response');
}
return hash;
}
/**
* Constructs the digest authorization header value from the provided details.
* @returns {string} The value to be used for the "Authorization" Header.
*/
getAuthorizationHeaderDigest(realm, nonce, method, path) {
const clientId = this.clientId, response = this.getDigestResponse(realm, nonce, method, path);
return `Digest username="${clientId}", realm="${realm}", nonce="${nonce}", uri="${path}", response="${response}"`;
}
/**
* Constructs the basic authorization header value from the provided details.
*
* This is really poor for security, it is only included to complete the implementation of the APIs, you are strongly
* advised to use the digest authorization instead.
* @returns {string} The value to be used for the "Authorization" Header.
*/
getAuthorizationHeaderBasic() {
const clientId = this.clientId, clientSecret = this.clientSecret, encoded = Buffer.from(`${clientId}:${clientSecret}`, 'ascii').toString('base64');
return `Basic ${encoded}`;
}
/**
* Exchanges the code for OAuth tokens.
* @param code The authorization code that is provided as part of the OAuth flow.
* @returns The OAuth Tokens obtained from the remote portal.
*/
getToken(code) {
const self = this, config = {
baseURL: self.baseUrl,
headers: {
'Accept': 'application/json'
},
responseType: 'json',
}, requestConfig = {
url: '/v2/oauth2/token',
method: 'POST',
params: {
code: code,
grant_type: 'authorization_code'
},
validateStatus: (status) => {
return status === 401;
}
}, start = Date.now();
const http = httpClient.create(config);
return http.request(requestConfig)
.then(res => {
return self._respondWithDigest(http, res, requestConfig);
})
.then(res => {
if (res.status === 200) {
return self._processTokens(start, res.data);
}
else {
throw new ApiError_1.ApiError(`Unexpected status code from getting token: ${res.status}`);
}
});
}
/**
* Refreshes the existing tokens by exchanging the current refresh token for new access and refresh tokens.
*
* After calling this the old tokens will no longer be valid. The new tokens obtained will be injected back into the
* API for future calls.
*
* You should ensure you save the new tokens in place of the previous ones that you used to establish the original
* remote connection.
*
* @param refreshToken The refresh token to exchange for new tokens.
* @returns Promise<Tokens> The new refreshed tokens.
*/
refreshTokens(refreshToken) {
const self = this, config = {
baseURL: self.baseUrl,
headers: {
'Accept': 'application/json'
},
responseType: 'json',
}, requestConfig = {
url: '/v2/oauth2/token',
method: 'POST',
params: {
grant_type: 'refresh_token',
refresh_token: refreshToken
},
validateStatus: (status) => {
return status === 401;
}
}, start = Date.now();
const http = httpClient.create(config);
return http.request(requestConfig)
.then(res => {
return self._respondWithDigest(http, res, requestConfig);
})
.then(res => {
if (res.status === 200) {
return self._processTokens(start, res.data);
}
else {
throw new ApiError_1.ApiError(`Unexpected status code from refreshing tokens: ${res.status}`);
}
});
}
/**
* Creates a new remote user
* @param remoteBridgeId The id of the hue bridge in the remote portal, usually 0.
* @param deviceType The user device type identifier (this is shown to the end users on the remote access portal). If not specified will default to 'node-hue-api-remote'.
* @returns The new remote username.
*/
createRemoteUsername(remoteBridgeId, deviceType) {
const self = this, accessToken = self.accessToken;
if (Number.isNaN(Number.parseInt(remoteBridgeId))) {
// default to bridge id 0 (as this will be the case for most users
remoteBridgeId = 0;
}
if (!accessToken) {
throw new ApiError_1.ApiError('No current valid access token, you need to fetch an access token before continuing.');
}
const remoteApi = httpClient.create({
baseURL: self.baseUrl,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
});
// return remoteApi.put(`/bridge/${remoteBridgeId}/config`, {'linkbutton': true})
return remoteApi.request({
url: `/bridge/${remoteBridgeId}/config`,
method: 'PUT',
data: { 'linkbutton': true },
json: true,
}).then(res => {
if (!(0, util_1.wasSuccessful)(res.data)) {
throw new ApiError_1.ApiError(`Issue with activating remote link button, attempt was not successful: ${JSON.stringify(res.data)}`);
}
// return remoteApi.post('/bridge', {devicetype: deviceType || 'node-hue-api-remote'})
return remoteApi.request({
url: '/bridge',
data: { devicetype: deviceType || 'node-hue-api-remote' },
method: 'POST',
json: true,
}).then(res => {
if ((0, util_1.wasSuccessful)(res.data)) {
return res.data[0].success.username;
}
else {
throw new ApiError_1.ApiError(`Failed to create a remote whitelist user: ${JSON.stringify(res.data)}`);
}
});
});
}
_respondWithDigest(http, res, requestConfig) {
// We need this information to build the digest Authorization header and get the nonce that we can use for the
// request that will be properly validated and issue us the authorization tokens.
const status = res.status;
if (status !== 401) {
throw new ApiError_1.ApiError(`Did not get the expected 401 response from the remote API that contains the www-authenticate details needed to proceed, got status ${status}`);
}
const wwwAuthenticate = getAuthenticationDetailsFromHeader(res.headers), digestHeader = this.getAuthorizationHeaderDigest(wwwAuthenticate.realm, wwwAuthenticate.nonce, requestConfig.method, requestConfig.url);
requestConfig.headers = {
'Authorization': digestHeader
};
requestConfig.validateStatus = undefined;
return http.request(requestConfig);
}
_processTokens(start, data) {
this.setAccessToken(data.access_token, start + (data.expires_in * 1000));
this.setRefreshToken(data.refresh_token, start + (data.expires_in * 1000));
// We have just set the tokens
return {
// @ts-ignore
accessToken: this._tokens.accessToken,
// @ts-ignore
refreshToken: this._tokens.refreshToken,
};
}
}
exports.RemoteApi = RemoteApi;
function getAuthenticationDetailsFromHeader(headers) {
// if (!response || !response.headers) {
// throw new ApiError('Response object is missing headers property');
// }
if (!headers) {
throw new ApiError_1.ApiError('No headers provided');
}
if (!headers['www-authenticate']) {
throw new ApiError_1.ApiError('Response is missing the "www-authenticate" header');
}
const wwwAuthenticate = headers['www-authenticate'];
const realmResult = /realm="(.*?)"/.exec(wwwAuthenticate);
if (!realmResult) {
throw new ApiError_1.ApiError(`Realm was not found in www-authenticate header '${wwwAuthenticate}'`);
}
const nonceResult = /nonce="(.*?)"/.exec(wwwAuthenticate);
if (!nonceResult) {
throw new ApiError_1.ApiError(`Nonce was not found in www-authenitcate header '${wwwAuthenticate}'`);
}
return {
realm: realmResult[1],
nonce: nonceResult[1],
};
}