mirror of
				https://github.com/retspen/webvirtcloud
				synced 2025-07-31 12:41:08 +00:00 
			
		
		
		
	replace tunnel.py with sshtunnel.py some small fixes
This commit is contained in:
		
							parent
							
								
									b6cb81c3bc
								
							
						
					
					
						commit
						0b86e34203
					
				
					 5 changed files with 14265 additions and 158 deletions
				
			
		|  | @ -15,16 +15,12 @@ if ROOT_PATH not in sys.path: | ||||||
| 
 | 
 | ||||||
| django.setup() | django.setup() | ||||||
| 
 | 
 | ||||||
| # VENV_PATH = ROOT_PATH + '/venv/lib/python3.6/site-packages' |  | ||||||
| # if VENV_PATH not in sys.path: |  | ||||||
| #     sys.path.append(VENV_PATH) |  | ||||||
| 
 |  | ||||||
| import re | import re | ||||||
| import socket | import socket | ||||||
| from six.moves import http_cookies as Cookie | from six.moves import http_cookies as Cookie | ||||||
| from webvirtcloud.settings import WS_PUBLIC_PORT, WS_HOST, WS_CERT | from webvirtcloud.settings import WS_PUBLIC_PORT, WS_HOST, WS_CERT | ||||||
| from vrtManager.connection import CONN_SSH, CONN_SOCKET | from vrtManager.connection import CONN_SSH, CONN_SOCKET | ||||||
| from console.tunnel import Tunnel | from console.sshtunnels import SSHTunnels | ||||||
| from optparse import OptionParser | from optparse import OptionParser | ||||||
| 
 | 
 | ||||||
| parser = OptionParser() | parser = OptionParser() | ||||||
|  | @ -119,7 +115,8 @@ def get_connection_infos(token): | ||||||
|         console_port = conn.get_console_port() |         console_port = conn.get_console_port() | ||||||
|         console_socket = conn.get_console_socket() |         console_socket = conn.get_console_socket() | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         logging.error('Fail to retrieve console connection infos for token %s : %s' % (token, e)) |         logging.error( | ||||||
|  |             'Fail to retrieve console connection infos for token %s : %s' % (token, e)) | ||||||
|         raise |         raise | ||||||
|     return (connhost, connport, connuser, conntype, console_host, |     return (connhost, connport, connuser, conntype, console_host, | ||||||
|             console_port, console_socket) |             console_port, console_socket) | ||||||
|  | @ -131,6 +128,7 @@ class CompatibilityMixIn(object): | ||||||
|         # from the request to a cookie header, we should check |         # from the request to a cookie header, we should check | ||||||
|         # also for this behavior |         # also for this behavior | ||||||
|         hcookie = self.headers.get('cookie') |         hcookie = self.headers.get('cookie') | ||||||
|  | 
 | ||||||
|         if hcookie: |         if hcookie: | ||||||
|             cookie = Cookie.SimpleCookie() |             cookie = Cookie.SimpleCookie() | ||||||
|             for hcookie_part in hcookie.split(';'): |             for hcookie_part in hcookie.split(';'): | ||||||
|  | @ -145,6 +143,7 @@ class CompatibilityMixIn(object): | ||||||
|                     if 'token' in cookie: |                     if 'token' in cookie: | ||||||
|                         token = cookie['token'].value |                         token = cookie['token'].value | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|         (connhost, connport, connuser, conntype, console_host, console_port, |         (connhost, connport, connuser, conntype, console_host, console_port, | ||||||
|          console_socket) = get_connection_infos(token) |          console_socket) = get_connection_infos(token) | ||||||
| 
 | 
 | ||||||
|  | @ -176,10 +175,11 @@ class CompatibilityMixIn(object): | ||||||
|                 error_msg = "Try to open tunnel on %s@%s:%s on console %s:%s " |                 error_msg = "Try to open tunnel on %s@%s:%s on console %s:%s " | ||||||
|                 error_msg += "(or socket %s)" |                 error_msg += "(or socket %s)" | ||||||
|                 self.msg(error_msg % (connuser, connhost, connport, |                 self.msg(error_msg % (connuser, connhost, connport, | ||||||
|                          console_host, console_port, console_socket)) |                                       console_host, console_port, console_socket)) | ||||||
|                 tunnel = Tunnel() |                 tunnel = SSHTunnels(connhost, connuser, connport, | ||||||
|                 fd = tunnel.open(connhost, connuser, connport, |  | ||||||
|                                  console_host, console_port, console_socket) |                                  console_host, console_port, console_socket) | ||||||
|  |                 fd = tunnel.open_new() | ||||||
|  |                 tunnel.unlock() | ||||||
|                 tsock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) |                 tsock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) | ||||||
|             except Exception as e: |             except Exception as e: | ||||||
|                 self.msg("Fail to open tunnel : %s" % e) |                 self.msg("Fail to open tunnel : %s" % e) | ||||||
|  | @ -198,18 +198,17 @@ class CompatibilityMixIn(object): | ||||||
|         try: |         try: | ||||||
|             self.msg("Start proxying") |             self.msg("Start proxying") | ||||||
|             self.do_proxy(tsock) |             self.do_proxy(tsock) | ||||||
|         except: |         except Exception: | ||||||
|             if tunnel: |             if tunnel: | ||||||
|                 self.vmsg( |                 self.vmsg( | ||||||
|                     "%s:%s (via %s@%s:%s) : Target closed" % |                     "%s:%s (via %s@%s:%s) : Websocket client or Target closed" % | ||||||
|                     (console_host, console_port, connuser, connhost, connport)) |                     (console_host, console_port, connuser, connhost, connport)) | ||||||
|                 if tsock: |                 if tsock: | ||||||
|                     tsock.shutdown(socket.SHUT_RDWR) |                     tsock.shutdown(socket.SHUT_RDWR) | ||||||
|                     tsock.close() |                     tsock.close() | ||||||
|                 tunnel.close() |                 tunnel.close_all() | ||||||
|             raise |             raise | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| if USE_HANDLER: | if USE_HANDLER: | ||||||
|     class NovaProxyRequestHandler(ProxyRequestHandler, CompatibilityMixIn): |     class NovaProxyRequestHandler(ProxyRequestHandler, CompatibilityMixIn): | ||||||
|         def msg(self, *args, **kwargs): |         def msg(self, *args, **kwargs): | ||||||
|  |  | ||||||
							
								
								
									
										209
									
								
								console/sshtunnels.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								console/sshtunnels.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,209 @@ | ||||||
|  | # Copyright (C) 2014, 2015 Red Hat, Inc. | ||||||
|  | # | ||||||
|  | # This work is licensed under the GNU GPLv2 or later. | ||||||
|  | # See the COPYING file in the top-level directory. | ||||||
|  | 
 | ||||||
|  | import functools | ||||||
|  | import os | ||||||
|  | import queue | ||||||
|  | import socket | ||||||
|  | import signal | ||||||
|  | import threading | ||||||
|  | 
 | ||||||
|  | import logging as log | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class _TunnelScheduler(object): | ||||||
|  |     """ | ||||||
|  |     If the user is using Spice + SSH URI + no SSH keys, we need to | ||||||
|  |     serialize connection opening otherwise ssh-askpass gets all angry. | ||||||
|  |     This handles the locking and scheduling. | ||||||
|  |     It's only instantiated once for the whole app, because we serialize | ||||||
|  |     independent of connection, vm, etc. | ||||||
|  |     """ | ||||||
|  |     def __init__(self): | ||||||
|  |         self._thread = None | ||||||
|  |         self._queue = queue.Queue() | ||||||
|  |         self._lock = threading.Lock() | ||||||
|  | 
 | ||||||
|  |     def _handle_queue(self): | ||||||
|  |         while True: | ||||||
|  |             lock_cb, cb, args, = self._queue.get() | ||||||
|  |             lock_cb() | ||||||
|  |             cb(*args) | ||||||
|  | 
 | ||||||
|  |     def schedule(self, lock_cb, cb, *args): | ||||||
|  |         if not self._thread: | ||||||
|  |             self._thread = threading.Thread(name="Tunnel thread", | ||||||
|  |                                             target=self._handle_queue, | ||||||
|  |                                             args=()) | ||||||
|  |             self._thread.daemon = True | ||||||
|  |         if not self._thread.is_alive(): | ||||||
|  |             self._thread.start() | ||||||
|  |         self._queue.put((lock_cb, cb, args)) | ||||||
|  | 
 | ||||||
|  |     def lock(self): | ||||||
|  |         self._lock.acquire() | ||||||
|  |     def unlock(self): | ||||||
|  |         self._lock.release() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | _tunnel_scheduler = _TunnelScheduler() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class _Tunnel(object): | ||||||
|  |     def __init__(self): | ||||||
|  |         self._pid = None | ||||||
|  |         self._closed = False | ||||||
|  |         self._errfd = None | ||||||
|  | 
 | ||||||
|  |     def close(self): | ||||||
|  |         if self._closed: | ||||||
|  |             return | ||||||
|  |         self._closed = True | ||||||
|  | 
 | ||||||
|  |         log.debug("Close tunnel PID=%s ERRFD=%s", | ||||||
|  |                       self._pid, self._errfd and self._errfd.fileno() or None) | ||||||
|  | 
 | ||||||
|  |         # Since this is a socket object, the file descriptor is closed | ||||||
|  |         # when it's garbage collected. | ||||||
|  |         self._errfd = None | ||||||
|  | 
 | ||||||
|  |         if self._pid: | ||||||
|  |             os.kill(self._pid, signal.SIGKILL) | ||||||
|  |             os.waitpid(self._pid, 0) | ||||||
|  |         self._pid = None | ||||||
|  | 
 | ||||||
|  |     def get_err_output(self): | ||||||
|  |         errout = "" | ||||||
|  |         while True: | ||||||
|  |             try: | ||||||
|  |                 new = self._errfd.recv(1024) | ||||||
|  |             except Exception: | ||||||
|  |                 break | ||||||
|  | 
 | ||||||
|  |             if not new: | ||||||
|  |                 break | ||||||
|  | 
 | ||||||
|  |             errout += new.decode() | ||||||
|  | 
 | ||||||
|  |         return errout | ||||||
|  | 
 | ||||||
|  |     def open(self, argv, sshfd): | ||||||
|  |         if self._closed: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         errfds = socket.socketpair() | ||||||
|  |         pid = os.fork() | ||||||
|  |         if pid == 0: | ||||||
|  |             errfds[0].close() | ||||||
|  | 
 | ||||||
|  |             os.dup2(sshfd.fileno(), 0) | ||||||
|  |             os.dup2(sshfd.fileno(), 1) | ||||||
|  |             os.dup2(errfds[1].fileno(), 2) | ||||||
|  |             os.execlp(*argv) | ||||||
|  |             os._exit(1)  # pylint: disable=protected-access | ||||||
|  | 
 | ||||||
|  |         sshfd.close() | ||||||
|  |         errfds[1].close() | ||||||
|  | 
 | ||||||
|  |         self._errfd = errfds[0] | ||||||
|  |         self._errfd.setblocking(0) | ||||||
|  |         log.debug("Opened tunnel PID=%d ERRFD=%d", | ||||||
|  |                       pid, self._errfd.fileno()) | ||||||
|  | 
 | ||||||
|  |         self._pid = pid | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _make_ssh_command(connhost, connuser, connport, gaddr, gport, gsocket): | ||||||
|  |      | ||||||
|  | 
 | ||||||
|  |     # Build SSH cmd | ||||||
|  |     argv = ["ssh", "ssh"] | ||||||
|  |     if connport: | ||||||
|  |         argv += ["-p", str(connport)] | ||||||
|  | 
 | ||||||
|  |     if connuser: | ||||||
|  |         argv += ['-l', connuser] | ||||||
|  | 
 | ||||||
|  |     argv += [connhost] | ||||||
|  | 
 | ||||||
|  |     # Build 'nc' command run on the remote host | ||||||
|  |     # | ||||||
|  |     # This ugly thing is a shell script to detect availability of | ||||||
|  |     # the -q option for 'nc': debian and suse based distros need this | ||||||
|  |     # flag to ensure the remote nc will exit on EOF, so it will go away | ||||||
|  |     # when we close the VNC tunnel. If it doesn't go away, subsequent | ||||||
|  |     # VNC connection attempts will hang. | ||||||
|  |     # | ||||||
|  |     # Fedora's 'nc' doesn't have this option, and apparently defaults | ||||||
|  |     # to the desired behavior. | ||||||
|  |     # | ||||||
|  |     if gsocket: | ||||||
|  |         nc_params = "-U %s" % gsocket | ||||||
|  |     else: | ||||||
|  |         nc_params = "%s %s" % (gaddr, gport) | ||||||
|  | 
 | ||||||
|  |     nc_cmd = ( | ||||||
|  |         """nc -q 2>&1 | grep "requires an argument" >/dev/null;""" | ||||||
|  |         """if [ $? -eq 0 ] ; then""" | ||||||
|  |         """   CMD="nc -q 0 %(nc_params)s";""" | ||||||
|  |         """else""" | ||||||
|  |         """   CMD="nc %(nc_params)s";""" | ||||||
|  |         """fi;""" | ||||||
|  |         """eval "$CMD";""" % | ||||||
|  |         {'nc_params': nc_params}) | ||||||
|  | 
 | ||||||
|  |     argv.append("sh -c") | ||||||
|  |     argv.append("'%s'" % nc_cmd) | ||||||
|  | 
 | ||||||
|  |     argv_str = functools.reduce(lambda x, y: x + " " + y, argv[1:]) | ||||||
|  |     log.debug("Pre-generated ssh command for info: %s", argv_str) | ||||||
|  |     return argv | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class SSHTunnels(object): | ||||||
|  |     def __init__(self, connhost, connuser, connport, gaddr, gport, gsocket): | ||||||
|  |         self._tunnels = [] | ||||||
|  |         self._sshcommand = _make_ssh_command(connhost, connuser, connport, gaddr, gport, gsocket) | ||||||
|  |         self._locked = False | ||||||
|  | 
 | ||||||
|  |     def open_new(self): | ||||||
|  |         t = _Tunnel() | ||||||
|  |         self._tunnels.append(t) | ||||||
|  | 
 | ||||||
|  |         # socket FDs are closed when the object is garbage collected. This | ||||||
|  |         # can close an FD behind spice/vnc's back which causes crashes. | ||||||
|  |         # | ||||||
|  |         # Dup a bare FD for the viewer side of things, but keep the high | ||||||
|  |         # level socket object for the SSH side, since it simplifies things | ||||||
|  |         # in that area. | ||||||
|  |         viewerfd, sshfd = socket.socketpair() | ||||||
|  |         _tunnel_scheduler.schedule(self._lock, t.open, self._sshcommand, sshfd) | ||||||
|  | 
 | ||||||
|  |         retfd = os.dup(viewerfd.fileno()) | ||||||
|  |         log.debug("Generated tunnel fd=%s for viewer", retfd) | ||||||
|  |         return retfd | ||||||
|  | 
 | ||||||
|  |     def close_all(self): | ||||||
|  |         for l in self._tunnels: | ||||||
|  |             l.close() | ||||||
|  |         self._tunnels = [] | ||||||
|  |         self.unlock() | ||||||
|  | 
 | ||||||
|  |     def get_err_output(self): | ||||||
|  |         errstrings = [] | ||||||
|  |         for l in self._tunnels: | ||||||
|  |             e = l.get_err_output().strip() | ||||||
|  |             if e and e not in errstrings: | ||||||
|  |                 errstrings.append(e) | ||||||
|  |         return "\n".join(errstrings) | ||||||
|  | 
 | ||||||
|  |     def _lock(self): | ||||||
|  |         _tunnel_scheduler.lock() | ||||||
|  |         self._locked = True | ||||||
|  | 
 | ||||||
|  |     def unlock(self, *args, **kwargs): | ||||||
|  |         if self._locked: | ||||||
|  |             _tunnel_scheduler.unlock(*args, **kwargs) | ||||||
|  |             self._locked = False | ||||||
|  | @ -111,7 +111,7 @@ | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // uri = scheme + host + ":" + port; |         // uri = scheme + host + ":" + port; | ||||||
|         uri = scheme + "{{ ws_host }}:{{ ws_port }}{{ ws_path }}"; |         uri = scheme + "{{ ws_host }}:{{ ws_port }}"; | ||||||
| 
 | 
 | ||||||
|         if (path) { |         if (path) { | ||||||
|             uri += path[0] == '/' ? path : ('/' + path); |             uri += path[0] == '/' ? path : ('/' + path); | ||||||
|  |  | ||||||
|  | @ -1,144 +0,0 @@ | ||||||
| # |  | ||||||
| # This class provide from VirtManager project, from console.py |  | ||||||
| # file. |  | ||||||
| # |  | ||||||
| # Copyright (C) 2006-2008 Red Hat, Inc. |  | ||||||
| # Copyright (C) 2006 Daniel P. Berrange <berrange@redhat.com> |  | ||||||
| # Copyright (C) 2010 Marc-André Lureau <marcandre.lureau@redhat.com> |  | ||||||
| # |  | ||||||
| # This program is free software; you can redistribute it and/or modify |  | ||||||
| # it under the terms of the GNU General Public License as published by |  | ||||||
| # the Free Software Foundation; either version 2 of the License, or |  | ||||||
| # (at your option) any later version. |  | ||||||
| # |  | ||||||
| # This program is distributed in the hope that it will be useful, |  | ||||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
| # GNU General Public License for more details. |  | ||||||
| # |  | ||||||
| # You should have received a copy of the GNU General Public License |  | ||||||
| # along with this program; if not, write to the Free Software |  | ||||||
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, |  | ||||||
| # MA 02110-1301 USA. |  | ||||||
| # |  | ||||||
| 
 |  | ||||||
| import os |  | ||||||
| import socket |  | ||||||
| import signal |  | ||||||
| import logging |  | ||||||
| from functools import reduce |  | ||||||
| 
 |  | ||||||
| class Tunnel(object): |  | ||||||
|     def __init__(self): |  | ||||||
|         self.outfd = None |  | ||||||
|         self.errfd = None |  | ||||||
|         self.pid = None |  | ||||||
| 
 |  | ||||||
|     def open(self, connhost, connuser, connport, gaddr, gport, gsocket): |  | ||||||
|         if self.outfd is not None: |  | ||||||
|             return -1 |  | ||||||
| 
 |  | ||||||
|         # Build SSH cmd |  | ||||||
|         argv = ["ssh", "ssh"] |  | ||||||
|         if connport: |  | ||||||
|             argv += ["-p", str(connport)] |  | ||||||
| 
 |  | ||||||
|         if connuser: |  | ||||||
|             argv += ['-l', connuser] |  | ||||||
| 
 |  | ||||||
|         argv += [connhost] |  | ||||||
| 
 |  | ||||||
|         # Build 'nc' command run on the remote host |  | ||||||
|         # |  | ||||||
|         # This ugly thing is a shell script to detect availability of |  | ||||||
|         # the -q option for 'nc': debian and suse based distros need this |  | ||||||
|         # flag to ensure the remote nc will exit on EOF, so it will go away |  | ||||||
|         # when we close the VNC tunnel. If it doesn't go away, subsequent |  | ||||||
|         # VNC connection attempts will hang. |  | ||||||
|         # |  | ||||||
|         # Fedora's 'nc' doesn't have this option, and apparently defaults |  | ||||||
|         # to the desired behavior. |  | ||||||
|         # |  | ||||||
|         if gsocket: |  | ||||||
|             nc_params = "-U %s" % gsocket |  | ||||||
|         else: |  | ||||||
|             nc_params = "%s %s" % (gaddr, gport) |  | ||||||
| 
 |  | ||||||
|         nc_cmd = ( |  | ||||||
|             """nc -q 2>&1 | grep "requires an argument" >/dev/null;""" |  | ||||||
|             """if [ $? -eq 0 ] ; then""" |  | ||||||
|             """   CMD="nc -q 0 %(nc_params)s";""" |  | ||||||
|             """else""" |  | ||||||
|             """   CMD="nc %(nc_params)s";""" |  | ||||||
|             """fi;""" |  | ||||||
|             """eval "$CMD";""" % |  | ||||||
|             {'nc_params': nc_params}) |  | ||||||
| 
 |  | ||||||
|         argv.append("sh -c") |  | ||||||
|         argv.append("'%s'" % nc_cmd) |  | ||||||
| 
 |  | ||||||
|         argv_str = reduce(lambda x, y: x + " " + y, argv[1:]) |  | ||||||
|         logging.debug("Creating SSH tunnel: %s", argv_str) |  | ||||||
| 
 |  | ||||||
|         fds = socket.socketpair() |  | ||||||
|         errorfds = socket.socketpair() |  | ||||||
| 
 |  | ||||||
|         pid = os.fork() |  | ||||||
|         if pid == 0: |  | ||||||
|             fds[0].close() |  | ||||||
|             errorfds[0].close() |  | ||||||
| 
 |  | ||||||
|             os.close(0) |  | ||||||
|             os.close(1) |  | ||||||
|             os.close(2) |  | ||||||
|             os.dup2(fds[1].fileno(), 0) |  | ||||||
|             os.dup2(fds[1].fileno(), 1) |  | ||||||
|             os.dup2(errorfds[1].fileno(), 2) |  | ||||||
|             os.execlp(*argv) |  | ||||||
|             os._exit(1) |  | ||||||
|         else: |  | ||||||
|             fds[1].close() |  | ||||||
|             errorfds[1].close() |  | ||||||
| 
 |  | ||||||
|         logging.debug("Tunnel PID=%d OUTFD=%d ERRFD=%d", |  | ||||||
|                       pid, fds[0].fileno(), errorfds[0].fileno()) |  | ||||||
|         errorfds[0].setblocking(False) |  | ||||||
| 
 |  | ||||||
|         self.outfd = fds[0] |  | ||||||
|         self.errfd = errorfds[0] |  | ||||||
|         self.pid = pid |  | ||||||
| 
 |  | ||||||
|         fd = fds[0].fileno() |  | ||||||
|         if fd < 0: |  | ||||||
|             raise SystemError("can't open a new tunnel: fd=%d" % fd) |  | ||||||
|         return fd |  | ||||||
| 
 |  | ||||||
|     def close(self): |  | ||||||
|         if self.outfd is None: |  | ||||||
|             return |  | ||||||
| 
 |  | ||||||
|         logging.debug("Shutting down tunnel PID=%d OUTFD=%d ERRFD=%d", |  | ||||||
|                       self.pid, self.outfd.fileno(), |  | ||||||
|                       self.errfd.fileno()) |  | ||||||
|         self.outfd.close() |  | ||||||
|         self.outfd = None |  | ||||||
|         self.errfd.close() |  | ||||||
|         self.errfd = None |  | ||||||
| 
 |  | ||||||
|         os.kill(self.pid, signal.SIGKILL) |  | ||||||
|         self.pid = None |  | ||||||
| 
 |  | ||||||
|     def get_err_output(self): |  | ||||||
|         errout = "" |  | ||||||
|         while True: |  | ||||||
|             try: |  | ||||||
|                 new = self.errfd.recv(1024) |  | ||||||
|             except: |  | ||||||
|                 break |  | ||||||
| 
 |  | ||||||
|             if not new: |  | ||||||
|                 break |  | ||||||
| 
 |  | ||||||
|             errout += new |  | ||||||
| 
 |  | ||||||
|         return errout |  | ||||||
							
								
								
									
										14043
									
								
								static/js/novnc/vendor/sinon.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										14043
									
								
								static/js/novnc/vendor/sinon.js
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue