82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | ## 📝 License
93 |
94 | ```
95 | Copyright © 2021 - theapache64
96 |
97 | Licensed under the Apache License, Version 2.0 (the "License");
98 | you may not use this file except in compliance with the License.
99 | You may obtain a copy of the License at
100 |
101 | http://www.apache.org/licenses/LICENSE-2.0
102 |
103 | Unless required by applicable law or agreed to in writing, software
104 | distributed under the License is distributed on an "AS IS" BASIS,
105 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
106 | See the License for the specific language governing permissions and
107 | limitations under the License.
108 | ```
109 |
110 | _This README was generated by [readgen](https://github.com/theapache64/readgen)_ ❤
111 |
--------------------------------------------------------------------------------
/src/test/kotlin/com/github/theapache64/gpa/api/PlayTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.theapache64.gpa.api
2 |
3 | import com.akdeniz.googleplaycrawler.GooglePlayAPI
4 | import com.akdeniz.googleplaycrawler.GooglePlayException
5 | import com.github.theapache64.expekt.should
6 | import com.github.theapache64.gpa.model.Account
7 | import com.github.theapache64.gpa.utils.runBlockingTest
8 | import org.apache.http.client.ClientProtocolException
9 | import org.junit.jupiter.api.BeforeAll
10 | import org.junit.jupiter.api.RepeatedTest
11 | import org.junit.jupiter.api.Test
12 | import org.junit.jupiter.api.TestInstance
13 | import java.io.File
14 | import java.io.FileOutputStream
15 |
16 | @TestInstance(TestInstance.Lifecycle.PER_CLASS)
17 | internal class PlayTest {
18 |
19 |
20 | private lateinit var api: GooglePlayAPI
21 |
22 | @BeforeAll
23 | @Test
24 | fun givenValidCreds_whenLogin_thenSuccess() = runBlockingTest {
25 | val username = System.getenv("PLAY_API_GOOGLE_USERNAME")!!
26 | val password = System.getenv("PLAY_API_GOOGLE_PASSWORD")!!
27 |
28 | val account = Play.login(username, password)
29 | account.should.not.`null`
30 | api = Play.getApi(account)
31 | }
32 |
33 | @Test
34 | fun givenInvalidCreds_whenLogin_thenError() = runBlockingTest {
35 | try {
36 | Play.login("", "")
37 | assert(false)
38 | } catch (e: ClientProtocolException) {
39 | assert(true)
40 | }
41 | }
42 |
43 | @Test
44 | fun givenValidPackageName_whenGetPackageDetails_thenSuccess() {
45 | val packageName = "com.wrumer.wrumerapp"
46 | val details = api.details(packageName)
47 | details.docV2.docid.should.equal(packageName)
48 | }
49 |
50 | @Test
51 | fun givenValidPackageName_whenGetPackageDetails_thenSuccess2() {
52 | val packageName = "com.meesho.supply"
53 | val details = api.details(packageName)
54 | println(details.docV2)
55 | }
56 |
57 | @Test
58 | fun givenInvalidPackageName_whenGetPackageDetails_thenError() {
59 | val packageName = ""
60 | try {
61 | api.details(packageName)
62 | assert(false)
63 | } catch (e: GooglePlayException) {
64 | assert(true)
65 | }
66 | }
67 |
68 | @Test
69 | fun givenValidKeyword_whenSearch_thenSuccess() = runBlockingTest {
70 | val keyword = "WhatsApp"
71 | var serp = Play.search(keyword, api)
72 | val firstPageSize = serp.content.size
73 | firstPageSize.should.above(0) // first page
74 | serp = Play.search(keyword, api, serp)
75 | serp.content.size.should.above(firstPageSize) // first page + second page
76 | }
77 |
78 |
79 | @Test
80 | @RepeatedTest(3)
81 | fun givenValidSmallPackageName_whenDownload_thenSuccess() {
82 | downloadApkAndTest("org.telegram.messenger")
83 | }
84 |
85 | @Test
86 | fun givenValidMediumPackageName_whenDownload_thenSuccess() {
87 | downloadApkAndTest("com.wrumer.wrumerapp")
88 | }
89 |
90 | /**
91 | * To download APK
92 | */
93 | private fun downloadApkAndTest(packageName: String) {
94 | val apkFile = File("$packageName.apk")
95 | val details = api.details(packageName)
96 | val versionCode = details.docV2.details.appDetails.versionCode
97 | val downloadData = api.purchaseAndDeliver(
98 | packageName,
99 | versionCode,
100 | 1,
101 | )
102 | downloadData.openApp().use { input ->
103 | FileOutputStream(apkFile).use { output ->
104 | input.copyTo(output)
105 | }
106 | }
107 |
108 | apkFile.exists().should.`true`
109 | apkFile.delete() // test finished, so deleting downloaded file
110 | }
111 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.toptal.com/developers/gitignore/api/kotlin,intellij,gradle
3 | # Edit at https://www.toptal.com/developers/gitignore?templates=kotlin,intellij,gradle
4 |
5 | ### Intellij ###
6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
8 |
9 | # User-specific stuff
10 | .idea/**/workspace.xml
11 | .idea/**/tasks.xml
12 | .idea/**/usage.statistics.xml
13 | .idea/**/dictionaries
14 | .idea/**/shelf
15 |
16 | # Generated files
17 | .idea/**/contentModel.xml
18 |
19 | # Sensitive or high-churn files
20 | .idea/**/dataSources/
21 | .idea/**/dataSources.ids
22 | .idea/**/dataSources.local.xml
23 | .idea/**/sqlDataSources.xml
24 | .idea/**/dynamic.xml
25 | .idea/**/uiDesigner.xml
26 | .idea/**/dbnavigator.xml
27 |
28 | # Gradle
29 | .idea/**/gradle.xml
30 | .idea/**/libraries
31 |
32 | # Gradle and Maven with auto-import
33 | # When using Gradle or Maven with auto-import, you should exclude module files,
34 | # since they will be recreated, and may cause churn. Uncomment if using
35 | # auto-import.
36 | # .idea/artifacts
37 | # .idea/compiler.xml
38 | # .idea/jarRepositories.xml
39 | # .idea/modules.xml
40 | # .idea/*.iml
41 | # .idea/modules
42 | # *.iml
43 | # *.ipr
44 |
45 | # CMake
46 | cmake-build-*/
47 |
48 | # Mongo Explorer plugin
49 | .idea/**/mongoSettings.xml
50 |
51 | # File-based project format
52 | *.iws
53 |
54 | # IntelliJ
55 | out/
56 |
57 | # mpeltonen/sbt-idea plugin
58 | .idea_modules/
59 |
60 | # JIRA plugin
61 | atlassian-ide-plugin.xml
62 |
63 | # Cursive Clojure plugin
64 | .idea/replstate.xml
65 |
66 | # Crashlytics plugin (for Android Studio and IntelliJ)
67 | com_crashlytics_export_strings.xml
68 | crashlytics.properties
69 | crashlytics-build.properties
70 | fabric.properties
71 |
72 | # Editor-based Rest Client
73 | .idea/httpRequests
74 |
75 | # Android studio 3.1+ serialized cache file
76 | .idea/caches/build_file_checksums.ser
77 |
78 | ### Intellij Patch ###
79 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
80 |
81 | # *.iml
82 | # modules.xml
83 | # .idea/misc.xml
84 | # *.ipr
85 |
86 | # Sonarlint plugin
87 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
88 | .idea/**/sonarlint/
89 |
90 | # SonarQube Plugin
91 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
92 | .idea/**/sonarIssues.xml
93 |
94 | # Markdown Navigator plugin
95 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
96 | .idea/**/markdown-navigator.xml
97 | .idea/**/markdown-navigator-enh.xml
98 | .idea/**/markdown-navigator/
99 |
100 | # Cache file creation bug
101 | # See https://youtrack.jetbrains.com/issue/JBR-2257
102 | .idea/$CACHE_FILE$
103 |
104 | # CodeStream plugin
105 | # https://plugins.jetbrains.com/plugin/12206-codestream
106 | .idea/codestream.xml
107 |
108 | ### Kotlin ###
109 | # Compiled class file
110 | *.class
111 |
112 | # Log file
113 | *.log
114 |
115 | # BlueJ files
116 | *.ctxt
117 |
118 | # Mobile Tools for Java (J2ME)
119 | .mtj.tmp/
120 |
121 | # Package Files #
122 | *.jar
123 | *.war
124 | *.nar
125 | *.ear
126 | *.zip
127 | *.tar.gz
128 | *.rar
129 |
130 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
131 | hs_err_pid*
132 |
133 | ### Gradle ###
134 | .gradle
135 | build/
136 |
137 | # Ignore Gradle GUI config
138 | gradle-app.setting
139 |
140 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
141 | !gradle-wrapper.jar
142 |
143 | # Cache of project
144 | .gradletasknamecache
145 |
146 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
147 | # gradle/wrapper/gradle-wrapper.properties
148 |
149 | ### Gradle Patch ###
150 | **/build/
151 |
152 | # End of https://www.toptal.com/developers/gitignore/api/kotlin,intellij,gradle
153 | build
154 | TestAccount.kt
155 | *.apk
--------------------------------------------------------------------------------
/src/main/kotlin/com/github/theapache64/gpa/core/net/DefaultTlsAuthentication.kt:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | * Copyright 2020 Patrick Ahlbrecht
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 | * use this file except in compliance with the License. You may obtain a copy
6 | * of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | * License for the specific language governing permissions and limitations under
14 | * the License.
15 | */
16 | package com.github.theapache64.gpa.core.net
17 |
18 | import org.bouncycastle.tls.*
19 | import java.io.ByteArrayInputStream
20 | import java.io.IOException
21 | import java.security.KeyStore
22 | import java.security.cert.*
23 | import javax.net.ssl.TrustManager
24 | import javax.net.ssl.TrustManagerFactory
25 | import javax.net.ssl.X509TrustManager
26 |
27 | class DefaultTlsAuthentication(selectedCipherSuite: Int) : ServerOnlyTlsAuthentication() {
28 | private var trustManagers: Array16 | * xxxx: 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff ................ 17 | *18 | * Where xxxx is the offset into the buffer in 16 byte chunks, followed 19 | * by ascii coded hexadecimal bytes followed by the ASCII representation of 20 | * the bytes or '.' if they are not valid bytes. 21 | * 22 | * @author Chuck McManis 23 | */ 24 | 25 | public class HexDumpEncoder { 26 | 27 | protected PrintStream pStream; 28 | 29 | private int offset; 30 | private int thisLineLength; 31 | private int currentByte; 32 | private byte thisLine[] = new byte[16]; 33 | 34 | static void hexDigit(PrintStream p, byte x) { 35 | char c; 36 | 37 | c = (char) ((x >> 4) & 0xf); 38 | if (c > 9) 39 | c = (char) ((c-10) + 'A'); 40 | else 41 | c = (char)(c + '0'); 42 | p.write(c); 43 | c = (char) (x & 0xf); 44 | if (c > 9) 45 | c = (char)((c-10) + 'A'); 46 | else 47 | c = (char)(c + '0'); 48 | p.write(c); 49 | } 50 | 51 | protected int bytesPerAtom() { 52 | return (1); 53 | } 54 | 55 | protected int bytesPerLine() { 56 | return (16); 57 | } 58 | 59 | /** 60 | * Encode the prefix for the entire buffer. By default is simply 61 | * opens the PrintStream for use by the other functions. 62 | */ 63 | protected void encodeBufferPrefix(OutputStream aStream) throws IOException { 64 | offset = 0; 65 | pStream = new PrintStream(aStream); 66 | } 67 | 68 | protected void encodeLinePrefix(OutputStream o, int len) throws IOException { 69 | hexDigit(pStream, (byte)((offset >>> 8) & 0xff)); 70 | hexDigit(pStream, (byte)(offset & 0xff)); 71 | pStream.print(": "); 72 | currentByte = 0; 73 | thisLineLength = len; 74 | } 75 | 76 | protected void encodeAtom(OutputStream o, byte buf[], int off, int len) throws IOException { 77 | thisLine[currentByte] = buf[off]; 78 | hexDigit(pStream, buf[off]); 79 | pStream.print(" "); 80 | currentByte++; 81 | if (currentByte == 8) 82 | pStream.print(" "); 83 | } 84 | 85 | protected void encodeLineSuffix(OutputStream o) throws IOException { 86 | if (thisLineLength < 16) { 87 | for (int i = thisLineLength; i < 16; i++) { 88 | pStream.print(" "); 89 | if (i == 7) 90 | pStream.print(" "); 91 | } 92 | } 93 | pStream.print(" "); 94 | for (int i = 0; i < thisLineLength; i++) { 95 | if ((thisLine[i] < ' ') || (thisLine[i] > 'z')) { 96 | pStream.print("."); 97 | } else { 98 | pStream.write(thisLine[i]); 99 | } 100 | } 101 | pStream.println(); 102 | offset += thisLineLength; 103 | } 104 | 105 | 106 | /** 107 | * Encode bytes from the input stream, and write them as text characters 108 | * to the output stream. This method will run until it exhausts the 109 | * input stream, but does not print the line suffix for a final 110 | * line that is shorter than bytesPerLine(). 111 | */ 112 | public void encode(InputStream inStream, OutputStream outStream) 113 | throws IOException { 114 | int j; 115 | int numBytes; 116 | byte tmpbuffer[] = new byte[bytesPerLine()]; 117 | 118 | encodeBufferPrefix(outStream); 119 | 120 | while (true) { 121 | numBytes = readFully(inStream, tmpbuffer); 122 | if (numBytes == 0) { 123 | break; 124 | } 125 | encodeLinePrefix(outStream, numBytes); 126 | for (j = 0; j < numBytes; j += bytesPerAtom()) { 127 | 128 | if ((j + bytesPerAtom()) <= numBytes) { 129 | encodeAtom(outStream, tmpbuffer, j, bytesPerAtom()); 130 | } else { 131 | encodeAtom(outStream, tmpbuffer, j, (numBytes)- j); 132 | } 133 | } 134 | encodeLineSuffix(outStream); 135 | } 136 | encodeBufferSuffix(outStream); 137 | } 138 | 139 | /** 140 | * Encode the buffer in aBuffer and write the encoded 141 | * result to the OutputStream aStream. 142 | */ 143 | public void encode(byte aBuffer[], OutputStream aStream) 144 | throws IOException { 145 | ByteArrayInputStream inStream = new ByteArrayInputStream(aBuffer); 146 | encode(inStream, aStream); 147 | } 148 | 149 | /** 150 | * A 'streamless' version of encode that simply takes a buffer of 151 | * bytes and returns a string containing the encoded buffer. 152 | */ 153 | public String encode(byte aBuffer[]) { 154 | ByteArrayOutputStream outStream = new ByteArrayOutputStream(); 155 | ByteArrayInputStream inStream = new ByteArrayInputStream(aBuffer); 156 | String retVal = null; 157 | try { 158 | encode(inStream, outStream); 159 | // explicit ascii->unicode conversion 160 | retVal = outStream.toString("8859_1"); 161 | } catch (Exception IOException) { 162 | // This should never happen. 163 | throw new Error("CharacterEncoder.encode internal error"); 164 | } 165 | return (retVal); 166 | } 167 | 168 | /** 169 | * This method works around the bizarre semantics of BufferedInputStream's 170 | * read method. 171 | */ 172 | protected int readFully(InputStream in, byte buffer[]) 173 | throws java.io.IOException { 174 | for (int i = 0; i < buffer.length; i++) { 175 | int q = in.read(); 176 | if (q == -1) 177 | return i; 178 | buffer[i] = (byte)q; 179 | } 180 | return buffer.length; 181 | } 182 | 183 | /** 184 | * Encode the suffix for the entire buffer. 185 | */ 186 | protected void encodeBufferSuffix(OutputStream aStream) throws IOException { 187 | } 188 | 189 | 190 | /** 191 | * Return a byte array from the remaining bytes in this ByteBuffer. 192 | *
193 | * The ByteBuffer's position will be advanced to ByteBuffer's limit. 194 | *
195 | * To avoid an extra copy, the implementation will attempt to return the 196 | * byte array backing the ByteBuffer. If this is not possible, a 197 | * new byte array will be created. 198 | */ 199 | private byte [] getBytes(ByteBuffer bb) { 200 | /* 201 | * This should never return a BufferOverflowException, as we're 202 | * careful to allocate just the right amount. 203 | */ 204 | byte [] buf = null; 205 | 206 | /* 207 | * If it has a usable backing byte buffer, use it. Use only 208 | * if the array exactly represents the current ByteBuffer. 209 | */ 210 | if (bb.hasArray()) { 211 | byte [] tmp = bb.array(); 212 | if ((tmp.length == bb.capacity()) && 213 | (tmp.length == bb.remaining())) { 214 | buf = tmp; 215 | bb.position(bb.limit()); 216 | } 217 | } 218 | 219 | if (buf == null) { 220 | /* 221 | * This class doesn't have a concept of encode(buf, len, off), 222 | * so if we have a partial buffer, we must reallocate 223 | * space. 224 | */ 225 | buf = new byte[bb.remaining()]; 226 | 227 | /* 228 | * position() automatically updated 229 | */ 230 | bb.get(buf); 231 | } 232 | 233 | return buf; 234 | } 235 | 236 | /** 237 | * Encode the aBuffer ByteBuffer and write the encoded 238 | * result to the OutputStream aStream. 239 | *
240 | * The ByteBuffer's position will be advanced to ByteBuffer's limit. 241 | */ 242 | public void encode(ByteBuffer aBuffer, OutputStream aStream) 243 | throws IOException { 244 | byte [] buf = getBytes(aBuffer); 245 | encode(buf, aStream); 246 | } 247 | 248 | /** 249 | * A 'streamless' version of encode that simply takes a ByteBuffer 250 | * and returns a string containing the encoded buffer. 251 | *
252 | * The ByteBuffer's position will be advanced to ByteBuffer's limit. 253 | */ 254 | public String encode(ByteBuffer aBuffer) { 255 | byte [] buf = getBytes(aBuffer); 256 | return encode(buf); 257 | } 258 | 259 | /** 260 | * Encode bytes from the input stream, and write them as text characters 261 | * to the output stream. This method will run until it exhausts the 262 | * input stream. It differs from encode in that it will add the 263 | * line at the end of a final line that is shorter than bytesPerLine(). 264 | */ 265 | public void encodeBuffer(InputStream inStream, OutputStream outStream) 266 | throws IOException { 267 | int j; 268 | int numBytes; 269 | byte tmpbuffer[] = new byte[bytesPerLine()]; 270 | 271 | encodeBufferPrefix(outStream); 272 | 273 | while (true) { 274 | numBytes = readFully(inStream, tmpbuffer); 275 | if (numBytes == 0) { 276 | break; 277 | } 278 | encodeLinePrefix(outStream, numBytes); 279 | for (j = 0; j < numBytes; j += bytesPerAtom()) { 280 | if ((j + bytesPerAtom()) <= numBytes) { 281 | encodeAtom(outStream, tmpbuffer, j, bytesPerAtom()); 282 | } else { 283 | encodeAtom(outStream, tmpbuffer, j, (numBytes)- j); 284 | } 285 | } 286 | encodeLineSuffix(outStream); 287 | if (numBytes < bytesPerLine()) { 288 | break; 289 | } 290 | } 291 | encodeBufferSuffix(outStream); 292 | } 293 | 294 | /** 295 | * Encode the buffer in aBuffer and write the encoded 296 | * result to the OutputStream aStream. 297 | */ 298 | public void encodeBuffer(byte aBuffer[], OutputStream aStream) 299 | throws IOException { 300 | ByteArrayInputStream inStream = new ByteArrayInputStream(aBuffer); 301 | encodeBuffer(inStream, aStream); 302 | } 303 | 304 | /** 305 | * A 'streamless' version of encode that simply takes a buffer of 306 | * bytes and returns a string containing the encoded buffer. 307 | */ 308 | public String encodeBuffer(byte aBuffer[]) { 309 | ByteArrayOutputStream outStream = new ByteArrayOutputStream(); 310 | ByteArrayInputStream inStream = new ByteArrayInputStream(aBuffer); 311 | try { 312 | encodeBuffer(inStream, outStream); 313 | } catch (Exception IOException) { 314 | // This should never happen. 315 | throw new Error("CharacterEncoder.encodeBuffer internal error"); 316 | } 317 | return (outStream.toString()); 318 | } 319 | 320 | /** 321 | * Encode the aBuffer ByteBuffer and write the encoded 322 | * result to the OutputStream aStream. 323 | *
324 | * The ByteBuffer's position will be advanced to ByteBuffer's limit. 325 | */ 326 | public void encodeBuffer(ByteBuffer aBuffer, OutputStream aStream) 327 | throws IOException { 328 | byte [] buf = getBytes(aBuffer); 329 | encodeBuffer(buf, aStream); 330 | } 331 | 332 | /** 333 | * A 'streamless' version of encode that simply takes a ByteBuffer 334 | * and returns a string containing the encoded buffer. 335 | *
336 | * The ByteBuffer's position will be advanced to ByteBuffer's limit.
337 | */
338 | public String encodeBuffer(ByteBuffer aBuffer) {
339 | byte [] buf = getBytes(aBuffer);
340 | return encodeBuffer(buf);
341 | }
342 |
343 | }
344 |
--------------------------------------------------------------------------------
/src/main/java/com/akdeniz/googleplaycrawler/Utils.java:
--------------------------------------------------------------------------------
1 | package com.akdeniz.googleplaycrawler;
2 |
3 | import java.io.ByteArrayOutputStream;
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 | import java.math.BigInteger;
7 | import java.security.KeyFactory;
8 | import java.security.KeyManagementException;
9 | import java.security.MessageDigest;
10 | import java.security.NoSuchAlgorithmException;
11 | import java.security.PublicKey;
12 | import java.security.spec.RSAPublicKeySpec;
13 | import java.util.Arrays;
14 | import java.util.Date;
15 | import java.util.HashMap;
16 | import java.util.Map;
17 | import java.util.StringTokenizer;
18 |
19 | import javax.crypto.Cipher;
20 | import javax.net.ssl.SSLContext;
21 | import javax.net.ssl.TrustManager;
22 |
23 | import org.apache.http.conn.scheme.Scheme;
24 | import org.apache.http.conn.ssl.SSLSocketFactory;
25 |
26 | import com.akdeniz.googleplaycrawler.GooglePlay.AndroidBuildProto;
27 | import com.akdeniz.googleplaycrawler.GooglePlay.AndroidCheckinProto;
28 | import com.akdeniz.googleplaycrawler.GooglePlay.AndroidCheckinRequest;
29 | import com.akdeniz.googleplaycrawler.GooglePlay.DeviceConfigurationProto;
30 | import com.akdeniz.googleplaycrawler.misc.Base64;
31 | import com.akdeniz.googleplaycrawler.misc.DummyX509TrustManager;
32 |
33 | /**
34 | *
35 | * @author akdeniz
36 | *
37 | */
38 | public class Utils {
39 |
40 | private static final String GOOGLE_PUBLIC_KEY = "AAAAgMom/1a/v0lblO2Ubrt60J2gcuXSljGFQXgcyZWveWLEwo6prwgi3"
41 | + "iJIZdodyhKZQrNWp5nKJ3srRXcUW+F1BD3baEVGcmEgqaLZUNBjm057pKRI16kB0YppeGx5qIQ5QjKzsR8ETQbKLNWgRY0Q"
42 | + "RNVz34kMJR3P/LgHax/6rmf5AAAAAwEAAQ==";
43 |
44 | /**
45 | * Parses key-value response into map.
46 | */
47 | public static Map The padding '=' characters at the end are considered optional, but
106 | * if any are present, there must be the correct number of them.
107 | *
108 | * @param str the input String to decode, which is converted to
109 | * bytes using the default charset
110 | * @param flags controls certain features of the decoded output.
111 | * Pass {@code DEFAULT} to decode standard Base64.
112 | *
113 | * @throws IllegalArgumentException if the input contains
114 | * incorrect padding
115 | */
116 | public static byte[] decode(String str, int flags) {
117 | return decode(str.getBytes(), flags);
118 | }
119 |
120 | /**
121 | * Decode the Base64-encoded data in input and return the data in
122 | * a new byte array.
123 | *
124 | * The padding '=' characters at the end are considered optional, but
125 | * if any are present, there must be the correct number of them.
126 | *
127 | * @param input the input array to decode
128 | * @param flags controls certain features of the decoded output.
129 | * Pass {@code DEFAULT} to decode standard Base64.
130 | *
131 | * @throws IllegalArgumentException if the input contains
132 | * incorrect padding
133 | */
134 | public static byte[] decode(byte[] input, int flags) {
135 | return decode(input, 0, input.length, flags);
136 | }
137 |
138 | /**
139 | * Decode the Base64-encoded data in input and return the data in
140 | * a new byte array.
141 | *
142 | * The padding '=' characters at the end are considered optional, but
143 | * if any are present, there must be the correct number of them.
144 | *
145 | * @param input the data to decode
146 | * @param offset the position within the input array at which to start
147 | * @param len the number of bytes of input to decode
148 | * @param flags controls certain features of the decoded output.
149 | * Pass {@code DEFAULT} to decode standard Base64.
150 | *
151 | * @throws IllegalArgumentException if the input contains
152 | * incorrect padding
153 | */
154 | public static byte[] decode(byte[] input, int offset, int len, int flags) {
155 | // Allocate space for the most data the input could represent.
156 | // (It could contain less if it contains whitespace, etc.)
157 | Decoder decoder = new Decoder(flags, new byte[len*3/4]);
158 |
159 | if (!decoder.process(input, offset, len, true)) {
160 | throw new IllegalArgumentException("bad base-64");
161 | }
162 |
163 | // Maybe we got lucky and allocated exactly enough output space.
164 | if (decoder.op == decoder.output.length) {
165 | return decoder.output;
166 | }
167 |
168 | // Need to shorten the array, so allocate a new one of the
169 | // right size and copy.
170 | byte[] temp = new byte[decoder.op];
171 | System.arraycopy(decoder.output, 0, temp, 0, decoder.op);
172 | return temp;
173 | }
174 |
175 | /* package */ static class Decoder extends Coder {
176 | /**
177 | * Lookup table for turning bytes into their position in the
178 | * Base64 alphabet.
179 | */
180 | private static final int DECODE[] = {
181 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
182 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
183 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
184 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
185 | -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
186 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
187 | -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
188 | 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
189 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
190 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
191 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
192 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
193 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
194 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
195 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
196 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
197 | };
198 |
199 | /**
200 | * Decode lookup table for the "web safe" variant (RFC 3548
201 | * sec. 4) where - and _ replace + and /.
202 | */
203 | private static final int DECODE_WEBSAFE[] = {
204 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
205 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
206 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
207 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
208 | -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
209 | 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,
210 | -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
211 | 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
212 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
213 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
214 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
215 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
216 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
217 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
218 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
219 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
220 | };
221 |
222 | /** Non-data values in the DECODE arrays. */
223 | private static final int SKIP = -1;
224 | private static final int EQUALS = -2;
225 |
226 | /**
227 | * States 0-3 are reading through the next input tuple.
228 | * State 4 is having read one '=' and expecting exactly
229 | * one more.
230 | * State 5 is expecting no more data or padding characters
231 | * in the input.
232 | * State 6 is the error state; an error has been detected
233 | * in the input and no future input can "fix" it.
234 | */
235 | private int state; // state number (0 to 6)
236 | private int value;
237 |
238 | final private int[] alphabet;
239 |
240 | public Decoder(int flags, byte[] output) {
241 | this.output = output;
242 |
243 | alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE;
244 | state = 0;
245 | value = 0;
246 | }
247 |
248 | /**
249 | * @return an overestimate for the number of bytes {@code
250 | * len} bytes could decode to.
251 | */
252 | public int maxOutputSize(int len) {
253 | return len * 3/4 + 10;
254 | }
255 |
256 | /**
257 | * Decode another block of input data.
258 | *
259 | * @return true if the state machine is still healthy. false if
260 | * bad base-64 data has been detected in the input stream.
261 | */
262 | public boolean process(byte[] input, int offset, int len, boolean finish) {
263 | if (this.state == 6) return false;
264 |
265 | int p = offset;
266 | len += offset;
267 |
268 | // Using local variables makes the decoder about 12%
269 | // faster than if we manipulate the member variables in
270 | // the loop. (Even alphabet makes a measurable
271 | // difference, which is somewhat surprising to me since
272 | // the member variable is final.)
273 | int state = this.state;
274 | int value = this.value;
275 | int op = 0;
276 | final byte[] output = this.output;
277 | final int[] alphabet = this.alphabet;
278 |
279 | while (p < len) {
280 | // Try the fast path: we're starting a new tuple and the
281 | // next four bytes of the input stream are all data
282 | // bytes. This corresponds to going through states
283 | // 0-1-2-3-0. We expect to use this method for most of
284 | // the data.
285 | //
286 | // If any of the next four bytes of input are non-data
287 | // (whitespace, etc.), value will end up negative. (All
288 | // the non-data values in decode are small negative
289 | // numbers, so shifting any of them up and or'ing them
290 | // together will result in a value with its top bit set.)
291 | //
292 | // You can remove this whole block and the output should
293 | // be the same, just slower.
294 | if (state == 0) {
295 | while (p+4 <= len &&
296 | (value = ((alphabet[input[p] & 0xff] << 18) |
297 | (alphabet[input[p+1] & 0xff] << 12) |
298 | (alphabet[input[p+2] & 0xff] << 6) |
299 | (alphabet[input[p+3] & 0xff]))) >= 0) {
300 | output[op+2] = (byte) value;
301 | output[op+1] = (byte) (value >> 8);
302 | output[op] = (byte) (value >> 16);
303 | op += 3;
304 | p += 4;
305 | }
306 | if (p >= len) break;
307 | }
308 |
309 | // The fast path isn't available -- either we've read a
310 | // partial tuple, or the next four input bytes aren't all
311 | // data, or whatever. Fall back to the slower state
312 | // machine implementation.
313 |
314 | int d = alphabet[input[p++] & 0xff];
315 |
316 | switch (state) {
317 | case 0:
318 | if (d >= 0) {
319 | value = d;
320 | ++state;
321 | } else if (d != SKIP) {
322 | this.state = 6;
323 | return false;
324 | }
325 | break;
326 |
327 | case 1:
328 | if (d >= 0) {
329 | value = (value << 6) | d;
330 | ++state;
331 | } else if (d != SKIP) {
332 | this.state = 6;
333 | return false;
334 | }
335 | break;
336 |
337 | case 2:
338 | if (d >= 0) {
339 | value = (value << 6) | d;
340 | ++state;
341 | } else if (d == EQUALS) {
342 | // Emit the last (partial) output tuple;
343 | // expect exactly one more padding character.
344 | output[op++] = (byte) (value >> 4);
345 | state = 4;
346 | } else if (d != SKIP) {
347 | this.state = 6;
348 | return false;
349 | }
350 | break;
351 |
352 | case 3:
353 | if (d >= 0) {
354 | // Emit the output triple and return to state 0.
355 | value = (value << 6) | d;
356 | output[op+2] = (byte) value;
357 | output[op+1] = (byte) (value >> 8);
358 | output[op] = (byte) (value >> 16);
359 | op += 3;
360 | state = 0;
361 | } else if (d == EQUALS) {
362 | // Emit the last (partial) output tuple;
363 | // expect no further data or padding characters.
364 | output[op+1] = (byte) (value >> 2);
365 | output[op] = (byte) (value >> 10);
366 | op += 2;
367 | state = 5;
368 | } else if (d != SKIP) {
369 | this.state = 6;
370 | return false;
371 | }
372 | break;
373 |
374 | case 4:
375 | if (d == EQUALS) {
376 | ++state;
377 | } else if (d != SKIP) {
378 | this.state = 6;
379 | return false;
380 | }
381 | break;
382 |
383 | case 5:
384 | if (d != SKIP) {
385 | this.state = 6;
386 | return false;
387 | }
388 | break;
389 | }
390 | }
391 |
392 | if (!finish) {
393 | // We're out of input, but a future call could provide
394 | // more.
395 | this.state = state;
396 | this.value = value;
397 | this.op = op;
398 | return true;
399 | }
400 |
401 | // Done reading input. Now figure out where we are left in
402 | // the state machine and finish up.
403 |
404 | switch (state) {
405 | case 0:
406 | // Output length is a multiple of three. Fine.
407 | break;
408 | case 1:
409 | // Read one extra input byte, which isn't enough to
410 | // make another output byte. Illegal.
411 | this.state = 6;
412 | return false;
413 | case 2:
414 | // Read two extra input bytes, enough to emit 1 more
415 | // output byte. Fine.
416 | output[op++] = (byte) (value >> 4);
417 | break;
418 | case 3:
419 | // Read three extra input bytes, enough to emit 2 more
420 | // output bytes. Fine.
421 | output[op++] = (byte) (value >> 10);
422 | output[op++] = (byte) (value >> 2);
423 | break;
424 | case 4:
425 | // Read one padding '=' when we expected 2. Illegal.
426 | this.state = 6;
427 | return false;
428 | case 5:
429 | // Read all the padding '='s we expected and no more.
430 | // Fine.
431 | break;
432 | }
433 |
434 | this.state = state;
435 | this.op = op;
436 | return true;
437 | }
438 | }
439 |
440 | // --------------------------------------------------------
441 | // encoding
442 | // --------------------------------------------------------
443 |
444 | /**
445 | * Base64-encode the given data and return a newly allocated
446 | * String with the result.
447 | *
448 | * @param input the data to encode
449 | * @param flags controls certain features of the encoded output.
450 | * Passing {@code DEFAULT} results in output that
451 | * adheres to RFC 2045.
452 | */
453 | public static String encodeToString(byte[] input, int flags) {
454 | try {
455 | return new String(encode(input, flags), "US-ASCII");
456 | } catch (UnsupportedEncodingException e) {
457 | // US-ASCII is guaranteed to be available.
458 | throw new AssertionError(e);
459 | }
460 | }
461 |
462 | /**
463 | * Base64-encode the given data and return a newly allocated
464 | * String with the result.
465 | *
466 | * @param input the data to encode
467 | * @param offset the position within the input array at which to
468 | * start
469 | * @param len the number of bytes of input to encode
470 | * @param flags controls certain features of the encoded output.
471 | * Passing {@code DEFAULT} results in output that
472 | * adheres to RFC 2045.
473 | */
474 | public static String encodeToString(byte[] input, int offset, int len, int flags) {
475 | try {
476 | return new String(encode(input, offset, len, flags), "US-ASCII");
477 | } catch (UnsupportedEncodingException e) {
478 | // US-ASCII is guaranteed to be available.
479 | throw new AssertionError(e);
480 | }
481 | }
482 |
483 | /**
484 | * Base64-encode the given data and return a newly allocated
485 | * byte[] with the result.
486 | *
487 | * @param input the data to encode
488 | * @param flags controls certain features of the encoded output.
489 | * Passing {@code DEFAULT} results in output that
490 | * adheres to RFC 2045.
491 | */
492 | public static byte[] encode(byte[] input, int flags) {
493 | return encode(input, 0, input.length, flags);
494 | }
495 |
496 | /**
497 | * Base64-encode the given data and return a newly allocated
498 | * byte[] with the result.
499 | *
500 | * @param input the data to encode
501 | * @param offset the position within the input array at which to
502 | * start
503 | * @param len the number of bytes of input to encode
504 | * @param flags controls certain features of the encoded output.
505 | * Passing {@code DEFAULT} results in output that
506 | * adheres to RFC 2045.
507 | */
508 | public static byte[] encode(byte[] input, int offset, int len, int flags) {
509 | Encoder encoder = new Encoder(flags, null);
510 |
511 | // Compute the exact length of the array we will produce.
512 | int output_len = len / 3 * 4;
513 |
514 | // Account for the tail of the data and the padding bytes, if any.
515 | if (encoder.do_padding) {
516 | if (len % 3 > 0) {
517 | output_len += 4;
518 | }
519 | } else {
520 | switch (len % 3) {
521 | case 0: break;
522 | case 1: output_len += 2; break;
523 | case 2: output_len += 3; break;
524 | }
525 | }
526 |
527 | // Account for the newlines, if any.
528 | if (encoder.do_newline && len > 0) {
529 | output_len += (((len-1) / (3 * Encoder.LINE_GROUPS)) + 1) *
530 | (encoder.do_cr ? 2 : 1);
531 | }
532 |
533 | encoder.output = new byte[output_len];
534 | encoder.process(input, offset, len, true);
535 |
536 | assert encoder.op == output_len;
537 |
538 | return encoder.output;
539 | }
540 |
541 | /* package */ static class Encoder extends Coder {
542 | /**
543 | * Emit a new line every this many output tuples. Corresponds to
544 | * a 76-character line length (the maximum allowable according to
545 | * RFC 2045).
546 | */
547 | public static final int LINE_GROUPS = 19;
548 |
549 | /**
550 | * Lookup table for turning Base64 alphabet positions (6 bits)
551 | * into output bytes.
552 | */
553 | private static final byte ENCODE[] = {
554 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
555 | 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
556 | 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
557 | 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
558 | };
559 |
560 | /**
561 | * Lookup table for turning Base64 alphabet positions (6 bits)
562 | * into output bytes.
563 | */
564 | private static final byte ENCODE_WEBSAFE[] = {
565 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
566 | 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
567 | 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
568 | 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
569 | };
570 |
571 | final private byte[] tail;
572 | /* package */ int tailLen;
573 | private int count;
574 |
575 | final public boolean do_padding;
576 | final public boolean do_newline;
577 | final public boolean do_cr;
578 | final private byte[] alphabet;
579 |
580 | public Encoder(int flags, byte[] output) {
581 | this.output = output;
582 |
583 | do_padding = (flags & NO_PADDING) == 0;
584 | do_newline = (flags & NO_WRAP) == 0;
585 | do_cr = (flags & CRLF) != 0;
586 | alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE;
587 |
588 | tail = new byte[2];
589 | tailLen = 0;
590 |
591 | count = do_newline ? LINE_GROUPS : -1;
592 | }
593 |
594 | /**
595 | * @return an overestimate for the number of bytes {@code
596 | * len} bytes could encode to.
597 | */
598 | public int maxOutputSize(int len) {
599 | return len * 8/5 + 10;
600 | }
601 |
602 | public boolean process(byte[] input, int offset, int len, boolean finish) {
603 | // Using local variables makes the encoder about 9% faster.
604 | final byte[] alphabet = this.alphabet;
605 | final byte[] output = this.output;
606 | int op = 0;
607 | int count = this.count;
608 |
609 | int p = offset;
610 | len += offset;
611 | int v = -1;
612 |
613 | // First we need to concatenate the tail of the previous call
614 | // with any input bytes available now and see if we can empty
615 | // the tail.
616 |
617 | switch (tailLen) {
618 | case 0:
619 | // There was no tail.
620 | break;
621 |
622 | case 1:
623 | if (p+2 <= len) {
624 | // A 1-byte tail with at least 2 bytes of
625 | // input available now.
626 | v = ((tail[0] & 0xff) << 16) |
627 | ((input[p++] & 0xff) << 8) |
628 | (input[p++] & 0xff);
629 | tailLen = 0;
630 | };
631 | break;
632 |
633 | case 2:
634 | if (p+1 <= len) {
635 | // A 2-byte tail with at least 1 byte of input.
636 | v = ((tail[0] & 0xff) << 16) |
637 | ((tail[1] & 0xff) << 8) |
638 | (input[p++] & 0xff);
639 | tailLen = 0;
640 | }
641 | break;
642 | }
643 |
644 | if (v != -1) {
645 | output[op++] = alphabet[(v >> 18) & 0x3f];
646 | output[op++] = alphabet[(v >> 12) & 0x3f];
647 | output[op++] = alphabet[(v >> 6) & 0x3f];
648 | output[op++] = alphabet[v & 0x3f];
649 | if (--count == 0) {
650 | if (do_cr) output[op++] = '\r';
651 | output[op++] = '\n';
652 | count = LINE_GROUPS;
653 | }
654 | }
655 |
656 | // At this point either there is no tail, or there are fewer
657 | // than 3 bytes of input available.
658 |
659 | // The main loop, turning 3 input bytes into 4 output bytes on
660 | // each iteration.
661 | while (p+3 <= len) {
662 | v = ((input[p] & 0xff) << 16) |
663 | ((input[p+1] & 0xff) << 8) |
664 | (input[p+2] & 0xff);
665 | output[op] = alphabet[(v >> 18) & 0x3f];
666 | output[op+1] = alphabet[(v >> 12) & 0x3f];
667 | output[op+2] = alphabet[(v >> 6) & 0x3f];
668 | output[op+3] = alphabet[v & 0x3f];
669 | p += 3;
670 | op += 4;
671 | if (--count == 0) {
672 | if (do_cr) output[op++] = '\r';
673 | output[op++] = '\n';
674 | count = LINE_GROUPS;
675 | }
676 | }
677 |
678 | if (finish) {
679 | // Finish up the tail of the input. Note that we need to
680 | // consume any bytes in tail before any bytes
681 | // remaining in input; there should be at most two bytes
682 | // total.
683 |
684 | if (p-tailLen == len-1) {
685 | int t = 0;
686 | v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4;
687 | tailLen -= t;
688 | output[op++] = alphabet[(v >> 6) & 0x3f];
689 | output[op++] = alphabet[v & 0x3f];
690 | if (do_padding) {
691 | output[op++] = '=';
692 | output[op++] = '=';
693 | }
694 | if (do_newline) {
695 | if (do_cr) output[op++] = '\r';
696 | output[op++] = '\n';
697 | }
698 | } else if (p-tailLen == len-2) {
699 | int t = 0;
700 | v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) |
701 | (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2);
702 | tailLen -= t;
703 | output[op++] = alphabet[(v >> 12) & 0x3f];
704 | output[op++] = alphabet[(v >> 6) & 0x3f];
705 | output[op++] = alphabet[v & 0x3f];
706 | if (do_padding) {
707 | output[op++] = '=';
708 | }
709 | if (do_newline) {
710 | if (do_cr) output[op++] = '\r';
711 | output[op++] = '\n';
712 | }
713 | } else if (do_newline && op > 0 && count != LINE_GROUPS) {
714 | if (do_cr) output[op++] = '\r';
715 | output[op++] = '\n';
716 | }
717 |
718 | assert tailLen == 0;
719 | assert p == len;
720 | } else {
721 | // Save the leftovers in tail to be consumed on the next
722 | // call to encodeInternal.
723 |
724 | if (p == len-1) {
725 | tail[tailLen++] = input[p];
726 | } else if (p == len-2) {
727 | tail[tailLen++] = input[p];
728 | tail[tailLen++] = input[p+1];
729 | }
730 | }
731 |
732 | this.op = op;
733 | this.count = count;
734 |
735 | return true;
736 | }
737 | }
738 |
739 | private Base64() { } // don't instantiate
740 | }
741 |
--------------------------------------------------------------------------------
/src/main/java/com/akdeniz/googleplaycrawler/GooglePlayAPI.java:
--------------------------------------------------------------------------------
1 | package com.akdeniz.googleplaycrawler;
2 |
3 | import java.io.IOException;
4 | import java.io.InputStream;
5 | import java.math.BigInteger;
6 | import java.security.Key;
7 | import java.security.KeyFactory;
8 | import java.security.MessageDigest;
9 | import java.security.PublicKey;
10 | import java.security.spec.RSAPublicKeySpec;
11 | import java.util.ArrayList;
12 | import java.util.List;
13 | import java.util.Map;
14 | import java.util.PropertyResourceBundle;
15 | import java.util.ResourceBundle;
16 |
17 | import javax.crypto.Cipher;
18 |
19 | import org.apache.http.HttpEntity;
20 | import org.apache.http.HttpResponse;
21 | import org.apache.http.NameValuePair;
22 | import org.apache.http.client.ClientProtocolException;
23 | import org.apache.http.client.HttpClient;
24 | import org.apache.http.client.entity.UrlEncodedFormEntity;
25 | import org.apache.http.client.methods.HttpGet;
26 | import org.apache.http.client.methods.HttpPost;
27 | import org.apache.http.client.methods.HttpUriRequest;
28 | import org.apache.http.client.utils.URLEncodedUtils;
29 | import org.apache.http.conn.ClientConnectionManager;
30 | import org.apache.http.entity.ByteArrayEntity;
31 | import org.apache.http.impl.client.DefaultHttpClient;
32 | import org.apache.http.impl.conn.PoolingClientConnectionManager;
33 | import org.apache.http.impl.conn.SchemeRegistryFactory;
34 | import org.apache.http.message.BasicNameValuePair;
35 |
36 | import com.akdeniz.googleplaycrawler.GooglePlay.AndroidAppDeliveryData;
37 | import com.akdeniz.googleplaycrawler.GooglePlay.AndroidCheckinRequest;
38 | import com.akdeniz.googleplaycrawler.GooglePlay.AndroidCheckinResponse;
39 | //import com.akdeniz.googleplaycrawler.GooglePlay.BrowseResponse;
40 | import com.akdeniz.googleplaycrawler.GooglePlay.BulkDetailsRequest;
41 | import com.akdeniz.googleplaycrawler.GooglePlay.BulkDetailsRequest.Builder;
42 | import com.akdeniz.googleplaycrawler.GooglePlay.BulkDetailsResponse;
43 | import com.akdeniz.googleplaycrawler.GooglePlay.BuyResponse;
44 | import com.akdeniz.googleplaycrawler.GooglePlay.DetailsResponse;
45 | import com.akdeniz.googleplaycrawler.GooglePlay.ListResponse;
46 | import com.akdeniz.googleplaycrawler.GooglePlay.ResponseWrapper;
47 | import com.akdeniz.googleplaycrawler.GooglePlay.ReviewResponse;
48 | import com.akdeniz.googleplaycrawler.GooglePlay.SearchResponse;
49 | import com.akdeniz.googleplaycrawler.GooglePlay.UploadDeviceConfigRequest;
50 | import com.akdeniz.googleplaycrawler.GooglePlay.UploadDeviceConfigResponse;
51 | import com.akdeniz.googleplaycrawler.misc.Base64;
52 |
53 | /**
54 | * This class provides
55 | *
60 | * XXX : DO NOT call checkin, login and download consecutively. To allow
61 | * server to catch up, sleep for a while before download! (5 sec will do!) Also
62 | * it is recommended to call checkin once and use generated android-id for
63 | * further operations.
64 | *
809 | * Note that changing this value has no affect on localized application list
810 | * that server provides. It depends on only your IP location.
811 | *
812 | *
813 | * @param localization
814 | * can be en-EN, en-US, tr-TR, fr-FR ... (default : en-EN)
815 | */
816 | public void setLocalization(String localization) {
817 | this.localization = localization;
818 | }
819 |
820 | /**
821 | * @return the useragent
822 | */
823 | public String getUseragent() {
824 | return useragent;
825 | }
826 |
827 | /**
828 | * @param useragent
829 | * the useragent to set
830 | */
831 | public void setUseragent(String useragent) {
832 | this.useragent = useragent;
833 | }
834 |
835 | }
836 |
--------------------------------------------------------------------------------
checkin, search, details, bulkDetails, browse, list and download
56 | * capabilities. It uses Apache Commons HttpClient for POST and GET
57 | * requests.
58 | *
59 | * checkin() or set by using setAndroidID before
136 | * using other abilities.
137 | */
138 | public GooglePlayAPI(String email, String password) {
139 | this.setEmail(email);
140 | this.password = password;
141 | setClient(new DefaultHttpClient(getConnectionManager()));
142 | // setUseragent("Android-Finsky/3.10.14 (api=3,versionCode=8016014,sdk=15,device=GT-I9300,hardware=aries,product=GT-I9300)");
143 | // setUseragent("Android-Finsky/6.5.08.D-all (versionCode=80650800,sdk=24,device=dream2lte,hardware=dream2lte,product=dream2ltexx,build=NRD90M:user)");
144 | setUseragent("Android-Finsky/13.1.32-all (versionCode=81313200,sdk=24,device=dream2lte,hardware=dream2lte,product=dream2ltexx,build=NRD90M:user)");
145 | }
146 |
147 | /**
148 | * Connection manager to allow concurrent connections.
149 | *
150 | * @return {@link ClientConnectionManager} instance
151 | */
152 | public static ClientConnectionManager getConnectionManager() {
153 | PoolingClientConnectionManager connManager = new PoolingClientConnectionManager(
154 | SchemeRegistryFactory.createDefault());
155 | connManager.setMaxTotal(100);
156 | connManager.setDefaultMaxPerRoute(30);
157 | return connManager;
158 | }
159 |
160 | /**
161 | * Performs authentication on "ac2dm" service and match up android id,
162 | * security token and email by checking them in on this server.
163 | *
164 | * This function sets check-inded android ID and that can be taken either by
165 | * using getToken() or from returned
166 | * {@link AndroidCheckinResponse} instance.
167 | *
168 | */
169 | public AndroidCheckinResponse checkin() throws Exception {
170 |
171 | // this first checkin is for generating android-id
172 | AndroidCheckinResponse checkinResponse = postCheckin(Utils
173 | .generateAndroidCheckinRequest().toByteArray());
174 | this.setAndroidID(BigInteger.valueOf(checkinResponse.getGsfId()).toString(
175 | 16));
176 | setSecurityToken((BigInteger.valueOf(checkinResponse.getSecurityToken())
177 | .toString(16)));
178 |
179 | String c2dmAuth = loginAC2DM();
180 | // login();
181 | // String c2dmAuth= getToken();
182 |
183 | AndroidCheckinRequest.Builder checkInbuilder = AndroidCheckinRequest
184 | .newBuilder(Utils.generateAndroidCheckinRequest());
185 |
186 | AndroidCheckinRequest build = checkInbuilder
187 | .setId(new BigInteger(this.getAndroidID(), 16).longValue())
188 | .setSecurityToken(new BigInteger(getSecurityToken(), 16).longValue())
189 | .addAccountCookie("[" + getEmail() + "]").addAccountCookie(c2dmAuth)
190 | .build();
191 | // this is the second checkin to match credentials with android-id
192 | return postCheckin(build.toByteArray());
193 | }
194 |
195 | private static int readInt(byte[] bArr, int i) {
196 | return (((((bArr[i] & 255) << 24) | 0) | ((bArr[i + 1] & 255) << 16)) | ((bArr[i + 2] & 255) << 8))
197 | | (bArr[i + 3] & 255);
198 | }
199 |
200 | public static PublicKey createKeyFromString(String str, byte[] bArr) {
201 | try {
202 | byte[] decode = Base64.decode(str, 0);
203 | int readInt = readInt(decode, 0);
204 | byte[] obj = new byte[readInt];
205 | System.arraycopy(decode, 4, obj, 0, readInt);
206 | BigInteger bigInteger = new BigInteger(1, obj);
207 | int readInt2 = readInt(decode, readInt + 4);
208 | byte[] obj2 = new byte[readInt2];
209 | System.arraycopy(decode, readInt + 8, obj2, 0, readInt2);
210 | BigInteger bigInteger2 = new BigInteger(1, obj2);
211 | decode = MessageDigest.getInstance("SHA-1").digest(decode);
212 | bArr[0] = (byte) 0;
213 | System.arraycopy(decode, 0, bArr, 1, 4);
214 | return KeyFactory.getInstance("RSA").generatePublic(
215 | new RSAPublicKeySpec(bigInteger, bigInteger2));
216 | }
217 | catch (Throwable e) {
218 | throw new RuntimeException(e);
219 | }
220 | }
221 |
222 | private static String encryptString(String str) {
223 | int i = 0;
224 | ResourceBundle bundle = PropertyResourceBundle
225 | .getBundle("com.akdeniz.googleplaycrawler.crypt");
226 | String string = bundle.getString("key");
227 |
228 | byte[] obj = new byte[5];
229 | Key createKeyFromString = createKeyFromString(string, obj);
230 | if (createKeyFromString == null) {
231 | return null;
232 | }
233 | try {
234 | Cipher instance = Cipher
235 | .getInstance("RSA/ECB/OAEPWITHSHA1ANDMGF1PADDING");
236 | byte[] bytes = str.getBytes("UTF-8");
237 | int length = ((bytes.length - 1) / 86) + 1;
238 | byte[] obj2 = new byte[(length * 133)];
239 | while (i < length) {
240 | instance.init(1, createKeyFromString);
241 | byte[] doFinal = instance.doFinal(bytes, i * 86,
242 | i == length + -1 ? bytes.length - (i * 86) : 86);
243 | System.arraycopy(obj, 0, obj2, i * 133, obj.length);
244 | System.arraycopy(doFinal, 0, obj2, (i * 133) + obj.length,
245 | doFinal.length);
246 | i++;
247 | }
248 | return Base64.encodeToString(obj2, 10);
249 | }
250 | catch (Throwable e) {
251 | throw new RuntimeException(e);
252 | }
253 | }
254 |
255 | /**
256 | * Logins AC2DM server and returns authentication string.
257 | */
258 | public String loginAC2DM() throws IOException {
259 | HttpEntity c2dmResponseEntity = executePost(URL_LOGIN,
260 | new String[][] {
261 | { "Email", this.getEmail() },
262 | { "EncryptedPasswd",
263 | encryptString(this.getEmail() + "\u0000" + this.password) },
264 | { "add_account", "1" }, { "service", "ac2dm" },
265 | { "accountType", ACCOUNT_TYPE_HOSTED_OR_GOOGLE },
266 | { "has_permission", "1" }, { "source", "android" },
267 | { "app", "com.google.android.gsf" }, { "device_country", "us" },
268 | { "device_country", "us" }, { "lang", "en" },
269 | { "sdk_version", "16" }, }, null);
270 |
271 | MapsetToken. This function does not performs
292 | * authentication, it simply sets authentication token.
293 | */
294 | public void login(String token) throws Exception {
295 | setToken(token);
296 | }
297 |
298 | /**
299 | * Authenticates on server with given email and password and sets
300 | * authentication token. This token can be used to login instead of using
301 | * email and password every time.
302 | */
303 | public void login() throws Exception {
304 | /*
305 | * HttpEntity responseEntity = executePost(URL_LOGIN, new String[][] { {
306 | * "Email", this.getEmail() }, { "EncryptedPasswd",
307 | * encryptString(this.getEmail()+"\u0000"+this.password) }, { "service",
308 | * "androidmarket" }, { "add_account", "1"}, { "accountType",
309 | * ACCOUNT_TYPE_HOSTED_OR_GOOGLE }, { "has_permission", "1" }, { "source",
310 | * "android" }, { "androidId", this.getAndroidID() }, { "app",
311 | * "com.android.vending" }, { "device_country", "en" }, { "lang", "en" }, {
312 | * "sdk_version", "17" }, }, null);
313 | *
314 | * Mapsearch(query, null, null)
325 | */
326 | public SearchResponse search(String query) throws IOException {
327 | return search(query, null, null);
328 | }
329 |
330 | /**
331 | * Fetches a search results for given query. Offset and numberOfResult
332 | * parameters are optional and null can be passed!
333 | */
334 | public SearchResponse search(String query, Integer offset,
335 | Integer numberOfResult) throws IOException {
336 |
337 | ResponseWrapper responseWrapper = executeGETRequest(
338 | SEARCH_URL,
339 | new String[][] {
340 | { "c", "3" },
341 | { "q", query },
342 | { "o", (offset == null) ? null : String.valueOf(offset) },
343 | {
344 | "n",
345 | (numberOfResult == null) ? null : String
346 | .valueOf(numberOfResult) }, });
347 |
348 | return responseWrapper.getPayload().getSearchResponse();
349 | }
350 |
351 | public ResponseWrapper searchApp(String query) throws IOException {
352 | ResponseWrapper responseWrapper = executeGETRequest(SEARCH_URL,
353 | new String[][] { { "c", "3" }, { "q", query },
354 |
355 | });
356 |
357 | return responseWrapper;
358 | }
359 |
360 | public ResponseWrapper getList(String url) throws IOException {
361 | return executeGETRequest(FDFE_URL+url, null);
362 | }
363 |
364 | /**
365 | * Fetches detailed information about passed package name. If it is needed to
366 | * fetch information about more than one application, consider to use
367 | * bulkDetails.
368 | */
369 | public DetailsResponse details(String packageName) throws IOException {
370 | ResponseWrapper responseWrapper = executeGETRequest(DETAILS_URL,
371 | new String[][] { { "doc", packageName }, });
372 |
373 | return responseWrapper.getPayload().getDetailsResponse();
374 | }
375 |
376 | /** Equivalent of details but bulky one! */
377 | public BulkDetailsResponse bulkDetails(Listlist(categoryId, null, null, null). It
406 | * fetches sub-categories of given category!
407 | */
408 | public ListResponse list(String categoryId) throws IOException {
409 | return list(categoryId, null, null, null);
410 | }
411 |
412 | /**
413 | * Fetches applications within supplied category and sub-category. If
414 | * null is given for sub-category, it fetches sub-categories of
415 | * passed category.
416 | *
417 | * Default values for offset and numberOfResult are "0" and "20" respectively.
418 | * These values are determined by Google Play Store.
419 | */
420 | public ListResponse list(String categoryId, String subCategoryId,
421 | Integer offset, Integer numberOfResult) throws IOException {
422 | ResponseWrapper responseWrapper = executeGETRequest(
423 | LIST_URL,
424 | new String[][] {
425 | { "c", "3" },
426 | { "cat", categoryId },
427 | { "ctr", subCategoryId },
428 | { "o", (offset == null) ? null : String.valueOf(offset) },
429 | {
430 | "n",
431 | (numberOfResult == null) ? null : String
432 | .valueOf(numberOfResult) }, });
433 |
434 | return responseWrapper.getPayload().getListResponse();
435 | }
436 |
437 | public ListResponse nextPage(String url) throws IOException {
438 | ResponseWrapper responseWrapper = executeGETRequest(FDFE_URL + url, null);
439 | return responseWrapper.getPayload().getListResponse();
440 | }
441 |
442 | /**
443 | * Downloads given application package name, version and offer type. Version
444 | * code and offer type can be fetch by details interface.
445 | **/
446 | public DownloadData download(String packageName, int versionCode,
447 | int offerType) throws IOException {
448 |
449 | BuyResponse buyResponse = purchase(packageName, versionCode, offerType);
450 |
451 | return new DownloadData(this, buyResponse.getPurchaseStatusResponse()
452 | .getAppDeliveryData());
453 |
454 | }
455 |
456 | public DownloadData delivery(String packageName, int versionCode,
457 | int offerType) throws IOException {
458 | ResponseWrapper responseWrapper = executeGETRequest(DELIVERY_URL,
459 | new String[][] { { "ot", String.valueOf(offerType) },
460 | { "doc", packageName }, { "vc", String.valueOf(versionCode) }, });
461 |
462 | AndroidAppDeliveryData appDeliveryData = responseWrapper.getPayload()
463 | .getDeliveryResponse().getAppDeliveryData();
464 | return new DownloadData(this, appDeliveryData);
465 | }
466 |
467 | public DownloadData purchaseAndDeliver(String packageName, int versionCode,
468 | int offerType) throws IOException {
469 | BuyResponse buyResponse = purchase(packageName, versionCode, offerType);
470 | AndroidAppDeliveryData ada = buyResponse.getPurchaseStatusResponse()
471 | .getAppDeliveryData();
472 | if (ada.hasDownloadUrl() && ada.getDownloadAuthCookieCount() > 0) {
473 | // This is for backwards compatibility.
474 | return new DownloadData(this, ada);
475 | }
476 | return delivery(packageName, versionCode, offerType);
477 | }
478 |
479 | /**
480 | * Posts given check-in request content and returns
481 | * {@link AndroidCheckinResponse}.
482 | */
483 | private AndroidCheckinResponse postCheckin(byte[] request) throws IOException {
484 |
485 | HttpEntity httpEntity = executePost(CHECKIN_URL, new ByteArrayEntity(
486 | request), new String[][] {
487 | { "User-Agent", "Android-Checkin/2.0 (generic JRO03E); gzip" },
488 | { "Host", "android.clients.google.com" },
489 | { "Content-Type", "application/x-protobuffer" } });
490 | return AndroidCheckinResponse.parseFrom(httpEntity.getContent());
491 | }
492 |
493 | /**
494 | * This function is used for fetching download url and donwload cookie, rather
495 | * than actual purchasing.
496 | */
497 | private BuyResponse purchase(String packageName, int versionCode,
498 | int offerType) throws IOException {
499 |
500 | ResponseWrapper responseWrapper = executePOSTRequest(PURCHASE_URL,
501 | new String[][] { { "ot", String.valueOf(offerType) },
502 | { "doc", packageName }, { "vc", String.valueOf(versionCode) }, });
503 |
504 | return responseWrapper.getPayload().getBuyResponse();
505 | }
506 |
507 | /**
508 | * Fetches url content by executing GET request with provided cookie string.
509 | */
510 | public InputStream executeDownload(String url, String cookie)
511 | throws IOException {
512 |
513 | if (cookie!= null) {
514 | String[][] headerParams = new String[][] {
515 | { "Cookie", cookie },
516 | { "User-Agent",
517 | "AndroidDownloadManager/4.1.1 (Linux; U; Android 4.1.1; Nexus S Build/JRO03E)" }, };
518 |
519 | HttpEntity httpEntity = executeGet(url, null, headerParams);
520 | return httpEntity.getContent();
521 | }
522 | else {
523 | String[][] headerParams = new String[][] {
524 | { "User-Agent",
525 | "AndroidDownloadManager/4.1.1 (Linux; U; Android 4.1.1; Nexus S Build/JRO03E)" }, };
526 |
527 | HttpEntity httpEntity = executeGet(url, null, headerParams);
528 | return httpEntity.getContent();
529 | }
530 | }
531 |
532 | /**
533 | * Fetches the reviews of given package name by sorting passed choice.
534 | *
535 | * Default values for offset and numberOfResult are "0" and "20" respectively.
536 | * These values are determined by Google Play Store.
537 | */
538 | public ReviewResponse reviews(String packageName, REVIEW_SORT sort,
539 | Integer offset, Integer numberOfResult) throws IOException {
540 | ResponseWrapper responseWrapper = executeGETRequest(
541 | REVIEWS_URL,
542 | new String[][] {
543 | { "doc", packageName },
544 | { "sort", (sort == null) ? null : String.valueOf(sort.value) },
545 | { "o", (offset == null) ? null : String.valueOf(offset) },
546 | {
547 | "n",
548 | (numberOfResult == null) ? null : String
549 | .valueOf(numberOfResult) } });
550 |
551 | return responseWrapper.getPayload().getReviewResponse();
552 | }
553 |
554 | /**
555 | * Uploads device configuration to google server so that can be seen from web
556 | * as a registered device!!
557 | *
558 | * @see https://play.google.com/store/account
559 | */
560 | public UploadDeviceConfigResponse uploadDeviceConfig() throws Exception {
561 |
562 | UploadDeviceConfigRequest request = UploadDeviceConfigRequest.newBuilder()
563 | .setDeviceConfiguration(Utils.getDeviceConfigurationProto())
564 | .setManufacturer("Samsung").build();
565 | ResponseWrapper responseWrapper = executePOSTRequest(
566 | UPLOADDEVICECONFIG_URL, request.toByteArray(), "application/x-protobuf");
567 | return responseWrapper.getPayload().getUploadDeviceConfigResponse();
568 | }
569 |
570 | /**
571 | * Fetches the recommendations of given package name.
572 | *
573 | * Default values for offset and numberOfResult are "0" and "20" respectively.
574 | * These values are determined by Google Play Store.
575 | */
576 | public ListResponse recommendations(String packageName,
577 | RECOMMENDATION_TYPE type, Integer offset, Integer numberOfResult)
578 | throws IOException {
579 | ResponseWrapper responseWrapper = executeGETRequest(
580 | RECOMMENDATIONS_URL,
581 | new String[][] {
582 | { "c", "3" },
583 | { "doc", packageName },
584 | { "rt", (type == null) ? null : String.valueOf(type.value) },
585 | { "o", (offset == null) ? null : String.valueOf(offset) },
586 | {
587 | "n",
588 | (numberOfResult == null) ? null : String
589 | .valueOf(numberOfResult) } });
590 |
591 | return responseWrapper.getPayload().getListResponse();
592 | }
593 |
594 | /* =======================Helper Functions====================== */
595 |
596 | /**
597 | * Executes GET request and returns result as {@link ResponseWrapper}.
598 | * Standard header parameters will be used for request.
599 | *
600 | * @see getHeaderParameters
601 | * */
602 | private ResponseWrapper executeGETRequest(String path, String[][] datapost)
603 | throws IOException {
604 |
605 | HttpEntity httpEntity = executeGet(path, datapost,
606 | getHeaderParameters(this.getToken(), null));
607 | return GooglePlay.ResponseWrapper.parseFrom(httpEntity.getContent());
608 |
609 | }
610 |
611 | /**
612 | * Executes POST request and returns result as {@link ResponseWrapper}.
613 | * Standard header parameters will be used for request.
614 | *
615 | * @see getHeaderParameters
616 | * */
617 | private ResponseWrapper executePOSTRequest(String path, String[][] datapost)
618 | throws IOException {
619 |
620 | HttpEntity httpEntity = executePost(path, datapost,
621 | getHeaderParameters(this.getToken(), null));
622 | return GooglePlay.ResponseWrapper.parseFrom(httpEntity.getContent());
623 |
624 | }
625 |
626 | /**
627 | * Executes POST request and returns result as {@link ResponseWrapper}.
628 | * Content type can be specified for given byte array.
629 | */
630 | private ResponseWrapper executePOSTRequest(String url, byte[] datapost,
631 | String contentType) throws IOException {
632 |
633 | HttpEntity httpEntity = executePost(url, new ByteArrayEntity(datapost),
634 | getHeaderParameters(this.getToken(), contentType));
635 | return GooglePlay.ResponseWrapper.parseFrom(httpEntity.getContent());
636 |
637 | }
638 |
639 | /**
640 | * Executes POST request on given URL with POST parameters and header
641 | * parameters.
642 | */
643 | private HttpEntity executePost(String url, String[][] postParams,
644 | String[][] headerParams) throws IOException {
645 |
646 | List