| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| import time |
| import random |
| import string |
| import hashlib |
| |
| from libcloud.utils.py3 import httplib, urlencode, basestring |
| from libcloud.common.base import JsonResponse, ConnectionUserAndKey |
| from libcloud.common.types import ProviderError, InvalidCredsError |
| |
| SALT_CHARACTERS = string.ascii_letters + string.digits |
| |
| |
| class NFSNException(ProviderError): |
| def __init__(self, value, http_code, code, driver=None): |
| self.code = code |
| super().__init__(value, http_code, driver) |
| |
| |
| class NFSNResponse(JsonResponse): |
| def parse_error(self): |
| if self.status == httplib.UNAUTHORIZED: |
| raise InvalidCredsError("Invalid provider credentials") |
| |
| body = self.parse_body() |
| |
| if isinstance(body, basestring): |
| return body + " (HTTP Code: %d)" % self.status |
| |
| error = body.get("error", None) |
| debug = body.get("debug", None) |
| # If we only have one of "error" or "debug", use the one that we have. |
| # If we have both, use both, with a space character in between them. |
| value = "No message specified" |
| if error is not None: |
| value = error |
| if debug is not None: |
| value = debug |
| if error is not None and value is not None: |
| value = error + " " + value |
| value = value + " (HTTP Code: %d)" % self.status |
| |
| return value |
| |
| |
| class NFSNConnection(ConnectionUserAndKey): |
| host = "api.nearlyfreespeech.net" |
| responseCls = NFSNResponse |
| allow_insecure = False |
| |
| def _header(self, action, data): |
| """Build the contents of the X-NFSN-Authentication HTTP header. See |
| https://members.nearlyfreespeech.net/wiki/API/Introduction for |
| more explanation.""" |
| login = self.user_id |
| timestamp = self._timestamp() |
| salt = self._salt() |
| api_key = self.key |
| data = urlencode(data) |
| data_hash = hashlib.sha1(data.encode("utf-8")).hexdigest() |
| |
| string = ";".join((login, timestamp, salt, api_key, action, data_hash)) |
| string_hash = hashlib.sha1(string.encode("utf-8")).hexdigest() |
| |
| return ";".join((login, timestamp, salt, string_hash)) |
| |
| def request(self, action, params=None, data="", headers=None, method="GET"): |
| """Add the X-NFSN-Authentication header to an HTTP request.""" |
| if not headers: |
| headers = {} |
| if not params: |
| params = {} |
| header = self._header(action, data) |
| |
| headers["X-NFSN-Authentication"] = header |
| if method == "POST": |
| headers["Content-Type"] = "application/x-www-form-urlencoded" |
| |
| return ConnectionUserAndKey.request(self, action, params, data, headers, method) |
| |
| def encode_data(self, data): |
| """NFSN expects the body to be regular key-value pairs that are not |
| JSON-encoded.""" |
| if data: |
| data = urlencode(data) |
| return data |
| |
| def _salt(self): |
| """Return a 16-character alphanumeric string.""" |
| r = random.SystemRandom() |
| return "".join(r.choice(SALT_CHARACTERS) for _ in range(16)) |
| |
| def _timestamp(self): |
| """Return the current number of seconds since the Unix epoch, |
| as a string.""" |
| return str(int(time.time())) |