Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / notification / client / PhabricatorNotificationServerRef.php
blobf3a49afc9f92345d275279f475c767ac8091475a
1 <?php
3 final class PhabricatorNotificationServerRef
4 extends Phobject {
6 private $type;
7 private $host;
8 private $port;
9 private $protocol;
10 private $path;
11 private $isDisabled;
13 const KEY_REFS = 'notification.refs';
15 public function setType($type) {
16 $this->type = $type;
17 return $this;
20 public function getType() {
21 return $this->type;
24 public function setHost($host) {
25 $this->host = $host;
26 return $this;
29 public function getHost() {
30 return $this->host;
33 public function setPort($port) {
34 $this->port = $port;
35 return $this;
38 public function getPort() {
39 return $this->port;
42 public function setProtocol($protocol) {
43 $this->protocol = $protocol;
44 return $this;
47 public function getProtocol() {
48 return $this->protocol;
51 public function setPath($path) {
52 $this->path = $path;
53 return $this;
56 public function getPath() {
57 return $this->path;
60 public function setIsDisabled($is_disabled) {
61 $this->isDisabled = $is_disabled;
62 return $this;
65 public function getIsDisabled() {
66 return $this->isDisabled;
69 public static function getLiveServers() {
70 $cache = PhabricatorCaches::getRequestCache();
72 $refs = $cache->getKey(self::KEY_REFS);
73 if (!$refs) {
74 $refs = self::newRefs();
75 $cache->setKey(self::KEY_REFS, $refs);
78 return $refs;
81 public static function newRefs() {
82 $configs = PhabricatorEnv::getEnvConfig('notification.servers');
84 $refs = array();
85 foreach ($configs as $config) {
86 $ref = id(new self())
87 ->setType($config['type'])
88 ->setHost($config['host'])
89 ->setPort($config['port'])
90 ->setProtocol($config['protocol'])
91 ->setPath(idx($config, 'path'))
92 ->setIsDisabled(idx($config, 'disabled', false));
93 $refs[] = $ref;
96 return $refs;
99 public static function getEnabledServers() {
100 $servers = self::getLiveServers();
102 foreach ($servers as $key => $server) {
103 if ($server->getIsDisabled()) {
104 unset($servers[$key]);
108 return array_values($servers);
111 public static function getEnabledAdminServers() {
112 $servers = self::getEnabledServers();
114 foreach ($servers as $key => $server) {
115 if (!$server->isAdminServer()) {
116 unset($servers[$key]);
120 return array_values($servers);
123 public static function getEnabledClientServers($with_protocol) {
124 $servers = self::getEnabledServers();
126 foreach ($servers as $key => $server) {
127 if ($server->isAdminServer()) {
128 unset($servers[$key]);
129 continue;
132 $protocol = $server->getProtocol();
133 if ($protocol != $with_protocol) {
134 unset($servers[$key]);
135 continue;
139 return array_values($servers);
142 public function isAdminServer() {
143 return ($this->type == 'admin');
146 public function getURI($to_path = null) {
147 if ($to_path === null || !strlen($to_path)) {
148 $to_path = '';
149 } else {
150 $to_path = ltrim($to_path, '/');
153 $base_path = $this->getPath();
154 if ($base_path === null || !strlen($base_path)) {
155 $base_path = '';
156 } else {
157 $base_path = rtrim($base_path, '/');
159 $full_path = $base_path.'/'.$to_path;
161 $uri = id(new PhutilURI('http://'.$this->getHost()))
162 ->setProtocol($this->getProtocol())
163 ->setPort($this->getPort())
164 ->setPath($full_path);
166 $instance = PhabricatorEnv::getEnvConfig('cluster.instance');
167 if ($instance !== null && strlen($instance)) {
168 $uri->replaceQueryParam('instance', $instance);
171 return $uri;
174 public function getWebsocketURI($to_path = null) {
175 $instance = PhabricatorEnv::getEnvConfig('cluster.instance');
176 if ($instance !== null && strlen($instance)) {
177 $to_path = $to_path.'~'.$instance.'/';
180 $uri = $this->getURI($to_path);
182 if ($this->getProtocol() == 'https') {
183 $uri->setProtocol('wss');
184 } else {
185 $uri->setProtocol('ws');
188 return $uri;
191 public function testClient() {
192 if ($this->isAdminServer()) {
193 throw new Exception(
194 pht('Unable to test client on an admin server!'));
197 $server_uri = $this->getURI();
199 try {
200 id(new HTTPSFuture($server_uri))
201 ->setTimeout(2)
202 ->resolvex();
203 } catch (HTTPFutureHTTPResponseStatus $ex) {
204 // This is what we expect when things are working correctly.
205 if ($ex->getStatusCode() == 501) {
206 return true;
208 throw $ex;
211 throw new Exception(
212 pht('Got HTTP 200, but expected HTTP 501 (WebSocket Upgrade)!'));
215 public function loadServerStatus() {
216 if (!$this->isAdminServer()) {
217 throw new Exception(
218 pht(
219 'Unable to load server status: this is not an admin server!'));
222 $server_uri = $this->getURI('/status/');
224 list($body) = $this->newFuture($server_uri)
225 ->resolvex();
227 return phutil_json_decode($body);
230 public function postMessage(array $data) {
231 if (!$this->isAdminServer()) {
232 throw new Exception(
233 pht('Unable to post message: this is not an admin server!'));
236 $server_uri = $this->getURI('/');
237 $payload = phutil_json_encode($data);
239 $this->newFuture($server_uri, $payload)
240 ->setMethod('POST')
241 ->resolvex();
244 private function newFuture($uri, $data = null) {
245 if ($data === null) {
246 $future = new HTTPSFuture($uri);
247 } else {
248 $future = new HTTPSFuture($uri, $data);
251 $future->setTimeout(2);
253 // At one point, a HackerOne researcher reported a "Location:" redirect
254 // attack here (if the attacker can gain control of the notification
255 // server or the configuration).
257 // Although this attack is not particularly concerning, we don't expect
258 // Aphlict to ever issue a "Location:" header, so receiving one indicates
259 // something is wrong and declining to follow the header may make debugging
260 // easier.
262 $future->setFollowLocation(false);
264 return $future;