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