Passed
Push — master ( 8821e7...5919b1 )
by Lawrence
01:36
created

Server::setInput()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 6
rs 9.4285
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
            // arguments
156
            $response["class"][$key]["arguments"] = !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
        // component is plinker endpoint
213
        $ns = "\\Plinker\\Core\\Endpoint\\".ucfirst($component);
214
215
        if (class_exists($ns)) {
216
            //
217
            $response = $this->execute($ns, $action);
218
        } else {
219
            if (empty($component) && $action === "info") {
220
                $response = $this->info();
221
            } else {
222
                $response = sprintf(Server::ERROR_EXT_COMPONENT, $component);
223
            }
224
        }
225
        
226
        return $response;
227
    }
228
229
    /**
230
     * Execute user component
231
     */
232
    private function executeUserComponent($component, $action)
233
    {
234
        $ns = null;
235
        
236
        //
237
        if (!empty($this->config["classes"][$component][0])) {
238
            $ns = $this->config["classes"][$component][0];
239
        }
240
241
        //
242
        if (!empty($this->config["classes"][$component][1])) {
243
            $this->config = array_merge(
244
                $this->config,
245
                $this->config["classes"][$component][1]
246
            );
247
        }
248
249
        //
250
        if (!empty($ns) && !file_exists($ns)) {
251
            $this->response = serialize([
252
                "error" => sprintf(Server::ERROR_USR_COMPONENT, $component),
253
                "code"  => 422
254
            ]);
255
            return;
256
        }
257
258
        //
259
        require($ns);
260
261
        //
262
        if (!class_exists($component)) {
263
            $this->response = serialize([
264
                "error" => sprintf(Server::ERROR_USR_COMPONENT, $component),
265
                "code"  => 422
266
            ]);
267
            return;
268
        }
269
270
        //
271
        return $this->execute($component, $action);
272
    }
273
274
    /**
275
     * Execute component
276
     *
277
     * @param  string $ns      component class namespace
278
     * @param  string $action  component action
279
     * @return string
280
     */
281
    private function execute($ns, $action)
282
    {
283
        // filter out (secret, server, timeout) from construct config
284
        $config = array_filter($this->config['config'], function ($value) {
285
            return is_array($value) || array_key_exists($value, ['secret', 'server', 'timeout']);
286
        });
287
288
        // init component
289
        $component = new $ns($config);
290
291
        // call method
292
        if (method_exists($component, $action)) {
293
            $response = call_user_func_array(
294
                [
295
                    $component,
296
                    $action
297
                ],
298
                $this->input["params"]
299
            );
300
        } else {
301
            $response = sprintf(Server::ERROR_ACTION, $action, $ns);
302
        }
303
304
        return $response;
305
    }
306
}
307