<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

/**
 * Header transport. Writes and reads data with
 *
 * THeaderTransport (any protocol)
 * or TFramedTransport format
 * or unframed TBinaryProtocol.
 * (or TBinary over HTTP if someone wants to implement it)
 *
 * @package thrift.transport
 */
class THeaderTransport extends TFramedTransport
  implements TTransportSupportsHeaders {
  const int TBINARY_PROTOCOL = 0;
  const int TJSON_PROTOCOL = 1;
  const int TCOMPACT_PROTOCOL = 2;

  const int HEADER_MAGIC = 0x0fff;
  const int HTTP_MAGIC = 0x504f5354; // POST
  const int MAX_FRAME_SIZE = 0x3fffffff;

  // Transforms
  const int ZLIB_TRANSFORM = 0x01;

  // Infos
  const int INFO_KEYVALUE = 0x01;

  // Client types
  const int HEADER_CLIENT_TYPE = 0x00;
  const int FRAMED_DEPRECATED = 0x01;
  const int UNFRAMED_DEPRECATED = 0x02;
  const int HTTP_CLIENT_TYPE = 0x03;
  const int UNKNOWN_CLIENT_TYPE = 0x04;

  // default to binary
  protected int $protoId_ = self::TBINARY_PROTOCOL;
  protected int $clientType_ = self::HEADER_CLIENT_TYPE;
  protected Set<int> $supportedProtocols;
  protected Vector<int> $readTrans_;
  protected Vector<int> $writeTrans_;
  protected int $seqId_ = 0;
  protected int $flags_ = 0;

  protected Map<string, string> $readHeaders;
  protected Map<string, string> $writeHeaders;
  protected Map<string, string> $persistentWriteHeaders;

  const string IDENTITY_HEADER = "identity";
  const string ID_VERSION_HEADER = "id_version";
  const int ID_VERSION = 1;
  protected ?string $identity = null;

  /**
   * Constructor.
   *
   * @param TTransport $transport Underlying transport
   */
  public function __construct(
    ?TTransport $transport = null,
    ?Traversable<int> $protocols = null,
  ) {
    parent::__construct($transport, true, true);
    $this->readTrans_ = Vector {};
    $this->writeTrans_ = Vector {};
    $this->readHeaders = Map {};
    $this->writeHeaders = Map {};
    $this->persistentWriteHeaders = Map {};
    $this->supportedProtocols = Set {
      self::HEADER_CLIENT_TYPE,
      self::FRAMED_DEPRECATED,
      self::UNFRAMED_DEPRECATED,
      self::HTTP_CLIENT_TYPE,
    };
    if ($protocols) {
      $this->supportedProtocols->addAll($protocols);
    }
  }

  public function getProtocolID(): int {
    return $this->protoId_;
  }

  public function setProtocolID(int $protoId): this {
    $this->protoId_ = $protoId;
    return $this;
  }

  public function addTransform(int $trans_id): this {
    $this->writeTrans_[] = $trans_id;
    return $this;
  }

  public function resetProtocol(): void {
    if ($this->clientType_ === self::HTTP_CLIENT_TYPE) {
      $this->flush();
    }
    $this->clientType_ = self::HEADER_CLIENT_TYPE;
    $this->readFrame(0);
  }

  public function setHeader(string $str_key, @string $str_value): this {
    $this->writeHeaders[$str_key] = (string) $str_value;
    return $this;
  }

  public function setPersistentHeader(
    string $str_key,
    @string $str_value,
  ): this {
    $this->persistentWriteHeaders[$str_key] = (string) $str_value;
    return $this;
  }

  public function getWriteHeaders(): KeyedContainer<string, string> {
    return $this->writeHeaders;
  }

  public function getPersistentWriteHeaders(): KeyedContainer<string, string> {
    return $this->persistentWriteHeaders;
  }

  public function getHeaders(): KeyedContainer<string, string> {
    return $this->readHeaders;
  }

  public function clearHeaders(): void {
    $this->writeHeaders = Map {};
  }

  public function clearPersistentHeaders(): void {
    $this->persistentWriteHeaders = Map {};
  }

  public function getPeerIdentity(): ?string {
    if ($this->readHeaders->contains(self::IDENTITY_HEADER)) {
      if ((int) $this->readHeaders[self::ID_VERSION_HEADER] ===
          self::ID_VERSION) {
        return $this->readHeaders[self::IDENTITY_HEADER];
      }
    }

    return null;
  }

  public function setIdentity(string $identity): this {
    $this->identity = $identity;
    return $this;
  }

  /**
   * Reads from the buffer. When more data is required reads another entire
   * chunk and serve future reads out of that.
   *
   * @param int $len How much data
   */
  public function read(int $len): @string { // Task #5347782
    if ($this->clientType_ === self::UNFRAMED_DEPRECATED) {
      return $this->transport_->readAll($len);
    }

    if (strlen($this->rBuf_) === 0) {
      $this->readFrame($len);
    }

    // Return substr
    $out = substr($this->rBuf_, $this->rIndex_, $len);
    $this->rIndex_ += $len;

    if (strlen($this->rBuf_) <= $this->rIndex_) {
      $this->rBuf_ = '';
      $this->rIndex_ = 0;
    }
    return $out;
  }

  /**
   * Reads a chunk of data into the internal read buffer.
   */
  private function readFrame(int $req_sz): void {
    $buf = $this->transport_->readAll(4);
    $val = unpack('N', $buf);
    $sz = $val[1];

    if (($sz & TBinaryProtocol::VERSION_MASK) === TBinaryProtocol::VERSION_1) {
      $this->clientType_ = self::UNFRAMED_DEPRECATED;
      if ($req_sz <= 4) {
        $this->rBuf_ = $buf;
      } else {
        $this->rBuf_ = $buf.$this->transport_->readAll($req_sz - 4);
      }
    } else if ($sz === self::HTTP_MAGIC) {
      throw new TTransportException(
        'PHP HeaderTransport does not support HTTP',
        TTransportException::INVALID_CLIENT,
      );
    } else {
      // Either header format or framed. Check next byte
      $buf2 = $this->transport_->readAll(4);
      $val2 = unpack('N', $buf2);
      $version = $val2[1];
      if (($version & TBinaryProtocol::VERSION_MASK) ===
          TBinaryProtocol::VERSION_1) {
        $this->clientType_ = self::FRAMED_DEPRECATED;
        if ($sz - 4 > self::MAX_FRAME_SIZE) {
          throw new TTransportException(
            'Frame size is too large',
            TTransportException::INVALID_FRAME_SIZE,
          );
        }
        $this->rBuf_ = $buf2.$this->transport_->readAll($sz - 4);
      } else if (($version & 0xffff0000) === (self::HEADER_MAGIC << 16)) {
        $this->clientType_ = self::HEADER_CLIENT_TYPE;
        if ($sz - 4 > self::MAX_FRAME_SIZE) {
          throw new TTransportException(
            'Frame size is too large',
            TTransportException::INVALID_FRAME_SIZE,
          );
        }
        $this->flags_ = ($version & 0x0000ffff);
        // read seqId
        $buf3 = $this->transport_->readAll(4);
        $val3 = unpack('N', $buf3);
        $this->seqId_ = $val3[1];
        // read header_size
        $buf4 = $this->transport_->readAll(2);
        $val4 = unpack('n', $buf4);
        $header_size = $val4[1];

        $data = $buf.$buf2.$buf3.$buf4;
        $index = strlen($data);

        $data .= $this->transport_->readAll($sz - 10);
        $this->readHeaderFormat($sz - 10, $header_size, $data, $index);
      } else {
        $this->clientType_ = self::UNKNOWN_CLIENT_TYPE;
        throw new TTransportException(
          'Unknown client type',
          TTransportException::INVALID_CLIENT,
        );
      }
    }

    if (!$this->supportedProtocols->contains($this->clientType_)) {
      throw new TTransportException(
        'Client type not supported on this server',
        TTransportException::INVALID_CLIENT,
      );
    }

  }

  protected function readVarint(string $data, int &$index): int {
    $result = 0;
    $shift = 0;
    while (true) {
      $x = substr($data, $index++, 1);
      $byte = ord($x);
      $result |= ($byte & 0x7f) << $shift;
      if (($byte >> 7) === 0) {
        return $result;
      }
      $shift += 7;
    }

    throw new TTransportException("THeaderTransport: You shouldn't be here");
  }

  protected function getVarint(int $data): string {
    $out = "";
    while (true) {
      if (($data & ~0x7f) === 0) {
        $out .= chr($data);
        break;
      } else {
        $out .= chr(($data & 0xff) | 0x80);
        $data = $data >> 7;
      }
    }
    return $out;
  }

  protected function writeString(string $str): string {
    $buf = $this->getVarint(strlen($str));
    $buf .= $str;
    return $buf;
  }

  protected function readString(
    string $data,
    int &$index,
    int $limit,
  ): string {
    $str_sz = $this->readVarint($data, $index);
    if ($str_sz + $index > $limit) {
      throw new TTransportException(
        'String read too long',
        TTransportException::INVALID_FRAME_SIZE,
      );
    }

    $str = substr($data, $index, $str_sz);
    $index += $str_sz;
    return $str;
  }

  protected function readHeaderFormat(
    int $sz,
    int $header_size,
    string $data,
    int $index,
  ): void {
    $this->readTrans_ = Vector {};

    $header_size = $header_size * 4;
    if ($header_size > $sz) {
      throw new TTransportException(
        'Header size is larger than frame',
        TTransportException::INVALID_FRAME_SIZE,
      );
    }
    $end_of_header = $index + $header_size;

    $this->protoId_ = $this->readVarint($data, $index);
    $numHeaders = $this->readVarint($data, $index);
    if ($this->protoId_ === 1 &&
        $this->clientType_ !== self::HTTP_CLIENT_TYPE) {
      throw new TTransportException(
        'Trying to recv JSON encoding over binary',
        TTransportException::INVALID_CLIENT,
      );
    }

    // Read in the headers.  Data for each header varies.
    for ($i = 0; $i < $numHeaders; $i++) {
      $transId = $this->readVarint($data, $index);
      switch ($transId) {
        case self::ZLIB_TRANSFORM:
          $this->readTrans_[] = $transId;
          break;

        default:
          throw new TApplicationException(
            'Unknown transform in client request',
            TApplicationException::INVALID_TRANSFORM,
          );
      }
    }
    // Make sure that the read transforms are applied in the reverse order
    // from when the data was written.
    $this->readTrans_ = array_reverse($this->readTrans_);

    // Read the info headers
    $this->readHeaders = Map {};
    while ($index < $end_of_header) {
      $infoId = $this->readVarint($data, $index);
      switch ($infoId) {
        case self::INFO_KEYVALUE:
          $num_keys = $this->readVarint($data, $index);
          for ($i = 0; $i < $num_keys; $i++) {
            $strKey = $this->readString($data, $index, $end_of_header);
            $strValue = $this->readString($data, $index, $end_of_header);
            $this->readHeaders[$strKey] = $strValue;
          }
          break;
        default:
          // End of infos
          break;
      }
    }

    $this->rBuf_ =
      $this->untransform(substr($data, $end_of_header, $sz - $header_size));
  }

  protected function transform(string $data): string {
    // UNSAFE_BLOCK: the only way we know that these possibly iterated compressors
    // will produce reeasonable results is by complicated promises of the way
    // thrift sets things up, which Hack has no way to understand.
    foreach ($this->writeTrans_ as $trans) {
      switch ($trans) {
        case self::ZLIB_TRANSFORM:
          $data = gzcompress($data);
          break;

        default:
          throw new TTransportException(
            'Unknown transform during send',
            TTransportException::INVALID_TRANSFORM,
          );
      }
    }
    return $data;
  }

  protected function untransform(string $data): @string { // Task #5347782
    // UNSAFE_BLOCK: the only way we know that these possibly iterated compressors
    // will produce reeasonable results is by complicated promises of the way
    // thrift sets things up, which Hack has no way to understand.
    foreach ($this->readTrans_ as $trans) {
      switch ($trans) {
        case self::ZLIB_TRANSFORM:
          $data = gzuncompress($data);
          break;

        default:
          throw new TApplicationException(
            'Unknown transform during recv',
            TTransportException::INVALID_TRANSFORM,
          );
      }
    }
    return $data;
  }

  /**
   * Writes the output buffer in header format, or format
   * client responded with (framed, unframed, http)
   */
  public function flush(): void {
    $this->flushImpl(false);
  }

  public function onewayFlush(): void {
    $this->flushImpl(true);
  }

  private function flushImpl(bool $oneway): void {
    if (strlen($this->wBuf_) == 0) {
      if ($oneway) {
        $this->transport_->onewayFlush();
      } else {
        $this->transport_->flush();
      }
      return;
    }

    $out = $this->transform($this->wBuf_);

    // Note that we clear the internal wBuf_ prior to the underlying write
    // to ensure we're in a sane state (i.e. internal buffer cleaned)
    // if the underlying write throws up an exception
    $this->wBuf_ = '';

    if ($this->protoId_ === 1 &&
        $this->clientType_ !== self::HTTP_CLIENT_TYPE) {
      throw new TTransportException(
        'Trying to send JSON encoding over binary',
        TTransportException::INVALID_CLIENT,
      );
    }

    $buf = '';
    if ($this->clientType_ === self::HEADER_CLIENT_TYPE) {
      $transformData = '';

      $num_headers = 0;
      // For now, no transform requires data.
      foreach ($this->writeTrans_ as $trans) {
        ++$num_headers;
        $transformData .= $this->getVarint($trans);
      }

      // Add in special flags.
      if ($this->identity !== null) {
        $this->writeHeaders[self::ID_VERSION_HEADER] =
          (string) self::ID_VERSION;
        $this->writeHeaders[self::IDENTITY_HEADER] = $this->identity;
      }

      $infoData = '';
      if ($this->writeHeaders || $this->persistentWriteHeaders) {
        $infoData .= $this->getVarint(self::INFO_KEYVALUE);
        $infoData .= $this->getVarint(
          count($this->writeHeaders) +
          count($this->persistentWriteHeaders),
        );
        foreach ($this->persistentWriteHeaders as $str_key => $str_value) {
          $infoData .= $this->writeString($str_key);
          $infoData .= $this->writeString($str_value);
        }
        foreach ($this->writeHeaders as $str_key => $str_value) {
          $infoData .= $this->writeString($str_key);
          $infoData .= $this->writeString($str_value);
        }
      }
      $this->writeHeaders = Map {};

      $headerData =
        $this->getVarint($this->protoId_).$this->getVarint($num_headers);
      $header_size =
        strlen($transformData) + strlen($infoData) + strlen($headerData);
      $paddingSize = 4 - ($header_size % 4);
      $header_size += $paddingSize;

      $buf = (string) pack('nn', self::HEADER_MAGIC, $this->flags_);
      $buf .= (string) pack('Nn', $this->seqId_, $header_size / 4);

      $buf .= $headerData.$transformData;
      $buf .= $infoData;

      // Pad out the header with 0x00
      for ($i = 0; $i < $paddingSize; $i++) {
        $buf .= (string) pack('C', '\0');
      }

      // Append the data
      $buf .= $out;

      // Prepend the size.
      $buf = (string) pack('N', strlen($buf)).$buf;
    } else if ($this->clientType_ === self::FRAMED_DEPRECATED) {
      $buf = (string) pack('N', strlen($out));
      $buf .= $out;
    } else if ($this->clientType_ === self::UNFRAMED_DEPRECATED) {
      $buf = $out;
    } else if ($this->clientType_ === self::HTTP_CLIENT_TYPE) {
      throw new TTransportException(
        'HTTP not implemented',
        TTransportException::INVALID_CLIENT,
      );
    } else {
      throw new TTransportException(
        'Unknown client type',
        TTransportException::INVALID_CLIENT,
      );
    }

    if (strlen($buf) > self::MAX_FRAME_SIZE) {
      throw new TTransportException(
        'Attempting to send oversize frame',
        TTransportException::INVALID_FRAME_SIZE,
      );
    }

    $this->transport_->write($buf);
    if ($oneway) {
      $this->transport_->onewayFlush();
    } else {
      $this->transport_->flush();
    }
  }
}


<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

class THeaderTransportFactory extends TTransportFactory {
  /**
   * @static
   * @param TTransport $transport
   * @return TTransport
   */
  public function getTransport(TTransport $transport): THeaderTransport {
    $p = Vector {
      THeaderTransport::HEADER_CLIENT_TYPE,
      THeaderTransport::FRAMED_DEPRECATED,
      THeaderTransport::UNFRAMED_DEPRECATED,
    };
    return new THeaderTransport($transport, $p);
  }
}


<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

/**
 * HTTP client for Thrift
 *
 * @package thrift.transport
 */
class THttpClient extends TTransport implements IThriftRemoteConn {

  /**
   * The host to connect to
   *
   * @var string
   */
  protected string $host_;

  /**
   * The port to connect on
   *
   * @var int
   */
  protected int $port_;

  /**
   * The URI to request
   *
   * @var string
   */
  protected string $uri_;

  /**
   * The scheme to use for the request, i.e. http, https
   *
   * @var string
   */
  protected string $scheme_;

  /**
   * Buffer for the HTTP request data
   *
   * @var string
   */
  protected string $buf_;

  /**
   * Input data stream.
   *
   * @var resource
   */
  protected string $data_;

  /**
   * Error message
   *
   * @var string
   */
  protected ?string $errstr_;

  /**
   * Read timeout
   *
   * @var float
   */
  protected ?float $timeout_;

  /**
   * Custom HTTP headers
   *
   * @var array
   */
  protected ?KeyedContainer<string, string> $custom_headers_;

  /**
   * Debugging on?
   *
   * @var bool
   */
  protected bool $debug_ = false;

  /**
   * Debug handler
   *
   * @var mixed
   */
  protected (function(string): bool) $debugHandler_;

  /**
   * Specifies the maximum number of bytes to read
   * at once from internal stream.
   */
  protected ?int $maxReadChunkSize_ = null;

  /**
   * Path to a custom CA bundle to use when verifying an HTTPS certificate.
   * Useful when talking to a service with a cert signed by the FB CA.
   */
  protected ?string $caInfo_;

  /**
   * Make a new HTTP client.
   *
   * @param string $host
   * @param int    $port
   * @param string $uri
   */
  public function __construct(
    string $host,
    int $port = 80,
    string $uri = '',
    string $scheme = 'http',
    ?(function(string): bool) $debugHandler = null,
  ) {
    if ($uri && ($uri[0] != '/')) {
      $uri = '/'.$uri;
    }
    $this->scheme_ = $scheme;
    $this->host_ = $host;
    $this->port_ = $port;
    $this->uri_ = $uri;
    $this->buf_ = '';
    $this->data_ = '';
    $this->errstr_ = '';
    $this->timeout_ = null;
    $this->custom_headers_ = null;
    $this->debugHandler_ = $debugHandler ?: fun('error_log');
  }

  /**
   * Sets the internal max read chunk size.
   * null for no limit (default).
   */
  public function setMaxReadChunkSize(int $maxReadChunkSize): void {
    $this->maxReadChunkSize_ = $maxReadChunkSize;
  }

  public function setCAInfo(string $path_to_ca_bundle): void {
    $this->caInfo_ = $path_to_ca_bundle;
  }

  /**
   * Set read timeout
   *
   * @param float $timeout
   */
  public function setTimeoutSecs(float $timeout): void {
    $this->timeout_ = $timeout;
  }

  /**
   * Set custom headers
   *
   * @param array of key value pairs of custom headers
   */
  public function setCustomHeaders(
    KeyedContainer<string, string> $custom_headers,
  ): void {
    $this->custom_headers_ = $custom_headers;
  }

  /**
   * Get custom headers
   *
   * @return array key value pairs of custom headers
   */
  public function getCustomHeaders(): ?KeyedContainer<string, string> {
    return $this->custom_headers_;
  }

  /**
   * Get the remote host that this transport is connected to.
   *
   * @return string host
   */
  public function getHost(): string {
    return $this->host_;
  }

  /**
   * Get the remote port that this transport is connected to.
   *
   * @return int port
   */
  public function getPort(): int {
    return $this->port_;
  }

  /**
   * Get the scheme used to connect to the remote server.
   *
   * @return string scheme
   */
  public function getScheme(): string {
    return $this->scheme_;
  }

  /**
   * Sets debugging output on or off
   *
   * @param bool $debug
   */
  public function setDebug(bool $debug): void {
    $this->debug_ = $debug;
  }

  /**
   * Whether this transport is open.
   *
   * @return boolean true if open
   */
  public function isOpen(): bool {
    return true;
  }

  /**
   * Open the transport for reading/writing
   *
   * @throws TTransportException if cannot open
   */
  public function open(): void {}

  /**
   * Close the transport.
   */
  public function close(): void {
    $this->data_ = '';
  }

  /**
   * Read some data into the array.
   *
   * @param int    $len How much to read
   * @return string The data that has been read
   * @throws TTransportException if cannot read any more data
   */
  public function read(int $len): string {
    if ($this->maxReadChunkSize_ !== null) {
      $len = min($len, $this->maxReadChunkSize_);
    }

    if (strlen($this->data_) < $len) {
      $data = $this->data_;
      $this->data_ = '';
    } else {
      $data = substr($this->data_, 0, $len);
      $this->data_ = substr($this->data_, $len);
    }

    if ($data === false || $data === '') {
      throw new TTransportException(
        'THttpClient: Could not read '.
        $len.
        ' bytes from '.
        $this->host_.
        ':'.
        $this->port_.
        '/'.
        $this->uri_.
        " Reason: ".
        $this->errstr_,
        TTransportException::UNKNOWN,
      );
    }
    return $data;
  }

  /**
   * Writes some data into the pending buffer
   *
   * @param string $buf  The data to write
   * @throws TTransportException if writing fails
   */
  public function write(string $buf): void {
    $this->buf_ .= $buf;
  }

  /**
   * Opens and sends the actual request over the HTTP connection
   *
   * @throws TTransportException if a writing error occurs
   */
  public function flush(): void {
    $host = $this->host_.':'.$this->port_;
    $user_agent = 'PHP/THttpClient';
    /* HH_FIXME[2050] We know $_SERVER exists, even if Hack doesn't */
    $script = (string) urlencode(basename($_SERVER['SCRIPT_FILENAME']));
    if ($script) {
      $user_agent .= ' ('.$script.')';
    }

    $curl = curl_init($this->scheme_.'://'.$host.$this->uri_);

    $headers = array(
      'Host: '.$host,
      'Accept: application/x-thrift',
      'User-Agent: '.$user_agent,
      'Content-Type: application/x-thrift',
      'Content-Length: '.(string) strlen($this->buf_),
    );

    if ($this->custom_headers_ !== null) {
      foreach ($this->custom_headers_ as $header => $value) {
        $headers[] = $header.': '.$value;
      }
    }
    curl_setopt($curl, CURLOPT_PROXY, '');
    curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($curl, CURLOPT_POST, true);
    curl_setopt($curl, CURLOPT_MAXREDIRS, 1);
    curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($curl, CURLOPT_POSTFIELDS, $this->buf_);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_BINARYTRANSFER, true);
    curl_setopt($curl, CURLOPT_TIMEOUT, $this->timeout_);
    if ($this->caInfo_ !== null) {
      curl_setopt($curl, CURLOPT_CAINFO, $this->caInfo_);
    }

    $this->buf_ = '';
    $this->data_ = curl_exec($curl);

    $this->errstr_ = curl_error($curl);
    curl_close($curl);

    // Connect failed?
    if ($this->data_ === false) {
      $error = 'THttpClient: Could not connect to '.$host.$this->uri_;
      throw new TTransportException($error, TTransportException::NOT_OPEN);
    }
  }

  public function isReadable(): bool {
    return (bool) $this->data_;
  }

  public function isWritable(): bool {
    return true;
  }

  public function getRecvTimeout(): int {
    return 0;
  }
  public function setRecvTimeout(int $timeout): void { /* do nothing */
  }
}


<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

/** Inherits from THttpClient */

/**
 * Multi-IP HTTP client for Thrift. The client randomly selects and uses a host
 * from its pool.
 *
 * @package thrift.transport
 */
class THttpClientPool extends THttpClient {

  /**
   * Servers belonging to this pool. Array of associative arrays with 'host' and
   * 'port' keys.
   */
  protected array<(string, int)> $servers_ = array();

  /**
   * Try hosts in order? or randomized?
   *
   * @var bool
   */
  private bool $randomize_ = true;

  /**
   * The number of times to try to connect to a particular endpoint.
   *
   * @var int
   */
  private int $numTries_ = 1;

  /**
   * Make a new multi-IP HTTP client.
   *
   * @param array  $hosts   List of remote hostnames or IPs.
   * @param mixed  $ports   Array of remote ports, or a single
   *                        common port.
   * @param string $uri     URI of the endpoint.
   * @param string $scheme  http or https
   */
  public function __construct(
    KeyedContainer<mixed, string> $hosts,
    KeyedContainer<mixed, int> $ports,
    string $uri = '',
    string $scheme = 'http',
    ?(function(string): bool) $debugHandler = null,
  ) {
    parent::__construct('', 0, $uri, $scheme, $debugHandler);

    foreach ($hosts as $key => $host) {
      $this->servers_[] = tuple($host, $ports[$key]);
    }
  }

  /**
   * Add a server to the pool. This function does not prevent you from adding a
   * duplicate server entry.
   *
   * @param string $host  hostname or IP
   * @param int    $port  port
   */
  public function addServer(string $host, int $port): void {
    $this->servers_[] = tuple($host, $port);
  }

  /**
   * Turns random endpoint selection on or off.
   *
   * @param bool $randomize
   */
  public function setRandomize(bool $randomize): void {
    $this->randomize_ = $randomize;
  }

  /**
   * Sets how many times to try to connect to an endpoint before giving up on
   * it.
   *
   * @param int $numTries
   */
  public function setNumTries(int $numTries): void {
    $this->numTries_ = $numTries;
  }

  /**
   * Trys to open a connection and send the actual HTTP request to the first
   * available server in the pool.
   */
  public function flush(): void {
    if ($this->randomize_) {
      shuffle($this->servers_);
    }

    foreach ($this->servers_ as $server) {
      $this->host_ = $server[0];
      $this->port_ = $server[1];

      $j = $this->numTries_;
      while ($j > 0) {
        try {
          parent::flush();
          return;
        } catch (TTransportException $e) {
          if ($this->debug_) {
            call_user_func($this->debugHandler_, $e->getMessage());
          }
          --$j;
        }
      }
    }

    $this->host_ = '';
    $this->port_ = 0;
    $error =
      'THttpClientPool: Could not connect to any of the servers '.
      'in the pool';
    throw new TTransportException($error, TTransportException::NOT_OPEN);
  }
}


<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

/**
 * A memory buffer is a tranpsort that simply reads from and writes to an
 * in-memory string buffer. Anytime you call write on it, the data is simply
 * placed into a buffer, and anytime you call read, data is read from that
 * buffer.
 *
 * @package thrift.transport
 */
class TMemoryBuffer extends TTransport implements IThriftBufferedTransport {

  private string $buf_ = '';
  private int $index_ = 0;
  private ?int $length_ = null;

  /**
   * Constructor. Optionally pass an initial value
   * for the buffer.
   */
  public function __construct(string $buf = '') {
    $this->buf_ = (string) $buf;
  }

  public function isOpen(): bool {
    return true;
  }

  public function open(): void {}

  public function close(): void {}

  private function length(): int {
    if ($this->length_ === null) {
      $this->length_ = strlen($this->buf_);
    }
    return $this->length_;
  }

  public function available(): int {
    return $this->length() - $this->index_;
  }

  public function minBytesAvailable(): int {
    return $this->available();
  }

  public function write(string $buf): void {
    $this->buf_ .= $buf;
    $this->length_ = null; // reset length
  }

  public function read(int $len): string {
    $available = $this->available();
    if ($available === 0) {
      $buffer_dump = bin2hex($this->buf_);
      throw new TTransportException(
        'TMemoryBuffer: Could not read '.
        $len.
        ' bytes from buffer.'.
        ' Original length is '.
        $this->length().
        ' Current index is '.
        $this->index_.
        ' Buffer content <start>'.
        $buffer_dump.
        '<end>',
        TTransportException::UNKNOWN,
      );
    }

    if ($available < $len) {
      $len = $available;
    }
    $ret = $this->peek($len);
    $this->index_ += $len;
    return $ret;
  }

  public function peek(int $len, int $start = 0): string {
    return
      $len === 1
        ? $this->buf_[$this->index_ + $start]
        : substr($this->buf_, $this->index_ + $start, $len);
  }

  public function putBack(string $buf): void {
    if ($this->available() === 0) {
      $this->buf_ = $buf;
    } else {
      $remaining = (string) substr($this->buf_, $this->index_);
      $this->buf_ = $buf.$remaining;
    }
    $this->length_ = null;
    $this->index_ = 0;
  }

  public function getBuffer(): @string { // Task #5347782
    if ($this->index_ === 0) {
      return $this->buf_;
    }
    return substr($this->buf_, $this->index_);
  }

  public function resetBuffer(): void {
    $this->buf_ = '';
    $this->index_ = 0;
    $this->length_ = null;
  }
}


<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

/**
 * NonBlocking implementation of TSocket. Does internal
 * buffering on sends and recvs. An external socket
 * select loop can drive io. (See ClientSet.php).
 *
 * @package thrift.transport
 */
class TNonBlockingSocket extends TTransport implements IThriftRemoteConn {

  /**
   * Handle to PHP socket
   *
   * @var resource
   */
  private ?resource $handle_ = null;

  /**
   * Remote hostname
   *
   * @var string
   */
  protected string $host_ = 'localhost';

  /**
   * Remote port
   *
   * @var int
   */
  protected int $port_ = 9090;

  protected bool $ipV6_ = false;

  /**
   * The write buffer.
   *
   *  @var string
   */
  protected string $wBuf_ = '';

  /**
   * The read buffer and pos.
   *
   *  @var string
   *  @var int
   */
  protected string $rBuf_ = '';
  protected int $rBufPos_ = 0;

  /**
   * Debug on?
   *
   * @var bool
   */
  protected bool $debug_ = false;

  /**
   * Debug handler
   *
   * @var mixed
   */
  protected (function(string): bool) $debugHandler_;

  /**
   * Socket recv buffer capacity.
   * @var int or null.
   */
  private ?int $sockRecvCapacity_ = null;

  /**
   * Socket constructor
   *
   * @param string $host         Remote hostname
   * @param int    $port         Remote port
   * @param string $debugHandler Function to call for error logging
   */

  public function __construct(
    string $host = 'localhost',
    int $port = 9090,
    ?(function(string): bool) $debugHandler = null,
  ) {
    $this->host_ = $host;
    $this->port_ = $port;
    $this->ipV6_ = strlen(@inet_pton($host)) == 16;

    $this->debugHandler_ = $debugHandler ?: fun('error_log');
  }

  /**
   * Get the host that this socket is connected to
   *
   * @return string host
   */
  public function getHost(): string {
    return $this->host_;
  }

  /**
   * Get the remote port that this socket is connected to
   *
   * @return int port
   */
  public function getPort(): int {
    return $this->port_;
  }

  /**
   * Get the socket for this connection. So we can select on it.
   *
   * @return socket resource
   */
  public function getSocket(): ?resource {
    return $this->handle_;
  }

  /**
   * Tests whether this is open
   *
   * @return bool true if the socket is open
   */
  public function isOpen(): bool {
    return is_resource($this->handle_);
  }

  /**
   * These methods are required for IThriftRemoteConn, but are not used
   */
  public function getRecvTimeout(): int {
    return 0;
  }

  public function setRecvTimeout(int $timeout): void {
    throw new TTransportException('setRecvTimeout is insupported');
  }

  public function isReadable(): bool {
    return $this->isOpen();
  }

  public function isWritable(): bool {
    return $this->isOpen();
  }

  /**
   * Set debugging
   *
   * @param bool on/off
   */
  public function setDebug(bool $debug): void {
    $this->debug_ = $debug;
  }

  /**
   * Connects the socket.
   */
  public function open(): void {
    if ($this->ipV6_) {
      $handle = socket_create(AF_INET6, SOCK_STREAM, SOL_TCP);
    } else {
      $handle = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
    }

    if ($handle === false) {
      $error = 'TNonBlockingSocket: Could not create socket';
      throw new TTransportException($error);
    }

    $this->handle_ = $handle;

    if (!socket_set_nonblock($this->handle_)) {
      $error = 'TNonBlockingSocket: Could not set nonblocking.';
      throw new TTransportException($error);
    }

    $res = @socket_connect(
      $this->handle_,
      $this->host_,
      $this->port_,
    );

    if (!$res) {
      $errno = socket_last_error($this->handle_);
      $errstr = socket_strerror($errno);
      $error =
        'TNonBlockingSocket: socket_connect error ('.
        (string) $errstr.'['.(string) $errno.'])';
      if ($errno != 115) {
        if ($this->debug_) {
          call_user_func($this->debugHandler_, $error);
        }
      }
      throw new TTransportException($error);
    }

    $wBuf_ = '';
    $rBuf_ = '';
    $rBufPos_ = 0;

    $this->sockRecvCapacity_ =
      socket_get_option($this->handle_, SOL_SOCKET, SO_RCVBUF);
    if ($this->sockRecvCapacity_ == false) {
      $this->sockRecvCapacity_ = null;
    }
  }

  /**
   * Closes the socket.
   */
  public function close(): void {
    if ($this->handle_ !== null) {
      @socket_close($this->handle_);
    }
    $this->handle_ = null;
  }

  /**
   * Do a nonblocking read. If all of $len is not there, throw an exception.
   * Save unread data to the recv buffer so subsequent reads can retrieve it.
   *
   * @param int $len How many bytes
   * @return string Binary data
   */
  public function readAll(int $len): string {
    // We may already have data for this read in the buffer
    $ret = (string) substr($this->rBuf_, $this->rBufPos_);
    // set the buffer to all but this read
    $this->rBuf_ = (string) substr($this->rBuf_, 0, $this->rBufPos_);

    if ($len <= strlen($ret)) {
      $this->rBuf_ .= $ret;
      $this->rBufPos_ += $len;

      return substr($ret, 0, $len);
    }

    // we already have this much for this read
    $len -= strlen($ret);

    while (true) {
      $buf = $this->read($len);

      if ($buf === '') {
        // Put back what we may have already read for this read.
        // Don't advance pos
        $this->rBuf_ .= $ret;

        throw new TTransportException(
          'TNonBlockingSocket: readAll could not'.
          ' read '.
          $len.
          ' bytes from '.
          $this->host_.
          ':'.
          $this->port_,
        );
      } else if (($sz = strlen($buf)) < $len) {
        $ret .= $buf;
        $len -= $sz;
      } else {
        $ret .= $buf;
        $this->rBuf_ .= $ret;
        $this->rBufPos_ += strlen($ret); // advance pos to next read

        return $ret;
      }
    }

    throw new TTransportException(
      "TNonBlockingSocket: You shouldn't be here",
    );
  }

  /**
   * Occasionally we will restart several readAlls due to a failure,
   * EAGAIN for instance, and we want to read all data we already buffered.
   * In this case call resetBufferPos. This occurs for example when a gen
   * client does several recevies, and fails on one other than the first.
   * It then retries all of them
   */
  public function resetBufferPos(): void {
    $this->rBufPos_ = 0;
  }

  /*
   * Clear the buffer and reset it
   */
  public function clearBuf(): void {
    $this->rBuf_ = '';
    $this->rBufPos_ = 0;
  }

  /**
   * Read from the socket
   *
   * @param int $len How many bytes
   * @return string Binary data
   */
  public function read(int $len): string {
    if ($this->sockRecvCapacity_ !== null) {
      $len = min($len, $this->sockRecvCapacity_);
    }

    $data = @socket_read($this->handle_, $len);

    if ($data === false || $data === '') {
      $errno = socket_last_error($this->handle_);
      $errstr = socket_strerror($errno);
      $error =
        "read: no data to be read ".
        $this->host_.
        ':'.
        $this->port_.
        ' ('.
        (string) $errstr.' ['.(string) $errno.'])';
      if ($this->debug_) {
        call_user_func($this->debugHandler_, $error);
      }

      return '';
    }

    return $data;
  }

  /**
   * Do a buffered write. Use doWrite to notify when the socket can be written.
   *
   * @param string $buf The data to write
   */
  public function write(string $buf): void {
    $this->wBuf_ .= $buf;
  }

  public function doWrite(): void {
    $got = @socket_write($this->handle_, $this->wBuf_);

    if ($got === 0 || $got === false) {
      // Could not write
      $errno = socket_last_error($this->handle_);
      $errstr = socket_strerror($errno);
      $error =
        'doWrite: write failed ('.
        (string) $errno.
        '): '.
        (string) $errstr.' '.$this->host_.':'.$this->port_;
      throw new TTransportException($error);
    }

    $this->wBuf_ = substr($this->wBuf_, $got);
  }

  /**
   * Do we have buffered data to send?
   *
   * @return bool
   */
  public function haveData(): bool {
    return strlen($this->wBuf_) > 0;
  }

  /**
   * No flush implemented.
   * Generated code will flush on send, we'd like to send as data becomes
   * available without blocking.
   */
  public function flush(): void {}
}


<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

/**
 * Wrapper class over TNonBlockingSocket that suppresses
 * the exception "operation in progress 115" when the open()
 * is called. This is helpful for classes that don't
 * handle properly this event. Ie. TFramedTransport.
 */
class TNonBlockingSocketNoThrow extends TNonBlockingSocket {
  public function open(): void {
    try {
      parent::open();
    } catch (Exception $e) {
      $op_in_progress =
        (strpos($e->getMessage(), "socket_connect error") !== false) &&
        (strpos($e->getMessage(), "[115]") !== false);
      if (!$op_in_progress) {
        throw $e;
      }
    }
  }
}


<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

/**
 * Transport that only accepts writes and ignores them.
 * This is useful for measuring the serialized size of structures.
 *
 * @package thrift.transport
 */
class TNullTransport extends TTransport {

  public function isOpen(): bool {
    return true;
  }

  public function open(): void {}

  public function close(): void {}

  public function read(int $len): string {
    throw new TTransportException("Can't read from TNullTransport.");
  }

  public function write(string $buf): void {}

}


<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

/**
 * Php stream transport. Reads to and writes from the php standard streams
 * php://input and php://output
 *
 * @package thrift.transport
 */
class TPhpStream extends TTransport {

  const int MODE_R = 1;
  const int MODE_W = 2;

  private ?resource $inStream_ = null;

  private ?resource $outStream_ = null;

  private bool $read_ = false;

  private bool $write_ = false;

  /**
   * Specifies the maximum number of bytes to read
   * at once from internal stream.
   */
  private ?int $maxReadChunkSize_ = null;

  public function __construct(int $mode) {
    $this->read_ = (bool) ($mode & self::MODE_R);
    $this->write_ = (bool) ($mode & self::MODE_W);
  }

  /**
   * Sets the internal max read chunk size.
   * null for no limit (default).
   */
  public function setMaxReadChunkSize(int $maxReadChunkSize): void {
    $this->maxReadChunkSize_ = $maxReadChunkSize;
  }

  public function open(): void {
    if ($this->read_) {
      $this->inStream_ = @fopen(self::inStreamName(), 'r');
      if (!is_resource($this->inStream_)) {
        throw new TException('TPhpStream: Could not open php://input');
      }
    }
    if ($this->write_) {
      $this->outStream_ = @fopen('php://output', 'w');
      if (!is_resource($this->outStream_)) {
        throw new TException('TPhpStream: Could not open php://output');
      }
    }
  }

  public function close(): void {
    if ($this->read_) {
      @fclose($this->inStream_);
      $this->inStream_ = null;
    }
    if ($this->write_) {
      @fclose($this->outStream_);
      $this->outStream_ = null;
    }
  }

  public function isOpen(): bool {
    return
      (!$this->read_ || is_resource($this->inStream_)) &&
      (!$this->write_ || is_resource($this->outStream_));
  }

  public function read(int $len): string {
    if ($this->maxReadChunkSize_ !== null) {
      $len = min($len, $this->maxReadChunkSize_);
    }

    $data = @fread($this->inStream_, $len);

    if ($data === false || $data === '') {
      throw new TException('TPhpStream: Could not read '.$len.' bytes');
    }
    return $data;
  }

  public function write(string $buf): void {
    while (strlen($buf) > 0) {
      $got = @fwrite($this->outStream_, $buf);

      if ($got === 0 || $got === false) {
        throw new TException(
          'TPhpStream: Could not write '.(string) strlen($buf).' bytes',
        );
      }
      $buf = substr($buf, $got);
    }
  }

  public function flush(): void {
    @fflush($this->outStream_);
  }

  private static function inStreamName(): string {
    if (php_sapi_name() == 'cli') {
      return 'php://stdin';
    }
    return 'php://input';
  }
}


<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

/**
 * Server socket class
 */
class TServerSocket {
  protected ?string $host;
  protected int $port;
  protected ?resource $handle;

  private int $send_buffer_size;
  private int $recv_buffer_size;
  public function __construct(
    int $port,
    int $send_buffer_size = 512,
    int $recv_buffer_size = 512,
  ) {
    $this->host = null;
    $this->port = $port;
    $this->handle = null;
    $this->send_buffer_size = $send_buffer_size;
    $this->recv_buffer_size = $recv_buffer_size;
  }

  public function listen(): void {
    foreach (varray['[::]', '0.0.0.0'] as $addr) {
      $errno = 0;
      $errstr = '';
      $this->handle = PHP\stream_socket_server(
        'tcp://'.$addr.':'.$this->port,
        &$errno,
        &$errstr,
        STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
      );

      if ($this->handle !== false) {
        if ($this->port === 0) {
          $address = Str\split(
            PHP\stream_socket_get_name($this->handle, false),
            ':',
          );
          $this->port = PHP\intval($address[PHP\count($address) - 1]);
        }
        break;
      }
    }
  }

  public function accept(int $timeout = -1): ?TBufferedTransport {
    if ($timeout !== 0) {
      $client = PHP\stream_socket_accept(
        nullthrows($this->handle),
        (float)$timeout,
      );
    } else {
      $client = @PHP\stream_socket_accept(
        nullthrows($this->handle),
        (float)$timeout,
      );
    }

    if (!$client) {
      return null;
    }

    $socket = new TSocket();
    $socket->setHandle($client);

    // We need to create a buffered transport because TSocket's isReadable
    // is not reliable in PHP (hphp is fine) and buffered transport is more
    // efficient (60% faster in my tests)
    $transport = new TBufferedTransport(
      $socket,
      $this->send_buffer_size,
      $this->recv_buffer_size,
    );

    return $transport;
  }

  public function close(): void {
    /* HH_FIXME[4016] Exposed by banning unset and isset in partial mode */
    if (isset($this->handle)) {
      PHP\fclose($this->handle);
      $this->handle = null;
    }
  }

  public function port(): int {
    return $this->port;
  }
}


<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

/**
 * Sockets implementation of the TTransport interface.
 *
 * @package thrift.transport
 */
class TSocket extends TTransport
  implements TTransportStatus, InstrumentedTTransport, IThriftRemoteConn {

  use InstrumentedTTransportTrait;

  /**
   * Handle to PHP socket
   *
   * @var resource
   */
  private ?resource $handle_ = null;

  /**
   * Remote hostname
   *
   * @var string
   */
  protected string $host_ = 'localhost';

  /**
   * Remote port
   *
   * @var int
   */
  protected int $port_ = 9090;

  /**
   * Local port
   *
   * @var int
   */
  protected int $lport_ = 0;

  /**
   * Send timeout in milliseconds
   *
   * @var int
   */
  private int $sendTimeout_ = 100;

  /**
   * Recv timeout in milliseconds
   *
   * @var int
   */
  private int $recvTimeout_ = 750;

  /**
   * Is send timeout set?
   *
   * @var bool
   */
  private bool $sendTimeoutSet_ = false;

  /**
   * Persistent socket or plain?
   *
   * @var bool
   */
  private bool $persist_ = false;

  /**
   * When the current read is started
   *
   * @var int, null means no read is started
   */
  private ?int $readAttemptStart_ = null;

  /**
   * When the current write is started
   *
   * @var int, null means no write is started
   */
  private ?int $writeAttemptStart_ = null;

  /**
   * Debugging on?
   *
   * @var bool
   */
  protected bool $debug_ = false;

  /**
   * Debug handler
   *
   * @var mixed
   */
  protected (function(string): bool) $debugHandler_;

  /**
   * error string (in case of open failure)
   *
   * @var string or null
   */
  protected ?string $errstr_ = null;

  /**
   * error number (in case of open failure)
   *
   * @var int or null
   */
  protected ?int $errno_ = null;

  /**
   * Specifies the maximum number of bytes to read
   * at once from internal stream.
   */
  protected ?int $maxReadChunkSize_ = null;

  /**
   * Socket constructor
   *
   * @param string $host         Remote hostname
   * @param int    $port         Remote port
   * @param bool   $persist      Whether to use a persistent socket
   * @param string $debugHandler Function to call for error logging
   */
  public function __construct(
    string $host = 'localhost',
    int $port = 9090,
    bool $persist = false,
    ?(function(string): bool) $debugHandler = null,
  ) {
      $this->host_ = $host;
    $this->port_ = $port;
    $this->persist_ = $persist;
    $this->debugHandler_ = $debugHandler ?: fun('error_log');
  }

  /**
   * Sets the internal max read chunk size.
   * null for no limit (default).
   */
  public function setMaxReadChunkSize(int $maxReadChunkSize): void {
    $this->maxReadChunkSize_ = $maxReadChunkSize;
  }

  /**
   * Sets the socket handle
   * @param resource $handle
   * @return $this
   */
  public function setHandle(resource $handle): this {
    $this->handle_ = $handle;
    return $this;
  }

  /**
   * Gets the meta_data for the current handle
   */
  public function getMetaData(): array<string, mixed> {
    return stream_get_meta_data($this->handle_);
  }

  /**
   * Gets the send timeout.
   *
   * @return int timeout
   */
  public function getSendTimeout(): int {
    return $this->sendTimeout_;
  }

  /**
   * Sets the send timeout.
   *
   * @param int $timeout  Timeout in milliseconds.
   */
  public function setSendTimeout(int $timeout): void {
    $this->sendTimeout_ = $timeout;
  }

  /**
   * Gets the receive timeout.
   *
   * @return int timeout
   */
  public function getRecvTimeout(): int {
    return $this->recvTimeout_;
  }

  /**
   * Sets the receive timeout.
   *
   * @param int $timeout  Timeout in milliseconds.
   */
  public function setRecvTimeout(int $timeout): void {
    $this->recvTimeout_ = $timeout;
  }

  /**
   * Sets debugging output on or off
   *
   * @param bool $debug
   */
  public function setDebug(bool $debug): void {
    $this->debug_ = $debug;
  }

  /**
   * Get the host that this socket is connected to
   *
   * @return string host
   */
  public function getHost(): string {
    return $this->host_;
  }

  /**
   * Get the remote port that this socket is connected to
   *
   * @return int port
   */
  public function getPort(): int {
    return $this->port_;
  }

  /**
   * Get the error string in case of open failure
   *
   * @return errstr_ or null
   */
  public function getErrStr(): ?string {
    return $this->errstr_;
  }

  /**
   * Get the error number in case of open failure
   *
   * @return errno_ or null
   */
  public function getErrNo(): ?int {
    return $this->errno_;
  }

  /**
   * Tests whether this is open
   *
   * @return bool true if the socket is open
   */
  public function isOpen(): bool {
    return is_resource($this->handle_);
  }

  /**
   * Connects the socket.
   */
  public function open(): void {
    if ($this->isOpen()) {
      throw new TTransportException(
        'TSocket: socket already connected',
        TTransportException::ALREADY_OPEN,
      );
    }

    if ($this->host_ === null) {
      throw new TTransportException(
        'TSocket: cannot open null host',
        TTransportException::NOT_OPEN,
      );
    }

    if ($this->port_ <= 0) {
      throw new TTransportException(
        'TSocket: cannot open without port',
        TTransportException::NOT_OPEN,
      );
    }

    $handle = null;
    if ($this->persist_) {
      $handle = @pfsockopen(
        $this->host_,
        $this->port_,
        $this->errno_,
        $this->errstr_,
        $this->sendTimeout_ / 1000.0,
      );
    } else {
      $handle = @fsockopen(
        $this->host_,
        $this->port_,
        $this->errno_,
        $this->errstr_,
        $this->sendTimeout_ / 1000.0,
      );
    }

    // Connect failed?
    if (!$handle) {
      $error =
        'TSocket: could not connect to '.$this->host_.':'.$this->port_;
      $error .= ' ('.$this->errstr_.' ['.$this->errno_.'])';
      if ($this->debug_) {
        call_user_func($this->debugHandler_, $error);
      }
      throw new TTransportException(
        $error,
        TTransportException::COULD_NOT_CONNECT,
      );
    }

    $this->handle_ = $handle;

    $sock_name = stream_socket_get_name($this->handle_, false);
    // IPv6 is returned [2401:db00:20:702c:face:0:7:0]:port
    // or when stream_socket_get_name is buggy it is
    // 2401:db00:20:702c:face:0:7:0:port
    $this->lport_ = end(explode(":", $sock_name));

    stream_set_timeout($this->handle_, 0, $this->sendTimeout_ * 1000);
    $this->sendTimeoutSet_ = true;
  }

  /**
   * Closes the socket.
   */
  public function close(): void {
    if (!$this->persist_) {
      @fclose($this->handle_);
      $this->handle_ = null;
    }
  }

  /**
   *  Test to see if the socket is ready for reading. This method returns
   *  immediately. If calling this method in a loop one should sleep or do
   *  other work else CPU cycles will be wasted.
   *
   *  @return bool  True if a non-blocking read of at least one character can
   *                be preformed on the socket.
   */
  public function isReadable(): bool {
    return $this->isSocketActionable($this->handle_, $check_read = true);
  }

  /**
   *  Test to see if the socket is ready for writing. This method returns
   *  immediately. If calling this method in a loop one should sleep or do
   *  other work else CPU cycles will be wasted.
   *
   *  @return bool True if a non-blocking write can be preformed on the socket.
   */
  public function isWritable(): bool {
    $writable =
      $this->isSocketActionable($this->handle_, $check_read = false);
    if (!$writable && $this->sendTimeout_ > 0) {
      if ($this->writeAttemptStart_ === null) {
        $this->writeAttemptStart_ = microtime(true);
      }
      if (microtime(true) -
          (int) $this->writeAttemptStart_ > ($this->sendTimeout_ / 1000.0)) {
        throw new TTransportException(
          'TSocket: socket not writable after '.$this->sendTimeout_.'ms',
          TTransportException::TIMED_OUT,
        );
      }
    }
    return $writable;
  }

  private function isSocketActionable(
    ?resource $socket,
    bool $check_read,
  ): bool {
    // the socket is technically actionable, although any read or write will
    // fail since close() was already called.
    if ($socket === null) {
      return true;
    }

    $read = $write = array();
    if ($check_read) {
      $read = array($socket);
    } else {
      $write = array($socket);
    }

    $excpt = array();
    $ret = stream_select($read, $write, $excpt, 0, 0);
    if ($ret === false) {
      $error = 'TSocket: stream_select failed on socket.';
      if ($this->debug_) {
        call_user_func($this->debugHandler_, $error);
      }
      throw new TTransportException($error);
    }

    return $ret !== 0;
  }

  /**
   * Reads maximum min($len, $maxReadChunkSize_) bytes
   * from the stream.
   */
  private function readChunk(int $len): ?string {
    if ($this->maxReadChunkSize_ !== null) {
      $len = min($len, $this->maxReadChunkSize_);
    }

    $res = @fread($this->handle_, $len);
    $size = strlen($res);

    $this->onRead($size);
    return $res;
  }

  /**
   * Uses stream get contents to do the reading
   *
   * @param int $len How many bytes
   * @return string Binary data
   */
  public function readAll(int $len): string {
    if ($this->sendTimeoutSet_) {
      $sec = 0;
      if ($this->recvTimeout_ > 1000) {
        $msec = $this->recvTimeout_ % 1000;
        $sec = ($this->recvTimeout_ - $msec) / 1000;
      } else {
        $msec = $this->recvTimeout_;
      }
      stream_set_timeout($this->handle_, $sec, $msec * 1000);
      $this->sendTimeoutSet_ = false;
    }
    // This call does not obey stream_set_timeout values!
    // $buf = @stream_get_contents($this->handle_, $len);
    $pre = '';
    while (true) {
      $t_start = microtime(true);
      $buf = $this->readChunk($len);
      $t_stop = microtime(true);
      if ($buf === null || $buf === '') {
        $read_err_detail = sprintf(
          '%d bytes from %s:%d to localhost:%d. Spent %2.2f ms.',
          $len,
          $this->host_,
          $this->port_,
          $this->lport_,
          ($t_stop - $t_start) * 1000,
        );
        $md = stream_get_meta_data($this->handle_);
        if ($md['timed_out']) {
          throw new TTransportException(
            'TSocket: timeout while reading '.$read_err_detail,
            TTransportException::TIMED_OUT,
          );
        } else {
          $md_str = str_replace("\n", " ", print_r($md, true));
          throw new TTransportException(
            'TSocket: could not read '.$read_err_detail,
            TTransportException::COULD_NOT_READ,
          );
        }
      } else if (($sz = strlen($buf)) < $len) {
        $md = stream_get_meta_data($this->handle_);
        if ($md['timed_out']) {
          $read_err_detail = sprintf(
            '%d bytes from %s:%d to localhost:%d. Spent %2.2f ms.',
            $len,
            $this->host_,
            $this->port_,
            $this->lport_,
            ($t_stop - $t_start) * 1000,
          );
          throw new TTransportException(
            'TSocket: timeout while reading '.$read_err_detail,
            TTransportException::TIMED_OUT,
          );
        } else {
          $pre .= $buf;
          $len -= $sz;
        }
      } else {
        $this->readAttemptStart_ = null;
        $res = $pre.$buf;
        $this->onRead(strlen($res));
        return $res;
      }
    }

    throw new TTransportException("TSocket: You shouldn't be here");
  }

  /**
   * Read from the socket
   *
   * @param int $len How many bytes
   * @return string Binary data
   */
  public function read(int $len): string {
    if ($this->sendTimeoutSet_) {
      stream_set_timeout($this->handle_, 0, $this->recvTimeout_ * 1000);
      $this->sendTimeoutSet_ = false;
    }
    $t_start = microtime(true);
    $data = $this->readChunk($len);
    $t_stop = microtime(true);
    if ($data === null || $data === '') {
      $read_err_detail = sprintf(
        '%d bytes from %s:%d to localhost:%d. Spent %2.2f ms.',
        $len,
        $this->host_,
        $this->port_,
        $this->lport_,
        ($t_stop - $t_start) * 1000,
      );
      $md = stream_get_meta_data($this->handle_);
      if ($md['timed_out']) {
        throw new TTransportException(
          'TSocket: timeout while reading '.$read_err_detail,
          TTransportException::TIMED_OUT,
        );
      } else {
        $md_str = str_replace("\n", " ", print_r($md, true));
        throw new TTransportException(
          'TSocket: could not read '.$read_err_detail,
          TTransportException::COULD_NOT_READ,
        );
      }
    } else {
      $this->readAttemptStart_ = null;
    }

    $this->onRead(strlen($data));
    return $data;
  }

  /**
   * Perform a nonblocking read.
   * @param int $len Number of bytes to read
   * @return string Binary data or '' is no data is read
   */
  public function nonBlockingRead(int $len): string {
    $md = stream_get_meta_data($this->handle_);
    $is_blocking = $md['blocked'];

    // If the stream is currently blocking, we will set to nonblocking
    // first
    if ($is_blocking && !stream_set_blocking($this->handle_, 0)) {
      throw new TTransportException(
        'TSocket: '.'cannot set stream to non-blocking',
      );
    }

    $data = $this->readChunk($len);

    if ($data === null) {
      throw new TTransportException('TSocket: failed in non-blocking read');
    }

    // Switch back to blocking mode is necessary
    if ($is_blocking && !stream_set_blocking($this->handle_, 1)) {
      throw new TTransportException(
        'TSocket: '.'cannot swtich stream back to blocking',
      );
    }
    $this->onRead(strlen($data));
    return $data;
  }

  /**
   * Write to the socket.
   *
   * @param string $buf The data to write
   */
  public function write(string $buf): void {
    if ($this->handle_ === null) {
      throw new TException('TSocket: handle_ is null');
    }

    $this->onWrite(strlen($buf));

    if (!$this->sendTimeoutSet_) {
      stream_set_timeout($this->handle_, 0, $this->sendTimeout_ * 1000);
      $this->sendTimeoutSet_ = true;
    }

    while (strlen($buf) > 0) {
      $buflen = strlen($buf);
      $t_start = microtime(true);
      $got = @fwrite($this->handle_, $buf);
      $write_time = microtime(true) - $t_start;

      if ($got === 0 || !is_int($got)) {
        $read_err_detail = sprintf(
          '%d bytes from %s:%d to localhost:%d. Spent %2.2f ms.',
          $buflen,
          $this->host_,
          $this->port_,
          $this->lport_,
          $write_time * 1000,
        );
        $md = stream_get_meta_data($this->handle_);
        if ($md['timed_out']) {
          throw new TTransportException(
            'TSocket: timeout while writing '.$read_err_detail,
            TTransportException::TIMED_OUT,
          );
        } else {
          $md_str = str_replace("\n", " ", print_r($md, true));
          throw new TTransportException(
            'TSocket: could not write '.$read_err_detail,
            TTransportException::COULD_NOT_WRITE,
          );
        }
      }
      $buf = substr($buf, $got);
    }

    $this->writeAttemptStart_ = null;
  }

  /**
   * Flush output to the socket.
   */
  public function flush(): void {
    $ret = fflush($this->handle_);

    if ($ret === false) {
      throw new TTransportException(
        'TSocket: could not flush '.$this->host_.':'.$this->port_,
      );
    }
  }
}


<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

/** Inherits from Socket */

/**
 * Sockets implementation of the TTransport interface that allows connection
 * to a pool of servers.
 *
 * @package thrift.transport
 */
class TSocketPool extends TSocket {

  /**
   * Custom caching functions for environments without AP
   */
  private ?(function(string): mixed) $fakeAPCFetch_ = null;
  private ?(function(string, mixed, int): bool) $fakeAPCStore_ = null;
  private ?(function(string): void) $apcLogger_ = null;

  /**
   * Remote servers. List of host:port pairs.
   */
  private array<(string, int)> $servers_ = array();

  /**
   * How many times to retry each host in connect
   *
   * @var int
   */
  private int $numRetries_ = 1;

  /**
   * Retry interval in seconds, how long to not try a host if it has been
   * marked as down.
   *
   * @var int
   */
  private int $retryInterval_ = 60;

  /**
   * Max consecutive failures before marking a host down.
   *
   * @var int
   */
  private int $maxConsecutiveFailures_ = 1;

  /**
   * Try hosts in order? or Randomized?
   *
   * @var bool
   */
  private bool $randomize_ = true;

  /**
   * Always try last host, even if marked down?
   *
   * @var bool
   */
  private bool $alwaysTryLast_ = true;

  /**
   * Always retry the host without wait if there was a transient
   * connection failure (such as Resource temporarily unavailable).
   * If this is set, the wait_and_retry mechanism will ignore the
   * value in $retryInterval_ for transient failures.
   *
   * @var bool
   */
  private bool $alwaysRetryForTransientFailure_ = false;

  const int ERROR_RESOURCE_TEMPORARILY_UNAVAILABLE = 11;

  /**
   * Socket pool constructor
   *
   * @param array  $hosts        List of remote hostnames
   * @param mixed  $ports        Array of remote ports, or a single common port
   * @param bool   $persist      Whether to use a persistent socket
   * @param mixed  $debugHandler Function for error logging
   */
  public function __construct(
    KeyedContainer<int, string> $hosts = array('localhost'),
    KeyedContainer<int, int> $ports = array(9090),
    bool $persist = false,
    ?(function(string): bool) $debugHandler = null,
  ) {
    parent::__construct('', 0, $persist, $debugHandler);

    foreach ($hosts as $key => $host) {
      $this->servers_[] = tuple((string) $host, (int) $ports[$key]);
    }
  }

  /**
   * Add a server to the pool
   *
   * This function does not prevent you from adding a duplicate server entry.
   *
   * @param string $host hostname or IP
   * @param int $port port
   */
  public function addServer(string $host, int $port): void {
    $this->servers_[] = tuple($host, $port);
  }

  /**
   * Sets how many time to keep retrying a host in the connect function.
   *
   * @param int $numRetries
   */
  public function setNumRetries(int $numRetries): void {
    $this->numRetries_ = $numRetries;
  }

  /**
   * Sets how long to wait until retrying a host if it was marked down
   *
   * @param int $numRetries
   */
  public function setRetryInterval(int $retryInterval): void {
    $this->retryInterval_ = $retryInterval;
  }

  /**
   * Sets how many time to keep retrying a host before marking it as down.
   *
   * @param int $numRetries
   */
  public function setMaxConsecutiveFailures(
    int $maxConsecutiveFailures,
  ): void {
    $this->maxConsecutiveFailures_ = $maxConsecutiveFailures;
  }

  /**
   * Turns randomization in connect order on or off.
   *
   * @param bool $randomize
   */
  public function setRandomize(bool $randomize): void {
    $this->randomize_ = $randomize;
  }

  /**
   * Whether to always try the last server.
   *
   * @param bool $alwaysTryLast
   */
  public function setAlwaysTryLast(bool $alwaysTryLast): void {
    $this->alwaysTryLast_ = $alwaysTryLast;
  }

  /**
   * Whether to always retry the host without wait for
   * transient connection failures
   *
   * @param bool $alwaysRetry
   */
  public function setAlwaysRetryForTransientFailure(bool $alwaysRetry): void {
    $this->alwaysRetryForTransientFailure_ = $alwaysRetry;
  }

  /**
   * Connects the socket by iterating through all the servers in the pool
   * and trying to find one that:
   * 1. is not marked down after consecutive failures
   * 2. can really be connected to
   *
   * @return bool  false: any IP in the pool failed to connect before returning
   *               true: no failures
   */
  public function open(): void {
    // Check if we want order randomization
    if ($this->randomize_) {
      // warning: don't use shuffle here because it leads to uneven
      // load distribution
      $n = count($this->servers_);
      $s = $this->servers_;
      for ($i = 1; $i < $n; $i++) {
        $j = mt_rand(0, $i);
        $tmp = $s[$i];
        $s[$i] = $s[$j];
        $s[$j] = $tmp;
      }
      $this->servers_ = $s;
    }

    // Count servers to identify the "last" one
    $numServers = count($this->servers_);
    $has_conn_errors = false;

    $fail_reason = array(); // reasons of conn failures
    for ($i = 0; $i < $numServers; ++$i) {

      // host port is stored as an array
      list($host, $port) = $this->servers_[$i];

      $failtimeKey = TSocketPool::getAPCFailtimeKey($host, $port);
      // Cache miss? Assume it's OK
      $lastFailtime = (int) $this->apcFetch($failtimeKey);
      $this->apcLog(
        "TSocketPool: host $host:$port last fail time: ".$lastFailtime,
      );

      $retryIntervalPassed = false;

      // Cache hit...make sure enough the retry interval has elapsed
      if ($lastFailtime > 0) {
        $elapsed = time() - $lastFailtime;
        if ($elapsed > $this->retryInterval_) {
          $retryIntervalPassed = true;
          if ($this->debug_ && $this->debugHandler_ !== null) {
            $dh = $this->debugHandler_;
            $dh(
              'TSocketPool: retryInterval '.
              '('.
              $this->retryInterval_.
              ') '.
              'has passed for host '.
              $host.
              ':'.
              $port,
            );
          }
        }
      }

      // Only connect if not in the middle of a fail interval, OR if this
      // is the LAST server we are trying, just hammer away on it
      $isLastServer = false;
      if ($this->alwaysTryLast_) {
        $isLastServer = ($i == ($numServers - 1));
      }

      if (($lastFailtime === 0) ||
          ($isLastServer) ||
          ($lastFailtime > 0 && $retryIntervalPassed)) {

        // Set underlying TSocket params to this one
          $this->host_ = $host;
        $this->port_ = $port;

        // Try up to numRetries_ connections per server
        for ($attempt = 0; $attempt < $this->numRetries_; $attempt++) {
          try {
            // Use the underlying TSocket open function
            parent::open();

            // Only clear the failure counts if required to do so
            if ($lastFailtime > 0) {
              $this->apcStore($failtimeKey, 0);
            }

            // Successful connection, return now
            return;

          } catch (TException $tx) {
            // Connection failed
            // keep the reason for the last try
            $errstr = $this->getErrStr();
            $errno = $this->getErrNo();
            if ($errstr !== null || $errno !== null) {
              $fail_reason[$i] = '('.$errstr.'['.$errno.'])';
            } else {
              $fail_reason[$i] = '(?)';
            }
          }
        }

        // For transient errors (like Resource temporarily unavailable),
        // we might want not to cache the failure.
        if ($this->alwaysRetryForTransientFailure_ &&
            $this->isTransientConnectFailure($this->getErrNo())) {
          continue;
        }

        $dh = ($this->debug_ ? $this->debugHandler_ : null);

        $has_conn_errors = $this->recordFailure(
          $host,
          $port,
          $this->maxConsecutiveFailures_,
          $this->retryInterval_,
          $dh,
        );
      } else {
        $fail_reason[$i] = '(cached-down)';
      }
    }

    // Holy shit we failed them all. The system is totally ill!
    $error = 'TSocketPool: All hosts in pool are down. ';
    $hosts = array();
    foreach ($this->servers_ as $i => $server) {
      // array(host, port) (reasons, if exist)
      list($host, $port) = $server;
      $h = $host.':'.$port;
      if (array_key_exists($i, $fail_reason)) {
        $h .= (string) $fail_reason[$i];
      }
      $hosts[] = $h;
    }
    $hostlist = implode(',', $hosts);
    $error .= '('.$hostlist.')';
    if ($this->debug_ && $this->debugHandler_ !== null) {
      $dh = $this->debugHandler_;
      $dh($error);
    }
    throw new TTransportException($error);
  }

  public static function getAPCFailtimeKey(string $host, int $port): string {
    // Check APC cache for a record of this server being down
    return 'thrift_failtime:'.$host.':'.$port.'~';
  }

  /**
   * Record the failure in APC
   * @param string  host  dest IP
   * @param int     port  dest port
   * @param int     max_failures   max consec errors before mark host/port down
   * @param int     down_period    how long to mark the host/port down
   *
   * @return bool  if mark a host/port down
   */
  public function recordFailure(
    string $host,
    int $port,
    int $max_failures,
    int $down_period,
    ?(function(string): bool) $log_handler = null,
  ): bool {
    $marked_down = false;
    // Mark failure of this host in the cache
    $failtimeKey = self::getAPCFailtimeKey($host, $port);
    $consecfailsKey = 'thrift_consecfails:'.$host.':'.$port.'~';

    // Ignore cache misses
    $consecfails = $this->apcFetch($consecfailsKey);
    if ($consecfails === false) {
      $consecfails = 0;
    }
    $consecfails = (int) $consecfails;

    // Increment by one
    $consecfails++;

    // Log and cache this failure
    if ($consecfails >= $max_failures) {
      if ($log_handler !== null) {
        $log_handler(
          'TSocketPool: marking '.
          $host.
          ':'.
          $port.
          ' as down for '.
          $down_period.
          ' secs '.
          'after '.
          $consecfails.
          ' failed attempts.',
        );
      }
      // Store the failure time
      $curr_time = time();
      $this->apcStore($failtimeKey, $curr_time);
      $this->apcLog(
        'TSocketPool: marking '.
        $host.
        ':'.
        $port.
        ' as down for '.
        $down_period.
        ' secs '.
        'after '.
        $consecfails.
        ' failed attempts.('.
        "max_failures=$max_failures)",
      );
      $marked_down = true;

      // Clear the count of consecutive failures
      $this->apcStore($consecfailsKey, 0);
    } else {
      $this->apcLog(
        "TSocketPool: increased $host:$port consec fails to ".$consecfails,
      );
      $this->apcStore($consecfailsKey, $consecfails);
    }

    return $marked_down;
  }

  /**
   * !! To call this API, you must give apc_is_useful() as the last param
   */
  public function overrideAPCFunctions(
    ?(function(string): mixed) $fake_apc_fetch,
    ?(function(string, mixed, int): bool) $fake_apc_store,
    ?(function(string): void) $logger,
    bool $apc_is_useful,
  ): void {
    if (!$apc_is_useful) {
      $this->fakeAPCFetch_ = $fake_apc_fetch;
      $this->fakeAPCStore_ = $fake_apc_store;
      $this->apcLogger_ = $logger;
    }
  }

  /**
   * Wrapper function around apc_fetch to be able to fetch from fake APC
   * when there is no real APC
   */
  protected function apcFetch(string $key): mixed {
    if ($this->fakeAPCFetch_ === null) {
      return apc_fetch($key);
    } else {
      // try fake APC here
      $ff = $this->fakeAPCFetch_;
      return $ff($key);
    }
  }

  /**
   * wrapper function around apc_store to be able to store variable to fake APC
   * when there is no real APC
   */
  protected function apcStore(string $key, mixed $value, int $ttl = 0): bool {
    if ($this->fakeAPCStore_ === null) {
      return apc_store($key, $value, $ttl);
    } else {
      // try fake APC here
      $fs = $this->fakeAPCStore_;
      return $fs($key, $value, $ttl);
    }
  }

  protected function apcLog(string $message): void {
    if ($this->apcLogger_ !== null) {
      $logger = $this->apcLogger_;
      $logger($message);
    }
  }

  /**
   * Whether a connection failure is a transient failure
   * based on the error-code
   *
   * @param int $error_code
   * @return bool true or false
   */
  private function isTransientConnectFailure(?int $error_code): bool {
    if ($error_code === null) {
      return false;
    }
    // todo: add more to this list
    switch ($error_code) {
      case self::ERROR_RESOURCE_TEMPORARILY_UNAVAILABLE:
        return true;
      default:
        return false;
    }
  }
}


<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

/**
 * Base interface for a transport agent.
 *
 * @package thrift.transport
 */
abstract class TTransport {

  /**
   * Name of the service targeted (for logging).
   */
  protected ?string $service_ = null;

  /**
   * Whether this transport is open.
   *
   * @return boolean true if open
   */
  public abstract function isOpen(): bool;

  /**
   * Open the transport for reading/writing
   *
   * @throws TTransportException if cannot open
   */
  public abstract function open(): void;

  /**
   * Close the transport.
   */
  public abstract function close(): void;

  /**
   * Read some data into the array.
   *
   * @param int    $len How much to read
   * @return string The data that has been read
   * @throws TTransportException if cannot read any more data
   */
  public abstract function read(int $len): string;

  /**
   * Guarantees that the full amount of data is read.
   *
   * @return string The data, of exact length
   * @throws TTransportException if cannot read data
   */
  public function readAll(int $len): string {
    // return $this->read($len);

    $data = '';
    $got = 0;
    while (($got = strlen($data)) < $len) {
      $data .= $this->read($len - $got);
    }
    return $data;
  }

  /**
   * Writes the given data out.
   *
   * @param string $buf  The data to write
   * @throws TTransportException if writing fails
   */
  public abstract function write(string $buf): void;

  /**
   * Flushes any pending data out of a buffer
   *
   * @throws TTransportException if a writing error occurs
   */
  public function flush(): void {}

  /**
   * Flushes any pending data out of a buffer for a oneway call
   *
   * @throws TTransportException if a writing error occurs
   */
  public function onewayFlush(): void {
    // Default to flush()
    $this->flush();
  }
}


<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

/**
 * Transport exceptions
 *
 * Types deriving from TTransport exception may not be able to
 * translate their custom error into the set of error code
 * supported by TTransportException. For that the $shortMessage
 * facility is provided.
 *
 * @param mixed  $message Message (string) or type-spec (array)
 * @param mixed  $code Code (integer) or values (array)
 * @param string $shortMessage (string)
 */
class TTransportException extends TException {

  const UNKNOWN = 0;
  const NOT_OPEN = 1;
  const ALREADY_OPEN = 2;
  const TIMED_OUT = 3;
  const END_OF_FILE = 4;
  const INVALID_CLIENT = 5;
  const INVALID_FRAME_SIZE = 6;
  const INVALID_TRANSFORM = 7;
  const COULD_NOT_CONNECT = 8;
  const COULD_NOT_READ = 9;
  const COULD_NOT_WRITE = 10;

  protected string $shortMessage;

  public function __construct(
    ?string $message = null,
    int $code = 0,
    string $short_message = '',
  ) {
    $this->shortMessage = $short_message;
    parent::__construct($message, $code);
  }

  public function getShortMessage(): string {
    return $this->shortMessage;
  }
}


<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

class TTransportFactory {
  /**
   * @static
   * @param TTransport $transport
   * @return TTransport
   */
  public function getTransport(TTransport $transport): TTransport {
    return $transport;
  }
}


<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

/**
 *  Determine (as best as possible) whether the transport can preform
 *  non-blocking read and write operations.
 */
interface TTransportStatus {

  /**
   *  Test whether the transport is ready for a non-blocking read. It is
   *  possible, though, that a transport is ready for a partial read, but a full
   *  read will block.
   *
   *  In the case a transport becomes unavailable for reading due to an error
   *  an exception should be raised. Any timeout logic should also raise an
   *  exception.
   *
   *  @return bool True if a non-blocking read can be preformed on the
   *               transport.
   */
  public function isReadable(): bool;

  /**
   *  Test whether the transport is ready for a non-blocking write.
   *
   *  In the case a transport becomes unavailable for writing due to an error
   *  an exception should be raised. Any timeout logic should also raise an
   *  exception.
   *
   *  @return bool True if a non-blocking write can be preformed on the
   *               transport.
   */
  public function isWritable(): bool;
}


<?hh
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * 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.
 *
 * @package thrift.transport
 */

interface TTransportSupportsHeaders {
  public function setHeader(string $str_key, @string $str_value): this;
  public function setPersistentHeader(
    string $str_key,
    @string $str_value,
  ): this;
  public function getWriteHeaders(): KeyedContainer<string, string>;
  public function getPersistentWriteHeaders(): KeyedContainer<string, string>;
  public function getHeaders(): KeyedContainer<string, string>;
  public function clearHeaders(): void;
  public function clearPersistentHeaders(): void;
}


//Copyright (c) Meta Platforms, Inc. and affiliates.
//
//This source code is licensed under the MIT license found in the
//LICENSE file in the root directory of this source tree.

<?hh

abstract class BinaryClassificationMultistageInference{
  /**
   * Returns the combined bin key in which `$data` falls according to the
   * quantile bins found in `$quantiles`.
   */
  private function dataToCombinedBin(
    vec<vec<float>> $quantiles,
    vec<float> $data,
  ): string {
    $num_quantile_bins = C\count($quantiles);
    $num_features = C\count($quantiles[0]);

    $combined_bin = "";
    for ($j = 0; $j < $num_features; $j++) {
      $quantile_bin = 0;
      for ($i = 0; $i < $num_quantile_bins; $i++) {
        if ($data[$j] > $quantiles[$i][$j]) {
          $quantile_bin++;
        }
      }
      $combined_bin .= $quantile_bin;
    }
    return $combined_bin;
  }

  /**
   * Normalizes data to the combined bin metrics found in `$weights_info`. Then,
   * performs logistic regression inference on `$data`.
   */
  private function lrInference(
    dict<string, vec<float>> $weights_info,
    vec<float> $data,
  ): float {
    $mean = vec($weights_info["mean"]);
    $std = vec($weights_info["std"]);
    $eps = vec($weights_info["eps"]);
    $weights = vec($weights_info["weights"]);
    $len_data_row = C\count($data);
    for ($i = 0; $i < $len_data_row; $i++) {
      $data[$i] = ($data[$i] - $mean[$i]) / ($std[$i] + $eps[0]);
    }
    $sum = 0.0;
    $sum += $weights[0];
    for ($i = 0; $i < $len_data_row; $i++) {
      $sum += $data[$i] * $weights[$i + 1];
    }
    $prob = 1 / (1 + Math\exp(-$sum));
    return $prob;
  }

  /**
   * Return the features values (provided by `$id_to_feature_value`) of the
   * `$feature_ids` vector in that order. Features not found are set to zero.
   * Features are normalized according to `$data_mean`, `$data_std`, and `$eps`.
   */
  private function getData(
    dict<string, string> $id_to_feature_value,
    vec<string> $feature_ids,
    vec<float> $data_mean,
    vec<float> $data_std,
    float $eps,
  ): vec<float> {
    $num_features = C\count($feature_ids);
    $data = vec[];
    for ($i = 0; $i < $num_features; $i++) {
      if (C\contains_key($id_to_feature_value, $feature_ids[$i])) {
        $feature_val = (float)$id_to_feature_value[$feature_ids[$i]];
      } else {
        $feature_val = 0.0;
      }
      $data[] = ($feature_val - $data_mean[$i]) / ($data_std[$i] + $eps);
    }
    return $data;
  }

  /**
   * This first stage inference is called first by genProbability to attempt a
   * quick and accurate inference using the LRwBins model. The function returns
   * the probability of the logistic regression model or -1 if first-stage
   * inference cannot handle the data.
   */
  private async function genFirstStageInference(
    this::TContext $context,
  ): Awaitable<?float> {
    // load in the first-stage inference model
    $config_name = "experimental/danielsjohnson/model";
    $raw_config = Configerator::getRaw($config_name);
    $json_a = fb_json_decode($raw_config, true);

    // access the loop features
    $feature_handler = await $this->genConfigBackedFeatureHandler();
    $features_wrapper = await $feature_handler->genFeaturesAndContext(
      new LooperMemoizingWrapper($context),
    );
    list($features, $_contexts) = $features_wrapper;
    $ids = $features->getIDs();

    // map feature ids to feature values
    $id_to_feature_value = dict[];
    foreach ($ids as $id) {
      $id_to_feature_value[(string)$id] =
        PHP\strval($features->getFeature($id));
    }

    // create vector of feature values in the order used to construct the
    // combined bins (the order found in `$bin_feature_ids`)
    $eps = (float)$json_a["eps"];
    $bin_feature_ids = vec($json_a["bin_feature_ids"]);
    $bin_data_mean = vec($json_a["bin_data_mean"]);
    $bin_data_std = vec($json_a["bin_data_std"]);
    $bin_data = $this->getData(
      $id_to_feature_value,
      $bin_feature_ids,
      $bin_data_mean,
      $bin_data_std,
      $eps,
    );

    // get the combined bin key of the combined bin that `$bin_data` falls into
    $quantiles = vec($json_a["quantiles"]);
    $combined_bin = $this->dataToCombinedBin($quantiles, $bin_data);

    // check if the first-stage inference model is used for this combined bin
    $combined_bin_weights = $json_a["combined_bins"];
    if (C\contains_key($combined_bin_weights, $combined_bin)) {
      // first-stage inference is used for this prediction
      // create vector of feature values in the same order as their corresponding
      // logistic regression weights (the order found in `$inference_feature_ids`)
      $inference_feature_ids = vec($json_a["inference_feature_ids"]);
      $inference_data_mean = vec($json_a["inference_data_mean"]);
      $inference_data_std = vec($json_a["inference_data_std"]);
      $inference_data = $this->getData(
        $id_to_feature_value,
        $inference_feature_ids,
        $inference_data_mean,
        $inference_data_std,
        $eps,
      );

      // access the combined bin weights and perform logistic regression
      // inference on `$inference_data`
      $prob = $this->lrInference(
        $combined_bin_weights[$combined_bin],
        $inference_data,
      );
    } else {
      // first-stage inference is not used for this combined bin
      $prob = -1.0;
    }
    return $prob;
  }
}


<?php
// Copyright 2004-present Facebook.  All rights reserved.
phutil_register_library('mysql-source-arcanist', __FILE__);


<?php

/**
 * This file is automatically generated. Use 'arc liberate' to rebuild it.
 * @generated
 * @phutil-library-version 2
 */

phutil_register_library_map(array(
  '__library_version__' => 2,
  'class' =>
  array(
    'BaseDirectoryScopedFormatLinter' => 'lint/linter/BaseDirectoryScopedFormatLinter.php',
    'FacebookInnoDBTabLinter' => 'lint/linter/FacebookInnoDBTabLinter.php',
    'FacebookMySQLArcanistConfiguration' => 'arcanist/FacebookMySQLArcanistConfiguration.php',
    'FacebookMySQLAssertUsageLinter' => 'lint/linter/FacebookMySQLAssertUsageLinter.php',
    'FacebookMySQLLintEngine' => 'lint/FacebookMySQLLintEngine.php',
    'FacebookMySQLLinter' => 'lint/linter/FacebookMySQLLinter.php',
    'FacebookMySQLPrintfLinter' => 'lint/linter/FacebookMySQLPrintfLinter.php',
    'FacebookMySQLTestResultLinter' => 'lint/linter/FacebookMySQLTestResultLinter.php',
  ),
  'function' =>
  array(
  ),
  'xmap' =>
  array(
    'BaseDirectoryScopedFormatLinter' => 'ArcanistLinter',
    'FacebookInnoDBTabLinter' => 'ArcanistLinter',
    'FacebookMySQLArcanistConfiguration' => 'ArcanistConfiguration',
    'FacebookMySQLAssertUsageLinter' => 'ArcanistLinter',
    'FacebookMySQLClangFormatLinter' => 'BaseDirectoryScopedFormatLinter',
    'FacebookMySQLLintEngine' => 'ArcanistLintEngine',
    'FacebookMySQLLinter' => 'ArcanistLinter',
    'FacebookMySQLPrintfLinter' => 'ArcanistLinter',
    'FacebookMySQLTestResultLinter' => 'ArcanistLinter',
  ),
));


<?php
// Copyright 2004-present Facebook. All Rights Reserved.

class FacebookMySQLArcanistConfiguration extends ArcanistConfiguration {
  // This hook is called just after this class is instantiated.
  public function updateConfig($working_copy, $configuration_manager) {
    $using_sandcastle = self::isUsingSandcastle();
    $console = PhutilConsole::getConsole();

    $tools_config_file = self::pathInTools(false, $working_copy, '.arcconfig');
    if (!Filesystem::pathExists($tools_config_file)) {
      // At this point we have determined that we aren't running in the
      // environment where there's a `tools` directory at the same level as
      // MySQL source code. Let's check if we're inside Sandcastle first.
      if ($using_sandcastle) {
        $tools_config_file = self::pathInTools(
          true, $working_copy, '.arcconfig');
        $console->writeErr(
          "Operating in Sandcastle. Using: %s\n", $tools_config_file);
      } else {
        $console->writeErr(
          "Operating in Facebook-external mode. File not found: %s\n",
          $tools_config_file
        );

        return;
      }
    }

    $project_root = $working_copy->getProjectRoot();
    $configuration_manager->setRuntimeConfig("lint.engine",
      "FacebookMySQLLintEngine");
    $configuration_manager->setRuntimeConfig("lint.cpplint.prefix",
      $project_root . "/arcanist/lint/cpp_linter/");
    $configuration_manager->setRuntimeConfig("lint.cpplint.options",
      "--filter=-build/include_order,-whitespace/braces,-whitespace/newline");
    $console->writeErr("Linters are enabled.\n");

    $tools_config_data = Filesystem::readFile($tools_config_file);
    $tools_config = json_decode($tools_config_data, true);
    if (!is_array($tools_config)) {
      throw new ArcanistUsageException(
        $tools_config_file." is not a valid JSON file."
      );
    }

    // Load the tool's libraries too.
    foreach (idx($tools_config, 'load', array()) as $location) {
      $full_path = self::pathInTools(
        $using_sandcastle, $working_copy, $location);

      $console->writeErr("Loading library from ".$full_path."\n");
      phutil_load_library($full_path);
    }

    $keys_to_merge = array('unit.engine', 'events.listeners');
    foreach ($keys_to_merge as $key) {
      $value = idx($tools_config, $key);
      if (!is_null($value)) {
        $configuration_manager->setRuntimeConfig($key, $value);
      }
    }
  }

  public static function isUsingSandcastle() {
    return getenv("SANDCASTLE");
  }

  public static function pathInTools($using_sandcastle,
                                     $working_copy, $suffix) {
    if ($using_sandcastle) {
      $project_root = $working_copy->getProjectRoot();
      return $project_root.'/tools/'.$suffix;
    } else {
      $project_root = $working_copy->getProjectRoot();
      return $project_root.'/../tools/'.$suffix;
    }
  }

}