1# Copyright 2015 Google Inc. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""OAuth 2.0 utitilies for Google Developer Shell environment."""
16
17import datetime
18import json
19import os
20import socket
21
22from oauth2client import _helpers
23from oauth2client import client
24
25DEVSHELL_ENV = 'DEVSHELL_CLIENT_PORT'
26
27
28class Error(Exception):
29    """Errors for this module."""
30    pass
31
32
33class CommunicationError(Error):
34    """Errors for communication with the Developer Shell server."""
35
36
37class NoDevshellServer(Error):
38    """Error when no Developer Shell server can be contacted."""
39
40# The request for credential information to the Developer Shell client socket
41# is always an empty PBLite-formatted JSON object, so just define it as a
42# constant.
43CREDENTIAL_INFO_REQUEST_JSON = '[]'
44
45
46class CredentialInfoResponse(object):
47    """Credential information response from Developer Shell server.
48
49    The credential information response from Developer Shell socket is a
50    PBLite-formatted JSON array with fields encoded by their index in the
51    array:
52
53    * Index 0 - user email
54    * Index 1 - default project ID. None if the project context is not known.
55    * Index 2 - OAuth2 access token. None if there is no valid auth context.
56    * Index 3 - Seconds until the access token expires. None if not present.
57    """
58
59    def __init__(self, json_string):
60        """Initialize the response data from JSON PBLite array."""
61        pbl = json.loads(json_string)
62        if not isinstance(pbl, list):
63            raise ValueError('Not a list: ' + str(pbl))
64        pbl_len = len(pbl)
65        self.user_email = pbl[0] if pbl_len > 0 else None
66        self.project_id = pbl[1] if pbl_len > 1 else None
67        self.access_token = pbl[2] if pbl_len > 2 else None
68        self.expires_in = pbl[3] if pbl_len > 3 else None
69
70
71def _SendRecv():
72    """Communicate with the Developer Shell server socket."""
73
74    port = int(os.getenv(DEVSHELL_ENV, 0))
75    if port == 0:
76        raise NoDevshellServer()
77
78    sock = socket.socket()
79    sock.connect(('localhost', port))
80
81    data = CREDENTIAL_INFO_REQUEST_JSON
82    msg = '{0}\n{1}'.format(len(data), data)
83    sock.sendall(_helpers._to_bytes(msg, encoding='utf-8'))
84
85    header = sock.recv(6).decode()
86    if '\n' not in header:
87        raise CommunicationError('saw no newline in the first 6 bytes')
88    len_str, json_str = header.split('\n', 1)
89    to_read = int(len_str) - len(json_str)
90    if to_read > 0:
91        json_str += sock.recv(to_read, socket.MSG_WAITALL).decode()
92
93    return CredentialInfoResponse(json_str)
94
95
96class DevshellCredentials(client.GoogleCredentials):
97    """Credentials object for Google Developer Shell environment.
98
99    This object will allow a Google Developer Shell session to identify its
100    user to Google and other OAuth 2.0 servers that can verify assertions. It
101    can be used for the purpose of accessing data stored under the user
102    account.
103
104    This credential does not require a flow to instantiate because it
105    represents a two legged flow, and therefore has all of the required
106    information to generate and refresh its own access tokens.
107    """
108
109    def __init__(self, user_agent=None):
110        super(DevshellCredentials, self).__init__(
111            None,  # access_token, initialized below
112            None,  # client_id
113            None,  # client_secret
114            None,  # refresh_token
115            None,  # token_expiry
116            None,  # token_uri
117            user_agent)
118        self._refresh(None)
119
120    def _refresh(self, http_request):
121        self.devshell_response = _SendRecv()
122        self.access_token = self.devshell_response.access_token
123        expires_in = self.devshell_response.expires_in
124        if expires_in is not None:
125            delta = datetime.timedelta(seconds=expires_in)
126            self.token_expiry = client._UTCNOW() + delta
127        else:
128            self.token_expiry = None
129
130    @property
131    def user_email(self):
132        return self.devshell_response.user_email
133
134    @property
135    def project_id(self):
136        return self.devshell_response.project_id
137
138    @classmethod
139    def from_json(cls, json_data):
140        raise NotImplementedError(
141            'Cannot load Developer Shell credentials from JSON.')
142
143    @property
144    def serialization_data(self):
145        raise NotImplementedError(
146            'Cannot serialize Developer Shell credentials.')
147