Skip to content
This repository was archived by the owner on Apr 28, 2020. It is now read-only.

Commit e2d6d1c

Browse files
committed
add config page and approved hosts logic
1 parent b7e4c08 commit e2d6d1c

File tree

8 files changed

+374
-33
lines changed

8 files changed

+374
-33
lines changed

extension/manifest.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,18 @@
1515
"content_scripts": [
1616
{
1717
"matches": [
18-
"https://github.com/*",
19-
"https://gitlab.com/*"
18+
"https://*/*"
2019
],
2120
"js": [
2221
"out/content.js"
2322
]
2423
}
2524
],
2625
"permissions": [
27-
"nativeMessaging"
26+
"<all_urls>",
27+
"nativeMessaging",
28+
"storage",
29+
"tabs"
2830
],
2931
"icons": {
3032
"128": "logo128.png"

extension/src/background.ts

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { ExtensionMessage, WebSocketMessage } from "./common";
1+
import {
2+
ExtensionMessage,
3+
WebSocketMessage,
4+
getApprovedHosts,
5+
addApprovedHost
6+
} from "./common";
27

38
export class SailConnector {
49
private port: chrome.runtime.Port;
@@ -90,24 +95,72 @@ chrome.runtime.onMessage.addListener((data: ExtensionMessage, sender, sendRespon
9095
return;
9196
}
9297

93-
// onMessage forwards WebSocketMessages to the tab that
94-
// launched Sail.
95-
const onMessage = (message: WebSocketMessage) => {
96-
chrome.tabs.sendMessage(sender.tab.id, message);
97-
};
98-
connector.connect().then((sailUrl) => {
99-
const socketUrl = sailUrl.replace("http:", "ws:") + "/api/v1/run";
100-
return doConnection(socketUrl, data.projectUrl, onMessage).then((conn) => {
98+
// Check that the tab is an approved host, otherwise ask
99+
// the user for permission before launching Sail.
100+
const url = new URL(sender.tab.url);
101+
const host = url.hostname;
102+
getApprovedHosts()
103+
.then((hosts) => {
104+
for (let h of hosts) {
105+
if (h === host || (h.startsWith(".") && (host === h.substr(1) || host.endsWith(h)))) {
106+
// Approved host.
107+
return true;
108+
}
109+
}
110+
111+
// If not approved, ask for approval.
112+
return new Promise((resolve, reject) => {
113+
chrome.tabs.executeScript(sender.tab.id, {
114+
code: `confirm("Launch Sail? This will add this host to your approved hosts list.")`,
115+
}, (result) => {
116+
if (chrome.runtime.lastError) {
117+
return reject(chrome.runtime.lastError.message);
118+
}
119+
120+
if (result) {
121+
// The user approved the confirm dialog.
122+
addApprovedHost(host)
123+
.then(() => resolve(true))
124+
.catch(reject);
125+
return;
126+
}
127+
128+
return false;
129+
});
130+
});
131+
})
132+
.then((approved) => {
133+
if (!approved) {
134+
return;
135+
}
136+
137+
// Start Sail.
138+
// onMessage forwards WebSocketMessages to the tab that
139+
// launched Sail.
140+
const onMessage = (message: WebSocketMessage) => {
141+
chrome.tabs.sendMessage(sender.tab.id, message);
142+
};
143+
connector.connect().then((sailUrl) => {
144+
const socketUrl = sailUrl.replace("http:", "ws:") + "/api/v1/run";
145+
return doConnection(socketUrl, data.projectUrl, onMessage).then((conn) => {
146+
sendResponse({
147+
type: "sail",
148+
});
149+
});
150+
}).catch((ex) => {
151+
sendResponse({
152+
type: "sail",
153+
error: ex.toString(),
154+
});
155+
});
156+
})
157+
.catch((ex) => {
101158
sendResponse({
102159
type: "sail",
160+
error: ex.toString(),
103161
});
162+
104163
});
105-
}).catch((ex) => {
106-
sendResponse({
107-
type: "sail",
108-
error: ex.toString(),
109-
});
110-
})
111164
} else {
112165
// Check if we can get a sail URL.
113166
connector.connect().then(() => {

extension/src/common.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
1+
// approvedHostsKey is the key in extension storage used for storing the
2+
// string[] containing hosts approved by the user. For versioning purposes, the
3+
// number at the end of the key should be incremented if the method used to
4+
// store approved hosts changes.
5+
export const approvedHostsKey = "approved_hosts_0";
6+
7+
// defaultApprovedHosts is the default approved hosts list. This list should
8+
// only include GitHub.com, GitLab.com, BitBucket.com, etc.
9+
export const defaultApprovedHosts = [
10+
".github.com",
11+
".gitlab.com",
12+
//".bitbucket.com",
13+
];
14+
15+
// ExtensionMessage is used for communication within the extension.
116
export interface ExtensionMessage {
217
readonly type: "sail";
318
readonly error?: string;
419
readonly projectUrl?: string;
520
}
621

22+
// WebSocketMessage is a message from sail itself, sent over the WebSocket
23+
// connection.
724
export interface WebSocketMessage {
825
readonly type: string;
926
readonly v: any;
1027
}
1128

29+
// launchSail starts an instance of sail and instructs it to launch the
30+
// specified project URL. Terminal output will be sent to the onMessage handler.
1231
export const launchSail = (projectUrl: string, onMessage: (WebSocketMessage) => void): Promise<void> => {
1332
const listener = (message: any) => {
1433
if (message.type && message.v) {
@@ -34,6 +53,8 @@ export const launchSail = (projectUrl: string, onMessage: (WebSocketMessage) =>
3453
});
3554
};
3655

56+
// sailAvailable resolves if the native host manifest is available and allows
57+
// the extension to connect to Sail. This does not attempt a connection to Sail.
3758
export const sailAvailable = (): Promise<void> => {
3859
return new Promise<void>((resolve, reject) => {
3960
chrome.runtime.sendMessage({
@@ -49,3 +70,52 @@ export const sailAvailable = (): Promise<void> => {
4970
});
5071
});
5172
};
73+
74+
// getApprovedHosts gets the approved hosts list from storage.
75+
export const getApprovedHosts = (): Promise<string[]> => {
76+
return new Promise((resolve, reject) => {
77+
chrome.storage.sync.get(approvedHostsKey, (items) => {
78+
if (chrome.runtime.lastError) {
79+
return reject(chrome.runtime.lastError.message);
80+
}
81+
82+
if (!Array.isArray(items[approvedHostsKey])) {
83+
// No approved hosts.
84+
return resolve(defaultApprovedHosts);
85+
}
86+
87+
resolve(items[approvedHostsKey]);
88+
});
89+
});
90+
};
91+
92+
// setApprovedHosts sets the approved hosts key in storage. No validation is
93+
// performed.
94+
export const setApprovedHosts = (hosts: string[]): Promise<void> => {
95+
return new Promise((resolve, reject) => {
96+
chrome.storage.sync.set({ [approvedHostsKey]: hosts }, () => {
97+
if (chrome.runtime.lastError) {
98+
return reject(chrome.runtime.lastError.message);
99+
}
100+
101+
resolve();
102+
});
103+
});
104+
};
105+
106+
// addApprovedHost adds a single host to the approved hosts list. No validation
107+
// (except duplicate entry checking) is performed. The host is lowercased
108+
// automatically.
109+
export const addApprovedHost = async (host: string): Promise<void> => {
110+
host = host.toLowerCase();
111+
112+
// Check for duplicates.
113+
let hosts = await getApprovedHosts();
114+
if (hosts.includes(host)) {
115+
return;
116+
}
117+
118+
// Add new host and set approved hosts.
119+
hosts.push(host);
120+
await setApprovedHosts(hosts);
121+
};

extension/src/config.html

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Sail Extension</title>
6+
</head>
7+
<body>
8+
<header>
9+
<h1>Sail</h1>
10+
</header>
11+
12+
<section id="sail-available">
13+
<p id="sail-available-status">Fetching Sail URL...</p>
14+
</section>
15+
16+
<section id="approved-hosts">
17+
<h3>Approved Hosts</h3>
18+
<p>
19+
Approved hosts can start Sail without requiring you to
20+
approve it via a popup. Without this, any website could
21+
launch Sail and launch a malicious repository.
22+
</p>
23+
<p>
24+
For more information, please refer to
25+
<a href="https://github.com/cdr/sail/issues/237" target="_blank">cdr/sail#237</a>
26+
</p>
27+
28+
<table>
29+
<tbody id="approved-hosts-entries">
30+
<tr>
31+
<td>Loading entries...</td>
32+
</tr>
33+
34+
<!--
35+
<tr>
36+
<td>
37+
<input type="checkbox" class="host-checkbox">
38+
</td>
39+
<td>
40+
.host.com
41+
</td>
42+
<td>
43+
<button class="host-remove-btn">
44+
Remove
45+
</button>
46+
</td>
47+
</tr>
48+
-->
49+
</tbody>
50+
</table>
51+
52+
<div>
53+
<h4>Add an approved host:</h4>
54+
<p>
55+
If you prepend your host with a period (<code>.</code>),
56+
Sail will match all subdomains on that host as well as
57+
the host itself.
58+
</p>
59+
60+
<input id="approved-hosts-add-input" type="text" pattern="^(\.?[^\.]+)+$">
61+
<button id="approved-hosts-add">Add</button>
62+
</div>
63+
</section>
64+
65+
<script src="/out/config.js"></script>
66+
</body>
67+
</html>

0 commit comments

Comments
 (0)