Skip to content

Commit

Permalink
Commit initial files
Browse files Browse the repository at this point in the history
  • Loading branch information
NotStatilko committed Aug 9, 2022
0 parents commit f09b309
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 0 deletions.
51 changes: 51 additions & 0 deletions README.md
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 added __init__.py
Empty file.
119 changes: 119 additions & 0 deletions pyoutline.py
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()
20 changes: 20 additions & 0 deletions setup.py
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
''',
)
47 changes: 47 additions & 0 deletions tools.py
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

0 comments on commit f09b309

Please sign in to comment.