Commit c8493fbb authored by Gabriel Engel's avatar Gabriel Engel Committed by GitHub

Merge pull request #153 from RocketChat/fix/validate-server-with-insecure-connection

App connects to insecure servers.
parents 70b0c82e 0d252ef3
...@@ -38,8 +38,10 @@ public class DDPClientWrapper { ...@@ -38,8 +38,10 @@ public class DDPClientWrapper {
/** /**
* Connect to WebSocket server with DDP client. * Connect to WebSocket server with DDP client.
*/ */
public Task<DDPClientCallback.Connect> connect(@Nullable String session) { public Task<DDPClientCallback.Connect> connect(@Nullable String session,
return ddpClient.connect("wss://" + hostname + "/websocket", session); boolean usesSecureConnection) {
final String protocol = usesSecureConnection ? "wss://" : "ws://";
return ddpClient.connect(protocol + hostname + "/websocket", session);
} }
/** /**
......
package chat.rocket.android.api.rest;
import android.support.annotation.NonNull;
import org.json.JSONObject;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.ResponseBody;
import rx.Emitter;
import rx.Observable;
public class DefaultServerPolicyApi implements ServerPolicyApi {
private static final String API_INFO_PATH = "/api/info";
private final OkHttpClient client;
private final String host;
public DefaultServerPolicyApi(@NonNull OkHttpClient client, @NonNull String host) {
this.client = client;
this.host = host;
}
@Override
public Observable<Response<JSONObject>> getApiInfoSecurely() {
return getApiInfo(SECURE_PROTOCOL);
}
@Override
public Observable<Response<JSONObject>> getApiInfoInsecurely() {
return getApiInfo(INSECURE_PROTOCOL);
}
private Observable<Response<JSONObject>> getApiInfo(@NonNull String protocol) {
return Observable.fromEmitter(responseEmitter -> {
final Call call = client.newCall(createRequest(protocol));
call.enqueue(getOkHttpCallback(responseEmitter, protocol));
responseEmitter.setCancellation(call::cancel);
}, Emitter.BackpressureMode.LATEST);
}
private Request createRequest(@NonNull String protocol) {
return new Request.Builder()
.url(protocol + host + API_INFO_PATH)
.get()
.build();
}
private okhttp3.Callback getOkHttpCallback(@NonNull Emitter<Response<JSONObject>> emitter,
@NonNull String protocol) {
return new okhttp3.Callback() {
@Override
public void onFailure(Call call, IOException ioException) {
emitter.onError(ioException);
}
@Override
public void onResponse(Call call, okhttp3.Response response) throws IOException {
if (!response.isSuccessful()) {
emitter.onNext(new Response<>(false, protocol, null));
emitter.onCompleted();
return;
}
final ResponseBody body = response.body();
if (body == null || body.contentLength() == 0) {
emitter.onNext(new Response<>(false, protocol, null));
emitter.onCompleted();
return;
}
try {
emitter.onNext(new Response<>(true, protocol, new JSONObject(body.string())));
} catch (Exception e) {
emitter.onNext(new Response<>(false, protocol, null));
}
emitter.onCompleted();
}
};
}
}
package chat.rocket.android.api.rest;
public class Response<T> {
private final boolean successful;
private final String protocol;
private final T data;
public Response(boolean successful, String protocol, T data) {
this.successful = successful;
this.protocol = protocol;
this.data = data;
}
public boolean isSuccessful() {
return successful;
}
public String getProtocol() {
return protocol;
}
public T getData() {
return data;
}
}
package chat.rocket.android.api.rest;
import org.json.JSONObject;
import rx.Observable;
public interface ServerPolicyApi {
String SECURE_PROTOCOL = "https://";
String INSECURE_PROTOCOL = "http://";
Observable<Response<JSONObject>> getApiInfoSecurely();
Observable<Response<JSONObject>> getApiInfoInsecurely();
}
...@@ -7,13 +7,19 @@ import org.json.JSONObject; ...@@ -7,13 +7,19 @@ import org.json.JSONObject;
import chat.rocket.android.BuildConfig; import chat.rocket.android.BuildConfig;
import chat.rocket.android.R; import chat.rocket.android.R;
import chat.rocket.android.RocketChatCache; import chat.rocket.android.RocketChatCache;
import chat.rocket.android.api.rest.DefaultServerPolicyApi;
import chat.rocket.android.api.rest.ServerPolicyApi;
import chat.rocket.android.helper.LogcatIfError; import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.helper.OkHttpHelper; import chat.rocket.android.helper.OkHttpHelper;
import chat.rocket.android.helper.ServerPolicyApiValidationHelper;
import chat.rocket.android.helper.ServerPolicyHelper; import chat.rocket.android.helper.ServerPolicyHelper;
import chat.rocket.android.helper.TextUtils; import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.ServerConfig; import chat.rocket.android.model.ServerConfig;
import chat.rocket.android.realm_helper.RealmObjectObserver; import chat.rocket.android.realm_helper.RealmObjectObserver;
import chat.rocket.android.realm_helper.RealmStore; import chat.rocket.android.realm_helper.RealmStore;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
/** /**
* Input server host. * Input server host.
...@@ -24,6 +30,8 @@ public class InputHostnameFragment extends AbstractServerConfigFragment { ...@@ -24,6 +30,8 @@ public class InputHostnameFragment extends AbstractServerConfigFragment {
realm.where(ServerConfig.class).equalTo(ServerConfig.ID, serverConfigId)) realm.where(ServerConfig.class).equalTo(ServerConfig.ID, serverConfigId))
.setOnUpdateListener(this::onRenderServerConfig); .setOnUpdateListener(this::onRenderServerConfig);
Subscription serverPolicySubscription;
public InputHostnameFragment() { public InputHostnameFragment() {
} }
...@@ -49,24 +57,38 @@ public class InputHostnameFragment extends AbstractServerConfigFragment { ...@@ -49,24 +57,38 @@ public class InputHostnameFragment extends AbstractServerConfigFragment {
private void handleConnect() { private void handleConnect() {
final String hostname = ServerPolicyHelper.enforceHostname(getHostname()); final String hostname = ServerPolicyHelper.enforceHostname(getHostname());
ServerPolicyHelper.isApiVersionValid(OkHttpHelper.getClientForUploadFile(), hostname, final ServerPolicyApi serverPolicyApi =
new ServerPolicyHelper.Callback() { new DefaultServerPolicyApi(OkHttpHelper.getClientForUploadFile(), hostname);
@Override
public void isValid() { final ServerPolicyApiValidationHelper validationHelper =
getActivity().runOnUiThread(() -> onServerValid(hostname)); new ServerPolicyApiValidationHelper(serverPolicyApi);
}
if (serverPolicySubscription != null) {
@Override serverPolicySubscription.unsubscribe();
public void isNotValid() { }
getActivity().runOnUiThread(() ->
showError(getString(R.string.input_hostname_invalid_server_message))); serverPolicySubscription = ServerPolicyHelper.isApiVersionValid(validationHelper)
} .subscribeOn(Schedulers.io())
}); .observeOn(AndroidSchedulers.mainThread())
.subscribe(
serverValidation -> {
if (serverValidation.isValid()) {
onServerValid(hostname, serverValidation.usesSecureConnection());
} else {
showError(getString(R.string.input_hostname_invalid_server_message));
}
},
throwable -> {
showError(getString(R.string.connection_error_try_later));
});
} }
@Override @Override
public void onDestroyView() { public void onDestroyView() {
serverConfigObserver.unsub(); serverConfigObserver.unsub();
if (serverPolicySubscription != null) {
serverPolicySubscription.unsubscribe();
}
super.onDestroyView(); super.onDestroyView();
} }
...@@ -76,7 +98,7 @@ public class InputHostnameFragment extends AbstractServerConfigFragment { ...@@ -76,7 +98,7 @@ public class InputHostnameFragment extends AbstractServerConfigFragment {
return TextUtils.or(TextUtils.or(editor.getText(), editor.getHint()), "").toString(); return TextUtils.or(TextUtils.or(editor.getText(), editor.getHint()), "").toString();
} }
private void onServerValid(final String hostname) { private void onServerValid(final String hostname, boolean usesSecureConnection) {
RocketChatCache.get(getContext()).edit() RocketChatCache.get(getContext()).edit()
.putString(RocketChatCache.KEY_SELECTED_SERVER_CONFIG_ID, serverConfigId) .putString(RocketChatCache.KEY_SELECTED_SERVER_CONFIG_ID, serverConfigId)
.apply(); .apply();
...@@ -87,6 +109,7 @@ public class InputHostnameFragment extends AbstractServerConfigFragment { ...@@ -87,6 +109,7 @@ public class InputHostnameFragment extends AbstractServerConfigFragment {
.put(ServerConfig.HOSTNAME, hostname) .put(ServerConfig.HOSTNAME, hostname)
.put(ServerConfig.ERROR, JSONObject.NULL) .put(ServerConfig.ERROR, JSONObject.NULL)
.put(ServerConfig.SESSION, JSONObject.NULL) .put(ServerConfig.SESSION, JSONObject.NULL)
.put(ServerConfig.SECURE_CONNECTION, usesSecureConnection)
.put(ServerConfig.STATE, ServerConfig.STATE_READY))) .put(ServerConfig.STATE, ServerConfig.STATE_READY)))
.continueWith(new LogcatIfError()); .continueWith(new LogcatIfError());
} }
......
package chat.rocket.android.helper;
import android.support.annotation.NonNull;
import chat.rocket.android.api.rest.ServerPolicyApi;
import rx.Observable;
public class ServerPolicyApiValidationHelper {
private final ServerPolicyApi serverPolicyApi;
public ServerPolicyApiValidationHelper(@NonNull ServerPolicyApi serverPolicyApi) {
this.serverPolicyApi = serverPolicyApi;
}
public Observable<ServerPolicyHelper.ServerInfo> getApiVersion() {
return serverPolicyApi.getApiInfoSecurely()
.onErrorResumeNext(serverPolicyApi.getApiInfoInsecurely())
.map(response -> new ServerPolicyHelper.ServerInfo(
response.getProtocol().equals(ServerPolicyApi.SECURE_PROTOCOL),
response.getData()
));
}
}
...@@ -4,17 +4,11 @@ import android.support.annotation.NonNull; ...@@ -4,17 +4,11 @@ import android.support.annotation.NonNull;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.IOException; import rx.Observable;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class ServerPolicyHelper { public class ServerPolicyHelper {
private static final String DEFAULT_HOST = ".rocket.chat"; private static final String DEFAULT_HOST = ".rocket.chat";
private static final String API_INFO_PATH = "/api/info";
private static final String VERSION_PROPERTY = "version"; private static final String VERSION_PROPERTY = "version";
public static String enforceHostname(String hostname) { public static String enforceHostname(String hostname) {
...@@ -25,36 +19,12 @@ public class ServerPolicyHelper { ...@@ -25,36 +19,12 @@ public class ServerPolicyHelper {
return removeTrailingSlash(removeProtocol(enforceDefaultHost(hostname))); return removeTrailingSlash(removeProtocol(enforceDefaultHost(hostname)));
} }
public static void isApiVersionValid(@NonNull OkHttpClient client, @NonNull String host, public static Observable<ServerValidation> isApiVersionValid(
@NonNull Callback callback) { @NonNull ServerPolicyApiValidationHelper serverPolicyApiValidationHelper) {
Request request; return serverPolicyApiValidationHelper.getApiVersion()
try { .map(serverInfo ->
request = new Request.Builder() new ServerValidation(isValid(serverInfo.getApiInfo()),
.url("https://" + host + API_INFO_PATH) serverInfo.usesSecureConnection()));
.get()
.build();
} catch (Exception e) {
callback.isNotValid();
return;
}
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(Call call, IOException exception) {
// some connection error
callback.isNotValid();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful() || !isValid(response.body())) {
callback.isNotValid();
return;
}
callback.isValid();
}
});
} }
@NonNull @NonNull
...@@ -80,14 +50,12 @@ public class ServerPolicyHelper { ...@@ -80,14 +50,12 @@ public class ServerPolicyHelper {
return hostname.replaceAll("/+$", ""); return hostname.replaceAll("/+$", "");
} }
private static boolean isValid(ResponseBody body) { private static boolean isValid(JSONObject jsonObject) {
if (body == null || body.contentLength() == 0) { if (jsonObject == null) {
return false; return false;
} }
try { try {
final JSONObject jsonObject = new JSONObject(body.string());
return jsonObject.has(VERSION_PROPERTY) return jsonObject.has(VERSION_PROPERTY)
&& isVersionValid(jsonObject.getString(VERSION_PROPERTY)); && isVersionValid(jsonObject.getString(VERSION_PROPERTY));
} catch (Exception e) { } catch (Exception e) {
...@@ -104,9 +72,39 @@ public class ServerPolicyHelper { ...@@ -104,9 +72,39 @@ public class ServerPolicyHelper {
return versionParts.length >= 3 && Integer.parseInt(versionParts[1]) >= 49; return versionParts.length >= 3 && Integer.parseInt(versionParts[1]) >= 49;
} }
public interface Callback { public static class ServerInfo {
void isValid(); private final boolean secureConnection;
private final JSONObject apiInfo;
public ServerInfo(boolean secureConnection, JSONObject apiInfo) {
this.secureConnection = secureConnection;
this.apiInfo = apiInfo;
}
public boolean usesSecureConnection() {
return secureConnection;
}
public JSONObject getApiInfo() {
return apiInfo;
}
}
public static class ServerValidation {
private final boolean valid;
private final boolean secureConnection;
void isNotValid(); public ServerValidation(boolean valid, boolean secureConnection) {
this.valid = valid;
this.secureConnection = secureConnection;
}
public boolean isValid() {
return valid;
}
public boolean usesSecureConnection() {
return secureConnection;
}
} }
} }
...@@ -19,6 +19,7 @@ public class ServerConfig extends RealmObject { ...@@ -19,6 +19,7 @@ public class ServerConfig extends RealmObject {
public static final String HOSTNAME = "hostname"; public static final String HOSTNAME = "hostname";
public static final String STATE = "state"; public static final String STATE = "state";
public static final String SESSION = "session"; public static final String SESSION = "session";
public static final String SECURE_CONNECTION = "secureConnection";
public static final String ERROR = "error"; public static final String ERROR = "error";
public static final int STATE_READY = 0; public static final int STATE_READY = 0;
...@@ -30,6 +31,7 @@ public class ServerConfig extends RealmObject { ...@@ -30,6 +31,7 @@ public class ServerConfig extends RealmObject {
private String hostname; private String hostname;
private int state; private int state;
private String session; private String session;
private boolean secureConnection;
private String error; private String error;
/** /**
...@@ -93,6 +95,14 @@ public class ServerConfig extends RealmObject { ...@@ -93,6 +95,14 @@ public class ServerConfig extends RealmObject {
this.session = session; this.session = session;
} }
public boolean usesSecureConnection() {
return secureConnection;
}
public void setSecureConnection(boolean usesSecureConnection) {
this.secureConnection = usesSecureConnection;
}
public String getError() { public String getError() {
return error; return error;
} }
......
...@@ -170,7 +170,7 @@ public class RocketChatWebSocketThread extends HandlerThread { ...@@ -170,7 +170,7 @@ public class RocketChatWebSocketThread extends HandlerThread {
realm.where(ServerConfig.class).equalTo(ServerConfig.ID, serverConfigId).findFirst()); realm.where(ServerConfig.class).equalTo(ServerConfig.ID, serverConfigId).findFirst());
prepareWebSocket(config.getHostname()); prepareWebSocket(config.getHostname());
return ddpClient.connect(config.getSession()) return ddpClient.connect(config.getSession(), config.usesSecureConnection())
.onSuccessTask(task -> { .onSuccessTask(task -> {
final String session = task.getResult().session; final String session = task.getResult().session;
defaultRealm.executeTransaction(realm -> defaultRealm.executeTransaction(realm ->
......
...@@ -44,6 +44,7 @@ ...@@ -44,6 +44,7 @@
<string name="video_upload_message_spec_title">Attach video</string> <string name="video_upload_message_spec_title">Attach video</string>
<string name="input_hostname_invalid_server_message">Invalid server version</string> <string name="input_hostname_invalid_server_message">Invalid server version</string>
<string name="connection_error_try_later">There\'s a connection error. Please try later.</string>
<string name="version_info_text">Version: %s</string> <string name="version_info_text">Version: %s</string>
</resources> </resources>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment