) {
24 |
25 | val logManager = LogManager.getLogManager()
26 | JavaNiddler::class.java.getResourceAsStream("/logging.properties")
27 | .use { `is` -> logManager.readConfiguration(`is`) }
28 |
29 | val niddler = JavaNiddler.Builder()
30 | .setPort(0)
31 | .setCacheSize(1024L * 1024L)
32 | .setNiddlerInformation(Niddler.NiddlerServerInfo("Niddler-Example",
33 | "Example java niddler application", "download"))
34 | .build()
35 |
36 | niddler.start()
37 |
38 | if (args.isNotEmpty()) {
39 | println("Waiting for debugger")
40 | niddler.debugger().waitForConnection {
41 | println("Debugger connected")
42 | }
43 | }
44 |
45 | val okHttp = OkHttpClient.Builder()
46 | .addInterceptor(NiddlerOkHttpInterceptor(niddler, "Default Interceptor"))
47 | .build()
48 |
49 | val request = Request.Builder()
50 | .get()
51 | .url("https://jsonplaceholder.typicode.com/posts")
52 | .build()
53 | val request2 = request.newBuilder().build()
54 | val request3 = Request.Builder()
55 | .url("http://httpbin.org/post")
56 | .post(FormBody.Builder().add("token", "add10 92201").add("type", "custom").build())
57 | .build()
58 | val request4 = Request.Builder()
59 | .get()
60 | .url(HttpUrl.parse("http://httpbin.org/xml")!!.newBuilder().addQueryParameter("param", "=102:~19em/%;").build())
61 | .build()
62 | val request5 = Request.Builder()
63 | .url("http://httpbin.org/post")
64 | .post(RequestBody.create(MediaType.parse("application/json"), "{\"nullValue\":null}"))
65 | .build()
66 | val request6 = Request.Builder()
67 | .url("http://httpbin.org/post")
68 | .post(RequestBody.create(MediaType.parse("application/octet-stream"), binaryBlob()))
69 | .build()
70 |
71 | val response1 = okHttp.newCall(request).execute()
72 | println("Request 1 executed (" + response1.code() + ")")
73 | val response2 = okHttp.newCall(request2).execute()
74 | println("Request 2 executed (" + response2.code() + ")")
75 | val response3 = okHttp.newCall(request3).execute()
76 | println("Request 3 executed (" + response3.code() + ")")
77 | val response4 = okHttp.newCall(request4).execute()
78 | println("Request 4 executed (" + response4.code() + ")")
79 | val response5 = okHttp.newCall(request5).execute()
80 | println("Request 5 executed (" + response5.code() + ")")
81 | val response6 = okHttp.newCall(request6).execute()
82 | println("Request 6 executed (" + response6.code() + ")")
83 |
84 | println("Press return to stop")
85 | System.`in`.read()
86 |
87 | println("Stopping niddler")
88 | niddler.close()
89 | }
90 |
91 | private fun binaryBlob(): ByteArray {
92 | val out = ByteArrayOutputStream()
93 | val zipOut = ZipOutputStream(out)
94 | zipOut.putNextEntry(ZipEntry("Example file 1"))
95 |
96 | val binaryBytes = ByteArray(100)
97 | Random().nextBytes(binaryBytes)
98 |
99 | zipOut.write(binaryBytes)
100 |
101 | zipOut.closeEntry()
102 | zipOut.close()
103 |
104 | return out.toByteArray()
105 | }
--------------------------------------------------------------------------------
/niddler-base/src/main/java/com/chimerapps/niddler/util/ConditionVariable.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2006 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy 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,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.chimerapps.niddler.util;
17 |
18 | import androidx.annotation.RestrictTo;
19 |
20 | /**
21 | * Class that implements the condition variable locking paradigm.
22 | *
23 | *
24 | * This differs from the built-in java.lang.Object wait() and notify()
25 | * in that this class contains the condition to wait on itself. That means
26 | * open(), close() and block() are sticky. If open() is called before block(),
27 | * block() will not block, and instead return immediately.
28 | *
29 | *
30 | * This class uses itself as the object to wait on, so if you wait()
31 | * or notify() on a ConditionVariable, the results are undefined.
32 | */
33 | @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
34 | public class ConditionVariable {
35 | private volatile boolean mCondition;
36 |
37 | /**
38 | * Create the ConditionVariable in the default closed state.
39 | */
40 | public ConditionVariable() {
41 | mCondition = false;
42 | }
43 |
44 | /**
45 | * Create the ConditionVariable with the given state.
46 | *
47 | *
48 | * Pass true for opened and false for closed.
49 | */
50 | public ConditionVariable(boolean state) {
51 | mCondition = state;
52 | }
53 |
54 | /**
55 | * Open the condition, and release all threads that are blocked.
56 | *
57 | *
58 | * Any threads that later approach block() will not block unless close()
59 | * is called.
60 | */
61 | public void open() {
62 | synchronized (this) {
63 | boolean old = mCondition;
64 | mCondition = true;
65 | if (!old) {
66 | this.notifyAll();
67 | }
68 | }
69 | }
70 |
71 | /**
72 | * Reset the condition to the closed state.
73 | *
74 | *
75 | * Any threads that call block() will block until someone calls open.
76 | */
77 | public void close() {
78 | synchronized (this) {
79 | mCondition = false;
80 | }
81 | }
82 |
83 | /**
84 | * Block the current thread until the condition is opened.
85 | *
86 | *
87 | * If the condition is already opened, return immediately.
88 | */
89 | public void block() {
90 | synchronized (this) {
91 | while (!mCondition) {
92 | try {
93 | this.wait();
94 | } catch (InterruptedException e) {
95 | }
96 | }
97 | }
98 | }
99 |
100 | /**
101 | * Block the current thread until the condition is opened or until
102 | * timeout milliseconds have passed.
103 | *
104 | *
105 | * If the condition is already opened, return immediately.
106 | *
107 | * @param timeout the maximum time to wait in milliseconds.
108 | * @return true if the condition was opened, false if the call returns
109 | * because of the timeout.
110 | */
111 | public boolean block(long timeout) {
112 | // Object.wait(0) means wait forever, to mimic this, we just
113 | // call the other block() method in that case. It simplifies
114 | // this code for the common case.
115 | if (timeout != 0) {
116 | synchronized (this) {
117 | long now = System.currentTimeMillis();
118 | long end = now + timeout;
119 | while (!mCondition && now < end) {
120 | try {
121 | this.wait(end - now);
122 | } catch (InterruptedException e) {
123 | }
124 | now = System.currentTimeMillis();
125 | }
126 | return mCondition;
127 | }
128 | } else {
129 | this.block();
130 | return true;
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/intellij,jetbrains,osx,java,gradle,git,infer,android
3 |
4 | ### Intellij ###
5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
7 |
8 | # User-specific stuff:
9 | .idea/workspace.xml
10 | .idea/tasks.xml
11 |
12 | signing.properties
13 |
14 | # Sensitive or high-churn files:
15 | .idea/dataSources/
16 | .idea/dataSources.ids
17 | .idea/dataSources.xml
18 | .idea/dataSources.local.xml
19 | .idea/sqlDataSources.xml
20 | .idea/dynamic.xml
21 | .idea/uiDesigner.xml
22 |
23 | # Gradle:
24 | .idea
25 | *.iml
26 |
27 | # Mongo Explorer plugin:
28 | .idea/mongoSettings.xml
29 |
30 | ## File-based project format:
31 | *.iws
32 |
33 | ## Plugin-specific files:
34 |
35 | # IntelliJ
36 | /out/
37 |
38 | # mpeltonen/sbt-idea plugin
39 | .idea_modules/
40 |
41 | # JIRA plugin
42 | atlassian-ide-plugin.xml
43 |
44 | # Crashlytics plugin (for Android Studio and IntelliJ)
45 | com_crashlytics_export_strings.xml
46 | crashlytics.properties
47 | crashlytics-build.properties
48 | fabric.properties
49 |
50 | ### Intellij Patch ###
51 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
52 |
53 | # *.iml
54 | # modules.xml
55 | # .idea/misc.xml
56 | # *.ipr
57 |
58 |
59 | ### JetBrains ###
60 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
61 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
62 |
63 | # User-specific stuff:
64 |
65 | # Sensitive or high-churn files:
66 |
67 | # Gradle:
68 |
69 | # Mongo Explorer plugin:
70 |
71 | ## File-based project format:
72 |
73 | ## Plugin-specific files:
74 |
75 | # IntelliJ
76 |
77 | # mpeltonen/sbt-idea plugin
78 |
79 | # JIRA plugin
80 |
81 | # Crashlytics plugin (for Android Studio and IntelliJ)
82 |
83 | ### JetBrains Patch ###
84 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
85 |
86 | # *.iml
87 | # modules.xml
88 | # .idea/misc.xml
89 | # *.ipr
90 |
91 |
92 | ### OSX ###
93 | *.DS_Store
94 | .AppleDouble
95 | .LSOverride
96 |
97 | # Icon must end with two \r
98 | Icon
99 | # Thumbnails
100 | ._*
101 | # Files that might appear in the root of a volume
102 | .DocumentRevisions-V100
103 | .fseventsd
104 | .Spotlight-V100
105 | .TemporaryItems
106 | .Trashes
107 | .VolumeIcon.icns
108 | .com.apple.timemachine.donotpresent
109 | # Directories potentially created on remote AFP share
110 | .AppleDB
111 | .AppleDesktop
112 | Network Trash Folder
113 | Temporary Items
114 | .apdisk
115 |
116 |
117 | ### Git ###
118 | *.orig
119 |
120 |
121 | ### infer ###
122 | # infer- http://fbinfer.com/
123 | infer-out
124 |
125 |
126 | ### Android ###
127 | # Built application files
128 | *.apk
129 | *.ap_
130 |
131 | # Files for the ART/Dalvik VM
132 | *.dex
133 |
134 | # Java class files
135 | *.class
136 |
137 | # Generated files
138 | bin/
139 | gen/
140 | out/
141 |
142 | # Gradle files
143 | .gradle/
144 | build/
145 |
146 | # Local configuration file (sdk path, etc)
147 | local.properties
148 |
149 | # Proguard folder generated by Eclipse
150 | proguard/
151 |
152 | # Log Files
153 | *.log
154 |
155 | # Android Studio Navigation editor temp files
156 | .navigation/
157 |
158 | # Android Studio captures folder
159 | captures/
160 |
161 | # Intellij
162 | #*.iml
163 |
164 | # Keystore files
165 | *.jks
166 |
167 | # External native build folder generated in Android Studio 2.2 and later
168 | .externalNativeBuild
169 |
170 | ### Android Patch ###
171 | gen-external-apklibs
172 |
173 |
174 | ### Java ###
175 |
176 | # Mobile Tools for Java (J2ME)
177 | .mtj.tmp/
178 |
179 | # Package Files #
180 | *.jar
181 | *.war
182 | *.ear
183 |
184 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
185 | hs_err_pid*
186 |
187 |
188 | ### Gradle ###
189 | .gradle
190 | /build/
191 |
192 | # Ignore Gradle GUI config
193 | gradle-app.setting
194 |
195 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
196 | !gradle-wrapper.jar
197 |
198 | # Cache of project
199 | .gradletasknamecache
200 |
201 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
202 | # gradle/wrapper/gradle-wrapper.properties
203 |
--------------------------------------------------------------------------------
/niddler-base/src/main/java/com/chimerapps/niddler/core/NiddlerImpl.java:
--------------------------------------------------------------------------------
1 | package com.chimerapps.niddler.core;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import com.chimerapps.niddler.core.debug.NiddlerDebugger;
6 | import com.chimerapps.niddler.util.LogUtil;
7 |
8 | import org.java_websocket.WebSocket;
9 |
10 | import java.io.IOException;
11 | import java.net.UnknownHostException;
12 | import java.util.HashMap;
13 | import java.util.List;
14 | import java.util.Map;
15 |
16 | /**
17 | * @author Nicola Verbeeck
18 | * @version 1
19 | */
20 | class NiddlerImpl implements NiddlerServer.WebSocketListener {
21 |
22 | private static final String LOG_TAG = Niddler.class.getSimpleName();
23 |
24 | private NiddlerServer mServer;
25 | private Niddler.PlatformNiddler mPlatform;
26 |
27 | private final MessagesCache mMessageCache;
28 | private final Niddler.NiddlerServerInfo mNiddlerServerInfo;
29 |
30 | private boolean mIsStarted = false;
31 | private boolean mIsClosed = false;
32 | @NonNull
33 | private final Map mStaticBlacklistMessage;
34 |
35 |
36 | NiddlerImpl(final String password,
37 | final int port,
38 | final long cacheSize,
39 | final Niddler.NiddlerServerInfo niddlerServerInfo,
40 | final StaticBlacklistDispatchListener blacklistListener,
41 | final int pid) {
42 | try {
43 | mServer = new NiddlerServer(password, port, niddlerServerInfo.name, niddlerServerInfo.icon, this, blacklistListener, pid);
44 | } catch (final UnknownHostException ex) {
45 | LogUtil.niddlerLogError(LOG_TAG, "Failed to start server: " + ex.getLocalizedMessage());
46 | }
47 | mMessageCache = new MessagesCache(cacheSize);
48 | mNiddlerServerInfo = niddlerServerInfo;
49 | mStaticBlacklistMessage = new HashMap<>();
50 | }
51 |
52 | @Override
53 | public void onConnectionOpened(final WebSocket conn) {
54 | if (mNiddlerServerInfo != null) {
55 | conn.send(MessageBuilder.buildMessage(mNiddlerServerInfo));
56 | }
57 | for (final String message : mMessageCache.get()) {
58 | conn.send(message);
59 | }
60 | if (!mStaticBlacklistMessage.isEmpty()) {
61 | for (final Map.Entry entry : mStaticBlacklistMessage.entrySet()) {
62 | conn.send(entry.getValue());
63 | }
64 | }
65 | }
66 |
67 | void start() {
68 | if ((mServer != null) && !mIsStarted) {
69 | mServer.start();
70 | mIsStarted = true;
71 | LogUtil.niddlerLogDebug(LOG_TAG, "Started niddler server on " + mServer.getAddress());
72 | }
73 | }
74 |
75 | void setPlatform(final Niddler.PlatformNiddler platform) {
76 | mPlatform = platform;
77 | }
78 |
79 | void close() throws IOException {
80 | final Niddler.PlatformNiddler platform = mPlatform;
81 | if (platform != null) {
82 | platform.closePlatform();
83 | }
84 |
85 | if (mServer != null) {
86 | try {
87 | mServer.stop();
88 | } catch (final InterruptedException e) {
89 | throw new IOException(e);
90 | } finally {
91 | mIsClosed = true;
92 | mMessageCache.clear();
93 | }
94 | }
95 | }
96 |
97 | NiddlerDebugger debugger() {
98 | return mServer.debugger();
99 | }
100 |
101 | boolean isStarted() {
102 | return mIsStarted;
103 | }
104 |
105 | boolean isClosed() {
106 | return mIsClosed;
107 | }
108 |
109 | void send(final String message) {
110 | if (mServer != null) {
111 | mMessageCache.put(message);
112 | mServer.sendToAll(message);
113 | }
114 | }
115 |
116 | void onStaticBlacklistChanged(@NonNull final String id, @NonNull final String name,
117 | @NonNull final List blacklist) {
118 | final String message = MessageBuilder.buildMessage(id, name, blacklist);
119 | mStaticBlacklistMessage.put(id, message);
120 | if (isStarted() && !isClosed() && !mStaticBlacklistMessage.isEmpty()) {
121 | mServer.sendToAll(message);
122 | }
123 |
124 | }
125 |
126 | int getPort() {
127 | return mServer.getPort();
128 | }
129 |
130 | interface StaticBlacklistDispatchListener {
131 |
132 | /**
133 | * Called when the static blacklist should be updated to reflect the new enabled status
134 | *
135 | * @param pattern The pattern to enable/disable
136 | * @param enabled Flag indicating if the static blacklist item is enabled or disabled
137 | * @param id The id of the blacklist handler to update
138 | */
139 | void setBlacklistItemEnabled(@NonNull final String id, @NonNull final String pattern, final boolean enabled);
140 |
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/niddler-base/src/main/java/com/chimerapps/niddler/interceptor/okhttp/NiddlerOkHttpRequest.java:
--------------------------------------------------------------------------------
1 | package com.chimerapps.niddler.interceptor.okhttp;
2 |
3 | import com.chimerapps.niddler.core.NiddlerRequest;
4 |
5 | import java.io.IOException;
6 | import java.io.OutputStream;
7 | import java.util.Collections;
8 | import java.util.List;
9 | import java.util.Map;
10 | import java.util.TreeMap;
11 | import java.util.UUID;
12 |
13 | import androidx.annotation.NonNull;
14 | import androidx.annotation.Nullable;
15 | import okhttp3.MediaType;
16 | import okhttp3.Protocol;
17 | import okhttp3.Request;
18 | import okhttp3.RequestBody;
19 | import okio.BufferedSink;
20 | import okio.Okio;
21 |
22 | /**
23 | * @author Maarten Van Giel
24 | * @author Nicola Verbeeck
25 | */
26 | final class NiddlerOkHttpRequest implements NiddlerRequest {
27 |
28 | private final Request mRequest;
29 | private final String mRequestId;
30 | private final String mMessageId;
31 | private final long mTimestamp;
32 | @Nullable
33 | private final Map mExtraHeaders;
34 | @Nullable
35 | private final StackTraceElement[] mStackTraceElements;
36 | @Nullable
37 | private final NiddlerOkHttpInterceptor.NiddlerRequestContext mRequestContext;
38 | private final Map mMetadata;
39 |
40 | NiddlerOkHttpRequest(final Request request, final String requestId, @Nullable final Map extraHeaders,
41 | @Nullable final StackTraceElement[] stackTraceElements,
42 | @Nullable final NiddlerOkHttpInterceptor.NiddlerRequestContext requestContext,
43 | @Nullable final Map metadata) {
44 | mRequest = request;
45 | mRequestId = requestId;
46 | mMessageId = UUID.randomUUID().toString();
47 | mTimestamp = System.currentTimeMillis();
48 | mExtraHeaders = extraHeaders;
49 | mStackTraceElements = stackTraceElements;
50 | mRequestContext = requestContext;
51 | mMetadata = new TreeMap<>();
52 |
53 | if (metadata != null) {
54 | mMetadata.putAll(metadata);
55 | }
56 | }
57 |
58 | public void addMetadata(@NonNull final String key, @NonNull final String value) {
59 | mMetadata.put(key, value);
60 | }
61 |
62 | @NonNull
63 | @Override
64 | public String getMessageId() {
65 | return mMessageId;
66 | }
67 |
68 | @NonNull
69 | @Override
70 | public String getRequestId() {
71 | return mRequestId;
72 | }
73 |
74 | @Override
75 | public long getTimestamp() {
76 | return mTimestamp;
77 | }
78 |
79 | @NonNull
80 | @Override
81 | public String getUrl() {
82 | return mRequest.url().toString();
83 | }
84 |
85 | @NonNull
86 | @Override
87 | public Map getMetadata() {
88 | return mMetadata;
89 | }
90 |
91 | @NonNull
92 | @Override
93 | public Map> getHeaders() {
94 | final Map> headers = mRequest.headers().toMultimap();
95 | if (!headers.containsKey("Content-Type") && (mRequest.body() != null)) {
96 | final MediaType contentType = mRequest.body().contentType();
97 | if (contentType != null) {
98 | headers.put("Content-Type", Collections.singletonList(contentType.toString()));
99 | }
100 | }
101 | if (mExtraHeaders != null) {
102 | for (final Map.Entry keyValueEntry : mExtraHeaders.entrySet()) {
103 | if (!headers.containsKey(keyValueEntry.getKey())) {
104 | headers.put(keyValueEntry.getKey(), Collections.singletonList(keyValueEntry.getValue()));
105 | }
106 | }
107 | }
108 | return headers;
109 | }
110 |
111 | @Nullable
112 | @Override
113 | public StackTraceElement[] getRequestStackTrace() {
114 | return mStackTraceElements;
115 | }
116 |
117 | @NonNull
118 | @Override
119 | public String getMethod() {
120 | return mRequest.method();
121 | }
122 |
123 | @Nullable
124 | @Override
125 | public List getRequestContext() {
126 | if (mRequestContext == null) {
127 | return null;
128 | }
129 | return mRequestContext.getContextInformation();
130 | }
131 |
132 | @Override
133 | public void writeBody(@NonNull final OutputStream stream) {
134 | try {
135 | final BufferedSink buffer = Okio.buffer(Okio.sink(stream));
136 |
137 | final RequestBody body = mRequest.body();
138 | if (body != null) {
139 | body.writeTo(buffer);
140 | buffer.flush();
141 | }
142 | } catch (final IOException e) {
143 | e.printStackTrace();
144 | }
145 | }
146 |
147 | @SuppressWarnings("deprecation")
148 | static String httpVersion(final Protocol protocol) {
149 | switch (protocol) {
150 | case HTTP_1_0:
151 | return "http/1.0";
152 | case HTTP_1_1:
153 | return "http/1.1";
154 | case SPDY_3:
155 | return "spdy/3.1";
156 | case HTTP_2:
157 | return "http/2.0";
158 | }
159 | return "";
160 | }
161 |
162 | }
163 |
--------------------------------------------------------------------------------
/niddler/src/main/java/com/chimerapps/niddler/core/NiddlerServiceLifeCycleWatcher.java:
--------------------------------------------------------------------------------
1 | package com.chimerapps.niddler.core;
2 |
3 | import android.app.Activity;
4 | import android.app.AlertDialog;
5 | import android.app.Application;
6 | import android.app.Dialog;
7 | import android.app.DialogFragment;
8 | import android.app.Fragment;
9 | import android.content.Context;
10 | import android.content.DialogInterface;
11 | import android.content.Intent;
12 | import android.content.ServiceConnection;
13 | import android.os.Bundle;
14 | import androidx.annotation.NonNull;
15 |
16 | import com.chimerapps.niddler.service.NiddlerService;
17 |
18 | import java.lang.ref.WeakReference;
19 |
20 | import static com.chimerapps.niddler.core.Niddler.INTENT_EXTRA_WAIT_FOR_DEBUGGER;
21 |
22 | /**
23 | * @author Nicola Verbeeck
24 | * Date 22/11/16.
25 | */
26 | class NiddlerServiceLifeCycleWatcher implements Application.ActivityLifecycleCallbacks {
27 |
28 | private static final String WAIT_FOR_DEBUGGER_FRAGMENT_ID = "Niddler-Wait-For-Debugger";
29 |
30 | @NonNull
31 | private final ServiceConnection mServiceConnection;
32 | @NonNull
33 | private final Niddler mNiddler;
34 |
35 | NiddlerServiceLifeCycleWatcher(@NonNull final ServiceConnection connection, @NonNull final Niddler niddler) {
36 | mServiceConnection = connection;
37 | mNiddler = niddler;
38 | }
39 |
40 | @Override
41 | public void onActivityCreated(final Activity activity, final Bundle bundle) {
42 | final int waitForDebugger = activity.getIntent().getIntExtra(INTENT_EXTRA_WAIT_FOR_DEBUGGER, 0);
43 | if (waitForDebugger == 1) {
44 |
45 | final WeakReference weakActivity = new WeakReference<>(activity);
46 |
47 | if (mNiddler.debugger().waitForConnection(new Runnable() {
48 | @Override
49 | public void run() {
50 | final Activity startingActivity = weakActivity.get();
51 | if (startingActivity != null) {
52 | startingActivity.runOnUiThread(new Runnable() {
53 | @Override
54 | public void run() {
55 | final Fragment frag = startingActivity.getFragmentManager().findFragmentByTag(WAIT_FOR_DEBUGGER_FRAGMENT_ID);
56 | if (frag != null) {
57 | startingActivity.getFragmentManager().beginTransaction().remove(frag).commitAllowingStateLoss();
58 | }
59 | }
60 | });
61 | }
62 | }
63 | })) {
64 | final AlertDialogFragment fragment = new AlertDialogFragment();
65 | fragment.setRetainInstance(true);
66 | fragment.mNiddler = mNiddler;
67 | activity.getFragmentManager().beginTransaction().add(fragment, WAIT_FOR_DEBUGGER_FRAGMENT_ID).commitAllowingStateLoss();
68 | }
69 | }
70 | }
71 |
72 | @Override
73 | public void onActivityStarted(final Activity activity) {
74 | final Intent serviceIntent = new Intent(activity, NiddlerService.class);
75 | try {
76 | activity.startService(serviceIntent);
77 | activity.bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE);
78 | } catch(final Throwable ignore) {
79 | }
80 | }
81 |
82 | @Override
83 | public void onActivityResumed(final Activity activity) {
84 | //Not handled
85 | }
86 |
87 | @Override
88 | public void onActivityPaused(final Activity activity) {
89 | //Not handled
90 | }
91 |
92 | @Override
93 | public void onActivityStopped(final Activity activity) {
94 | try {
95 | activity.unbindService(mServiceConnection);
96 | } catch (final Throwable ignored) {
97 | //Ignore
98 | }
99 | }
100 |
101 | @Override
102 | public void onActivitySaveInstanceState(final Activity activity, final Bundle bundle) {
103 | //Not handled
104 | }
105 |
106 | @Override
107 | public void onActivityDestroyed(final Activity activity) {
108 | //Not handled
109 | }
110 |
111 | public static class AlertDialogFragment extends DialogFragment {
112 |
113 | public Niddler mNiddler;
114 |
115 | @Override
116 | public Dialog onCreateDialog(final Bundle savedInstanceState) {
117 | return new AlertDialog.Builder(getActivity())
118 | .setMessage("Waiting for niddler debugger to attach")
119 | .setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
120 | @Override
121 | public void onClick(final DialogInterface dialog, final int which) {
122 | dialog.cancel();
123 | }
124 | })
125 | .create();
126 | }
127 |
128 | @Override
129 | public void onCancel(final DialogInterface dialog) {
130 | super.onCancel(dialog);
131 | if (mNiddler != null) {
132 | mNiddler.debugger().cancelWaitForConnection();
133 | }
134 | }
135 |
136 | @Override
137 | public void onResume() {
138 | super.onResume();
139 | if (mNiddler != null) {
140 | if (!mNiddler.debugger().isWaitingForConnection()) {
141 | dismissAllowingStateLoss();
142 | }
143 | }
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/niddler-base/src/main/java/com/chimerapps/niddler/interceptor/okhttp/NiddlerOkHttpResponse.java:
--------------------------------------------------------------------------------
1 | package com.chimerapps.niddler.interceptor.okhttp;
2 |
3 | import com.chimerapps.niddler.core.NiddlerRequest;
4 | import com.chimerapps.niddler.core.NiddlerResponse;
5 |
6 | import java.io.IOException;
7 | import java.io.OutputStream;
8 | import java.util.Collections;
9 | import java.util.List;
10 | import java.util.Map;
11 | import java.util.TreeMap;
12 | import java.util.UUID;
13 |
14 | import androidx.annotation.NonNull;
15 | import androidx.annotation.Nullable;
16 | import okhttp3.Response;
17 | import okhttp3.ResponseBody;
18 | import okio.Buffer;
19 | import okio.BufferedSource;
20 |
21 | /**
22 | * @author Maarten Van Giel
23 | * @author Nicola Verbeeck
24 | */
25 | final class NiddlerOkHttpResponse implements NiddlerResponse {
26 |
27 | private final Response mResponse;
28 | private final String mRequestId;
29 | private final String mMessageId;
30 | private final long mTimestamp;
31 | @Nullable
32 | private final NiddlerRequest mActualNetworkRequest;
33 | @Nullable
34 | private final NiddlerResponse mActualNetworkReply;
35 | private final int mWriteTime;
36 | private final int mReadTime;
37 | private final int mWaitTime;
38 | @Nullable
39 | private final Map mExtraHeaders;
40 | private final Map mMetadata;
41 |
42 | NiddlerOkHttpResponse(final Response response,
43 | final String requestId,
44 | @Nullable final NiddlerRequest actualNetworkRequest,
45 | @Nullable final NiddlerResponse actualNetworkReply,
46 | final int writeTime,
47 | final int readTime,
48 | final int waitTime,
49 | @Nullable final Map extraHeaders,
50 | @Nullable final Map metadata) {
51 | mResponse = response;
52 | mRequestId = requestId;
53 | mActualNetworkRequest = actualNetworkRequest;
54 | mActualNetworkReply = actualNetworkReply;
55 | mWriteTime = writeTime;
56 | mReadTime = readTime;
57 | mWaitTime = waitTime;
58 | mMessageId = UUID.randomUUID().toString();
59 | mTimestamp = System.currentTimeMillis();
60 | mExtraHeaders = extraHeaders;
61 | mMetadata = new TreeMap<>();
62 | if (metadata != null) {
63 | mMetadata.putAll(metadata);
64 | }
65 | }
66 |
67 | public void addMetadata(@NonNull final String key, @NonNull final String value) {
68 | mMetadata.put(key, value);
69 | }
70 |
71 | @NonNull
72 | @Override
73 | public String getMessageId() {
74 | return mMessageId;
75 | }
76 |
77 | @NonNull
78 | @Override
79 | public String getRequestId() {
80 | return mRequestId;
81 | }
82 |
83 | @Override
84 | public long getTimestamp() {
85 | return mTimestamp;
86 | }
87 |
88 | @NonNull
89 | @Override
90 | public Map> getHeaders() {
91 | final Map> finalHeaders = mResponse.headers().toMultimap();
92 | if (mExtraHeaders != null) {
93 | for (final Map.Entry keyValueEntry : mExtraHeaders.entrySet()) {
94 | if (!finalHeaders.containsKey(keyValueEntry.getKey())) {
95 | finalHeaders.put(keyValueEntry.getKey(), Collections.singletonList(keyValueEntry.getValue()));
96 | }
97 | }
98 | }
99 | return finalHeaders;
100 | }
101 |
102 | @NonNull
103 | @Override
104 | public Map getMetadata() {
105 | return mMetadata;
106 | }
107 |
108 | @NonNull
109 | @Override
110 | public Integer getStatusCode() {
111 | return mResponse.code();
112 | }
113 |
114 | @Nullable
115 | @Override
116 | public NiddlerRequest actualNetworkRequest() {
117 | return mActualNetworkRequest;
118 | }
119 |
120 | @Nullable
121 | @Override
122 | public NiddlerResponse actualNetworkReply() {
123 | return mActualNetworkReply;
124 | }
125 |
126 | @NonNull
127 | @Override
128 | public String getStatusLine() {
129 | return mResponse.message();
130 | }
131 |
132 | @NonNull
133 | @Override
134 | public String getHttpVersion() {
135 | return NiddlerOkHttpRequest.httpVersion(mResponse.protocol());
136 | }
137 |
138 | @Override
139 | public int getWriteTime() {
140 | return mWriteTime;
141 | }
142 |
143 | @Override
144 | public int getReadTime() {
145 | return mReadTime;
146 | }
147 |
148 | @Override
149 | public int getWaitTime() {
150 | return mWaitTime;
151 | }
152 |
153 | @Override
154 | public void writeBody(@NonNull final OutputStream stream) {
155 | final ResponseBody body = mResponse.body();
156 | try {
157 | if (body != null) {
158 | final BufferedSource source = body.source();
159 | source.request(Long.MAX_VALUE); // Buffer entire body
160 |
161 | final Buffer buffer = source.buffer();
162 | stream.write(buffer.clone().readByteArray());
163 | stream.flush();
164 | }
165 | } catch (final IllegalStateException ignored) {
166 | // Can't read this body anymore from okhttp 5.0.0-alpha7 and up
167 | } catch (final IOException e) {
168 | e.printStackTrace();
169 | }
170 | }
171 |
172 | @Nullable
173 | @Override
174 | public StackTraceElement[] getErrorStackTrace() {
175 | return null;
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/scripts/publish-mavencentral-android.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'maven-publish'
2 | apply plugin: 'signing'
3 |
4 | group = artifactGroup
5 | version = artifactVersion
6 |
7 | ext["signing.keyId"] = ''
8 | ext["signing.password"] = ''
9 | ext["signing.secretKeyRingFile"] = ''
10 | ext["ossrhUsername"] = ''
11 | ext["ossrhPassword"] = ''
12 | ext["sonatypeStagingProfileId"] = ''
13 |
14 | def secretPropsFile = project.rootProject.file('signing.properties')
15 | if (secretPropsFile.exists()) {
16 | Properties p = new Properties()
17 | new FileInputStream(secretPropsFile).withCloseable { is ->
18 | p.load(is)
19 | }
20 | p.each { name, value ->
21 | ext[name] = value
22 | }
23 | } else {
24 | ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
25 | ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
26 | ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')
27 | ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
28 | ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
29 | ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
30 | }
31 |
32 | task("generateJavadoc", type: Javadoc) {
33 | source = android.sourceSets.main.java.getSrcDirs()
34 | options {
35 | links "http://docs.oracle.com/javase/7/docs/api/"
36 | linksOffline "https://developer.android.com/reference",
37 | "${android.sdkDirectory}/docs/reference"
38 | encoding = "UTF-8"
39 | }
40 | exclude "**/BuildConfig.java"
41 | exclude "**/R.java"
42 | doFirst {
43 | classpath =
44 | files(
45 | android.sourceSets.main.java.srcDirs,
46 | project.android.getBootClasspath(),
47 | project.file("../niddler-lib-base/src/main/java"))
48 | }
49 | failOnError false
50 | }
51 |
52 | task("androidJavadocsJar", type: Jar, dependsOn: generateJavadoc) {
53 | archiveClassifier.set('javadoc')
54 | from generateJavadoc.destinationDir
55 | }
56 |
57 | task sourceJar(type: Jar) {
58 | archiveClassifier.set('sources')
59 | from android.sourceSets.main.java.getSrcDirs()
60 | }
61 |
62 | artifacts {
63 | archives sourceJar
64 | archives androidJavadocsJar
65 | }
66 |
67 | publishing {
68 | publications {
69 | release(MavenPublication) {
70 | groupId "$artifactGroup"
71 | artifactId "$artifactName"
72 | version "$artifactVersion"
73 |
74 | artifact sourceJar
75 | artifact androidJavadocsJar
76 | artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
77 |
78 | pom {
79 | name = artifactName
80 | description = artifactDescription
81 | url = 'https://github.com/Chimerapps/niddler'
82 |
83 | licenses {
84 | license {
85 | name = "The Apache Software License, Version 2.0"
86 | url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
87 | }
88 | }
89 | developers {
90 | developer {
91 | name = "Nicola Verbeeck"
92 | email = "nicola@chimerapps.com"
93 | organization = "Chimerapps"
94 | }
95 | developer {
96 | name = "Maarten Van Giel"
97 | email = "contact@maarten.vg"
98 | }
99 | }
100 |
101 | scm {
102 | connection = 'scm:git:github.com/Chimerapps/niddler.git'
103 | developerConnection = 'scm:git:ssh://github.com/Chimerapps/niddler.git'
104 | url = 'https://github.com/Chimerapps/niddler'
105 | }
106 |
107 | withXml {
108 | def dependenciesNode = asNode().appendNode('dependencies')
109 |
110 | project.configurations.implementation.allDependencies.each {
111 | def dependencyNode = dependenciesNode.appendNode('dependency')
112 | dependencyNode.appendNode('groupId', it.group)
113 | dependencyNode.appendNode('artifactId', it.name)
114 | dependencyNode.appendNode('version', it.version)
115 | }
116 | }
117 | }
118 | }
119 | }
120 |
121 | repositories {
122 | maven {
123 | // This is an arbitrary name, you may also use "mavencentral" or
124 | // any other name that's descriptive for you
125 | name = "sonatype"
126 | url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
127 | credentials {
128 | username ossrhUsername
129 | password ossrhPassword
130 | }
131 | }
132 | }
133 | }
134 |
135 | signing {
136 | sign publishing.publications
137 | }
--------------------------------------------------------------------------------
/niddler/src/main/java/com/chimerapps/niddler/core/AndroidNiddler.java:
--------------------------------------------------------------------------------
1 | package com.chimerapps.niddler.core;
2 |
3 | import android.app.Application;
4 | import android.content.ComponentName;
5 | import android.content.ServiceConnection;
6 | import android.content.pm.ApplicationInfo;
7 | import android.content.pm.PackageManager;
8 | import android.os.Build;
9 | import android.os.IBinder;
10 | import android.os.Process;
11 |
12 | import androidx.annotation.Nullable;
13 |
14 | import com.chimerapps.niddler.service.NiddlerService;
15 | import com.chimerapps.niddler.util.AndroidLogUtil;
16 | import com.chimerapps.niddler.util.LogUtil;
17 |
18 | import java.lang.ref.WeakReference;
19 |
20 | /**
21 | * @author Nicola Verbeeck
22 | * @version 1
23 | */
24 | public final class AndroidNiddler extends Niddler implements Niddler.PlatformNiddler {
25 |
26 | private long mAutoStopAfter = -1;
27 | private NiddlerServiceLifeCycleWatcher mLifeCycleWatcher = null;
28 |
29 | private WeakReference mNiddlerService;
30 |
31 | private AndroidNiddler(final String password, final int port, final long cacheSize,
32 | final NiddlerServerInfo niddlerServerInfo, final int maxStackTraceSize) {
33 | super(password, port, cacheSize, niddlerServerInfo, maxStackTraceSize, Process.myPid());
34 | mNiddlerImpl.setPlatform(this);
35 | }
36 |
37 | /**
38 | * Attaches the Niddler instance to the application's activity lifecycle callbacks, thus starting and stopping a NiddlerService
39 | * when activities start and stop. This will show a notification with which you can stop Niddler at any time.
40 | *
41 | * @param application the application to attach the Niddler instance to
42 | */
43 | @SuppressWarnings("WeakerAccess")
44 | public void attachToApplication(final Application application) {
45 | attachToApplication(application, -1L);
46 | }
47 |
48 | /**
49 | * Attaches the Niddler instance to the application's activity lifecycle callbacks, thus starting and stopping a NiddlerService
50 | * when activities start and stop. This will show a notification with which you can stop Niddler at any time.
51 | *
52 | * @param application the application to attach the Niddler instance to
53 | * @param autoStopAfter Automatically stop the niddler background service after x milliseconds. Use -1 to keep the service running and use 0 to stop the service immediately
54 | */
55 | @SuppressWarnings("WeakerAccess")
56 | public void attachToApplication(final Application application, final long autoStopAfter) {
57 | mAutoStopAfter = autoStopAfter;
58 | if (mLifeCycleWatcher == null) {
59 | final Niddler niddler = this;
60 | mLifeCycleWatcher = new NiddlerServiceLifeCycleWatcher(new ServiceConnection() {
61 | @Override
62 | public void onServiceConnected(final ComponentName name, final IBinder service) {
63 | if (!(service instanceof NiddlerService.NiddlerBinder)) //We are under test or something?
64 | {
65 | return;
66 | }
67 |
68 | final NiddlerService innerService = ((NiddlerService.NiddlerBinder) service).getService();
69 | mNiddlerService = new WeakReference<>(innerService);
70 | innerService.initialize(niddler, mAutoStopAfter);
71 | }
72 |
73 | @Override
74 | public void onServiceDisconnected(final ComponentName name) {
75 | mNiddlerService = null;
76 | }
77 | }, niddler);
78 | }
79 | application.unregisterActivityLifecycleCallbacks(mLifeCycleWatcher);
80 | application.registerActivityLifecycleCallbacks(mLifeCycleWatcher);
81 | }
82 |
83 | @Override
84 | public void closePlatform() {
85 | final NiddlerService niddlerService = mNiddlerService == null ? null : mNiddlerService.get();
86 | if (niddlerService != null) {
87 | niddlerService.stopSelf();
88 | mNiddlerService = null;
89 | }
90 | }
91 |
92 | /**
93 | * Creates a server info based on the application's package name and some device fields.
94 | * To provide a session icon, you can use meta data in the AndroidManifest. Eg: {@code }.
95 | * Icons can be names which will be resolved inside the UI or from plugin 2.9.9 and up base64 encoded images
96 | *
97 | * @param application The application niddler is instrumenting
98 | * @return A server info document to use in the {@link Niddler.Builder}
99 | */
100 | public static Niddler.NiddlerServerInfo fromApplication(final Application application) {
101 | return new Niddler.NiddlerServerInfo(application.getPackageName(), Build.MANUFACTURER + " " + Build.PRODUCT, getIconFromMeta(application));
102 | }
103 |
104 | @Nullable
105 | private static String getIconFromMeta(final Application application) {
106 | try {
107 | final ApplicationInfo app = application.getPackageManager().getApplicationInfo(application.getPackageName(), PackageManager.GET_META_DATA);
108 | return app.metaData.getString("com.niddler.icon");
109 | } catch (final Throwable e) {
110 | return null;
111 | }
112 | }
113 |
114 | public static class Builder extends Niddler.Builder {
115 |
116 | public Builder() {
117 | LogUtil.instance = new AndroidLogUtil();
118 | }
119 |
120 | @Override
121 | public AndroidNiddler build() {
122 | return new AndroidNiddler(mPassword, mPort, mCacheSize, mNiddlerServerInfo, mMaxStackTraceSize);
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/niddler/src/main/java/com/chimerapps/niddler/service/NiddlerService.java:
--------------------------------------------------------------------------------
1 | package com.chimerapps.niddler.service;
2 |
3 | import android.app.Notification;
4 | import android.app.NotificationManager;
5 | import android.app.PendingIntent;
6 | import android.app.Service;
7 | import android.content.Context;
8 | import android.content.Intent;
9 | import android.os.Binder;
10 | import android.os.Build;
11 | import android.os.Handler;
12 | import android.os.IBinder;
13 | import android.os.Looper;
14 | import android.util.Log;
15 |
16 | import com.chimerapps.niddler.R;
17 | import com.chimerapps.niddler.core.Niddler;
18 |
19 | import java.io.IOException;
20 |
21 | /**
22 | * @author Maarten Van Giel
23 | * @author Nicola Verbeeck
24 | */
25 | public class NiddlerService extends Service {
26 |
27 | private static final String LOG_TAG = NiddlerService.class.getSimpleName();
28 | private static final int NOTIFICATION_ID = 147;
29 | private static final long PORT_WAIT_DELAY = 500L;
30 |
31 | private IBinder mBinder;
32 | private NotificationManager mNotificationManager;
33 | private Niddler mNiddler;
34 | private int mBindCount;
35 | private long mAutoStopAfter;
36 | private Handler mHandler;
37 | private final Handler mNotificationPrepareHandler = new Handler(Looper.getMainLooper());
38 |
39 | @Override
40 | public IBinder onBind(final Intent intent) {
41 | ++mBindCount;
42 | mHandler.removeCallbacksAndMessages(null);
43 | return mBinder;
44 | }
45 |
46 | @Override
47 | public boolean onUnbind(final Intent intent) {
48 | if (--mBindCount <= 0) {
49 | mBindCount = 0;
50 |
51 | if (mAutoStopAfter > 0) {
52 | mHandler.postDelayed(new Runnable() {
53 | @Override
54 | public void run() {
55 | closeNiddler();
56 | }
57 | }, mAutoStopAfter);
58 | } else if (mAutoStopAfter == 0) {
59 | closeNiddler();
60 | }
61 | }
62 | return true;
63 | }
64 |
65 | @Override
66 | public void onRebind(final Intent intent) {
67 | ++mBindCount;
68 | mHandler.removeCallbacksAndMessages(null);
69 | super.onRebind(intent);
70 | }
71 |
72 | public void initialize(final Niddler niddler, final long autoStopAfter) {
73 | if ((niddler == null) || niddler.isClosed()) {
74 | stopSelf();
75 | return;
76 | }
77 | mAutoStopAfter = autoStopAfter;
78 | mNiddler = niddler;
79 | if (!mNiddler.isStarted()) {
80 | mNiddler.start();
81 | }
82 | createNotification();
83 | }
84 |
85 | public class NiddlerBinder extends Binder {
86 | public NiddlerService getService() {
87 | return NiddlerService.this;
88 | }
89 | }
90 |
91 | @Override
92 | public int onStartCommand(final Intent intent, final int flags, final int startId) {
93 | if ((intent != null) && (intent.getAction() != null) && intent.getAction().equals("STOP")) {
94 | closeNiddler();
95 | }
96 | return START_NOT_STICKY;
97 | }
98 |
99 | @Override
100 | public void onCreate() {
101 | super.onCreate();
102 | mBinder = new NiddlerBinder();
103 | mHandler = new Handler(Looper.getMainLooper());
104 | mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
105 | Log.d(LOG_TAG, "NiddlerService created!");
106 | }
107 |
108 | @Override
109 | public void onDestroy() {
110 | super.onDestroy();
111 | removeNotification();
112 | mBinder = null;
113 | Log.d(LOG_TAG, "NiddlerService destroyed!");
114 | }
115 |
116 | private void closeNiddler() {
117 | if (mNiddler != null) {
118 | try {
119 | mNiddler.close();
120 | } catch (final IOException e) {
121 | Log.w(LOG_TAG, "Failed to close niddler", e);
122 | }
123 | }
124 | removeNotification();
125 | }
126 |
127 | private void createNotification() {
128 | mNotificationPrepareHandler.removeCallbacksAndMessages(null);
129 | if (mNiddler.getPort() == 0) {
130 | mNotificationPrepareHandler.postDelayed(new Runnable() {
131 | @Override
132 | public void run() {
133 | createNotification();
134 | }
135 | }, PORT_WAIT_DELAY);
136 | return;
137 | }
138 |
139 | final Notification.Builder notificationBuilder;
140 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
141 | notificationBuilder = OreoCompatHelper.createNotificationBuilder(this);
142 | } else {
143 | notificationBuilder = new Notification.Builder(this);
144 | }
145 |
146 | final Intent intent = new Intent(this, NiddlerService.class);
147 | intent.setAction("STOP");
148 | final PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, MarshmallowCompatHelper.getPendingIntentFlags());
149 |
150 | notificationBuilder.setContentTitle("Niddler")
151 | .setContentText(getString(R.string.niddler_running_notification));
152 |
153 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
154 | JellyBeanCompatHelper.addBigText(notificationBuilder, getString(R.string.niddler_running_notification_big, getPackageName(), mNiddler.getPort()));
155 | }
156 |
157 | notificationBuilder.setContentIntent(pendingIntent)
158 | .setSmallIcon(android.R.drawable.ic_menu_preferences);
159 |
160 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
161 | KitkatCompatHelper.setLocalOnly(notificationBuilder, true);
162 | }
163 |
164 | final Notification notification = JellyBeanCompatHelper.build(notificationBuilder);
165 |
166 | notification.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
167 | mNotificationManager.notify(NOTIFICATION_ID, notification);
168 | }
169 |
170 | private void removeNotification() {
171 | mNotificationPrepareHandler.removeCallbacksAndMessages(null);
172 | mNotificationManager.cancel(NOTIFICATION_ID);
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn ( ) {
37 | echo "$*"
38 | }
39 |
40 | die ( ) {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/niddler-base/src/main/java/com/chimerapps/niddler/util/Base64.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Licensed to the Apache Software Foundation (ASF) under one or more
3 | * contributor license agreements. See the NOTICE file distributed with
4 | * this work for additional information regarding copyright ownership.
5 | * The ASF licenses this file to You under the Apache License, Version 2.0
6 | * (the "License"); you may not use this file except in compliance with
7 | * the License. You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | /**
18 | * @author Alexander Y. Kleymenov
19 | */
20 | package com.chimerapps.niddler.util;
21 |
22 | import java.io.UnsupportedEncodingException;
23 |
24 | final class Base64 {
25 | private Base64() {
26 | }
27 |
28 | public static byte[] decode(String in) {
29 | // Ignore trailing '=' padding and whitespace from the input.
30 | int limit = in.length();
31 | for (; limit > 0; limit--) {
32 | char c = in.charAt(limit - 1);
33 | if (c != '=' && c != '\n' && c != '\r' && c != ' ' && c != '\t') {
34 | break;
35 | }
36 | }
37 |
38 | // If the input includes whitespace, this output array will be longer than necessary.
39 | byte[] out = new byte[(int) (limit * 6L / 8L)];
40 | int outCount = 0;
41 | int inCount = 0;
42 |
43 | int word = 0;
44 | for (int pos = 0; pos < limit; pos++) {
45 | char c = in.charAt(pos);
46 |
47 | int bits;
48 | if (c >= 'A' && c <= 'Z') {
49 | // char ASCII value
50 | // A 65 0
51 | // Z 90 25 (ASCII - 65)
52 | bits = c - 65;
53 | } else if (c >= 'a' && c <= 'z') {
54 | // char ASCII value
55 | // a 97 26
56 | // z 122 51 (ASCII - 71)
57 | bits = c - 71;
58 | } else if (c >= '0' && c <= '9') {
59 | // char ASCII value
60 | // 0 48 52
61 | // 9 57 61 (ASCII + 4)
62 | bits = c + 4;
63 | } else if (c == '+' || c == '-') {
64 | bits = 62;
65 | } else if (c == '/' || c == '_') {
66 | bits = 63;
67 | } else if (c == '\n' || c == '\r' || c == ' ' || c == '\t') {
68 | continue;
69 | } else {
70 | return null;
71 | }
72 |
73 | // Append this char's 6 bits to the word.
74 | word = (word << 6) | (byte) bits;
75 |
76 | // For every 4 chars of input, we accumulate 24 bits of output. Emit 3 bytes.
77 | inCount++;
78 | if (inCount % 4 == 0) {
79 | out[outCount++] = (byte) (word >> 16);
80 | out[outCount++] = (byte) (word >> 8);
81 | out[outCount++] = (byte) word;
82 | }
83 | }
84 |
85 | int lastWordChars = inCount % 4;
86 | if (lastWordChars == 1) {
87 | // We read 1 char followed by "===". But 6 bits is a truncated byte! Fail.
88 | return null;
89 | } else if (lastWordChars == 2) {
90 | // We read 2 chars followed by "==". Emit 1 byte with 8 of those 12 bits.
91 | word = word << 12;
92 | out[outCount++] = (byte) (word >> 16);
93 | } else if (lastWordChars == 3) {
94 | // We read 3 chars, followed by "=". Emit 2 bytes for 16 of those 18 bits.
95 | word = word << 6;
96 | out[outCount++] = (byte) (word >> 16);
97 | out[outCount++] = (byte) (word >> 8);
98 | }
99 |
100 | // If we sized our out array perfectly, we're done.
101 | if (outCount == out.length) {
102 | return out;
103 | }
104 |
105 | // Copy the decoded bytes to a new, right-sized array.
106 | byte[] prefix = new byte[outCount];
107 | System.arraycopy(out, 0, prefix, 0, outCount);
108 | return prefix;
109 | }
110 |
111 | private static final byte[] MAP = new byte[]{
112 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
113 | 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
114 | 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4',
115 | '5', '6', '7', '8', '9', '+', '/'
116 | };
117 |
118 | private static final byte[] URL_MAP = new byte[]{
119 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
120 | 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
121 | 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4',
122 | '5', '6', '7', '8', '9', '-', '_'
123 | };
124 |
125 | public static String encode(byte[] in) {
126 | return encode(in, MAP);
127 | }
128 |
129 | public static String encodeUrl(byte[] in) {
130 | return encode(in, URL_MAP);
131 | }
132 |
133 | private static String encode(byte[] in, byte[] map) {
134 | int length = (in.length + 2) / 3 * 4;
135 | byte[] out = new byte[length];
136 | int index = 0, end = in.length - in.length % 3;
137 | for (int i = 0; i < end; i += 3) {
138 | out[index++] = map[(in[i] & 0xff) >> 2];
139 | out[index++] = map[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)];
140 | out[index++] = map[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)];
141 | out[index++] = map[(in[i + 2] & 0x3f)];
142 | }
143 | switch (in.length % 3) {
144 | case 1:
145 | out[index++] = map[(in[end] & 0xff) >> 2];
146 | out[index++] = map[(in[end] & 0x03) << 4];
147 | out[index++] = '=';
148 | out[index++] = '=';
149 | break;
150 | case 2:
151 | out[index++] = map[(in[end] & 0xff) >> 2];
152 | out[index++] = map[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)];
153 | out[index++] = map[((in[end + 1] & 0x0f) << 2)];
154 | out[index++] = '=';
155 | break;
156 | }
157 | try {
158 | return new String(out, "US-ASCII");
159 | } catch (UnsupportedEncodingException e) {
160 | throw new AssertionError(e);
161 | }
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/niddler-example-android/src/main/java/com/chimerapps/sampleapplication/activity/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.icapps.sampleapplication.activity
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.util.Log
6 | import android.view.View
7 | import androidx.appcompat.app.AppCompatActivity
8 | import com.icapps.sampleapplication.NiddlerSampleApplication
9 | import com.icapps.sampleapplication.R
10 | import com.icapps.sampleapplication.api.ExampleJsonApi
11 | import com.icapps.sampleapplication.api.ExampleXMLApi
12 | import com.icapps.sampleapplication.api.Post
13 | import com.icapps.sampleapplication.api.TimeoutApi
14 | import okhttp3.MediaType.Companion.toMediaType
15 | import okhttp3.MultipartBody
16 | import okhttp3.RequestBody.Companion.toRequestBody
17 | import okhttp3.ResponseBody
18 | import retrofit2.Call
19 | import retrofit2.Callback
20 | import retrofit2.Response
21 | import java.net.URL
22 |
23 | class MainActivity : AppCompatActivity() {
24 |
25 | private lateinit var jsonApi: ExampleJsonApi
26 | private lateinit var theXMLApi: ExampleXMLApi
27 | private lateinit var timeoutApi: TimeoutApi
28 |
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 | setContentView(R.layout.activity_main)
32 |
33 | jsonApi = (application as NiddlerSampleApplication).jsonPlaceholderApi
34 | theXMLApi = (application as NiddlerSampleApplication).xmlPlaceholderApi
35 | timeoutApi = (application as NiddlerSampleApplication).timeoutApi
36 |
37 | findViewById(R.id.newActivity).setOnClickListener { startActivity(Intent(this@MainActivity, MainActivity::class.java)) }
38 |
39 | findViewById(R.id.buttonJson).setOnClickListener {
40 | jsonApi.posts.enqueue(object : Callback> {
41 | override fun onResponse(call: Call>, response: Response>) {
42 | Log.w("Response", "Got JSON response")
43 | }
44 |
45 | override fun onFailure(call: Call>, t: Throwable) {
46 | Log.e("Response", "Got JSON response failure!", t)
47 | }
48 | })
49 | }
50 |
51 | findViewById(R.id.buttonXML).setOnClickListener {
52 | theXMLApi.menu.enqueue(object : Callback {
53 | override fun onResponse(call: Call, response: Response) {
54 | Log.w("Response", "Got xml response")
55 | }
56 |
57 | override fun onFailure(call: Call, t: Throwable) {
58 | Log.e("Response", "Got xml response failure!", t)
59 | }
60 | })
61 | }
62 |
63 | findViewById(R.id.buttonPost).setOnClickListener {
64 | jsonApi.createPost(makeMessage(), makeAttachment()).enqueue(object : Callback {
65 | override fun onResponse(call: Call, response: Response) {
66 | Log.w("Response", "Got xml response")
67 | }
68 |
69 | override fun onFailure(call: Call, t: Throwable) {
70 | Log.e("Response", "Got xml response failure!", t)
71 | }
72 | })
73 | }
74 |
75 | findViewById(R.id.buttonTimeoutOk).setOnClickListener {
76 | timeoutApi.getOk().enqueue(object : Callback {
77 | override fun onResponse(call: Call, response: Response) {
78 | Log.w("Response", "Got timeout response: ${response.code()}")
79 | }
80 |
81 | override fun onFailure(call: Call, t: Throwable) {
82 | Log.e("Response", "Got timeout response failure!", t)
83 | }
84 | })
85 | }
86 |
87 | findViewById(R.id.buttonTimeoutNotFound).setOnClickListener {
88 | timeoutApi.getNotFound().enqueue(object : Callback {
89 | override fun onResponse(call: Call, response: Response) {
90 | Log.w("Response", "Got timeout response ${response.code()}")
91 | }
92 |
93 | override fun onFailure(call: Call, t: Throwable) {
94 | Log.e("Response", "Got timeout response failure!", t)
95 | }
96 | })
97 | }
98 |
99 | findViewById(R.id.buttonTimeoutTimeout).setOnClickListener {
100 | timeoutApi.getOkTimeout().enqueue(object : Callback {
101 | override fun onResponse(call: Call, response: Response) {
102 | Log.w("Response", "Got timeout response: ${response.code()}")
103 | }
104 |
105 | override fun onFailure(call: Call, t: Throwable) {
106 | Log.e("Response", "Got timeout response failure!", t)
107 | }
108 | })
109 | }
110 |
111 | findViewById(R.id.buttonUrlConnection).setOnClickListener {
112 | Thread {
113 | val connection = URL("https://jsonplaceholder.typicode.com/posts").openConnection()
114 | connection.setRequestProperty("X-Debug", "true");
115 | Log.d("Response", connection
116 | .getInputStream().bufferedReader().readText());
117 | }.start()
118 | }
119 | }
120 |
121 | private fun makeMessage(): MultipartBody.Part {
122 | return MultipartBody.Part.create("{\"body\":\"This is the json part of a multipart upload example\"}".toRequestBody("application/json".toMediaType()))
123 | }
124 |
125 | private fun makeAttachment(): MultipartBody.Part {
126 | val imageBytes = assets.open("image.jpeg").use { it.readBytes() }
127 |
128 | return MultipartBody.Part.create(imageBytes.toRequestBody("image/jpeg".toMediaType(), 0, imageBytes.size))
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Niddler [](https://maven-badges.herokuapp.com/maven-central/com.chimerapps.niddler/niddler)
2 |
3 | ## Niddler has migrated on maven central to `com.chimerapps.niddler:...`!!!
4 |
5 | 
6 |
7 | Niddler is a network debugging utility for Android and java apps that caches network requests/responses, and exposes them over a websocket based protocol. It comes with a convenient interceptor for Square's [OkHttpClient](http://square.github.io/okhttp/), as well as a no-op interceptor for use in release scenario's.
8 |
9 | Niddler is meant to be used with [Niddler-ui](https://github.com/Chimerapps/niddler-ui), which is a plugin for IntelliJ/Android Studio. When used together it allows you to visualize network activity, easily navigate JSON/XML responses, debug, ...
10 |
11 | ## Example use (Android)
12 | build.gradle:
13 | ``` groovy
14 | //Ensure jcenter is in the repo list
15 | debugCompile 'com.chimerapps.niddler:niddler:{latest version}'
16 | releaseCompile 'com.chimerapps.niddler:niddler-noop:{latest version}'
17 | ```
18 |
19 | Example usage with Android Application:
20 | ```kotlin
21 | class NiddlerSampleApplication : Application() {
22 |
23 | override fun onCreate() {
24 | super.onCreate()
25 |
26 | val niddler = AndroidNiddler.Builder()
27 | .setPort(0) //Use port 0 to prevent conflicting ports, auto-discovery will find it anyway!
28 | .setNiddlerInformation(AndroidNiddler.fromApplication(this)) //Set com.niddler.icon in AndroidManifest meta-data to an icon you wish to use for this session
29 | .setMaxStackTraceSize(10)
30 | .build()
31 |
32 | niddler.attachToApplication(this) //Make the niddler service start whenever an activity starts
33 |
34 | //Create an interceptor for okHttp 3+
35 | val okHttpInterceptor = NiddlerOkHttpInterceptor(niddler, "Default")
36 | //Blacklist some items based on regex on the URL, these won't show up in niddler
37 | okHttpInterceptor.blacklist(".*raw\\.githubusercontent\\.com.*")
38 |
39 | //Create okhttp client. Note that we add this interceptor as an application layer interceptor, this ensures we see 'unpacked' responses
40 | //When using multiple interceptors, add niddler last!
41 | val okHttpClient = OkHttpClient.Builder()
42 | .addInterceptor(okHttpInterceptor)
43 | .build()
44 |
45 | // Every request done with this OkHttpClient will now be logged with Niddler
46 |
47 | //Advanced configuration, add stack traces when using retrofit
48 | val retrofitBuilder = Retrofit.Builder()
49 | .baseUrl("https://example.com")
50 | .client(okHttpClient)
51 | ...
52 |
53 | //Inject custom call factory that adds stack trace information to retrofit
54 | NiddlerRetrofitCallInjector.inject(retrofitBuilder, niddler, okHttpClient)
55 | val retrofitInstance = retrofitBuilder.build()
56 |
57 | ...
58 | }
59 |
60 | }
61 | ```
62 |
63 | Calling `niddler.attachToApplication(application)` will launch a service with a notification. The service is partly bound to the lifecycle of your app, when activities start, it starts the server up. The notification provides visual feedback that Niddler is running, and allows you to stop the Niddler service. It is also a good reminder that Niddler is a debugging tool and not meant to be included in production apps.
64 |
65 | Using the service is not required. You can also call `niddler.start()` and `niddler.close()` if you wish to start and stop Niddler manually.
66 |
67 | ## Example use (Java)
68 | build.gradle:
69 | ``` groovy
70 | //Ensure jcenter is in the repo list (1.1.1 is the latest stable version)
71 | debugCompile 'com.chimerapps.niddler:niddler-java:1.1.1'
72 | releaseCompile 'com.chimerapps.niddler:niddler-java-noop:1.1.1'
73 | ```
74 |
75 | Use with java application:
76 | ```java
77 | public class Sample {
78 |
79 | public static void main(final String[] args) {
80 | final JavaNiddlerNiddler niddler = new JavaNiddler.Builder("superSecretPassword")
81 | .setPort(0)
82 | .setNiddlerInformation(Niddler.NiddlerServerInfo("Example", "Example description"))
83 | .build();
84 |
85 | final OkHttpClient okHttpClient = new OkHttpClient.Builder()
86 | .addInterceptor(new NiddlerOkHttpInterceptor(niddler))
87 | .build();
88 |
89 | niddler.start();
90 |
91 | //Run application
92 | // Every request done with this OkHttpClient will now be logged with Niddler
93 |
94 | niddler.close();
95 | }
96 |
97 | }
98 | ```
99 |
100 | For instructions on how to access the captured network data, see [niddler-ui](https://github.com/Chimerapps/niddler-ui)
101 |
102 | ### Waiting for debugger attached
103 | When launching android apps instrumented with the automatic niddler service, you can pass an intent extra to the starting activity that will force the app to 'wait' until a niddler
104 | debugger is connected.
105 |
106 | You have to pass `--ei Niddler-Wait-For-Debugger 1` to the activity manager (see the run configuration options dropdown in Android Studio) to enable this.
107 |
108 | ## Session icons
109 | Niddler supports reporting session icons to the UI since version 1.1.0. These icons provide an extra visual cue when browsing running sessions. You can pass the icon by
110 | passing it to the `NiddlerServerInfo` when building the niddler instance.
111 |
112 | By default the following 4 icons are supported by the plugin: android, apple, dart, flutter. To use custom icons, place them in the .idea/niddler folder (square 20x20 or 40x40 @2x) with the
113 | name of file, the name of the icon
114 |
115 | ### Android specific
116 | Since v 1.1.1
117 |
118 |
119 | When using the convenience method to create `NiddlerServerInfo` from an Android application context, you can pass the icon by setting it in the `com.niddler.icon` `meta-data` of the manifest.
120 |
121 | Eg:
122 | ```xml
123 |
125 |
126 |
127 |
128 |
129 | ```
130 |
--------------------------------------------------------------------------------
/niddler-base/src/main/java/com/chimerapps/niddler/core/debug/NiddlerDebugger.java:
--------------------------------------------------------------------------------
1 | package com.chimerapps.niddler.core.debug;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.annotation.Nullable;
5 |
6 | import com.chimerapps.niddler.core.NiddlerRequest;
7 | import com.chimerapps.niddler.core.NiddlerResponse;
8 |
9 | import java.io.IOException;
10 | import java.util.List;
11 | import java.util.Map;
12 |
13 | /**
14 | * @author Nicola Verbeeck
15 | * @version 1
16 | */
17 | public interface NiddlerDebugger {
18 |
19 | /**
20 | * Checks if the debugger is active at the time
21 | *
22 | * @return True if the debugger was active when checking, false if it is disabled
23 | */
24 | boolean isActive();
25 |
26 | /**
27 | * Checks if the given url has been added to the blacklist and thus does not need to be tracked
28 | *
29 | * @param url The url to check
30 | * @return True if the request should not be logged through niddler, false if there is no blacklist for the url
31 | */
32 | boolean isBlacklisted(@NonNull final CharSequence url);
33 |
34 | /**
35 | * Override the request with an updated version. This is executed AFTER checking the blacklist and when the request is overridden, it is NOT checked again against the blacklist
36 | * If the request is overridden, it will still follow the regular flow of being checked against other debugger actions (handleRequest, ...)
37 | *
38 | * @param request The request to match
39 | * @return The new request to use instead of the original request or null when the request has not been overridden
40 | */
41 | @Nullable
42 | DebugRequest overrideRequest(@NonNull final NiddlerRequest request);
43 |
44 | /**
45 | * Checks if the debugger wants to override the response for the given request. This is executed BEFORE an actual request is put on the network
46 | *
47 | * @param request The request to match
48 | * @return Either the response to use instead of performing the network call or null when the debugger does not want to interfere at this time
49 | */
50 | @Nullable
51 | DebugResponse handleRequest(@NonNull final NiddlerRequest request);
52 |
53 | /**
54 | * Checks if the debugger wants to override the response for the given request. This is executed AFTER performing the actual network request and thus allows the debugger
55 | * to override what is reported back to the client
56 | *
57 | * @param request The request to match
58 | * @param response The response to match
59 | * @return Either the response to use instead the network response or null when the debugger does not want to interfere at this time
60 | */
61 | @Nullable
62 | DebugResponse handleResponse(@NonNull final NiddlerRequest request, @NonNull final NiddlerResponse response);
63 |
64 | /**
65 | * Applies a delay before executing the request. This delay is executed BEFORE checking if the request should have been blacklisted.
66 | * See {@link com.chimerapps.niddler.core.Niddler#NIDDLER_DEBUG_TIMING_RESPONSE_HEADER}
67 | *
68 | * @return True if the system interfered by causing a delay, false if no delay was added.
69 | * @throws IOException When sleeping the thread fails
70 | */
71 | boolean applyDelayBeforeBlacklist() throws IOException;
72 |
73 | /**
74 | * Applies a delay before executing the request. This delay is executed after checking if the request should have been blacklisted, this means that blacklisted items will
75 | * not be delayed.
76 | * See {@link com.chimerapps.niddler.core.Niddler#NIDDLER_DEBUG_TIMING_RESPONSE_HEADER}
77 | *
78 | * @return True if the system interfered by causing a delay, false if no delay was added.
79 | * @throws IOException When sleeping the thread fails
80 | */
81 | boolean applyDelayAfterBlacklist() throws IOException;
82 |
83 | /**
84 | * Ensures that the request took at least a predetermined time (in milliseconds) to execute.
85 | * This provides more granular control over the execution time as this takes into account the time the request has spent waiting for network and for the debugger
86 | * See {@link com.chimerapps.niddler.core.Niddler#NIDDLER_DEBUG_TIMING_RESPONSE_HEADER}
87 | *
88 | * @param startTime The system time in nanoseconds ({@link System#nanoTime()}) right before the request
89 | * @return True if the system interfered by causing a delay, false if no delay was added.
90 | * @throws IOException When sleeping the thread fails
91 | */
92 | boolean ensureCallTime(final long startTime) throws IOException;
93 |
94 | /**
95 | * Instructs the debugger server to hold all calls until a debugger has connected and has started a session. Can be cancelled with {#cancelWaitForConnection}
96 | *
97 | * @param onDebuggerConnected Callback to be executed when a debugger has connected and started a session. Can be called on any thread
98 | * @return True if we transitioned to the waiting state and the called should inform the user, false if not
99 | */
100 | boolean waitForConnection(@NonNull final Runnable onDebuggerConnected);
101 |
102 | /**
103 | * @return True if the debugger server is currently waiting until a debugger has connected
104 | */
105 | boolean isWaitingForConnection();
106 |
107 | /**
108 | * Tells the debugger server to stop waiting for a debugger connection and proceed
109 | */
110 | void cancelWaitForConnection();
111 |
112 | abstract class DebugMessage {
113 | /**
114 | * Headers, optional
115 | */
116 | @Nullable
117 | public final Map> headers;
118 |
119 | /**
120 | * base64 encoded body if any
121 | */
122 | @Nullable
123 | public final String encodedBody;
124 |
125 | /**
126 | * Mime type of the body, must be set if the body is non-null
127 | */
128 | @Nullable
129 | public final String bodyMimeType;
130 |
131 | DebugMessage(@Nullable final Map> headers, @Nullable final String encodedBody, @Nullable final String bodyMimeType) {
132 | this.headers = headers;
133 | this.encodedBody = encodedBody;
134 | this.bodyMimeType = bodyMimeType;
135 | }
136 | }
137 |
138 | class DebugResponse extends DebugMessage {
139 | /**
140 | * Status code
141 | */
142 | public final int code;
143 |
144 | /**
145 | * Status message
146 | */
147 | @NonNull
148 | public final String message;
149 |
150 | public DebugResponse(final int code,
151 | @NonNull final String message,
152 | @Nullable final Map> headers,
153 | @Nullable final String encodedBody,
154 | @Nullable final String bodyMimeType) {
155 | super(headers, encodedBody, bodyMimeType);
156 | this.code = code;
157 | this.message = message;
158 | }
159 | }
160 |
161 | class DebugRequest extends DebugMessage {
162 | /**
163 | * The url of the request, required
164 | */
165 | @NonNull
166 | public final String url;
167 |
168 | /**
169 | * Method of the request, required
170 | */
171 | @NonNull
172 | public final String method;
173 |
174 | public DebugRequest(@NonNull final String url,
175 | @NonNull final String method,
176 | @Nullable final Map> headers,
177 | @Nullable final String encodedBody,
178 | @Nullable final String bodyMimeType) {
179 | super(headers, encodedBody, bodyMimeType);
180 | this.url = url;
181 | this.method = method;
182 | }
183 | }
184 |
185 | }
186 |
--------------------------------------------------------------------------------
/niddler-noop-base/src/main/java/com/chimerapps/niddler/core/debug/NiddlerDebugger.java:
--------------------------------------------------------------------------------
1 | package com.chimerapps.niddler.core.debug;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.annotation.Nullable;
5 |
6 | import com.chimerapps.niddler.core.NiddlerRequest;
7 | import com.chimerapps.niddler.core.NiddlerResponse;
8 |
9 | import java.io.IOException;
10 | import java.util.List;
11 | import java.util.Map;
12 |
13 | /**
14 | * @author Nicola Verbeeck
15 | * @version 1
16 | */
17 | public interface NiddlerDebugger {
18 |
19 | /**
20 | * Checks if the debugger is active at the time
21 | *
22 | * @return True if the debugger was active when checking, false if it is disabled
23 | */
24 | boolean isActive();
25 |
26 | /**
27 | * Checks if the given url has been added to the blacklist and thus does not need to be tracked
28 | *
29 | * @param url The url to check
30 | * @return True if the request should not be logged through niddler, false if there is no blacklist for the url
31 | */
32 | boolean isBlacklisted(@NonNull final CharSequence url);
33 |
34 | /**
35 | * Override the request with an updated version. This is executed AFTER checking the blacklist and when the request is overridden, it is NOT checked again against the blacklist
36 | * If the request is overridden, it will still follow the regular flow of being checked against other debugger actions (handleRequest, ...)
37 | *
38 | * @param request The request to match
39 | * @return The new request to use instead of the original request or null when the request has not been overridden
40 | */
41 | @Nullable
42 | DebugRequest overrideRequest(@NonNull final NiddlerRequest request);
43 |
44 | /**
45 | * Checks if the debugger wants to override the response for the given request. This is executed BEFORE an actual request is put on the network
46 | *
47 | * @param request The request to match
48 | * @return Either the response to use instead of performing the network call or null when the debugger does not want to interfere at this time
49 | */
50 | @Nullable
51 | DebugResponse handleRequest(@NonNull final NiddlerRequest request);
52 |
53 | /**
54 | * Checks if the debugger wants to override the response for the given request. This is executed AFTER performing the actual network request and thus allows the debugger
55 | * to override what is reported back to the client
56 | *
57 | * @param request The request to match
58 | * @param response The response to match
59 | * @return Either the response to use instead the network response or null when the debugger does not want to interfere at this time
60 | */
61 | @Nullable
62 | DebugResponse handleResponse(@NonNull final NiddlerRequest request, @NonNull final NiddlerResponse response);
63 |
64 | /**
65 | * Applies a delay before executing the request. This delay is executed BEFORE checking if the request should have been blacklisted.
66 | * See {@link com.chimerapps.niddler.core.Niddler#NIDDLER_DEBUG_TIMING_RESPONSE_HEADER}
67 | *
68 | * @return True if the system interfered by causing a delay, false if no delay was added.
69 | * @throws IOException When sleeping the thread fails
70 | */
71 | boolean applyDelayBeforeBlacklist() throws IOException;
72 |
73 | /**
74 | * Applies a delay before executing the request. This delay is executed after checking if the request should have been blacklisted, this means that blacklisted items will
75 | * not be delayed.
76 | * See {@link com.chimerapps.niddler.core.Niddler#NIDDLER_DEBUG_TIMING_RESPONSE_HEADER}
77 | *
78 | * @return True if the system interfered by causing a delay, false if no delay was added.
79 | * @throws IOException When sleeping the thread fails
80 | */
81 | boolean applyDelayAfterBlacklist() throws IOException;
82 |
83 | /**
84 | * Ensures that the request took at least a predetermined time (in milliseconds) to execute.
85 | * This provides more granular control over the execution time as this takes into account the time the request has spent waiting for network and for the debugger
86 | * See {@link com.chimerapps.niddler.core.Niddler#NIDDLER_DEBUG_TIMING_RESPONSE_HEADER}
87 | *
88 | * @param startTime The system time in nanoseconds ({@link System#nanoTime()}) right before the request
89 | * @return True if the system interfered by causing a delay, false if no delay was added.
90 | * @throws IOException When sleeping the thread fails
91 | */
92 | boolean ensureCallTime(final long startTime) throws IOException;
93 |
94 | /**
95 | * Instructs the debugger server to hold all calls until a debugger has connected and has started a session. Can be cancelled with {#cancelWaitForConnection}
96 | *
97 | * @param onDebuggerConnected Callback to be executed when a debugger has connected and started a session. Can be called on any thread
98 | * @return True if we transitioned to the waiting state and the called should inform the user, false if not
99 | */
100 | boolean waitForConnection(@NonNull final Runnable onDebuggerConnected);
101 |
102 | /**
103 | * @return True if the debugger server is currently waiting until a debugger has connected
104 | */
105 | boolean isWaitingForConnection();
106 |
107 | /**
108 | * Tells the debugger server to stop waiting for a debugger connection and proceed
109 | */
110 | void cancelWaitForConnection();
111 |
112 | abstract class DebugMessage {
113 | /**
114 | * Headers, optional
115 | */
116 | @Nullable
117 | public final Map> headers;
118 |
119 | /**
120 | * base64 encoded body if any
121 | */
122 | @Nullable
123 | public final String encodedBody;
124 |
125 | /**
126 | * Mime type of the body, must be set if the body is non-null
127 | */
128 | @Nullable
129 | public final String bodyMimeType;
130 |
131 | DebugMessage(@Nullable final Map> headers, @Nullable final String encodedBody, @Nullable final String bodyMimeType) {
132 | this.headers = headers;
133 | this.encodedBody = encodedBody;
134 | this.bodyMimeType = bodyMimeType;
135 | }
136 | }
137 |
138 | class DebugResponse extends DebugMessage {
139 | /**
140 | * Status code
141 | */
142 | public final int code;
143 |
144 | /**
145 | * Status message
146 | */
147 | @NonNull
148 | public final String message;
149 |
150 | public DebugResponse(final int code,
151 | @NonNull final String message,
152 | @Nullable final Map> headers,
153 | @Nullable final String encodedBody,
154 | @Nullable final String bodyMimeType) {
155 | super(headers, encodedBody, bodyMimeType);
156 | this.code = code;
157 | this.message = message;
158 | }
159 | }
160 |
161 | class DebugRequest extends DebugMessage {
162 | /**
163 | * The url of the request, required
164 | */
165 | @NonNull
166 | public final String url;
167 |
168 | /**
169 | * Method of the request, required
170 | */
171 | @NonNull
172 | public final String method;
173 |
174 | public DebugRequest(@NonNull final String url,
175 | @NonNull final String method,
176 | @Nullable final Map> headers,
177 | @Nullable final String encodedBody,
178 | @Nullable final String bodyMimeType) {
179 | super(headers, encodedBody, bodyMimeType);
180 | this.url = url;
181 | this.method = method;
182 | }
183 | }
184 |
185 | }
186 |
--------------------------------------------------------------------------------
/niddler-noop-base/src/main/java/com/chimerapps/niddler/core/Niddler.java:
--------------------------------------------------------------------------------
1 | package com.chimerapps.niddler.core;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.annotation.Nullable;
5 |
6 | import com.chimerapps.niddler.core.debug.NiddlerDebugger;
7 |
8 | import java.io.Closeable;
9 | import java.util.List;
10 |
11 | /**
12 | * @author Maarten Van Giel
13 | * @author Nicola Verbeeck
14 | */
15 | @SuppressWarnings("WeakerAccess")
16 | public abstract class Niddler implements Closeable {
17 |
18 | public static final String NIDDLER_DEBUG_RESPONSE_HEADER = "X-Niddler-Debug";
19 | public static final String NIDDLER_DEBUG_TIMING_RESPONSE_HEADER = "X-Niddler-Debug-Timing";
20 | public static final String INTENT_EXTRA_WAIT_FOR_DEBUGGER = "Niddler-Wait-For-Debugger";
21 |
22 | private static final StackTraceKey EMPTY = new StackTraceKey();
23 |
24 | private static final FakeNiddlerDebugger mFakeDebugger = new FakeNiddlerDebugger();
25 |
26 | protected Niddler() {
27 | }
28 |
29 | public boolean isStackTracingEnabled() {
30 | return false;
31 | }
32 |
33 | @Nullable
34 | public StackTraceElement[] popTraceForId(@NonNull final StackTraceKey stackTraceId) {
35 | return null;
36 | }
37 |
38 | @NonNull
39 | public StackTraceKey pushStackTrace(@NonNull final StackTraceElement[] trace) {
40 | return EMPTY;
41 | }
42 |
43 | public void logRequest(final NiddlerRequest request) {
44 | }
45 |
46 | public void logResponse(final NiddlerResponse response) {
47 | }
48 |
49 | public void start() {
50 | }
51 |
52 | /**
53 | * Niddler supports a single active debugger at any given time. This debugger has capabilities that add items to a blacklist, return default responses upon request, ...
54 | * Interceptors should implement as many of the integrations as they can. For an example take a look at {@link com.chimerapps.niddler.interceptor.okhttp.NiddlerOkHttpInterceptor}
55 | *
56 | * @return The debugger for niddler
57 | */
58 | @NonNull
59 | public NiddlerDebugger debugger() {
60 | return mFakeDebugger;
61 | }
62 |
63 | @Override
64 | public void close() {
65 | }
66 |
67 | /**
68 | * Indicates if niddler is configured to log requests, use this to determine in your interceptor if you need
69 | * to generate a message
70 | *
71 | * @return True if this is the real niddler, in no-op mode, this returns false
72 | */
73 | @SuppressWarnings("unused")
74 | public static boolean enabled() {
75 | return false;
76 | }
77 |
78 | /**
79 | * @return True if the niddler server is started
80 | */
81 | public boolean isStarted() {
82 | return false;
83 | }
84 |
85 | /**
86 | * @return True if the server is stopped
87 | */
88 | public boolean isClosed() {
89 | return true;
90 | }
91 |
92 | /**
93 | * @return The socket port we are listening on
94 | */
95 | public int getPort() {
96 | return -1;
97 | }
98 |
99 | /**
100 | * Notifies niddler that the static blacklist has been modified
101 | *
102 | * @param blacklist The current blacklist. Thread safe
103 | * @param id The id of the blacklist handler
104 | * @param name The name of the blacklist handler
105 | */
106 | public void onStaticBlacklistChanged(@NonNull final String id, @NonNull final String name,
107 | @NonNull final List blacklist) {
108 | }
109 |
110 | /**
111 | * Registers a listener for static blacklist updates
112 | *
113 | * @param listener The blacklist update listener to register
114 | */
115 | public void registerBlacklistListener(@NonNull final StaticBlacklistListener listener) {
116 | }
117 |
118 | @SuppressWarnings({"WeakerAccess", "unused", "PackageVisibleField", "StaticMethodOnlyUsedInOneClass"})
119 | public static final class NiddlerServerInfo {
120 |
121 | /**
122 | * Protocol version 4:
123 | * - Support for configuration (blacklist, basic debugging)
124 | */
125 | static final int PROTOCOL_VERSION = 4;
126 | final String name;
127 | final String description;
128 | @Nullable
129 | final String icon;
130 |
131 | public NiddlerServerInfo(final String name, final String description, @Nullable final String icon) {
132 | this.name = name;
133 | this.description = description;
134 | this.icon = icon;
135 | }
136 |
137 | public NiddlerServerInfo(final String name, final String description) {
138 | this(name, description, null);
139 | }
140 | }
141 |
142 | @SuppressWarnings({"unused", "SameParameterValue", "MagicNumber"})
143 | public static abstract class Builder {
144 |
145 | /**
146 | * Creates a new builder with a given password to use for the niddler server authentication
147 | *
148 | * @param password The password to use
149 | */
150 | public Builder(final String password) {
151 | }
152 |
153 | /**
154 | * Creates a new builder that has authentication disabled
155 | */
156 | public Builder() {
157 | }
158 |
159 | /**
160 | * Sets the maximum request stack trace depth. 0 by default
161 | *
162 | * @param maxStackTraceSize Max stack trace depth. Set to 0 to disable request origin tracing
163 | * @return Builder
164 | */
165 | public Builder setMaxStackTraceSize(int maxStackTraceSize) {
166 | return this;
167 | }
168 |
169 | /**
170 | * Sets the port on which Niddler will listen for incoming connections
171 | *
172 | * @param port The port to be used
173 | * @return Builder
174 | */
175 | public Builder setPort(final int port) {
176 | return this;
177 | }
178 |
179 | /**
180 | * Sets the cache size to be used for caching requests and responses while there is no client connected
181 | *
182 | * @param cacheSize The cache size to be used, in bytes
183 | * @return Builder
184 | */
185 | public Builder setCacheSize(final long cacheSize) {
186 | return this;
187 | }
188 |
189 | /**
190 | * Sets additional information about this Niddler server which will be shown on the client side
191 | *
192 | * @param niddlerServerInfo The additional information about this Niddler server
193 | * @return Builder
194 | */
195 | public Builder setNiddlerInformation(final NiddlerServerInfo niddlerServerInfo) {
196 | return this;
197 | }
198 |
199 | /**
200 | * Builds a Niddler instance with the configured parameters
201 | *
202 | * @return a Niddler instance
203 | */
204 | public abstract T build();
205 |
206 | }
207 |
208 | interface PlatformNiddler {
209 | void closePlatform();
210 | }
211 |
212 | public static class StackTraceKey {
213 | }
214 |
215 | /**
216 | * Static blacklist entry
217 | */
218 | public static class StaticBlackListEntry {
219 |
220 | public boolean matches(@NonNull final CharSequence sequence) {
221 | return false;
222 | }
223 |
224 | public boolean setEnabled(final boolean value) {
225 | return false;
226 | }
227 |
228 | public boolean isEnabled() {
229 | return false;
230 | }
231 |
232 | @NonNull
233 | public String pattern() {
234 | return "";
235 | }
236 |
237 | public boolean isForPattern(@NonNull final String pattern) {
238 | return false;
239 | }
240 | }
241 |
242 | /**
243 | * Listener for updating the static blacklist
244 | */
245 | public interface StaticBlacklistListener {
246 |
247 | /**
248 | * The id of the blacklist handler. This id must not change during the lifetime of the handler
249 | *
250 | * @return The id of the blacklist handler
251 | */
252 | @NonNull
253 | String getId();
254 |
255 | /**
256 | * Called when the static blacklist should be updated to reflect the new enabled status
257 | *
258 | * @param pattern The pattern to enable/disable
259 | * @param enabled Flag indicating if the static blacklist item is enabled or disabled
260 | */
261 | void setBlacklistItemEnabled(@NonNull final String pattern, final boolean enabled);
262 |
263 | }
264 | }
265 |
--------------------------------------------------------------------------------
/niddler-base/src/main/java/com/chimerapps/niddler/core/MessageBuilder.java:
--------------------------------------------------------------------------------
1 | package com.chimerapps.niddler.core;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import com.chimerapps.niddler.util.LogUtil;
6 | import com.chimerapps.niddler.util.StringUtil;
7 |
8 | import org.json.JSONArray;
9 | import org.json.JSONException;
10 | import org.json.JSONObject;
11 |
12 | import java.io.ByteArrayOutputStream;
13 | import java.io.IOException;
14 | import java.util.List;
15 | import java.util.Locale;
16 | import java.util.Map;
17 |
18 | /**
19 | * @author Nicola Verbeeck
20 | * Date 22/11/16.
21 | */
22 | final class MessageBuilder {
23 |
24 | private MessageBuilder() {
25 | //Utility class
26 | }
27 |
28 | static String buildMessage(final NiddlerRequest request, final int stackTraceMaxDepth) {
29 | final JSONObject object = buildMessageJson(request, stackTraceMaxDepth);
30 | if (object == null) {
31 | return null;
32 | }
33 | return object.toString();
34 | }
35 |
36 | static JSONObject buildMessageJson(final NiddlerRequest request, final int stackTraceMaxDepth) {
37 | if (request == null) {
38 | return null;
39 | }
40 | final JSONObject object = new JSONObject();
41 | try {
42 | object.put("type", "request");
43 | initGeneric(object, request);
44 | object.put("method", request.getMethod());
45 | object.put("url", request.getUrl());
46 |
47 | final StackTraceElement[] trace = request.getRequestStackTrace();
48 | if (trace != null) {
49 | final int end = Math.min(trace.length, stackTraceMaxDepth);
50 | if (end > 0) {
51 | for (int i = 0; i < end; ++i) {
52 | object.append("trace", (trace[i].toString()));
53 | }
54 | }
55 | }
56 | final List context = request.getRequestContext();
57 | if (context != null && !context.isEmpty()) {
58 | for (final String contextItem : context) {
59 | object.append("context", contextItem);
60 | }
61 | }
62 | } catch (final JSONException e) {
63 | LogUtil.niddlerLogError("MessageBuilder", "Failed to create json: ", e);
64 |
65 | return null;
66 | }
67 | return object;
68 | }
69 |
70 | static String buildMessage(final NiddlerResponse response) {
71 | final JSONObject object = buildMessageJson(response);
72 | if (object == null) {
73 | return null;
74 | }
75 | return object.toString();
76 | }
77 |
78 | static JSONObject buildMessageJson(final NiddlerResponse response) {
79 | if (response == null) {
80 | return null;
81 | }
82 | final JSONObject object = new JSONObject();
83 | try {
84 | object.put("type", "response");
85 | initGeneric(object, response);
86 | object.put("statusCode", response.getStatusCode());
87 | object.put("networkRequest", buildMessageJson(response.actualNetworkRequest(), 0));
88 | object.put("networkReply", buildMessageJson(response.actualNetworkReply()));
89 | object.put("writeTime", response.getWriteTime());
90 | object.put("readTime", response.getReadTime());
91 | object.put("waitTime", response.getWaitTime());
92 | object.put("httpVersion", response.getHttpVersion());
93 | object.put("statusLine", response.getStatusLine());
94 |
95 | final StackTraceElement[] trace = response.getErrorStackTrace();
96 | if (trace != null) {
97 | for (final StackTraceElement stackTraceElement : trace) {
98 | object.append("trace", (stackTraceElement.toString()));
99 | }
100 | }
101 | } catch (final JSONException e) {
102 | LogUtil.niddlerLogError("MessageBuilder", "Failed to create json: ", e);
103 |
104 | return null;
105 | }
106 | return object;
107 | }
108 |
109 | @NonNull
110 | static String buildMessage(final Niddler.NiddlerServerInfo serverInfo) {
111 | final JSONObject object = new JSONObject();
112 | try {
113 | object.put("type", "serverInfo");
114 | object.put("serverName", serverInfo.name);
115 | object.put("serverDescription", serverInfo.description);
116 | object.put("icon", serverInfo.icon);
117 | } catch (final JSONException e) {
118 | LogUtil.niddlerLogError("MessageBuilder", "Failed to create json: ", e);
119 |
120 | return "";
121 | }
122 | return object.toString();
123 | }
124 |
125 | @NonNull
126 | static String buildMessage(final ServerAuth.AuthRequest request) {
127 | final JSONObject object = new JSONObject();
128 | try {
129 | object.put("type", "authRequest");
130 | object.put("hash", request.hashKey);
131 | if (!StringUtil.isEmpty(request.packageName)) {
132 | object.put("package", request.packageName);
133 | }
134 | } catch (final JSONException e) {
135 | LogUtil.niddlerLogError("MessageBuilder", "Failed to create json: ", e);
136 |
137 | return "";
138 | }
139 | return object.toString();
140 | }
141 |
142 | @NonNull
143 | static String buildAuthSuccess() {
144 | return "{\"type\":\"authSuccess\"}";
145 | }
146 |
147 | @NonNull
148 | static String buildMessage(@NonNull final String id, @NonNull final String name,
149 | @NonNull final List blacklist) {
150 | if (blacklist.isEmpty()) {
151 | return "{\"type\":\"staticBlacklist\"}";
152 | }
153 | final JSONObject object = new JSONObject();
154 | try {
155 | object.put("type", "staticBlacklist");
156 | object.put("id", id);
157 | object.put("name", name);
158 | final JSONArray array = new JSONArray();
159 | for (final Niddler.StaticBlackListEntry blackListEntry : blacklist) {
160 | final JSONObject inner = new JSONObject();
161 | inner.put("pattern", blackListEntry.pattern());
162 | inner.put("enabled", blackListEntry.isEnabled());
163 | array.put(inner);
164 | }
165 | object.put("entries", array);
166 | } catch (final JSONException e) {
167 | LogUtil.niddlerLogError("MessageBuilder", "Failed to create json: ", e);
168 | return "";
169 | }
170 | return object.toString();
171 | }
172 |
173 | private static void initGeneric(final JSONObject object, final NiddlerMessageBase base) throws JSONException {
174 | object.put("messageId", base.getMessageId());
175 | object.put("requestId", base.getRequestId());
176 | object.put("timestamp", base.getTimestamp());
177 | object.put("headers", createHeadersObject(base));
178 | object.put("metadata", createMetadataObject(base));
179 | object.put("body", createBody(base));
180 | }
181 |
182 | private static String createBody(final NiddlerMessageBase base) {
183 | final ByteArrayOutputStream out = new ByteArrayOutputStream();
184 | try {
185 | base.writeBody(out);
186 | } catch (final IOException e) {
187 | LogUtil.niddlerLogError("MessageBuilder", "Failed to write body", e);
188 |
189 | return null;
190 | }
191 | final byte[] bytes = out.toByteArray();
192 | if (bytes == null) {
193 | return null;
194 | }
195 | return StringUtil.toString(bytes);
196 | }
197 |
198 | private static JSONObject createHeadersObject(final NiddlerMessageBase base) throws JSONException {
199 | final Map> headers = base.getHeaders();
200 | if (headers == null || headers.isEmpty()) {
201 | return null;
202 | }
203 |
204 | final JSONObject object = new JSONObject();
205 | for (final Map.Entry> headerEntry : headers.entrySet()) {
206 | final JSONArray array = new JSONArray();
207 | for (final String s : headerEntry.getValue()) {
208 | array.put(s);
209 | }
210 | object.put(headerEntry.getKey().toLowerCase(Locale.getDefault()), array);
211 | }
212 | return object;
213 | }
214 |
215 | private static JSONObject createMetadataObject(final NiddlerMessageBase base) throws JSONException {
216 | final Map headers = base.getMetadata();
217 | if (headers == null || headers.isEmpty()) {
218 | return null;
219 | }
220 |
221 | final JSONObject object = new JSONObject();
222 | for (final Map.Entry entry : headers.entrySet()) {
223 | object.put(entry.getKey(), entry.getValue());
224 | }
225 | return object;
226 | }
227 |
228 | static String buildProtocolVersionMessage() {
229 | return "{\"type\":\"protocol\",\"protocolVersion\":" + Niddler.NiddlerServerInfo.PROTOCOL_VERSION + "}";
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/niddler-base/src/main/java/com/chimerapps/niddler/core/NiddlerServer.java:
--------------------------------------------------------------------------------
1 | package com.chimerapps.niddler.core;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.annotation.Nullable;
5 |
6 | import com.chimerapps.niddler.core.debug.NiddlerDebugger;
7 | import com.chimerapps.niddler.util.LogUtil;
8 | import com.chimerapps.niddler.util.StringUtil;
9 |
10 | import org.java_websocket.WebSocket;
11 | import org.java_websocket.handshake.ClientHandshake;
12 | import org.java_websocket.server.WebSocketServer;
13 | import org.json.JSONException;
14 | import org.json.JSONObject;
15 |
16 | import java.io.IOException;
17 | import java.net.InetSocketAddress;
18 | import java.net.UnknownHostException;
19 | import java.nio.channels.NotYetConnectedException;
20 | import java.util.Iterator;
21 | import java.util.LinkedList;
22 | import java.util.List;
23 | import java.util.UUID;
24 |
25 | /**
26 | * @author Maarten Van Giel
27 | * @author Nicola Verbeeck
28 | */
29 | class NiddlerServer extends WebSocketServer {
30 |
31 | private static final String LOG_TAG = NiddlerServer.class.getSimpleName();
32 |
33 | private final String mPackageName;
34 | private final WebSocketListener mListener;
35 | private final List mConnections;
36 | private final String mPassword;
37 | private final NiddlerDebuggerImpl mNiddlerDebugger;
38 | private final NiddlerServerAnnouncementManager mServerAnnouncementManager;
39 | private final NiddlerImpl.StaticBlacklistDispatchListener mStaticBlacklistListener;
40 | private final String mTag;
41 |
42 | private NiddlerServer(final String password,
43 | final InetSocketAddress address,
44 | final String packageName,
45 | @Nullable final String icon,
46 | final WebSocketListener listener,
47 | final NiddlerImpl.StaticBlacklistDispatchListener blacklistListener,
48 | final int pid) {
49 | super(address);
50 | mPackageName = packageName;
51 | mListener = listener;
52 | mPassword = password;
53 | mConnections = new LinkedList<>();
54 | mNiddlerDebugger = new NiddlerDebuggerImpl();
55 | mServerAnnouncementManager = new NiddlerServerAnnouncementManager(packageName, this, pid);
56 | mStaticBlacklistListener = blacklistListener;
57 | mTag = UUID.randomUUID().toString().substring(0, 6);
58 |
59 | if (icon != null) {
60 | mServerAnnouncementManager.addExtension(new NiddlerServerAnnouncementManager.IconAnnouncementExtension(icon));
61 | }
62 | mServerAnnouncementManager.addExtension(new NiddlerServerAnnouncementManager.TagAnnouncementExtension(mTag));
63 | }
64 |
65 | NiddlerServer(final String password,
66 | final int port,
67 | final String packageName,
68 | @Nullable final String icon,
69 | final WebSocketListener listener,
70 | final NiddlerImpl.StaticBlacklistDispatchListener blacklistListener,
71 | final int pid) throws UnknownHostException {
72 | this(password, new InetSocketAddress(port), packageName, icon, listener, blacklistListener, pid);
73 | }
74 |
75 | @Override
76 | public void start() {
77 | mServerAnnouncementManager.stop();
78 | super.start();
79 | }
80 |
81 | @Override
82 | public void stop() throws IOException, InterruptedException {
83 | mServerAnnouncementManager.stop();
84 | super.stop();
85 | }
86 |
87 | @Override
88 | public final void onOpen(final WebSocket conn, final ClientHandshake handshake) {
89 | LogUtil.niddlerLogDebug(LOG_TAG, "New socket connection: " + handshake.getResourceDescriptor());
90 |
91 | final ServerConnection connection = new ServerConnection(conn);
92 | synchronized (mConnections) {
93 | mConnections.add(connection);
94 | }
95 | if (StringUtil.isEmpty(mPassword)) {
96 | connection.noAuth();
97 | authSuccess(conn);
98 | } else {
99 | connection.sendAuthRequest(mPackageName);
100 | }
101 | }
102 |
103 | @Override
104 | public final void onClose(final WebSocket conn, final int code, final String reason, final boolean remote) {
105 | LogUtil.niddlerLogDebug(LOG_TAG, "Connection closed: " + conn);
106 |
107 | synchronized (mConnections) {
108 | final Iterator iterator = mConnections.iterator();
109 | while (iterator.hasNext()) {
110 | final ServerConnection connection = iterator.next();
111 | mNiddlerDebugger.onConnectionClosed(connection);
112 | connection.closed();
113 | if (connection.isFor(conn)) {
114 | iterator.remove();
115 | }
116 | }
117 | }
118 | }
119 |
120 | @Override
121 | public void onStart() {
122 | mServerAnnouncementManager.stop();
123 | mServerAnnouncementManager.start();
124 | LogUtil.niddlerLogStartup("Niddler Server running on " + getPort() + " [" + mTag + "][waitingForDebugger=" + mNiddlerDebugger.isWaitingForConnection() + "]");
125 | }
126 |
127 | private static final String MESSAGE_AUTH = "authReply";
128 | private static final String MESSAGE_START_DEBUG = "startDebug";
129 | private static final String MESSAGE_END_DEBUG = "endDebug";
130 | private static final String MESSAGE_DEBUG_CONTROL = "controlDebug";
131 | private static final String MESSAGE_STATIC_BLACKLIST_UPDATE = "controlStaticBlacklist";
132 |
133 | @Override
134 | public final void onMessage(final WebSocket conn, final String message) {
135 | final ServerConnection connection = getConnection(conn);
136 | if (connection == null) {
137 | conn.close();
138 | return;
139 | }
140 |
141 | try {
142 | final JSONObject object = new JSONObject(message);
143 | final String type = object.optString("type", null);
144 | switch (type) {
145 | case MESSAGE_AUTH:
146 | if (!connection.checkAuthReply(MessageParser.parseAuthReply(object), mPassword)) {
147 | LogUtil.niddlerLogWarning(LOG_TAG, "Client sent wrong authentication code!");
148 | return;
149 | }
150 | authSuccess(conn);
151 | break;
152 | case MESSAGE_START_DEBUG:
153 | if (connection.canReceiveData()) {
154 | mNiddlerDebugger.onDebuggerAttached(connection);
155 | }
156 | break;
157 | case MESSAGE_END_DEBUG:
158 | mNiddlerDebugger.onDebuggerConnectionClosed();
159 | break;
160 | case MESSAGE_DEBUG_CONTROL:
161 | mNiddlerDebugger.onControlMessage(object, connection);
162 | break;
163 | case MESSAGE_STATIC_BLACKLIST_UPDATE:
164 | mStaticBlacklistListener.setBlacklistItemEnabled(object.getString("id"), object.getString("pattern"), object.getBoolean("enabled"));
165 | break;
166 | default:
167 | LogUtil.niddlerLogWarning(LOG_TAG, "Received unsolicited message from client: " + message);
168 | }
169 | } catch (final JSONException e) {
170 | LogUtil.niddlerLogWarning(LOG_TAG, "Received non-json message from server: " + message, e);
171 | }
172 | }
173 |
174 | private ServerConnection getConnection(final WebSocket conn) {
175 | synchronized (mConnections) {
176 | for (final ServerConnection connection : mConnections) {
177 | if (connection.isFor(conn)) {
178 | return connection;
179 | }
180 | }
181 | }
182 | return null;
183 | }
184 |
185 | private void authSuccess(final WebSocket conn) {
186 | if (mListener != null) {
187 | mListener.onConnectionOpened(conn);
188 | }
189 | }
190 |
191 | @Override
192 | public final void onError(final WebSocket conn, final Exception ex) {
193 | LogUtil.niddlerLogError(LOG_TAG, "WebSocket error", ex);
194 |
195 | final ServerConnection connection = getConnection(conn);
196 | if (connection != null) {
197 | mNiddlerDebugger.onConnectionClosed(connection);
198 | connection.closed();
199 | }
200 | }
201 |
202 | /**
203 | * Sends a String message to all sockets
204 | *
205 | * @param message the message to be sent
206 | */
207 | final synchronized void sendToAll(final String message) {
208 | synchronized (mConnections) {
209 | for (final ServerConnection connection : mConnections) {
210 | try {
211 | if (connection.canReceiveData()) {
212 | connection.send(message);
213 | }
214 | } catch (final NotYetConnectedException ignored) {
215 | //Nothing to do, wait for the connection to complete
216 | } catch (final IllegalArgumentException ignored) {
217 | LogUtil.niddlerLogError(LOG_TAG, "WebSocket error", ignored);
218 | }
219 | }
220 | }
221 | }
222 |
223 | @NonNull
224 | NiddlerDebugger debugger() {
225 | return mNiddlerDebugger;
226 | }
227 |
228 | interface WebSocketListener {
229 | void onConnectionOpened(final WebSocket conn);
230 | }
231 |
232 | }
233 |
--------------------------------------------------------------------------------
/niddler-urlconnection/src/main/java/com/chimerapps/niddler/urlconnection/DelegatingHttpsUrlConnection.java:
--------------------------------------------------------------------------------
1 | package com.chimerapps.niddler.urlconnection;
2 |
3 | import com.chimerapps.niddler.core.Niddler;
4 |
5 | import java.io.IOException;
6 | import java.io.InputStream;
7 | import java.io.OutputStream;
8 | import java.net.ProtocolException;
9 | import java.net.Proxy;
10 | import java.net.URL;
11 | import java.security.Permission;
12 | import java.security.Principal;
13 | import java.security.cert.Certificate;
14 | import java.util.List;
15 | import java.util.Map;
16 |
17 | import javax.net.ssl.HostnameVerifier;
18 | import javax.net.ssl.HttpsURLConnection;
19 | import javax.net.ssl.SSLPeerUnverifiedException;
20 | import javax.net.ssl.SSLSocketFactory;
21 |
22 | import androidx.annotation.NonNull;
23 | import androidx.annotation.RequiresApi;
24 |
25 | /**
26 | * @author Nicola Verbeeck
27 | */
28 | class DelegatingHttpsUrlConnection extends HttpsURLConnection {
29 |
30 | private final HttpsURLConnection delegate;
31 | private final DelegatingHttpUrlConnection httpDelegate;
32 |
33 | public DelegatingHttpsUrlConnection(@NonNull final URL url,
34 | @NonNull final HttpsURLConnection delegate,
35 | @NonNull final Niddler niddler,
36 | @NonNull final NiddlerUrlConnectionHandler connectionHandler) {
37 | super(url);
38 | this.delegate = delegate;
39 | this.httpDelegate = new DelegatingHttpUrlConnection(url, delegate, niddler, connectionHandler);
40 | }
41 |
42 | public DelegatingHttpsUrlConnection(@NonNull final URL url,
43 | @NonNull final Niddler niddler,
44 | @NonNull final NiddlerUrlConnectionHandler connectionHandler) throws IOException {
45 | this(url, (HttpsURLConnection) url.openConnection(), niddler, connectionHandler);
46 | }
47 |
48 | public DelegatingHttpsUrlConnection(@NonNull final URL url,
49 | @NonNull final Proxy proxy,
50 | @NonNull final Niddler niddler,
51 | @NonNull final NiddlerUrlConnectionHandler connectionHandler) throws IOException {
52 | this(url, (HttpsURLConnection) url.openConnection(proxy), niddler, connectionHandler);
53 | }
54 |
55 | @Override
56 | public String getCipherSuite() {
57 | return delegate.getCipherSuite();
58 | }
59 |
60 | @Override
61 | public Certificate[] getLocalCertificates() {
62 | return delegate.getLocalCertificates();
63 | }
64 |
65 | @Override
66 | public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException {
67 | return delegate.getServerCertificates();
68 | }
69 |
70 | @Override
71 | public void disconnect() {
72 | httpDelegate.disconnect();
73 | }
74 |
75 | @Override
76 | public boolean usingProxy() {
77 | return httpDelegate.usingProxy();
78 | }
79 |
80 | @Override
81 | public void connect() throws IOException {
82 | httpDelegate.connect();
83 | }
84 |
85 | @Override
86 | public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
87 | return delegate.getPeerPrincipal();
88 | }
89 |
90 | @Override
91 | public Principal getLocalPrincipal() {
92 | return delegate.getLocalPrincipal();
93 | }
94 |
95 | @Override
96 | public void setHostnameVerifier(final HostnameVerifier hostnameVerifier) {
97 | delegate.setHostnameVerifier(hostnameVerifier);
98 | }
99 |
100 | @Override
101 | public HostnameVerifier getHostnameVerifier() {
102 | return delegate.getHostnameVerifier();
103 | }
104 |
105 | @Override
106 | public void setSSLSocketFactory(final SSLSocketFactory sslSocketFactory) {
107 | delegate.setSSLSocketFactory(sslSocketFactory);
108 | }
109 |
110 | @Override
111 | public SSLSocketFactory getSSLSocketFactory() {
112 | return delegate.getSSLSocketFactory();
113 | }
114 |
115 | @Override
116 | public String getHeaderFieldKey(final int i) {
117 | return httpDelegate.getHeaderFieldKey(i);
118 | }
119 |
120 | @Override
121 | public void setFixedLengthStreamingMode(final int i) {
122 | httpDelegate.setFixedLengthStreamingMode(i);
123 | }
124 |
125 | @Override
126 | public void setFixedLengthStreamingMode(final long l) {
127 | httpDelegate.setFixedLengthStreamingMode(l);
128 | }
129 |
130 | @Override
131 | public void setChunkedStreamingMode(final int i) {
132 | httpDelegate.setChunkedStreamingMode(i);
133 | }
134 |
135 | @Override
136 | public String getHeaderField(final int i) {
137 | return httpDelegate.getHeaderField(i);
138 | }
139 |
140 | @Override
141 | public void setInstanceFollowRedirects(final boolean b) {
142 | httpDelegate.setInstanceFollowRedirects(b);
143 | }
144 |
145 | @Override
146 | public boolean getInstanceFollowRedirects() {
147 | return httpDelegate.getInstanceFollowRedirects();
148 | }
149 |
150 | @Override
151 | public void setRequestMethod(final String s) throws ProtocolException {
152 | httpDelegate.setRequestMethod(s);
153 | }
154 |
155 | @Override
156 | public String getRequestMethod() {
157 | return httpDelegate.getRequestMethod();
158 | }
159 |
160 | @Override
161 | public int getResponseCode() throws IOException {
162 | return httpDelegate.getResponseCode();
163 | }
164 |
165 | @Override
166 | public String getResponseMessage() throws IOException {
167 | return httpDelegate.getResponseMessage();
168 | }
169 |
170 | @Override
171 | public long getHeaderFieldDate(final String s, final long l) {
172 | return httpDelegate.getHeaderFieldDate(s, l);
173 | }
174 |
175 | @Override
176 | public Permission getPermission() throws IOException {
177 | return httpDelegate.getPermission();
178 | }
179 |
180 | @Override
181 | public InputStream getErrorStream() {
182 | return httpDelegate.getErrorStream();
183 | }
184 |
185 | @Override
186 | public void setConnectTimeout(final int i) {
187 | httpDelegate.setConnectTimeout(i);
188 | }
189 |
190 | @Override
191 | public int getConnectTimeout() {
192 | return httpDelegate.getConnectTimeout();
193 | }
194 |
195 | @Override
196 | public void setReadTimeout(final int i) {
197 | httpDelegate.setReadTimeout(i);
198 | }
199 |
200 | @Override
201 | public int getReadTimeout() {
202 | return httpDelegate.getReadTimeout();
203 | }
204 |
205 | @Override
206 | public URL getURL() {
207 | return httpDelegate.getURL();
208 | }
209 |
210 | @Override
211 | public int getContentLength() {
212 | return httpDelegate.getContentLength();
213 | }
214 |
215 | @RequiresApi(api = 24)
216 | @Override
217 | public long getContentLengthLong() {
218 | return httpDelegate.getContentLengthLong();
219 | }
220 |
221 | @Override
222 | public String getContentType() {
223 | return httpDelegate.getContentType();
224 | }
225 |
226 | @Override
227 | public String getContentEncoding() {
228 | return httpDelegate.getContentEncoding();
229 | }
230 |
231 | @Override
232 | public long getExpiration() {
233 | return httpDelegate.getExpiration();
234 | }
235 |
236 | @Override
237 | public long getDate() {
238 | return httpDelegate.getDate();
239 | }
240 |
241 | @Override
242 | public long getLastModified() {
243 | return httpDelegate.getLastModified();
244 | }
245 |
246 | @Override
247 | public String getHeaderField(final String s) {
248 | return httpDelegate.getHeaderField(s);
249 | }
250 |
251 | @Override
252 | public Map> getHeaderFields() {
253 | return httpDelegate.getHeaderFields();
254 | }
255 |
256 | @Override
257 | public int getHeaderFieldInt(final String s, final int i) {
258 | return httpDelegate.getHeaderFieldInt(s, i);
259 | }
260 |
261 | @RequiresApi(api = 24)
262 | @Override
263 | public long getHeaderFieldLong(final String s, final long l) {
264 | return httpDelegate.getHeaderFieldLong(s, l);
265 | }
266 |
267 | @Override
268 | public Object getContent() throws IOException {
269 | return httpDelegate.getContent();
270 | }
271 |
272 | @Override
273 | public Object getContent(final Class[] classes) throws IOException {
274 | return httpDelegate.getContent(classes);
275 | }
276 |
277 | @Override
278 | public InputStream getInputStream() throws IOException {
279 | return httpDelegate.getInputStream();
280 | }
281 |
282 | @Override
283 | public OutputStream getOutputStream() throws IOException {
284 | return httpDelegate.getOutputStream();
285 | }
286 |
287 | @Override
288 | public String toString() {
289 | return httpDelegate.toString();
290 | }
291 |
292 | @Override
293 | public void setDoInput(final boolean b) {
294 | httpDelegate.setDoInput(b);
295 | }
296 |
297 | @Override
298 | public boolean getDoInput() {
299 | return httpDelegate.getDoInput();
300 | }
301 |
302 | @Override
303 | public void setDoOutput(final boolean b) {
304 | httpDelegate.setDoOutput(b);
305 | }
306 |
307 | @Override
308 | public boolean getDoOutput() {
309 | return httpDelegate.getDoOutput();
310 | }
311 |
312 | @Override
313 | public void setAllowUserInteraction(final boolean b) {
314 | httpDelegate.setAllowUserInteraction(b);
315 | }
316 |
317 | @Override
318 | public boolean getAllowUserInteraction() {
319 | return httpDelegate.getAllowUserInteraction();
320 | }
321 |
322 | @Override
323 | public void setUseCaches(final boolean b) {
324 | httpDelegate.setUseCaches(b);
325 | }
326 |
327 | @Override
328 | public boolean getUseCaches() {
329 | return httpDelegate.getUseCaches();
330 | }
331 |
332 | @Override
333 | public void setIfModifiedSince(final long l) {
334 | httpDelegate.setIfModifiedSince(l);
335 | }
336 |
337 | @Override
338 | public long getIfModifiedSince() {
339 | return httpDelegate.getIfModifiedSince();
340 | }
341 |
342 | @Override
343 | public boolean getDefaultUseCaches() {
344 | return httpDelegate.getDefaultUseCaches();
345 | }
346 |
347 | @Override
348 | public void setDefaultUseCaches(final boolean b) {
349 | httpDelegate.setDefaultUseCaches(b);
350 | }
351 |
352 | @Override
353 | public void setRequestProperty(final String s, final String s1) {
354 | httpDelegate.setRequestProperty(s, s1);
355 | }
356 |
357 | @Override
358 | public void addRequestProperty(final String s, final String s1) {
359 | httpDelegate.addRequestProperty(s, s1);
360 | }
361 |
362 | @Override
363 | public String getRequestProperty(final String s) {
364 | return httpDelegate.getRequestProperty(s);
365 | }
366 |
367 | @Override
368 | public Map> getRequestProperties() {
369 | return httpDelegate.getRequestProperties();
370 | }
371 | }
372 |
--------------------------------------------------------------------------------
/niddler-urlconnection/src/main/java/com/chimerapps/niddler/urlconnection/NiddlerUrlConnectionHandler.java:
--------------------------------------------------------------------------------
1 | package com.chimerapps.niddler.urlconnection;
2 |
3 | import com.chimerapps.niddler.core.Niddler;
4 | import com.chimerapps.niddler.core.debug.NiddlerDebugger;
5 |
6 | import java.io.IOException;
7 | import java.lang.reflect.Method;
8 | import java.net.Proxy;
9 | import java.net.URL;
10 | import java.net.URLConnection;
11 | import java.net.URLStreamHandler;
12 | import java.net.URLStreamHandlerFactory;
13 | import java.util.List;
14 | import java.util.UUID;
15 | import java.util.concurrent.CopyOnWriteArrayList;
16 | import java.util.regex.Pattern;
17 |
18 | import javax.net.ssl.HttpsURLConnection;
19 |
20 | import androidx.annotation.NonNull;
21 | import androidx.annotation.Nullable;
22 |
23 | /**
24 | * Helper class for installing niddler as the URL handler for http and https connections
25 | *
26 | * @author Nicola Verbeeck
27 | */
28 | public final class NiddlerUrlConnectionHandler {
29 |
30 | @Nullable
31 | private final Niddler niddler;
32 | @NonNull
33 | private final List blacklist;
34 | @NonNull
35 | private final String id = UUID.randomUUID().toString();
36 | @Nullable
37 | private final NiddlerDebugger debugger;
38 |
39 | private NiddlerUrlConnectionHandler(@Nullable final Niddler niddler) {
40 | this.niddler = niddler;
41 | blacklist = new CopyOnWriteArrayList<>();
42 | if (niddler != null) {
43 | debugger = niddler.debugger();
44 | niddler.registerBlacklistListener(new Niddler.StaticBlacklistListener() {
45 |
46 | @NonNull
47 | @Override
48 | public String getId() {
49 | return id;
50 | }
51 |
52 | @Override
53 | public void setBlacklistItemEnabled(@NonNull final String pattern, final boolean enabled) {
54 | NiddlerUrlConnectionHandler.this.setBlacklistItemEnabled(pattern, enabled);
55 | }
56 | });
57 | } else {
58 | debugger = null;
59 | }
60 | }
61 |
62 | /**
63 | * Registers niddler to the URL handlers for HTTP and HTTPS. This method only works correctly when it is used before any other factories have been set for HTTP and HTTPs.
64 | *
65 | * Note that this method uses some reflection tricks to get the default delegates, if this fails, we use quasi-recursion
66 | * Note: If niddler is in no-op mode, this method does nothing
67 | *
68 | * @param niddler The niddler instance to use
69 | * @return An instance of the connection handler that allows some configuration to be update
70 | */
71 | @NonNull
72 | public static NiddlerUrlConnectionHandler install(@NonNull final Niddler niddler) {
73 | if (!Niddler.enabled()) {
74 | return new NiddlerUrlConnectionHandler(null);
75 | }
76 | final NiddlerUrlConnectionHandler connectionHandler = new NiddlerUrlConnectionHandler(niddler);
77 | if (!(installWithDelegatesFromURL(niddler, connectionHandler) || installWithDelegatesFromAndroid(niddler, connectionHandler))) {
78 | installUsingFactoryHacking(niddler, connectionHandler);
79 | }
80 |
81 | return connectionHandler;
82 |
83 | }
84 |
85 | /**
86 | * Registers niddler to the URL handlers for HTTP and HTTPS. This convenience method allows you to specify the handlers for http and https that will be used.
87 | *
88 | * Note: If niddler is in no-op mode, this method does nothing
89 | *
90 | * @param niddler The niddler instance to use
91 | * @param httpHandler The URLStreamHandler for http connections
92 | * @param httpsHandler The URLStreamHandler for https connections
93 | * @return An instance of the connection handler that allows some configuration to be update
94 | */
95 | public static NiddlerUrlConnectionHandler install(@NonNull final Niddler niddler, @NonNull final URLStreamHandler httpHandler, @NonNull final URLStreamHandler httpsHandler) {
96 | if (!Niddler.enabled()) {
97 | return new NiddlerUrlConnectionHandler(null);
98 | }
99 | final NiddlerUrlConnectionHandler connectionHandler = new NiddlerUrlConnectionHandler(niddler);
100 | installFactoryWithDelegates(niddler, httpHandler, httpsHandler, connectionHandler);
101 | return connectionHandler;
102 | }
103 |
104 | /**
105 | * Adds a static blacklist on the given url pattern. The pattern is interpreted as a java regex ({@link Pattern}). Items matching the blacklist are not tracked by niddler.
106 | * This blacklist is independent from any debugger blacklists
107 | *
108 | * @param urlPattern The pattern to add to the blacklist
109 | * @return This instance
110 | */
111 | @NonNull
112 | public NiddlerUrlConnectionHandler blacklist(@NonNull final String urlPattern) {
113 | if (niddler != null) {
114 | blacklist.add(new Niddler.StaticBlackListEntry(urlPattern));
115 | niddler.onStaticBlacklistChanged(id, "URLConnection", blacklist);
116 | }
117 | return this;
118 | }
119 |
120 | boolean isBlacklisted(@NonNull final CharSequence url) {
121 | for (final Niddler.StaticBlackListEntry entry : blacklist) {
122 | if (entry.matches(url)) {
123 | return true;
124 | }
125 | }
126 | if (debugger != null) {
127 | return debugger.isBlacklisted(url);
128 | }
129 | return false;
130 | }
131 |
132 | /**
133 | * Allows you to enable/disable static blacklist items based on the pattern. This only affects the static blacklist, independent from debugger blacklists
134 | *
135 | * @param pattern The pattern to enable/disable in the blacklist. If a pattern is added that does not exist yet in the blacklist, it is added
136 | * @param enabled Flag indicating if the static blacklist item should be enabled or disabled
137 | */
138 | private void setBlacklistItemEnabled(@NonNull final String pattern, final boolean enabled) {
139 | if (niddler == null) {
140 | return;
141 | }
142 | boolean modified = false;
143 | for (final Niddler.StaticBlackListEntry blackListEntry : blacklist) {
144 | if (blackListEntry.isForPattern(pattern)) {
145 | if (blackListEntry.setEnabled(enabled)) {
146 | modified = true;
147 | }
148 | }
149 | }
150 | if (!modified) {
151 | final Niddler.StaticBlackListEntry entry = new Niddler.StaticBlackListEntry(pattern);
152 | entry.setEnabled(enabled);
153 | blacklist.add(entry);
154 | }
155 | niddler.onStaticBlacklistChanged(id, "URLConnection", blacklist);
156 | }
157 |
158 | @SuppressWarnings("PrivateApi")
159 | private static boolean installWithDelegatesFromAndroid(@NonNull final Niddler niddler, @NonNull final NiddlerUrlConnectionHandler handler) {
160 | try {
161 | final URLStreamHandler httpDelegate = (URLStreamHandler) Class.forName("com.android.okhttp.HttpHandler").newInstance();
162 | final URLStreamHandler httpsDelegate = (URLStreamHandler) Class.forName("com.android.okhttp.HttpsHandler").newInstance();
163 |
164 | installFactoryWithDelegates(niddler, httpDelegate, httpsDelegate, handler);
165 |
166 | return true;
167 | } catch (final Throwable ignored) {
168 | //Ignore
169 | return false;
170 | }
171 | }
172 |
173 | private static boolean installWithDelegatesFromURL(@NonNull final Niddler niddler, @NonNull final NiddlerUrlConnectionHandler handler) {
174 | try {
175 | final Method method = URL.class.getDeclaredMethod("getURLStreamHandler", String.class);
176 | method.setAccessible(true);
177 | final URLStreamHandler httpDelegate = (URLStreamHandler) method.invoke(null, "http");
178 | final URLStreamHandler httpsDelegate = (URLStreamHandler) method.invoke(null, "https");
179 |
180 | installFactoryWithDelegates(niddler, httpDelegate, httpsDelegate, handler);
181 | return true;
182 | } catch (final Throwable ignored) {
183 | //Ignore
184 | return false;
185 | }
186 | }
187 |
188 | private static void installFactoryWithDelegates(@NonNull final Niddler niddler,
189 | @NonNull final URLStreamHandler httpDelegate,
190 | @NonNull final URLStreamHandler httpsDelegate,
191 | @NonNull final NiddlerUrlConnectionHandler handler) {
192 | URL.setURLStreamHandlerFactory(new URLStreamHandlerFactory() {
193 | @Override
194 | public URLStreamHandler createURLStreamHandler(final String protocol) {
195 | if ("http".equals(protocol)) {
196 | return new URLStreamHandler() {
197 | @Override
198 | protected URLConnection openConnection(final URL url) throws IOException {
199 | return new DelegatingHttpUrlConnection(url, URLStreamHandlerHelper.openConnection(httpDelegate, url), niddler, handler);
200 | }
201 |
202 | @Override
203 | protected URLConnection openConnection(final URL url, final Proxy proxy) throws IOException {
204 | return new DelegatingHttpUrlConnection(url, URLStreamHandlerHelper.openConnection(httpDelegate, url, proxy), niddler, handler);
205 | }
206 | };
207 | } else if ("https".equals(protocol)) {
208 | return new URLStreamHandler() {
209 | @Override
210 | protected URLConnection openConnection(final URL url) throws IOException {
211 | return new DelegatingHttpsUrlConnection(url, (HttpsURLConnection) URLStreamHandlerHelper.openConnection(httpsDelegate, url), niddler, handler);
212 | }
213 |
214 | @Override
215 | protected URLConnection openConnection(final URL url, final Proxy proxy) throws IOException {
216 | return new DelegatingHttpsUrlConnection(url, (HttpsURLConnection) URLStreamHandlerHelper.openConnection(httpsDelegate, url, proxy), niddler, handler);
217 | }
218 | };
219 | }
220 | return null;
221 | }
222 | });
223 | }
224 |
225 | private static void installUsingFactoryHacking(@NonNull final Niddler niddler, @NonNull final NiddlerUrlConnectionHandler handler) {
226 | URL.setURLStreamHandlerFactory(new URLStreamHandlerFactory() {
227 | @Override
228 | public URLStreamHandler createURLStreamHandler(final String protocol) {
229 | if ("http".equals(protocol) || "https".equals(protocol)) {
230 | final URLStreamHandlerFactory parent = this;
231 | return new URLStreamHandler() {
232 | @Override
233 | protected URLConnection openConnection(final URL url) throws IOException {
234 | synchronized (parent) {
235 | try {
236 | URL.setURLStreamHandlerFactory(null);
237 |
238 | if ("http".equals(protocol)) {
239 | return new DelegatingHttpUrlConnection(url, niddler, handler);
240 | } else {
241 | return new DelegatingHttpsUrlConnection(url, niddler, handler);
242 | }
243 | } finally {
244 | URL.setURLStreamHandlerFactory(parent);
245 | }
246 | }
247 | }
248 |
249 | @Override
250 | protected URLConnection openConnection(final URL url, final Proxy proxy) throws IOException {
251 | synchronized (parent) {
252 | try {
253 | URL.setURLStreamHandlerFactory(null);
254 |
255 | if ("http".equals(protocol)) {
256 | return new DelegatingHttpUrlConnection(url, proxy, niddler, handler);
257 | } else {
258 | return new DelegatingHttpsUrlConnection(url, proxy, niddler, handler);
259 | }
260 | } finally {
261 | URL.setURLStreamHandlerFactory(parent);
262 | }
263 | }
264 | }
265 | };
266 | }
267 | return null;
268 | }
269 | });
270 | }
271 | }
272 |
--------------------------------------------------------------------------------