Completed
Push — master ( 25c087...a46420 )
by Lawrence
01:31
created

Server::executeCoreComponent()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 3
nop 2
dl 0
loc 17
ccs 0
cts 8
cp 0
crap 20
rs 9.2
c 0
b 0
f 0
1
<?php
2
/*
3
 +------------------------------------------------------------------------+
4
 | Plinker-RPC PHP                                                        |
5
 +------------------------------------------------------------------------+
6
 | Copyright (c)2017-2018 (https://github.com/plinker-rpc/core)           |
7
 +------------------------------------------------------------------------+
8
 | This source file is subject to MIT License                             |
9
 | that is bundled with this package in the file LICENSE.                 |
10
 |                                                                        |
11
 | If you did not receive a copy of the license and are unable to         |
12
 | obtain it through the world-wide-web, please send an email             |
13
 | to [email protected] so we can send you a copy immediately.        |
14
 +------------------------------------------------------------------------+
15
 | Authors: Lawrence Cherone <[email protected]>                     |
16
 +------------------------------------------------------------------------+
17
 */
18
19
namespace Plinker\Core;
20
21
/**
22
 * Plinker\Core\Server
23
 */
24
final class Server
25
{
26
    /**
27
     * @var
28
     */
29
    protected $input;
30
31
    /**
32
     * @var
33
     */
34
    protected $config;
35
36
    /**
37
     * @var
38
     */
39
    protected $signer;
40
    
41
    /**
42
     * @var
43
     */
44
    protected $response;
45
    
46
47
    /**
48
     * @const - error strings
49
     */
50
    const ERROR_IP            = "IP not in allowed list (%s)";
51
    const ERROR_TOKEN         = "Plinker token mismatch";
52
    const ERROR_DECODE        = "Failed to decode payload, check secret";
53
    const ERROR_USR_COMPONENT = "User component class (%s) not found";
54
    const ERROR_EXT_COMPONENT = "Component (%s) not implemented";
55
    const ERROR_ACTION        = "Component action (%s) not implemented in: %s";
56
57
    /**
58
     * Class construct
59
     *
60
     * @param  array $config - config array which holds object configuration
61
     * @return void
62
     */
63 2
    public function __construct($config = [])
64
    {
65 2
        $this->config = array_merge([
66 2
            "debug"       => false,
67
            "secret"      => null,
68
            "allowed_ips" => []
69 2
        ], $config);
70
71
        // check and set client timeout
72 2
        if (!isset($this->config["timeout"]) || !is_numeric($this->config["timeout"])) {
73 2
            $this->config["timeout"] = 10;
74
        }
75 2
    }
76
    
77
    /**
78
     * Sets inbound input value into scope
79
     *
80
     * @return void
81
     */
82
    private function setInput()
83
    {
84
        $this->input = file_get_contents("php://input");
85
        if (!empty($this->input)) {
86
            $this->input = gzinflate($this->input);
87
            $this->input = json_decode($this->input, true);
88
        }
89
    }
90
    
91
    /**
92
     * Check allowed IPs
93
     *
94
     * @return bool
95
     */
96
    private function checkAllowedIp($ip, $allowed_ips = [])
97
    {
98
        return !(!empty($allowed_ips) && !in_array($ip, $allowed_ips));
99
    }
100
    
101
    /**
102
     * Execute core component
103
     */
104
    private function executeCoreComponent($component, $action)
105
    {
106
        // component is plinker endpoint
107
        $ns = "\\Plinker\\Core\\Endpoint\\".ucfirst($component);
108
109
        if (class_exists($ns)) {
110
            //
111
            $response = $this->execute($ns, $action);
112
        } else {
113
            if (empty($component) && $action === "info") {
114
                $response = $this->info();
115
            } else {
116
                $response = sprintf(Server::ERROR_EXT_COMPONENT, $component);
117
            }
118
        }
119
        
120
        return $response;
121
    }
122
123
    /**
124
     * Execute user component
125
     */
126
    private function executeUserComponent($component, $action)
127
    {
128
        $ns = null;
129
        
130
        //
131
        if (!empty($this->config["classes"][$component][0])) {
132
            $ns = $this->config["classes"][$component][0];
133
        }
134
135
        //
136
        if (!empty($this->config["classes"][$component][1])) {
137
            $this->config = array_merge(
138
                $this->config,
139
                $this->config["classes"][$component][1]
140
            );
141
        }
142
143
        //
144
        if (!empty($ns) && !file_exists($ns)) {
145
            $this->response = serialize([
146
                "error" => sprintf(Server::ERROR_USR_COMPONENT, $component),
147
                "code"  => 422
148
            ]);
149
            return;
150
        }
151
152
        //
153
        require($ns);
154
155
        //
156
        if (!class_exists($component)) {
157
            $this->response = serialize([
158
                "error" => sprintf(Server::ERROR_USR_COMPONENT, $component),
159
                "code"  => 422
160
            ]);
161
            return;
162
        }
163
164
        //
165
        return $this->execute($component, $action);
166
    }
167
168
    /**
169
     * Listen method
170
     *
171
     * <code>
172
     *  $server->listen();
173
     * </code>
174
     *
175
     * @return void
176
     */
177
    public function listen()
178
    {
179
        $this->setInput();
180
        
181
        // load signer
182
        if (!$this->signer) {
183
            $this->signer = new Lib\Signer($this->config);
184
        }
185
186
        // check allowed ips
187
        if (!$this->checkAllowedIp($_SERVER["REMOTE_ADDR"], $this->config["allowed_ips"])) {
188
            $this->response = [
189
                "error" => sprintf(Server::ERROR_IP, $_SERVER["REMOTE_ADDR"]),
190
                "code" => 403
191
            ];
192
            return;
193
        }
194
195
        // check header token matches data token
196
        if (empty($this->input["token"]) || $_SERVER["HTTP_PLINKER"] != $this->input["token"]) {
197
            $this->response = [
198
                "error" => Server::ERROR_TOKEN,
199
                "code" => 422
200
            ];
201
            return;
202
        }
203
204
        // decode input payload
205
        $this->input = $this->signer->decode($this->input);
206
207
        // could not decode payload
208
        if ($this->input === null) {
209
            $this->response = [
210
                "error" => Server::ERROR_DECODE,
211
                "code" => 422
212
            ];
213
            return;
214
        }
215
216
        // import user config
217
        $this->config = array_merge(
218
            $this->config,
219
            $this->input
220
        );
221
222
        // user component
223
        if (array_key_exists($this->input["component"], $this->config["classes"])) {
224
            //
225
            $response = $this->executeUserComponent($this->input["component"], $this->input["action"]);
226
        }
227
        // core component
228
        else {
229
            $response = $this->executeCoreComponent($this->input["component"], $this->input["action"]);
230
        }
231
232
        $this->response = $response;
233
    }
234
235
    /**
236
     * Return info about available classes
237
     *
238
     * <code>
239
     *  $client->info();
240
     * </code>
241
     *
242
     * @return array
243
     */
244
    private function info()
245
    {
246
        $response = [
247
            "class" => []
248
        ];
249
        foreach ($this->config["classes"] as $key => $val) {
250
            //
251
            require($val[0]);
252
253
            $reflection = new \ReflectionClass($key);
254
255
            foreach ($reflection->getMethods() as $method) {
256
                if (!in_array($method->getName(), ["__construct"])) {
257
                    $param = [];
258
                    foreach ($method->getParameters() as $parameter) {
259
                        $param[] = $parameter->getName();
260
                    }
261
                    $response["class"][$key]["methods"][$method->getName()] = $param;
262
                }
263
            }
264
        }
265
266
        return $response;
267
    }
268
269
    /**
270
     * Execute component
271
     *
272
     * @param  string $ns      component class namespace
273
     * @param  string $action  component action
274
     * @return string
275
     */
276
    private function execute($ns, $action)
277
    {
278
        $component = new $ns($this->config);
279
280
        if (method_exists($component, $action)) {
281
            $response = call_user_func_array(
282
                [
283
                    $component,
284
                    $action
285
                ],
286
                $this->input["params"]
287
            );
288
        } else {
289
            $response = sprintf(Server::ERROR_ACTION, $action, $ns);
290
        }
291
292
        return $response;
293
    }
294
    
295
    /**
296
     *
297
     */
298
    public function __destruct()
299
    {
300
        if (!headers_sent()) {
301
            header("Content-Type: text/plain; charset=utf-8");
302
        }
303
        
304
        if (!is_null($this->signer)) {
305
            echo json_encode($this->signer->encode($this->response), JSON_PRETTY_PRINT);
306
        }
307
    }
308
}
309