OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 # Virtual Me2Me implementation. This script runs and manages the processes | 6 # Virtual Me2Me implementation. This script runs and manages the processes |
7 # required for a Virtual Me2Me desktop, which are: X server, X desktop | 7 # required for a Virtual Me2Me desktop, which are: X server, X desktop |
8 # session, and Host process. | 8 # session, and Host process. |
9 # This script is intended to run continuously as a background daemon | 9 # This script is intended to run continuously as a background daemon |
10 # process, running under an ordinary (non-root) user account. | 10 # process, running under an ordinary (non-root) user account. |
(...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
67 FIRST_X_DISPLAY_NUMBER = 20 | 67 FIRST_X_DISPLAY_NUMBER = 20 |
68 | 68 |
69 X_AUTH_FILE = os.path.expanduser("~/.Xauthority") | 69 X_AUTH_FILE = os.path.expanduser("~/.Xauthority") |
70 os.environ["XAUTHORITY"] = X_AUTH_FILE | 70 os.environ["XAUTHORITY"] = X_AUTH_FILE |
71 | 71 |
72 | 72 |
73 # Globals needed by the atexit cleanup() handler. | 73 # Globals needed by the atexit cleanup() handler. |
74 g_desktops = [] | 74 g_desktops = [] |
75 g_pidfile = None | 75 g_pidfile = None |
76 | 76 |
77 class Config: | |
78 def __init__(self, path): | |
79 self.path = path | |
80 self.data = {} | |
81 self.changed = False | |
82 | |
83 def load(self): | |
84 try: | |
85 settings_file = open(self.path, 'r') | |
86 self.data = json.load(settings_file) | |
87 self.changed = False | |
88 settings_file.close() | |
89 except: | |
90 return False | |
91 return True | |
92 | |
93 def save(self): | |
94 if not self.changed: | |
95 return True | |
96 try: | |
97 old_umask = os.umask(0066) | |
98 settings_file = open(self.path, 'w') | |
99 settings_file.write(json.dumps(self.data, indent=2)) | |
100 settings_file.close() | |
101 os.umask(old_umask) | |
102 except: | |
103 return False | |
104 self.changed = False | |
105 return True | |
106 | |
107 def copy_from(self, config): | |
108 """ Copy all parameters from |config|. | |
109 Args: | |
110 config: Config object to copy from | |
111 """ | |
112 self.data.update(config.data) | |
113 self.changed = True | |
114 | |
115 def get(self, key): | |
116 return self.data.get(key) | |
117 | |
118 def __getitem__(self, key): | |
119 return self.data[key] | |
120 | |
121 def __setitem__(self, key, value): | |
122 self.data[key] = value | |
123 self.changed = True | |
124 | |
125 def __delitem__(self, key): | |
126 del self.data[key] | |
127 self.changed = True | |
77 | 128 |
78 class Authentication: | 129 class Authentication: |
79 """Manage authentication tokens for Chromoting/xmpp""" | 130 """Manage authentication tokens for Chromoting/xmpp""" |
80 | 131 |
81 def __init__(self, config_file): | 132 def __init__(self, config): |
82 self.config_file = config_file | 133 self.config = config |
83 | 134 |
84 def generate_tokens(self): | 135 def generate_tokens(self): |
85 """Prompt for username/password and use them to generate new authentication | 136 """Prompt for username/password and use them to generate new authentication |
86 tokens. | 137 tokens. |
87 | 138 |
88 Raises: | 139 Raises: |
89 Exception: Failed to get new authentication tokens. | 140 Exception: Failed to get new authentication tokens. |
90 """ | 141 """ |
91 print "Email:", | 142 print "Email:", |
92 self.login = raw_input() | 143 self.login = raw_input() |
93 password = getpass.getpass("App-specific password: ") | 144 password = getpass.getpass("App-specific password: ") |
94 | 145 |
95 chromoting_auth = gaia_auth.GaiaAuthenticator('chromoting') | 146 chromoting_auth = gaia_auth.GaiaAuthenticator('chromoting') |
96 self.chromoting_auth_token = chromoting_auth.authenticate(self.login, | 147 self.chromoting_auth_token = chromoting_auth.authenticate(self.login, |
97 password) | 148 password) |
98 | 149 |
99 xmpp_authenticator = gaia_auth.GaiaAuthenticator('chromiumsync') | 150 xmpp_authenticator = gaia_auth.GaiaAuthenticator('chromiumsync') |
100 self.xmpp_auth_token = xmpp_authenticator.authenticate(self.login, | 151 self.xmpp_auth_token = xmpp_authenticator.authenticate(self.login, |
101 password) | 152 password) |
102 | 153 |
103 def load_config(self): | 154 def load_config(self): |
104 try: | 155 try: |
105 settings_file = open(self.config_file, 'r') | 156 self.login = self.config["xmpp_login"] |
106 data = json.load(settings_file) | 157 self.chromoting_auth_token = self.config["chromoting_auth_token"] |
107 settings_file.close() | 158 self.xmpp_auth_token = self.config["xmpp_auth_token"] |
108 self.login = data["xmpp_login"] | 159 except KeyError: |
109 self.chromoting_auth_token = data["chromoting_auth_token"] | |
110 self.xmpp_auth_token = data["xmpp_auth_token"] | |
111 except: | |
112 return False | 160 return False |
113 return True | 161 return True |
114 | 162 |
115 def save_config(self): | 163 def save_config(self): |
116 data = { | 164 self.config["xmpp_login"] = self.login |
117 "xmpp_login": self.login, | 165 self.config["chromoting_auth_token"] = self.chromoting_auth_token |
118 "chromoting_auth_token": self.chromoting_auth_token, | 166 self.config["xmpp_auth_token"] = self.xmpp_auth_token |
119 "xmpp_auth_token": self.xmpp_auth_token, | |
120 } | |
121 # File will contain private keys, so deny read/write access to others. | |
122 old_umask = os.umask(0066) | |
123 settings_file = open(self.config_file, 'w') | |
124 settings_file.write(json.dumps(data, indent=2)) | |
125 settings_file.close() | |
126 os.umask(old_umask) | |
127 | 167 |
168 def clear_config(self): | |
169 del self.config["xmpp_login"] | |
170 del self.config["chromoting_auth_token"] | |
171 del self.config["xmpp_auth_token"] | |
128 | 172 |
129 class Host: | 173 class Host: |
130 """This manages the configuration for a host. | 174 """This manages the configuration for a host. |
131 | 175 |
132 Callers should instantiate a Host object (passing in a filename where the | 176 Callers should instantiate a Host object (passing in a filename where the |
133 config will be kept), then should call either of the methods: | 177 config will be kept), then should call either of the methods: |
134 | 178 |
135 * register(auth): Create a new Host configuration and register it | 179 * register(auth): Create a new Host configuration and register it |
136 with the Directory Service (the "auth" parameter is used to | 180 with the Directory Service (the "auth" parameter is used to |
137 authenticate with the Service). | 181 authenticate with the Service). |
138 * load_config(): Load a config from disk, with details of an existing Host | 182 * load_config(): Load a config from disk, with details of an existing Host |
139 registration. | 183 registration. |
140 | 184 |
141 After calling register() (or making any config changes) the method | 185 After calling register() (or making any config changes) the method |
142 save_config() should be called to save the details to disk. | 186 save_config() should be called to save the details to disk. |
143 """ | 187 """ |
144 | 188 |
145 server = 'www.googleapis.com' | 189 server = 'www.googleapis.com' |
146 url = 'https://' + server + '/chromoting/v1/@me/hosts' | 190 url = 'https://' + server + '/chromoting/v1/@me/hosts' |
147 | 191 |
148 def __init__(self, config_file, auth): | 192 def __init__(self, config, auth): |
149 """ | 193 """ |
150 Args: | 194 Args: |
151 config_file: Host configuration file path | 195 config: Host configuration object |
152 auth: Authentication object with credentials for authenticating with the | 196 auth: Authentication object with credentials for authenticating with the |
153 Directory service. | 197 Directory service. |
154 """ | 198 """ |
155 self.config_file = config_file | 199 self.config = config |
156 self.auth = auth | 200 self.auth = auth |
157 self.host_id = str(uuid.uuid1()) | 201 self.host_id = str(uuid.uuid1()) |
158 self.host_name = socket.gethostname() | 202 self.host_name = socket.gethostname() |
159 self.host_secret_hash = None | 203 self.host_secret_hash = None |
160 self.private_key = None | 204 self.private_key = None |
161 | 205 |
162 def register(self): | 206 def register(self): |
163 """Generates a private key for the stored |host_id|, and registers it with | 207 """Generates a private key for the stored |host_id|, and registers it with |
164 the Directory service. | 208 the Directory service. |
165 | 209 |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
215 def set_pin(self, pin): | 259 def set_pin(self, pin): |
216 if pin == "": | 260 if pin == "": |
217 self.host_secret_hash = "plain:" | 261 self.host_secret_hash = "plain:" |
218 else: | 262 else: |
219 self.host_secret_hash = "hmac:" + base64.b64encode( | 263 self.host_secret_hash = "hmac:" + base64.b64encode( |
220 hmac.new(str(self.host_id), pin, hashlib.sha256).digest()) | 264 hmac.new(str(self.host_id), pin, hashlib.sha256).digest()) |
221 | 265 |
222 def is_pin_set(self): | 266 def is_pin_set(self): |
223 return self.host_secret_hash | 267 return self.host_secret_hash |
224 | 268 |
225 def load_config(self): | 269 def load_config(self): |
Lambros
2012/08/08 22:11:29
Now that load_config no longer pulls data from a f
Sergey Ulanov
2012/08/08 22:41:03
You can look at it as if self.config is just a ref
| |
226 try: | 270 try: |
227 settings_file = open(self.config_file, 'r') | 271 self.host_id = self.config["host_id"] |
228 data = json.load(settings_file) | 272 self.host_name = self.config["host_name"] |
229 settings_file.close() | 273 self.host_secret_hash = self.config.get("host_secret_hash") |
230 except: | 274 self.private_key = self.config["private_key"] |
231 logging.info("Failed to load: " + self.config_file) | 275 except KeyError: |
232 return False | 276 return False |
233 self.host_id = data["host_id"] | |
234 self.host_name = data["host_name"] | |
235 self.host_secret_hash = data.get("host_secret_hash") | |
236 self.private_key = data["private_key"] | |
237 return True | 277 return True |
238 | 278 |
239 def save_config(self): | 279 def save_config(self): |
Lambros
2012/08/08 22:11:29
Same applies as for load_config.
Sergey Ulanov
2012/08/08 22:41:03
Done.
| |
240 data = { | 280 self.config["host_id"] = self.host_id |
241 "host_id": self.host_id, | 281 self.config["host_name"] = self.host_name |
242 "host_name": self.host_name, | 282 self.config["host_secret_hash"] = self.host_secret_hash |
243 "host_secret_hash": self.host_secret_hash, | 283 self.config["private_key"] = self.private_key |
244 "private_key": self.private_key, | |
245 } | |
246 if self.host_secret_hash: | |
247 data["host_secret_hash"] = self.host_secret_hash | |
248 | 284 |
249 old_umask = os.umask(0066) | 285 def clear_config(self): |
Lambros
2012/08/08 22:11:29
Similar thing here. clear_config operates on self
Sergey Ulanov
2012/08/08 22:41:03
Moved this function to Config class.
| |
250 settings_file = open(self.config_file, 'w') | 286 del self.config["host_id"] |
251 settings_file.write(json.dumps(data, indent=2)) | 287 del self.config["host_name"] |
252 settings_file.close() | 288 del self.config["host_secret_hash"] |
253 os.umask(old_umask) | 289 del self.config["private_key"] |
254 | |
255 | 290 |
256 class Desktop: | 291 class Desktop: |
257 """Manage a single virtual desktop""" | 292 """Manage a single virtual desktop""" |
258 | 293 |
259 def __init__(self, sizes): | 294 def __init__(self, sizes): |
260 self.x_proc = None | 295 self.x_proc = None |
261 self.session_proc = None | 296 self.session_proc = None |
262 self.host_proc = None | 297 self.host_proc = None |
263 self.sizes = sizes | 298 self.sizes = sizes |
264 g_desktops.append(self) | 299 g_desktops.append(self) |
(...skipping 111 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
376 self.session_proc = subprocess.Popen(XSESSION_COMMAND, | 411 self.session_proc = subprocess.Popen(XSESSION_COMMAND, |
377 stdin=open(os.devnull, "r"), | 412 stdin=open(os.devnull, "r"), |
378 cwd=HOME_DIR, | 413 cwd=HOME_DIR, |
379 env=self.child_env) | 414 env=self.child_env) |
380 if not self.session_proc.pid: | 415 if not self.session_proc.pid: |
381 raise Exception("Could not start X session") | 416 raise Exception("Could not start X session") |
382 | 417 |
383 def launch_host(self, host): | 418 def launch_host(self, host): |
384 # Start remoting host | 419 # Start remoting host |
385 args = [locate_executable(REMOTING_COMMAND), | 420 args = [locate_executable(REMOTING_COMMAND), |
386 "--host-config=%s" % (host.config_file)] | 421 "--host-config=%s" % (host.config.path)] |
387 if host.auth.config_file != host.config_file: | |
388 args.append("--auth-config=%s" % (host.auth.config_file)) | |
389 self.host_proc = subprocess.Popen(args, env=self.child_env) | 422 self.host_proc = subprocess.Popen(args, env=self.child_env) |
390 logging.info(args) | 423 logging.info(args) |
391 if not self.host_proc.pid: | 424 if not self.host_proc.pid: |
392 raise Exception("Could not start remoting host") | 425 raise Exception("Could not start remoting host") |
393 | 426 |
394 | 427 |
395 class PidFile: | 428 class PidFile: |
396 """Class to allow creating and deleting a file which holds the PID of the | 429 """Class to allow creating and deleting a file which holds the PID of the |
397 running process. This is used to detect if a process is already running, and | 430 running process. This is used to detect if a process is already running, and |
398 inform the user of the PID. On process termination, the PID file is | 431 inform the user of the PID. On process termination, the PID file is |
(...skipping 296 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
695 | 728 |
696 atexit.register(cleanup) | 729 atexit.register(cleanup) |
697 | 730 |
698 for s in [signal.SIGHUP, signal.SIGINT, signal.SIGTERM, signal.SIGUSR1]: | 731 for s in [signal.SIGHUP, signal.SIGINT, signal.SIGTERM, signal.SIGUSR1]: |
699 signal.signal(s, signal_handler) | 732 signal.signal(s, signal_handler) |
700 | 733 |
701 # Ensure full path to config directory exists. | 734 # Ensure full path to config directory exists. |
702 if not os.path.exists(CONFIG_DIR): | 735 if not os.path.exists(CONFIG_DIR): |
703 os.makedirs(CONFIG_DIR, mode=0700) | 736 os.makedirs(CONFIG_DIR, mode=0700) |
704 | 737 |
705 host_config_file = os.path.join(CONFIG_DIR, "host#%s.json" % host_hash) | 738 host_config = Config(os.path.join(CONFIG_DIR, "host#%s.json" % host_hash)) |
739 host_config.load() | |
706 | 740 |
707 # --silent option is specified when we are started from WebApp UI. Don't use | 741 auth = Authentication(host_config) |
708 # separate auth file in that case. | 742 auth_config_loaded = auth.load_config() |
709 # TODO(sergeyu): Always use host config for auth parameters. | 743 if not auth_config_loaded and not options.silent: |
710 if options.silent: | 744 # If we failed to load authentication parameters from the host config |
711 auth_config_file = host_config_file | 745 # then try loading them from the legacy auth.json file. |
712 else: | 746 auth_config = Config(os.path.join(CONFIG_DIR, "auth.json")) |
713 auth_config_file = os.path.join(CONFIG_DIR, "auth.json") | 747 if auth_config.load(): |
748 host_config.copy_from(auth_config) | |
749 auth_config_loaded = auth.load_config() | |
714 | 750 |
715 auth = Authentication(auth_config_file) | 751 host = Host(host_config, auth) |
716 auth_config_loaded = auth.load_config() | |
717 | |
718 host = Host(host_config_file, auth) | |
719 host_config_loaded = host.load_config() | 752 host_config_loaded = host.load_config() |
720 | 753 |
721 if options.silent: | 754 if options.silent: |
722 if not host_config_loaded or not auth_config_loaded: | 755 if not host_config_loaded or not auth_config_loaded: |
723 logging.error("Failed to load host configuration.") | 756 logging.error("Failed to load host configuration.") |
724 return 1 | 757 return 1 |
725 else: | 758 else: |
726 need_auth_tokens = not auth_config_loaded | 759 need_auth_tokens = not auth_config_loaded |
727 need_register_host = not host_config_loaded | 760 need_register_host = not host_config_loaded |
728 # Outside the loop so user doesn't get asked twice. | 761 # Outside the loop so user doesn't get asked twice. |
729 if need_register_host: | 762 if need_register_host: |
730 host.ask_pin() | 763 host.ask_pin() |
731 elif options.new_pin or not host.is_pin_set(): | 764 elif options.new_pin or not host.is_pin_set(): |
732 host.ask_pin() | 765 host.ask_pin() |
733 host.save_config() | 766 host.save_config() |
734 running, pid = PidFile(pid_filename).check() | 767 running, pid = PidFile(pid_filename).check() |
735 if running and pid != 0: | 768 if running and pid != 0: |
736 os.kill(pid, signal.SIGUSR1) | 769 os.kill(pid, signal.SIGUSR1) |
737 print "The running instance has been updated with the new PIN." | 770 print "The running instance has been updated with the new PIN." |
738 return 0 | 771 return 0 |
739 | 772 |
740 # The loop is to deal with the case of registering a new Host with | 773 # The loop is to deal with the case of registering a new Host with |
741 # previously-saved auth tokens (from a previous run of this script), which | 774 # previously-saved auth tokens (from a previous run of this script), which |
742 # may require re-prompting for username & password. | 775 # may require re-prompting for username & password. |
743 while True: | 776 while True: |
744 try: | 777 if need_auth_tokens: |
745 if need_auth_tokens: | 778 try: |
746 auth.generate_tokens() | 779 auth.generate_tokens() |
747 auth.save_config() | 780 auth.save_config() |
Lambros
2012/08/08 22:11:29
Since save_config() no longer writes to disk, this
Sergey Ulanov
2012/08/08 22:41:03
Done.
| |
748 need_auth_tokens = False | 781 need_auth_tokens = False |
749 except Exception: | 782 except Exception: |
750 logging.error("Authentication failed") | 783 logging.error("Authentication failed") |
751 return 1 | 784 return 1 |
752 | 785 |
753 try: | 786 if need_register_host: |
754 if need_register_host: | 787 try: |
755 host.register() | 788 host.register() |
756 host.save_config() | 789 host.save_config() |
757 except urllib2.HTTPError, err: | 790 except urllib2.HTTPError, err: |
758 if err.getcode() == 401: | 791 if err.getcode() == 401: |
759 # Authentication failed - re-prompt for username & password. | 792 # Authentication failed - re-prompt for username & password. |
760 need_auth_tokens = True | 793 need_auth_tokens = True |
761 continue | 794 continue |
762 else: | 795 else: |
763 # Not an authentication error. | 796 # Not an authentication error. |
764 logging.error("Directory returned error: " + str(err)) | 797 logging.error("Directory returned error: " + str(err)) |
765 logging.error(err.read()) | 798 logging.error(err.read()) |
766 return 1 | 799 return 1 |
767 | 800 |
768 # |auth| and |host| are both set up, so break out of the loop. | 801 # |auth| and |host| are both set up, so break out of the loop. |
769 break | 802 break |
770 | 803 |
804 if not host_config.save(): | |
805 logging.error("Faled to save host configuration.") | |
806 return 1 | |
807 | |
771 global g_pidfile | 808 global g_pidfile |
772 g_pidfile = PidFile(pid_filename) | 809 g_pidfile = PidFile(pid_filename) |
773 running, pid = g_pidfile.check() | 810 running, pid = g_pidfile.check() |
774 | 811 |
775 if running: | 812 if running: |
776 print "An instance of this script is already running." | 813 print "An instance of this script is already running." |
777 print "Use the -k flag to terminate the running instance." | 814 print "Use the -k flag to terminate the running instance." |
778 print "If this isn't the case, delete '%s' and try again." % pid_filename | 815 print "If this isn't the case, delete '%s' and try again." % pid_filename |
779 return 1 | 816 return 1 |
780 | 817 |
(...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
859 logging.info("Host process terminated") | 896 logging.info("Host process terminated") |
860 desktop.host_proc = None | 897 desktop.host_proc = None |
861 | 898 |
862 # These exit-codes must match the ones used by the host. | 899 # These exit-codes must match the ones used by the host. |
863 # See remoting/host/constants.h. | 900 # See remoting/host/constants.h. |
864 # Delete the host or auth configuration depending on the returned error | 901 # Delete the host or auth configuration depending on the returned error |
865 # code, so the next time this script is run, a new configuration | 902 # code, so the next time this script is run, a new configuration |
866 # will be created and registered. | 903 # will be created and registered. |
867 if os.WEXITSTATUS(status) == 2: | 904 if os.WEXITSTATUS(status) == 2: |
868 logging.info("Host configuration is invalid - exiting.") | 905 logging.info("Host configuration is invalid - exiting.") |
869 try: | 906 auth.clear_config() |
870 os.remove(host.config_file) | 907 host.clear_config() |
871 os.remove(auth.config_file) | 908 host_config.save() |
872 except: | |
873 pass | |
874 return 0 | 909 return 0 |
875 elif os.WEXITSTATUS(status) == 3: | 910 elif os.WEXITSTATUS(status) == 3: |
876 logging.info("Host ID has been deleted - exiting.") | 911 logging.info("Host ID has been deleted - exiting.") |
877 try: | 912 host.clear_config() |
878 os.remove(host.config_file) | 913 host_config.save() |
879 except: | |
880 pass | |
881 return 0 | 914 return 0 |
882 elif os.WEXITSTATUS(status) == 4: | 915 elif os.WEXITSTATUS(status) == 4: |
883 logging.info("OAuth credentials are invalid - exiting.") | 916 logging.info("OAuth credentials are invalid - exiting.") |
884 try: | 917 auth.clear_config() |
885 os.remove(auth.config_file) | 918 host_config.save() |
886 except: | |
887 pass | |
888 return 0 | 919 return 0 |
889 elif os.WEXITSTATUS(status) == 5: | 920 elif os.WEXITSTATUS(status) == 5: |
890 logging.info("Host domain is blocked by policy - exiting.") | 921 logging.info("Host domain is blocked by policy - exiting.") |
891 os.remove(host.config_file) | 922 os.remove(host.config_file) |
892 return 0 | 923 return 0 |
893 | 924 |
894 if __name__ == "__main__": | 925 if __name__ == "__main__": |
895 logging.basicConfig(level=logging.DEBUG) | 926 logging.basicConfig(level=logging.DEBUG) |
896 sys.exit(main()) | 927 sys.exit(main()) |
OLD | NEW |