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 code | Description |
---|---|
400 | Request not recognized |
403 | HMAC 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
Attribute | Type | Description |
---|---|---|
type | string | value is start |
workspaceId | string | workspaceId of robot |
runtimeLinkToken | string | Single use link token that the started runtime should use to link to control room |
runtimeId | string | runtimeId that the runtime should assign itself. |
maxLifetimeSeconds | number | Max 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 code | Description |
---|---|
200 | Runtime Started |
500 | Runtime creation failed |
Stop Command
When receiving a stop command:
- The On-demand Workers Provisioner may stop the runtime execution
Request
Attribute | Type | Description |
---|---|---|
type | string | value is stop |
workspaceId | string | workspaceId of robot |
runtimeId | string | runtimeId 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 code | Description |
---|---|
200 | Runtime stopped or command not implemented |
500 | Runtime stopping failed |
Status Command
Request
Attribute | Type | Description |
---|---|---|
type | string | value is status |
Response
Attribute | Type | Description |
---|---|---|
version | number | value is 1 |
status | string | '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()