blob: 1945581dae7c5fd685a3b54699891a5c32597fa5 [file] [log] [blame]
Yilin Yang19da6932019-12-10 13:39:28 +08001#!/usr/bin/env python3
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08002# Copyright 2015 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Yilin Yang14d02a22019-11-01 11:32:03 +08006from __future__ import division
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08007from __future__ import print_function
8
9import argparse
10import ast
11import base64
12import fcntl
Peter Shih13e78c52018-01-23 12:57:07 +080013import functools
Wei-Ning Huangba768ab2016-02-07 14:38:06 +080014import getpass
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080015import hashlib
Yilin Yang752db712019-09-27 15:42:38 +080016import http.client
Yilin Yang8d4f9d02019-11-28 17:12:11 +080017from io import StringIO
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080018import json
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080019import logging
20import os
21import re
22import select
23import signal
24import socket
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080025import ssl
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080026import struct
27import subprocess
28import sys
29import tempfile
30import termios
31import threading
32import time
33import tty
Peter Shih99b73ec2017-06-16 17:54:15 +080034import unicodedata # required by pyinstaller, pylint: disable=unused-import
Yilin Yangf54fb912020-01-08 11:42:38 +080035import urllib.error
36import urllib.parse
37import urllib.request
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080038
Peter Shih99b73ec2017-06-16 17:54:15 +080039import jsonrpclib
Stimim Chena30447c2020-10-06 10:04:00 +080040from jsonrpclib import config
Peter Shih99b73ec2017-06-16 17:54:15 +080041from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080042from ws4py.client import WebSocketBaseClient
Peter Shih99b73ec2017-06-16 17:54:15 +080043import yaml
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080044
Yilin Yang83c8f442020-05-05 13:46:51 +080045from cros.factory.utils import process_utils
46
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080047
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080048_CERT_DIR = os.path.expanduser('~/.config/ovl')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080049
50_ESCAPE = '~'
51_BUFSIZ = 8192
52_OVERLORD_PORT = 4455
53_OVERLORD_HTTP_PORT = 9000
54_OVERLORD_CLIENT_DAEMON_PORT = 4488
55_OVERLORD_CLIENT_DAEMON_RPC_ADDR = ('127.0.0.1', _OVERLORD_CLIENT_DAEMON_PORT)
56
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080057_CONNECT_TIMEOUT = 3
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +080058_DEFAULT_HTTP_TIMEOUT = 30
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080059_LIST_CACHE_TIMEOUT = 2
60_DEFAULT_TERMINAL_WIDTH = 80
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +080061_RETRY_TIMES = 3
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080062
63# echo -n overlord | md5sum
64_HTTP_BOUNDARY_MAGIC = '9246f080c855a69012707ab53489b921'
65
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +080066# Terminal resize control
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080067_CONTROL_START = 128
68_CONTROL_END = 129
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +080069
70# Stream control
71_STDIN_CLOSED = '##STDIN_CLOSED##'
72
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080073_SSH_CONTROL_SOCKET_PREFIX = os.path.join(tempfile.gettempdir(),
74 'ovl-ssh-control-')
75
Wei-Ning Huang47c79b82016-05-24 01:24:46 +080076_TLS_CERT_FAILED_WARNING = """
77@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
78@ WARNING: REMOTE HOST VERIFICATION HAS FAILED! @
79@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
80IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
81Someone could be eavesdropping on you right now (man-in-the-middle attack)!
82It is also possible that the server is using a self-signed certificate.
83The fingerprint for the TLS host certificate sent by the remote host is
84
85%s
86
87Do you want to trust this certificate and proceed? [Y/n] """
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +080088
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080089_TLS_CERT_CHANGED_WARNING = """
90@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
91@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
92@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
93IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
94Someone could be eavesdropping on you right now (man-in-the-middle attack)!
Wei-Ning Huang47c79b82016-05-24 01:24:46 +080095It is also possible that the TLS host certificate has just been changed.
96The fingerprint for the TLS host certificate sent by the remote host is
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +080097
98%s
99
100Remove '%s' if you still want to proceed.
101SSL Certificate verification failed."""
102
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800103
104def GetVersionDigest():
105 """Return the sha1sum of the current executing script."""
Wei-Ning Huang31609662016-08-11 00:22:25 +0800106 # Check python script by default
107 filename = __file__
108
109 # If we are running from a frozen binary, we should calculate the checksum
110 # against that binary instead of the python script.
111 # See: https://pyinstaller.readthedocs.io/en/stable/runtime-information.html
112 if getattr(sys, 'frozen', False):
113 filename = sys.executable
114
Yilin Yang0412c272019-12-05 16:57:40 +0800115 with open(filename, 'rb') as f:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800116 return hashlib.sha1(f.read()).hexdigest()
117
118
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800119def GetTLSCertPath(host):
120 return os.path.join(_CERT_DIR, '%s.cert' % host)
121
122
123def UrlOpen(state, url):
Yilin Yangf54fb912020-01-08 11:42:38 +0800124 """Wrapper for urllib.request.urlopen.
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800125
126 It selects correct HTTP scheme according to self._state.ssl, add HTTP
127 basic auth headers, and add specify correct SSL context.
128 """
129 url = MakeRequestUrl(state, url)
Yilin Yangf54fb912020-01-08 11:42:38 +0800130 request = urllib.request.Request(url)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800131 if state.username is not None and state.password is not None:
132 request.add_header(*BasicAuthHeader(state.username, state.password))
Yilin Yangf54fb912020-01-08 11:42:38 +0800133 return urllib.request.urlopen(request, timeout=_DEFAULT_HTTP_TIMEOUT,
134 context=state.ssl_context)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800135
136
137def GetTLSCertificateSHA1Fingerprint(cert_pem):
138 beg = cert_pem.index('\n')
139 end = cert_pem.rindex('\n', 0, len(cert_pem) - 2)
140 cert_pem = cert_pem[beg:end] # Remove BEGIN/END CERTIFICATE boundary
141 cert_der = base64.b64decode(cert_pem)
142 return hashlib.sha1(cert_der).hexdigest()
143
144
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800145def KillGraceful(pid, wait_secs=1):
146 """Kill a process gracefully by first sending SIGTERM, wait for some time,
147 then send SIGKILL to make sure it's killed."""
148 try:
149 os.kill(pid, signal.SIGTERM)
150 time.sleep(wait_secs)
151 os.kill(pid, signal.SIGKILL)
152 except OSError:
153 pass
154
155
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800156def AutoRetry(action_name, retries):
157 """Decorator for retry function call."""
158 def Wrap(func):
Peter Shih13e78c52018-01-23 12:57:07 +0800159 @functools.wraps(func)
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800160 def Loop(*args, **kwargs):
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800161 for unused_i in range(retries):
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800162 try:
163 func(*args, **kwargs)
164 except Exception as e:
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800165 print('error: %s: %s: retrying ...' % (args[0], e))
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800166 else:
167 break
168 else:
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800169 print('error: failed to %s %s' % (action_name, args[0]))
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +0800170 return Loop
171 return Wrap
172
173
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800174def BasicAuthHeader(user, password):
175 """Return HTTP basic auth header."""
Stimim Chena30447c2020-10-06 10:04:00 +0800176 credential = base64.b64encode(
177 b'%s:%s' % (user.encode('utf-8'), password.encode('utf-8')))
178 return ('Authorization', 'Basic %s' % credential.decode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800179
180
181def GetTerminalSize():
182 """Retrieve terminal window size."""
183 ws = struct.pack('HHHH', 0, 0, 0, 0)
184 ws = fcntl.ioctl(0, termios.TIOCGWINSZ, ws)
185 lines, columns, unused_x, unused_y = struct.unpack('HHHH', ws)
186 return lines, columns
187
188
189def MakeRequestUrl(state, url):
190 return 'http%s://%s' % ('s' if state.ssl else '', url)
191
192
Fei Shaobd07c9a2020-06-15 19:04:50 +0800193class ProgressBar:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800194 SIZE_WIDTH = 11
195 SPEED_WIDTH = 10
196 DURATION_WIDTH = 6
197 PERCENTAGE_WIDTH = 8
198
199 def __init__(self, name):
200 self._start_time = time.time()
201 self._name = name
202 self._size = 0
203 self._width = 0
204 self._name_width = 0
205 self._name_max = 0
206 self._stat_width = 0
207 self._max = 0
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800208 self._CalculateSize()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800209 self.SetProgress(0)
210
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800211 def _CalculateSize(self):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800212 self._width = GetTerminalSize()[1] or _DEFAULT_TERMINAL_WIDTH
213 self._name_width = int(self._width * 0.3)
214 self._name_max = self._name_width
215 self._stat_width = self.SIZE_WIDTH + self.SPEED_WIDTH + self.DURATION_WIDTH
216 self._max = (self._width - self._name_width - self._stat_width -
217 self.PERCENTAGE_WIDTH)
218
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800219 def _SizeToHuman(self, size_in_bytes):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800220 if size_in_bytes < 1024:
221 unit = 'B'
222 value = size_in_bytes
223 elif size_in_bytes < 1024 ** 2:
224 unit = 'KiB'
Yilin Yang14d02a22019-11-01 11:32:03 +0800225 value = size_in_bytes / 1024
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800226 elif size_in_bytes < 1024 ** 3:
227 unit = 'MiB'
Yilin Yang14d02a22019-11-01 11:32:03 +0800228 value = size_in_bytes / (1024 ** 2)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800229 elif size_in_bytes < 1024 ** 4:
230 unit = 'GiB'
Yilin Yang14d02a22019-11-01 11:32:03 +0800231 value = size_in_bytes / (1024 ** 3)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800232 return ' %6.1f %3s' % (value, unit)
233
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800234 def _SpeedToHuman(self, speed_in_bs):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800235 if speed_in_bs < 1024:
236 unit = 'B'
237 value = speed_in_bs
238 elif speed_in_bs < 1024 ** 2:
239 unit = 'K'
Yilin Yang14d02a22019-11-01 11:32:03 +0800240 value = speed_in_bs / 1024
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800241 elif speed_in_bs < 1024 ** 3:
242 unit = 'M'
Yilin Yang14d02a22019-11-01 11:32:03 +0800243 value = speed_in_bs / (1024 ** 2)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800244 elif speed_in_bs < 1024 ** 4:
245 unit = 'G'
Yilin Yang14d02a22019-11-01 11:32:03 +0800246 value = speed_in_bs / (1024 ** 3)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800247 return ' %6.1f%s/s' % (value, unit)
248
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800249 def _DurationToClock(self, duration):
Yilin Yang14d02a22019-11-01 11:32:03 +0800250 return ' %02d:%02d' % (duration // 60, duration % 60)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800251
252 def SetProgress(self, percentage, size=None):
253 current_width = GetTerminalSize()[1]
254 if self._width != current_width:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800255 self._CalculateSize()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800256
257 if size is not None:
258 self._size = size
259
260 elapse_time = time.time() - self._start_time
Yilin Yang14d02a22019-11-01 11:32:03 +0800261 speed = self._size / elapse_time
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800262
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800263 size_str = self._SizeToHuman(self._size)
264 speed_str = self._SpeedToHuman(speed)
265 elapse_str = self._DurationToClock(elapse_time)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800266
267 width = int(self._max * percentage / 100.0)
268 sys.stdout.write(
269 '%*s' % (- self._name_max,
270 self._name if len(self._name) <= self._name_max else
271 self._name[:self._name_max - 4] + ' ...') +
272 size_str + speed_str + elapse_str +
273 ((' [' + '#' * width + ' ' * (self._max - width) + ']' +
274 '%4d%%' % int(percentage)) if self._max > 2 else '') + '\r')
275 sys.stdout.flush()
276
277 def End(self):
278 self.SetProgress(100.0)
279 sys.stdout.write('\n')
280 sys.stdout.flush()
281
282
Fei Shaobd07c9a2020-06-15 19:04:50 +0800283class DaemonState:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800284 """DaemonState is used for storing Overlord state info."""
285 def __init__(self):
286 self.version_sha1sum = GetVersionDigest()
287 self.host = None
288 self.port = None
289 self.ssl = False
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800290 self.ssl_self_signed = False
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800291 self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800292 self.ssh = False
293 self.orig_host = None
294 self.ssh_pid = None
295 self.username = None
296 self.password = None
297 self.selected_mid = None
298 self.forwards = {}
299 self.listing = []
300 self.last_list = 0
301
302
Fei Shaobd07c9a2020-06-15 19:04:50 +0800303class OverlordClientDaemon:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800304 """Overlord Client Daemon."""
305 def __init__(self):
Stimim Chena30447c2020-10-06 10:04:00 +0800306 # Use full module path for jsonrpclib to resolve.
307 import cros.factory.tools.ovl
308 self._state = cros.factory.tools.ovl.DaemonState()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800309 self._server = None
310
311 def Start(self):
312 self.StartRPCServer()
313
314 def StartRPCServer(self):
315 self._server = SimpleJSONRPCServer(_OVERLORD_CLIENT_DAEMON_RPC_ADDR,
316 logRequests=False)
317 exports = [
318 (self.State, 'State'),
319 (self.Ping, 'Ping'),
320 (self.GetPid, 'GetPid'),
321 (self.Connect, 'Connect'),
322 (self.Clients, 'Clients'),
323 (self.SelectClient, 'SelectClient'),
324 (self.AddForward, 'AddForward'),
325 (self.RemoveForward, 'RemoveForward'),
326 (self.RemoveAllForward, 'RemoveAllForward'),
327 ]
328 for func, name in exports:
329 self._server.register_function(func, name)
330
331 pid = os.fork()
332 if pid == 0:
Peter Shih4d55ded2017-07-03 17:19:01 +0800333 for fd in range(3):
334 os.close(fd)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800335 self._server.serve_forever()
336
337 @staticmethod
338 def GetRPCServer():
339 """Returns the Overlord client daemon RPC server."""
340 server = jsonrpclib.Server('http://%s:%d' %
341 _OVERLORD_CLIENT_DAEMON_RPC_ADDR)
342 try:
343 server.Ping()
344 except Exception:
345 return None
346 return server
347
348 def State(self):
349 return self._state
350
351 def Ping(self):
352 return True
353
354 def GetPid(self):
355 return os.getpid()
356
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800357 def _GetJSON(self, path):
358 url = '%s:%d%s' % (self._state.host, self._state.port, path)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800359 return json.loads(UrlOpen(self._state, url).read())
360
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800361 def _TLSEnabled(self):
362 """Determine if TLS is enabled on given server address."""
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800363 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
364 try:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800365 # Allow any certificate since we only want to check if server talks TLS.
366 context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
367 context.verify_mode = ssl.CERT_NONE
368
369 sock = context.wrap_socket(sock, server_hostname=self._state.host)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800370 sock.settimeout(_CONNECT_TIMEOUT)
371 sock.connect((self._state.host, self._state.port))
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800372 return True
Wei-Ning Huangecc80b82016-07-01 16:55:10 +0800373 except ssl.SSLError:
374 return False
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800375 except socket.error: # Connect refused or timeout
376 raise
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800377 except Exception:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800378 return False # For whatever reason above failed, assume False
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800379
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800380 def _CheckTLSCertificate(self, check_hostname=True):
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800381 """Check TLS certificate.
382
383 Returns:
384 A tupple (check_result, if_certificate_is_loaded)
385 """
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800386 def _DoConnect(context):
387 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
388 try:
389 sock.settimeout(_CONNECT_TIMEOUT)
390 sock = context.wrap_socket(sock, server_hostname=self._state.host)
391 sock.connect((self._state.host, self._state.port))
392 except ssl.SSLError:
393 return False
394 finally:
395 sock.close()
396
397 # Save SSLContext for future use.
398 self._state.ssl_context = context
399 return True
400
401 # First try connect with built-in certificates
402 tls_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
403 if _DoConnect(tls_context):
404 return True
405
406 # Try with already saved certificate, if any.
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800407 tls_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
408 tls_context.verify_mode = ssl.CERT_REQUIRED
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800409 tls_context.check_hostname = check_hostname
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800410
411 tls_cert_path = GetTLSCertPath(self._state.host)
412 if os.path.exists(tls_cert_path):
413 tls_context.load_verify_locations(tls_cert_path)
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800414 self._state.ssl_self_signed = True
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800415
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800416 return _DoConnect(tls_context)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800417
418 def Connect(self, host, port=_OVERLORD_HTTP_PORT, ssh_pid=None,
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800419 username=None, password=None, orig_host=None,
420 check_hostname=True):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800421 self._state.username = username
422 self._state.password = password
423 self._state.host = host
424 self._state.port = port
425 self._state.ssl = False
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800426 self._state.ssl_self_signed = False
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800427 self._state.orig_host = orig_host
428 self._state.ssh_pid = ssh_pid
429 self._state.selected_mid = None
430
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800431 tls_enabled = self._TLSEnabled()
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800432 if tls_enabled:
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800433 result = self._CheckTLSCertificate(check_hostname)
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800434 if not result:
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800435 if self._state.ssl_self_signed:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800436 return ('SSLCertificateChanged', ssl.get_server_certificate(
437 (self._state.host, self._state.port)))
Yilin Yang15a3f8f2020-01-03 17:49:00 +0800438 return ('SSLVerifyFailed', ssl.get_server_certificate(
439 (self._state.host, self._state.port)))
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800440
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800441 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800442 self._state.ssl = tls_enabled
443 UrlOpen(self._state, '%s:%d' % (host, port))
Yilin Yangf54fb912020-01-08 11:42:38 +0800444 except urllib.error.HTTPError as e:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800445 return ('HTTPError', e.getcode(), str(e), e.read().strip())
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800446 except Exception as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800447 return str(e)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800448 else:
449 return True
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800450
451 def Clients(self):
452 if time.time() - self._state.last_list <= _LIST_CACHE_TIMEOUT:
453 return self._state.listing
454
Peter Shihcf0f3b22017-06-19 15:59:22 +0800455 self._state.listing = self._GetJSON('/api/agents/list')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800456 self._state.last_list = time.time()
457 return self._state.listing
458
459 def SelectClient(self, mid):
460 self._state.selected_mid = mid
461
462 def AddForward(self, mid, remote, local, pid):
463 self._state.forwards[local] = (mid, remote, pid)
464
465 def RemoveForward(self, local_port):
466 try:
467 unused_mid, unused_remote, pid = self._state.forwards[local_port]
468 KillGraceful(pid)
469 del self._state.forwards[local_port]
470 except (KeyError, OSError):
471 pass
472
473 def RemoveAllForward(self):
474 for unused_mid, unused_remote, pid in self._state.forwards.values():
475 try:
476 KillGraceful(pid)
477 except OSError:
478 pass
479 self._state.forwards = {}
480
481
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800482class SSLEnabledWebSocketBaseClient(WebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800483 def __init__(self, state, *args, **kwargs):
484 cafile = ssl.get_default_verify_paths().openssl_cafile
485 # For some system / distribution, python can not detect system cafile path.
486 # In such case we fallback to the default path.
487 if not os.path.exists(cafile):
488 cafile = '/etc/ssl/certs/ca-certificates.crt'
489
490 if state.ssl_self_signed:
491 cafile = GetTLSCertPath(state.host)
492
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800493 ssl_options = {
494 'cert_reqs': ssl.CERT_REQUIRED,
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800495 'ca_certs': cafile
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800496 }
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800497 # ws4py does not allow you to specify SSLContext, but rather passing in the
498 # argument of ssl.wrap_socket
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800499 super(SSLEnabledWebSocketBaseClient, self).__init__(
500 ssl_options=ssl_options, *args, **kwargs)
501
502
503class TerminalWebSocketClient(SSLEnabledWebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800504 def __init__(self, state, mid, escape, *args, **kwargs):
505 super(TerminalWebSocketClient, self).__init__(state, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800506 self._mid = mid
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800507 self._escape = escape
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800508 self._stdin_fd = sys.stdin.fileno()
509 self._old_termios = None
510
511 def handshake_ok(self):
512 pass
513
514 def opened(self):
515 nonlocals = {'size': (80, 40)}
516
517 def _ResizeWindow():
518 size = GetTerminalSize()
519 if size != nonlocals['size']: # Size not changed, ignore
520 control = {'command': 'resize', 'params': list(size)}
521 payload = chr(_CONTROL_START) + json.dumps(control) + chr(_CONTROL_END)
522 nonlocals['size'] = size
523 try:
524 self.send(payload, binary=True)
525 except Exception:
526 pass
527
528 def _FeedInput():
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800529 self._old_termios = termios.tcgetattr(self._stdin_fd)
530 tty.setraw(self._stdin_fd)
531
532 READY, ENTER_PRESSED, ESCAPE_PRESSED = range(3)
533
534 try:
535 state = READY
536 while True:
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800537 # Check if terminal is resized
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800538 _ResizeWindow()
539
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800540 ch = sys.stdin.read(1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800541
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800542 # Scan for escape sequence
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800543 if self._escape:
544 if state == READY:
545 state = ENTER_PRESSED if ch == chr(0x0d) else READY
546 elif state == ENTER_PRESSED:
547 state = ESCAPE_PRESSED if ch == self._escape else READY
548 elif state == ESCAPE_PRESSED:
549 if ch == '.':
550 self.close()
551 break
552 else:
553 state = READY
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800554
Wei-Ning Huang85e763d2016-01-21 15:53:18 +0800555 self.send(ch)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800556 except (KeyboardInterrupt, RuntimeError):
557 pass
558
559 t = threading.Thread(target=_FeedInput)
560 t.daemon = True
561 t.start()
562
563 def closed(self, code, reason=None):
Peter Shihf84a8972017-06-19 15:18:24 +0800564 del code, reason # Unused.
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800565 termios.tcsetattr(self._stdin_fd, termios.TCSANOW, self._old_termios)
566 print('Connection to %s closed.' % self._mid)
567
Yilin Yangf64670b2020-01-06 11:22:18 +0800568 def received_message(self, message):
569 if message.is_binary:
Stimim Chena30447c2020-10-06 10:04:00 +0800570 sys.stdout.write(message.data.decode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800571 sys.stdout.flush()
572
573
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800574class ShellWebSocketClient(SSLEnabledWebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800575 def __init__(self, state, output, *args, **kwargs):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800576 """Constructor.
577
578 Args:
579 output: output file object.
580 """
581 self.output = output
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800582 super(ShellWebSocketClient, self).__init__(state, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800583
584 def handshake_ok(self):
585 pass
586
587 def opened(self):
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800588 def _FeedInput():
589 try:
590 while True:
Stimim Chena30447c2020-10-06 10:04:00 +0800591 data = sys.stdin.buffer.read(1)
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800592
Peter Shihf84a8972017-06-19 15:18:24 +0800593 if not data:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800594 self.send(_STDIN_CLOSED * 2)
595 break
596 self.send(data, binary=True)
597 except (KeyboardInterrupt, RuntimeError):
598 pass
599
600 t = threading.Thread(target=_FeedInput)
601 t.daemon = True
602 t.start()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800603
604 def closed(self, code, reason=None):
605 pass
606
Yilin Yangf64670b2020-01-06 11:22:18 +0800607 def received_message(self, message):
608 if message.is_binary:
Stimim Chena30447c2020-10-06 10:04:00 +0800609 self.output.write(message.data.decode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800610 self.output.flush()
611
612
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800613class ForwarderWebSocketClient(SSLEnabledWebSocketBaseClient):
Wei-Ning Huang47c79b82016-05-24 01:24:46 +0800614 def __init__(self, state, sock, *args, **kwargs):
615 super(ForwarderWebSocketClient, self).__init__(state, *args, **kwargs)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800616 self._sock = sock
617 self._stop = threading.Event()
618
619 def handshake_ok(self):
620 pass
621
622 def opened(self):
623 def _FeedInput():
624 try:
625 self._sock.setblocking(False)
626 while True:
627 rd, unused_w, unused_x = select.select([self._sock], [], [], 0.5)
628 if self._stop.is_set():
629 break
630 if self._sock in rd:
631 data = self._sock.recv(_BUFSIZ)
Peter Shihf84a8972017-06-19 15:18:24 +0800632 if not data:
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +0800633 self.close()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800634 break
635 self.send(data, binary=True)
636 except Exception:
637 pass
638 finally:
639 self._sock.close()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800640
641 t = threading.Thread(target=_FeedInput)
642 t.daemon = True
643 t.start()
644
645 def closed(self, code, reason=None):
Peter Shihf84a8972017-06-19 15:18:24 +0800646 del code, reason # Unused.
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800647 self._stop.set()
648 sys.exit(0)
649
Yilin Yangf64670b2020-01-06 11:22:18 +0800650 def received_message(self, message):
651 if message.is_binary:
652 self._sock.send(message.data)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800653
654
655def Arg(*args, **kwargs):
656 return (args, kwargs)
657
658
659def Command(command, help_msg=None, args=None):
660 """Decorator for adding argparse parameter for a method."""
661 if args is None:
662 args = []
663 def WrapFunc(func):
Peter Shih13e78c52018-01-23 12:57:07 +0800664 @functools.wraps(func)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800665 def Wrapped(*args, **kwargs):
666 return func(*args, **kwargs)
Peter Shih99b73ec2017-06-16 17:54:15 +0800667 # pylint: disable=protected-access
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800668 Wrapped.__arg_attr = {'command': command, 'help': help_msg, 'args': args}
669 return Wrapped
670 return WrapFunc
671
672
673def ParseMethodSubCommands(cls):
674 """Decorator for a class using the @Command decorator.
675
676 This decorator retrieve command info from each method and append it in to the
677 SUBCOMMANDS class variable, which is later used to construct parser.
678 """
Yilin Yang879fbda2020-05-14 13:52:30 +0800679 for unused_key, method in cls.__dict__.items():
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800680 if hasattr(method, '__arg_attr'):
Peter Shih99b73ec2017-06-16 17:54:15 +0800681 # pylint: disable=protected-access
682 cls.SUBCOMMANDS.append(method.__arg_attr)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800683 return cls
684
685
686@ParseMethodSubCommands
Fei Shaobd07c9a2020-06-15 19:04:50 +0800687class OverlordCLIClient:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800688 """Overlord command line interface client."""
689
690 SUBCOMMANDS = []
691
692 def __init__(self):
693 self._parser = self._BuildParser()
694 self._selected_mid = None
695 self._server = None
696 self._state = None
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800697 self._escape = None
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800698
699 def _BuildParser(self):
700 root_parser = argparse.ArgumentParser(prog='ovl')
701 subparsers = root_parser.add_subparsers(help='sub-command')
702
703 root_parser.add_argument('-s', dest='selected_mid', action='store',
704 default=None,
705 help='select target to execute command on')
706 root_parser.add_argument('-S', dest='select_mid_before_action',
707 action='store_true', default=False,
708 help='select target before executing command')
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800709 root_parser.add_argument('-e', dest='escape', metavar='ESCAPE_CHAR',
710 action='store', default=_ESCAPE, type=str,
711 help='set shell escape character, \'none\' to '
712 'disable escape completely')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800713
714 for attr in self.SUBCOMMANDS:
715 parser = subparsers.add_parser(attr['command'], help=attr['help'])
716 parser.set_defaults(which=attr['command'])
717 for arg in attr['args']:
718 parser.add_argument(*arg[0], **arg[1])
719
720 return root_parser
721
722 def Main(self):
723 # We want to pass the rest of arguments after shell command directly to the
724 # function without parsing it.
725 try:
726 index = sys.argv.index('shell')
727 except ValueError:
728 args = self._parser.parse_args()
729 else:
730 args = self._parser.parse_args(sys.argv[1:index + 1])
731
732 command = args.which
733 self._selected_mid = args.selected_mid
734
Wei-Ning Huang0c520e92016-03-19 20:01:10 +0800735 if args.escape and args.escape != 'none':
736 self._escape = args.escape[0]
737
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800738 if command == 'start-server':
739 self.StartServer()
740 return
Fei Shao12ecf382020-06-23 18:32:26 +0800741 if command == 'kill-server':
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800742 self.KillServer()
743 return
744
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800745 self.CheckDaemon()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800746 if command == 'status':
747 self.Status()
748 return
Fei Shao12ecf382020-06-23 18:32:26 +0800749 if command == 'connect':
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800750 self.Connect(args)
751 return
752
753 # The following command requires connection to the server
754 self.CheckConnection()
755
756 if args.select_mid_before_action:
757 self.SelectClient(store=False)
758
759 if command == 'select':
760 self.SelectClient(args)
761 elif command == 'ls':
Peter Shih99b73ec2017-06-16 17:54:15 +0800762 self.ListClients(args)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800763 elif command == 'shell':
764 command = sys.argv[sys.argv.index('shell') + 1:]
765 self.Shell(command)
766 elif command == 'push':
767 self.Push(args)
768 elif command == 'pull':
769 self.Pull(args)
770 elif command == 'forward':
771 self.Forward(args)
772
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +0800773 def _SaveTLSCertificate(self, host, cert_pem):
774 try:
775 os.makedirs(_CERT_DIR)
776 except Exception:
777 pass
778 with open(GetTLSCertPath(host), 'w') as f:
779 f.write(cert_pem)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800780
781 def _HTTPPostFile(self, url, filename, progress=None, user=None, passwd=None):
782 """Perform HTTP POST and upload file to Overlord.
783
784 To minimize the external dependencies, we construct the HTTP post request
785 by ourselves.
786 """
787 url = MakeRequestUrl(self._state, url)
788 size = os.stat(filename).st_size
789 boundary = '-----------%s' % _HTTP_BOUNDARY_MAGIC
790 CRLF = '\r\n'
Yilin Yangf54fb912020-01-08 11:42:38 +0800791 parse = urllib.parse.urlparse(url)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800792
793 part_headers = [
794 '--' + boundary,
795 'Content-Disposition: form-data; name="file"; '
796 'filename="%s"' % os.path.basename(filename),
797 'Content-Type: application/octet-stream',
798 '', ''
799 ]
800 part_header = CRLF.join(part_headers)
801 end_part = CRLF + '--' + boundary + '--' + CRLF
802
803 content_length = len(part_header) + size + len(end_part)
804 if parse.scheme == 'http':
Stimim Chena30447c2020-10-06 10:04:00 +0800805 h = http.client.HTTPConnection(parse.netloc)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800806 else:
Stimim Chena30447c2020-10-06 10:04:00 +0800807 h = http.client.HTTPSConnection(parse.netloc,
808 context=self._state.ssl_context)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800809
810 post_path = url[url.index(parse.netloc) + len(parse.netloc):]
811 h.putrequest('POST', post_path)
812 h.putheader('Content-Length', content_length)
813 h.putheader('Content-Type', 'multipart/form-data; boundary=%s' % boundary)
814
815 if user and passwd:
816 h.putheader(*BasicAuthHeader(user, passwd))
817 h.endheaders()
Stimim Chena30447c2020-10-06 10:04:00 +0800818 h.send(part_header.encode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800819
820 count = 0
Stimim Chena30447c2020-10-06 10:04:00 +0800821 with open(filename, 'rb') as f:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800822 while True:
823 data = f.read(_BUFSIZ)
824 if not data:
825 break
826 count += len(data)
827 if progress:
Yilin Yang14d02a22019-11-01 11:32:03 +0800828 progress(count * 100 // size, count)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800829 h.send(data)
830
Stimim Chena30447c2020-10-06 10:04:00 +0800831 h.send(end_part.encode('utf-8'))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800832 progress(100)
833
834 if count != size:
835 logging.warning('file changed during upload, upload may be truncated.')
836
Stimim Chena30447c2020-10-06 10:04:00 +0800837 resp = h.getresponse()
838 return resp.status == 200
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800839
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800840 def CheckDaemon(self):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800841 self._server = OverlordClientDaemon.GetRPCServer()
842 if self._server is None:
843 print('* daemon not running, starting it now on port %d ... *' %
844 _OVERLORD_CLIENT_DAEMON_PORT)
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800845 self.StartServer()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800846
847 self._state = self._server.State()
848 sha1sum = GetVersionDigest()
849
850 if sha1sum != self._state.version_sha1sum:
851 print('ovl server is out of date. killing...')
852 KillGraceful(self._server.GetPid())
Wei-Ning Huang5564eea2016-01-19 14:36:45 +0800853 self.StartServer()
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800854
855 def GetSSHControlFile(self, host):
856 return _SSH_CONTROL_SOCKET_PREFIX + host
857
858 def SSHTunnel(self, user, host, port):
859 """SSH forward the remote overlord server.
860
861 Overlord server may not have port 9000 open to the public network, in such
862 case we can SSH forward the port to localhost.
863 """
864
865 control_file = self.GetSSHControlFile(host)
866 try:
867 os.unlink(control_file)
868 except Exception:
869 pass
870
871 subprocess.Popen([
872 'ssh', '-Nf',
873 '-M', # Enable master mode
874 '-S', control_file,
875 '-L', '9000:localhost:9000',
876 '-p', str(port),
877 '%s%s' % (user + '@' if user else '', host)
878 ]).wait()
879
Yilin Yang83c8f442020-05-05 13:46:51 +0800880 p = process_utils.Spawn([
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800881 'ssh',
882 '-S', control_file,
883 '-O', 'check', host,
Yilin Yang83c8f442020-05-05 13:46:51 +0800884 ], read_stderr=True, ignore_stdout=True)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800885
Yilin Yang83c8f442020-05-05 13:46:51 +0800886 s = re.search(r'pid=(\d+)', p.stderr_data)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800887 if s:
888 return int(s.group(1))
889
890 raise RuntimeError('can not establish ssh connection')
891
892 def CheckConnection(self):
893 if self._state.host is None:
894 raise RuntimeError('not connected to any server, abort')
895
896 try:
897 self._server.Clients()
898 except Exception:
899 raise RuntimeError('remote server disconnected, abort')
900
901 if self._state.ssh_pid is not None:
902 ret = subprocess.Popen(['kill', '-0', str(self._state.ssh_pid)],
903 stdout=subprocess.PIPE,
904 stderr=subprocess.PIPE).wait()
905 if ret != 0:
906 raise RuntimeError('ssh tunnel disconnected, please re-connect')
907
908 def CheckClient(self):
909 if self._selected_mid is None:
910 if self._state.selected_mid is None:
911 raise RuntimeError('No client is selected')
912 self._selected_mid = self._state.selected_mid
913
Peter Shihcf0f3b22017-06-19 15:59:22 +0800914 if not any(client['mid'] == self._selected_mid
915 for client in self._server.Clients()):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800916 raise RuntimeError('client %s disappeared' % self._selected_mid)
917
918 def CheckOutput(self, command):
919 headers = []
920 if self._state.username is not None and self._state.password is not None:
921 headers.append(BasicAuthHeader(self._state.username,
922 self._state.password))
923
924 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
Yilin Yang8d4f9d02019-11-28 17:12:11 +0800925 sio = StringIO()
Peter Shihe03450b2017-06-14 14:02:08 +0800926 ws = ShellWebSocketClient(
927 self._state, sio, scheme + '%s:%d/api/agent/shell/%s?command=%s' % (
928 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +0800929 urllib.parse.quote(self._selected_mid),
930 urllib.parse.quote(command)),
Peter Shihe03450b2017-06-14 14:02:08 +0800931 headers=headers)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800932 ws.connect()
933 ws.run()
934 return sio.getvalue()
935
936 @Command('status', 'show Overlord connection status')
937 def Status(self):
938 if self._state.host is None:
939 print('Not connected to any host.')
940 else:
941 if self._state.ssh_pid is not None:
942 print('Connected to %s with SSH tunneling.' % self._state.orig_host)
943 else:
944 print('Connected to %s:%d.' % (self._state.host, self._state.port))
945
946 if self._selected_mid is None:
947 self._selected_mid = self._state.selected_mid
948
949 if self._selected_mid is None:
950 print('No client is selected.')
951 else:
952 print('Client %s selected.' % self._selected_mid)
953
954 @Command('connect', 'connect to Overlord server', [
955 Arg('host', metavar='HOST', type=str, default='localhost',
956 help='Overlord hostname/IP'),
957 Arg('port', metavar='PORT', type=int,
958 default=_OVERLORD_HTTP_PORT, help='Overlord port'),
959 Arg('-f', '--forward', dest='ssh_forward', default=False,
960 action='store_true',
961 help='connect with SSH forwarding to the host'),
962 Arg('-p', '--ssh-port', dest='ssh_port', default=22,
963 type=int, help='SSH server port for SSH forwarding'),
964 Arg('-l', '--ssh-login', dest='ssh_login', default='',
965 type=str, help='SSH server login name for SSH forwarding'),
966 Arg('-u', '--user', dest='user', default=None,
967 type=str, help='Overlord HTTP auth username'),
968 Arg('-w', '--passwd', dest='passwd', default=None, type=str,
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800969 help='Overlord HTTP auth password'),
970 Arg('-i', '--no-check-hostname', dest='check_hostname',
971 default=True, action='store_false',
972 help='Ignore SSL cert hostname check')])
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +0800973 def Connect(self, args):
974 ssh_pid = None
975 host = args.host
976 orig_host = args.host
977
978 if args.ssh_forward:
979 # Kill previous SSH tunnel
980 self.KillSSHTunnel()
981
982 ssh_pid = self.SSHTunnel(args.ssh_login, args.host, args.ssh_port)
983 host = 'localhost'
984
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800985 username_provided = args.user is not None
986 password_provided = args.passwd is not None
987 prompt = False
988
Peter Shih533566a2018-09-05 17:48:03 +0800989 for unused_i in range(3): # pylint: disable=too-many-nested-blocks
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800990 try:
991 if prompt:
992 if not username_provided:
Yilin Yang8cc5dfb2019-10-22 15:58:53 +0800993 args.user = input('Username: ')
Wei-Ning Huangba768ab2016-02-07 14:38:06 +0800994 if not password_provided:
995 args.passwd = getpass.getpass('Password: ')
996
997 ret = self._server.Connect(host, args.port, ssh_pid, args.user,
Wei-Ning Huang4f8f6092017-06-09 19:35:35 +0800998 args.passwd, orig_host,
999 args.check_hostname)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001000 if isinstance(ret, list):
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001001 if ret[0].startswith('SSL'):
1002 cert_pem = ret[1]
1003 fp = GetTLSCertificateSHA1Fingerprint(cert_pem)
1004 fp_text = ':'.join([fp[i:i+2] for i in range(0, len(fp), 2)])
1005
1006 if ret[0] == 'SSLCertificateChanged':
1007 print(_TLS_CERT_CHANGED_WARNING % (fp_text, GetTLSCertPath(host)))
1008 return
Fei Shao12ecf382020-06-23 18:32:26 +08001009 if ret[0] == 'SSLVerifyFailed':
Wei-Ning Huang47c79b82016-05-24 01:24:46 +08001010 print(_TLS_CERT_FAILED_WARNING % (fp_text), end='')
Yilin Yang8cc5dfb2019-10-22 15:58:53 +08001011 response = input()
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001012 if response.lower() in ['y', 'ye', 'yes']:
1013 self._SaveTLSCertificate(host, cert_pem)
Wei-Ning Huang47c79b82016-05-24 01:24:46 +08001014 print('TLS host Certificate trusted, you will not be prompted '
1015 'next time.\n')
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001016 continue
Fei Shaof91ab8f2020-06-23 17:54:03 +08001017 print('connection aborted.')
1018 return
Fei Shao12ecf382020-06-23 18:32:26 +08001019 if ret[0] == 'HTTPError':
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001020 code, except_str, body = ret[1:]
1021 if code == 401:
1022 print('connect: %s' % body)
1023 prompt = True
1024 if not username_provided or not password_provided:
1025 continue
Fei Shaof91ab8f2020-06-23 17:54:03 +08001026 break
Fei Shao0e4e2c62020-06-23 18:22:26 +08001027 logging.error('%s; %s', except_str, body)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001028
1029 if ret is not True:
1030 print('can not connect to %s: %s' % (host, ret))
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001031 else:
1032 print('connection to %s:%d established.' % (host, args.port))
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001033 except Exception as e:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001034 logging.error(e)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001035 else:
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001036 break
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001037
Wei-Ning Huang5564eea2016-01-19 14:36:45 +08001038 @Command('start-server', 'start overlord CLI client server')
1039 def StartServer(self):
1040 self._server = OverlordClientDaemon.GetRPCServer()
1041 if self._server is None:
1042 OverlordClientDaemon().Start()
1043 time.sleep(1)
1044 self._server = OverlordClientDaemon.GetRPCServer()
1045 if self._server is not None:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001046 print('* daemon started successfully *\n')
Wei-Ning Huang5564eea2016-01-19 14:36:45 +08001047
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001048 @Command('kill-server', 'kill overlord CLI client server')
1049 def KillServer(self):
1050 self._server = OverlordClientDaemon.GetRPCServer()
1051 if self._server is None:
1052 return
1053
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001054 self._state = self._server.State()
1055
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001056 # Kill SSH Tunnel
1057 self.KillSSHTunnel()
1058
1059 # Kill server daemon
1060 KillGraceful(self._server.GetPid())
1061
1062 def KillSSHTunnel(self):
1063 if self._state.ssh_pid is not None:
1064 KillGraceful(self._state.ssh_pid)
1065
Peter Shihcf0f3b22017-06-19 15:59:22 +08001066 def _FilterClients(self, clients, prop_filters, mid=None):
1067 def _ClientPropertiesMatch(client, key, regex):
1068 try:
1069 return bool(re.search(regex, client['properties'][key]))
1070 except KeyError:
1071 return False
1072
1073 for prop_filter in prop_filters:
1074 key, sep, regex = prop_filter.partition('=')
1075 if not sep:
1076 # The filter doesn't contains =.
1077 raise ValueError('Invalid filter condition %r' % filter)
1078 clients = [c for c in clients if _ClientPropertiesMatch(c, key, regex)]
1079
1080 if mid is not None:
1081 client = next((c for c in clients if c['mid'] == mid), None)
1082 if client:
1083 return [client]
1084 clients = [c for c in clients if c['mid'].startswith(mid)]
1085 return clients
1086
1087 @Command('ls', 'list clients', [
1088 Arg('-f', '--filter', default=[], dest='filters', action='append',
1089 help=('Conditions to filter clients by properties. '
1090 'Should be in form "key=regex", where regex is the regular '
1091 'expression that should be found in the value. '
1092 'Multiple --filter arguments would be ANDed.')),
Peter Shih99b73ec2017-06-16 17:54:15 +08001093 Arg('-v', '--verbose', default=False, action='store_true',
1094 help='Print properties of each client.')
1095 ])
1096 def ListClients(self, args):
Peter Shihcf0f3b22017-06-19 15:59:22 +08001097 clients = self._FilterClients(self._server.Clients(), args.filters)
1098 for client in clients:
1099 if args.verbose:
Peter Shih99b73ec2017-06-16 17:54:15 +08001100 print(yaml.safe_dump(client, default_flow_style=False))
Peter Shihcf0f3b22017-06-19 15:59:22 +08001101 else:
1102 print(client['mid'])
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001103
1104 @Command('select', 'select default client', [
Peter Shihcf0f3b22017-06-19 15:59:22 +08001105 Arg('-f', '--filter', default=[], dest='filters', action='append',
1106 help=('Conditions to filter clients by properties. '
1107 'Should be in form "key=regex", where regex is the regular '
1108 'expression that should be found in the value. '
1109 'Multiple --filter arguments would be ANDed.')),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001110 Arg('mid', metavar='mid', nargs='?', default=None)])
1111 def SelectClient(self, args=None, store=True):
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001112 mid = args.mid if args is not None else None
Wei-Ning Huang6796a132017-06-22 14:46:32 +08001113 filters = args.filters if args is not None else []
1114 clients = self._FilterClients(self._server.Clients(), filters, mid=mid)
Peter Shihcf0f3b22017-06-19 15:59:22 +08001115
1116 if not clients:
1117 raise RuntimeError('select: client not found')
Fei Shao0e4e2c62020-06-23 18:22:26 +08001118 if len(clients) == 1:
Peter Shihcf0f3b22017-06-19 15:59:22 +08001119 mid = clients[0]['mid']
1120 else:
1121 # This case would not happen when args.mid is specified.
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001122 print('Select from the following clients:')
1123 for i, client in enumerate(clients):
Peter Shihcf0f3b22017-06-19 15:59:22 +08001124 print(' %d. %s' % (i + 1, client['mid']))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001125
1126 print('\nSelection: ', end='')
1127 try:
Yilin Yang8cc5dfb2019-10-22 15:58:53 +08001128 choice = int(input()) - 1
Peter Shihcf0f3b22017-06-19 15:59:22 +08001129 mid = clients[choice]['mid']
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001130 except ValueError:
1131 raise RuntimeError('select: invalid selection')
1132 except IndexError:
1133 raise RuntimeError('select: selection out of range')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001134
1135 self._selected_mid = mid
1136 if store:
1137 self._server.SelectClient(mid)
1138 print('Client %s selected' % mid)
1139
1140 @Command('shell', 'open a shell or execute a shell command', [
1141 Arg('command', metavar='CMD', nargs='?', help='command to execute')])
1142 def Shell(self, command=None):
1143 if command is None:
1144 command = []
1145 self.CheckClient()
1146
1147 headers = []
1148 if self._state.username is not None and self._state.password is not None:
1149 headers.append(BasicAuthHeader(self._state.username,
1150 self._state.password))
1151
1152 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
Peter Shihe03450b2017-06-14 14:02:08 +08001153 if command:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001154 cmd = ' '.join(command)
Peter Shihe03450b2017-06-14 14:02:08 +08001155 ws = ShellWebSocketClient(
1156 self._state, sys.stdout,
1157 scheme + '%s:%d/api/agent/shell/%s?command=%s' % (
1158 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001159 urllib.parse.quote(self._selected_mid), urllib.parse.quote(cmd)),
Peter Shihe03450b2017-06-14 14:02:08 +08001160 headers=headers)
1161 else:
1162 ws = TerminalWebSocketClient(
1163 self._state, self._selected_mid, self._escape,
1164 scheme + '%s:%d/api/agent/tty/%s' % (
1165 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001166 urllib.parse.quote(self._selected_mid)),
Peter Shihe03450b2017-06-14 14:02:08 +08001167 headers=headers)
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001168 try:
1169 ws.connect()
1170 ws.run()
1171 except socket.error as e:
1172 if e.errno == 32: # Broken pipe
1173 pass
1174 else:
1175 raise
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001176
1177 @Command('push', 'push a file or directory to remote', [
Wei-Ning Huang37e61102016-02-07 13:41:07 +08001178 Arg('srcs', nargs='+', metavar='SOURCE'),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001179 Arg('dst', metavar='DESTINATION')])
1180 def Push(self, args):
1181 self.CheckClient()
1182
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001183 @AutoRetry('push', _RETRY_TIMES)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001184 def _push(src, dst):
1185 src_base = os.path.basename(src)
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001186
1187 # Local file is a link
1188 if os.path.islink(src):
1189 pbar = ProgressBar(src_base)
1190 link_path = os.readlink(src)
1191 self.CheckOutput('mkdir -p %(dirname)s; '
1192 'if [ -d "%(dst)s" ]; then '
1193 'ln -sf "%(link_path)s" "%(dst)s/%(link_name)s"; '
1194 'else ln -sf "%(link_path)s" "%(dst)s"; fi' %
1195 dict(dirname=os.path.dirname(dst),
1196 link_path=link_path, dst=dst,
1197 link_name=src_base))
1198 pbar.End()
1199 return
1200
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001201 mode = '0%o' % (0x1FF & os.stat(src).st_mode)
1202 url = ('%s:%d/api/agent/upload/%s?dest=%s&perm=%s' %
Peter Shihe03450b2017-06-14 14:02:08 +08001203 (self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001204 urllib.parse.quote(self._selected_mid), dst, mode))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001205 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001206 UrlOpen(self._state, url + '&filename=%s' % src_base)
Yilin Yangf54fb912020-01-08 11:42:38 +08001207 except urllib.error.HTTPError as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001208 msg = json.loads(e.read()).get('error', None)
1209 raise RuntimeError('push: %s' % msg)
1210
1211 pbar = ProgressBar(src_base)
1212 self._HTTPPostFile(url, src, pbar.SetProgress,
1213 self._state.username, self._state.password)
1214 pbar.End()
1215
Wei-Ning Huang37e61102016-02-07 13:41:07 +08001216 def _push_single_target(src, dst):
1217 if os.path.isdir(src):
1218 dst_exists = ast.literal_eval(self.CheckOutput(
1219 'stat %s >/dev/null 2>&1 && echo True || echo False' % dst))
1220 for root, unused_x, files in os.walk(src):
1221 # If destination directory does not exist, we should strip the first
1222 # layer of directory. For example: src_dir contains a single file 'A'
1223 #
1224 # push src_dir dest_dir
1225 #
1226 # If dest_dir exists, the resulting directory structure should be:
1227 # dest_dir/src_dir/A
1228 # If dest_dir does not exist, the resulting directory structure should
1229 # be:
1230 # dest_dir/A
1231 dst_root = root if dst_exists else root[len(src):].lstrip('/')
1232 for name in files:
1233 _push(os.path.join(root, name),
1234 os.path.join(dst, dst_root, name))
1235 else:
1236 _push(src, dst)
1237
1238 if len(args.srcs) > 1:
1239 dst_type = self.CheckOutput('stat \'%s\' --printf \'%%F\' '
1240 '2>/dev/null' % args.dst).strip()
1241 if not dst_type:
1242 raise RuntimeError('push: %s: No such file or directory' % args.dst)
1243 if dst_type != 'directory':
1244 raise RuntimeError('push: %s: Not a directory' % args.dst)
1245
1246 for src in args.srcs:
1247 if not os.path.exists(src):
1248 raise RuntimeError('push: can not stat "%s": no such file or directory'
1249 % src)
1250 if not os.access(src, os.R_OK):
1251 raise RuntimeError('push: can not open "%s" for reading' % src)
1252
1253 _push_single_target(src, args.dst)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001254
1255 @Command('pull', 'pull a file or directory from remote', [
1256 Arg('src', metavar='SOURCE'),
1257 Arg('dst', metavar='DESTINATION', default='.', nargs='?')])
1258 def Pull(self, args):
1259 self.CheckClient()
1260
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001261 @AutoRetry('pull', _RETRY_TIMES)
Peter Shihe6afab32018-09-11 17:16:48 +08001262 def _pull(src, dst, ftype, perm=0o644, link=None):
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001263 try:
1264 os.makedirs(os.path.dirname(dst))
1265 except Exception:
1266 pass
1267
1268 src_base = os.path.basename(src)
1269
1270 # Remote file is a link
1271 if ftype == 'l':
1272 pbar = ProgressBar(src_base)
1273 if os.path.exists(dst):
1274 os.remove(dst)
1275 os.symlink(link, dst)
1276 pbar.End()
1277 return
1278
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001279 url = ('%s:%d/api/agent/download/%s?filename=%s' %
Peter Shihe03450b2017-06-14 14:02:08 +08001280 (self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001281 urllib.parse.quote(self._selected_mid), urllib.parse.quote(src)))
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001282 try:
Wei-Ning Huangd3a57dc2016-02-09 16:31:04 +08001283 h = UrlOpen(self._state, url)
Yilin Yangf54fb912020-01-08 11:42:38 +08001284 except urllib.error.HTTPError as e:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001285 msg = json.loads(e.read()).get('error', 'unkown error')
1286 raise RuntimeError('pull: %s' % msg)
1287 except KeyboardInterrupt:
1288 return
1289
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001290 pbar = ProgressBar(src_base)
Stimim Chena30447c2020-10-06 10:04:00 +08001291 with open(dst, 'wb') as f:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001292 os.fchmod(f.fileno(), perm)
1293 total_size = int(h.headers.get('Content-Length'))
1294 downloaded_size = 0
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001295
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001296 while True:
1297 data = h.read(_BUFSIZ)
Peter Shihf84a8972017-06-19 15:18:24 +08001298 if not data:
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001299 break
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001300 downloaded_size += len(data)
Yilin Yang14d02a22019-11-01 11:32:03 +08001301 pbar.SetProgress(downloaded_size * 100 / total_size,
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001302 downloaded_size)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001303 f.write(data)
1304 pbar.End()
1305
1306 # Use find to get a listing of all files under a root directory. The 'stat'
1307 # command is used to retrieve the filename and it's filemode.
1308 output = self.CheckOutput(
1309 'cd $HOME; '
1310 'stat "%(src)s" >/dev/null && '
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001311 'find "%(src)s" \'(\' -type f -o -type l \')\' '
1312 '-printf \'%%m\t%%p\t%%y\t%%l\n\''
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001313 % {'src': args.src})
1314
1315 # We got error from the stat command
1316 if output.startswith('stat: '):
1317 sys.stderr.write(output)
1318 return
1319
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001320 entries = output.strip('\n').split('\n')
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001321 common_prefix = os.path.dirname(args.src)
1322
1323 if len(entries) == 1:
1324 entry = entries[0]
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001325 perm, src_path, ftype, link = entry.split('\t', -1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001326 if os.path.isdir(args.dst):
1327 dst = os.path.join(args.dst, os.path.basename(src_path))
1328 else:
1329 dst = args.dst
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001330 _pull(src_path, dst, ftype, int(perm, base=8), link)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001331 else:
1332 if not os.path.exists(args.dst):
1333 common_prefix = args.src
1334
1335 for entry in entries:
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001336 perm, src_path, ftype, link = entry.split('\t', -1)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001337 rel_dst = src_path[len(common_prefix):].lstrip('/')
Wei-Ning Huangee7ca8d2015-12-12 05:48:02 +08001338 _pull(src_path, os.path.join(args.dst, rel_dst), ftype,
1339 int(perm, base=8), link)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001340
1341 @Command('forward', 'forward remote port to local port', [
1342 Arg('--list', dest='list_all', action='store_true', default=False,
1343 help='list all port forwarding sessions'),
1344 Arg('--remove', metavar='LOCAL_PORT', dest='remove', type=int,
1345 default=None,
1346 help='remove port forwarding for local port LOCAL_PORT'),
1347 Arg('--remove-all', dest='remove_all', action='store_true',
1348 default=False, help='remove all port forwarding'),
1349 Arg('remote', metavar='REMOTE_PORT', type=int, nargs='?'),
1350 Arg('local', metavar='LOCAL_PORT', type=int, nargs='?')])
1351 def Forward(self, args):
1352 if args.list_all:
1353 max_len = 10
Peter Shihf84a8972017-06-19 15:18:24 +08001354 if self._state.forwards:
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001355 max_len = max([len(v[0]) for v in self._state.forwards.values()])
1356
1357 print('%-*s %-8s %-8s' % (max_len, 'Client', 'Remote', 'Local'))
1358 for local in sorted(self._state.forwards.keys()):
1359 value = self._state.forwards[local]
1360 print('%-*s %-8s %-8s' % (max_len, value[0], value[1], local))
1361 return
1362
1363 if args.remove_all:
1364 self._server.RemoveAllForward()
1365 return
1366
1367 if args.remove:
1368 self._server.RemoveForward(args.remove)
1369 return
1370
1371 self.CheckClient()
1372
Wei-Ning Huang9083b7c2016-01-26 16:44:11 +08001373 if args.remote is None:
1374 raise RuntimeError('remote port not specified')
1375
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001376 if args.local is None:
1377 args.local = args.remote
1378 remote = int(args.remote)
1379 local = int(args.local)
1380
1381 def HandleConnection(conn):
1382 headers = []
1383 if self._state.username is not None and self._state.password is not None:
1384 headers.append(BasicAuthHeader(self._state.username,
1385 self._state.password))
1386
1387 scheme = 'ws%s://' % ('s' if self._state.ssl else '')
1388 ws = ForwarderWebSocketClient(
Wei-Ning Huang47c79b82016-05-24 01:24:46 +08001389 self._state, conn,
Peter Shihe03450b2017-06-14 14:02:08 +08001390 scheme + '%s:%d/api/agent/forward/%s?port=%d' % (
1391 self._state.host, self._state.port,
Yilin Yangf54fb912020-01-08 11:42:38 +08001392 urllib.parse.quote(self._selected_mid), remote),
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001393 headers=headers)
1394 try:
1395 ws.connect()
1396 ws.run()
1397 except Exception as e:
1398 print('error: %s' % e)
1399 finally:
1400 ws.close()
1401
1402 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1403 server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1404 server.bind(('0.0.0.0', local))
1405 server.listen(5)
1406
1407 pid = os.fork()
1408 if pid == 0:
1409 while True:
1410 conn, unused_addr = server.accept()
1411 t = threading.Thread(target=HandleConnection, args=(conn,))
1412 t.daemon = True
1413 t.start()
1414 else:
1415 self._server.AddForward(self._selected_mid, remote, local, pid)
1416
1417
1418def main():
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001419 # Setup logging format
1420 logger = logging.getLogger()
Stimim Chena30447c2020-10-06 10:04:00 +08001421 logger.setLevel(logging.DEBUG)
Wei-Ning Huangba768ab2016-02-07 14:38:06 +08001422 handler = logging.StreamHandler()
1423 formatter = logging.Formatter('%(asctime)s %(message)s', '%Y/%m/%d %H:%M:%S')
1424 handler.setFormatter(formatter)
1425 logger.addHandler(handler)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001426
1427 # Add DaemonState to JSONRPC lib classes
Stimim Chena30447c2020-10-06 10:04:00 +08001428 config.DEFAULT.classes.add(DaemonState)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001429
1430 ovl = OverlordCLIClient()
1431 try:
1432 ovl.Main()
1433 except KeyboardInterrupt:
1434 print('Ctrl-C received, abort')
1435 except Exception as e:
Stimim Chena30447c2020-10-06 10:04:00 +08001436 logging.exception('exit with error [%s]', e)
Wei-Ning Huang91aaeed2015-09-24 14:51:56 +08001437
1438
1439if __name__ == '__main__':
1440 main()