This commit is contained in:
Mai Gillmann
2019-12-17 14:09:10 +01:00
parent 66e908fc8a
commit 4791d00a43
2122 changed files with 423791 additions and 0 deletions
@@ -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
@@ -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)
@@ -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