PHP XML API service

Pozdravljeni,
Potreboval bi malo pomoči oz. predlogov glede izdelave custom API-ja ki komunicira z mysql bazo in se nahaja na cpanel shared hostingu...
Na ta API se bodo povezovale custom embedded naprave:
72 MHz 32-bit ARM 7 Processor
16MB RAM and 4.5MB FLASH
GPRS/3G Modem

Ker nisem nikoli ničesar programiral v php-ju bi vas prosil če preverite primer kode in poveste kaj ni ok oz. kaj bi vi spremenili...
XML ki bi ga naprava poslala za prijavo v sistem bi bil:

<?xml version='1.0' encoding="iso-8859-1" ?>
<packet>
    <mac_address>00:00:00:00:00:00</mac_address>
    <serial>000000</serial>
    <hostname>testdevice</hostname>
    <model>devmodel</model>
    <sw_version>1.00.0000</sw_version>
    <hw_version>1.00</hw_version>
</packet>

Koda login skripte pa bi izgledala takole:

<?php
    include('config.php');

    function getClientIPAddress()
    {
        if (!empty($_SERVER['HTTP_CLIENT_IP']))
        {
            $ip=$_SERVER['HTTP_CLIENT_IP'];
        }
        elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
        {
            $ip=$_SERVER['HTTP_X_FORWARDED_FOR'];
        }
        else
        {
            $ip=$_SERVER['REMOTE_ADDR'];
        }
        return $ip;
    }

    $db = mysql_connect($dbhost, $dbuser, $dbpasswd);
    mysql_select_db ($dbname) or die ("Cannot connect to database");
    mysql_query("SET NAMES 'utf8'");

    libxml_use_internal_errors(true);
    $request_xml = new SimpleXMLElement($HTTP_RAW_POST_DATA);
    if (!$request_xml)
    {
        echo "Failed loading XML\n";
        foreach(libxml_get_errors() as $error)
        {
            echo "\t", $error->message;
        }
    }
    else
    {
        $sql = "SELECT id, active, REPLACE(UUID(),'-', '') AS session FROM devices WHERE mac_address='" . $request_xml->mac_address . "' AND serial='" . $request_xml->serial . "' AND hostname='" . $request_xml->hostname . "'";
        $result = mysql_query($sql);
        $device = mysql_fetch_object($result);

        $response_xml = new SimpleXMLElement("<?xml version='1.0' encoding=\"iso-8859-1\" ?><packet></packet>");
        $response_xml->addChild('device_id', $device->id);
        $response_xml->addChild('active', $device->active);
        $response_xml->addChild('session', $device->session);

        if(is_null($device->id))
        {
            header('Content-Type: text/xml');
            echo $response_xml->asXML();
        }
        else
        {
            if($device->active = 1)
            {
                mysql_query("UPDATE devices SET status='01', dt_last_online=Now(), session='" . $device->session . "', sw_version='" . $request_xml->sw_version . "', hw_version='" . $request_xml->hw_version . "', ip_address='" . getClientIPAddress() . "' WHERE id=" . $device->id . ";");
                mysql_query("INSERT INTO events (dt, device_id, event, response, description) VALUES (Now(), " . $device->id . ", '0001', '01', '" . $device->session . "');");
                header('Content-Type: text/xml');
                echo $response_xml->asXML();
            }
            else
            {
                mysql_query("UPDATE devices SET status='02', dt_last_online=Now(), session='', sw_version='" . $request_xml->sw_version . "', hw_version='" . $request_xml->hw_version . "', ip_address='" . getClientIPAddress() . "' WHERE id=" . $device->id . ";");
                mysql_query("INSERT INTO events (dt, device_id, event, response, description) VALUES (Now(), " . $device->id . ", '0001', '02', '');");
                header('Content-Type: text/xml');
                echo $response_xml->asXML();
            }
        }
    }
    mysql_close($db);
?>

Response bo XML dokument ki izgleda takole:

<?xml version='1.0' encoding="iso-8859-1" ?>
<packet>
    <device_id>25638</device_id>
    <status>1</status>
    <session>c4cb465cd3a211e087f6006438a934f5</session>
</packet>

Torej kaj bi vi spremenili in zakaj? Kje sem ga lomil? Za kakršnokoli pomoč vam bom hvaležen.

10 odgovorov

hm ali nihče ne zna pomagati svetovati kako naj se lotim zadeve oz. ali je tak način ok?
Ker ne morem urejati prvega posta bom dodal kodo v kateri sem v večini primerov "preprečil" SQL Injection me pa zanima ali je smiselno dodati še stripslashes() preden kličem mysqlrealescape_string() ???
Nova koda:

<?php
    include('config.php');

    error_reporting(0);
    libxml_use_internal_errors(true);
    libxml_clear_errors();

    function getClientIPAddress()
    {
        if (!empty($_SERVER['HTTP_CLIENT_IP']))
        {
            $ip=$_SERVER['HTTP_CLIENT_IP'];
        }
        elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
        {
            $ip=$_SERVER['HTTP_X_FORWARDED_FOR'];
        }
        else
        {
            $ip=$_SERVER['REMOTE_ADDR'];
        }
        return $ip;
    }

    $db = mysql_connect($dbhost, $dbuser, $dbpasswd);
    mysql_select_db ($dbname) or die ("Cannot connect to database");
    mysql_query("SET NAMES 'utf8'");

    try {
        $request_xml = new SimpleXMLElement($HTTP_RAW_POST_DATA);
    } catch (Exception $e) {}

    if (!$request_xml)
    {
        $response_xml = new SimpleXMLElement("<?xml version='1.0' encoding=\"iso-8859-1\" ?><error></error>");
        foreach(libxml_get_errors() as $error)
        {
            $response_xml->addChild('message', trim($error->message));
        }
        libxml_clear_errors();
        header('Content-Type: text/xml');
        echo $response_xml->asXML();
    }
    else
    {
        $mac_address = mysql_real_escape_string($request_xml->mac_address);
        $serial = mysql_real_escape_string($request_xml->serial);
        $hostname = mysql_real_escape_string($request_xml->hostname);
        $sw_version = mysql_real_escape_string($request_xml->sw_version);
        $hw_version = mysql_real_escape_string($request_xml->hw_version);

        $sql = "SELECT id, active, REPLACE(UUID(),'-', '') AS session FROM devices WHERE mac_address='" . $mac_address . "' AND serial='" . $serial . "' AND hostname='" . $hostname . "'";
        $result = mysql_query($sql);
        $device = mysql_fetch_object($result);

        $response_xml = new SimpleXMLElement("<?xml version='1.0' encoding=\"iso-8859-1\" ?><packet></packet>");
        $response_xml->addChild('device_id', $device->id);
        $response_xml->addChild('active', $device->active);
        $response_xml->addChild('session', $device->session);

        if(is_null($device->id))
        {
            header('Content-Type: text/xml');
            echo $response_xml->asXML();
        }
        else
        {
            if($device->active = 1)
            {
                mysql_query("UPDATE devices SET status='01', dt_last_online=Now(), session='" . $device->session . "', sw_version='" . $sw_version . "', hw_version='" . hw_version . "', ip_address='" . getClientIPAddress() . "' WHERE id=" . $device->id . ";");
                mysql_query("INSERT INTO events (dt, device_id, event, response, description) VALUES (Now(), " . $device->id . ", '0001', '01', '" . $device->session . "');");
                header('Content-Type: text/xml');
                echo $response_xml->asXML();
            }
            else
            {
                mysql_query("UPDATE devices SET status='02', dt_last_online=Now(), session='', sw_version='" . $sw_version . "', hw_version='" . hw_version . "', ip_address='" . getClientIPAddress() . "' WHERE id=" . $device->id . ";");
                mysql_query("INSERT INTO events (dt, device_id, event, response, description) VALUES (Now(), " . $device->id . ", '0001', '02', '');");
                header('Content-Type: text/xml');
                echo $response_xml->asXML();
            }
        }
    }
    mysql_close($db);
?>

Ne razumem sploh kje je težava. Iščeš koncept ... ali ti dejansko kaj ne dela?

Roky:
Ne razumem sploh kje je težava. Iščeš koncept ... ali ti dejansko kaj ne dela?

V bistvu zadeva dela, zanima me predvsem če sem glede security-a kje mimo vsekal in je skripta potencialno nevarna, oz. če sem kje kaj spregledal in bo zadeva požirala preveč resoursev. Z 1 clientom to ne morem testirat, ko pa bo laufalo na produkcijskem strežniku in gor 100-200 naprav pa je tudi težko narediti kakšne večje spremembe :)

#1 Validacija
Naredi dodatno validacijo, v smilu intval tam kjer veš da je integer, regex kjer veš da je mac address itd.

$mac_address = mysql_real_escape_string($request_xml->mac_address);
$serial = mysql_real_escape_string($request_xml->serial);
$hostname = mysql_real_escape_string($request_xml->hostname);
$sw_version = mysql_real_escape_string($request_xml->sw_version);
$hw_version = mysql_real_escape_string($request_xml->hw_version);

#2 Pri prvem blogu kar dobiš noter HTTP RAW POST XML. Kako pa veš, da ni kakšen čudn xml? Raje ga prej preveri

validateXMLFile($xmlString) {
        libxml_use_internal_errors(true);

        $doc = new DOMDocument();
        $doc->loadXML($xmlString);

        $errors = libxml_get_errors();
        if (empty($errors)) {
            return true;
        }

        $error = $errors[0];
        if ($error->level < 3) {
            return true;
        }

        $errorsMsg = '';
        foreach ($errors as $error) {
            $errorsMsg .= trim($error->message) . "\n";
        }

        return $errorsMsg;
    }
if ($validationStatus !== true) {
   echo 'Napaka z XML-jem, ni validiran';
   exit; // al karkol
}

Ostalo je kul.

2

Lahko bi uvedel kak error reporting sistem, vsaj za beleženje mysql napak.

Pri "UPDATE DEVICES" manjka $ pred hw_version

hw_version='" . hw_version . "'

Poglej si še mysqlfreeresult.

Kako pogosto naprave pošiljajo requeste, boš vedel ti. Če vsaka naprava naredi request 1x na dan ali 1x na sekundo je razlika več kot občutna :)

Če te skrbi, pripravi testno okolje in simuliraj 500 naprav. (mogoče ab)

edit: in tako kot pravi Roky: dodatna validacija regex in intval()!

LP

3

Hvala obema za odgovore.
1.)Kar se tiče validacije sem v kodo dodal funkcije za validacijo:

function validate_macaddress($val)
    {
        if(preg_match('/^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$/', $val))
        {
            return $val;
        }
        else
        {
            return "00:00:00:00:00:00"
        }
    }

    function validate_serial($val)
    {
        if(preg_match('/^[0-9]{6}$/', $val))
        {
            return $val;
        }
        else
        {
            return "000000"
        }
    }

    function validate_hostname($val)
    {
        if(preg_match('/(?=^.{1,254}$)(^(?:(?!\d+\.|-)[a-zA-Z0-9_\-]{1,63}(?<!-)\.?)+(?:[a-zA-Z]{2,})$)/', $val))
        {
            return $val;
        }
        else
        {
            return "default"
        }
    }

    function validate_sw_version($val)
    {
        if(preg_match('/^(\d+)((\.{1}\d+)*)(\.{0})$/', $val))
        {
            return $val;
        }
        else
        {
            return "0.00.0000"
        }
    }

    function validate_hw_version($val)
    {
        if(preg_match('/^(\d+)((\.{1}\d+)*)(\.{0})$/', $val))
        {
            return $val;
        }
        else
        {
            return "0.00"
        }
    }

In nato spremenil validacijo spremeljivk:

$mac_address = mysql_real_escape_string(validate_macaddress($request_xml->mac_address));
        $serial = mysql_real_escape_string(validate_serial($request_xml->serial));
        $hostname = mysql_real_escape_string(validate_hostname($request_xml->hostname));
        $sw_version = mysql_real_escape_string(validate_sw_version($request_xml->sw_version));
        $hw_version = mysql_real_escape_string(validate_hw_version($request_xml->hw_version));

2.)Bom dodal preverjanje XML-ja.
3.)Sem popravil manjkajoči $ v query-u in na koncu predno zaprem bazo dodal mysqlfreeresult().
4.)login requestov bo zelo malo oz. samo takrat ko bo naprava izgubila povezavo s strežnikom ali se resetirala. Bodo pa še drugi requesti in sicer pošiljanje statusa naprave ki bo vsakih 10-30s(Odvisno od konfiguracije) in pa pošiljanje statusa modula priklopljenega na napravo(max. 4 moduli) vsakih 5-20sekund. dosti manjši interval pomoje itak ni možen zaradi samega pinga gprs/3g omrežja :) Ti xml-ji bodo tudi malo večji kot login ker bodo vsebovali več podatkov...

@Roky: Sem šele zdele opazil glede validateXMLFile ...
v kodi že preverim če je struktura XML-ja pravilna:

try {
        $request_xml = new SimpleXMLElement($HTTP_RAW_POST_DATA);
    } catch (Exception $e) {}

    if (!$request_xml)
    {
        //NAPAKA XML NIMA PRAVILNE STRUKTURE IN KOT REZULTAT VRNE ERROR XML DOKUMENT
        $response_xml = new SimpleXMLElement("<?xml version='1.0' encoding=\"iso-8859-1\" ?><error></error>");
        foreach(libxml_get_errors() as $error)
        {
            $response_xml->addChild('message', trim($error->message));
        }
        libxml_clear_errors();
        header('Content-Type: text/xml');
        echo $response_xml->asXML();
    }
    else
    {
        //XML STRUKTURA JE OK
    }

Če pogledaš sem imel to narejeno v drugem postu... tako da trenutna koda izgleda takole:

<?php
    include('config.php');

    libxml_use_internal_errors(true);
    libxml_clear_errors();

    function getClientIPAddress()
    {
        if (!empty($_SERVER['HTTP_CLIENT_IP']))
        {
            $ip=$_SERVER['HTTP_CLIENT_IP'];
        }
        elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
        {
            $ip=$_SERVER['HTTP_X_FORWARDED_FOR'];
        }
        else
        {
            $ip=$_SERVER['REMOTE_ADDR'];
        }
        return $ip;
    }

    function validate_macaddress($val)
    {
        if(preg_match('/^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$/', $val))
        {
            return $val;
        }
        else
        {
            return "00:00:00:00:00:00";
        }
    }

    function validate_serial($val)
    {
        if(preg_match('/^[0-9]{6}$/', $val))
        {
            return $val;
        }
        else
        {
            return "000000";
        }
    }

    function validate_hostname($val)
    {
        if(preg_match('/(?=^.{1,254}$)(^(?:(?!\d+\.|-)[a-zA-Z0-9_\-]{1,63}(?<!-)\.?)+(?:[a-zA-Z]{2,})$)/', $val))
        {
            return $val;
        }
        else
        {
            return "default";
        }
    }

    function validate_sw_version($val)
    {
        if(preg_match('/^(\d+)((\.{1}\d+)*)(\.{0})$/', $val))
        {
            return $val;
        }
        else
        {
            return "0.00.0000";
        }
    }

    function validate_hw_version($val)
    {
        if(preg_match('/^(\d+)((\.{1}\d+)*)(\.{0})$/', $val))
        {
            return $val;
        }
        else
        {
            return "0.00";
        }
    }

    $db = mysql_connect($dbhost, $dbuser, $dbpasswd);
    mysql_select_db ($dbname) or die ("Cannot connect to database");
    mysql_query("SET NAMES 'utf8'");

    try {
        $request_xml = new SimpleXMLElement($HTTP_RAW_POST_DATA);
    } catch (Exception $e) {}

    if (!$request_xml)
    {
        $response_xml = new SimpleXMLElement("<?xml version='1.0' encoding=\"iso-8859-1\" ?><error></error>");
        foreach(libxml_get_errors() as $error)
        {
            $response_xml->addChild('message', trim($error->message));
        }
        libxml_clear_errors();
        header('Content-Type: text/xml');
        echo $response_xml->asXML();
    }
    else
    {
        $mac_address = mysql_real_escape_string(validate_macaddress($request_xml->mac_address));
        $serial = mysql_real_escape_string(validate_serial($request_xml->serial));
        $hostname = mysql_real_escape_string(validate_hostname($request_xml->hostname));
        $sw_version = mysql_real_escape_string(validate_sw_version($request_xml->sw_version));
        $hw_version = mysql_real_escape_string(validate_hw_version($request_xml->hw_version));

        $sql = "SELECT id, active, REPLACE(UUID(),'-', '') AS session FROM devices WHERE mac_address='" . $mac_address . "' AND serial='" . $serial . "' AND hostname='" . $hostname . "'";
        $result = mysql_query($sql);
        $device = mysql_fetch_object($result);

        $response_xml = new SimpleXMLElement("<?xml version='1.0' encoding=\"iso-8859-1\" ?><packet></packet>");
        $response_xml->addChild('device_id', $device->id);
        $response_xml->addChild('active', $device->active);
        $response_xml->addChild('session', $device->session);

        if(is_null($device->id))
        {}
        else
        {
            if($device->active = 1)
            {
                mysql_query("UPDATE devices SET status='01', dt_last_online=Now(), session='" . $device->session . "', sw_version='" . $sw_version . "', hw_version='" . $hw_version . "', ip_address='" . getClientIPAddress() . "' WHERE id=" . $device->id . ";");
                mysql_query("INSERT INTO events (dt, device_id, event, response, description) VALUES (Now(), " . $device->id . ", '0001', '01', '" . $device->session . "');");
            }
            else
            {
                mysql_query("UPDATE devices SET status='02', dt_last_online=Now(), session='', sw_version='" . $sw_version . "', hw_version='" . $hw_version . "', ip_address='" . getClientIPAddress() . "' WHERE id=" . $device->id . ";");
                mysql_query("INSERT INTO events (dt, device_id, event, response, description) VALUES (Now(), " . $device->id . ", '0001', '02', '');");
            }
        }
        header('Content-Type: text/xml');
        echo $response_xml->asXML();
    }
    mysql_free_result($result);
    mysql_close($db);
?>

Aha, OK, pol je pa to to.

Imam še eno vprašanje in sicer preko socketa pošljem serverju request:

POST /API/ HTTP/1.0
Host: www.domena.com
Content-type: text/xml
Content-length: 116
Connection: Close

<UserAuthOn><IdentType value='KEYBOARD'/><IdentCode value='12345678'/><Tstamp value='1977-1-1 1:1:37'/></UserAuthOn>

Vendar ne dobim pravilne povratne informacije. Od serverja dobim:

HTTP/1.1 200 OK
Server: nginx
Date: Mon, 28 Nov 2011 13:23:55 GMT
Content-Type: text/xml
Connection: close
X-Powered-By: PHP/5.3.8

Torej brez "Content-Length:" in vsebine. Preko Firefox REST Clienta dela BP. Kakšna ideja kaj je narobe v mojem requestu?

Naj dodam še to da GET za download datotek mi dela BP:

GET /API/ HTTP/1.0
Host: www.domena.com
Connection: Close