17.12
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
class Event(object):
|
||||
def __init__(self, channel, type, data, id=None):
|
||||
self.channel = channel
|
||||
self.type = type
|
||||
self.data = data
|
||||
self.id = id
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import os
|
||||
from six.moves.urllib_parse import urlparse
|
||||
import requests
|
||||
from django_grip import get_pubcontrol
|
||||
from django.apps import apps
|
||||
if apps.is_installed('django.contrib.staticfiles'):
|
||||
from django.contrib.staticfiles.management.commands import runserver
|
||||
else:
|
||||
from django.core.management.commands import runserver
|
||||
|
||||
|
||||
class Command(runserver.Command):
|
||||
help = 'Set ngrok tunnel as origin for GRIP service, then invoke runserver'
|
||||
|
||||
def setup(self):
|
||||
host = None
|
||||
port = None
|
||||
ssl_host = None
|
||||
ssl_port = None
|
||||
|
||||
resp = requests.get('http://localhost:4040/api/tunnels')
|
||||
tunnels = resp.json()['tunnels']
|
||||
for tunnel in tunnels:
|
||||
if tunnel['proto'] in ('http', 'https'):
|
||||
parsed = urlparse(tunnel['public_url'])
|
||||
if tunnel['proto'] == 'http':
|
||||
host = parsed.hostname
|
||||
port = parsed.port if parsed.port is not None else 80
|
||||
elif tunnel['proto'] == 'https':
|
||||
ssl_host = parsed.hostname
|
||||
ssl_port = parsed.port if parsed.port is not None else 443
|
||||
|
||||
if host is None and ssl_host is None:
|
||||
self.stderr.write('Error: no ngrok tunnels found')
|
||||
return
|
||||
|
||||
pub = get_pubcontrol()
|
||||
if len(pub.clients) == 0:
|
||||
self.stderr.write('Error: no GRIP proxy configured')
|
||||
return
|
||||
|
||||
pub.set_origin(
|
||||
host=host,
|
||||
port=port,
|
||||
ssl_host=ssl_host,
|
||||
ssl_port=ssl_port,
|
||||
rewrite_host=True)
|
||||
|
||||
self.stdout.write(
|
||||
'Setting ngrok tunnel %s as GRIP origin' % (host or ssl_host))
|
||||
|
||||
def run(self, **options):
|
||||
# be sure to execute setup() only once, even if autoreload is used
|
||||
use_reloader = options['use_reloader']
|
||||
if not use_reloader or os.environ.get('RUN_MAIN') != 'true':
|
||||
self.setup()
|
||||
|
||||
super(Command, self).run(**options)
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
// MIT License:
|
||||
//
|
||||
// Copyright (C) 2017 Fanout, Inc.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// 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.
|
||||
|
||||
var ReconnectingEventSource = function (url, configuration) {
|
||||
this._eventSource = null;
|
||||
this._lastEventId = null;
|
||||
this._timer = null;
|
||||
this._listeners = {};
|
||||
|
||||
this.url = url;
|
||||
this.readyState = 0;
|
||||
this.retry_time = 3000;
|
||||
|
||||
if (configuration != undefined && configuration.lastEventId) {
|
||||
this._lastEventId = configuration.lastEventId;
|
||||
delete configuration['lastEventId'];
|
||||
}
|
||||
|
||||
this._configuration = configuration;
|
||||
|
||||
var self = this;
|
||||
this._onevent_wrapped = function (event) { self._onevent(event); };
|
||||
|
||||
this._start();
|
||||
};
|
||||
|
||||
ReconnectingEventSource.prototype._start = function () {
|
||||
var url = this.url;
|
||||
|
||||
if (this._lastEventId) {
|
||||
if (url.indexOf('?') === -1) {
|
||||
url += '?';
|
||||
} else {
|
||||
url += '&';
|
||||
}
|
||||
url += 'lastEventId=' + encodeURIComponent(this._lastEventId);
|
||||
}
|
||||
|
||||
this._eventSource = new EventSource(url, this._configuration);
|
||||
|
||||
var self = this;
|
||||
|
||||
this._eventSource.onopen = function (event) { self._onopen(event); };
|
||||
this._eventSource.onerror = function (event) { self._onerror(event); };
|
||||
|
||||
// apply listen types
|
||||
for (var type in this._listeners) {
|
||||
this._eventSource.addEventListener(type, this._onevent_wrapped);
|
||||
}
|
||||
};
|
||||
|
||||
ReconnectingEventSource.prototype._onopen = function (event) {
|
||||
if (this.readyState == 0) {
|
||||
this.readyState = 1;
|
||||
this.onopen(event);
|
||||
}
|
||||
};
|
||||
|
||||
ReconnectingEventSource.prototype._onerror = function (event) {
|
||||
if (this.readyState == 1) {
|
||||
this.readyState = 0;
|
||||
this.onerror(event);
|
||||
}
|
||||
|
||||
if (this._eventSource) {
|
||||
if(this._eventSource.readyState == 2) {
|
||||
// reconnect with new object
|
||||
this._eventSource.close();
|
||||
this._eventSource = null;
|
||||
|
||||
var self = this;
|
||||
this._timer = setTimeout(function () {
|
||||
self._start();
|
||||
}, this.retry_time);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ReconnectingEventSource.prototype._onevent = function (event) {
|
||||
if (event.lastEventId) {
|
||||
this._lastEventId = event.lastEventId;
|
||||
}
|
||||
|
||||
var l = this._listeners[event.type];
|
||||
if (l != undefined) {
|
||||
// operate on a copy
|
||||
l = l.slice();
|
||||
for (var n = 0; n < l.length; n++) {
|
||||
l[n](event);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type == 'message') {
|
||||
this.onmessage(event);
|
||||
}
|
||||
};
|
||||
|
||||
ReconnectingEventSource.prototype.onopen = function (event) {
|
||||
// user may override
|
||||
};
|
||||
|
||||
ReconnectingEventSource.prototype.onerror = function (event) {
|
||||
// user may override
|
||||
};
|
||||
|
||||
ReconnectingEventSource.prototype.onmessage = function (event) {
|
||||
// user may override
|
||||
};
|
||||
|
||||
ReconnectingEventSource.prototype.close = function () {
|
||||
if (this._timer) {
|
||||
clearTimeout(this._timer);
|
||||
this._timer = null;
|
||||
}
|
||||
|
||||
if (this._eventSource) {
|
||||
this._eventSource.close();
|
||||
this._eventSource = null;
|
||||
}
|
||||
|
||||
this.readyState = 2;
|
||||
};
|
||||
|
||||
ReconnectingEventSource.prototype.addEventListener = function (type, callback) {
|
||||
var type = type.toString();
|
||||
var l = this._listeners[type];
|
||||
if (l == undefined) {
|
||||
l = [];
|
||||
this._listeners[type] = l;
|
||||
if (this._eventSource) {
|
||||
this._eventSource.addEventListener(type, this._onevent_wrapped);
|
||||
}
|
||||
}
|
||||
for (var n = 0; n < l.length; n++) {
|
||||
if (l[n] === callback) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
l.push(callback);
|
||||
};
|
||||
|
||||
ReconnectingEventSource.prototype.removeEventListener = function (type, callback) {
|
||||
var type = type.toString();
|
||||
var l = this._listeners[type];
|
||||
if (l == undefined) {
|
||||
return;
|
||||
}
|
||||
for (var n = 0; n < l.length; n++) {
|
||||
if (l[n] === callback) {
|
||||
l.splice(n, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (l.length == 0) {
|
||||
delete this._listeners[type];
|
||||
if (this._eventSource) {
|
||||
this._eventSource.removeEventListener(type, this._onevent_wrapped);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,165 @@
|
||||
import json
|
||||
import threading
|
||||
import importlib
|
||||
import six
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from gripcontrol import HttpStreamFormat
|
||||
|
||||
try:
|
||||
from urllib import quote
|
||||
except ImportError:
|
||||
from urllib.parse import quote
|
||||
|
||||
tlocal = threading.local()
|
||||
|
||||
def have_channels():
|
||||
try:
|
||||
from channels.generic.http import AsyncHttpConsumer
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
# return dict of (channel, last-id)
|
||||
def parse_last_event_id(s):
|
||||
out = {}
|
||||
parts = s.split(',')
|
||||
for part in parts:
|
||||
channel, last_id = part.split(':')
|
||||
out[channel] = last_id
|
||||
return out
|
||||
|
||||
def make_id(ids):
|
||||
id_parts = []
|
||||
for channel, id in six.iteritems(ids):
|
||||
enc_channel = quote(channel)
|
||||
id_parts.append('%s:%s' % (enc_channel, id))
|
||||
return ','.join(id_parts)
|
||||
|
||||
def build_id_escape(s):
|
||||
out = ''
|
||||
for c in s:
|
||||
if c == '%':
|
||||
out += '%%'
|
||||
else:
|
||||
out += c
|
||||
return out
|
||||
|
||||
def sse_encode_event(event_type, data, event_id=None, escape=False):
|
||||
data_str = json.dumps(data, cls=DjangoJSONEncoder)
|
||||
if escape:
|
||||
event_type = build_id_escape(event_type)
|
||||
data_str = build_id_escape(data_str)
|
||||
out = 'event: %s\n' % event_type
|
||||
if event_id:
|
||||
out += 'id: %s\n' % event_id
|
||||
out += 'data: %s\n\n' % data_str
|
||||
return out
|
||||
|
||||
def sse_error_response(condition, text, extra=None):
|
||||
if extra is None:
|
||||
extra = {}
|
||||
data = {'condition': condition, 'text': text}
|
||||
for k, v in six.iteritems(extra):
|
||||
data[k] = v
|
||||
body = sse_encode_event('stream-error', data, event_id='error')
|
||||
return HttpResponse(body, content_type='text/event-stream')
|
||||
|
||||
def publish_event(channel, event_type, data, pub_id, pub_prev_id,
|
||||
skip_user_ids=None):
|
||||
from django_grip import publish
|
||||
|
||||
if skip_user_ids is None:
|
||||
skip_user_ids = []
|
||||
|
||||
content_filters = []
|
||||
if pub_id:
|
||||
event_id = '%I'
|
||||
content_filters.append('build-id')
|
||||
else:
|
||||
event_id = None
|
||||
content = sse_encode_event(event_type, data, event_id=event_id, escape=bool(pub_id))
|
||||
meta = {}
|
||||
if skip_user_ids:
|
||||
meta['skip_users'] = ','.join(skip_user_ids)
|
||||
publish(
|
||||
'events-%s' % quote(channel),
|
||||
HttpStreamFormat(content, content_filters=content_filters),
|
||||
id=pub_id,
|
||||
prev_id=pub_prev_id,
|
||||
meta=meta)
|
||||
|
||||
def publish_kick(user_id, channel):
|
||||
from django_grip import publish
|
||||
|
||||
msg = 'Permission denied to channels: %s' % channel
|
||||
data = {'condition': 'forbidden', 'text': msg, 'channels': [channel]}
|
||||
content = sse_encode_event('stream-error', data, event_id='error')
|
||||
meta = {'require_sub': 'events-%s' % channel}
|
||||
publish(
|
||||
'user-%s' % user_id,
|
||||
HttpStreamFormat(content),
|
||||
id='kick-1',
|
||||
meta=meta)
|
||||
publish(
|
||||
'user-%s' % user_id,
|
||||
HttpStreamFormat(close=True),
|
||||
id='kick-2',
|
||||
prev_id='kick-1',
|
||||
meta=meta)
|
||||
|
||||
def load_class(name):
|
||||
at = name.rfind('.')
|
||||
if at == -1:
|
||||
raise ValueError('class name contains no \'.\'')
|
||||
module_name = name[0:at]
|
||||
class_name = name[at + 1:]
|
||||
return getattr(importlib.import_module(module_name), class_name)()
|
||||
|
||||
# load and keep in thread local storage
|
||||
def get_class(name):
|
||||
if not hasattr(tlocal, 'loaded'):
|
||||
tlocal.loaded = {}
|
||||
c = tlocal.loaded.get(name)
|
||||
if c is None:
|
||||
c = load_class(name)
|
||||
tlocal.loaded[name] = c
|
||||
return c
|
||||
|
||||
def get_class_from_setting(setting_name, default=None):
|
||||
if hasattr(settings, setting_name):
|
||||
return get_class(getattr(settings, setting_name))
|
||||
elif default:
|
||||
return get_class(default)
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_storage():
|
||||
return get_class_from_setting('EVENTSTREAM_STORAGE_CLASS')
|
||||
|
||||
def get_channelmanager():
|
||||
return get_class_from_setting(
|
||||
'EVENTSTREAM_CHANNELMANAGER_CLASS',
|
||||
'django_eventstream.channelmanager.DefaultChannelManager')
|
||||
|
||||
def add_default_headers(headers):
|
||||
headers['Cache-Control'] = 'no-cache'
|
||||
headers['X-Accel-Buffering'] = 'no'
|
||||
augment_cors_headers(headers)
|
||||
|
||||
def augment_cors_headers(headers):
|
||||
cors_origin = ''
|
||||
if hasattr(settings, 'EVENTSTREAM_ALLOW_ORIGIN'):
|
||||
cors_origin = settings.EVENTSTREAM_ALLOW_ORIGIN
|
||||
|
||||
if cors_origin:
|
||||
headers['Access-Control-Allow-Origin'] = cors_origin
|
||||
|
||||
allow_credentials = False
|
||||
if hasattr(settings, 'EVENTSTREAM_ALLOW_CREDENTIALS'):
|
||||
allow_credentials = settings.EVENTSTREAM_ALLOW_CREDENTIALS
|
||||
|
||||
if allow_credentials:
|
||||
headers['Access-Control-Allow-Credentials'] = 'true'
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.http import HttpResponseBadRequest
|
||||
from .utils import add_default_headers
|
||||
|
||||
def events(request, **kwargs):
|
||||
from .eventrequest import EventRequest
|
||||
from .eventstream import EventPermissionError, get_events
|
||||
from .utils import sse_error_response
|
||||
|
||||
try:
|
||||
event_request = EventRequest(request, view_kwargs=kwargs)
|
||||
event_response = get_events(event_request)
|
||||
response = event_response.to_http_response(request)
|
||||
except EventRequest.ResumeNotAllowedError as e:
|
||||
response = HttpResponseBadRequest(
|
||||
'Invalid request: %s.\n' % str(e))
|
||||
except EventRequest.GripError as e:
|
||||
if request.grip.proxied:
|
||||
response = sse_error_response(
|
||||
'internal-error',
|
||||
'Invalid internal request.')
|
||||
else:
|
||||
response = sse_error_response(
|
||||
'bad-request',
|
||||
'Invalid request: %s.' % str(e))
|
||||
except EventRequest.Error as e:
|
||||
response = sse_error_response(
|
||||
'bad-request',
|
||||
'Invalid request: %s.' % str(e))
|
||||
except EventPermissionError as e:
|
||||
response = sse_error_response(
|
||||
'forbidden',
|
||||
str(e),
|
||||
{'channels': e.channels})
|
||||
|
||||
add_default_headers(response)
|
||||
|
||||
return response
|
||||
Reference in New Issue
Block a user