Writing your own client

This section contains an explanation of the OpenSRS client/server data exchange. This information is useful if you want to write your own client.

XML client protocol fundamentals

In our XML Client Protocol (XCP), the sender of a message (request or reply) must always precede the message with the header 'Content-Length: X', where 'X' is the number of bytes in the actual message (without the header). This header must be followed by a carriage return and line feed combination. Counted bytes only occur on the first non-blank line.

Content-length: 55\015\012 # carriage return/line feed # blank lines are ignored
<?xml version='1.0' encoding='UTF-8' standalone='no' ?> <!DOCTYPE OPS_envelope SYSTEM 'ops.dtd'>
<OPS_envelope>
  <header>
<version>0.9</version> </header>
<body>
    <data_block>
[ etc ]

XCP allows empty space to be placed in the XML message, as long as the XML is still valid, so empty lines or end-of-line characters may be inserted (though they must be counted in the byte count).

For more information regarding the transmission of data over HTTPS, refer to this document: http://www.ietf.org/rfc/rfc2616.txt

MD5 examples

The following examples show how to add an MD5 Signature and create an XML packet for the various client languages.

use Digest::MD5 qw/md5_hex/; 
md5_hex(md5_hex($xml, $private_key),$private_key)
md5(md5($xml.$private_key).$private_key);
protected String md5Sum(String str) { String sum = new 
   String();
      try {
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        sum = String.format("%032x", new BigInteger(1, md5.digest(str.getBytes())));
         } catch (Exception ex) {
         }
      return sum;
}
public String getSignature(String xml) {
      return md5Sum(md5Sum(xml + privateKey) + privateKey); 
   }
md5_obj = hashlib.md5()
md5_obj.update((xml + api_key).encode())
signature = md5_obj.hexdigest()

md5_obj = hashlib.md5()
md5_obj.update((signature + api_key).encode())
signature = md5_obj.hexdigest()
md5 = Digest::MD5.new
md5.update(xml + api_key)
signature = md5.hexdigest

md5 = Digest::MD5.new
md5.update(signature + api_key)
signature = md5.hexdigest

Coding examples

#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket::SSL;
use Digest::MD5 qw/md5_hex/;
use LWP::UserAgent;
use Data::Dumper;

my $TEST_MODE = 1;    # Will connect to OpenSRS's test environment

my $connection_options = {
    'live' => {

        # IP whitelisting required
        api_host_port => 'rr-n1-tor.opensrs.net:55443',
        api_key => '<YOUR API KEY>',
        reseller_username => '<YOUR RESELLER USERNAME>',
    },
    'test' => {

        # IP whitelisting not required
        api_host_port => 'horizon.opensrs.net:55443',
        api_key => '<YOUR API KEY>',
        reseller_username => '<YOUR RESELLER USERNAME>',
    },

};

my $connection_details;

if ($TEST_MODE) {
    $connection_details = $connection_options->{'test'};
}
else {
    $connection_details = $connection_options->{'live'};
}

my $ua = new LWP::UserAgent;

# Simple lookup command
my $xml = <<DATA;
<?xml version='1.0' encoding='UTF-8' standalone='no' ?>
<!DOCTYPE OPS_envelope SYSTEM 'ops.dtd'>
<OPS_envelope>
<header>
    <version>0.9</version>
</header>
<body>
<data_block>
    <dt_assoc>
        <item key="protocol">XCP</item>
        <item key="action">LOOKUP</item>
        <item key="object">DOMAIN</item>
        <item key="attributes">
         <dt_assoc>
                <item key="domain">myfirstopensrsapitest.com</item>
         </dt_assoc>
        </item>
    </dt_assoc>
</data_block>
</body>
</OPS_envelope>
DATA

my $response = $ua->post(
    "https://$connection_details->{api_host_port}",
    'Content-Type' => 'text/xml',
    'X-Username'   => $connection_details->{reseller_username},
    'X-Signature'  => md5_hex(
        md5_hex( $xml, $connection_details->{api_key} ),
        $connection_details->{api_key}
    ),
    'Content' => $xml
);

print "Request to $connection_details->{api_host_port} as reseller: $connection_details->{reseller_username}\n$xml\n";

print "Response:\n";
if ( $response->is_success ) {
    print Dumper( $response->content );
}
else {
    print Dumper($response);
}
<?php

// Note: Requires cURL library
$TEST_MODE = true;

$connection_options = [
    'live' => [
        'api_host_port' => 'https://rr-n1-tor.opensrs.net:55443',
        'api_key' => '<YOUR API KEY>',
        'reseller_username' => '<YOUR RESELLER USERNAME>'
    ],
    'test' => [
        'api_host_port' => 'https://horizon.opensrs.net:55443',
        'api_key' => '<YOUR API KEY>',
        'reseller_username' => '<YOUR RESELLER USERNAME>'
    ]
];

if ($TEST_MODE) {
    $connection_details = $connection_options['test'];
} else {
    $connection_details = $connection_options['live'];
}

$xml = <<<EOD
<?xml version='1.0' encoding='UTF-8' standalone='no' ?>
<!DOCTYPE OPS_envelope SYSTEM 'ops.dtd'>
<OPS_envelope>
<header>
    <version>0.9</version>
</header>
<body>
<data_block>
    <dt_assoc>
        <item key="protocol">XCP</item>
        <item key="action">LOOKUP</item>
        <item key="object">DOMAIN</item>
        <item key="attributes">
         <dt_assoc>
                <item key="domain">myfirstopensrsapitest.com</item>
         </dt_assoc>
        </item>
    </dt_assoc>
</data_block>
</body>
</OPS_envelope> 
EOD;

$data = [
    'Content-Type:text/xml',
    'X-Username:' . $connection_details['reseller_username'],
    'X-Signature:' . md5(md5($xml . $connection_details['api_key']) .  $connection_details['api_key']),
];

$ch = curl_init($connection_details['api_host_port']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $data);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);

$response = curl_exec($ch);

echo 'Request as reseller: ' . $connection_details['reseller_username'] . "\n" .  $xml . "\n";

echo "Response\n";
echo $response . "\n";
?>
import java.util.HashMap;
import java.io.*;
import java.security.MessageDigest;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;

public class OpenSRSAPITest {

    public static void main (String[] args) {

        final boolean TEST_MODE = true;

        HashMap <String, String> liveDetails = new HashMap <String,String>();
        liveDetails.put("apiHostPort","https://rr-n1-tor.opensrs.net:55443");
        liveDetails.put("apiKey", "<YOUR_API_KEY>");
        liveDetails.put("resellerUsername", "<YOUR_RESELLER_USERNAME>");

        HashMap <String, String> testDetails = new HashMap <String,String>();
        testDetails.put("apiHostPort", "https://horizon.opensrs.net:55443");
        testDetails.put("apiKey", "<YOUR_API_KEY>");
        testDetails.put("resellerUsername", "<YOUR_RESELLER_USERNAME>");

        HashMap <String, String> connectionDetails;

        if (TEST_MODE)
            connectionDetails = testDetails;
        else
            connectionDetails = liveDetails;

        // Simple lookup command
        String xml = "<?xml version='1.0' encoding='UTF-8' standalone='no' ?>" +
        "<!DOCTYPE OPS_envelope SYSTEM 'ops.dtd'>" +
        "<OPS_envelope>" +
        "<header>" +
            "<version>0.9</version>" +
        "</header>" +
        "<body>" +
        "<data_block>" +
            "<dt_assoc>" +
                "<item key='protocol'>XCP</item>" +
                "<item key='action'>LOOKUP</item>" +
                "<item key='object'>DOMAIN</item>" +
                "<item key='attributes'>" +
                    "<dt_assoc>" +
                        "<item key='domain'>myfirstopensrsapitest.com</item>" +
                    "</dt_assoc>" +
                "</item>" +
            "</dt_assoc>" +
        "</data_block>" +
        "</body>" +
        "</OPS_envelope>";

// Create signature
        String signature = md5_hex( md5_hex(xml + connectionDetails.get("apiKey")) + connectionDetails.get("apiKey"));

        System.out.println("Request as reseller: " + connectionDetails.get("resellerUsername"));
        System.out.println(xml);

        // Make HTTP Post
        try {
            URL url = new URL(connectionDetails.get("apiHostPort"));
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setDoOutput(true);
            conn.setRequestMethod("POST");

            conn.setRequestProperty("Content-Type", "text/xml");
            conn.setRequestProperty("X-Username", connectionDetails.get("resellerUsername"));
            conn.setRequestProperty("X-Signature", signature);

            OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream());
            writer.write(xml);
            writer.flush();

            System.out.println("Response Code: " + conn.getResponseCode());
            System.out.println("Response Message: " + conn.getResponseMessage());
            System.out.println("Response XML:");
            String line;
            BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            while ((line = reader.readLine()) != null) {
              System.out.println(line);
            }
            writer.close();
            reader.close();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static String md5_hex(String s) {
        try {
            MessageDigest m = MessageDigest.getInstance("MD5");
            m.update(s.getBytes(), 0, s.length());
            BigInteger i = new BigInteger(1,m.digest());
            return String.format("%1$032x", i);
        }
        catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}
#!/usr/bin/python

import requests
import hashlib

TEST_MODE = 1

connection_options = {
        'live' : {
         # IP whitelisting required
             'reseller_username': '<YOUR RESELLER USERNAME>',
             'api_key':'<YOUR API KEY>',
             'api_host_port': 'https://rr-n1-tor.opensrs.net:55443',
        },
        'test' : {
             # IP whitelisting not required
             'reseller_username': '<YOUR RESELLER USERNAME>',
             'api_key':'<YOUR API KEY>',
             'api_host_port': 'https://horizon.opensrs.net:55443',

        }
}

if TEST_MODE == 1:
    connection_details = connection_options['test']
else:
    connection_details = connection_options['live']

xml = '''
<?xml version='1.0' encoding='UTF-8' standalone='no' ?>
<!DOCTYPE OPS_envelope SYSTEM 'ops.dtd'>
<OPS_envelope>
<header>
    <version>0.9</version>
</header>
<body>
<data_block>
    <dt_assoc>
        <item key="protocol">XCP</item>
        <item key="action">LOOKUP</item>
        <item key="object">DOMAIN</item>
        <item key="attributes">
         <dt_assoc>
                <item key="domain">myfirstopensrsapitest.com</item>
         </dt_assoc>
        </item>
    </dt_assoc>
</data_block>
</body>
</OPS_envelope>
'''

md5_obj = hashlib.md5()
md5_obj.update((xml + connection_details['api_key']).encode())
signature = md5_obj.hexdigest()

md5_obj = hashlib.md5()
md5_obj.update((signature + connection_details['api_key']).encode())
signature = md5_obj.hexdigest()

headers = {
        'Content-Type':'text/xml',
        'X-Username': connection_details['reseller_username'],
        'X-Signature':signature,
};

print("Request to {} as reseller {}:".format(connection_details['api_host_port'],connection_details['reseller_username']))
print(xml)

r = requests.post(connection_details['api_host_port'], data = xml, headers=headers )

print("Response:")
if r.status_code == requests.codes.ok:
    print(r.text)
else:
    print (r.status_code)
    print (r.text)
#!/usr/bin/ruby

require 'uri'
require 'net/http'
require 'net/https'
require 'digest'

TEST_MODE = 1
connection_options = {
        'live' => {
         # IP whitelisting required
             'reseller_username' => '<YOUR RESELLER USERNAME>',
             'api_key' => '<YOUR API KEY>',
             'api_host_port' => 'https://rr-n1-tor.opensrs.net:55443',
        },
        'test' => {
             # IP whitelisting not required
             'reseller_username' => '<YOUR RESELLER USERNAME>',
             'api_key' => '<YOUR API KEY>',
             'api_host_port' => 'https://horizon.opensrs.net:55443',

        }
}

if TEST_MODE == 1
    connection_details = connection_options['test']
else
    connection_details = connection_options['live']
end

xml = <<-eos
<?xml version='1.0' encoding='UTF-8' standalone='no' ?>
<!DOCTYPE OPS_envelope SYSTEM 'ops.dtd'>
<OPS_envelope>
<header>
    <version>0.9</version>
</header>
<body>
<data_block>
    <dt_assoc>
        <item key="protocol">XCP</item>
        <item key="action">LOOKUP</item>
        <item key="object">DOMAIN</item>
        <item key="attributes">
         <dt_assoc>
                <item key="domain">myfirstopensrsapitest.com</item>
         </dt_assoc>
        </item>
    </dt_assoc>
</data_block>
</body>
</OPS_envelope>
eos

md5 = Digest::MD5.new
md5.update(xml + connection_details['api_key'])
signature = md5.hexdigest

md5 = Digest::MD5.new
md5.update(signature + connection_details['api_key'])
signature = md5.hexdigest

uri = URI.parse("#{connection_details['api_host_port']}/")
https = Net::HTTP.new(uri.host,uri.port)
https.use_ssl = true
req = Net::HTTP::Post.new(uri.path, initheader = { 'Content-Type' =>'text/xml', 
	'X-Username' => connection_details['reseller_username'], 
	'X-Signature' => signature,
})

req.body = xml

puts "Request to #{connection_details['api_host_port']} as reseller #{connection_details['reseller_username']}:"
res = https.request(req)
puts xml

puts "Response:"
if res.code == "200"
	puts res.body
else
	puts res.code
	puts res.message
end

Data exchange

For each line of data passed, you must prepend the data with the length of the string packed in 'network' or big-endian order. In Perl, this is accomplished by:
$length = pack('n', length($data));
Where $data is the information you are going to send.
For example, assuming you have a socket SERVER already open to the server process, you could send data as follows:
print SERVER pack('n', length($data));
print SERVER $data;
Since you must always send the length of the string first, it will not work to simply telnet to the OpenSRS server and begin issuing commands.

Authentication handshake

The first step in communicating with the server process is the authentication handshake. This proceeds between the reseller Client and reseller Agent (server) as follows:

ProcessDescription
1Reseller ClientInitiates connection with Reseller Agent (server process) on a specific TCP/IP hostname:port.
Horizon: horizon.opensrs.net:55000 Live: rr-n1-tor.opensrs.net:55000
2Reseller AgentServer sends an XCP 'check version' request. Perl Example (hash):
{
'protocol' => 'XCP',
'action' => 'check',
'object' => 'version',
'attributes' => {
'sender' => 'OpenSRS SERVER',
'version' => '$VERSION',
'state' => 'ready'
}
}
The values of $VERSION could be something such as 'XML:0.1', which indicates the language spoken and the minimum version of the client required by this RSA. At this point, the only value for 'state' is 'ready'. Other states may be added in the future.
3Reseller ClientClient responds with an XCP 'check version' response (where version is the client's protocol version.)
Note: This number should not be changed. It allows for API changes and backward compatibility. If you change the version number of the client, results may be unpredictable.
Perl Example (hash):
{
'protocol' => 'XCP', 'action' => 'check', 'object' => 'version', 'attributes' => {
'sender' => 'OpenSRS CLIENT',
'version' => 'XML:0.1',
'state' => 'ready'
}
}
The only difference here is the value of the sender attribute. Again, the only valid state at this point is 'ready'.
4Reseller ClientClient sends user data for authentication. This is done using the XCP 'authenticate user' request.
Note: As a reseller, you have a password and a username. Do NOT send the password in this request, it is not needed. The current XML Perl Client actually sends the username in both the username and password fields. This is because the data packets are not encrypted at this stage of the transmission.
Perl Example (hash):
{
'protocol' => 'XCP',
'action' => 'authenticate',
'object' => 'user',
'attributes' => {
'crypt_type' => '',
'username' => '',
'password' => ''
}
}
The crypt_type can be either 'des' or 'blowfish'.
5Reseller AgentIf authentication is successful, the Reseller Agent (server side), sends the first challenge, but without XML. The challenge is a random number of random bits.
6Reseller ClientThe client returns the challenge's md5 checksum, encrypted with the Reseller's private key and without XML.
7Reseller AgentIf the challenge is successful, the Reseller Agent (server) replies with an XCP 'authenticate user' response.
Perl Example (hash):
{
'protocol' => 'XCP',
'action' => 'reply',
'response_code' => '200',
'response_text' => 'Authentication Successful'
}
If the Reseller Agent deems that the Reseller Client has failed the challenge, it closes the socket without sending a decline reply because it is assumed that the Reseller Client cannot understand any of the encrypted messages anyway.
Another possible response code would be code 310, if the reseller's command rate is exceeded.
8Reseller ClientIf the Reseller Client receives a response code of 200, it can then send its first XCP command. All further communication for the established session is encrypted.
The first XCP command the client must send after being authenticated is 'set(cookie)'. This is required because the cookie is used for all further authenticated commands.

Encryption

Supported ciphers

We currently supports the DES and Blowfish encryption algorithms.
The suggested method of using these encryption types is through their respective Perl modules, Crypt::DES and Crypt::Blowfish, which are then accessed through a common interface created by Crypt::CBC. For your convenience, Crypt::CBC is now included in the OpenSRS client distribution.
If you are unable to install Crypt::DES or Crypt::Blowfish there is a third option available: Crypt::Blowfish_PP, which is a module for Blowfish written in Pure Perl (PP). Our initial testing has shown this module to be at least 10 times slower than the standard Crypt::Blowfish, but it may be used as a last resort.

Private key

DES only supports keys of 8 bytes, while Blowfish supports keys of up to 56 bytes for greater security.

Private keys in OpenSRS are 112 characters in length (56 bytes), to provide the maximum security for people using Blowfish.

When creating your encryption cipher, do not use the private key in raw form. Instead, first pack the key into a hexadecimal binary string. In Perl this is accomplished with:

$private_key = pack('H*', $private_key);

You may then use the private key to create your encryption cipher, authenticate, and begin sending data to the server.