diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f41a33 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +**/__pycache__ +**/dist +**/randomapi.egg-info diff --git a/LICENSE b/LICENSE index 7bd10cc..ccd2e5e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ -The MIT License (MIT) +MIT License (MIT) -Copyright (c) 2014 mitchchn +Original work Copyright (c) 2014 mitchchn +Modified work Copyright (c) 2020 Tantusar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +19,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index c7db68e..aff3ea2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ randomapi ========= +[![PyPI version](https://badge.fury.io/py/randomapi.svg)](https://badge.fury.io/py/randomapi) + Python implementation of the RANDOM.org JSON-RPC API: -http://api.random.org/json-rpc/1/ +http://api.random.org/json-rpc/2/ RANDOM.org generates true random numbers using a seed based on atmospheric radio noise. This is useful for applications where pseudo-random generators are not good enough, such as cryptography. @@ -18,13 +20,16 @@ Features Requirements ------------ -- Python 2.6 or higher +- Python 2.6 or higher, or 3.6 or higher - An API key from: http://api.random.org Example Usage ------------- +```py +from randomapi import RandomJSONRPC - # Returns a list of 5 true random numbers between 0 and 10 +# Returns a list of 5 true random numbers between 0 and 10 - random_client = RandomJSONRPC(api_key) # Requires a valid API key - nums = random_client.generate_integers(n=5, min=0, max=10) +random_client = RandomJSONRPC(api_key) # Requires a valid API key +nums = random_client.generate_integers(n=5, min=0, max=10).parse() +``` diff --git a/randomapi.py b/randomapi.py index 5ecaabe..794fab1 100644 --- a/randomapi.py +++ b/randomapi.py @@ -4,15 +4,18 @@ randomapi.py: a Python implementation of the RANDOM.org JSON-RPC API Author: Mitchell Cohen (mitch.cohen@me.com) -http://github.com/mitchchn/randomapi +https://github.com/mitchchn/randomapi -Date: April 20, 2014 -Version: 0.1 +Maintainer: Thomas Chick (twitter.com/Tantusar) +https://github.com/tantusar/randomapi + +Date: January 18, 2020 +Version: 0.3.2 RANDOM.org API reference: -- https://api.random.org/json-rpc/1/ +- https://api.random.org/json-rpc/2/ -randomapi.py supports all basic and signed methods in Release 1 +randomapi.py supports all basic and signed methods in Release 2 of the RANDOM.ORG API. It respects delay requests from the server and has the ability to verify digitally-signed data. @@ -24,24 +27,35 @@ # Returns a list of 5 random numbers between 0 and 10 random_client = RandomJSONRPC(api_key) # Requires a valid API key - nums = random_client.generate_integers(n=5, min=0, max=10) + nums = random_client.generate_integers(n=5, min=0, max=10).parse() """ import time import json import logging -import urllib2 + +# Python 2/3 Compatibility +try: + from urllib.request import urlopen, Request + from urllib.error import HTTPError + from urllib.parse import urlparse, urlencode +except ImportError: + from urlparse import urlparse + from urllib import urlencode + from urllib2 import urlopen, Request, HTTPError + import uuid from collections import OrderedDict ###################### Constants ############################# -JSON_URL = "https://api.random.org/json-rpc/1/invoke" +JSON_URL = "https://api.random.org/json-rpc/2/invoke" # RANDOM.org API method names INTEGER_METHOD = "generateIntegers" +INTEGER_SEQUENCE_METHOD = "generateIntegerSequences" DECIMAL_METHOD = "generateDecimalFractions" GAUSSIAN_METHOD = "generateGaussians" STRING_METHOD = "generateStrings" @@ -50,11 +64,13 @@ USAGE_METHOD = "getUsage" SIGNED_INTEGER_METHOD = "generateSignedIntegers" +SIGNED_INTEGER_SEQUENCE_METHOD = "generateSignedIntegerSequences" SIGNED_DECIMAL_METHOD = "generateDecimalFractions" SIGNED_GAUSSIAN_METHOD = "generateSignedGaussians" SIGNED_STRING_METHOD = "generateSignedStrings" SIGNED_UUID_METHOD = "generateSignedUUIDs" SIGNED_BLOB_METHOD = "generateSignedBlobs" +RESULT_METHOD = "getResult" VERIFY_SIGNATURE_METHOD = "verifySignature" # RANDOM.org API parameters @@ -68,6 +84,7 @@ RANDOM = "random" AUTHENTICITY = "authenticity" SIGNATURE = "signature" +SERIAL_NUMBER = "serialNumber" # RANDOM.org blob formats @@ -77,10 +94,11 @@ def valid_json_methods(): '''Returns a list of valid JSON-RPC method names from the RANDOM.org API''' - return [INTEGER_METHOD, DECIMAL_METHOD, GAUSSIAN_METHOD, STRING_METHOD, - UUID_METHOD, BLOB_METHOD, USAGE_METHOD, SIGNED_INTEGER_METHOD, - SIGNED_BLOB_METHOD, SIGNED_DECIMAL_METHOD, SIGNED_GAUSSIAN_METHOD, - SIGNED_STRING_METHOD, SIGNED_UUID_METHOD, VERIFY_SIGNATURE_METHOD] + return [INTEGER_METHOD, INTEGER_SEQUENCE_METHOD, DECIMAL_METHOD, GAUSSIAN_METHOD, + STRING_METHOD, UUID_METHOD, BLOB_METHOD, USAGE_METHOD, SIGNED_INTEGER_METHOD, + SIGNED_INTEGER_SEQUENCE_METHOD, SIGNED_BLOB_METHOD, SIGNED_DECIMAL_METHOD, + SIGNED_GAUSSIAN_METHOD, SIGNED_STRING_METHOD, SIGNED_UUID_METHOD, + RESULT_METHOD, VERIFY_SIGNATURE_METHOD] def parse_random(json_string): @@ -105,12 +123,13 @@ def compose_api_call(json_method_name, *args, **kwargs): Returns a fully-formed JSON-RPC string for a RANDOM.org API method :param json_method_name: Name of the method. Can be one of: - INTEGER_METHOD, DECIMAL_METHOD, GAUSSIAN_METHOD, STRING_METHOD, - UUID_METHOD, BLOB_METHOD, USAGE_METHOD, SIGNED_INTEGER_METHOD, - SIGNED_BLOB_METHOD, SIGNED_DECIMAL_METHOD, SIGNED_GAUSSIAN_METHOD, - SIGNED_STRING_METHOD, SIGNED_UUID_METHOD, VERIFY_SIGNATURE_METHOD + INTEGER_METHOD, INTEGER_SEQUENCE_METHOD, DECIMAL_METHOD, GAUSSIAN_METHOD, + STRING_METHOD, UUID_METHOD, BLOB_METHOD, USAGE_METHOD, SIGNED_INTEGER_METHOD, + SIGNED_INTEGER_SEQUENCE_METHOD, SIGNED_BLOB_METHOD, SIGNED_DECIMAL_METHOD, + SIGNED_GAUSSIAN_METHOD, SIGNED_STRING_METHOD, SIGNED_UUID_METHOD, + RESULT_METHOD, VERIFY_SIGNATURE_METHOD :param args: Positional parameters - :param kwargs: Named parameters. See: https://api.random.org/json-rpc/1/basic + :param kwargs: Named parameters. See: https://api.random.org/json-rpc/2/basic for descriptions of methods and their parameters. """ @@ -126,12 +145,12 @@ def compose_api_call(json_method_name, *args, **kwargs): params = args request_data = { - "method": unicode(json_method_name), - "id": unicode(uuid.uuid4()), - "jsonrpc": u"2.0", + "method": str(json_method_name), + "id": str(uuid.uuid4()), + "jsonrpc": "2.0", "params": params } - return json.dumps(request_data) + return json.dumps(request_data).encode('utf-8') def http_request(url, json_string): @@ -141,9 +160,9 @@ def http_request(url, json_string): :param json_string: JSON-String """ - request = urllib2.Request(url, data=json_string) + request = Request(url, data=json_string) request.add_header("Content-Type", "application/json") - response = urllib2.urlopen(request) + response = urlopen(request) response_string = response.read() response.close() @@ -160,7 +179,7 @@ def __init__(self, api_key): The class is simple to use: simply instantiate a RandomJSONRPC object with a valid API key, and call the appropriate method on the server. - For a list of available methods and parameters, see: + For a list of available methods and parameters, see: :param api_key: String representing a RANDOM.org JSON-RPC API key """ @@ -168,22 +187,6 @@ def __init__(self, api_key): self._time_of_last_request = 0 self._advisory_delay = 0 - def _refresh(self): - self._json_data = {} - self._result = {} - self._random = [] - self._signature = "" - - def _populate(self): - if RESULT in self._json_data: - self._result = self._json_data[RESULT] - if ADVISORY_DELAY in self._result: - self._advisory_delay = float(self._result[ADVISORY_DELAY]) / 1000.0 - if RANDOM in self._result: - self._random = self._result[RANDOM] - if SIGNATURE in self._result: - self._signature = self._result[SIGNATURE] - def delay_request(self, requested_delay): elapsed = time.time() - self._time_of_last_request remaining_time = requested_delay - elapsed @@ -193,21 +196,12 @@ def delay_request(self, requested_delay): if remaining_time - elapsed > 0: time.sleep(remaining_time) - def check_errors(self): - '''Checks to see if the received JSON object has errors''' - if 'error' in self._json_data: - error = self._json_data['error'] - code = error['code'] - message = error['message'] - raise Exception( -"""Error code: {}. Message: {} -See: https://api.random.org/json-rpc/1/error-codes""".format(code, message)) - - def send_request(self, request_string): + def send_request(self, request_string, method=""): '''Wraps outgoing JSON requests''' - # Wipe out any data from previous request - self._refresh() + # Create a new response class, using an ordered dict to + # preserve the integrity of signed data + # Respect delay requests from the server if self._time_of_last_request == 0: @@ -220,14 +214,10 @@ def send_request(self, request_string): self._time_of_last_request = time.time() # Use an ordered dict to preserve the integrity of signed data - self._json_data = json_to_ordered_dict(json_string) - self.check_errors() - self._populate() - return self - - def parse(self): - '''Parses the received JSON data object and returns the random data''' - return self._random['data'] + response = RandomJSONResponse(json_to_ordered_dict(json_string), method) + if ADVISORY_DELAY in response._result: + self._advisory_delay = float(response._result[ADVISORY_DELAY]) / 1000.0 + return response ####################### RANDOM.org API methods ########################## @@ -236,7 +226,14 @@ def generate_integers(self, n, min, max, replacement=True, base=10): request_string = compose_api_call( INTEGER_METHOD, apiKey=self.api_key, n=n, min=min, max=max, replacement=replacement, base=base) - return self.send_request(request_string).parse() + return self.send_request(request_string, INTEGER_METHOD) + + def generate_integer_sequences(self, n, length, min, max, replacement=True, base=10): + '''Returns a list of lists of true random integers with a user-defined range''' + request_string = compose_api_call( + INTEGER_SEQUENCE_METHOD, apiKey=self.api_key, + n=n, length=length, min=min, max=max, replacement=replacement, base=base) + return self.send_request(request_string, INTEGER_SEQUENCE_METHOD) def generate_decimal_fractions(self, n, decimal_places, replacement=True): '''Returns a list of true random decimal fractions between [0,1] @@ -244,7 +241,7 @@ def generate_decimal_fractions(self, n, decimal_places, replacement=True): request_string = compose_api_call( DECIMAL_METHOD, apiKey=self.api_key, n=n, decimalPlaces=decimal_places, replacement=replacement) - return self.send_request(request_string).parse() + return self.send_request(request_string, DECIMAL_METHOD) def generate_gaussians(self, n, mean, standard_deviation, significant_digits): @@ -254,7 +251,7 @@ def generate_gaussians(self, n, mean, standard_deviation, n=n, mean=mean, standardDeviation=standard_deviation, significantDigits=significant_digits) - return self.send_request(request_string).parse() + return self.send_request(request_string, GAUSSIAN_METHOD) def generate_strings(self, n, length, characters, replacement=True): '''Returns a list of true random strings composed from a user-defined @@ -262,28 +259,27 @@ def generate_strings(self, n, length, characters, replacement=True): request_string = compose_api_call( STRING_METHOD, apiKey=self.api_key, n=n, length=length, characters=characters, replacement=replacement) - return self.send_request(request_string).parse() + return self.send_request(request_string, STRING_METHOD) def generate_uuids(self, n): '''Returns a list of true random UUIDs (version 4)''' request_string = compose_api_call( UUID_METHOD, apiKey=self.api_key, n=n) - return self.send_request(request_string).parse() + return self.send_request(request_string, UUID_METHOD) def generate_blobs(self, n, size, format=FORMAT_BASE64): '''Returns a list of Binary Large OBjects (BLOBs) containing true random data''' request_string = compose_api_call( BLOB_METHOD, apiKey=self.api_key, n=n, size=size, format=format) - return self.send_request(request_string).parse() + return self.send_request(request_string, BLOB_METHOD) def get_usage(self): '''Returns a dictionary of usage information for the client's API key.''' request_string = compose_api_call( USAGE_METHOD, apiKey=self.api_key) - self.send_request(request_string) - return self._result + return self.send_request(request_string, USAGE_METHOD) ####################### Digitally-signed API methods ########################## @@ -291,14 +287,20 @@ def generate_signed_integers(self, n, min, max, replacement=True, base=10): request_string = compose_api_call( SIGNED_INTEGER_METHOD, apiKey=self.api_key, n=n, min=min, max=max, replacement=replacement, base=base) - return self.send_request(request_string).parse() + return self.send_request(request_string, SIGNED_INTEGER_METHOD) + + def generate_signed_integer_sequences(self, n, length, min, max, replacement=True, base=10): + request_string = compose_api_call( + SIGNED_INTEGER_SEQUENCE_METHOD, apiKey=self.api_key, n=n, length=length, min=min, + max=max, replacement=replacement, base=base) + return self.send_request(request_string, SIGNED_INTEGER_SEQUENCE_METHOD) def generate_signed_decimal_fractions(self, n, decimal_places, replacement=True): request_string = compose_api_call( SIGNED_DECIMAL_METHOD, apiKey=self.api_key, n=n, decimalPlaces=decimal_places, replacement=replacement) - return self.send_request(request_string).parse() + return self.send_request(request_string, SIGNED_DECIMAL_METHOD) def generate_signed_gaussians(self, n, mean, standard_deviation, significant_digits): @@ -307,24 +309,33 @@ def generate_signed_gaussians(self, n, mean, standard_deviation, n=n, mean=mean, standardDeviation=standard_deviation, significantDigits=significant_digits) - return self.send_request(request_string).parse() + return self.send_request(request_string, SIGNED_GAUSSIAN_METHOD) def generate_signed_strings(self, n, length, characters, replacement=True): request_string = compose_api_call( SIGNED_STRING_METHOD, apiKey=self.api_key, n=n, length=length, characters=characters, replacement=replacement) - return self.send_request(request_string).parse() + return self.send_request(request_string, SIGNED_STRING_METHOD) def generate_signed_uuids(self, n): request_string = compose_api_call( SIGNED_UUID_METHOD, apiKey=self.api_key, n=n) - return self.send_request(request_string).parse() + return self.send_request(request_string, SIGNED_UUID_METHOD) def generate_signed_blobs(self, n, size, format=FORMAT_BASE64): request_string = compose_api_call( SIGNED_BLOB_METHOD, apiKey=self.api_key, n=n, size=size, format=format) - return self.send_request(request_string).parse() + return self.send_request(request_string, SIGNED_BLOB_METHOD) + + def get_result(self, serial_number): + '''Returns the result of a previous request given a supplied serial + number.''' + request_string = compose_api_call( + RESULT_METHOD, apiKey=self.api_key, serialNumber=serial_number) + response = self.send_request(request_string, RESULT_METHOD) + response._method = response._random['method'] + return response def verify_signature(self): """ @@ -337,9 +348,56 @@ def verify_signature(self): VERIFY_SIGNATURE_METHOD, random=self._random, signature=self._signature) - self.send_request(json_string) + response = self.send_request(json_string, VERIFY_SIGNATURE_METHOD) - if AUTHENTICITY in self._result: - return self._result[AUTHENTICITY] + if AUTHENTICITY in response._result: + return response._result[AUTHENTICITY] else: raise Exception("Unable to verify authenticity of signed data") + +class RandomJSONResponse: + def __init__(self, json_data, method=""): + self._json_data = json_data + self._result = {} + self._random = [] + self._signature = "" + self._serial_number = 0 + self._method = method + self.check_errors() + self._populate() + + def check_errors(self): + '''Checks to see if the received JSON object has errors''' + if 'error' in self._json_data: + error = self._json_data['error'] + code = error['code'] + message = error['message'] + raise Exception( +"""Error code: {}. Message: {} +See: https://api.random.org/json-rpc/2/error-codes""".format(code, message)) + + def _populate(self): + if RESULT in self._json_data: + self._result = self._json_data[RESULT] + if RANDOM in self._result: + self._random = self._result[RANDOM] + if SIGNATURE in self._result: + self._signature = self._result[SIGNATURE] + if SERIAL_NUMBER in self._random: + self._serial_number = self._random[SERIAL_NUMBER] + + def parse(self): + '''Parses the received JSON data object and returns the random data''' + return self._random['data'] + + def __repr__(self): + try: + return "" + except: + return "" + + def __str__(self): + try: + return str(self._random['data']) + except: + return "" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b88034e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1e42452 --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +from setuptools import setup + +with open("README.md", "r") as fh: + long_description = fh.read() + +setup( + name = 'randomapi', + version = '0.3.2', + description = 'RANDOM.org JSON-RPC API implementation', + long_description = long_description, + long_description_content_type = 'text/markdown', + author = 'Mitchell Cohen ', + author_email = 'mitch.cohen@me.com', + maintainer = 'Thomas Chick (twitter.com/Tantusar)', + py_modules = ['randomapi'], + url = 'https://github.com/Tantusar/randomapi', + license = 'MIT License', + classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', + 'Operating System :: OS Independent', + ], + python_requires = '>=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, >=3.6', + download_url = 'https://github.com/Tantusar/randomapi/archive/v0.3.2.tar.gz' +)