4
989

Send an HTTP request to a web service

Direct exchanges between web services have become essential. Learn how to send a GET or a POST HTTP(S) request to a web server and how to publish a machine to machine interface.

NOTE: The function sendhttp and its variants of the iZend library is a direct and more complete application of this article.

Scenario

sendfax is a web service which can fax documents. A site offers this service via a form. A user fills in the form and sends it to the site. The site extracts the content of the form and sends it to the service. The service validates the request, runs it and sends back a return code. The site displays the result of the request to the user.

Fax express

Who will be receiving the fax?

Enter a series of telephone numbers.

Which document do you want to fax?

Select a text file or an image.

Fill in the form and press Fax.

Fill in the form and press Fax. Try sending different incomplete forms. NOTE: In this demonstration form, no fax is actually sent.

The navigator generates an HTTP/POST document and sends it to the faxexpress function of the site. faxexpress extracts the content of the document and transmits the list of telephone numbers and the attached file directly to the sendfax service with the help of the sendhttp function. sendfax analyzes the parameters of the request. In case of error, if the list of telephone numbers is empty or invalid or if the file is missing or too big, the service sends back an HTTP document containing the text KO_MISSINGNUMBER, KO_BADNUMBER, KO_MISSINGFILE or KO_BADFILE. NOTE: A real service will probably have other parameters like an identifier and a password. If all the parameters are correct, the service sends the fax and returns an HTTP document containing the text OK or KO_FAXNOTSENT in case of error. The site analyzes the response returned by the service, the HTTP return code and the content of the document, and returns a document to the navigator containing either a confirmation message or one or several error messages.

The entire dialog between the site and the service, faxpress and sendfax, is in proper HTTP. A sophisticated service can very well return different HTTP error codes, like 400 Bad Request when a request is rejected or 403 Forbidden if the requester can't be identified. The format of the data exchanged between a client and a service can also be more complex and the dialog can be encoded in serialized PHP or in XML.

Making of

Organize the content of your site by creating in the root of the site the folders /public for the pages published on the web, api for service entry points and /library for your own PHP code. Add the files faxexpress.php, sendfax.php and sendhttp.php respectively in the folders public, api and library with the following contents:

  1. /
    1. public
      1. faxexpress.php
    2. api
      1. sendfax.php
    3. library
      1. sendhttp.php
  1. <?php
  2. require_once 'sendhttp.php';

Loads the code of the sendhttp function.

  1. define('OK', 'OK');
  2. define('KO_MISSINGNUMBER', 'KO_MISSINGNUMBER');
  3. define('KO_BADNUMBER', 'KO_BADNUMBER');
  4. define('KO_MISSINGFILE', 'KO_MISSINGFILE');
  5. define('KO_BADFILE', 'KO_BADFILE');
  6. define('KO_FAXNOTSENT', 'KO_FAXNOTSENT');

Defines the return codes sent back by the service.

  1. $site_host=$_SERVER['SERVER_NAME'];
  2. $path='/api/sendfax.php';
  3. $proto='http';

Initializes the address of the server and the access path to the service. Set $proto preferably to 'https' if the server is secured.

  1. $to=$files=false;
  2. $action='init';
  3.  
  4. if (isset($_POST['fax_send'])) {
  5.     $action='send';
  6. }
  7.  
  8. switch($action) {
  9.     case 'send':
  10.         if (isset($_POST['fax_to'])) {
  11.             preg_match_all('/([\d]+)/', $_POST['fax_to'], $r);
  12.             $to=implode(' ', $r[0]);
  13.         }
  14.  
  15.         if (isset($_FILES['fax_document']['tmp_name']) and $_FILES['fax_document']['tmp_name']) {
  16.             $name=$_FILES['fax_document']['name'];
  17.             $tmp_name=$_FILES['fax_document']['tmp_name'];
  18.             $type=$_FILES['fax_document']['type'];
  19.             $files=array('document' => array('name' => $name, 'type' => $type, 'tmp_name' => $tmp_name));
  20.         }
  21.         break;
  22.     default:
  23.         break;
  24. }

Initializes the parameters of the form, extracts the requested action from the form then the list of telephone numbers and the attached file.

  1. $fax_sent=false;
  2. $missing_number=false;
  3. $bad_number=false;
  4. $missing_file=false;
  5. $bad_file=false;
  6. $fax_not_sent=false;

Initializes all the information and error flags of the form. NOTE: The input fields are not validated on purpose to show that a service must in all cases control the data which it is transmitted.

  1. switch($action) {
  2.     case 'send':
  3.         $url=$proto.'://'.$site_host.$path;
  4.  
  5.         $args = array(
  6.             'to'        => $to,
  7.         );
  8.         $response=sendpost($url, $args, $files, true);
  9.  
  10.         if (!$response or $response[0] != 200) {
  11.             $fax_not_sent=true;
  12.             break;
  13.         }

Builds the URL and the parameters of the call to the service. Sends the request with the sendhttp function. sendhttp sends back an array containing the HTTP return code, the header of the response line by line and the body of the response, or false in case of error. If the response is false or if the HTTP return code isn't 200, the communication with the server has failed or the service has rejected the request.

  1.         $r=$response[2];
  2.  
  3.         $codes=explode(';', $r);
  4.  
  5.         foreach ($codes as $c) {
  6.             switch($c) {
  7.                 case OK:
  8.                     $fax_sent=true;
  9.                     break;
  10.                 case KO_MISSINGNUMBER:
  11.                     $missing_number=true;
  12.                     break;
  13.                 case KO_BADNUMBER:
  14.                     $bad_number=true;
  15.                     break;
  16.                 case KO_MISSINGFILE:
  17.                     $missing_file=true;
  18.                     break;
  19.                 case KO_BADFILE:
  20.                     $bad_file=true;
  21.                     break;
  22.                 case KO_FAXNOTSENT:
  23.                 default:
  24.                     $fax_not_sent=true;
  25.                     break;
  26.             }
  27.         }
  28.         break;
  29.     default:
  30.         break;
  31. }
  32. ?>

Extracts the body of the responsee and analyzes the return codes sent back by the service.

The rest of the code displays the input form.

  1. <h6 id="faxexpress">Fax express</h6>
  2. <form enctype="multipart/form-data" method="post" action="#faxexpress">
  3. <p>Who will be receiving the fax?</p>
  4. <p class="<?php echo ($missing_number or $bad_number) ? 'error' : 'info' ?>">Enter a series of telephone numbers.</p>
  5. <p><textarea name="fax_to" id="fax_to" cols="50" rows="2"><?php echo $to; ?></textarea></p>
  6. <p>Which document do you want to fax?</p>
  7. <p class="<?php echo ($missing_file or $bad_file) ? 'error' : 'info' ?>">Select a text file or an image.</p>
  8. <p><input type="hidden" name="MAX_FILE_SIZE" value="200000" /><input name="fax_document" id="fax_document" type="file" size="25" /></p>
  9. <p><input name="fax_send" id="fax_send" type="submit" value="Fax" /></p>
  10. <?php if ($fax_not_sent): ?>
  11. <p class="error">Sending the fax has failed.</p>
  12. <?php elseif ($fax_sent): ?>
  13. <p class="info">Your fax has been sent.</p>
  14. <?php else: ?>
  15. <p class="info">Fill in the form and press Fax.</p>
  16. <?php endif; ?>
  17. </form>
  1. function chkphone($s) {
  2.     return preg_match('/^[0-9]{10,11}$|^\+[0-9]{11}$/', $s);
  3. }
  4.  
  5. function normphone($s) {
  6.     switch(strlen($s)) {
  7.         case 10:
  8.             return '+33'. substr($s,1);
  9.         case 11:
  10.             return '+'. $s;
  11.         default:
  12.             return $s;
  13.     }
  14. }

checkphone returns true if $s is a telephone number with 10 or 11 digits, false if not.

normphone adds the prefix +33 to a telephone number with 10 digits or just a + in front of a telephone number with 11 digits. NOTE: Adapt these functions for your local dialing.

  1. define('OK', 'OK');
  2. define('KO_MISSINGNUMBER', 'KO_MISSINGNUMBER');
  3. define('KO_BADNUMBER', 'KO_BADNUMBER');
  4. define('KO_MISSINGFILE', 'KO_MISSINGFILE');
  5. define('KO_BADFILE', 'KO_BADFILE');
  6. define('KO_FAXNOTSENT', 'KO_FAXNOTSENT');
  7.  
  8. $errorlist=array();

Defines the codes returned by sendhttp and initializes the list of errors $errorlist.

  1. $to=false;
  2.  
  3. if (isset($_POST['to'])) {
  4.     $to=$_POST['to'];
  5.     preg_match_all('/(\b[\d+]+\b)/', $to, $tolist);
  6.     $to=$tolist[0];
  7. }
  8.  
  9. if (!$to) {
  10.     $errorlist[]=KO_MISSINGNUMBER;
  11. }
  12. else {
  13.     $tellist=array();
  14.     foreach ($to as $tel){
  15.         if (!chkphone($tel)) {
  16.             $errorlist[]=KO_BADNUMBER;
  17.             break;
  18.         }
  19.         $tellist[] = normphone($tel);
  20.     }
  21.  
  22.     $to=array_unique($tellist);
  23. }

Extracts groups of consecutive digits from $_POST['to']. Checks that all the groups of digits are telephone numbers, normalizes them and deletes duplicated numbers. Adds the error KO_MISSINGNUMBER to $errorlist if the list of telephone numbers is empty. Adds the error KO_BADNUMBER to $errorlist if a number is invalid.

  1. $data=false;
  2.  
  3. $maxsize = 200000;
  4. if (!isset($_FILES['document']) or !$_FILES['document']['tmp_name'] or $_FILES['document']['error'] != 0) {
  5.     $errorlist[]=KO_MISSINGFILE;
  6. }
  7. else if ($_FILES['document']['size'] == 0 or $_FILES['document']['size'] > $maxsize) {
  8.     $errorlist[]=KO_BADFILE;
  9. }
  10.  
  11. if (!$errorlist) {
  12.     $tmpdir = '/tmp';
  13.     $filecopy = $tmpdir . '/' . basename( $_FILES['document']['name']);
  14.  
  15.     if (move_uploaded_file($_FILES['document']['tmp_name'], $filecopy))  {
  16.         $data64=@file_get_contents($filecopy);
  17.  
  18.         $data=base64_decode($data64);
  19.         if ($data === false) {
  20.             $errorlist[]=KO_BADFILE;
  21.         }
  22.         /* fax document */
  23.         @unlink($filecopy);
  24.     }
  25.     else {
  26.         $errorlist[]=KO_BADFILE;
  27.     }
  28. }

Extracts the attached file from $_FILES. Adds the error KO_MISSINGFILE to $errorlist if the file is missing or in case of a transfer error. Adds the error KO_BADFILE to $errorlist if the size of the file is invalid. If no error has been found, copies the transmitted file in a temporary zone and decodes its content. Sends the fax. Adds KO_FAXNOTSENT to $errorlist in case of error. Deletes the temporaty file. NOTE: In this demonstration version, the fax isn't really sent.

  1. if (!$errorlist) {
  2.     $errorlist[]=OK;
  3. }

Initializes the list of return codes to OK if no error has been encountered.

  1. header('Content-Type: text/plain');
  2. echo implode(';', $errorlist); // just one line with no newline

Sends back a document of the type text/plain consisting of a single line of text containing one or several return codes separated by a ; (SEMICOLON).

  1. function sendhttp($method, $url, $args, $files=false, $base64=false) {

sendhttp accepts 4 arguments: a request type, a URL, a liste of parameters and an optional list of files.

  1.     $purl = parse_url($url);
  2.     if ($purl === false)
  3.     {
  4.         return false;
  5.     }

Decomposes the URL with the PHP function parse_url. Returns false if $url is invalid.

  1.     $scheme = isset($purl['scheme']) ? $purl['scheme'] : 'http';
  2.     switch($scheme) {
  3.         case 'https':
  4.             $proto = 'ssl';
  5.             break;
  6.         case 'http':
  7.             $proto = 'tcp';
  8.             break;
  9.         default:
  10.             return false;
  11.     }
  12.     $host = isset($purl['host']) ? $purl['host'] : 'localhost';
  13.     $portnum = isset($purl['portnum']) ? $purl['portnum'] : $scheme == 'https' ? 443 : 80;
  14.     $path = isset($purl['path']) ? $purl['path'] : '';

Extracts the different components of the URL and assigns default values.

  1.     $user_agent = 'iZend';
  2.  
  3.     $header_string = $content_string = '';
  4.  
  5.     switch ($method) {

Initializes the header and the body of the HTTP document. Tests the type of the request.

  1.         case 'POST':
  2.             if ($files && is_array($files)) {
  3.                 $boundary = md5(microtime());
  4.                 $content_type = 'multipart/form-data; boundary='.$boundary;
  5.  
  6.                 $content_string = '';
  7.  
  8.                 if ($args && is_array($args)) {
  9.                     foreach ($args as $k => $v) {
  10.                         $content_string .= '--' . $boundary . "\r\n";
  11.                         $content_string .= 'Content-Disposition: form-data; name="' . $k . '"' . "\r\n\r\n" . $v . "\r\n";
  12.                     }
  13.                 }
  14.                 foreach ($files as $k => $v ) {
  15.                     $data = file_get_contents($v['tmp_name']);
  16.                     if ($data === false) {
  17.                         break;
  18.                     }
  19.                     $content_string .= '--' . $boundary . "\r\n";
  20.                     $content_string .= 'Content-Disposition: form-data; name="' . $k . '"; filename="' . $v['name'] . '"' . "\r\n";
  21.                     $content_string .= 'Content-Type: ' . $v['type'] . "\r\n";
  22.                     if ($base64) {
  23.                         $content_string .= 'Content-Transfer-Encoding: base64' . "\r\n\r\n";
  24.                         $content_string .= chunk_split(base64_encode($data)) . "\r\n";
  25.                     }
  26.                     else {
  27.                         $content_string .= 'Content-Transfer-Encoding: binary' . "\r\n\r\n";
  28.                         $content_string .= $data . "\r\n";
  29.                     }
  30.                 }
  31.                 $content_string .= '--' . $boundary . '--'. "\r\n";
  32.             }
  33.             else {
  34.                 $content_type = 'application/x-www-form-urlencoded';
  35.                 if ($args && is_array($args)) {
  36.                     $content_string = http_build_args($args);
  37.                 }
  38.             }
  39.             $content_length = strlen($content_string);
  40.             $header_string="POST $path HTTP/1.0\r\nHost: $host\r\nUser-Agent: $user_agent\r\nContent-Type: $content_type\r\nContent-Length: $content_length\r\nConnection: close\r\n\r\n";
  41.             break;

If the request is a POST, if the document has one or several attached files, writes a document of the type multipart/form-data, otherwise writes a document of the type application/x-www-form-urlencoded. In the first case, builds the body of the document with a first part which defines a unique separator then a series of secondary parts each containing one of the attached files encoded in base64. In the second case, builds the body of an HTTP document containing the parameters of the request, if any, properly formatted with http_build_args.

  1.         case 'GET':
  2.             if ($args && is_array($args)) {
  3.                 $path .= '?'.http_build_args($args);
  4.             }
  5.             $header_string="GET $path HTTP/1.0\r\nHost: $host\r\nUser-Agent: $user_agent\r\nConnection: close\r\n\r\n";
  6.             break;

If the request is a GET, adds the parameters, if any, properly formatted with http_build_args after the access path, then builds the header of the HTTP document. The body of the document is empty.

  1.         default:
  2.             return false;
  3.     }

Returns false if $method isn't 'POST' or 'GET'.

  1.     $socket = @fsockopen($proto.'://'.$host, $portnum);
  2.     if ($socket === false) {
  3.         return false;
  4.     }

Opens the connection with the server. Returns false in case of error.

  1.     if (fwrite($socket, $header_string) === false) {
  2.         return false;
  3.     }
  4.     if ($content_string) {
  5.         $content_len = strlen($content_string);
  6.         for ($written = 0; $written < $content_len; $written += $w) {
  7.             $w = fwrite($socket, $written == 0 ? $content_string : substr($content_string, $written));
  8.             if ($w === false) {
  9.                 return false;
  10.             }
  11.         }
  12.     }

Writes the header of the document then the body of the document in several packets if necessary.

  1.     $response = '';
  2.     while (!feof($socket)) {
  3.         $response .= fread($socket, 8192);
  4.     }

Reads the response from the server.

  1.     fclose($socket);

Closes the connection with the server.

  1.     list($response_headers, $response_body) = explode("\r\n\r\n", $response, 2);
  2.  
  3.     $response_header_lines = explode("\r\n", $response_headers);
  4.     $http_response_line = array_shift($response_header_lines);
  5.  
  6.     if (preg_match('@^HTTP/[0-9]\.[0-9] ([0-9]{3})@', $http_response_line, $r)) {
  7.         $response_code = $r[1];
  8.     }
  9.     else {
  10.         $response_code = 0;
  11.     }

Splits the header and the body of the document. Separates the header line by line. Extracts the first line of the header to get the return code.

  1.     $response_header_array = array();
  2.     foreach ($response_header_lines as $header_line) {
  3.         list($header, $value) = explode(': ', $header_line, 2);
  4.         $response_header_array[$header] = $value;
  5.     }

Saves the header of the response in an associative array.

  1.     return array($response_code, $response_header_array, $response_body);
  2. }

Returns an array with the HTTP code sent back by the service, the MIME header of the document in an associative array and the plain content of the response.

  1. function sendget($url, $args) {
  2.     return sendhttp('GET', $url, $args);
  3. }

sendget returns the result of calling sendhttp with the arguments $url and $args and $method set to 'GET'.

  1. function sendpost($url, $args, $files=false, $base64=false ) {
  2.     return sendhttp('POST', $url, $args, $files, $base64);
  3. }

sendpost returns the result of calling sendhttp with the arguments $url, $args and $files and $base64 and $method set to 'POST'.

  1. function http_build_args($args) {
  2.     $args_string = '';
  3.  
  4.     foreach ($args as $name => $value) {
  5.         $args_string .= ($args_string ? '&' : '') . urlencode($name) . '=' . urlencode($value);
  6.     }
  7.  
  8.     return $args_string;
  9. }

http_build_args returns the associative array $args in the form of a properly formatted URL string of parameters.

sendhttp
SYNOPSIS

sendhttp( $method, $url, $args, $files=false, $base64=false )

sendget( $url, $args )

sendpost( $url, $args, $files=false, $base64=false )

DESCRIPTION

sendhtpp produces a GET or a POST HTTP 1.0 document, transmits it to an HTTP server and returns the HTTP code, the header and the body of the document sent back by the server or false in case of error.

$method is set to 'GET' or 'POST' depending on the type of request expected by the server.

$url addresses the PHP code of the server which is going to analyze the document, run an action and return a regular HTTP document. $url has the format [proto://]host[:portnum]/path. Set proto to 'https' to transmit a document in a secure mode. host gives the name or the IP address of the server. If necessary, add portnum to specify a port number different from 80 or 443 for HTTP or HTTPS connections with a server listening to particular ports. path gives the path to the PHP code on the server.

$args contains a list of parameters for the called service arranged in an associative array { 'param1' => val1, ... }. $args can pass one or several fields directly in a URL for an HTTP GET request or extracted from an HTML form for an HTTP POST request.

$files contains the list of files attached to the request in the form an associative array { 'docname' => {'name' => 'filename', 'type' => 'mimetype', 'tmp_name' => 'pathname'}, ... }. This parameter is optional. Typically, it's used to pass a file transmitted by an <input type="file" name="docname"... /> tag in an HTML form. If $base64 is true, the contents of the files are encoded in base64.

sendhttp sends back an array containing the HTTP return code, the header of the response and the body of the response. If an argument is invalid or if the connection has failed, sendhttp returns false.

sendget and sendpost call sendhttp with $method respectively set to 'GET' and 'POST'.

EXAMPLE
$ php -a

Runs the PHP interpreter in interactive mode.

php> $r = sendget('http://www.google.com/search', array('q' => 'frasq.org'));

Same as typing http:://www.google.com/search?q=frasq.org in the address bar of a navigator.

php> print_r($r[0]);
200

Displays the HTTP return code.

php> print_r($r[1]);
Array
(
    [Date] => ...
    [Expires] => -1
    [Cache-Control] => private, max-age=0
    [Content-Type] => text/html; charset=ISO-8859-1
    ...
)

Displays the MIME header of the document.

php> echo $r[1]['Content-Type'];
text/html; charset=ISO-8859-1

Displays the content type of the document.

php> print_r($r[2]);
<!doctype html><head><title>frasq.org - Google Search</title> ...

Displays the document.

Application

Google publishes an API for drawing charts and other images - Google Charts API. One function can be used to obtain a PNG image of a QR code. Simply pass the proper parameters in an <img>:

<img src="http://chart.googleapis.com/chart?cht=qr&chf=bg,s,ffffff&chs=63x63&chld=M|0&chl=www.frasq.org" />

To call this service in a program, wrap sendhttp in a specific function:

  1. require_once 'sendhttp.php';

Loads the sendhttp function.

  1. function qrencode($s, $size=100, $quality='M') {
  2.     $url = 'http://chart.googleapis.com/chart';
  3.     $args = array(
  4.         'cht'   => 'qr',
  5.         'chf'   => 'bg,s,ffffff',
  6.         'chs'   => "${size}x${size}",
  7.         'chld'  => "${quality}|0",
  8.         'chl'   => $s,
  9.     );
  10.  
  11.     $response=sendget($url, $args);
  12.  
  13.     if (!$response or $response[0] != 200) {
  14.         return false;
  15.     }
  16.  
  17.     return $response[2];
  18. }

qrencode sends a GET request to the service at the address http://chart.googleapis.com/chart and returns the binary image data or false in case of error. $s contains the string of characters to be encoded. $size is in pixels. $quality can be either L, M, Q or H. See the documentation for all the details.

Decoding the image of a QR code is more challenging. Suppose you haven't found a true HTTP API but just a form which lets you upload an image and returns the decoded text: ZXing Decoder Online. Try it. This form returns the decoded text or another HTML document in case of error.

Extract the source code of the form:

<form enctype="multipart/form-data" method="post" action="decode">
<p><input name="f" size="50" type="file"/>&nbsp;<input type="submit"/>
<input value="true" name="full" type="hidden"/></p></form>

Write a function which passes these parameters in an HTTP POST request:

  1. function qrdecode($file, $filetype='image/png') {
  2.     $url = 'http://zxing.org/w/decode';
  3.     $args = array(
  4.         'full'  => 'true',
  5.     );
  6.     $files=array('f' => array('name' => basename($file), 'tmp_name' => $file, 'type' => $filetype));
  7.  
  8.     $response=sendpost($url, $args, $files, false); // DON'T encode data in base64
  9.  
  10.     if (!$response or $response[0] != 200) {
  11.         return false;
  12.     }
  13.  
  14.     if (preg_match('#<html>.*</html>#', $response[2])) {
  15.         return false;
  16.     }
  17.    
  18.     return strip_tags($response[2]);
  19. }

qrdecode sends a POST request at the address http://zxing.org/w/decode with an attached file. If the service returns another HTML document, qrdecode returns false.

IMPORTANT: Be very careful when inserting data returned by another program. If possible, remove all tags with strip_tags and add your own formatting.

Read the manual Write a CMS in PHP to learn how to structure the development of a web site.

Comments

Your comment:
[p] [b] [i] [u] [s] [quote] [pre] [br] [code] [url] [email] strip help 2000

Enter a maximum of 2000 characters.
Improve the presentation of your text with the following formatting tags:
[p]paragraph[/p], [b]bold[/b], [i]italics[/i], [u]underline[/u], [s]strike[/s], [quote]citation[/quote], [pre]as is[/pre], [br]line break,
[url]http://www.izend.org[/url], [url=http://www.izend.org]site[/url], [email]izend@izend.org[/email], [email=izend@izend.org]izend[/email],
[code]command[/code], [code=language]source code in c, java, php, html, javascript, xml, css, sql, bash, dos, make, etc.[/code].