Completed
Push — master ( dd13f3...29c3ff )
by Vitor
13s
created

Gateway   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 149
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 2
Bugs 0 Features 1
Metric Value
wmc 27
eloc 90
c 2
b 0
f 1
dl 0
loc 149
ccs 0
cts 97
cp 0
rs 10

3 Methods

Rating   Name   Duplication   Size   Complexity  
A cliConfigure() 0 21 2
A __construct() 0 7 1
D send() 0 103 24
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * SPDX-FileCopyrightText: 2024 Christoph Wurst <[email protected]>
7
 * SPDX-License-Identifier: AGPL-3.0-or-later
8
 */
9
10
namespace OCA\TwoFactorGateway\Provider\Channel\Signal;
11
12
use OCA\TwoFactorGateway\Exception\MessageTransmissionException;
13
use OCA\TwoFactorGateway\Provider\Gateway\AGateway;
14
use OCP\AppFramework\Utility\ITimeFactory;
15
use OCP\Http\Client\IClientService;
16
use OCP\IAppConfig;
17
use Psr\Log\LoggerInterface;
18
use Symfony\Component\Console\Helper\QuestionHelper;
19
use Symfony\Component\Console\Input\InputInterface;
20
use Symfony\Component\Console\Output\OutputInterface;
21
use Symfony\Component\Console\Question\Question;
22
23
/**
24
 * An integration of https://gitlab.com/morph027/signal-web-gateway
25
 *
26
 * @method string getUrl()
27
 * @method AGateway setUrl(string $url)
28
 * @method string getAccount()
29
 * @method AGateway setAccount(string $account)
30
 */
31
class Gateway extends AGateway {
32
	public const SCHEMA = [
33
		'name' => 'Signal',
34
		'instructions' => 'The gateway can send authentication to your Signal mobile and deskop app.',
35
		'fields' => [
36
			['field' => 'url', 'prompt' => 'Please enter the URL of the Signal gateway (leave blank to use default):', 'default' => 'http://localhost:5000'],
37
			['field' => 'account', 'prompt' => 'Please enter the account (phone-number) of the sending signal account (leave blank if a phone-number is not required):', 'default' => ''],
38
		],
39
	];
40
	public const ACCOUNT_UNNECESSARY = 'unneccessary';
41
42
	public function __construct(
43
		public IAppConfig $appConfig,
44
		private IClientService $clientService,
45
		private ITimeFactory $timeFactory,
46
		private LoggerInterface $logger,
47
	) {
48
		parent::__construct($appConfig);
49
	}
50
51
	#[\Override]
52
	public function send(string $identifier, string $message, array $extra = []): void {
53
		$client = $this->clientService->newClient();
54
		// determine type of gateway
55
56
		// test for native signal-cli JSON RPC.
57
		$response = $client->post(
58
			$this->getUrl() . '/api/v1/rpc',
59
			[
60
				'http_errors' => false,
61
				'json' => [
62
					'jsonrpc' => '2.0',
63
					'method' => 'version',
64
					'id' => 'version_' . $this->timeFactory->getTime(),
65
				],
66
			]);
67
		if ($response->getStatusCode() === 200 || $response->getStatusCode() === 201) {
68
			// native signal-cli JSON RPC.
69
70
			// Groups have to be detected and passed with the "group-id" parameter. We assume a group is given as base64 encoded string
71
			$groupId = base64_decode($identifier, strict: true);
72
			$isGroup = $groupId !== false && base64_encode($groupId) === $identifier;
73
			$recipientKey = $isGroup ? 'group-id' : 'recipient';
74
			$params = [
75
				'message' => $message,
76
				$recipientKey => $identifier,
77
				'account' => $this->getAccount(), // mandatory for native RPC API
78
			];
79
			$response = $response = $client->post(
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
80
				$this->getUrl() . '/api/v1/rpc',
81
				[
82
					'json' => [
83
						'jsonrpc' => '2.0',
84
						'method' => 'send',
85
						'id' => 'code_' . $this->timeFactory->getTime(),
86
						'params' => $params,
87
					],
88
				]);
89
			$body = $response->getBody();
90
			$json = json_decode($body, true);
0 ignored issues
show
Bug introduced by
It seems like $body can also be of type null and resource; however, parameter $json of json_decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

90
			$json = json_decode(/** @scrutinizer ignore-type */ $body, true);
Loading history...
91
			$statusCode = $response->getStatusCode();
92
			// The 201 "created" is probably a bug.
93
			if ($statusCode < 200 || $statusCode >= 300 || is_null($json) || !is_array($json) || ($json['jsonrpc'] ?? null) != '2.0' || !isset($json['result']['timestamp'])) {
94
				throw new MessageTransmissionException("error reported by Signal gateway, status=$statusCode, body=$body}");
95
			}
96
		} else {
97
			// Try gateway in the style of https://gitlab.com/morph027/signal-cli-dbus-rest-api
98
			$response = $client->get($this->getUrl() . '/v1/about');
99
			if ($response->getStatusCode() === 200) {
100
				// Not so "ńew style" gateway, see
101
				// https://gitlab.com/morph027/signal-cli-dbus-rest-api
102
				// https://gitlab.com/morph027/python-signal-cli-rest-api
103
				// https://github.com/bbernhard/signal-cli-rest-api
104
				$body = $response->getBody();
105
				$json = json_decode($body, true);
106
				$versions = $json['versions'] ?? [];
107
				if (is_array($versions) && in_array('v2', $versions)) {
108
					$json = [
109
						'recipients' => $identifier,
110
						'message' => $message,
111
					];
112
					$account = $this->getAccount();
113
					if ($account != self::ACCOUNT_UNNECESSARY) {
114
						$json['account'] = $account;
115
					}
116
					$response = $client->post(
117
						$this->getUrl() . '/v2/send',
118
						[
119
							'json' => $json,
120
						]
121
					);
122
				} else {
123
					$response = $client->post(
124
						$this->getUrl() . '/v1/send/' . $identifier,
125
						[
126
							'json' => [ 'message' => $message ],
127
						]
128
					);
129
				}
130
				$body = (string)$response->getBody();
131
				$json = json_decode($body, true);
132
				$status = $response->getStatusCode();
133
				if ($status !== 201 || is_null($json) || !is_array($json) || !isset($json['timestamp'])) {
134
					throw new MessageTransmissionException("error reported by Signal gateway, status=$status, body=$body}");
135
				}
136
			} else {
137
				// Try old deprecated gateway https://gitlab.com/morph027/signal-web-gateway
138
				$response = $client->post(
139
					$this->getUrl() . '/v1/send/' . $identifier,
140
					[
141
						'body' => [
142
							'to' => $identifier,
143
							'message' => $message,
144
						],
145
						'json' => [ 'message' => $message ],
146
					]
147
				);
148
				$body = (string)$response->getBody();
149
				$json = json_decode($body, true);
150
151
				$status = $response->getStatusCode();
152
				if ($status !== 200 || is_null($json) || !is_array($json) || !isset($json['success']) || $json['success'] !== true) {
153
					throw new MessageTransmissionException("error reported by Signal gateway, status=$status, body=$body}");
154
				}
155
			}
156
		}
157
	}
158
159
	#[\Override]
160
	public function cliConfigure(InputInterface $input, OutputInterface $output): int {
161
		$helper = new QuestionHelper();
162
		$urlQuestion = new Question(self::SCHEMA['fields'][0]['prompt'], self::SCHEMA['fields'][0]['default']);
163
		$url = $helper->ask($input, $output, $urlQuestion);
164
		$output->writeln("Using $url.");
165
166
		$this->setUrl($url);
167
168
		$accountQuestion = new Question(self::SCHEMA['fields'][1]['prompt'], self::SCHEMA['fields'][1]['default']);
169
		$account = $helper->ask($input, $output, $accountQuestion);
170
		if ($account == '') {
171
			$account = self::ACCOUNT_UNNECESSARY;
172
			$output->writeln('A signal account is not needed, assuming it is hardcoded into the signal gateway server.');
173
		} else {
174
			$output->writeln("Using $account.");
175
		}
176
177
		$this->setAccount($account);
178
179
		return 0;
180
	}
181
}
182