Initial commit

This commit is contained in:
BarsTiger
2022-01-23 21:38:12 +02:00
commit 001eb4b555
15 changed files with 554 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
# Client
**/__pycache__/
/Source/Client/apps/
/Source/Client/build/
/Source/Client/sources/
/Source/Client/*.spec
/Source/Client/*.bak
/Source/Client/config.cfg

33
auth.py Normal file
View File

@@ -0,0 +1,33 @@
import json
def get_auth():
with open('config.cfg') as f:
config = json.load(f)
try:
if config['auth']:
return config['auth']
else:
raise Exception('No auth found')
except:
print('[!] No auth found, please login first')
print('email')
email = input('> ')
print('password')
password = input('> ')
config['auth'] = {'email': email, 'password': password}
with open('config.cfg', 'w') as f:
json.dump(config, f)
print('[OK] Auth created')
return config['auth']
def del_auth():
with open('config.cfg') as f:
config = json.load(f)
if config['auth']:
config['auth'] = None
with open('config.cfg', 'w') as f:
json.dump(config, f)
print('[OK] Auth deleted')

1
build.bat Normal file
View File

@@ -0,0 +1 @@
pyinstaller --noconfirm --icon "icon.ico" --console --onefile horsy.py

1
build.bat.bak Normal file
View File

@@ -0,0 +1 @@
pyinstaller --nocinfirm --icon "icon.ico" --console --onefile horsy.py

5
console.py Normal file
View File

@@ -0,0 +1,5 @@
import os
def cls():
os.system('cls' if os.name == 'nt' else 'clear')

75
horsy.py Normal file
View File

@@ -0,0 +1,75 @@
import argparse
import tui as tui
from console import cls
from manager import *
from virustotal import add_to_cfg
from uploader import upload
# Getting the arguments
parser = argparse.ArgumentParser(description='horsy - the best package manager')
parser.add_argument('option', help='options for horsy (install/i | uninstall/un | source/s | update/u | list/l | '
'upload)',
choices=['install', 'i', 'uninstall', 'un', 'source', 's', 'update', 'u', 'list', 'l', 'upload'],
nargs='?')
parser.add_argument('app', help='app to install/uninstall/download source', nargs='?')
parser.add_argument('--vt', help='your virustotal api key (account -> api key in VT)', dest='vt_key')
args = parser.parse_args()
option = args.option
app = args.app
# Checking if the user has a new VT key
if args.vt_key:
if args.vt_key != 'disable':
add_to_cfg(args.vt_key)
else:
add_to_cfg(None)
# Checking directories and files
if not os.path.exists('apps'):
os.makedirs('apps')
if not os.path.exists('sources'):
os.makedirs('sources')
if not os.path.isfile('config.cfg'):
with open('config.cfg', 'w') as f:
f.write('{}')
# Displaying the logo
os.system('title horsy')
cls()
print('''
__ __ _______ ______ _______ __ __
| | | || || _ | | || | | |
| |_| || _ || | || | _____|| |_| |
| || | | || |_||_ | |_____ | |
| || |_| || __ ||_____ ||_ _|
| _ || || | | | _____| | | |
|__| |__||_______||___| |_||_______| |___|
''')
isNoArgs = False
# Checking if arguments are empty to use in-app CLI
if not args.option:
option = ['install', 'uninstall', 'source', 'update', 'list', 'upload'][tui.menu(['install app', 'uninstall app',
'get source', 'update app',
'list of installed apps',
'upload your app'])]
isNoArgs = True
if not args.app:
if option not in ['list', 'upload']:
print('\n')
app = tui.get(f'Select app to {option}')
if option in 'upload':
upload()
if option in ['install', 'i']:
install(app)
if option in ['uninstall', 'un']:
uninstall(app)
if isNoArgs:
input('[EXIT] Press enter to exit horsy...')

40
horsy.spec Normal file
View File

@@ -0,0 +1,40 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(['horsy.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='horsy',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None , icon='icon.ico')

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

138
manager.py Normal file
View File

@@ -0,0 +1,138 @@
import json
import threading
from rich import print
import requests
import vars
from tqdm import tqdm
import os
import zipfile
from virustotal import get_key, scan_file, get_report
def install(package, is_gui=False):
horsypath = os.popen('echo %HORSYPATH%').read().replace('\n', '') + '/'
r = requests.get(f"{vars.protocol}{vars.server_url}/packages/json/{package}").text
try:
r = json.loads(r)
except:
print("[red]Error with unsupported message[/]")
return
try:
if r["message"] == "not found":
print("[red]Package not found[/]")
return
if r["message"] == "Internal server error":
print("[red]Internal server error[/]")
return
except:
pass
try:
print(f"[green]App {r['name']} found, information loaded[/]")
if not os.path.exists('{1}apps/{0}'.format(r['name'], horsypath)):
os.makedirs('{1}apps/{0}'.format(r['name'], horsypath))
if not is_gui:
print(f"Downloading {r['url'].split('/')[-1]}")
chunk_size = 1024
file_r = requests.get(r['url'], stream=True)
with open('{2}apps/{0}/{1}'.format(r['name'], r['url'].split('/')[-1], horsypath), "wb") as f:
pbar = tqdm(unit="B", unit_scale=True, total=int(file_r.headers['Content-Length']))
for chunk in file_r.iter_content(chunk_size=chunk_size):
if chunk:
pbar.update(len(chunk))
f.write(chunk)
pbar.close()
print(f"Starting virustotal scan")
if not get_key():
print(f"[red]Virustotal api key not found[/]")
print(f"You can add it by entering [bold]horsy --vt \[your key][/] in terminal")
else:
print(f"[green]Virustotal api key found[/]")
print(f"[italic white]If you want to disable scan, type [/][bold]horsy --vt disable[/]"
f"[italic white] in terminal[/]")
scan_file('{2}apps/{0}/{1}'.format(r['name'], r['url'].split('/')[-1], horsypath))
print(f"[green]Virustotal scan finished[/]")
analysis = get_report('{2}apps/{0}/{1}'.format(r['name'], r['url'].split('/')[-1], horsypath))
print(f"[green]You can see report by opening: [white]{analysis['link']}[/]")
print(f"{analysis['detect']['malicious']} antivirus flagged this file as malicious")
print(f"[green][OK] Done[/]")
def unzip(file, where):
with zipfile.ZipFile(file, 'r') as zip_ref:
zip_ref.extractall(where)
print(f"[green]Extracted[/]")
if r['url'].split('.')[-1] == 'zip':
print(f"Extracting {r['url'].split('/')[-1]}")
unzip('{2}apps/{0}/{1}'.format(r['name'], r['url'].split('/')[-1], horsypath),
'{1}apps/{0}'.format(r['name'], horsypath))
if r['download']:
print(f"Found dependency")
if not is_gui:
print(f"Downloading {r['download'].split('/')[-1]}")
chunk_size = 1024
file_r = requests.get(r['download'], stream=True)
with open('{2}apps/{0}/{1}'.format(r['name'], r['download'].split('/')[-1], horsypath), "wb") as f:
pbar = tqdm(unit="B", unit_scale=True, total=int(file_r.headers['Content-Length']))
for chunk in file_r.iter_content(chunk_size=chunk_size):
if chunk:
pbar.update(len(chunk))
f.write(chunk)
pbar.close()
print(f"Starting virustotal scan")
if not get_key():
print(f"[red]Virustotal api key not found[/]")
print(f"You can add it by entering [italic white]horsy --vt \[your key][/] in terminal")
else:
print(f"[green]Virustotal api key found[/]")
scan_file('{2}apps/{0}/{1}'.format(r['name'], r['download'].split('/')[-1], horsypath))
print(f"[green]Virustotal scan finished[/]")
analysis = get_report('{2}apps/{0}/{1}'.format(r['name'], r['download'].split('/')[-1], horsypath))
print(f"[green]You can see report by opening: [white]{analysis['link']}[/]")
print(f"{analysis['detect']['malicious']} antivirus flagged this file as malicious")
if analysis['detect']['malicious'] > 0:
print(f"[red]Dependency can be malicious. It may run now, if this added to installation "
f"config[/]")
input("Press enter if you want continue, or ctrl+c to exit")
if r['install']:
print(f"Found install option")
threading.Thread(target=os.system, args=('{2}apps/{0}/{1}'.format(r['name'], r['install'], horsypath),)) \
.start()
print(f"Generating launch script")
with open('{1}apps/{0}.bat'.format(r['name'], horsypath), 'w') as f:
f.write(f"@ECHO off\n")
f.write(f"{horsypath}apps/{r['name']}/{r['run']} %*\n")
print(f"[green][OK] All done![/]")
print(f"[green]You can run your app by entering [italic white]{r['name']}[/] in terminal[/]")
except:
print("[red]Unexpected error[/]")
raise
return
def uninstall(package, is_gui=False):
horsypath = os.popen('echo %HORSYPATH%').read().replace('\n', '') + '/'
if not is_gui:
if os.path.exists('{1}apps/{0}'.format(package, horsypath)):
os.system('rmdir /s /q "{1}apps/{0}"'.format(package, horsypath))
print(f"[green][OK] Files deleted[/]")
else:
print(f"[red]App {package} is not installed or doesn't have files[/]")
if os.path.isfile('{1}apps/{0}.bat'.format(package, horsypath)):
os.remove("{1}apps/{0}.bat".format(package, horsypath))
print(f"[green][OK] Launch script deleted[/]")
else:
print(f"[red]App {package} is not installed or doesn't have launch script[/]")

31
path.py Normal file
View File

@@ -0,0 +1,31 @@
# Module for PATH actions
import os
def add_to_path(program_path: str):
import winreg
with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as root: # Get the current user's registry
with winreg.OpenKey(root, "Environment", 0, winreg.KEY_ALL_ACCESS) as key: # Open the environment key
existing_path_value = os.popen('echo %PATH%').read() # Get the existing path value
print(existing_path_value)
new_path_value = existing_path_value + ";" + program_path # Connect the new path to the existing path
winreg.SetValueEx(key, "PATH", 0, winreg.REG_EXPAND_SZ, new_path_value) # Update the path value
def delete_from_path(program_path: str):
import winreg
with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as root: # Get the current user's registry
with winreg.OpenKey(root, "Environment", 0, winreg.KEY_ALL_ACCESS) as key: # Open the environment key
existing_path_value = os.popen('echo %PATH%').read() # Get the existing path value
new_path_value = existing_path_value.replace(program_path + ";", "") # Remove the program path from path
winreg.SetValueEx(key, "PATH", 0, winreg.REG_EXPAND_SZ, new_path_value) # Update the path value
def add_var(horsy_path: str):
import winreg
with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as root: # Get the current user's registry
with winreg.OpenKey(root, "Environment", 0, winreg.KEY_ALL_ACCESS) as key: # Open the environment key
winreg.SetValueEx(key, "HORSYPATH", 0, winreg.REG_EXPAND_SZ, horsy_path) # Update the path value

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
rich
requests
Cryptography
Pyinstaller
tqdm
vt-py

19
tui.py Normal file
View File

@@ -0,0 +1,19 @@
def menu(options: list) -> int:
for i in range(len(options)):
print(str(i) + ' - ' + options[i])
user_input = None
while user_input is None:
try:
user_input = int(input('\n> '))
if user_input < 0 or user_input >= len(options):
user_input = None
print('Choose number between 0 and ' + str(len(options) - 1))
except ValueError:
print('Choose number option')
return user_input
def get(description: str) -> str:
print(description)
return input('> ')

142
uploader.py Normal file
View File

@@ -0,0 +1,142 @@
import json
import time
import requests
from rich import print
from auth import get_auth, del_auth
import re
import vars
import os
def matches(s):
return re.match("^[a-z_-]*$", s) is not None
def urlmatch(s):
return re.match("^https?://.*.(?:zip|exe)$", s) is not None
def upload():
print('Welcome to the uploader')
print('Before starting, please make sure you have done your project and [blink]uploaded[/] it to any hosting '
'service or file sharing service')
input('[OK] Press enter to continue...')
auth = get_auth()
print('Please enter the name of your project. It should contain only lowercase letters, '
'underscores and dashes')
project_name = None
while project_name is None:
project_name = input('> ')
if not matches(project_name) or len(project_name) > 64 or len(project_name) < 3:
print('[red]Invalid project name[/red]')
project_name = None
print('Please paste there project description. It should be a short text under 256 characters')
description = None
while description is None:
description = input('> ')
if len(description) > 256:
print('[red]Description is too long[/red]')
description = None
print('Please paste there url of executable file. It should be a link to exe or zip file hosted somewhere. '
'If app needs dependencies or specific launch options (python, node, etc), you can add them later')
url = None
while url is None:
url = input('> ')
if not urlmatch(url):
print('[red]Invalid file url, also it should end on .exe or .zip[/red]')
url = None
print('Please paste there url of your project on GitHub or somewhere else. It should be a link to source code '
'of your app. It can be archive, repository, site, whatever you want, optional but highly recommended.'
'If you don\'t want to add it, just press Enter')
source_url = input('> ')
source_url = None if source_url == '' else source_url
print('If your app needs any dependencies, please paste its link here. It can be exe of installer from official '
'site. If you don\'t want to add it, just press Enter')
download = None
while download is None:
download = input('> ')
if download == '':
download = None
break
if not urlmatch(download):
print('[red]Invalid download url[/red]')
download = None
print('Please add which files should be run during installation. It should be an executable file name.'
'If you don\'t want to add it, just press Enter')
install = input('> ')
install = None if install == '' else install
print('Please specify main executable command. It can be executable file name (some-file.exe) or command, that '
'launches your script (python some-file.py, etc)')
run = None
while run is None:
run = input('> ')
if run == '':
print('[red]Please, specify runtime[/red]')
run = None
request = {
'auth': auth,
'name': project_name,
'description': description,
'url': url,
'sourceUrl': source_url,
'download': download,
'install': install,
'run': run
}
# request = {
# "auth": {"email": "meshko_a@dlit.dp.ua", "password": "VeryGoodPassword"},
# "name": "testapp",
# "description": "Very good description",
# # "url": "https://github.com/Cactus-0/cabanchik/raw/main/dist/cabanchik.exe",
# "sourceUrl": "https://github.com/Cactus-0/cabanchik",
# "download": "https://www.python.org/ftp/python/3.10.2/python-3.10.2-amd64.exe",
# "install": "python-3.10.2-amd64.exe",
# "run": "cabanchik.exe"
# }
r = None
while r is None:
try:
r = requests.post(vars.protocol + vars.server_url + '/packages/new', json=request).text
r = json.loads(r)
if r['message'] == 'Unauthorized':
print('[red]Invalid credentials[/red]')
print('Deleting auth from config')
del_auth()
request['auth'] = get_auth()
print(r)
r = None
elif r['message'] == 'Internal server error':
print('[red]Internal server error, request is broken[/red]')
break
elif r['message'] == 'Invalid body':
print('[red]Invalid request body, try again[/red]')
break
elif r['message'] == 'Success':
print('[green]Success, your project is created. You can install it by running[/] '
'[i]horsy install {0}[/]'.format(request['name']))
break
else:
print('[red]Unknown error[/red]')
print('Server response:')
print(r)
break
except:
with open(f'error_{time.time()}.txt', 'w') as f:
f.write(str(r))
print(f'[red]Something went wrong with unsupported error. You can check servers response in '
f'{os.getcwd()}/{f.name}[/red]')
break

2
vars.py Normal file
View File

@@ -0,0 +1,2 @@
protocol = "http://"
server_url = 'localhost:60666'

53
virustotal.py Normal file
View File

@@ -0,0 +1,53 @@
import json
import requests
import os
import hashlib
def add_to_cfg(key):
with open('config.cfg') as f:
config = json.load(f)
config['vt-key'] = key
with open('config.cfg', 'w') as f:
json.dump(config, f)
def get_key():
with open('config.cfg') as f:
config = json.load(f)
try:
return config['vt-key']
except KeyError:
return None
def scan_file(filename):
api_url = 'https://www.virustotal.com/api/v3/files'
headers = {'x-apikey': get_key()}
with open(filename, 'rb') as file:
files = {'file': (filename, file)}
if os.path.getsize(filename) < 33554432:
response = requests.post(api_url, headers=headers, files=files)
return response.json()['data']['id']
else:
api_url = 'https://www.virustotal.com/api/v3/files/upload_url'
response = requests.get(api_url, headers=headers)
response = requests.post(response.json()['data'], headers=headers, files=files)
return response.json()['data']['id']
def get_report(filename):
hash_md5 = hashlib.md5()
with open(filename, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
api_url = 'https://www.virustotal.com/api/v3/files/' + hash_md5.hexdigest()
headers = {'x-apikey': get_key()}
response = requests.get(api_url, headers=headers)
analysis = dict()
analysis['detect'] = response.json()['data']['attributes']['last_analysis_stats']
analysis['link'] = 'https://www.virustotal.com/gui/file/' + response.json()['data']['id']
return analysis