1<?php
2
3/**
4 *
5 * phpIPAM API client to work with phpIPAM APIv2
6 *
7 *
8 */
9class phpipam_api_client  {
10
11    /**
12     * Debug flag for curl
13     *
14     * (default value: false)
15     *
16     * @var bool
17     * @access public
18     */
19    public $debug = false;
20
21    /**
22     * API server URL
23     *
24     * (default value: false)
25     *
26     * @var bool|string
27     * @access private
28     */
29    private $api_url = false;
30
31    /**
32     * API APP identifier
33     *
34     * (default value: false)
35     *
36     * @var bool|string
37     * @access private
38     */
39    private $api_app_id = false;
40
41    /**
42     * API key
43     *
44     * (default value: false)
45     *
46     * @var bool|string
47     * @access private
48     */
49    private $api_key = false;
50
51    /**
52     * Flag if we need to encrypt API communication
53     *
54     * (default value: false)
55     *
56     * @var bool
57     * @access private
58     */
59    private $api_encrypt = false;
60
61    /**
62     * phpipam account username / passwork for authentication
63     *
64     * (default value: false)
65     *
66     * @var bool|string
67     * @access private
68     */
69    private $api_username = false;
70    private $api_password = false;
71
72    /**
73     * Holder for CUrL connection
74     *
75     * (default value: false)
76     *
77     * @var bool|mixed
78     * @access private
79     */
80    private $Connection = false;
81
82    /**
83     * Access token for phpipam
84     *
85     * (default value: false)
86     *
87     * @var bool|string
88     * @access private
89     */
90    private $token = false;
91
92    /**
93     * When token expires
94     *
95     * (default value: false)
96     *
97     * @var bool|string
98     * @access private
99     */
100    private $token_expires = false;
101
102    /**
103     * api_server_method
104     *
105     * (default value: false)
106     *
107     * @var bool|string
108     * @access private
109     */
110    private $api_server_method = false;
111
112    /**
113     * api_server_controller (sections, subnets, ...)
114     *
115     * (default value: false)
116     *
117     * @var bool|string
118     * @access private
119     */
120    private $api_server_controller = false;
121
122    /**
123     * Identifiers to add to URL
124     *
125     * (default value: false)
126     *
127     * @var bool
128     * @access private
129     */
130    private $api_server_identifiers = false;
131
132    /**
133     * List of valid API methods
134     *
135     * @var mixed
136     * @access private
137     */
138    private $api_server_valid_methods = array(
139        "OPTIONS", "GET", "POST", "PATCH", "DELETE",
140    );
141
142    /**
143     * HTTP error codes for responses
144     *
145     * @var mixed
146     * @access public
147     */
148    public $error_codes = array(
149        // OK
150        200 => "OK",
151        201 => "Created",
152        202 => "Accepted",
153        204 => "No Content",
154        // Client errors
155        400 => "Bad Request",
156        401 => "Unauthorized",
157        403 => "Forbidden",
158        404 => "Not Found",
159        405 => "Method Not Allowed",
160        415 => "Unsupported Media Type",
161        // Server errors
162        500 => "Internal Server Error",
163        501 => "Not Implemented",
164        503 => "Service Unavailable",
165        505 => "HTTP Version Not Supported",
166        511 => "Network Authentication Required",
167    );
168
169    /**
170     * Set result format
171     *
172     * (default value: array("json", "array", "object", "xml"))
173     *
174     * @var mixed
175     * @access private
176     */
177    private $result_format_available = array(
178        "json", "array", "object", "xml",
179    );
180
181    /**
182     * Result format
183     *
184     * value in $result_format_available
185     *
186     * (default value: "json")
187     *
188     * @var string
189     * @access private
190     */
191    private $result_format = "json";
192
193    /**
194     * To store result
195     *
196     * @var mixed
197     * @access private
198     */
199    private $result = array(
200        "success" => true,
201        "code"    => 204,
202        "message" => ""
203    );
204
205    /**
206     * Reponse headers
207     *
208     * @var mixed
209     */
210    public $response_headers = array ();
211
212
213    /**
214     * class constructor...
215     *
216     * @access public
217     * @param bool|string $api_url (default: false)
218     * @param bool|string $app_id (default: false)
219     * @param bool|string $api_key (default: false)
220     * @param bool|string $username (default: false)
221     * @param bool|string $password (default: false)
222     * @param string $result_format (default: "json")
223     */
224    public function __construct($api_url = false, $app_id = false, $api_key = false, $username = false, $password = false, $result_format = "json") {
225        // set app server URL if provided
226        if ($api_url!==false) {
227            $this->set_api_url ($api_url);
228        }
229        // set app_id if provided
230        if ($app_id!==false) {
231            $this->set_api_app_id ($app_id);
232        }
233        // set api key if provided
234        if ($api_key!==false) {
235            $this->set_api_key ($api_key);
236        }
237        // set user/pass if provided
238        if ($username!==false && $password!==false) {
239            $this->set_api_authparams ($username, $password);
240        }
241        // set result format if provided
242        if (strlen($result_format)>0) {
243            $this->set_result_format ($result_format);
244        }
245        // check for required php extensions
246        $this->validate_php_extensions ();
247    }
248
249    /**
250     * Saves error and exits script
251     *
252     * @access public
253     * @param mixed $content
254     * @return void
255     */
256    public function exception ($content) {
257        //set result parameters
258        $this->result = array(
259            'code'    => 400,
260            'success' => false,
261            'message' => $content
262        );
263        // print result
264        $this->print_result ();
265        // exit
266        #die();
267        return;
268    }
269
270    /**
271     * Returns last result
272     *
273     * @access public
274     * @return void
275     */
276    public function get_result () {
277        # output result
278        if ($this->result_format=="json") {
279            return json_encode($this->result);
280        }
281        elseif ($this->result_format=="array") {
282            return $this->result;
283        }
284        elseif ($this->result_format=="object") {
285            return (object) $this->result;
286        }
287        elseif ($this->result_format=="xml") {
288            // new SimpleXMLElement object
289            $xml = new SimpleXMLElement('<'.$_GET['controller'].'/>');
290            // generate xml from result
291            $this->array_to_xml($xml, $this->result);
292            // return XML result
293            return $xml->asXML();
294        }
295    }
296
297    /**
298     * Prints last result
299     *
300     * @access public
301     * @return void
302     */
303    public function print_result () {
304        # output result
305        if ($this->result_format=="json") {
306            print json_encode($this->result);
307        }
308        elseif ($this->result_format=="array") {
309            var_dump($this->result);
310        }
311        elseif ($this->result_format=="object") {
312            var_dump( (object) $this->result);
313        }
314        elseif ($this->result_format=="xml") {
315            // new SimpleXMLElement object
316            $xml = new SimpleXMLElement('<apiclient/>');
317            // generate xml from result
318            $this->array_to_xml($xml, $this->result);
319            // return XML result
320            print $xml->asXML();
321        }
322    }
323
324    /**
325     * Transforms array to XML
326     *
327     * @access private
328     * @param SimpleXMLElement $object
329     * @param array $data
330     * @return void
331     */
332    private function array_to_xml(SimpleXMLElement $object, array $data) {
333        // loop through values
334        foreach ($data as $key => $value) {
335            // if spaces exist in key replace them with underscores
336            if(strpos($key, " ")>0) {
337                $key = str_replace(" ", "_", $key);
338            }
339
340            // if key is numeric append item
341            if(is_numeric($key)) {
342                $key = "item".$key;
343            }
344
345            // if array add child
346            if (is_array($value)) {
347                $new_object = $object->addChild($key);
348                $this->array_to_xml($new_object, $value);
349            }
350            // else write value
351            else {
352                $object->addChild($key, $value);
353            }
354        }
355    }
356
357    /**
358     * Check if all extensions are present
359     *
360     * @access private
361     * @return void
362     */
363    private function validate_php_extensions () {
364        // Required extensions
365        $required_ext  = array("openssl", "curl");
366        // mcrypt for crypted extensions
367        if($this->api_key !== false)
368            $required_ext[] = "mcrypt";
369        // json
370        if($this->result_format == "json")
371            $required_ext[] = "json";
372        // xml
373        if($this->result_format == "xml")
374            $required_ext[] = "xmlreader";
375
376        // Available extensions
377        $available_ext = get_loaded_extensions();
378
379        // check
380        foreach ($required_ext as $e) {
381            if(!in_array($e, $available_ext)) {
382                $this->exception("Missing php extension ($e)");
383            }
384        }
385    }
386
387    /**
388     * Debugging flag
389     *
390     * @access public
391     * @param bool $debug (default: false)
392     * @return void
393     */
394    public function set_debug ($debug = false) {
395        if(is_bool($debug)) {
396            $this->debug = $debug;
397        }
398    }
399
400    /**
401     * Checks requested result format and saves it
402     *
403     * @access public
404     * @param string $result_format (default: "json")
405     * @return void
406     */
407    public function set_result_format ($result_format = "json") {
408        if (strlen($result_format)>0) {
409            if (!in_array($result_format, $this->result_format_available)) {
410                $this->exception ("Invalid result format");
411            }
412            else {
413                // recheck extensions
414                $this->validate_php_extensions ();
415                // set
416                $this->result_format = $result_format;
417            }
418        }
419    }
420
421    /**
422     * Set API url parameter
423     *
424     * @access public
425     * @param mixed $api_url
426     * @return void
427     */
428    public function set_api_url ($api_url) {
429        // we need http/https
430        if(strpos($api_url, "http://")!==false || strpos($api_url, "https://")!==false) {
431            // trim
432            $api_url = trim($api_url);
433            // add last / if missing
434            if (substr($api_url, -1)!=="/") { $api_url .= "/"; }
435            // save
436            $this->api_url = $api_url;
437        }
438        else {
439            $this->exception("Invalid API URL");
440        }
441    }
442
443    /**
444     * Sets api app_id variable
445     *
446     * @access public
447     * @param bool $id (default: false)
448     * @return void
449     */
450    public function set_api_app_id ($app_id = false) {
451        if ($app_id!==false) {
452            // name must be more than 2 and alphanumberic
453            if(strlen($app_id)<3 || strlen($app_id)>12 || !ctype_alnum($app_id)) {
454                $this->exception("Invalid APP id");
455            }
456            else {
457                $this->api_app_id = $app_id;
458            }
459        }
460        else {
461            $this->exception("Invalid APP id");
462        }
463    }
464
465    /**
466     * Set api key
467     *
468     * @access public
469     * @param bool $api_key (default: false)
470     * @return void
471     */
472    public function set_api_key ($api_key = false) {
473        #if ($api_key!==false) {
474        if ($api_key!=false) {
475            $this->api_key = $api_key;
476
477            // set encrypt flag
478            $this->api_encrypt = true;
479        }
480        else {
481            $this->exception("Invalid APP id");
482        }
483    }
484
485    /**
486     * Sets username/password for URL auth
487     *
488     * @access public
489     * @param bool $username (default: false)
490     * @param bool $password (default: false)
491     * @return void
492     */
493    public function set_api_authparams ($username = false, $password = false) {
494        if($username===false || $password===false) {
495            $this->exception("Invalid username or password");
496        }
497        else {
498            $this->api_username = $username;
499            $this->api_password = $password;
500        }
501    }
502
503    /**
504     * Sreets api method.
505     *
506     * @access public
507     * @param string $method (default: "GET")
508     * @return void
509     */
510    public function set_api_method ($method = "GET") {
511        // validate
512        $this->set_api_method_validate ($method);
513        // set
514        $this->api_server_method = strtoupper($method);
515    }
516
517    /**
518     * Validates API method against available
519     *
520     * @access private
521     * @param mixed $method
522     * @return void
523     */
524    private function set_api_method_validate ($method) {
525        if(!in_array(strtoupper($method), $this->api_server_valid_methods)) {
526            $this->exception("Invalid method $method");
527        }
528    }
529
530    /**
531     * Sets API controller - required
532     *
533     * @access public
534     * @param bool|string $controller (default: false)
535     * @return void
536     */
537    public function set_api_controller ($controller = false) {
538        #if($controller!==false) {
539        if($controller!=false) {
540            $this->api_server_controller = strtolower($controller);
541        }
542    }
543
544    /**
545     * Sets additional identifiers to be passed to URL directly
546     *
547     *  e.g.:  /api/appid/controller/<identifier1>/<identifier2>/
548     *
549     * @access public
550     * @param mixed $identifiers
551     * @return void
552     */
553    public function set_api_identifiers ($identifiers) {
554        $this->api_server_identifiers = false;         // clear this to forget any previous settings
555        if(is_array($identifiers)) {
556            if(sizeof($identifiers)>0 && !$this->api_encrypt) {
557                // reset
558                $this->api_server_identifiers = implode("/", $identifiers);
559            }
560            elseif (sizeof($identifiers)>0 && $this->api_encrypt) {
561                $this->api_server_identifiers = array();
562                foreach ($identifiers as $cnt=>$i) {
563                    if($cnt==0) {
564                        $this->api_server_identifiers['id'] = $i;
565                    }
566                    else {
567                        $this->api_server_identifiers['id'.($cnt+1)] = $i;
568                    }
569                }
570            }
571        }
572    }
573
574
575    /* @api-server communication --------------- */
576
577    /**
578     * Executes request to API server
579     *
580     * @access public
581     * @param bool|string $method (default: false)
582     * @param bool|string $controller (default: false)
583     * @param mixed $identifiers (default: array())
584     * @param mixed $params (default: array())
585     * @param bool|string $token_file (default: false)
586     * @return void
587     */
588    public function execute ($method = false, $controller = false, $identifiers = array(), $params = array(), $token_file = false) {
589        // check and set method
590        $this->set_api_method ($method);
591        // set api controller
592        $this->set_api_controller ($controller);
593        // set api identifiers
594        $this->set_api_identifiers ($identifiers);
595
596        // set connection
597        $this->curl_set_connection ($token_file);
598        // save params
599        $this->curl_set_params ($params);
600        // set HTTP method
601        $this->curl_set_http_method ();
602
603        // if not encrypted set params
604        if(!$this->api_encrypt) {
605            // add token to header, authenticate if it fails
606            $this->curl_add_token_header ($token_file);
607        }
608        // if token is set execute
609        if ($this->token !== false) {
610            // execute
611            $res = $this->curl_execute ();
612            // save result
613            $this->result = (array) $res;
614
615            // check for invalid token and retry
616            if ($this->result['code']=="401" && $token_file!==false) {
617                // remove old token
618                $this->delete_token_file ($token_file);
619                // auth again
620                $this->curl_add_token_header ($token_file);
621                // execute
622                $res = $this->curl_execute ();
623                // save result
624                $this->result = (array) $res;
625            }
626        }
627        // exncrypted request
628        elseif ($this->api_encrypt) {
629            // execute
630            $res = $this->curl_execute ();
631            // save result
632            $this->result = (array) $res;
633        }
634        // save reult
635        $this->curl_save_headers ();
636    }
637
638    /**
639     * Opens cURL resource and sets initial parameters
640     *
641     * @access private
642     * @param mixed $token_file
643     * @return void
644     */
645    private function curl_set_connection ($token_file) {
646        // check if it exists
647        #if ($this->Connection===false) {
648        if ($this->Connection==false) {
649            // Get cURL resource
650            $this->Connection = curl_init();
651
652        // set URL
653        if($this->api_server_controller===false) {
654            $url = $this->api_url.$this->api_app_id."/";
655        }
656        else {
657            $url = $this->api_url.$this->api_app_id.str_replace("//", "/", "/".$this->api_server_controller."/".$this->api_server_identifiers."/");
658        }
659
660        // set default curl options and params
661        curl_setopt_array($this->Connection, array(
662                CURLOPT_RETURNTRANSFER => true,
663                CURLOPT_URL => $url,
664                CURLOPT_HEADER => false,
665                CURLOPT_VERBOSE => $this->debug,
666                CURLOPT_TIMEOUT => 9,
667                CURLOPT_HTTPHEADER => array(
668                    'Content-Type: application/json',
669                    # https://stackoverflow.com/a/33550871
670                    'Connection: Keep-Alive',
671                ),
672                CURLOPT_USERAGENT => 'phpipam-api php class',
673                # https://stackoverflow.com/a/43277274
674                CURLOPT_ENCODING => 'gzip,deflate',
675                #CURLOPT_ENCODING => 'deflate',
676                // ssl
677                CURLOPT_SSL_VERIFYHOST => false,
678                CURLOPT_SSL_VERIFYPEER => false,
679                // save headers
680                CURLINFO_HEADER_OUT => true,
681            ) );
682        }
683    }
684
685    /**
686     * Adds params to request if required
687     *
688     * @access private
689     * @param mixed $params
690     * @return void
691     */
692    private function curl_set_params ($params) {
693        // params set ?
694        if (is_array($params) && !$this->api_encrypt ) {
695            if (sizeof($params)>0) {
696                # 9163a784aff4e9689dd0a5330a826f70247a2ce2
697                if ($this->api_server_method == 'GET')
698                    curl_setopt($this->Connection, CURLOPT_URL, $this->api_url.$this->api_app_id.str_replace("//", "/", "/".$this->api_server_controller."/".$this->api_server_identifiers."/?".http_build_query($params)));
699                else
700                    curl_setopt($this->Connection, CURLOPT_POSTFIELDS, json_encode($params));
701            }
702        }
703        // encrypt
704        elseif ($this->api_encrypt) {
705            // empty
706            if(!is_array($params))
707                $params = array();
708            if(!is_array($this->api_server_identifiers))
709                $this->api_server_identifiers = array();
710
711            // join identifiers and parameters
712            $params = array_merge($this->api_server_identifiers, $params);
713            $params['controller'] = $this->api_server_controller;
714
715            // create encrypted request
716            $encrypted_request = base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $this->api_key, json_encode($params), MCRYPT_MODE_ECB));
717
718            // escape +
719            $encrypted_request = urlencode($encrypted_request);
720
721            // reset url
722            curl_setopt($this->Connection, CURLOPT_URL, $this->api_url."?app_id=".$this->api_app_id."&enc_request=".$encrypted_request);
723        }
724    }
725
726    /**
727     * Sets HTTP method to use for queries
728     *
729     * @access private
730     * @return void
731     */
732    private function curl_set_http_method () {
733        curl_setopt($this->Connection, CURLOPT_CUSTOMREQUEST, $this->api_server_method);
734    }
735
736    /**
737     * Adds token to http header
738     *
739     * @access private
740     * @param mixed $token_file
741     * @return void
742     */
743    private function curl_add_token_header ($token_file) {
744        if($token_file!==false) {
745            // open file and save token
746            $token = @file($token_file);
747            // save token
748            if(isset($token[0])) {
749                $this->token = trim($token[0]);
750                $this->token_expires = trim($token[1]);
751                // is token still valid ?
752                if (strlen($this->token)<2 && $this->token_expires < time()) {
753                    // initiate authentication
754                    $this->curl_authenticate ();
755                    //save token to file
756                    $this->write_token_file ($token_file);
757                }
758            }
759            else {
760                $this->curl_authenticate ();
761                //save token to file
762                $this->write_token_file ($token_file);
763
764            }
765        }
766        // token not saved, try to retrieve it
767        else {
768            $this->curl_authenticate ();
769        }
770
771        // add token to headers
772        $this->curl_add_http_header ("token", $this->token);
773    }
774
775    /**
776     * Adds http headers
777     *
778     * @access private
779     * @param mixed $name
780     * @param mixed $value
781     * @return void
782     */
783    private function curl_add_http_header ($name, $value) {
784        $headers = array(
785            "Content-Type: application/json",
786            # https://stackoverflow.com/a/33550871
787            #'Connection: Keep-Alive',
788            "$name: $value"
789        );
790        // save
791        curl_setopt($this->Connection, CURLOPT_HTTPHEADER, $headers);
792    }
793
794    /**
795     * Writes token to token file
796     *
797     * @access private
798     * @param mixed $filename
799     * @return void
800     */
801    private function write_token_file ($filename) {
802        //save token
803        try {
804            $myfile = fopen($filename, "w");
805            fwrite($myfile, $this->token);
806            fwrite($myfile, "\n");
807            fwrite($myfile, $this->token_expires);
808            // close file
809            fclose($myfile);
810        }
811        catch ( Exception $e ) {
812             $this->exception("Cannot write file $filename");
813        }
814    }
815
816    /**
817     * Removes token file if expired / invalid
818     *
819     * @access private
820     * @param mixed $token_file
821     * @return void
822     */
823    private function delete_token_file ($token_file) {
824        //save token
825        try {
826            $myfile = fopen($token_file, "w");
827            fwrite($myfile, "");
828            // close file
829            fclose($myfile);
830        }
831        catch ( Exception $e ) {
832             $this->exception("Cannot write file $token_file");
833        }
834    }
835
836    /**
837     * Executes request.
838     *
839     * @access private
840     * @return void
841     */
842    private function curl_execute () {
843        // send request and save response
844        $resp = curl_exec($this->Connection);
845
846        // curl error check
847        if (curl_errno($this->Connection)) {
848            $this->exception("Curl error: ".curl_error($this->Connection));
849        }
850        else {
851            // return result object
852            return json_decode($resp);
853        }
854    }
855
856    /**
857     * Store result code
858     *
859     * @method curl_save_result_code
860     * @return void
861     */
862    private function curl_save_headers () {
863        // save result and result code
864        $this->response_headers = curl_getinfo($this->Connection);
865    }
866
867    /**
868     * send authenticate request and save token if provided, otherwise throw error.
869     *
870     * @access private
871     * @return void
872     */
873    private function curl_authenticate () {
874        // Get cURL resource
875        $c_auth = curl_init();
876
877        // set default curl options and params
878        curl_setopt_array($c_auth, array(
879            CURLOPT_RETURNTRANSFER => true,
880            CURLOPT_URL => $this->api_url.$this->api_app_id."/user/",
881            #CURLOPT_HEADER => true,
882            CURLOPT_HEADER => false,
883            # useless due to libcurl internal design
884            # https://bugs.php.net/bug.php?id=65348
885            #CURLOPT_VERBOSE => $this->debug,
886            CURLOPT_TIMEOUT => 9,
887            CURLOPT_USERAGENT => 'phpipam-api php class',
888            # https://stackoverflow.com/a/43277274
889            CURLOPT_ENCODING => 'gzip,deflate',
890            #CURLOPT_ENCODING => 'deflate',
891            // ssl
892            CURLOPT_SSL_VERIFYHOST => false,
893            CURLOPT_SSL_VERIFYPEER => false,
894            CURLOPT_POST => true,
895            # https://github.com/phpipam/phpipam-api-clients/issues/17
896            CURLOPT_POSTFIELDS => '',
897            # https://
898            #CURLOPT_FOLLOWLOCATION => true,
899            # http://epic.grnet.gr/guides/api-auth/
900            CURLOPT_USERPWD => $this->api_username.":".$this->api_password,
901            CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
902            # https://stackoverflow.com/a/33550871
903            # https://github.com/phpipam/phpipam/issues/1191#issuecomment-309450838
904            CURLOPT_HTTPHEADER => array(
905                #'Content-Length: 0',
906                'Connection: Keep-Alive',
907                #'Content-Type: application/x-www-form-urlencoded;charset=UTF-8',
908                # charset => 415 : Invalid content type
909                'Content-Type: application/x-www-form-urlencoded',
910               ),
911            )
912        );
913        // send request and save response
914        $resp = curl_exec($c_auth);
915
916        // curl error check
917        #if (curl_errno($c_auth)) {
918        if ($resp===false) {
919            $this->exception("Curl error: ".curl_error($c_auth));
920        }
921        else {
922            // return result object
923            $auth_resp = json_decode($resp);
924            // ok ?
925            if ($auth_resp->code == 200) {
926                if (isset($auth_resp->data->token)) {
927                    // save token
928                    $this->token = $auth_resp->data->token;
929                    $this->token_expires = strtotime($auth_resp->data->expires);
930                }
931                else {
932                    $this->exception("Cannot obtain access token");
933                }
934            }
935            // error
936            else {
937                // save response
938                $this->result = $auth_resp;
939            }
940        }
941        #curl_close($c_auth);
942    }
943
944}
945
946/** modelines
947 * vi: tabstop=4 shifwidth=4 autoindent beautify
948 * vim: ff=unix ts=4 sts=4 sw=4 ai et sta fenc=utf-8 bf ft=php
949 * atom:set useSoftTabs tabLength=4 lineending=lf encoding=utf-8
950 * -*- Mode: tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
951 */
952?>
953