From 8301b4bc5b24561b77d3381ea2e8ff8c72368669 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Tue, 17 May 2022 15:54:22 +0200 Subject: use snowflake if necessary to update invalid vpn cert. Show cert update message in UI --- .../bitmaskclient/base/fragments/EipFragment.java | 59 ++++++++++++---------- 1 file changed, 32 insertions(+), 27 deletions(-) (limited to 'app/src/main/java/se/leap/bitmaskclient/base') diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java index dfa45614..e654b18c 100644 --- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java @@ -16,6 +16,27 @@ */ package se.leap.bitmaskclient.base.fragments; +import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_NONETWORK; +import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message; +import static se.leap.bitmaskclient.base.models.Constants.ASK_TO_CANCEL_VPN; +import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START; +import static se.leap.bitmaskclient.base.models.Constants.EIP_EARLY_ROUTES; +import static se.leap.bitmaskclient.base.models.Constants.EIP_RESTART_ON_BOOT; +import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_LOG_IN; +import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_SWITCH_PROVIDER; +import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; +import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDefaultBitmask; +import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getPreferredCity; +import static se.leap.bitmaskclient.base.utils.ViewHelper.convertDimensionToPx; +import static se.leap.bitmaskclient.eip.EipSetupObserver.gatewayOrder; +import static se.leap.bitmaskclient.eip.EipSetupObserver.reconnectingWithDifferentGateway; +import static se.leap.bitmaskclient.eip.GatewaysManager.Load.UNKNOWN; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_GEOIP_JSON; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.USER_MESSAGE; + import android.app.Activity; import android.content.ComponentName; import android.content.Context; @@ -27,18 +48,15 @@ import android.graphics.ColorMatrixColorFilter; import android.os.Bundle; import android.os.IBinder; import android.os.Vibrator; -import android.text.TextUtils; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.AppCompatButton; import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatTextView; import androidx.fragment.app.DialogFragment; @@ -75,29 +93,6 @@ import se.leap.bitmaskclient.providersetup.activities.CustomProviderSetupActivit import se.leap.bitmaskclient.providersetup.activities.LoginActivity; import se.leap.bitmaskclient.providersetup.models.LeapSRPSession; -import static android.view.View.INVISIBLE; -import static android.view.View.VISIBLE; -import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_NONETWORK; -import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message; -import static se.leap.bitmaskclient.base.models.Constants.ASK_TO_CANCEL_VPN; -import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START; -import static se.leap.bitmaskclient.base.models.Constants.EIP_EARLY_ROUTES; -import static se.leap.bitmaskclient.base.models.Constants.EIP_RESTART_ON_BOOT; -import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY; -import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP; -import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_LOG_IN; -import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_SWITCH_PROVIDER; -import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; -import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDefaultBitmask; -import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getPreferredCity; -import static se.leap.bitmaskclient.base.utils.ViewHelper.convertDimensionToPx; -import static se.leap.bitmaskclient.eip.EipSetupObserver.gatewayOrder; -import static se.leap.bitmaskclient.eip.EipSetupObserver.reconnectingWithDifferentGateway; -import static se.leap.bitmaskclient.eip.GatewaysManager.Load.UNKNOWN; -import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_GEOIP_JSON; -import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE; -import static se.leap.bitmaskclient.providersetup.ProviderAPI.USER_MESSAGE; - public class EipFragment extends Fragment implements Observer { public final static String TAG = EipFragment.class.getSimpleName(); @@ -416,7 +411,16 @@ public class EipFragment extends Fragment implements Observer { } Log.d(TAG, "eip fragment eipStatus state: " + eipStatus.getState() + " - level: " + eipStatus.getLevel() + " - is reconnecting: " + eipStatus.isReconnecting()); - if (eipStatus.isConnecting()) { + if (eipStatus.isUpdatingVpnCert()) { + setMainButtonEnabled(false); + showConnectionTransitionLayout(true); + locationButton.setText(getString(R.string.eip_status_start_pending)); + locationButton.setLocationLoad(UNKNOWN); + locationButton.showBridgeIndicator(false); + locationButton.showRecommendedIndicator(false); + mainDescription.setText(null); + subDescription.setText(getString(R.string.updating_certificate_message)); + } else if (eipStatus.isConnecting()) { setMainButtonEnabled(true); showConnectionTransitionLayout(true); locationButton.setText(getString(R.string.eip_status_start_pending)); @@ -574,6 +578,7 @@ public class EipFragment extends Fragment implements Observer { } private void updateInvalidVpnCertificate() { + EipStatus.getInstance().setUpdatingVpnCert(true); ProviderAPICommand.execute(getContext(), UPDATE_INVALID_VPN_CERTIFICATE, provider); } -- cgit v1.2.3 From 217fc3e6fb4deaf7e1d194e941d7d0b159ce16ab Mon Sep 17 00:00:00 2001 From: cyBerta Date: Thu, 19 May 2022 12:53:22 +0200 Subject: allow to cancel connection attempt during vpn certificate updates, if tor is running, it will be shut down --- .../bitmaskclient/base/fragments/EipFragment.java | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) (limited to 'app/src/main/java/se/leap/bitmaskclient/base') diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java index e654b18c..fb27bcea 100644 --- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java @@ -92,6 +92,9 @@ import se.leap.bitmaskclient.providersetup.ProviderListActivity; import se.leap.bitmaskclient.providersetup.activities.CustomProviderSetupActivity; import se.leap.bitmaskclient.providersetup.activities.LoginActivity; import se.leap.bitmaskclient.providersetup.models.LeapSRPSession; +import se.leap.bitmaskclient.tor.TorServiceCommand; +import se.leap.bitmaskclient.tor.TorStatusObservable; +import se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus; public class EipFragment extends Fragment implements Observer { @@ -272,7 +275,7 @@ public class EipFragment extends Fragment implements Observer { } void handleIcon() { - if (isOpenVpnRunningWithoutNetwork() || eipStatus.isConnected() || eipStatus.isConnecting()) + if (isOpenVpnRunningWithoutNetwork() || eipStatus.isConnected() || eipStatus.isConnecting() || eipStatus.isUpdatingVpnCert()) handleSwitchOff(); else handleSwitchOn(); @@ -308,7 +311,7 @@ public class EipFragment extends Fragment implements Observer { } private void handleSwitchOff() { - if (isOpenVpnRunningWithoutNetwork() || eipStatus.isConnecting()) { + if (isOpenVpnRunningWithoutNetwork() || eipStatus.isConnecting() || eipStatus.isUpdatingVpnCert()) { askPendingStartCancellation(); } else if (eipStatus.isConnected()) { askToStopEIP(); @@ -359,7 +362,14 @@ public class EipFragment extends Fragment implements Observer { showPendingStartCancellation = true; alertDialog = alertBuilder.setTitle(activity.getString(R.string.eip_cancel_connect_title)) .setMessage(activity.getString(R.string.eip_cancel_connect_text)) - .setPositiveButton((android.R.string.yes), (dialog, which) -> stopEipIfPossible()) + .setPositiveButton((android.R.string.yes), (dialog, which) -> { + Context context = getContext(); + if (context != null && eipStatus.isUpdatingVpnCert() && + TorStatusObservable.isRunning()) { + TorServiceCommand.stopTorServiceAsync(context.getApplicationContext()); + } + stopEipIfPossible(); + }) .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> { }).setOnDismissListener(dialog -> showPendingStartCancellation = false).show(); } catch (IllegalStateException e) { @@ -412,7 +422,7 @@ public class EipFragment extends Fragment implements Observer { Log.d(TAG, "eip fragment eipStatus state: " + eipStatus.getState() + " - level: " + eipStatus.getLevel() + " - is reconnecting: " + eipStatus.isReconnecting()); if (eipStatus.isUpdatingVpnCert()) { - setMainButtonEnabled(false); + setMainButtonEnabled(true); showConnectionTransitionLayout(true); locationButton.setText(getString(R.string.eip_status_start_pending)); locationButton.setLocationLoad(UNKNOWN); @@ -578,7 +588,7 @@ public class EipFragment extends Fragment implements Observer { } private void updateInvalidVpnCertificate() { - EipStatus.getInstance().setUpdatingVpnCert(true); + eipStatus.setUpdatingVpnCert(true); ProviderAPICommand.execute(getContext(), UPDATE_INVALID_VPN_CERTIFICATE, provider); } -- cgit v1.2.3 From 6569f4c99565ace16d8a58bc5feb522c693f2194 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Thu, 19 May 2022 12:54:47 +0200 Subject: reduce log noise in error handling --- app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java | 1 - 1 file changed, 1 deletion(-) (limited to 'app/src/main/java/se/leap/bitmaskclient/base') diff --git a/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java b/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java index d01edf5d..e9a5032c 100644 --- a/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java +++ b/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java @@ -327,7 +327,6 @@ public class MainActivity extends AppCompatActivity implements EipSetupListener, JSONObject errorJson = new JSONObject(reasonToFail); newFragment = MainActivityErrorDialog.newInstance(provider, errorJson); } catch (JSONException e) { - e.printStackTrace(); newFragment = MainActivityErrorDialog.newInstance(provider, reasonToFail); } newFragment.show(fragmentTransaction, MainActivityErrorDialog.TAG); -- cgit v1.2.3 From 464b9e44cd491c75479b13448b001ed2a64dcf9c Mon Sep 17 00:00:00 2001 From: cyBerta Date: Thu, 19 May 2022 16:26:06 +0200 Subject: show in EipFragment bridge status message if bridges are used to update the VPN certificate --- .../bitmaskclient/base/fragments/EipFragment.java | 38 +++++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) (limited to 'app/src/main/java/se/leap/bitmaskclient/base') diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java index fb27bcea..c777e727 100644 --- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java @@ -17,7 +17,6 @@ package se.leap.bitmaskclient.base.fragments; import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_NONETWORK; -import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message; import static se.leap.bitmaskclient.base.models.Constants.ASK_TO_CANCEL_VPN; import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START; import static se.leap.bitmaskclient.base.models.Constants.EIP_EARLY_ROUTES; @@ -48,6 +47,10 @@ import android.graphics.ColorMatrixColorFilter; import android.os.Bundle; import android.os.IBinder; import android.os.Vibrator; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.RelativeSizeSpan; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; @@ -94,7 +97,6 @@ import se.leap.bitmaskclient.providersetup.activities.LoginActivity; import se.leap.bitmaskclient.providersetup.models.LeapSRPSession; import se.leap.bitmaskclient.tor.TorServiceCommand; import se.leap.bitmaskclient.tor.TorStatusObservable; -import se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus; public class EipFragment extends Fragment implements Observer { @@ -121,6 +123,7 @@ public class EipFragment extends Fragment implements Observer { private Unbinder unbinder; private EipStatus eipStatus; + private TorStatusObservable torStatusObservable; private GatewaysManager gatewaysManager; @@ -170,6 +173,7 @@ public class EipFragment extends Fragment implements Observer { super.onCreate(savedInstanceState); openVpnConnection = new EipFragmentServiceConnection(); eipStatus = EipStatus.getInstance(); + torStatusObservable = TorStatusObservable.getInstance(); Activity activity = getActivity(); if (activity != null) { preferences = getActivity().getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE); @@ -185,6 +189,7 @@ public class EipFragment extends Fragment implements Observer { @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { eipStatus.addObserver(this); + torStatusObservable.addObserver(this); View view = inflater.inflate(R.layout.f_eip, container, false); unbinder = ButterKnife.bind(this, view); @@ -262,6 +267,7 @@ public class EipFragment extends Fragment implements Observer { public void onDestroyView() { super.onDestroyView(); eipStatus.deleteObserver(this); + torStatusObservable.deleteObserver(this); unbinder.unbind(); } @@ -402,14 +408,20 @@ public class EipFragment extends Fragment implements Observer { public void update(Observable observable, Object data) { if (observable instanceof EipStatus) { eipStatus = (EipStatus) observable; - Activity activity = getActivity(); - if (activity != null) { - activity.runOnUiThread(this::handleNewState); - } else { - Log.e("EipFragment", "activity is null"); - } + handleNewStateOnMain(); } else if (observable instanceof ProviderObservable) { provider = ((ProviderObservable) observable).getCurrentProvider(); + } else if (observable instanceof TorStatusObservable && EipStatus.getInstance().isUpdatingVpnCert()) { + handleNewStateOnMain(); + } + } + + private void handleNewStateOnMain() { + Activity activity = getActivity(); + if (activity != null) { + activity.runOnUiThread(this::handleNewState); + } else { + Log.e("EipFragment", "activity is null"); } } @@ -429,7 +441,15 @@ public class EipFragment extends Fragment implements Observer { locationButton.showBridgeIndicator(false); locationButton.showRecommendedIndicator(false); mainDescription.setText(null); - subDescription.setText(getString(R.string.updating_certificate_message)); + String torStatus = TorStatusObservable.getStringForCurrentStatus(getContext()); + subDescription.setText(torStatus); + if (!TextUtils.isEmpty(torStatus)) { + Spannable spannable = new SpannableString(torStatus); + spannable.setSpan(new RelativeSizeSpan(0.75f), 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + subDescription.setText(TextUtils.concat(getString(R.string.updating_certificate_message) + "\n", spannable)); + } else { + subDescription.setText(getString(R.string.updating_certificate_message)); + } } else if (eipStatus.isConnecting()) { setMainButtonEnabled(true); showConnectionTransitionLayout(true); -- cgit v1.2.3 From fb38b7b60b888cc9a4caed5ce0a271b1f7d487ea Mon Sep 17 00:00:00 2001 From: cyBerta Date: Thu, 19 May 2022 16:28:05 +0200 Subject: add providerObservable as member variable to EipFragment, ensure the Fragment has always the latest provider object --- .../main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'app/src/main/java/se/leap/bitmaskclient/base') diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java index c777e727..deac214d 100644 --- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java @@ -123,6 +123,7 @@ public class EipFragment extends Fragment implements Observer { private Unbinder unbinder; private EipStatus eipStatus; + private ProviderObservable providerObservable; private TorStatusObservable torStatusObservable; private GatewaysManager gatewaysManager; @@ -173,6 +174,7 @@ public class EipFragment extends Fragment implements Observer { super.onCreate(savedInstanceState); openVpnConnection = new EipFragmentServiceConnection(); eipStatus = EipStatus.getInstance(); + providerObservable = ProviderObservable.getInstance(); torStatusObservable = TorStatusObservable.getInstance(); Activity activity = getActivity(); if (activity != null) { @@ -190,6 +192,7 @@ public class EipFragment extends Fragment implements Observer { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { eipStatus.addObserver(this); torStatusObservable.addObserver(this); + providerObservable.addObserver(this); View view = inflater.inflate(R.layout.f_eip, container, false); unbinder = ButterKnife.bind(this, view); @@ -267,6 +270,7 @@ public class EipFragment extends Fragment implements Observer { public void onDestroyView() { super.onDestroyView(); eipStatus.deleteObserver(this); + providerObservable.deleteObserver(this); torStatusObservable.deleteObserver(this); unbinder.unbind(); } -- cgit v1.2.3 From 840fc607b987825f2b362ae6838d42ef4fda5e7b Mon Sep 17 00:00:00 2001 From: cyBerta Date: Fri, 20 May 2022 13:03:25 +0200 Subject: show error message if tor failed to start during the VPN certificate update --- .../java/se/leap/bitmaskclient/base/MainActivity.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'app/src/main/java/se/leap/bitmaskclient/base') diff --git a/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java b/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java index e9a5032c..8cb12652 100644 --- a/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java +++ b/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java @@ -49,6 +49,8 @@ import se.leap.bitmaskclient.eip.EIP; import se.leap.bitmaskclient.eip.EipCommand; import se.leap.bitmaskclient.eip.EipSetupListener; import se.leap.bitmaskclient.eip.EipSetupObserver; +import se.leap.bitmaskclient.eip.EipStatus; +import se.leap.bitmaskclient.providersetup.ProviderAPI; import se.leap.bitmaskclient.providersetup.activities.LoginActivity; import se.leap.bitmaskclient.providersetup.models.LeapSRPSession; @@ -74,6 +76,9 @@ import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORID; import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS; import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE; import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.TOR_EXCEPTION; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.TOR_TIMEOUT; +import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE; import static se.leap.bitmaskclient.providersetup.ProviderAPI.USER_MESSAGE; @@ -303,6 +308,19 @@ public class MainActivity extends AppCompatActivity implements EipSetupListener, askUserToLogIn(getString(vpn_certificate_user_message)); } break; + case TOR_TIMEOUT: + case TOR_EXCEPTION: + try { + Bundle resultData = intent.getParcelableExtra(BROADCAST_RESULT_KEY); + JSONObject jsonObject = new JSONObject(resultData.getString(ProviderAPI.ERRORS)); + String initialAction = jsonObject.optString(ProviderAPI.INITIAL_ACTION); + if (UPDATE_INVALID_VPN_CERTIFICATE.equals(initialAction)) { + showMainActivityErrorDialog(getString(downloading_vpn_certificate_failed)); + } + } catch (Exception e) { + //ignore + } + break; } } -- cgit v1.2.3 From 0ebc7e3a9e84f598a0221fe64f51d0e7906ac377 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Fri, 20 May 2022 13:08:03 +0200 Subject: remove unnecessary method call during EipFragment layouting --- app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java | 1 - 1 file changed, 1 deletion(-) (limited to 'app/src/main/java/se/leap/bitmaskclient/base') diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java index deac214d..a61680a1 100644 --- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java @@ -446,7 +446,6 @@ public class EipFragment extends Fragment implements Observer { locationButton.showRecommendedIndicator(false); mainDescription.setText(null); String torStatus = TorStatusObservable.getStringForCurrentStatus(getContext()); - subDescription.setText(torStatus); if (!TextUtils.isEmpty(torStatus)) { Spannable spannable = new SpannableString(torStatus); spannable.setSpan(new RelativeSizeSpan(0.75f), 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); -- cgit v1.2.3