<commit_before>
/**
 * @param {Object} target - the target object
 * @param {Array} keys - with keys that is to be extracted from the target
 */
const destruct = (target, ...keys) => {
  const deepKeys = getDeepKeys(keys)
  const deepObjects = getDeepObjects(target, deepKeys)
  const shallowObjects = getShallowObjects(target, keys)
  const collidingKeys = getCollidingKeys(deepObjects, shallowObjects)

  if (collidingKeys.length !== 0) throw new Error(`found colliding keys: ${collidingKeys.join(', ')}`)

  return Object.assign({}, shallowObjects, deepObjects)
}

/**
 * Finds all keys that are for deep objects.
 * Sorting out keys with .
 * @param {Array} keys
 */
const getDeepKeys = keys => keys.filter(key => key.indexOf('.') > 1)

/**
 * Builds the deep objects together
 * @param {Object} target
 * @param {Array} deepKeys
 */
const getDeepObjects = (target, deepKeys) => deepKeys
  .map(key => getDeepObject(target, key))
  .reduce((build, obj) => Object.assign({}, build, obj), {})

/**
 * recusivly walks down the target object until key is found.
 * @param {Object} target
 * @param {String} key - the current active key in .map above.
 */
const getDeepObject = (target, key) => {
  if (target && target.hasOwnProperty(key)) {
    return {[key]: target[key]}
  }

  const splittedKey = key.split('.')
  const parent = splittedKey.shift()
  const children = splittedKey.join('.')

  if (target.hasOwnProperty(parent)) {
    return getDeepObject(target[parent], children)
  } else {
    throw new Error(`Could not find key ${parent} on target object.`)
  }
}

/**
 * Takes all the shallow keys from the target object
 * @param {Object} target
 * @param {Array} keys
 */
const getShallowObjects = (target, keys) => keys
  .reduce((build, key) =>
    target.hasOwnProperty(key) ? Object.assign({}, build, {[key]: target[key]}) : build , {})

/**
 * Validates if there is any colliding keys from the deep and shallow objects
 * if not - they keys will "overwrite" eachother and one will disapear.
 * @param {Object} deepObjects
 * @param {Object} shallowObjects
 */
const getCollidingKeys = (deepObjects, shallowObjects) =>
  Object.keys(deepObjects).filter(key => Object.keys(shallowObjects).includes(key))

module.exports = destruct<commit_msg>nothing special<commit_after>
/**
 * @param {Object} target - the target object
 * @param {Array} keys - with keys that is to be extracted from the target
 */
const destruct = (target, ...keys) => {
  if (typeof target !== 'object') return

  const deepKeys = getDeepKeys(keys)
  const deepObjects = getDeepObjects(target, deepKeys)
  const shallowObjects = getShallowObjects(target, keys)
  const collidingKeys = getCollidingKeys(deepObjects, shallowObjects)

  if (collidingKeys.length !== 0) throw new Error(`found colliding keys: ${collidingKeys.join(', ')}`)

  return Object.assign({}, shallowObjects, deepObjects)
}

/**
 * Finds all keys that are for deep objects.
 * Sorting out keys with .
 * @param {Array} keys
 */
const getDeepKeys = keys => keys.filter(key => key.indexOf('.') > 1)

/**
 * Builds the deep objects together
 * @param {Object} target
 * @param {Array} deepKeys
 */
const getDeepObjects = (target, deepKeys) => deepKeys
  .map(key => getDeepObject(target, key))
  .reduce((build, obj) => Object.assign({}, build, obj), {})

/**
 * recusivly walks down the target object until key is found.
 * @param {Object} target
 * @param {String} key - the current active key in loop above.
 */
const getDeepObject = (target, key) => {
  if (target && target.hasOwnProperty(key)) {
    return {[key]: target[key]}
  }

  const splittedKey = key.split('.')
  const parent = splittedKey.shift()
  const children = splittedKey.join('.')

  if (target.hasOwnProperty(parent)) {
    return getDeepObject(target[parent], children)
  } else {
    throw new Error(`Could not find key ${parent} on target object.`)
  }
}

/**
 * Takes all the shallow keys from the target object
 * @param {Object} target
 * @param {Array} keys
 */
const getShallowObjects = (target, keys) => keys
  .reduce((build, key) =>
    target.hasOwnProperty(key) ? Object.assign({}, build, {[key]: target[key]}) : build , {})

/**
 * Validates if there is any colliding keys from the deep and shallow objects
 * if not - they keys will "overwrite" eachother and one will disapear.
 * @param {Object} deepObjects
 * @param {Object} shallowObjects
 */
const getCollidingKeys = (deepObjects, shallowObjects) =>
  Object.keys(deepObjects).filter(key => Object.keys(shallowObjects).includes(key))

module.exports = destruct<|endoftext|>


<commit_before>// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

import { Log } from './Log';
import { MetadataService } from './MetadataService';
import { UserInfoService } from './UserInfoService';
import { TokenClient } from './TokenClient';
import { ErrorResponse } from './ErrorResponse';
import { JoseUtil } from './JoseUtil';

const ProtocolClaims = ["nonce", "at_hash", "iat", "nbf", "exp", "aud", "iss", "c_hash"];

export class ResponseValidator {

    constructor(settings,
        MetadataServiceCtor = MetadataService,
        UserInfoServiceCtor = UserInfoService,
        joseUtil = JoseUtil,
        TokenClientCtor = TokenClient) {
        if (!settings) {
            Log.error("ResponseValidator.ctor: No settings passed to ResponseValidator");
            throw new Error("settings");
        }

        this._settings = settings;
        this._metadataService = new MetadataServiceCtor(this._settings);
        this._userInfoService = new UserInfoServiceCtor(this._settings);
        this._joseUtil = joseUtil;
        this._tokenClient = new TokenClientCtor(this._settings);
    }

    validateSigninResponse(state, response) {
        Log.debug("ResponseValidator.validateSigninResponse");

        return this._processSigninParams(state, response).then(response => {
            Log.debug("ResponseValidator.validateSigninResponse: state processed");
            return this._validateTokens(state, response).then(response => {
                Log.debug("ResponseValidator.validateSigninResponse: tokens validated");
                return this._processClaims(response).then(response => {
                    Log.debug("ResponseValidator.validateSigninResponse: claims processed");
                    return response;
                });
            });
        });
    }

    validateSignoutResponse(state, response) {
        if (state.id !== response.state) {
            Log.error("ResponseValidator.validateSignoutResponse: State does not match");
            return Promise.reject(new Error("State does not match"));
        }

        // now that we know the state matches, take the stored data
        // and set it into the response so callers can get their state
        // this is important for both success & error outcomes
        Log.debug("ResponseValidator.validateSignoutResponse: state validated");
        response.state = state.data;

        if (response.error) {
            Log.warn("ResponseValidator.validateSignoutResponse: Response was error", response.error);
            return Promise.reject(new ErrorResponse(response));
        }

        return Promise.resolve(response);
    }

    _processSigninParams(state, response) {
        if (state.id !== response.state) {
            Log.error("ResponseValidator._processSigninParams: State does not match");
            return Promise.reject(new Error("State does not match"));
        }

        if (!state.client_id) {
            Log.error("ResponseValidator._processSigninParams: No client_id on state");
            return Promise.reject(new Error("No client_id on state"));
        }

        if (!state.authority) {
            Log.error("ResponseValidator._processSigninParams: No authority on state");
            return Promise.reject(new Error("No authority on state"));
        }

        // this allows the authority to be loaded from the signin state
        if (!this._settings.authority) {
            this._settings.authority = state.authority;
        }
        // ensure we're using the correct authority if the authority is not loaded from signin state
        else if (this._settings.authority && this._settings.authority !== state.authority) {
            Log.error("ResponseValidator._processSigninParams: authority mismatch on settings vs. signin state");
            return Promise.reject(new Error("authority mismatch on settings vs. signin state"));
        }
        // this allows the client_id to be loaded from the signin state
        if (!this._settings.client_id) {
            this._settings.client_id = state.client_id;
        }
        // ensure we're using the correct client_id if the client_id is not loaded from signin state
        else if (this._settings.client_id && this._settings.client_id !== state.client_id) {
            Log.error("ResponseValidator._processSigninParams: client_id mismatch on settings vs. signin state");
            return Promise.reject(new Error("client_id mismatch on settings vs. signin state"));
        }

        // now that we know the state matches, take the stored data
        // and set it into the response so callers can get their state
        // this is important for both success & error outcomes
        Log.debug("ResponseValidator._processSigninParams: state validated");
        response.state = state.data;

        if (response.error) {
            Log.warn("ResponseValidator._processSigninParams: Response was error", response.error);
            return Promise.reject(new ErrorResponse(response));
        }

        if (state.nonce && !response.id_token) {
            Log.error("ResponseValidator._processSigninParams: Expecting id_token in response");
            return Promise.reject(new Error("No id_token in response"));
        }

        if (!state.nonce && response.id_token) {
            Log.error("ResponseValidator._processSigninParams: Not expecting id_token in response");
            return Promise.reject(new Error("Unexpected id_token in response"));
        }

        if (state.code_verifier && !response.code) {
            Log.error("ResponseValidator._processSigninParams: Expecting code in response");
            return Promise.reject(new Error("No code in response"));
        }

        if (!state.code_verifier && response.code) {
            Log.error("ResponseValidator._processSigninParams: Not expecting code in response");
            return Promise.reject(new Error("Unexpected code in response"));
        }

        return Promise.resolve(response);
    }

    _processClaims(response) {
        if (response.isOpenIdConnect) {
            Log.debug("ResponseValidator._processClaims: response is OIDC, processing claims");

            response.profile = this._filterProtocolClaims(response.profile);

            if (this._settings.loadUserInfo && response.access_token) {
                Log.debug("ResponseValidator._processClaims: loading user info");

                return this._userInfoService.getClaims(response.access_token).then(claims => {
                    Log.debug("ResponseValidator._processClaims: user info claims received from user info endpoint");

                    if (claims.sub !== response.profile.sub) {
                        Log.error("ResponseValidator._processClaims: sub from user info endpoint does not match sub in access_token");
                        return Promise.reject(new Error("sub from user info endpoint does not match sub in access_token"));
                    }

                    response.profile = this._mergeClaims(response.profile, claims);
                    Log.debug("ResponseValidator._processClaims: user info claims received, updated profile:", response.profile);

                    return response;
                });
            }
            else {
                Log.debug("ResponseValidator._processClaims: not loading user info");
            }
        }
        else {
            Log.debug("ResponseValidator._processClaims: response is not OIDC, not processing claims");
        }

        return Promise.resolve(response);
    }

    _mergeClaims(claims1, claims2) {
        var result = Object.assign({}, claims1);

        for (let name in claims2) {
            var values = claims2[name];
            if (!Array.isArray(values)) {
                values = [values];
            }

            for (let i = 0; i < values.length; i++) {
                let value = values[i];
                if (!result[name]) {
                    result[name] = value;
                }
                else if (Array.isArray(result[name])) {
                    if (result[name].indexOf(value) < 0) {
                        result[name].push(value);
                    }
                }
                else if (result[name] !== value) {
                    result[name] = [result[name], value];
                }
            }
        }

        return result;
    }

    _filterProtocolClaims(claims) {
        Log.debug("ResponseValidator._filterProtocolClaims, incoming claims:", claims);

        var result = Object.assign({}, claims);

        if (this._settings._filterProtocolClaims) {
            ProtocolClaims.forEach(type => {
                delete result[type];
            });

            Log.debug("ResponseValidator._filterProtocolClaims: protocol claims filtered", result);
        }
        else {
            Log.debug("ResponseValidator._filterProtocolClaims: protocol claims not filtered")
        }

        return result;
    }

    _validateTokens(state, response) {
        if (response.code) {
            Log.debug("ResponseValidator._validateTokens: Validating code");
            return this._processCode(state, response);
        }

        if (response.id_token) {
            if (response.access_token) {
                Log.debug("ResponseValidator._validateTokens: Validating id_token and access_token");
                return this._validateIdTokenAndAccessToken(state, response);
            }

            Log.debug("ResponseValidator._validateTokens: Validating id_token");
            return this._validateIdToken(state, response);
        }

        Log.debug("ResponseValidator._validateTokens: No code to process or id_token to validate");
        return Promise.resolve(response);
    }

    _processCode(state, response) {
        var request = {
            client_id: state.client_id,
            code : response.code,
            redirect_uri: state.redirect_uri,
            code_verifier: state.code_verifier,
        };

        return this._tokenClient.exchangeCode(request).then(tokenResponse => {

            for(var key in tokenResponse) {
                response[key] = tokenResponse[key];
            }

            if (response.id_token) {
                Log.debug("ResponseValidator._processCode: token response successful, processing id_token");
                return this._validateIdTokenAttributes(state, response);
            }
            else {
                Log.debug("ResponseValidator._processCode: token response successful, returning response");
            }

            return response;
        });
    }

    _validateIdTokenAttributes(state, response) {
        return this._metadataService.getIssuer().then(issuer => {

            let audience = state.client_id;
            let clockSkewInSeconds = this._settings.clockSkew;
            Log.debug("ResponseValidator._validateIdTokenAttributes: Validaing JWT attributes; using clock skew (in seconds) of: ", clockSkewInSeconds);

            return this._joseUtil.validateJwtAttributes(response.id_token, issuer, audience, clockSkewInSeconds).then(payload => {

                if (state.nonce && state.nonce !== payload.nonce) {
                    Log.error("ResponseValidator._validateIdTokenAttributes: Invalid nonce in id_token");
                    return Promise.reject(new Error("Invalid nonce in id_token"));
                }

                if (!payload.sub) {
                    Log.error("ResponseValidator._validateIdTokenAttributes: No sub present in id_token");
                    return Promise.reject(new Error("No sub present in id_token"));
                }

                response.profile = payload;
                return response;
            });
        });
    }

    _validateIdTokenAndAccessToken(state, response) {
        return this._validateIdToken(state, response).then(response => {
            return this._validateAccessToken(response);
        });
    }

    _validateIdToken(state, response) {
        if (!state.nonce) {
            Log.error("ResponseValidator._validateIdToken: No nonce on state");
            return Promise.reject(new Error("No nonce on state"));
        }

        let jwt = this._joseUtil.parseJwt(response.id_token);
        if (!jwt || !jwt.header || !jwt.payload) {
            Log.error("ResponseValidator._validateIdToken: Failed to parse id_token", jwt);
            return Promise.reject(new Error("Failed to parse id_token"));
        }

        if (state.nonce !== jwt.payload.nonce) {
            Log.error("ResponseValidator._validateIdToken: Invalid nonce in id_token");
            return Promise.reject(new Error("Invalid nonce in id_token"));
        }

        var kid = jwt.header.kid;

        return this._metadataService.getIssuer().then(issuer => {
            Log.debug("ResponseValidator._validateIdToken: Received issuer");

            return this._metadataService.getSigningKeys().then(keys => {
                if (!keys) {
                    Log.error("ResponseValidator._validateIdToken: No signing keys from metadata");
                    return Promise.reject(new Error("No signing keys from metadata"));
                }

                Log.debug("ResponseValidator._validateIdToken: Received signing keys");
                let key;
                if (!kid) {
                    keys = this._filterByAlg(keys, jwt.header.alg);

                    if (keys.length > 1) {
                        Log.error("ResponseValidator._validateIdToken: No kid found in id_token and more than one key found in metadata");
                        return Promise.reject(new Error("No kid found in id_token and more than one key found in metadata"));
                    }
                    else {
                        // kid is mandatory only when there are multiple keys in the referenced JWK Set document
                        // see http://openid.net/specs/openid-connect-core-1_0.html#Signing
                        key = keys[0];
                    }
                }
                else {
                    key = keys.filter(key => {
                        return key.kid === kid;
                    })[0];
                }

                if (!key) {
                    Log.error("ResponseValidator._validateIdToken: No key matching kid or alg found in signing keys");
                    return Promise.reject(new Error("No key matching kid or alg found in signing keys"));
                }

                let audience = state.client_id;

                let clockSkewInSeconds = this._settings.clockSkew;
                Log.debug("ResponseValidator._validateIdToken: Validaing JWT; using clock skew (in seconds) of: ", clockSkewInSeconds);

                return this._joseUtil.validateJwt(response.id_token, key, issuer, audience, clockSkewInSeconds).then(()=>{
                    Log.debug("ResponseValidator._validateIdToken: JWT validation successful");

                    if (!jwt.payload.sub) {
                        Log.error("ResponseValidator._validateIdToken: No sub present in id_token");
                        return Promise.reject(new Error("No sub present in id_token"));
                    }

                    response.profile = jwt.payload;

                    return response;
                });
            });
        });
    }

    _filterByAlg(keys, alg){
        var kty = null;
        if (alg.startsWith("RS")) {
            kty = "RSA";
        }
        else if (alg.startsWith("PS")) {
            kty = "PS";
        }
        else if (alg.startsWith("ES")) {
            kty = "EC";
        }
        else {
            Log.debug("ResponseValidator._filterByAlg: alg not supported: ", alg);
            return [];
        }

        Log.debug("ResponseValidator._filterByAlg: Looking for keys that match kty: ", kty);

        keys = keys.filter(key => {
            return key.kty === kty;
        });

        Log.debug("ResponseValidator._filterByAlg: Number of keys that match kty: ", kty, keys.length);

        return keys;
    }

    _validateAccessToken(response) {
        if (!response.profile) {
            Log.error("ResponseValidator._validateAccessToken: No profile loaded from id_token");
            return Promise.reject(new Error("No profile loaded from id_token"));
        }

        if (!response.profile.at_hash) {
            Log.error("ResponseValidator._validateAccessToken: No at_hash in id_token");
            return Promise.reject(new Error("No at_hash in id_token"));
        }

        if (!response.id_token) {
            Log.error("ResponseValidator._validateAccessToken: No id_token");
            return Promise.reject(new Error("No id_token"));
        }

        let jwt = this._joseUtil.parseJwt(response.id_token);
        if (!jwt || !jwt.header) {
            Log.error("ResponseValidator._validateAccessToken: Failed to parse id_token", jwt);
            return Promise.reject(new Error("Failed to parse id_token"));
        }

        var hashAlg = jwt.header.alg;
        if (!hashAlg || hashAlg.length !== 5) {
            Log.error("ResponseValidator._validateAccessToken: Unsupported alg:", hashAlg);
            return Promise.reject(new Error("Unsupported alg: " + hashAlg));
        }

        var hashBits = hashAlg.substr(2, 3);
        if (!hashBits) {
            Log.error("ResponseValidator._validateAccessToken: Unsupported alg:", hashAlg, hashBits);
            return Promise.reject(new Error("Unsupported alg: " + hashAlg));
        }

        hashBits = parseInt(hashBits);
        if (hashBits !== 256 && hashBits !== 384 && hashBits !== 512) {
            Log.error("ResponseValidator._validateAccessToken: Unsupported alg:", hashAlg, hashBits);
            return Promise.reject(new Error("Unsupported alg: " + hashAlg));
        }

        let sha = "sha" + hashBits;
        var hash = this._joseUtil.hashString(response.access_token, sha);
        if (!hash) {
            Log.error("ResponseValidator._validateAccessToken: access_token hash failed:", sha);
            return Promise.reject(new Error("Failed to validate at_hash"));
        }

        var left = hash.substr(0, hash.length / 2);
        var left_b64u = this._joseUtil.hexToBase64Url(left);
        if (left_b64u !== response.profile.at_hash) {
            Log.error("ResponseValidator._validateAccessToken: Failed to validate at_hash", left_b64u, response.profile.at_hash);
            return Promise.reject(new Error("Failed to validate at_hash"));
        }

        Log.debug("ResponseValidator._validateAccessToken: success");

        return Promise.resolve(response);
    }
}
<commit_msg>Use client_secret for code exchange in Auth Code Flow<commit_after>// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

import { Log } from './Log';
import { MetadataService } from './MetadataService';
import { UserInfoService } from './UserInfoService';
import { TokenClient } from './TokenClient';
import { ErrorResponse } from './ErrorResponse';
import { JoseUtil } from './JoseUtil';

const ProtocolClaims = ["nonce", "at_hash", "iat", "nbf", "exp", "aud", "iss", "c_hash"];

export class ResponseValidator {

    constructor(settings,
        MetadataServiceCtor = MetadataService,
        UserInfoServiceCtor = UserInfoService,
        joseUtil = JoseUtil,
        TokenClientCtor = TokenClient) {
        if (!settings) {
            Log.error("ResponseValidator.ctor: No settings passed to ResponseValidator");
            throw new Error("settings");
        }

        this._settings = settings;
        this._metadataService = new MetadataServiceCtor(this._settings);
        this._userInfoService = new UserInfoServiceCtor(this._settings);
        this._joseUtil = joseUtil;
        this._tokenClient = new TokenClientCtor(this._settings);
    }

    validateSigninResponse(state, response) {
        Log.debug("ResponseValidator.validateSigninResponse");

        return this._processSigninParams(state, response).then(response => {
            Log.debug("ResponseValidator.validateSigninResponse: state processed");
            return this._validateTokens(state, response).then(response => {
                Log.debug("ResponseValidator.validateSigninResponse: tokens validated");
                return this._processClaims(response).then(response => {
                    Log.debug("ResponseValidator.validateSigninResponse: claims processed");
                    return response;
                });
            });
        });
    }

    validateSignoutResponse(state, response) {
        if (state.id !== response.state) {
            Log.error("ResponseValidator.validateSignoutResponse: State does not match");
            return Promise.reject(new Error("State does not match"));
        }

        // now that we know the state matches, take the stored data
        // and set it into the response so callers can get their state
        // this is important for both success & error outcomes
        Log.debug("ResponseValidator.validateSignoutResponse: state validated");
        response.state = state.data;

        if (response.error) {
            Log.warn("ResponseValidator.validateSignoutResponse: Response was error", response.error);
            return Promise.reject(new ErrorResponse(response));
        }

        return Promise.resolve(response);
    }

    _processSigninParams(state, response) {
        if (state.id !== response.state) {
            Log.error("ResponseValidator._processSigninParams: State does not match");
            return Promise.reject(new Error("State does not match"));
        }

        if (!state.client_id) {
            Log.error("ResponseValidator._processSigninParams: No client_id on state");
            return Promise.reject(new Error("No client_id on state"));
        }

        if (!state.authority) {
            Log.error("ResponseValidator._processSigninParams: No authority on state");
            return Promise.reject(new Error("No authority on state"));
        }

        // this allows the authority to be loaded from the signin state
        if (!this._settings.authority) {
            this._settings.authority = state.authority;
        }
        // ensure we're using the correct authority if the authority is not loaded from signin state
        else if (this._settings.authority && this._settings.authority !== state.authority) {
            Log.error("ResponseValidator._processSigninParams: authority mismatch on settings vs. signin state");
            return Promise.reject(new Error("authority mismatch on settings vs. signin state"));
        }
        // this allows the client_id to be loaded from the signin state
        if (!this._settings.client_id) {
            this._settings.client_id = state.client_id;
        }
        // ensure we're using the correct client_id if the client_id is not loaded from signin state
        else if (this._settings.client_id && this._settings.client_id !== state.client_id) {
            Log.error("ResponseValidator._processSigninParams: client_id mismatch on settings vs. signin state");
            return Promise.reject(new Error("client_id mismatch on settings vs. signin state"));
        }

        // now that we know the state matches, take the stored data
        // and set it into the response so callers can get their state
        // this is important for both success & error outcomes
        Log.debug("ResponseValidator._processSigninParams: state validated");
        response.state = state.data;

        if (response.error) {
            Log.warn("ResponseValidator._processSigninParams: Response was error", response.error);
            return Promise.reject(new ErrorResponse(response));
        }

        if (state.nonce && !response.id_token) {
            Log.error("ResponseValidator._processSigninParams: Expecting id_token in response");
            return Promise.reject(new Error("No id_token in response"));
        }

        if (!state.nonce && response.id_token) {
            Log.error("ResponseValidator._processSigninParams: Not expecting id_token in response");
            return Promise.reject(new Error("Unexpected id_token in response"));
        }

        if (state.code_verifier && !response.code) {
            Log.error("ResponseValidator._processSigninParams: Expecting code in response");
            return Promise.reject(new Error("No code in response"));
        }

        if (!state.code_verifier && response.code) {
            Log.error("ResponseValidator._processSigninParams: Not expecting code in response");
            return Promise.reject(new Error("Unexpected code in response"));
        }

        return Promise.resolve(response);
    }

    _processClaims(response) {
        if (response.isOpenIdConnect) {
            Log.debug("ResponseValidator._processClaims: response is OIDC, processing claims");

            response.profile = this._filterProtocolClaims(response.profile);

            if (this._settings.loadUserInfo && response.access_token) {
                Log.debug("ResponseValidator._processClaims: loading user info");

                return this._userInfoService.getClaims(response.access_token).then(claims => {
                    Log.debug("ResponseValidator._processClaims: user info claims received from user info endpoint");

                    if (claims.sub !== response.profile.sub) {
                        Log.error("ResponseValidator._processClaims: sub from user info endpoint does not match sub in access_token");
                        return Promise.reject(new Error("sub from user info endpoint does not match sub in access_token"));
                    }

                    response.profile = this._mergeClaims(response.profile, claims);
                    Log.debug("ResponseValidator._processClaims: user info claims received, updated profile:", response.profile);

                    return response;
                });
            }
            else {
                Log.debug("ResponseValidator._processClaims: not loading user info");
            }
        }
        else {
            Log.debug("ResponseValidator._processClaims: response is not OIDC, not processing claims");
        }

        return Promise.resolve(response);
    }

    _mergeClaims(claims1, claims2) {
        var result = Object.assign({}, claims1);

        for (let name in claims2) {
            var values = claims2[name];
            if (!Array.isArray(values)) {
                values = [values];
            }

            for (let i = 0; i < values.length; i++) {
                let value = values[i];
                if (!result[name]) {
                    result[name] = value;
                }
                else if (Array.isArray(result[name])) {
                    if (result[name].indexOf(value) < 0) {
                        result[name].push(value);
                    }
                }
                else if (result[name] !== value) {
                    result[name] = [result[name], value];
                }
            }
        }

        return result;
    }

    _filterProtocolClaims(claims) {
        Log.debug("ResponseValidator._filterProtocolClaims, incoming claims:", claims);

        var result = Object.assign({}, claims);

        if (this._settings._filterProtocolClaims) {
            ProtocolClaims.forEach(type => {
                delete result[type];
            });

            Log.debug("ResponseValidator._filterProtocolClaims: protocol claims filtered", result);
        }
        else {
            Log.debug("ResponseValidator._filterProtocolClaims: protocol claims not filtered")
        }

        return result;
    }

    _validateTokens(state, response) {
        if (response.code) {
            Log.debug("ResponseValidator._validateTokens: Validating code");
            return this._processCode(state, response);
        }

        if (response.id_token) {
            if (response.access_token) {
                Log.debug("ResponseValidator._validateTokens: Validating id_token and access_token");
                return this._validateIdTokenAndAccessToken(state, response);
            }

            Log.debug("ResponseValidator._validateTokens: Validating id_token");
            return this._validateIdToken(state, response);
        }

        Log.debug("ResponseValidator._validateTokens: No code to process or id_token to validate");
        return Promise.resolve(response);
    }

    _processCode(state, response) {
        var request = {
            client_id: state.client_id,
            client_secret: this._settings.client_secret,
            code : response.code,
            redirect_uri: state.redirect_uri,
            code_verifier: state.code_verifier,
        };

        return this._tokenClient.exchangeCode(request).then(tokenResponse => {

            for(var key in tokenResponse) {
                response[key] = tokenResponse[key];
            }

            if (response.id_token) {
                Log.debug("ResponseValidator._processCode: token response successful, processing id_token");
                return this._validateIdTokenAttributes(state, response);
            }
            else {
                Log.debug("ResponseValidator._processCode: token response successful, returning response");
            }

            return response;
        });
    }

    _validateIdTokenAttributes(state, response) {
        return this._metadataService.getIssuer().then(issuer => {

            let audience = state.client_id;
            let clockSkewInSeconds = this._settings.clockSkew;
            Log.debug("ResponseValidator._validateIdTokenAttributes: Validaing JWT attributes; using clock skew (in seconds) of: ", clockSkewInSeconds);

            return this._joseUtil.validateJwtAttributes(response.id_token, issuer, audience, clockSkewInSeconds).then(payload => {

                if (state.nonce && state.nonce !== payload.nonce) {
                    Log.error("ResponseValidator._validateIdTokenAttributes: Invalid nonce in id_token");
                    return Promise.reject(new Error("Invalid nonce in id_token"));
                }

                if (!payload.sub) {
                    Log.error("ResponseValidator._validateIdTokenAttributes: No sub present in id_token");
                    return Promise.reject(new Error("No sub present in id_token"));
                }

                response.profile = payload;
                return response;
            });
        });
    }

    _validateIdTokenAndAccessToken(state, response) {
        return this._validateIdToken(state, response).then(response => {
            return this._validateAccessToken(response);
        });
    }

    _validateIdToken(state, response) {
        if (!state.nonce) {
            Log.error("ResponseValidator._validateIdToken: No nonce on state");
            return Promise.reject(new Error("No nonce on state"));
        }

        let jwt = this._joseUtil.parseJwt(response.id_token);
        if (!jwt || !jwt.header || !jwt.payload) {
            Log.error("ResponseValidator._validateIdToken: Failed to parse id_token", jwt);
            return Promise.reject(new Error("Failed to parse id_token"));
        }

        if (state.nonce !== jwt.payload.nonce) {
            Log.error("ResponseValidator._validateIdToken: Invalid nonce in id_token");
            return Promise.reject(new Error("Invalid nonce in id_token"));
        }

        var kid = jwt.header.kid;

        return this._metadataService.getIssuer().then(issuer => {
            Log.debug("ResponseValidator._validateIdToken: Received issuer");

            return this._metadataService.getSigningKeys().then(keys => {
                if (!keys) {
                    Log.error("ResponseValidator._validateIdToken: No signing keys from metadata");
                    return Promise.reject(new Error("No signing keys from metadata"));
                }

                Log.debug("ResponseValidator._validateIdToken: Received signing keys");
                let key;
                if (!kid) {
                    keys = this._filterByAlg(keys, jwt.header.alg);

                    if (keys.length > 1) {
                        Log.error("ResponseValidator._validateIdToken: No kid found in id_token and more than one key found in metadata");
                        return Promise.reject(new Error("No kid found in id_token and more than one key found in metadata"));
                    }
                    else {
                        // kid is mandatory only when there are multiple keys in the referenced JWK Set document
                        // see http://openid.net/specs/openid-connect-core-1_0.html#Signing
                        key = keys[0];
                    }
                }
                else {
                    key = keys.filter(key => {
                        return key.kid === kid;
                    })[0];
                }

                if (!key) {
                    Log.error("ResponseValidator._validateIdToken: No key matching kid or alg found in signing keys");
                    return Promise.reject(new Error("No key matching kid or alg found in signing keys"));
                }

                let audience = state.client_id;

                let clockSkewInSeconds = this._settings.clockSkew;
                Log.debug("ResponseValidator._validateIdToken: Validaing JWT; using clock skew (in seconds) of: ", clockSkewInSeconds);

                return this._joseUtil.validateJwt(response.id_token, key, issuer, audience, clockSkewInSeconds).then(()=>{
                    Log.debug("ResponseValidator._validateIdToken: JWT validation successful");

                    if (!jwt.payload.sub) {
                        Log.error("ResponseValidator._validateIdToken: No sub present in id_token");
                        return Promise.reject(new Error("No sub present in id_token"));
                    }

                    response.profile = jwt.payload;

                    return response;
                });
            });
        });
    }

    _filterByAlg(keys, alg){
        var kty = null;
        if (alg.startsWith("RS")) {
            kty = "RSA";
        }
        else if (alg.startsWith("PS")) {
            kty = "PS";
        }
        else if (alg.startsWith("ES")) {
            kty = "EC";
        }
        else {
            Log.debug("ResponseValidator._filterByAlg: alg not supported: ", alg);
            return [];
        }

        Log.debug("ResponseValidator._filterByAlg: Looking for keys that match kty: ", kty);

        keys = keys.filter(key => {
            return key.kty === kty;
        });

        Log.debug("ResponseValidator._filterByAlg: Number of keys that match kty: ", kty, keys.length);

        return keys;
    }

    _validateAccessToken(response) {
        if (!response.profile) {
            Log.error("ResponseValidator._validateAccessToken: No profile loaded from id_token");
            return Promise.reject(new Error("No profile loaded from id_token"));
        }

        if (!response.profile.at_hash) {
            Log.error("ResponseValidator._validateAccessToken: No at_hash in id_token");
            return Promise.reject(new Error("No at_hash in id_token"));
        }

        if (!response.id_token) {
            Log.error("ResponseValidator._validateAccessToken: No id_token");
            return Promise.reject(new Error("No id_token"));
        }

        let jwt = this._joseUtil.parseJwt(response.id_token);
        if (!jwt || !jwt.header) {
            Log.error("ResponseValidator._validateAccessToken: Failed to parse id_token", jwt);
            return Promise.reject(new Error("Failed to parse id_token"));
        }

        var hashAlg = jwt.header.alg;
        if (!hashAlg || hashAlg.length !== 5) {
            Log.error("ResponseValidator._validateAccessToken: Unsupported alg:", hashAlg);
            return Promise.reject(new Error("Unsupported alg: " + hashAlg));
        }

        var hashBits = hashAlg.substr(2, 3);
        if (!hashBits) {
            Log.error("ResponseValidator._validateAccessToken: Unsupported alg:", hashAlg, hashBits);
            return Promise.reject(new Error("Unsupported alg: " + hashAlg));
        }

        hashBits = parseInt(hashBits);
        if (hashBits !== 256 && hashBits !== 384 && hashBits !== 512) {
            Log.error("ResponseValidator._validateAccessToken: Unsupported alg:", hashAlg, hashBits);
            return Promise.reject(new Error("Unsupported alg: " + hashAlg));
        }

        let sha = "sha" + hashBits;
        var hash = this._joseUtil.hashString(response.access_token, sha);
        if (!hash) {
            Log.error("ResponseValidator._validateAccessToken: access_token hash failed:", sha);
            return Promise.reject(new Error("Failed to validate at_hash"));
        }

        var left = hash.substr(0, hash.length / 2);
        var left_b64u = this._joseUtil.hexToBase64Url(left);
        if (left_b64u !== response.profile.at_hash) {
            Log.error("ResponseValidator._validateAccessToken: Failed to validate at_hash", left_b64u, response.profile.at_hash);
            return Promise.reject(new Error("Failed to validate at_hash"));
        }

        Log.debug("ResponseValidator._validateAccessToken: success");

        return Promise.resolve(response);
    }
}
<|endoftext|>


<commit_before>
(function() {
  $('#user').changeAsObservable().select(function(event) { return event.target.value }).subscribe(function(data) { console.log("hello world", data) })
})()

<commit_msg>get updated summary whenever username is changed.<commit_after>
(function() {

  var getSummary = function(user) {
    return $.ajaxAsObservable({ url: "http://localhost:8080/summary/" + user })
  }

  var drawSummary = function(data) {
    debug(data)
  }

  var summaryRequests = $('#user').changeAsObservable().select(function(event) { return event.target.value }).selectAjax(getSummary)
  summaryRequests.where(isSuccess).select(ajaxResponseData).subscribe(drawSummary)

})()

<|endoftext|>


<commit_before>$(function() {
	let App = require("./App.vue");

	new Vue({
		el: "body",
		components: {
			App
		}
	});
});<commit_msg>improve code quality<commit_after>$(function() {
	let App = require("./App.vue");

	let app = new Vue({
		el: "body",
		components: {
			App
		}
	});
});<|endoftext|>


<commit_before>import React, { useState, useEffect } from 'react'
import MediaQuery from 'react-responsive';
import FetchingIndicator from '../Fetching/FetchingIndicator'
import SeguidorHeading from './SeguidorHeading'
import SeguidorView from './SeguidorView';

const Seguidor = (props) => {

  const {
    isFetching,
    error,
    materias,
    updateEstado
  } = props;

  useEffect(() => {
    if (materias.length === 0) { props.onLoad(); }

  }, [props, materias])

  const [view, setView] = useState('carousel');

  const changeViewType = (type) => {
    setView(type);
  }

  if (isFetching) { return <FetchingIndicator />; }

  if (error) { return
Hubo un error recuperando las materias.
; }

  return (

        <SeguidorHeading
          changeViewType={changeViewType}
          currentView={view}
        />

      <SeguidorView
        materias={materias}
        updateEstado={updateEstado}
        view={view}
      />

  );
}

export default Seguidor;
<commit_msg>Cambios olvidados en el ultimo update.<commit_after>import React, { useState, useEffect } from 'react';
import MediaQuery from 'react-responsive';
import FetchingIndicator from '../Fetching/FetchingIndicator';
import SeguidorHeading from './SeguidorHeading';
import SeguidorView from './SeguidorView';

const Seguidor = (props) => {

  const {
    isFetching,
    error,
    materias,
    updateEstado
  } = props;

  useEffect(() => {
    if (materias.length === 0) { props.onLoad(); }

  }, [props, materias])

  const [view, setView] = useState('carousel');

  const changeViewType = (type) => {
    setView(type);
  }

  if (isFetching) { return <FetchingIndicator />; }

  if (error) { return
Hubo un error recuperando las materias.
; }

  return (

        <MediaQuery maxDeviceWidth={1224}>
         <SeguidorHeading
           changeViewType={changeViewType}
           currentView={view}
           desktop={false}
         />
        </MediaQuery>
        <MediaQuery minDeviceWidth={1224}>
           <SeguidorHeading
             changeViewType={changeViewType}
             currentView={view}
             desktop={true}
           />
        </MediaQuery>

      <SeguidorView
        materias={materias}
        updateEstado={updateEstado}
        view={view}
      />

  );
}

export default Seguidor;
<|endoftext|>


<commit_before>import process, { exit } from 'node:process'
import { log } from '@serverless/utils/log.js'
import {
  commandOptions,
  CUSTOM_OPTION,
  defaultOptions,
  SERVER_SHUTDOWN_TIMEOUT,
} from './config/index.js'
import { gray } from './config/colors.js'

export default class ServerlessOffline {
  #cliOptions = null

  #http = null

  #lambda = null

  #options = null

  #schedule = null

  #serverless = null

  #webSocket = null

  commands = {
    offline: {
      // add start nested options
      commands: {
        functionsUpdated: {
          lifecycleEvents: ['cleanup'],
          type: 'entrypoint',
        },
        start: {
          lifecycleEvents: ['init', 'ready', 'end'],
          options: commandOptions,
          usage:
            'Simulates API Gateway to call your lambda functions offline using backward compatible initialization.',
        },
      },
      lifecycleEvents: ['start'],
      options: commandOptions,
      usage: 'Simulates API Gateway to call your lambda functions offline.',
    },
  }

  hooks = {
    'offline:functionsUpdated:cleanup': this.#cleanupFunctions.bind(this),
    'offline:start': this.#startWithExplicitEnd.bind(this),
    'offline:start:end': this.end.bind(this),
    'offline:start:init': this.start.bind(this),
    'offline:start:ready': this.#ready.bind(this),
  }

  constructor(serverless, cliOptions) {
    this.#cliOptions = cliOptions
    this.#serverless = serverless
  }

  // Entry point for the plugin (sls offline) when running 'sls offline start'
  async start() {
    this.#mergeOptions()

    const { httpEvents, lambdas, scheduleEvents, webSocketEvents } =
      this.#getEvents()

    if (lambdas.length > 0) {
      await this.#createLambda(lambdas)
    }

    const eventModules = []

    if (httpEvents.length > 0) {
      eventModules.push(this.#createHttp(httpEvents))
    }

    if (scheduleEvents.length > 0) {
      eventModules.push(this.#createSchedule(scheduleEvents))
    }

    if (webSocketEvents.length > 0) {
      eventModules.push(this.#createWebSocket(webSocketEvents))
    }

    await Promise.all(eventModules)
  }

  async #ready() {
    await this.#listenForTermination()
  }

  async end(skipExit) {
    log.info('Halting offline server')

    const eventModules = []

    if (this.#lambda) {
      eventModules.push(this.#lambda.cleanup())
      eventModules.push(this.#lambda.stop(SERVER_SHUTDOWN_TIMEOUT))
    }

    if (this.#http) {
      eventModules.push(this.#http.stop(SERVER_SHUTDOWN_TIMEOUT))
    }

    // if (this.#schedule) {
    //   eventModules.push(this.#schedule.stop())
    // }

    if (this.#webSocket) {
      eventModules.push(this.#webSocket.stop(SERVER_SHUTDOWN_TIMEOUT))
    }

    await Promise.all(eventModules)

    if (!skipExit) {
      exit(0)
    }
  }

  async #cleanupFunctions() {
    if (this.#lambda) {
      log.debug('Forcing cleanup of Lambda functions')
      await this.#lambda.cleanup()
    }
  }

  /**
   * Entry point for the plugin (serverless offline) when running 'serverless offline'
   * The call to this.end() would terminate the process before 'offline:start:end' could be consumed
   * by downstream plugins. When running serverless offline that can be expected, but docs say that
   * 'serverless offline start' will provide the init and end hooks for other plugins to consume
   * */
  async #startWithExplicitEnd() {
    await this.start()
    await this.#ready()
    await this.end()
  }

  async #listenForTermination() {
    const command = await new Promise((resolve) => {
      process
        // SIGINT will be usually sent when user presses ctrl+c
        .on('SIGINT', () => resolve('SIGINT'))
        // SIGTERM is a default termination signal in many cases,
        // for example when "killing" a subprocess spawned in node
        // with child_process methods
        .on('SIGTERM', () => resolve('SIGTERM'))
    })

    log.info(`Got ${command} signal. Offline Halting...`)
  }

  async #createLambda(lambdas, skipStart) {
    const { default: Lambda } = await import('./lambda/index.js')

    this.#lambda = new Lambda(this.#serverless, this.#options)

    this.#lambda.create(lambdas)

    if (!skipStart) {
      await this.#lambda.start()
    }
  }

  async #createHttp(events, skipStart) {
    const { default: Http } = await import('./events/http/index.js')

    this.#http = new Http(this.#serverless, this.#options, this.#lambda)

    await this.#http.createServer()

    this.#http.create(events)

    // HTTP Proxy defined in Resource
    this.#http.createResourceRoutes()

    // Not found handling
    // we have to create the 404 routes last, otherwise we could have
    // collisions with catch all routes, e.g. any (proxy+}
    this.#http.create404Route()

    if (!skipStart) {
      await this.#http.start()
    }
  }

  async #createSchedule(events) {
    const { default: Schedule } = await import('./events/schedule/index.js')

    this.#schedule = new Schedule(
      this.#lambda,
      this.#serverless.service.provider.region,
    )

    this.#schedule.create(events)
  }

  async #createWebSocket(events) {
    const { default: WebSocket } = await import('./events/websocket/index.js')

    this.#webSocket = new WebSocket(
      this.#serverless,
      this.#options,
      this.#lambda,
    )

    await this.#webSocket.createServer()

    this.#webSocket.create(events)

    await this.#webSocket.start()
  }

  #mergeOptions() {
    const {
      service: { custom = {}, provider },
    } = this.#serverless

    const customOptions = custom[CUSTOM_OPTION]

    // merge options
    // order of Precedence: command line options, custom options, defaults.
    this.#options = {
      ...defaultOptions,
      ...customOptions,
      ...this.#cliOptions,
    }

    // Parse CORS options
    this.#options.corsAllowHeaders = this.#options.corsAllowHeaders
      .replace(/\s/g, '')
      .split(',')
    this.#options.corsAllowOrigin = this.#options.corsAllowOrigin
      .replace(/\s/g, '')
      .split(',')
    this.#options.corsExposedHeaders = this.#options.corsExposedHeaders
      .replace(/\s/g, '')
      .split(',')

    this.#options.corsConfig = {
      credentials: !this.#options.corsDisallowCredentials,
      exposedHeaders: this.#options.corsExposedHeaders,
      headers: this.#options.corsAllowHeaders,
      origin: this.#options.corsAllowOrigin,
    }

    log.notice()
    log.notice(
      `Starting Offline at stage ${
        this.#options.stage || provider.stage
      } ${gray(`(${this.#options.region || provider.region})`)}`,
    )
    log.notice()
    log.debug('options:', this.#options)
  }

  #getEvents() {
    const { service } = this.#serverless

    const httpEvents = []
    const lambdas = []
    const scheduleEvents = []
    const webSocketEvents = []

    const functionKeys = service.getAllFunctions()

    functionKeys.forEach((functionKey) => {
      const functionDefinition = service.getFunction(functionKey)

      lambdas.push({ functionDefinition, functionKey })

      const events = service.getAllEventsInFunction(functionKey) || []

      events.forEach((event) => {
        const { http, httpApi, schedule, websocket } = event

        if ((http || httpApi) && functionDefinition.handler) {
          const httpEvent = {
            functionKey,
            handler: functionDefinition.handler,
            http: http || httpApi,
          }

          if (httpApi) {
            // Ensure definitions for 'httpApi' events are objects so that they can be marked
            // with an 'isHttpApi' property (they are handled differently to 'http' events)
            if (typeof httpEvent.http === 'string') {
              httpEvent.http = {
                routeKey: httpEvent.http === '*' ? '$default' : httpEvent.http,
              }
            } else if (typeof httpEvent.http === 'object') {
              if (!httpEvent.http.method) {
                log.warning(
                  `Event definition is missing a method for function "${functionKey}"`,
                )
                httpEvent.http.method = ''
              }
              if (
                httpEvent.http.method === '*' &&
                httpEvent.http.path === '*'
              ) {
                httpEvent.http.routeKey = '$default'
              } else {
                const resolvedMethod =
                  httpEvent.http.method === '*'
                    ? 'ANY'
                    : httpEvent.http.method.toUpperCase()
                httpEvent.http.routeKey = `${resolvedMethod} ${httpEvent.http.path}`
              }
              // Clear these properties to avoid confusion (they will be derived from the routeKey
              // when needed later)
              delete httpEvent.http.method
              delete httpEvent.http.path
            } else {
              log.warning(
                `Event definition must be a string or object but received ${typeof httpEvent.http} for function "${functionKey}"`,
              )
              httpEvent.http.routeKey = ''
            }

            httpEvent.http.isHttpApi = true

            if (
              functionDefinition.httpApi &&
              functionDefinition.httpApi.payload
            ) {
              httpEvent.http.payload = functionDefinition.httpApi.payload
            } else {
              httpEvent.http.payload =
                service.provider.httpApi && service.provider.httpApi.payload
                  ? service.provider.httpApi.payload
                  : '2.0'
            }
          }

          httpEvents.push(httpEvent)
        }

        if (schedule) {
          scheduleEvents.push({
            functionKey,
            schedule,
          })
        }

        if (websocket) {
          webSocketEvents.push({
            functionKey,
            websocket,
          })
        }
      })
    })

    return {
      httpEvents,
      lambdas,
      scheduleEvents,
      webSocketEvents,
    }
  }

  // TODO FIXME
  // TEMP quick fix to expose for testing, look for better solution
  internals() {
    return {
      createHttp: (events, skipStart) => {
        return this.#createHttp(events, skipStart)
      },

      createLambda: (lambdas, skipStart) => {
        return this.#createLambda(lambdas, skipStart)
      },

      getApiGatewayServer: () => {
        return this.#http.getServer()
      },

      getEvents: () => {
        return this.#getEvents()
      },

      mergeOptions: () => {
        this.#mergeOptions()
      },
    }
  }
}
<commit_msg>refactor: split get events for http and httpApi<commit_after>import process, { exit } from 'node:process'
import { log } from '@serverless/utils/log.js'
import {
  commandOptions,
  CUSTOM_OPTION,
  defaultOptions,
  SERVER_SHUTDOWN_TIMEOUT,
} from './config/index.js'
import { gray } from './config/colors.js'

export default class ServerlessOffline {
  #cliOptions = null

  #http = null

  #lambda = null

  #options = null

  #schedule = null

  #serverless = null

  #webSocket = null

  commands = {
    offline: {
      // add start nested options
      commands: {
        functionsUpdated: {
          lifecycleEvents: ['cleanup'],
          type: 'entrypoint',
        },
        start: {
          lifecycleEvents: ['init', 'ready', 'end'],
          options: commandOptions,
          usage:
            'Simulates API Gateway to call your lambda functions offline using backward compatible initialization.',
        },
      },
      lifecycleEvents: ['start'],
      options: commandOptions,
      usage: 'Simulates API Gateway to call your lambda functions offline.',
    },
  }

  hooks = {
    'offline:functionsUpdated:cleanup': this.#cleanupFunctions.bind(this),
    'offline:start': this.#startWithExplicitEnd.bind(this),
    'offline:start:end': this.end.bind(this),
    'offline:start:init': this.start.bind(this),
    'offline:start:ready': this.#ready.bind(this),
  }

  constructor(serverless, cliOptions) {
    this.#cliOptions = cliOptions
    this.#serverless = serverless
  }

  // Entry point for the plugin (sls offline) when running 'sls offline start'
  async start() {
    this.#mergeOptions()

    const {
      httpEvents,
      httpApiEvents,
      lambdas,
      scheduleEvents,
      webSocketEvents,
    } = this.#getEvents()

    if (lambdas.length > 0) {
      await this.#createLambda(lambdas)
    }

    const eventModules = []

    if (httpApiEvents.length > 0 || httpEvents.length > 0) {
      eventModules.push(this.#createHttp([...httpApiEvents, ...httpEvents]))
    }

    if (scheduleEvents.length > 0) {
      eventModules.push(this.#createSchedule(scheduleEvents))
    }

    if (webSocketEvents.length > 0) {
      eventModules.push(this.#createWebSocket(webSocketEvents))
    }

    await Promise.all(eventModules)
  }

  async #ready() {
    await this.#listenForTermination()
  }

  async end(skipExit) {
    log.info('Halting offline server')

    const eventModules = []

    if (this.#lambda) {
      eventModules.push(this.#lambda.cleanup())
      eventModules.push(this.#lambda.stop(SERVER_SHUTDOWN_TIMEOUT))
    }

    if (this.#http) {
      eventModules.push(this.#http.stop(SERVER_SHUTDOWN_TIMEOUT))
    }

    // if (this.#schedule) {
    //   eventModules.push(this.#schedule.stop())
    // }

    if (this.#webSocket) {
      eventModules.push(this.#webSocket.stop(SERVER_SHUTDOWN_TIMEOUT))
    }

    await Promise.all(eventModules)

    if (!skipExit) {
      exit(0)
    }
  }

  async #cleanupFunctions() {
    if (this.#lambda) {
      log.debug('Forcing cleanup of Lambda functions')
      await this.#lambda.cleanup()
    }
  }

  /**
   * Entry point for the plugin (serverless offline) when running 'serverless offline'
   * The call to this.end() would terminate the process before 'offline:start:end' could be consumed
   * by downstream plugins. When running serverless offline that can be expected, but docs say that
   * 'serverless offline start' will provide the init and end hooks for other plugins to consume
   * */
  async #startWithExplicitEnd() {
    await this.start()
    await this.#ready()
    await this.end()
  }

  async #listenForTermination() {
    const command = await new Promise((resolve) => {
      process
        // SIGINT will be usually sent when user presses ctrl+c
        .on('SIGINT', () => resolve('SIGINT'))
        // SIGTERM is a default termination signal in many cases,
        // for example when "killing" a subprocess spawned in node
        // with child_process methods
        .on('SIGTERM', () => resolve('SIGTERM'))
    })

    log.info(`Got ${command} signal. Offline Halting...`)
  }

  async #createLambda(lambdas, skipStart) {
    const { default: Lambda } = await import('./lambda/index.js')

    this.#lambda = new Lambda(this.#serverless, this.#options)

    this.#lambda.create(lambdas)

    if (!skipStart) {
      await this.#lambda.start()
    }
  }

  async #createHttp(events, skipStart) {
    const { default: Http } = await import('./events/http/index.js')

    this.#http = new Http(this.#serverless, this.#options, this.#lambda)

    await this.#http.createServer()

    this.#http.create(events)

    // HTTP Proxy defined in Resource
    this.#http.createResourceRoutes()

    // Not found handling
    // we have to create the 404 routes last, otherwise we could have
    // collisions with catch all routes, e.g. any (proxy+}
    this.#http.create404Route()

    if (!skipStart) {
      await this.#http.start()
    }
  }

  async #createSchedule(events) {
    const { default: Schedule } = await import('./events/schedule/index.js')

    this.#schedule = new Schedule(
      this.#lambda,
      this.#serverless.service.provider.region,
    )

    this.#schedule.create(events)
  }

  async #createWebSocket(events) {
    const { default: WebSocket } = await import('./events/websocket/index.js')

    this.#webSocket = new WebSocket(
      this.#serverless,
      this.#options,
      this.#lambda,
    )

    await this.#webSocket.createServer()

    this.#webSocket.create(events)

    await this.#webSocket.start()
  }

  #mergeOptions() {
    const {
      service: { custom = {}, provider },
    } = this.#serverless

    const customOptions = custom[CUSTOM_OPTION]

    // merge options
    // order of Precedence: command line options, custom options, defaults.
    this.#options = {
      ...defaultOptions,
      ...customOptions,
      ...this.#cliOptions,
    }

    // Parse CORS options
    this.#options.corsAllowHeaders = this.#options.corsAllowHeaders
      .replace(/\s/g, '')
      .split(',')
    this.#options.corsAllowOrigin = this.#options.corsAllowOrigin
      .replace(/\s/g, '')
      .split(',')
    this.#options.corsExposedHeaders = this.#options.corsExposedHeaders
      .replace(/\s/g, '')
      .split(',')

    this.#options.corsConfig = {
      credentials: !this.#options.corsDisallowCredentials,
      exposedHeaders: this.#options.corsExposedHeaders,
      headers: this.#options.corsAllowHeaders,
      origin: this.#options.corsAllowOrigin,
    }

    log.notice()
    log.notice(
      `Starting Offline at stage ${
        this.#options.stage || provider.stage
      } ${gray(`(${this.#options.region || provider.region})`)}`,
    )
    log.notice()
    log.debug('options:', this.#options)
  }

  #getEvents() {
    const { service } = this.#serverless

    const httpEvents = []
    const httpApiEvents = []
    const lambdas = []
    const scheduleEvents = []
    const webSocketEvents = []

    const functionKeys = service.getAllFunctions()

    functionKeys.forEach((functionKey) => {
      const functionDefinition = service.getFunction(functionKey)

      lambdas.push({ functionDefinition, functionKey })

      const events = service.getAllEventsInFunction(functionKey) || []

      events.forEach((event) => {
        const { http, httpApi, schedule, websocket } = event

        if (http && functionDefinition.handler) {
          const httpEvent = {
            functionKey,
            handler: functionDefinition.handler,
            http,
          }

          httpEvents.push(httpEvent)
        }

        if (httpApi && functionDefinition.handler) {
          const httpApiEvent = {
            functionKey,
            handler: functionDefinition.handler,
            http: httpApi,
          }

          // Ensure definitions for 'httpApi' events are objects so that they can be marked
          // with an 'isHttpApi' property (they are handled differently to 'http' events)
          if (typeof httpApiEvent.http === 'string') {
            httpApiEvent.http = {
              routeKey:
                httpApiEvent.http === '*' ? '$default' : httpApiEvent.http,
            }
          } else if (typeof httpApiEvent.http === 'object') {
            if (!httpApiEvent.http.method) {
              log.warning(
                `Event definition is missing a method for function "${functionKey}"`,
              )
              httpApiEvent.http.method = ''
            }
            if (
              httpApiEvent.http.method === '*' &&
              httpApiEvent.http.path === '*'
            ) {
              httpApiEvent.http.routeKey = '$default'
            } else {
              const resolvedMethod =
                httpApiEvent.http.method === '*'
                  ? 'ANY'
                  : httpApiEvent.http.method.toUpperCase()
              httpApiEvent.http.routeKey = `${resolvedMethod} ${httpApiEvent.http.path}`
            }
            // Clear these properties to avoid confusion (they will be derived from the routeKey
            // when needed later)
            delete httpApiEvent.http.method
            delete httpApiEvent.http.path
          } else {
            log.warning(
              `Event definition must be a string or object but received ${typeof httpApiEvent.http} for function "${functionKey}"`,
            )
            httpApiEvent.http.routeKey = ''
          }

          httpApiEvent.http.isHttpApi = true

          if (
            functionDefinition.httpApi &&
            functionDefinition.httpApi.payload
          ) {
            httpApiEvent.http.payload = functionDefinition.httpApi.payload
          } else {
            httpApiEvent.http.payload =
              service.provider.httpApi && service.provider.httpApi.payload
                ? service.provider.httpApi.payload
                : '2.0'
          }

          httpApiEvents.push(httpApiEvent)
        }

        if (schedule) {
          scheduleEvents.push({
            functionKey,
            schedule,
          })
        }

        if (websocket) {
          webSocketEvents.push({
            functionKey,
            websocket,
          })
        }
      })
    })

    return {
      httpApiEvents,
      httpEvents,
      lambdas,
      scheduleEvents,
      webSocketEvents,
    }
  }

  // TODO FIXME
  // TEMP quick fix to expose for testing, look for better solution
  internals() {
    return {
      createHttp: (events, skipStart) => {
        return this.#createHttp(events, skipStart)
      },

      createLambda: (lambdas, skipStart) => {
        return this.#createLambda(lambdas, skipStart)
      },

      getApiGatewayServer: () => {
        return this.#http.getServer()
      },

      getEvents: () => {
        return this.#getEvents()
      },

      mergeOptions: () => {
        this.#mergeOptions()
      },
    }
  }
}
<|endoftext|>


<commit_before>//
// Copyright (c) Microsoft and contributors.  All rights reserved.
//
// 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.
//

// Module dependencies.
var _ = require('underscore');
var util = require('util');

var azureutil = require('../util/util');

var StorageServiceSettings = require('./storageservicesettings');

var ServiceClient = require('./serviceclient');
var Constants = require('../util/constants');
var ServiceClientConstants = require('./serviceclientconstants');
var HeaderConstants = Constants.HeaderConstants;
var QueryStringConstants = Constants.QueryStringConstants;

/**
* Creates a new ServiceClient object.
*
* @constructor
* @param {string} storageAccount           The storage account.
* @param {string} storageAccessKey         The storage access key.
* @param {string} host                     The host for the service.
* @param {bool}   usePathStyleUri          Boolean value indicating wether to use path style uris.
* @param {object} authenticationProvider   The authentication provider object (e.g. sharedkey / sharedkeytable / sharedaccesssignature).
*/
function StorageServiceClient(storageAccount, storageAccessKey, host, usePathStyleUri, authenticationProvider) {
  this._setAccountCredentials(storageAccount, storageAccessKey);
  this.apiVersion = HeaderConstants.TARGET_STORAGE_VERSION;
  this.usePathStyleUri = usePathStyleUri;

  StorageServiceClient['super_'].call(this, host, authenticationProvider);

  this._initDefaultFilter();
}

util.inherits(StorageServiceClient, ServiceClient);

// Validation error messages
StorageServiceClient.incorrectStorageAccountErr = 'You must supply an account name or use the environment variable AZURE_STORAGE_ACCOUNT if you are not running in the emulator.';
StorageServiceClient.incorrectStorageAccessKeyErr = 'You must supply an account key or use the environment variable AZURE_STORAGE_ACCESS_KEY if you are not running in the emulator.';

/**
* Gets the storage settings.
*
* @param {string} [storageAccountOrConnectionString]  The storage account or the connection string.
* @param {string} [storageAccessKey]                  The storage access key.
* @param {string} [host]                              The host address.
*
* @return {StorageServiceSettings}
*/
StorageServiceClient.getStorageSettings = function (storageAccountOrConnectionString, storageAccessKey, host) {
  var storageServiceSettings;

  if (_.isString(storageAccountOrConnectionString) && !storageAccessKey) {
    // If storageAccountOrConnectionString was passed and no accessKey was passed, assume connection string
    storageServiceSettings = StorageServiceSettings.createFromConnectionString(storageAccountOrConnectionString);
  } else if (!(storageAccountOrConnectionString && storageAccessKey) && ServiceClient.isEmulated()) {
    // Dev storage scenario
    storageServiceSettings = StorageServiceSettings.getDevelopmentStorageAccountSettings();
  } else if (storageAccountOrConnectionString && storageAccessKey) {
    storageServiceSettings = StorageServiceSettings.createExplicitlyOrFromEnvironment(storageAccountOrConnectionString, storageAccessKey, host);
  } else {
    storageServiceSettings = StorageServiceSettings.createFromConfig(storageAccountOrConnectionString);
  }

  return storageServiceSettings;
};

/**
* Builds the request options to be passed to the http.request method.
*
* @param {WebResource} webResource The webresource where to build the options from.
* @param {object}      options     The request options.
* @param {function(error, requestOptions)}  callback  The callback function.
* @return {undefined}
*/
StorageServiceClient.prototype._buildRequestOptions = function (webResource, body, options, callback) {
  webResource.withHeader(HeaderConstants.STORAGE_VERSION_HEADER, this.apiVersion);
  webResource.withHeader(HeaderConstants.DATE_HEADER, new Date().toUTCString());
  webResource.withHeader(HeaderConstants.ACCEPT_HEADER, 'application/atom+xml,application/xml');
  webResource.withHeader(HeaderConstants.ACCEPT_CHARSET_HEADER, 'UTF-8');

  if (options) {
    if (options.timeoutIntervalInMs) {
      webResource.withQueryOption(QueryStringConstants.TIMEOUT, options.timeoutIntervalInMs);
    }

    if (options.accessConditions) {
      webResource.withHeaders(options.accessConditions,
        HeaderConstants.IF_MATCH,
        HeaderConstants.IF_MODIFIED_SINCE,
        HeaderConstants.IF_NONE_MATCH,
        HeaderConstants.IF_UNMODIFIED_SINCE);
    }

    if (options.sourceAccessConditions) {
      webResource.withHeaders(options.sourceAccessConditions,
        HeaderConstants.SOURCE_IF_MATCH_HEADER,
        HeaderConstants.SOURCE_IF_MODIFIED_SINCE_HEADER,
        HeaderConstants.SOURCE_IF_NONE_MATCH_HEADER,
        HeaderConstants.SOURCE_IF_UNMODIFIED_SINCE_HEADER);
    }
  }

  StorageServiceClient['super_'].prototype._buildRequestOptions.call(this, webResource, body, options, callback);
};

/**
* Retrieves the normalized path to be used in a request.
* This takes into consideration the usePathStyleUri object field
* which specifies if the request is against the emulator or against
* the live service. It also adds a leading "/" to the path in case
* it's not there before.
*
* @param {string} path The path to be normalized.
* @return {string} The normalized path.
*/
StorageServiceClient.prototype._getPath = function (path) {
  if (path === null || path === undefined) {
    path = '/';
  } else if (path.indexOf('/') !== 0) {
    path = '/' + path;
  }

  if (this.usePathStyleUri) {
    path = '/' + this.storageAccount + path;
  }

  return path;
};

/**
* Sets the account credentials taking into consideration the isEmulated value and the environment variables:
* AZURE_STORAGE_ACCOUNT and AZURE_STORAGE_ACCESS_KEY.
*
* @param {string} storageAccount          The storage account.
* @param {string} storageAccessKey        The storage access key.
* @return {undefined}
*/
StorageServiceClient.prototype._setAccountCredentials = function (storageAccount, storageAccessKey) {
  if (azureutil.objectIsNull(storageAccount)) {
    if (process.env[ServiceClientConstants.EnvironmentVariables.AZURE_STORAGE_ACCOUNT]) {
      storageAccount = process.env[ServiceClientConstants.EnvironmentVariables.AZURE_STORAGE_ACCOUNT];
    } else if (ServiceClient.isEmulated()) {
      storageAccount = ServiceClientConstants.DEVSTORE_STORAGE_ACCOUNT;
    }
  }

  if (azureutil.objectIsNull(storageAccessKey)) {
    if (process.env[ServiceClientConstants.EnvironmentVariables.AZURE_STORAGE_ACCESS_KEY]) {
      storageAccessKey = process.env[ServiceClient.EnvironmentVariables.AZURE_STORAGE_ACCESS_KEY];
    } else if (ServiceClient.isEmulated()) {
      storageAccessKey = ServiceClient.DEVSTORE_STORAGE_ACCESS_KEY;
    }
  }

  if (!azureutil.objectIsString(storageAccount) || azureutil.stringIsEmpty(storageAccount)) {
    throw new Error(StorageServiceClient.incorrectStorageAccountErr);
  }

  if (!azureutil.objectIsString(storageAccessKey) || azureutil.stringIsEmpty(storageAccessKey)) {
    throw new Error(StorageServiceClient.incorrectStorageAccessKeyErr);
  }

  this.storageAccount = storageAccount;
  this.storageAccessKey = storageAccessKey;
};

module.exports = StorageServiceClient;
<commit_msg>Fix (very) minor comment typos<commit_after>//
// Copyright (c) Microsoft and contributors.  All rights reserved.
//
// 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.
//

// Module dependencies.
var _ = require('underscore');
var util = require('util');

var azureutil = require('../util/util');

var StorageServiceSettings = require('./storageservicesettings');

var ServiceClient = require('./serviceclient');
var Constants = require('../util/constants');
var ServiceClientConstants = require('./serviceclientconstants');
var HeaderConstants = Constants.HeaderConstants;
var QueryStringConstants = Constants.QueryStringConstants;

/**
* Creates a new ServiceClient object.
*
* @constructor
* @param {string} storageAccount           The storage account.
* @param {string} storageAccessKey         The storage access key.
* @param {string} host                     The host for the service.
* @param {bool}   usePathStyleUri          Boolean value indicating whether to use path style URIs.
* @param {object} authenticationProvider   The authentication provider object (e.g. sharedkey / sharedkeytable / sharedaccesssignature).
*/
function StorageServiceClient(storageAccount, storageAccessKey, host, usePathStyleUri, authenticationProvider) {
  this._setAccountCredentials(storageAccount, storageAccessKey);
  this.apiVersion = HeaderConstants.TARGET_STORAGE_VERSION;
  this.usePathStyleUri = usePathStyleUri;

  StorageServiceClient['super_'].call(this, host, authenticationProvider);

  this._initDefaultFilter();
}

util.inherits(StorageServiceClient, ServiceClient);

// Validation error messages
StorageServiceClient.incorrectStorageAccountErr = 'You must supply an account name or use the environment variable AZURE_STORAGE_ACCOUNT if you are not running in the emulator.';
StorageServiceClient.incorrectStorageAccessKeyErr = 'You must supply an account key or use the environment variable AZURE_STORAGE_ACCESS_KEY if you are not running in the emulator.';

/**
* Gets the storage settings.
*
* @param {string} [storageAccountOrConnectionString]  The storage account or the connection string.
* @param {string} [storageAccessKey]                  The storage access key.
* @param {string} [host]                              The host address.
*
* @return {StorageServiceSettings}
*/
StorageServiceClient.getStorageSettings = function (storageAccountOrConnectionString, storageAccessKey, host) {
  var storageServiceSettings;

  if (_.isString(storageAccountOrConnectionString) && !storageAccessKey) {
    // If storageAccountOrConnectionString was passed and no accessKey was passed, assume connection string
    storageServiceSettings = StorageServiceSettings.createFromConnectionString(storageAccountOrConnectionString);
  } else if (!(storageAccountOrConnectionString && storageAccessKey) && ServiceClient.isEmulated()) {
    // Dev storage scenario
    storageServiceSettings = StorageServiceSettings.getDevelopmentStorageAccountSettings();
  } else if (storageAccountOrConnectionString && storageAccessKey) {
    storageServiceSettings = StorageServiceSettings.createExplicitlyOrFromEnvironment(storageAccountOrConnectionString, storageAccessKey, host);
  } else {
    storageServiceSettings = StorageServiceSettings.createFromConfig(storageAccountOrConnectionString);
  }

  return storageServiceSettings;
};

/**
* Builds the request options to be passed to the http.request method.
*
* @param {WebResource} webResource The webresource where to build the options from.
* @param {object}      options     The request options.
* @param {function(error, requestOptions)}  callback  The callback function.
* @return {undefined}
*/
StorageServiceClient.prototype._buildRequestOptions = function (webResource, body, options, callback) {
  webResource.withHeader(HeaderConstants.STORAGE_VERSION_HEADER, this.apiVersion);
  webResource.withHeader(HeaderConstants.DATE_HEADER, new Date().toUTCString());
  webResource.withHeader(HeaderConstants.ACCEPT_HEADER, 'application/atom+xml,application/xml');
  webResource.withHeader(HeaderConstants.ACCEPT_CHARSET_HEADER, 'UTF-8');

  if (options) {
    if (options.timeoutIntervalInMs) {
      webResource.withQueryOption(QueryStringConstants.TIMEOUT, options.timeoutIntervalInMs);
    }

    if (options.accessConditions) {
      webResource.withHeaders(options.accessConditions,
        HeaderConstants.IF_MATCH,
        HeaderConstants.IF_MODIFIED_SINCE,
        HeaderConstants.IF_NONE_MATCH,
        HeaderConstants.IF_UNMODIFIED_SINCE);
    }

    if (options.sourceAccessConditions) {
      webResource.withHeaders(options.sourceAccessConditions,
        HeaderConstants.SOURCE_IF_MATCH_HEADER,
        HeaderConstants.SOURCE_IF_MODIFIED_SINCE_HEADER,
        HeaderConstants.SOURCE_IF_NONE_MATCH_HEADER,
        HeaderConstants.SOURCE_IF_UNMODIFIED_SINCE_HEADER);
    }
  }

  StorageServiceClient['super_'].prototype._buildRequestOptions.call(this, webResource, body, options, callback);
};

/**
* Retrieves the normalized path to be used in a request.
* This takes into consideration the usePathStyleUri object field
* which specifies if the request is against the emulator or against
* the live service. It also adds a leading "/" to the path in case
* it's not there before.
*
* @param {string} path The path to be normalized.
* @return {string} The normalized path.
*/
StorageServiceClient.prototype._getPath = function (path) {
  if (path === null || path === undefined) {
    path = '/';
  } else if (path.indexOf('/') !== 0) {
    path = '/' + path;
  }

  if (this.usePathStyleUri) {
    path = '/' + this.storageAccount + path;
  }

  return path;
};

/**
* Sets the account credentials taking into consideration the isEmulated value and the environment variables:
* AZURE_STORAGE_ACCOUNT and AZURE_STORAGE_ACCESS_KEY.
*
* @param {string} storageAccount          The storage account.
* @param {string} storageAccessKey        The storage access key.
* @return {undefined}
*/
StorageServiceClient.prototype._setAccountCredentials = function (storageAccount, storageAccessKey) {
  if (azureutil.objectIsNull(storageAccount)) {
    if (process.env[ServiceClientConstants.EnvironmentVariables.AZURE_STORAGE_ACCOUNT]) {
      storageAccount = process.env[ServiceClientConstants.EnvironmentVariables.AZURE_STORAGE_ACCOUNT];
    } else if (ServiceClient.isEmulated()) {
      storageAccount = ServiceClientConstants.DEVSTORE_STORAGE_ACCOUNT;
    }
  }

  if (azureutil.objectIsNull(storageAccessKey)) {
    if (process.env[ServiceClientConstants.EnvironmentVariables.AZURE_STORAGE_ACCESS_KEY]) {
      storageAccessKey = process.env[ServiceClient.EnvironmentVariables.AZURE_STORAGE_ACCESS_KEY];
    } else if (ServiceClient.isEmulated()) {
      storageAccessKey = ServiceClient.DEVSTORE_STORAGE_ACCESS_KEY;
    }
  }

  if (!azureutil.objectIsString(storageAccount) || azureutil.stringIsEmpty(storageAccount)) {
    throw new Error(StorageServiceClient.incorrectStorageAccountErr);
  }

  if (!azureutil.objectIsString(storageAccessKey) || azureutil.stringIsEmpty(storageAccessKey)) {
    throw new Error(StorageServiceClient.incorrectStorageAccessKeyErr);
  }

  this.storageAccount = storageAccount;
  this.storageAccessKey = storageAccessKey;
};

module.exports = StorageServiceClient;
<|endoftext|>