Self-hosted On-demand Workers API

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

Authentication

All requests sent by the Control Room to the On-demand Workers 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 Workers 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 Workers Provisioner must verify the authenticity of the message by verifying the HMAC signature was generated with the shared secret
  • The On-demand Workers 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 Workers 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 Workers 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 Workers 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 Workers 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 Workers 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 HmacAuthenticatingServer(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 the 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=HmacAuthenticatingServer, 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()
Last edit: November 18, 2021