def sso_validate(payload, signature, secret): """ payload: provided by Discourse HTTP call to your SSO endpoint as sso GET param signature: provided by Discourse HTTP call to your SSO endpoint as sig GET param secret: the secret key you entered into Discourse sso secret return value: The nonce used by discourse to validate the redirect URL """ if None in [payload, signature]: raise DiscourseError('No SSO payload or signature.') if not secret: raise DiscourseError('Invalid secret..') payload = unquote(payload) if not payload: raise DiscourseError('Invalid payload..') decoded = base64.decodestring(payload) if 'nonce' not in decoded: raise DiscourseError('Invalid payload..') h = hmac.new(secret, payload, digestmod=hashlib.sha256) this_signature = h.hexdigest() if this_signature != signature: raise DiscourseError('Payload does not match signature.') nonce = decoded.split('=')[1] return nonce
def sso_validate(payload, signature, secret): """ payload: provided by Discourse HTTP call to your SSO endpoint as sso GET param signature: provided by Discourse HTTP call to your SSO endpoint as sig GET param secret: the secret key you entered into Discourse sso secret return value: The nonce used by discourse to validate the redirect URL """ if None in [payload, signature]: raise DiscourseError('No SSO payload or signature.') if not secret: raise DiscourseError('Invalid secret..') payload = unquote(payload) if not payload: raise DiscourseError('Invalid payload..') decoded = b64decode(payload.encode('utf-8')).decode('utf-8') if 'nonce' not in decoded: raise DiscourseError('Invalid payload..') h = hmac.new(secret.encode('utf-8'), payload.encode('utf-8'), digestmod=hashlib.sha256) this_signature = h.hexdigest() if this_signature != signature: raise DiscourseError('Payload does not match signature.') # Discourse returns querystring encoded value. We only need `nonce` qs = parse_qs(decoded) return qs['nonce'][0]
def _request(self, verb, path, params): params['api_key'] = self.api_key if 'api_username' not in params: params['api_username'] = self.api_username url = self.host + path response = requests.request(verb, url, allow_redirects=False, params=params, timeout=self.timeout) log.debug('response %s: %s', response.status_code, repr(response.text)) if not response.ok: try: msg = u','.join(response.json()['errors']) except (ValueError, TypeError, KeyError): if response.reason: msg = response.reason else: msg = u'{0}: {1}'.format(response.status_code, response.text) if 400 <= response.status_code < 500: raise DiscourseClientError(msg, response=response) raise DiscourseServerError(msg, response=response) if response.status_code == 302: raise DiscourseError( 'Unexpected Redirect, invalid api key or host?', response=response) json_content = 'application/json; charset=utf-8' content_type = response.headers['content-type'] if content_type != json_content: # some calls return empty html documents if response.content == ' ': return None raise DiscourseError( 'Invalid Response, expecting "{0}" got "{1}"'.format( json_content, content_type), response=response) try: decoded = response.json() except ValueError: raise DiscourseError('failed to decode response', response=response) if 'errors' in decoded: message = decoded.get('message') if not message: message = u','.join(decoded['errors']) raise DiscourseError(message, response=response) return decoded
def _request( self, verb, path, params=None, files=None, data=None, json=None, override_request_kwargs=None ): """ Executes HTTP request to API and handles response Args: verb: HTTP verb as string: GET, DELETE, PUT, POST path: the path on the Discourse API params: dictionary of parameters to include to the API override_request_kwargs: dictionary of requests.request keyword arguments to override defaults Returns: dictionary of response body data or None """ override_request_kwargs = override_request_kwargs or {} url = self.host + path headers = { "Accept": "application/json; charset=utf-8", "Api-Key": self.api_key, "Api-Username": self.api_username, } # How many times should we retry if rate limited retry_count = 4 # Extra time (on top of that required by API) to wait on a retry. retry_backoff = 1 while retry_count > 0: request_kwargs = dict( allow_redirects=False, params=params, files=files, data=data, json=json, headers=headers, timeout=self.timeout, ) request_kwargs.update(override_request_kwargs) response = requests.request(verb, url, **request_kwargs) log.debug("response %s: %s", response.status_code, repr(response.text)) if response.ok: break if not response.ok: try: msg = u",".join(response.json()["errors"]) except (ValueError, TypeError, KeyError): if response.reason: msg = response.reason else: msg = u"{0}: {1}".format(response.status_code, response.text) if 400 <= response.status_code < 500: if 429 == response.status_code: # This codepath relies on wait_seconds from Discourse v2.0.0.beta3 / v1.9.3 or higher. rj = response.json() wait_delay = ( retry_backoff + rj["extras"]["wait_seconds"] ) # how long to back off for. if retry_count > 1: time.sleep(wait_delay) retry_count -= 1 log.info( "We have been rate limited and waited {0} seconds ({1} retries left)".format( wait_delay, retry_count ) ) log.debug("API returned {0}".format(rj)) continue else: raise DiscourseClientError(msg, response=response) # Any other response.ok resulting in False raise DiscourseServerError(msg, response=response) if retry_count == 0: raise DiscourseRateLimitedError( "Number of rate limit retries exceeded. Increase retry_backoff or retry_count", response=response, ) if response.status_code == 302: raise DiscourseError( "Unexpected Redirect, invalid api key or host?", response=response ) json_content = "application/json; charset=utf-8" content_type = response.headers["content-type"] if content_type != json_content: # some calls return empty html documents if not response.content.strip(): return None raise DiscourseError( 'Invalid Response, expecting "{0}" got "{1}"'.format( json_content, content_type ), response=response, ) try: decoded = response.json() except ValueError: raise DiscourseError("failed to decode response", response=response) # Checking "errors" length because # data-explorer (e.g. POST /admin/plugins/explorer/queries/{}/run) # sends an empty errors array if "errors" in decoded and len(decoded["errors"]) > 0: message = decoded.get("message") if not message: message = u",".join(decoded["errors"]) raise DiscourseError(message, response=response) return decoded
def _request(self, verb, path, params={}, data={}): """ Executes HTTP request to API and handles response Args: verb: HTTP verb as string: GET, DELETE, PUT, POST path: the path on the Discourse API params: dictionary of parameters to include to the API Returns: """ params['api_key'] = self.api_key if 'api_username' not in params: params['api_username'] = self.api_username url = self.host + path headers = {'Accept': 'application/json; charset=utf-8'} response = requests.request(verb, url, allow_redirects=False, params=params, data=data, headers=headers, timeout=self.timeout) log.debug('response %s: %s', response.status_code, repr(response.text)) if not response.ok: try: msg = u','.join(response.json()['errors']) except (ValueError, TypeError, KeyError): if response.reason: msg = response.reason else: msg = u'{0}: {1}'.format(response.status_code, response.text) if 400 <= response.status_code < 500: raise DiscourseClientError(msg, response=response) raise DiscourseServerError(msg, response=response) if response.status_code == 302: raise DiscourseError( 'Unexpected Redirect, invalid api key or host?', response=response) json_content = 'application/json; charset=utf-8' content_type = response.headers['content-type'] if content_type != json_content: # some calls return empty html documents if not response.content.strip(): return None raise DiscourseError( 'Invalid Response, expecting "{0}" got "{1}"'.format( json_content, content_type), response=response) try: decoded = response.json() except ValueError: raise DiscourseError('failed to decode response', response=response) if 'errors' in decoded: message = decoded.get('message') if not message: message = u','.join(decoded['errors']) raise DiscourseError(message, response=response) return decoded