From c13574083ebe84a8f6e82d12b8815310b2487463 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Thu, 21 Jun 2018 08:55:14 +0200 Subject: [PATCH 001/142] Removed the entire source tree in preparation of v2.0 --- LICENSE | 202 --- README.md | 30 - client/pom.xml | 119 -- .../torchmind/stockpile/client/Stockpile.java | 150 --- common/pom.xml | 95 -- .../stockpile/data/v1/BlacklistResult.java | 84 -- .../stockpile/data/v1/NameLookupResult.java | 70 -- .../stockpile/data/v1/PlayerProfile.java | 178 --- .../stockpile/data/v1/ServerInformation.java | 120 -- .../torchmind/stockpile/data/v1/Version.java | 127 -- pom.xml | 357 ------ server/THIRDPARTY-LIBRARIES.md | 1109 ----------------- server/pom.xml | 308 ----- server/src/application.yml | 125 -- server/src/assembly/complete.xml | 52 - .../stockpile/server/StockpileServer.java | 43 - .../configuration/CacheConfiguration.java | 126 -- .../configuration/WebMvcConfiguration.java | 67 - .../CacheAggressivenessCondition.java | 43 - .../ConditionalOnCacheAggressiveness.java | 37 - .../controller/RestfulErrorController.java | 73 -- .../controller/v1/ApplicationController.java | 94 -- .../controller/v1/BlacklistController.java | 164 --- .../server/controller/v1/LoginController.java | 104 -- .../server/controller/v1/NameController.java | 97 -- .../controller/v1/ProfileController.java | 119 -- .../stockpile/server/entity/BaseEntity.java | 78 -- .../stockpile/server/entity/DisplayName.java | 67 - .../stockpile/server/entity/Profile.java | 140 --- .../server/entity/ProfileProperty.java | 110 -- .../repository/DisplayNameRepository.java | 66 - .../repository/ProfilePropertyRepository.java | 57 - .../entity/repository/ProfileRepository.java | 45 - .../InvalidProfileIdentifierException.java | 47 - .../server/error/NoSuchProfileException.java | 48 - .../server/error/ServiceException.java | 46 - .../error/TooManyRequestsException.java | 47 - .../server/service/StorageCleanupService.java | 257 ---- .../server/service/api/MojangUUID.java | 84 -- .../server/service/api/ProfileService.java | 376 ------ server/src/main/resources/banner.txt | 10 - server/src/main/resources/favicon.ico | 3 - server/src/main/resources/log4j2.xml | 60 - server/src/main/resources/static/index.html | 164 --- server/src/main/resources/static/logo.svg | 130 -- .../server/service/api/MojangUUIDTest.java | 58 - 46 files changed, 5986 deletions(-) delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 client/pom.xml delete mode 100644 client/src/main/java/com/torchmind/stockpile/client/Stockpile.java delete mode 100644 common/pom.xml delete mode 100644 common/src/main/java/com/torchmind/stockpile/data/v1/BlacklistResult.java delete mode 100644 common/src/main/java/com/torchmind/stockpile/data/v1/NameLookupResult.java delete mode 100644 common/src/main/java/com/torchmind/stockpile/data/v1/PlayerProfile.java delete mode 100644 common/src/main/java/com/torchmind/stockpile/data/v1/ServerInformation.java delete mode 100644 common/src/main/java/com/torchmind/stockpile/data/v1/Version.java delete mode 100644 pom.xml delete mode 100644 server/THIRDPARTY-LIBRARIES.md delete mode 100644 server/pom.xml delete mode 100644 server/src/application.yml delete mode 100644 server/src/assembly/complete.xml delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/StockpileServer.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/configuration/CacheConfiguration.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/configuration/WebMvcConfiguration.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/configuration/condition/CacheAggressivenessCondition.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/configuration/condition/ConditionalOnCacheAggressiveness.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/controller/RestfulErrorController.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/controller/v1/ApplicationController.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/controller/v1/BlacklistController.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/controller/v1/LoginController.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/controller/v1/NameController.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/controller/v1/ProfileController.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/entity/BaseEntity.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/entity/DisplayName.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/entity/Profile.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/entity/ProfileProperty.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/entity/repository/DisplayNameRepository.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/entity/repository/ProfilePropertyRepository.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/entity/repository/ProfileRepository.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/error/InvalidProfileIdentifierException.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/error/NoSuchProfileException.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/error/ServiceException.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/error/TooManyRequestsException.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/service/StorageCleanupService.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/service/api/MojangUUID.java delete mode 100644 server/src/main/java/com/torchmind/stockpile/server/service/api/ProfileService.java delete mode 100644 server/src/main/resources/banner.txt delete mode 100644 server/src/main/resources/favicon.ico delete mode 100644 server/src/main/resources/log4j2.xml delete mode 100644 server/src/main/resources/static/index.html delete mode 100644 server/src/main/resources/static/logo.svg delete mode 100644 server/src/test/java/com/torchmind/stockpile/server/service/api/MojangUUIDTest.java diff --git a/LICENSE b/LICENSE deleted file mode 100644 index d645695..0000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. diff --git a/README.md b/README.md deleted file mode 100644 index b0ad1cb..0000000 --- a/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Stockpile ![State](https://img.shields.io/badge/state-prototype-orange.svg) [![Latest Tag](https://img.shields.io/github/release/lordakkarin/stockpile.svg)](https://github.com/LordAkkarin/Stockpile/releases) ![Latest Version](https://img.shields.io/maven-central/v/com.torchmind.lithium/parent.svg) [![Gitter](https://badges.gitter.im/LordAkkarin/Stockpile.svg)](https://gitter.im/LordAkkarin/Stockpile?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - -Stockpile is a lightweight caching server for the Minecraft API. - -### Key Features - -* **Customizable Caching Levels** - allow you to tailor the application to your needs while ensuring you stay well below the rate limit. -* **Comprehensive Java API** - -## Need Help? - -The [official documentation][wiki] has help articles and specifications on the implementation. If, however, you still -require assistance with the application, you are welcome to join our [Gitter Channel][gitter] and ask veteran users and -developers. Make sure to include a detailed description of your problem when asking questions though: - -1. Include a complete error message along with its stack trace when applicable. -2. Describe the expected result. -3. Describe the actual result when different from the expected result. - -[wiki]: https://github.com/LordAkkarin/Stockpile/wiki - -## Contributing - -See [CONTRIBUTING.md](CONTRIBUTING.md) for information on working on Stockpile and submitting patches. Related projects -are listed on the [Implementations wiki page][implementations]. You can also join the [project's chat room][gitter] to -discuss future improvements or to get your custom implementation listed. - -[implementations]: https://github.com/LordAkkarin/Stockpile/wiki/Implementations -[gitter]: https://gitter.im/LordAkkarin/Stockpile diff --git a/client/pom.xml b/client/pom.xml deleted file mode 100644 index a2c3f65..0000000 --- a/client/pom.xml +++ /dev/null @@ -1,119 +0,0 @@ - - - - 4.0.0 - - - - parent - com.torchmind.stockpile - 1.0-SNAPSHOT - - - - client - jar - - - Stockpile Client - Provides a client implementation for the Stockpile caching server implementation. - - - - - - com.torchmind.stockpile - common - - - - - com.squareup.retrofit - retrofit - - - com.squareup.retrofit - converter-jackson - - - com.squareup.okhttp - okhttp - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - - - - - com.google.code.findbugs - jsr305 - - - - - junit - junit - - - org.mockito - mockito-core - - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - - org.apache.felix - maven-bundle-plugin - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - - - org.apache.maven.plugins - maven-source-plugin - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - - diff --git a/client/src/main/java/com/torchmind/stockpile/client/Stockpile.java b/client/src/main/java/com/torchmind/stockpile/client/Stockpile.java deleted file mode 100644 index 4aca6bf..0000000 --- a/client/src/main/java/com/torchmind/stockpile/client/Stockpile.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.client; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.torchmind.stockpile.data.v1.BlacklistResult; -import com.torchmind.stockpile.data.v1.NameLookupResult; -import com.torchmind.stockpile.data.v1.PlayerProfile; -import com.torchmind.stockpile.data.v1.ServerInformation; -import retrofit.Call; -import retrofit.JacksonConverterFactory; -import retrofit.Retrofit; -import retrofit.http.*; - -import javax.annotation.Nonnull; -import java.util.UUID; - -/** - * Stockpile Client - * - * Provides a simple method of accessing the Stockpile API. - * - * @author Johannes Donath - */ -public interface Stockpile { - - /** - * Creates a client instance for the specified base URL. - * - * @param baseUrl a base URL. - * @return a client instance. - */ - @Nonnull - static Stockpile create(@Nonnull String baseUrl) { - ObjectMapper mapper = new ObjectMapper(); - mapper.findAndRegisterModules(); - - Retrofit retrofit = (new Retrofit.Builder()) - .addConverterFactory(JacksonConverterFactory.create(mapper)) - .baseUrl(baseUrl) - .build(); - - return retrofit.create(Stockpile.class); - } - - /** - * Retrieves the current server and API version. - * - * @return a version. - */ - @Nonnull - @GET("/v1") - Call getServerInformation(); - - /** - * Instructs the server to handle a server handshake on behalf of the requesting server in order to update local - * profile records. - * - * @param username a username. - * @param serverId a server identifier. - * @return a player profile. - */ - @Nonnull - @GET("/v1/login") - Call login(@Nonnull @Query(value = "username", encoded = true) String username, @Nonnull @Query(value = "serverId", encoded = true) String serverId); - - /** - * Checks a hostname against the server blacklist. - * - * @param hostname a hostname. - * @return a blacklist result. - */ - @Nonnull - @POST("/v1/blacklist") - Call lookupBlacklistEntry(@Nonnull @Query(value = "hostname", encoded = true) String hostname); - - /** - * Looks up the UUID which corresponds to the supplied name. - * - * @param name a name. - * @return a lookup result. - */ - @Nonnull - @GET("/v1/name/{name}") - Call lookupName(@Nonnull @Path(value = "name", encoded = true) String name); - - /** - * Looks up a profile based on its identifier. - * - * @param identifier an identifier. - * @return a profile. - */ - @Nonnull - @GET("/v1/profile/{identifier}") - Call lookupProfile(@Nonnull @Path(value = "identifier", encoded = true) UUID identifier); - - /** - * Looks up a profile based on its name. - * - * @param name a name. - * @return a profile. - */ - @Nonnull - @GET("/v1/profile/{name}") - Call lookupProfile(@Nonnull @Path(value = "name", encoded = true) String name); - - /** - * Purges a display name from the server. - * - * @param name a name. - */ - @DELETE("/v1/name/{name}") - void purgeName(@Nonnull @Path(value = "name", encoded = true) String name); - - /** - * Purges a player profile from the server. - * - * @param identifier a profile identifier. - */ - @DELETE("/v1/profile/{identifier}") - void purgeProfile(@Nonnull @Path(value = "identifier", encoded = true) UUID identifier); - - /** - * Purges a player profile from the server using its display name. - * - * @param name a name. - */ - @DELETE("/v1/profile/{name}") - void purgeProfile(@Nonnull @Path(value = "name", encoded = true) String name); - - /** - * Requests the server to shut down gracefully. - */ - @POST("/v1/shutdown") - void requestShutdown(); -} diff --git a/common/pom.xml b/common/pom.xml deleted file mode 100644 index 3656faf..0000000 --- a/common/pom.xml +++ /dev/null @@ -1,95 +0,0 @@ - - - - 4.0.0 - - - - parent - com.torchmind.stockpile - 1.0-SNAPSHOT - - - - common - jar - - - Stockpile Common - Provides common representations for both Stockpile clients and servers. - - - - - - com.google.code.findbugs - jsr305 - - - - - junit - junit - - - org.mockito - mockito-core - - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - - org.apache.felix - maven-bundle-plugin - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - - - org.apache.maven.plugins - maven-source-plugin - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - - diff --git a/common/src/main/java/com/torchmind/stockpile/data/v1/BlacklistResult.java b/common/src/main/java/com/torchmind/stockpile/data/v1/BlacklistResult.java deleted file mode 100644 index 9176645..0000000 --- a/common/src/main/java/com/torchmind/stockpile/data/v1/BlacklistResult.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.data.v1; - -import javax.annotation.Nonnull; -import javax.annotation.concurrent.Immutable; -import javax.annotation.concurrent.ThreadSafe; -import java.util.Objects; - -/** - * Blacklist Result - * - * Represents the answer to a blacklist check. - * - * @author Johannes Donath - */ -@Immutable -@ThreadSafe -public class BlacklistResult { - private final boolean blacklisted; - private final String hostname; - - private BlacklistResult() { - this("*.example.org", false); - } - - public BlacklistResult(@Nonnull String hostname, boolean blacklisted) { - this.hostname = hostname; - this.blacklisted = blacklisted; - } - - /** - * Retrieves the blacklisted hostname as stored in the database. - * - * @return a hostname (may include wildcards). - */ - @Nonnull - public String getHostname() { - return hostname; - } - - /** - * Checks whether the host is blacklisted. - * - * @return true if blacklisted, false otherwise. - */ - public boolean isBlacklisted() { - return blacklisted; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || this.getClass() != o.getClass()) return false; - - BlacklistResult that = (BlacklistResult) o; - return this.blacklisted == that.blacklisted && Objects.equals(this.hostname, that.hostname); - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return Objects.hash(this.blacklisted, this.hostname); - } -} diff --git a/common/src/main/java/com/torchmind/stockpile/data/v1/NameLookupResult.java b/common/src/main/java/com/torchmind/stockpile/data/v1/NameLookupResult.java deleted file mode 100644 index 2d14e89..0000000 --- a/common/src/main/java/com/torchmind/stockpile/data/v1/NameLookupResult.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.data.v1; - -import javax.annotation.Nonnull; -import java.util.Objects; -import java.util.UUID; - -/** - * Name Lookup Result - * - * Represents a name lookup result which contains the latest known identifier. - * - * @author Johannes Donath - */ -public class NameLookupResult { - private final UUID identifier; - - @SuppressWarnings("ConstantConditions") - private NameLookupResult() { - this(null); - } - - public NameLookupResult(@Nonnull UUID identifier) { - this.identifier = identifier; - } - - /** - * Retrieves the last known associated identifier. - * @return an identifier. - */ - @Nonnull - public UUID getIdentifier() { - return this.identifier; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || this.getClass() != o.getClass()) return false; - - NameLookupResult that = (NameLookupResult) o; - return Objects.equals(this.identifier, that.identifier); - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return Objects.hash(this.identifier); - } -} diff --git a/common/src/main/java/com/torchmind/stockpile/data/v1/PlayerProfile.java b/common/src/main/java/com/torchmind/stockpile/data/v1/PlayerProfile.java deleted file mode 100644 index c2c0f9b..0000000 --- a/common/src/main/java/com/torchmind/stockpile/data/v1/PlayerProfile.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.data.v1; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import javax.annotation.concurrent.Immutable; -import javax.annotation.concurrent.ThreadSafe; -import java.time.Instant; -import java.util.*; - -/** - * Player Profile - * - * Represents a (possibly cached) player profile. - * - * @author Johannes Donath - */ -@Immutable -@ThreadSafe -public class PlayerProfile { - private final Instant cacheTimestamp; - private final UUID identifier; - private final String name; - private final List properties; - - private PlayerProfile() { - this(new UUID(0, 0), "Invalid", Collections.emptyList(), null); - } - - public PlayerProfile(@Nonnull UUID identifier, @Nonnull String name, @Nonnull List properties, @Nonnull Instant cacheTimestamp) { - this.identifier = identifier; - this.name = name; - this.properties = properties; - this.cacheTimestamp = cacheTimestamp; - } - - /** - * Searches a property within the profile record. - * - * @param name a profile name. - * @return a property or, if no property with the supplied name was found within the record, an empty optional. - */ - @Nonnull - public Optional findProperty(@Nonnull String name) { - return this.properties.stream().filter((p) -> p.getName().equals(name)).findAny(); - } - - /** - * Retrieves the timestamp this profile was last updated at. - * Note: This timestamp will never be more than 30 days in the past. - * - * @return a timestamp. - */ - @Nonnull - public Instant getCacheTimestamp() { - return this.cacheTimestamp; - } - - /** - * Retrieves the profile's globally unique identifier. - * - * @return an identifier. - */ - @Nonnull - public UUID getIdentifier() { - return this.identifier; - } - - /** - * Retrieves the profile's (cached) display name. - * - * @return a display name. - */ - @Nonnull - public String getName() { - return this.name; - } - - /** - * Retrieves a list of properties which have been assigned to the profile. - * - * @return an immutable set of properties. - */ - @Nonnull - public List getProperties() { - return Collections.unmodifiableList(this.properties); - } - - /** - * Player Profile Property - * - * Represents a single property within a player profile such as a skin texture. - */ - @Immutable - @ThreadSafe - public static class Property { - private final String name; - private final String signature; - private final String value; - - private Property() { - this("Invalid", "", null); - } - - public Property(@Nonnull String name, @Nonnull String value, @Nullable String signature) { - this.name = name; - this.value = value; - this.signature = signature; - } - - /** - * Retrieves the property's name. - * - * @return a name. - */ - @Nonnull - public String getName() { - return this.name; - } - - /** - * Retrieves the property's signature (if supplied). - * - * @return a signature or, if no value was cached, an empty optional. - */ - @Nullable - public String getSignature() { - return this.signature; - } - - /** - * Retrieves the property's value. - * - * @return a value. - */ - @Nonnull - public String getValue() { - return this.value; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || this.getClass() != o.getClass()) return false; - - Property property = (Property) o; - return Objects.equals(this.name, property.name) && - Objects.equals(this.signature, property.signature) && - Objects.equals(this.value, property.value); - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return Objects.hash(this.name, this.signature, this.value); - } - } -} diff --git a/common/src/main/java/com/torchmind/stockpile/data/v1/ServerInformation.java b/common/src/main/java/com/torchmind/stockpile/data/v1/ServerInformation.java deleted file mode 100644 index a1fc3ee..0000000 --- a/common/src/main/java/com/torchmind/stockpile/data/v1/ServerInformation.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.data.v1; - -import javax.annotation.Nonnull; -import javax.annotation.concurrent.Immutable; -import javax.annotation.concurrent.ThreadSafe; -import java.util.Objects; - -/** - * Server Information - * - * Provides basic environment information of the server. - * - * @author Johannes Donath - */ -@Immutable -@ThreadSafe -public class ServerInformation { - private final Version api; - private final String name; - private final String vendor; - private final String version; - - private ServerInformation() { - this(new Version(0, Version.State.DEPRECATED)); - } - - public ServerInformation(@Nonnull Version api) { - this("Stockpile", "Unknown", "Torchmind", api); - } - - public ServerInformation(String name, String version, String vendor, Version api) { - this.name = name; - this.version = version; - this.vendor = vendor; - this.api = api; - } - - public ServerInformation(@Nonnull Package p, @Nonnull Version api) { - this(p.getImplementationTitle(), p.getImplementationVersion(), p.getImplementationVendor(), api); - } - - /** - * Retrieves the API version. - * - * @return a version. - */ - @Nonnull - public Version getApi() { - return this.api; - } - - /** - * Retrieves the server's implementation name. - * - * @return an implementation name. - */ - @Nonnull - public String getName() { - return this.name; - } - - /** - * Retrieves the server's vendor. - * - * @return a vendor name. - */ - @Nonnull - public String getVendor() { - return this.vendor; - } - - /** - * Retrieves the server's implementation version. - * - * @return an implementation version. - */ - @Nonnull - public String getVersion() { - return this.version; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || this.getClass() != o.getClass()) return false; - ServerInformation that = (ServerInformation) o; - - return Objects.equals(this.name, that.name) && - Objects.equals(this.version, that.version) && - Objects.equals(this.vendor, that.vendor) && - Objects.equals(this.api, that.api); - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return Objects.hash(this.name, this.version, this.vendor, this.api); - } -} diff --git a/common/src/main/java/com/torchmind/stockpile/data/v1/Version.java b/common/src/main/java/com/torchmind/stockpile/data/v1/Version.java deleted file mode 100644 index f3feb26..0000000 --- a/common/src/main/java/com/torchmind/stockpile/data/v1/Version.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.data.v1; - -import javax.annotation.Nonnull; -import javax.annotation.concurrent.Immutable; -import javax.annotation.concurrent.ThreadSafe; -import java.util.Objects; - -/** - * Version - * - * Represents the Stockpile API version as well as its state. - * - * @author Johannes Donath - */ -@Immutable -@ThreadSafe -public class Version { - private final State state; - private final int version; - - private Version() { - this.state = State.DEPRECATED; - this.version = 0; - } - - public Version(int version, State state) { - this.version = version; - this.state = state; - } - - /** - * Retrieves the API state. - * - * @return a state. - * - * @see State for more information on API version states. - */ - @Nonnull - public State getState() { - return state; - } - - /** - * Retrieves the numeric version representation. - * - * The numeric version is bumped whenever a new feature is added or a backwards incompatible change is made. - * A server may additionally support multiple versions. - * - * @return a numeric version. - */ - public int getVersion() { - return version; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || this.getClass() != o.getClass()) return false; - Version version1 = (Version) o; - - return this.version == version1.version && this.state == version1.state; - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return Objects.hash(this.state, this.version); - } - - /** - * API Version State - * - * Provides a list of valid API version states. - */ - public enum State { - - /** - * Development - * - * Signifies that the API is still in development and thus considered unstable (e.g. it may change at - * any time and is more likely to produce errors or false results in edge cases). - */ - DEVELOPMENT, - - /** - * Stable - * - * Signifies that the API has been finalized and is thus considered stable (e.g. it will not be changed - * anymore and is unlikely to produce errors or false results in edge cases). - */ - STABLE, - - /** - * Deprecated - * - * Signifies that the API has been deprecated and thus clients should refrain from using it. - * - * Deprecated API versions may be removed in future versions of the Stockpile server implementation and - * may thus cause clients to not be able to connect to the server when an update is installed. - * - * Generally client implementors are expected to warn users of this circumstance when initializing to - * ensure updates are installed in a timely manner. - */ - DEPRECATED - } -} diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 208b7ac..0000000 --- a/pom.xml +++ /dev/null @@ -1,357 +0,0 @@ - - - - 4.0.0 - - - com.torchmind.stockpile - parent - 1.0-SNAPSHOT - pom - - - 3.3 - - - - UTF-8 - - - - - Apache License 2.0 - https://www.apache.org/licenses/LICENSE-2.0.txt - - - - - Stockpile - Provides a profile caching service for the Mojang Profile API. - https://github.com/LordAkkarin/Stockpile - 2016 - - - Torchmind - https://www.torchmind.com - - - - - Johannes Donath - Europe/Berlin - johannesd@torchmind.com - https://www.johannes-donath.com - - Torchmind - https://www.torchmind.com - - - - - master - https://github.com/LordAkkarin/Stockpile - scm:git:git@github.com:/LordAkkarin/Stockpile.git - scm:git:git@github.com:/LordAkkarin/Stockpile.git - - - - GitHub - https://github.com/LordAkkarin/Stockpile/issues - - - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot - - - true - - - - - - - - - com.torchmind.stockpile - common - 1.0-SNAPSHOT - - - - - com.squareup.retrofit - retrofit - 2.0.0-beta2 - - - com.squareup.retrofit - converter-jackson - 2.0.0-beta2 - - - com.squareup.okhttp - okhttp - 2.7.5 - - - - - io.spring.platform - platform-bom - 2.1.0.BUILD-SNAPSHOT - pom - import - - - - - com.google.code.findbugs - jsr305 - 3.0.1 - provided - - - - - com.google.guava - guava - 19.0 - - - - - junit - junit - 4.12 - test - - - org.mockito - mockito-core - 2.0.41-beta - test - - - - - - - ${project.groupId}.${project.artifactId}-${project.version} - clean install - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.5 - - - 1.8 - 1.8 - - - - - - org.apache.felix - maven-bundle-plugin - 3.0.1 - - - - bundle-manifest - process-classes - - - manifest - - - - - - - - org.apache.maven.plugins - maven-jar-plugin - 2.6 - - - - true - true - true - - - true - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - 1.3.5.RELEASE - - - - - repackage - - - - - - - - org.apache.maven.plugins - maven-source-plugin - 2.4 - - - - attach-sources - - - jar-no-fork - - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.10.3 - - - - attach-javadocs - - - jar - - - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.5 - - - - sign-artifacts - verify - - - sign - - - - - - 35578E37 - - - - - - com.akathist.maven.plugins.launch4j - launch4j-maven-plugin - 1.7.10 - - - - - org.apache.maven.plugins - maven-assembly-plugin - 2.6 - - - - make-assembly - package - - - single - - - - - - - - org.apache.maven.plugins - maven-install-plugin - 2.5.2 - - - - - org.apache.maven.plugins - maven-deploy-plugin - 2.8.2 - - - - - - - client - common - server - - - - - - deployment - - - - - org.apache.maven.plugins - maven-gpg-plugin - - - - - - diff --git a/server/THIRDPARTY-LIBRARIES.md b/server/THIRDPARTY-LIBRARIES.md deleted file mode 100644 index 13e0b36..0000000 --- a/server/THIRDPARTY-LIBRARIES.md +++ /dev/null @@ -1,1109 +0,0 @@ -Third Party Libraries -===================== - -Stockpile includes a set of libraries developed by third parties which are licensed under their very own open-source -licenses. We did not alter any of the included dependencies. All of them are packaged in their original binary forms as -provided through the maven central repository or their respective official repositories. - -The following list provides an overview of all included libraries and frameworks along with their respective licenses (a -detailed list of contributors can be found on the respective project's IP log): - -Public Domain -------------- - -* ANTLR 2 - -Apache License --------------- - -* Spring Framework -* Classmate -* geronimo-jta -* Guava -* Hibernate -* Hikari -* Jackson -* Jandex -* JBoss Logging -* Jetty -* Findbugs JSR 305 -* log4j -* SnakeYAML - -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. -``` - -BSD License (4-Clause) ----------------------- - -* Stax 2 - -``` -Copyright (c) , -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. -3. All advertising materials mentioning features or use of this software - must display the following acknowledgement: - This product includes software developed by the . -4. Neither the name of the nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY ''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 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. -``` - -ASM ---- - -``` -Copyright (c) 2000-2011 INRIA, France Telecom -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. - -3. Neither the name of the copyright holders nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -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 TH -``` - -Eclipse Public License -------- - -* AspectJ -* Jetty - -``` -THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION -OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - -a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and - -b) in the case of each subsequent Contributor: - -i) changes to the Program, and - -ii) additions to the Program; - -where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A -Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting -on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of -software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative -works of the Program. - -"Contributor" means any person or entity that distributes the Program. - -"Licensed Patents " mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of -its Contribution alone or when combined with the Program. - -"Program" means the Contributions distributed in accordance with this Agreement. - -"Recipient" means anyone who receives the Program under this Agreement, including all Contributors. - -2. GRANT OF RIGHTS - -a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, -royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute -and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code -form. - -b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, -royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the -Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the -combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such -addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not -apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. - -c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no -assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property -rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity -based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses -granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights -needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, -it is Recipient's responsibility to acquire that license before distributing the Program. - -d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to -grant the copyright license set forth in this Agreement. - -3. REQUIREMENTS - -A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: - -a) it complies with the terms and conditions of this Agreement; and - -b) its license agreement: - -i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including -warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and -fitness for a particular purpose; - -ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, -incidental and consequential damages, such as lost profits; - -iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any -other party; and - -iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it -in a reasonable manner on or through a medium customarily used for software exchange. - -When the Program is made available in source code form: - -a) it must be made available under this Agreement; and - -b) a copy of this Agreement must be included with each copy of the Program. - -Contributors may not remove or alter any copyright notices contained within the Program. - -Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows -subsequent Recipients to identify the originator of the Contribution. - -4. COMMERCIAL DISTRIBUTION - -Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and -the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes -the Program in a commercial product offering should do so in a manner which does not create potential liability for -other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor -("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") -against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions -brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such -Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The -obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property -infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in -writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor -in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim -at its own expense. - -For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is -then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties -related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. -Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to -those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, -the Commercial Contributor must pay those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR -CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, -NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for -determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise -of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with -applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), -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 OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED -HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or -enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such -provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) -alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such -Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such -litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or -conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such -noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution -of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses -granted by Recipient relating to the Program shall continue and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement -is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new -versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the -right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may -assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the -Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed -subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement -is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. -Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual -property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights -in the Program not expressly granted under this Agreement are reserved. - -This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States -of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause -of action arose. Each party waives its rights to a jury trial in any resulting litigation. -``` - -Dom4j ------ - -``` -Copyright 2001-2016 (C) MetaStuff, Ltd. and DOM4J contributors. All Rights Reserved. - -Redistribution and use of this software and associated documentation -("Software"), with or without modification, are permitted provided -that the following conditions are met: - -1. Redistributions of source code must retain copyright - statements and notices. Redistributions must also contain a - copy of this document. - -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. - -3. The name "DOM4J" must not be used to endorse or promote - products derived from this Software without prior written - permission of MetaStuff, Ltd. For written permission, - please contact dom4j-info@metastuff.com. - -4. Products derived from this Software may not be called "DOM4J" - nor may "DOM4J" appear in their names without prior written - permission of MetaStuff, Ltd. DOM4J is a registered - trademark of MetaStuff, Ltd. - -5. Due credit should be given to the DOM4J Project - https://dom4j.github.com/ - -THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS -``AS IS'' AND ANY EXPRESSED 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 -METASTUFF, LTD. OR ITS 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. -``` - -H2 --- - -``` -This software contains unmodified binary redistributions for -H2 database engine (http://www.h2database.com/), -which is dual licensed and available under the MPL 2.0 -(Mozilla Public License) or under the EPL 1.0 (Eclipse Public License). -An original copy of the license agreement can be found at: -http://www.h2database.com/html/license.html -``` - -GNU Lesser General Public License ---------------------------------- - -* Hibernate - -``` - GNU LESSER GENERAL PUBLIC LICENSE - Version 2.1, February 1999 - - Copyright (C) 1991, 1999 Free Software Foundation, Inc. - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts - as the successor of the GNU Library Public License, version 2, hence - the version number 2.1.] - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - - This license, the Lesser General Public License, applies to some -specially designated software packages--typically libraries--of the -Free Software Foundation and other authors who decide to use it. You -can use it too, but we suggest you first think carefully about whether -this license or the ordinary General Public License is the better -strategy to use in any particular case, based on the explanations below. - - When we speak of free software, we are referring to freedom of use, -not price. Our General Public Licenses are designed to make sure that -you have the freedom to distribute copies of free software (and charge -for this service if you wish); that you receive source code or can get -it if you want it; that you can change the software and use pieces of -it in new free programs; and that you are informed that you can do -these things. - - To protect your rights, we need to make restrictions that forbid -distributors to deny you these rights or to ask you to surrender these -rights. These restrictions translate to certain responsibilities for -you if you distribute copies of the library or if you modify it. - - For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link other code with the library, you must provide -complete object files to the recipients, so that they can relink them -with the library after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - - We protect your rights with a two-step method: (1) we copyright the -library, and (2) we offer you this license, which gives you legal -permission to copy, distribute and/or modify the library. - - To protect each distributor, we want to make it very clear that -there is no warranty for the free library. Also, if the library is -modified by someone else and passed on, the recipients should know -that what they have is not the original version, so that the original -author's reputation will not be affected by problems that might be -introduced by others. - - Finally, software patents pose a constant threat to the existence of -any free program. We wish to make sure that a company cannot -effectively restrict the users of a free program by obtaining a -restrictive license from a patent holder. Therefore, we insist that -any patent license obtained for a version of the library must be -consistent with the full freedom of use specified in this license. - - Most GNU software, including some libraries, is covered by the -ordinary GNU General Public License. This license, the GNU Lesser -General Public License, applies to certain designated libraries, and -is quite different from the ordinary General Public License. We use -this license for certain libraries in order to permit linking those -libraries into non-free programs. - - When a program is linked with a library, whether statically or using -a shared library, the combination of the two is legally speaking a -combined work, a derivative of the original library. The ordinary -General Public License therefore permits such linking only if the -entire combination fits its criteria of freedom. The Lesser General -Public License permits more lax criteria for linking other code with -the library. - - We call this license the "Lesser" General Public License because it -does Less to protect the user's freedom than the ordinary General -Public License. It also provides other free software developers Less -of an advantage over competing non-free programs. These disadvantages -are the reason we use the ordinary General Public License for many -libraries. However, the Lesser license provides advantages in certain -special circumstances. - - For example, on rare occasions, there may be a special need to -encourage the widest possible use of a certain library, so that it becomes -a de-facto standard. To achieve this, non-free programs must be -allowed to use the library. A more frequent case is that a free -library does the same job as widely used non-free libraries. In this -case, there is little to gain by limiting the free library to free -software only, so we use the Lesser General Public License. - - In other cases, permission to use a particular library in non-free -programs enables a greater number of people to use a large body of -free software. For example, permission to use the GNU C Library in -non-free programs enables many more people to use the whole GNU -operating system, as well as its variant, the GNU/Linux operating -system. - - Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is -linked with the Library has the freedom and the wherewithal to run -that program using a modified version of the Library. - - The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -"work based on the library" and a "work that uses the library". The -former contains code derived from the library, whereas the latter must -be combined with the library in order to run. - - GNU LESSER GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called "this License"). -Each licensee is addressed as "you". - - A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - - The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - - "Source code" for a work means the preferred form of the work for -making modifications to it. For a library, complete source code means -all the source code for all modules it contains, plus any associated -interface definition files, plus the scripts used to control compilation -and installation of the library. - - Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - - 1. You may copy and distribute verbatim copies of the Library's -complete source code as you receive it, in any medium, provided that -you conspicuously and appropriately publish on each copy an -appropriate copyright notice and disclaimer of warranty; keep intact -all the notices that refer to this License and to the absence of any -warranty; and distribute a copy of this License along with the -Library. - - You may charge a fee for the physical act of transferring a copy, -and you may at your option offer warranty protection in exchange for a -fee. - - 2. You may modify your copy or copies of the Library or any portion -of it, thus forming a work based on the Library, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices - stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no - charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a - table of data to be supplied by an application program that uses - the facility, other than as an argument passed when the facility - is invoked, then you must make a good faith effort to ensure that, - in the event an application does not supply such function or - table, the facility still operates, and performs whatever part of - its purpose remains meaningful. - - (For example, a function in a library to compute square roots has - a purpose that is entirely well-defined independent of the - application. Therefore, Subsection 2d requires that any - application-supplied function or table used by this function must - be optional: if the application does not supply it, the square - root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Library, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote -it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - - Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - - This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - - 4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you accompany -it with the complete corresponding machine-readable source code, which -must be distributed under the terms of Sections 1 and 2 above on a -medium customarily used for software interchange. - - If distribution of object code is made by offering access to copy -from a designated place, then offering equivalent access to copy the -source code from the same place satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - - 5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - - However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - - When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - - If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - - Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - - 6. As an exception to the Sections above, you may also combine or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - - You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - - a) Accompany the work with the complete corresponding - machine-readable source code for the Library including whatever - changes were used in the work (which must be distributed under - Sections 1 and 2 above); and, if the work is an executable linked - with the Library, with the complete machine-readable "work that - uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified - executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the - Library will not necessarily be able to recompile the application - to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (1) uses at run time a - copy of the library already present on the user's computer system, - rather than copying library functions into the executable, and (2) - will operate properly with a modified version of the library, if - the user installs one, as long as the modified version is - interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at - least three years, to give the same user the materials - specified in Subsection 6a, above, for a charge no more - than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy - from a designated place, offer equivalent access to copy the above - specified materials from the same place. - - e) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. - - For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be distributed need not include anything that is -normally distributed (in either source or binary form) with the major -components (compiler, kernel, and so on) of the operating system on -which the executable runs, unless that component itself accompanies -the executable. - - It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - - 7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities. This must be distributed under the terms of the - Sections above. - - b) Give prominent notice with the combined library of the fact - that part of it is a work based on the Library, and explaining - where to find the accompanying uncombined form of the same work. - - 8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library is void, and will automatically terminate your -rights under this License. However, parties who have received copies, -or rights, from you under this License will not have their licenses -terminated so long as such parties remain in full compliance. - - 9. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - - 10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -subject to these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties with -this License. - - 11. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any -particular circumstance, the balance of the section is intended to apply, -and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library under this License may add -an explicit geographical distribution limitation excluding those countries, -so that distribution is permitted only in or among countries not thus -excluded. In such case, this License incorporates the limitation as if -written in the body of this License. - - 13. The Free Software Foundation may publish revised and/or new -versions of the Lesser General Public License from time to time. -Such new versions will be similar in spirit to the present version, -but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library -specifies a version number of this License which applies to it and -"any later version", you have the option of following the terms and -conditions either of that version or of any later version published by -the Free Software Foundation. If the Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - - 14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -write to the author to ask for permission. For software which is -copyrighted by the Free Software Foundation, write to the Free -Software Foundation; we sometimes make exceptions for this. Our -decision will be guided by the two goals of preserving the free status -of all derivatives of our free software and of promoting the sharing -and reuse of software generally. - - NO WARRANTY - - 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY -KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN -WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY -AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU -FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR -CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE -LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING -RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A -FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Libraries - - If you develop a new library, and you want it to be of the greatest -possible use to the public, we recommend making it free software that -everyone can redistribute and change. You can do so by permitting -redistribution under these terms (or, alternatively, under the terms of the -ordinary General Public License). - - To apply these terms, attach the following notices to the library. It is -safest to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least the -"copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the library, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the - library `Frob' (a library for tweaking knobs) written by James Random Hacker. - - , 1 April 1990 - Ty Coon, President of Vice - -That's all there is to it! -``` - -Javassist ---------- - -``` -Copyright (C) 1999- by Shigeru Chiba, All rights reserved. - -Javassist (JAVA programming ASSISTant) makes Java bytecode manipulation simple. It is a class library for editing -bytecodes in Java; it enables Java programs to define a new class at runtime and to modify a class file when the JVM -loads it. Unlike other similar bytecode editors, Javassist provides two levels of API: source level and bytecode level. -If the users use the source- level API, they can edit a class file without knowledge of the specifications of the Java -bytecode. The whole API is designed with only the vocabulary of the Java language. You can even specify inserted bytecode -in the form of source text; Javassist compiles it on the fly. On the other hand, the bytecode-level API allows the users -to directly edit a class file as other editors. - -This software is distributed under the Mozilla Public License Version 1.1, the GNU Lesser General Public License Version -2.1 or later, or the Apache License Version 2.0. -``` - -slf4j ------ - -``` - Copyright (c) 2004-2013 QOS.ch - All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` - -PostgreSQL Connector --------------------- - -``` -PostgreSQL Database Management System -(formerly known as Postgres, then as Postgres95) - -Portions Copyright (c) 1996-2016, The PostgreSQL Global Development Group - -Portions Copyright (c) 1994, The Regents of the University of California - -Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and -without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the -following two paragraphs appear in all copies. - -IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR -CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF -THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" -BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR -MODIFICATIONS. -``` diff --git a/server/pom.xml b/server/pom.xml deleted file mode 100644 index 58f7e59..0000000 --- a/server/pom.xml +++ /dev/null @@ -1,308 +0,0 @@ - - - - 4.0.0 - - - - parent - com.torchmind.stockpile - 1.0-SNAPSHOT - - - - server - jar - - - Stockpile Server - Provides a Stockpile server implementation which acts as a caching service for Mojang APIs. - - - - - - com.torchmind.stockpile - common - - - - - com.h2database - h2 - - - org.postgresql - postgresql - - - - com.zaxxer - HikariCP - - - - org.hibernate - hibernate-java8 - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-jdbc - - - - org.apache.tomcat - tomcat-jdbc - - - - - org.springframework.boot - spring-boot-starter-jetty - - - org.springframework.boot - spring-boot-starter-logging - - - - ch.qos.logback - logback-classic - - - - - org.springframework.boot - spring-boot-starter-log4j2 - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-starter-tomcat - - - - - - javax.el - javax.el-api - - - - - com.fasterxml.jackson.core - jackson-annotations - - - com.fasterxml.jackson.core - jackson-core - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.dataformat - jackson-dataformat-xml - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - - - - - com.google.code.findbugs - jsr305 - - - - - com.google.guava - guava - - - - - junit - junit - - - org.mockito - mockito-core - - - - - - StockpileServer - - - - - ${project.basedir}/src/main/resources - . - false - - - banner.txt - favicon.ico - log4j2.xml - - - - - - ${project.basedir}/src/main/resources/static - ./static/ - true - - - index.html - logo.svg - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - - com.torchmind.stockpile.server.StockpileServer - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - org.apache.maven.plugins - maven-source-plugin - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - - - com.akathist.maven.plugins.launch4j - launch4j-maven-plugin - - - - l4j-clui - package - - - launch4j - - - - true - console - ${project.build.directory}/${project.build.finalName}.exe - ${project.build.finalName}.jar - Stockpile - ${project.basedir}/src/main/resources/favicon.ico - - - 1.8.0 - - - - 1.0.0.0 - ${project.version} - ${project.description} - ${project.organization.name} - 1.0.0.0 - ${project.version} - ${project.name} - ${project.artifactId} - ${project.build.finalName}.exe - - - - - - - - - org.apache.maven.plugins - maven-assembly-plugin - - - false - - - src/assembly/complete.xml - - - - - - - org.apache.maven.plugins - maven-install-plugin - - - true - - - - - - org.apache.maven.plugins - maven-deploy-plugin - - - true - - - - - diff --git a/server/src/application.yml b/server/src/application.yml deleted file mode 100644 index 2d98ba6..0000000 --- a/server/src/application.yml +++ /dev/null @@ -1,125 +0,0 @@ -# Stockpile Configuration File -# ============================ -# Generated for version ${project.version} - -# Server Settings -# =============== -server: - # Server Address - # ============== - # Specifies the address the server will bind to. - # This should preferably be an internal address which is not directly reachable from the internet. - # - # Default: 0.0.0.0 (all addresses) - address: 0.0.0.0 - - # Server Port - # =========== - # Specifies the port the server will bind to. - # - # Any value works here. Keep in mind, that ports 1024 and below are restricted and cannot be bound without - # Administrator privileges or setting a capability flag on the Java executable on modern systems. - # - # Default: 8080 - port: 8080 - - # Context Path - # ============ - # Specifies the context path (URL prefix) the server will respond to. - # - # Usually you will not need to change this unless you are planning on running this cache implementation using a - # reverse proxy. - # - # Default: / - contextPath: / - -# Cache Configuration -# =================== -cache: - # Cache Aggressiveness - # ==================== - # Specifies the cache's "aggressiveness" in terms of storage period. - # - # high => Display Names expire after 30 days, Profiles and their properties never expire - # moderate => Display Names, Profiles and Properties expire according to their configured TTLs. - # low => The cache will be populated but only used when the API refuses to respond to queries. - # - # Default: high - aggressiveness: high - - # Time-to-Live (TTL) Configuration - # ================================ - # Note: The settings in this section only take effect when the cache aggressiveness is set to "moderate". - ttl: - # Profile TTL - # =========== - # Specifies how long a Profile is cached in the database (in seconds). - # A value of -1 signifies an infinite caching period. - # - # Note: Profiles will only be kept around as long as there is at least one property or a name associated with - # it. - # - # Default: -1 - profile: -1 - - # Display Name TTL - # ================ - # Specifies how long display names are cached in the database (in seconds). - # A value of -1 signifies an infinite caching period. - # - # Default: 7200 - name: 7200 - - # Property TTL - # ============ - # Specifies how long properties are cached in the database (in seconds). - # A value of -1 signifies an infinite caching period. - # - # Note: Currently this feature only stores a player's skin. - # - # Default: 3600 - property: 3600 - -# Framework Configuration -# ======================= -spring: - datasource: - # Database URI - # ============ - # Specifies a JDBC URL to connect to in order to permanently store cache contents. - # - # Syntax: jdbc:://[:]/ - # Default: jdbc:h2:./cache - url: jdbc:h2:./cache - - # Username & Password - # =================== - # Specifies the username and password to pass to the database driver in order to authenticate with the backing - # server implementation. - # - # Default: stockpile:1234 - username: stockpile - password: 1234 - jpa: - # Schema Generation - # ================= - # Enables or disabled automatic database schema generation and updating. - # - # Note: It is recommended to keep this option enabled as it will ensure the database layout is compatible with - # the currently running version of the application. Only disable this if you know what you are doing! - # - # Default: true - generate-ddl: true - - # Schema Generation Mode - # ====================== - # Specifies how schema generation is handled by the application. - # - # none => Disable schema generation. - # validate => Verify the database and prevent application startup when states mismatch. - # update => Create and update tables according to the new specification. - # create => Creates a new database schema (deleting all previously stored data). - # create-drop => Creates a new schema and deletes it on (graceful) shutdown. - # - # Default: update - ddl-auto: update diff --git a/server/src/assembly/complete.xml b/server/src/assembly/complete.xml deleted file mode 100644 index 4c76f0e..0000000 --- a/server/src/assembly/complete.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - complete - - - zip - tar.gz - - - - - ${project.basedir} - . - - - THIRDPARTY-LIBRARIES.md - - - - ${project.basedir}/src - . - - - application.yml - - - - ${project.build.directory} - . - - - *.exe - *.jar - - - - diff --git a/server/src/main/java/com/torchmind/stockpile/server/StockpileServer.java b/server/src/main/java/com/torchmind/stockpile/server/StockpileServer.java deleted file mode 100644 index e7adbd2..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/StockpileServer.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.scheduling.annotation.EnableScheduling; - -import javax.annotation.Nonnull; - -/** - * Stockpile Server - * - * Provides an entry-point to the Java VM as well as the Spring framework. - * - * @author Johannes Donath - */ -@EnableScheduling -@SpringBootApplication -public class StockpileServer { - - /** - * Main Entry-Point - * @param arguments an array of command line arguments. - */ - public static void main(@Nonnull String[] arguments) { - SpringApplication.run(StockpileServer.class, arguments); - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/configuration/CacheConfiguration.java b/server/src/main/java/com/torchmind/stockpile/server/configuration/CacheConfiguration.java deleted file mode 100644 index 1f659c1..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/configuration/CacheConfiguration.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.configuration; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; -import org.springframework.stereotype.Component; - -import javax.annotation.Nonnull; - -/** - * Cache Configuration - * - * Represents an externalized cache configuration. - * - * @author Johannes Donath - */ -@Component -@ConfigurationProperties(prefix = "cache") -public class CacheConfiguration { - private Aggressiveness aggressiveness; - @NestedConfigurationProperty - private TimeToLiveConfiguration ttl = new TimeToLiveConfiguration(); - - @Nonnull - public Aggressiveness getAggressiveness() { - return this.aggressiveness; - } - - @Nonnull - public TimeToLiveConfiguration getTtl() { - return this.ttl; - } - - public void setAggressiveness(@Nonnull Aggressiveness aggressiveness) { - this.aggressiveness = aggressiveness; - } - - public void setTtl(@Nonnull TimeToLiveConfiguration ttl) { - this.ttl = ttl; - } - - /** - * Cache Aggressiveness - * - * Provides a list of general behavior presets. - */ - public enum Aggressiveness { - - /** - * Low Aggressiveness - * - * Switches the cache to write-only mode unless the backing API returns an error. - */ - LOW, - - /** - * Moderate Aggressiveness - * - * Enables user-customizable caching using the TTL options. - */ - MODERATE, - - /** - * High Aggressiveness - * - * Ensures all objects are cached for the longest possible time. Only push updates will alter the set of - * local records. - */ - HIGH, - - /** - * Unknown - * - * This is a placeholder annotation for cases in which unknown or default values are required. - */ - UNKNOWN - } - - /** - * Represents an externalized TTL configuration. - */ - public static class TimeToLiveConfiguration { - private long name = 7200; - private long profile = -1; - private long property = 3600; - - public long getName() { - return this.name; - } - - public long getProfile() { - return this.profile; - } - - public long getProperty() { - return this.property; - } - - public void setName(long name) { - this.name = name; - } - - public void setProfile(long profile) { - this.profile = profile; - } - - public void setProperty(long property) { - this.property = property; - } - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/configuration/WebMvcConfiguration.java b/server/src/main/java/com/torchmind/stockpile/server/configuration/WebMvcConfiguration.java deleted file mode 100644 index b69a03b..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/configuration/WebMvcConfiguration.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.configuration; - -import org.springframework.context.annotation.Configuration; -import org.springframework.http.MediaType; -import org.springframework.web.servlet.config.annotation.*; - -import javax.annotation.Nonnull; - -/** - * Web MVC Configuration - * - * Configures Spring's web MVC component for use with pure REST based communication. - * - * @author Johannes Donath - */ -@Configuration -public class WebMvcConfiguration extends WebMvcConfigurerAdapter { - - /** - * {@inheritDoc} - */ - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry.addResourceHandler("/index.html").addResourceLocations("classpath:/static/index.html"); - registry.addResourceHandler("/logo.svg").addResourceLocations("classpath:/static/logo.svg"); - } - - /** - * {@inheritDoc} - */ - @Override - public void configureContentNegotiation(@Nonnull ContentNegotiationConfigurer configurer) { - configurer.defaultContentType(MediaType.APPLICATION_JSON_UTF8); - configurer.mediaType("json", MediaType.APPLICATION_JSON_UTF8); - configurer.mediaType("xml", MediaType.APPLICATION_XML); - configurer.favorPathExtension(true); - configurer.ignoreUnknownPathExtensions(true); - configurer.ignoreAcceptHeader(false); - } - - /** - * {@inheritDoc} - */ - @Override - public void addCorsMappings(@Nonnull CorsRegistry registry) { - registry.addMapping("/**") - .allowedOrigins("*") - .allowedMethods("GET", "POST", "PUT", "DELETE") - .allowedHeaders("X-Authentication"); - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/configuration/condition/CacheAggressivenessCondition.java b/server/src/main/java/com/torchmind/stockpile/server/configuration/condition/CacheAggressivenessCondition.java deleted file mode 100644 index dff9f81..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/configuration/condition/CacheAggressivenessCondition.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.configuration.condition; - -import com.torchmind.stockpile.server.configuration.CacheConfiguration.Aggressiveness; -import org.springframework.context.annotation.Condition; -import org.springframework.context.annotation.ConditionContext; -import org.springframework.core.type.AnnotatedTypeMetadata; - -import javax.annotation.Nonnull; - -/** - * Cache Aggressiveness Condition - * - * Checks whether a specific cache aggressiveness is configured. - * - * @author Johannes Donath - */ -public class CacheAggressivenessCondition implements Condition { - - /** - * {@inheritDoc} - */ - @Override - public boolean matches(@Nonnull ConditionContext context, @Nonnull AnnotatedTypeMetadata metadata) { - Aggressiveness aggressiveness = (Aggressiveness) metadata.getAnnotationAttributes(ConditionalOnCacheAggressiveness.class.getName()).getOrDefault("value", Aggressiveness.HIGH); - return Aggressiveness.valueOf(context.getEnvironment().getProperty("cache.aggressiveness", "high").toUpperCase()) == aggressiveness; - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/configuration/condition/ConditionalOnCacheAggressiveness.java b/server/src/main/java/com/torchmind/stockpile/server/configuration/condition/ConditionalOnCacheAggressiveness.java deleted file mode 100644 index 2dafe83..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/configuration/condition/ConditionalOnCacheAggressiveness.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.configuration.condition; - -import com.torchmind.stockpile.server.configuration.CacheConfiguration.Aggressiveness; -import org.springframework.context.annotation.Conditional; - -import java.lang.annotation.*; - -/** - * Conditional on Cache Aggressiveness - * - * Instructs Spring to only initialize a bean if the configured application aggressiveness is equal to a certain preset. - * - * @author Johannes Donath - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Conditional(CacheAggressivenessCondition.class) -@Target({ElementType.TYPE, ElementType.METHOD}) -public @interface ConditionalOnCacheAggressiveness { - Aggressiveness value() default Aggressiveness.UNKNOWN; -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/controller/RestfulErrorController.java b/server/src/main/java/com/torchmind/stockpile/server/controller/RestfulErrorController.java deleted file mode 100644 index e2d92d1..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/controller/RestfulErrorController.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.controller; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.web.AbstractErrorController; -import org.springframework.boot.autoconfigure.web.ErrorAttributes; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.RestController; - -import javax.annotation.Nonnull; -import javax.annotation.concurrent.ThreadSafe; -import javax.servlet.http.HttpServletRequest; -import java.util.Map; - -/** - * Restful Error Controller - * - * Provides a controller which will handle all uncaught exceptions. - * - * @author Johannes Donath - */ -@ThreadSafe -@RestController -@RequestMapping("/error") -public class RestfulErrorController extends AbstractErrorController { - - @Autowired - public RestfulErrorController(@Nonnull ErrorAttributes errorAttributes) { - super(errorAttributes); - } - - /** - * Handles failed requests. - * - * @param request a request. - * @return an error response. - */ - @ResponseBody - @RequestMapping - public ResponseEntity> error(HttpServletRequest request) { - Map body = this.getErrorAttributes(request, false); - HttpStatus status = this.getStatus(request); - - return new ResponseEntity<>(body, status); - } - - /** - * {@inheritDoc} - */ - @Nonnull - @Override - public String getErrorPath() { - return "/error"; - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/controller/v1/ApplicationController.java b/server/src/main/java/com/torchmind/stockpile/server/controller/v1/ApplicationController.java deleted file mode 100644 index 3df0477..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/controller/v1/ApplicationController.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.controller.v1; - -import com.torchmind.stockpile.data.v1.ServerInformation; -import com.torchmind.stockpile.data.v1.Version; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; - -import javax.annotation.Nonnull; -import javax.annotation.concurrent.ThreadSafe; - -/** - * Root Controller - * - * Handles requests to the root endpoint within the API which informs clients of the local application version. - * - * @author Johannes Donath - */ -@ThreadSafe -@RestController -@RequestMapping("/v1") -public class ApplicationController { - public static final Version VERSION = new Version(1, Version.State.DEVELOPMENT); - private final ApplicationContext applicationContext; - - @Autowired - public ApplicationController(@Nonnull ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } - - /** - * /v1/ - * - * Returns version information on this specific API endpoint. - * - * @return a version information representation. - */ - @Nonnull - @RequestMapping(method = RequestMethod.GET) - public ServerInformation get() { - Package p = this.getClass().getPackage(); - - if (p != null) { - return new ServerInformation(p, VERSION); - } - - return new ServerInformation(VERSION); - } - - /** - * /v1/shutdown - * - * Shuts down the server. - */ - @RequestMapping(path = "/shutdown", method = RequestMethod.POST) - public void shutdown() { - if (!(this.applicationContext instanceof ConfigurableApplicationContext)) { - throw new IllegalStateException("Cannot shut down context: Unknown implementation"); - } - - (new Thread() { - @Override - public void run() { - // wait a bit to ensure the request has finished before we shut down - try { - Thread.sleep(500L); - } catch (InterruptedException ignore) { - } - - // actually instruct the context to shut down - ((ConfigurableApplicationContext) applicationContext).close(); - } - }).start(); - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/controller/v1/BlacklistController.java b/server/src/main/java/com/torchmind/stockpile/server/controller/v1/BlacklistController.java deleted file mode 100644 index 3e408ef..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/controller/v1/BlacklistController.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.controller.v1; - -import com.google.common.base.Joiner; -import com.google.common.base.Splitter; -import com.google.common.hash.Hashing; -import com.torchmind.stockpile.data.v1.BlacklistResult; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.web.bind.annotation.*; - -import javax.annotation.Nonnull; -import javax.annotation.PostConstruct; -import javax.annotation.concurrent.ThreadSafe; -import java.io.*; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.regex.Pattern; - -/** - * Blacklist Controller - * - * Provides a controller which allows directly checking against a cached version of the blocked server list. - * Note: This controller is hardcoded to update its caches once an hour and is not affected by any cache aggressiveness - * settings. - * - * @author Johannes Donath - */ -@ThreadSafe -@RestController -@RequestMapping("/v1/blacklist") -public class BlacklistController { - public static final String BLACKLIST_URL = "https://sessionserver.mojang.com/blockedservers"; - public static final Pattern IP_ADDRESS_PATTERN = Pattern.compile("^((?:([0-9]{1,2})|(?:([0-1][0-9]{2})|(2(?:([0-4][0-9])|(5[0-5])))))\\.){3}(?:(?:([0-1][0-9]{2})|(2(?:([0-4][0-9])|(5[0-5]))))|([0-9]{1,2}))$"); - public static final Logger logger = LogManager.getFormatterLogger(BlacklistController.class); - - private final List hashes = new CopyOnWriteArrayList<>(); - - /** - * POST /v1/blacklist/ - * - * Checks any hostname supplied as a form parameter in the post body against the server blacklist. - * - * @param hostname a hostname to check against. - * @return a blacklist result. - */ - @Nonnull - @RequestMapping(params = "hostname", method = RequestMethod.POST) - public BlacklistResult check(@Nonnull @RequestParam("hostname") String hostname) { - // before checking for wildcards check for exact matches of the hostname - hostname = hostname.toLowerCase(); - String hash = Hashing.sha1().hashString(hostname, StandardCharsets.ISO_8859_1).toString(); - - if (this.hashes.contains(hash)) { - return new BlacklistResult(hostname, true); - } - - if (IP_ADDRESS_PATTERN.matcher(hostname).matches()) { - return this.checkAddress(hostname); - } - - return this.checkHostname(hostname); - } - - /** - * Checks an IP address against the blacklist. - * - * @param address an address. - * @return a blacklist result. - */ - @Nonnull - private BlacklistResult checkAddress(@Nonnull String address) { - List addressParts = Splitter.on('.').splitToList(address); - - for (int i = (addressParts.size() - 1); i >= 1; --i) { - String currentAddress = Joiner.on('.').join(addressParts.subList(0, i)) + ".*"; - String hash = Hashing.sha1().hashString(currentAddress, StandardCharsets.ISO_8859_1).toString(); - - if (this.hashes.contains(hash)) { - return new BlacklistResult(currentAddress, true); - } - } - - return new BlacklistResult(address, false); - } - - /** - * POST /v1/blacklist/ - * - * Checks any hostname supplied in the post body against the server blacklist. - * - * @param hostname a hostname to check against. - * @return a blacklist result. - */ - @Nonnull - @RequestMapping(method = RequestMethod.POST) - public BlacklistResult checkBody(@Nonnull @RequestBody String hostname) { - return this.check(hostname); - } - - /** - * Checks a hostname against the blacklist. - * - * @param hostname a hostname. - * @return a blacklist result. - */ - @Nonnull - private BlacklistResult checkHostname(@Nonnull String hostname) { - List hostnameParts = Splitter.on('.').splitToList(hostname); - - for (int i = 1; i < hostnameParts.size(); ++i) { - String currentHostname = "*." + Joiner.on('.').join(hostnameParts.subList(i, hostnameParts.size())); - String hash = Hashing.sha1().hashString(currentHostname, StandardCharsets.ISO_8859_1).toString(); - - if (this.hashes.contains(hash)) { - return new BlacklistResult(currentHostname, true); - } - } - - return new BlacklistResult(hostname, false); - } - - /** - * Updates the local blacklist cache. - */ - @PostConstruct - @Scheduled(cron = "0 0 * * * ?") - public void updateHashes() { - try { - logger.info("Updating blacklist cache ..."); - URL url = new URL(BLACKLIST_URL); - - try (InputStream inputStream = url.openStream()) { - try (Reader reader = new InputStreamReader(inputStream)) { - try (BufferedReader bufferedReader = new BufferedReader(reader)) { - this.hashes.clear(); - bufferedReader.lines().forEach(this.hashes::add); - logger.info("Found %d blacklist entries.", this.hashes.size()); - } - } - } - } catch (IOException ex) { - logger.error("Could not update blacklist cache: %s", ex.getMessage()); - } - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/controller/v1/LoginController.java b/server/src/main/java/com/torchmind/stockpile/server/controller/v1/LoginController.java deleted file mode 100644 index 6c6fca3..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/controller/v1/LoginController.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.controller.v1; - -import com.torchmind.stockpile.data.v1.PlayerProfile; -import com.torchmind.stockpile.server.service.api.MojangUUID; -import com.torchmind.stockpile.server.service.api.ProfileService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Login Controller - * - * Provides a proxy to Mojang's login endpoint to improve caching on compatible servers. - * - * @author Johannes Donath - */ -@RestController -@RequestMapping("/v1/login") -public class LoginController { - private final ProfileService profileService; - - @Autowired - public LoginController(@Nonnull ProfileService profileService) { - this.profileService = profileService; - } - - /** - * GET /v1/login - * - * Proxies a login request and updates the cache. - * - * @param username a username. - * @param serverId a server identifier. - * @return a profile. - */ - @Nonnull - @RequestMapping(method = RequestMethod.GET) - public PlayerProfile login(@Nonnull @RequestParam("username") String username, @Nonnull @RequestParam("serverId") String serverId) { - return this.profileService.join(username, serverId).toRestRepresentation(); - } - - /** - * GET /v1/login - * - * Proxies a login request and updates the cache. - * Note: This version requires the X-Forward header to be present and causes the API to return a regular Mojang - * response. - * - * @param username a username. - * @param serverId a server identifier. - * @return a profile. - */ - @Nonnull - @RequestMapping(method = RequestMethod.GET, produces = "application/json", headers = "X-Forward") - public Map loginForward(@Nonnull @RequestParam("username") String username, @Nonnull @RequestParam("serverId") String serverId) { - PlayerProfile profile = this.login(username, serverId); - - Map root = new HashMap<>(); - root.put("id", (new MojangUUID(profile.getIdentifier())).toString()); - root.put("name", profile.getName()); - - { - List> properties = new ArrayList<>(); - profile.getProperties().forEach((p) -> { - Map property = new HashMap<>(); - property.put("name", p.getName()); - property.put("value", p.getValue()); - - if (p.getSignature() != null) { - property.put("signature", p.getSignature()); - } - - properties.add(property); - }); - root.put("properties", properties); - } - - return root; - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/controller/v1/NameController.java b/server/src/main/java/com/torchmind/stockpile/server/controller/v1/NameController.java deleted file mode 100644 index 9c3bf0e..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/controller/v1/NameController.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.controller.v1; - -import com.torchmind.stockpile.data.v1.NameLookupResult; -import com.torchmind.stockpile.server.service.api.ProfileService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.*; - -import javax.annotation.Nonnull; - -/** - * Name Controller - * - * Provides methods for looking up display names. - * - * @author Johannes Donath - */ -@RestController -@RequestMapping("/v1/name") -public class NameController { - private final ProfileService profileService; - - @Autowired - public NameController(@Nonnull ProfileService profileService) { - this.profileService = profileService; - } - - /** - * GET /v1/name/{name} - * - * Attempts to find the corresponding profile identifier for the specified name. - * - * @param name a name. - * @return a lookup result. - */ - @Nonnull - @RequestMapping(path = "/{name}", method = RequestMethod.GET) - public NameLookupResult lookup(@Nonnull @PathVariable("name") String name) { - return new NameLookupResult(this.profileService.findIdentifier(name)); - } - - /** - * POST /v1/name - * - * Attempts to find the corresponding profile identifier for the specified name. - * - * @param name a name. - * @return a lookup result. - */ - @Nonnull - @RequestMapping(method = RequestMethod.POST) - public NameLookupResult lookupBody(@Nonnull @RequestBody String name) { - return this.lookup(name); - } - - /** - * DELETE /v1/name/{name} - * - * Purges a name from the cache. - * - * @param name a display name. - */ - @ResponseStatus(HttpStatus.NO_CONTENT) - @RequestMapping(path = "/{name}", method = RequestMethod.DELETE) - public void purge(@Nonnull @PathVariable("name") String name) { - this.profileService.purgeName(name); - } - - /** - * DELETE /v1/name - * - * Purges a name from the cache. - * - * @param name a display name. - */ - @ResponseStatus(HttpStatus.NO_CONTENT) - @RequestMapping(method = RequestMethod.DELETE) - public void purgeBody(@Nonnull @RequestBody String name) { - this.purge(name); - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/controller/v1/ProfileController.java b/server/src/main/java/com/torchmind/stockpile/server/controller/v1/ProfileController.java deleted file mode 100644 index a34d95a..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/controller/v1/ProfileController.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.controller.v1; - -import com.torchmind.stockpile.data.v1.PlayerProfile; -import com.torchmind.stockpile.server.entity.Profile; -import com.torchmind.stockpile.server.service.api.ProfileService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import javax.annotation.Nonnull; -import javax.annotation.concurrent.ThreadSafe; -import java.util.UUID; -import java.util.regex.Pattern; - -/** - * Profile Controller - * - * Provides methods for accessing profile information. - * - * @author Johannes Donath - */ -@ThreadSafe -@RestController -@RequestMapping("/v1/profile") -public class ProfileController { - public static final Pattern UUID_PATTERN = Pattern.compile("^[A-F0-9]{8}-[A-F0-9]{4}-[1-5][0-9A-F]{3}-[89AB][0-9A-F]{3}-[A-F0-9]{12}$", Pattern.CASE_INSENSITIVE); - private final ProfileService profileService; - - @Autowired - public ProfileController(@Nonnull ProfileService profileService) { - this.profileService = profileService; - } - - /** - * GET /v1/profile/{name} - * - * Looks up a profile based on its name or identifier. - * - * @param name a name or identifier. - * @return a response entity. - */ - @Nonnull - @RequestMapping(path = "/{name}", method = RequestMethod.GET) - public ResponseEntity lookup(@Nonnull @PathVariable("name") String name) { - final Profile profile; - - if (UUID_PATTERN.matcher(name).matches()) { - UUID identifier = UUID.fromString(name); - profile = this.profileService.get(identifier); - } else { - profile = this.profileService.find(name); - } - - return new ResponseEntity<>(profile.toRestRepresentation(), (profile.isCached() ? HttpStatus.OK : HttpStatus.CREATED)); - } - - /** - * POST /v1/profile - * - * Looks up a profile based on its name or identifier. - * - * @param name a name or identifier. - * @return a response entity. - */ - @Nonnull - @RequestMapping(method = RequestMethod.POST) - public ResponseEntity lookupBody(@Nonnull @RequestBody String name) { - return this.lookup(name); - } - - /** - * DELETE /v1/profile/{name} - * - * Purges a profile from the cache. - * - * @param name a name or identifier. - */ - @ResponseStatus(HttpStatus.NO_CONTENT) - @RequestMapping(path = "/{name}", method = RequestMethod.DELETE) - public void purge(@Nonnull @PathVariable("name") String name) { - if (UUID_PATTERN.matcher(name).matches()) { - UUID identifier = UUID.fromString(name); - this.profileService.purge(identifier); - return; - } - - this.profileService.purge(name); - } - - /** - * DELETE /v1/profile - * - * Purges a profile from the cache. - * - * @param name a name or identifier. - */ - @ResponseStatus(HttpStatus.NO_CONTENT) - @RequestMapping(method = RequestMethod.DELETE) - public void purgeBody(@Nonnull @RequestBody String name) { - this.purge(name); - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/entity/BaseEntity.java b/server/src/main/java/com/torchmind/stockpile/server/entity/BaseEntity.java deleted file mode 100644 index d089b0b..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/entity/BaseEntity.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.entity; - -import org.hibernate.annotations.GenericGenerator; - -import javax.annotation.Nonnull; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -import javax.persistence.MappedSuperclass; -import java.util.Objects; -import java.util.UUID; - -/** - * Base Entity - * - * Provides a base type for entities. - * - * @author Johannes Donath - */ -@MappedSuperclass -public class BaseEntity { - @Id - @GeneratedValue(generator = "uuid") - @GenericGenerator(name = "uuid", strategy = "uuid2") - private final UUID identifier; - - protected BaseEntity() { - this.identifier = null; - } - - protected BaseEntity(@Nonnull UUID identifier) { - this.identifier = identifier; - } - - /** - * Retrieves the entity's globally unique identifier. - * - * @return an identifier. - */ - public UUID getIdentifier() { - return identifier; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof BaseEntity)) return false; - - BaseEntity that = (BaseEntity) o; - return Objects.equals(this.identifier, that.identifier); - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return Objects.hash(this.identifier); - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/entity/DisplayName.java b/server/src/main/java/com/torchmind/stockpile/server/entity/DisplayName.java deleted file mode 100644 index e78c8e4..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/entity/DisplayName.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.entity; - -import javax.annotation.Nonnull; -import javax.persistence.*; -import java.time.Instant; - -/** - * Display Name - * - * @author Johannes Donath - */ -@Entity -@Table(name = "profile_display_name") -public class DisplayName extends BaseEntity { - @Column(nullable = false) - private Instant lastSeen; - @Column(nullable = false, updatable = false) - private final String name; - @ManyToOne(optional = false, cascade = CascadeType.REFRESH) - private final Profile profile; - - private DisplayName() { - this.name = null; - this.profile = null; - } - - public DisplayName(String name, Instant lastSeen, Profile profile) { - this.name = name; - this.lastSeen = lastSeen; - this.profile = profile; - } - - @Nonnull - public Instant getLastSeen() { - return this.lastSeen; - } - - @Nonnull - public String getName() { - return this.name; - } - - @Nonnull - public Profile getProfile() { - return this.profile; - } - - public void setLastSeen(@Nonnull Instant lastSeen) { - this.lastSeen = lastSeen; - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/entity/Profile.java b/server/src/main/java/com/torchmind/stockpile/server/entity/Profile.java deleted file mode 100644 index e910ab2..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/entity/Profile.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.entity; - -import com.torchmind.stockpile.data.v1.PlayerProfile; - -import javax.annotation.Nonnull; -import javax.persistence.*; -import java.time.Instant; -import java.util.*; -import java.util.concurrent.CopyOnWriteArraySet; - -/** - * Profile - * - * Represents a cached Minecraft profile. - * - * @author Johannes Donath - */ -@Entity -@Table(name = "profile") -public class Profile { - @Transient - private final transient boolean cached; - @Id - private final UUID identifier; - @Column - private Instant lastSeen; - @OneToMany(orphanRemoval = true, mappedBy = "profile", fetch = FetchType.EAGER, cascade = CascadeType.REFRESH) - private final Set names; - @OneToMany(orphanRemoval = true, mappedBy = "profile", fetch = FetchType.EAGER, cascade = CascadeType.REFRESH) - private final Set properties; - - private Profile() { - this.identifier = null; - this.names = null; - this.properties = null; - this.lastSeen = Instant.now(); - - this.cached = true; - } - - public Profile(@Nonnull UUID identifier) { - this.identifier = identifier; - this.names = new CopyOnWriteArraySet<>(); - this.properties = new CopyOnWriteArraySet<>(); - this.lastSeen = Instant.now(); - - this.cached = false; - } - - public void addName(@Nonnull DisplayName name) { - this.names.add(name); - } - - public void addProperty(@Nonnull ProfileProperty property) { - this.properties.add(property); - } - - @Nonnull - public UUID getIdentifier() { - return identifier; - } - - @Nonnull - public Instant getLastSeen() { - return lastSeen; - } - - /** - * Retrieves the latest profile name. - * - * @return a name or, if no display name was found, an empty optional. - */ - @Nonnull - public Optional getLatestName() { - return this.getNames().stream().sorted((n1, n2) -> (int) Math.min(1, Math.max(-1, (n2.getLastSeen().toEpochMilli() - n1.getLastSeen().toEpochMilli())))).findFirst(); - } - - /** - * Retrieves a list of associated names. - * - * @return a list of names. - */ - @Nonnull - public Set getNames() { - return this.names; - } - - /** - * Retrieves a list of associated properties. - * - * @return a list of properties. - */ - @Nonnull - public Set getProperties() { - return this.properties; - } - - /** - * Checks whether the profile has just been created as a result of the request or whether it was received - * from the storage backend. - * - * @return true if cached, false otherwise. - */ - public boolean isCached() { - return this.cached; - } - - public void setLastSeen(@Nonnull Instant lastSeen) { - this.lastSeen = lastSeen; - } - - /** - * Converts a stored profile into a REST compatible resource. - * - * @return a REST profile. - */ - @Nonnull - public PlayerProfile toRestRepresentation() { - List properties = new ArrayList<>(); - this.getProperties().forEach((p) -> properties.add(p.toRestRepresentation())); - - return new PlayerProfile(this.getIdentifier(), this.getLatestName().map((n) -> n.getName()).orElse(null), properties, this.getLastSeen()); - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/entity/ProfileProperty.java b/server/src/main/java/com/torchmind/stockpile/server/entity/ProfileProperty.java deleted file mode 100644 index a378d4c..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/entity/ProfileProperty.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.entity; - -import com.torchmind.stockpile.data.v1.PlayerProfile; -import org.hibernate.annotations.Type; - -import javax.annotation.Nonnull; -import javax.persistence.*; -import java.time.Instant; - -/** - * Profile Property - * - * Represents a property which has been assigned to a profile. - * - * @author Johannes Donath - */ -@Entity -@Table(name = "profile_property") -public class ProfileProperty extends BaseEntity { - @Column - private Instant lastSeen; - @Column(nullable = false, updatable = false) - private final String name; - @ManyToOne(optional = false, cascade = CascadeType.REFRESH) - private final Profile profile; - @Column - @Type(type = "text") - private String signature; - @Type(type = "text") - @Column(nullable = false) - private String value; - - private ProfileProperty() { - this.name = null; - this.profile = null; - this.lastSeen = Instant.now(); - } - - public ProfileProperty(@Nonnull Profile profile, @Nonnull String name, @Nonnull String value, @Nonnull String signature) { - this.profile = profile; - - this.name = name; - this.value = value; - this.signature = signature; - this.lastSeen = Instant.now(); - } - - @Nonnull - public Instant getLastSeen() { - return lastSeen; - } - - @Nonnull - public String getName() { - return this.name; - } - - @Nonnull - public Profile getProfile() { - return this.profile; - } - - @Nonnull - public String getSignature() { - return this.signature; - } - - @Nonnull - public String getValue() { - return this.value; - } - - public void setLastSeen(@Nonnull Instant lastSeen) { - this.lastSeen = lastSeen; - } - - public void setSignature(@Nonnull String signature) { - this.signature = signature; - } - - public void setValue(@Nonnull String value) { - this.value = value; - } - - /** - * Converts a stored profile property into its REST representation. - * - * @return a REST compatible representation. - */ - @Nonnull - public PlayerProfile.Property toRestRepresentation() { - return new PlayerProfile.Property(this.name, this.value, this.signature); - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/entity/repository/DisplayNameRepository.java b/server/src/main/java/com/torchmind/stockpile/server/entity/repository/DisplayNameRepository.java deleted file mode 100644 index 6a85230..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/entity/repository/DisplayNameRepository.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.entity.repository; - -import com.torchmind.stockpile.server.entity.DisplayName; -import com.torchmind.stockpile.server.entity.Profile; -import org.springframework.data.repository.PagingAndSortingRepository; - -import javax.annotation.Nonnull; -import java.time.Instant; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.Stream; - -/** - * Display Name Repository - * - * Provides a management interface for finding, creating, updating and deleting display names. - * Note: This interface is implemented by Spring during runtime. - * - * @author Johannes Donath - */ -public interface DisplayNameRepository extends PagingAndSortingRepository { - - /** - * Searches for a set of display names that were last seen before the supplied timestamp occurred. - * - * @param lastSeen a timestamp. - * @return a stream of names. - */ - @Nonnull - Stream findByLastSeenLessThanOrderByLastSeenDesc(@Nonnull Instant lastSeen); - - /** - * Searches for a display name. - * - * @param name a display name. - * @return a display name or, if no record was found, an empty optional. - */ - @Nonnull - Optional findOneByName(@Nonnull String name); - - /** - * Searches for a display name in a specific profile. - * - * @param name a display name. - * @param profile a profile. - * @return a display name or, if no record was found, an empty optional. - */ - @Nonnull - Optional findOneByNameAndProfile(@Nonnull String name, @Nonnull Profile profile); -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/entity/repository/ProfilePropertyRepository.java b/server/src/main/java/com/torchmind/stockpile/server/entity/repository/ProfilePropertyRepository.java deleted file mode 100644 index c83052d..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/entity/repository/ProfilePropertyRepository.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.entity.repository; - -import com.torchmind.stockpile.server.entity.Profile; -import com.torchmind.stockpile.server.entity.ProfileProperty; -import org.springframework.data.repository.PagingAndSortingRepository; - -import javax.annotation.Nonnull; -import java.time.Instant; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.Stream; - -/** - * Profile Property Repository - * - * Provides a management interface for finding, creating, updating and deleting profile properties. - * Note: This interface is implemented by Spring during runtime. - * - * @author Johannes Donath - */ -public interface ProfilePropertyRepository extends PagingAndSortingRepository { - - /** - * Searches for a set of profile properties that were last seen before the supplied timestamp occurred. - * - * @param lastSeen a timestamp. - * @return a stream of profile properties. - */ - @Nonnull - Stream findByLastSeenLessThanOrderByLastSeenDesc(@Nonnull Instant lastSeen); - - /** - * Searches for a profile property with the specified name and profile. - * - * @param name a property name. - * @param profile a profile. - * @return a profile property or, if no property within the specified profile was found, an empty optional. - */ - @Nonnull - Optional findByNameAndProfile(@Nonnull String name, @Nonnull Profile profile); -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/entity/repository/ProfileRepository.java b/server/src/main/java/com/torchmind/stockpile/server/entity/repository/ProfileRepository.java deleted file mode 100644 index 6c7c930..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/entity/repository/ProfileRepository.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.entity.repository; - -import com.torchmind.stockpile.server.entity.Profile; -import org.springframework.data.repository.PagingAndSortingRepository; - -import javax.annotation.Nonnull; -import java.time.Instant; -import java.util.UUID; -import java.util.stream.Stream; - -/** - * Profile Repository - * - * Provides a management interface for finding, creating, updating and deleting Minecraft profiles. - * Note: This interface is implemented by Spring during runtime. - * - * @author Johannes Donath - */ -public interface ProfileRepository extends PagingAndSortingRepository { - - /** - * Searches for a set of profiles that were last seen before the supplied timestamp occurred. - * - * @param lastSeen a timestamp. - * @return a stream of profiles. - */ - @Nonnull - Stream findByLastSeenLessThanOrderByLastSeenDesc(@Nonnull Instant lastSeen); -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/error/InvalidProfileIdentifierException.java b/server/src/main/java/com/torchmind/stockpile/server/error/InvalidProfileIdentifierException.java deleted file mode 100644 index a36a08c..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/error/InvalidProfileIdentifierException.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.error; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -/** - * Invalid Profile Identifier Exception - * - * Provides a utility exception which notifies the backing request handler about a bad profile identifier which has - * been passed along with the request. - * - * @author Johannes Donath - */ -@ResponseStatus(HttpStatus.BAD_REQUEST) -public class InvalidProfileIdentifierException extends IllegalArgumentException { - - public InvalidProfileIdentifierException() { - } - - public InvalidProfileIdentifierException(String s) { - super(s); - } - - public InvalidProfileIdentifierException(String message, Throwable cause) { - super(message, cause); - } - - public InvalidProfileIdentifierException(Throwable cause) { - super(cause); - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/error/NoSuchProfileException.java b/server/src/main/java/com/torchmind/stockpile/server/error/NoSuchProfileException.java deleted file mode 100644 index 33475c0..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/error/NoSuchProfileException.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.error; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -import javax.annotation.Nonnull; -import java.util.NoSuchElementException; -import java.util.UUID; - -/** - * No Such Profile Exception - * - * Provides a utility exception which notifies the backing request handler about a missing entry. - * - * @author Johannes Donath - */ -@ResponseStatus(HttpStatus.NOT_FOUND) -public class NoSuchProfileException extends NoSuchElementException { - public static final String MESSAGE_FORMAT = "Cannot find profile \"%s\""; - - public NoSuchProfileException() { - super(); - } - - public NoSuchProfileException(@Nonnull UUID identifier) { - super(String.format(MESSAGE_FORMAT, identifier)); - } - - public NoSuchProfileException(@Nonnull String name) { - super(String.format(MESSAGE_FORMAT, name)); - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/error/ServiceException.java b/server/src/main/java/com/torchmind/stockpile/server/error/ServiceException.java deleted file mode 100644 index e7be735..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/error/ServiceException.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.error; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -/** - * Service Exception - * - * Represents errors on Mojang's side within the application. - * - * @author Johannes Donath - */ -@ResponseStatus(code = HttpStatus.BAD_GATEWAY) -public class ServiceException extends RuntimeException { - - public ServiceException() { - } - - public ServiceException(String message) { - super(message); - } - - public ServiceException(String message, Throwable cause) { - super(message, cause); - } - - public ServiceException(Throwable cause) { - super(cause); - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/error/TooManyRequestsException.java b/server/src/main/java/com/torchmind/stockpile/server/error/TooManyRequestsException.java deleted file mode 100644 index 2ddbb6f..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/error/TooManyRequestsException.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.error; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -/** - * Too Many Requests Exception - * - * Provides an exception to represent rate limiting errors within the Mojang API. - * - * @author Johannes Donath - */ -@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS) -public class TooManyRequestsException extends IllegalStateException { - - public TooManyRequestsException() { - super(); - } - - public TooManyRequestsException(String s) { - super(s); - } - - public TooManyRequestsException(String message, Throwable cause) { - super(message, cause); - } - - public TooManyRequestsException(Throwable cause) { - super(cause); - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/service/StorageCleanupService.java b/server/src/main/java/com/torchmind/stockpile/server/service/StorageCleanupService.java deleted file mode 100644 index 1ded00b..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/service/StorageCleanupService.java +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.service; - -import com.torchmind.stockpile.server.configuration.CacheConfiguration; -import com.torchmind.stockpile.server.configuration.condition.ConditionalOnCacheAggressiveness; -import com.torchmind.stockpile.server.entity.DisplayName; -import com.torchmind.stockpile.server.entity.Profile; -import com.torchmind.stockpile.server.entity.ProfileProperty; -import com.torchmind.stockpile.server.entity.repository.DisplayNameRepository; -import com.torchmind.stockpile.server.entity.repository.ProfilePropertyRepository; -import com.torchmind.stockpile.server.entity.repository.ProfileRepository; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; - -import javax.annotation.Nonnull; -import javax.annotation.PostConstruct; -import java.time.Duration; -import java.time.Instant; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Storage Cleanup Service - * - * Provides methods for cleaning up old cached values. - * - * @author Johannes Donath - */ -public interface StorageCleanupService { - - /** - * Abstract Cleanup Service - * - * Provides a base implementation for cleanup services. - */ - abstract class AbstractCleanupService implements StorageCleanupService { - protected final Logger logger = LogManager.getFormatterLogger(this.getClass()); - - /** - * Executes a database cleanup. - */ - protected abstract void cleanup(); - - /** - * Provides a common scheduler registration. - * - * @see #cleanup() - */ - @Scheduled(cron = "0 0/5 * * * ?") - public final void doCleanup() { - this.cleanup(); - } - - /** - * Ensures database cleanups are executed at least once during application startup. - */ - @PostConstruct - public final void doSetup() { - this.setup(); - - this.logger.info("Commencing initial database cleanup."); - this.cleanup(); - } - - /** - * Handles the service initialization. - */ - protected void setup() { - } - } - - /** - * High Aggressiveness Cleanup Service - * - * Cleans out all objects that reached the maximum possible TTL. - */ - @Service - @ConditionalOnCacheAggressiveness(CacheConfiguration.Aggressiveness.HIGH) - class HighAggressivenessCleanupService extends AbstractCleanupService { - public static final Duration NAME_EXPIRATION_DURATION = Duration.ofDays(37).minusMinutes(6); // 6 Minutes less than actual timeout to work around issues with the scheduler - private final DisplayNameRepository displayNameRepository; - - @Autowired - public HighAggressivenessCleanupService(@Nonnull DisplayNameRepository displayNameRepository) { - this.displayNameRepository = displayNameRepository; - } - - /** - * {@inheritDoc} - */ - @Override - public void cleanup() { - long count = 0; - logger.info("Cleaning up cached values using high aggressiveness ..."); - { - try (Stream stream = this.displayNameRepository.findByLastSeenLessThanOrderByLastSeenDesc(Instant.now().minus(NAME_EXPIRATION_DURATION))) { - List names = stream.collect(Collectors.toList()); - - count = names.size(); - this.displayNameRepository.delete(names); - } - } - logger.info("Cleanup successful: %d values have been removed.", count); - } - - } - - /** - * Low Aggressiveness Cleanup Service - * - * Cleans out all objects that reached their maximum TTL. - */ - @Service - @ConditionalOnCacheAggressiveness(CacheConfiguration.Aggressiveness.LOW) - class LowAggressivenessCleanupService extends HighAggressivenessCleanupService { - - @Autowired - public LowAggressivenessCleanupService(@Nonnull DisplayNameRepository displayNameRepository) { - super(displayNameRepository); - } - } - - /** - * Moderate Aggressiveness Cleanup Service - * - * Cleans out all objects that reached the configured TTL. - */ - @Service - @ConditionalOnCacheAggressiveness(CacheConfiguration.Aggressiveness.MODERATE) - class ModerateAggressivenessCleanupService extends AbstractCleanupService { - private final CacheConfiguration cacheConfiguration; - private final DisplayNameRepository displayNameRepository; - private final ProfilePropertyRepository profilePropertyRepository; - private final ProfileRepository profileRepository; - - @Autowired - public ModerateAggressivenessCleanupService(@Nonnull CacheConfiguration cacheConfiguration, @Nonnull DisplayNameRepository displayNameRepository, @Nonnull ProfilePropertyRepository profilePropertyRepository, @Nonnull ProfileRepository profileRepository) { - this.cacheConfiguration = cacheConfiguration; - this.displayNameRepository = displayNameRepository; - this.profilePropertyRepository = profilePropertyRepository; - this.profileRepository = profileRepository; - } - - /** - * {@inheritDoc} - */ - @Override - public void cleanup() { - long count = 0; - logger.info("Cleaning up cached values using moderate aggressiveness ..."); - { - if (this.cacheConfiguration.getTtl().getProfile() > 0) { - count += this.cleanupProfiles(); - } - - if (this.cacheConfiguration.getTtl().getName() > 0) { - count += this.cleanupDisplayNames(); - } - - if (this.cacheConfiguration.getTtl().getProperty() > 0) { - count += this.cleanupProperties(); - } - } - logger.info("Cleanup successful: %d values have been removed.", count); - } - - /** - * Removes all profiles which are considered expired based on the user's TTL configuration. - * - * @return the amount of deleted display names. - */ - private long cleanupDisplayNames() { - Instant expirationTimestamp = Instant.now().minusSeconds(this.cacheConfiguration.getTtl().getName()); - - try (Stream stream = this.displayNameRepository.findByLastSeenLessThanOrderByLastSeenDesc(expirationTimestamp)) { - List names = stream.collect(Collectors.toList()); - long count = names.size(); - - this.displayNameRepository.delete(names); - return count; - } - } - - /** - * Removes all profiles which are considered expired based on the user's TTL configuration. - * - * @return the amount of deleted profiles. - */ - private long cleanupProfiles() { - Instant expirationTimestamp = Instant.now().minusSeconds(this.cacheConfiguration.getTtl().getProfile()); - - try (Stream stream = this.profileRepository.findByLastSeenLessThanOrderByLastSeenDesc(expirationTimestamp)) { - List profiles = stream.collect(Collectors.toList()); - long count = profiles.size(); - - this.profileRepository.delete(profiles); - return count; - } - } - - /** - * Removes all profiles which are considered expired based on the user's TTL configuration. - * - * @return the amount of deleted properties. - */ - private long cleanupProperties() { - Instant expirationTimestamp = Instant.now().minusSeconds(this.cacheConfiguration.getTtl().getProperty()); - - try (Stream stream = this.profilePropertyRepository.findByLastSeenLessThanOrderByLastSeenDesc(expirationTimestamp)) { - List properties = stream.collect(Collectors.toList()); - long count = properties.size(); - - this.profilePropertyRepository.delete(properties); - return count; - } - } - - /** - * {@inheritDoc} - */ - @Override - protected void setup() { - if (this.cacheConfiguration.getTtl().getName() > Duration.ofDays(37).getSeconds()) { - logger.warn("+=============================+"); - logger.warn("| DANGEROUS TTL CONFIGURATION |"); - logger.warn("+-----------------------------+"); - logger.warn("| The server was configured |"); - logger.warn("| to cache display names for |"); - logger.warn("| more than 37 days! |"); - logger.warn("| |"); - logger.warn("| This WILL cause issues when |"); - logger.warn("| searching for profiles or |"); - logger.warn("| UUIDs based on names! |"); - logger.warn("+=============================+"); - } - } - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/service/api/MojangUUID.java b/server/src/main/java/com/torchmind/stockpile/server/service/api/MojangUUID.java deleted file mode 100644 index 4c4bf08..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/service/api/MojangUUID.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.service.api; - -import javax.annotation.Nonnull; -import java.util.Objects; -import java.util.UUID; - -/** - * Mojang UUID - * - * Represents a Mojang encoded UUID (a UUID which does not include dashes). - * - * @author Johannes Donath - */ -public class MojangUUID { - private final String encoded; - - public MojangUUID(@Nonnull String encoded) { - this.encoded = encoded; - } - - public MojangUUID(@Nonnull UUID uuid) { - this.encoded = uuid.toString().replace("-", ""); - } - - /** - * Converts a Mojang UUID into a regular sane UUID. - * - * @return a UUID. - */ - @Nonnull - public UUID toUUID() { - String group1 = this.encoded.substring(0, 8); - String group2 = this.encoded.substring(8, 12); - String group3 = this.encoded.substring(12, 16); - String group4 = this.encoded.substring(16, 20); - String group5 = this.encoded.substring(20); - - return UUID.fromString(group1 + '-' + group2 + '-' + group3 + '-' + group4 + '-' + group5); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || this.getClass() != o.getClass()) return false; - - MojangUUID that = (MojangUUID) o; - return Objects.equals(this.encoded, that.encoded); - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - return Objects.hash(this.encoded); - } - - /** - * {@inheritDoc} - */ - @Override - public String toString() { - return this.encoded; - } -} diff --git a/server/src/main/java/com/torchmind/stockpile/server/service/api/ProfileService.java b/server/src/main/java/com/torchmind/stockpile/server/service/api/ProfileService.java deleted file mode 100644 index 83f2a54..0000000 --- a/server/src/main/java/com/torchmind/stockpile/server/service/api/ProfileService.java +++ /dev/null @@ -1,376 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.service.api; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectReader; -import com.torchmind.stockpile.server.configuration.CacheConfiguration; -import com.torchmind.stockpile.server.entity.DisplayName; -import com.torchmind.stockpile.server.entity.Profile; -import com.torchmind.stockpile.server.entity.ProfileProperty; -import com.torchmind.stockpile.server.entity.repository.DisplayNameRepository; -import com.torchmind.stockpile.server.entity.repository.ProfilePropertyRepository; -import com.torchmind.stockpile.server.entity.repository.ProfileRepository; -import com.torchmind.stockpile.server.error.NoSuchProfileException; -import com.torchmind.stockpile.server.error.ServiceException; -import com.torchmind.stockpile.server.error.TooManyRequestsException; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import javax.annotation.concurrent.ThreadSafe; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.time.Instant; -import java.util.Optional; -import java.util.UUID; - -/** - * Profile Service - * - * Provides simplified methods of retrieving a specific profile - * - * @author Johannes Donath - */ -@Service -@ThreadSafe -public class ProfileService { - public static final String JOIN_URL_TEMPLATE = "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%1$s&serverId=%2$s"; - public static final String NAME_URL_TEMPLATE = "https://api.mojang.com/users/profiles/minecraft/%s"; - public static final String PROFILE_URL_TEMPLATE = "https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=false"; - public static final ObjectReader reader; - private final CacheConfiguration cacheConfiguration; - private final DisplayNameRepository displayNameRepository; - private static final Logger logger = LogManager.getFormatterLogger(ProfileService.class); - private final ProfilePropertyRepository profilePropertyRepository; - private final ProfileRepository profileRepository; - - static { - ObjectMapper mapper = new ObjectMapper(); - mapper.findAndRegisterModules(); - reader = mapper.reader(); - } - - @Autowired - public ProfileService(@Nonnull CacheConfiguration cacheConfiguration, @Nonnull ProfileRepository profileRepository, @Nonnull DisplayNameRepository displayNameRepository, @Nonnull ProfilePropertyRepository profilePropertyRepository) { - this.cacheConfiguration = cacheConfiguration; - this.profileRepository = profileRepository; - this.displayNameRepository = displayNameRepository; - this.profilePropertyRepository = profilePropertyRepository; - } - - /** - * Fetches an entire profile directly from Mojang and adds it to the local cache. - * - * @param identifier a profile identifier. - * @return a profile. - * - * @throws IOException when an error occurs while contacting Mojang. - * @throws TooManyRequestsException when the rate limit is exceeded. - */ - @Nonnull - private Profile fetch(@Nonnull UUID identifier) throws IOException { - return this.fetchProfile(new URL(String.format(PROFILE_URL_TEMPLATE, (new MojangUUID(identifier)).toString()))); - } - - /** - * Fetches a display name's associated identifier directly from Mojang. - * - * @param name a display name. - * @return an identifier. - * - * @throws IOException when an error occurs while contacting Mojang. - * @throws TooManyRequestsException when the rate limit is exceeded. - */ - @Nonnull - private UUID fetchIdentifier(@Nonnull String name) throws IOException { - URL identifierUrl = new URL(String.format(NAME_URL_TEMPLATE, name)); - HttpURLConnection connection = (HttpURLConnection) identifierUrl.openConnection(); - - switch (connection.getResponseCode()) { - case 204: - // Dear Mojang, - // 204 No Content is not the correct status code to signify no results - // Thanks for your time - throw new NoSuchProfileException(name); - case 429: - throw new TooManyRequestsException("Rate limit exceeded"); - } - - try (InputStream inputStream = connection.getInputStream()) { - JsonNode node = reader.readTree(inputStream); - UUID identifier = (new MojangUUID(node.get("id").asText())).toUUID(); - - // fetch existing profile or create an entirely new one - final Profile profile; - { - Profile prof = this.profileRepository.findOne(identifier); - - if (prof == null) { - profile = new Profile(identifier); - } else { - profile = prof; - profile.setLastSeen(Instant.now()); - } - } - this.profileRepository.save(profile); - - // fetch an existing display name or create an entirely new one - final String currentName = node.get("name").asText(); - final DisplayName displayName = this.displayNameRepository.findOneByNameAndProfile(currentName, profile).orElseGet(() -> new DisplayName(currentName, Instant.now(), profile)); - displayName.setLastSeen(Instant.now()); - this.displayNameRepository.save(displayName); - profile.addName(displayName); - - return identifier; - } - } - - /** - * Fetches a cached version of a display name identifier from the database backend. - * - * @param name a display name. - * @return an identifier or null. - */ - @Nullable - private UUID fetchIdentifierLocal(@Nonnull String name) { - return this.displayNameRepository.findOneByName(name).map((d) -> d.getProfile().getIdentifier()).orElse(null); - } - - /** - * Fetches a cached version of a profile from the database backend. - * - * @param identifier an identifier. - * @return a profile or null. - */ - @Nullable - private Profile fetchLocal(@Nonnull UUID identifier) { - return this.profileRepository.findOne(identifier); - } - - /** - * Fetches a profile from the specified URL. - * - * @param url a url. - * @return a profile - * - * @throws IOException when an error occurs. - */ - @Nonnull - private Profile fetchProfile(@Nonnull URL url) throws IOException { - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - - switch (connection.getResponseCode()) { - case 204: - // Dear Mojang, - // 204 No Content is not the correct status code to signify no results - // Thanks for your time - throw new NoSuchProfileException(); - case 429: - throw new TooManyRequestsException("Rate limit exceeded"); - } - - try (InputStream inputStream = connection.getInputStream()) { - JsonNode node = reader.readTree(inputStream); - UUID identifier = (new MojangUUID(node.get("id").asText())).toUUID(); - - // try to fetch a profile or create a new one if none is stored within the database - final Profile profile; - { - Profile prof = this.profileRepository.findOne(identifier); - - if (prof == null) { - profile = new Profile(identifier); - } else { - profile = prof; - profile.setLastSeen(Instant.now()); - } - } - this.profileRepository.save(profile); - - // try to fetch a display name for the current profile and create a new one if none is stored in - // the database - DisplayName displayName = this.displayNameRepository.findOneByNameAndProfile(node.get("name").asText(), profile).orElseGet(() -> new DisplayName(node.get("name").asText(), Instant.now(), profile)); - - displayName.setLastSeen(Instant.now()); - this.displayNameRepository.save(displayName); - - // iterate over all properties and create/update their respective values - node.get("properties").forEach((p) -> { - String name = p.get("name").asText(); - - ProfileProperty property = this.profilePropertyRepository.findByNameAndProfile(name, profile).orElseGet(() -> new ProfileProperty(profile, name, p.get("value").asText(), (p.has("signature") ? p.get("signature").asText() : null))); - - property.setLastSeen(Instant.now()); - this.profilePropertyRepository.save(property); - profile.addProperty(property); - }); - - return profile; - } - } - - /** - * Finds a profile based on its current display name in the local cache or directly in Mojang's database based - * on the current cache aggressiveness. - * - * @param name a display name. - * @return a profile. - * - * @throws NoSuchProfileException when the profile was not found. - * @throws ServiceException when the profile could not be accessed. - */ - @Nonnull - public Profile find(@Nonnull String name) { - return this.get(this.findIdentifier(name)); - } - - /** - * Finds a profile identifier based on its current display name in the local cache or directly in Mojang's - * database based on the current cache aggressiveness. - * - * @param name a display name. - * @return a profile. - */ - @Nonnull - public UUID findIdentifier(@Nonnull String name) { - if (this.cacheConfiguration.getAggressiveness() == CacheConfiguration.Aggressiveness.LOW) { - UUID identifier; - - try { - identifier = this.fetchIdentifier(name); - } catch (TooManyRequestsException | IOException ex) { - identifier = this.fetchIdentifierLocal(name); - } - - if (identifier == null) { - throw new NoSuchProfileException(name); - } - - return identifier; - } - - UUID identifier = this.fetchIdentifierLocal(name); - - if (identifier == null) { - try { - identifier = this.fetchIdentifier(name); - } catch (FileNotFoundException ex) { - throw new NoSuchProfileException(name); - } catch (TooManyRequestsException | IOException ex) { - logger.error("Failed to poll identifier for profile \"" + name + "\": " + ex.getMessage(), ex); - throw new ServiceException("Could not poll identifier from upstream: " + ex.getMessage(), ex); - } - } - - return identifier; - } - - /** - * Retrieves a profile from the cache or pulls a fresh copy directly from Mojang based on the current cache - * aggressiveness. - * - * @param identifier an identifier. - * @return a profile. - * - * @throws NoSuchProfileException when the profile was not found. - * @throws ServiceException when the profile could not be accessed. - */ - @Nonnull - public Profile get(@Nonnull UUID identifier) { - if (this.cacheConfiguration.getAggressiveness() == CacheConfiguration.Aggressiveness.LOW) { - try { - this.fetch(identifier); - } catch (FileNotFoundException ex) { - throw new NoSuchProfileException(identifier); - } catch (TooManyRequestsException | IOException ex) { - return Optional.ofNullable(this.fetchLocal(identifier)).orElseThrow(() -> new ServiceException("Cannot poll nor find cached profile \"" + identifier + "\": " + ex.getMessage(), ex)); - } - } - - Profile profile = this.fetchLocal(identifier); - - if (profile == null || profile.getNames().size() == 0 || profile.getProperties().size() == 0) { - try { - return this.fetch(identifier); - } catch (IOException ex) { - if (profile != null) { - return profile; - } - - logger.error("Could not poll profile for identifier \"" + identifier.toString() + "\": " + ex.getMessage(), ex); - throw new ServiceException("Could not poll profile from upstream: " + ex.getMessage(), ex); - } - } - - return profile; - } - - /** - * Fetches a profile by proxying a join request. - * - * @param username a username. - * @param serverId a serverId hash. - * @return a profile. - */ - @Nonnull - public Profile join(@Nonnull String username, @Nonnull String serverId) { - try { - return this.fetchProfile(new URL(String.format(JOIN_URL_TEMPLATE, username, serverId))); - } catch (FileNotFoundException ex) { - throw new NoSuchProfileException(username); - } catch (TooManyRequestsException | IOException ex) { - throw new ServiceException("Could not poll profile from session server: " + ex.getMessage(), ex); - } - } - - /** - * Purges an entire profile cache and all of its associated data. - * - * @param identifier an identifier. - */ - public void purge(@Nonnull UUID identifier) { - this.profileRepository.delete(identifier); - } - - /** - * Purges an entire profile cache and all of its associated data. - * - * @param name a display name. - */ - public void purge(@Nonnull String name) { - // FIXME: Custom delete methods will probably improve this method's performance - this.displayNameRepository.findOneByName(name).ifPresent((n) -> this.profileRepository.delete(n.getProfile())); - } - - /** - * Purges a single display name from the cache. - * - * @param name a name. - */ - public void purgeName(@Nonnull String name) { - // FIXME: Custom delete methods will probably improve this method's performance - this.displayNameRepository.findOneByName(name).ifPresent(this.displayNameRepository::delete); - } -} diff --git a/server/src/main/resources/banner.txt b/server/src/main/resources/banner.txt deleted file mode 100644 index 426d3de..0000000 --- a/server/src/main/resources/banner.txt +++ /dev/null @@ -1,10 +0,0 @@ - - _____ __ __ _ __ ___ ____ - / ___// /_____ _____/ /______ (_) /__ _ _< // __ \ - \__ \/ __/ __ \/ ___/ //_/ __ \/ / / _ \ | | / / // / / / - ___/ / /_/ /_/ / /__/ ,< / /_/ / / / __/ | |/ / // /_/ / -/____/\__/\____/\___/_/|_/ .___/_/_/\___/ |___/_(_)____/ - /_/ - -Copyright (C) 2016 Torchmind and other Contributors -Provided under the terms of the Apache License, Version 2.0 diff --git a/server/src/main/resources/favicon.ico b/server/src/main/resources/favicon.ico deleted file mode 100644 index 243a85b..0000000 --- a/server/src/main/resources/favicon.ico +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:72f10f77c74ecc0a5518be16b13ab9ea0ee84708ffc50dcc7848431badc2aef7 -size 4286 diff --git a/server/src/main/resources/log4j2.xml b/server/src/main/resources/log4j2.xml deleted file mode 100644 index 66f2f7c..0000000 --- a/server/src/main/resources/log4j2.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - ???? - [%d{yyyy-MM-dd HH:mm:ss.SSS}]%X{context} ${sys:PID} %5p [%t] --- %c{1}: %m%n - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/server/src/main/resources/static/index.html b/server/src/main/resources/static/index.html deleted file mode 100644 index 2ce9b16..0000000 --- a/server/src/main/resources/static/index.html +++ /dev/null @@ -1,164 +0,0 @@ - - - - - - - Stockpile v${project.version} - - - - - - - - -
- -
-

Hey there!

-

Thank you for choosing Stockpile! We work hard on this project and aim to provide the best quality possible to improve your daily life as a developer.
This application does not have an interface and thus we recommend that you use one of our Client Implementations.

-

For your convenience we have collected a set of resources to get you started quicker:

- - -
  • - Releases -
  • -
  • - Documentation -
  • -
  • - Source Code -
  • -
    - -

    Oh, and don't forget to check out the Configuration Guide to get the most out of your Stockpile installation!

    -

    - Happy coding!
    - The Stockpile Team -

    -
    - -
    - - diff --git a/server/src/main/resources/static/logo.svg b/server/src/main/resources/static/logo.svg deleted file mode 100644 index 5d76afd..0000000 --- a/server/src/main/resources/static/logo.svg +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/server/src/test/java/com/torchmind/stockpile/server/service/api/MojangUUIDTest.java b/server/src/test/java/com/torchmind/stockpile/server/service/api/MojangUUIDTest.java deleted file mode 100644 index 226a716..0000000 --- a/server/src/test/java/com/torchmind/stockpile/server/service/api/MojangUUIDTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2016 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 com.torchmind.stockpile.server.service.api; - -import org.junit.Assert; -import org.junit.Test; - -import java.util.UUID; - -/** - * Mojang UUID Test - * - * Tests the Mojang UUID representation against the relevant specifications. - * - * @author Johannes Donath - * @see MojangUUID - */ -public class MojangUUIDTest { - private static final String MOJANG_UUID_STRING = "4566e69fc90748ee8d71d7ba5aa00d20"; - private static final MojangUUID MOJANG_UUID_POJO = new MojangUUID(MOJANG_UUID_STRING); - private static final String UUID_STRING = "4566e69f-c907-48ee-8d71-d7ba5aa00d20"; - private static final UUID UUID_POJO = UUID.fromString(UUID_STRING); - - /** - * Tests the conversion from Mojang UUIDs to strings. - * - * @see MojangUUID#toString() - */ - @Test - public void testToString() { - Assert.assertEquals(MOJANG_UUID_STRING, MOJANG_UUID_POJO.toString()); - } - - /** - * Tests the conversion from Mojang UUIDs to regular UUIDs. - * - * @see MojangUUID#toUUID() - */ - @Test - public void testToUUID() { - Assert.assertEquals(UUID_POJO, MOJANG_UUID_POJO.toUUID()); - } - -} From cd8ee3c3719d4d2d2fbd7c44d5983183c4f91a54 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 24 Jun 2018 11:15:02 +0200 Subject: [PATCH 002/142] Updated the git ignore to reflect the new language and tools. --- .gitignore | 41 ++++++++++++++++------------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 5317cca..aa04587 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,23 @@ -# Created by https://www.gitignore.io/api/java,maven +# Created by https://www.gitignore.io/api/go -### Maven ### -target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties +### Go ### +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +# Test binary, build with `go test -c` +*.test -### Java ### -*.class +# Output of the go coverage tool, specifically when used with LiteIDE +*.out -# Mobile Tools for Java (J2ME) -.mtj.tmp/ -# Package Files # -*.jar -*.war -*.ear +# End of https://www.gitignore.io/api/go -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* - -### Stockpile ### -logs/ -/application.yml +# dep +vendor/ +Gopkg.lock From 892fc76e1c24e8e266d476ccca9971131eebe174 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 24 Jun 2018 11:15:56 +0200 Subject: [PATCH 003/142] Created an empty go dep configuration. --- Gopkg.toml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Gopkg.toml diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..5c879c7 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,3 @@ +[prune] + go-tests = true + unused-packages = true From dc3f839f526381f3fa00cd6f0e6778fb6677dce0 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 24 Jun 2018 11:17:42 +0200 Subject: [PATCH 004/142] Re-imported the license. --- LICENSE | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. From 4ee496851d4ffc9b14c5dab0a8599118df14afb7 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 24 Jun 2018 12:57:17 +0200 Subject: [PATCH 005/142] Created a basic makefile which automates the build process. --- Makefile | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9c5867f --- /dev/null +++ b/Makefile @@ -0,0 +1,78 @@ +APPLICATION_BRAND := vanilla +APPLICATION_VERSION := 2.0.0 +APPLICATION_COMMIT_HASH := `git log -1 --pretty=format:"%H"` +APPLICATION_TIMESTAMP := `date --utc "+%s"` + +LDFLAGS :=-ldflags "-X github.com/dotStart/Stockpile/metadata.brand=${APPLICATION_BRAND} -X github.com/dotStart/Stockpile/metadata.version=${APPLICATION_VERSION} -X github.com/dotStart/Stockpile/metadata.commitHash=${APPLICATION_COMMIT_HASH} -X \"github.com/dotStart/Stockpile/metadata.timestampRaw=${APPLICATION_TIMESTAMP}\"" + +.PHONY: print-config install-dependencies generate-sources build package + +clean: + @echo "==> Clearing previous build data" + @rm -rf build/ || true + @go clean -cache + +print-config: + @echo "==> Build Configuration" + @echo "" + @echo " Brand: ${APPLICATION_BRAND}" + @echo " Version: ${APPLICATION_VERSION}" + @echo " Commit SHA: ${APPLICATION_COMMIT_HASH}" + @echo " Timestamp: ${APPLICATION_TIMESTAMP}" + @echo "" + @echo " Linker Flags: ${LDFLAGS}" + @echo "" + +install-dependencies: + @echo "==> Installing dependencies" + @dep ensure -v + @echo "" + +generate-sources: + @echo "==> Generating protobuf sources" + # todo + @echo "" + +build: build/mac32/stockpile build/mac64/stockpile \ + build/linux32/stockpile build/linux64/stockpile build/linuxarm/stockpile \ + build/win32/stockpile.exe build/win64/stockpile.exe + +build/mac32/stockpile: + @echo "==> Compiling Stockpile for Mac OS (32-Bit)" + @export GOOS="darwin"; export GOARCH="386"; go build -v ${LDFLAGS} -o build/mac32/stockpile + @echo "" + +build/mac64/stockpile: + @echo "==> Compiling Stockpile for Mac OS (64-Bit)" + @export GOOS="darwin"; export GOARCH="amd64"; go build -v ${LDFLAGS} -o build/mac64/stockpile + @echo "" + +build/linux32/stockpile: + @echo "==> Compiling Stockpile for Linux (32-Bit)" + @export GOOS="linux"; export GOARCH="386"; go build -v ${LDFLAGS} -o build/linux32/stockpile + @echo "" + +build/linux64/stockpile: + @echo "==> Compiling Stockpile for Linux (64-Bit)" + @export GOOS="linux"; export GOARCH="amd64"; go build -v ${LDFLAGS} -o build/linux64/stockpile + @echo "" + +build/linuxarm/stockpile: + @echo "==> Compiling Stockpile for Linux (ARM)" + @export GOOS="linux"; export GOARCH="arm"; go build -v ${LDFLAGS} -o build/linuxarm/stockpile + @echo "" + +build/win32/stockpile.exe: + @echo "==> Compiling Stockpile for Windows (32-Bit)" + @export GOOS="windows"; export GOARCH="386"; go build -v ${LDFLAGS} -o build/win32/stockpile.exe + @echo "" + +build/win64/stockpile.exe: + @echo "==> Compiling Stockpile for Windows (64-Bit)" + @export GOOS="windows"; export GOARCH="amd64"; go build -v ${LDFLAGS} -o build/win64/stockpile.exe + @echo "" + +package: + @echo "==> Creating distribution packages" + @for dir in build/*; do if [ -d "$$dir" ]; then tar -czvf "$(basename "$$dir").tar.gz" --xform="s,$$dir/,," "$$dir"; fi; done + @echo "" From 0dd4d11b781c0843d250c4267fdb0770aa817a0a Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 24 Jun 2018 12:57:53 +0200 Subject: [PATCH 006/142] Created a metadata package which exposes information about the application build. --- metadata/methods.go | 54 +++++++++++++++++++++++++++++++++++++++++++ metadata/variables.go | 33 ++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 metadata/methods.go create mode 100644 metadata/variables.go diff --git a/metadata/methods.go b/metadata/methods.go new file mode 100644 index 0000000..b3525a8 --- /dev/null +++ b/metadata/methods.go @@ -0,0 +1,54 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 metadata + +import "time" + +// Identifies the application brand (typically "vanilla") +func Brand() string { + return brand +} + +// Evaluates whether this version is a custom build +func IsCustomBuild() bool { + return Brand() != "vanilla" +} + +// Retrieves the application version +func Version() string { + return version +} + +// Retrieves the full application version (including its build identifier) +func VersionFull() string { + versionExtension := "+dev" + if commitHash != "" { + versionExtension = "+git-" + commitHash + } + + return version + versionExtension +} + +// Retrieves the commit hash from which this version was built +func CommitHash() string { + return commitHash +} + +// Retrieves the date and time at which this version was built +func Timestamp() time.Time { + return timestampParsed +} diff --git a/metadata/variables.go b/metadata/variables.go new file mode 100644 index 0000000..be8db80 --- /dev/null +++ b/metadata/variables.go @@ -0,0 +1,33 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 metadata + +import ( + "strconv" + "time" +) + +// Exposes the application brand +var brand = "vanilla" +// Exposes the current application release version +var version = "0.0.0" +// Exposes the commit has of the head revision at compile time +var commitHash = "" + +var timestampRaw = "0" +var timestamp, _ = strconv.ParseInt(timestampRaw, 10, 64) +var timestampParsed = time.Unix(timestamp, 0) From e4c18779ec12aeed393e32845dcadf739d40651d Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 24 Jun 2018 13:20:54 +0200 Subject: [PATCH 007/142] Added runtime shortening of the commit hash. --- metadata/methods.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metadata/methods.go b/metadata/methods.go index b3525a8..e94b306 100644 --- a/metadata/methods.go +++ b/metadata/methods.go @@ -37,7 +37,7 @@ func Version() string { func VersionFull() string { versionExtension := "+dev" if commitHash != "" { - versionExtension = "+git-" + commitHash + versionExtension = "+git-" + commitHash[0:7] } return version + versionExtension From 7797bf7360b5f433b1bb871266b6633d95708948 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 24 Jun 2018 13:39:54 +0200 Subject: [PATCH 008/142] Added a commit hash substitution during development. --- metadata/methods.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/metadata/methods.go b/metadata/methods.go index e94b306..44c8840 100644 --- a/metadata/methods.go +++ b/metadata/methods.go @@ -45,6 +45,10 @@ func VersionFull() string { // Retrieves the commit hash from which this version was built func CommitHash() string { + if len(commitHash) == 0 { + return "development" + } + return commitHash } From bf5ab489e75ff24bfe3553ee904d9d9c80188d9a Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 24 Jun 2018 16:24:02 +0200 Subject: [PATCH 009/142] Removed the generate-sources target as we can simply rely on the go:generate tags. --- Makefile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 9c5867f..4472139 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ APPLICATION_TIMESTAMP := `date --utc "+%s"` LDFLAGS :=-ldflags "-X github.com/dotStart/Stockpile/metadata.brand=${APPLICATION_BRAND} -X github.com/dotStart/Stockpile/metadata.version=${APPLICATION_VERSION} -X github.com/dotStart/Stockpile/metadata.commitHash=${APPLICATION_COMMIT_HASH} -X \"github.com/dotStart/Stockpile/metadata.timestampRaw=${APPLICATION_TIMESTAMP}\"" -.PHONY: print-config install-dependencies generate-sources build package +.PHONY: print-config install-dependencies build package clean: @echo "==> Clearing previous build data" @@ -28,11 +28,6 @@ install-dependencies: @dep ensure -v @echo "" -generate-sources: - @echo "==> Generating protobuf sources" - # todo - @echo "" - build: build/mac32/stockpile build/mac64/stockpile \ build/linux32/stockpile build/linux64/stockpile build/linuxarm/stockpile \ build/win32/stockpile.exe build/win64/stockpile.exe From c9513d840dd2fd67d3a5b89fb9913071dfb81b6a Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 24 Jun 2018 16:25:02 +0200 Subject: [PATCH 010/142] Added the protocol buffer generated sources and build directory to the ignores. --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index aa04587..8d69599 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ # dep vendor/ Gopkg.lock + +# Stockpile +*.pb.go +build/ From 50a2d6864494c14587f5c84d9372cc3467c4e8a7 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Mon, 25 Jun 2018 16:18:22 +0200 Subject: [PATCH 011/142] Moved the entire source tree into a sub directory in order to permit the separation of RPC based plugins into their own executables. --- Makefile | 64 ++++++------------- stockpile/Makefile | 35 ++++++++++ {metadata => stockpile/metadata}/methods.go | 0 {metadata => stockpile/metadata}/variables.go | 0 4 files changed, 53 insertions(+), 46 deletions(-) create mode 100644 stockpile/Makefile rename {metadata => stockpile/metadata}/methods.go (100%) rename {metadata => stockpile/metadata}/variables.go (100%) diff --git a/Makefile b/Makefile index 4472139..cecfefd 100644 --- a/Makefile +++ b/Makefile @@ -2,15 +2,11 @@ APPLICATION_BRAND := vanilla APPLICATION_VERSION := 2.0.0 APPLICATION_COMMIT_HASH := `git log -1 --pretty=format:"%H"` APPLICATION_TIMESTAMP := `date --utc "+%s"` +export -LDFLAGS :=-ldflags "-X github.com/dotStart/Stockpile/metadata.brand=${APPLICATION_BRAND} -X github.com/dotStart/Stockpile/metadata.version=${APPLICATION_VERSION} -X github.com/dotStart/Stockpile/metadata.commitHash=${APPLICATION_COMMIT_HASH} -X \"github.com/dotStart/Stockpile/metadata.timestampRaw=${APPLICATION_TIMESTAMP}\"" +PLUGINS := $(wildcard plugins/*/.) -.PHONY: print-config install-dependencies build package - -clean: - @echo "==> Clearing previous build data" - @rm -rf build/ || true - @go clean -cache +all: print-config install-dependencies core core-plugins package print-config: @echo "==> Build Configuration" @@ -20,54 +16,30 @@ print-config: @echo " Commit SHA: ${APPLICATION_COMMIT_HASH}" @echo " Timestamp: ${APPLICATION_TIMESTAMP}" @echo "" - @echo " Linker Flags: ${LDFLAGS}" - @echo "" + +clean: + @echo "==> Clearing previous build data" + @rm -rf build/ || true + @go clean -cache install-dependencies: @echo "==> Installing dependencies" @dep ensure -v @echo "" -build: build/mac32/stockpile build/mac64/stockpile \ - build/linux32/stockpile build/linux64/stockpile build/linuxarm/stockpile \ - build/win32/stockpile.exe build/win64/stockpile.exe +core: + @echo "==> Building stockpile" + $(MAKE) -C stockpile/ -build/mac32/stockpile: - @echo "==> Compiling Stockpile for Mac OS (32-Bit)" - @export GOOS="darwin"; export GOARCH="386"; go build -v ${LDFLAGS} -o build/mac32/stockpile - @echo "" - -build/mac64/stockpile: - @echo "==> Compiling Stockpile for Mac OS (64-Bit)" - @export GOOS="darwin"; export GOARCH="amd64"; go build -v ${LDFLAGS} -o build/mac64/stockpile - @echo "" - -build/linux32/stockpile: - @echo "==> Compiling Stockpile for Linux (32-Bit)" - @export GOOS="linux"; export GOARCH="386"; go build -v ${LDFLAGS} -o build/linux32/stockpile - @echo "" - -build/linux64/stockpile: - @echo "==> Compiling Stockpile for Linux (64-Bit)" - @export GOOS="linux"; export GOARCH="amd64"; go build -v ${LDFLAGS} -o build/linux64/stockpile - @echo "" - -build/linuxarm/stockpile: - @echo "==> Compiling Stockpile for Linux (ARM)" - @export GOOS="linux"; export GOARCH="arm"; go build -v ${LDFLAGS} -o build/linuxarm/stockpile - @echo "" - -build/win32/stockpile.exe: - @echo "==> Compiling Stockpile for Windows (32-Bit)" - @export GOOS="windows"; export GOARCH="386"; go build -v ${LDFLAGS} -o build/win32/stockpile.exe - @echo "" - -build/win64/stockpile.exe: - @echo "==> Compiling Stockpile for Windows (64-Bit)" - @export GOOS="windows"; export GOARCH="amd64"; go build -v ${LDFLAGS} -o build/win64/stockpile.exe - @echo "" +core-plugins: + @echo "==> Building core plugins" + @for dir in $(PLUGINS); do \ + "$(MAKE)" -C $$dir; \ + done package: @echo "==> Creating distribution packages" @for dir in build/*; do if [ -d "$$dir" ]; then tar -czvf "$(basename "$$dir").tar.gz" --xform="s,$$dir/,," "$$dir"; fi; done @echo "" + +.PHONY: all diff --git a/stockpile/Makefile b/stockpile/Makefile new file mode 100644 index 0000000..b4470bb --- /dev/null +++ b/stockpile/Makefile @@ -0,0 +1,35 @@ +LDFLAGS :=-ldflags "-X github.com/dotStart/Stockpile/metadata.brand=${APPLICATION_BRAND} -X github.com/dotStart/Stockpile/metadata.version=${APPLICATION_VERSION} -X github.com/dotStart/Stockpile/metadata.commitHash=${APPLICATION_COMMIT_HASH} -X \"github.com/dotStart/Stockpile/metadata.timestampRaw=${APPLICATION_TIMESTAMP}\"" + +build: ../build/mac32/stockpile ../build/mac64/stockpile \ + ../build/linux32/stockpile ../build/linux64/stockpile ../build/linuxarm/stockpile \ + ../build/win32/stockpile.exe ../build/win64/stockpile.exe + +../build/mac32/stockpile: + @export GOOS="darwin"; export GOARCH="386"; go build -v ${LDFLAGS} -o ../build/mac32/stockpile + @echo "" + +../build/mac64/stockpile: + @export GOOS="darwin"; export GOARCH="amd64"; go build -v ${LDFLAGS} -o ../build/mac64/stockpile + @echo "" + +../build/linux32/stockpile: + @export GOOS="linux"; export GOARCH="386"; go build -v ${LDFLAGS} -o ../build/linux32/stockpile + @echo "" + +../build/linux64/stockpile: + @export GOOS="linux"; export GOARCH="amd64"; go build -v ${LDFLAGS} -o ../build/linux64/stockpile + @echo "" + +../build/linuxarm/stockpile: + @export GOOS="linux"; export GOARCH="arm"; go build -v ${LDFLAGS} -o ../build/linuxarm/stockpile + @echo "" + +../build/win32/stockpile.exe: + @export GOOS="windows"; export GOARCH="386"; go build -v ${LDFLAGS} -o ../build/win32/stockpile.exe + @echo "" + +../build/win64/stockpile.exe: + @export GOOS="windows"; export GOARCH="amd64"; go build -v ${LDFLAGS} -o ../build/win64/stockpile.exe + @echo "" + +.PHONY: build diff --git a/metadata/methods.go b/stockpile/metadata/methods.go similarity index 100% rename from metadata/methods.go rename to stockpile/metadata/methods.go diff --git a/metadata/variables.go b/stockpile/metadata/variables.go similarity index 100% rename from metadata/variables.go rename to stockpile/metadata/variables.go From 0aa4027349a928da45051069827590e5330a176b Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Mon, 25 Jun 2018 16:31:43 +0200 Subject: [PATCH 012/142] Created a base command struct for client commands. --- stockpile/command/base.go | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 stockpile/command/base.go diff --git a/stockpile/command/base.go b/stockpile/command/base.go new file mode 100644 index 0000000..fb273c6 --- /dev/null +++ b/stockpile/command/base.go @@ -0,0 +1,44 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 command + +import ( + "flag" + "fmt" + + "github.com/dotStart/Stockpile/stockpile/server" + "google.golang.org/grpc" +) + +type ClientCommand struct { + flagServerAddress string +} + +// creates a new grpc client using the command configuration +func (c *ClientCommand) createClient() (*grpc.ClientConn, error) { + client, err := grpc.Dial(c.flagServerAddress, grpc.WithInsecure()) + if err != nil { + return nil, err + } + + return client, nil +} + +func (c *ClientCommand) SetFlags(f *flag.FlagSet) { + f.StringVar(&c.flagServerAddress, "server-address", fmt.Sprintf("%s:%d", server.DefaultAddress, server.DefaultPort), "specifies the address of the target server") + // TODO: TLS +} From 31cb2bb6082cb5b3c1594d3f86d3d8a77afca197 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Mon, 25 Jun 2018 16:32:16 +0200 Subject: [PATCH 013/142] Created a utility for command outputs. --- stockpile/command/utility.go | 79 ++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 stockpile/command/utility.go diff --git a/stockpile/command/utility.go b/stockpile/command/utility.go new file mode 100644 index 0000000..a2168b5 --- /dev/null +++ b/stockpile/command/utility.go @@ -0,0 +1,79 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 command + +import ( + "fmt" + "io" + "reflect" + "strings" +) + +// prints a struct in a human readable table format +func writeTable(writer io.Writer, data interface{}) { + val := reflect.ValueOf(data) + typ := val.Type() + + headerCellLength := 3 + valueCellLength := 5 + + for i := 0; i < typ.NumField(); i++ { + fieldDef := typ.Field(i) + field := val.Field(i) + + if strings.HasPrefix(fieldDef.Name, "XXX_") { + continue + } + + keyLength := len(fieldDef.Name) + val := fmt.Sprintf("%v", field) + valueLength := len(val) + + if keyLength > headerCellLength { + headerCellLength = keyLength + } + if valueLength > valueCellLength { + valueCellLength = valueLength + } + } + + io.WriteString(writer, "Key") + io.WriteString(writer, strings.Repeat(" ", headerCellLength-3)) + io.WriteString(writer, " | ") + io.WriteString(writer, "Value\n") + + io.WriteString(writer, strings.Repeat("-", headerCellLength)) + io.WriteString(writer, "-+-") + io.WriteString(writer, strings.Repeat("-", valueCellLength)) + io.WriteString(writer, "\n") + + for i := 0; i < typ.NumField(); i++ { + fieldDef := typ.Field(i) + field := val.Field(i) + + if strings.HasPrefix(fieldDef.Name, "XXX_") { + continue + } + + io.WriteString(writer, fieldDef.Name) + io.WriteString(writer, strings.Repeat(" ", headerCellLength-len(fieldDef.Name))) + io.WriteString(writer, " | ") + + io.WriteString(writer, fmt.Sprintf("%v", field)) + io.WriteString(writer, "\n") + } +} From fc1da77875de5f7329f1ef894c6bef849d700c67 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Mon, 25 Jun 2018 16:32:46 +0200 Subject: [PATCH 014/142] Created basic methods for Mojang ID conversions. --- stockpile/mojang/id.go | 62 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 stockpile/mojang/id.go diff --git a/stockpile/mojang/id.go b/stockpile/mojang/id.go new file mode 100644 index 0000000..9f511d0 --- /dev/null +++ b/stockpile/mojang/id.go @@ -0,0 +1,62 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 mojang + +import ( + "fmt" + "strings" + + "github.com/google/uuid" +) + +// Parses an identifier (regardless of whether it is supplied in its Mojang or standard format) +func ParseId(id string) (uuid.UUID, error) { + if IsMojangId(id) { + return ToStandardId(id) + } + + return uuid.Parse(id) +} + +// Evaluates whether the passed ID is a mojang identifier +func IsMojangId(id string) bool { + return !strings.Contains(id, "-") +} + +// Converts a standard UUID into its Mojang format +func ToMojangId(id uuid.UUID) string { + return strings.Replace(id.String(), "-", "", -1) +} + +// Converts a Mojang UUID into its RFC format +func ToStandardId(id string) (uuid.UUID, error) { + encoded, err := ToStandardIdString(id) + if err != nil { + return uuid.Nil, err + } + + return uuid.Parse(encoded) +} + +// Converts a Mojang UUID into its RFC format +func ToStandardIdString(id string) (string, error) { + if len(id) != 32 { + return uuid.Nil.String(), fmt.Errorf("illegal Mojang identifier length: expected 32 characters but got %d", len(id)) + } + + return fmt.Sprintf("%s-%s-%s-%s-%s", id[0:8], id[8:12], id[12:16], id[16:20], id[20:32]), nil +} From 47f97713b0c1d6067d082044d18b583c2aaa3be7 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Tue, 26 Jun 2018 11:52:35 +0200 Subject: [PATCH 015/142] Created the main logic around the Mojang API client. --- stockpile/mojang/main.go | 69 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 stockpile/mojang/main.go diff --git a/stockpile/mojang/main.go b/stockpile/mojang/main.go new file mode 100644 index 0000000..1595440 --- /dev/null +++ b/stockpile/mojang/main.go @@ -0,0 +1,69 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 mojang + +import ( + "fmt" + "io" + "net/http" + "runtime" + + "github.com/dotStart/Stockpile/stockpile/metadata" + "github.com/op/go-logging" +) + +type MojangAPI struct { + logger *logging.Logger + http *http.Client +} + +// Creates a new Mojang API client +func New() *MojangAPI { + return &MojangAPI{ + logger: logging.MustGetLogger("api"), + http: &http.Client{}, + } +} + +// Executes an HTTP request +func (a *MojangAPI) execute(method string, uri string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequest(method, uri, body) + if err != nil { + return nil, err + } + + req.Header.Set("user-agent", fmt.Sprintf("Stockpile/%s (Go/%s; %s; +https://github.com/dotStart/Stockpile)", metadata.VersionFull(), runtime.Version(), metadata.Brand())) + req.Header.Set("content-type", "application/json") + + a.logger.Debugf("Sending request: %s %s", method, uri) + res, err := a.http.Do(req) + if err != nil { + return nil, err + } + + statusCategory := res.StatusCode / 100 + if statusCategory == 2 { + return res, nil + } + if statusCategory == 4 { + return nil, fmt.Errorf("client error (code %d): %s", res.StatusCode, uri) + } + if statusCategory == 5 { + return nil, fmt.Errorf("server error (code %d): %s", res.StatusCode, uri) + } + return nil, fmt.Errorf("unknown error (code %d): %s", res.StatusCode, uri) +} From 605b3182c03b7a658f6206e73129889ec897c94d Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Tue, 26 Jun 2018 12:03:37 +0200 Subject: [PATCH 016/142] Created client methods which permit the resolving of names and the retrieval of name histories. --- stockpile/mojang/name.go | 173 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 stockpile/mojang/name.go diff --git a/stockpile/mojang/name.go b/stockpile/mojang/name.go new file mode 100644 index 0000000..5f4aa63 --- /dev/null +++ b/stockpile/mojang/name.go @@ -0,0 +1,173 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 mojang + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/url" + "time" + + "github.com/google/uuid" +) + +var unixEpoch = time.Unix(0, 0) + +// represents a single profile id mapping between a display name and a mapping at a given time +// note that lastSeenAt and validUntil may be set to UNIX epoch if the initial mapping is requested +type ProfileId struct { + Id uuid.UUID + RawId string `json:"id"` + Name string `json:"name"` + LastSeenAt time.Time + ValidUntil time.Time +} + +// represents a single name change within a profile's history +// note that changedToAt and validUntil may be set to UNIX epoch when the entry represents the initial account name +type NameChange struct { + Name string `json:"name"` + ChangedToAt time.Time + RawChangedToAt int64 `json:"changedToAt"` + ValidUntil time.Time +} + +func (p *ProfileId) init(lastSeen time.Time) error { + id, err := ToStandardId(p.RawId) + if err != nil { + return err + } + + p.Id = id + p.LastSeenAt = lastSeen + + if lastSeen != unixEpoch { + p.ValidUntil = CalculateNameGracePeriodEnd(lastSeen) + } else { + p.ValidUntil = unixEpoch + } + + return nil +} + +func (c *NameChange) init() error { + c.ChangedToAt = time.Unix(c.RawChangedToAt, 0) + + if c.RawChangedToAt != 0 { + c.ValidUntil = CalculateNameGracePeriodEnd(c.ChangedToAt) + } else { + c.ValidUntil = unixEpoch + } + + return nil +} + +// retrieves the profile id (and some associated attributes) for a given display name at the specified time +// - if the UNIX epoch (e.g. zero) is passed instead of a real time, the initial account name will be checked (assuming +// that the account in question is a legacy account or has changed its name at least once) +// - if no profile matches the specified name, nil will be returned instead +func (a *MojangAPI) GetId(name string, at time.Time) (*ProfileId, error) { + res, err := a.execute("GET", fmt.Sprintf("https://api.mojang.com/users/profiles/minecraft/%s?at=%d", url.PathEscape(name), at.Unix()), nil) + if err != nil { + return nil, err + } + + if res.StatusCode == 204 { + return nil, nil + } + + profile := &ProfileId{} + defer res.Body.Close() + err = json.NewDecoder(res.Body).Decode(profile) + if err != nil { + return nil, err + } + + err = profile.init(at) + if err != nil { + return nil, err + } + return profile, nil +} + +// resolves a list of multiple names at the current time +// only 100 names may be resolved at a time +func (a *MojangAPI) BulkGetId(names []string) ([]ProfileId, error) { + if len(names) > 100 { + return nil, errors.New("cannot request more than 100 names") + } + + payload, err := json.Marshal(names) + if err != nil { + return nil, err + } + + at := time.Now() + res, err := a.execute("POST", "https://api.mojang.com/profiles/minecraft", bytes.NewBuffer(payload)) + if err != nil { + return nil, err + } + + if res.StatusCode == 204 { // TODO: verify whether this case actually occurs + return make([]ProfileId, 0), nil + } + + profiles := make([]ProfileId, 0) + defer res.Body.Close() + err = json.NewDecoder(res.Body).Decode(&profiles) + if err != nil { + return nil, err + } + + for _, profile := range profiles { + err = profile.init(at) + if err != nil { + return nil, err + } + } + return profiles, nil +} + +// retrieves the complete name change history for a given profile +// the initial account name is indicated by the lack of its timestamp (e.g. if set to UNIX epoch) +func (a *MojangAPI) GetHistory(id uuid.UUID) ([]NameChange, error) { + res, err := a.execute("GET", fmt.Sprintf("https://api.mojang.com/user/profiles/%s/names", ToMojangId(id)), nil) + if err != nil { + return nil, err + } + + if res.StatusCode == 204 { // TODO: Verify whether this case actually occurs (e.g. is the API consistent) + return nil, nil + } + + history := make([]NameChange, 0) + defer res.Body.Close() + err = json.NewDecoder(res.Body).Decode(&history) + if err != nil { + return nil, err + } + + for _, change := range history { + err = change.init() + if err != nil { + return nil, err + } + } + return history, nil +} From 6e6ecd19d2e9ed46887dbadbac943f1fd37969ef Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Tue, 26 Jun 2018 12:03:59 +0200 Subject: [PATCH 017/142] Created utility methods to calculate the validity periods of names. --- stockpile/mojang/utility.go | 47 +++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 stockpile/mojang/utility.go diff --git a/stockpile/mojang/utility.go b/stockpile/mojang/utility.go new file mode 100644 index 0000000..5f278c7 --- /dev/null +++ b/stockpile/mojang/utility.go @@ -0,0 +1,47 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 mojang + +import "time" + +// defines the total amount of time a name can be safely associated with a given profile +const NameValidityPeriod = time.Hour * 24 * 37 + +// defines the total amount of time that has to pass before a user can choose a new name again +const NameChangeRateLimitPeriod = time.Hour * 24 * 30 + +// calculates the beginning of a theoretical grace period +func CalculateNameGracePeriodBeginning(end time.Time) time.Time { + return end.Add(-NameValidityPeriod) +} + +// calculates the end of a theoretical grace period +func CalculateNameGracePeriodEnd(start time.Time) time.Time { + return start.Add(NameValidityPeriod) +} + +// evaluates whether a given name association is still considered valid (e.g. no other user was able to claim the name +// since it was last encountered) +func IsNameAssociationValidAt(at time.Time, lastSeen time.Time) bool { + return at.Sub(lastSeen) < NameValidityPeriod +} + +// evaluates whether a given name association is still considered valid (e.g. no other user was able to claim the name +// since it was last encountered) +func IsNameAssociationValid(lastSeen time.Time) bool { + return IsNameAssociationValidAt(time.Now(), lastSeen) +} From 47ea6b29db6a38f9796b694e7fdeefdf5c650713 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Tue, 26 Jun 2018 12:19:58 +0200 Subject: [PATCH 018/142] Created a client implementation for profiles. --- stockpile/mojang/profile.go | 121 ++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 stockpile/mojang/profile.go diff --git a/stockpile/mojang/profile.go b/stockpile/mojang/profile.go new file mode 100644 index 0000000..1acf108 --- /dev/null +++ b/stockpile/mojang/profile.go @@ -0,0 +1,121 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 mojang + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/google/uuid" +) + +type Profile struct { + Id uuid.UUID + RawId string `json:"id"` + Name string + Properties map[string]*ProfileProperty + RawProperties []ProfileProperty `json:"properties"` + Textures *ProfileTextures +} + +type ProfileProperty struct { + Name string `json:"name"` + Value string `json:"value"` + Signature string `json:"signature"` +} + +type ProfileTextures struct { + Timestamp time.Time + RawTimestamp int64 `json:"timestamp"` + ProfileId uuid.UUID + RawProfileId string `json:"profileId"` + ProfileName string `json:"profileName"` + Textures map[string]string + RawTextures map[string]ProfileTextureSpec `json:"textures"` +} + +type ProfileTextureSpec struct { + Url string `json:"url"` +} + +func (p *Profile) init() error { + id, err := ToStandardId(p.RawId) + if err != nil { + return err + } + + p.Id = id + + p.Properties = make(map[string]*ProfileProperty) + for _, prop := range p.RawProperties { + p.Properties[prop.Name] = &prop + } + + textures := p.Properties["textures"] + if textures != nil { + p.Textures = &ProfileTextures{} + err = json.Unmarshal([]byte(textures.Value), p.Textures) + if err != nil { + return err + } + } + + return nil +} + +func (p *ProfileTextures) init() error { + id, err := ToStandardId(p.RawProfileId) + if err != nil { + return err + } + + p.Timestamp = time.Unix(p.RawTimestamp/1000, p.RawTimestamp%1000*1000000) + p.ProfileId = id + + p.Textures = make(map[string]string) + for key, spec := range p.RawTextures { + p.Textures[key] = spec.Url + } + + return nil +} + +// retrieves a single profile from the server +func (a *MojangAPI) GetProfile(id uuid.UUID) (*Profile, error) { + res, err := a.execute("GET", fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/profile/%s", id.String()), nil) + if err != nil { + return nil, err + } + + if res.StatusCode == 204 || res.StatusCode == 404 { + return nil, nil + } + + profile := &Profile{} + defer res.Body.Close() + err = json.NewDecoder(res.Body).Decode(profile) + if err != nil { + return nil, err + } + + err = profile.init() + if err != nil { + return nil, err + } + return profile, nil +} From 120e8d2d5b9117769f4d4b1bf7667574ee4485ff Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Tue, 26 Jun 2018 13:03:50 +0200 Subject: [PATCH 019/142] Created methods for retrieving blacklists and checking their contents. --- stockpile/mojang/server.go | 153 +++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 stockpile/mojang/server.go diff --git a/stockpile/mojang/server.go b/stockpile/mojang/server.go new file mode 100644 index 0000000..09a25e6 --- /dev/null +++ b/stockpile/mojang/server.go @@ -0,0 +1,153 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 mojang + +import ( + "crypto/sha1" + "encoding/hex" + "errors" + "io/ioutil" + "regexp" + "strings" + + "golang.org/x/text/encoding/charmap" +) + +var ipPattern, _ = regexp.Compile("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$") + +// represents a server blacklist +type Blacklist struct { + hashes []string +} + +// retrieves the server blacklist +func (a *MojangAPI) GetBlacklist() (*Blacklist, error) { + res, err := a.execute("GET", "https://sessionserver.mojang.com/blockedservers", nil) + if err != nil { + return nil, err + } + + if res.StatusCode == 204 { + return NewBlacklist(make([]string, 0)) + } + + defer res.Body.Close() + encoded, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + + blacklistStr := string(encoded) + blacklistStr = strings.Replace(blacklistStr, "\r", "", -1) + + return NewBlacklist(strings.Split(blacklistStr, "\n")) +} + +// creates a new blacklist from the supplied list of hashes +func NewBlacklist(hashes []string) (*Blacklist, error) { + for _, hash := range hashes { + if len(hash) != 40 { + return nil, errors.New("one or more hashes are malformed") + } + } + + return &Blacklist{hashes: hashes}, nil +} + +// evaluates whether a certain hash is part of a blacklist +func (b *Blacklist) Contains(hash string) bool { + for _, blacklistedHash := range b.hashes { + if blacklistedHash == hash { + return true + } + } + + return false +} + +// evaluates whether the passed hostname has been blacklisted +func (b *Blacklist) IsBlacklisted(addr string) (bool, error) { + hash, err := calculateHash(addr) + if err != nil { + return false, err + } + if b.Contains(hash) { + return true, nil + } + + if isIpAddress(addr) { + return b.IsBlacklistedIP(addr) + } + + return b.IsBlacklistedDomain(addr) +} + +// evaluates whether a given IPv4 address has been blacklisted +func (b *Blacklist) IsBlacklistedIP(ip string) (bool, error) { + elements := strings.Split(ip, ".") // TODO: does Minecraft support IPv6 blacklisting? + for i := 3; i > 0; i-- { + addr := strings.Join(elements[0:i], ".") + strings.Repeat(".*", 4-i) + hash, err := calculateHash(addr) + if err != nil { + return false, err + } + + if b.Contains(hash) { + return true, nil + } + } + + return false, nil +} + +// evaluates whether a given hostname has been blacklisted +func (b *Blacklist) IsBlacklistedDomain(hostname string) (bool, error) { + elements := strings.Split(hostname, ".") + length := len(elements) + + for i := 1; i < length; i++ { + addr := strings.Repeat("*.", i) + strings.Join(elements[i:(length - i)], ".") + hash, err := calculateHash(addr) + if err != nil { + return false, err + } + + if b.Contains(hash) { + return true, nil + } + } + + return false, nil +} + +// calculates a blacklist compatible hash +func calculateHash(input string) (string, error) { + hash := sha1.New() + encoder := charmap.ISO8859_10.NewEncoder() + + encoded, err := encoder.Bytes([]byte(input)) + if err != nil { + return "", err + } + + return hex.EncodeToString(hash.Sum(encoded)), nil +} + +// evaluates whether the given address is an IPv4 address +func isIpAddress(address string) bool { + return ipPattern.MatchString(address) +} From e1f955d3881fe4506b9dd72b65950618e56d124c Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Tue, 26 Jun 2018 20:52:25 +0200 Subject: [PATCH 020/142] Improved debug logging. --- stockpile/mojang/main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stockpile/mojang/main.go b/stockpile/mojang/main.go index 1595440..42152a6 100644 --- a/stockpile/mojang/main.go +++ b/stockpile/mojang/main.go @@ -56,6 +56,8 @@ func (a *MojangAPI) execute(method string, uri string, body io.Reader) (*http.Re } statusCategory := res.StatusCode / 100 + a.logger.Debugf("Server responded with status code %d (category %d)", res.StatusCode, statusCategory) + if statusCategory == 2 { return res, nil } From 0acd1f37132bf95492fcd4c69b7d2a1317d420ec Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Tue, 26 Jun 2018 20:52:40 +0200 Subject: [PATCH 021/142] Return 404 results to permit handling in actual implementations and some slight changes to the first/last seen mechanic. --- stockpile/mojang/main.go | 2 +- stockpile/mojang/name.go | 25 ++++++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/stockpile/mojang/main.go b/stockpile/mojang/main.go index 42152a6..6c8bd22 100644 --- a/stockpile/mojang/main.go +++ b/stockpile/mojang/main.go @@ -58,7 +58,7 @@ func (a *MojangAPI) execute(method string, uri string, body io.Reader) (*http.Re statusCategory := res.StatusCode / 100 a.logger.Debugf("Server responded with status code %d (category %d)", res.StatusCode, statusCategory) - if statusCategory == 2 { + if statusCategory == 2 || res.StatusCode == 404 { return res, nil } if statusCategory == 4 { diff --git a/stockpile/mojang/name.go b/stockpile/mojang/name.go index 5f4aa63..ed95779 100644 --- a/stockpile/mojang/name.go +++ b/stockpile/mojang/name.go @@ -32,11 +32,12 @@ var unixEpoch = time.Unix(0, 0) // represents a single profile id mapping between a display name and a mapping at a given time // note that lastSeenAt and validUntil may be set to UNIX epoch if the initial mapping is requested type ProfileId struct { - Id uuid.UUID - RawId string `json:"id"` - Name string `json:"name"` - LastSeenAt time.Time - ValidUntil time.Time + Id uuid.UUID + RawId string `json:"id"` + Name string `json:"name"` + FirstSeenAt time.Time + LastSeenAt time.Time + ValidUntil time.Time } // represents a single name change within a profile's history @@ -48,17 +49,18 @@ type NameChange struct { ValidUntil time.Time } -func (p *ProfileId) init(lastSeen time.Time) error { +func (p *ProfileId) init(seen time.Time) error { id, err := ToStandardId(p.RawId) if err != nil { return err } p.Id = id - p.LastSeenAt = lastSeen + p.FirstSeenAt = seen + p.LastSeenAt = seen - if lastSeen != unixEpoch { - p.ValidUntil = CalculateNameGracePeriodEnd(lastSeen) + if seen != unixEpoch { + p.ValidUntil = CalculateNameGracePeriodEnd(seen) } else { p.ValidUntil = unixEpoch } @@ -66,6 +68,11 @@ func (p *ProfileId) init(lastSeen time.Time) error { return nil } +// evaluates whether the profile is still valid at the given time +func (p *ProfileId) IsValid(at time.Time) bool { + return !p.FirstSeenAt.After(at) && p.ValidUntil.After(at) +} + func (c *NameChange) init() error { c.ChangedToAt = time.Unix(c.RawChangedToAt, 0) From 25c0d37e0c23b87f39266c3bf9c2f60e9a9d8f53 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Tue, 26 Jun 2018 21:28:48 +0200 Subject: [PATCH 022/142] More logging. --- stockpile/mojang/name.go | 1 + 1 file changed, 1 insertion(+) diff --git a/stockpile/mojang/name.go b/stockpile/mojang/name.go index ed95779..079fdaf 100644 --- a/stockpile/mojang/name.go +++ b/stockpile/mojang/name.go @@ -96,6 +96,7 @@ func (a *MojangAPI) GetId(name string, at time.Time) (*ProfileId, error) { } if res.StatusCode == 204 { + a.logger.Debugf("Server reported no association for name \"%s\" at time %s", name, at) return nil, nil } From d21b93bf4bd33477dfbcdb4a85e0a77b406eab04 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Thu, 28 Jun 2018 13:38:04 +0200 Subject: [PATCH 023/142] Use reflect.TypeOf instead. --- stockpile/command/utility.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stockpile/command/utility.go b/stockpile/command/utility.go index a2168b5..47854f9 100644 --- a/stockpile/command/utility.go +++ b/stockpile/command/utility.go @@ -26,7 +26,7 @@ import ( // prints a struct in a human readable table format func writeTable(writer io.Writer, data interface{}) { val := reflect.ValueOf(data) - typ := val.Type() + typ := reflect.TypeOf(data) headerCellLength := 3 valueCellLength := 5 From c2cb2cd13ffc3469415fb83d199c811264d13fc4 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Thu, 28 Jun 2018 13:40:00 +0200 Subject: [PATCH 024/142] Use pointers instead of copying our data all over again. --- stockpile/mojang/name.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/stockpile/mojang/name.go b/stockpile/mojang/name.go index 079fdaf..0c12b46 100644 --- a/stockpile/mojang/name.go +++ b/stockpile/mojang/name.go @@ -116,7 +116,7 @@ func (a *MojangAPI) GetId(name string, at time.Time) (*ProfileId, error) { // resolves a list of multiple names at the current time // only 100 names may be resolved at a time -func (a *MojangAPI) BulkGetId(names []string) ([]ProfileId, error) { +func (a *MojangAPI) BulkGetId(names []string) ([]*ProfileId, error) { if len(names) > 100 { return nil, errors.New("cannot request more than 100 names") } @@ -133,10 +133,10 @@ func (a *MojangAPI) BulkGetId(names []string) ([]ProfileId, error) { } if res.StatusCode == 204 { // TODO: verify whether this case actually occurs - return make([]ProfileId, 0), nil + return make([]*ProfileId, 0), nil } - profiles := make([]ProfileId, 0) + profiles := make([]*ProfileId, 0) defer res.Body.Close() err = json.NewDecoder(res.Body).Decode(&profiles) if err != nil { @@ -154,7 +154,7 @@ func (a *MojangAPI) BulkGetId(names []string) ([]ProfileId, error) { // retrieves the complete name change history for a given profile // the initial account name is indicated by the lack of its timestamp (e.g. if set to UNIX epoch) -func (a *MojangAPI) GetHistory(id uuid.UUID) ([]NameChange, error) { +func (a *MojangAPI) GetHistory(id uuid.UUID) ([]*NameChange, error) { res, err := a.execute("GET", fmt.Sprintf("https://api.mojang.com/user/profiles/%s/names", ToMojangId(id)), nil) if err != nil { return nil, err @@ -164,7 +164,7 @@ func (a *MojangAPI) GetHistory(id uuid.UUID) ([]NameChange, error) { return nil, nil } - history := make([]NameChange, 0) + history := make([]*NameChange, 0) defer res.Body.Close() err = json.NewDecoder(res.Body).Decode(&history) if err != nil { From baf5ac4c29f4f25be63261b959458fea9962a5e7 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Thu, 28 Jun 2018 15:03:17 +0200 Subject: [PATCH 025/142] Improved pretty print. --- stockpile/command/utility.go | 92 ++++++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 9 deletions(-) diff --git a/stockpile/command/utility.go b/stockpile/command/utility.go index 47854f9..6348212 100644 --- a/stockpile/command/utility.go +++ b/stockpile/command/utility.go @@ -23,6 +23,60 @@ import ( "strings" ) +// evaluates whether a given field is hidden (specifically whether it is merely used to represent +// the internal state of a protobuf message) +func isHiddenField(field *reflect.StructField) bool { + return strings.HasPrefix(field.Name, "XXX_") +} + +// pretty prints an arbitrary value +func printValue(val reflect.Value) []string { + switch val.Kind() { + case reflect.Slice: + fallthrough + case reflect.Array: + encoded := make([]string, 0) + + for i := 0; i < val.Len(); i++ { + encodedVal := printValue(val.Index(i)) + + for _, str := range encodedVal { + encoded = append(encoded, str) + } + + if i+1 != val.Len() { + encoded = append(encoded, "") + } + } + return encoded + case reflect.Struct: + encoded := make([]string, 0) + + for i := 0; i < val.NumField(); i++ { + field := val.Type().Field(i) + fieldValue := val.Field(i) + + if isHiddenField(&field) { + continue + } + + encodedField := printValue(fieldValue) + for j, str := range encodedField { + if j == 0 { + encoded = append(encoded, field.Name+": "+str) + } else { + encoded = append(encoded, strings.Repeat(" ", len(field.Name)+2)+str) + } + } + } + return encoded + case reflect.Ptr: + return printValue(val.Elem()) + } + + return []string{fmt.Sprintf("%v", val)} +} + // prints a struct in a human readable table format func writeTable(writer io.Writer, data interface{}) { val := reflect.ValueOf(data) @@ -30,24 +84,31 @@ func writeTable(writer io.Writer, data interface{}) { headerCellLength := 3 valueCellLength := 5 + fieldCount := typ.NumField() + valueMap := make(map[string][]string) for i := 0; i < typ.NumField(); i++ { fieldDef := typ.Field(i) field := val.Field(i) - if strings.HasPrefix(fieldDef.Name, "XXX_") { + if isHiddenField(&fieldDef) { + fieldCount-- continue } + valueMap[fieldDef.Name] = printValue(field) + keyLength := len(fieldDef.Name) - val := fmt.Sprintf("%v", field) - valueLength := len(val) if keyLength > headerCellLength { headerCellLength = keyLength } - if valueLength > valueCellLength { - valueCellLength = valueLength + for _, str := range valueMap[fieldDef.Name] { + l := len(str) + + if l > valueCellLength { + valueCellLength = l + } } } @@ -63,9 +124,8 @@ func writeTable(writer io.Writer, data interface{}) { for i := 0; i < typ.NumField(); i++ { fieldDef := typ.Field(i) - field := val.Field(i) - if strings.HasPrefix(fieldDef.Name, "XXX_") { + if isHiddenField(&fieldDef) { continue } @@ -73,7 +133,21 @@ func writeTable(writer io.Writer, data interface{}) { io.WriteString(writer, strings.Repeat(" ", headerCellLength-len(fieldDef.Name))) io.WriteString(writer, " | ") - io.WriteString(writer, fmt.Sprintf("%v", field)) - io.WriteString(writer, "\n") + for j, str := range valueMap[fieldDef.Name] { + if j != 0 { + io.WriteString(writer, strings.Repeat(" ", headerCellLength)) + io.WriteString(writer, " | ") + } + + io.WriteString(writer, str) + io.WriteString(writer, "\n") + } + + if i+1 != fieldCount { + io.WriteString(writer, strings.Repeat("-", headerCellLength)) + io.WriteString(writer, "-+-") + io.WriteString(writer, strings.Repeat("-", valueCellLength)) + io.WriteString(writer, "\n") + } } } From ef65163f6700484ea07fb7c8d3b3cf586ec6de49 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Thu, 28 Jun 2018 16:15:03 +0200 Subject: [PATCH 026/142] Improved indication of array elements within the command line output. --- stockpile/command/utility.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/stockpile/command/utility.go b/stockpile/command/utility.go index 6348212..8ea4ac6 100644 --- a/stockpile/command/utility.go +++ b/stockpile/command/utility.go @@ -40,8 +40,12 @@ func printValue(val reflect.Value) []string { for i := 0; i < val.Len(); i++ { encodedVal := printValue(val.Index(i)) - for _, str := range encodedVal { - encoded = append(encoded, str) + for j, str := range encodedVal { + if j == 0 { + encoded = append(encoded, "-> " + str) + } else { + encoded = append(encoded, " " + str) + } } if i+1 != val.Len() { From 26f1a2ec1df6bb501f187ea244734e1ef53fad68 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Fri, 29 Jun 2018 17:32:37 +0200 Subject: [PATCH 027/142] Significantly improved the serialization logic and added support for serializing into KV store compatible formats. --- stockpile/mojang/name.go | 338 ++++++++++++++++++++++++++++++------ stockpile/mojang/profile.go | 238 ++++++++++++++++++++----- stockpile/mojang/server.go | 16 ++ 3 files changed, 492 insertions(+), 100 deletions(-) diff --git a/stockpile/mojang/name.go b/stockpile/mojang/name.go index 0c12b46..24ab3fd 100644 --- a/stockpile/mojang/name.go +++ b/stockpile/mojang/name.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/url" "time" @@ -33,58 +34,310 @@ var unixEpoch = time.Unix(0, 0) // note that lastSeenAt and validUntil may be set to UNIX epoch if the initial mapping is requested type ProfileId struct { Id uuid.UUID - RawId string `json:"id"` - Name string `json:"name"` + Name string FirstSeenAt time.Time LastSeenAt time.Time ValidUntil time.Time } -// represents a single name change within a profile's history -// note that changedToAt and validUntil may be set to UNIX epoch when the entry represents the initial account name -type NameChange struct { - Name string `json:"name"` - ChangedToAt time.Time - RawChangedToAt int64 `json:"changedToAt"` - ValidUntil time.Time +// represents a Mojang compatible representation of a profile id +type restProfileId struct { + Id string `json:"id"` + Name string `json:"name"` +} + +// represents a serializable version of the profile Id object +type serializableProfileId struct { + restProfileId + FirstSeenAt int64 `json:"firstSeenAt"` + LastSeenAt int64 `json:"lastSeenAt"` + ValidUntil int64 `json:"validUntil"` +} + +func (p *ProfileId) Serialize() ([]byte, error) { + enc := serializableProfileId{ + restProfileId: restProfileId{ + Id: p.Id.String(), + Name: p.Name, + }, + FirstSeenAt: p.FirstSeenAt.Unix(), + LastSeenAt: p.LastSeenAt.Unix(), + ValidUntil: p.ValidUntil.Unix(), + } + + return json.Marshal(&enc) +} + +func SerializeProfileIdArray(profileIds []*ProfileId) ([]byte, error) { + enc := make([]serializableProfileId, len(profileIds)) + for i, profileId := range profileIds { + enc[i] = serializableProfileId{ + restProfileId: restProfileId{ + Id: profileId.Id.String(), + Name: profileId.Name, + }, + FirstSeenAt: profileId.FirstSeenAt.Unix(), + LastSeenAt: profileId.LastSeenAt.Unix(), + ValidUntil: profileId.ValidUntil.Unix(), + } + } + return json.Marshal(enc) } -func (p *ProfileId) init(seen time.Time) error { - id, err := ToStandardId(p.RawId) +func (p *ProfileId) Deserialize(enc []byte) error { + parsed := serializableProfileId{} + err := json.Unmarshal(enc, &parsed) + if err != nil { + return err + } + + id, err := uuid.Parse(parsed.Id) if err != nil { return err } p.Id = id - p.FirstSeenAt = seen - p.LastSeenAt = seen + p.Name = parsed.Name + p.FirstSeenAt = time.Unix(parsed.FirstSeenAt, 0) + p.LastSeenAt = time.Unix(parsed.LastSeenAt, 0) + p.ValidUntil = time.Unix(parsed.ValidUntil, 0) + return nil +} + +func DeserializeProfileIdArray(enc []byte) ([]*ProfileId, error) { + parsed := make([]serializableProfileId, 0) + err := json.Unmarshal(enc, parsed) + if err != nil { + return nil, err + } + + res := make([]*ProfileId, len(parsed)) + for i, profileId := range parsed { + id, err := uuid.Parse(profileId.Id) + if err != nil { + return nil, err + } - if seen != unixEpoch { - p.ValidUntil = CalculateNameGracePeriodEnd(seen) - } else { - p.ValidUntil = unixEpoch + res[i] = &ProfileId{ + Id: id, + Name: profileId.Name, + FirstSeenAt: time.Unix(profileId.FirstSeenAt, 0), + LastSeenAt: time.Unix(profileId.LastSeenAt, 0), + ValidUntil: time.Unix(profileId.ValidUntil, 0), + } } + return res, nil +} +func (p *ProfileId) read(reader io.Reader) error { + parsed := restProfileId{} + err := json.NewDecoder(reader).Decode(&parsed) + if err != nil { + return err + } + + at := time.Now() + id, err := ParseId(parsed.Id) + if err != nil { + return err + } + + p.Id = id + p.Name = parsed.Name + p.FirstSeenAt = at + p.LastSeenAt = at + p.ValidUntil = CalculateNameGracePeriodEnd(time.Now()) return nil } +func ReadProfileIdArray(reader io.Reader) ([]*ProfileId, error) { + parsed := make([]restProfileId, 0) + err := json.NewDecoder(reader).Decode(&parsed) + if err != nil { + return nil, err + } + + at := time.Now() + res := make([]*ProfileId, len(parsed)) + for i, profileId := range parsed { + id, err := ParseId(profileId.Id) + if err != nil { + return nil, err + } + + res[i] = &ProfileId{ + Id: id, + Name: profileId.Name, + FirstSeenAt: at, + LastSeenAt: at, + ValidUntil: CalculateNameGracePeriodEnd(at), + } + } + return res, nil +} + +// updates the time at which this id has been discovered +func (p *ProfileId) UpdateDiscovery(at time.Time) { + p.FirstSeenAt = at + p.LastSeenAt = at + p.ValidUntil = CalculateNameGracePeriodEnd(at) +} + +// updates the last time at which this id has been encountered and the respective expiration times +// if the passed time is set before the current last encounter, the method will return immediately +// without changing the profile state +func (p *ProfileId) UpdateExpiration(seen time.Time) { + if p.LastSeenAt.After(seen) { + return // nothing to do + } + + p.LastSeenAt = seen + p.ValidUntil = CalculateNameGracePeriodEnd(seen) +} + // evaluates whether the profile is still valid at the given time func (p *ProfileId) IsValid(at time.Time) bool { return !p.FirstSeenAt.After(at) && p.ValidUntil.After(at) } -func (c *NameChange) init() error { - c.ChangedToAt = time.Unix(c.RawChangedToAt, 0) +// encapsulates a name history +type NameChangeHistory struct { + History []*NameChange +} + +func (h *NameChangeHistory) Serialize() ([]byte, error) { + return SerializeNameChangeArray(h.History) +} + +func (h *NameChangeHistory) Deserialize(enc []byte) error { + history, err := DeserializeNameChangeArray(enc) + if err != nil { + return err + } + h.History = history + return nil +} + +func (h *NameChangeHistory) read(reader io.Reader) error { + history, err := ReadNameChangeArray(reader) + if err != nil { + return err + } + h.History = history + return nil +} + +// represents a single name change within a profile's history +// note that changedToAt and validUntil may be set to UNIX epoch when the entry represents the initial account name +type NameChange struct { + Name string + ChangedToAt time.Time + ValidUntil time.Time +} + +// represents a Mojang compatible version of a name change record +type restNameChange struct { + Name string `json:"name"` + ChangedToAt int64 `json:"changedToAt"` +} + +// represents a serializable version of the name change object +type serializableNameChange struct { + restNameChange + ValidUntil int64 `json:"validUntil"` +} + +func (p *NameChange) Serialize() ([]byte, error) { + enc := serializableNameChange{ + restNameChange: restNameChange{ + Name: p.Name, + ChangedToAt: p.ChangedToAt.Unix(), + }, + ValidUntil: p.ValidUntil.Unix(), + } - if c.RawChangedToAt != 0 { - c.ValidUntil = CalculateNameGracePeriodEnd(c.ChangedToAt) - } else { - c.ValidUntil = unixEpoch + return json.Marshal(&enc) +} + +func SerializeNameChangeArray(history []*NameChange) ([]byte, error) { + enc := make([]*serializableNameChange, len(history)) + for i, change := range history { + enc[i] = &serializableNameChange{ + restNameChange: restNameChange{ + Name: change.Name, + ChangedToAt: change.ChangedToAt.Unix(), + }, + ValidUntil: change.ValidUntil.Unix(), + } + } + + return json.Marshal(enc) +} + +func (p *NameChange) Deserialize(enc []byte) error { + parsed := serializableNameChange{} + err := json.Unmarshal(enc, &parsed) + if err != nil { + return err } + p.Name = parsed.Name + p.ChangedToAt = time.Unix(parsed.ChangedToAt, 0) + p.ValidUntil = time.Unix(parsed.ValidUntil, 0) return nil } +func DeserializeNameChangeArray(enc []byte) ([]*NameChange, error) { + parsed := make([]serializableNameChange, 0) + err := json.Unmarshal(enc, parsed) + if err != nil { + return nil, err + } + + res := make([]*NameChange, len(parsed)) + for i, change := range parsed { + res[i] = &NameChange{ + Name: change.Name, + ChangedToAt: time.Unix(change.ChangedToAt, 0), + ValidUntil: time.Unix(change.ValidUntil, 0), + } + } + return res, nil +} + +func (p *NameChange) read(reader io.Reader) error { + parsed := restNameChange{} + err := json.NewDecoder(reader).Decode(&parsed) + if err != nil { + return err + } + + p.Name = parsed.Name + p.ChangedToAt = time.Unix(parsed.ChangedToAt, 0) + p.ValidUntil = CalculateNameGracePeriodEnd(p.ChangedToAt) + return nil +} + +func ReadNameChangeArray(reader io.Reader) ([]*NameChange, error) { + parsed := make([]*restNameChange, 0) + err := json.NewDecoder(reader).Decode(&parsed) + if err != nil { + return nil, err + } + + res := make([]*NameChange, len(parsed)) + for i, change := range parsed { + at := time.Unix(change.ChangedToAt, 0) + + res[i] = &NameChange{ + Name: change.Name, + ChangedToAt: at, + ValidUntil: CalculateNameGracePeriodEnd(at), + } + } + return res, nil +} + // retrieves the profile id (and some associated attributes) for a given display name at the specified time // - if the UNIX epoch (e.g. zero) is passed instead of a real time, the initial account name will be checked (assuming // that the account in question is a legacy account or has changed its name at least once) @@ -102,16 +355,8 @@ func (a *MojangAPI) GetId(name string, at time.Time) (*ProfileId, error) { profile := &ProfileId{} defer res.Body.Close() - err = json.NewDecoder(res.Body).Decode(profile) - if err != nil { - return nil, err - } - - err = profile.init(at) - if err != nil { - return nil, err - } - return profile, nil + err = profile.read(res.Body) + return profile, err } // resolves a list of multiple names at the current time @@ -126,7 +371,6 @@ func (a *MojangAPI) BulkGetId(names []string) ([]*ProfileId, error) { return nil, err } - at := time.Now() res, err := a.execute("POST", "https://api.mojang.com/profiles/minecraft", bytes.NewBuffer(payload)) if err != nil { return nil, err @@ -136,25 +380,13 @@ func (a *MojangAPI) BulkGetId(names []string) ([]*ProfileId, error) { return make([]*ProfileId, 0), nil } - profiles := make([]*ProfileId, 0) defer res.Body.Close() - err = json.NewDecoder(res.Body).Decode(&profiles) - if err != nil { - return nil, err - } - - for _, profile := range profiles { - err = profile.init(at) - if err != nil { - return nil, err - } - } - return profiles, nil + return ReadProfileIdArray(res.Body) } // retrieves the complete name change history for a given profile // the initial account name is indicated by the lack of its timestamp (e.g. if set to UNIX epoch) -func (a *MojangAPI) GetHistory(id uuid.UUID) ([]*NameChange, error) { +func (a *MojangAPI) GetHistory(id uuid.UUID) (*NameChangeHistory, error) { res, err := a.execute("GET", fmt.Sprintf("https://api.mojang.com/user/profiles/%s/names", ToMojangId(id)), nil) if err != nil { return nil, err @@ -164,18 +396,12 @@ func (a *MojangAPI) GetHistory(id uuid.UUID) ([]*NameChange, error) { return nil, nil } - history := make([]*NameChange, 0) + history := &NameChangeHistory{} defer res.Body.Close() - err = json.NewDecoder(res.Body).Decode(&history) + err = history.read(res.Body) if err != nil { return nil, err } - for _, change := range history { - err = change.init() - if err != nil { - return nil, err - } - } return history, nil } diff --git a/stockpile/mojang/profile.go b/stockpile/mojang/profile.go index 1acf108..d35e218 100644 --- a/stockpile/mojang/profile.go +++ b/stockpile/mojang/profile.go @@ -19,77 +19,235 @@ package mojang import ( "encoding/json" "fmt" + "io" "time" "github.com/google/uuid" ) type Profile struct { - Id uuid.UUID - RawId string `json:"id"` - Name string - Properties map[string]*ProfileProperty - RawProperties []ProfileProperty `json:"properties"` - Textures *ProfileTextures + Id uuid.UUID + Name string + Properties map[string]*ProfileProperty + Textures *ProfileTextures } -type ProfileProperty struct { - Name string `json:"name"` - Value string `json:"value"` - Signature string `json:"signature"` +type restProfile struct { + Id string `json:"id"` + Name string `json:"name"` + Properties []*ProfileProperty `json:"properties"` } -type ProfileTextures struct { - Timestamp time.Time - RawTimestamp int64 `json:"timestamp"` - ProfileId uuid.UUID - RawProfileId string `json:"profileId"` - ProfileName string `json:"profileName"` - Textures map[string]string - RawTextures map[string]ProfileTextureSpec `json:"textures"` +type serializableProfile struct { + restProfile + Textures *serializableProfileTextures } -type ProfileTextureSpec struct { - Url string `json:"url"` +func (p *Profile) Serialize() ([]byte, error) { + i := 0 + props := make([]*ProfileProperty, len(p.Properties)) + for _, prop := range p.Properties { + props[i] = prop + i++ + } + + var tex *serializableProfileTextures = nil + if p.Textures != nil { + tex = &serializableProfileTextures{ + Timestamp: p.Textures.Timestamp.Unix(), + ProfileId: p.Textures.ProfileId.String(), + ProfileName: p.Textures.ProfileName, + Textures: p.Textures.Textures, + } + } + + enc := serializableProfile{ + restProfile: restProfile{ + Id: p.Id.String(), + Name: p.Name, + Properties: props, + }, + Textures: tex, + } + return json.Marshal(&enc) } -func (p *Profile) init() error { - id, err := ToStandardId(p.RawId) +func (p *Profile) Deserialize(enc []byte) error { + parsed := serializableProfile{} + err := json.Unmarshal(enc, &parsed) if err != nil { return err } + id, err := uuid.Parse(parsed.Id) + if err != nil { + return err + } p.Id = id + p.Name = parsed.Name p.Properties = make(map[string]*ProfileProperty) - for _, prop := range p.RawProperties { - p.Properties[prop.Name] = &prop + for _, prop := range parsed.Properties { + p.Properties[prop.Name] = prop } - textures := p.Properties["textures"] - if textures != nil { - p.Textures = &ProfileTextures{} - err = json.Unmarshal([]byte(textures.Value), p.Textures) + p.Textures = nil + if parsed.Textures != nil { + id, err := uuid.Parse(parsed.Textures.ProfileId) if err != nil { return err } + + p.Textures = &ProfileTextures{ + Timestamp: time.Unix(parsed.Textures.Timestamp, 0), + ProfileId: id, + ProfileName: parsed.Textures.ProfileName, + Textures: parsed.Textures.Textures, + } + } + return nil +} + +func (p *Profile) read(reader io.Reader) error { + parsed := restProfile{} + err := json.NewDecoder(reader).Decode(&parsed) + if err != nil { + return err } + id, err := uuid.Parse(parsed.Id) + if err != nil { + return err + } + p.Id = id + p.Name = parsed.Name + + p.Properties = make(map[string]*ProfileProperty) + for _, prop := range parsed.Properties { + p.Properties[prop.Name] = prop + } + + p.Textures = nil + texProp := p.Properties["textures"] + if texProp != nil { + parsedProp := restProfileTextures{} + err := json.Unmarshal([]byte(texProp.Value), &parsedProp) + if err != nil { + return err + } + + id, err := ParseId(parsedProp.ProfileId) + if err != nil { + return err + } + + textures := make(map[string]string) + for key, spec := range parsedProp.Textures { + textures[key] = spec.Url + } + + p.Textures = &ProfileTextures{ + Timestamp: time.Unix(parsedProp.Timestamp, 0), + ProfileId: id, + ProfileName: parsedProp.ProfileName, + Textures: textures, + } + } return nil } -func (p *ProfileTextures) init() error { - id, err := ToStandardId(p.RawProfileId) +type ProfileProperty struct { + Name string `json:"name"` + Value string `json:"value"` + Signature string `json:"signature"` +} + +func (p *ProfileProperty) Serialize() ([]byte, error) { + return json.Marshal(p) +} + +func (p *ProfileProperty) Deserialize(enc []byte) error { + return json.Unmarshal(enc, p) +} + +func (p *ProfileProperty) read(reader io.Reader) error { + return json.NewDecoder(reader).Decode(p) +} + +type ProfileTextures struct { + Timestamp time.Time + ProfileId uuid.UUID + ProfileName string + Textures map[string]string +} + +type restProfileTextures struct { + Timestamp int64 `json:"timestamp"` + ProfileId string `json:"profileId"` + ProfileName string `json:"profileName"` + Textures map[string]restProfileTextureSpec `json:"textures"` +} + +type serializableProfileTextures struct { + Timestamp int64 `json:"timestamp"` + ProfileId string `json:"profileId"` + ProfileName string `json:"profileName"` + Textures map[string]string +} + +type restProfileTextureSpec struct { + Url string `json:"url"` +} + +func (t *ProfileTextures) Serialize() ([]byte, error) { + enc := &serializableProfileTextures{ + Timestamp: t.Timestamp.Unix(), + ProfileId: t.ProfileId.String(), + ProfileName: t.ProfileName, + Textures: t.Textures, + } + + return json.Marshal(enc) +} + +func (t *ProfileTextures) Deserialize(enc []byte) error { + parsed := serializableProfileTextures{} + err := json.Unmarshal(enc, &parsed) if err != nil { return err } - p.Timestamp = time.Unix(p.RawTimestamp/1000, p.RawTimestamp%1000*1000000) - p.ProfileId = id + id, err := uuid.Parse(parsed.ProfileId) + if err != nil { + return err + } + + t.Timestamp = time.Unix(parsed.Timestamp, 0) + t.ProfileId = id + t.ProfileName = parsed.ProfileName + t.Textures = parsed.Textures + return nil +} + +func (t *ProfileTextures) read(reader io.Reader) error { + parsed := restProfileTextures{} + err := json.NewDecoder(reader).Decode(&parsed) + if err != nil { + return err + } + + id, err := ParseId(parsed.ProfileId) + if err != nil { + return err + } - p.Textures = make(map[string]string) - for key, spec := range p.RawTextures { - p.Textures[key] = spec.Url + t.Timestamp = time.Unix(parsed.Timestamp, 0) + t.ProfileId = id + t.ProfileName = parsed.ProfileName + + textures := make(map[string]string) + for key, spec := range parsed.Textures { + textures[key] = spec.Url } return nil @@ -108,14 +266,6 @@ func (a *MojangAPI) GetProfile(id uuid.UUID) (*Profile, error) { profile := &Profile{} defer res.Body.Close() - err = json.NewDecoder(res.Body).Decode(profile) - if err != nil { - return nil, err - } - - err = profile.init() - if err != nil { - return nil, err - } + err = profile.read(res.Body) return profile, nil } diff --git a/stockpile/mojang/server.go b/stockpile/mojang/server.go index 09a25e6..b5f5978 100644 --- a/stockpile/mojang/server.go +++ b/stockpile/mojang/server.go @@ -19,6 +19,7 @@ package mojang import ( "crypto/sha1" "encoding/hex" + "encoding/json" "errors" "io/ioutil" "regexp" @@ -68,6 +69,21 @@ func NewBlacklist(hashes []string) (*Blacklist, error) { return &Blacklist{hashes: hashes}, nil } +func (b *Blacklist) Serialize() ([]byte, error) { + return json.Marshal(b.hashes) +} + +func (b *Blacklist) Deserialize(enc []byte) error { + hashes := make([]string, 0) + err := json.Unmarshal(enc, &hashes) + if err != nil { + return err + } + + b.hashes = hashes + return nil +} + // evaluates whether a certain hash is part of a blacklist func (b *Blacklist) Contains(hash string) bool { for _, blacklistedHash := range b.hashes { From 483b7f18b41edf060c443dd91652b8250d1252e1 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Fri, 29 Jun 2018 17:54:30 +0200 Subject: [PATCH 028/142] Added support for custom conversion methods. --- stockpile/command/utility.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/stockpile/command/utility.go b/stockpile/command/utility.go index 8ea4ac6..0d3876f 100644 --- a/stockpile/command/utility.go +++ b/stockpile/command/utility.go @@ -31,6 +31,12 @@ func isHiddenField(field *reflect.StructField) bool { // pretty prints an arbitrary value func printValue(val reflect.Value) []string { + convMethod := val.MethodByName("String") + if convMethod.IsValid() { + retValues := convMethod.Call([]reflect.Value{}) + return []string{retValues[0].String()} + } + switch val.Kind() { case reflect.Slice: fallthrough @@ -42,9 +48,9 @@ func printValue(val reflect.Value) []string { for j, str := range encodedVal { if j == 0 { - encoded = append(encoded, "-> " + str) + encoded = append(encoded, "-> "+str) } else { - encoded = append(encoded, " " + str) + encoded = append(encoded, " "+str) } } From 63bf4c8898a3674767f8275582f29efdb04c1bbe Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Fri, 29 Jun 2018 19:31:57 +0200 Subject: [PATCH 029/142] Corrected the id path parameter. --- stockpile/mojang/profile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stockpile/mojang/profile.go b/stockpile/mojang/profile.go index d35e218..ed1e56c 100644 --- a/stockpile/mojang/profile.go +++ b/stockpile/mojang/profile.go @@ -255,7 +255,7 @@ func (t *ProfileTextures) read(reader io.Reader) error { // retrieves a single profile from the server func (a *MojangAPI) GetProfile(id uuid.UUID) (*Profile, error) { - res, err := a.execute("GET", fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/profile/%s", id.String()), nil) + res, err := a.execute("GET", fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/profile/%s", ToMojangId(id)), nil) if err != nil { return nil, err } From 455b0c813a51f49a7a48e0b25b27ef59182573eb Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Fri, 29 Jun 2018 19:48:59 +0200 Subject: [PATCH 030/142] Added support for nil pointers. --- stockpile/command/utility.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/stockpile/command/utility.go b/stockpile/command/utility.go index 0d3876f..2d549da 100644 --- a/stockpile/command/utility.go +++ b/stockpile/command/utility.go @@ -31,6 +31,10 @@ func isHiddenField(field *reflect.StructField) bool { // pretty prints an arbitrary value func printValue(val reflect.Value) []string { + if !val.IsValid() { + return []string{"unset"} + } + convMethod := val.MethodByName("String") if convMethod.IsValid() { retValues := convMethod.Call([]reflect.Value{}) From 7995b58a4033553cd355a6c57c085e42f2395992 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Fri, 29 Jun 2018 19:57:36 +0200 Subject: [PATCH 031/142] Fixed an issue where the id is not parsed according to the API spec. --- stockpile/mojang/profile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stockpile/mojang/profile.go b/stockpile/mojang/profile.go index ed1e56c..9a955af 100644 --- a/stockpile/mojang/profile.go +++ b/stockpile/mojang/profile.go @@ -115,7 +115,7 @@ func (p *Profile) read(reader io.Reader) error { return err } - id, err := uuid.Parse(parsed.Id) + id, err := ParseId(parsed.Id) if err != nil { return err } From 870020e567f64e474c585cc406bc6eea4e9d4781 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Fri, 29 Jun 2018 20:15:10 +0200 Subject: [PATCH 032/142] Added support for maps and introduced shortening of overly long strings. --- stockpile/command/utility.go | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/stockpile/command/utility.go b/stockpile/command/utility.go index 2d549da..baa873f 100644 --- a/stockpile/command/utility.go +++ b/stockpile/command/utility.go @@ -29,6 +29,15 @@ func isHiddenField(field *reflect.StructField) bool { return strings.HasPrefix(field.Name, "XXX_") } +// shortens long strings for display within the command line +func strEllipsis(str string) string { + if len(str) > 36 { // 36 == uuid + return str[:3] + "..." + str[len(str)-3:] + } + + return str +} + // pretty prints an arbitrary value func printValue(val reflect.Value) []string { if !val.IsValid() { @@ -37,8 +46,7 @@ func printValue(val reflect.Value) []string { convMethod := val.MethodByName("String") if convMethod.IsValid() { - retValues := convMethod.Call([]reflect.Value{}) - return []string{retValues[0].String()} + return []string{strEllipsis(convMethod.Call([]reflect.Value{})[0].String())} } switch val.Kind() { @@ -63,6 +71,20 @@ func printValue(val reflect.Value) []string { } } return encoded + case reflect.Map: + encoded := make([]string, 0) + for _, key := range val.MapKeys() { + encodedValue := printValue(val.MapIndex(key)) + encoded = append(encoded, strings.Join(printValue(key), ", ")+":") + for _, str := range encodedValue { + encoded = append(encoded, " "+str) + } + } + return encoded + case reflect.Ptr: + return printValue(val.Elem()) + case reflect.String: + return []string{strEllipsis(val.String())} case reflect.Struct: encoded := make([]string, 0) @@ -84,8 +106,6 @@ func printValue(val reflect.Value) []string { } } return encoded - case reflect.Ptr: - return printValue(val.Elem()) } return []string{fmt.Sprintf("%v", val)} From 4b816865cf37b04198b91acb233b48ab77e7996c Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Fri, 29 Jun 2018 20:20:13 +0200 Subject: [PATCH 033/142] Updated the request url to properly request a signed version of the profile. --- stockpile/mojang/profile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stockpile/mojang/profile.go b/stockpile/mojang/profile.go index 9a955af..cd686ab 100644 --- a/stockpile/mojang/profile.go +++ b/stockpile/mojang/profile.go @@ -255,7 +255,7 @@ func (t *ProfileTextures) read(reader io.Reader) error { // retrieves a single profile from the server func (a *MojangAPI) GetProfile(id uuid.UUID) (*Profile, error) { - res, err := a.execute("GET", fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/profile/%s", ToMojangId(id)), nil) + res, err := a.execute("GET", fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=false", ToMojangId(id)), nil) if err != nil { return nil, err } From 6a1ccc60a169bad3cb02a34c510ae6dca36abdb3 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Fri, 29 Jun 2018 20:34:40 +0200 Subject: [PATCH 034/142] Fixed an issue with the parsing of the textures property and error propagation. --- stockpile/mojang/profile.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/stockpile/mojang/profile.go b/stockpile/mojang/profile.go index cd686ab..635adfb 100644 --- a/stockpile/mojang/profile.go +++ b/stockpile/mojang/profile.go @@ -17,6 +17,7 @@ package mojang import ( + "encoding/base64" "encoding/json" "fmt" "io" @@ -130,8 +131,13 @@ func (p *Profile) read(reader io.Reader) error { p.Textures = nil texProp := p.Properties["textures"] if texProp != nil { + extractedValue, err := base64.StdEncoding.DecodeString(texProp.Value) + if err != nil { + return err + } + parsedProp := restProfileTextures{} - err := json.Unmarshal([]byte(texProp.Value), &parsedProp) + err = json.Unmarshal(extractedValue, &parsedProp) if err != nil { return err } @@ -267,5 +273,5 @@ func (a *MojangAPI) GetProfile(id uuid.UUID) (*Profile, error) { profile := &Profile{} defer res.Body.Close() err = profile.read(res.Body) - return profile, nil + return profile, err } From f9f97a0c1785cc1b9f718b6841545ae564e2c4f0 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Fri, 29 Jun 2018 20:36:04 +0200 Subject: [PATCH 035/142] Extended the maximum string length a little further to completely display urls. --- stockpile/command/utility.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stockpile/command/utility.go b/stockpile/command/utility.go index baa873f..eb32244 100644 --- a/stockpile/command/utility.go +++ b/stockpile/command/utility.go @@ -31,7 +31,7 @@ func isHiddenField(field *reflect.StructField) bool { // shortens long strings for display within the command line func strEllipsis(str string) string { - if len(str) > 36 { // 36 == uuid + if len(str) > 128 { // 36 == uuid return str[:3] + "..." + str[len(str)-3:] } From 651d27be8ea45dbe4c5ce723834877f8870cd221 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 01:15:23 +0200 Subject: [PATCH 036/142] Exposed the list of blacklist hashes. I'm not entirely happy with the way that this looks but we'll need to copy them back and forth in our RPC conversion. --- stockpile/mojang/server.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/stockpile/mojang/server.go b/stockpile/mojang/server.go index b5f5978..8e69c0d 100644 --- a/stockpile/mojang/server.go +++ b/stockpile/mojang/server.go @@ -32,7 +32,7 @@ var ipPattern, _ = regexp.Compile("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){ // represents a server blacklist type Blacklist struct { - hashes []string + Hashes []string } // retrieves the server blacklist @@ -66,11 +66,11 @@ func NewBlacklist(hashes []string) (*Blacklist, error) { } } - return &Blacklist{hashes: hashes}, nil + return &Blacklist{Hashes: hashes}, nil } func (b *Blacklist) Serialize() ([]byte, error) { - return json.Marshal(b.hashes) + return json.Marshal(b.Hashes) } func (b *Blacklist) Deserialize(enc []byte) error { @@ -80,13 +80,13 @@ func (b *Blacklist) Deserialize(enc []byte) error { return err } - b.hashes = hashes + b.Hashes = hashes return nil } // evaluates whether a certain hash is part of a blacklist func (b *Blacklist) Contains(hash string) bool { - for _, blacklistedHash := range b.hashes { + for _, blacklistedHash := range b.Hashes { if blacklistedHash == hash { return true } From 82ef381b11b293fedf4e3d629a994dbb71bb0d78 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 02:11:06 +0200 Subject: [PATCH 037/142] Added logging, added support for the secondary login phase, corrected an issue with the hash computation. --- stockpile/mojang/server.go | 47 +++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/stockpile/mojang/server.go b/stockpile/mojang/server.go index 8e69c0d..f2fc686 100644 --- a/stockpile/mojang/server.go +++ b/stockpile/mojang/server.go @@ -20,11 +20,13 @@ import ( "crypto/sha1" "encoding/hex" "encoding/json" - "errors" + "fmt" "io/ioutil" + "net/url" "regexp" "strings" + "github.com/op/go-logging" "golang.org/x/text/encoding/charmap" ) @@ -32,6 +34,8 @@ var ipPattern, _ = regexp.Compile("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){ // represents a server blacklist type Blacklist struct { + logger *logging.Logger + Hashes []string } @@ -58,15 +62,39 @@ func (a *MojangAPI) GetBlacklist() (*Blacklist, error) { return NewBlacklist(strings.Split(blacklistStr, "\n")) } +// performs the server-side phase of the online handshake +func (a *MojangAPI) Login(displayName string, serverId string, ip string) (*Profile, error) { + res, err := a.execute("GET", fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s", url.QueryEscape(displayName), url.QueryEscape(serverId), url.QueryEscape(ip)), nil) + if err != nil { + return nil, err + } + + profile := &Profile{} + defer res.Body.Close() + err = profile.read(res.Body) + if err != nil { + return nil, err + } + + return profile, nil +} + // creates a new blacklist from the supplied list of hashes func NewBlacklist(hashes []string) (*Blacklist, error) { for _, hash := range hashes { + if hash == "" { + continue // skip extras + } + if len(hash) != 40 { - return nil, errors.New("one or more hashes are malformed") + return nil, fmt.Errorf("encountered malformed hash \"%s\": must be exactly 40 characters long", hash) } } - return &Blacklist{Hashes: hashes}, nil + return &Blacklist{ + logger: logging.MustGetLogger("blacklist"), + Hashes: hashes, + }, nil } func (b *Blacklist) Serialize() ([]byte, error) { @@ -98,6 +126,7 @@ func (b *Blacklist) Contains(hash string) bool { // evaluates whether the passed hostname has been blacklisted func (b *Blacklist) IsBlacklisted(addr string) (bool, error) { hash, err := calculateHash(addr) + b.logger.Debugf("Checking address %s (hash: %s) against blacklist", addr, hash) if err != nil { return false, err } @@ -116,8 +145,9 @@ func (b *Blacklist) IsBlacklisted(addr string) (bool, error) { func (b *Blacklist) IsBlacklistedIP(ip string) (bool, error) { elements := strings.Split(ip, ".") // TODO: does Minecraft support IPv6 blacklisting? for i := 3; i > 0; i-- { - addr := strings.Join(elements[0:i], ".") + strings.Repeat(".*", 4-i) + addr := strings.Join(elements[:i], ".") + ".*" hash, err := calculateHash(addr) + b.logger.Debugf("Checking IP %s (hash: %s) against blacklist", addr, hash) if err != nil { return false, err } @@ -136,8 +166,9 @@ func (b *Blacklist) IsBlacklistedDomain(hostname string) (bool, error) { length := len(elements) for i := 1; i < length; i++ { - addr := strings.Repeat("*.", i) + strings.Join(elements[i:(length - i)], ".") + addr := "*." + strings.Join(elements[i:], ".") hash, err := calculateHash(addr) + b.logger.Debugf("Checking domain %s (hash: %s) against blacklist", addr, hash) if err != nil { return false, err } @@ -152,15 +183,15 @@ func (b *Blacklist) IsBlacklistedDomain(hostname string) (bool, error) { // calculates a blacklist compatible hash func calculateHash(input string) (string, error) { - hash := sha1.New() encoder := charmap.ISO8859_10.NewEncoder() - encoded, err := encoder.Bytes([]byte(input)) + encoded, err := encoder.String(input) if err != nil { return "", err } - return hex.EncodeToString(hash.Sum(encoded)), nil + hash := sha1.Sum([]byte(encoded)) + return hex.EncodeToString(hash[:]), nil } // evaluates whether the given address is an IPv4 address From 06c9bfd1daf1dec29f620456d11d6a30613da699 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 17:43:36 +0200 Subject: [PATCH 038/142] Created a representation for the server configuration. --- stockpile/server/config.go | 247 +++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 stockpile/server/config.go diff --git a/stockpile/server/config.go b/stockpile/server/config.go new file mode 100644 index 0000000..dc48349 --- /dev/null +++ b/stockpile/server/config.go @@ -0,0 +1,247 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 server + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/hashicorp/hcl2/gohcl" + "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/hcl2/hclparse" +) + +// defines the default address to listen on when none is given +const DefaultAddress = "0.0.0.0" + +// defines the default port to listen on when none is given +const DefaultPort = 36623 + +// Represents a server configuration (typically parsed from one or more HCL files) +type Config struct { + BindAddress string `hcl:"bind-address,attr"` + EnableLegacyApi bool `hcl:"legacy-api,attr"` + Storage *StorageConfig `hcl:"storage,block"` + Ttl *TtlConfig `hcl:"ttl,block"` +} + +// Represents a storage backend configuration +// The "type" parameter will simply equal the executable name within the plugin directory while all parameters are +// passed on upon startup +type StorageConfig struct { + Type string `hcl:"type,label"` + Parameters hcl.Body `hcl:",remain"` +} + +// Represents the TTL (Time To Live) configuration (e.g. caching durations for various value types) +type TtlConfig struct { + Name time.Duration + RawName string `hcl:"name,attr"` + NameHistory time.Duration + RawNameHistory string `hcl:"name-history,attr"` + Profile time.Duration + RawProfile string `hcl:"profile,attr"` + Blacklist time.Duration + RawBlacklist string `hcl:"blacklist,attr"` +} + +// Creates an empty configuration +func EmptyConfig() *Config { + return &Config{} +} + +func DefaultConfig() *Config { + cfg := &Config{ + BindAddress: fmt.Sprintf("%s:%d", DefaultAddress, DefaultPort), + EnableLegacyApi: false, + Storage: &StorageConfig{ + Type: "mem", + }, + Ttl: &TtlConfig{ + Name: mojang.NameValidityPeriod, // Full Mojang limit + NameHistory: mojang.NameChangeRateLimitPeriod / 4, // 1/4th of the Mojang limit + Profile: time.Hour * 24 * 7, // 7 days + Blacklist: time.Hour * 24 * 7, // 7 days + }, + } + + // since parse may be called on this config we'll have to copy the string representations as well + ttl := cfg.Ttl + ttl.RawName = ttl.Name.String() + ttl.RawNameHistory = ttl.NameHistory.String() + ttl.RawProfile = ttl.Profile.String() + ttl.RawBlacklist = ttl.Blacklist.String() + + return cfg +} + +func DevelopmentConfig() *Config { + return DefaultConfig().Merge(&Config{ + BindAddress: fmt.Sprintf("%s:%d", "127.0.0.1", DefaultPort), + EnableLegacyApi: true, + }) +} + +// Loads a file or directory +func LoadConfig(path string) (*Config, error) { + file, err := os.Stat(path) + if err != nil { + return nil, err + } + + if file.IsDir() { + return LoadConfigDirectory(path) + } + + base := DefaultConfig() + cfg, err := LoadConfigFile(path) + if err != nil { + return nil, err + } + base.Merge(cfg) + return base, nil +} + +// Loads an entire directory of configuration files +func LoadConfigDirectory(path string) (*Config, error) { + files, err := ioutil.ReadDir(path) + if err != nil { + return nil, err + } + + base := DefaultConfig() + for _, file := range files { + if file.IsDir() { + continue + } + + if strings.HasSuffix(file.Name(), ".hcl") || strings.HasSuffix(file.Name(), ".json") { + cfg, err := LoadConfigFile(filepath.Join(path, file.Name())) + if err != nil { + return nil, err + } + base.Merge(cfg) + } + } + + return base, nil +} + +// Loads a single configuration file +func LoadConfigFile(path string) (*Config, error) { + parser := hclparse.NewParser() + file, diag := parser.ParseHCLFile(path) + + if diag.HasErrors() { + return nil, fmt.Errorf("failed to load configuration file \"%s\": %s", path, diag.Error()) + } + + cfg := EmptyConfig() + diag = gohcl.DecodeBody(file.Body, nil, cfg) + cfg.Parse() + + if diag.HasErrors() { + return nil, fmt.Errorf("failed to load configuration file \"%s\": %s", path, diag.Error()) + } + return cfg, nil +} + +// Merges two configuration instances into one +func (c *Config) Merge(other *Config) *Config { + if other.BindAddress != "" { + c.BindAddress = other.BindAddress + } + + if other.EnableLegacyApi { + c.EnableLegacyApi = true + } + + if c.Storage == nil { + c.Storage = other.Storage + } else if other.Storage != nil { + c.Storage.Merge(other.Storage) + } + + if c.Ttl == nil { + c.Ttl = other.Ttl + } else if other.Ttl != nil { + c.Ttl.Merge(other.Ttl) + } + + return c +} + +func (c *StorageConfig) Merge(other *StorageConfig) *StorageConfig { + if other.Type != "" { + c.Type = other.Type + c.Parameters = other.Parameters + } + return c +} + +func (c *TtlConfig) Merge(other *TtlConfig) *TtlConfig { + if other.Name != 0 { + c.Name = other.Name + } + if other.NameHistory != 0 { + c.NameHistory = other.NameHistory + } + if c.Profile != 0 { + c.Profile = other.Profile + } + if other.Blacklist != 0 { + c.Blacklist = other.Blacklist + } + return c +} + +func (c *TtlConfig) Parse() error { + name, err := time.ParseDuration(c.RawName) + if err != nil { + return err + } + + nameHistory, err := time.ParseDuration(c.RawNameHistory) + if err != nil { + return err + } + + profile, err := time.ParseDuration(c.RawProfile) + if err != nil { + return err + } + + blacklist, err := time.ParseDuration(c.RawBlacklist) + if err != nil { + return err + } + + c.Name = name + c.NameHistory = nameHistory + c.Profile = profile + c.Blacklist = blacklist + return nil +} + +func (c *Config) Parse() error { + return c.Ttl.Parse() +} From 14255751ab4186edeec04f9e6d7de8c6f0ad5499 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 17:45:49 +0200 Subject: [PATCH 039/142] Created RPC definitions for our profile and server endpoints. --- stockpile/server/rpc/common.proto | 40 +++++++++++ stockpile/server/rpc/profile.proto | 112 +++++++++++++++++++++++++++++ stockpile/server/rpc/server.proto | 63 ++++++++++++++++ 3 files changed, 215 insertions(+) create mode 100644 stockpile/server/rpc/common.proto create mode 100644 stockpile/server/rpc/profile.proto create mode 100644 stockpile/server/rpc/server.proto diff --git a/stockpile/server/rpc/common.proto b/stockpile/server/rpc/common.proto new file mode 100644 index 0000000..5527f01 --- /dev/null +++ b/stockpile/server/rpc/common.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package rpc; +option java_package = "tv.dotstart.stockpile"; + +/** + * Used in services where no input parameters are expected. + */ +message EmptyRequest { +} + +/** + * Used in services where no outputs are expected. + */ +message EmptyResponse { +} + +/** + * Represents a complete user profile. + */ +message Profile { + string id = 1; + string name = 2; + repeated ProfileProperty properties = 3; + ProfileTextures textures = 4; // not set if no skin/cape are set for this account +} + +message ProfileProperty { + string name = 1; + string value = 2; + string signature = 3; +} + +message ProfileTextures { + string profileId = 1; + string profileName = 2; + string skinUrl = 3; + string capeUrl = 4; + int64 timestamp = 5; +} diff --git a/stockpile/server/rpc/profile.proto b/stockpile/server/rpc/profile.proto new file mode 100644 index 0000000..c963e42 --- /dev/null +++ b/stockpile/server/rpc/profile.proto @@ -0,0 +1,112 @@ +syntax = "proto3"; + +package rpc; +option java_package = "tv.dotstart.stockpile"; + +import "common.proto"; + +/** + * Exposes cached (and slightly more consistent) versions of the Mojang profile APIs. + */ +service ProfileService { + /** + * Resolves the profile identifier and correct casing of a given name. + * + * When unix epoch (e.g. zero) is passed instead of an actual timestamp, the + * original user of a name will be resolved (e.g. associations prior to name + * changing support). + * + * If no profile has been associated with the specified name, an unpopulated + * object is returned instead. + */ + rpc GetId (GetIdRequest) returns (ProfileId); + + /** + * Retrieves a complete history of name changes for the profile associated + * with a given identifier. + * + * Names which have been changed to at unix epoch (e.g. zero) refer to the + * original account name. + * + * When no profile with the specified identifier exists, an unpopulated object + * is returned instead. + */ + rpc GetNameHistory (IdRequest) returns (NameHistory); + + /** + * Resolves the profile identifiers and correct casings of multiple names at + * once. + * + * If a name cannot be found, its association will be omitted from the + * resulting array. + * + * Bulk requests do not accept timestamps and will always resolve associations + * at the current time. + */ + rpc BulkGetId (BulkIdRequest) returns (BulkIdResponse); + + /** + * Retrieves a profile based on its associated identifier. + * + * If no profile with the specified identifier exists, an unpopulated object + * is returned instead. + */ + rpc GetProfile (IdRequest) returns (Profile); +} + +/** + * Used to transmit Mojang or RFC formatted UUIDs as the sole parameter. + */ +message IdRequest { + string id = 1; +} + +/** + * Stores the parameters for id requests (based on the respective display name and timestamp) + */ +message GetIdRequest { + string name = 1; + int64 timestamp = 2; +} + +/** + * Represents a profile <-> name mapping at a specified time. + */ +message ProfileId { + string id = 1; + string name = 2; + int64 validUntil = 5; + int64 firstSeenAt = 6; + int64 lastSeenAt = 7; +} + +/** + * Represents a complete name history. + */ +message NameHistory { + repeated NameHistoryEntry history = 1; + int64 validUntil = 2; +} + +/** + * Represents a single entry in the name history. + */ +message NameHistoryEntry { + string name = 1; + int64 changedToAt = 2; + int64 validUntil = 3; +} + +/** + * Stores the parameters for bulk id requests. + */ +message BulkIdRequest { + repeated string names = 1; +} + +/** + * Represents a list of bulk id responses. + */ +message BulkIdResponse { + repeated ProfileId ids = 1; +} diff --git a/stockpile/server/rpc/server.proto b/stockpile/server/rpc/server.proto new file mode 100644 index 0000000..1937f81 --- /dev/null +++ b/stockpile/server/rpc/server.proto @@ -0,0 +1,63 @@ +syntax = "proto3"; + +package rpc; +option java_package = "tv.dotstart.stockpile"; + +import "common.proto"; + +/** + * Exposes cached (and slightly more consistent) versions of the Mojang server + * APIs (e.g. to access the blacklist or to perform cache assisted logins). + */ +service ServerService { + /** + * Retrieves a cached version of the entire server blacklist. + */ + rpc GetBlacklist (EmptyRequest) returns (Blacklist); + + /** + * Evaluates whether a given address has been blacklisted. + * + * This method accepts both IP v4 addresses and regular hostnames. + */ + rpc CheckBlacklist (CheckBlacklistRequest) returns (CheckBlacklistResponse); + + /** + * Performs a cache assisted login (e.g. when a player joins). + * + * The player profile will automatically be placed inside the cache storage + * backend when this method is invoked and will thus greatly reduce the + * latency of succeeding requests. + * + * Logins do not count towards the API rate limit. + */ + rpc Login (LoginRequest) returns (Profile); +} + +/** + * Represents the current server blacklist. + */ +message Blacklist { + repeated string hashes = 1; +} + +/** + * Represents a request which evaluates whether the passed hostnames or ip + * addresses match the server blacklist + */ +message CheckBlacklistRequest { + repeated string addresses = 1; +} + +/** + * represents a response to a prior blacklist check + */ +message CheckBlacklistResponse { + repeated string matchedAddresses = 1; +} + +message LoginRequest { + string displayName = 1; + string serverId = 2; + string ip = 3; +} From cc1d7eee24e0b97975553d4984c95db7e61f1534 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 17:46:20 +0200 Subject: [PATCH 040/142] Created utility methods for conversions between RPC and parsed objects. --- stockpile/server/rpc/utility.go | 292 ++++++++++++++++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 stockpile/server/rpc/utility.go diff --git a/stockpile/server/rpc/utility.go b/stockpile/server/rpc/utility.go new file mode 100644 index 0000000..e58dd94 --- /dev/null +++ b/stockpile/server/rpc/utility.go @@ -0,0 +1,292 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 rpc + +import ( + "errors" + "time" + + "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/google/uuid" +) + +// Converts a profileId into its fully parsed representation +func ProfileIdFromRpc(rpc *ProfileId) (*mojang.ProfileId, error) { + if !rpc.IsPopulated() { + return nil, nil + } + + id, err := uuid.Parse(rpc.Id) + if err != nil { + return nil, err + } + + return &mojang.ProfileId{ + Id: id, + Name: rpc.Name, + FirstSeenAt: time.Unix(rpc.FirstSeenAt, 0), + LastSeenAt: time.Unix(rpc.LastSeenAt, 0), + ValidUntil: time.Unix(rpc.ValidUntil, 0), + }, nil +} + +// Converts an array of profileIds into their fully parsed representation +func ProfileIdsFromRpcArray(rpc []*ProfileId) ([]*mojang.ProfileId, error) { + arr := make([]*mojang.ProfileId, len(rpc)) + for i, encoded := range rpc { + profileId, err := ProfileIdFromRpc(encoded) + if err != nil { + return nil, err + } + if profileId == nil { + return nil, errors.New("encountered one or more unpopulated profiles in response") + } + arr[i] = profileId + } + return arr, nil +} + +// Converts a profileId into its rpc representation +func ProfileIdToRpc(profileId *mojang.ProfileId) *ProfileId { + return &ProfileId{ + Id: profileId.Id.String(), + Name: profileId.Name, + FirstSeenAt: profileId.FirstSeenAt.Unix(), + LastSeenAt: profileId.LastSeenAt.Unix(), + ValidUntil: profileId.ValidUntil.Unix(), + } +} + +// Converts an array of profileIds into their rpc representation +func ProfileIdsToRpcArray(profileIds []*mojang.ProfileId) []*ProfileId { + arr := make([]*ProfileId, len(profileIds)) + for i, profileId := range profileIds { + arr[i] = ProfileIdToRpc(profileId) + } + return arr +} + +// converts a name change into its rpc representation +func NameChangeToRpc(change *mojang.NameChange) *NameHistoryEntry { + return &NameHistoryEntry{ + Name: change.Name, + ChangedToAt: change.ChangedToAt.Unix(), + ValidUntil: change.ValidUntil.Unix(), + } +} + +// converts an array of name changes into their rpc representation +func NameChangesToRpcArray(changes []*mojang.NameChange) []*NameHistoryEntry { + arr := make([]*NameHistoryEntry, len(changes)) + for i, change := range changes { + arr[i] = NameChangeToRpc(change) + } + return arr +} + +// converts a name change from its rpc representation +func NameChangeFromRpc(rpc *NameHistoryEntry) *mojang.NameChange { + return &mojang.NameChange{ + Name: rpc.Name, + ChangedToAt: time.Unix(rpc.ChangedToAt, 0), + ValidUntil: time.Unix(rpc.ValidUntil, 0), + } +} + +// converts an array of name changes from their rpc representation +func NameChangesFromRpcArray(rpc []*NameHistoryEntry) []*mojang.NameChange { + arr := make([]*mojang.NameChange, len(rpc)) + for i, change := range rpc { + arr[i] = NameChangeFromRpc(change) + } + return arr +} + +// converts a name history element into its rpc representation +func NameHistoryToRpc(history *mojang.NameChangeHistory) *NameHistory { + if history == nil || len(history.History) == 0 { + return &NameHistory{} + } + + return &NameHistory{ + History: NameChangesToRpcArray(history.History), + } +} + +// converts a name history from its rpc representation +func NameHistoryFromRpc(history *NameHistory) *mojang.NameChangeHistory { + if !history.IsPopulated() { + return nil + } + + return &mojang.NameChangeHistory{ + History: NameChangesFromRpcArray(history.History), + } +} + +// converts the result of a bulk id resolve operation into its rpc representation +func BulkIdsToRpc(ids []*mojang.ProfileId) *BulkIdResponse { + if ids == nil || len(ids) == 0 { + return &BulkIdResponse{} + } + + return &BulkIdResponse{ + Ids: ProfileIdsToRpcArray(ids), + } +} + +// converts the result of a bulk id resolve operation from its rpc representation +func BulkIdsFromRpc(rpc *BulkIdResponse) ([]*mojang.ProfileId, error) { + if !rpc.IsPopulated() { + return nil, nil + } + + return ProfileIdsFromRpcArray(rpc.Ids) +} + +// converts a profile into its rpc representation +func ProfileToRpc(profile *mojang.Profile) *Profile { + var tex *ProfileTextures = nil + if profile.Textures != nil { + tex = ProfileTexturesToRpc(profile.Textures) + } + + return &Profile{ + Id: profile.Id.String(), + Name: profile.Name, + Properties: ProfilePropertiesToRpcArray(profile.Properties), + Textures: tex, + } +} + +// converts a profile from its rpc representation +func ProfileFromRpc(rpc *Profile) (*mojang.Profile, error) { + id, err := uuid.Parse(rpc.Id) + if err != nil { + return nil, err + } + + var tex *mojang.ProfileTextures + if rpc.Textures != nil { + tex, err = ProfileTexturesFromRpc(rpc.Textures) + if err != nil { + return nil, err + } + } + + return &mojang.Profile{ + Id: id, + Name: rpc.Name, + Properties: ProfilePropertiesFromRpcArray(rpc.Properties), + Textures: tex, + }, nil +} + +// converts a map of profile properties into their rpc representation +func ProfilePropertiesToRpcArray(props map[string]*mojang.ProfileProperty) []*ProfileProperty { + arr := make([]*ProfileProperty, 0) + for _, prop := range props { + arr = append(arr, ProfilePropertyToRpc(prop)) + } + return arr +} + +// converts an array of profile properties from their rpc representation +func ProfilePropertiesFromRpcArray(rpc []*ProfileProperty) map[string]*mojang.ProfileProperty { + arr := make(map[string]*mojang.ProfileProperty) + for _, prop := range rpc { + arr[prop.Name] = ProfilePropertyFromRpc(prop) + } + return arr +} + +// converts a profile property into its rpc representation +func ProfilePropertyToRpc(prop *mojang.ProfileProperty) *ProfileProperty { + return &ProfileProperty{ + Name: prop.Name, + Value: prop.Value, + Signature: prop.Signature, + } +} + +// converts a profile property from its rpc representation +func ProfilePropertyFromRpc(rpc *ProfileProperty) *mojang.ProfileProperty { + return &mojang.ProfileProperty{ + Name: rpc.Name, + Value: rpc.Value, + Signature: rpc.Signature, + } +} + +// converts a profile textures attribute into its rpc representation +func ProfileTexturesToRpc(tex *mojang.ProfileTextures) *ProfileTextures { + return &ProfileTextures{ + ProfileId: tex.ProfileId.String(), + ProfileName: tex.ProfileName, + SkinUrl: tex.Textures["SKIN"], // TODO: this sucks + CapeUrl: tex.Textures["CAPE"], + Timestamp: tex.Timestamp.Unix(), + } +} + +func ProfileTexturesFromRpc(rpc *ProfileTextures) (*mojang.ProfileTextures, error) { + id, err := uuid.Parse(rpc.ProfileId) + if err != nil { + return nil, err + } + + textures := make(map[string]string) + textures["SKIN"] = rpc.SkinUrl + textures["CAPE"] = rpc.CapeUrl + + return &mojang.ProfileTextures{ + Timestamp: time.Unix(rpc.Timestamp, 0), + ProfileId: id, + ProfileName: rpc.ProfileName, + Textures: textures, + }, nil +} + +func BlacklistToRpc(blacklist *mojang.Blacklist) *Blacklist { + return &Blacklist{ + Hashes: blacklist.Hashes, + } +} + +func BlacklistFromRpc(blacklist *Blacklist) (*mojang.Blacklist, error) { + return mojang.NewBlacklist(blacklist.Hashes) +} + +// evaluates whether the message has been populated with actual data (e.g. whether it is not empty) +func (p *ProfileId) IsPopulated() bool { + return p.Id != "" +} + +// evaluates whether the message has been populated with actual data (e.g. whether it is not empty) +func (r *NameHistory) IsPopulated() bool { + return r.History != nil && len(r.History) != 0 +} + +// evaluates whether the message has been populated with actual data (e.g. whether it is not empty) +func (r *BulkIdResponse) IsPopulated() bool { + return r.Ids != nil && len(r.Ids) != 0 +} + +// evaluates whether the message has been populated with actual data (e.g. whether it is not empty) +func (p *Profile) IsPopulated() bool { + return p.Id != "" +} From 7fd30211e9e0bcefa7f08bac78c2b2e87dd122c0 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 17:46:44 +0200 Subject: [PATCH 041/142] Created a wrapper around the go plugin API. --- stockpile/plugin/main.go | 87 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 stockpile/plugin/main.go diff --git a/stockpile/plugin/main.go b/stockpile/plugin/main.go new file mode 100644 index 0000000..740334a --- /dev/null +++ b/stockpile/plugin/main.go @@ -0,0 +1,87 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 plugin + +import ( + "errors" + "plugin" + "runtime" + "strings" + + "github.com/dotStart/Stockpile/stockpile/server" +) + +// represents the metadata associated with a plugin implementation +type Metadata struct { + Name string + Authors []string + Website string +} + +// represents a loaded plugin +type StockpilePlugin struct { + handle *plugin.Plugin + storageFactory func(backend *server.Config) (StorageBackend, error) + + Meta Metadata +} + +// opens an arbitrary plugin +func Open(path string) (*StockpilePlugin, error) { + if runtime.GOOS == "windows" { + return nil, errors.New("plugins are not supported on windows") + } + + if !strings.HasSuffix(path, ".so") { + path += ".so" + } + + handle, err := plugin.Open(path) + if err != nil { + return nil, err + } + + metaSymbol, err := handle.Lookup("GetMetadata") + if err != nil { + return nil, err + } + + var storageFactory func(backend *server.Config) (StorageBackend, error) + storageFactorySymbol, err := handle.Lookup("CreateStorageBackend") + if err != nil { + storageFactory = nil + } else { + storageFactory = storageFactorySymbol.(func(backend *server.Config) (StorageBackend, error)) + } + + return &StockpilePlugin{ + handle: handle, + storageFactory: storageFactory, + + Meta: metaSymbol.(func() (Metadata))(), + }, nil +} + +// evaluates whether the plugin provides a storage backend implementation +func (p *StockpilePlugin) HasStorageBackendImplementation() bool { + return p.storageFactory != nil +} + +// creates a new storage backend using the plugin's registered provider +func (p *StockpilePlugin) CreateStorageBackend(cfg *server.Config) (StorageBackend, error) { + return p.storageFactory(cfg) +} From 9a45d516c9395d29de8caa5991e9450956a39adf Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 17:47:00 +0200 Subject: [PATCH 042/142] Created a definition for storage backends (and a memory implementation). --- stockpile/plugin/storage.go | 229 ++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 stockpile/plugin/storage.go diff --git a/stockpile/plugin/storage.go b/stockpile/plugin/storage.go new file mode 100644 index 0000000..60053b8 --- /dev/null +++ b/stockpile/plugin/storage.go @@ -0,0 +1,229 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 plugin + +import ( + "strings" + "time" + + "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/dotStart/Stockpile/stockpile/server" + "github.com/google/uuid" + "github.com/op/go-logging" +) + +// provides an abstraction layer between the application and a storage backend +type StorageBackend interface { + // Profile Data + GetProfileId(name string, at time.Time) (*mojang.ProfileId, error) + PutProfileId(profileId *mojang.ProfileId) error + GetNameHistory(id uuid.UUID) (*mojang.NameChangeHistory, error) + PutNameHistory(id uuid.UUID, history *mojang.NameChangeHistory) error + GetProfile(id uuid.UUID) (*mojang.Profile, error) + PutProfile(profile *mojang.Profile) error + + // Server Data + GetBlacklist() (*mojang.Blacklist, error) + PutBlacklist(blacklist *mojang.Blacklist) error +} + +type MemoryStorageBackend struct { + cfg *server.Config + logger *logging.Logger + + profileId map[string][]expirationWrapper + nameHistory map[uuid.UUID]*expirationWrapper + profile map[uuid.UUID]*expirationWrapper + + blacklist *expirationWrapper +} + +// creates a new memory based storage backend +func NewMemoryStorageBackend(cfg *server.Config) *MemoryStorageBackend { + return &MemoryStorageBackend{ + cfg: cfg, + logger: logging.MustGetLogger("memdb"), + + profileId: make(map[string][]expirationWrapper), + nameHistory: make(map[uuid.UUID]*expirationWrapper), + profile: make(map[uuid.UUID]*expirationWrapper), + } +} + +func (m *MemoryStorageBackend) GetProfileId(name string, at time.Time) (*mojang.ProfileId, error) { + m.clearExpiredEntries() + + name = strings.ToLower(name) + m.logger.Debugf("Checking profile associations for \"%s\" at time %s", name, at) + mappings := m.profileId[name] + if mappings == nil { + m.logger.Debugf("No associations for \"%s\"", name) + return nil, nil + } + + for _, exp := range mappings { + association := exp.content.(*mojang.ProfileId) + if association.IsValid(at) { + m.logger.Debugf("Association to profile %s matches", association.Id) + return association, nil + } else { + m.logger.Debugf("Association to profile %s is invalid", association.Id) + } + } + + return nil, nil +} + +func (m *MemoryStorageBackend) PutProfileId(profileId *mojang.ProfileId) error { + m.clearExpiredEntries() + + name := strings.ToLower(profileId.Name) + m.logger.Debugf("Updating association for name \"%s\" to profile %s at time %s (valid until %s)", profileId.Name, profileId.Id, profileId.LastSeenAt, profileId.ValidUntil) + mappings := m.profileId[name] + + if mappings == nil { + mappings = make([]expirationWrapper, 1) + } + + mappings = append(mappings, expirationWrapper{ + content: profileId, + createdAt: time.Now(), + }) + m.profileId[name] = mappings + + return nil +} + +func (m *MemoryStorageBackend) GetNameHistory(id uuid.UUID) (*mojang.NameChangeHistory, error) { + m.clearExpiredEntries() + + exp := m.nameHistory[id] + if exp == nil { + return nil, nil + } + + return exp.content.(*mojang.NameChangeHistory), nil +} + +func (m *MemoryStorageBackend) PutNameHistory(id uuid.UUID, history *mojang.NameChangeHistory) error { + m.clearExpiredEntries() + + m.logger.Debugf("Storing history for profile %s (consisting of %d elements)", id.String(), len(history.History)) + m.nameHistory[id] = &expirationWrapper{ + content: history, + createdAt: time.Now(), + } + + return nil +} + +func (m *MemoryStorageBackend) GetProfile(id uuid.UUID) (*mojang.Profile, error) { + m.clearExpiredEntries() + + exp := m.profile[id] + if exp == nil { + return nil, nil + } + + return exp.content.(*mojang.Profile), nil +} + +func (m *MemoryStorageBackend) PutProfile(profile *mojang.Profile) error { + m.clearExpiredEntries() + + m.logger.Debugf("Storing profile %s", profile.Id) + m.profile[profile.Id] = &expirationWrapper{ + content: profile, + createdAt: time.Now(), + } + + return nil +} + +// clears all expired entries from the database +func (m *MemoryStorageBackend) clearExpiredEntries() { // TODO: run on a timer instead? + m.logger.Debug("Purging expired data") + + deletedProfileIds := 0 + deletedNameHistories := 0 + deletedProfiles := 0 + deletedBlacklists := 0 + + for profileId, mappings := range m.profileId { + for i, exp := range mappings { + if !exp.isValid(m.cfg.Ttl.Name) { + deletedProfileIds++ + + if len(mappings) == 1 { + delete(m.profileId, profileId) + continue + } + + mappings = append(mappings[:i], mappings[i+1:]...) + continue + } + } + } + + for key, history := range m.nameHistory { + if !history.isValid(m.cfg.Ttl.NameHistory) { + deletedNameHistories++ + delete(m.nameHistory, key) + } + } + + for key, profile := range m.profile { + if !profile.isValid(m.cfg.Ttl.Profile) { + deletedProfiles++ + delete(m.profile, key) + } + } + + if m.blacklist != nil && m.blacklist.isValid(m.cfg.Ttl.Blacklist) { + deletedBlacklists = 1 + m.blacklist = nil + } + + m.logger.Debugf("Removed %d profile Ids, %d name histories, %d profiles and %d blacklists from memory", deletedProfileIds, deletedNameHistories, deletedProfiles, deletedBlacklists) +} + +func (m *MemoryStorageBackend) GetBlacklist() (*mojang.Blacklist, error) { + if m.blacklist == nil { + return nil, nil + } + + return m.blacklist.content.(*mojang.Blacklist), nil +} + +func (m *MemoryStorageBackend) PutBlacklist(blacklist *mojang.Blacklist) error { + m.blacklist = &expirationWrapper{ + content: blacklist, + createdAt: time.Now(), + } + return nil +} + +// provides a primitive wrapper object which handles expiration in the memory storage backend +type expirationWrapper struct { + content interface{} + createdAt time.Time +} + +// evaluates whether a particular entry is still considered valid +func (w *expirationWrapper) isValid(ttl time.Duration) bool { + return time.Since(w.createdAt) <= ttl +} From 3f4751b1bb87c34ed531b3688d8548d9b9fcf397 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 17:47:37 +0200 Subject: [PATCH 043/142] Created a profile service implementation. --- stockpile/server/service/profile.go | 191 ++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 stockpile/server/service/profile.go diff --git a/stockpile/server/service/profile.go b/stockpile/server/service/profile.go new file mode 100644 index 0000000..94e815a --- /dev/null +++ b/stockpile/server/service/profile.go @@ -0,0 +1,191 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 service + +import ( + "errors" + "time" + + "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/dotStart/Stockpile/stockpile/plugin" + "github.com/dotStart/Stockpile/stockpile/server" + "github.com/dotStart/Stockpile/stockpile/server/rpc" + "github.com/op/go-logging" + "golang.org/x/net/context" +) + +type ProfileServiceImpl struct { + logger *logging.Logger + api *mojang.MojangAPI + cfg *server.Config + storage plugin.StorageBackend +} + +func NewProfileService(api *mojang.MojangAPI, cfg *server.Config, backend plugin.StorageBackend) (*ProfileServiceImpl) { + return &ProfileServiceImpl{ + logger: logging.MustGetLogger("profile-srv"), + api: api, + cfg: cfg, + storage: backend, + } +} + +func (s *ProfileServiceImpl) GetId(_ context.Context, req *rpc.GetIdRequest) (*rpc.ProfileId, error) { + at := time.Unix(req.Timestamp, 0) + s.logger.Debugf("Processing request for profile id for name \"%s\" at time %s", req.Name, at) + association, err := s.storage.GetProfileId(req.Name, at) + if err != nil { + s.logger.Errorf("Database responded with error during lookup of name \"%s\" at time %s: %s", req.Name, at.String(), err) + } else if association != nil { + s.logger.Debugf("Returning cached result of database: %+v", association) + } + if err != nil || association == nil { + s.logger.Debugf("Cache miss - Requesting information from upstream") + association, err = s.api.GetId(req.Name, time.Unix(req.Timestamp, 0)) + if err != nil { + s.logger.Errorf("Failed to retrieve association of name \"%s\" at time %s: %s", req.Name, at.String(), err) + return nil, err + } + + if association != nil { + s.logger.Debugf("Pushing updated information to cache backend") + err = s.storage.PutProfileId(association) + if err != nil { + s.logger.Errorf("Failed to push profile association to storage backend: %s", err) + } + } + } + + if association == nil { + s.logger.Debugf("No profile with name \"%s\" at time %s", req.Name, at) + return &rpc.ProfileId{}, nil + } + + s.logger.Debugf("Display name \"%s\" resolved to profile %s at time %s", req.Name, association.Id, at) + return rpc.ProfileIdToRpc(association), nil +} + +func (s *ProfileServiceImpl) GetNameHistory(_ context.Context, req *rpc.IdRequest) (*rpc.NameHistory, error) { + id, err := mojang.ParseId(req.Id) + if err != nil { + return nil, err + } + + s.logger.Debugf("Processing request for name history for profile %s", id) + history, err := s.storage.GetNameHistory(id) + if err != nil { + s.logger.Errorf("Database responded with error during lookup of history of profile \"%s\": %s", id, err) + } + if err != nil || history == nil { + history, err = s.api.GetHistory(id) + if err != nil { + s.logger.Errorf("Failed to retrieve history of profile \"%s\": %s", id, err) + return nil, err + } + + if history == nil { + s.logger.Debugf("No profile with id %s for name history request", id) + return &rpc.NameHistory{}, nil + } + + s.logger.Debugf("Updated history with %d elements from upstream", len(history.History)) + err = s.storage.PutNameHistory(id, history) + if err != nil { + s.logger.Errorf("Failed to push name history to storage backend: %s", err) + } + } + + s.logger.Debugf("Name history for profile %s consists of %d elements", id, len(history.History)) + return rpc.NameHistoryToRpc(history), nil +} + +func (s *ProfileServiceImpl) BulkGetId(_ context.Context, req *rpc.BulkIdRequest) (*rpc.BulkIdResponse, error) { + if len(req.Names) > 100 { + return nil, errors.New("cannot process more than 100 names at once") + } + + s.logger.Debugf("Processing request for ids of %d profiles", len(req.Names)) + + names := make([]string, 0) + results := make([]*mojang.ProfileId, 0) + for _, name := range req.Names { + profileId, err := s.storage.GetProfileId(name, time.Now()) + if profileId == nil || err != nil { + names = append(names, name) + + if err != nil { + s.logger.Errorf("Failed to resolve profileId for name \"%s\" from storage backend: %s", name, err) + } + } else { + s.logger.Debugf("Resolved name \"%s\" to profile %s via cache", name, profileId.Id) + results = append(results, profileId) + } + } + s.logger.Debugf("Resolved %d out of %d names from cache (%d remain)", len(results), len(req.Names), len(names)) + + if len(names) != 0 { + upstreamResults, err := s.api.BulkGetId(names) + if err != nil { + s.logger.Errorf("Failed to resolve %d names from upstream: %s", len(names), err) + return nil, err + } + + for _, profileId := range upstreamResults { + err := s.storage.PutProfileId(profileId) + if err != nil { + s.logger.Errorf("Failed to push profile association to storage backend: %s", err) + } + } + + results = append(results, upstreamResults...) + } + + return rpc.BulkIdsToRpc(results), nil +} + +func (s *ProfileServiceImpl) GetProfile(_ context.Context, req *rpc.IdRequest) (*rpc.Profile, error) { + id, err := mojang.ParseId(req.Id) + if err != nil { + return nil, err + } + + s.logger.Debugf("Processing request for profile %s", id) + profile, err := s.storage.GetProfile(id) + if err != nil || profile == nil { + if err != nil { + s.logger.Errorf("Failed to resolve profile %s from storage backend: %s", id, err) + } + + s.logger.Debugf("Cache miss - Requesting update from upstream server") + profile, err = s.api.GetProfile(id) + if err != nil { + s.logger.Errorf("Failed to resolve profile %s from upstream server: %s", id, err) + return nil, err + } + if profile == nil { + return &rpc.Profile{}, nil + } + + err = s.storage.PutProfile(profile) + if err != nil { + s.logger.Errorf("Failed to push profile to storage backend: %s", err) + } + // TODO: Update profile mappings + } + + return rpc.ProfileToRpc(profile), nil +} From 37713014de425f400e92398777730f021aed81cd Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 17:47:48 +0200 Subject: [PATCH 044/142] Created a server service implementation. --- stockpile/server/service/server.go | 116 +++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 stockpile/server/service/server.go diff --git a/stockpile/server/service/server.go b/stockpile/server/service/server.go new file mode 100644 index 0000000..0656fc2 --- /dev/null +++ b/stockpile/server/service/server.go @@ -0,0 +1,116 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 service + +import ( + "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/dotStart/Stockpile/stockpile/plugin" + "github.com/dotStart/Stockpile/stockpile/server" + "github.com/dotStart/Stockpile/stockpile/server/rpc" + "github.com/op/go-logging" + "golang.org/x/net/context" +) + +type ServerServiceImpl struct { + logger *logging.Logger + api *mojang.MojangAPI + cfg *server.Config + storage plugin.StorageBackend +} + +func NewServerService(api *mojang.MojangAPI, cfg *server.Config, backend plugin.StorageBackend) *ServerServiceImpl { + return &ServerServiceImpl{ + logger: logging.MustGetLogger("server-srv"), + api: api, + cfg: cfg, + storage: backend, + } +} + +func (s *ServerServiceImpl) getBlacklist() (*mojang.Blacklist, error) { + blacklist, err := s.storage.GetBlacklist() + if err != nil || blacklist == nil { + if err != nil { + s.logger.Errorf("Failed to retrieve blacklist from storage backend: %s", err) + } + + s.logger.Debugf("Cache miss - Requesting update from upstream") + blacklist, err = s.api.GetBlacklist() + if err != nil { + s.logger.Errorf("Failed to retrieve blacklist: %s", err) + return nil, err + } + + s.logger.Debugf("Updating cached version") + err = s.storage.PutBlacklist(blacklist) + if err != nil { + s.logger.Errorf("Failed to push blacklist to storage backend: %s", err) + } + } + + return blacklist, err +} + +func (s *ServerServiceImpl) GetBlacklist(context.Context, *rpc.EmptyRequest) (*rpc.Blacklist, error) { + s.logger.Debugf("Processing request for complete server blacklist") + blacklist, err := s.getBlacklist() + if err != nil { + return nil, err + } + + return rpc.BlacklistToRpc(blacklist), nil +} + +func (s *ServerServiceImpl) CheckBlacklist(_ context.Context, req *rpc.CheckBlacklistRequest) (*rpc.CheckBlacklistResponse, error) { + s.logger.Debugf("Processing request to check blacklist for %d addresses", len(req.Addresses)) + blacklist, err := s.getBlacklist() + if err != nil { + return nil, err + } + + matches := make([]string, 0) + for _, addr := range req.Addresses { + match, err := blacklist.IsBlacklisted(addr) + if err != nil { + return nil, err + } + + if match { + matches = append(matches, addr) + } + } + return &rpc.CheckBlacklistResponse{ + MatchedAddresses: matches, + }, nil +} + +func (s *ServerServiceImpl) Login(_ context.Context, req *rpc.LoginRequest) (*rpc.Profile, error) { + s.logger.Debugf("Processing request to login user \"%s\" with serverId %s and ip %s", req.DisplayName, req.ServerId, req.Ip) + profile, err := s.api.Login(req.DisplayName, req.ServerId, req.Ip) + if err != nil { + s.logger.Errorf("Failed to perform login: %s", err) + return nil, err + } + + err = s.storage.PutProfile(profile) + if err != nil { + s.logger.Errorf("Failed to push profile to storage backend: %s", err) + } + // TODO: Update profile mappings + + return rpc.ProfileToRpc(profile), nil +} From 993dc0984ad415848193155fc637856658fc2910 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 17:48:19 +0200 Subject: [PATCH 045/142] Created a wrapper to simplify the server startup and state storage. --- stockpile/server/service/main.go | 92 ++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 stockpile/server/service/main.go diff --git a/stockpile/server/service/main.go b/stockpile/server/service/main.go new file mode 100644 index 0000000..f175554 --- /dev/null +++ b/stockpile/server/service/main.go @@ -0,0 +1,92 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 service + +//go:generate protoc -I ../rpc --go_out=plugins=grpc:../rpc common.proto profile.proto server.proto + +import ( + "fmt" + "net" + "path" + + "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/dotStart/Stockpile/stockpile/plugin" + "github.com/dotStart/Stockpile/stockpile/server" + "github.com/dotStart/Stockpile/stockpile/server/rpc" + "github.com/op/go-logging" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +// Represents an RPC server +type Server struct { + logger *logging.Logger + cfg *server.Config + storage plugin.StorageBackend + + srv *grpc.Server +} + +// Constructs a new RPC server instance and starts it +func NewServer(config *server.Config) (*Server, error) { + logger := logging.MustGetLogger("rpc") + + var storage plugin.StorageBackend + if config.Storage.Type == "mem" { + logger.Warning("Using in-memory storage") + storage = plugin.NewMemoryStorageBackend(config) + } else { + plg, err := plugin.Open(path.Join("plugins", config.Storage.Type)) + if err != nil { + return nil, err + } + + if !plg.HasStorageBackendImplementation() { + return nil, fmt.Errorf("selected plugin \"%s\" does not provide a storage backend implementation", config.Storage.Type) + } + + storage, err = plg.CreateStorageBackend(config) + if err != nil { + return nil, err + } + + logger.Infof("Using database plugin: %s", config.Storage.Type) + } + + return &Server{ + logger: logger, + cfg: config, + storage: storage, + }, nil +} + +// Starts listening on an arbitrary socket +func (s *Server) Listen(listener net.Listener) { + api := mojang.New() + + s.srv = grpc.NewServer() + grpc.NewServer() + rpc.RegisterProfileServiceServer(s.srv, NewProfileService(api, s.cfg, s.storage)) + rpc.RegisterServerServiceServer(s.srv, NewServerService(api, s.cfg, s.storage)) + reflection.Register(s.srv) + s.srv.Serve(listener) +} + +// Stops listening +func (s *Server) Stop() { + s.srv.Stop() +} From 5e0b323f478ddddbcf02a1fb1eaad09b489a6b5d Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 17:48:35 +0200 Subject: [PATCH 046/142] Created a server command. --- stockpile/command/server.go | 149 ++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 stockpile/command/server.go diff --git a/stockpile/command/server.go b/stockpile/command/server.go new file mode 100644 index 0000000..d4ae92d --- /dev/null +++ b/stockpile/command/server.go @@ -0,0 +1,149 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 command + +import ( + "context" + "flag" + "fmt" + "net" + "os" + + "github.com/dotStart/Stockpile/stockpile/metadata" + "github.com/dotStart/Stockpile/stockpile/server" + "github.com/dotStart/Stockpile/stockpile/server/service" + "github.com/google/subcommands" + "github.com/op/go-logging" + "github.com/soheilhy/cmux" +) + +type ServerCommand struct { + flagConfig string + flagDevelopment bool + flagLogLevel string +} + +func (*ServerCommand) Name() string { + return "server" +} + +func (*ServerCommand) Synopsis() string { + return "starts a new Stockpile server instance" +} + +func (*ServerCommand) Usage() string { + return `Usage: stockpile server [options] + +This command starts a new Stockpile server instance which relies to API requests. By default, Stockpile +will only start the RPC server. + +Start a server with a configuration file: + + $ stockpile server -config=/etc/stockpile/config.hcl + +Run in development mode: + + $ stockpile server -dev + +For a full list of examples, please refer to the documentation. + +Available command specific flags: + +` +} + +func (c *ServerCommand) SetFlags(f *flag.FlagSet) { + f.StringVar(&c.flagConfig, "config", "", "specifies a configuration file or directory") + f.BoolVar(&c.flagDevelopment, "dev", false, "enables development mode") + f.StringVar(&c.flagLogLevel, "log-level", "info", "specifies a log level") +} + +func (c *ServerCommand) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + var cfg *server.Config + if c.flagDevelopment { + cfg = server.DevelopmentConfig() + } + if c.flagConfig != "" { + fileCfg, err := server.LoadConfig(c.flagConfig) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: Failed to read the configuration file(s): %s", err) + return 1 + } + + if cfg == nil { + cfg = fileCfg + } else { + cfg.Merge(fileCfg) + } + } + + level, err := logging.LogLevel(c.flagLogLevel) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: Illegal log level \"%s\": %s", c.flagLogLevel, err) + return 1 + } + + format := logging.MustStringFormatter(`%{color}%{time:15:04:05.000} [%{level:.4s}] %{module} : %{color:reset} %{message}`) + backend := logging.AddModuleLevel(logging.NewBackendFormatter(logging.NewLogBackend(os.Stdout, "", 0), format)) + backend.SetLevel(level, "") + logging.SetBackend(backend) + + fmt.Printf("==> Stockpile Configuration\n\n") + fmt.Printf(" Server Address: %s\n", cfg.BindAddress) + fmt.Printf(" Version: %s\n", metadata.VersionFull()) + fmt.Printf(" Commit Hash: %s\n", metadata.CommitHash()) + fmt.Printf(" Log Level: %s\n", c.flagLogLevel) + fmt.Printf(" Storage Backend: %s\n", cfg.Storage.Type) + fmt.Printf(" PID: %d\n\n", os.Getpid()) + + fmt.Printf("==> TTL Configuration\n\n") + fmt.Printf(" Names: %s\n", cfg.Ttl.Name) + fmt.Printf(" Profile: %s\n", cfg.Ttl.Profile) + fmt.Printf(" Name History: %s\n", cfg.Ttl.NameHistory) + fmt.Printf(" Blacklist: %s\n\n", cfg.Ttl.Profile) + + var log = logging.MustGetLogger("stockpile") + + if c.flagDevelopment { + log.Warning("Stockpile is running in development mode") + } + + // initialize the shared network listener and mux first so we can detect potential binding errors early on + listener, err := net.Listen("tcp", cfg.BindAddress) + if err != nil { + log.Fatalf("Failed to listen on %s (TCP): %s", cfg.BindAddress, err) + } + + mux := cmux.New(listener) + + // initialize the RPC server at all times (only differ between mux policies depending on whether the legacy API or UI + // is enabled) + rpcPolicy := cmux.Any() + if cfg.EnableLegacyApi { + rpcPolicy = cmux.HTTP2HeaderField("content-type", "application/grpc") + } + rpcServer, err := service.NewServer(cfg) + if err != nil { + log.Fatalf("Failed to initialize grpc server: %s", err) + } + go rpcServer.Listen(mux.Match(rpcPolicy)) + defer rpcServer.Stop() + log.Info("Enabled grpc server") + + mux.Serve() + return 0 +} From 39c5f0284fae06ad15c0f8ce10a8b72a940cbbdf Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 17:48:54 +0200 Subject: [PATCH 047/142] Created commands for command line access to an arbitrary server. --- stockpile/command/blacklist.go | 91 ++++++++++++++++++++ stockpile/command/history.go | 95 +++++++++++++++++++++ stockpile/command/id.go | 152 +++++++++++++++++++++++++++++++++ stockpile/command/profile.go | 98 +++++++++++++++++++++ 4 files changed, 436 insertions(+) create mode 100644 stockpile/command/blacklist.go create mode 100644 stockpile/command/history.go create mode 100644 stockpile/command/id.go create mode 100644 stockpile/command/profile.go diff --git a/stockpile/command/blacklist.go b/stockpile/command/blacklist.go new file mode 100644 index 0000000..f557c2e --- /dev/null +++ b/stockpile/command/blacklist.go @@ -0,0 +1,91 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 command + +import ( + "flag" + "fmt" + "os" + + "github.com/dotStart/Stockpile/stockpile/server/rpc" + "github.com/google/subcommands" + "golang.org/x/net/context" +) + +type BlacklistCommand struct { + ClientCommand +} + +func (*BlacklistCommand) Name() string { + return "check-blacklist" +} + +func (*BlacklistCommand) Synopsis() string { + return "checks whether an address has been blacklisted using a remote Stockpile server" +} + +func (*BlacklistCommand) Usage() string { + return `Usage: stockpile check-blacklist
    [address2] [address3] ... + +This command evaluates whether a given server address has been blacklisted: + + $ stockpile check-blacklist example.org + +You may also pass multiple addresses at once, if desired: + + $ stockpile get-id example.org example.net + +Available command specific flags: + +` +} + +func (c *BlacklistCommand) SetFlags(f *flag.FlagSet) { + c.ClientCommand.SetFlags(f) +} + +func (c *BlacklistCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + client, err := c.createClient() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to establish a connection to server \"%s\": %s\n", c.flagServerAddress, err) + return 1 + } + + if f.NArg() == 0 { + fmt.Fprintf(os.Stderr, "Illegal command invocation: address is required\n") + return 1 + } + + serverService := rpc.NewServerServiceClient(client) + res, err := serverService.CheckBlacklist(ctx, &rpc.CheckBlacklistRequest{ + Addresses: f.Args(), + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Command execution has failed: %s\n", err) + return 1 + } + + if len(res.MatchedAddresses) == 0 { + fmt.Fprintf(os.Stderr, "None of the passed addresses match") + return 0 + } + + for _, match := range res.MatchedAddresses { + fmt.Printf("%s matches the blacklist\n", match) + } + return 0 +} diff --git a/stockpile/command/history.go b/stockpile/command/history.go new file mode 100644 index 0000000..ef92875 --- /dev/null +++ b/stockpile/command/history.go @@ -0,0 +1,95 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 command + +import ( + "flag" + "fmt" + "os" + + "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/dotStart/Stockpile/stockpile/server/rpc" + "github.com/google/subcommands" + "golang.org/x/net/context" +) + +type HistoryCommand struct { + ClientCommand + + flagTimestamp string +} + +func (*HistoryCommand) Name() string { + return "name-history" +} + +func (*HistoryCommand) Synopsis() string { + return "queries a profile's name history" +} + +func (*HistoryCommand) Usage() string { + return `Usage: stockpile name-history [options] + +This command retrieves the name history of a profile from a Stockpile server: + + $ stockpile name-history d71a5dac-4e71-443b-8158-4389c269e44d + +Note that Mojang formatted UUIDs are also supported: + + $ stockpile name-history d71a5dac4e71443b81584389c269e44d + +Available command specific flags: + +` +} + +func (c *HistoryCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + client, err := c.createClient() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to establish a connection to server \"%s\": %s\n", c.flagServerAddress, err) + return 1 + } + + if f.NArg() != 1 { + fmt.Fprintf(os.Stderr, "Illegal command invocation: id is required\n") + return 1 + } + + id, err := mojang.ParseId(f.Arg(0)) + if err != nil { + fmt.Fprintf(os.Stderr, "Illegal profile id: %s", id) + return 1 + } + + profileService := rpc.NewProfileServiceClient(client) + res, err := profileService.GetNameHistory(ctx, &rpc.IdRequest{ + Id: id.String(), + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Command execution has failed: %s\n", err) + return 1 + } + + if !res.IsPopulated() { + fmt.Fprintf(os.Stderr, "No such profile") + return 1 + } + + history := rpc.NameHistoryFromRpc(res) + writeTable(os.Stdout, *history) + return 0 +} diff --git a/stockpile/command/id.go b/stockpile/command/id.go new file mode 100644 index 0000000..8970e5e --- /dev/null +++ b/stockpile/command/id.go @@ -0,0 +1,152 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 command + +import ( + "flag" + "fmt" + "os" + "time" + + "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/dotStart/Stockpile/stockpile/server/rpc" + "github.com/google/subcommands" + "golang.org/x/net/context" +) + +type IdCommand struct { + ClientCommand + + flagTimestamp string +} + +func (*IdCommand) Name() string { + return "get-id" +} + +func (*IdCommand) Synopsis() string { + return "queries a user's profile id from a Stockpile server" +} + +func (*IdCommand) Usage() string { + return `Usage: stockpile get-id [options] [name2] [name3] ... + +This command retrieves the profile identifier (and some additional data) from a Stockpile server: + + $ stockpile get-id dotStart + +When unspecified, the names will be resolved at the current time. You may, however, also specify a +custom time to resolve a name at: + + $ stockpile get-id --time=2016-05-02T15:04:05Z07:00 dotStart + +In addition, you may also retrieve the profile identifiers of multiple names at once: + + $ stockpile get-id dotStart MiniDigger + +Note that the current time will always be substituted when multiple names are passed. + +Available command specific flags: + +` +} + +func (c *IdCommand) SetFlags(f *flag.FlagSet) { + c.ClientCommand.SetFlags(f) + f.StringVar(&c.flagTimestamp, "time", "now", "defines the time at which this name should be resolved (for instance \""+time.RFC3339+"\"; may also be \"now\" for the current time or \"zero\" for the initial account name)") +} + +func (c *IdCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + client, err := c.createClient() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to establish a connection to server \"%s\": %s\n", c.flagServerAddress, err) + return 1 + } + + if f.NArg() == 0 { + fmt.Fprintf(os.Stderr, "Illegal command invocation: display-name is required\n") + return 1 + } + + if f.NArg() == 1 { + var timestamp time.Time + if c.flagTimestamp == "now" { + timestamp = time.Now() + } else if c.flagTimestamp == "0" { + timestamp = time.Unix(0, 0) + } else { + timestamp, err = time.Parse(time.RFC3339, c.flagTimestamp) + if err != nil { + fmt.Fprintf(os.Stderr, "Illegal request time \"%s\": %s\n", c.flagTimestamp, err) + return 1 + } + } + + profileService := rpc.NewProfileServiceClient(client) + res, err := profileService.GetId(ctx, &rpc.GetIdRequest{ + Name: f.Arg(0), + Timestamp: timestamp.Unix(), + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Command execution has failed: %s\n", err) + return 1 + } + + if !res.IsPopulated() { + fmt.Fprintf(os.Stderr, "No such profile\n") + return 1 + } + + profile, err := rpc.ProfileIdFromRpc(res) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to convert profile: %s", err) + return 1 + } + writeTable(os.Stdout, *profile) + return 0 + } + + profileService := rpc.NewProfileServiceClient(client) + res, err := profileService.BulkGetId(ctx, &rpc.BulkIdRequest{ + Names: f.Args(), + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Command execution has failed: %s\n", err) + return 1 + } + + if !res.IsPopulated() { + fmt.Fprintf(os.Stderr, "No such profile") + return 1 + } + + profileIds, err := rpc.ProfileIdsFromRpcArray(res.Ids) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to decode one or more profile ids: %s", err) + return 1 + } + + writeTable( + os.Stdout, + struct { + Ids []*mojang.ProfileId + }{ + profileIds, + }, + ) + return 0 +} diff --git a/stockpile/command/profile.go b/stockpile/command/profile.go new file mode 100644 index 0000000..c6ed31d --- /dev/null +++ b/stockpile/command/profile.go @@ -0,0 +1,98 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 command + +import ( + "flag" + "fmt" + "os" + + "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/dotStart/Stockpile/stockpile/server/rpc" + "github.com/google/subcommands" + "golang.org/x/net/context" +) + +type ProfileCommand struct { + ClientCommand +} + +func (*ProfileCommand) Name() string { + return "get-profile" +} + +func (*ProfileCommand) Synopsis() string { + return "queries a user's profile from a Stockpile server" +} + +func (*ProfileCommand) Usage() string { + return `Usage: stockpile profile [options] + +This command retrieves a profile from a Stockpile server: + + $ stockpile get-profile d71a5dac-4e71-443b-8158-4389c269e44d + +Available command specific flags: + +` +} + +func (c *ProfileCommand) SetFlags(f *flag.FlagSet) { + c.ClientCommand.SetFlags(f) +} + +func (c *ProfileCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + client, err := c.createClient() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to establish a connection to server \"%s\": %s\n", c.flagServerAddress, err) + return 1 + } + + if f.NArg() != 1 { + fmt.Fprintf(os.Stderr, "Illegal command invocation: id is required\n") + return 1 + } + + id, err := mojang.ParseId(f.Arg(0)) + if err != nil { + fmt.Fprintf(os.Stderr, "Illegal profile id: %s", id) + return 1 + } + + profileService := rpc.NewProfileServiceClient(client) + res, err := profileService.GetProfile(ctx, &rpc.IdRequest{ + Id: id.String(), + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Command execution has failed: %s\n", err) + return 1 + } + + if !res.IsPopulated() { + fmt.Fprintf(os.Stderr, "No such profile") + return 1 + } + + profile, err := rpc.ProfileFromRpc(res) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to decode profile: %s", err) + return 1 + } + + writeTable(os.Stdout, *profile) + return 0 +} From 9362d4c5c1ce678f625fdb1b8d615357ac370af9 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 17:50:14 +0200 Subject: [PATCH 048/142] Split the storage interface definition and its default implementation into separate parts. --- stockpile/plugin/storage-mem.go | 203 ++++++++++++++++++++++++++++++++ stockpile/plugin/storage.go | 190 ------------------------------ stockpile/plugin/utility.go | 30 +++++ 3 files changed, 233 insertions(+), 190 deletions(-) create mode 100644 stockpile/plugin/storage-mem.go create mode 100644 stockpile/plugin/utility.go diff --git a/stockpile/plugin/storage-mem.go b/stockpile/plugin/storage-mem.go new file mode 100644 index 0000000..1a596cd --- /dev/null +++ b/stockpile/plugin/storage-mem.go @@ -0,0 +1,203 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 plugin + +import ( + "strings" + "time" + + "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/dotStart/Stockpile/stockpile/server" + "github.com/google/uuid" + "github.com/op/go-logging" +) + +type MemoryStorageBackend struct { + cfg *server.Config + logger *logging.Logger + + profileId map[string][]expirationWrapper + nameHistory map[uuid.UUID]*expirationWrapper + profile map[uuid.UUID]*expirationWrapper + + blacklist *expirationWrapper +} + +// creates a new memory based storage backend +func NewMemoryStorageBackend(cfg *server.Config) *MemoryStorageBackend { + return &MemoryStorageBackend{ + cfg: cfg, + logger: logging.MustGetLogger("memdb"), + + profileId: make(map[string][]expirationWrapper), + nameHistory: make(map[uuid.UUID]*expirationWrapper), + profile: make(map[uuid.UUID]*expirationWrapper), + } +} + +func (m *MemoryStorageBackend) GetProfileId(name string, at time.Time) (*mojang.ProfileId, error) { + m.clearExpiredEntries() + + name = strings.ToLower(name) + m.logger.Debugf("Checking profile associations for \"%s\" at time %s", name, at) + mappings := m.profileId[name] + if mappings == nil { + m.logger.Debugf("No associations for \"%s\"", name) + return nil, nil + } + + for _, exp := range mappings { + association := exp.content.(*mojang.ProfileId) + if association.IsValid(at) { + m.logger.Debugf("Association to profile %s matches", association.Id) + return association, nil + } else { + m.logger.Debugf("Association to profile %s is invalid", association.Id) + } + } + + return nil, nil +} + +func (m *MemoryStorageBackend) PutProfileId(profileId *mojang.ProfileId) error { + m.clearExpiredEntries() + + name := strings.ToLower(profileId.Name) + m.logger.Debugf("Updating association for name \"%s\" to profile %s at time %s (valid until %s)", profileId.Name, profileId.Id, profileId.LastSeenAt, profileId.ValidUntil) + mappings := m.profileId[name] + + if mappings == nil { + mappings = make([]expirationWrapper, 1) + } + + mappings = append(mappings, expirationWrapper{ + content: profileId, + createdAt: time.Now(), + }) + m.profileId[name] = mappings + + return nil +} + +func (m *MemoryStorageBackend) GetNameHistory(id uuid.UUID) (*mojang.NameChangeHistory, error) { + m.clearExpiredEntries() + + exp := m.nameHistory[id] + if exp == nil { + return nil, nil + } + + return exp.content.(*mojang.NameChangeHistory), nil +} + +func (m *MemoryStorageBackend) PutNameHistory(id uuid.UUID, history *mojang.NameChangeHistory) error { + m.clearExpiredEntries() + + m.logger.Debugf("Storing history for profile %s (consisting of %d elements)", id.String(), len(history.History)) + m.nameHistory[id] = &expirationWrapper{ + content: history, + createdAt: time.Now(), + } + + return nil +} + +func (m *MemoryStorageBackend) GetProfile(id uuid.UUID) (*mojang.Profile, error) { + m.clearExpiredEntries() + + exp := m.profile[id] + if exp == nil { + return nil, nil + } + + return exp.content.(*mojang.Profile), nil +} + +func (m *MemoryStorageBackend) PutProfile(profile *mojang.Profile) error { + m.clearExpiredEntries() + + m.logger.Debugf("Storing profile %s", profile.Id) + m.profile[profile.Id] = &expirationWrapper{ + content: profile, + createdAt: time.Now(), + } + + return nil +} + +// clears all expired entries from the database +func (m *MemoryStorageBackend) clearExpiredEntries() { // TODO: run on a timer instead? + m.logger.Debug("Purging expired data") + + deletedProfileIds := 0 + deletedNameHistories := 0 + deletedProfiles := 0 + deletedBlacklists := 0 + + for profileId, mappings := range m.profileId { + for i, exp := range mappings { + if !exp.isValid(m.cfg.Ttl.Name) { + deletedProfileIds++ + + if len(mappings) == 1 { + delete(m.profileId, profileId) + continue + } + + mappings = append(mappings[:i], mappings[i+1:]...) + continue + } + } + } + + for key, history := range m.nameHistory { + if !history.isValid(m.cfg.Ttl.NameHistory) { + deletedNameHistories++ + delete(m.nameHistory, key) + } + } + + for key, profile := range m.profile { + if !profile.isValid(m.cfg.Ttl.Profile) { + deletedProfiles++ + delete(m.profile, key) + } + } + + if m.blacklist != nil && m.blacklist.isValid(m.cfg.Ttl.Blacklist) { + deletedBlacklists = 1 + m.blacklist = nil + } + + m.logger.Debugf("Removed %d profile Ids, %d name histories, %d profiles and %d blacklists from memory", deletedProfileIds, deletedNameHistories, deletedProfiles, deletedBlacklists) +} + +func (m *MemoryStorageBackend) GetBlacklist() (*mojang.Blacklist, error) { + if m.blacklist == nil { + return nil, nil + } + + return m.blacklist.content.(*mojang.Blacklist), nil +} + +func (m *MemoryStorageBackend) PutBlacklist(blacklist *mojang.Blacklist) error { + m.blacklist = &expirationWrapper{ + content: blacklist, + createdAt: time.Now(), + } + return nil +} diff --git a/stockpile/plugin/storage.go b/stockpile/plugin/storage.go index 60053b8..ad8b96a 100644 --- a/stockpile/plugin/storage.go +++ b/stockpile/plugin/storage.go @@ -17,13 +17,10 @@ package plugin import ( - "strings" "time" "github.com/dotStart/Stockpile/stockpile/mojang" - "github.com/dotStart/Stockpile/stockpile/server" "github.com/google/uuid" - "github.com/op/go-logging" ) // provides an abstraction layer between the application and a storage backend @@ -40,190 +37,3 @@ type StorageBackend interface { GetBlacklist() (*mojang.Blacklist, error) PutBlacklist(blacklist *mojang.Blacklist) error } - -type MemoryStorageBackend struct { - cfg *server.Config - logger *logging.Logger - - profileId map[string][]expirationWrapper - nameHistory map[uuid.UUID]*expirationWrapper - profile map[uuid.UUID]*expirationWrapper - - blacklist *expirationWrapper -} - -// creates a new memory based storage backend -func NewMemoryStorageBackend(cfg *server.Config) *MemoryStorageBackend { - return &MemoryStorageBackend{ - cfg: cfg, - logger: logging.MustGetLogger("memdb"), - - profileId: make(map[string][]expirationWrapper), - nameHistory: make(map[uuid.UUID]*expirationWrapper), - profile: make(map[uuid.UUID]*expirationWrapper), - } -} - -func (m *MemoryStorageBackend) GetProfileId(name string, at time.Time) (*mojang.ProfileId, error) { - m.clearExpiredEntries() - - name = strings.ToLower(name) - m.logger.Debugf("Checking profile associations for \"%s\" at time %s", name, at) - mappings := m.profileId[name] - if mappings == nil { - m.logger.Debugf("No associations for \"%s\"", name) - return nil, nil - } - - for _, exp := range mappings { - association := exp.content.(*mojang.ProfileId) - if association.IsValid(at) { - m.logger.Debugf("Association to profile %s matches", association.Id) - return association, nil - } else { - m.logger.Debugf("Association to profile %s is invalid", association.Id) - } - } - - return nil, nil -} - -func (m *MemoryStorageBackend) PutProfileId(profileId *mojang.ProfileId) error { - m.clearExpiredEntries() - - name := strings.ToLower(profileId.Name) - m.logger.Debugf("Updating association for name \"%s\" to profile %s at time %s (valid until %s)", profileId.Name, profileId.Id, profileId.LastSeenAt, profileId.ValidUntil) - mappings := m.profileId[name] - - if mappings == nil { - mappings = make([]expirationWrapper, 1) - } - - mappings = append(mappings, expirationWrapper{ - content: profileId, - createdAt: time.Now(), - }) - m.profileId[name] = mappings - - return nil -} - -func (m *MemoryStorageBackend) GetNameHistory(id uuid.UUID) (*mojang.NameChangeHistory, error) { - m.clearExpiredEntries() - - exp := m.nameHistory[id] - if exp == nil { - return nil, nil - } - - return exp.content.(*mojang.NameChangeHistory), nil -} - -func (m *MemoryStorageBackend) PutNameHistory(id uuid.UUID, history *mojang.NameChangeHistory) error { - m.clearExpiredEntries() - - m.logger.Debugf("Storing history for profile %s (consisting of %d elements)", id.String(), len(history.History)) - m.nameHistory[id] = &expirationWrapper{ - content: history, - createdAt: time.Now(), - } - - return nil -} - -func (m *MemoryStorageBackend) GetProfile(id uuid.UUID) (*mojang.Profile, error) { - m.clearExpiredEntries() - - exp := m.profile[id] - if exp == nil { - return nil, nil - } - - return exp.content.(*mojang.Profile), nil -} - -func (m *MemoryStorageBackend) PutProfile(profile *mojang.Profile) error { - m.clearExpiredEntries() - - m.logger.Debugf("Storing profile %s", profile.Id) - m.profile[profile.Id] = &expirationWrapper{ - content: profile, - createdAt: time.Now(), - } - - return nil -} - -// clears all expired entries from the database -func (m *MemoryStorageBackend) clearExpiredEntries() { // TODO: run on a timer instead? - m.logger.Debug("Purging expired data") - - deletedProfileIds := 0 - deletedNameHistories := 0 - deletedProfiles := 0 - deletedBlacklists := 0 - - for profileId, mappings := range m.profileId { - for i, exp := range mappings { - if !exp.isValid(m.cfg.Ttl.Name) { - deletedProfileIds++ - - if len(mappings) == 1 { - delete(m.profileId, profileId) - continue - } - - mappings = append(mappings[:i], mappings[i+1:]...) - continue - } - } - } - - for key, history := range m.nameHistory { - if !history.isValid(m.cfg.Ttl.NameHistory) { - deletedNameHistories++ - delete(m.nameHistory, key) - } - } - - for key, profile := range m.profile { - if !profile.isValid(m.cfg.Ttl.Profile) { - deletedProfiles++ - delete(m.profile, key) - } - } - - if m.blacklist != nil && m.blacklist.isValid(m.cfg.Ttl.Blacklist) { - deletedBlacklists = 1 - m.blacklist = nil - } - - m.logger.Debugf("Removed %d profile Ids, %d name histories, %d profiles and %d blacklists from memory", deletedProfileIds, deletedNameHistories, deletedProfiles, deletedBlacklists) -} - -func (m *MemoryStorageBackend) GetBlacklist() (*mojang.Blacklist, error) { - if m.blacklist == nil { - return nil, nil - } - - return m.blacklist.content.(*mojang.Blacklist), nil -} - -func (m *MemoryStorageBackend) PutBlacklist(blacklist *mojang.Blacklist) error { - m.blacklist = &expirationWrapper{ - content: blacklist, - createdAt: time.Now(), - } - return nil -} - -// provides a primitive wrapper object which handles expiration in the memory storage backend -type expirationWrapper struct { - content interface{} - createdAt time.Time -} - -// evaluates whether a particular entry is still considered valid -func (w *expirationWrapper) isValid(ttl time.Duration) bool { - return time.Since(w.createdAt) <= ttl -} diff --git a/stockpile/plugin/utility.go b/stockpile/plugin/utility.go new file mode 100644 index 0000000..5ec4ce4 --- /dev/null +++ b/stockpile/plugin/utility.go @@ -0,0 +1,30 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 plugin + +import "time" + +// provides a primitive wrapper object which handles expiration in the memory storage backend +type expirationWrapper struct { + content interface{} + createdAt time.Time +} + +// evaluates whether a particular entry is still considered valid +func (w *expirationWrapper) isValid(ttl time.Duration) bool { + return time.Since(w.createdAt) <= ttl +} From dd7afe8d6254266897eca29fb1ee45bd460659b1 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 17:50:41 +0200 Subject: [PATCH 049/142] Created an application entry point. --- stockpile/main.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 stockpile/main.go diff --git a/stockpile/main.go b/stockpile/main.go new file mode 100644 index 0000000..ee25957 --- /dev/null +++ b/stockpile/main.go @@ -0,0 +1,42 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 main + +import ( + "context" + "flag" + "os" + + "github.com/dotStart/Stockpile/stockpile/command" + "github.com/google/subcommands" +) + +// Application Entry Point +func main() { + subcommands.Register(subcommands.HelpCommand(), "") + subcommands.Register(subcommands.CommandsCommand(), "") + subcommands.Register(&command.ServerCommand{}, "") + + subcommands.Register(&command.IdCommand{}, "Client") + subcommands.Register(&command.HistoryCommand{}, "Client") + subcommands.Register(&command.ProfileCommand{}, "Client") + subcommands.Register(&command.BlacklistCommand{}, "Client") + + flag.Parse() + ctx := context.Background() + os.Exit(int(subcommands.Execute(ctx))) +} From b6e57164c64373b81f214edbc612e94e778574ba Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 17:56:59 +0200 Subject: [PATCH 050/142] Added a basic readme to reduce confusion (for now). --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..036a90b --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +Stockpile +========= + +Lightweight and protocol aware API proxy for the Mojang APIs. + +**Looking for Stockpile 1.0?** Its source code may still be accessed [here](https://github.com/dotStart/Stockpile/tree/v1.0.0-SNAPSHOT) + +Key Features +------------ + +* Customizable (broad configuration options and plugin support) +* gRPC based (clients may be generated for most popular languages) +* Completely Open Source + +License +------- + +``` +Copyright [year] [name] <[email]> +and other copyright owners as documented in the project's IP log. + +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. +``` From 1e1ed65b18d776c998f64ad5c635bdd726ca3fa8 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 18:08:07 +0200 Subject: [PATCH 051/142] Added hcl representations of the default and dev configs respectively. --- docs/default-config.hcl | 16 ++++++++++++++++ docs/dev-config.hcl | 14 ++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 docs/default-config.hcl create mode 100644 docs/dev-config.hcl diff --git a/docs/default-config.hcl b/docs/default-config.hcl new file mode 100644 index 0000000..8b83d94 --- /dev/null +++ b/docs/default-config.hcl @@ -0,0 +1,16 @@ +bind-address = "0.0.0.0:36623" +legacy-api = false + +// no storage backend in default - required for actual operation +// example: +// storage "mem" { +// param1 = false +// param2 = "hostname:port" +// } + +ttl { + name = "888h" + name-history = "180h" + profile = "168h" + blacklist = "168h" +} diff --git a/docs/dev-config.hcl b/docs/dev-config.hcl new file mode 100644 index 0000000..9d72fe7 --- /dev/null +++ b/docs/dev-config.hcl @@ -0,0 +1,14 @@ +bind-address = "127.0.0.1:36623" +legacy-api = true + +storage "mem" { + param1 = false + param2 = "hostname:port" +} + +ttl { + name = "888h" + name-history = "180h" + profile = "168h" + blacklist = "168h" +} From 57c18fb6de946f863ab2065bd76720c71008a500 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 18:09:12 +0200 Subject: [PATCH 052/142] Removed the example parameters from the mem storage backend as it does not accept any parameters. --- docs/dev-config.hcl | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/dev-config.hcl b/docs/dev-config.hcl index 9d72fe7..e662f98 100644 --- a/docs/dev-config.hcl +++ b/docs/dev-config.hcl @@ -2,8 +2,6 @@ bind-address = "127.0.0.1:36623" legacy-api = true storage "mem" { - param1 = false - param2 = "hostname:port" } ttl { From 159d57f7ceb7094d41cdaf71aac00a541e43c6fd Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 18:16:43 +0200 Subject: [PATCH 053/142] Added some basic configuration validation. --- stockpile/server/config.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/stockpile/server/config.go b/stockpile/server/config.go index dc48349..bd77a28 100644 --- a/stockpile/server/config.go +++ b/stockpile/server/config.go @@ -17,6 +17,7 @@ package server import ( + "errors" "fmt" "io/ioutil" "os" @@ -118,7 +119,7 @@ func LoadConfig(path string) (*Config, error) { return nil, err } base.Merge(cfg) - return base, nil + return base, base.validate() } // Loads an entire directory of configuration files @@ -143,7 +144,7 @@ func LoadConfigDirectory(path string) (*Config, error) { } } - return base, nil + return base, base.validate() } // Loads a single configuration file @@ -245,3 +246,23 @@ func (c *TtlConfig) Parse() error { func (c *Config) Parse() error { return c.Ttl.Parse() } + +func (c *Config) validate() error { + if c.BindAddress == "" { + return errors.New("illegal bind address") + } + + if c.Storage == nil { + return errors.New("missing storage backend configuration") + } + + if c.Storage.Type == "" { + return errors.New("illegal storage backend type") + } + + if c.Ttl == nil { + return errors.New("missing ttl configuration") + } + + return nil +} From 4f9ecf5278547f81f60446bf71929a1a43d70997 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 20:07:00 +0200 Subject: [PATCH 054/142] Fixed an issue where elements would be skipped when a mapping expires. --- stockpile/plugin/storage-mem.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/stockpile/plugin/storage-mem.go b/stockpile/plugin/storage-mem.go index 1a596cd..66bcdbd 100644 --- a/stockpile/plugin/storage-mem.go +++ b/stockpile/plugin/storage-mem.go @@ -149,7 +149,9 @@ func (m *MemoryStorageBackend) clearExpiredEntries() { // TODO: run on a timer i deletedBlacklists := 0 for profileId, mappings := range m.profileId { - for i, exp := range mappings { + for i := 0; i < len(mappings); { + exp := mappings[i] + if !exp.isValid(m.cfg.Ttl.Name) { deletedProfileIds++ @@ -161,6 +163,8 @@ func (m *MemoryStorageBackend) clearExpiredEntries() { // TODO: run on a timer i mappings = append(mappings[:i], mappings[i+1:]...) continue } + + i++ } } From a1a1ff06d3ab1d00cb1ca2e5941fe3aff00268ee Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 20:36:51 +0200 Subject: [PATCH 055/142] Introduced methods for purging cached entries from storage backends. --- stockpile/plugin/storage-mem.go | 88 ++++++++++++++++++++++++++------- stockpile/plugin/storage.go | 4 ++ 2 files changed, 75 insertions(+), 17 deletions(-) diff --git a/stockpile/plugin/storage-mem.go b/stockpile/plugin/storage-mem.go index 66bcdbd..e65cc8f 100644 --- a/stockpile/plugin/storage-mem.go +++ b/stockpile/plugin/storage-mem.go @@ -93,6 +93,38 @@ func (m *MemoryStorageBackend) PutProfileId(profileId *mojang.ProfileId) error { return nil } +func (m *MemoryStorageBackend) PurgeProfileId(name string, at time.Time) error { + m.clearExpiredEntries() + + m.logger.Debugf("Purging profile associations for \"%s\" at time %s", name, at) + mappings := m.profileId[name] + if mappings == nil { + m.logger.Debugf("No associations for \"%s\"", name) + return nil + } + + if at.Unix() == -1 { + delete(m.profileId, name) + return nil + } + + for i := 0; i < len(mappings); { + exp := mappings[i] + association := exp.content.(*mojang.ProfileId) + + if association.IsValid(at) { + m.logger.Debugf("Purging association to profile %s", association.Id) + mappings = append(mappings[:i], mappings[:i+1]...) + continue + } + + i++ + } + + m.profileId[name] = mappings + return nil +} + func (m *MemoryStorageBackend) GetNameHistory(id uuid.UUID) (*mojang.NameChangeHistory, error) { m.clearExpiredEntries() @@ -107,7 +139,7 @@ func (m *MemoryStorageBackend) GetNameHistory(id uuid.UUID) (*mojang.NameChangeH func (m *MemoryStorageBackend) PutNameHistory(id uuid.UUID, history *mojang.NameChangeHistory) error { m.clearExpiredEntries() - m.logger.Debugf("Storing history for profile %s (consisting of %d elements)", id.String(), len(history.History)) + m.logger.Debugf("Storing history for profile %s (consisting of %d elements)", id, len(history.History)) m.nameHistory[id] = &expirationWrapper{ content: history, createdAt: time.Now(), @@ -116,6 +148,14 @@ func (m *MemoryStorageBackend) PutNameHistory(id uuid.UUID, history *mojang.Name return nil } +func (m *MemoryStorageBackend) PurgeNameHistory(id uuid.UUID) error { + m.clearExpiredEntries() + + m.logger.Debugf("Purging history for profile %s", id) + delete(m.nameHistory, id) + return nil +} + func (m *MemoryStorageBackend) GetProfile(id uuid.UUID) (*mojang.Profile, error) { m.clearExpiredEntries() @@ -139,6 +179,36 @@ func (m *MemoryStorageBackend) PutProfile(profile *mojang.Profile) error { return nil } +func (m *MemoryStorageBackend) PurgeProfile(id uuid.UUID) error { + m.clearExpiredEntries() + + m.logger.Debugf("Purging profile %s", id) + delete(m.profile, id) + return nil +} + +func (m *MemoryStorageBackend) GetBlacklist() (*mojang.Blacklist, error) { + if m.blacklist == nil { + return nil, nil + } + + return m.blacklist.content.(*mojang.Blacklist), nil +} + +func (m *MemoryStorageBackend) PutBlacklist(blacklist *mojang.Blacklist) error { + m.blacklist = &expirationWrapper{ + content: blacklist, + createdAt: time.Now(), + } + return nil +} + +func (m *MemoryStorageBackend) PurgeBlacklist() error { + m.logger.Debugf("Purging blacklist") + m.blacklist = nil + return nil +} + // clears all expired entries from the database func (m *MemoryStorageBackend) clearExpiredEntries() { // TODO: run on a timer instead? m.logger.Debug("Purging expired data") @@ -189,19 +259,3 @@ func (m *MemoryStorageBackend) clearExpiredEntries() { // TODO: run on a timer i m.logger.Debugf("Removed %d profile Ids, %d name histories, %d profiles and %d blacklists from memory", deletedProfileIds, deletedNameHistories, deletedProfiles, deletedBlacklists) } - -func (m *MemoryStorageBackend) GetBlacklist() (*mojang.Blacklist, error) { - if m.blacklist == nil { - return nil, nil - } - - return m.blacklist.content.(*mojang.Blacklist), nil -} - -func (m *MemoryStorageBackend) PutBlacklist(blacklist *mojang.Blacklist) error { - m.blacklist = &expirationWrapper{ - content: blacklist, - createdAt: time.Now(), - } - return nil -} diff --git a/stockpile/plugin/storage.go b/stockpile/plugin/storage.go index ad8b96a..1e4e660 100644 --- a/stockpile/plugin/storage.go +++ b/stockpile/plugin/storage.go @@ -28,12 +28,16 @@ type StorageBackend interface { // Profile Data GetProfileId(name string, at time.Time) (*mojang.ProfileId, error) PutProfileId(profileId *mojang.ProfileId) error + PurgeProfileId(name string, at time.Time) error GetNameHistory(id uuid.UUID) (*mojang.NameChangeHistory, error) PutNameHistory(id uuid.UUID, history *mojang.NameChangeHistory) error + PurgeNameHistory(id uuid.UUID) error GetProfile(id uuid.UUID) (*mojang.Profile, error) PutProfile(profile *mojang.Profile) error + PurgeProfile(id uuid.UUID) error // Server Data GetBlacklist() (*mojang.Blacklist, error) PutBlacklist(blacklist *mojang.Blacklist) error + PurgeBlacklist() error } From 3cf8a15be02c98f4149585404157a2bc0c22d8c6 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 30 Jun 2018 21:44:27 +0200 Subject: [PATCH 056/142] Added an explicit close function for storage backends to permit graceful cleanup of storage backend resources. --- stockpile/command/server.go | 2 +- stockpile/plugin/storage-mem.go | 4 ++++ stockpile/plugin/storage.go | 2 ++ stockpile/server/service/main.go | 6 ++++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/stockpile/command/server.go b/stockpile/command/server.go index d4ae92d..48c7688 100644 --- a/stockpile/command/server.go +++ b/stockpile/command/server.go @@ -141,7 +141,7 @@ func (c *ServerCommand) Execute(_ context.Context, f *flag.FlagSet, _ ...interfa log.Fatalf("Failed to initialize grpc server: %s", err) } go rpcServer.Listen(mux.Match(rpcPolicy)) - defer rpcServer.Stop() + defer rpcServer.Destroy() log.Info("Enabled grpc server") mux.Serve() diff --git a/stockpile/plugin/storage-mem.go b/stockpile/plugin/storage-mem.go index e65cc8f..5a91444 100644 --- a/stockpile/plugin/storage-mem.go +++ b/stockpile/plugin/storage-mem.go @@ -49,6 +49,10 @@ func NewMemoryStorageBackend(cfg *server.Config) *MemoryStorageBackend { } } +func (m *MemoryStorageBackend) Close() error { + return nil +} + func (m *MemoryStorageBackend) GetProfileId(name string, at time.Time) (*mojang.ProfileId, error) { m.clearExpiredEntries() diff --git a/stockpile/plugin/storage.go b/stockpile/plugin/storage.go index 1e4e660..d3b73e0 100644 --- a/stockpile/plugin/storage.go +++ b/stockpile/plugin/storage.go @@ -25,6 +25,8 @@ import ( // provides an abstraction layer between the application and a storage backend type StorageBackend interface { + Close() error + // Profile Data GetProfileId(name string, at time.Time) (*mojang.ProfileId, error) PutProfileId(profileId *mojang.ProfileId) error diff --git a/stockpile/server/service/main.go b/stockpile/server/service/main.go index f175554..f02964d 100644 --- a/stockpile/server/service/main.go +++ b/stockpile/server/service/main.go @@ -90,3 +90,9 @@ func (s *Server) Listen(listener net.Listener) { func (s *Server) Stop() { s.srv.Stop() } + +// destroys the server instance permanently +func (s *Server) Destroy() { + s.srv.Stop() + s.storage.Close() +} From d99938ad719e356863dde55d964c6581b0cd5ab9 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 1 Jul 2018 18:23:43 +0200 Subject: [PATCH 057/142] Added a method to evaluate whether two profileIds overlap (e.g. are part of the same assignment). --- stockpile/mojang/name.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/stockpile/mojang/name.go b/stockpile/mojang/name.go index 24ab3fd..6f80cf9 100644 --- a/stockpile/mojang/name.go +++ b/stockpile/mojang/name.go @@ -200,6 +200,16 @@ func (p *ProfileId) IsValid(at time.Time) bool { return !p.FirstSeenAt.After(at) && p.ValidUntil.After(at) } +// evaluates whether two profileIds theoretically overlap +// +// two profiles are considered to overlap if their validity period overlaps at any point in time or +// if their assignments are equal while less than 30 days have passed (e.g. it is impossible for +// another user to claim and unclaim the name in the meantime due to the grace period) +// TODO: I have no clue how and whether Mojang handles theft of names with content creators +func (p *ProfileId) IsOverlappingWith(other *ProfileId) bool { + return p.IsValid(other.FirstSeenAt) || p.IsValid(other.ValidUntil) || (p.Id == other.Id && p.ValidUntil.Add(NameChangeRateLimitPeriod).After(p.FirstSeenAt)) +} + // encapsulates a name history type NameChangeHistory struct { History []*NameChange From 89e7f7c62c046d0174452e905a969fe092c95c5a Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 1 Jul 2018 18:40:59 +0200 Subject: [PATCH 058/142] Improved cache updating for in-memory data stores. --- stockpile/plugin/storage-mem.go | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/stockpile/plugin/storage-mem.go b/stockpile/plugin/storage-mem.go index 5a91444..dd60fd5 100644 --- a/stockpile/plugin/storage-mem.go +++ b/stockpile/plugin/storage-mem.go @@ -83,17 +83,27 @@ func (m *MemoryStorageBackend) PutProfileId(profileId *mojang.ProfileId) error { name := strings.ToLower(profileId.Name) m.logger.Debugf("Updating association for name \"%s\" to profile %s at time %s (valid until %s)", profileId.Name, profileId.Id, profileId.LastSeenAt, profileId.ValidUntil) mappings := m.profileId[name] + found := false + if mappings != nil { + for _, e := range mappings { + entry := e.content.(*mojang.ProfileId) + if entry.IsOverlappingWith(profileId) { + entry.UpdateExpiration(profileId.LastSeenAt) + found = true + } + } + } else { + mappings = make([]expirationWrapper, 0) + } - if mappings == nil { - mappings = make([]expirationWrapper, 1) + if !found { + mappings = append(mappings, expirationWrapper{ + content: profileId, + createdAt: time.Now(), + }) } - mappings = append(mappings, expirationWrapper{ - content: profileId, - createdAt: time.Now(), - }) m.profileId[name] = mappings - return nil } From 6446836f2fdca1e32a27aa08fbfc67e3cf245e95 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 1 Jul 2018 18:49:25 +0200 Subject: [PATCH 059/142] Fixed an issue which prevents parsing of configuration files when the ttl is omitted. --- stockpile/server/config.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stockpile/server/config.go b/stockpile/server/config.go index bd77a28..0759c49 100644 --- a/stockpile/server/config.go +++ b/stockpile/server/config.go @@ -244,7 +244,10 @@ func (c *TtlConfig) Parse() error { } func (c *Config) Parse() error { - return c.Ttl.Parse() + if c.Ttl != nil { + return c.Ttl.Parse() + } + return nil } func (c *Config) validate() error { From 649594e55e657656e65085b2cd7ac2fc78d34e9c Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 1 Jul 2018 19:21:24 +0200 Subject: [PATCH 060/142] Corrected an issue where the additional empty lines at the end of blacklist responses were actually interpreted. --- stockpile/mojang/server.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/stockpile/mojang/server.go b/stockpile/mojang/server.go index f2fc686..c952418 100644 --- a/stockpile/mojang/server.go +++ b/stockpile/mojang/server.go @@ -81,14 +81,19 @@ func (a *MojangAPI) Login(displayName string, serverId string, ip string) (*Prof // creates a new blacklist from the supplied list of hashes func NewBlacklist(hashes []string) (*Blacklist, error) { - for _, hash := range hashes { + for i := 0; i < len(hashes); { + hash := hashes[i] + if hash == "" { + hashes = append(hashes[:i], hashes[i+1:]...) continue // skip extras } if len(hash) != 40 { return nil, fmt.Errorf("encountered malformed hash \"%s\": must be exactly 40 characters long", hash) } + + i++ } return &Blacklist{ From fdfe04fa488e4b089c9521faf9301ef6d6aa3d31 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 1 Jul 2018 19:22:02 +0200 Subject: [PATCH 061/142] Fixed issues with the (de-)serialization of entities in some storage implementations. --- stockpile/mojang/name.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stockpile/mojang/name.go b/stockpile/mojang/name.go index 6f80cf9..bda3376 100644 --- a/stockpile/mojang/name.go +++ b/stockpile/mojang/name.go @@ -81,7 +81,7 @@ func SerializeProfileIdArray(profileIds []*ProfileId) ([]byte, error) { ValidUntil: profileId.ValidUntil.Unix(), } } - return json.Marshal(enc) + return json.Marshal(&enc) } func (p *ProfileId) Deserialize(enc []byte) error { @@ -106,7 +106,7 @@ func (p *ProfileId) Deserialize(enc []byte) error { func DeserializeProfileIdArray(enc []byte) ([]*ProfileId, error) { parsed := make([]serializableProfileId, 0) - err := json.Unmarshal(enc, parsed) + err := json.Unmarshal(enc, &parsed) if err != nil { return nil, err } @@ -281,7 +281,7 @@ func SerializeNameChangeArray(history []*NameChange) ([]byte, error) { } } - return json.Marshal(enc) + return json.Marshal(&enc) } func (p *NameChange) Deserialize(enc []byte) error { @@ -299,7 +299,7 @@ func (p *NameChange) Deserialize(enc []byte) error { func DeserializeNameChangeArray(enc []byte) ([]*NameChange, error) { parsed := make([]serializableNameChange, 0) - err := json.Unmarshal(enc, parsed) + err := json.Unmarshal(enc, &parsed) if err != nil { return nil, err } From 33cc6bda964c3ad1d563000b3a97c9f86cd52c30 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 1 Jul 2018 19:23:39 +0200 Subject: [PATCH 062/142] Fixed an issue where accessing deserialized blacklist instances would cause the server to crash. --- stockpile/mojang/server.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/stockpile/mojang/server.go b/stockpile/mojang/server.go index c952418..c41de51 100644 --- a/stockpile/mojang/server.go +++ b/stockpile/mojang/server.go @@ -30,12 +30,11 @@ import ( "golang.org/x/text/encoding/charmap" ) +var blacklistLogger = logging.MustGetLogger("blacklist") var ipPattern, _ = regexp.Compile("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$") // represents a server blacklist type Blacklist struct { - logger *logging.Logger - Hashes []string } @@ -97,7 +96,6 @@ func NewBlacklist(hashes []string) (*Blacklist, error) { } return &Blacklist{ - logger: logging.MustGetLogger("blacklist"), Hashes: hashes, }, nil } @@ -131,7 +129,7 @@ func (b *Blacklist) Contains(hash string) bool { // evaluates whether the passed hostname has been blacklisted func (b *Blacklist) IsBlacklisted(addr string) (bool, error) { hash, err := calculateHash(addr) - b.logger.Debugf("Checking address %s (hash: %s) against blacklist", addr, hash) + blacklistLogger.Debugf("Checking address %s (hash: %s) against blacklist", addr, hash) if err != nil { return false, err } @@ -152,7 +150,7 @@ func (b *Blacklist) IsBlacklistedIP(ip string) (bool, error) { for i := 3; i > 0; i-- { addr := strings.Join(elements[:i], ".") + ".*" hash, err := calculateHash(addr) - b.logger.Debugf("Checking IP %s (hash: %s) against blacklist", addr, hash) + blacklistLogger.Debugf("Checking IP %s (hash: %s) against blacklist", addr, hash) if err != nil { return false, err } @@ -173,7 +171,7 @@ func (b *Blacklist) IsBlacklistedDomain(hostname string) (bool, error) { for i := 1; i < length; i++ { addr := "*." + strings.Join(elements[i:], ".") hash, err := calculateHash(addr) - b.logger.Debugf("Checking domain %s (hash: %s) against blacklist", addr, hash) + blacklistLogger.Debugf("Checking domain %s (hash: %s) against blacklist", addr, hash) if err != nil { return false, err } From b15e08ba691cf480f2103e5fc33b50e63f0e2586 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 1 Jul 2018 19:25:18 +0200 Subject: [PATCH 063/142] Replaced the types of various configuration properties with pointers to mark them optional within hcl. --- stockpile/command/server.go | 6 +++--- stockpile/server/config.go | 24 ++++++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/stockpile/command/server.go b/stockpile/command/server.go index 48c7688..9f854f8 100644 --- a/stockpile/command/server.go +++ b/stockpile/command/server.go @@ -103,7 +103,7 @@ func (c *ServerCommand) Execute(_ context.Context, f *flag.FlagSet, _ ...interfa logging.SetBackend(backend) fmt.Printf("==> Stockpile Configuration\n\n") - fmt.Printf(" Server Address: %s\n", cfg.BindAddress) + fmt.Printf(" Server Address: %s\n", *cfg.BindAddress) fmt.Printf(" Version: %s\n", metadata.VersionFull()) fmt.Printf(" Commit Hash: %s\n", metadata.CommitHash()) fmt.Printf(" Log Level: %s\n", c.flagLogLevel) @@ -123,7 +123,7 @@ func (c *ServerCommand) Execute(_ context.Context, f *flag.FlagSet, _ ...interfa } // initialize the shared network listener and mux first so we can detect potential binding errors early on - listener, err := net.Listen("tcp", cfg.BindAddress) + listener, err := net.Listen("tcp", *cfg.BindAddress) if err != nil { log.Fatalf("Failed to listen on %s (TCP): %s", cfg.BindAddress, err) } @@ -133,7 +133,7 @@ func (c *ServerCommand) Execute(_ context.Context, f *flag.FlagSet, _ ...interfa // initialize the RPC server at all times (only differ between mux policies depending on whether the legacy API or UI // is enabled) rpcPolicy := cmux.Any() - if cfg.EnableLegacyApi { + if *cfg.EnableLegacyApi { rpcPolicy = cmux.HTTP2HeaderField("content-type", "application/grpc") } rpcServer, err := service.NewServer(cfg) diff --git a/stockpile/server/config.go b/stockpile/server/config.go index 0759c49..e08454b 100644 --- a/stockpile/server/config.go +++ b/stockpile/server/config.go @@ -39,8 +39,8 @@ const DefaultPort = 36623 // Represents a server configuration (typically parsed from one or more HCL files) type Config struct { - BindAddress string `hcl:"bind-address,attr"` - EnableLegacyApi bool `hcl:"legacy-api,attr"` + BindAddress *string `hcl:"bind-address,attr"` + EnableLegacyApi *bool `hcl:"legacy-api,attr"` Storage *StorageConfig `hcl:"storage,block"` Ttl *TtlConfig `hcl:"ttl,block"` } @@ -71,9 +71,12 @@ func EmptyConfig() *Config { } func DefaultConfig() *Config { + addr := fmt.Sprintf("%s:%d", "127.0.0.1", DefaultPort) + legacyEnabled := false + cfg := &Config{ - BindAddress: fmt.Sprintf("%s:%d", DefaultAddress, DefaultPort), - EnableLegacyApi: false, + BindAddress: &addr, + EnableLegacyApi: &legacyEnabled, Storage: &StorageConfig{ Type: "mem", }, @@ -96,9 +99,10 @@ func DefaultConfig() *Config { } func DevelopmentConfig() *Config { + legacyEnabled := true + return DefaultConfig().Merge(&Config{ - BindAddress: fmt.Sprintf("%s:%d", "127.0.0.1", DefaultPort), - EnableLegacyApi: true, + EnableLegacyApi: &legacyEnabled, }) } @@ -168,12 +172,12 @@ func LoadConfigFile(path string) (*Config, error) { // Merges two configuration instances into one func (c *Config) Merge(other *Config) *Config { - if other.BindAddress != "" { + if other.BindAddress != nil { c.BindAddress = other.BindAddress } - if other.EnableLegacyApi { - c.EnableLegacyApi = true + if other.EnableLegacyApi != nil { + c.EnableLegacyApi = other.EnableLegacyApi } if c.Storage == nil { @@ -251,7 +255,7 @@ func (c *Config) Parse() error { } func (c *Config) validate() error { - if c.BindAddress == "" { + if c.BindAddress == nil { return errors.New("illegal bind address") } From dcfa54707cb00d91736cca63e33ca2b970b1231c Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 1 Jul 2018 19:25:42 +0200 Subject: [PATCH 064/142] Created a method for calculating hashes for storage purposes. --- stockpile/plugin/utility.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/stockpile/plugin/utility.go b/stockpile/plugin/utility.go index 5ec4ce4..00dac8a 100644 --- a/stockpile/plugin/utility.go +++ b/stockpile/plugin/utility.go @@ -16,7 +16,12 @@ */ package plugin -import "time" +import ( + "crypto/sha1" + "encoding/hex" + "strings" + "time" +) // provides a primitive wrapper object which handles expiration in the memory storage backend type expirationWrapper struct { @@ -28,3 +33,9 @@ type expirationWrapper struct { func (w *expirationWrapper) isValid(ttl time.Duration) bool { return time.Since(w.createdAt) <= ttl } + +// calculates a unified cache for a given input value (typically for primitive built-in cache types) +func calculateHash(input string) string { + enc := sha1.Sum([]byte(strings.ToLower(input))) + return hex.EncodeToString(enc[:]) +} From 903a483ae3b2e86aad157d19f4bbbdaa7ea89dd2 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 1 Jul 2018 19:26:00 +0200 Subject: [PATCH 065/142] Created a file based storage implementation. --- stockpile/plugin/storage-file.go | 324 +++++++++++++++++++++++++++++++ stockpile/server/service/main.go | 7 + 2 files changed, 331 insertions(+) create mode 100644 stockpile/plugin/storage-file.go diff --git a/stockpile/plugin/storage-file.go b/stockpile/plugin/storage-file.go new file mode 100644 index 0000000..1f6eff9 --- /dev/null +++ b/stockpile/plugin/storage-file.go @@ -0,0 +1,324 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 plugin + +import ( + "errors" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/dotStart/Stockpile/stockpile/server" + "github.com/google/uuid" + "github.com/hashicorp/hcl2/gohcl" + "github.com/op/go-logging" +) + +const lockExpiration = time.Minute * 5 // keep-alive occurs every minute, expiration after 5 minutes +const lockKeepalive = time.Minute + +const dirPerms = 664 // rw-rw-r-- +const filePerms = 664 // rw-rw-r-- + +type FileStorageBackend struct { + cfg *server.Config + fileCfg *FileStorageBackendCfg + logger *logging.Logger + lockPath string + lockTicker *time.Ticker +} + +type FileStorageBackendCfg struct { + Path string `hcl:"path,attr"` +} + +// creates a new file storage backend +func NewFileStorageBackend(cfg *server.Config) (*FileStorageBackend, error) { + fileCfg := &FileStorageBackendCfg{} + gohcl.DecodeBody(cfg.Storage.Parameters, nil, fileCfg) + + lockPath := filepath.Join(fileCfg.Path, "storage.lock") + _, err := os.Stat(fileCfg.Path) + if err != nil { + if !os.IsNotExist(err) { + return nil, err + } + + err = os.MkdirAll(fileCfg.Path, dirPerms) + if err != nil { + return nil, err + } + } else { + stat, err := os.Stat(lockPath) + if err == nil { + if !stat.ModTime().Add(lockExpiration).Before(time.Now()) { + return nil, errors.New("file storage directory is locked by another instance - verify whether another instance is running and delete 'storage.lock' if this issue persists") + } + } + if !os.IsNotExist(err) { + return nil, err + } + } + + err = ioutil.WriteFile(lockPath, []byte{}, filePerms) + if err != nil { + return nil, err + } + + impl := &FileStorageBackend{ + cfg: cfg, + fileCfg: fileCfg, + logger: logging.MustGetLogger("file"), + lockPath: lockPath, + lockTicker: time.NewTicker(lockKeepalive), + } + go impl.updateLock() + return impl, nil +} + +// updates the modification time of the lock file periodically to prevent its automatic expiration +func (f *FileStorageBackend) updateLock() { + for range f.lockTicker.C { + f.logger.Debugf("Updating database lock") + ioutil.WriteFile(f.lockPath, []byte{}, filePerms) + } +} + +// retrieves the data of a previously stored cache entry (given that it exists and is still +// considered valid in accordance with its ttl) +func (f *FileStorageBackend) getCacheEntry(category string, key string, ttl time.Duration) ([]byte, error) { + path := filepath.Join(f.fileCfg.Path, category, strings.ToLower(key)) + + stat, err := os.Stat(path) + if os.IsNotExist(err) { + return nil, nil + } + if err != nil { + return nil, err + } + if ttl != -1 && stat.ModTime().Add(ttl).Before(time.Now()) { + return nil, nil + } + + return ioutil.ReadFile(path) +} + +// creates or updates a cache entry +func (f *FileStorageBackend) putCacheEntry(category string, key string, data []byte) error { + dir := filepath.Join(f.fileCfg.Path, category) + path := filepath.Join(dir, strings.ToLower(key)) + + _, err := os.Stat(dir) + if err != nil { + if !os.IsNotExist(err) { + return err + } + + os.MkdirAll(dir, dirPerms) + } + + return ioutil.WriteFile(path, data, filePerms) +} + +// purges a cache entry (if it exists) +func (f *FileStorageBackend) purgeCacheEntry(category string, key string) error { + path := filepath.Join(f.fileCfg.Path, category, strings.ToLower(key)) + + _, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + return nil + } + + return err + } + + return os.Remove(path) +} + +func (f *FileStorageBackend) GetProfileId(name string, at time.Time) (*mojang.ProfileId, error) { + enc, err := f.getCacheEntry("name", calculateHash(name), f.cfg.Ttl.Name) + if err != nil { + return nil, err + } + + ids, err := mojang.DeserializeProfileIdArray(enc) + if err != nil { + return nil, err + } + + for _, id := range ids { + if id.IsValid(at) { + return id, nil + } + } + + return nil, nil +} + +func (f *FileStorageBackend) PutProfileId(profileId *mojang.ProfileId) error { + key := calculateHash(profileId.Name) + enc, err := f.getCacheEntry("name", key, f.cfg.Ttl.Name) + if err != nil { + return err + } + var entries []*mojang.ProfileId + found := false + if enc != nil { + entries, err = mojang.DeserializeProfileIdArray(enc) + if err != nil { + return err + } + + // if there is an overlap (e.g. the passed profile was encountered while during the local + // validity period or was assigned to the same profile within the 30 day limitation period) + for _, e := range entries { + if e.IsOverlappingWith(profileId) { + e.UpdateExpiration(profileId.LastSeenAt) + found = true + break + } + } + } else { + entries = make([]*mojang.ProfileId, 0) + found = false + } + + if !found { + entries = append(entries, profileId) + } + + enc, err = mojang.SerializeProfileIdArray(entries) + if err != nil { + return err + } + return f.putCacheEntry("name", key, enc) +} + +func (f *FileStorageBackend) PurgeProfileId(name string, at time.Time) error { + key := calculateHash(name) + enc, err := f.getCacheEntry("name", key, f.cfg.Ttl.NameHistory) + if err != nil { + return err + } + + entries, err := mojang.DeserializeProfileIdArray(enc) + if err != nil { + return err + } + + for i := 0; i < len(entries); { + e := entries[i] + if e.IsValid(at) { + entries = append(entries[:i], entries[i+1:]...) + break + } + i++ + } + + if len(entries) == 0 { + f.purgeCacheEntry("name", key) + return nil + } + + enc, err = mojang.SerializeProfileIdArray(entries) + if err != nil { + return err + } + return f.putCacheEntry("name", key, enc) +} + +func (f *FileStorageBackend) GetNameHistory(id uuid.UUID) (*mojang.NameChangeHistory, error) { + enc, err := f.getCacheEntry("history", id.String(), f.cfg.Ttl.NameHistory) + if err != nil { + return nil, err + } + + history := &mojang.NameChangeHistory{} + err = history.Deserialize(enc) + return history, err +} + +func (f *FileStorageBackend) PutNameHistory(id uuid.UUID, history *mojang.NameChangeHistory) error { + enc, err := history.Serialize() + if err != nil { + return err + } + + return f.putCacheEntry("history", id.String(), enc) +} + +func (f *FileStorageBackend) PurgeNameHistory(id uuid.UUID) error { + return f.purgeCacheEntry("history", id.String()) +} + +func (f *FileStorageBackend) GetProfile(id uuid.UUID) (*mojang.Profile, error) { + enc, err := f.getCacheEntry("profile", id.String(), f.cfg.Ttl.Profile) + if err != nil { + return nil, err + } + + profile := &mojang.Profile{} + err = profile.Deserialize(enc) + return profile, err +} + +func (f *FileStorageBackend) PutProfile(profile *mojang.Profile) error { + enc, err := profile.Serialize() + if err != nil { + return err + } + + return f.putCacheEntry("profile", profile.Id.String(), enc) +} + +func (f *FileStorageBackend) PurgeProfile(id uuid.UUID) error { + return f.purgeCacheEntry("profile", id.String()) +} + +// Server Data +func (f *FileStorageBackend) GetBlacklist() (*mojang.Blacklist, error) { + enc, err := f.getCacheEntry("misc", "blacklist", f.cfg.Ttl.Blacklist) + if err != nil { + return nil, err + } + + blacklist := &mojang.Blacklist{} + err = blacklist.Deserialize(enc) + return blacklist, err +} + +func (f *FileStorageBackend) PutBlacklist(blacklist *mojang.Blacklist) error { + enc, err := blacklist.Serialize() + if err != nil { + return err + } + + return f.putCacheEntry("misc", "blacklist", enc) +} + +func (f *FileStorageBackend) PurgeBlacklist() error { + return f.purgeCacheEntry("misc", "blacklist") +} + +func (f *FileStorageBackend) Close() error { + f.lockTicker.Stop() + return os.Remove(f.lockPath) +} diff --git a/stockpile/server/service/main.go b/stockpile/server/service/main.go index f02964d..f285a96 100644 --- a/stockpile/server/service/main.go +++ b/stockpile/server/service/main.go @@ -49,6 +49,13 @@ func NewServer(config *server.Config) (*Server, error) { if config.Storage.Type == "mem" { logger.Warning("Using in-memory storage") storage = plugin.NewMemoryStorageBackend(config) + } else if config.Storage.Type == "file" { + logger.Info("Using file storage") + var err error + storage, err = plugin.NewFileStorageBackend(config) + if err != nil { + return nil, err + } } else { plg, err := plugin.Open(path.Join("plugins", config.Storage.Type)) if err != nil { From 03ef8b7d371f675ba1690bbbe9c86c261a6f26be Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sun, 1 Jul 2018 19:36:32 +0200 Subject: [PATCH 066/142] Created an example production config. --- docs/production-config.hcl | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/production-config.hcl diff --git a/docs/production-config.hcl b/docs/production-config.hcl new file mode 100644 index 0000000..ae6c7fa --- /dev/null +++ b/docs/production-config.hcl @@ -0,0 +1,7 @@ +bind-address = "127.0.0.1:36623" + +// file storage is technically suited for small production deployments, however, a proper storage +// server like redis is recommended for higher volumes +storage "file" { + path = "data" +} From 8ca48b5e8bb3650bdcc46ef156bd784f8c1689b6 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Mon, 2 Jul 2018 03:11:38 +0200 Subject: [PATCH 067/142] Added ignores for Vagrant. --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 8d69599..9f0ca08 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,12 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +### Vagrant ### +# General +.vagrant/ + +# Log files (if you are creating logs in debug mode, uncomment this) +# *.logs # End of https://www.gitignore.io/api/go From 9acc357ee19b35ad091651e6978e7fd0c23ada5d Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Mon, 2 Jul 2018 03:12:00 +0200 Subject: [PATCH 068/142] Fixed the locations of our version symbols. --- stockpile/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stockpile/Makefile b/stockpile/Makefile index b4470bb..3844659 100644 --- a/stockpile/Makefile +++ b/stockpile/Makefile @@ -1,4 +1,4 @@ -LDFLAGS :=-ldflags "-X github.com/dotStart/Stockpile/metadata.brand=${APPLICATION_BRAND} -X github.com/dotStart/Stockpile/metadata.version=${APPLICATION_VERSION} -X github.com/dotStart/Stockpile/metadata.commitHash=${APPLICATION_COMMIT_HASH} -X \"github.com/dotStart/Stockpile/metadata.timestampRaw=${APPLICATION_TIMESTAMP}\"" +LDFLAGS :=-ldflags "-X github.com/dotStart/Stockpile/metadata.brand=${APPLICATION_BRAND} -X github.com/dotStart/Stockpile/stockpile/metadata.version=${APPLICATION_VERSION} -X github.com/dotStart/Stockpile/stockpile/metadata.commitHash=${APPLICATION_COMMIT_HASH} -X \"github.com/dotStart/Stockpile/stockpile/metadata.timestampRaw=${APPLICATION_TIMESTAMP}\"" build: ../build/mac32/stockpile ../build/mac64/stockpile \ ../build/linux32/stockpile ../build/linux64/stockpile ../build/linuxarm/stockpile \ From cd6f47d2fee3b69e968232683390d42049ed163e Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Mon, 2 Jul 2018 03:12:51 +0200 Subject: [PATCH 069/142] Created an incredibly basic Vagrant image for development purposes. --- Vagrantfile | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Vagrantfile diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..f61c6ea --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,16 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# --------------------------------------------------- # +# DO NOT DEPLOY THIS IMAGE IN PRODUCTION ENVIRONMENTS # +# --------------------------------------------------- # +# This image is designed specifically for development # +# purposes (specifically to develop and test plugins # +# in environments where they are not natively # +# supported (specifically Windows machines) # +# --------------------------------------------------- # +Vagrant.configure("2") do |config| + config.vm.box = "hashicorp/precise64" + config.vm.network "forwarded_port", guest: 36623, host: 36623, host_ip: "127.0.0.1" + config.vm.synced_folder "build/linux64", "/opt/stockpile" +end From 87fe17bd6e7f0de12e7c1f3d100e583071defbb8 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Mon, 2 Jul 2018 03:28:06 +0200 Subject: [PATCH 070/142] Significantly simplified the main application makefile. --- stockpile/Makefile | 37 ++++++++----------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/stockpile/Makefile b/stockpile/Makefile index 3844659..15fae70 100644 --- a/stockpile/Makefile +++ b/stockpile/Makefile @@ -1,35 +1,14 @@ +PLATFORMS := darwin/386 darwin/amd64 linux/386 linux/amd64 linux/arm windows/386 windows/amd64 LDFLAGS :=-ldflags "-X github.com/dotStart/Stockpile/metadata.brand=${APPLICATION_BRAND} -X github.com/dotStart/Stockpile/stockpile/metadata.version=${APPLICATION_VERSION} -X github.com/dotStart/Stockpile/stockpile/metadata.commitHash=${APPLICATION_COMMIT_HASH} -X \"github.com/dotStart/Stockpile/stockpile/metadata.timestampRaw=${APPLICATION_TIMESTAMP}\"" -build: ../build/mac32/stockpile ../build/mac64/stockpile \ - ../build/linux32/stockpile ../build/linux64/stockpile ../build/linuxarm/stockpile \ - ../build/win32/stockpile.exe ../build/win64/stockpile.exe +# magical formula: +temp = $(subst /, ,$@) +os = $(word 1, $(temp)) +arch = $(word 2, $(temp)) -../build/mac32/stockpile: - @export GOOS="darwin"; export GOARCH="386"; go build -v ${LDFLAGS} -o ../build/mac32/stockpile - @echo "" +build: $(PLATFORMS) -../build/mac64/stockpile: - @export GOOS="darwin"; export GOARCH="amd64"; go build -v ${LDFLAGS} -o ../build/mac64/stockpile - @echo "" - -../build/linux32/stockpile: - @export GOOS="linux"; export GOARCH="386"; go build -v ${LDFLAGS} -o ../build/linux32/stockpile - @echo "" - -../build/linux64/stockpile: - @export GOOS="linux"; export GOARCH="amd64"; go build -v ${LDFLAGS} -o ../build/linux64/stockpile - @echo "" - -../build/linuxarm/stockpile: - @export GOOS="linux"; export GOARCH="arm"; go build -v ${LDFLAGS} -o ../build/linuxarm/stockpile - @echo "" - -../build/win32/stockpile.exe: - @export GOOS="windows"; export GOARCH="386"; go build -v ${LDFLAGS} -o ../build/win32/stockpile.exe - @echo "" - -../build/win64/stockpile.exe: - @export GOOS="windows"; export GOARCH="amd64"; go build -v ${LDFLAGS} -o ../build/win64/stockpile.exe - @echo "" +$(PLATFORMS): + @export GOOS=$(os); export GOARCH=$(arch); go build -v ${LDFLAGS} -o ../build/$(os)-$(arch)/stockpile .PHONY: build From 9be01add7d56122f0006f87feb79758fc74f65b4 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Mon, 2 Jul 2018 16:06:46 +0200 Subject: [PATCH 071/142] A bunch of changes to support building on Linux. This step is necessary as plugins do not seem to cross compile correctly and thus need to be omitted from the build process unless we are building on 64-Bit Linux. --- Vagrantfile | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index f61c6ea..ddb2c8d 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -12,5 +12,33 @@ Vagrant.configure("2") do |config| config.vm.box = "hashicorp/precise64" config.vm.network "forwarded_port", guest: 36623, host: 36623, host_ip: "127.0.0.1" - config.vm.synced_folder "build/linux64", "/opt/stockpile" + config.vm.synced_folder ".", "/usr/src/go/src/github.com/dotStart/Stockpile" + + config.vm.provision "shell", inline: <<-SHELL + echo "==> Initializing basic build environment" + sudo apt-get update + sudo apt-get install -y curl git make + if [ ! -d "/usr/local/go" ]; then \ + echo "Fetching go SDK ..."; \ + curl --silent -Lo /usr/src/go.tar.gz https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz; \ + echo "Extracting go ..."; \ + tar -C /usr/local -xzf /usr/src/go.tar.gz; \ + fi + export PATH=$PATH:/usr/local/go/bin:/usr/src/go/bin + export GOPATH=/usr/src/go + mkdir -p /usr/src/go/bin + if [ ! -f "/usr/src/go/bin/dep" ]; then \ + echo "==> Building go dep"; \ + go get -d -u github.com/golang/dep; \ + cd $(go env GOPATH)/src/github.com/golang/dep; \ + DEP_LATEST=$(git describe --abbrev=0 --tags); \ + git checkout $DEP_LATEST; \ + go install -ldflags="-X main.version=$DEP_LATEST" ./cmd/dep; \ + git checkout master; \ + fi + echo "==> Building initial binaries" + cd $(go env GOPATH)/src/github.com/dotStart/Stockpile + make + cp -R /usr/src/go/src/github.com/dotStart/Stockpile/build/linux-amd64 /opt/stockpile + SHELL end From 4ec1f3d748b3b476542ba2ae5cab2c95e85c5128 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Mon, 2 Jul 2018 16:06:59 +0200 Subject: [PATCH 072/142] Added extensions to the executables. --- stockpile/Makefile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stockpile/Makefile b/stockpile/Makefile index 15fae70..518045d 100644 --- a/stockpile/Makefile +++ b/stockpile/Makefile @@ -1,14 +1,15 @@ -PLATFORMS := darwin/386 darwin/amd64 linux/386 linux/amd64 linux/arm windows/386 windows/amd64 +PLATFORMS := darwin/386 darwin/amd64 linux/386 linux/amd64 linux/arm windows/386/.exe windows/amd64/.exe LDFLAGS :=-ldflags "-X github.com/dotStart/Stockpile/metadata.brand=${APPLICATION_BRAND} -X github.com/dotStart/Stockpile/stockpile/metadata.version=${APPLICATION_VERSION} -X github.com/dotStart/Stockpile/stockpile/metadata.commitHash=${APPLICATION_COMMIT_HASH} -X \"github.com/dotStart/Stockpile/stockpile/metadata.timestampRaw=${APPLICATION_TIMESTAMP}\"" # magical formula: temp = $(subst /, ,$@) os = $(word 1, $(temp)) arch = $(word 2, $(temp)) +ext = $(word 3, $(temp)) build: $(PLATFORMS) $(PLATFORMS): - @export GOOS=$(os); export GOARCH=$(arch); go build -v ${LDFLAGS} -o ../build/$(os)-$(arch)/stockpile + @export GOOS=$(os); export GOARCH=$(arch); go build -v ${LDFLAGS} -o ../build/$(os)-$(arch)/stockpile$(ext) .PHONY: build From 836c4c55eaf50a004d9966d86780c5444f8c06ba Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Mon, 2 Jul 2018 16:07:40 +0200 Subject: [PATCH 073/142] Corrected the brand variable path. --- stockpile/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stockpile/Makefile b/stockpile/Makefile index 518045d..dca5de9 100644 --- a/stockpile/Makefile +++ b/stockpile/Makefile @@ -1,5 +1,5 @@ PLATFORMS := darwin/386 darwin/amd64 linux/386 linux/amd64 linux/arm windows/386/.exe windows/amd64/.exe -LDFLAGS :=-ldflags "-X github.com/dotStart/Stockpile/metadata.brand=${APPLICATION_BRAND} -X github.com/dotStart/Stockpile/stockpile/metadata.version=${APPLICATION_VERSION} -X github.com/dotStart/Stockpile/stockpile/metadata.commitHash=${APPLICATION_COMMIT_HASH} -X \"github.com/dotStart/Stockpile/stockpile/metadata.timestampRaw=${APPLICATION_TIMESTAMP}\"" +LDFLAGS :=-ldflags "-X github.com/dotStart/Stockpile/stockpile/metadata.brand=${APPLICATION_BRAND} -X github.com/dotStart/Stockpile/stockpile/metadata.version=${APPLICATION_VERSION} -X github.com/dotStart/Stockpile/stockpile/metadata.commitHash=${APPLICATION_COMMIT_HASH} -X \"github.com/dotStart/Stockpile/stockpile/metadata.timestampRaw=${APPLICATION_TIMESTAMP}\"" # magical formula: temp = $(subst /, ,$@) From 0db8297e9a91877e5395298e27887df1b7c03448 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Mon, 2 Jul 2018 16:11:05 +0200 Subject: [PATCH 074/142] Added a version attribute to the plugin metadata struct. --- stockpile/plugin/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/stockpile/plugin/main.go b/stockpile/plugin/main.go index 740334a..761f9ae 100644 --- a/stockpile/plugin/main.go +++ b/stockpile/plugin/main.go @@ -28,6 +28,7 @@ import ( // represents the metadata associated with a plugin implementation type Metadata struct { Name string + Version string Authors []string Website string } From 055b0d219f1318a9a56aa18b0ad9d0a7bdf2e47f Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Mon, 2 Jul 2018 18:06:21 +0200 Subject: [PATCH 075/142] Improved logging for plugin loading. Otherwise it would be incredibly hard for us to figure out which third party plugins have been enabled when crashes occur. --- stockpile/plugin/main.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/stockpile/plugin/main.go b/stockpile/plugin/main.go index 761f9ae..629bd80 100644 --- a/stockpile/plugin/main.go +++ b/stockpile/plugin/main.go @@ -23,8 +23,11 @@ import ( "strings" "github.com/dotStart/Stockpile/stockpile/server" + "github.com/op/go-logging" ) +var logger = logging.MustGetLogger("plugin") + // represents the metadata associated with a plugin implementation type Metadata struct { Name string @@ -61,14 +64,25 @@ func Open(path string) (*StockpilePlugin, error) { return nil, err } + capabilities := make([]string, 0) var storageFactory func(backend *server.Config) (StorageBackend, error) storageFactorySymbol, err := handle.Lookup("CreateStorageBackend") if err != nil { storageFactory = nil } else { storageFactory = storageFactorySymbol.(func(backend *server.Config) (StorageBackend, error)) + capabilities = append(capabilities, "storage") } + metadata := metaSymbol.(func() (Metadata))() + logger.Info("==> Loaded plugin: %s", metadata.Name) + logger.Info("") + logger.Info(" Version: %s", metadata.Version) + logger.Info(" Website: %s", metadata.Website) + logger.Info(" Author(s): %s", strings.Join(metadata.Authors, ", ")) + logger.Info(" Capabilities: %s", strings.Join(capabilities, ", ")) + logger.Info("") + return &StockpilePlugin{ handle: handle, storageFactory: storageFactory, From 912be4784fb65b86c5eb4033c6fd928bd5c87d25 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Mon, 2 Jul 2018 20:04:51 +0200 Subject: [PATCH 076/142] Redesigned the plugin system to permit multiple loaded instances at once in a more generic manner. --- docs/default-config.hcl | 1 + docs/dev-config.hcl | 1 + docs/production-config.hcl | 1 + stockpile/plugin/factory.go | 25 +++++++ stockpile/plugin/instance.go | 117 +++++++++++++++++++++++++++++++ stockpile/plugin/main.go | 102 --------------------------- stockpile/plugin/manager.go | 96 +++++++++++++++++++++++++ stockpile/plugin/metadata.go | 25 +++++++ stockpile/plugin/storage-file.go | 2 +- stockpile/plugin/storage-mem.go | 4 +- stockpile/plugin/utility.go | 4 ++ stockpile/server/config.go | 13 +++- stockpile/server/service/main.go | 39 ++++------- 13 files changed, 297 insertions(+), 133 deletions(-) create mode 100644 stockpile/plugin/factory.go create mode 100644 stockpile/plugin/instance.go delete mode 100644 stockpile/plugin/main.go create mode 100644 stockpile/plugin/manager.go create mode 100644 stockpile/plugin/metadata.go diff --git a/docs/default-config.hcl b/docs/default-config.hcl index 8b83d94..c9ffea9 100644 --- a/docs/default-config.hcl +++ b/docs/default-config.hcl @@ -1,3 +1,4 @@ +plugin-dir = "plugins" bind-address = "0.0.0.0:36623" legacy-api = false diff --git a/docs/dev-config.hcl b/docs/dev-config.hcl index e662f98..797a438 100644 --- a/docs/dev-config.hcl +++ b/docs/dev-config.hcl @@ -1,3 +1,4 @@ +plugin-dir = "plugins" bind-address = "127.0.0.1:36623" legacy-api = true diff --git a/docs/production-config.hcl b/docs/production-config.hcl index ae6c7fa..5b6c96d 100644 --- a/docs/production-config.hcl +++ b/docs/production-config.hcl @@ -1,3 +1,4 @@ +plugin-dir = "plugins" bind-address = "127.0.0.1:36623" // file storage is technically suited for small production deployments, however, a proper storage diff --git a/stockpile/plugin/factory.go b/stockpile/plugin/factory.go new file mode 100644 index 0000000..0268a66 --- /dev/null +++ b/stockpile/plugin/factory.go @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 plugin + +import "github.com/dotStart/Stockpile/stockpile/server" + +// provides an initializer function for plugins +type Initializer = func(*Context) error + +// provides a factory for storage backend instances +type StorageBackendFactory = func(*server.Config) (StorageBackend, error) diff --git a/stockpile/plugin/instance.go b/stockpile/plugin/instance.go new file mode 100644 index 0000000..233abb0 --- /dev/null +++ b/stockpile/plugin/instance.go @@ -0,0 +1,117 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 plugin + +import ( + "fmt" + "plugin" + "reflect" + "runtime" +) + +// represents a loaded plugin instance +type Plugin struct { + handle *plugin.Plugin + + Metadata Metadata + Context *Context +} + +// loads an arbitrary plugin from the specified location +func Load(path string) (*Plugin, error) { + // at the moment the go plugin architecture isn't particularly consistent so we won't be able to + // load anything unless we're on Linux or Mac OS + if !PluginsAvailable { + return nil, fmt.Errorf("plugins are not supported on platform %s", runtime.GOOS) + } + + handle, err := plugin.Open(path) + if err != nil { + return nil, fmt.Errorf("cannot open plugin \"%s\": %s", path, err) + } + + metadataHandle, err := handle.Lookup("Metadata") + if err != nil { + return nil, fmt.Errorf("plugin \"%s\" does not expose its metadata: %s", path, err) + } + + metadata, ok := metadataHandle.(*Metadata) + if !ok { + return nil, fmt.Errorf("plugin \"%s\" defines an illegal metadata symbol: expected *Metadata but got %s", path, reflect.TypeOf(metadataHandle)) + } + + initializerHandle, err := handle.Lookup("InitializePlugin") + if err != nil { + return nil, fmt.Errorf("plugin \"%s\" does not expose an initializer: %s", path, err) + } + + initializer, ok := initializerHandle.(Initializer) + if !ok { + return nil, fmt.Errorf("plugin \"%s\" defines an illegal initializer symbol: expected func(*plugin.Context) error but got %s", path, reflect.TypeOf(initializerHandle)) + } + + ctx := &Context{ + storage: make(map[string]StorageBackendFactory), + } + + err = initializer(ctx) + if err != nil { + return nil, fmt.Errorf("plugin \"%s\" failed to initialize: %s", path, err) + } + + return &Plugin{ + handle: handle, + Metadata: *metadata, + Context: ctx, + }, nil +} + +// represents the context associated with a given plugin +// we use this instance to simplify the registration of plugin implementations +type Context struct { + storage map[string]StorageBackendFactory +} + +// merges two context instances with each other +func (c *Context) merge(other *Context) error { + for key, factory := range other.storage { + current := c.storage[key] + if current != nil { + return fmt.Errorf("storage backend with identifier \"%s\" is already defined") + } + + c.storage[key] = factory + } + + return nil +} + +// retrieves the storage backend factory for the specified identifier +func (c *Context) GetStorageBackend(id string) StorageBackendFactory { + return c.storage[id] +} + +// registers a new storage backend with the context +func (c *Context) RegisterStorageBackend(id string, factory StorageBackendFactory) error { + current := c.storage[id] + if current != nil { + return fmt.Errorf("storage backend with id \"%s\" has already been registered", id) + } + + c.storage[id] = factory + return nil +} diff --git a/stockpile/plugin/main.go b/stockpile/plugin/main.go deleted file mode 100644 index 629bd80..0000000 --- a/stockpile/plugin/main.go +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2018 Johannes Donath - * and other copyright owners as documented in the project's IP log. - * - * 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 plugin - -import ( - "errors" - "plugin" - "runtime" - "strings" - - "github.com/dotStart/Stockpile/stockpile/server" - "github.com/op/go-logging" -) - -var logger = logging.MustGetLogger("plugin") - -// represents the metadata associated with a plugin implementation -type Metadata struct { - Name string - Version string - Authors []string - Website string -} - -// represents a loaded plugin -type StockpilePlugin struct { - handle *plugin.Plugin - storageFactory func(backend *server.Config) (StorageBackend, error) - - Meta Metadata -} - -// opens an arbitrary plugin -func Open(path string) (*StockpilePlugin, error) { - if runtime.GOOS == "windows" { - return nil, errors.New("plugins are not supported on windows") - } - - if !strings.HasSuffix(path, ".so") { - path += ".so" - } - - handle, err := plugin.Open(path) - if err != nil { - return nil, err - } - - metaSymbol, err := handle.Lookup("GetMetadata") - if err != nil { - return nil, err - } - - capabilities := make([]string, 0) - var storageFactory func(backend *server.Config) (StorageBackend, error) - storageFactorySymbol, err := handle.Lookup("CreateStorageBackend") - if err != nil { - storageFactory = nil - } else { - storageFactory = storageFactorySymbol.(func(backend *server.Config) (StorageBackend, error)) - capabilities = append(capabilities, "storage") - } - - metadata := metaSymbol.(func() (Metadata))() - logger.Info("==> Loaded plugin: %s", metadata.Name) - logger.Info("") - logger.Info(" Version: %s", metadata.Version) - logger.Info(" Website: %s", metadata.Website) - logger.Info(" Author(s): %s", strings.Join(metadata.Authors, ", ")) - logger.Info(" Capabilities: %s", strings.Join(capabilities, ", ")) - logger.Info("") - - return &StockpilePlugin{ - handle: handle, - storageFactory: storageFactory, - - Meta: metaSymbol.(func() (Metadata))(), - }, nil -} - -// evaluates whether the plugin provides a storage backend implementation -func (p *StockpilePlugin) HasStorageBackendImplementation() bool { - return p.storageFactory != nil -} - -// creates a new storage backend using the plugin's registered provider -func (p *StockpilePlugin) CreateStorageBackend(cfg *server.Config) (StorageBackend, error) { - return p.storageFactory(cfg) -} diff --git a/stockpile/plugin/manager.go b/stockpile/plugin/manager.go new file mode 100644 index 0000000..0df02c2 --- /dev/null +++ b/stockpile/plugin/manager.go @@ -0,0 +1,96 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 plugin + +import ( + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/op/go-logging" +) + +const pluginExt = ".so" + +type Manager struct { + logger *logging.Logger + path string + Context *Context + Plugins []*Plugin +} + +// creates a new empty plugin manager with the given base path +func NewManager(path string) *Manager { + ctx := &Context{ + storage: make(map[string]StorageBackendFactory), + } + ctx.RegisterStorageBackend("mem", NewMemoryStorageBackend) + ctx.RegisterStorageBackend("file", NewFileStorageBackend) + + return &Manager{ + logger: logging.MustGetLogger("plugin"), + path: path, + Context: ctx, + } +} + +// loads all plugins in the plugin directory +func (m *Manager) LoadAll() error { + if !PluginsAvailable { + m.logger.Warningf("Plugins are unavailable on platform %s - Plugin manager startup has been skipped", runtime.GOOS) + return nil + } + + files, err := ioutil.ReadDir(m.path) + if err != nil { + if os.IsNotExist(err) { + m.logger.Warningf("Plugin directory \"%s\" does not exist", m.path) + return nil + } + + return err + } + + for _, file := range files { + if !strings.HasSuffix(file.Name(), pluginExt) { + continue + } + + path := filepath.Join(m.path, file.Name()) + m.Load(path) + } + + return nil +} + +// loads a plugin from the specified path +func (m *Manager) Load(path string) { + plugin, err := Load(path) + if err != nil { + m.logger.Errorf("Failed to load plugin from path \"%s\": %s", path, err) + } else { + m.Plugins = append(m.Plugins, plugin) + err = m.Context.merge(plugin.Context) + if err != nil { + m.logger.Errorf("Failed to register one or more components of plugin \"%s\" v%s (defined by file \"%s\"): %s", plugin.Metadata.Name, plugin.Metadata.Version, path, err) + } + + m.logger.Infof("Loaded plugin \"%s\" v%s by %s (%s) from file %s", plugin.Metadata.Name, plugin.Metadata.Version, strings.Join(plugin.Metadata.Authors, ", "), plugin.Metadata.Website, path) + } +} diff --git a/stockpile/plugin/metadata.go b/stockpile/plugin/metadata.go new file mode 100644 index 0000000..e6b4ace --- /dev/null +++ b/stockpile/plugin/metadata.go @@ -0,0 +1,25 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 plugin + +// represents the metadata associated with a plugin implementation +type Metadata struct { + Name string + Version string + Authors []string + Website string +} diff --git a/stockpile/plugin/storage-file.go b/stockpile/plugin/storage-file.go index 1f6eff9..0a37afc 100644 --- a/stockpile/plugin/storage-file.go +++ b/stockpile/plugin/storage-file.go @@ -50,7 +50,7 @@ type FileStorageBackendCfg struct { } // creates a new file storage backend -func NewFileStorageBackend(cfg *server.Config) (*FileStorageBackend, error) { +func NewFileStorageBackend(cfg *server.Config) (StorageBackend, error) { fileCfg := &FileStorageBackendCfg{} gohcl.DecodeBody(cfg.Storage.Parameters, nil, fileCfg) diff --git a/stockpile/plugin/storage-mem.go b/stockpile/plugin/storage-mem.go index dd60fd5..15c743a 100644 --- a/stockpile/plugin/storage-mem.go +++ b/stockpile/plugin/storage-mem.go @@ -38,7 +38,7 @@ type MemoryStorageBackend struct { } // creates a new memory based storage backend -func NewMemoryStorageBackend(cfg *server.Config) *MemoryStorageBackend { +func NewMemoryStorageBackend(cfg *server.Config) (StorageBackend, error) { return &MemoryStorageBackend{ cfg: cfg, logger: logging.MustGetLogger("memdb"), @@ -46,7 +46,7 @@ func NewMemoryStorageBackend(cfg *server.Config) *MemoryStorageBackend { profileId: make(map[string][]expirationWrapper), nameHistory: make(map[uuid.UUID]*expirationWrapper), profile: make(map[uuid.UUID]*expirationWrapper), - } + }, nil } func (m *MemoryStorageBackend) Close() error { diff --git a/stockpile/plugin/utility.go b/stockpile/plugin/utility.go index 00dac8a..98bdcd1 100644 --- a/stockpile/plugin/utility.go +++ b/stockpile/plugin/utility.go @@ -19,10 +19,14 @@ package plugin import ( "crypto/sha1" "encoding/hex" + "runtime" "strings" "time" ) +// defines whether plugins are available on the current platform +const PluginsAvailable = runtime.GOOS == "darwin" || runtime.GOOS == "linux" + // provides a primitive wrapper object which handles expiration in the memory storage backend type expirationWrapper struct { content interface{} diff --git a/stockpile/server/config.go b/stockpile/server/config.go index e08454b..9a0b906 100644 --- a/stockpile/server/config.go +++ b/stockpile/server/config.go @@ -39,6 +39,7 @@ const DefaultPort = 36623 // Represents a server configuration (typically parsed from one or more HCL files) type Config struct { + PluginDir *string `hcl:"plugin-dir"` BindAddress *string `hcl:"bind-address,attr"` EnableLegacyApi *bool `hcl:"legacy-api,attr"` Storage *StorageConfig `hcl:"storage,block"` @@ -71,10 +72,12 @@ func EmptyConfig() *Config { } func DefaultConfig() *Config { + pluginDir := "plugins" addr := fmt.Sprintf("%s:%d", "127.0.0.1", DefaultPort) legacyEnabled := false cfg := &Config{ + PluginDir: &pluginDir, BindAddress: &addr, EnableLegacyApi: &legacyEnabled, Storage: &StorageConfig{ @@ -172,6 +175,10 @@ func LoadConfigFile(path string) (*Config, error) { // Merges two configuration instances into one func (c *Config) Merge(other *Config) *Config { + if other.PluginDir != nil { + c.PluginDir = other.PluginDir + } + if other.BindAddress != nil { c.BindAddress = other.BindAddress } @@ -255,8 +262,12 @@ func (c *Config) Parse() error { } func (c *Config) validate() error { + if c.PluginDir == nil { + return errors.New("missing plugin directory") + } + if c.BindAddress == nil { - return errors.New("illegal bind address") + return errors.New("missing bind address") } if c.Storage == nil { diff --git a/stockpile/server/service/main.go b/stockpile/server/service/main.go index f285a96..328126c 100644 --- a/stockpile/server/service/main.go +++ b/stockpile/server/service/main.go @@ -21,7 +21,6 @@ package service import ( "fmt" "net" - "path" "github.com/dotStart/Stockpile/stockpile/mojang" "github.com/dotStart/Stockpile/stockpile/plugin" @@ -36,6 +35,7 @@ import ( type Server struct { logger *logging.Logger cfg *server.Config + plugin *plugin.Manager storage plugin.StorageBackend srv *grpc.Server @@ -45,38 +45,23 @@ type Server struct { func NewServer(config *server.Config) (*Server, error) { logger := logging.MustGetLogger("rpc") - var storage plugin.StorageBackend - if config.Storage.Type == "mem" { - logger.Warning("Using in-memory storage") - storage = plugin.NewMemoryStorageBackend(config) - } else if config.Storage.Type == "file" { - logger.Info("Using file storage") - var err error - storage, err = plugin.NewFileStorageBackend(config) - if err != nil { - return nil, err - } - } else { - plg, err := plugin.Open(path.Join("plugins", config.Storage.Type)) - if err != nil { - return nil, err - } + plugin := plugin.NewManager(*config.PluginDir) + plugin.LoadAll() - if !plg.HasStorageBackendImplementation() { - return nil, fmt.Errorf("selected plugin \"%s\" does not provide a storage backend implementation", config.Storage.Type) - } - - storage, err = plg.CreateStorageBackend(config) - if err != nil { - return nil, err - } - - logger.Infof("Using database plugin: %s", config.Storage.Type) + storageFactory := plugin.Context.GetStorageBackend(config.Storage.Type) + if storageFactory == nil { + return nil, fmt.Errorf("no such storage backend: %s", config.Storage.Type) + } + storage, err := storageFactory(config) + if err != nil { + return nil, fmt.Errorf("failed to initialize storage backend \"%s\": %s", err) } + logger.Infof("Using database plugin: %s", config.Storage.Type) return &Server{ logger: logger, cfg: config, + plugin: plugin, storage: storage, }, nil } From 11fe48ead0a010e2f4ec56e67002b2244cff5fb1 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Mon, 2 Jul 2018 20:14:06 +0200 Subject: [PATCH 077/142] Reduced the plugin log output slightly. --- stockpile/plugin/manager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stockpile/plugin/manager.go b/stockpile/plugin/manager.go index 0df02c2..b86e4b5 100644 --- a/stockpile/plugin/manager.go +++ b/stockpile/plugin/manager.go @@ -91,6 +91,6 @@ func (m *Manager) Load(path string) { m.logger.Errorf("Failed to register one or more components of plugin \"%s\" v%s (defined by file \"%s\"): %s", plugin.Metadata.Name, plugin.Metadata.Version, path, err) } - m.logger.Infof("Loaded plugin \"%s\" v%s by %s (%s) from file %s", plugin.Metadata.Name, plugin.Metadata.Version, strings.Join(plugin.Metadata.Authors, ", "), plugin.Metadata.Website, path) + m.logger.Infof("Loaded plugin \"%s\" v%s from file %s", plugin.Metadata.Name, plugin.Metadata.Version, path) } } From 97454c0952a98230f672044fffd7d90528a58728 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Tue, 3 Jul 2018 03:41:17 +0200 Subject: [PATCH 078/142] Fixed logging of bind errors. --- stockpile/command/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stockpile/command/server.go b/stockpile/command/server.go index 9f854f8..3a3c582 100644 --- a/stockpile/command/server.go +++ b/stockpile/command/server.go @@ -125,7 +125,7 @@ func (c *ServerCommand) Execute(_ context.Context, f *flag.FlagSet, _ ...interfa // initialize the shared network listener and mux first so we can detect potential binding errors early on listener, err := net.Listen("tcp", *cfg.BindAddress) if err != nil { - log.Fatalf("Failed to listen on %s (TCP): %s", cfg.BindAddress, err) + log.Fatalf("Failed to listen on %s (TCP): %s", *cfg.BindAddress, err) } mux := cmux.New(listener) From 8d5310854559769ef7200abad96488a76b521df3 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Tue, 3 Jul 2018 04:27:07 +0200 Subject: [PATCH 079/142] Upgraded our development Vagrant image to the current Ubuntu LTS and added redis to the image. --- Vagrantfile | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index ddb2c8d..a0f5aaa 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -10,24 +10,18 @@ # supported (specifically Windows machines) # # --------------------------------------------------- # Vagrant.configure("2") do |config| - config.vm.box = "hashicorp/precise64" + config.vm.box = "ubuntu/bionic64" config.vm.network "forwarded_port", guest: 36623, host: 36623, host_ip: "127.0.0.1" - config.vm.synced_folder ".", "/usr/src/go/src/github.com/dotStart/Stockpile" + config.vm.synced_folder ".", "/opt/go/src/github.com/dotStart/Stockpile" config.vm.provision "shell", inline: <<-SHELL echo "==> Initializing basic build environment" sudo apt-get update - sudo apt-get install -y curl git make - if [ ! -d "/usr/local/go" ]; then \ - echo "Fetching go SDK ..."; \ - curl --silent -Lo /usr/src/go.tar.gz https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz; \ - echo "Extracting go ..."; \ - tar -C /usr/local -xzf /usr/src/go.tar.gz; \ - fi - export PATH=$PATH:/usr/local/go/bin:/usr/src/go/bin - export GOPATH=/usr/src/go - mkdir -p /usr/src/go/bin - if [ ! -f "/usr/src/go/bin/dep" ]; then \ + sudo apt-get install -y curl git golang make redis-server + + export GOPATH=/opt/go + export PATH="$PATH:/opt/go/bin" + if [ ! -f "$(go env GOPATH)/bin/dep" ]; then \ echo "==> Building go dep"; \ go get -d -u github.com/golang/dep; \ cd $(go env GOPATH)/src/github.com/golang/dep; \ @@ -36,9 +30,10 @@ Vagrant.configure("2") do |config| go install -ldflags="-X main.version=$DEP_LATEST" ./cmd/dep; \ git checkout master; \ fi + echo "==> Building initial binaries" cd $(go env GOPATH)/src/github.com/dotStart/Stockpile make - cp -R /usr/src/go/src/github.com/dotStart/Stockpile/build/linux-amd64 /opt/stockpile + cp -R $(go env GOPATH)/src/github.com/dotStart/Stockpile/build/linux-amd64 /opt/stockpile SHELL end From c2edb31bbb558d7db10ce826bbdb5aaecb08f21a Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Tue, 3 Jul 2018 04:53:59 +0200 Subject: [PATCH 080/142] Switched to a more generic storage system to simplify the implementation of similar storage systems. --- stockpile/plugin/storage-encoded.go | 231 ++++++++++++++++++++++++++++ stockpile/plugin/storage-file.go | 198 ++---------------------- 2 files changed, 245 insertions(+), 184 deletions(-) create mode 100644 stockpile/plugin/storage-encoded.go diff --git a/stockpile/plugin/storage-encoded.go b/stockpile/plugin/storage-encoded.go new file mode 100644 index 0000000..694c2ad --- /dev/null +++ b/stockpile/plugin/storage-encoded.go @@ -0,0 +1,231 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 plugin + +import ( + "time" + + "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/dotStart/Stockpile/stockpile/server" + "github.com/google/uuid" +) + +// provides a storage backend which provides format agnostic storage of cache entries for all +// entry types +type EncodedStorageBackend struct { + cfg *server.Config + impl EncodedStorageBackendInterface +} + +type EncodedStorageBackendInterface interface { + GetCacheEntry(category string, name string, ttl time.Duration) ([]byte, error) + PutCacheEntry(category string, name string, encoded []byte, ttl time.Duration) error + PurgeCacheEntry(category string, name string) error + + Close() error +} + +func NewEncodedStorageBackend(cfg *server.Config, impl EncodedStorageBackendInterface) *EncodedStorageBackend { + return &EncodedStorageBackend{ + cfg: cfg, + impl: impl, + } +} + +func (f *EncodedStorageBackend) GetProfileId(name string, at time.Time) (*mojang.ProfileId, error) { + enc, err := f.impl.GetCacheEntry("name", calculateHash(name), f.cfg.Ttl.Name) + if err != nil { + return nil, err + } + if enc == nil { + return nil, nil + } + + ids, err := mojang.DeserializeProfileIdArray(enc) + if err != nil { + return nil, err + } + + for _, id := range ids { + if id.IsValid(at) { + return id, nil + } + } + + return nil, nil +} + +func (f *EncodedStorageBackend) PutProfileId(profileId *mojang.ProfileId) error { + key := calculateHash(profileId.Name) + enc, err := f.impl.GetCacheEntry("name", key, f.cfg.Ttl.Name) + if err != nil { + return err + } + var entries []*mojang.ProfileId + found := false + if enc != nil { + entries, err = mojang.DeserializeProfileIdArray(enc) + if err != nil { + return err + } + + // if there is an overlap (e.g. the passed profile was encountered while during the local + // validity period or was assigned to the same profile within the 30 day limitation period) + for _, e := range entries { + if e.IsOverlappingWith(profileId) { + e.UpdateExpiration(profileId.LastSeenAt) + found = true + break + } + } + } else { + entries = make([]*mojang.ProfileId, 0) + found = false + } + + if !found { + entries = append(entries, profileId) + } + + enc, err = mojang.SerializeProfileIdArray(entries) + if err != nil { + return err + } + return f.impl.PutCacheEntry("name", key, enc, f.cfg.Ttl.Name) +} + +func (f *EncodedStorageBackend) PurgeProfileId(name string, at time.Time) error { + key := calculateHash(name) + enc, err := f.impl.GetCacheEntry("name", key, f.cfg.Ttl.Name) + if err != nil { + return err + } + if enc == nil { + return nil + } + + entries, err := mojang.DeserializeProfileIdArray(enc) + if err != nil { + return err + } + + for i := 0; i < len(entries); { + e := entries[i] + if e.IsValid(at) { + entries = append(entries[:i], entries[i+1:]...) + break + } + i++ + } + + if len(entries) == 0 { + f.impl.PurgeCacheEntry("name", key) + return nil + } + + enc, err = mojang.SerializeProfileIdArray(entries) + if err != nil { + return err + } + return f.impl.PutCacheEntry("name", key, enc, f.cfg.Ttl.Name) +} + +func (f *EncodedStorageBackend) GetNameHistory(id uuid.UUID) (*mojang.NameChangeHistory, error) { + enc, err := f.impl.GetCacheEntry("history", id.String(), f.cfg.Ttl.NameHistory) + if err != nil { + return nil, err + } + if enc == nil { + return nil, nil + } + + history := &mojang.NameChangeHistory{} + err = history.Deserialize(enc) + return history, err +} + +func (f *EncodedStorageBackend) PutNameHistory(id uuid.UUID, history *mojang.NameChangeHistory) error { + enc, err := history.Serialize() + if err != nil { + return err + } + + return f.impl.PutCacheEntry("history", id.String(), enc, f.cfg.Ttl.NameHistory) +} + +func (f *EncodedStorageBackend) PurgeNameHistory(id uuid.UUID) error { + return f.impl.PurgeCacheEntry("history", id.String()) +} + +func (f *EncodedStorageBackend) GetProfile(id uuid.UUID) (*mojang.Profile, error) { + enc, err := f.impl.GetCacheEntry("profile", id.String(), f.cfg.Ttl.Profile) + if err != nil { + return nil, err + } + if enc == nil { + return nil, nil + } + + profile := &mojang.Profile{} + err = profile.Deserialize(enc) + return profile, err +} + +func (f *EncodedStorageBackend) PutProfile(profile *mojang.Profile) error { + enc, err := profile.Serialize() + if err != nil { + return err + } + + return f.impl.PutCacheEntry("profile", profile.Id.String(), enc, f.cfg.Ttl.Profile) +} + +func (f *EncodedStorageBackend) PurgeProfile(id uuid.UUID) error { + return f.impl.PurgeCacheEntry("profile", id.String()) +} + +// Server Data +func (f *EncodedStorageBackend) GetBlacklist() (*mojang.Blacklist, error) { + enc, err := f.impl.GetCacheEntry("misc", "blacklist", f.cfg.Ttl.Blacklist) + if err != nil { + return nil, err + } + if enc == nil { + return nil, nil + } + + blacklist := &mojang.Blacklist{} + err = blacklist.Deserialize(enc) + return blacklist, err +} + +func (f *EncodedStorageBackend) PutBlacklist(blacklist *mojang.Blacklist) error { + enc, err := blacklist.Serialize() + if err != nil { + return err + } + + return f.impl.PutCacheEntry("misc", "blacklist", enc, f.cfg.Ttl.Blacklist) +} + +func (f *EncodedStorageBackend) PurgeBlacklist() error { + return f.impl.PurgeCacheEntry("misc", "blacklist") +} + +func (f *EncodedStorageBackend) Close() error { + return f.impl.Close() +} diff --git a/stockpile/plugin/storage-file.go b/stockpile/plugin/storage-file.go index 0a37afc..d43a8b3 100644 --- a/stockpile/plugin/storage-file.go +++ b/stockpile/plugin/storage-file.go @@ -24,9 +24,7 @@ import ( "strings" "time" - "github.com/dotStart/Stockpile/stockpile/mojang" "github.com/dotStart/Stockpile/stockpile/server" - "github.com/google/uuid" "github.com/hashicorp/hcl2/gohcl" "github.com/op/go-logging" ) @@ -37,10 +35,9 @@ const lockKeepalive = time.Minute const dirPerms = 664 // rw-rw-r-- const filePerms = 664 // rw-rw-r-- -type FileStorageBackend struct { - cfg *server.Config - fileCfg *FileStorageBackendCfg +type fileStorageBackendInterface struct { logger *logging.Logger + cfg *FileStorageBackendCfg lockPath string lockTicker *time.Ticker } @@ -82,19 +79,18 @@ func NewFileStorageBackend(cfg *server.Config) (StorageBackend, error) { return nil, err } - impl := &FileStorageBackend{ - cfg: cfg, - fileCfg: fileCfg, + impl := &fileStorageBackendInterface{ logger: logging.MustGetLogger("file"), + cfg: fileCfg, lockPath: lockPath, lockTicker: time.NewTicker(lockKeepalive), } go impl.updateLock() - return impl, nil + return NewEncodedStorageBackend(cfg, impl), nil } // updates the modification time of the lock file periodically to prevent its automatic expiration -func (f *FileStorageBackend) updateLock() { +func (f *fileStorageBackendInterface) updateLock() { for range f.lockTicker.C { f.logger.Debugf("Updating database lock") ioutil.WriteFile(f.lockPath, []byte{}, filePerms) @@ -103,8 +99,8 @@ func (f *FileStorageBackend) updateLock() { // retrieves the data of a previously stored cache entry (given that it exists and is still // considered valid in accordance with its ttl) -func (f *FileStorageBackend) getCacheEntry(category string, key string, ttl time.Duration) ([]byte, error) { - path := filepath.Join(f.fileCfg.Path, category, strings.ToLower(key)) +func (f *fileStorageBackendInterface) GetCacheEntry(category string, key string, ttl time.Duration) ([]byte, error) { + path := filepath.Join(f.cfg.Path, category, strings.ToLower(key)) stat, err := os.Stat(path) if os.IsNotExist(err) { @@ -121,8 +117,8 @@ func (f *FileStorageBackend) getCacheEntry(category string, key string, ttl time } // creates or updates a cache entry -func (f *FileStorageBackend) putCacheEntry(category string, key string, data []byte) error { - dir := filepath.Join(f.fileCfg.Path, category) +func (f *fileStorageBackendInterface) PutCacheEntry(category string, key string, data []byte, ttl time.Duration) error { + dir := filepath.Join(f.cfg.Path, category) path := filepath.Join(dir, strings.ToLower(key)) _, err := os.Stat(dir) @@ -138,8 +134,8 @@ func (f *FileStorageBackend) putCacheEntry(category string, key string, data []b } // purges a cache entry (if it exists) -func (f *FileStorageBackend) purgeCacheEntry(category string, key string) error { - path := filepath.Join(f.fileCfg.Path, category, strings.ToLower(key)) +func (f *fileStorageBackendInterface) PurgeCacheEntry(category string, key string) error { + path := filepath.Join(f.cfg.Path, category, strings.ToLower(key)) _, err := os.Stat(path) if err != nil { @@ -153,172 +149,6 @@ func (f *FileStorageBackend) purgeCacheEntry(category string, key string) error return os.Remove(path) } -func (f *FileStorageBackend) GetProfileId(name string, at time.Time) (*mojang.ProfileId, error) { - enc, err := f.getCacheEntry("name", calculateHash(name), f.cfg.Ttl.Name) - if err != nil { - return nil, err - } - - ids, err := mojang.DeserializeProfileIdArray(enc) - if err != nil { - return nil, err - } - - for _, id := range ids { - if id.IsValid(at) { - return id, nil - } - } - - return nil, nil -} - -func (f *FileStorageBackend) PutProfileId(profileId *mojang.ProfileId) error { - key := calculateHash(profileId.Name) - enc, err := f.getCacheEntry("name", key, f.cfg.Ttl.Name) - if err != nil { - return err - } - var entries []*mojang.ProfileId - found := false - if enc != nil { - entries, err = mojang.DeserializeProfileIdArray(enc) - if err != nil { - return err - } - - // if there is an overlap (e.g. the passed profile was encountered while during the local - // validity period or was assigned to the same profile within the 30 day limitation period) - for _, e := range entries { - if e.IsOverlappingWith(profileId) { - e.UpdateExpiration(profileId.LastSeenAt) - found = true - break - } - } - } else { - entries = make([]*mojang.ProfileId, 0) - found = false - } - - if !found { - entries = append(entries, profileId) - } - - enc, err = mojang.SerializeProfileIdArray(entries) - if err != nil { - return err - } - return f.putCacheEntry("name", key, enc) -} - -func (f *FileStorageBackend) PurgeProfileId(name string, at time.Time) error { - key := calculateHash(name) - enc, err := f.getCacheEntry("name", key, f.cfg.Ttl.NameHistory) - if err != nil { - return err - } - - entries, err := mojang.DeserializeProfileIdArray(enc) - if err != nil { - return err - } - - for i := 0; i < len(entries); { - e := entries[i] - if e.IsValid(at) { - entries = append(entries[:i], entries[i+1:]...) - break - } - i++ - } - - if len(entries) == 0 { - f.purgeCacheEntry("name", key) - return nil - } - - enc, err = mojang.SerializeProfileIdArray(entries) - if err != nil { - return err - } - return f.putCacheEntry("name", key, enc) -} - -func (f *FileStorageBackend) GetNameHistory(id uuid.UUID) (*mojang.NameChangeHistory, error) { - enc, err := f.getCacheEntry("history", id.String(), f.cfg.Ttl.NameHistory) - if err != nil { - return nil, err - } - - history := &mojang.NameChangeHistory{} - err = history.Deserialize(enc) - return history, err -} - -func (f *FileStorageBackend) PutNameHistory(id uuid.UUID, history *mojang.NameChangeHistory) error { - enc, err := history.Serialize() - if err != nil { - return err - } - - return f.putCacheEntry("history", id.String(), enc) -} - -func (f *FileStorageBackend) PurgeNameHistory(id uuid.UUID) error { - return f.purgeCacheEntry("history", id.String()) -} - -func (f *FileStorageBackend) GetProfile(id uuid.UUID) (*mojang.Profile, error) { - enc, err := f.getCacheEntry("profile", id.String(), f.cfg.Ttl.Profile) - if err != nil { - return nil, err - } - - profile := &mojang.Profile{} - err = profile.Deserialize(enc) - return profile, err -} - -func (f *FileStorageBackend) PutProfile(profile *mojang.Profile) error { - enc, err := profile.Serialize() - if err != nil { - return err - } - - return f.putCacheEntry("profile", profile.Id.String(), enc) -} - -func (f *FileStorageBackend) PurgeProfile(id uuid.UUID) error { - return f.purgeCacheEntry("profile", id.String()) -} - -// Server Data -func (f *FileStorageBackend) GetBlacklist() (*mojang.Blacklist, error) { - enc, err := f.getCacheEntry("misc", "blacklist", f.cfg.Ttl.Blacklist) - if err != nil { - return nil, err - } - - blacklist := &mojang.Blacklist{} - err = blacklist.Deserialize(enc) - return blacklist, err -} - -func (f *FileStorageBackend) PutBlacklist(blacklist *mojang.Blacklist) error { - enc, err := blacklist.Serialize() - if err != nil { - return err - } - - return f.putCacheEntry("misc", "blacklist", enc) -} - -func (f *FileStorageBackend) PurgeBlacklist() error { - return f.purgeCacheEntry("misc", "blacklist") -} - -func (f *FileStorageBackend) Close() error { - f.lockTicker.Stop() - return os.Remove(f.lockPath) +func (f *fileStorageBackendInterface) Close() error { + return nil } From c720b54a1e5ec8c590857a2d85cf55a4a7acfd8e Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Tue, 3 Jul 2018 04:54:22 +0200 Subject: [PATCH 081/142] Created a redis storage backend. --- plugins/redis/Makefile | 22 ++++++++++ plugins/redis/main.go | 34 +++++++++++++++ plugins/redis/storage.go | 91 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 plugins/redis/Makefile create mode 100644 plugins/redis/main.go create mode 100644 plugins/redis/storage.go diff --git a/plugins/redis/Makefile b/plugins/redis/Makefile new file mode 100644 index 0000000..bda3235 --- /dev/null +++ b/plugins/redis/Makefile @@ -0,0 +1,22 @@ +PLUGIN_VERSION := "1.0" +LDFLAGS := -ldflags "-X github.com/dotStart/Stockpile/plugins/redis.version=${PLUGIN_VERSION}" + +UNAME := $(shell uname) +ifeq ($(UNAME), Linux) +PLATFORMS := linux/amd64/so +else +PLATFORMS := +endif + +# magical formula: +temp = $(subst /, ,$@) +os = $(word 1, $(temp)) +arch = $(word 2, $(temp)) +ext = $(word 3, $(temp)) + +build: $(PLATFORMS) + +$(PLATFORMS): + @export GOOS=$(os); export GOARCH=$(arch); export CGO_ENABLED=1; go build -v -buildmode=plugin ${LDFLAGS} -o ../../build/$(os)-$(arch)/plugins/redis.$(ext) + +.PHONY: build diff --git a/plugins/redis/main.go b/plugins/redis/main.go new file mode 100644 index 0000000..c97abc0 --- /dev/null +++ b/plugins/redis/main.go @@ -0,0 +1,34 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 main + +import ( + "github.com/dotStart/Stockpile/stockpile/plugin" +) + +var version string +var Metadata = plugin.Metadata{ + Name: "Redis Storage Backend", + Version: version, + Authors: []string{"Johannes \".start\" Donath"}, + Website: "https://dotStart.github.io/Stockpile", +} + +func InitializePlugin(ctx *plugin.Context) error { + ctx.RegisterStorageBackend("redis", NewRedisStorageBackend) + return nil +} diff --git a/plugins/redis/storage.go b/plugins/redis/storage.go new file mode 100644 index 0000000..ccceb9c --- /dev/null +++ b/plugins/redis/storage.go @@ -0,0 +1,91 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 main + +import ( + "fmt" + "time" + + "github.com/dotStart/Stockpile/stockpile/plugin" + "github.com/dotStart/Stockpile/stockpile/server" + "github.com/go-redis/redis" + "github.com/hashicorp/hcl2/gohcl" + "github.com/op/go-logging" +) + +var defaultPassword = "" + +type redisStorageBackendInterface struct { + logger *logging.Logger + cfg *RedisStorageBackendConfig + client *redis.Client +} + +type RedisStorageBackendConfig struct { + Address string `hcl:"address,attr"` + Password *string `hcl:"password,attr"` + DatabaseId int `hcl:"database,attr"` +} + +func NewRedisStorageBackend(cfg *server.Config) (plugin.StorageBackend, error) { + redisCfg := &RedisStorageBackendConfig{} + diag := gohcl.DecodeBody(cfg.Storage.Parameters, nil, redisCfg) + if diag.HasErrors() { + return nil, fmt.Errorf("illegal backend configuration: %s", diag.Error()) + } + + if redisCfg.Password == nil { + redisCfg.Password = &defaultPassword + } + + client := redis.NewClient(&redis.Options{ + Addr: redisCfg.Address, + Password: *redisCfg.Password, + DB: redisCfg.DatabaseId, + }) + + _, err := client.Ping().Result() + if err != nil { + return nil, fmt.Errorf("cannot reach configured redis server: %s", err) + } + + return plugin.NewEncodedStorageBackend(cfg, &redisStorageBackendInterface{ + logger: logging.MustGetLogger("redis"), + cfg: redisCfg, + client: client, + }), nil +} + +func (f *redisStorageBackendInterface) GetCacheEntry(category string, key string, ttl time.Duration) ([]byte, error) { + enc, err := f.client.Get(fmt.Sprintf("%s_%s", category, key)).Bytes() + if err == redis.Nil { + return nil, nil + } + return enc, err +} + +func (f *redisStorageBackendInterface) PutCacheEntry(category string, key string, data []byte, ttl time.Duration) error { + return f.client.Set(fmt.Sprintf("%s_%s", category, key), data, ttl).Err() +} + +func (f *redisStorageBackendInterface) PurgeCacheEntry(category string, key string) error { + return f.client.Del(fmt.Sprintf("%s_%s", category, key)).Err() +} + +func (f *redisStorageBackendInterface) Close() error { + return f.client.Close() +} From f1c3e7a182a36959d35101ab547807a2a6f84914 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Tue, 3 Jul 2018 04:57:21 +0200 Subject: [PATCH 082/142] Created an example redis configuration file. --- docs/redis-config.hcl | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs/redis-config.hcl diff --git a/docs/redis-config.hcl b/docs/redis-config.hcl new file mode 100644 index 0000000..a9f7686 --- /dev/null +++ b/docs/redis-config.hcl @@ -0,0 +1,8 @@ +plugin-dir = "plugins" +bind-address = "0.0.0.0:36623" + +storage "redis" { + address = "localhost:6379" + // password = "admin1234" + database = 0 +} From 97d2f6e162f62f14e9194bc2c710899547ee5dff Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Tue, 3 Jul 2018 05:10:20 +0200 Subject: [PATCH 083/142] Moved all storage related implementations into their own package. --- plugins/redis/storage.go | 6 +-- stockpile/plugin/factory.go | 7 +++- stockpile/plugin/manager.go | 5 ++- stockpile/plugin/utility.go | 21 ---------- stockpile/server/service/main.go | 3 +- stockpile/server/service/profile.go | 6 +-- stockpile/server/service/server.go | 6 +-- .../storage-encoded.go => storage/encoded.go} | 2 +- .../storage-file.go => storage/file.go} | 2 +- .../{plugin/storage.go => storage/main.go} | 2 +- .../{plugin/storage-mem.go => storage/mem.go} | 2 +- stockpile/storage/utility.go | 41 +++++++++++++++++++ 12 files changed, 64 insertions(+), 39 deletions(-) rename stockpile/{plugin/storage-encoded.go => storage/encoded.go} (99%) rename stockpile/{plugin/storage-file.go => storage/file.go} (99%) rename stockpile/{plugin/storage.go => storage/main.go} (98%) rename stockpile/{plugin/storage-mem.go => storage/mem.go} (99%) create mode 100644 stockpile/storage/utility.go diff --git a/plugins/redis/storage.go b/plugins/redis/storage.go index ccceb9c..e42a0b2 100644 --- a/plugins/redis/storage.go +++ b/plugins/redis/storage.go @@ -20,8 +20,8 @@ import ( "fmt" "time" - "github.com/dotStart/Stockpile/stockpile/plugin" "github.com/dotStart/Stockpile/stockpile/server" + "github.com/dotStart/Stockpile/stockpile/storage" "github.com/go-redis/redis" "github.com/hashicorp/hcl2/gohcl" "github.com/op/go-logging" @@ -41,7 +41,7 @@ type RedisStorageBackendConfig struct { DatabaseId int `hcl:"database,attr"` } -func NewRedisStorageBackend(cfg *server.Config) (plugin.StorageBackend, error) { +func NewRedisStorageBackend(cfg *server.Config) (storage.StorageBackend, error) { redisCfg := &RedisStorageBackendConfig{} diag := gohcl.DecodeBody(cfg.Storage.Parameters, nil, redisCfg) if diag.HasErrors() { @@ -63,7 +63,7 @@ func NewRedisStorageBackend(cfg *server.Config) (plugin.StorageBackend, error) { return nil, fmt.Errorf("cannot reach configured redis server: %s", err) } - return plugin.NewEncodedStorageBackend(cfg, &redisStorageBackendInterface{ + return storage.NewEncodedStorageBackend(cfg, &redisStorageBackendInterface{ logger: logging.MustGetLogger("redis"), cfg: redisCfg, client: client, diff --git a/stockpile/plugin/factory.go b/stockpile/plugin/factory.go index 0268a66..ebe7154 100644 --- a/stockpile/plugin/factory.go +++ b/stockpile/plugin/factory.go @@ -16,10 +16,13 @@ */ package plugin -import "github.com/dotStart/Stockpile/stockpile/server" +import ( + "github.com/dotStart/Stockpile/stockpile/server" + "github.com/dotStart/Stockpile/stockpile/storage" +) // provides an initializer function for plugins type Initializer = func(*Context) error // provides a factory for storage backend instances -type StorageBackendFactory = func(*server.Config) (StorageBackend, error) +type StorageBackendFactory = func(*server.Config) (storage.StorageBackend, error) diff --git a/stockpile/plugin/manager.go b/stockpile/plugin/manager.go index b86e4b5..b94be78 100644 --- a/stockpile/plugin/manager.go +++ b/stockpile/plugin/manager.go @@ -23,6 +23,7 @@ import ( "runtime" "strings" + "github.com/dotStart/Stockpile/stockpile/storage" "github.com/op/go-logging" ) @@ -40,8 +41,8 @@ func NewManager(path string) *Manager { ctx := &Context{ storage: make(map[string]StorageBackendFactory), } - ctx.RegisterStorageBackend("mem", NewMemoryStorageBackend) - ctx.RegisterStorageBackend("file", NewFileStorageBackend) + ctx.RegisterStorageBackend("mem", storage.NewMemoryStorageBackend) + ctx.RegisterStorageBackend("file", storage.NewFileStorageBackend) return &Manager{ logger: logging.MustGetLogger("plugin"), diff --git a/stockpile/plugin/utility.go b/stockpile/plugin/utility.go index 98bdcd1..2bd3c99 100644 --- a/stockpile/plugin/utility.go +++ b/stockpile/plugin/utility.go @@ -17,29 +17,8 @@ package plugin import ( - "crypto/sha1" - "encoding/hex" "runtime" - "strings" - "time" ) // defines whether plugins are available on the current platform const PluginsAvailable = runtime.GOOS == "darwin" || runtime.GOOS == "linux" - -// provides a primitive wrapper object which handles expiration in the memory storage backend -type expirationWrapper struct { - content interface{} - createdAt time.Time -} - -// evaluates whether a particular entry is still considered valid -func (w *expirationWrapper) isValid(ttl time.Duration) bool { - return time.Since(w.createdAt) <= ttl -} - -// calculates a unified cache for a given input value (typically for primitive built-in cache types) -func calculateHash(input string) string { - enc := sha1.Sum([]byte(strings.ToLower(input))) - return hex.EncodeToString(enc[:]) -} diff --git a/stockpile/server/service/main.go b/stockpile/server/service/main.go index 328126c..a9df236 100644 --- a/stockpile/server/service/main.go +++ b/stockpile/server/service/main.go @@ -26,6 +26,7 @@ import ( "github.com/dotStart/Stockpile/stockpile/plugin" "github.com/dotStart/Stockpile/stockpile/server" "github.com/dotStart/Stockpile/stockpile/server/rpc" + "github.com/dotStart/Stockpile/stockpile/storage" "github.com/op/go-logging" "google.golang.org/grpc" "google.golang.org/grpc/reflection" @@ -36,7 +37,7 @@ type Server struct { logger *logging.Logger cfg *server.Config plugin *plugin.Manager - storage plugin.StorageBackend + storage storage.StorageBackend srv *grpc.Server } diff --git a/stockpile/server/service/profile.go b/stockpile/server/service/profile.go index 94e815a..fcfc6a9 100644 --- a/stockpile/server/service/profile.go +++ b/stockpile/server/service/profile.go @@ -21,9 +21,9 @@ import ( "time" "github.com/dotStart/Stockpile/stockpile/mojang" - "github.com/dotStart/Stockpile/stockpile/plugin" "github.com/dotStart/Stockpile/stockpile/server" "github.com/dotStart/Stockpile/stockpile/server/rpc" + "github.com/dotStart/Stockpile/stockpile/storage" "github.com/op/go-logging" "golang.org/x/net/context" ) @@ -32,10 +32,10 @@ type ProfileServiceImpl struct { logger *logging.Logger api *mojang.MojangAPI cfg *server.Config - storage plugin.StorageBackend + storage storage.StorageBackend } -func NewProfileService(api *mojang.MojangAPI, cfg *server.Config, backend plugin.StorageBackend) (*ProfileServiceImpl) { +func NewProfileService(api *mojang.MojangAPI, cfg *server.Config, backend storage.StorageBackend) (*ProfileServiceImpl) { return &ProfileServiceImpl{ logger: logging.MustGetLogger("profile-srv"), api: api, diff --git a/stockpile/server/service/server.go b/stockpile/server/service/server.go index 0656fc2..dfdda40 100644 --- a/stockpile/server/service/server.go +++ b/stockpile/server/service/server.go @@ -18,9 +18,9 @@ package service import ( "github.com/dotStart/Stockpile/stockpile/mojang" - "github.com/dotStart/Stockpile/stockpile/plugin" "github.com/dotStart/Stockpile/stockpile/server" "github.com/dotStart/Stockpile/stockpile/server/rpc" + "github.com/dotStart/Stockpile/stockpile/storage" "github.com/op/go-logging" "golang.org/x/net/context" ) @@ -29,10 +29,10 @@ type ServerServiceImpl struct { logger *logging.Logger api *mojang.MojangAPI cfg *server.Config - storage plugin.StorageBackend + storage storage.StorageBackend } -func NewServerService(api *mojang.MojangAPI, cfg *server.Config, backend plugin.StorageBackend) *ServerServiceImpl { +func NewServerService(api *mojang.MojangAPI, cfg *server.Config, backend storage.StorageBackend) *ServerServiceImpl { return &ServerServiceImpl{ logger: logging.MustGetLogger("server-srv"), api: api, diff --git a/stockpile/plugin/storage-encoded.go b/stockpile/storage/encoded.go similarity index 99% rename from stockpile/plugin/storage-encoded.go rename to stockpile/storage/encoded.go index 694c2ad..65b0bfd 100644 --- a/stockpile/plugin/storage-encoded.go +++ b/stockpile/storage/encoded.go @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package plugin +package storage import ( "time" diff --git a/stockpile/plugin/storage-file.go b/stockpile/storage/file.go similarity index 99% rename from stockpile/plugin/storage-file.go rename to stockpile/storage/file.go index d43a8b3..9079d8e 100644 --- a/stockpile/plugin/storage-file.go +++ b/stockpile/storage/file.go @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package plugin +package storage import ( "errors" diff --git a/stockpile/plugin/storage.go b/stockpile/storage/main.go similarity index 98% rename from stockpile/plugin/storage.go rename to stockpile/storage/main.go index d3b73e0..d0b108b 100644 --- a/stockpile/plugin/storage.go +++ b/stockpile/storage/main.go @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package plugin +package storage import ( "time" diff --git a/stockpile/plugin/storage-mem.go b/stockpile/storage/mem.go similarity index 99% rename from stockpile/plugin/storage-mem.go rename to stockpile/storage/mem.go index 15c743a..b6b5c34 100644 --- a/stockpile/plugin/storage-mem.go +++ b/stockpile/storage/mem.go @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package plugin +package storage import ( "strings" diff --git a/stockpile/storage/utility.go b/stockpile/storage/utility.go new file mode 100644 index 0000000..afca487 --- /dev/null +++ b/stockpile/storage/utility.go @@ -0,0 +1,41 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 storage + +import ( + "crypto/sha1" + "encoding/hex" + "strings" + "time" +) + +// provides a primitive wrapper object which handles expiration in the memory storage backend +type expirationWrapper struct { + content interface{} + createdAt time.Time +} + +// evaluates whether a particular entry is still considered valid +func (w *expirationWrapper) isValid(ttl time.Duration) bool { + return time.Since(w.createdAt) <= ttl +} + +// calculates a unified cache for a given input value (typically for primitive built-in cache types) +func calculateHash(input string) string { + enc := sha1.Sum([]byte(strings.ToLower(input))) + return hex.EncodeToString(enc[:]) +} From 6da77a9af683e95caa31f891518cad3089de7fbe Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Tue, 3 Jul 2018 05:14:21 +0200 Subject: [PATCH 084/142] Moved the interface documentation. --- stockpile/storage/encoded.go | 5 +++++ stockpile/storage/file.go | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/stockpile/storage/encoded.go b/stockpile/storage/encoded.go index 65b0bfd..ba8ad82 100644 --- a/stockpile/storage/encoded.go +++ b/stockpile/storage/encoded.go @@ -32,10 +32,15 @@ type EncodedStorageBackend struct { } type EncodedStorageBackendInterface interface { + // retrieves the data of a previously stored cache entry (given that it exists and is still + // considered valid in accordance with its ttl) GetCacheEntry(category string, name string, ttl time.Duration) ([]byte, error) + // creates or updates a cache entry PutCacheEntry(category string, name string, encoded []byte, ttl time.Duration) error + // purges a cache entry (if it exists) PurgeCacheEntry(category string, name string) error + // clears all allocated resources Close() error } diff --git a/stockpile/storage/file.go b/stockpile/storage/file.go index 9079d8e..f2a1f26 100644 --- a/stockpile/storage/file.go +++ b/stockpile/storage/file.go @@ -97,8 +97,6 @@ func (f *fileStorageBackendInterface) updateLock() { } } -// retrieves the data of a previously stored cache entry (given that it exists and is still -// considered valid in accordance with its ttl) func (f *fileStorageBackendInterface) GetCacheEntry(category string, key string, ttl time.Duration) ([]byte, error) { path := filepath.Join(f.cfg.Path, category, strings.ToLower(key)) @@ -116,7 +114,6 @@ func (f *fileStorageBackendInterface) GetCacheEntry(category string, key string, return ioutil.ReadFile(path) } -// creates or updates a cache entry func (f *fileStorageBackendInterface) PutCacheEntry(category string, key string, data []byte, ttl time.Duration) error { dir := filepath.Join(f.cfg.Path, category) path := filepath.Join(dir, strings.ToLower(key)) @@ -133,7 +130,6 @@ func (f *fileStorageBackendInterface) PutCacheEntry(category string, key string, return ioutil.WriteFile(path, data, filePerms) } -// purges a cache entry (if it exists) func (f *fileStorageBackendInterface) PurgeCacheEntry(category string, key string) error { path := filepath.Join(f.cfg.Path, category, strings.ToLower(key)) From 551457780bdcdd37dabd7fe16b1045a06f14b37a Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Tue, 3 Jul 2018 08:19:09 +0200 Subject: [PATCH 085/142] Moved the logic for cache population to a separate module to a dedicated implementation in order to share them between various server implementations (specifically the UI and the legacy API). --- stockpile/cache/main.go | 43 +++++++ stockpile/cache/profile.go | 175 ++++++++++++++++++++++++++++ stockpile/cache/server.go | 79 +++++++++++++ stockpile/cache/utility.go | 38 ++++++ stockpile/server/service/main.go | 26 ++--- stockpile/server/service/profile.go | 152 +++++------------------- stockpile/server/service/server.go | 58 ++------- 7 files changed, 385 insertions(+), 186 deletions(-) create mode 100644 stockpile/cache/main.go create mode 100644 stockpile/cache/profile.go create mode 100644 stockpile/cache/server.go create mode 100644 stockpile/cache/utility.go diff --git a/stockpile/cache/main.go b/stockpile/cache/main.go new file mode 100644 index 0000000..42e7c15 --- /dev/null +++ b/stockpile/cache/main.go @@ -0,0 +1,43 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 cache + +import ( + "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/dotStart/Stockpile/stockpile/storage" + "github.com/op/go-logging" +) + +// provides an abstraction layer between callers, the caching system and the upstream API +type Cache struct { + logger *logging.Logger + upstream *mojang.MojangAPI + storage storage.StorageBackend +} + +// creates a new cache client using +func New(upstream *mojang.MojangAPI, storage storage.StorageBackend) *Cache { + return &Cache{ + logger: logging.MustGetLogger("cache"), + upstream: upstream, + storage: storage, + } +} + +func (c *Cache) Close() error { + return c.storage.Close() +} diff --git a/stockpile/cache/profile.go b/stockpile/cache/profile.go new file mode 100644 index 0000000..16aecab --- /dev/null +++ b/stockpile/cache/profile.go @@ -0,0 +1,175 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 cache + +import ( + "fmt" + "strings" + "time" + + "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/google/uuid" +) + +// retrieves the profile to which a given display name has been assigned at a specific time +func (c *Cache) GetProfileId(name string, at time.Time) (*mojang.ProfileId, error) { + c.logger.Debugf("processing query for profile Id associated with name \"%s\" at time %s", name, at) + + id, err := c.storage.GetProfileId(name, at) + if err != nil { + c.logger.Errorf("storage backend responded with error: %s", err) + id = nil + } + if id == nil { + c.logger.Debugf("cache miss - requesting update from upstream") + + id, err = c.upstream.GetId(name, at) + if err != nil { + return nil, fmt.Errorf("upstream responded with error: %s", err) + } + + if id != nil { + err := c.storage.PutProfileId(id) + if err != nil { + return nil, fmt.Errorf("storage backend responded with error: %s", err) + } + + c.logger.Debugf("wrote new data to storage backend") + } else { + c.logger.Debugf("cannot find resource on upstream") + } + } else { + c.logger.Debugf("query fulfilled using cached data") + } + return id, nil +} + +func (c *Cache) BulkGetProfileId(names []string) ([]*mojang.ProfileId, error) { + c.logger.Debugf("processing query for profile Ids associated with names %s", strings.Join(names, ", ")) + + ids := make([]*mojang.ProfileId, 0) + at := time.Now() + + for i := 0; i < len(names); { + name := names[i] + id, err := c.storage.GetProfileId(name, at) // TODO: bulk lookup support in storage backend? + if err != nil { + c.logger.Errorf("storage backend responded with error: %s", err) + continue + } + + if id != nil { + ids = append(ids, id) + names = append(names[:i], names[i+1:]...) + continue + } + + i++ + } + c.logger.Debugf("resolved %d profile Ids from cache, %d will be resolved from upstream", len(ids), len(names)) + + if len(names) == 0 { + c.logger.Debugf("query fulfilled using cached data") + return ids, nil + } + + newIds, err := c.upstream.BulkGetId(names) + if err != nil { + return nil, fmt.Errorf("upstream responded with error: %s", err) + } + + for _, id := range newIds { + err := c.storage.PutProfileId(id) // TODO: bulk upload support in storage backend? + if err != nil { + return nil, fmt.Errorf("storage backend responded with error: %s", err) + } + } + + c.logger.Debugf("wrote new data to storage backend") + return append(ids, newIds...), nil +} + +// retrieves the name history of a given profile +func (c *Cache) GetNameHistory(id uuid.UUID) (*mojang.NameChangeHistory, error) { + c.logger.Debugf("processing query for name history of profile %s", id) + + history, err := c.storage.GetNameHistory(id) + if err != nil { + c.logger.Errorf("storage backend responded with an error: %s", err) + history = nil + } + if history == nil { + c.logger.Debugf("cache miss - requesting update from upstream") + + history, err = c.upstream.GetHistory(id) + if err != nil { + return nil, fmt.Errorf("upstream responded with error: %s", err) + } + + if history != nil { + err := c.storage.PutNameHistory(id, history) + if err != nil { + return nil, fmt.Errorf("storage backend responded with error: %s", err) + } + + c.logger.Debugf("wrote new data to storage backend") + } else { + c.logger.Debugf("cannot find resource on upstream") + } + } else { + c.logger.Debugf("query fulfilled using cached data") + } + return history, nil +} + +// retrieves a single profile +func (c *Cache) GetProfile(id uuid.UUID) (*mojang.Profile, error) { + c.logger.Debugf("processing query for profile %s", id) + + profile, err := c.storage.GetProfile(id) + if err != nil { + c.logger.Errorf("storage backend responded with an error: %s", err) + profile = nil + } + if profile == nil { + c.logger.Debugf("cache miss - requesting update from upstream") + + profile, err = c.upstream.GetProfile(id) + if err != nil { + return nil, fmt.Errorf("upstream responded with error: %s", err) + } + + if profile != nil { + err := c.storage.PutProfile(profile) + if err != nil { + return nil, fmt.Errorf("storage backend responded with error: %s", err) + } + + err = c.updateNameMapping(profile) + if err != nil { + return nil, fmt.Errorf("storage backend responded with error: %s", err) + } + + c.logger.Debugf("wrote new data to storage backend") + } else { + c.logger.Debugf("cannot find resource on upstream") + } + } else { + c.logger.Debugf("query fulfilled using cached data") + } + return profile, nil +} diff --git a/stockpile/cache/server.go b/stockpile/cache/server.go new file mode 100644 index 0000000..5a7f9cc --- /dev/null +++ b/stockpile/cache/server.go @@ -0,0 +1,79 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 cache + +import ( + "fmt" + + "github.com/dotStart/Stockpile/stockpile/mojang" +) + +// retrieves the current server blacklist +func (c *Cache) GetBlacklist() (*mojang.Blacklist, error) { + c.logger.Debugf("processing query for server blacklist") + + blacklist, err := c.storage.GetBlacklist() + if err != nil { + c.logger.Errorf("storage backend responded with an error: %s", err) + blacklist = nil + } + if blacklist == nil { + c.logger.Debugf("cache miss - requesting update from upstream") + + blacklist, err = c.upstream.GetBlacklist() + if err != nil { + return nil, fmt.Errorf("upstream responded with error: %s", err) + } + + if blacklist != nil { + err = c.storage.PutBlacklist(blacklist) + if err != nil { + return nil, fmt.Errorf("storage backend responded with error: %s", err) + } + + c.logger.Debugf("wrote new data to storage backend") + } else { + c.logger.Debugf("cannot find resource on upstream") + } + } else { + c.logger.Debugf("query fulfilled using cached data") + } + return blacklist, nil +} + +// performs a cache assisted server login +func (c *Cache) Login(displayName string, serverId string, ip string) (*mojang.Profile, error) { + c.logger.Debugf("processing login for user \"%s\" on server \"%s\" (with address \"%s\")", displayName, serverId, ip) + + profile, err := c.upstream.Login(displayName, serverId, ip) + if err != nil { + return nil, fmt.Errorf("upstream responded with error: %s", err) + } + + err = c.storage.PutProfile(profile) + if err != nil { + return nil, fmt.Errorf("storage backend responded with error: %s", err) + } + + err = c.updateNameMapping(profile) + if err != nil { + return nil, fmt.Errorf("storage backend responded with error: %s", err) + } + + c.logger.Debugf("wrote new data to storage backend") + return profile, nil +} diff --git a/stockpile/cache/utility.go b/stockpile/cache/utility.go new file mode 100644 index 0000000..1a07344 --- /dev/null +++ b/stockpile/cache/utility.go @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 cache + +import ( + "time" + + "github.com/dotStart/Stockpile/stockpile/mojang" +) + +// adjusts the name associations for the data discovered through a profile request +func (c *Cache) updateNameMapping(profile *mojang.Profile) error { + at := time.Now() + + mapping := &mojang.ProfileId{ + Id: profile.Id, + Name: profile.Name, + FirstSeenAt: at, + } + mapping.UpdateExpiration(at) + + c.storage.PutProfileId(mapping) + return nil +} diff --git a/stockpile/server/service/main.go b/stockpile/server/service/main.go index a9df236..ce73b29 100644 --- a/stockpile/server/service/main.go +++ b/stockpile/server/service/main.go @@ -22,11 +22,11 @@ import ( "fmt" "net" + "github.com/dotStart/Stockpile/stockpile/cache" "github.com/dotStart/Stockpile/stockpile/mojang" "github.com/dotStart/Stockpile/stockpile/plugin" "github.com/dotStart/Stockpile/stockpile/server" "github.com/dotStart/Stockpile/stockpile/server/rpc" - "github.com/dotStart/Stockpile/stockpile/storage" "github.com/op/go-logging" "google.golang.org/grpc" "google.golang.org/grpc/reflection" @@ -34,10 +34,10 @@ import ( // Represents an RPC server type Server struct { - logger *logging.Logger - cfg *server.Config - plugin *plugin.Manager - storage storage.StorageBackend + logger *logging.Logger + cfg *server.Config + plugin *plugin.Manager + cache *cache.Cache srv *grpc.Server } @@ -60,21 +60,19 @@ func NewServer(config *server.Config) (*Server, error) { logger.Infof("Using database plugin: %s", config.Storage.Type) return &Server{ - logger: logger, - cfg: config, - plugin: plugin, - storage: storage, + logger: logger, + cfg: config, + plugin: plugin, + cache: cache.New(mojang.New(), storage), }, nil } // Starts listening on an arbitrary socket func (s *Server) Listen(listener net.Listener) { - api := mojang.New() - s.srv = grpc.NewServer() grpc.NewServer() - rpc.RegisterProfileServiceServer(s.srv, NewProfileService(api, s.cfg, s.storage)) - rpc.RegisterServerServiceServer(s.srv, NewServerService(api, s.cfg, s.storage)) + rpc.RegisterProfileServiceServer(s.srv, NewProfileService(s.cache)) + rpc.RegisterServerServiceServer(s.srv, NewServerService(s.cache)) reflection.Register(s.srv) s.srv.Serve(listener) } @@ -87,5 +85,5 @@ func (s *Server) Stop() { // destroys the server instance permanently func (s *Server) Destroy() { s.srv.Stop() - s.storage.Close() + s.cache.Close() } diff --git a/stockpile/server/service/profile.go b/stockpile/server/service/profile.go index fcfc6a9..c657355 100644 --- a/stockpile/server/service/profile.go +++ b/stockpile/server/service/profile.go @@ -20,63 +20,49 @@ import ( "errors" "time" + "github.com/dotStart/Stockpile/stockpile/cache" "github.com/dotStart/Stockpile/stockpile/mojang" - "github.com/dotStart/Stockpile/stockpile/server" "github.com/dotStart/Stockpile/stockpile/server/rpc" - "github.com/dotStart/Stockpile/stockpile/storage" "github.com/op/go-logging" "golang.org/x/net/context" ) type ProfileServiceImpl struct { - logger *logging.Logger - api *mojang.MojangAPI - cfg *server.Config - storage storage.StorageBackend + logger *logging.Logger + cache *cache.Cache } -func NewProfileService(api *mojang.MojangAPI, cfg *server.Config, backend storage.StorageBackend) (*ProfileServiceImpl) { +func NewProfileService(cache *cache.Cache) (*ProfileServiceImpl) { return &ProfileServiceImpl{ - logger: logging.MustGetLogger("profile-srv"), - api: api, - cfg: cfg, - storage: backend, + logger: logging.MustGetLogger("profile-srv"), + cache: cache, } } func (s *ProfileServiceImpl) GetId(_ context.Context, req *rpc.GetIdRequest) (*rpc.ProfileId, error) { at := time.Unix(req.Timestamp, 0) - s.logger.Debugf("Processing request for profile id for name \"%s\" at time %s", req.Name, at) - association, err := s.storage.GetProfileId(req.Name, at) + + profile, err := s.cache.GetProfileId(req.Name, at) if err != nil { - s.logger.Errorf("Database responded with error during lookup of name \"%s\" at time %s: %s", req.Name, at.String(), err) - } else if association != nil { - s.logger.Debugf("Returning cached result of database: %+v", association) + return nil, err } - if err != nil || association == nil { - s.logger.Debugf("Cache miss - Requesting information from upstream") - association, err = s.api.GetId(req.Name, time.Unix(req.Timestamp, 0)) - if err != nil { - s.logger.Errorf("Failed to retrieve association of name \"%s\" at time %s: %s", req.Name, at.String(), err) - return nil, err - } + if profile == nil { + return &rpc.ProfileId{}, nil + } + return rpc.ProfileIdToRpc(profile), nil +} - if association != nil { - s.logger.Debugf("Pushing updated information to cache backend") - err = s.storage.PutProfileId(association) - if err != nil { - s.logger.Errorf("Failed to push profile association to storage backend: %s", err) - } - } +func (s *ProfileServiceImpl) BulkGetId(_ context.Context, req *rpc.BulkIdRequest) (*rpc.BulkIdResponse, error) { + if len(req.Names) > 100 { + return nil, errors.New("cannot process more than 100 names at once") } - if association == nil { - s.logger.Debugf("No profile with name \"%s\" at time %s", req.Name, at) - return &rpc.ProfileId{}, nil + ids, err := s.cache.BulkGetProfileId(req.Names) + if err != nil { + return nil, err } - s.logger.Debugf("Display name \"%s\" resolved to profile %s at time %s", req.Name, association.Id, at) - return rpc.ProfileIdToRpc(association), nil + return rpc.BulkIdsToRpc(ids), nil } func (s *ProfileServiceImpl) GetNameHistory(_ context.Context, req *rpc.IdRequest) (*rpc.NameHistory, error) { @@ -85,106 +71,26 @@ func (s *ProfileServiceImpl) GetNameHistory(_ context.Context, req *rpc.IdReques return nil, err } - s.logger.Debugf("Processing request for name history for profile %s", id) - history, err := s.storage.GetNameHistory(id) + history, err := s.cache.GetNameHistory(id) if err != nil { - s.logger.Errorf("Database responded with error during lookup of history of profile \"%s\": %s", id, err) - } - if err != nil || history == nil { - history, err = s.api.GetHistory(id) - if err != nil { - s.logger.Errorf("Failed to retrieve history of profile \"%s\": %s", id, err) - return nil, err - } - - if history == nil { - s.logger.Debugf("No profile with id %s for name history request", id) - return &rpc.NameHistory{}, nil - } - - s.logger.Debugf("Updated history with %d elements from upstream", len(history.History)) - err = s.storage.PutNameHistory(id, history) - if err != nil { - s.logger.Errorf("Failed to push name history to storage backend: %s", err) - } + return nil, err } - s.logger.Debugf("Name history for profile %s consists of %d elements", id, len(history.History)) return rpc.NameHistoryToRpc(history), nil } -func (s *ProfileServiceImpl) BulkGetId(_ context.Context, req *rpc.BulkIdRequest) (*rpc.BulkIdResponse, error) { - if len(req.Names) > 100 { - return nil, errors.New("cannot process more than 100 names at once") - } - - s.logger.Debugf("Processing request for ids of %d profiles", len(req.Names)) - - names := make([]string, 0) - results := make([]*mojang.ProfileId, 0) - for _, name := range req.Names { - profileId, err := s.storage.GetProfileId(name, time.Now()) - if profileId == nil || err != nil { - names = append(names, name) - - if err != nil { - s.logger.Errorf("Failed to resolve profileId for name \"%s\" from storage backend: %s", name, err) - } - } else { - s.logger.Debugf("Resolved name \"%s\" to profile %s via cache", name, profileId.Id) - results = append(results, profileId) - } - } - s.logger.Debugf("Resolved %d out of %d names from cache (%d remain)", len(results), len(req.Names), len(names)) - - if len(names) != 0 { - upstreamResults, err := s.api.BulkGetId(names) - if err != nil { - s.logger.Errorf("Failed to resolve %d names from upstream: %s", len(names), err) - return nil, err - } - - for _, profileId := range upstreamResults { - err := s.storage.PutProfileId(profileId) - if err != nil { - s.logger.Errorf("Failed to push profile association to storage backend: %s", err) - } - } - - results = append(results, upstreamResults...) - } - - return rpc.BulkIdsToRpc(results), nil -} - func (s *ProfileServiceImpl) GetProfile(_ context.Context, req *rpc.IdRequest) (*rpc.Profile, error) { id, err := mojang.ParseId(req.Id) if err != nil { return nil, err } - s.logger.Debugf("Processing request for profile %s", id) - profile, err := s.storage.GetProfile(id) - if err != nil || profile == nil { - if err != nil { - s.logger.Errorf("Failed to resolve profile %s from storage backend: %s", id, err) - } - - s.logger.Debugf("Cache miss - Requesting update from upstream server") - profile, err = s.api.GetProfile(id) - if err != nil { - s.logger.Errorf("Failed to resolve profile %s from upstream server: %s", id, err) - return nil, err - } - if profile == nil { - return &rpc.Profile{}, nil - } - - err = s.storage.PutProfile(profile) - if err != nil { - s.logger.Errorf("Failed to push profile to storage backend: %s", err) - } - // TODO: Update profile mappings + profile, err := s.cache.GetProfile(id) + if err != nil { + return nil, err + } + if profile == nil { + return &rpc.Profile{}, nil } return rpc.ProfileToRpc(profile), nil diff --git a/stockpile/server/service/server.go b/stockpile/server/service/server.go index dfdda40..a1ab2ef 100644 --- a/stockpile/server/service/server.go +++ b/stockpile/server/service/server.go @@ -17,57 +17,26 @@ package service import ( - "github.com/dotStart/Stockpile/stockpile/mojang" - "github.com/dotStart/Stockpile/stockpile/server" + "github.com/dotStart/Stockpile/stockpile/cache" "github.com/dotStart/Stockpile/stockpile/server/rpc" - "github.com/dotStart/Stockpile/stockpile/storage" "github.com/op/go-logging" "golang.org/x/net/context" ) type ServerServiceImpl struct { - logger *logging.Logger - api *mojang.MojangAPI - cfg *server.Config - storage storage.StorageBackend + logger *logging.Logger + cache *cache.Cache } -func NewServerService(api *mojang.MojangAPI, cfg *server.Config, backend storage.StorageBackend) *ServerServiceImpl { +func NewServerService(cache *cache.Cache) *ServerServiceImpl { return &ServerServiceImpl{ - logger: logging.MustGetLogger("server-srv"), - api: api, - cfg: cfg, - storage: backend, + logger: logging.MustGetLogger("server-srv"), + cache: cache, } } -func (s *ServerServiceImpl) getBlacklist() (*mojang.Blacklist, error) { - blacklist, err := s.storage.GetBlacklist() - if err != nil || blacklist == nil { - if err != nil { - s.logger.Errorf("Failed to retrieve blacklist from storage backend: %s", err) - } - - s.logger.Debugf("Cache miss - Requesting update from upstream") - blacklist, err = s.api.GetBlacklist() - if err != nil { - s.logger.Errorf("Failed to retrieve blacklist: %s", err) - return nil, err - } - - s.logger.Debugf("Updating cached version") - err = s.storage.PutBlacklist(blacklist) - if err != nil { - s.logger.Errorf("Failed to push blacklist to storage backend: %s", err) - } - } - - return blacklist, err -} - func (s *ServerServiceImpl) GetBlacklist(context.Context, *rpc.EmptyRequest) (*rpc.Blacklist, error) { - s.logger.Debugf("Processing request for complete server blacklist") - blacklist, err := s.getBlacklist() + blacklist, err := s.cache.GetBlacklist() if err != nil { return nil, err } @@ -76,8 +45,7 @@ func (s *ServerServiceImpl) GetBlacklist(context.Context, *rpc.EmptyRequest) (*r } func (s *ServerServiceImpl) CheckBlacklist(_ context.Context, req *rpc.CheckBlacklistRequest) (*rpc.CheckBlacklistResponse, error) { - s.logger.Debugf("Processing request to check blacklist for %d addresses", len(req.Addresses)) - blacklist, err := s.getBlacklist() + blacklist, err := s.cache.GetBlacklist() if err != nil { return nil, err } @@ -99,18 +67,10 @@ func (s *ServerServiceImpl) CheckBlacklist(_ context.Context, req *rpc.CheckBlac } func (s *ServerServiceImpl) Login(_ context.Context, req *rpc.LoginRequest) (*rpc.Profile, error) { - s.logger.Debugf("Processing request to login user \"%s\" with serverId %s and ip %s", req.DisplayName, req.ServerId, req.Ip) - profile, err := s.api.Login(req.DisplayName, req.ServerId, req.Ip) + profile, err := s.cache.Login(req.DisplayName, req.ServerId, req.Ip) if err != nil { - s.logger.Errorf("Failed to perform login: %s", err) return nil, err } - err = s.storage.PutProfile(profile) - if err != nil { - s.logger.Errorf("Failed to push profile to storage backend: %s", err) - } - // TODO: Update profile mappings - return rpc.ProfileToRpc(profile), nil } From 512c977dc3697045a9dffc5ea108967f11133f8c Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Wed, 4 Jul 2018 17:14:32 +0200 Subject: [PATCH 086/142] Removed the legacy API flag for now. --- stockpile/command/server.go | 2 +- stockpile/server/config.go | 22 ++++++---------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/stockpile/command/server.go b/stockpile/command/server.go index 3a3c582..9b15cb7 100644 --- a/stockpile/command/server.go +++ b/stockpile/command/server.go @@ -133,7 +133,7 @@ func (c *ServerCommand) Execute(_ context.Context, f *flag.FlagSet, _ ...interfa // initialize the RPC server at all times (only differ between mux policies depending on whether the legacy API or UI // is enabled) rpcPolicy := cmux.Any() - if *cfg.EnableLegacyApi { + if true { // TODO: UI flag rpcPolicy = cmux.HTTP2HeaderField("content-type", "application/grpc") } rpcServer, err := service.NewServer(cfg) diff --git a/stockpile/server/config.go b/stockpile/server/config.go index 9a0b906..bb1d57f 100644 --- a/stockpile/server/config.go +++ b/stockpile/server/config.go @@ -39,11 +39,10 @@ const DefaultPort = 36623 // Represents a server configuration (typically parsed from one or more HCL files) type Config struct { - PluginDir *string `hcl:"plugin-dir"` - BindAddress *string `hcl:"bind-address,attr"` - EnableLegacyApi *bool `hcl:"legacy-api,attr"` - Storage *StorageConfig `hcl:"storage,block"` - Ttl *TtlConfig `hcl:"ttl,block"` + PluginDir *string `hcl:"plugin-dir"` + BindAddress *string `hcl:"bind-address,attr"` + Storage *StorageConfig `hcl:"storage,block"` + Ttl *TtlConfig `hcl:"ttl,block"` } // Represents a storage backend configuration @@ -74,12 +73,10 @@ func EmptyConfig() *Config { func DefaultConfig() *Config { pluginDir := "plugins" addr := fmt.Sprintf("%s:%d", "127.0.0.1", DefaultPort) - legacyEnabled := false cfg := &Config{ - PluginDir: &pluginDir, - BindAddress: &addr, - EnableLegacyApi: &legacyEnabled, + PluginDir: &pluginDir, + BindAddress: &addr, Storage: &StorageConfig{ Type: "mem", }, @@ -102,10 +99,7 @@ func DefaultConfig() *Config { } func DevelopmentConfig() *Config { - legacyEnabled := true - return DefaultConfig().Merge(&Config{ - EnableLegacyApi: &legacyEnabled, }) } @@ -183,10 +177,6 @@ func (c *Config) Merge(other *Config) *Config { c.BindAddress = other.BindAddress } - if other.EnableLegacyApi != nil { - c.EnableLegacyApi = other.EnableLegacyApi - } - if c.Storage == nil { c.Storage = other.Storage } else if other.Storage != nil { From 3386109620b241cc2d1b684e4393cb8772050420 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Thu, 5 Jul 2018 02:18:53 +0200 Subject: [PATCH 087/142] Added a counter to the cache implementation to implement some feedback on rate limit usage. --- stockpile/cache/main.go | 36 ++++++++++++++++++++++++++++++++---- stockpile/cache/profile.go | 3 +++ stockpile/cache/server.go | 1 + 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/stockpile/cache/main.go b/stockpile/cache/main.go index 42e7c15..070e998 100644 --- a/stockpile/cache/main.go +++ b/stockpile/cache/main.go @@ -17,6 +17,9 @@ package cache import ( + "sync/atomic" + "time" + "github.com/dotStart/Stockpile/stockpile/mojang" "github.com/dotStart/Stockpile/stockpile/storage" "github.com/op/go-logging" @@ -27,17 +30,42 @@ type Cache struct { logger *logging.Logger upstream *mojang.MojangAPI storage storage.StorageBackend + + resetTicker *time.Ticker + requestCounter uint64 } // creates a new cache client using func New(upstream *mojang.MojangAPI, storage storage.StorageBackend) *Cache { - return &Cache{ - logger: logging.MustGetLogger("cache"), - upstream: upstream, - storage: storage, + cache := &Cache{ + logger: logging.MustGetLogger("cache"), + upstream: upstream, + storage: storage, + resetTicker: time.NewTicker(time.Minute), } + go cache.resetRequestCounter() + return cache +} + +// increments the request counter by one +func (c *Cache) incrementRequestCounter() { + atomic.AddUint64(&c.requestCounter, 1) +} + +// regularly clears the request counter +func (c *Cache) resetRequestCounter() { + for _ = range c.resetTicker.C { + atomic.StoreUint64(&c.requestCounter, 0) + } +} + +// retrieves the amount of requests which have been submitted to the upstream servers within the +// last minute +func (c *Cache) GetRateLimitAllocation() uint64 { + return atomic.LoadUint64(&c.requestCounter) } func (c *Cache) Close() error { + c.resetTicker.Stop() return c.storage.Close() } diff --git a/stockpile/cache/profile.go b/stockpile/cache/profile.go index 16aecab..52c5e64 100644 --- a/stockpile/cache/profile.go +++ b/stockpile/cache/profile.go @@ -37,6 +37,7 @@ func (c *Cache) GetProfileId(name string, at time.Time) (*mojang.ProfileId, erro if id == nil { c.logger.Debugf("cache miss - requesting update from upstream") + c.incrementRequestCounter() id, err = c.upstream.GetId(name, at) if err != nil { return nil, fmt.Errorf("upstream responded with error: %s", err) @@ -87,6 +88,7 @@ func (c *Cache) BulkGetProfileId(names []string) ([]*mojang.ProfileId, error) { return ids, nil } + c.incrementRequestCounter() newIds, err := c.upstream.BulkGetId(names) if err != nil { return nil, fmt.Errorf("upstream responded with error: %s", err) @@ -115,6 +117,7 @@ func (c *Cache) GetNameHistory(id uuid.UUID) (*mojang.NameChangeHistory, error) if history == nil { c.logger.Debugf("cache miss - requesting update from upstream") + c.incrementRequestCounter() history, err = c.upstream.GetHistory(id) if err != nil { return nil, fmt.Errorf("upstream responded with error: %s", err) diff --git a/stockpile/cache/server.go b/stockpile/cache/server.go index 5a7f9cc..62e3a8a 100644 --- a/stockpile/cache/server.go +++ b/stockpile/cache/server.go @@ -34,6 +34,7 @@ func (c *Cache) GetBlacklist() (*mojang.Blacklist, error) { if blacklist == nil { c.logger.Debugf("cache miss - requesting update from upstream") + c.incrementRequestCounter() blacklist, err = c.upstream.GetBlacklist() if err != nil { return nil, fmt.Errorf("upstream responded with error: %s", err) From 78dd6c49c3392e7b61558fefaacc825f965b1e9a Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Thu, 5 Jul 2018 03:12:28 +0200 Subject: [PATCH 088/142] Removed a redundant variable definition. --- stockpile/cache/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stockpile/cache/main.go b/stockpile/cache/main.go index 070e998..5150988 100644 --- a/stockpile/cache/main.go +++ b/stockpile/cache/main.go @@ -54,7 +54,7 @@ func (c *Cache) incrementRequestCounter() { // regularly clears the request counter func (c *Cache) resetRequestCounter() { - for _ = range c.resetTicker.C { + for range c.resetTicker.C { atomic.StoreUint64(&c.requestCounter, 0) } } From ba27527c6d871cd9a173dc38079d04971e262ef8 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Thu, 5 Jul 2018 04:52:33 +0200 Subject: [PATCH 089/142] Added basic checks for our command dependencies. --- Makefile | 32 +++++++++++++++++++++++++++++--- plugins/redis/Makefile | 2 +- stockpile/Makefile | 2 +- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index cecfefd..40602aa 100644 --- a/Makefile +++ b/Makefile @@ -2,11 +2,37 @@ APPLICATION_BRAND := vanilla APPLICATION_VERSION := 2.0.0 APPLICATION_COMMIT_HASH := `git log -1 --pretty=format:"%H"` APPLICATION_TIMESTAMP := `date --utc "+%s"` + +GIT := $(shell command -v git 2> /dev/null) +DEP := $(shell command -v dep 2> /dev/null) +GO := $(shell command -v go 2> /dev/null) export PLUGINS := $(wildcard plugins/*/.) -all: print-config install-dependencies core core-plugins package +all: check-env print-config install-dependencies core core-plugins package + +check-env: + @echo "==> Checking prerequisites" + @echo -n "Checking for git ... " +ifndef GIT + @echo "Not found" + $(error "git is unavailable") +endif + @echo $(GIT) + @echo -n "Checking for go ... " +ifndef GO + @echo "Not Found" + $(error "go is unavailable") +endif + @echo $(GO) + @echo -n "Checking for dep ... " +ifndef DEP + @echo -n "Not Found" + $(error "dep is unavailable") +endif + @echo $(DEP) + @echo "" print-config: @echo "==> Build Configuration" @@ -20,11 +46,11 @@ print-config: clean: @echo "==> Clearing previous build data" @rm -rf build/ || true - @go clean -cache + @$(GO) clean -cache install-dependencies: @echo "==> Installing dependencies" - @dep ensure -v + @$(DEP) ensure -v @echo "" core: diff --git a/plugins/redis/Makefile b/plugins/redis/Makefile index bda3235..c173317 100644 --- a/plugins/redis/Makefile +++ b/plugins/redis/Makefile @@ -17,6 +17,6 @@ ext = $(word 3, $(temp)) build: $(PLATFORMS) $(PLATFORMS): - @export GOOS=$(os); export GOARCH=$(arch); export CGO_ENABLED=1; go build -v -buildmode=plugin ${LDFLAGS} -o ../../build/$(os)-$(arch)/plugins/redis.$(ext) + @export GOOS=$(os); export GOARCH=$(arch); export CGO_ENABLED=1; $(GO) build -v -buildmode=plugin ${LDFLAGS} -o ../../build/$(os)-$(arch)/plugins/redis.$(ext) .PHONY: build diff --git a/stockpile/Makefile b/stockpile/Makefile index dca5de9..012f804 100644 --- a/stockpile/Makefile +++ b/stockpile/Makefile @@ -10,6 +10,6 @@ ext = $(word 3, $(temp)) build: $(PLATFORMS) $(PLATFORMS): - @export GOOS=$(os); export GOARCH=$(arch); go build -v ${LDFLAGS} -o ../build/$(os)-$(arch)/stockpile$(ext) + @export GOOS=$(os); export GOARCH=$(arch); $(GO) build -v ${LDFLAGS} -o ../build/$(os)-$(arch)/stockpile$(ext) .PHONY: build From b099c6b2d4241450ddf2015c8e5416923220cd7f Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Thu, 5 Jul 2018 04:54:17 +0200 Subject: [PATCH 090/142] Added a dependency to bindata-assetfs. --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 40602aa..1e4ec90 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,7 @@ clean: install-dependencies: @echo "==> Installing dependencies" + @$(GO) get -u github.com/elazarl/go-bindata-assetfs/go-bindata-assetfs @$(DEP) ensure -v @echo "" From 565270beb5f2ba195b390e8548dab5a18d41ab92 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Fri, 6 Jul 2018 05:08:15 +0200 Subject: [PATCH 091/142] Implemented a channel based event system to expose changes to grpc clients and the web interface. --- stockpile/cache/event.go | 95 ++++++++++++++++++++++++++++++++++++++ stockpile/cache/main.go | 2 + stockpile/cache/profile.go | 36 ++++++++++++++- stockpile/cache/server.go | 17 ++++++- stockpile/cache/utility.go | 9 ++++ 5 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 stockpile/cache/event.go diff --git a/stockpile/cache/event.go b/stockpile/cache/event.go new file mode 100644 index 0000000..bc05ff9 --- /dev/null +++ b/stockpile/cache/event.go @@ -0,0 +1,95 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 cache + +import ( + "errors" + "time" + + "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/google/uuid" +) + +// represents an even which has occurred within the cache +type Event struct { + Type EventType + Key interface{} + Object interface{} +} + +func (e *Event) ProfileIdPayload() (*mojang.ProfileId, error) { + if e.Type != ProfileIdEvent { + return nil, errors.New("cannot convert event payload to ProfileId") + } + + return e.Object.(*mojang.ProfileId), nil +} + +func (e *Event) NameChangeHistoryPayload() (*mojang.NameChangeHistory, error) { + if e.Type != NameHistoryEvent { + return nil, errors.New("cannot convert event payload to NameChangeHistory") + } + + return e.Object.(*mojang.NameChangeHistory), nil +} + +func (e *Event) ProfilePayload() (*mojang.Profile, error) { + if e.Type != ProfileEvent { + return nil, errors.New("cannot convert event payload to Profile") + } + + return e.Object.(*mojang.Profile), nil +} + +func (e *Event) BlacklistPayload() (*mojang.Blacklist, error) { + if e.Type != BlacklistEvent { + return nil, errors.New("cannot convert event payload to Blacklist") + } + + return e.Object.(*mojang.Blacklist), nil +} + +func (e *Event) ProfileIdKey() (*ProfileIdKey, error) { + if e.Type != ProfileIdEvent { + return nil, errors.New("cannot convert event key to ProfileIdKey") + } + + return e.Key.(*ProfileIdKey), nil +} + +func (e *Event) IdKey() (*uuid.UUID, error) { + if e.Type != NameHistoryEvent && e.Type != ProfileEvent { + return nil, errors.New("cannot convert event key to UUID") + } + + return e.Key.(*uuid.UUID), nil +} + +// indicates which type of data is changed as part of an event +type EventType int32 + +const ( + ProfileIdEvent EventType = 0 + NameHistoryEvent EventType = 1 + ProfileEvent EventType = 2 + BlacklistEvent EventType = 3 +) + +type ProfileIdKey struct { + Name string + At time.Time +} diff --git a/stockpile/cache/main.go b/stockpile/cache/main.go index 5150988..220f323 100644 --- a/stockpile/cache/main.go +++ b/stockpile/cache/main.go @@ -33,6 +33,7 @@ type Cache struct { resetTicker *time.Ticker requestCounter uint64 + Events chan *Event } // creates a new cache client using @@ -42,6 +43,7 @@ func New(upstream *mojang.MojangAPI, storage storage.StorageBackend) *Cache { upstream: upstream, storage: storage, resetTicker: time.NewTicker(time.Minute), + Events: make(chan *Event), } go cache.resetRequestCounter() return cache diff --git a/stockpile/cache/profile.go b/stockpile/cache/profile.go index 52c5e64..89dde85 100644 --- a/stockpile/cache/profile.go +++ b/stockpile/cache/profile.go @@ -50,6 +50,16 @@ func (c *Cache) GetProfileId(name string, at time.Time) (*mojang.ProfileId, erro } c.logger.Debugf("wrote new data to storage backend") + + c.Events <- &Event{ + Type: ProfileIdEvent, + Key: &ProfileIdKey{ + Name: name, + At: at, + }, + Object: id, + } + c.logger.Debugf("notified event channel") } else { c.logger.Debugf("cannot find resource on upstream") } @@ -99,9 +109,19 @@ func (c *Cache) BulkGetProfileId(names []string) ([]*mojang.ProfileId, error) { if err != nil { return nil, fmt.Errorf("storage backend responded with error: %s", err) } + + c.Events <- &Event{ + Type: ProfileIdEvent, + Key: &ProfileIdKey{ + Name: id.Name, + At: at, + }, + Object: id, + } } c.logger.Debugf("wrote new data to storage backend") + c.logger.Debugf("notified event channel") return append(ids, newIds...), nil } @@ -128,8 +148,14 @@ func (c *Cache) GetNameHistory(id uuid.UUID) (*mojang.NameChangeHistory, error) if err != nil { return nil, fmt.Errorf("storage backend responded with error: %s", err) } - c.logger.Debugf("wrote new data to storage backend") + + c.Events <- &Event{ + Type: NameHistoryEvent, + Key: &id, + Object: history, + } + c.logger.Debugf("notified event channel") } else { c.logger.Debugf("cannot find resource on upstream") } @@ -166,8 +192,14 @@ func (c *Cache) GetProfile(id uuid.UUID) (*mojang.Profile, error) { if err != nil { return nil, fmt.Errorf("storage backend responded with error: %s", err) } - c.logger.Debugf("wrote new data to storage backend") + + c.Events <- &Event{ + Type: ProfileEvent, + Key: &id, + Object: profile, + } + c.logger.Debugf("notified event channel") } else { c.logger.Debugf("cannot find resource on upstream") } diff --git a/stockpile/cache/server.go b/stockpile/cache/server.go index 62e3a8a..b5b4b4a 100644 --- a/stockpile/cache/server.go +++ b/stockpile/cache/server.go @@ -45,8 +45,14 @@ func (c *Cache) GetBlacklist() (*mojang.Blacklist, error) { if err != nil { return nil, fmt.Errorf("storage backend responded with error: %s", err) } - c.logger.Debugf("wrote new data to storage backend") + + c.Events <- &Event{ + Type: BlacklistEvent, + Key: nil, + Object: blacklist, + } + c.logger.Debugf("notified event channel") } else { c.logger.Debugf("cannot find resource on upstream") } @@ -74,7 +80,14 @@ func (c *Cache) Login(displayName string, serverId string, ip string) (*mojang.P if err != nil { return nil, fmt.Errorf("storage backend responded with error: %s", err) } - c.logger.Debugf("wrote new data to storage backend") + + c.Events <- &Event{ + Type: ProfileEvent, + Key: profile.Id, + Object: profile, + } + c.logger.Debugf("notified event channel") + return profile, nil } diff --git a/stockpile/cache/utility.go b/stockpile/cache/utility.go index 1a07344..a418451 100644 --- a/stockpile/cache/utility.go +++ b/stockpile/cache/utility.go @@ -34,5 +34,14 @@ func (c *Cache) updateNameMapping(profile *mojang.Profile) error { mapping.UpdateExpiration(at) c.storage.PutProfileId(mapping) + + c.Events <- &Event{ + Type: ProfileIdEvent, + Key: &ProfileIdKey{ + Name: profile.Name, + At: at, + }, + Object: mapping, + } return nil } From e3ed40e9feeccc30a7cebe067317ad150bcf3cb2 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Fri, 6 Jul 2018 05:09:39 +0200 Subject: [PATCH 092/142] Defined a new service which exposes cache events via the grpc api. --- stockpile/server/rpc/events.proto | 39 ++++++ stockpile/server/rpc/utility.go | 204 +++++++++++++++++++++++++++++ stockpile/server/service/events.go | 47 +++++++ stockpile/server/service/main.go | 3 +- 4 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 stockpile/server/rpc/events.proto create mode 100644 stockpile/server/service/events.go diff --git a/stockpile/server/rpc/events.proto b/stockpile/server/rpc/events.proto new file mode 100644 index 0000000..b825e7a --- /dev/null +++ b/stockpile/server/rpc/events.proto @@ -0,0 +1,39 @@ +syntax = "proto3"; + +package rpc; +option java_package = "tv.dotstart.stockpile"; + +import "google/protobuf/any.proto"; +import "common.proto"; + +service EventService { + rpc StreamEvents (EmptyRequest) returns (stream Event); +} + +message Event { + EventType type = 1; + EventAction action = 2; + google.protobuf.Any key = 3; + google.protobuf.Any object = 4; +} + +enum EventType { + PROFILE_ID = 0; + NAME_HISTORY = 1; + PROFILE = 2; + BLACKLIST = 3; +} + +enum EventAction { + POPULATED = 0; + UPDATED = 1; +} + +message ProfileIdKey { // TODO: Replace keys with common representation + string name = 1; + int64 at = 2; +} + +message IdKey { + string id = 1; +} diff --git a/stockpile/server/rpc/utility.go b/stockpile/server/rpc/utility.go index e58dd94..9e72af5 100644 --- a/stockpile/server/rpc/utility.go +++ b/stockpile/server/rpc/utility.go @@ -18,12 +18,19 @@ package rpc import ( "errors" + "fmt" "time" + "github.com/dotStart/Stockpile/stockpile/cache" "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/dotStart/Stockpile/vendor.orig/github.com/golang/protobuf/proto" + "github.com/dotStart/Stockpile/vendor.orig/github.com/golang/protobuf/ptypes" + "github.com/golang/protobuf/ptypes/any" "github.com/google/uuid" ) +const MessageTypeBaseUrl = "github.com/dotStart/Stockpile/stockpile/server/rpc/" + // Converts a profileId into its fully parsed representation func ProfileIdFromRpc(rpc *ProfileId) (*mojang.ProfileId, error) { if !rpc.IsPopulated() { @@ -271,6 +278,203 @@ func BlacklistFromRpc(blacklist *Blacklist) (*mojang.Blacklist, error) { return mojang.NewBlacklist(blacklist.Hashes) } +// converts an arbitrary event into its rpc representation +func EventToRpc(event *cache.Event) (*Event, error) { + key, err := EventKeyToRpc(event.Key) + if err != nil { + return nil, err + } + + obj, err := EventPayloadToRpc(event.Object) + if err != nil { + return nil, err + } + + enc, err := ptypes.MarshalAny(obj) + if err != nil { + return nil, err + } + + return &Event{ + Type: EventTypeToRpc(event.Type), + Key: key, + Object: enc, + }, nil +} + +func EventFromRpc(event *Event) (*cache.Event, error) { + typ, err := EventTypeFromRpc(event.Type) + if err != nil { + return nil, err + } + + key, err := EventKeyFromRpc(event.Key) + if err != nil { + return nil, err + } + + payload, err := EventPayloadFromRpc(event.Object) + if err != nil { + return nil, err + } + + return &cache.Event{ + Type: typ, + Key: key, + Object: payload, + }, nil +} + +// converts an event type into its rpc representation +func EventTypeToRpc(typ cache.EventType) EventType { + switch typ { + case cache.ProfileIdEvent: + return EventType_PROFILE_ID + case cache.NameHistoryEvent: + return EventType_NAME_HISTORY + case cache.ProfileEvent: + return EventType_PROFILE + case cache.BlacklistEvent: + return EventType_BLACKLIST + default: + return -1 // TODO: Unknown? + } +} + +// converts an event type from its rpc representation +func EventTypeFromRpc(typ EventType) (cache.EventType, error) { + switch typ { + case EventType_PROFILE_ID: + return cache.ProfileIdEvent, nil + case EventType_NAME_HISTORY: + return cache.NameHistoryEvent, nil + case EventType_PROFILE: + return cache.ProfileEvent, nil + case EventType_BLACKLIST: + return cache.BlacklistEvent, nil + default: + return -1, fmt.Errorf("illegal event type: %d", typ) + } +} + +// encodes an arbitrary key type into its rpc representation +func EventKeyToRpc(key interface{}) (*any.Any, error) { + // nil key is passed as is as there is no identifying information there + if key == nil { + return nil, nil + } + + profileId, ok := key.(*cache.ProfileIdKey) + if ok { + return ptypes.MarshalAny(&ProfileIdKey{ + Name: profileId.Name, + At: profileId.At.Unix(), + }) + } + + id, ok := key.(*uuid.UUID) + if ok { + return ptypes.MarshalAny(&IdKey{ + Id: id.String(), + }) + } + + return nil, fmt.Errorf("unknown key type: %v", key) +} + +// decodes an arbitrary key type from its rpc representation +func EventKeyFromRpc(key *any.Any) (interface{}, error) { + if key == nil { + return nil, nil + } + + obj := &ptypes.DynamicAny{} + err := ptypes.UnmarshalAny(key, obj) + if err != nil { + return nil, err + } + + profileId, ok := obj.Message.(*ProfileIdKey) + if ok { + return &cache.ProfileIdKey{ + Name: profileId.Name, + At: time.Unix(profileId.At, 0), + }, nil + } + + id, ok := obj.Message.(*IdKey) + if ok { + i, err := uuid.Parse(id.Id) + return &i, err + } + + return nil, fmt.Errorf("unknown key type: %v", obj.Message) +} + +// converts an event payload into its rpc format +func EventPayloadToRpc(payload interface{}) (proto.Message, error) { + if payload == nil { + return nil, errors.New("payload cannot be nil") + } + + profileId, ok := payload.(*mojang.ProfileId) + if ok { + return ProfileIdToRpc(profileId), nil + } + + history, ok := payload.(*mojang.NameChangeHistory) + if ok { + return NameHistoryToRpc(history), nil + } + + profile, ok := payload.(*mojang.Profile) + if ok { + return ProfileToRpc(profile), nil + } + + blacklist, ok := payload.(*mojang.Blacklist) + if ok { + return BlacklistToRpc(blacklist), nil + } + + return nil, fmt.Errorf("illegal payload value: %v", payload) +} + +// converts an event payload from its rpc format +func EventPayloadFromRpc(payload *any.Any) (interface{}, error) { + if payload == nil { + return nil, errors.New("payload cannot be nil") + } + + obj := &ptypes.DynamicAny{} + err := ptypes.UnmarshalAny(payload, obj) + if err != nil { + return nil, err + } + + profileId, ok := obj.Message.(*ProfileId) + if ok { + return ProfileIdFromRpc(profileId) + } + + history, ok := obj.Message.(*NameHistory) + if ok { + return NameHistoryFromRpc(history), nil + } + + profile, ok := obj.Message.(*Profile) + if ok { + return ProfileFromRpc(profile) + } + + blacklist, ok := obj.Message.(*Blacklist) + if ok { + return BlacklistFromRpc(blacklist) + } + + return nil, fmt.Errorf("illegal payload value: %v", payload) +} + // evaluates whether the message has been populated with actual data (e.g. whether it is not empty) func (p *ProfileId) IsPopulated() bool { return p.Id != "" diff --git a/stockpile/server/service/events.go b/stockpile/server/service/events.go new file mode 100644 index 0000000..b1e7c06 --- /dev/null +++ b/stockpile/server/service/events.go @@ -0,0 +1,47 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 service + +import ( + "github.com/dotStart/Stockpile/stockpile/cache" + "github.com/dotStart/Stockpile/stockpile/server/rpc" + "github.com/op/go-logging" +) + +type EventServiceImpl struct { + logger *logging.Logger + cache *cache.Cache +} + +func NewEventService(cache *cache.Cache) (*EventServiceImpl) { + return &EventServiceImpl{ + logger: logging.MustGetLogger("event-srv"), + cache: cache, + } +} + +func (s *EventServiceImpl) StreamEvents(_ *rpc.EmptyRequest, srv rpc.EventService_StreamEventsServer) error { + for e := range s.cache.Events { + enc, err := rpc.EventToRpc(e) + if err != nil { + s.logger.Errorf("Failed to encode event %v: %s", e, err) + continue + } + srv.Send(enc) + } + return nil +} diff --git a/stockpile/server/service/main.go b/stockpile/server/service/main.go index ce73b29..a418cf9 100644 --- a/stockpile/server/service/main.go +++ b/stockpile/server/service/main.go @@ -16,7 +16,7 @@ */ package service -//go:generate protoc -I ../rpc --go_out=plugins=grpc:../rpc common.proto profile.proto server.proto +//go:generate protoc -I ../rpc --go_out=plugins=grpc:../rpc common.proto events.proto profile.proto server.proto import ( "fmt" @@ -73,6 +73,7 @@ func (s *Server) Listen(listener net.Listener) { grpc.NewServer() rpc.RegisterProfileServiceServer(s.srv, NewProfileService(s.cache)) rpc.RegisterServerServiceServer(s.srv, NewServerService(s.cache)) + rpc.RegisterEventServiceServer(s.srv, NewEventService(s.cache)) reflection.Register(s.srv) s.srv.Serve(listener) } From d52b6a56b105456027f5f662d1a873479b1e37df Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Fri, 6 Jul 2018 05:10:14 +0200 Subject: [PATCH 093/142] Created a basic cli command to access some of the event API. --- stockpile/command/listen.go | 109 ++++++++++++++++++++++++++++++++++++ stockpile/main.go | 1 + 2 files changed, 110 insertions(+) create mode 100644 stockpile/command/listen.go diff --git a/stockpile/command/listen.go b/stockpile/command/listen.go new file mode 100644 index 0000000..af8edad --- /dev/null +++ b/stockpile/command/listen.go @@ -0,0 +1,109 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 command + +import ( + "flag" + "fmt" + "io" + "os" + "time" + + "github.com/dotStart/Stockpile/stockpile/cache" + "github.com/dotStart/Stockpile/stockpile/server/rpc" + "github.com/google/subcommands" + "golang.org/x/net/context" +) + +type ListenCommand struct { + ClientCommand +} + +func (*ListenCommand) Name() string { + return "listen" +} + +func (*ListenCommand) Synopsis() string { + return "listens for events on a Stockpile server" +} + +func (*ListenCommand) Usage() string { + return `Usage: stockpile listen [options] + +This command listens to all record changes processed by a given Stockpile server: + + $ stockpile listen + +Available command specific flags: + +` +} + +func (c *ListenCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + client, err := c.createClient() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to establish a connection to server \"%s\": %s\n", c.flagServerAddress, err) + return 1 + } + + eventService := rpc.NewEventServiceClient(client) + stream, err := eventService.StreamEvents(ctx, &rpc.EmptyRequest{}) + if err != nil { + fmt.Fprintf(os.Stderr, "Server responded with error: %s", err) + return 1 + } + + fmt.Fprintf(os.Stderr, "Listening for events:\n") + for true { + event, err := stream.Recv() + if err != nil { + if err == io.EOF { + fmt.Fprintf(os.Stderr, "End of stream") + break + } + + fmt.Fprintf(os.Stderr, "Failed to poll event from server: %s", err) + return 1 + } + + decoded, err := rpc.EventFromRpc(event) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to convert event: %s\n", err) + } + + var entry string + switch decoded.Type { + case cache.ProfileIdEvent: + profileId, _ := decoded.ProfileIdPayload() + entry = fmt.Sprintf("Updated name association for name \"%s\" to profile %s (valid from %s until %s)", profileId.Name, profileId.Id, profileId.FirstSeenAt, profileId.ValidUntil) + case cache.NameHistoryEvent: + id, _ := decoded.IdKey() + entry = fmt.Sprintf("Updated name history for profile %s", id) + case cache.ProfileEvent: + profile, _ := decoded.ProfilePayload() + entry = fmt.Sprintf("Updated profile %s (display name: \"%s\")", profile.Id, profile.Name) + case cache.BlacklistEvent: + entry = fmt.Sprintf("Updated blacklist") + default: + entry = "Unknown Event" + } + + fmt.Fprintf(os.Stdout, "[%s] %s\n", time.Now(), entry) + } + + return 0 +} diff --git a/stockpile/main.go b/stockpile/main.go index ee25957..1b8556f 100644 --- a/stockpile/main.go +++ b/stockpile/main.go @@ -35,6 +35,7 @@ func main() { subcommands.Register(&command.HistoryCommand{}, "Client") subcommands.Register(&command.ProfileCommand{}, "Client") subcommands.Register(&command.BlacklistCommand{}, "Client") + subcommands.Register(&command.ListenCommand{}, "Client") flag.Parse() ctx := context.Background() From 338b476262db1ade671246c136d5906443bbfa7e Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Fri, 6 Jul 2018 20:13:58 +0200 Subject: [PATCH 094/142] Corrected the reset ticker duration. --- stockpile/cache/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stockpile/cache/main.go b/stockpile/cache/main.go index 220f323..915d65a 100644 --- a/stockpile/cache/main.go +++ b/stockpile/cache/main.go @@ -42,7 +42,7 @@ func New(upstream *mojang.MojangAPI, storage storage.StorageBackend) *Cache { logger: logging.MustGetLogger("cache"), upstream: upstream, storage: storage, - resetTicker: time.NewTicker(time.Minute), + resetTicker: time.NewTicker(time.Minute * 10), Events: make(chan *Event), } go cache.resetRequestCounter() @@ -51,6 +51,7 @@ func New(upstream *mojang.MojangAPI, storage storage.StorageBackend) *Cache { // increments the request counter by one func (c *Cache) incrementRequestCounter() { + c.logger.Debugf("Incremented request counter") atomic.AddUint64(&c.requestCounter, 1) } From 2419a1ead5d71a955e1891a54194302e35b49917 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Fri, 6 Jul 2018 22:43:21 +0200 Subject: [PATCH 095/142] Corrected some issues with the parsing of timestamps. --- stockpile/mojang/name.go | 4 ++-- stockpile/mojang/profile.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/stockpile/mojang/name.go b/stockpile/mojang/name.go index bda3376..317d764 100644 --- a/stockpile/mojang/name.go +++ b/stockpile/mojang/name.go @@ -323,7 +323,7 @@ func (p *NameChange) read(reader io.Reader) error { } p.Name = parsed.Name - p.ChangedToAt = time.Unix(parsed.ChangedToAt, 0) + p.ChangedToAt = time.Unix(parsed.ChangedToAt / 1000, parsed.ChangedToAt % 1000 * 1000000) p.ValidUntil = CalculateNameGracePeriodEnd(p.ChangedToAt) return nil } @@ -337,7 +337,7 @@ func ReadNameChangeArray(reader io.Reader) ([]*NameChange, error) { res := make([]*NameChange, len(parsed)) for i, change := range parsed { - at := time.Unix(change.ChangedToAt, 0) + at := time.Unix(change.ChangedToAt / 1000, change.ChangedToAt % 1000 * 1000000) res[i] = &NameChange{ Name: change.Name, diff --git a/stockpile/mojang/profile.go b/stockpile/mojang/profile.go index 635adfb..dcdb6f5 100644 --- a/stockpile/mojang/profile.go +++ b/stockpile/mojang/profile.go @@ -153,7 +153,7 @@ func (p *Profile) read(reader io.Reader) error { } p.Textures = &ProfileTextures{ - Timestamp: time.Unix(parsedProp.Timestamp, 0), + Timestamp: time.Unix(parsedProp.Timestamp / 1000, parsedProp.Timestamp % 1000 * 1000000), ProfileId: id, ProfileName: parsedProp.ProfileName, Textures: textures, From 45e51330b484d4776bf0fd21b7cf3ed034db4a56 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 7 Jul 2018 03:45:27 +0200 Subject: [PATCH 096/142] Implemented a basic web UI. --- .gitignore | 1 + Gopkg.toml | 6 + Makefile | 17 +- Vagrantfile | 2 +- docs/default-config.hcl | 2 +- docs/dev-config.hcl | 2 +- docs/production-config.hcl | 1 + docs/redis-config.hcl | 1 + stockpile/Makefile | 19 +- stockpile/command/server.go | 49 +- stockpile/server/config.go | 14 + stockpile/server/rpc/utility.go | 4 +- stockpile/server/service/main.go | 25 +- stockpile/server/ui/main.go | 123 + stockpile/ui/.babelrc | 3 + stockpile/ui/.gitignore | 86 + stockpile/ui/package-lock.json | 7907 ++++++++++++++++++++++++++++++ stockpile/ui/package.json | 33 + stockpile/ui/src/index.html | 94 + stockpile/ui/src/script/app.js | 108 + stockpile/ui/src/style/app.css | 50 + stockpile/ui/webpack.config.js | 73 + 22 files changed, 8584 insertions(+), 36 deletions(-) create mode 100644 stockpile/server/ui/main.go create mode 100644 stockpile/ui/.babelrc create mode 100644 stockpile/ui/.gitignore create mode 100644 stockpile/ui/package-lock.json create mode 100644 stockpile/ui/package.json create mode 100644 stockpile/ui/src/index.html create mode 100644 stockpile/ui/src/script/app.js create mode 100644 stockpile/ui/src/style/app.css create mode 100644 stockpile/ui/webpack.config.js diff --git a/.gitignore b/.gitignore index 9f0ca08..14ef96e 100644 --- a/.gitignore +++ b/.gitignore @@ -29,5 +29,6 @@ vendor/ Gopkg.lock # Stockpile +*.gen.go *.pb.go build/ diff --git a/Gopkg.toml b/Gopkg.toml index 5c879c7..55f3fda 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -1,3 +1,9 @@ +required = ["github.com/elazarl/go-bindata-assetfs"] + [prune] go-tests = true unused-packages = true + +[[constraint]] + name = "github.com/elazarl/go-bindata-assetfs" + version = "1.0.0" diff --git a/Makefile b/Makefile index 1e4ec90..e0004db 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,8 @@ APPLICATION_TIMESTAMP := `date --utc "+%s"` GIT := $(shell command -v git 2> /dev/null) DEP := $(shell command -v dep 2> /dev/null) GO := $(shell command -v go 2> /dev/null) +NODE := $(shell command -v node 2> /dev/null) +NPM := $(shell command -v npm 2> /dev/null) export PLUGINS := $(wildcard plugins/*/.) @@ -28,10 +30,22 @@ endif @echo $(GO) @echo -n "Checking for dep ... " ifndef DEP - @echo -n "Not Found" + @echo "Not Found" $(error "dep is unavailable") endif @echo $(DEP) + @echo -n "Checking for node ... " +ifndef NODE + @echo "Not Found" + $(error "node is unavailable") +endif + @echo $(NODE) + @echo -n "Checking for npm ... " +ifndef NPM + @echo "Not Found" + $(error "npm is unavailable") +endif + @echo $(NPM) @echo "" print-config: @@ -51,6 +65,7 @@ clean: install-dependencies: @echo "==> Installing dependencies" @$(GO) get -u github.com/elazarl/go-bindata-assetfs/go-bindata-assetfs + @$(GO) get -u github.com/jteeuwen/go-bindata/go-bindata @$(DEP) ensure -v @echo "" diff --git a/Vagrantfile b/Vagrantfile index a0f5aaa..c1179d6 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -17,7 +17,7 @@ Vagrant.configure("2") do |config| config.vm.provision "shell", inline: <<-SHELL echo "==> Initializing basic build environment" sudo apt-get update - sudo apt-get install -y curl git golang make redis-server + sudo apt-get install -y curl git golang make nodejs npm redis-server export GOPATH=/opt/go export PATH="$PATH:/opt/go/bin" diff --git a/docs/default-config.hcl b/docs/default-config.hcl index c9ffea9..2be7f06 100644 --- a/docs/default-config.hcl +++ b/docs/default-config.hcl @@ -1,6 +1,6 @@ plugin-dir = "plugins" bind-address = "0.0.0.0:36623" -legacy-api = false +ui = true // no storage backend in default - required for actual operation // example: diff --git a/docs/dev-config.hcl b/docs/dev-config.hcl index 797a438..e957e52 100644 --- a/docs/dev-config.hcl +++ b/docs/dev-config.hcl @@ -1,6 +1,6 @@ plugin-dir = "plugins" bind-address = "127.0.0.1:36623" -legacy-api = true +ui = true storage "mem" { } diff --git a/docs/production-config.hcl b/docs/production-config.hcl index 5b6c96d..f9b51ea 100644 --- a/docs/production-config.hcl +++ b/docs/production-config.hcl @@ -1,5 +1,6 @@ plugin-dir = "plugins" bind-address = "127.0.0.1:36623" +ui = true // file storage is technically suited for small production deployments, however, a proper storage // server like redis is recommended for higher volumes diff --git a/docs/redis-config.hcl b/docs/redis-config.hcl index a9f7686..dd0555d 100644 --- a/docs/redis-config.hcl +++ b/docs/redis-config.hcl @@ -1,5 +1,6 @@ plugin-dir = "plugins" bind-address = "0.0.0.0:36623" +ui = true storage "redis" { address = "localhost:6379" diff --git a/stockpile/Makefile b/stockpile/Makefile index 012f804..1012e1d 100644 --- a/stockpile/Makefile +++ b/stockpile/Makefile @@ -1,15 +1,30 @@ PLATFORMS := darwin/386 darwin/amd64 linux/386 linux/amd64 linux/arm windows/386/.exe windows/amd64/.exe LDFLAGS :=-ldflags "-X github.com/dotStart/Stockpile/stockpile/metadata.brand=${APPLICATION_BRAND} -X github.com/dotStart/Stockpile/stockpile/metadata.version=${APPLICATION_VERSION} -X github.com/dotStart/Stockpile/stockpile/metadata.commitHash=${APPLICATION_COMMIT_HASH} -X \"github.com/dotStart/Stockpile/stockpile/metadata.timestampRaw=${APPLICATION_TIMESTAMP}\"" +DF = $(shell command -v df 2> /dev/null) + # magical formula: temp = $(subst /, ,$@) os = $(word 1, $(temp)) arch = $(word 2, $(temp)) ext = $(word 3, $(temp)) -build: $(PLATFORMS) +build: build-ui $(PLATFORMS) + +.ONESHELL: +build-ui: + @cd ui + @NPM_FLAGS= +ifdef DF + @if $(DF) -t vboxsf . > /dev/null 2> /dev/null; then \ + echo "VBox filesystem detected - Enabling compatibility flags"; \ + NPM_FLAGS=--no-bin-links; \ + fi +endif + @"$(NPM)" install $(NPM_FLAGS) + @"$(NPM)" run build $(PLATFORMS): - @export GOOS=$(os); export GOARCH=$(arch); $(GO) build -v ${LDFLAGS} -o ../build/$(os)-$(arch)/stockpile$(ext) + @export GOOS=$(os); export GOARCH=$(arch); $(GO) generate -v github.com/dotStart/Stockpile/stockpile/server/rpc github.com/dotStart/Stockpile/stockpile/server/ui && $(GO) build -v ${LDFLAGS} -o ../build/$(os)-$(arch)/stockpile$(ext) .PHONY: build diff --git a/stockpile/command/server.go b/stockpile/command/server.go index 9b15cb7..fe220c0 100644 --- a/stockpile/command/server.go +++ b/stockpile/command/server.go @@ -21,20 +21,26 @@ import ( "flag" "fmt" "net" + "net/http" "os" + "github.com/dotStart/Stockpile/stockpile/cache" "github.com/dotStart/Stockpile/stockpile/metadata" + "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/dotStart/Stockpile/stockpile/plugin" "github.com/dotStart/Stockpile/stockpile/server" "github.com/dotStart/Stockpile/stockpile/server/service" + "github.com/dotStart/Stockpile/stockpile/server/ui" "github.com/google/subcommands" "github.com/op/go-logging" "github.com/soheilhy/cmux" ) type ServerCommand struct { - flagConfig string - flagDevelopment bool - flagLogLevel string + flagConfig string + flagDevelopment bool + flagLogLevel string + flagCorsOverride string } func (*ServerCommand) Name() string { @@ -70,6 +76,7 @@ func (c *ServerCommand) SetFlags(f *flag.FlagSet) { f.StringVar(&c.flagConfig, "config", "", "specifies a configuration file or directory") f.BoolVar(&c.flagDevelopment, "dev", false, "enables development mode") f.StringVar(&c.flagLogLevel, "log-level", "info", "specifies a log level") + f.StringVar(&c.flagCorsOverride, "cors-override", "", "specifies a host from which CORS requests are permitted") } func (c *ServerCommand) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { @@ -130,13 +137,28 @@ func (c *ServerCommand) Execute(_ context.Context, f *flag.FlagSet, _ ...interfa mux := cmux.New(listener) + // initialize the plugin system and cache manager + pluginManager := plugin.NewManager(*cfg.PluginDir) + pluginManager.LoadAll() + + storageFactory := pluginManager.Context.GetStorageBackend(cfg.Storage.Type) + if storageFactory == nil { + log.Fatalf("no such storage backend: %s", cfg.Storage.Type) + } + storage, err := storageFactory(cfg) + if err != nil { + log.Fatalf("failed to initialize storage backend \"%s\": %s", err) + } + log.Infof("Using database plugin: %s", cfg.Storage.Type) + cacheImpl := cache.New(mojang.New(), storage) + // initialize the RPC server at all times (only differ between mux policies depending on whether the legacy API or UI // is enabled) rpcPolicy := cmux.Any() - if true { // TODO: UI flag + if *cfg.UiEnabled { rpcPolicy = cmux.HTTP2HeaderField("content-type", "application/grpc") } - rpcServer, err := service.NewServer(cfg) + rpcServer, err := service.NewServer(cacheImpl) if err != nil { log.Fatalf("Failed to initialize grpc server: %s", err) } @@ -144,6 +166,23 @@ func (c *ServerCommand) Execute(_ context.Context, f *flag.FlagSet, _ ...interfa defer rpcServer.Destroy() log.Info("Enabled grpc server") + if *cfg.UiEnabled { + httpMux := http.NewServeMux() + + if c.flagCorsOverride != "" { + log.Warningf("CORS override configured: %s", c.flagCorsOverride) + } + + // instance currently unused + ui.NewServer(httpMux, c.flagCorsOverride, cacheImpl) + + httpSrv := &http.Server{ + Handler: httpMux, + } + go httpSrv.Serve(mux.Match(cmux.Any())) + log.Info("Enabled ui") + } + mux.Serve() return 0 } diff --git a/stockpile/server/config.go b/stockpile/server/config.go index bb1d57f..1e53f1e 100644 --- a/stockpile/server/config.go +++ b/stockpile/server/config.go @@ -41,6 +41,7 @@ const DefaultPort = 36623 type Config struct { PluginDir *string `hcl:"plugin-dir"` BindAddress *string `hcl:"bind-address,attr"` + UiEnabled *bool `hcl:"ui,attr"` Storage *StorageConfig `hcl:"storage,block"` Ttl *TtlConfig `hcl:"ttl,block"` } @@ -73,10 +74,12 @@ func EmptyConfig() *Config { func DefaultConfig() *Config { pluginDir := "plugins" addr := fmt.Sprintf("%s:%d", "127.0.0.1", DefaultPort) + uiEnabled := false cfg := &Config{ PluginDir: &pluginDir, BindAddress: &addr, + UiEnabled: &uiEnabled, Storage: &StorageConfig{ Type: "mem", }, @@ -99,7 +102,10 @@ func DefaultConfig() *Config { } func DevelopmentConfig() *Config { + uiEnabled := true + return DefaultConfig().Merge(&Config{ + UiEnabled: &uiEnabled, }) } @@ -177,6 +183,10 @@ func (c *Config) Merge(other *Config) *Config { c.BindAddress = other.BindAddress } + if other.UiEnabled != nil { + c.UiEnabled = other.UiEnabled + } + if c.Storage == nil { c.Storage = other.Storage } else if other.Storage != nil { @@ -260,6 +270,10 @@ func (c *Config) validate() error { return errors.New("missing bind address") } + if c.UiEnabled == nil { + return errors.New("missing ui flag") + } + if c.Storage == nil { return errors.New("missing storage backend configuration") } diff --git a/stockpile/server/rpc/utility.go b/stockpile/server/rpc/utility.go index 9e72af5..3df9cd7 100644 --- a/stockpile/server/rpc/utility.go +++ b/stockpile/server/rpc/utility.go @@ -23,8 +23,8 @@ import ( "github.com/dotStart/Stockpile/stockpile/cache" "github.com/dotStart/Stockpile/stockpile/mojang" - "github.com/dotStart/Stockpile/vendor.orig/github.com/golang/protobuf/proto" - "github.com/dotStart/Stockpile/vendor.orig/github.com/golang/protobuf/ptypes" + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes" "github.com/golang/protobuf/ptypes/any" "github.com/google/uuid" ) diff --git a/stockpile/server/service/main.go b/stockpile/server/service/main.go index a418cf9..de0f45d 100644 --- a/stockpile/server/service/main.go +++ b/stockpile/server/service/main.go @@ -19,13 +19,9 @@ package service //go:generate protoc -I ../rpc --go_out=plugins=grpc:../rpc common.proto events.proto profile.proto server.proto import ( - "fmt" "net" "github.com/dotStart/Stockpile/stockpile/cache" - "github.com/dotStart/Stockpile/stockpile/mojang" - "github.com/dotStart/Stockpile/stockpile/plugin" - "github.com/dotStart/Stockpile/stockpile/server" "github.com/dotStart/Stockpile/stockpile/server/rpc" "github.com/op/go-logging" "google.golang.org/grpc" @@ -35,35 +31,18 @@ import ( // Represents an RPC server type Server struct { logger *logging.Logger - cfg *server.Config - plugin *plugin.Manager cache *cache.Cache srv *grpc.Server } // Constructs a new RPC server instance and starts it -func NewServer(config *server.Config) (*Server, error) { +func NewServer(cache *cache.Cache) (*Server, error) { logger := logging.MustGetLogger("rpc") - plugin := plugin.NewManager(*config.PluginDir) - plugin.LoadAll() - - storageFactory := plugin.Context.GetStorageBackend(config.Storage.Type) - if storageFactory == nil { - return nil, fmt.Errorf("no such storage backend: %s", config.Storage.Type) - } - storage, err := storageFactory(config) - if err != nil { - return nil, fmt.Errorf("failed to initialize storage backend \"%s\": %s", err) - } - logger.Infof("Using database plugin: %s", config.Storage.Type) - return &Server{ logger: logger, - cfg: config, - plugin: plugin, - cache: cache.New(mojang.New(), storage), + cache: cache, }, nil } diff --git a/stockpile/server/ui/main.go b/stockpile/server/ui/main.go new file mode 100644 index 0000000..f574e0d --- /dev/null +++ b/stockpile/server/ui/main.go @@ -0,0 +1,123 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 ui + +import ( + "net/http" + "time" + + "github.com/dotStart/Stockpile/stockpile/cache" + "github.com/dotStart/Stockpile/stockpile/metadata" + "github.com/googollee/go-socket.io" + "github.com/op/go-logging" +) + +//go:generate go-bindata-assetfs -pkg ui -o data.gen.go -prefix ../../ui/dist -ignore semantic.(js|css|min.js) ../../ui/dist/... + +type Server struct { + logger *logging.Logger + io *socketio.Server + cache *cache.Cache + + rateLimitTicker *time.Ticker + + corsOverride string +} + +func NewServer(httpSrv *http.ServeMux, corsOverride string, cacheImpl *cache.Cache) (*Server, error) { + io, err := socketio.NewServer(nil) + if err != nil { + return nil, err + } + + srv := &Server{ + logger: logging.MustGetLogger("ui"), + io: io, + cache: cacheImpl, + + rateLimitTicker: time.NewTicker(time.Minute), + + corsOverride: corsOverride, + } + + io.On("connection", srv.onSocketConnect) + io.On("disconnection", srv.onSocketDisconnect) + go srv.forwardRateLimit() + go srv.forwardCacheEvents() + + httpSrv.HandleFunc("/ui/socket.io/", srv.handleSocket) + httpSrv.Handle("/ui/", http.StripPrefix("/ui/", http.FileServer(assetFS()))) + httpSrv.HandleFunc("/", srv.handleRootRequest) + return srv, nil +} + +// handles requests to the root endpoint (either by redirecting to the UI endpoint or by responding +// with a 404) +func (s *Server) handleRootRequest(w http.ResponseWriter, req *http.Request) { + if req.URL.Path != "/" { + http.NotFound(w, req) + return + } + + http.Redirect(w, req, "/ui/", http.StatusFound) +} + +func (s *Server) handleSocket(w http.ResponseWriter, req *http.Request) { + if s.corsOverride != "" { + w.Header().Set("Access-Control-Allow-Origin", s.corsOverride) + w.Header().Set("Access-Control-Allow-Credentials", "true") + } + s.io.ServeHTTP(w, req) +} + +// forwards the current rate limit to connected clients +func (s *Server) forwardRateLimit() { + for range s.rateLimitTicker.C { + s.io.BroadcastTo("ui", "rate-limit", s.cache.GetRateLimitAllocation()) + } +} + +// forwards all cache events to connected clients +func (s *Server) forwardCacheEvents() { + for e := range s.cache.Events { + // TODO: socket.io eats serialization errors here - use this to debug until this issue is fixed + /*_, err := json.Marshal(e) + if err != nil { + s.logger.Errorf("ENCODING ERROR: %s %v", err, e) + } */ + s.logger.Debugf("forwarding event of type %T (using key %T) to active clients", e.Object, e.Key) + s.io.BroadcastTo("ui", "cache", e) + } +} + +func (s *Server) onSocketConnect(io socketio.Socket) { + s.logger.Debugf("client %s (id: %s) established websocket connection", io.Request().RemoteAddr, io.Id()) + io.Join("ui") + io.Emit( + "system", + struct { + Version string `json:"version"` + }{ + Version: metadata.VersionFull(), + }, + ) + io.Emit("rate-limit", s.cache.GetRateLimitAllocation()) +} + +func (s *Server) onSocketDisconnect(io socketio.Socket) { + s.logger.Debugf("client %s (id %s) has disconnected", io.Request().RemoteAddr, io.Id()) +} diff --git a/stockpile/ui/.babelrc b/stockpile/ui/.babelrc new file mode 100644 index 0000000..002b4aa --- /dev/null +++ b/stockpile/ui/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["env"] +} diff --git a/stockpile/ui/.gitignore b/stockpile/ui/.gitignore new file mode 100644 index 0000000..ac73db3 --- /dev/null +++ b/stockpile/ui/.gitignore @@ -0,0 +1,86 @@ + +# Created by https://www.gitignore.io/api/node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + + +# End of https://www.gitignore.io/api/node + +### Webpack ### +dist/ + +### Semantic UI ### +semantic/ diff --git a/stockpile/ui/package-lock.json b/stockpile/ui/package-lock.json new file mode 100644 index 0000000..9725721 --- /dev/null +++ b/stockpile/ui/package-lock.json @@ -0,0 +1,7907 @@ +{ + "name": "stockpile", + "version": "2.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@webassemblyjs/ast": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.5.13.tgz", + "integrity": "sha1-gRVaVwvVgDow7DFDa8LJwO3jjyU=", + "dev": true, + "requires": { + "@webassemblyjs/helper-module-context": "1.5.13", + "@webassemblyjs/helper-wasm-bytecode": "1.5.13", + "@webassemblyjs/wast-parser": "1.5.13", + "debug": "3.1.0", + "mamacro": "0.0.3" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.5.13.tgz", + "integrity": "sha1-Kc4LqpdBH3DozOaM6cD52Bmk4pg=", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.5.13.tgz", + "integrity": "sha1-5JsFHWfuGaVuKbmqi9lJtbREKlk=", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.5.13.tgz", + "integrity": "sha1-hzuwobRkSSMRN8EmLd/QVpUZWh4=", + "dev": true, + "requires": { + "debug": "3.1.0" + } + }, + "@webassemblyjs/helper-code-frame": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.5.13.tgz", + "integrity": "sha1-G9IYG2oL4U4ATw/p9aZg0mU2K1g=", + "dev": true, + "requires": { + "@webassemblyjs/wast-printer": "1.5.13" + } + }, + "@webassemblyjs/helper-fsm": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.5.13.tgz", + "integrity": "sha1-zfPZ0zAF1UOlxeWtqr9nn/qNuSQ=", + "dev": true + }, + "@webassemblyjs/helper-module-context": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.5.13.tgz", + "integrity": "sha1-3Cnd+1HtZXZVKG+UpdctikiRR8U=", + "dev": true, + "requires": { + "debug": "3.1.0", + "mamacro": "0.0.3" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.5.13.tgz", + "integrity": "sha1-AyRYF/CnYjguYXMxRvV3Pe8Vp0c=", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.5.13.tgz", + "integrity": "sha1-78dvRKENMHO1hLQ8OKF53xc9XH0=", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-buffer": "1.5.13", + "@webassemblyjs/helper-wasm-bytecode": "1.5.13", + "@webassemblyjs/wasm-gen": "1.5.13", + "debug": "3.1.0" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.5.13.tgz", + "integrity": "sha1-Vz6XyMEuTuuzFspf3gID3dkLA2Q=", + "dev": true, + "requires": { + "ieee754": "1.1.12" + } + }, + "@webassemblyjs/leb128": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.5.13.tgz", + "integrity": "sha1-q1Lrq5zsKDwcGJesHagzoEo/TO4=", + "dev": true, + "requires": { + "long": "4.0.0" + }, + "dependencies": { + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha1-mntxz7fTYaGU6lVSQckvdGjVvyg=", + "dev": true + } + } + }, + "@webassemblyjs/utf8": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.5.13.tgz", + "integrity": "sha1-a1PSzYYc+U+pnB8Sd53eaS+8JGk=", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.5.13.tgz", + "integrity": "sha1-yc71ZkwkXPEbOzpzEQyRVYMXJKg=", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-buffer": "1.5.13", + "@webassemblyjs/helper-wasm-bytecode": "1.5.13", + "@webassemblyjs/helper-wasm-section": "1.5.13", + "@webassemblyjs/wasm-gen": "1.5.13", + "@webassemblyjs/wasm-opt": "1.5.13", + "@webassemblyjs/wasm-parser": "1.5.13", + "@webassemblyjs/wast-printer": "1.5.13", + "debug": "3.1.0" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.5.13.tgz", + "integrity": "sha1-jm6hE8S0MvpmVAGJ55sW16FAcA4=", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-wasm-bytecode": "1.5.13", + "@webassemblyjs/ieee754": "1.5.13", + "@webassemblyjs/leb128": "1.5.13", + "@webassemblyjs/utf8": "1.5.13" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.5.13.tgz", + "integrity": "sha1-FHqtdxen7kIRw2shpfTDDd3zMTg=", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-buffer": "1.5.13", + "@webassemblyjs/wasm-gen": "1.5.13", + "@webassemblyjs/wasm-parser": "1.5.13", + "debug": "3.1.0" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.5.13.tgz", + "integrity": "sha1-b0ZRbFuyOQT731gAkjPC3YpUxy8=", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-api-error": "1.5.13", + "@webassemblyjs/helper-wasm-bytecode": "1.5.13", + "@webassemblyjs/ieee754": "1.5.13", + "@webassemblyjs/leb128": "1.5.13", + "@webassemblyjs/utf8": "1.5.13" + } + }, + "@webassemblyjs/wast-parser": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.5.13.tgz", + "integrity": "sha1-VyenBdOXrmo66Z1/VGCs8uxkbuo=", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/floating-point-hex-parser": "1.5.13", + "@webassemblyjs/helper-api-error": "1.5.13", + "@webassemblyjs/helper-code-frame": "1.5.13", + "@webassemblyjs/helper-fsm": "1.5.13", + "long": "3.2.0", + "mamacro": "0.0.3" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.5.13.tgz", + "integrity": "sha1-uzTVKMFLT1eefsEeeT7FCtfNfJU=", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/wast-parser": "1.5.13", + "long": "3.2.0" + } + }, + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "dev": true, + "requires": { + "mime-types": "2.1.18", + "negotiator": "0.6.1" + } + }, + "acorn": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz", + "integrity": "sha1-8JWCkpdwanyXdpWMCvyJMKm52dg=", + "dev": true + }, + "acorn-dynamic-import": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz", + "integrity": "sha1-kBzu5Mf6rvfgetKkfokGddpQong=", + "dev": true, + "requires": { + "acorn": "5.7.1" + } + }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, + "ajv": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.2.tgz", + "integrity": "sha512-hOs7GfvI6tUI1LfZddH82ky6mOMyTuY0mk7kE2pWpmhhUSkumzaTO5vbVwij39MdwPQWCV4Zv57Eo06NtL/GVA==", + "dev": true, + "requires": { + "fast-deep-equal": "2.0.1", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.4.1", + "uri-js": "4.2.2" + } + }, + "ajv-keywords": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", + "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "dev": true + }, + "ansi-escapes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", + "dev": true + }, + "ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.2" + } + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "3.1.10", + "normalize-path": "2.1.1" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=", + "dev": true + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-flatten": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz", + "integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=", + "dev": true + }, + "array-includes": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", + "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.12.0" + } + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha1-O7xCdd1YTMGxCAm4nU6LY6aednU=" + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha1-ucK/WAXx5kqt7tbfOiv6+1pz9aA=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true, + "requires": { + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha1-ePrtjD0HSrgfIrTphdeehzj3IPg=" + }, + "atob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", + "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "babel-core": { + "version": "6.26.3", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", + "integrity": "sha1-suLwnjQtDwyI4vAuBneUEl51wgc=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-generator": "6.26.1", + "babel-helpers": "6.24.1", + "babel-messages": "6.23.0", + "babel-register": "6.26.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "convert-source-map": "1.5.1", + "debug": "2.6.9", + "json5": "0.5.1", + "lodash": "4.17.10", + "minimatch": "3.0.4", + "path-is-absolute": "1.0.1", + "private": "0.1.8", + "slash": "1.0.0", + "source-map": "0.5.7" + }, + "dependencies": { + "convert-source-map": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", + "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha1-GERAjTuPDTWkBOp6wYDwh6YBvZA=", + "dev": true, + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.10", + "source-map": "0.5.7", + "trim-right": "1.0.1" + }, + "dependencies": { + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "jsesc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + } + } + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", + "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", + "dev": true, + "requires": { + "babel-helper-explode-assignable-expression": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "6.24.1", + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.10" + } + }, + "babel-helper-explode-assignable-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", + "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "dev": true, + "requires": { + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.10" + } + }, + "babel-helper-remap-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", + "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "dev": true, + "requires": { + "babel-helper-optimise-call-expression": "6.24.1", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-helper-vue-jsx-merge-props": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz", + "integrity": "sha1-Iq69OzOQIyjlEyk6jkmSs4T58bY=" + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-loader": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-7.1.5.tgz", + "integrity": "sha1-4+4M1zlKpVfgE7AtPkkr/QeqbWg=", + "dev": true, + "requires": { + "find-cache-dir": "1.0.0", + "loader-utils": "1.1.0", + "mkdirp": "0.5.1" + }, + "dependencies": { + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } + } + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", + "dev": true + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", + "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", + "dev": true + }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", + "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", + "dev": true + }, + "babel-plugin-transform-async-to-generator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", + "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", + "dev": true, + "requires": { + "babel-helper-remap-async-to-generator": "6.24.1", + "babel-plugin-syntax-async-functions": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "lodash": "4.17.10" + } + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "dev": true, + "requires": { + "babel-helper-define-map": "6.26.0", + "babel-helper-function-name": "6.24.1", + "babel-helper-optimise-call-expression": "6.24.1", + "babel-helper-replace-supers": "6.24.1", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "dev": true, + "requires": { + "babel-helper-function-name": "6.24.1", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-commonjs": "6.26.2", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", + "integrity": "sha1-WKeThjqefKhwvcWogRF/+sJ9tvM=", + "dev": true, + "requires": { + "babel-plugin-transform-strict-mode": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0" + } + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "dev": true, + "requires": { + "babel-helper-replace-supers": "6.24.1", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "dev": true, + "requires": { + "babel-helper-call-delegate": "6.24.1", + "babel-helper-get-function-arity": "6.24.1", + "babel-runtime": "6.26.0", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "dev": true, + "requires": { + "babel-helper-regex": "6.26.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "dev": true, + "requires": { + "babel-helper-regex": "6.26.0", + "babel-runtime": "6.26.0", + "regexpu-core": "2.0.0" + }, + "dependencies": { + "regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "dev": true, + "requires": { + "regenerate": "1.4.0", + "regjsgen": "0.2.0", + "regjsparser": "0.1.5" + } + } + } + }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", + "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", + "dev": true, + "requires": { + "babel-helper-builder-binary-assignment-operator-visitor": "6.24.1", + "babel-plugin-syntax-exponentiation-operator": "6.13.0", + "babel-runtime": "6.26.0" + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", + "dev": true, + "requires": { + "regenerator-transform": "0.10.1" + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0" + } + }, + "babel-preset-env": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.7.0.tgz", + "integrity": "sha1-3qefpOvriDzTXasH4mDBycBN93o=", + "dev": true, + "requires": { + "babel-plugin-check-es2015-constants": "6.22.0", + "babel-plugin-syntax-trailing-function-commas": "6.22.0", + "babel-plugin-transform-async-to-generator": "6.24.1", + "babel-plugin-transform-es2015-arrow-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "6.22.0", + "babel-plugin-transform-es2015-block-scoping": "6.26.0", + "babel-plugin-transform-es2015-classes": "6.24.1", + "babel-plugin-transform-es2015-computed-properties": "6.24.1", + "babel-plugin-transform-es2015-destructuring": "6.23.0", + "babel-plugin-transform-es2015-duplicate-keys": "6.24.1", + "babel-plugin-transform-es2015-for-of": "6.23.0", + "babel-plugin-transform-es2015-function-name": "6.24.1", + "babel-plugin-transform-es2015-literals": "6.22.0", + "babel-plugin-transform-es2015-modules-amd": "6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "6.26.2", + "babel-plugin-transform-es2015-modules-systemjs": "6.24.1", + "babel-plugin-transform-es2015-modules-umd": "6.24.1", + "babel-plugin-transform-es2015-object-super": "6.24.1", + "babel-plugin-transform-es2015-parameters": "6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "6.24.1", + "babel-plugin-transform-es2015-spread": "6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "6.24.1", + "babel-plugin-transform-es2015-template-literals": "6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "6.23.0", + "babel-plugin-transform-es2015-unicode-regex": "6.24.1", + "babel-plugin-transform-exponentiation-operator": "6.24.1", + "babel-plugin-transform-regenerator": "6.26.0", + "browserslist": "3.2.8", + "invariant": "2.2.4", + "semver": "5.5.0" + }, + "dependencies": { + "browserslist": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-3.2.8.tgz", + "integrity": "sha1-sABTYdZHHw9ZUnl6dvyYXx+Xj8Y=", + "dev": true, + "requires": { + "caniuse-lite": "1.0.30000864", + "electron-to-chromium": "1.3.51" + } + } + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "dev": true, + "requires": { + "babel-core": "6.26.3", + "babel-runtime": "6.26.0", + "core-js": "2.5.7", + "home-or-tmp": "2.0.0", + "lodash": "4.17.10", + "mkdirp": "0.5.1", + "source-map-support": "0.4.18" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "2.5.7", + "regenerator-runtime": "0.11.1" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.10" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.4", + "lodash": "4.17.10" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.10", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha1-ry87iPpvXB5MY00aD46sT1WzleM=", + "dev": true + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha1-e95c7RRbbVUakNuH+DxVi060io8=", + "dev": true, + "requires": { + "cache-base": "1.0.1", + "class-utils": "0.3.6", + "component-emitter": "1.2.1", + "define-property": "1.0.0", + "isobject": "3.0.1", + "mixin-deep": "1.3.1", + "pascalcase": "0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha1-yrHmEY8FEJXli1KBrqjBzSK/wOM=", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha1-pfwpi4G54Nyi5FiCR4S2XFK6WI4=", + "dev": true + }, + "binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", + "dev": true + }, + "blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=" + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha1-2VUfnemPH82h5oPRfukaBgLuLrk=", + "dev": true + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha1-LN4J617jQfSEdGuwMJsyU7GxRC8=", + "dev": true + }, + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "content-type": "1.0.4", + "debug": "2.6.9", + "depd": "1.1.2", + "http-errors": "1.6.3", + "iconv-lite": "0.4.19", + "on-finished": "2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "1.6.16" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha1-90aPYBNfXl2tM5nAqBvpoWA6CCs=", + "dev": true + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "requires": { + "array-flatten": "2.1.1", + "deep-equal": "1.0.1", + "dns-equal": "1.0.0", + "dns-txt": "2.0.2", + "multicast-dns": "6.2.3", + "multicast-dns-service-types": "1.1.0" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha1-WXn9PxTNUxVl5fot8av/8d+u5yk=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "repeat-element": "1.1.2", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha1-Mmc0ZC9APavDADIJhTu3CtQo70g=", + "dev": true, + "requires": { + "buffer-xor": "1.0.3", + "cipher-base": "1.0.4", + "create-hash": "1.2.0", + "evp_bytestokey": "1.0.3", + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha1-jWR0wbhwv9q807z8wZNKEOlPFfA=", + "dev": true, + "requires": { + "browserify-aes": "1.2.0", + "browserify-des": "1.0.1", + "evp_bytestokey": "1.0.3" + } + }, + "browserify-des": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.1.tgz", + "integrity": "sha1-M0MSTbbXrVPiaogmMYcSvchFD5w=", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "des.js": "1.0.0", + "inherits": "2.0.3" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "randombytes": "2.0.6" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.2.0", + "create-hmac": "1.1.7", + "elliptic": "6.4.0", + "inherits": "2.0.3", + "parse-asn1": "5.1.1" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha1-KGlFnZqjviRf6P4sofRuLn9U1z8=", + "dev": true, + "requires": { + "pako": "1.0.6" + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "1.3.0", + "ieee754": "1.1.12", + "isarray": "1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "buffer-from": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", + "integrity": "sha1-h/yqOimDWOCt5uRCz86EB0DRrQQ=", + "dev": true + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha1-Uvq8xqYG0aADAoAmSO9o9jnaJow=", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "cacache": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", + "integrity": "sha1-ZFI2eZnv+dQYiu/ZoU6dfGomNGA=", + "dev": true, + "requires": { + "bluebird": "3.5.1", + "chownr": "1.0.1", + "glob": "7.1.2", + "graceful-fs": "4.1.11", + "lru-cache": "4.1.3", + "mississippi": "2.0.0", + "mkdirp": "0.5.1", + "move-concurrently": "1.0.1", + "promise-inflight": "1.0.1", + "rimraf": "2.6.2", + "ssri": "5.3.0", + "unique-filename": "1.1.0", + "y18n": "4.0.0" + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha1-Cn9GQWgxyLZi7jb+TnxZ129marI=", + "dev": true, + "requires": { + "collection-visit": "1.0.0", + "component-emitter": "1.2.1", + "get-value": "2.0.6", + "has-value": "1.0.0", + "isobject": "3.0.1", + "set-value": "2.0.0", + "to-object-path": "0.3.0", + "union-value": "1.0.0", + "unset-value": "1.0.0" + } + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, + "camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", + "dev": true, + "requires": { + "no-case": "2.3.2", + "upper-case": "1.1.3" + } + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "2.1.1", + "map-obj": "1.0.1" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, + "caniuse-lite": { + "version": "1.0.30000864", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000864.tgz", + "integrity": "sha1-egjHjaZw8jwG8RqpGIMbjy3WDdw=", + "dev": true + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.4.0" + } + }, + "chardet": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.5.0.tgz", + "integrity": "sha512-9ZTaoBaePSCFvNlNGrsyI8ZVACP2svUtq0DkM7t4K2ClAa96sqOIRjAzDTc8zXzFt1cZR46rRzLTiHFSJ+Qw0g==", + "dev": true + }, + "chokidar": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "dev": true, + "requires": { + "anymatch": "2.0.0", + "async-each": "1.0.1", + "braces": "2.3.2", + "fsevents": "1.2.4", + "glob-parent": "3.1.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "4.0.0", + "lodash.debounce": "4.0.8", + "normalize-path": "2.1.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0", + "upath": "1.1.0" + } + }, + "chownr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz", + "integrity": "sha1-Rakb0sIMlBHwljtarrmhuV4JzEg=", + "dev": true, + "requires": { + "tslib": "1.9.3" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha1-h2Dk7MJy9MNjUy+SbYdKriwTl94=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha1-+TNprouafOAv1B+q0MqDAzGQxGM=", + "dev": true, + "requires": { + "arr-union": "3.1.0", + "define-property": "0.2.5", + "isobject": "3.0.1", + "static-extend": "0.1.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "clean-css": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", + "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "wrap-ansi": "2.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "1.0.0", + "object-visit": "1.0.1" + } + }, + "color-convert": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", + "integrity": "sha1-SYgbj7pn3xKpa98/VsCqueeRMUc=", + "dev": true, + "requires": { + "color-name": "1.1.1" + } + }, + "color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", + "dev": true + }, + "commander": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.16.0.tgz", + "integrity": "sha1-8WOQWTmWzrTz7rAgsx14Uo9/ilA=", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + }, + "compressible": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.14.tgz", + "integrity": "sha1-MmxfUH+7BV9UEWeCuWmoG2einac=", + "dev": true, + "requires": { + "mime-db": "1.34.0" + }, + "dependencies": { + "mime-db": { + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.34.0.tgz", + "integrity": "sha1-RS0Oz/XDA0am3B5kseruDTcZ/5o=", + "dev": true + } + } + }, + "compression": { + "version": "1.7.2", + "resolved": "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz", + "integrity": "sha1-qv+81qr4VLROuygDU9WtFlH1mmk=", + "dev": true, + "requires": { + "accepts": "1.3.5", + "bytes": "3.0.0", + "compressible": "2.0.14", + "debug": "2.6.9", + "on-headers": "1.0.1", + "safe-buffer": "5.1.1", + "vary": "1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha1-kEvfGUzTEi/Gdcd/xKw9T/D9GjQ=", + "dev": true, + "requires": { + "buffer-from": "1.1.0", + "inherits": "2.0.3", + "readable-stream": "2.3.6", + "typedarray": "0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "connect-history-api-fallback": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", + "integrity": "sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=", + "dev": true + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "0.1.4" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", + "dev": true + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=", + "dev": true + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha1-kilzmMrjSTf8r9bsgTnBgFHwteA=", + "dev": true, + "requires": { + "aproba": "1.2.0", + "fs-write-stream-atomic": "1.0.10", + "iferr": "0.1.5", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "run-queue": "1.0.3" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-js": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha1-+XJgj/DOrWi4QaFqky0LGDeRgU4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha1-yREbbzMEXEaX8UR4f5JUzcd8Rf8=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "elliptic": "6.4.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha1-iJB4rxGmN1a8+1m9IhmWvjqe8ZY=", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "inherits": "2.0.3", + "md5.js": "1.3.4", + "ripemd160": "2.0.2", + "sha.js": "2.4.11" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha1-aRcMeLOrlXFHsriwRXLkfq0iQ/8=", + "dev": true, + "requires": { + "cipher-base": "1.0.4", + "create-hash": "1.2.0", + "inherits": "2.0.3", + "ripemd160": "2.0.2", + "safe-buffer": "5.1.2", + "sha.js": "2.4.11" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha1-Sl7Hxk364iw6FBJNus3uhG2Ay8Q=", + "dev": true, + "requires": { + "nice-try": "1.0.4", + "path-key": "2.0.1", + "semver": "5.5.0", + "shebang-command": "1.2.0", + "which": "1.3.1" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha1-OWz58xN/A+S45TLFj2mCVOAPgOw=", + "dev": true, + "requires": { + "browserify-cipher": "1.0.1", + "browserify-sign": "4.0.4", + "create-ecdh": "4.0.3", + "create-hash": "1.2.0", + "create-hmac": "1.1.7", + "diffie-hellman": "5.0.3", + "inherits": "2.0.3", + "pbkdf2": "3.0.16", + "public-encrypt": "4.0.2", + "randombytes": "2.0.6", + "randomfill": "1.0.4" + } + }, + "css-loader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-1.0.0.tgz", + "integrity": "sha512-tMXlTYf3mIMt3b0dDCOQFJiVvxbocJ5Ho577WiGPYPZcqVEO218L2iU22pDXzkTZCLDE+9AmGSUkWxeh/nZReA==", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "css-selector-tokenizer": "0.7.0", + "icss-utils": "2.1.0", + "loader-utils": "1.1.0", + "lodash.camelcase": "4.3.0", + "postcss": "6.0.23", + "postcss-modules-extract-imports": "1.2.0", + "postcss-modules-local-by-default": "1.2.0", + "postcss-modules-scope": "1.1.0", + "postcss-modules-values": "1.3.0", + "postcss-value-parser": "3.3.0", + "source-list-map": "2.0.0" + }, + "dependencies": { + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } + } + } + }, + "css-select": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "dev": true, + "requires": { + "boolbase": "1.0.0", + "css-what": "2.1.0", + "domutils": "1.5.1", + "nth-check": "1.0.1" + } + }, + "css-selector-tokenizer": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz", + "integrity": "sha1-5piEdK6MlTR3v15+/s/OzNnPTIY=", + "dev": true, + "requires": { + "cssesc": "0.1.0", + "fastparse": "1.1.1", + "regexpu-core": "1.0.0" + } + }, + "css-what": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", + "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=", + "dev": true + }, + "cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", + "dev": true + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } + }, + "cyclist": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", + "dev": true + }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true, + "requires": { + "es5-ext": "0.10.45" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, + "define-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", + "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "dev": true, + "requires": { + "foreach": "2.0.5", + "object-keys": "1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha1-1Flono1lS6d+AqgX+HENcCyxbp0=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2", + "isobject": "3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "del": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", + "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "dev": true, + "requires": { + "globby": "6.1.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.1", + "p-map": "1.2.0", + "pify": "3.0.0", + "rimraf": "2.6.2" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1" + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-node": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.3.tgz", + "integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha1-QOjumPVaIUlgcUaSHGPhrl89KHU=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "miller-rabin": "4.0.1", + "randombytes": "2.0.6" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha1-EqpCaYEHW+UAuRDu3NC0fdfe2lo=", + "dev": true, + "requires": { + "ip": "1.1.5", + "safe-buffer": "5.1.2" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "requires": { + "buffer-indexof": "1.1.1" + } + }, + "dom-converter": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz", + "integrity": "sha1-pF71cnuJDJv/5tfIduexnLDhfzs=", + "dev": true, + "requires": { + "utila": "0.3.3" + }, + "dependencies": { + "utila": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", + "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", + "dev": true + } + } + }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "dev": true, + "requires": { + "domelementtype": "1.1.3", + "entities": "1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", + "dev": true + } + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha1-PTH1AZGmdJ3RN1p/Ui6CPULlTto=", + "dev": true + }, + "domelementtype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", + "dev": true + }, + "domhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.1.0.tgz", + "integrity": "sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=", + "dev": true, + "requires": { + "domelementtype": "1.3.0" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0.1.0", + "domelementtype": "1.3.0" + } + }, + "duplexify": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz", + "integrity": "sha1-WSkD9dgLONA3IgVBJk1poZj7NBA=", + "dev": true, + "requires": { + "end-of-stream": "1.4.1", + "inherits": "2.0.3", + "readable-stream": "2.3.6", + "stream-shift": "1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.51", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.51.tgz", + "integrity": "sha1-akK0nar38ipbN7mR2vlJ8029ubU=", + "dev": true + }, + "elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0", + "hash.js": "1.1.4", + "hmac-drbg": "1.0.1", + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "1.4.0" + } + }, + "engine.io-client": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", + "integrity": "sha1-b1TAR13khxWKGnx30QF4cItq3TY=", + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "3.1.0", + "engine.io-parser": "2.1.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "3.3.3", + "xmlhttprequest-ssl": "1.5.5", + "yeast": "0.1.2" + } + }, + "engine.io-parser": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.2.tgz", + "integrity": "sha1-TA9M/3mq7su9z96maoI8YIVAkZY=", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.4", + "has-binary2": "1.0.3" + } + }, + "enhanced-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", + "integrity": "sha1-Qcfgv9/nSsH/4eV61qXGyfN0Kn8=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "memory-fs": "0.4.1", + "tapable": "1.0.0" + } + }, + "entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", + "dev": true + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha1-RoTXF3mtOa8Xfj8AeZb3xnyFJhg=", + "dev": true, + "requires": { + "prr": "1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha1-tKxAZIEH/c3PriQvQovqihTU8b8=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "es-abstract": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", + "integrity": "sha1-nbvdJ8aFbwABQhyhh4LXhr+KYWU=", + "dev": true, + "requires": { + "es-to-primitive": "1.1.1", + "function-bind": "1.1.1", + "has": "1.0.3", + "is-callable": "1.1.4", + "is-regex": "1.0.4" + } + }, + "es-to-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", + "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "dev": true, + "requires": { + "is-callable": "1.1.4", + "is-date-object": "1.0.1", + "is-symbol": "1.0.1" + } + }, + "es5-ext": { + "version": "0.10.45", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.45.tgz", + "integrity": "sha1-C/33tHPaWRnVrfO9Jc63VPzMNlM=", + "dev": true, + "requires": { + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1", + "next-tick": "1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.45", + "es6-symbol": "3.1.1" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.45" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "dev": true, + "requires": { + "esrecurse": "4.2.1", + "estraverse": "4.2.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha1-AHo7n9vCs7uH5IeeoZyS/b05Qs8=", + "dev": true, + "requires": { + "estraverse": "4.2.0" + } + }, + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "eventemitter3": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", + "integrity": "sha1-CQtNbNvWRe0Qv3UNS1QHlC17oWM=", + "dev": true + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "eventsource": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-0.1.6.tgz", + "integrity": "sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI=", + "dev": true, + "requires": { + "original": "1.0.1" + } + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha1-f8vbGY3HGVlDLv4ThCaE4FJaywI=", + "dev": true, + "requires": { + "md5.js": "1.3.4", + "safe-buffer": "5.1.2" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "4.1.3", + "shebang-command": "1.2.0", + "which": "1.3.1" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "express": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", + "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", + "dev": true, + "requires": { + "accepts": "1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.2", + "content-disposition": "0.5.2", + "content-type": "1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "1.1.2", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "etag": "1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "1.1.2", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "2.0.3", + "qs": "6.5.1", + "range-parser": "1.2.0", + "safe-buffer": "5.1.1", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "1.4.0", + "type-is": "1.6.16", + "utils-merge": "1.0.1", + "vary": "1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=", + "dev": true + } + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "external-editor": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.0.tgz", + "integrity": "sha512-mpkfj0FEdxrIhOC04zk85X7StNtr0yXnG7zCb+8ikO8OJi2jsHh5YGoknNTyXgsbHOf1WOOcVU3kPFWT2WgCkQ==", + "dev": true, + "requires": { + "chardet": "0.5.0", + "iconv-lite": "0.4.23", + "tmp": "0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha1-rQD+TcYSqSMuhxhxHcXLWrAoVUM=", + "dev": true, + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, + "fastparse": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", + "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=", + "dev": true + }, + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "dev": true, + "requires": { + "websocket-driver": "0.7.0" + } + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5" + } + }, + "file-loader": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz", + "integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "schema-utils": "0.4.5" + }, + "dependencies": { + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha1-7r9O2EAHnIP0JJA4ydcDAIMBsQU=", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.2", + "statuses": "1.4.0", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "find-cache-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", + "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", + "dev": true, + "requires": { + "commondir": "1.0.1", + "make-dir": "1.3.0", + "pkg-dir": "2.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "flush-write-stream": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", + "integrity": "sha1-xdWG7zivYJdlC0m8QbVfq7GfNb0=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "follow-redirects": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.1.tgz", + "integrity": "sha512-v9GI1hpaqq1ZZR6pBD1+kI7O24PhDvNGNodjS3MdcEqyrahCp8zbtpv+2B/krUnSmUH80lbAS7MrdeK5IylgKg==", + "dev": true, + "requires": { + "debug": "3.1.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", + "dev": true + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "iferr": "0.1.5", + "imurmurhash": "0.1.4", + "readable-stream": "1.0.34" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "dev": true, + "optional": true, + "requires": { + "nan": "2.10.0", + "node-pre-gyp": "0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": "2.1.2" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "minipass": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "minizlib": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "2.2.4" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "2.6.9", + "iconv-lite": "0.4.21", + "sax": "1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "1.0.3", + "mkdirp": "0.5.1", + "needle": "2.2.0", + "nopt": "4.0.1", + "npm-packlist": "1.1.10", + "npmlog": "4.1.2", + "rc": "1.2.7", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "4.4.1" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.5" + } + }, + "npm-bundled": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "3.0.1", + "npm-bundled": "1.0.3" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "0.5.1", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true, + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "1.0.1", + "fs-minipass": "1.2.5", + "minipass": "2.2.4", + "minizlib": "1.1.0", + "mkdirp": "0.5.1", + "safe-buffer": "5.1.1", + "yallist": "3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "3.0.2", + "bundled": true, + "dev": true + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=", + "dev": true + }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", + "dev": true + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "3.1.0", + "path-dirname": "1.0.2" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + } + } + }, + "global-modules-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/global-modules-path/-/global-modules-path-2.1.0.tgz", + "integrity": "sha1-kj7FJOhya7DBpO1LjiHh/4DIi7s=", + "dev": true + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=", + "dev": true + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "handle-thing": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz", + "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha1-ci18v8H2qoJB8W3YFOAR4fQeh5Y=", + "dev": true, + "requires": { + "function-bind": "1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha1-d3asYn8+p3JQz8My2rfd9eT10R0=", + "requires": { + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "1.0.0", + "isobject": "3.0.1" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "hash.js": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.4.tgz", + "integrity": "sha1-i1Dh811RvQHl7Z7OTb41Scz6Cjw=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimalistic-assert": "1.0.1" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "1.1.4", + "minimalistic-assert": "1.0.1", + "minimalistic-crypto-utils": "1.0.1" + } + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "dev": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "hosted-git-info": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.0.tgz", + "integrity": "sha512-cdKzzZ7mHqOQeEeFy5nnVZMZ6hvEdeOw8B1l+pMdGf6V7hcRg/ll1pPi3gBleHp3U/B88Xq/a2SGuO4WVW2/qw==", + "dev": true + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "obuf": "1.1.2", + "readable-stream": "2.3.6", + "wbuf": "1.7.3" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "html-entities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", + "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", + "dev": true + }, + "html-minifier": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.18.tgz", + "integrity": "sha1-/IsCgmy7r8beGaEDxByDCpHP/lo=", + "dev": true, + "requires": { + "camel-case": "3.0.0", + "clean-css": "4.1.11", + "commander": "2.16.0", + "he": "1.1.1", + "param-case": "2.1.1", + "relateurl": "0.2.7", + "uglify-js": "3.4.3" + } + }, + "html-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", + "dev": true, + "requires": { + "html-minifier": "3.5.18", + "loader-utils": "0.2.17", + "lodash": "4.17.10", + "pretty-error": "2.1.1", + "tapable": "1.0.0", + "toposort": "1.0.7", + "util.promisify": "1.0.0" + } + }, + "htmlparser2": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", + "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=", + "dev": true, + "requires": { + "domelementtype": "1.3.0", + "domhandler": "2.1.0", + "domutils": "1.1.6", + "readable-stream": "1.0.34" + }, + "dependencies": { + "domutils": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.1.6.tgz", + "integrity": "sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=", + "dev": true, + "requires": { + "domelementtype": "1.3.0" + } + } + } + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": "1.4.0" + } + }, + "http-parser-js": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz", + "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=", + "dev": true + }, + "http-proxy": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", + "integrity": "sha1-etOElGWPhGBeL220Q230EPTlvpo=", + "dev": true, + "requires": { + "eventemitter3": "3.1.0", + "follow-redirects": "1.5.1", + "requires-port": "1.0.0" + } + }, + "http-proxy-middleware": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", + "integrity": "sha1-CYfmu1pWBuWmkWjY+WeofxXdiqs=", + "dev": true, + "requires": { + "http-proxy": "1.17.0", + "is-glob": "4.0.0", + "lodash": "4.17.10", + "micromatch": "3.1.10" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha1-KXhx9jvlB63Pv8pxXQzQ7thOmmM=", + "dev": true, + "requires": { + "safer-buffer": "2.1.2" + } + }, + "icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", + "dev": true + }, + "icss-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-2.1.0.tgz", + "integrity": "sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=", + "dev": true, + "requires": { + "postcss": "6.0.23" + } + }, + "ieee754": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", + "integrity": "sha1-UL8k5bnIu5ivSWTJQc2wkY2ntgs=", + "dev": true + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "import-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", + "integrity": "sha1-Xk/9wD9P5sAJxnKb6yljHC+CJ7w=", + "dev": true, + "requires": { + "pkg-dir": "2.0.0", + "resolve-cwd": "2.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "inquirer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.0.0.tgz", + "integrity": "sha512-tISQWRwtcAgrz+SHPhTH7d3e73k31gsOy6i1csonLc0u1dVK/wYvuOnFeiWqC5OXFIYbmrIFInef31wbT8MEJg==", + "dev": true, + "requires": { + "ansi-escapes": "3.1.0", + "chalk": "2.4.1", + "cli-cursor": "2.1.0", + "cli-width": "2.2.0", + "external-editor": "3.0.0", + "figures": "2.0.0", + "lodash": "4.17.10", + "mute-stream": "0.0.7", + "run-async": "2.3.0", + "rxjs": "6.2.1", + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "through": "2.3.8" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "internal-ip": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-1.2.0.tgz", + "integrity": "sha1-rp+/k7mEh4eF1QqN4bNWlWBYz1w=", + "dev": true, + "requires": { + "meow": "3.7.0" + } + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha1-YQ88ksk1nOHbYW5TgAjSP/NRWOY=", + "dev": true, + "requires": { + "loose-envify": "1.3.1" + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "ipaddr.js": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", + "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "1.11.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha1-HhrfIZ4e62hNaR+dagX/DTCiTXU=", + "dev": true + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha1-Nm2CQN3kh8pRgjsaufB6EKeCUco=", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=", + "dev": true + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha1-WsSLNF72dTOb1sekipEhELJBz1I=", + "dev": true, + "requires": { + "is-path-inside": "1.0.1" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "1.0.3" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-symbol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", + "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha1-0YUOuXkezRjmGCzhKjDzlmNLsZ0=", + "dev": true + }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "jquery": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", + "integrity": "sha1-lYzinoHJeQ8xvneS311NlfxX+8o=" + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha1-u4Z8+zRQ5pEHwTHRxRS6s9yLyqk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", + "dev": true + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "killable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.0.tgz", + "integrity": "sha1-2ouEvUfeU5WHj5XWTQLyRJ/gXms=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE=", + "dev": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "loader-runner": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz", + "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=", + "dev": true + }, + "loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1", + "object-assign": "4.1.1" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha1-V0Dhxdbw39pK2TI7UzIQfva0xAo=", + "dev": true, + "requires": { + "chalk": "2.4.1" + } + }, + "loglevel": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz", + "integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=", + "dev": true + }, + "loglevelnext": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/loglevelnext/-/loglevelnext-1.0.5.tgz", + "integrity": "sha1-NvxPWZbWZA9Tn/IDuoGWQWgNdaI=", + "dev": true, + "requires": { + "es6-symbol": "3.1.1", + "object.assign": "4.1.0" + } + }, + "long": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", + "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=", + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", + "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "dev": true, + "requires": { + "js-tokens": "3.0.2" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", + "dev": true + }, + "lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha1-ecEDO4BRW9bSTsmTPoYMp17ifww=", + "dev": true, + "requires": { + "pify": "3.0.0" + } + }, + "mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha1-rSyVdhl8nxq/MI0Hh4Zb2XWj8+Q=", + "dev": true + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "1.0.1" + } + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "dev": true, + "requires": { + "hash-base": "3.0.4", + "inherits": "2.0.3" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "dev": true, + "requires": { + "mimic-fn": "1.2.0" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "0.1.7", + "readable-stream": "2.3.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "2.1.0", + "decamelize": "1.2.0", + "loud-rejection": "1.6.0", + "map-obj": "1.0.1", + "minimist": "1.2.0", + "normalize-package-data": "2.4.0", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "redent": "1.0.0", + "trim-newlines": "1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha1-cIWbyVyYQJUvNZoGij/En57PrCM=", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.13", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha1-8IA1HIZbDcViqEYpZtqlNUPHik0=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "brorand": "1.1.0" + } + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "dev": true + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha1-o0kgUKXLm2NFBUHjnZeI0icng9s=", + "dev": true + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha1-bzI/YKg9ERRvgx/xH9ZuL+VQO7g=", + "dev": true, + "requires": { + "mime-db": "1.33.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha1-ggyGo5M0ZA6ZUWkovQP8qIBX0CI=", + "dev": true + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha1-LhlN4ERibUoQ5/f7wAznPoPk1cc=", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mississippi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz", + "integrity": "sha1-NEKlCPr8KFAEhv7qmUCWduTuWm8=", + "dev": true, + "requires": { + "concat-stream": "1.6.2", + "duplexify": "3.6.0", + "end-of-stream": "1.4.1", + "flush-write-stream": "1.0.3", + "from2": "2.3.0", + "parallel-transform": "1.1.0", + "pump": "2.0.1", + "pumpify": "1.5.1", + "stream-each": "1.2.2", + "through2": "2.0.3" + } + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha1-pJ5yaNzhoNlpjkUybFYm3zVD0P4=", + "dev": true, + "requires": { + "for-in": "1.0.2", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha1-p0cPnkJnM9gb2B4RVSZOOjUHyrQ=", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "moment": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "requires": { + "aproba": "1.2.0", + "copy-concurrently": "1.0.5", + "fs-write-stream-atomic": "1.0.10", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "run-queue": "1.0.3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha1-oOx72QVcQoL3kMPIL04o2zsxsik=", + "dev": true, + "requires": { + "dns-packet": "1.3.1", + "thunky": "1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha1-uHqKpPwN6P5r6IiVs4mD/yZb0Rk=", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "fragment-cache": "0.2.1", + "is-windows": "1.0.2", + "kind-of": "6.0.2", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "dev": true + }, + "neo-async": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.1.tgz", + "integrity": "sha1-rLkJ4yex6H7J7xX0G4omlRKtQe4=", + "dev": true + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, + "nice-try": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.4.tgz", + "integrity": "sha1-2Tli9sUvLBVYwPvabVEoGfHv4cQ=", + "dev": true + }, + "no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha1-YLgTOWvjmz8SiKTB7V0efSi0ZKw=", + "dev": true, + "requires": { + "lower-case": "1.1.4" + } + }, + "node-forge": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", + "integrity": "sha1-bBUsNFzhHFL0ZcKr2VfoY5zWdN8=", + "dev": true + }, + "node-libs-browser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", + "integrity": "sha1-X5QmPUBPbkR2fXJpAf/wVHjWAN8=", + "dev": true, + "requires": { + "assert": "1.4.1", + "browserify-zlib": "0.2.0", + "buffer": "4.9.1", + "console-browserify": "1.1.0", + "constants-browserify": "1.0.0", + "crypto-browserify": "3.12.0", + "domain-browser": "1.2.0", + "events": "1.1.1", + "https-browserify": "1.0.0", + "os-browserify": "0.3.0", + "path-browserify": "0.0.0", + "process": "0.11.10", + "punycode": "1.4.1", + "querystring-es3": "0.2.1", + "readable-stream": "2.3.6", + "stream-browserify": "2.0.1", + "stream-http": "2.8.3", + "string_decoder": "1.1.1", + "timers-browserify": "2.0.10", + "tty-browserify": "0.0.0", + "url": "0.11.0", + "util": "0.10.4", + "vm-browserify": "0.0.4" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", + "dev": true, + "requires": { + "hosted-git-info": "2.7.0", + "is-builtin-module": "1.0.0", + "semver": "5.5.0", + "validate-npm-package-license": "3.0.3" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, + "nth-check": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", + "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", + "dev": true, + "requires": { + "boolbase": "1.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "0.1.1", + "define-property": "0.2.5", + "kind-of": "3.2.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "object-keys": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", + "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha1-lovxEA15Vrs8oIbwBvhGs7xACNo=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "function-bind": "1.1.1", + "has-symbols": "1.0.0", + "object-keys": "1.0.12" + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "es-abstract": "1.12.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha1-Cb6jND1BhZ69RGKS0RydTbYZCE4=", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", + "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "1.2.0" + } + }, + "opn": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", + "integrity": "sha1-ZIcVZchjh18FLP31PT48ta21Oxw=", + "dev": true, + "requires": { + "is-wsl": "1.1.0" + } + }, + "original": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.1.tgz", + "integrity": "sha1-sKU/9Cupl6jJzR+12q60K51pMZA=", + "dev": true, + "requires": { + "url-parse": "1.4.1" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha1-QrwpAKa1uL0XN2yOiCtlr8zyS/I=", + "dev": true, + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha1-uGvV8MJWkJEcdZD8v8IBDVSzzLg=", + "dev": true, + "requires": { + "p-try": "1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "1.3.0" + } + }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha1-5OlPMR6rvIYzoeeZCBZfyiYkG2s=", + "dev": true + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha1-AQEhG6pwxLykoPY/Igbpe3368lg=", + "dev": true + }, + "parallel-transform": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", + "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "dev": true, + "requires": { + "cyclist": "0.2.2", + "inherits": "2.0.3", + "readable-stream": "2.3.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", + "dev": true, + "requires": { + "no-case": "2.3.2" + } + }, + "parse-asn1": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", + "integrity": "sha1-9r8pOBgzK9DatU77Fgh3JHRebKg=", + "dev": true, + "requires": { + "asn1.js": "4.10.1", + "browserify-aes": "1.2.0", + "create-hash": "1.2.0", + "evp_bytestokey": "1.0.3", + "pbkdf2": "3.0.16" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.2" + } + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "1.0.2" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "1.0.2" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "pbkdf2": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz", + "integrity": "sha1-dAQgjsawG2LYW/g4U6gGT42cKlw=", + "dev": true, + "requires": { + "create-hash": "1.2.0", + "create-hmac": "1.1.7", + "ripemd160": "2.0.2", + "safe-buffer": "5.1.2", + "sha.js": "2.4.11" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "2.1.0" + } + }, + "portfinder": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.13.tgz", + "integrity": "sha1-uzLs2HwnEErm7kS1o8y/Drsa7ek=", + "dev": true, + "requires": { + "async": "1.5.2", + "debug": "2.6.9", + "mkdirp": "0.5.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dev": true, + "requires": { + "chalk": "2.4.1", + "source-map": "0.6.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "postcss-modules-extract-imports": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz", + "integrity": "sha1-ZhQOzs447wa/DT41XWm/WdFB6oU=", + "dev": true, + "requires": { + "postcss": "6.0.23" + } + }, + "postcss-modules-local-by-default": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", + "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", + "dev": true, + "requires": { + "css-selector-tokenizer": "0.7.0", + "postcss": "6.0.23" + } + }, + "postcss-modules-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", + "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", + "dev": true, + "requires": { + "css-selector-tokenizer": "0.7.0", + "postcss": "6.0.23" + } + }, + "postcss-modules-values": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", + "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", + "dev": true, + "requires": { + "icss-replace-symbols": "1.1.0", + "postcss": "6.0.23" + } + }, + "postcss-value-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", + "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=", + "dev": true + }, + "pretty-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", + "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", + "dev": true, + "requires": { + "renderkid": "2.0.1", + "utila": "0.4.0" + } + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha1-I4Hts2ifelPWUxkAYPz4ItLzaP8=", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o=", + "dev": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "proxy-addr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", + "integrity": "sha1-NV8mJQWmIWRrMTCnKOtkfiIFU0E=", + "dev": true, + "requires": { + "forwarded": "0.1.2", + "ipaddr.js": "1.6.0" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "public-encrypt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", + "integrity": "sha1-RuuRByBr9zSJ+LhbadkTNMZhCZQ=", + "dev": true, + "requires": { + "bn.js": "4.11.8", + "browserify-rsa": "4.0.1", + "create-hash": "1.2.0", + "parse-asn1": "5.1.1", + "randombytes": "2.0.6" + } + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha1-Ejma3W5M91Jtlzy8i1zi4pCLOQk=", + "dev": true, + "requires": { + "end-of-stream": "1.4.1", + "once": "1.4.0" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha1-NlE74karJ1cLGjdKXOJ4v9dDcM4=", + "dev": true, + "requires": { + "duplexify": "3.6.0", + "inherits": "2.0.3", + "pump": "2.0.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "querystringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz", + "integrity": "sha1-+j7W5o6xUVlFfImze8ZHKDMZV1U=", + "dev": true + }, + "randombytes": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha1-0wLFIpSFiISKjTAMkytEwkIx2oA=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha1-ySGW/IarQr6YPxvzF3giSTHWFFg=", + "dev": true, + "requires": { + "randombytes": "2.0.6", + "safe-buffer": "5.1.2" + } + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "dev": true + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", + "dev": true + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "dev": true, + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.4.0" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha1-90aPYBNfXl2tM5nAqBvpoWA6CCs=", + "dev": true + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", + "dev": true + } + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + } + } + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "0.0.1", + "string_decoder": "0.10.31" + } + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.3.6", + "set-immediate-shim": "1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "2.1.0", + "strip-indent": "1.0.1" + } + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha1-SoVuxLVuQHfFV1icroXnpMiGmhE=", + "dev": true + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk=" + }, + "regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha1-HkmWg3Ix2ot/PPQRTXG1aRoGgN0=", + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "private": "0.1.8" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha1-H07OJ+ALC2XgJHpoEOaoXYOldSw=", + "dev": true, + "requires": { + "extend-shallow": "3.0.2", + "safe-regex": "1.1.0" + } + }, + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "dev": true, + "requires": { + "regenerate": "1.4.0", + "regjsgen": "0.2.0", + "regjsparser": "0.1.5" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "0.5.0" + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "renderkid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.1.tgz", + "integrity": "sha1-iYyr/Ivt5Le5ETWj/9Mj5YwNsxk=", + "dev": true, + "requires": { + "css-select": "1.2.0", + "dom-converter": "0.1.4", + "htmlparser2": "3.3.0", + "strip-ansi": "3.0.1", + "utila": "0.3.3" + }, + "dependencies": { + "utila": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", + "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", + "dev": true + } + } + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "requires": { + "resolve-from": "3.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "2.0.1", + "signal-exit": "3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha1-uKSCXVvbH8P29Twrwz+BOIaBx7w=", + "dev": true + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha1-ocGm9iR1FXe6XQeRTLyShQWFiQw=", + "dev": true, + "requires": { + "hash-base": "3.0.4", + "inherits": "2.0.3" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "2.1.0" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "requires": { + "aproba": "1.2.0" + } + }, + "rxjs": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.1.tgz", + "integrity": "sha1-JGzr7BiabLwUOj759i1vTJGBPKE=", + "dev": true, + "requires": { + "tslib": "1.9.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "0.1.15" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=", + "dev": true + }, + "schema-utils": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz", + "integrity": "sha1-IYNvBgiqwXt4+ePiTa/xSlyhOj4=", + "dev": true, + "requires": { + "ajv": "6.5.2", + "ajv-keywords": "3.2.0" + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "selfsigned": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.3.tgz", + "integrity": "sha1-1ijs+eNzX4TouvupNrPPhb6kOCM=", + "dev": true, + "requires": { + "node-forge": "0.7.5" + } + }, + "semantic-ui-css": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/semantic-ui-css/-/semantic-ui-css-2.3.2.tgz", + "integrity": "sha512-necQD95BgiDvmjdbGBdQRHs9is1PpxBkpTL2m+IcgahE/VNai19pk8ugeoXm/jLGou/I50JYFZgOpdVmWZEmnw==", + "requires": { + "jquery": "3.3.1" + } + }, + "semantic-ui-vue": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/semantic-ui-vue/-/semantic-ui-vue-0.2.11.tgz", + "integrity": "sha1-NfrwmN9OSlj4g7d0Z6uRL78n2Yw=", + "requires": { + "babel-helper-vue-jsx-merge-props": "2.0.3", + "babel-runtime": "6.26.0", + "lodash": "4.17.10" + } + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha1-bsyh4PjBVtFBWXVZhI32RzCmu8E=", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "1.1.2", + "destroy": "1.0.4", + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "etag": "1.8.1", + "fresh": "0.5.2", + "http-errors": "1.6.3", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "2.3.0", + "range-parser": "1.2.0", + "statuses": "1.4.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "serialize-javascript": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.5.0.tgz", + "integrity": "sha1-GqM2FiyIqJDdrVOEuuvJOmVRYf4=", + "dev": true + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "1.3.5", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "1.0.3", + "http-errors": "1.6.3", + "mime-types": "2.1.18", + "parseurl": "1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha1-CV6Ecv1bRiN9tQzkhqQ/S4bGzsE=", + "dev": true, + "requires": { + "encodeurl": "1.0.2", + "escape-html": "1.0.3", + "parseurl": "1.3.2", + "send": "0.16.2" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha1-ca5KiPD+77v1LR6mBPP7MV67YnQ=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "split-string": "3.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha1-0L2FU2iHtv58DYGMuWLZ2RxU5lY=", + "dev": true + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha1-N6XPC4HsvGlD3hCbopYNGyZYSuc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "safe-buffer": "5.1.2" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha1-ZJIufFZbDhQgS6GqfWlkJ40lGC0=", + "dev": true, + "requires": { + "base": "0.11.2", + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "map-cache": "0.2.2", + "source-map": "0.5.7", + "source-map-resolve": "0.5.2", + "use": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha1-bBdfhv8UvbByRWPo88GwIaKGhTs=", + "dev": true, + "requires": { + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha1-FpwvbT3x+ZJhgHI2XJsOofaHhlY=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha1-2Eh2Mh0Oet0DmQQGq7u9NrqSaMc=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha1-OxWXRqZmBLBPjIFSS6NlxfFNhuw=", + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha1-+VZHlIbyrNeXAGk/b3uAXkWrVuI=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "socket.io-client": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz", + "integrity": "sha1-3LOBA0NqtFeN2wJmOK4vIbYjZx8=", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "3.1.0", + "engine.io-client": "3.2.1", + "has-binary2": "1.0.3", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "3.2.0", + "to-array": "0.1.4" + } + }, + "socket.io-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", + "integrity": "sha1-58Yii2qh+BTmFIrqMltRqpSZ4Hc=", + "requires": { + "component-emitter": "1.2.1", + "debug": "3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, + "sockjs": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", + "integrity": "sha1-2Xa76ACve9IK4IWY1YI5NQiZPA0=", + "dev": true, + "requires": { + "faye-websocket": "0.10.0", + "uuid": "3.3.2" + } + }, + "sockjs-client": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.4.tgz", + "integrity": "sha1-W6vjhrd15M8U51IJEUUmVAFsixI=", + "dev": true, + "requires": { + "debug": "2.6.9", + "eventsource": "0.1.6", + "faye-websocket": "0.11.1", + "inherits": "2.0.3", + "json3": "3.3.2", + "url-parse": "1.4.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "faye-websocket": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", + "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", + "dev": true, + "requires": { + "websocket-driver": "0.7.0" + } + } + } + }, + "source-list-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", + "integrity": "sha1-qqR0A/eyRakvvJfqCPJQ1gh+0IU=", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha1-cuLMNAlVQ+Q7LGKyxMENSpBU8lk=", + "dev": true, + "requires": { + "atob": "2.1.1", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" + } + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha1-Aoam3ovkJkEzhZTpfM6nXwosWF8=", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha1-BaW01xU6GVvJLDxCW2nzsqlSTII=", + "dev": true, + "requires": { + "spdx-expression-parse": "3.0.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha1-LHrmEFbHFKW5ubKyr30xHvXHj+k=", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha1-meEZt6XaAOBUkcn6M4t5BII7QdA=", + "dev": true, + "requires": { + "spdx-exceptions": "2.1.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha1-enzShHDMbToc/m1miG9rxDDTrIc=", + "dev": true + }, + "spdy": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", + "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=", + "dev": true, + "requires": { + "debug": "2.6.9", + "handle-thing": "1.2.5", + "http-deceiver": "1.2.7", + "safe-buffer": "5.1.2", + "select-hose": "2.0.0", + "spdy-transport": "2.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "spdy-transport": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.1.0.tgz", + "integrity": "sha1-S7sVqv/tC+791WrWHb3Iuj4st6E=", + "dev": true, + "requires": { + "debug": "2.6.9", + "detect-node": "2.0.3", + "hpack.js": "2.1.6", + "obuf": "1.1.2", + "readable-stream": "2.3.6", + "safe-buffer": "5.1.2", + "wbuf": "1.7.3" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha1-fLCd2jqGWFcFxks5pkZgOGguj+I=", + "dev": true, + "requires": { + "extend-shallow": "3.0.2" + } + }, + "ssri": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz", + "integrity": "sha1-ujhyycbTOgcEp9cf8EXl7EiZnQY=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "0.2.5", + "object-copy": "0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + } + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha1-u3PURtonlhBu/MG2AaJT1sRr0Ic=", + "dev": true + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "stream-each": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.2.tgz", + "integrity": "sha1-joxGP5HaiZF3h2WHP+TZYNj2Fr0=", + "dev": true, + "requires": { + "end-of-stream": "1.4.1", + "stream-shift": "1.0.0" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha1-stJCRpKIpaJ+xP6JM6z2I95lFPw=", + "dev": true, + "requires": { + "builtin-status-codes": "3.0.0", + "inherits": "2.0.3", + "readable-stream": "2.3.6", + "to-arraybuffer": "1.0.1", + "xtend": "4.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "4.0.1" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + }, + "tapable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.0.0.tgz", + "integrity": "sha1-y7Y52QAu7ZxrWXXrIFmNeTbx+fI=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "dev": true, + "requires": { + "readable-stream": "2.3.6", + "xtend": "4.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.2", + "string_decoder": "1.1.1", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + } + } + }, + "thunky": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.2.tgz", + "integrity": "sha1-qGLgGOP7HqLsP85dVWBc9X8kc3E=", + "dev": true + }, + "timers-browserify": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", + "integrity": "sha1-HSjj0qrfHVpZlsTp+VYBzQU0gK4=", + "dev": true, + "requires": { + "setimmediate": "1.0.5" + } + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=", + "dev": true, + "requires": { + "os-tmpdir": "1.0.2" + } + }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha1-E8/dmzNlUvMLUfM6iuG0Knp1mc4=", + "dev": true, + "requires": { + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "regex-not": "1.0.2", + "safe-regex": "1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "repeat-string": "1.6.1" + } + }, + "toposort": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", + "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", + "dev": true + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha1-1+TdeSRdhUKMTX5IIqeZF5VMooY=", + "dev": true + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha1-+JzjQVQcZysl7nrjxz3uOyvlAZQ=", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.18" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "uglify-js": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.3.tgz", + "integrity": "sha512-RbOgGjF04sFUNSi8xGOTB9AmtVmMmVVAL5a7lxIgJ8urejJen+priq0ooRIHHa8AXI/dSvNF9yYMz9OP4PhybQ==", + "dev": true, + "requires": { + "commander": "2.16.0", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "uglifyjs-webpack-plugin": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.7.tgz", + "integrity": "sha1-V2ON2ZyFOh6/6dl7QhYKilB/nQA=", + "dev": true, + "requires": { + "cacache": "10.0.4", + "find-cache-dir": "1.0.0", + "schema-utils": "0.4.5", + "serialize-javascript": "1.5.0", + "source-map": "0.6.1", + "uglify-es": "3.3.9", + "webpack-sources": "1.1.0", + "worker-farm": "1.6.0" + }, + "dependencies": { + "commander": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", + "integrity": "sha1-aWS8pnaF33wfFDDFhPB9dZeIW5w=", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", + "dev": true + }, + "uglify-es": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", + "integrity": "sha1-DBxPBwC+2NvBJM2zBNJZLKID5nc=", + "dev": true, + "requires": { + "commander": "2.13.0", + "source-map": "0.6.1" + } + } + } + }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha1-n+FTahCmZKZSZqHjzPhf02MCvJw=" + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" + } + } + } + }, + "unique-filename": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.0.tgz", + "integrity": "sha1-0F8v5AMlYIcfMOk8vnNe6iAVFPM=", + "dev": true, + "requires": { + "unique-slug": "2.0.0" + } + }, + "unique-slug": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.0.tgz", + "integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=", + "dev": true, + "requires": { + "imurmurhash": "0.1.4" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "0.3.1", + "isobject": "3.0.1" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, + "upath": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", + "integrity": "sha1-NSVll+RqWB20eT0M5H+prr/J+r0=", + "dev": true + }, + "upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha1-lMVA4f93KVbiKZUHwBCupsiDjrA=", + "dev": true, + "requires": { + "punycode": "2.1.1" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "url-join": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.0.tgz", + "integrity": "sha1-TTNA6AfTdzvamZH4MFrNzCpmXSo=", + "dev": true + }, + "url-loader": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.0.1.tgz", + "integrity": "sha512-rAonpHy7231fmweBKUFe0bYnlGDty77E+fm53NZdij7j/YOpyGzc7ttqG1nAXl3aRs0k41o0PC3TvGXQiw2Zvw==", + "dev": true, + "requires": { + "loader-utils": "1.1.0", + "mime": "2.3.1", + "schema-utils": "0.4.5" + }, + "dependencies": { + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } + }, + "mime": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz", + "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==", + "dev": true + } + } + }, + "url-parse": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.1.tgz", + "integrity": "sha1-TeydrT3IWF+GL+1GHS4Zu/Yj3zA=", + "dev": true, + "requires": { + "querystringify": "2.0.0", + "requires-port": "1.0.0" + } + }, + "use": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", + "integrity": "sha1-FHFr8D/f79AwQK71jYtLhfOnxUQ=", + "dev": true, + "requires": { + "kind-of": "6.0.2" + } + }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha1-OqASW/5mikZy3liFfTrOJ+y3aQE=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha1-RA9xZaRZyaFtwUXrjnLzVocJcDA=", + "dev": true, + "requires": { + "define-properties": "1.1.2", + "object.getownpropertydescriptors": "2.0.3" + } + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=", + "dev": true + }, + "v8-compile-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.0.tgz", + "integrity": "sha1-UmSS41/GFoZChHALcEPgG67gnwo=", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha1-gWQ7y+8b3+zUYjeT3EZIlIupgzg=", + "dev": true, + "requires": { + "spdx-correct": "3.0.0", + "spdx-expression-parse": "3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true, + "requires": { + "indexof": "0.0.1" + } + }, + "vue": { + "version": "2.5.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.5.16.tgz", + "integrity": "sha1-B+23XoQSqu7YceuvqZ9GclhKAIU=" + }, + "watchpack": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", + "integrity": "sha1-S8EsLr6KonenHx0/FNaFx7RGzQA=", + "dev": true, + "requires": { + "chokidar": "2.0.4", + "graceful-fs": "4.1.11", + "neo-async": "2.5.1" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha1-wdjRSTFtPqhShIiVy2oL/oh7h98=", + "dev": true, + "requires": { + "minimalistic-assert": "1.0.1" + } + }, + "webpack": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.15.0.tgz", + "integrity": "sha512-S2fpUI4WLg9gsg1CZ9SE790C1fI2zbxiBCdl9xAnttdLjXhzfCYSYu+TeSEz6ZrLcmQ8RpJoieOAAS0p27nTog==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.5.13", + "@webassemblyjs/helper-module-context": "1.5.13", + "@webassemblyjs/wasm-edit": "1.5.13", + "@webassemblyjs/wasm-opt": "1.5.13", + "@webassemblyjs/wasm-parser": "1.5.13", + "acorn": "5.7.1", + "acorn-dynamic-import": "3.0.0", + "ajv": "6.5.2", + "ajv-keywords": "3.2.0", + "chrome-trace-event": "1.0.0", + "enhanced-resolve": "4.1.0", + "eslint-scope": "3.7.1", + "json-parse-better-errors": "1.0.2", + "loader-runner": "2.3.0", + "loader-utils": "1.1.0", + "memory-fs": "0.4.1", + "micromatch": "3.1.10", + "mkdirp": "0.5.1", + "neo-async": "2.5.1", + "node-libs-browser": "2.1.0", + "schema-utils": "0.4.5", + "tapable": "1.0.0", + "uglifyjs-webpack-plugin": "1.2.7", + "watchpack": "1.6.0", + "webpack-sources": "1.1.0" + }, + "dependencies": { + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } + } + } + }, + "webpack-cli": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.0.8.tgz", + "integrity": "sha1-kO3c8EpL/DGqjA7cTHZ4W8TxzNk=", + "dev": true, + "requires": { + "chalk": "2.4.1", + "cross-spawn": "6.0.5", + "enhanced-resolve": "4.1.0", + "global-modules-path": "2.1.0", + "import-local": "1.0.0", + "inquirer": "6.0.0", + "interpret": "1.1.0", + "loader-utils": "1.1.0", + "supports-color": "5.4.0", + "v8-compile-cache": "2.0.0", + "yargs": "11.1.0" + }, + "dependencies": { + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } + } + } + }, + "webpack-dev-middleware": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.1.3.tgz", + "integrity": "sha1-izKqQ9qa55Nowb8Rg/K2z14fOe0=", + "dev": true, + "requires": { + "loud-rejection": "1.6.0", + "memory-fs": "0.4.1", + "mime": "2.3.1", + "path-is-absolute": "1.0.1", + "range-parser": "1.2.0", + "url-join": "4.0.0", + "webpack-log": "1.2.0" + }, + "dependencies": { + "mime": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz", + "integrity": "sha1-sWIcVNY7l8R9PP5/chX31kUXw2k=", + "dev": true + } + } + }, + "webpack-dev-server": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.1.4.tgz", + "integrity": "sha512-itcIUDFkHuj1/QQxzUFOEXXmxOj5bku2ScLEsOFPapnq2JRTm58gPdtnBphBJOKL2+M3p6+xygL64bI+3eyzzw==", + "dev": true, + "requires": { + "ansi-html": "0.0.7", + "array-includes": "3.0.3", + "bonjour": "3.5.0", + "chokidar": "2.0.4", + "compression": "1.7.2", + "connect-history-api-fallback": "1.5.0", + "debug": "3.1.0", + "del": "3.0.0", + "express": "4.16.3", + "html-entities": "1.2.1", + "http-proxy-middleware": "0.18.0", + "import-local": "1.0.0", + "internal-ip": "1.2.0", + "ip": "1.1.5", + "killable": "1.0.0", + "loglevel": "1.6.1", + "opn": "5.3.0", + "portfinder": "1.0.13", + "selfsigned": "1.10.3", + "serve-index": "1.9.1", + "sockjs": "0.3.19", + "sockjs-client": "1.1.4", + "spdy": "3.4.7", + "strip-ansi": "3.0.1", + "supports-color": "5.4.0", + "webpack-dev-middleware": "3.1.3", + "webpack-log": "1.2.0", + "yargs": "11.0.0" + }, + "dependencies": { + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yargs": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.0.0.tgz", + "integrity": "sha1-wFKTEAbF7udGEOX8A1S+39CKIBs=", + "dev": true, + "requires": { + "cliui": "4.1.0", + "decamelize": "1.2.0", + "find-up": "2.1.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "9.0.2" + } + } + } + }, + "webpack-log": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-1.2.0.tgz", + "integrity": "sha1-pLNM2msitRjbsKsy5WeWLVxypD0=", + "dev": true, + "requires": { + "chalk": "2.4.1", + "log-symbols": "2.2.0", + "loglevelnext": "1.0.5", + "uuid": "3.3.2" + } + }, + "webpack-sources": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.1.0.tgz", + "integrity": "sha1-oQHrrlnWUHNU1x2AE5UKOot6WlQ=", + "dev": true, + "requires": { + "source-list-map": "2.0.0", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", + "dev": true + } + } + }, + "websocket-driver": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", + "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "dev": true, + "requires": { + "http-parser-js": "0.4.13", + "websocket-extensions": "0.1.3" + } + }, + "websocket-extensions": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", + "integrity": "sha1-XS/yKXcAPsaHpLhwc9+7rBRszyk=", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha1-pFBD1U9YBTFtqNYvn1CRjT2nCwo=", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "worker-farm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz", + "integrity": "sha1-rsxAWXb6talVJhgIRvDboojzpKA=", + "dev": true, + "requires": { + "errno": "0.1.7" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha1-8c+E/i1ekB686U767OeF8YeiKPI=", + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.2", + "ultron": "1.1.1" + } + }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha1-le+U+F7MgdAHwmThkKEg8KPIVms=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", + "dev": true, + "requires": { + "cliui": "4.1.0", + "decamelize": "1.2.0", + "find-up": "2.1.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "9.0.2" + }, + "dependencies": { + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + } + } + }, + "yargs-parser": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", + "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", + "dev": true, + "requires": { + "camelcase": "4.1.0" + } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + } + } +} diff --git a/stockpile/ui/package.json b/stockpile/ui/package.json new file mode 100644 index 0000000..832bada --- /dev/null +++ b/stockpile/ui/package.json @@ -0,0 +1,33 @@ +{ + "name": "stockpile", + "version": "2.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "webpack --mode=production", + "watch": "webpack --mode=development --watch", + "serve": "webpack-dev-server --mode=development --define SERVER_ADDR_OVERRIDE='localhost:36623'" + }, + "author": "", + "license": "Apache-2.0", + "devDependencies": { + "babel-core": "^6.26.3", + "babel-loader": "^7.1.5", + "babel-preset-env": "^1.7.0", + "css-loader": "^1.0.0", + "file-loader": "^1.1.11", + "html-webpack-plugin": "^3.2.0", + "uglifyjs-webpack-plugin": "^1.2.7", + "url-loader": "^1.0.1", + "webpack": "^4.15.0", + "webpack-cli": "^3.0.8", + "webpack-dev-server": "^3.1.4" + }, + "dependencies": { + "moment": "^2.22.2", + "semantic-ui-css": "^2.3.2", + "semantic-ui-vue": "^0.2.11", + "socket.io-client": "^2.1.1", + "vue": "^2.5.16" + } +} diff --git a/stockpile/ui/src/index.html b/stockpile/ui/src/index.html new file mode 100644 index 0000000..1696558 --- /dev/null +++ b/stockpile/ui/src/index.html @@ -0,0 +1,94 @@ + + + + + Stockpile UI + + + + + + +
    + +
    +
    +

    Cache Events

    + +
    +
    + +
    + + + + + + + + diff --git a/stockpile/ui/src/script/app.js b/stockpile/ui/src/script/app.js new file mode 100644 index 0000000..4a6ced4 --- /dev/null +++ b/stockpile/ui/src/script/app.js @@ -0,0 +1,108 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ +import Vue from 'vue' +import SuiVue from 'semantic-ui-vue' +import moment from 'moment' +import io from 'socket.io-client' + +const ProfileIdEvent = 0; +const NameHistoryEvent = 1; +const ProfileEvent = 2; +const BlacklistEvent = 3; + +const GOLANG_RFC3399 = 'YYYY-MM-DDTHH:mm:ss.SSSSSSSSSZ ZZ'; + +const opts = { + path: '/ui/socket.io' +}; +const socket = typeof SERVER_ADDR_OVERRIDE === "undefined" ? io(opts) + : io('http://' + SERVER_ADDR_OVERRIDE, opts); + +// import third party components +Vue.use(SuiVue); + +// define a bunch of components for our event feed +Vue.component('profileId-event', { + template: '#cache-event-profileId', + props: ['event'], + computed: { + firstSeenAt: function () { + return moment(this.event.Object.FirstSeenAt, GOLANG_RFC3399).calendar() + }, + validUntil: function () { + return moment(this.event.Object.ValidUntil, GOLANG_RFC3399).calendar() + } + } +}); + +Vue.component('nameHistory-event', { + template: '#cache-event-nameHistory', + props: ['event'] +}); + +Vue.component('profile-event', { + template: '#cache-event-profile', + props: ['event'] +}); + +Vue.component('blacklist-event', { + template: '#cache-event-blacklist', + props: ['event'] +}); + +Vue.component('event-list', { + template: '#cache-event-list', + props: ['events'] +}); + +const app = new Vue({ + el: '#stockpile', + data: { + connected: false, + rateLimitAllocation: 0, + version: '', + events: [] + }, + computed: { + rateLimitLabel: function () { + return `Rate Limit: ${this.rateLimitAllocation} / 600` + }, + rateLimitPercent: function () { + return this.rateLimitAllocation / 600 * 100 + } + } +}); + +socket.on('system', (sys) => { + console.log('Stockpile v' + sys.version) + app.version = sys.version +}) + +socket.on('rate-limit', (allocation) => { + console.log('Current rate limit allocation: ' + allocation); + app.rateLimitAllocation = allocation +}); + +socket.on('cache', (data) => { + console.log('Event: ' + JSON.stringify(data)); + + if (app.events.length > 50) { + app.events.splice(-1, 1) + } + + app.events.unshift(data); +}); diff --git a/stockpile/ui/src/style/app.css b/stockpile/ui/src/style/app.css new file mode 100644 index 0000000..83020f5 --- /dev/null +++ b/stockpile/ui/src/style/app.css @@ -0,0 +1,50 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ +@import "~semantic-ui-css/semantic.min.css"; + +html, +body, +#stockpile { + min-height: 100vh; +} + +#stockpile { + display: flex; + flex-direction: column; +} + +#page-body { + flex-grow: 1; +} + +#page-header .ui.progress { + background-color: rgba(255, 255, 255, 0.25); + margin: 0; +} + +#page-header .ui.progress .bar { + background-color: #1b1c1d; +} + +#page-header .ui.progress > .label { + font-weight: 400; + top: 0; +} + +#page-footer .copyright { + color: rgba(255, 255, 255, 0.5); +} diff --git a/stockpile/ui/webpack.config.js b/stockpile/ui/webpack.config.js new file mode 100644 index 0000000..f56d802 --- /dev/null +++ b/stockpile/ui/webpack.config.js @@ -0,0 +1,73 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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. + */ +const path = require('path'); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); + +const SRC_DIR = path.join(__dirname, "src"); +const DIST_DIR = path.join(__dirname, "dist/ui"); // assetfs requires a root dir + +console.log(' UI Input Directory: ' + SRC_DIR); +console.log('UI Output Directory: ' + DIST_DIR); + +module.exports = { + entry: path.join(SRC_DIR, "script/app.js"), + output: { + path: DIST_DIR, + filename: "[name].js" + }, + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: "babel-loader" + }, + { + test: /\.(png|woff|woff2|eot|ttf|svg)$/, + loader: 'url-loader?limit=100000' + }, + { + test: /\.css$/, + use: [ + "css-loader" + ] + } + ] + }, + resolve: { + alias: { + vue: 'vue/dist/vue.js' + } + }, + optimization: { + minimizer: [ + new UglifyJsPlugin({ + sourceMap: true + }) + ] + }, + plugins: [ + new HtmlWebpackPlugin({ + filename: 'index.html', + template: path.join(SRC_DIR, 'index.html'), + minify: true, + hash: true, + xhtml: true + }) + ] +}; From 753df2bfd104adecb6daab8908d24002c7382b5a Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 7 Jul 2018 04:18:01 +0200 Subject: [PATCH 097/142] Migrated to google's empty message specification to save us some time. --- stockpile/command/listen.go | 3 ++- stockpile/server/rpc/common.proto | 12 ------------ stockpile/server/rpc/events.proto | 4 ++-- stockpile/server/rpc/server.proto | 3 ++- stockpile/server/service/events.go | 3 ++- stockpile/server/service/server.go | 3 ++- 6 files changed, 10 insertions(+), 18 deletions(-) diff --git a/stockpile/command/listen.go b/stockpile/command/listen.go index af8edad..5aca231 100644 --- a/stockpile/command/listen.go +++ b/stockpile/command/listen.go @@ -27,6 +27,7 @@ import ( "github.com/dotStart/Stockpile/stockpile/server/rpc" "github.com/google/subcommands" "golang.org/x/net/context" + empty "github.com/golang/protobuf/ptypes/empty" ) type ListenCommand struct { @@ -61,7 +62,7 @@ func (c *ListenCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...inter } eventService := rpc.NewEventServiceClient(client) - stream, err := eventService.StreamEvents(ctx, &rpc.EmptyRequest{}) + stream, err := eventService.StreamEvents(ctx, &empty.Empty{}) if err != nil { fmt.Fprintf(os.Stderr, "Server responded with error: %s", err) return 1 diff --git a/stockpile/server/rpc/common.proto b/stockpile/server/rpc/common.proto index 5527f01..d513810 100644 --- a/stockpile/server/rpc/common.proto +++ b/stockpile/server/rpc/common.proto @@ -3,18 +3,6 @@ syntax = "proto3"; package rpc; option java_package = "tv.dotstart.stockpile"; -/** - * Used in services where no input parameters are expected. - */ -message EmptyRequest { -} - -/** - * Used in services where no outputs are expected. - */ -message EmptyResponse { -} - /** * Represents a complete user profile. */ diff --git a/stockpile/server/rpc/events.proto b/stockpile/server/rpc/events.proto index b825e7a..90d306f 100644 --- a/stockpile/server/rpc/events.proto +++ b/stockpile/server/rpc/events.proto @@ -4,10 +4,10 @@ package rpc; option java_package = "tv.dotstart.stockpile"; import "google/protobuf/any.proto"; -import "common.proto"; +import "google/protobuf/empty.proto"; service EventService { - rpc StreamEvents (EmptyRequest) returns (stream Event); + rpc StreamEvents (google.protobuf.Empty) returns (stream Event); } message Event { diff --git a/stockpile/server/rpc/server.proto b/stockpile/server/rpc/server.proto index 1937f81..4446ffd 100644 --- a/stockpile/server/rpc/server.proto +++ b/stockpile/server/rpc/server.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package rpc; option java_package = "tv.dotstart.stockpile"; +import "google/protobuf/empty.proto"; import "common.proto"; /** @@ -13,7 +14,7 @@ service ServerService { /** * Retrieves a cached version of the entire server blacklist. */ - rpc GetBlacklist (EmptyRequest) returns (Blacklist); + rpc GetBlacklist (google.protobuf.Empty) returns (Blacklist); /** * Evaluates whether a given address has been blacklisted. diff --git a/stockpile/server/service/events.go b/stockpile/server/service/events.go index b1e7c06..ab9f54a 100644 --- a/stockpile/server/service/events.go +++ b/stockpile/server/service/events.go @@ -19,6 +19,7 @@ package service import ( "github.com/dotStart/Stockpile/stockpile/cache" "github.com/dotStart/Stockpile/stockpile/server/rpc" + empty "github.com/golang/protobuf/ptypes/empty" "github.com/op/go-logging" ) @@ -34,7 +35,7 @@ func NewEventService(cache *cache.Cache) (*EventServiceImpl) { } } -func (s *EventServiceImpl) StreamEvents(_ *rpc.EmptyRequest, srv rpc.EventService_StreamEventsServer) error { +func (s *EventServiceImpl) StreamEvents(_ *empty.Empty, srv rpc.EventService_StreamEventsServer) error { for e := range s.cache.Events { enc, err := rpc.EventToRpc(e) if err != nil { diff --git a/stockpile/server/service/server.go b/stockpile/server/service/server.go index a1ab2ef..e6fa0c7 100644 --- a/stockpile/server/service/server.go +++ b/stockpile/server/service/server.go @@ -21,6 +21,7 @@ import ( "github.com/dotStart/Stockpile/stockpile/server/rpc" "github.com/op/go-logging" "golang.org/x/net/context" + empty "github.com/golang/protobuf/ptypes/empty" ) type ServerServiceImpl struct { @@ -35,7 +36,7 @@ func NewServerService(cache *cache.Cache) *ServerServiceImpl { } } -func (s *ServerServiceImpl) GetBlacklist(context.Context, *rpc.EmptyRequest) (*rpc.Blacklist, error) { +func (s *ServerServiceImpl) GetBlacklist(context.Context, *empty.Empty) (*rpc.Blacklist, error) { blacklist, err := s.cache.GetBlacklist() if err != nil { return nil, err From 8ec370651aecdce110eb18fcde51c2998beb9abc Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 7 Jul 2018 04:34:29 +0200 Subject: [PATCH 098/142] Sorted imports. --- stockpile/command/listen.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stockpile/command/listen.go b/stockpile/command/listen.go index 5aca231..eec0b39 100644 --- a/stockpile/command/listen.go +++ b/stockpile/command/listen.go @@ -25,9 +25,9 @@ import ( "github.com/dotStart/Stockpile/stockpile/cache" "github.com/dotStart/Stockpile/stockpile/server/rpc" + "github.com/golang/protobuf/ptypes/empty" "github.com/google/subcommands" "golang.org/x/net/context" - empty "github.com/golang/protobuf/ptypes/empty" ) type ListenCommand struct { From c092e739199f90a803301bce152ba82ab4d3eea6 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 7 Jul 2018 05:06:36 +0200 Subject: [PATCH 099/142] Added support for querying the server status and plugin list. --- stockpile/command/plugin.go | 76 ++++++++++++++++++++++++++++++ stockpile/command/server.go | 2 +- stockpile/command/status.go | 70 +++++++++++++++++++++++++++ stockpile/main.go | 8 ++-- stockpile/server/rpc/system.proto | 30 ++++++++++++ stockpile/server/rpc/utility.go | 37 +++++++++++++++ stockpile/server/service/main.go | 10 ++-- stockpile/server/service/system.go | 56 ++++++++++++++++++++++ 8 files changed, 282 insertions(+), 7 deletions(-) create mode 100644 stockpile/command/plugin.go create mode 100644 stockpile/command/status.go create mode 100644 stockpile/server/rpc/system.proto create mode 100644 stockpile/server/service/system.go diff --git a/stockpile/command/plugin.go b/stockpile/command/plugin.go new file mode 100644 index 0000000..5d5682d --- /dev/null +++ b/stockpile/command/plugin.go @@ -0,0 +1,76 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 command + +import ( + "flag" + "fmt" + "os" + + "github.com/dotStart/Stockpile/stockpile/server/rpc" + "github.com/golang/protobuf/ptypes/empty" + "github.com/google/subcommands" + "golang.org/x/net/context" +) + +type PluginCommand struct { + ClientCommand +} + +func (*PluginCommand) Name() string { + return "plugins" +} + +func (*PluginCommand) Synopsis() string { + return "displays a list of commands loaded on the Stockpile server" +} + +func (*PluginCommand) Usage() string { + return `Usage: stockpile plugins [options] + +This command displays the list of loaded plugins on a given Stockpile server: + + $ stockpile plugins + +Available command specific flags: + +` +} + +func (c *PluginCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + client, err := c.createClient() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to establish a connection to server \"%s\": %s\n", c.flagServerAddress, err) + return 1 + } + + systemService := rpc.NewSystemServiceClient(client) + res, err := systemService.GetPlugins(ctx, &empty.Empty{}) + if err != nil { + fmt.Fprintf(os.Stderr, "server responded with error: %s", err) + return 1 + } + + pluginList := rpc.PluginMetadataListFromRpc(res) + fmt.Fprintf(os.Stdout, "server has %d plugin(s) loaded:\n\n", len(pluginList)) + + for _, plugin := range pluginList { + writeTable(os.Stdout, *plugin) + fmt.Fprintf(os.Stdout, "\n") + } + return 0 +} diff --git a/stockpile/command/server.go b/stockpile/command/server.go index fe220c0..e0d77b5 100644 --- a/stockpile/command/server.go +++ b/stockpile/command/server.go @@ -158,7 +158,7 @@ func (c *ServerCommand) Execute(_ context.Context, f *flag.FlagSet, _ ...interfa if *cfg.UiEnabled { rpcPolicy = cmux.HTTP2HeaderField("content-type", "application/grpc") } - rpcServer, err := service.NewServer(cacheImpl) + rpcServer, err := service.NewServer(pluginManager, cacheImpl) if err != nil { log.Fatalf("Failed to initialize grpc server: %s", err) } diff --git a/stockpile/command/status.go b/stockpile/command/status.go new file mode 100644 index 0000000..87399c8 --- /dev/null +++ b/stockpile/command/status.go @@ -0,0 +1,70 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 command + +import ( + "flag" + "fmt" + "os" + + "github.com/dotStart/Stockpile/stockpile/server/rpc" + "github.com/golang/protobuf/ptypes/empty" + "github.com/google/subcommands" + "golang.org/x/net/context" +) + +type StatusCommand struct { + ClientCommand +} + +func (*StatusCommand) Name() string { + return "status" +} + +func (*StatusCommand) Synopsis() string { + return "displays the current status of a Stockpile server" +} + +func (*StatusCommand) Usage() string { + return `Usage: stockpile status [options] + +This command displays basic status information of a given Stockpile server: + + $ stockpile status + +Available command specific flags: + +` +} + +func (c *StatusCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + client, err := c.createClient() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to establish a connection to server \"%s\": %s\n", c.flagServerAddress, err) + return 1 + } + + systemService := rpc.NewSystemServiceClient(client) + status, err := systemService.GetStatus(ctx, &empty.Empty{}) + if err != nil { + fmt.Fprintf(os.Stderr, "server responded with error: %s", err) + return 1 + } + + writeTable(os.Stdout, *status) + return 0 +} diff --git a/stockpile/main.go b/stockpile/main.go index 1b8556f..a7099e2 100644 --- a/stockpile/main.go +++ b/stockpile/main.go @@ -31,11 +31,13 @@ func main() { subcommands.Register(subcommands.CommandsCommand(), "") subcommands.Register(&command.ServerCommand{}, "") - subcommands.Register(&command.IdCommand{}, "Client") - subcommands.Register(&command.HistoryCommand{}, "Client") - subcommands.Register(&command.ProfileCommand{}, "Client") subcommands.Register(&command.BlacklistCommand{}, "Client") + subcommands.Register(&command.HistoryCommand{}, "Client") + subcommands.Register(&command.IdCommand{}, "Client") subcommands.Register(&command.ListenCommand{}, "Client") + subcommands.Register(&command.PluginCommand{}, "Client") + subcommands.Register(&command.ProfileCommand{}, "Client") + subcommands.Register(&command.StatusCommand{}, "Client") flag.Parse() ctx := context.Background() diff --git a/stockpile/server/rpc/system.proto b/stockpile/server/rpc/system.proto new file mode 100644 index 0000000..5ad204c --- /dev/null +++ b/stockpile/server/rpc/system.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package rpc; +option java_package = "tv.dotstart.stockpile"; + +import "google/protobuf/empty.proto"; + +service SystemService { + rpc GetStatus (google.protobuf.Empty) returns (Status); + rpc GetPlugins (google.protobuf.Empty) returns (PluginList); +} + +message Status { + string Brand = 1; + string Version = 2; + string VersionFull = 3; + string CommitHash = 4; + int64 BuildTimestamp = 5; +} + +message PluginList { + repeated Plugin Plugins = 1; +} + +message Plugin { + string Name = 1; + string Version = 2; + repeated string Authors = 3; + string Website = 4; +} diff --git a/stockpile/server/rpc/utility.go b/stockpile/server/rpc/utility.go index 3df9cd7..1082d18 100644 --- a/stockpile/server/rpc/utility.go +++ b/stockpile/server/rpc/utility.go @@ -23,6 +23,7 @@ import ( "github.com/dotStart/Stockpile/stockpile/cache" "github.com/dotStart/Stockpile/stockpile/mojang" + "github.com/dotStart/Stockpile/stockpile/plugin" "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes" "github.com/golang/protobuf/ptypes/any" @@ -475,6 +476,42 @@ func EventPayloadFromRpc(payload *any.Any) (interface{}, error) { return nil, fmt.Errorf("illegal payload value: %v", payload) } +func PluginMetadataListToRpc(list []*plugin.Metadata) *PluginList { + enc := make([]*Plugin, len(list)) + for i, metadata := range list { + enc[i] = PluginMetadataToRpc(metadata) + } + return &PluginList{ + Plugins: enc, + } +} + +func PluginMetadataListFromRpc(list *PluginList) []*plugin.Metadata { + decoded := make([]*plugin.Metadata, len(list.Plugins)) + for i, metadata := range list.Plugins { + decoded[i] = PluginMetadataFromRpc(metadata) + } + return decoded +} + +func PluginMetadataToRpc(metadata *plugin.Metadata) *Plugin { + return &Plugin{ + Name: metadata.Name, + Version: metadata.Version, + Authors: metadata.Authors, + Website: metadata.Website, + } +} + +func PluginMetadataFromRpc(metadata *Plugin) *plugin.Metadata { + return &plugin.Metadata{ + Name: metadata.Name, + Version: metadata.Version, + Authors: metadata.Authors, + Website: metadata.Website, + } +} + // evaluates whether the message has been populated with actual data (e.g. whether it is not empty) func (p *ProfileId) IsPopulated() bool { return p.Id != "" diff --git a/stockpile/server/service/main.go b/stockpile/server/service/main.go index de0f45d..bd2a3f2 100644 --- a/stockpile/server/service/main.go +++ b/stockpile/server/service/main.go @@ -16,12 +16,13 @@ */ package service -//go:generate protoc -I ../rpc --go_out=plugins=grpc:../rpc common.proto events.proto profile.proto server.proto +//go:generate protoc -I ../rpc --go_out=plugins=grpc:../rpc common.proto events.proto profile.proto server.proto system.proto import ( "net" "github.com/dotStart/Stockpile/stockpile/cache" + "github.com/dotStart/Stockpile/stockpile/plugin" "github.com/dotStart/Stockpile/stockpile/server/rpc" "github.com/op/go-logging" "google.golang.org/grpc" @@ -31,17 +32,19 @@ import ( // Represents an RPC server type Server struct { logger *logging.Logger + plugin *plugin.Manager cache *cache.Cache srv *grpc.Server } // Constructs a new RPC server instance and starts it -func NewServer(cache *cache.Cache) (*Server, error) { +func NewServer(plugin *plugin.Manager, cache *cache.Cache) (*Server, error) { logger := logging.MustGetLogger("rpc") return &Server{ logger: logger, + plugin: plugin, cache: cache, }, nil } @@ -50,9 +53,10 @@ func NewServer(cache *cache.Cache) (*Server, error) { func (s *Server) Listen(listener net.Listener) { s.srv = grpc.NewServer() grpc.NewServer() + rpc.RegisterEventServiceServer(s.srv, NewEventService(s.cache)) rpc.RegisterProfileServiceServer(s.srv, NewProfileService(s.cache)) rpc.RegisterServerServiceServer(s.srv, NewServerService(s.cache)) - rpc.RegisterEventServiceServer(s.srv, NewEventService(s.cache)) + rpc.RegisterSystemServiceServer(s.srv, NewSystemService(s.plugin)) reflection.Register(s.srv) s.srv.Serve(listener) } diff --git a/stockpile/server/service/system.go b/stockpile/server/service/system.go new file mode 100644 index 0000000..680a472 --- /dev/null +++ b/stockpile/server/service/system.go @@ -0,0 +1,56 @@ +/* + * Copyright 2018 Johannes Donath + * and other copyright owners as documented in the project's IP log. + * + * 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 service + +import ( + "github.com/dotStart/Stockpile/stockpile/metadata" + "github.com/dotStart/Stockpile/stockpile/plugin" + "github.com/dotStart/Stockpile/stockpile/server/rpc" + "github.com/golang/protobuf/ptypes/empty" + "github.com/op/go-logging" + "golang.org/x/net/context" +) + +type SystemServiceImpl struct { + logger *logging.Logger + plugin *plugin.Manager +} + +func NewSystemService(plugin *plugin.Manager) (*SystemServiceImpl) { + return &SystemServiceImpl{ + logger: logging.MustGetLogger("system-srv"), + plugin: plugin, + } +} + +func (s *SystemServiceImpl) GetStatus(context.Context, *empty.Empty) (*rpc.Status, error) { + return &rpc.Status{ + Brand: metadata.Brand(), + Version: metadata.Version(), + VersionFull: metadata.VersionFull(), + CommitHash: metadata.CommitHash(), + BuildTimestamp: metadata.Timestamp().Unix(), + }, nil +} + +func (s *SystemServiceImpl) GetPlugins(context.Context, *empty.Empty) (*rpc.PluginList, error) { + list := make([]*plugin.Metadata, len(s.plugin.Plugins)) + for i, p := range s.plugin.Plugins { + list[i] = &p.Metadata + } + return rpc.PluginMetadataListToRpc(list), nil +} From 5f9124981dc8f70fbe11077307a39e333643a368 Mon Sep 17 00:00:00 2001 From: Johannes Donath Date: Sat, 7 Jul 2018 06:13:51 +0200 Subject: [PATCH 100/142] Added the plugin list to the system introduction package and added additional messages for cases where no data would otherwise be displayed. --- stockpile/command/server.go | 2 +- stockpile/server/ui/main.go | 15 +++++++++++-- stockpile/ui/src/index.html | 41 ++++++++++++++++++++++++++++++++-- stockpile/ui/src/script/app.js | 10 ++++++--- 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/stockpile/command/server.go b/stockpile/command/server.go index e0d77b5..f7425d3 100644 --- a/stockpile/command/server.go +++ b/stockpile/command/server.go @@ -174,7 +174,7 @@ func (c *ServerCommand) Execute(_ context.Context, f *flag.FlagSet, _ ...interfa } // instance currently unused - ui.NewServer(httpMux, c.flagCorsOverride, cacheImpl) + ui.NewServer(httpMux, c.flagCorsOverride, pluginManager, cacheImpl) httpSrv := &http.Server{ Handler: httpMux, diff --git a/stockpile/server/ui/main.go b/stockpile/server/ui/main.go index f574e0d..0268b42 100644 --- a/stockpile/server/ui/main.go +++ b/stockpile/server/ui/main.go @@ -22,6 +22,7 @@ import ( "github.com/dotStart/Stockpile/stockpile/cache" "github.com/dotStart/Stockpile/stockpile/metadata" + "github.com/dotStart/Stockpile/stockpile/plugin" "github.com/googollee/go-socket.io" "github.com/op/go-logging" ) @@ -31,6 +32,7 @@ import ( type Server struct { logger *logging.Logger io *socketio.Server + plugin *plugin.Manager cache *cache.Cache rateLimitTicker *time.Ticker @@ -38,7 +40,7 @@ type Server struct { corsOverride string } -func NewServer(httpSrv *http.ServeMux, corsOverride string, cacheImpl *cache.Cache) (*Server, error) { +func NewServer(httpSrv *http.ServeMux, corsOverride string, plugin *plugin.Manager, cacheImpl *cache.Cache) (*Server, error) { io, err := socketio.NewServer(nil) if err != nil { return nil, err @@ -47,6 +49,7 @@ func NewServer(httpSrv *http.ServeMux, corsOverride string, cacheImpl *cache.Cac srv := &Server{ logger: logging.MustGetLogger("ui"), io: io, + plugin: plugin, cache: cacheImpl, rateLimitTicker: time.NewTicker(time.Minute), @@ -107,12 +110,20 @@ func (s *Server) forwardCacheEvents() { func (s *Server) onSocketConnect(io socketio.Socket) { s.logger.Debugf("client %s (id: %s) established websocket connection", io.Request().RemoteAddr, io.Id()) io.Join("ui") + + pluginList := make([]*plugin.Metadata, len(s.plugin.Plugins)) + for i, plugin := range s.plugin.Plugins { + pluginList[i] = &plugin.Metadata + } + io.Emit( "system", struct { - Version string `json:"version"` + Version string `json:"version"` + Plugins []*plugin.Metadata `json:"plugins"` }{ Version: metadata.VersionFull(), + Plugins: pluginList, }, ) io.Emit("rate-limit", s.cache.GetRateLimitAllocation()) diff --git a/stockpile/ui/src/index.html b/stockpile/ui/src/index.html index 1696558..59e9a2b 100644 --- a/stockpile/ui/src/index.html +++ b/stockpile/ui/src/index.html @@ -24,8 +24,45 @@
    -

    Cache Events

    - +
    +

    Cache Events

    +
    + +
    +
    No Events
    +

    No cache events have been reported since the start of your session

    +
    +
    + +
    +
    +

    Plugins ({{ plugins.length }})

    + +
    + +
    +
    No loaded Plugins
    +

    No plugins are loaded on this server at the moment

    +
    +
    + + + + + + + + + + + + + + + + +
    NameVersionAuthor(s)
    {{ plugin.Name }}{{ plugin.Name }}{{ plugin.Version }}{{ author }}
    +