package de.tuberlin.onedrivesdk.networking;
import com.google.gson.*;
import com.google.gson.annotations.Expose;
import com.squareup.okhttp.*;
import de.tuberlin.onedrivesdk.OneDriveException;
import de.tuberlin.onedrivesdk.common.ExceptionEventHandler;
import de.tuberlin.onedrivesdk.common.OneDriveScope;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import java.io.*;
import java.lang.reflect.Type;
import java.net.URLEncoder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Handel's authentication and continues refresh of the accessToken.
*/
public class OneDriveSession implements Runnable {
private static final Logger logger = LogManager.getLogger(OneDriveSession.class);
private final static String ENDPOINT = "https://login.live.com";
private final long refreshDelay = 3000 * 1000;//3000 sec to ms
@Expose
private final String clientID;
@Expose
private final String clientSecret;
private final OneDriveScope[] scopes;
private OkHttpClient client;
private ExecutorService refreshThread;
private ExceptionEventHandler refreshExceptionHandler;
private String tokenType;
@Expose
private long expiresIn;
@Expose
private String accessToken;
@Expose
private String refreshToken;
@Expose
private long lastRefresh = Long.MIN_VALUE;
private String redirect_uri;
private boolean keepRefreshing = true;
private OneDriveSession(OkHttpClient client, String clientID, String clientSecret, String redirectUri, ExceptionEventHandler refreshExceptionHandler, OneDriveScope[] scopes) {
this.client = client;
this.clientID = clientID;
this.clientSecret = clientSecret;
this.scopes = scopes;
if (redirectUri != null) {
try {
this.redirect_uri = URLEncoder.encode(redirectUri, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("redirectURL is not a valid url... or something else went horrobly wrong at this point..." + e.getMessage());
}
}
this.refreshExceptionHandler = refreshExceptionHandler;
logger.info("initialize session for " + clientID);
}
private OneDriveSession(OkHttpClient client, String clientID, String clientSecret, String redirectUri, OneDriveScope[] scopes) {
this(client, clientID, clientSecret, redirectUri, null, scopes);
}
/**
* creates a OneDriveSession used to handle the authentication process
*
* @param client Used for HTTP Communication
* @param clientID @see https://dev.onedrive.com/auth/msa_oauth.htm CodeFlow
* @param clientSecret @see https://dev.onedrive.com/auth/msa_oauth.htm CodeFlow
* @param redirect_uri @see https://dev.onedrive.com/auth/msa_oauth.htm can be null
* @param exceptionCallback Method that is called if the RefreshLoop encounters an exception
* @param scopes @see https://dev.onedrive.com/auth/msa_oauth.htm
* @return
*/
public static OneDriveSession initializeSession(OkHttpClient client, String clientID, String clientSecret,
String redirect_uri, ExceptionEventHandler exceptionCallback, OneDriveScope[] scopes) {
return new OneDriveSession(client, clientID, clientSecret, redirect_uri, exceptionCallback, scopes);
}
public static OneDriveSession initializeSession(OkHttpClient client, String clientID, String clientSecret,
String redirect_uri, OneDriveScope... scopes) {
return new OneDriveSession(client, clientID, clientSecret, redirect_uri, scopes);
}
public static void authorizeSession(OneDriveSession session, String code) throws OneDriveException {
//The body of the second step in the code-flow guide
String oAuthCodeRedeemBodyString = String.format("client_id=%s&client_secret=%s&code=%s&grant_type=authorization_code",
session.getClientID(), session.getClientSecret(), code);
if (session.redirect_uri != null) {
oAuthCodeRedeemBodyString += String.format("&redirect_uri=%s", session.redirect_uri);
}
handleAuthRequest(session, oAuthCodeRedeemBodyString);
}
public static void refreshSession(OneDriveSession session, String refreshToken) throws OneDriveException {
session.setRefreshToken(refreshToken);
session.refresh();
}
private static void handleAuthRequest(OneDriveSession session, String messageBody) throws OneDriveAuthenticationException {
JSONParser jsonParser = new JSONParser();
//Url of the second step of the Code-FLow guide
String oAuthCodeRedeemURL = String.format("%s/oauth20_token.srf", ENDPOINT);
RequestBody oAuthCodeRedeemBody = RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), messageBody);
// Create request for remote resource.
Request request = new Request.Builder()
.url(oAuthCodeRedeemURL)
.post(oAuthCodeRedeemBody)
.build();
// Execute the request and retrieve the response.
String responseBody = null;
try {
Response response = session.getClient().newCall(request).execute();
responseBody = response.body().string();
JSONObject resp = (JSONObject) jsonParser.parse(responseBody);
if (resp.containsKey("error")) {
String error = (String) resp.get("error");
String error_description = (String) resp.get("error_description");
throw new OneDriveException(String.format("[%s] %s", error, error_description));
} else {
session.setTokenType((String) resp.get("token_type"));
session.setExpiresIn((Long) resp.get("expires_in"));
session.setAccessToken((String) resp.get("access_token"));
session.setRefreshToken((String) resp.get("refresh_token"));
session.setLastRefresh(System.currentTimeMillis());
}
} catch (OneDriveException e) {
throw new OneDriveAuthenticationException("Internal Error:", e);
} catch (IOException e) {
throw new OneDriveAuthenticationException("Could not establish a connection.", e);
} catch (ParseException pa) {
throw new OneDriveAuthenticationException("Could not process the server response.", pa);
} catch (Exception e) {
throw new OneDriveAuthenticationException("A undefined error accrued during the authentication request.\n" + responseBody, e);
}
logger.info("successfully authorized session for " + session.clientID);
}
private static Gson builderGson() {
final GsonBuilder builder = new GsonBuilder().excludeFieldsWithModifiers();
builder.registerTypeAdapter(OneDriveSession.class, new JsonDeserializer<OneDriveSession>() {
@Override
public OneDriveSession deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
JsonObject retValue = jsonElement.getAsJsonObject();
String accessToken = retValue.get("accessToken").getAsString();
String refreshToken = retValue.get("refreshToken").getAsString();
String clientID = retValue.get("clientID").getAsString();
String clientSecret = retValue.get("clientSecret").getAsString();
long expiresIn = retValue.get("expiresIn").getAsLong();
long lastRefresh = retValue.get("lastRefresh").getAsLong();
OneDriveSession session = new OneDriveSession(new OkHttpClient(), clientID, clientSecret, null, null, new OneDriveScope[]{OneDriveScope.OFFLINE_ACCESS});
session.setRefreshToken(refreshToken);
session.setAccessToken(accessToken);
session.setLastRefresh(lastRefresh);
session.setExpiresIn(expiresIn);
return session;
}
});
builder.registerTypeAdapter(OneDriveSession.class, new JsonSerializer<OneDriveSession>() {
@Override
public JsonElement serialize(OneDriveSession session, Type type, JsonSerializationContext jsonSerializationContext) {
JsonObject retValue = new JsonObject();
retValue.add("accessToken", new JsonPrimitive(session.getAccessToken()));
retValue.add("refreshToken", new JsonPrimitive(session.getRefreshToken()));
retValue.add("clientID", new JsonPrimitive(session.getClientID()));
retValue.add("clientSecret", new JsonPrimitive(session.getClientSecret()));
retValue.add("expiresIn", new JsonPrimitive(session.getExpiresIn()));
retValue.add("lastRefresh", new JsonPrimitive(session.getLastRefresh()));
return retValue;
}
});
final Gson gson = builder.create();
return gson;
}
public static OneDriveSession readFromFile(File f) throws IOException {
StringBuilder json = new StringBuilder();
try (BufferedReader r = new BufferedReader(new FileReader(f))) {
String l;
while ((l = r.readLine()) != null) {
json.append(l);
}
}
final OneDriveSession session = builderGson().fromJson(json.toString(), OneDriveSession.class);
session.setClient(new OkHttpClient());
return session;
}
public static void write(OneDriveSession that, File outputFile) throws IOException, OneDriveException {
that.refresh();
String json = builderGson().toJson(that);
try (BufferedWriter bf = new BufferedWriter(new FileWriter(outputFile))) {
bf.write(json);
}
}
public long getLastRefresh() {
return lastRefresh;
}
public void setLastRefresh(long lastRefresh) {
this.lastRefresh = lastRefresh;
}
public boolean isAuthenticated() {
return lastRefresh + expiresIn * 1000 >= System.currentTimeMillis();
}
public OkHttpClient getClient() {
return client;
}
private void setClient(OkHttpClient client) {
this.client = client;
}
public String getClientID() {
return clientID;
}
public String getClientSecret() {
return clientSecret;
}
public String getScopeString() {
StringBuilder bul = new StringBuilder();
for (OneDriveScope os : scopes) {
bul.append(os.getCode()).append(" ");
}
return bul.toString().trim();
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public long getExpiresIn() {
return expiresIn;
}
public void setExpiresIn(long expiresIn) {
this.expiresIn = expiresIn;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getRefreshToken() {
return this.refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public void refresh() throws OneDriveException {
String oAuthRefreshBodyString = String.format("client_id=%s&client_secret=%s&refresh_token=%s&grant_type=refresh_token", clientID, clientSecret, refreshToken);
handleAuthRequest(this, oAuthRefreshBodyString);
}
public String getAccessURL() {
String scope = "";
try {
scope = URLEncoder.encode(getScopeString(), "UTF-8");
} catch (UnsupportedEncodingException e) {
logger.error("Error while encoding scopeString to url, using UTF-8",e);
}
String uri = String.format("%s/oauth20_authorize.srf?client_id=%s&scope=%s&response_type=code", ENDPOINT, clientID, scope);
if (this.redirect_uri != null) {
uri += String.format("&redirect_uri=%s", this.redirect_uri);
}
return uri;
}
@Override
public String toString() {
return "OneDriveSession{" +
"accessToken='" + accessToken + '\'' +
"\n, expiresIn=" + expiresIn +
"\n, refreshToken='" + refreshToken + '\'' +
"\n, lastRefresh=" + lastRefresh + '\'' +
"\n, scope=" + getScopeString() +
'}';
}
public void run() {
//initial delay
try {
Thread.sleep(refreshDelay);
} catch (InterruptedException e) {
}
//continuously refresh thread
while (keepRefreshing) {
logger.info("refreshing session");
try {
refresh();
Thread.sleep(refreshDelay);
} catch (OneDriveException e) {
logger.info("failed to refresh session - attempting recovery");
long retryTime = System.currentTimeMillis() + 1000 * 30;
while (System.currentTimeMillis() <= retryTime && keepRefreshing) {
try {
refresh();
} catch (OneDriveException e1) {
try {
Thread.sleep(500);
} catch (InterruptedException e2) {
}
}
}
logger.error("could not refresh session", e);
if (refreshExceptionHandler != null) {
refreshExceptionHandler.handle(this, e);
}
//backoff after error
try {
Thread.sleep(10000);
} catch (Exception e1) {
}
} catch (Exception e) {
}
}
}
public void startRefreshThread() {
if (this.refreshThread == null) {
this.refreshThread = Executors.newSingleThreadExecutor();
this.refreshThread.submit(this);
logger.info("starting refresh thread");
}
}
public void terminate() {
if (this.refreshThread != null) {
keepRefreshing = false;
refreshThread.shutdownNow();
logger.info("stopping refresh thread");
}
}
}