text: ﻿// 
  // 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 request = require('request');
  var url = require('url');
  var util = require('util');
  var xml2js = require('xml2js');
  var events = require('events');
  var _ = require('underscore');
  var guid = require('node-uuid');
  var os = require('os');
  var crypto = require('crypto');
  var extend = require('extend');
  var Parser = require('json-edm-parser');
  
  var azureutil = require('../util/util');
  var validate = require('../util/validate');
  var SR = require('../util/sr');
  var WebResource = require('../http/webresource');
  
  var ServiceSettings = require('./servicesettings');
  var StorageServiceSettings = require('./storageservicesettings');
  var Constants = require('../util/constants');
  var StorageUtilities = require('../util/storageutilities');
  var ServicePropertiesResult = require('../models/servicepropertiesresult');
  var TableUtilities = require('../../services/table/tableutilities');
  
  var SharedKey = require('../signing/sharedkey');
  var SharedAccessSignature = require('../signing/sharedaccesssignature');
  
  var HeaderConstants = Constants.HeaderConstants;
  var QueryStringConstants = Constants.QueryStringConstants;
  var HttpResponseCodes = Constants.HttpConstants.HttpResponseCodes;
  var StorageServiceClientConstants = Constants.StorageServiceClientConstants;
  var defaultRequestLocationMode = Constants.RequestLocationMode.PRIMARY_ONLY;
  var RequestLocationMode = Constants.RequestLocationMode;
  
  var Logger = require('../diagnostics/logger');
  var errors = require('../errors/errors');
  var ArgumentError = errors.ArgumentError;
  var ArgumentNullError = errors.ArgumentNullError;
  var TimeoutError = errors.TimeoutError;
  var StorageError = errors.StorageError;
  
  /**
  * Creates a new StorageServiceClient object.
  *
  * @class
  * The StorageServiceClient class is the base class of all the service classes.
  * @constructor
  * @param {string} storageAccount           The storage account.
  * @param {string} storageAccessKey         The storage access key.
  * @param {object} host                     The host for the service.
  * @param {bool}   usePathStyleUri          Boolean value indicating wether to use path style uris.
  * @param {string} sasToken                 The Shared Access Signature token.
  */
  function StorageServiceClient(storageAccount, storageAccessKey, host, usePathStyleUri, sasToken) {
    StorageServiceClient['super_'].call(this);
  
    if(storageAccount && storageAccessKey) {
      // account and key
      this.storageAccount = storageAccount;
      this.storageAccessKey = storageAccessKey;
      this.storageCredentials = new SharedKey(this.storageAccount, this.storageAccessKey, usePathStyleUri);
    } else if (sasToken) {
      // sas
      this.sasToken = sasToken;
      this.storageCredentials = new SharedAccessSignature(sasToken);
    } else {
      // anonymous
      this.anonymous = true;
      this.storageCredentials = {
        signRequest: function(webResource, callback){
          // no op, anonymous access
          callback(null);
        }
      };
    }
  
    if(host){
      this.setHost(host);
    }
  
    this.apiVersion = HeaderConstants.TARGET_STORAGE_VERSION;
    this.usePathStyleUri = usePathStyleUri;
  
    this._initDefaultFilter();
  
    /**
     * The logger of the service. To change the log level of the services, set the `[logger.level]{@link Logger#level}`.
     * @name StorageServiceClient#logger
     * @type Logger
     * */
    this.logger = new Logger(Logger.LogLevels.INFO);
  
    this._setDefaultProxy();
  
    this.xml2jsSettings = StorageServiceClient._getDefaultXml2jsSettings();
    this.defaultLocationMode = StorageUtilities.LocationMode.PRIMARY_ONLY;
  }
  
  util.inherits(StorageServiceClient, events.EventEmitter);
  
  /**
  * Gets the default xml2js settings.
  * @ignore
  * @return {object} The default settings
  */
  StorageServiceClient._getDefaultXml2jsSettings = function() {
    var xml2jsSettings = _.clone(xml2js.defaults['0.2']);
  
    // these determine what happens if the xml contains attributes
    xml2jsSettings.attrkey = Constants.TableConstants.XML_METADATA_MARKER;
    xml2jsSettings.charkey = Constants.TableConstants.XML_VALUE_MARKER;
  
    // from xml2js guide: always put child nodes in an array if true; otherwise an array is created only if there is more than one.
    xml2jsSettings.explicitArray = false;
  
    return xml2jsSettings;
  };
  
  /**
  * Sets a host for the service.
  * @ignore
  * @param {string}     host                              The host for the service.
  */
  StorageServiceClient.prototype.setHost = function (host) {
    var parseHost = function(hostUri){
      var parsedHost;
      if(!azureutil.objectIsNull(hostUri)) {
        if(hostUri.indexOf('http') === -1 && hostUri.indexOf('//') !== 0){
          hostUri = '//' + hostUri;
        }
        parsedHost = url.parse(hostUri, false, true);
  
        if(!parsedHost.protocol){
          parsedHost.protocol = ServiceSettings.DEFAULT_PROTOCOL;
        }
  
        if (!parsedHost.port) {
          if (parsedHost.protocol === Constants.HTTPS) {
            parsedHost.port = Constants.DEFAULT_HTTPS_PORT;
          } else {
            parsedHost.port = Constants.DEFAULT_HTTP_PORT;
          }
        }
  
        parsedHost = url.format({
          protocol: parsedHost.protocol,
          port: parsedHost.port,
          hostname: parsedHost.hostname,
          pathname: parsedHost.pathname
        });
      }
  
      return parsedHost;
    };
  
    validate.isValidHost(host);
  
    this.host = {
      primaryHost: parseHost(host.primaryHost),
      secondaryHost: parseHost(host.secondaryHost)
    };
  };
  
  /**
  * Performs a REST service request through HTTP expecting an input stream.
  * @ignore
  *
  * @param {WebResource} webResource                      The webresource on which to perform the request.
  * @param {string}      outputData                       The outgoing request data as a raw string.
  * @param {object}      [options]                        The request options.
  * @param {int}         [options.timeoutIntervalInMs]    The timeout interval, in milliseconds, to use for the request.
  * @param {function}    callback                         The response callback function.
  */
  StorageServiceClient.prototype.performRequest = function (webResource, outputData, options, callback) {
    this._performRequest(webResource, { outputData: outputData }, options, callback);
  };
  
  /**
  * Performs a REST service request through HTTP expecting an input stream.
  * @ignore
  *
  * @param {WebResource} webResource                      The webresource on which to perform the request.
  * @param {Stream}      outputStream                     The outgoing request data as a stream.
  * @param {object}      [options]                        The request options.
  * @param {int}         [options.timeoutIntervalInMs]    The timeout interval, in milliseconds, to use for the request.
  * @param {function}    callback                         The response callback function.
  */
  StorageServiceClient.prototype.performRequestOutputStream = function (webResource, outputStream, options, callback) {
    this._performRequest(webResource, { outputStream: outputStream }, options, callback);
  };
  
  /**
  * Performs a REST service request through HTTP expecting an input stream.
  * @ignore
  *
  * @param {WebResource} webResource                      The webresource on which to perform the request.
  * @param {string}      outputData                       The outgoing request data as a raw string.
  * @param {Stream}      inputStream                      The ingoing response data as a stream.
  * @param {object}      [options]                        The request options.
  * @param {int}         [options.timeoutIntervalInMs]    The timeout interval, in milliseconds, to use for the request.
  * @param {function}    callback                         The response callback function.
  */
  StorageServiceClient.prototype.performRequestInputStream = function (webResource, outputData, inputStream, options, callback) {
    this._performRequest(webResource, { outputData: outputData, inputStream: inputStream }, options, callback);
  };
  
  /**
  * Performs a REST service request through HTTP.
  * @ignore
  *
  * @param {WebResource} webResource                      The webresource on which to perform the request.
  * @param {object}      body                             The request body.
  * @param {string}      [body.outputData]                The outgoing request data as a raw string.
  * @param {Stream}      [body.outputStream]              The outgoing request data as a stream.
  * @param {Stream}      [body.inputStream]               The ingoing response data as a stream.
  * @param {object}      [options]                        The request options.
  * @param {string}      [options.clientRequestId]        A string that represents the client request ID with a 1KB character limit.
  * @param {int}         [options.timeoutIntervalInMs]    The timeout interval, in milliseconds, to use for the request.
  * @param {function}    callback                         The response callback function.
  */
  StorageServiceClient.prototype._performRequest = function (webResource, body, options, callback) {
    var self = this;
  
    // Sets a requestId on the webResource
    if(!options.clientRequestId) {
      options.clientRequestId = guid.v1();
    }
    
    webResource.withHeader(HeaderConstants.CLIENT_REQUEST_ID, options.clientRequestId);
  
    // Sets the user-agent string
    var userAgentComment = util.format('(NODE-VERSION %s; %s %s)', process.version, os.type(), os.release());
    webResource.withHeader(HeaderConstants.USER_AGENT, Constants.USER_AGENT_PRODUCT_NAME + '/' + Constants.USER_AGENT_PRODUCT_VERSION + ' ' + userAgentComment);
  
    // Initialize the location that the request is going to be sent to.
    if(azureutil.objectIsNull(options.locationMode)) {
      options.locationMode = this.defaultLocationMode;
    }
  
    // Initialize the location that the request can be sent to.
    if(azureutil.objectIsNull(options.requestLocationMode)) {
      options.requestLocationMode = defaultRequestLocationMode;
    }
  
    // Initialize whether nagling is used or not.
    if(azureutil.objectIsNull(options.useNagleAlgorithm)) {
      options.useNagleAlgorithm = this.useNagleAlgorithm;
    }
  
    this._initializeLocation(options);
    
    // Initialize the operationExpiryTime
    this._setOperationExpiryTime(options);
  
    var operation = function (options, next) {
      self._validateLocation(options);
      var currentLocation = options.currentLocation;
      self._buildRequestOptions(webResource, body, options, function (err, finalRequestOptions) {
        if (err) {
          callback({ error: err, response: null }, function (finalRequestOptions, finalCallback) {
            finalCallback(finalRequestOptions);
          });
        } else {
          self.logger.log(Logger.LogLevels.DEBUG, 'FINAL REQUEST OPTIONS:\n' + util.inspect(finalRequestOptions));
  
          if(self._maximumExecutionTimeExceeded(Date.now(), options.operationExpiryTime)) {
            callback({ error: new TimeoutError(SR.MAXIMUM_EXECUTION_TIMEOUT_EXCEPTION), response: null }, function (finalRequestOptions, finalCallback) {
              finalCallback(finalRequestOptions);
            });
          } else {
            var processResponseCallback = function (error, response) {
              var responseObject;
  
              if (error) {
                responseObject = { error: error, response: null };
              } else {
                responseObject = self._processResponse(webResource, response, options);
                responseObject.contentMD5 = response.contentMD5;
                responseObject.length = response.length;
              }
  
              responseObject.operationEndTime = new Date();
              // Required for listing operations to make sure successive operations go to the same location.
              responseObject.targetLocation = currentLocation;
              
              callback(responseObject, next);
            };
  
            var endResponse;
            var buildRequest = function (headersOnly, inputStream) {
              // Build request (if body was set before, request will process immediately, if not it'll wait for the piping to happen
              var requestStream;
  
              var requestWithDefaults;
              
              if(self.proxy) {
                if(requestWithDefaults === undefined) {
                  requestWithDefaults = request.defaults({'proxy':self.proxy});
                }
              } else {
                requestWithDefaults = request;
              }
  
              if (headersOnly) {
                requestStream = requestWithDefaults(finalRequestOptions);
  
                requestStream.on('error', processResponseCallback);
                requestStream.on('response', function (response) {
                  var isValid = WebResource.validResponse(response.statusCode);
                  if (!isValid) {
                      // When getting invalid response, try to get the error message for future steps to extract the detailed error information
                      var contentLength = parseInt(response.headers['content-length']);
                      var errorMessageBuffer;
                      var index = 0;
                      if (contentLength !== undefined) {
                        errorMessageBuffer = new Buffer(contentLength);
                      }
                      
                      requestStream.on('data', function (data) {
                        if (contentLength !== undefined) {
                          data.copy(errorMessageBuffer, index);
                          index += data.length;
                        } else {
                          if (!errorMessageBuffer) {
                            errorMessageBuffer = data;
                          } else {
                            errorMessageBuffer = Buffer.concat([errorMessageBuffer, data]);
                          }
                        }
                      });
                      requestStream.on('end', function () {
                        if (errorMessageBuffer) {
                          // Strip the UTF8 BOM following the same ways as 'request' module
                          if (errorMessageBuffer.length > 3 &&
                            errorMessageBuffer[0] === 239 &&
                            errorMessageBuffer[1] === 187 &&
                            errorMessageBuffer[2] === 191) {
                            response.body = errorMessageBuffer.toString('utf8', 3);
                          } else {
                            response.body = errorMessageBuffer.toString('utf8');
                          }
                        }
                        processResponseCallback(null, response);
                      });
                  } else {
                    // Only pipe to the destination stream when we get a valid response from service
                    // Error message should NOT be piped to the destination stream
                    if (inputStream) {
                      requestStream.pipe(inputStream);
                    }
  
                    var responseLength = 0;
                    var internalHash = crypto.createHash('md5');
                    requestStream.on('data', function(data) {
                      responseLength += data.length;
                      internalHash.update(data);
                    });
                                    
                    response.on('end', function () {
                      // Calculate and set MD5 here
                      if(azureutil.objectIsNull(options.disableContentMD5Validation) || options.disableContentMD5Validation === false) {
                        response.contentMD5 = internalHash.digest('base64');
                      }
                        
                      response.length = responseLength;
                      endResponse = response;
                    });
                  }
                });
              } else {
                requestStream = requestWithDefaults(finalRequestOptions, processResponseCallback);
              }
  
              //If useNagleAlgorithm is not set or the value is set and is false, setNoDelay is set to true.
              if (azureutil.objectIsNull(options.useNagleAlgorithm) || options.useNagleAlgorithm === false) {
                requestStream.on('request', function(httpRequest) {
                  httpRequest.setNoDelay(true);
                });
              }
  
              // Workaround to avoid request from potentially setting unwanted (rejected) headers by the service
              var oldEnd = requestStream.end;
              requestStream.end = function () {
                if (finalRequestOptions.headers['content-length']) {
                  requestStream.headers['content-length'] = finalRequestOptions.headers['content-length'];
                } else if (requestStream.headers['content-length']) {
                  delete requestStream.headers['content-length'];
                }
  
                oldEnd.call(requestStream);
              };
  
              // Bubble events up -- This is when the request is going to be made.
              requestStream.on('response', function (response) {
                self.emit('receivedResponseEvent', response);
              });
  
              return requestStream;
            };
  
            if (body && body.outputData) {
              finalRequestOptions.body = body.outputData;
            }
  
            // Pipe any input / output streams
            if (body && body.inputStream) {
              body.inputStream.on('close', function () {
                if (endResponse) {
                  processResponseCallback(null, endResponse);
                  endResponse = null;
                }
              });
              body.inputStream.on('end', function () {
                if (endResponse) {
                  processResponseCallback(null, endResponse);
                  endResponse = null;
                }
              });
              body.inputStream.on('finish', function () {
                if (endResponse) {
                  processResponseCallback(null, endResponse);
                  endResponse = null;
                }
              });
              buildRequest(true, body.inputStream);
            } else if (body && body.outputStream) {
              var sendUnchunked = function () {
                var size = finalRequestOptions.headers['content-length'] ?
                  finalRequestOptions.headers['content-length'] :
                  Constants.BlobConstants.MAX_SINGLE_UPLOAD_BLOB_SIZE_IN_BYTES;
  
                var concatBuf = new Buffer(size);
                var index = 0;
  
                body.outputStream.on('data', function (d) {
                  if(self._maximumExecutionTimeExceeded(Date.now(), options.operationExpiryTime)) {
                    processResponseCallback(new TimeoutError(SR.MAXIMUM_EXECUTION_TIMEOUT_EXCEPTION));
                  } else {
                    d.copy(concatBuf, index);
                    index += d.length;
                  }
                }).on('end', function () {
                  var requestStream = buildRequest();
                  requestStream.write(concatBuf);
                  requestStream.end();
                });
                if (azureutil.isStreamPaused(body.outputStream)) {
                  body.outputStream.resume();
                }
              };
  
              var sendStream = function () {
                // NOTE: workaround for an unexpected EPIPE exception when piping streams larger than 29 MB
                if (!azureutil.objectIsNull(finalRequestOptions.headers['content-length']) && finalRequestOptions.headers['content-length'] < 29 * 1024 * 1024) {
                  body.outputStream.pipe(buildRequest());
  
                  if (azureutil.isStreamPaused(body.outputStream)) {
                    body.outputStream.resume();
                  }
                } else {
                  sendUnchunked();
                }
              };
  
              if (!body.outputStream.readable) {
                // if the content length is zero, build the request and don't send a body
                if (finalRequestOptions.headers['content-length'] === 0) {
                  buildRequest();
                } else {
                  // otherwise, wait until we know the readable stream is actually valid before piping
                  body.outputStream.on('open', function () {
                    sendStream();
                  });
                }
              } else {
                sendStream();
              }
  
              // This catches any errors that happen while creating the readable stream (usually invalid names)
              body.outputStream.on('error', function (error) {
                processResponseCallback(error);
              });
            } else {
              buildRequest();
            }
          }
        }
      });
    };
  
    // The filter will do what it needs to the requestOptions and will provide a
    // function to be handled after the reply
    self.filter(options, function (postFiltersRequestOptions, nextPostCallback) {
      if(self._maximumExecutionTimeExceeded(Date.now() + postFiltersRequestOptions.retryInterval, postFiltersRequestOptions.operationExpiryTime)) {
        callback({ error: new TimeoutError(SR.MAXIMUM_EXECUTION_TIMEOUT_EXCEPTION), response: null}, function (postFiltersRequestOptions, finalCallback) {
          finalCallback(postFiltersRequestOptions);
        });
      } else {
        // If there is a filter, flow is:
        // filter -> operation -> process response
        if(postFiltersRequestOptions.retryContext) {
          var func = function() {
            operation(postFiltersRequestOptions, nextPostCallback);
          };
  
          // Sleep for retryInterval before making the request
          setTimeout(func, postFiltersRequestOptions.retryInterval);
        } else {
          // No retry policy filter specified
          operation(postFiltersRequestOptions, nextPostCallback);
        }
      }
    });
  };
  
  
  /**
  * Builds the request options to be passed to the http.request method.
  * @ignore
  * @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.
  */
  StorageServiceClient.prototype._buildRequestOptions = function (webResource, body, options, callback) {
    webResource.withHeader(HeaderConstants.STORAGE_VERSION, this.apiVersion);
    webResource.withHeader(HeaderConstants.MS_DATE, new Date().toUTCString());
    if (!webResource.headers[HeaderConstants.ACCEPT]) {
      webResource.withHeader(HeaderConstants.ACCEPT, 'application/atom+xml,application/xml');
    }
    webResource.withHeader(HeaderConstants.ACCEPT_CHARSET, 'UTF-8');
  
    if(azureutil.objectIsNull(options.timeoutIntervalInMs)) {
      options.timeoutIntervalInMs = this.defaultTimeoutIntervalInMs;
    }
  
    if(!azureutil.objectIsNull(options.timeoutIntervalInMs) && options.timeoutIntervalInMs > 0) {
      webResource.withQueryOption(QueryStringConstants.TIMEOUT, Math.ceil(options.timeoutIntervalInMs / 1000));
    }
    
    if(options.accessConditions) {
      webResource.withHeader(HeaderConstants.IF_MATCH, options.accessConditions.EtagMatch);
      webResource.withHeader(HeaderConstants.IF_MODIFIED_SINCE, options.accessConditions.DateModifedSince);
      webResource.withHeader(HeaderConstants.IF_NONE_MATCH, options.accessConditions.EtagNonMatch);
      webResource.withHeader(HeaderConstants.IF_UNMODIFIED_SINCE, options.accessConditions.DateUnModifiedSince);
      webResource.withHeader(HeaderConstants.SEQUENCE_NUMBER_EQUAL, options.accessConditions.SequenceNumberEqual);
      webResource.withHeader(HeaderConstants.SEQUENCE_NUMBER_LESS_THAN, options.accessConditions.SequenceNumberLessThan);
      webResource.withHeader(HeaderConstants.SEQUENCE_NUMBER_LESS_THAN_OR_EQUAL, options.accessConditions.SequenceNumberLessThanOrEqual);
      webResource.withHeader(HeaderConstants.BLOB_CONDITION_MAX_SIZE, options.accessConditions.MaxBlobSize);
      webResource.withHeader(HeaderConstants.BLOB_CONDITION_APPEND_POSITION, options.accessConditions.MaxAppendPosition);
    }
  
    if(options.sourceAccessConditions) {
      webResource.withHeader(HeaderConstants.SOURCE_IF_MATCH, options.sourceAccessConditions.EtagMatch);
      webResource.withHeader(HeaderConstants.SOURCE_IF_MODIFIED_SINCE, options.sourceAccessConditions.DateModifedSince);
      webResource.withHeader(HeaderConstants.SOURCE_IF_NONE_MATCH, options.sourceAccessConditions.EtagNonMatch);
      webResource.withHeader(HeaderConstants.SOURCE_IF_UNMODIFIED_SINCE, options.sourceAccessConditions.DateUnModifiedSince);
    }
    
    if (!webResource.headers || webResource.headers[HeaderConstants.CONTENT_TYPE] === undefined) {
      // work around to add an empty content type header to prevent the request module from magically adding a content type.
      webResource.headers[HeaderConstants.CONTENT_TYPE] = '';
    } else if (webResource.headers && webResource.headers[HeaderConstants.CONTENT_TYPE] === null) {
      delete webResource.headers[HeaderConstants.CONTENT_TYPE];
    }
  
    if (!webResource.headers || webResource.headers[HeaderConstants.CONTENT_LENGTH] === undefined) {
      if (body && body.outputData) {
        webResource.withHeader(HeaderConstants.CONTENT_LENGTH, Buffer.byteLength(body.outputData, 'UTF8'));
      } else if (webResource.headers[HeaderConstants.CONTENT_LENGTH] === undefined) {
        webResource.withHeader(HeaderConstants.CONTENT_LENGTH, 0);
      }
    } else if (webResource.headers && webResource.headers[HeaderConstants.CONTENT_LENGTH] === null) {
      delete webResource.headers[HeaderConstants.CONTENT_LENGTH];
    }
  
    // Sets the request url in the web resource.
    this._setRequestUrl(webResource, options);
  
    this.emit('sendingRequestEvent', webResource);
  
    // Now that the web request is finalized, sign it
    this.storageCredentials.signRequest(webResource, function (error) {
      var requestOptions = null;
  
      if (!error) {
        var targetUrl = webResource.uri;
  
        requestOptions = {
          url: url.format(targetUrl),
          method: webResource.method,
          headers: webResource.headers,
        };
  
        if(options) {
          //set encoding of response data. If set to null, the body is returned as a Buffer
          requestOptions.encoding = options.responseEncoding;
        }
      }
  
      callback(error, requestOptions);
    });
  };
  
  /**
  * Process the response.
  * @ignore
  *
  * @param {WebResource} webResource                The web resource that made the request.
  * @param {Response}    response                   The response object.
  * @param {Options}     options                    The response parsing options.
  * @param {String}      options.payloadFormat      The payload format.
  * @return The normalized responseObject.
  */
  StorageServiceClient.prototype._processResponse = function (webResource, response, options) {
    var self = this;
  
    function convertRawHeadersToHeaders(rawHeaders) {
      var headers = {};
      if(!rawHeaders) {
        return undefined;
      }
  
      for(var i = 0; i < rawHeaders.length; i++) {
        var headerName;
        if (rawHeaders[i].indexOf(HeaderConstants.PREFIX_FOR_STORAGE_METADATA) === 0) {
          headerName = rawHeaders[i];
        } else {
          headerName = rawHeaders[i].toLowerCase();
        }
        headers[headerName] = rawHeaders[++i];
      }
  
      return headers;
    }
  
    var validResponse = WebResource.validResponse(response.statusCode);
    var rsp = StorageServiceClient._buildResponse(validResponse, response.body, convertRawHeadersToHeaders(response.rawHeaders) || response.headers, response.statusCode, response.md5);
    var responseObject;
  
    if (validResponse && webResource.rawResponse) {
      responseObject = { error: null, response: rsp };
    } else {
      // attempt to parse the response body, errors will be returned in rsp.error without modifying the body
      rsp = StorageServiceClient._parseResponse(rsp, self.xml2jsSettings, options);
  
      if (validResponse && !rsp.error) {
          responseObject = { error: null, response: rsp };
      } else {
        rsp.isSuccessful = false;
  
        if (response.statusCode < 400 || response.statusCode >= 500) {
          this.logger.log(Logger.LogLevels.DEBUG,
              'ERROR code = ' + response.statusCode + ' :\n' + util.inspect(rsp.body));
        }
  
        // responseObject.error should contain normalized parser errors if they occured in _parseResponse
        // responseObject.response.body should contain the raw response body in that case
        var errorBody = rsp.body;
        if(rsp.error) {
          errorBody = rsp.error;
          delete rsp.error;
        }
  
        if (!errorBody) {
          var code = Object.keys(HttpResponseCodes).filter(function (name) {
            if (HttpResponseCodes[name] === rsp.statusCode) {
              return name;
            }
          });
  
          errorBody = { error: { code: code[0] } };
        }
  
        var normalizedError = StorageServiceClient._normalizeError(errorBody, response);
        responseObject = { error: normalizedError, response: rsp };
      }
    }
  
    this.logger.log(Logger.LogLevels.DEBUG, 'RESPONSE:\n' + util.inspect(responseObject));
  
    return responseObject;
  };
  
  /**
  * Associate a filtering operation with this StorageServiceClient. Filtering operations
  * can include logging, automatically retrying, etc. Filter operations are objects
  * that implement a method with the signature:
  *
  *     "function handle (requestOptions, next)".
  *
  * After doing its preprocessing on the request options, the method needs to call
  * "next" passing a callback with the following signature:
  * signature:
  *
  *     "function (returnObject, finalCallback, next)"
  *
  * In this callback, and after processing the returnObject (the response from the
  * request to the server), the callback needs to either invoke next if it exists to
  * continue processing other filters or simply invoke finalCallback otherwise to end
  * up the service invocation.
  *
  * @param {Object} filter The new filter object.
  * @return {StorageServiceClient} A new service client with the filter applied.
  */
  StorageServiceClient.prototype.withFilter = function (newFilter) {
      // Create a new object with the same members as the current service
      var derived = _.clone(this);
  
      // If the current service has a filter, merge it with the new filter
      // (allowing us to effectively pipeline a series of filters)
      var parentFilter = this.filter;
      var mergedFilter = newFilter;
      if (parentFilter !== undefined) {
        // The parentFilterNext is either the operation or the nextPipe function generated on a previous merge
        // Ordering is [f3 pre] -> [f2 pre] -> [f1 pre] -> operation -> [f1 post] -> [f2 post] -> [f3 post]
        mergedFilter = function (originalRequestOptions, parentFilterNext) {
          newFilter.handle(originalRequestOptions, function (postRequestOptions, newFilterCallback) {
            // handle parent filter pre and get Parent filter post
            var next = function (postPostRequestOptions, parentFilterCallback) {
              // The parentFilterNext is the filter next to the merged filter.
              // For 2 filters, that'd be the actual operation.
              parentFilterNext(postPostRequestOptions, function (responseObject, responseCallback, finalCallback) {
                parentFilterCallback(responseObject, finalCallback, function (postResponseObject) {
                  newFilterCallback(postResponseObject, responseCallback, finalCallback);
                });
              });
            };
  
            parentFilter(postRequestOptions, next);
          });
        };
      }
  
      // Store the filter so it can be applied in performRequest
      derived.filter = mergedFilter;
      return derived;
    };
  
  /*
  * Builds a response object with normalized key names.
  * @ignore
  *
  * @param {Bool}     isSuccessful    Boolean value indicating if the request was successful
  * @param {Object}   body            The response body.
  * @param {Object}   headers         The response headers.
  * @param {int}      statusCode      The response status code.
  * @param {string}   md5             The response's content md5 hash.
  * @return {Object} A response object.
  */
  StorageServiceClient._buildResponse = function (isSuccessful, body, headers, statusCode, md5) {
    return {
      isSuccessful: isSuccessful,
      statusCode: statusCode,
      body: body,
      headers: headers,
      md5: md5
    };
  };
  
  /**
  * Parses a server response body from XML or JSON into a JS object.
  * This is done using the xml2js library.
  * @ignore
  *
  * @param {object}     response                   The response object with a property "body" with a XML or JSON string content.
  * @param {object}     xml2jsSettings             The XML to json settings.
  * @param {Options}    options                    The response parsing options.
  * @param {String}     options.payloadFormat      The payload format.
  * @return {object} The same response object with the body part as a JS object instead of a XML or JSON string.
  */
  StorageServiceClient._parseResponse = function (response, xml2jsSettings, options) {
    function parseXml(body) {
      var parsed;
      var parser = new xml2js.Parser(xml2jsSettings);
      parser.parseString(azureutil.removeBOM(body.toString()), function (err, parsedBody) {
        if (err) {
          var xmlError = new SyntaxError('EXMLFORMAT');
          xmlError.innerError = err;
          throw xmlError;
        } else { parsed = parsedBody; }
      });
  
      return parsed;
    }
  
    if (response.body && Buffer.byteLength(response.body.toString()) > 0) {
      var contentType = '';
      if (response.headers && response.headers['content-type']) {
        contentType = response.headers['content-type'].toLowerCase();
      }
  
      try {
        if (contentType.indexOf('application/json') !== -1) {
          if (options && options.payloadFormat && options.payloadFormat !== TableUtilities.PayloadFormat.NO_METADATA) {
            var parser = new Parser();
            parser.onValue = function (value) {
              response.body = value;
            };
            parser.write(response.body);
          } else {
            response.body = JSON.parse(response.body);
          }
        } else if (contentType.indexOf('application/xml') !== -1 || contentType.indexOf('application/atom+xml') !== -1) {
          response.body = parseXml(response.body);
        } else {
          throw new SyntaxError(SR.CONTENT_TYPE_MISSING, null);
        }
      } catch (e) {
        response.error = e;
      }
    }
  
    return response;
  };
  
  /**
  * 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.
  * @param {object} [sasToken]                          The sas token.
  *
  * @return {StorageServiceSettings}
  */
  StorageServiceClient.getStorageSettings = function (storageAccountOrConnectionString, storageAccessKey, host, sasToken, endpointSuffix) {
    var storageServiceSettings;
    if (storageAccountOrConnectionString && !storageAccessKey && !sasToken) {
      // If storageAccountOrConnectionString was passed and no accessKey was passed, assume connection string
      storageServiceSettings = StorageServiceSettings.createFromConnectionString(storageAccountOrConnectionString);
    } else if ((storageAccountOrConnectionString && storageAccessKey) || sasToken || host) {
      // Account and key or credentials or anonymous
      storageServiceSettings = StorageServiceSettings.createExplicitly(storageAccountOrConnectionString, storageAccessKey, host, sasToken, endpointSuffix);
    } else {
      // Use environment variables
      storageServiceSettings = StorageServiceSettings.createFromEnvironment();
    }
  
    return storageServiceSettings;
  };
  
  /**
  * Sets the webResource's requestUrl based on the service client settings.
  * @ignore
  *
  * @param {WebResource} webResource The web resource where to set the request url.
  */
  StorageServiceClient.prototype._setRequestUrl = function (webResource, options) {
    // Normalize the path
    webResource.path = this._getPath(webResource.path);
  
    if(!this.host){
      throw new ArgumentNullError('this.host', SR.STORAGE_HOST_LOCATION_REQUIRED);
    }
  
    var host = this.host.primaryHost;
  
    if(!azureutil.objectIsNull(options) && options.currentLocation === Constants.StorageLocation.SECONDARY) {
      host = this.host.secondaryHost;
    }
  
    if(host && host.lastIndexOf('/') !== (host.length - 1)){
      host = host + '/';
    }
    
    var fullPath = url.format({pathname: webResource.path, query: webResource.queryString});
    webResource.uri = url.resolve(host, fullPath);
    webResource.path = url.parse(webResource.uri).pathname;
  };
  
  /**
  * Retrieves the normalized path to be used in a request.
  * It also removes any leading "/" of the path in case
  * it's there before.
  * @ignore
  * @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.substring(1);
    }
  
    return path;
  };
  
  /**
  * Initializes the default filter.
  * This filter is responsible for chaining the pre filters request into the operation and, after processing the response,
  * pass it to the post processing filters. This method should only be invoked by the StorageServiceClient constructor.
  * @ignore
  *
  */
  StorageServiceClient.prototype._initDefaultFilter = function () {
    this.filter = function (requestOptions, nextPreCallback) {
      if (nextPreCallback) {
        // Handle the next pre callback and pass the function to be handled as post call back.
        nextPreCallback(requestOptions, function (returnObject, finalCallback, nextPostCallback) {
          if (nextPostCallback) {
            nextPostCallback(returnObject);
          } else if (finalCallback) {
            finalCallback(returnObject);
          }
        });
      }
    };
  };
  
  /**
  * Retrieves the metadata headers from the response headers.
  * @ignore
  *
  * @param {object} headers The metadata headers.
  * @return {object} An object with the metadata headers (without the "x-ms-" prefix).
  */
  StorageServiceClient.prototype.parseMetadataHeaders = function (headers) {
    var metadata = {};
  
    if (!headers) {
      return metadata;
    }
  
    for (var header in headers) {
      if (header.indexOf(HeaderConstants.PREFIX_FOR_STORAGE_METADATA) === 0) {
        var key = header.substr(HeaderConstants.PREFIX_FOR_STORAGE_METADATA.length, header.length - HeaderConstants.PREFIX_FOR_STORAGE_METADATA.length);
        metadata[key] = headers[header];
      }
    }
    
    return metadata;
  };
  
  /**
  * Gets the properties of a storage account’s service, including Azure Storage Analytics.
  * @ignore
  *
  * @this {StorageServiceClient}
  * @param {object}       [options]                               The request options.
  * @param {LocationMode} [options.locationMode]                  Specifies the location mode used to decide which location the request should be sent to. 
  *                                                               Please see StorageUtilities.LocationMode for the possible values.
  * @param {int}          [options.timeoutIntervalInMs]           The server timeout interval, in milliseconds, to use for the request.
  * @param {int}          [options.maximumExecutionTimeInMs]      The maximum execution time, in milliseconds, across all potential retries, to use when making this request.
  *                                                               The maximum execution time interval begins at the time that the client begins building the request. The maximum
  *                                                               execution time is checked intermittently while performing requests, and before executing retries.
  * @param {bool}         [options.useNagleAlgorithm]             Determines whether the Nagle algorithm is used; true to use the Nagle algorithm; otherwise, false.
  *                                                               The default value is false.
  * @param {errorOrResult}  callback                              `error` will contain information if an error occurs; otherwise, `result` will contain the properties 
  *                                                               and `response` will contain information related to this operation.
  */
  StorageServiceClient.prototype.getAccountServiceProperties = function (optionsOrCallback, callback) {
    var userOptions;
    azureutil.normalizeArgs(optionsOrCallback, callback, function (o, c) { userOptions = o; callback = c; });
    
    validate.validateArgs('getServiceProperties', function (v) {
      v.callback(callback);
    });
    
    var options = extend(true, {}, userOptions);
    
    var webResource = WebResource.get()
      .withQueryOption(QueryStringConstants.COMP, 'properties')
      .withQueryOption(QueryStringConstants.RESTYPE, 'service');
    
    options.requestLocationMode = RequestLocationMode.PRIMARY_OR_SECONDARY;
    
    var processResponseCallback = function (responseObject, next) {
      responseObject.servicePropertiesResult = null;
      if (!responseObject.error) {
        responseObject.servicePropertiesResult = ServicePropertiesResult.parse(responseObject.response.body.StorageServiceProperties);
      }
      
      // function to be called after all filters
      var finalCallback = function (returnObject) {
        callback(returnObject.error, returnObject.servicePropertiesResult, returnObject.response);
      };
      
      // call the first filter
      next(responseObject, finalCallback);
    };
    
    this.performRequest(webResource, null, options, processResponseCallback);
  };
  
  /**
  * Sets the properties of a storage account’s service, including Azure Storage Analytics.
  * You can also use this operation to set the default request version for all incoming requests that do not have a version specified.
  *
  * @this {StorageServiceClient}
  * @param {object}             serviceProperties                        The service properties.
  * @param {object}             [options]                                The request options.
  * @param {LocationMode}       [options.locationMode]                   Specifies the location mode used to decide which location the request should be sent to. 
  *                                                                      Please see StorageUtilities.LocationMode for the possible values.
  * @param {int}                [options.timeoutIntervalInMs]            The server timeout interval, in milliseconds, to use for the request.
  * @param {int}                [options.maximumExecutionTimeInMs]       The maximum execution time, in milliseconds, across all potential retries, to use when making this request.
  *                                                                      The maximum execution time interval begins at the time that the client begins building the request. The maximum
  *                                                                      execution time is checked intermittently while performing requests, and before executing retries.
  * @param {bool}               [options.useNagleAlgorithm]              Determines whether the Nagle algorithm is used; true to use the Nagle algorithm; otherwise, false.
  *                                                                      The default value is false.
  * @param {errorOrResponse}    callback                                 `error` will contain information
  *                                                                      if an error occurs; otherwise, `response`
  *                                                                      will contain information related to this operation.
  */
  StorageServiceClient.prototype.setAccountServiceProperties = function (serviceProperties, optionsOrCallback, callback) {
    var userOptions;
    azureutil.normalizeArgs(optionsOrCallback, callback, function (o, c) { userOptions = o; callback = c; });
    
    validate.validateArgs('setServiceProperties', function (v) {
      v.object(serviceProperties, 'serviceProperties');
      v.callback(callback);
    });
    
    var options = extend(true, {}, userOptions);
    var servicePropertiesXml = ServicePropertiesResult.serialize(serviceProperties);
    
    var webResource = WebResource.put()
      .withQueryOption(QueryStringConstants.COMP, 'properties')
      .withQueryOption(QueryStringConstants.RESTYPE, 'service')
      .withHeader(HeaderConstants.CONTENT_TYPE, 'application/xml;charset="utf-8"')
      .withHeader(HeaderConstants.CONTENT_LENGTH, Buffer.byteLength(servicePropertiesXml))
      .withBody(servicePropertiesXml);
    
    var processResponseCallback = function (responseObject, next) {
      var finalCallback = function (returnObject) {
        callback(returnObject.error, returnObject.response);
      };
      
      next(responseObject, finalCallback);
    };
    
    this.performRequest(webResource, webResource.body, options, processResponseCallback);
  };
  
  // Other functions
  
  /**
  * Processes the error body into a normalized error object with all the properties lowercased.
  *
  * Error information may be returned by a service call with additional debugging information:
  * http://msdn.microsoft.com/en-us/library/windowsazure/dd179382.aspx
  *
  * Table services returns these properties lowercased, example, "code" instead of "Code". So that the user
  * can always expect the same format, this method lower cases everything.
  *
  * @ignore
  *
  * @param {Object} error The error object as returned by the service and parsed to JSON by the xml2json.
  * @return {Object} The normalized error object with all properties lower cased.
  */
  StorageServiceClient._normalizeError = function (error, response) {
    if (azureutil.objectIsString(error)) {
      return new StorageError(error, null);
    } else if (error) {
      var normalizedError = {};
  
      // blob/queue errors should have error.Error, table errors should have error['odata.error']
      var errorProperties = error.Error || error.error || error['odata.error'] || error['m:error'] || error;
      normalizedError.code = errorProperties.message; // The message exists when there is error.Error.
      
      for (var property in errorProperties) {
        if (errorProperties.hasOwnProperty(property)) {
          var key = property.toLowerCase();
          if(key.indexOf('m:') === 0) {
            key = key.substring(2);
          }
          
          normalizedError[key] = errorProperties[property];
  
          // if this is a table error, message is an object - flatten it to normalize with blob/queue errors
          // ex: "message":{"lang":"en-US","value":"The specified resource does not exist."} becomes message: "The specified resource does not exist."
          if (key === 'message' && _.isObject(errorProperties[property])) {
            if (errorProperties[property]['value']) {
              normalizedError[key] = errorProperties[property]['value'];
            }
          }
        }
      }
  
      // add status code and server request id if available
      if (response) {
        if (response.statusCode) {
          normalizedError.statusCode = response.statusCode;
        }
  
        if (response.headers && response.headers['x-ms-request-id']) {
          normalizedError.requestId = response.headers['x-ms-request-id'];
        }
      }
  
      var errorObject = new StorageError(normalizedError.code, normalizedError);
      return errorObject;
    }
  
    return null;
  };
  
  /*
  * Sets proxy object specified by caller.
  *
  * @param {object}   proxy       proxy to use for tunneling
  *                               {
  *                                host: hostname
  *                                port: port number
  *                                proxyAuth: 'user:password' for basic auth
  *                                headers: {...} headers for proxy server
  *                                key: key for proxy server
  *                                ca: ca for proxy server
  *                                cert: cert for proxy server
  *                               }
  *                               if null or undefined, clears proxy
  */
  StorageServiceClient.prototype.setProxy = function (proxy) {
    if (proxy) {
      this.proxy = proxy;
    } else {
      this.proxy = null;
    }
  };
  
  /**
  * Sets the service host default proxy from the environment.
  * Can be overridden by calling _setProxyUrl or _setProxy
  *
  */
  StorageServiceClient.prototype._setDefaultProxy = function () {
    var proxyUrl = StorageServiceClient._loadEnvironmentProxyValue();
    if (proxyUrl) {
      var parsedUrl = url.parse(proxyUrl);
      if (!parsedUrl.port) {
        parsedUrl.port = 80;
      }
      this.setProxy(parsedUrl);
    } else {
      this.setProxy(null);
    }
  };
  
  /*
  * Loads the fields "useProxy" and respective protocol, port and url
  * from the environment values HTTPS_PROXY and HTTP_PROXY
  * in case those are set.
  * @ignore
  *
  * @return {string} or null
  */
  StorageServiceClient._loadEnvironmentProxyValue = function () {
    var proxyUrl = null;
    if (process.env[StorageServiceClientConstants.EnvironmentVariables.HTTPS_PROXY]) {
      proxyUrl = process.env[StorageServiceClientConstants.EnvironmentVariables.HTTPS_PROXY];
    } else if (process.env[StorageServiceClientConstants.EnvironmentVariables.HTTPS_PROXY.toLowerCase()]) {
      proxyUrl = process.env[StorageServiceClientConstants.EnvironmentVariables.HTTPS_PROXY.toLowerCase()];
    } else if (process.env[StorageServiceClientConstants.EnvironmentVariables.HTTP_PROXY]) {
      proxyUrl = process.env[StorageServiceClientConstants.EnvironmentVariables.HTTP_PROXY];
    } else if (process.env[StorageServiceClientConstants.EnvironmentVariables.HTTP_PROXY.toLowerCase()]) {
      proxyUrl = process.env[StorageServiceClientConstants.EnvironmentVariables.HTTP_PROXY.toLowerCase()];
    }
  
    return proxyUrl;
  };
  
  /**
  * Initializes the location to which the operation is being sent to.
  */
  StorageServiceClient.prototype._initializeLocation = function (options) {
    if(!azureutil.objectIsNull(options.locationMode)) {
      switch(options.locationMode) {
      case StorageUtilities.LocationMode.PRIMARY_ONLY:
      case StorageUtilities.LocationMode.PRIMARY_THEN_SECONDARY:
        options.currentLocation = Constants.StorageLocation.PRIMARY;
        break;
      case StorageUtilities.LocationMode.SECONDARY_ONLY:
      case StorageUtilities.LocationMode.SECONDARY_THEN_PRIMARY:
        options.currentLocation = Constants.StorageLocation.SECONDARY;
        break;
      default:
        throw new RangeError(util.format(SR.ARGUMENT_OUT_OF_RANGE_ERROR, 'locationMode', options.locationMode));
      }
    } else {
      options.locationMode = StorageUtilities.LocationMode.PRIMARY_ONLY;
      options.currentLocation = Constants.StorageLocation.PRIMARY;
    }
  };
  
  /**
  * Validates the location to which the operation is being sent to.
  */
  StorageServiceClient.prototype._validateLocation = function (options) {
    if(this._invalidLocationMode(options.locationMode)) {
      throw new ArgumentNullError('host', SR.STORAGE_HOST_MISSING_LOCATION);
    }
  
    switch(options.requestLocationMode) {
    case Constants.RequestLocationMode.PRIMARY_ONLY:
      if(options.locationMode === StorageUtilities.LocationMode.SECONDARY_ONLY) {
        throw new ArgumentError('host.primaryHost', SR.PRIMARY_ONLY_COMMAND);
      }
  
      options.currentLocation = Constants.StorageLocation.PRIMARY;
      options.locationMode = StorageUtilities.LocationMode.PRIMARY_ONLY;
      break;
  
    case Constants.RequestLocationMode.SECONDARY_ONLY:
      if(options.locationMode === StorageUtilities.LocationMode.PRIMARY_ONLY) {
        throw new ArgumentError('host.secondaryHost', SR.SECONDARY_ONLY_COMMAND);
      }
  
      options.currentLocation = Constants.StorageLocation.SECONDARY;
      options.locationMode = StorageUtilities.LocationMode.SECONDARY_ONLY;
      break;
  
    default:
     // no op
    }
  };
  
  /**
  * Checks whether we have the relevant host information based on the locationMode.
  */
  StorageServiceClient.prototype._invalidLocationMode = function (locationMode) {
    switch(locationMode) {
    case StorageUtilities.LocationMode.PRIMARY_ONLY:
      return azureutil.objectIsNull(this.host.primaryHost);
    case StorageUtilities.LocationMode.SECONDARY_ONLY:
      return azureutil.objectIsNull(this.host.secondaryHost);
    default:
      return (azureutil.objectIsNull(this.host.primaryHost) || azureutil.objectIsNull(this.host.secondaryHost));
    }
  };
  
  /**
  * Checks to see if the maximum execution timeout provided has been exceeded.
  */
  StorageServiceClient.prototype._maximumExecutionTimeExceeded = function (currentTime, expiryTime) {
    if(!azureutil.objectIsNull(expiryTime) && currentTime > expiryTime) {
      return true;
    } else {
      return false;
    }
  };
  
  /**
  * Sets the operation expiry time.
  */
  StorageServiceClient.prototype._setOperationExpiryTime = function (options) {
    if(azureutil.objectIsNull(options.operationExpiryTime)) {
      if(!azureutil.objectIsNull(options.maximumExecutionTimeInMs)) {
        options.operationExpiryTime = Date.now() + options.maximumExecutionTimeInMs;
      } else if(this.defaultMaximumExecutionTimeInMs) {
        options.operationExpiryTime = Date.now() + this.defaultMaximumExecutionTimeInMs;
      }
    }
  };
  
  module.exports = StorageServiceClient;
  