... | ... |
@@ -22,12 +22,15 @@ import signal |
22 | 22 |
import logging |
23 | 23 |
import argparse |
24 | 24 |
import asyncio |
25 |
+import ipaddress |
|
25 | 26 |
|
26 | 27 |
import structlog |
27 | 28 |
from termcolor import colored |
28 | 29 |
import aiohttp |
30 |
+import aiohttp.web |
|
29 | 31 |
|
30 | 32 |
from . import api, vpn, __version__ |
33 |
+from . import web as nord_web |
|
31 | 34 |
from ._utils import sudo_requires_password, prompt_for_sudo, LockError |
32 | 35 |
|
33 | 36 |
|
... | ... |
@@ -163,6 +166,29 @@ def parse_arguments(): |
163 | 166 |
help='Reject hosts that have a load greater ' |
164 | 167 |
'than this threshold') |
165 | 168 |
|
169 |
+ web_parser = subparsers.add_parser( |
|
170 |
+ 'web', |
|
171 |
+ help="Run nord as a web app", |
|
172 |
+ description="Serve a web app that provides a GUI for selecting the " |
|
173 |
+ "country to connect to.") |
|
174 |
+ |
|
175 |
+ web_parser.add_argument('--debug', action='store_true', |
|
176 |
+ help='Print debugging information') |
|
177 |
+ web_parser.add_argument('-u', '--username', type=str, |
|
178 |
+ required=True, |
|
179 |
+ help='NordVPN account username') |
|
180 |
+ web_parser.add_argument('-P', '--port', type=int, default=8000, |
|
181 |
+ help='Port on which to run the web app') |
|
182 |
+ web_parser.add_argument('-H', '--host', type=ipaddress.ip_address, |
|
183 |
+ default='127.0.0.1', |
|
184 |
+ help='IP address on which to run the web app') |
|
185 |
+ # methods of password entry |
|
186 |
+ passwd = web_parser.add_mutually_exclusive_group(required=True) |
|
187 |
+ passwd.add_argument('-p', '--password', type=str, |
|
188 |
+ help='NordVPN account password') |
|
189 |
+ passwd.add_argument('-f', '--password-file', type=argparse.FileType(), |
|
190 |
+ help='Path to file containing NordVPN password') |
|
191 |
+ |
|
166 | 192 |
args = parser.parse_args() |
167 | 193 |
|
168 | 194 |
if not args.command: |
... | ... |
@@ -218,6 +244,44 @@ async def connect(args): |
218 | 244 |
raise Abort(str(error)) |
219 | 245 |
|
220 | 246 |
|
247 |
+async def web(args): |
|
248 |
+ """Run nord as a web app""" |
|
249 |
+ |
|
250 |
+ username = args.username |
|
251 |
+ password = args.password or args.password_file.readline().strip() |
|
252 |
+ |
|
253 |
+ # Group requests together to reduce overall latency |
|
254 |
+ async with api.Client() as client: |
|
255 |
+ output = await asyncio.gather( |
|
256 |
+ client.valid_credentials(username, password), |
|
257 |
+ sudo_requires_password(), |
|
258 |
+ ) |
|
259 |
+ valid_credentials, require_sudo = output |
|
260 |
+ |
|
261 |
+ if not valid_credentials: |
|
262 |
+ raise Abort('invalid username/password combination') |
|
263 |
+ |
|
264 |
+ if require_sudo: |
|
265 |
+ print('sudo password required for OpenVPN') |
|
266 |
+ try: |
|
267 |
+ await prompt_for_sudo() |
|
268 |
+ except PermissionError: |
|
269 |
+ # 'sudo' will already have notified the user about the failure |
|
270 |
+ raise Abort() |
|
271 |
+ |
|
272 |
+ app = nord_web.init_app(client, (username, password)) |
|
273 |
+ runner = aiohttp.web.AppRunner(app, handle_signals=True) |
|
274 |
+ await runner.setup() |
|
275 |
+ site = aiohttp.web.TCPSite(runner, str(args.host), args.port) |
|
276 |
+ await site.start() |
|
277 |
+ print(colored(f'=== Listening {args.host}:{args.port} ===', |
|
278 |
+ color='white', attrs=['bold'])) |
|
279 |
+ try: |
|
280 |
+ await app['shutdown_signal'].wait() |
|
281 |
+ finally: |
|
282 |
+ await runner.cleanup() |
|
283 |
+ |
|
284 |
+ |
|
221 | 285 |
async def _get_host_and_config(client, args): |
222 | 286 |
# get the host |
223 | 287 |
if args.server: |