def rt_request(self, request): """Handle a Django-RT internal API request.""" # Check client IP is allowed if not settings.DEBUG and settings.RT_COURIER_IPS: if request.META['REMOTE_ADDR'] not in settings.RT_COURIER_IPS: return HttpResponseForbidden() # Deserialize ResourceRequest from body and verify signature #! TODO since the request is still considered unauthorized at this point, add exception handling here for bad data: #! * utf-8 errors #! * json errors #! * data errors #! * bad signature body = request.body.decode('utf-8') res_req = ResourceRequest.from_json(body) res_req.path = self.rt_get_path(request) res_req.verify_signature() # Open Redis connection redis_conn = redis.StrictRedis( host=settings.RT_REDIS_HOST, port=settings.RT_REDIS_PORT, db=settings.RT_REDIS_DB, password=settings.RT_REDIS_PASSWORD ) # Get subscription status sub_key = get_subscription_key(res_req.sub_id) sub_status = redis_conn.get(sub_key).decode('utf-8') if not sub_status: return HttpResponseBadRequest('Invalid subscription ID') if res_req.action == 'subscribe': # Handle subscription request # Check subscription has 'requested' status if sub_status != 'requested': return HttpResponseBadRequest('Invalid subscription ID') # Check subscription is allowed if self.rt_get_permission('subscribe', request) is not True: return HttpResponseForbidden() # Set subscription status to 'granted' result = redis_conn.set(sub_key, 'granted') assert result # Return Resource object res = self.rt_get_resource(request) return JsonResponse(res.serialize(), content_type=Resource.CONTENT_TYPE+'; charset=utf-8' ) else: # Shouldn't ever land here assert False
def cleanup_request(self, sub_id, redis_conn, redis_subscription): logger.debug('Connection closed; cleaning up') # Close existing Redis connection if redis_conn: redis_conn.close() # Ensure subscription key is removed if sub_id: # Open new connection (this is necessary as asyncio_redis won't allow the DEL command to run after a subscription) redis_conn = yield from asyncio_redis.Connection.create( host=settings.RT_REDIS_HOST, port=settings.RT_REDIS_PORT, db=settings.RT_REDIS_DB, password=settings.RT_REDIS_PASSWORD) # Remove key res = yield from redis_conn.delete([get_subscription_key(sub_id)]) if res: logger.debug('Removed subscription %s' % (sub_id, )) # Close connection redis_conn.close()
def cleanup_request(self, sub_id, redis_conn, redis_subscription): logger.debug('Connection closed; cleaning up') # Close existing Redis connection if redis_conn: redis_conn.close() # Ensure subscription key is removed if sub_id: # Open new connection (this is necessary as asyncio_redis won't allow the DEL command to run after a subscription) redis_conn = yield from asyncio_redis.Connection.create( host=settings.RT_REDIS_HOST, port=settings.RT_REDIS_PORT, db=settings.RT_REDIS_DB, password=settings.RT_REDIS_PASSWORD ) # Remove key res = yield from redis_conn.delete([get_subscription_key(sub_id)]) if res: logger.debug('Removed subscription %s' % (sub_id,)) # Close connection redis_conn.close()
def handle_sse(self, path, suffix, env, start_response): res_path = path # Append slash to resource path if URL ends with slash if suffix.endswith('/'): res_path += '/' req_hdrs = self.get_headers(env) # Check route is a Django-RT resource try: logger.debug('Verifying %s is an RT resource' % (res_path, )) verify_resource_view(res_path) except NotAnRtResourceError: logger.debug('Not an RT resource; aborting') start_response(self.full_status(406), []) return [b''] except ResourceError as e: logger.debug('Caught ResourceError; aborting') start_response(self.full_status(e.status), []) return [b''] # Connect to Redis server redis_conn = redis.StrictRedis(host=settings.RT_REDIS_HOST, port=settings.RT_REDIS_PORT, db=settings.RT_REDIS_DB, password=settings.RT_REDIS_PASSWORD) # Create subscription while True: sub_id = generate_subscription_id() result = redis_conn.setnx(get_subscription_key(sub_id), 'requested') if result: break logger.debug('Created subscription ID: %s' % (sub_id, )) # Request resource from Django API try: logger.debug('Requesting subscription for %s' % (res_path, )) res = self.request_resource(path, sub_id, req_hdrs) except NotAnRtResourceError: logger.debug( "Subscription denied: not an rt resource. This shouldn't happen..." ) start_response(self.full_status(406), []) return [b''] except ResourceError as e: logger.debug('Subscription denied: HTTP error %d' % (e.status, )) start_response(self.full_status(e.status), []) return [b''] # Check subscription status and change to 'subscribed' sub_key = get_subscription_key(sub_id) sub_status = redis_conn.get(sub_key).decode('utf-8') assert sub_status == 'granted' logger.debug('Subscription granted') if redis_conn.set(sub_key, 'subscribed'): logger.debug('Subscription %s status changed to "subscribed"' % (sub_id, )) # Delete subscription key (not currently used for anything else) redis_conn.delete(sub_key) # Subscribe to Redis channel pubsub = redis_conn.pubsub() pubsub.subscribe(get_full_channel_name(res.channel)) # Prepare response hdrs = {'Content-Type': 'text/event-stream'} cors_hdrs = get_cors_headers(req_hdrs.get('ORIGIN', None)) hdrs.update(cors_hdrs) start_response('200 OK', [hdr for hdr in hdrs.items()]) # Loop first_event = True while True: # Wait for event on channel msg = pubsub.get_message( timeout=settings.RT_SSE_HEARTBEAT if settings. RT_SSE_HEARTBEAT else (10 * 60.0)) if msg: if msg['type'] == 'message': # Deserialize ResourceEvent event_json = msg['data'].decode('utf-8') event = ResourceEvent.from_json(event_json) # Create SSE event sse_evt = SseEvent.from_resource_event(event) # Send 'retry' field on first SSE event delivered if first_event: sse_evt.retry = settings.RT_SSE_RETRY first_event = False # Send SSE event to client yield sse_evt.as_utf8() else: # Timeout, send SSE heartbeat if necessary if settings.RT_SSE_HEARTBEAT: yield SseHeartbeat().as_utf8()
def handle_sse(self, path, suffix, env, start_response): res_path = path # Append slash to resource path if URL ends with slash if suffix.endswith("/"): res_path += "/" req_hdrs = self.get_headers(env) # Check route is a Django-RT resource try: logger.debug("Verifying %s is an RT resource" % (res_path,)) verify_resource_view(res_path) except NotAnRtResourceError: logger.debug("Not an RT resource; aborting") start_response(self.full_status(406), []) return [b""] except ResourceError as e: logger.debug("Caught ResourceError; aborting") start_response(self.full_status(e.status), []) return [b""] # Connect to Redis server redis_conn = redis.StrictRedis( host=settings.RT_REDIS_HOST, port=settings.RT_REDIS_PORT, db=settings.RT_REDIS_DB, password=settings.RT_REDIS_PASSWORD, ) # Create subscription while True: sub_id = generate_subscription_id() result = redis_conn.setnx(get_subscription_key(sub_id), "requested") if result: break logger.debug("Created subscription ID: %s" % (sub_id,)) # Request resource from Django API try: logger.debug("Requesting subscription for %s" % (res_path,)) res = self.request_resource(path, sub_id, req_hdrs) except NotAnRtResourceError: logger.debug("Subscription denied: not an rt resource. This shouldn't happen...") start_response(self.full_status(406), []) return [b""] except ResourceError as e: logger.debug("Subscription denied: HTTP error %d" % (e.status,)) start_response(self.full_status(e.status), []) return [b""] # Check subscription status and change to 'subscribed' sub_key = get_subscription_key(sub_id) sub_status = redis_conn.get(sub_key).decode("utf-8") assert sub_status == "granted" logger.debug("Subscription granted") if redis_conn.set(sub_key, "subscribed"): logger.debug('Subscription %s status changed to "subscribed"' % (sub_id,)) # Delete subscription key (not currently used for anything else) redis_conn.delete(sub_key) # Subscribe to Redis channel pubsub = redis_conn.pubsub() pubsub.subscribe(get_full_channel_name(res.channel)) # Prepare response hdrs = {"Content-Type": "text/event-stream"} cors_hdrs = get_cors_headers(req_hdrs.get("ORIGIN", None)) hdrs.update(cors_hdrs) start_response("200 OK", [hdr for hdr in hdrs.items()]) # Loop first_event = True while True: # Wait for event on channel msg = pubsub.get_message(timeout=settings.RT_SSE_HEARTBEAT if settings.RT_SSE_HEARTBEAT else (10 * 60.0)) if msg: if msg["type"] == "message": # Deserialize ResourceEvent event_json = msg["data"].decode("utf-8") event = ResourceEvent.from_json(event_json) # Create SSE event sse_evt = SseEvent.from_resource_event(event) # Send 'retry' field on first SSE event delivered if first_event: sse_evt.retry = settings.RT_SSE_RETRY first_event = False # Send SSE event to client yield sse_evt.as_utf8() else: # Timeout, send SSE heartbeat if necessary if settings.RT_SSE_HEARTBEAT: yield SseHeartbeat().as_utf8()
def handle_sse(self, request): res_path = request.match_info.get('resource') res_path = '/' + res_path # Append slash to resource path if URL ends with slash suffix = request.match_info.get('suffix') if suffix.endswith('/'): res_path += '/' redis_conn = None redis_subscription = None sub_id = None try: # Check route is a Django-RT resource try: logger.debug('Verifying %s is an RT resource' % (res_path,)) verify_resource_view(res_path) except NotAnRtResourceError: logger.debug('Not an RT resource; aborting') return web.Response(status=406) except ResourceError as e: logger.debug('Caught ResourceError; aborting') return web.Response(status=e.status) # Connect to Redis server redis_conn = yield from asyncio_redis.Connection.create( host=settings.RT_REDIS_HOST, port=settings.RT_REDIS_PORT, db=settings.RT_REDIS_DB, password=settings.RT_REDIS_PASSWORD ) # Create subscription while True: sub_id = generate_subscription_id() result = yield from redis_conn.setnx(get_subscription_key(sub_id), 'requested') if result: break logger.debug('Created subscription ID: %s' % (sub_id,)) # Request resource from Django API try: logger.debug('Requesting subscription for %s' % (res_path,)) res = yield from self.request_resource(res_path, request, sub_id) except NotAnRtResourceError: logger.debug("Subscription denied: not an rt resource. This shouldn't happen...") return web.Response(status=406) except ResourceError as e: logger.debug('Subscription denied: HTTP error %d' % (e.status,)) return web.Response(status=e.status) # Check subscription status and change to 'subscribed' sub_key = get_subscription_key(sub_id) sub_status = yield from redis_conn.get(sub_key) assert sub_status == 'granted' logger.debug('Subscription granted') result = yield from redis_conn.set(sub_key, 'subscribed') if result: logger.debug('Subscription %s status changed to "subscribed"' % (sub_id,)) # Delete subscription key (not currently used for anything else) yield from redis_conn.delete([sub_key]) # Subscribe to Redis channel chan = get_full_channel_name(res.channel) logger.debug('Subscribing to Redis channel %s' % (chan,)) redis_subscription = yield from redis_conn.start_subscribe() yield from redis_subscription.subscribe([ chan ]) # Prepare response response = web.StreamResponse() response.content_type = 'text/event-stream' cors_hdrs = get_cors_headers(request.headers.get('Origin', None)) response.headers.update(cors_hdrs) yield from response.prepare(request) # Loop first_event = True while True: # Wait for event on channel try: reply = yield from asyncio.wait_for( redis_subscription.next_published(), settings.RT_SSE_HEARTBEAT ) except asyncio.TimeoutError: # Timeout, send SSE heartbeat response.write(SseHeartbeat().as_utf8()) yield from response.drain() else: # Deserialize ResourceEvent event = ResourceEvent.from_json(reply.value) # Create SSE event sse_evt = SseEvent.from_resource_event(event) # Send 'retry' field on first SSE event delivered if first_event: sse_evt.retry = settings.RT_SSE_RETRY first_event = False # Send SSE event to client response.write(sse_evt.as_utf8()) yield from response.drain() yield from response.write_eof() return response finally: # Cleanup asyncio.async(self.cleanup_request(sub_id, redis_conn, redis_subscription))
def handle_sse(self, request): res_path = request.match_info.get('resource') res_path = '/' + res_path # Append slash to resource path if URL ends with slash suffix = request.match_info.get('suffix') if suffix.endswith('/'): res_path += '/' redis_conn = None redis_subscription = None sub_id = None try: # Check route is a Django-RT resource try: logger.debug('Verifying %s is an RT resource' % (res_path, )) verify_resource_view(res_path) except NotAnRtResourceError: logger.debug('Not an RT resource; aborting') return web.Response(status=406) except ResourceError as e: logger.debug('Caught ResourceError; aborting') return web.Response(status=e.status) # Connect to Redis server redis_conn = yield from asyncio_redis.Connection.create( host=settings.RT_REDIS_HOST, port=settings.RT_REDIS_PORT, db=settings.RT_REDIS_DB, password=settings.RT_REDIS_PASSWORD) # Create subscription while True: sub_id = generate_subscription_id() result = yield from redis_conn.setnx( get_subscription_key(sub_id), 'requested') if result: break logger.debug('Created subscription ID: %s' % (sub_id, )) # Request resource from Django API try: logger.debug('Requesting subscription for %s' % (res_path, )) res = yield from self.request_resource(res_path, request, sub_id) except NotAnRtResourceError: logger.debug( "Subscription denied: not an rt resource. This shouldn't happen..." ) return web.Response(status=406) except ResourceError as e: logger.debug('Subscription denied: HTTP error %d' % (e.status, )) return web.Response(status=e.status) # Check subscription status and change to 'subscribed' sub_key = get_subscription_key(sub_id) sub_status = yield from redis_conn.get(sub_key) assert sub_status == 'granted' logger.debug('Subscription granted') result = yield from redis_conn.set(sub_key, 'subscribed') if result: logger.debug('Subscription %s status changed to "subscribed"' % (sub_id, )) # Delete subscription key (not currently used for anything else) yield from redis_conn.delete([sub_key]) # Subscribe to Redis channel chan = get_full_channel_name(res.channel) logger.debug('Subscribing to Redis channel %s' % (chan, )) redis_subscription = yield from redis_conn.start_subscribe() yield from redis_subscription.subscribe([chan]) # Prepare response response = web.StreamResponse() response.content_type = 'text/event-stream' cors_hdrs = get_cors_headers(request.headers.get('Origin', None)) response.headers.update(cors_hdrs) yield from response.prepare(request) # Loop first_event = True while True: # Wait for event on channel try: reply = yield from asyncio.wait_for( redis_subscription.next_published(), settings.RT_SSE_HEARTBEAT) except asyncio.TimeoutError: # Timeout, send SSE heartbeat response.write(SseHeartbeat().as_utf8()) yield from response.drain() else: # Deserialize ResourceEvent event = ResourceEvent.from_json(reply.value) # Create SSE event sse_evt = SseEvent.from_resource_event(event) # Send 'retry' field on first SSE event delivered if first_event: sse_evt.retry = settings.RT_SSE_RETRY first_event = False # Send SSE event to client response.write(sse_evt.as_utf8()) yield from response.drain() yield from response.write_eof() return response finally: # Cleanup asyncio. async (self.cleanup_request(sub_id, redis_conn, redis_subscription))