1 /*
2  * RemoteServerAuth.java
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 package org.rstudio.studio.client.server.remote;
16 
17 import com.google.gwt.core.client.Scheduler;
18 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
19 import com.google.gwt.json.client.JSONArray;
20 import com.google.gwt.json.client.JSONObject;
21 import com.google.gwt.json.client.JSONString;
22 import com.google.gwt.user.client.Random;
23 import com.google.gwt.user.client.Timer;
24 import com.google.gwt.user.client.ui.FormPanel;
25 import com.google.gwt.user.client.ui.FormPanel.SubmitCompleteEvent;
26 import com.google.gwt.user.client.ui.FormPanel.SubmitCompleteHandler;
27 import com.google.gwt.user.client.ui.RootPanel;
28 import org.rstudio.core.client.Debug;
29 import org.rstudio.core.client.jsonrpc.RequestLog;
30 import org.rstudio.core.client.jsonrpc.RequestLogEntry;
31 import org.rstudio.core.client.jsonrpc.RequestLogEntry.ResponseType;
32 import org.rstudio.core.client.jsonrpc.RpcError;
33 import org.rstudio.core.client.jsonrpc.RpcResponse;
34 import org.rstudio.studio.client.server.Bool;
35 import org.rstudio.studio.client.server.ServerError;
36 import org.rstudio.studio.client.server.ServerRequestCallback;
37 
38 import java.util.ArrayList;
39 
40 class RemoteServerAuth
41 {
42    public static final int CREDENTIALS_UPDATE_SUCCESS = 1;
43    public static final int CREDENTIALS_UPDATE_FAILURE = 2;
44    public static final int CREDENTIALS_UPDATE_UNSUPPORTED = 3;
45 
RemoteServerAuth(RemoteServer remoteServer)46    public RemoteServerAuth(RemoteServer remoteServer)
47    {
48       remoteServer_ = remoteServer;
49    }
50 
51    private Timer periodicUpdateTimer_ = null;
52 
schedulePeriodicCredentialsUpdate()53    public void schedulePeriodicCredentialsUpdate()
54    {
55       // create the callback
56       periodicUpdateTimer_ = new Timer() {
57          @Override
58          public void run()
59          {
60             updateCredentials(new ServerRequestCallback<Integer>() {
61 
62                @Override
63                public void onResponseReceived(Integer response)
64                {
65                   switch(response)
66                   {
67                   case CREDENTIALS_UPDATE_SUCCESS:
68                      // do nothing (we just successfully updated our
69                      // credentials)
70                      break;
71 
72                   case CREDENTIALS_UPDATE_FAILURE:
73                      // we are not authorized, blow the client away
74                      remoteServer_.handleUnauthorizedError();
75                      break;
76 
77                   case CREDENTIALS_UPDATE_UNSUPPORTED:
78                      // not supported by the back end so cancel the timer
79                      periodicUpdateTimer_.cancel();
80                      break;
81                   }
82                }
83 
84                @Override
85                public void onError(ServerError serverError)
86                {
87                   // if method is not supported then cancel the timer
88                   Debug.logError(serverError);
89                }
90             });
91 
92 
93          }
94       };
95 
96       // schedule for every 5 minutes
97       final int kMinutes = 5;
98       int milliseconds = kMinutes * 60 * 1000;
99       periodicUpdateTimer_.scheduleRepeating(milliseconds);
100    }
101 
attemptToUpdateCredentials()102    public void attemptToUpdateCredentials()
103    {
104       updateCredentials(new ServerRequestCallback<Integer>() {
105 
106          @Override
107          public void onResponseReceived(Integer response)
108          {
109             // this method does nothing in the case of both successfully
110             // updating credentials and method not found. however, if
111             // the credentials update fails then it needs to blow
112             // away the client
113 
114             if (response.intValue() == CREDENTIALS_UPDATE_FAILURE)
115             {
116                remoteServer_.handleUnauthorizedError();
117             }
118          }
119 
120          @Override
121          public void onError(ServerError serverError)
122          {
123             Debug.logError(serverError);
124          }
125       });
126    }
127 
128    // save previous form as a precaution against forms which are not
129    // cleaned up due to the submit handler not being called
130    private static ArrayList<FormPanel> previousUpdateCredentialsForms_ = new ArrayList<>();
131 
safeCleanupPreviousUpdateCredentials()132    private void safeCleanupPreviousUpdateCredentials()
133    {
134       try
135       {
136          for (int i=0; i<previousUpdateCredentialsForms_.size(); i++)
137          {
138             FormPanel formPanel = previousUpdateCredentialsForms_.get(i);
139             RootPanel.get().remove(formPanel);
140          }
141 
142          previousUpdateCredentialsForms_.clear();
143       }
144       catch(Throwable e)
145       {
146       }
147    }
148 
updateCredentials( final ServerRequestCallback<Integer> requestCallback)149    public void updateCredentials(
150                          final ServerRequestCallback<Integer> requestCallback)
151    {
152       // safely cleanup any previously active update credentials forms
153       safeCleanupPreviousUpdateCredentials();
154 
155       // create a hidden form panel to submit the update credentials to
156       // (we do this so GWT manages the trickiness associated with
157       // managing and reading the contents of a hidden iframe)
158       final FormPanel updateCredentialsForm = new FormPanel();
159       updateCredentialsForm.setMethod(FormPanel.METHOD_GET);
160       updateCredentialsForm.setEncoding(FormPanel.ENCODING_URLENCODED);
161 
162       // form url
163       String url = remoteServer_.getApplicationURL("auth-update-credentials");
164       updateCredentialsForm.setAction(url);
165 
166       // request log entry (fake up a json rpc method call to conform
167       // to the data format expected by RequestLog
168       String requestId = Integer.toString(Random.nextInt());
169       String requestData = createRequestData();
170       final RequestLogEntry logEntry = RequestLog.log(requestId, requestData);
171 
172       // form submit complete handler
173       updateCredentialsForm.addSubmitCompleteHandler(new SubmitCompleteHandler(){
174 
175          public void onSubmitComplete(SubmitCompleteEvent event)
176          {
177             // parse the results
178             String results = event.getResults();
179             RpcResponse response = RpcResponse.parse(event.getResults());
180             if (response != null)
181             {
182                logEntry.logResponse(ResponseType.Normal, results);
183 
184                // check for error
185                RpcError rpcError = response.getError();
186                if (rpcError != null)
187                {
188                   if (rpcError.getCode() == RpcError.METHOD_NOT_FOUND)
189                   {
190                      requestCallback.onResponseReceived(CREDENTIALS_UPDATE_UNSUPPORTED);
191                   }
192                   else
193                   {
194                      requestCallback.onError(new RemoteServerError(rpcError));
195                   }
196                }
197                else // must be a valid response
198                {
199                   Bool authenticated = response.getResult();
200                   if (authenticated.getValue())
201                   {
202                      requestCallback.onResponseReceived(CREDENTIALS_UPDATE_SUCCESS);
203                   }
204                   else
205                   {
206                      requestCallback.onResponseReceived(CREDENTIALS_UPDATE_FAILURE);
207                   }
208                }
209             }
210             else // error parsing results
211             {
212                logEntry.logResponse(ResponseType.Error, results);
213 
214                // form message
215                String msg = "Error parsing results: " +
216                             (results != null ? results : "(null)");
217 
218                // we don't expect this so debug log to flag our attention
219                Debug.log("UPDATE CREDENTIALS: " + msg);
220 
221                // return the error
222                RpcError rpcError = RpcError.create(RpcError.PARSE_ERROR, msg);
223                requestCallback.onError(new RemoteServerError(rpcError));
224             }
225 
226             // remove the hidden form (from both last-ditch list and DOM)
227             previousUpdateCredentialsForms_.remove(updateCredentialsForm);
228             Scheduler.get().scheduleDeferred(new ScheduledCommand() {
229                public void execute()
230                {
231                   RootPanel.get().remove(updateCredentialsForm);
232                }
233             });
234          }
235       });
236 
237       // add the (hidden) form panel to the document and last ditch list
238       RootPanel.get().add(updateCredentialsForm, -1000, -1000);
239       previousUpdateCredentialsForms_.add(updateCredentialsForm);
240 
241       // submit the form
242       updateCredentialsForm.submit();
243    }
244 
createRequestData()245    private String createRequestData()
246    {
247       JSONObject request = new JSONObject();
248       request.put("method", new JSONString("update_credentials"));
249       request.put("params", new JSONArray());
250       return request.toString();
251    }
252 
253    private final RemoteServer remoteServer_;
254 }
255