1/*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 *   http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19var util = require('util');
20var EventEmitter = require('events').EventEmitter;
21var thrift = require('./thrift');
22
23var TBufferedTransport = require('./buffered_transport');
24var TBinaryProtocol = require('./binary_protocol');
25var InputBufferUnderrunError = require('./input_buffer_underrun_error');
26
27var createClient = require('./create_client');
28
29/**
30 * @class
31 * @name ConnectOptions
32 * @property {string} transport - The Thrift layered transport to use (TBufferedTransport, etc).
33 * @property {string} protocol - The Thrift serialization protocol to use (TBinaryProtocol, etc.).
34 * @property {string} path - The URL path to POST to (e.g. "/", "/mySvc", "/thrift/quoteSvc", etc.).
35 * @property {object} header - A standard Node.js header hash, an object hash containing key/value
36 *        pairs where the key is the header name string and the value is the header value string.
37 * @property {object} requestOptions - Options passed on to http request. Details:
38 * https://developer.harmonyos.com/en/docs/documentation/doc-references/js-apis-net-http-0000001168304341#section12262183471518
39 * @example
40 *     //Use a connection that requires ssl/tls, closes the connection after each request,
41 *     //  uses the buffered transport layer, uses the JSON protocol and directs RPC traffic
42 *     //  to https://thrift.example.com:9090/hello
43 *     import http from '@ohos.net.http' // HTTP module of OpenHarmonyOS
44 *     var thrift = require('thrift');
45 *     var options = {
46 *        transport: thrift.TBufferedTransport,
47 *        protocol: thrift.TJSONProtocol,
48 *        path: "/hello",
49 *        headers: {"Connection": "close"}
50 *     };
51 *     // With OpenHarmonyOS HTTP module, HTTPS is supported by default. To support HTTP, See:
52 *     // https://developer.harmonyos.com/en/docs/documentation/doc-references/js-apis-net-http-0000001168304341#EN-US_TOPIC_0000001171944450__s56d19203690d4782bfc74069abb6bd71
53 *     var con = thrift.createOhosConnection(http.createHttp, "thrift.example.com", 9090, options);
54 *     var client = thrift.createOhosClient(myService, connection);
55 *     client.myServiceFunction();
56 */
57
58/**
59 * Initializes a Thrift HttpConnection instance (use createHttpConnection() rather than
60 *    instantiating directly).
61 * @constructor
62 * @param {ConnectOptions} options - The configuration options to use.
63 * @throws {error} Exceptions other than InputBufferUnderrunError are rethrown
64 * @event {error} The "error" event is fired when a Node.js error event occurs during
65 *     request or response processing, in which case the node error is passed on. An "error"
66 *     event may also be fired when the connection can not map a response back to the
67 *     appropriate client (an internal error), generating a TApplicationException.
68 * @classdesc OhosConnection objects provide Thrift end point transport
69 *     semantics implemented over the OpenHarmonyOS http.request() method.
70 * @see {@link createOhosConnection}
71 */
72var OhosConnection = exports.OhosConnection = function(options) {
73  //Initialize the emitter base object
74  EventEmitter.call(this);
75
76  //Set configuration
77  var self = this;
78  this.options = options || {};
79  this.host = this.options.host;
80  this.port = this.options.port;
81  this.path = this.options.path || '/';
82  //OpenHarmonyOS needs URL for initiating an HTTP request.
83  this.url =
84    this.port === 80
85      ? this.host.replace(/\/$/, '') + this.path
86      : this.host.replace(/\/$/, '') + ':' + this.port + this.path;
87  this.transport = this.options.transport || TBufferedTransport;
88  this.protocol = this.options.protocol || TBinaryProtocol;
89  //Inherit method from OpenHarmonyOS HTTP module
90  this.createHttp = this.options.createHttp;
91
92  //Prepare HTTP request options
93  this.requestOptions = {
94    method: 'POST',
95    header: this.options.header || {},
96    readTimeout: this.options.readTimeout || 60000,
97    connectTimeout: this.options.connectTimeout || 60000
98  };
99  for (var attrname in this.options.requestOptions) {
100    this.requestOptions[attrname] = this.options.requestOptions[attrname];
101  }
102  /*jshint -W069 */
103  if (!this.requestOptions.header['Connection']) {
104    this.requestOptions.header['Connection'] = 'keep-alive';
105  }
106  /*jshint +W069 */
107
108  //The sequence map is used to map seqIDs back to the
109  //  calling client in multiplexed scenarios
110  this.seqId2Service = {};
111
112  function decodeCallback(transport_with_data) {
113    var proto = new self.protocol(transport_with_data);
114    try {
115      while (true) {
116        var header = proto.readMessageBegin();
117        var dummy_seqid = header.rseqid * -1;
118        var client = self.client;
119        //The Multiplexed Protocol stores a hash of seqid to service names
120        //  in seqId2Service. If the SeqId is found in the hash we need to
121        //  lookup the appropriate client for this call.
122        //  The client var is a single client object when not multiplexing,
123        //  when using multiplexing it is a service name keyed hash of client
124        //  objects.
125        //NOTE: The 2 way interdependencies between protocols, transports,
126        //  connections and clients in the Node.js implementation are irregular
127        //  and make the implementation difficult to extend and maintain. We
128        //  should bring this stuff inline with typical thrift I/O stack
129        //  operation soon.
130        //  --ra
131        var service_name = self.seqId2Service[header.rseqid];
132        if (service_name) {
133          client = self.client[service_name];
134          delete self.seqId2Service[header.rseqid];
135        }
136        /*jshint -W083 */
137        client._reqs[dummy_seqid] = function(err, success){
138          transport_with_data.commitPosition();
139          var clientCallback = client._reqs[header.rseqid];
140          delete client._reqs[header.rseqid];
141          if (clientCallback) {
142            process.nextTick(function() {
143              clientCallback(err, success);
144            });
145          }
146        };
147        /*jshint +W083 */
148        if(client['recv_' + header.fname]) {
149          client['recv_' + header.fname](proto, header.mtype, dummy_seqid);
150        } else {
151          delete client._reqs[dummy_seqid];
152          self.emit("error",
153                    new thrift.TApplicationException(
154                       thrift.TApplicationExceptionType.WRONG_METHOD_NAME,
155                       "Received a response to an unknown RPC function"));
156        }
157      }
158    }
159    catch (e) {
160      if (e instanceof InputBufferUnderrunError) {
161        transport_with_data.rollbackPosition();
162      } else {
163        self.emit('error', e);
164      }
165    }
166  }
167
168
169  //Response handler
170  //////////////////////////////////////////////////
171  this.responseCallback = function(error, response) {
172    //Response will be a struct like:
173    // https://developer.harmonyos.com/en/docs/documentation/doc-references/js-apis-net-http-0000001168304341#section15920192914312
174    var data = [];
175    var dataLen = 0;
176
177    if (error) {
178      self.emit('error', error);
179      return;
180    }
181
182    if (!response || response.responseCode !== 200) {
183      self.emit('error', new THTTPException(response));
184    }
185
186    // With OpenHarmonyOS running in a Browser (e.g. Browserify), chunk
187    // will be a string or an ArrayBuffer.
188    if (
189      typeof response.result == 'string' ||
190      Object.prototype.toString.call(response.result) == '[object Uint8Array]'
191    ) {
192      // Wrap ArrayBuffer/string in a Buffer so data[i].copy will work
193      data.push(Buffer.from(response.result));
194    }
195    dataLen += response.result.length;
196
197    var buf = Buffer.alloc(dataLen);
198    for (var i = 0, len = data.length, pos = 0; i < len; i++) {
199      data[i].copy(buf, pos);
200      pos += data[i].length;
201    }
202    //Get the receiver function for the transport and
203    //  call it with the buffer
204    self.transport.receiver(decodeCallback)(buf);
205  };
206
207  /**
208   * Writes Thrift message data to the connection
209   * @param {Buffer} data - A Node.js Buffer containing the data to write
210   * @returns {void} No return value.
211   * @event {error} the "error" event is raised upon request failure passing the
212   *     Node.js error object to the listener.
213   */
214  this.write = function(data) {
215    //To initiate multiple HTTP requests, we must create an HttpRequest object
216    // for each HTTP request
217    var http = self.createHttp();
218    var opts = self.requestOptions;
219    opts.header["Content-length"] = data.length;
220    if (!opts.header["Content-Type"])
221      opts.header["Content-Type"] = "application/x-thrift";
222    // extraData not support array data currently
223    opts.extraData = data.toString();
224    http.request(self.url, opts, self.responseCallback);
225  };
226};
227util.inherits(OhosConnection, EventEmitter);
228
229/**
230 * Creates a new OhosConnection object, used by Thrift clients to connect
231 *    to Thrift HTTP based servers.
232 * @param {Function} createHttp - OpenHarmonyOS method to initiate or destroy an HTTP request.
233 * @param {string} host - The host name or IP to connect to.
234 * @param {number} port - The TCP port to connect to.
235 * @param {ConnectOptions} options - The configuration options to use.
236 * @returns {OhosConnection} The connection object.
237 * @see {@link ConnectOptions}
238 */
239exports.createOhosConnection = function(createHttp, host, port, options) {
240  options.createHttp = createHttp;
241  options.host = host;
242  options.port = port || 80;
243  return new OhosConnection(options);
244};
245
246exports.createOhosClient = createClient;
247
248function THTTPException(response) {
249  thrift.TApplicationException.call(this);
250  if (Error.captureStackTrace !== undefined) {
251    Error.captureStackTrace(this, this.constructor);
252  }
253
254  this.name = this.constructor.name;
255  this.responseCode = response.responseCode;
256  this.response = response;
257  this.type = thrift.TApplicationExceptionType.PROTOCOL_ERROR;
258  this.message =
259    'Received a response with a bad HTTP status code: ' + response.responseCode;
260}
261util.inherits(THTTPException, thrift.TApplicationException);
262