Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C9 #200

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open

C9 #200

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README-c9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## Modifictions for cloud9

Cloud9 is web IDE system that can proxy web & websockets connections
to applications. Because of the proxying, the base REMI system does
not work -- for example, the original REMI code uses the IP address of
the server to configure the JavaScript code to specify the host that
the WS should connect to. In C9, this is the internal (non-routed) IP
address. Thus, a number of small changes are made to make the code work
better in C9.

Another change has to do with the key exchange in WS. For reasons not
completely clear to me, the C9 proxy appears to lower-case the HTTP
response received by the server. This causes the base REMI code to
fail, so I added a specific for extracting the WS key when the lower
can version on C9.

To install on C9, use `sudo pip install git+https://github.com/dirkcgrunwald/remi.git@c9`

You should then be able to run any of the examples.
35 changes: 35 additions & 0 deletions remi/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -2975,6 +2975,41 @@ def set_position(self, x, y):
self.attributes['cx'] = str(x)
self.attributes['cy'] = str(y)

class SvgEllipse(SvgShape):
"""svg ellipse - an ellipse represented filled and with a stroke."""

@decorate_constructor_parameter_types([int, int, int])
def __init__(self, x, y, rx, ry, **kwargs):
"""
Args:
x (int): the x center point of the circle
y (int): the y center point of the circle
rx (int): radius in x
ry (int): radius in y
kwargs: See Widget.__init__()
"""
super(SvgEllipse, self).__init__(x, y, **kwargs)
self.set_radius(rx, ry)
self.type = 'ellipse'

def set_radius(self, rx, ry):
"""Sets the circle radius.

Args:
radius (int): the circle radius
"""
self.attributes['rx'] = str(rx)
self.attributes['ry'] = str(ry)

def set_position(self, x, y):
"""Sets the circle position.

Args:
x (int): the x coordinate
y (int): the y coordinate
"""
self.attributes['cx'] = str(x)
self.attributes['cy'] = str(y)

class SvgLine(Widget):

Expand Down
65 changes: 49 additions & 16 deletions remi/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,11 @@ def __init__(self, *args, **kwargs):
def setup(self):
global clients
socketserver.StreamRequestHandler.setup(self)
self._log.info('connection established: %r' % (self.client_address,))
self._log.info('WebSocket connection established: %r' % (self.client_address,))
self.handshake_done = False

def handle(self):
self._log.debug('handle')
self._log.debug('WebSocket handle')
# on some systems like ROS, the default socket timeout
# is less than expected, we force it to infinite (None) as default socket value
self.request.settimeout(None)
Expand All @@ -147,10 +147,15 @@ def handle(self):
else:
if not self.read_next_message():
k = get_instance_key(self)
clients[k].websockets.remove(self)
self.handshake_done = False
self._log.debug('ws ending websocket service')
break
try:
clients[k].websockets.remove(self)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @dirkcgrunwald !
Looking at changes you made, I suppose that beside others, these 8 grouped lines (from 150 to 158) are the necessary part to be C9 compatible. Others are refinements. Is this correct?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 2-3 others parts that are needed.

In if 'C9_HOSTNAME' in os.environ:

  •        key = data.decode().split('sec-websocket-key: ')[1].split('\r\n')[0]
    
  •    else:
    
  •        key = data.decode().split('Sec-WebSocket-Key: ')[1].split('\r\n')[0]
    

it appears that the C9 proxy makes the response headers in lower case. It's also possible to use a try...except to try the upper case, fail and then revert to lower case.

In addition, in the default setup, if you use '0.0.0.0' as the host IP, remi changes this to 127.0.0.1, which isn't proxied. The user doesn't know the IP address of their server because of the proxy, so they need to leave '0.0.0.0' as an option. I had guarded those changes with checks for the C9 environment.

This may all be moot since C9 is being changed to AWS Cloud and I think those don't use the proxy.

self.handshake_done = False
self._log.debug('ws ending websocket service')
break
except:
self.handshake_done = True
self._log.debug('Tried to remove self when not in client...')
return

@staticmethod
def bytetonum(b):
Expand All @@ -165,6 +170,10 @@ def read_next_message(self):
length = self.rfile.read(2)
except ValueError:
# socket was closed, just return without errors
self._log.debug('Socket was closed, returning without errors')
return False
self._log.debug('WS response, length is %r' % length)
if len(length) < 1:
return False
length = self.bytetonum(length[1]) & 127
if length == 126:
Expand All @@ -175,10 +184,13 @@ def read_next_message(self):
decoded = ''
for char in self.rfile.read(length):
decoded += chr(self.bytetonum(char) ^ masks[len(decoded) % 4])
self._log.debug('Decoded WebSocket message %r' % decoded)
self.on_message(from_websocket(decoded))
except socket.timeout:
self._log.debug('WebSocket timeout')
return False
except Exception:
except Exception as ex:
self._log.debug('Other exception %r' % ex )
return False
return True

Expand Down Expand Up @@ -211,17 +223,31 @@ def send_message(self, message):
self.request.send(out)

def handshake(self):
self._log.debug('handshake')
self._log.debug('WebSocket handshake')
data = self.request.recv(1024).strip()
key = data.decode().split('Sec-WebSocket-Key: ')[1].split('\r\n')[0]
#self._log.debug('WebSocket handshake data is %s' % data.decode())
#
# Code used to look for Sec-WebSocket-Key, some clients respond in lower case
#
try:
key = data.decode().split('Sec-WebSocket-Key: ')[1].split('\r\n')[0]
except IndexError:
try:
#
# Try Cloud9 method..
#
key = data.decode().split('sec-websocket-key: ')[1].split('\r\n')[0]
except:
return
digest = hashlib.sha1((key.encode("utf-8")+self.magic))
self._log.debug('key is ' + key)
digest = digest.digest()
digest = base64.b64encode(digest)
response = 'HTTP/1.1 101 Switching Protocols\r\n'
response += 'Upgrade: websocket\r\n'
response += 'Connection: Upgrade\r\n'
response += 'Sec-WebSocket-Accept: %s\r\n\r\n' % digest.decode("utf-8")
self._log.info('handshake complete')
self._log.info('WebSocket handshake complete')
self.request.sendall(response.encode("utf-8"))
self.handshake_done = True

Expand Down Expand Up @@ -345,7 +371,6 @@ def run(self):

# noinspection PyPep8Naming
class App(BaseHTTPRequestHandler, object):

"""
This class will handles any incoming request from the browser
The main application class can subclass this
Expand Down Expand Up @@ -382,6 +407,7 @@ def log_message(self, format_string, *args):
def log_error(self, format_string, *args):
msg = format_string % args
self._log.error("%s %s" % (self.address_string(), msg))
raise('Exception')

def _instance(self):
global clients
Expand Down Expand Up @@ -636,6 +662,8 @@ def _instance(self):
};
</script>""" % (net_interface_ip, wsport, pending_messages_queue_length, websocket_timeout_timer_ms)

self._log.debug('Prpeare javascript with interface %s and port %s' % (net_interface_ip, wsport))

# add built in js, extend with user js
clients[k].js_body_end += ('\n' + '\n'.join(self._get_list_from_app_args('js_body_end')))
# use the default css, but append a version based on its hash, to stop browser caching
Expand Down Expand Up @@ -932,9 +960,14 @@ def __init__(self, server_address, RequestHandlerClass, websocket_address,

class Server(object):
# noinspection PyShadowingNames
def __init__(self, gui_class, title='', start=True, address='127.0.0.1', port=8081, username=None, password=None,
def __init__(self, gui_class, title='', start=True,
address=os.getenv('IP','0.0.0.0'),
port=int(os.getenv('PORT',8080)),
username=None, password=None,
multiple_instance=False, enable_file_cache=True, update_interval=0.1, start_browser=True,
websocket_timeout_timer_ms=1000, websocket_port=0, host_name=None,
websocket_timeout_timer_ms=1000,
websocket_port=int(os.getenv('PORT',8080))+1,
host_name=os.getenv('C9_HOSTNAME', None),
pending_messages_queue_length=1000, userdata=()):
global http_server_instance
http_server_instance = self
Expand Down Expand Up @@ -998,7 +1031,7 @@ def start(self):
self._title, *self._userdata)
shost, sport = self._sserver.socket.getsockname()[:2]
# when listening on multiple net interfaces the browsers connects to localhost
if shost == '0.0.0.0':
if shost == '0.0.0.0' and 'C9_IP' not in os.environ:
shost = '127.0.0.1'
self._base_address = 'http://%s:%s/' % (shost,sport)
self._log.info('Started httpserver %s' % self._base_address)
Expand All @@ -1010,7 +1043,7 @@ def start(self):
# use default browser instead of always forcing IE on Windows
if os.name == 'nt':
webbrowser.get('windows-default').open(self._base_address)
else:
elif 'C9_IP' not in os.environ:
webbrowser.open(self._base_address)
self._sth = threading.Thread(target=self._sserver.serve_forever)
self._sth.daemon = False
Expand Down Expand Up @@ -1051,7 +1084,7 @@ def stop(self):
class StandaloneServer(Server):
def __init__(self, gui_class, title='', width=800, height=600, resizable=True, fullscreen=False, start=True,
userdata=()):
Server.__init__(self, gui_class, title=title, start=False, address='127.0.0.1', port=0, username=None,
Server.__init__(self, gui_class, title=title, start=False, address='0.0.0.0', port=0, username=None,
password=None,
multiple_instance=False, enable_file_cache=True, update_interval=0.1, start_browser=False,
websocket_timeout_timer_ms=1000, websocket_port=0, host_name=None,
Expand Down