From cf4e9fab1d1a4eec2dc7401682cdaca87c58640d Mon Sep 17 00:00:00 2001 From: Adam Date: Sun, 14 May 2017 19:23:44 -0400 Subject: [PATCH] Add account plugin, and support for acquiring a session with the account webservice --- .../http/api/account/LoginClient.java | 69 +++++ .../http/api/account/OAuthResponse.java | 53 ++++ .../api/ws/RuntimeTypeAdapterFactory.java | 240 ++++++++++++++++++ .../http/api/ws/WebsocketGsonFactory.java | 46 ++++ .../http/api/ws/WebsocketMessage.java | 30 +++ .../http/api/ws/messages/Handshake.java | 43 ++++ .../messages}/LoginResponse.java | 31 ++- .../runelite/http/api/ws/messages/Ping.java | 43 ++++ .../http/service/account/AccountService.java | 32 ++- .../http/service/ws/SessionManager.java | 72 ++++++ .../http/service/{ => ws}/WSService.java | 37 ++- .../runelite/http/service/ws/WSSession.java | 81 ++++++ .../java/net/runelite/client/RuneLite.java | 93 ++++++- .../java/net/runelite/client/WSClient.java | 47 +++- .../client/account/AccountSession.java | 65 +++++ .../runelite/client/events/SessionOpen.java | 34 +++ .../client/plugins/PluginManager.java | 2 + .../client/plugins/account/AccountPlugin.java | 156 ++++++++++++ .../client/plugins/account/login_icon.png | Bin 0 -> 463 bytes 19 files changed, 1144 insertions(+), 30 deletions(-) create mode 100644 http-api/src/main/java/net/runelite/http/api/account/LoginClient.java create mode 100644 http-api/src/main/java/net/runelite/http/api/account/OAuthResponse.java create mode 100644 http-api/src/main/java/net/runelite/http/api/ws/RuntimeTypeAdapterFactory.java create mode 100644 http-api/src/main/java/net/runelite/http/api/ws/WebsocketGsonFactory.java create mode 100644 http-api/src/main/java/net/runelite/http/api/ws/WebsocketMessage.java create mode 100644 http-api/src/main/java/net/runelite/http/api/ws/messages/Handshake.java rename http-api/src/main/java/net/runelite/http/api/{account => ws/messages}/LoginResponse.java (75%) create mode 100644 http-api/src/main/java/net/runelite/http/api/ws/messages/Ping.java create mode 100644 http-service/src/main/java/net/runelite/http/service/ws/SessionManager.java rename http-service/src/main/java/net/runelite/http/service/{ => ws}/WSService.java (71%) create mode 100644 http-service/src/main/java/net/runelite/http/service/ws/WSSession.java create mode 100644 runelite-client/src/main/java/net/runelite/client/account/AccountSession.java create mode 100644 runelite-client/src/main/java/net/runelite/client/events/SessionOpen.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/account/AccountPlugin.java create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/account/login_icon.png diff --git a/http-api/src/main/java/net/runelite/http/api/account/LoginClient.java b/http-api/src/main/java/net/runelite/http/api/account/LoginClient.java new file mode 100644 index 0000000000..60947380c2 --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/account/LoginClient.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.api.account; + +import com.google.gson.Gson; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import net.runelite.http.api.RuneliteAPI; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LoginClient +{ + private static final Logger logger = LoggerFactory.getLogger(LoginClient.class); + + private final OkHttpClient client = new OkHttpClient(); + private final Gson gson = new Gson(); + + public OAuthResponse login() throws IOException + { + HttpUrl.Builder builder = RuneliteAPI.getApiBase().newBuilder() + .addPathSegment("account") + .addPathSegment("login"); + + HttpUrl url = builder.build(); + + logger.debug("Built URI: {}", url); + + Request request = new Request.Builder() + .url(url) + .build(); + + Response response = client.newCall(request).execute(); + + try (ResponseBody body = response.body()) + { + InputStream in = body.byteStream(); + return gson.fromJson(new InputStreamReader(in), OAuthResponse.class); + } + } +} diff --git a/http-api/src/main/java/net/runelite/http/api/account/OAuthResponse.java b/http-api/src/main/java/net/runelite/http/api/account/OAuthResponse.java new file mode 100644 index 0000000000..dc9d3c34ad --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/account/OAuthResponse.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.api.account; + +import java.util.UUID; + +public class OAuthResponse +{ + private String oauthUrl; + private UUID uid; + + public String getOauthUrl() + { + return oauthUrl; + } + + public void setOauthUrl(String oauthUrl) + { + this.oauthUrl = oauthUrl; + } + + public UUID getUid() + { + return uid; + } + + public void setUid(UUID uid) + { + this.uid = uid; + } +} diff --git a/http-api/src/main/java/net/runelite/http/api/ws/RuntimeTypeAdapterFactory.java b/http-api/src/main/java/net/runelite/http/api/ws/RuntimeTypeAdapterFactory.java new file mode 100644 index 0000000000..2819c92ceb --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/ws/RuntimeTypeAdapterFactory.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.runelite.http.api.ws; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.Streams; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * Adapts values whose runtime type may differ from their declaration type. This + * is necessary when a field's type is not the same type that GSON should create + * when deserializing that field. For example, consider these types: + *
   {@code
+ *   abstract class Shape {
+ *     int x;
+ *     int y;
+ *   }
+ *   class Circle extends Shape {
+ *     int radius;
+ *   }
+ *   class Rectangle extends Shape {
+ *     int width;
+ *     int height;
+ *   }
+ *   class Diamond extends Shape {
+ *     int width;
+ *     int height;
+ *   }
+ *   class Drawing {
+ *     Shape bottomShape;
+ *     Shape topShape;
+ *   }
+ * }
+ *

Without additional type information, the serialized JSON is ambiguous. Is + * the bottom shape in this drawing a rectangle or a diamond?

   {@code
+ *   {
+ *     "bottomShape": {
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * This class addresses this problem by adding type information to the + * serialized JSON and honoring that type information when the JSON is + * deserialized:
   {@code
+ *   {
+ *     "bottomShape": {
+ *       "type": "Diamond",
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "type": "Circle",
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * Both the type field name ({@code "type"}) and the type labels ({@code + * "Rectangle"}) are configurable. + * + *

Registering Types

+ * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field + * name to the {@link #of} factory method. If you don't supply an explicit type + * field name, {@code "type"} will be used.
   {@code
+ *   RuntimeTypeAdapterFactory shapeAdapterFactory
+ *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+ * }
+ * Next register all of your subtypes. Every subtype must be explicitly + * registered. This protects your application from injection attacks. If you + * don't supply an explicit type label, the type's simple name will be used. + *
   {@code
+ *   shapeAdapter.registerSubtype(Rectangle.class, "Rectangle");
+ *   shapeAdapter.registerSubtype(Circle.class, "Circle");
+ *   shapeAdapter.registerSubtype(Diamond.class, "Diamond");
+ * }
+ * Finally, register the type adapter factory in your application's GSON builder: + *
   {@code
+ *   Gson gson = new GsonBuilder()
+ *       .registerTypeAdapterFactory(shapeAdapterFactory)
+ *       .create();
+ * }
+ * Like {@code GsonBuilder}, this API supports chaining:
   {@code
+ *   RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ *       .registerSubtype(Rectangle.class)
+ *       .registerSubtype(Circle.class)
+ *       .registerSubtype(Diamond.class);
+ * }
+ */ +public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { + private final Class baseType; + private final String typeFieldName; + private final Map> labelToSubtype = new LinkedHashMap>(); + private final Map, String> subtypeToLabel = new LinkedHashMap, String>(); + + private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory(baseType, typeFieldName); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as + * the type field name. + */ + public static RuntimeTypeAdapterFactory of(Class baseType) { + return new RuntimeTypeAdapterFactory(baseType, "type"); + } + + /** + * Registers {@code type} identified by {@code label}. Labels are case + * sensitive. + * + * @throws IllegalArgumentException if either {@code type} or {@code label} + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { + if (type == null || label == null) { + throw new NullPointerException(); + } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } + + /** + * Registers {@code type} identified by its {@link Class#getSimpleName simple + * name}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type) { + return registerSubtype(type, type.getSimpleName()); + } + + public TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != baseType) { + return null; + } + + final Map> labelToDelegate + = new LinkedHashMap>(); + final Map, TypeAdapter> subtypeToDelegate + = new LinkedHashMap, TypeAdapter>(); + for (Map.Entry> entry : labelToSubtype.entrySet()) { + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + } + + return new TypeAdapter() { + @Override public R read(JsonReader in) throws IOException { + JsonElement jsonElement = Streams.parse(in); + JsonElement labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); + if (labelJsonElement == null) { + throw new JsonParseException("cannot deserialize " + baseType + + " because it does not define a field named " + typeFieldName); + } + String label = labelJsonElement.getAsString(); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + + label + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override public void write(JsonWriter out, R value) throws IOException { + Class srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + if (jsonObject.has(typeFieldName)) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + " because it already defines a field named " + typeFieldName); + } + JsonObject clone = new JsonObject(); + clone.add(typeFieldName, new JsonPrimitive(label)); + for (Map.Entry e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + Streams.write(clone, out); + } + }.nullSafe(); + } +} diff --git a/http-api/src/main/java/net/runelite/http/api/ws/WebsocketGsonFactory.java b/http-api/src/main/java/net/runelite/http/api/ws/WebsocketGsonFactory.java new file mode 100644 index 0000000000..71c421d491 --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/ws/WebsocketGsonFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.api.ws; + +import net.runelite.http.api.ws.messages.LoginResponse; +import net.runelite.http.api.ws.messages.Handshake; +import net.runelite.http.api.ws.messages.Ping; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public class WebsocketGsonFactory +{ + public static Gson build() + { + RuntimeTypeAdapterFactory typeAdapter = RuntimeTypeAdapterFactory.of(WebsocketMessage.class) + .registerSubtype(Handshake.class) + .registerSubtype(LoginResponse.class) + .registerSubtype(Ping.class); + + return new GsonBuilder() + .registerTypeAdapterFactory(typeAdapter) + .create(); + } +} diff --git a/http-api/src/main/java/net/runelite/http/api/ws/WebsocketMessage.java b/http-api/src/main/java/net/runelite/http/api/ws/WebsocketMessage.java new file mode 100644 index 0000000000..cec9c17c48 --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/ws/WebsocketMessage.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.api.ws; + +public class WebsocketMessage +{ + +} diff --git a/http-api/src/main/java/net/runelite/http/api/ws/messages/Handshake.java b/http-api/src/main/java/net/runelite/http/api/ws/messages/Handshake.java new file mode 100644 index 0000000000..557f8f524b --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/ws/messages/Handshake.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.api.ws.messages; + +import java.util.UUID; +import net.runelite.http.api.ws.WebsocketMessage; + +public class Handshake extends WebsocketMessage +{ + private UUID session; + + public UUID getSession() + { + return session; + } + + public void setSession(UUID session) + { + this.session = session; + } +} diff --git a/http-api/src/main/java/net/runelite/http/api/account/LoginResponse.java b/http-api/src/main/java/net/runelite/http/api/ws/messages/LoginResponse.java similarity index 75% rename from http-api/src/main/java/net/runelite/http/api/account/LoginResponse.java rename to http-api/src/main/java/net/runelite/http/api/ws/messages/LoginResponse.java index b4bbf0da0c..b517e773a2 100644 --- a/http-api/src/main/java/net/runelite/http/api/account/LoginResponse.java +++ b/http-api/src/main/java/net/runelite/http/api/ws/messages/LoginResponse.java @@ -22,32 +22,31 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package net.runelite.http.api.account; +package net.runelite.http.api.ws.messages; -import java.util.UUID; +import net.runelite.http.api.ws.WebsocketMessage; -public class LoginResponse +/** + * Called after a successful login to the server + * @author Adam + */ +public class LoginResponse extends WebsocketMessage { - private String oauthUrl; - private UUID uid; + private String username; - public String getOauthUrl() + public String getUsername() { - return oauthUrl; + return username; } - public void setOauthUrl(String oauthUrl) + public void setUsername(String username) { - this.oauthUrl = oauthUrl; + this.username = username; } - public UUID getUid() + @Override + public String toString() { - return uid; - } - - public void setUid(UUID uid) - { - this.uid = uid; + return "LoginResponse{" + "username=" + username + '}'; } } diff --git a/http-api/src/main/java/net/runelite/http/api/ws/messages/Ping.java b/http-api/src/main/java/net/runelite/http/api/ws/messages/Ping.java new file mode 100644 index 0000000000..3bc514cd56 --- /dev/null +++ b/http-api/src/main/java/net/runelite/http/api/ws/messages/Ping.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.api.ws.messages; + +import java.time.Instant; +import net.runelite.http.api.ws.WebsocketMessage; + +public class Ping extends WebsocketMessage +{ + private Instant time; + + public Instant getTime() + { + return time; + } + + public void setTime(Instant time) + { + this.time = time; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/account/AccountService.java b/http-service/src/main/java/net/runelite/http/service/account/AccountService.java index 6a5f69fd93..732a52d111 100644 --- a/http-service/src/main/java/net/runelite/http/service/account/AccountService.java +++ b/http-service/src/main/java/net/runelite/http/service/account/AccountService.java @@ -39,7 +39,11 @@ import java.io.IOException; import java.util.UUID; import java.util.concurrent.ExecutionException; import net.runelite.http.api.RuneliteAPI; -import net.runelite.http.api.account.LoginResponse; +import net.runelite.http.api.account.OAuthResponse; +import net.runelite.http.api.ws.messages.LoginResponse; +import net.runelite.http.service.ws.SessionManager; +import net.runelite.http.service.ws.WSService; +import net.runelite.http.service.ws.WSSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sql2o.Connection; @@ -117,7 +121,7 @@ public class AccountService } } - public LoginResponse login(Request request, Response response) + public OAuthResponse login(Request request, Response response) { UUID uuid = UUID.randomUUID(); @@ -135,7 +139,7 @@ public class AccountService String authorizationUrl = service.getAuthorizationUrl(); - LoginResponse lr = new LoginResponse(); + OAuthResponse lr = new OAuthResponse(); lr.setOauthUrl(authorizationUrl); lr.setUid(uuid); @@ -205,13 +209,33 @@ public class AccountService .addParameter("uuid", state.getUuid().toString()) .executeUpdate(); - logger.info("Created session for user {}", user.getUsername()); + logger.info("Created session for user {}", userInfo.getEmail()); } response.redirect(RL_REDIR); + + notifySession(state.getUuid(), userInfo.getEmail()); + return ""; } + private void notifySession(UUID uuid, String username) + { + WSSession session = SessionManager.findSession(uuid); + if (session == null) + { + logger.info("Session {} logged in - but no websocket session", uuid); + return; + } + + WSService service = session.getServlet(); + + LoginResponse response = new LoginResponse(); + response.setUsername(username); + + service.send(response); + } + public Object logout(Request request, Response response) { SessionEntry session = request.session().attribute("session"); diff --git a/http-service/src/main/java/net/runelite/http/service/ws/SessionManager.java b/http-service/src/main/java/net/runelite/http/service/ws/SessionManager.java new file mode 100644 index 0000000000..1f5b8847a2 --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/ws/SessionManager.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.ws; + +import com.google.common.base.Objects; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import javax.websocket.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SessionManager +{ + private static final Logger logger = LoggerFactory.getLogger(SessionManager.class); + + private static final Set sessions = Collections.synchronizedSet(new HashSet<>()); + + public static void add(WSService service, Session session) + { + WSSession wssession = new WSSession(service, session); + + logger.info("Adding service {} session {}", service, session); + + sessions.add(wssession); + } + + public static void remove(Session session) + { + WSSession wssession = new WSSession(null, session); + sessions.remove(wssession); + } + + public static WSSession findSession(UUID uuid) + { + synchronized (sessions) + { + for (WSSession session : sessions) + { + if (Objects.equal(session.getServlet().getUuid(), uuid)) + { + return session; + } + } + } + + return null; + } +} diff --git a/http-service/src/main/java/net/runelite/http/service/WSService.java b/http-service/src/main/java/net/runelite/http/service/ws/WSService.java similarity index 71% rename from http-service/src/main/java/net/runelite/http/service/WSService.java rename to http-service/src/main/java/net/runelite/http/service/ws/WSService.java index 7dc476f516..8a98da802e 100644 --- a/http-service/src/main/java/net/runelite/http/service/WSService.java +++ b/http-service/src/main/java/net/runelite/http/service/ws/WSService.java @@ -22,8 +22,10 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package net.runelite.http.service; +package net.runelite.http.service.ws; +import com.google.gson.Gson; +import java.util.UUID; import javax.websocket.CloseReason; import javax.websocket.EndpointConfig; import javax.websocket.OnClose; @@ -32,6 +34,9 @@ import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; +import net.runelite.http.api.ws.messages.Handshake; +import net.runelite.http.api.ws.WebsocketGsonFactory; +import net.runelite.http.api.ws.WebsocketMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,9 +45,30 @@ public class WSService { private static final Logger logger = LoggerFactory.getLogger(WSService.class); + private static final Gson gson = WebsocketGsonFactory.build(); + + private Session session; + private UUID uuid; + + public UUID getUuid() + { + return uuid; + } + + public void send(WebsocketMessage message) + { + String json = gson.toJson(message, WebsocketMessage.class); + + logger.debug("Sending {}", json); + + session.getAsyncRemote().sendText(json); + } + @OnOpen public void onOpen(Session session, EndpointConfig config) { + this.session = session; + SessionManager.add(this, session); logger.info("New session {}", session); } @@ -59,8 +85,15 @@ public class WSService } @OnMessage - public void onMessage(Session session, String message) + public void onMessage(Session session, String text) { + WebsocketMessage message = gson.fromJson(text, WebsocketMessage.class); logger.info("Got message: {}", message); + + if (message instanceof Handshake) + { + Handshake hs = (Handshake) message; + uuid = hs.getSession(); + } } } diff --git a/http-service/src/main/java/net/runelite/http/service/ws/WSSession.java b/http-service/src/main/java/net/runelite/http/service/ws/WSSession.java new file mode 100644 index 0000000000..9f615133ac --- /dev/null +++ b/http-service/src/main/java/net/runelite/http/service/ws/WSSession.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.http.service.ws; + +import java.util.Objects; +import javax.websocket.Session; + +public class WSSession +{ + private final WSService servlet; + private final Session session; + + public WSSession(WSService servlet, Session session) + { + this.servlet = servlet; + this.session = session; + } + + public WSService getServlet() + { + return servlet; + } + + public Session getSession() + { + return session; + } + + @Override + public int hashCode() + { + int hash = 3; + hash = 29 * hash + Objects.hashCode(this.session); + return hash; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null) + { + return false; + } + if (getClass() != obj.getClass()) + { + return false; + } + final WSSession other = (WSSession) obj; + if (!Objects.equals(this.session, other.session)) + { + return false; + } + return true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLite.java b/runelite-client/src/main/java/net/runelite/client/RuneLite.java index 7c55677948..8b61f25cf9 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -26,18 +26,25 @@ package net.runelite.client; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.SubscriberExceptionContext; +import com.google.gson.Gson; import java.awt.AWTException; import java.awt.Image; import java.awt.SystemTray; import java.awt.TrayIcon; import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; import java.io.IOException; +import java.io.InputStreamReader; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import javax.imageio.ImageIO; import joptsimple.OptionParser; import joptsimple.OptionSet; import net.runelite.api.Client; +import net.runelite.client.account.AccountSession; +import net.runelite.client.events.SessionOpen; import net.runelite.client.menus.MenuManager; import net.runelite.client.plugins.PluginManager; import net.runelite.client.ui.ClientUI; @@ -50,7 +57,7 @@ public class RuneLite private static final Logger logger = LoggerFactory.getLogger(RuneLite.class); public static final File RUNELITE_DIR = new File(System.getProperty("user.home"), ".runelite"); - public static final File REPO_DIR = new File(RUNELITE_DIR, "repository"); + public static final File SESSION_FILE = new File(RUNELITE_DIR, "session"); public static Image ICON; @@ -65,7 +72,9 @@ public class RuneLite private OverlayRenderer renderer; private EventBus eventBus = new EventBus(this::eventExceptionHandler); private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(4); - private final WSClient wsclient = new WSClient(); + private WSClient wsclient; + + private AccountSession accountSession; static { @@ -106,6 +115,8 @@ public class RuneLite pluginManager.loadAll(); renderer = new OverlayRenderer(); + + loadSession(); } private void setupTrayIcon() @@ -130,6 +141,79 @@ public class RuneLite } } + private void loadSession() + { + if (!SESSION_FILE.exists()) + { + logger.info("No session file exists"); + return; + } + + try (FileInputStream in = new FileInputStream(SESSION_FILE)) + { + accountSession = new Gson().fromJson(new InputStreamReader(in), AccountSession.class); + + logger.debug("Loaded session for {}", accountSession.getUsername()); + } + catch (Exception ex) + { + logger.warn("Unable to load session file", ex); + return; + } + + openSession(accountSession); + } + + public void saveSession() + { + if (accountSession == null) + { + return; + } + + try (FileWriter fw = new FileWriter(SESSION_FILE)) + { + new Gson().toJson(accountSession, fw); + + logger.debug("Saved session to {}", SESSION_FILE); + } + catch (IOException ex) + { + logger.warn("Unable to save session file", ex); + } + } + + /** + * Set the given session as the active session and open a socket to the + * server with the given session + * @param session + */ + public void openSession(AccountSession session) + { + boolean needExecutor = false; + + if (wsclient != null) + { + wsclient.close(); + } + else + { + needExecutor = true; + } + + wsclient = new WSClient(session); + wsclient.connect(); + + if (needExecutor) + { + executor.scheduleWithFixedDelay(wsclient::ping, WSClient.PING_TIME.getSeconds(), WSClient.PING_TIME.getSeconds(), TimeUnit.SECONDS); + } + + accountSession = session; + + eventBus.post(new SessionOpen()); + } + private void eventExceptionHandler(Throwable exception, SubscriberExceptionContext context) { logger.warn("uncaught exception in event subscriber", exception); @@ -189,4 +273,9 @@ public class RuneLite { return trayIcon; } + + public AccountSession getAccountSession() + { + return accountSession; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/WSClient.java b/runelite-client/src/main/java/net/runelite/client/WSClient.java index f3777f7398..f89d4f0d86 100644 --- a/runelite-client/src/main/java/net/runelite/client/WSClient.java +++ b/runelite-client/src/main/java/net/runelite/client/WSClient.java @@ -24,7 +24,16 @@ */ package net.runelite.client; +import com.google.common.eventbus.EventBus; +import com.google.gson.Gson; +import java.time.Duration; +import java.time.Instant; +import net.runelite.client.account.AccountSession; import net.runelite.http.api.RuneliteAPI; +import net.runelite.http.api.ws.messages.Handshake; +import net.runelite.http.api.ws.messages.Ping; +import net.runelite.http.api.ws.WebsocketGsonFactory; +import net.runelite.http.api.ws.WebsocketMessage; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; @@ -37,22 +46,47 @@ public class WSClient extends WebSocketListener implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(WSClient.class); + public static final Duration PING_TIME = Duration.ofSeconds(30); + + private static final Gson gson = WebsocketGsonFactory.build(); + private static final EventBus eventBus = RuneLite.getRunelite().getEventBus(); + private final OkHttpClient client = new OkHttpClient(); + private final AccountSession session; private WebSocket webSocket; - public WSClient() + public WSClient(AccountSession session) { - connect(); + this.session = session; } - private void connect() + public void connect() { Request request = new Request.Builder() .url(RuneliteAPI.getWsEndpoint()) .build(); webSocket = client.newWebSocket(request, this); + + Handshake handshake = new Handshake(); + handshake.setSession(session.getUuid()); + send(handshake); + } + + public void ping() + { + Ping ping = new Ping(); + ping.setTime(Instant.now()); + send(ping); + } + + public void send(WebsocketMessage message) + { + String json = gson.toJson(message, WebsocketMessage.class); + webSocket.send(json); + + logger.debug("Sent: {}", json); } @Override @@ -65,14 +99,15 @@ public class WSClient extends WebSocketListener implements AutoCloseable public void onOpen(WebSocket webSocket, Response response) { logger.info("Websocket {} opened", webSocket); - - webSocket.send("Hello"); } @Override public void onMessage(WebSocket webSocket, String text) { - logger.debug("Got message: {}", text); + WebsocketMessage message = gson.fromJson(text, WebsocketMessage.class); + logger.debug("Got message: {}", message); + + eventBus.post(message); } @Override diff --git a/runelite-client/src/main/java/net/runelite/client/account/AccountSession.java b/runelite-client/src/main/java/net/runelite/client/account/AccountSession.java new file mode 100644 index 0000000000..72fe8768c7 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/account/AccountSession.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.account; + +import java.time.Instant; +import java.util.UUID; + +public class AccountSession +{ + private UUID uuid; + private String username; + private Instant created; + + public UUID getUuid() + { + return uuid; + } + + public void setUuid(UUID uuid) + { + this.uuid = uuid; + } + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public Instant getCreated() + { + return created; + } + + public void setCreated(Instant created) + { + this.created = created; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/events/SessionOpen.java b/runelite-client/src/main/java/net/runelite/client/events/SessionOpen.java new file mode 100644 index 0000000000..ee9b01b4ad --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/events/SessionOpen.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.events; + +/** + * Called when a session has been opened with the server + * @author Adam + */ +public class SessionOpen +{ + +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java index 81ee3a1cea..9b76189696 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java @@ -32,6 +32,7 @@ import java.util.Collection; import java.util.List; import java.util.stream.Collectors; import net.runelite.client.RuneLite; +import net.runelite.client.plugins.account.AccountPlugin; import net.runelite.client.plugins.boosts.Boosts; import net.runelite.client.plugins.bosstimer.BossTimers; import net.runelite.client.plugins.clanchat.ClanChat; @@ -75,6 +76,7 @@ public class PluginManager plugins.add(new PestControl()); plugins.add(new ClanChat()); plugins.add(new Zulrah()); + plugins.add(new AccountPlugin()); if (RuneLite.getOptions().has("developer-mode")) { diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/account/AccountPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/account/AccountPlugin.java new file mode 100644 index 0000000000..19dbebc262 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/account/AccountPlugin.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2017, Adam + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.account; + +import com.google.common.eventbus.Subscribe; +import java.awt.Desktop; +import java.awt.event.ActionEvent; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Instant; +import java.util.concurrent.ScheduledExecutorService; +import javax.imageio.ImageIO; +import javax.swing.ImageIcon; +import net.runelite.client.RuneLite; +import net.runelite.client.account.AccountSession; +import net.runelite.client.events.SessionOpen; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.ui.ClientUI; +import net.runelite.client.ui.NavigationButton; +import net.runelite.client.util.RunnableExceptionLogger; +import net.runelite.http.api.account.LoginClient; +import net.runelite.http.api.account.OAuthResponse; +import net.runelite.http.api.ws.messages.LoginResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AccountPlugin extends Plugin +{ + private static final Logger logger = LoggerFactory.getLogger(AccountPlugin.class); + + private final RuneLite runelite = RuneLite.getRunelite(); + private final ClientUI ui = runelite.getGui(); + private final NavigationButton loginButton = new NavigationButton("Login"); + + private final LoginClient loginClient = new LoginClient(); + + @Override + protected void startUp() throws Exception + { + loginButton.getButton().addActionListener(this::loginClick); + + ImageIcon icon = new ImageIcon(ImageIO.read(getClass().getResourceAsStream("login_icon.png"))); + loginButton.getButton().setIcon(icon); + + ui.getNavigationPanel().addNavigation(loginButton); + } + + @Override + protected void shutDown() throws Exception + { + } + + private void loginClick(ActionEvent ae) + { + ScheduledExecutorService executor = runelite.getExecutor(); + executor.execute(RunnableExceptionLogger.wrap(this::openLoginPage)); + } + + private void openLoginPage() + { + OAuthResponse login; + + try + { + login = loginClient.login(); + } + catch (IOException ex) + { + logger.warn("Unable to get oauth url", ex); + return; + } + + // Create new session + AccountSession session = new AccountSession(); + session.setUuid(login.getUid()); + session.setCreated(Instant.now()); + + runelite.openSession(session); + + if (!Desktop.isDesktopSupported()) + { + logger.info("Desktop is not supported. Visit {}", login.getOauthUrl()); + return; + } + + Desktop desktop = Desktop.getDesktop(); + if (!desktop.isSupported(Desktop.Action.BROWSE)) + { + logger.info("Desktop browser is not supported. Visit {}", login.getOauthUrl()); + return; + } + + try + { + desktop.browse(new URI(login.getOauthUrl())); + + logger.debug("Opened browser to {}", login.getOauthUrl()); + } + catch (IOException | URISyntaxException ex) + { + logger.warn("Unable to open login page", ex); + } + } + + @Subscribe + public void onLogin(LoginResponse loginResponse) + { + logger.debug("Now logged in as {}", loginResponse.getUsername()); + + runelite.getGui().setTitle("RuneLite (" + loginResponse.getUsername() + ")"); + + AccountSession session = runelite.getAccountSession(); + session.setUsername(loginResponse.getUsername()); + + runelite.saveSession(); + } + + @Subscribe + public void onSessionOpen(SessionOpen sessionOpen) + { + AccountSession session = runelite.getAccountSession(); + + if (session.getUsername() == null) + { + return; // No username yet + } + + logger.debug("Session opened as {}", session.getUsername()); + + runelite.getGui().setTitle("RuneLite (" + session.getUsername() + ")"); + } + +} diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/account/login_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/account/login_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e14125e7e10d68950b3a50e2af0d9c4a79d83a2c GIT binary patch literal 463 zcmV;=0WkiFP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0c1%;K~y+Tjgs9< z!cZ8-`*Zugv8%!iLT~!8TtY@=6H=%wg1{)c=t_u9#W_C`EpN1Ax~9dMpR9N1Y5K&$f3)x0FWDe8v?5OD>)`l-aOs)Rk5aVkUrlZJ#(-{Vp|et*KeY)dsPm zp+ZyO#Kke7c91`@OzWxY4lpM$z?X&&5=*q6D&q>m0u&~o;Ez3F7K)o!4!YA#R=C^d zL^@DeKE8!W(iB{2359J_9nlb?PD~%K@ zodi;jc=gU)Te5WkDY&*T&O>oq^k*A!y2%Ov9}E2J$FYFgsr$vh05Td3U>qRKp`VJX z)u9_T%$#A2RPD2%Rm`oZ9w$(hmRt002ovPDHLk FV1k#F!7Ts) literal 0 HcmV?d00001