Webinar

November 3rd, 2021 12:00 PM EDT
Automation for Field Services & DistributionNovember 3rd, 2021 12:00 PM EDT
Learn how creating a digital workforce can improve your supply chain processes!

Self-hosted On-demand Runtime Environments API

This page described the protocol used with the Self-hosted On-demand Runtime Environments -feature to help in the development of custom On-demand Runtime Environments Provisioners.

Authentication

All requests sent by the Control Room to the On-demand Runtime Environments Provisioner are authenticated with an HMAC algorithm that relies on a secret that is shared between the two parties. The implemented algorithm is loosely based on the Signature Version 4 Signing process used by Amazon on AWS. The Calculated signature is in header x-rc-signature

In addition to the signature, a timestamp is included in the headers and the recipient must verify the timestamp is recent to protect against replay attacks.

Commands

The following are the supported commands for the On-demand Runtime Environments API. Control Room sends these commands to the HTTP(S) endpoint configured into the Control room and signs them with the HMAC algorithm described above.

  • Both responses and requests with payload must be sent in application/json content type.
  • The On-demand Runtime Environments Provisioner must verify the authenticity of the message by verifying the HMAC signature was generated with the shared secret
  • The On-demand Runtime Environments Provisioner must verify the timestamp of the message was generated within 15 minutes of receiving the request
  • Control room must retry a request if the response code is 500
  • Control room may not retry a request if the response code is 400,403

Common HTTP status codes

The On-demand Runtime Environments Provisioner must implement the sending of the following status codes for all requests.

Status codeDescription
400Request not recognized
403HMAC check failed

Start Command

When receiving a start command:

  • On-demand Runtime Environments Provisioner must start a runtime
  • The runtime must link itself to the cloud using the runtimeLinkToken given in the request
  • The runtime must assume the runtime Id given in the request in the field runtimeId.

Request

AttributeTypeDescription
typestringvalue is start
workspaceIdstringworkspaceId of robot
runtimeLinkTokenstringSingle use link token that the started runtime should use to link to control room
runtimeIdstringruntimeId that the runtime should assign itself.
maxLifetimeSecondsnumberMax lifetime of runtime in seconds. Provisioner may stop runtime if runtime is active for longer than this time.

Response

The On-demand Runtime Environments Provisioner must implement the following status codes in addition to the common status codes.

Status codeDescription
200Runtime Started
500Runtime creation failed

Stop Command

When receiving a stop command:

  • The On-demand Runtime Environments Provisioner may stop the runtime execution

Request

AttributeTypeDescription
typestringvalue is stop
workspaceIdstringworkspaceId of robot
runtimeIdstringruntimeId that the runtime should assign itself.

Response

The On-demand Runtime Environments Provisioner must implement the following status codes in addition to the common status codes.

Status codeDescription
200Runtime stopped or command not implemented
500Runtime stopping failed

Status Command

Request

AttributeTypeDescription
typestringvalue is status

Response

AttributeTypeDescription
versionnumbervalue is 1
statusstring'OK' if everything is ok, error message otherwise

Example implementation of Authentication in Python

The following is an example implementation of the HMAC and timestamp verification written in Python.

from http.server import BaseHTTPRequestHandler, HTTPServer
import logging
import hashlib
import base64
import hmac
from time import time



class S(BaseHTTPRequestHandler):
    def do_POST(self):
        content_length = int(self.headers['Content-Length']) # <--- Gets the size of data
        post_data = self.rfile.read(content_length) # <--- Gets the data itself

        # shared secret
        secret = 'secret'

        # variables
        algorithm = 'sha256'
        method = 'POST'
        uri = self.path
        querystring = ''

        # values from headers
        signature_from_header = self.headers['x-rc-signature']
        timestamp = self.headers['x-rc-timestamp']
        signed_headers = self.headers['x-rc-signed-headers']

        headers = []
        for header in signed_headers.split(';'):
            headers.append(header + ':' + self.headers[header])

        # body checksum
        payload_hash = base64.b64encode(hashlib.sha256(post_data).digest()).decode('utf-8')

        # construct string that represents request and hash it
        request = method + '\n' + uri + '\n' + querystring + '\n' + '\n'.join(headers) + '\n' + signed_headers + '\n' + payload_hash
        request_hash = base64.b64encode(hashlib.sha256(request.encode('utf-8')).digest()).decode('utf-8')

        # construct string that represents algorith, timestamp and request hash
        string_to_sign = algorithm + '\n' + timestamp + '\n' + request_hash

        # sign the string with the shared secret
        signature = hmac.new(secret.encode('utf-8'), (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest()

        # Verify teh signature matches
        signature_valid = signature == signature_from_header;

        # Verify the timestamp is recent
        timestamp_current = int(time())
        timestamp_delta = abs(timestamp_current - int(timestamp));
        timestamp_valid = timestamp_delta < 15*60; # 15 minutes

        authenticated = signature_valid and timestamp_valid

        print('authenticated: ', authenticated)

        self.send_response(200 if authenticated else 403)
        self.end_headers()



def run(server_class=HTTPServer, handler_class=S, port=8080):
    logging.basicConfig(level=logging.INFO)
    server_address = ('', port)
    httpd = server_class(server_address, handler_class)
    logging.info('Starting httpd...\n')
    print(httpd)
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    httpd.server_close()
    logging.info('Stopping httpd...\n')

if __name__ == '__main__':
    from sys import argv
    if len(argv) == 2:
        run(port=int(argv[1]))
    else:
        run()

November 18, 2021