-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit f09b309
Showing
5 changed files
with
237 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# PyOutline: A simple CLI app to start Outline Proxy | ||
|
||
With **PyOutline** you can easily run [ShadowSocks proxy](https://en.wikipedia.org/wiki/Shadowsocks) from the [**Outline keys**](https://getoutline.me/access-keys/). | ||
|
||
## Installation | ||
|
||
**PIP** ([PyPI](https://pypi.org/project/pyoutline/)) | ||
``` | ||
pip install pyoutline | ||
``` | ||
**With clone from GitHub** | ||
``` | ||
git clone https://github.com/NonProjects/pyoutline | ||
pip install ./pyoutline | ||
``` | ||
## Requirements | ||
|
||
You will need ShadowSocks. The easiest way to install it: | ||
``` | ||
pip install https://github.com/shadowsocks/shadowsocks/archive/master.zip | ||
``` | ||
If you're on Linux then you can install it via your packet manager: | ||
``` | ||
apt install shadowsocks-libev # Debian 11 | ||
``` | ||
## Usage | ||
|
||
The "*How to use*" is pretty simple: | ||
``` | ||
pyoutline client -k "ss://YWVzLTI1Ni1nY206Y2RCSURWNDJEQ3duZklO@ak1344.free.www.outline.network:8118" | ||
``` | ||
If you want to transform Outline Key into the ShadowSocks: | ||
``` | ||
pyoutline to-shadowsocks -k "ss://YWVzLTI1Ni1nY206Y2RCSURWNDJEQ3duZklO@ak1344.free.www.outline.network:8118" | ||
# ^ ss-local -s "ak1344.free.www.outline.network" -p 8118 -k "cdBIDV42DCwnfIN" -m "aes-256-gcm" -l 53735 | ||
``` | ||
Set your own port or ask system to set the free one | ||
``` | ||
pyoutline client -p 50000 # Set port 50000, script will ask you for Key | ||
pyoutline client -r # Get a random port, script will ask you for Key | ||
``` | ||
You can also specify keys from the file. First working Key will be used: | ||
``` | ||
pyoutline client -k /home/user/outline_keys.txt | ||
``` | ||
The insides of the file with keys should be placed like this: | ||
``` | ||
ss://YWVzLTI1Ni1nY206Y2RCSURWNDJEQ3duZklO@ak1344.free.www.outline.network:8118 | ||
ss://YWVzLTI1Ni1nY206VEV6amZBWXEySWp0dW9T@ak1343.free.www.outline.network:6679 | ||
ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpHIXlCd1BXSDNWYW8=@ak1338.free.www.outline.network:810 | ||
``` |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import click | ||
|
||
from traceback import format_exception | ||
from sys import exit, argv as sys_argv | ||
from subprocess import call | ||
from pathlib import Path | ||
|
||
from tools import get_free_port, OutlineKey | ||
|
||
COLORS = [ | ||
'red','cyan','blue','green', | ||
'white','yellow','magenta', | ||
'bright_black','bright_red' | ||
] | ||
for color in COLORS: | ||
# No problem with using exec function here | ||
exec(f'{color} = lambda t: click.style(t, fg="{color}", bold=True)') | ||
|
||
@click.group() | ||
def cli(): | ||
pass | ||
|
||
def safe_cli(): | ||
try: | ||
cli() | ||
except Exception as e: | ||
e = ''.join(format_exception( | ||
etype = None, | ||
value = e, | ||
tb = e.__traceback__ | ||
)) | ||
click.echo(red(e)) | ||
exit(1) | ||
|
||
@cli.command() | ||
@click.option( | ||
'--outline-key', '-k', required=True, prompt=True, | ||
help='Outline VPN access Key (ss://...), can be path to file with keys' | ||
) | ||
@click.option( | ||
'--random-port', '-r', is_flag=True, | ||
help='Will set random listener port, otherwise 53735' | ||
) | ||
@click.option( | ||
'--port', '-p', type=click.IntRange(49152,65535), | ||
help='Listener proxy port' | ||
) | ||
def to_shadowsocks(outline_key, random_port, port): | ||
"""Will transform Outline Proxy Key/file with Keys to ShadowSocks""" | ||
|
||
keys = Path(outline_key) | ||
|
||
if keys.exists(): | ||
keys = open(keys).read().strip().split('\n') | ||
keys = [key.strip() for key in keys] | ||
else: | ||
keys = [outline_key] | ||
|
||
for key in keys: | ||
try: | ||
ok = OutlineKey(key) | ||
ss = ok.shadowsocks(random_port=random_port, port=port) | ||
click.echo(bright_black(ss + '\n')) | ||
except ValueError: | ||
click.echo(red('Invalid Key specified!')) | ||
|
||
@cli.command() | ||
@click.option( | ||
'--outline-key', '-k', required=True, prompt=True, | ||
help='Outline VPN access Key (ss://...), can be path to file with keys' | ||
) | ||
@click.option( | ||
'--random-port', '-r', is_flag=True, | ||
help='Will set random listener port, otherwise 25250' | ||
) | ||
@click.option( | ||
'--port', '-p', type=click.IntRange(49152,65535), | ||
help='Listener proxy port' | ||
) | ||
def client(outline_key, random_port, port): | ||
"""Will start Outline Proxy from Key / file with Keys""" | ||
|
||
keys = Path(outline_key) | ||
|
||
if keys.exists(): | ||
keys = open(keys).read().strip().split('\n') | ||
keys = [key.strip() for key in keys] | ||
else: | ||
keys = [outline_key] | ||
|
||
for key in keys: | ||
click.echo(yellow(f'Trying {key[:32]}...')) | ||
|
||
try: | ||
ok = OutlineKey(key) | ||
|
||
if not ok.is_alive: | ||
click.echo(red(f'{key[:32]}... is offline or not valid.\n')) | ||
continue | ||
else: | ||
click.echo(green(f'{key[:32]}... is OK! Connecting...\n')) | ||
|
||
ss = ok.shadowsocks(random_port=random_port, port=port) | ||
call(ss.replace('"','').split(' ')) | ||
|
||
except ValueError: | ||
click.echo(red('Invalid Key specified!')) | ||
except FileNotFoundError: | ||
ss = ss.replace('ss-local', 'sslocal') | ||
call(ss.replace('"','').split(' ')) | ||
except Exception as e: | ||
click.echo(red(e)) | ||
exit(1) | ||
|
||
click.echo(red('No working keys found.')) | ||
exit(1) | ||
|
||
if __name__ == '__main__': | ||
safe_cli() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
from setuptools import setup | ||
|
||
setup( | ||
name="pyoutline", | ||
version='0.1', | ||
license='MIT', | ||
description = 'A simple CLI app to start Outline Proxy', | ||
long_description = open('README.md').read(), | ||
long_description_content_type='text/markdown', | ||
author = 'NonProjects', | ||
author_email = '[email protected]', | ||
url = 'https://github.com/NonProjects/pyoutline', | ||
download_url = '', | ||
py_modules=['pyoutline', 'tools'], | ||
install_requires=['click'], | ||
entry_points=''' | ||
[console_scripts] | ||
pyoutline=pyoutline:safe_cli | ||
''', | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
from socket import socket, gaierror | ||
from base64 import urlsafe_b64decode | ||
|
||
def get_free_port(): | ||
"""This func will return free port""" | ||
with socket() as s: | ||
s.bind(('', 0)) | ||
return s.getsockname()[1] | ||
|
||
class OutlineKey: | ||
def __init__(self, key: str): | ||
self.key = key | ||
try: | ||
pass_enc = urlsafe_b64decode(key.split('ss://')[1].split('@')[0]) | ||
self.enc, self.password = pass_enc.decode().split(':') | ||
|
||
server_port = key.split('@')[1].split('#')[0].split(':') | ||
self.server, self.port = server_port | ||
except Exception as e: | ||
raise ValueError(f'Invalid Key! {e}') from None | ||
|
||
def __repr__(self) -> str: | ||
return f'OutlineKey({self.key}) # at {id(self)}' | ||
|
||
def __str__(self) -> str: | ||
return ( | ||
f"""Key: {self.key}\nEnc: {self.enc}\nPassword: {self.password}\n""" | ||
f"""Server: {self.server}\nPort: {self.port}""" | ||
) | ||
def shadowsocks(self, random_port: bool=False, port: int=None) -> str: | ||
port = get_free_port() if random_port else (port if port else 53735) | ||
return ( | ||
f"""ss-local -s "{self.server}" -p {self.port} -k """ | ||
f"""\"{self.password}" -m "{self.enc}" -l {port}""" | ||
) | ||
@property | ||
def is_alive(self): | ||
"""Will return True if addr:port is accessible""" | ||
with socket() as s: | ||
try: | ||
s.bind((self.server, int(self.port))) | ||
except gaierror: | ||
return False | ||
except OSError: | ||
return True | ||
except: | ||
return False |