Completed
Push — master ( fad20c...da5feb )
by Lawrence
01:25
created

Server::execute()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 17
nc 8
nop 2
dl 0
loc 31
rs 8.5806
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
 * @codeCoverageIgnore
25
 */
26
final class Server
27
{
28
    /**
29
     * @var
30
     */
31
    protected $input;
32
33
    /**
34
     * @var
35
     */
36
    protected $config;
37
38
    /**
39
     * @var
40
     */
41
    protected $signer;
42
    
43
    /**
44
     * @var
45
     */
46
    protected $response;
47
    
48
49
    /**
50
     * @const - error strings
51
     */
52
    const ERROR_IP            = "IP not in allowed list (%s)";
53
    const ERROR_TOKEN         = "Plinker token mismatch";
54
    const ERROR_DECODE        = "Failed to decode payload, check secret";
55
    const ERROR_USR_COMPONENT = "User component class (%s) not found";
56
    const ERROR_EXT_COMPONENT = "Component (%s) not implemented";
57
    const ERROR_ACTION        = "Component action (%s) not implemented in: %s";
58
59
    /**
60
     * Class construct
61
     *
62
     * @param  array $config - config array which holds object configuration
63
     * @return void
64
     */
65
    public function __construct($config = [])
66
    {
67
        $this->config = array_merge([
68
            "secret"      => null,
69
            "allowed_ips" => []
70
        ], $config);
71
    }
72
    
73
    /**
74
     * Listen method
75
     *
76
     * <code>
77
     *  $server->listen();
78
     * </code>
79
     *
80
     * @return string
81
     */
82
    public function listen()
83
    {
84
        $this->setInput();
85
        
86
        // load signer
87
        if (!$this->signer) {
88
            $this->signer = new Lib\Signer($this->config);
89
        }
90
91
        // check allowed ips
92
        if (!$this->checkAllowedIp($_SERVER["REMOTE_ADDR"], $this->config["allowed_ips"])) {
93
            return json_encode($this->signer->encode([
94
                "error" => sprintf(Server::ERROR_IP, $_SERVER["REMOTE_ADDR"]),
95
                "code" => 403
96
            ]), JSON_PRETTY_PRINT);
97
        }
98
99
        // check header token matches data token
100
        if (empty($this->input["token"]) || $_SERVER["HTTP_PLINKER"] != $this->input["token"]) {
101
            return json_encode($this->signer->encode([
102
                "error" => Server::ERROR_TOKEN,
103
                "code" => 422
104
            ]), JSON_PRETTY_PRINT);
105
        }
106
107
        // decode input payload
108
        $this->input = $this->signer->decode($this->input);
109
110
        // could not decode payload
111
        if ($this->input === null) {
112
            return json_encode($this->signer->encode([
113
                "error" => Server::ERROR_DECODE,
114
                "code" => 422
115
            ]), JSON_PRETTY_PRINT);
116
        }
117
118
        // import user config
119
        $this->config = array_merge(
120
            $this->config,
121
            $this->input
122
        );
123
124
        // user component
125
        if (array_key_exists($this->input["component"], $this->config["classes"])) {
126
            //
127
            $response = $this->executeUserComponent($this->input["component"], $this->input["action"]);
128
        }
129
        // core component
130
        else {
131
            $response = $this->executeCoreComponent($this->input["component"], $this->input["action"]);
132
        }
133
134
        // sign response and return
135
        return json_encode($this->signer->encode($response), JSON_PRETTY_PRINT);
136
    }
137
138
    /**
139
     * Return info about available classes
140
     *
141
     * <code>
142
     *  $client->info();
143
     * </code>
144
     *
145
     * @return array
146
     */
147
    private function info()
148
    {
149
        $response = [
150
            'class' => []
151
        ];
152
        
153
        foreach ($this->config["classes"] as $key => $val) {
154
            
155
            // addtional config
156
            $response["class"][$key]["config"] = !empty($val[1]) ? $val[1] : [];
157
            
158
            // check class file exists
159
            if (!file_exists($val[0])) {
160
                $response["class"][$key]["methods"] = [];
161
                continue;
162
            }
163
            
164
            //
165
            require($val[0]);
166
167
            $reflection = new \ReflectionClass($key);
168
169
            foreach ($reflection->getMethods() as $method) {
170
                if (!in_array($method->getName(), ["__construct"])) {
171
                    $param = [];
172
                    foreach ($method->getParameters() as $parameter) {
173
                        $param[] = $parameter->getName();
174
                    }
175
                    $response["class"][$key]["methods"][$method->getName()] = $param;
176
                }
177
            }
178
        }
179
180
        return $response;
181
    }
182
    
183
    /**
184
     * Sets inbound input value into scope
185
     *
186
     * @return void
187
     */
188
    private function setInput()
189
    {
190
        $this->input = file_get_contents("php://input");
191
        if (!empty($this->input)) {
192
            $this->input = gzinflate($this->input);
193
            $this->input = json_decode($this->input, true);
194
        }
195
    }
196
    
197
    /**
198
     * Check allowed IPs
199
     *
200
     * @return bool
201
     */
202
    private function checkAllowedIp($ip, $allowed_ips = [])
203
    {
204
        return !(!empty($allowed_ips) && !in_array($ip, $allowed_ips));
205
    }
206
    
207
    /**
208
     * Execute core component
209
     */
210
    private function executeCoreComponent($component, $action)
211
    {
212
        // define component namespace
213
        $ns = "\\Plinker\\".ucfirst($component);
214
215
        if (class_exists($ns)) {
216
            //
217
            $response = $this->execute($ns, $action);
218
        } elseif (class_exists($ns."\\".ucfirst($component))) {
219
            //
220
            $ns = "\\Plinker\\".ucfirst($component)."\\".ucfirst($component);
221
            $response = $this->execute($ns, $action);
222
        } else {
223
            if (empty($component) && $action === "info") {
224
                $response = $this->info();
225
            } else {
226
                $response = sprintf(Server::ERROR_EXT_COMPONENT, $component);
227
            }
228
        }
229
        
230
        return $response;
231
    }
232
233
    /**
234
     * Execute user component
235
     */
236
    private function executeUserComponent($component, $action)
237
    {
238
        $ns = null;
239
        
240
        //
241
        if (!empty($this->config["classes"][$component][0])) {
242
            $ns = $this->config["classes"][$component][0];
243
        }
244
245
        //
246
        if (!empty($this->config["classes"][$component][1])) {
247
            $this->config['config'] = array_merge(
248
                $this->config['config'],
249
                $this->config["classes"][$component][1]
250
            );
251
        }
252
253
        //
254
        if (!empty($ns) && !file_exists($ns)) {
255
            $this->response = serialize([
256
                "error" => sprintf(Server::ERROR_USR_COMPONENT, $component),
257
                "code"  => 422
258
            ]);
259
            return;
260
        }
261
262
        //
263
        require($ns);
264
265
        //
266
        if (!class_exists($component)) {
267
            $this->response = serialize([
268
                "error" => sprintf(Server::ERROR_USR_COMPONENT, $component),
269
                "code"  => 422
270
            ]);
271
            return;
272
        }
273
274
        //
275
        return $this->execute($component, $action);
276
    }
277
278
    /**
279
     * Execute component
280
     *
281
     * @param  string $ns      component class namespace
282
     * @param  string $action  component action
283
     * @return string
284
     */
285
    private function execute($ns, $action)
286
    {
287
        // filter out (secret, server, timeout) from construct config
288
        $config = array_filter($this->config['config'], function ($key) {
289
            return !in_array($key, ['secret', 'server', 'timeout']);
290
        }, ARRAY_FILTER_USE_KEY);
291
292
        // init component
293
        $component = new $ns($config);
294
295
        // call method, if exception return exception class
296
        try {
297
            if (method_exists($component, $action)) {
298
                $response = call_user_func_array(
299
                    [
300
                        $component,
301
                        $action
302
                    ],
303
                    $this->input["params"]
304
                );
305
            } else {
306
                $response = sprintf(Server::ERROR_ACTION, $action, $ns);
307
            }
308
            // Catch exception and return Exception\Server - PHP 5.6 compat
309
        } catch (\Exception $e) {
310
            return new Exception\Server($e->getMessage());
311
        } catch (Exception\Server $e) {
312
            return new Exception\Server($e->getMessage());
313
        }
314
315
        return $response;
316
    }
317
}
318