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 {
/**
* Connect to WebSocket server with DDP client.
*/
public Task<DDPClientCallback.Connect> connect(@Nullable String session) {
return ddpClient.connect("wss://" + hostname + "/websocket", session);
public Task<DDPClientCallback.Connect> connect(@Nullable String 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;
import chat.rocket.android.BuildConfig;
import chat.rocket.android.R;
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.OkHttpHelper;
import chat.rocket.android.helper.ServerPolicyApiValidationHelper;
import chat.rocket.android.helper.ServerPolicyHelper;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.ServerConfig;
import chat.rocket.android.realm_helper.RealmObjectObserver;
import chat.rocket.android.realm_helper.RealmStore;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
/**
* Input server host.
......@@ -24,6 +30,8 @@ public class InputHostnameFragment extends AbstractServerConfigFragment {
realm.where(ServerConfig.class).equalTo(ServerConfig.ID, serverConfigId))
.setOnUpdateListener(this::onRenderServerConfig);
Subscription serverPolicySubscription;
public InputHostnameFragment() {
}
......@@ -49,24 +57,38 @@ public class InputHostnameFragment extends AbstractServerConfigFragment {
private void handleConnect() {
final String hostname = ServerPolicyHelper.enforceHostname(getHostname());
ServerPolicyHelper.isApiVersionValid(OkHttpHelper.getClientForUploadFile(), hostname,
new ServerPolicyHelper.Callback() {
@Override
public void isValid() {
getActivity().runOnUiThread(() -> onServerValid(hostname));
}
@Override
public void isNotValid() {
getActivity().runOnUiThread(() ->
showError(getString(R.string.input_hostname_invalid_server_message)));
}
});
final ServerPolicyApi serverPolicyApi =
new DefaultServerPolicyApi(OkHttpHelper.getClientForUploadFile(), hostname);
final ServerPolicyApiValidationHelper validationHelper =
new ServerPolicyApiValidationHelper(serverPolicyApi);
if (serverPolicySubscription != null) {
serverPolicySubscription.unsubscribe();
}
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
public void onDestroyView() {
serverConfigObserver.unsub();
if (serverPolicySubscription != null) {
serverPolicySubscription.unsubscribe();
}
super.onDestroyView();
}
......@@ -76,7 +98,7 @@ public class InputHostnameFragment extends AbstractServerConfigFragment {
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()
.putString(RocketChatCache.KEY_SELECTED_SERVER_CONFIG_ID, serverConfigId)
.apply();
......@@ -87,6 +109,7 @@ public class InputHostnameFragment extends AbstractServerConfigFragment {
.put(ServerConfig.HOSTNAME, hostname)
.put(ServerConfig.ERROR, JSONObject.NULL)
.put(ServerConfig.SESSION, JSONObject.NULL)
.put(ServerConfig.SECURE_CONNECTION, usesSecureConnection)
.put(ServerConfig.STATE, ServerConfig.STATE_READY)))
.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;
import org.json.JSONObject;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import rx.Observable;
public class ServerPolicyHelper {
private static final String DEFAULT_HOST = ".rocket.chat";
private static final String API_INFO_PATH = "/api/info";
private static final String VERSION_PROPERTY = "version";
public static String enforceHostname(String hostname) {
......@@ -25,36 +19,12 @@ public class ServerPolicyHelper {
return removeTrailingSlash(removeProtocol(enforceDefaultHost(hostname)));
}
public static void isApiVersionValid(@NonNull OkHttpClient client, @NonNull String host,
@NonNull Callback callback) {
Request request;
try {
request = new Request.Builder()
.url("https://" + host + API_INFO_PATH)
.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();
}
});
public static Observable<ServerValidation> isApiVersionValid(
@NonNull ServerPolicyApiValidationHelper serverPolicyApiValidationHelper) {
return serverPolicyApiValidationHelper.getApiVersion()
.map(serverInfo ->
new ServerValidation(isValid(serverInfo.getApiInfo()),
serverInfo.usesSecureConnection()));
}
@NonNull
......@@ -80,14 +50,12 @@ public class ServerPolicyHelper {
return hostname.replaceAll("/+$", "");
}
private static boolean isValid(ResponseBody body) {
if (body == null || body.contentLength() == 0) {
private static boolean isValid(JSONObject jsonObject) {
if (jsonObject == null) {
return false;
}
try {
final JSONObject jsonObject = new JSONObject(body.string());
return jsonObject.has(VERSION_PROPERTY)
&& isVersionValid(jsonObject.getString(VERSION_PROPERTY));
} catch (Exception e) {
......@@ -104,9 +72,39 @@ public class ServerPolicyHelper {
return versionParts.length >= 3 && Integer.parseInt(versionParts[1]) >= 49;
}
public interface Callback {
void isValid();
public static class ServerInfo {
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 {
public static final String HOSTNAME = "hostname";
public static final String STATE = "state";
public static final String SESSION = "session";
public static final String SECURE_CONNECTION = "secureConnection";
public static final String ERROR = "error";
public static final int STATE_READY = 0;
......@@ -30,6 +31,7 @@ public class ServerConfig extends RealmObject {
private String hostname;
private int state;
private String session;
private boolean secureConnection;
private String error;
/**
......@@ -93,6 +95,14 @@ public class ServerConfig extends RealmObject {
this.session = session;
}
public boolean usesSecureConnection() {
return secureConnection;
}
public void setSecureConnection(boolean usesSecureConnection) {
this.secureConnection = usesSecureConnection;
}
public String getError() {
return error;
}
......
......@@ -170,7 +170,7 @@ public class RocketChatWebSocketThread extends HandlerThread {
realm.where(ServerConfig.class).equalTo(ServerConfig.ID, serverConfigId).findFirst());
prepareWebSocket(config.getHostname());
return ddpClient.connect(config.getSession())
return ddpClient.connect(config.getSession(), config.usesSecureConnection())
.onSuccessTask(task -> {
final String session = task.getResult().session;
defaultRealm.executeTransaction(realm ->
......
......@@ -44,6 +44,7 @@
<string name="video_upload_message_spec_title">Attach video</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>
</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