Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make proxy related permissions optional #214

Merged
merged 2 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,16 @@ local storage). It will require some manual configuration:
Mullvad Browser Extension requires the following permissions:

- `management` to be able to recommend third party extensions
- `privacy` to disable webRTC
- `proxy` to configure and use Mullvad proxy servers
- `privacy` to disable webRTC and check HTTPS-Only status
- `storage` to save preferences
- `search` to recommend other search engines
- `tabs` to be able to show proxy settings based on the active tab
- `*://*.mullvad.net/*` to get proxy servers list and display your connection information (See
`Network requests` for details)

The following permissions are optional, but are needed to use the proxy feature:

- `proxy` to configure and use Mullvad proxy servers
- `tabs` to show proxy settings from active tab
- `<all_urls>` to have granular proxy settings

_Permissions are automatically accepted when testing the extension._
Expand Down
16 changes: 8 additions & 8 deletions src/background/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { addExtListeners } from '@/helpers/extensions';
import { initBrowserAction } from '@/helpers/browserAction';
import { initProxyRequests } from '@/helpers/socksProxy';
import { addExtensionsListeners } from '@/helpers/extensions';
import { cleanProxyListeners, initProxyListeners } from '@/helpers/socksProxy';

// only on dev mode
if (import.meta.hot) {
Expand All @@ -9,10 +8,11 @@ if (import.meta.hot) {
}

// Add listeners on extension actions
addExtListeners();
addExtensionsListeners();

// Update browserAction for tabs and add listeners
initBrowserAction();
// Init proxy listeners
initProxyListeners();

// Add listener for proxy requests
initProxyRequests();
// Listeners for permissions changes
browser.permissions.onAdded.addListener(initProxyListeners);
browser.permissions.onRemoved.addListener(cleanProxyListeners);
14 changes: 9 additions & 5 deletions src/composables/useActiveTab.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { ref } from 'vue';
import useProxyPermissions from '@/composables/useProxyPermissions';

const { proxyPermissionsGranted } = useProxyPermissions();
const activeTabHost = ref('');
const isAboutPage = ref(false);

const getActiveTab = async () => {
const activeWindow = await browser.windows.getCurrent({ populate: true });
const activeTab = activeWindow.tabs!.find((tab) => tab.active);
if (proxyPermissionsGranted.value) {
const activeWindow = await browser.windows.getCurrent({ populate: true });
const activeTab = activeWindow.tabs!.find((tab) => tab.active);

const activeTabURL = new URL(activeTab!.url!);
activeTabHost.value = activeTabURL.hostname;
isAboutPage.value = activeTabURL.protocol === 'about:';
const activeTabURL = new URL(activeTab!.url!);
activeTabHost.value = activeTabURL.hostname;
isAboutPage.value = activeTabURL.protocol === 'about:';
}
};

const useActiveTab = () => {
Expand Down
20 changes: 20 additions & 0 deletions src/composables/useProxyPermissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ref } from 'vue';
import { getProxyPermissions, requestProxyPermissions } from '@/helpers/permissions';

const useProxyPermissions = () => {
const proxyPermissionsGranted = ref(false);

const checkProxyPermissions = async () => {
proxyPermissionsGranted.value = await getProxyPermissions();
};

const triggerProxyPermissions = async () => {
proxyPermissionsGranted.value = await requestProxyPermissions();
};

checkProxyPermissions();

return { proxyPermissionsGranted, triggerProxyPermissions };
};

export default useProxyPermissions;
2 changes: 1 addition & 1 deletion src/helpers/browserAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const updateTabsProxyBadges = async () => {
}
};

const updatedTabListener = async (
export const updatedTabListener = async (
_tabId: number,
_changeInfo: browser.tabs._OnUpdatedChangeInfo,
tab: browser.tabs.Tab,
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const { updateRecommendation } = useRecommendations();

type ExtensionInfo = browser.management.ExtensionInfo;

export const addExtListeners = () => {
export const addExtensionsListeners = () => {
management.onInstalled.addListener(onInstall);
management.onUninstalled.addListener(onUninstall);
management.onEnabled.addListener(onEnable);
Expand Down
13 changes: 13 additions & 0 deletions src/helpers/permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const getProxyPermissions = async () => {
return await browser.permissions.contains({
permissions: ['proxy', 'tabs'],
origins: ['<all_urls>'],
});
};

export const requestProxyPermissions = async () => {
return await browser.permissions.request({
permissions: ['proxy', 'tabs'],
origins: ['<all_urls>'],
});
};
31 changes: 30 additions & 1 deletion src/helpers/socksProxy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { RequestDetails, ProxyDetails } from './socksProxy.types';
import ipaddr from 'ipaddr.js';

import { RequestDetails, ProxyDetails } from './socksProxy.types';
import { getProxyPermissions } from './permissions';
import { initBrowserAction, updatedTabListener } from './browserAction';

const getGlobalProxyDetails = async (): Promise<ProxyDetails> => {
const response = await browser.storage.local.get('globalProxyDetails');

Expand Down Expand Up @@ -39,6 +42,32 @@ export const initProxyRequests = () => {
browser.proxy.onRequest.addListener(handleProxyRequest, { urls: ['<all_urls>'] });
};

export const initProxyListeners = async () => {
const proxyPermissionsGranted = await getProxyPermissions();
if (proxyPermissionsGranted) {
await removeProxyListeners();
await addProxyListeners();
}
};

export const cleanProxyListeners = async () => {
const proxyPermissionsGranted = await getProxyPermissions();

if (!proxyPermissionsGranted) {
await removeProxyListeners();
}
};

const addProxyListeners = async () => {
initBrowserAction();
initProxyRequests();
};

const removeProxyListeners = async () => {
browser.tabs.onUpdated.removeListener(updatedTabListener);
browser.proxy.onRequest.removeListener(handleProxyRequest);
};

// TODO decide what how to handle fallback proxy (if proxy is invalid, it will fallback to Firefox proxy if configured)
// https://bugzilla.mozilla.org/show_bug.cgi?id=1750561

Expand Down
12 changes: 2 additions & 10 deletions src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,8 @@ export async function getManifest() {
'48': './assets/mullvad-logo.svg',
'96': './assets/mullvad-logo.svg',
},
permissions: [
'management',
'privacy',
'proxy',
'search',
'storage',
'tabs',
'*://*.mullvad.net/*',
'<all_urls>',
],
permissions: ['management', 'privacy', 'search', 'storage', '*://*.mullvad.net/*'],
optional_permissions: ['proxy', 'tabs', '<all_urls>'],
browser_specific_settings: {
gecko: {
strict_min_version: '91.1.0',
Expand Down
15 changes: 11 additions & 4 deletions src/popup/views/Home.vue
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
<script lang="ts" setup>
import { computed, inject } from 'vue';
import { inject, onMounted, ref } from 'vue';

import ConnectionDetails from '@/components/ConnectionDetails/ConnectionDetails.vue';
import IconLabel from '@/components/IconLabel.vue';
import NotificationsCarousel from '@/components/NotificationsCarousel.vue';

import useActiveTab from '@/composables/useActiveTab';
import { ConnectionKey, defaultConnection } from '@/composables/useConnection';
import useProxyPermissions from '@/composables/useProxyPermissions';
import useSocksProxy from '@/composables/useSocksProxy';
import useStore from '@/composables/useStore';

const { proxyPermissionsGranted } = useProxyPermissions();
const { activeTabHost } = useActiveTab();
const { currentHostProxyDetails, currentHostProxyEnabled } = useSocksProxy();
const { excludedHosts } = useStore();
const { isLoading, isError, connection } = inject(ConnectionKey, defaultConnection);

const currentHostExcluded = computed(() => excludedHosts.value.includes(activeTabHost.value));
const currentHostExcluded = ref(false);

onMounted(async () => {
if (proxyPermissionsGranted.value) {
currentHostExcluded.value = excludedHosts.value.includes(activeTabHost.value);
}
});
</script>

<template>
<NotificationsCarousel v-if="!isLoading && !isError" />
<ConnectionDetails />

<div v-if="!isLoading">
<div v-if="!isLoading && proxyPermissionsGranted">
<IconLabel v-if="currentHostExcluded" type="info" class="my-2">
<strong>{{ activeTabHost }}</strong> is set to never be proxied
</IconLabel>
Expand Down
43 changes: 34 additions & 9 deletions src/popup/views/Proxy.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
<script lang="ts" setup>
import { computed, inject } from 'vue';
import { NCard } from 'naive-ui';

import Button from '@/components/Buttons/Button.vue';
import IconLabel from '@/components/IconLabel.vue';
import LocationDrawer from '@/components/ConnectionDetails/LocationDrawer.vue';
import ProxyGlobal from '@/components/Proxy/ProxyGlobal.vue';
import ProxyHost from '@/components/Proxy/ProxyHost.vue';
import TitleCategory from '@/components/TitleCategory.vue';

import useActiveTab from '@/composables/useActiveTab';
import { ConnectionKey, defaultConnection } from '@/composables/useConnection';
import useProxyPermissions from '@/composables/useProxyPermissions';

const { proxyPermissionsGranted, triggerProxyPermissions } = useProxyPermissions();
const { isAboutPage } = useActiveTab();
const { connection } = inject(ConnectionKey, defaultConnection);

Expand All @@ -20,13 +25,33 @@ const isWireGuard = computed(
</script>

<template>
<IconLabel v-if="!isWireGuard" type="warning" class="my-2">
Connect first to Mullvad VPN (WireGuard) to use the proxy.
</IconLabel>

<div>
<ProxyHost v-if="!isAboutPage" />
<ProxyGlobal />
<LocationDrawer />
</div>
<template v-if="proxyPermissionsGranted">
<IconLabel v-if="!isWireGuard" type="warning" class="my-2">
Connect first to Mullvad VPN (WireGuard) to use the proxy.
</IconLabel>

<div>
<ProxyHost v-if="!isAboutPage" />
<ProxyGlobal />
<LocationDrawer />
</div>
</template>

<template v-else>
<n-card :bordered="false" class="mb-4">
<div class="flex justify-between">
<TitleCategory title="Permissions required" />
</div>

<IconLabel type="warning" class="my-2">
<ul>
<li>- <strong>tabs</strong> to show proxy settings from the active tab</li>
<li>- <strong>proxy</strong> to configure and use Mullvad proxy servers</li>
<li>- <strong>&lt;all_urls&gt;</strong> to have granular proxy settings</li>
</ul>
</IconLabel>

<Button class="mt-3" @click="triggerProxyPermissions"> Grant permissions </Button>
</n-card>
</template>
</template>
Loading