Skip to content

Commit

Permalink
add github actions build
Browse files Browse the repository at this point in the history
  • Loading branch information
mdminhazulhaque committed Nov 21, 2022
1 parent 4134560 commit c08ca5b
Show file tree
Hide file tree
Showing 13 changed files with 8,693 additions and 76 deletions.
87 changes: 87 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Build

on:
push:
tags:
- 'v*'

jobs:
upload-release:

runs-on: ubuntu-18.04
needs:
- build-linux
- build-windows

steps:
- uses: actions/checkout@v1
- name: create release
id: create_release
uses: actions/create-release@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
- name: download artifacts
uses: actions/download-artifact@v1
with:
name: uploads
- name: upload linux
id: upload-linux
uses: actions/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./uploads/app-linux.bin
asset_name: ssh-tunnel-manager-linux-${{ github.ref_name }}
asset_content_type: application/x-executable
- name: upload windows
id: upload-windows
uses: actions/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./uploads/app-windows.exe
asset_name: ssh-tunnel-manager-windows-${{ github.ref_name }}.exe
asset_content_type: application/x-dosexec

build-linux:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v4
with:
python-version: 3.8.10
- name: build executable
run: |
pip install -r requirements.txt
pip install pyinstaller
pyinstaller --onefile --noconsole --name app-linux.bin app.py
- name: upload artifact
uses: actions/upload-artifact@v1
with:
name: uploads
path: dist/app-linux.bin

build-windows:
runs-on: windows-2019
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v4
with:
python-version: 3.8.10
- name: build executable
run: |
pip install -r requirements.txt
pip install pyinstaller
pyinstaller --onefile --noconsole --name app-windows.exe app.py
- name: upload artifact
uses: actions/upload-artifact@v1
with:
name: uploads
path: dist/app-windows.exe
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
__pycache__
build
dist
app.spec
.env
config.yml
config.yml-*
.directory
Expand Down
Binary file added .screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# SSH Tunnel Manager

A PyQt GUI to manage SSH tunnels
A cross-platform, PyQt GUI to manage SSH tunnels

![SSH Tunnel Manager](screenshot.png)
![SSH Tunnel Manager](.screenshot.png)

## Usage
## Installation (Standalone)

* Install dependencies: `pip3 install PyQt5 urllib yaml`
You can download the standalone executable from the [Release](https://github.com/mdminhazulhaque/ssh-tunnel-manager/releases) section.

## Installation (From Source)

* Install dependencies: `pip install -r requirements.txt`
* Create a config: `cp config.example.yml config.yml`
* Run the app: `python3 app.py`
* You can modify `sshtunnelmgr.desktop` and put in `~/.local/share/application` to create a app menu shortcut
Expand All @@ -33,11 +37,19 @@ The key `browser_open` is optional. If provided, it will open the provided URL i
The application saves the tunnel information into a `dict` and can `kill` it when the `Stop` button is clicked.
> WARNING: To allow SSH to bind on privileged ports, run `sudo setcap CAP_NET_BIND_SERVICE=+eip /usr/bin/ssh`
## SSH bind on Privileged Ports
Binding on privileged ports will fail unless the user/program has administrative access.
For Linux/macOS, run the following command to allow SSH program to allow binding on privileged ports.
```bash
sudo setcap CAP_NET_BIND_SERVICE=+eip /usr/bin/ssh
```

## Icons

If you put image files (png/jpg/bmp) in `./icons/` with the same filename as the `name` of tunnel, it will appear as icon for that specific entry.
If you put image files (png/jpg/bmp) in `./icons/` with the same filename as the `name` field of tunnel configuration, it will appear as icon for that specific entry.

For example, the tunnel identifier is `kubernetes`, so `./icons/kubernetes.png` will be set as the form's icon.

Expand Down
123 changes: 53 additions & 70 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
from PyQt5.QtWidgets import QWidget, QLabel, QLineEdit, QPushButton, QApplication, QGridLayout, QDialog, QMessageBox
from PyQt5.QtCore import QProcess, Qt, QUrl, QSharedMemory
from PyQt5.QtGui import QIcon, QDesktopServices, QPixmap
from PyQt5 import uic

from urllib.parse import urlparse

import sys
import yaml
Expand All @@ -18,97 +19,76 @@
import os
import requests

from urllib.parse import urlparse

CONF_FILE = "config.yml"

class LANG:
TITLE = "SSH Tunnel Manager"
START = "Start"
STOP = "Stop"
ADD = "Add"
CLOSE = "Close"
QSS_START = "QPushButton { background-color: #34A853; }"
QSS_STOP = "QPushButton { background-color: #EA4335; }"
QSS_OPEN = "QPushButton { background-color: #4285F4; }"
REMOTE_ADDRESS = "remote_address"
LOCAL_PORT = "local_port"
PROXY_HOST = "proxy_host"
BROWSER_OPEN = "browser_open"
ICON = "icon"
ICON_WINDOW = "icons/settings.png"
ICON_START = "icons/start.png"
ICON_STOP = "icons/stop.png"
ICON_SSH_KILL = "icons/kill.png"
SSH = "ssh"
HEADER_NAME = "Name"
HEADER_LOCAL_ADDRESS = "Local Address"
HEADER_REMOTE_ADDRESS = "Remote Address"
HEADER_JUMP_HOST = "Proxy Host"
HEADER_ACTION = "Action"
SSH_KILL = "killall ssh"
from tunnel import Ui_Tunnel
from tunnelconfig import Ui_TunnelConfig
from vars import CONF_FILE, LANG, KEYS, ICONS, CMDS
import icons

class TunnelConfig(QDialog):
def __init__(self, parent, data):
super(TunnelConfig, self).__init__(parent)
uic.loadUi("tunnelconfig.ui", self)

self.remote_address.setText(data.get(LANG.REMOTE_ADDRESS))
self.proxy_host.setText(data.get(LANG.PROXY_HOST))
self.browser_open.setText(data.get(LANG.BROWSER_OPEN))
self.local_port.setValue(data.get(LANG.LOCAL_PORT))
self.ui = Ui_TunnelConfig()
self.ui.setupUi(self)

self.remote_address.textChanged.connect(self.render_ssh_command)
self.proxy_host.textChanged.connect(self.render_ssh_command)
self.local_port.valueChanged.connect(self.render_ssh_command)
self.copy.clicked.connect(self.do_copy_ssh_command)
self.ui.remote_address.setText(data.get(KEYS.REMOTE_ADDRESS))
self.ui.proxy_host.setText(data.get(KEYS.PROXY_HOST))
self.ui.browser_open.setText(data.get(KEYS.BROWSER_OPEN))
self.ui.local_port.setValue(data.get(KEYS.LOCAL_PORT))

self.ui.remote_address.textChanged.connect(self.render_ssh_command)
self.ui.proxy_host.textChanged.connect(self.render_ssh_command)
self.ui.local_port.valueChanged.connect(self.render_ssh_command)
self.ui.copy.clicked.connect(self.do_copy_ssh_command)

self.render_ssh_command()

def render_ssh_command(self):
ssh_command = F"ssh -L 127.0.0.1:{self.local_port.value()}:{self.remote_address.text()} {self.proxy_host.text()}"
self.ssh_command.setText(ssh_command)
ssh_command = F"ssh -L 127.0.0.1:{self.ui.local_port.value()}:{self.ui.remote_address.text()} {self.ui.proxy_host.text()}"
self.ui.ssh_command.setText(ssh_command)

def do_copy_ssh_command(self):
cb = QApplication.clipboard()
cb.clear(mode=cb.Clipboard)
cb.setText(self.ssh_command.text(), mode=cb.Clipboard)
cb.setText(self.ui.ssh_command.text(), mode=cb.Clipboard)

def as_dict(self):
return {
LANG.REMOTE_ADDRESS: self.remote_address.text(),
LANG.PROXY_HOST: self.proxy_host.text(),
LANG.BROWSER_OPEN: self.browser_open.text(),
LANG.LOCAL_PORT: self.local_port.value(),
KEYS.REMOTE_ADDRESS: self.ui.remote_address.text(),
KEYS.PROXY_HOST: self.ui.proxy_host.text(),
KEYS.BROWSER_OPEN: self.ui.browser_open.text(),
KEYS.LOCAL_PORT: self.ui.local_port.value(),
}

class Tunnel(QWidget):
def __init__(self, name, data):
super(Tunnel, self).__init__()
uic.loadUi("tunnel.ui", self)

self.ui = Ui_Tunnel()
self.ui.setupUi(self)

self.tunnelconfig = TunnelConfig(self, data)
self.tunnelconfig.setWindowTitle(name)
self.tunnelconfig.setModal(True)
self.name.setText(name)
self.ui.name.setText(name)

self.tunnelconfig.icon = F"./icons/{name}.png"

if not os.path.exists(self.tunnelconfig.icon):
self.tunnelconfig.icon = "./icons/robi.png"
self.tunnelconfig.icon = ICONS.TUNNEL

self.icon.setPixmap(QPixmap(self.tunnelconfig.icon))
self.action_tunnel.clicked.connect(self.do_tunnel)
self.action_settings.clicked.connect(self.tunnelconfig.show)
self.action_open.clicked.connect(self.do_open_browser)
self.ui.icon.setPixmap(QPixmap(self.tunnelconfig.icon))
self.ui.action_tunnel.clicked.connect(self.do_tunnel)
self.ui.action_settings.clicked.connect(self.tunnelconfig.show)
self.ui.action_open.clicked.connect(self.do_open_browser)

self.process = None

def do_open_browser(self):
browser_open = self.tunnelconfig.browser_open.text()
browser_open = self.tunnelconfig.ui.browser_open.text()
if browser_open:
urlobj = urlparse(browser_open)
local_port = self.tunnelconfig.local_port.value()
local_port = self.tunnelconfig.ui.local_port.value()
new_url = urlobj._replace(netloc=F"{urlobj.hostname}:{local_port}").geturl()
QDesktopServices.openUrl(QUrl(new_url))

Expand All @@ -119,12 +99,12 @@ def do_tunnel(self):
self.start_tunnel()

def start_tunnel(self):
params = self.tunnelconfig.ssh_command.text().split(" ")
params = self.tunnelconfig.ui.ssh_command.text().split(" ")

self.process = QProcess()
self.process.start(params[0], params[1:])

self.action_tunnel.setIcon(QIcon(LANG.ICON_STOP))
self.ui.action_tunnel.setIcon(QIcon(ICONS.STOP))

self.do_open_browser()

Expand All @@ -135,7 +115,7 @@ def stop_tunnel(self):
except:
pass

self.action_tunnel.setIcon(QIcon(LANG.ICON_START))
self.ui.action_tunnel.setIcon(QIcon(ICONS.START))

class TunnelManager(QWidget):
def __init__(self):
Expand All @@ -147,13 +127,13 @@ def __init__(self):
self.grid = QGridLayout(self)
self.tunnels = []

for i, name in enumerate(data):
for i, name in enumerate(sorted(data.keys())):
tunnel = Tunnel(name, data[name])
self.tunnels.append(tunnel)
self.grid.addWidget(tunnel, i, 0)

self.kill_button = QPushButton(LANG.SSH_KILL)
self.kill_button.setIcon(QIcon(LANG.ICON_SSH_KILL))
self.kill_button = QPushButton(LANG.KILL_SSH)
self.kill_button.setIcon(QIcon(ICONS.KILL_SSH))
self.kill_button.setFocusPolicy(Qt.NoFocus)
self.kill_button.clicked.connect(self.do_killall_ssh)

Expand All @@ -162,18 +142,21 @@ def __init__(self):
self.setLayout(self.grid)
self.resize(10, 10)
self.setWindowTitle(LANG.TITLE)
self.setWindowIcon(QIcon(LANG.ICON_WINDOW))
self.setWindowIcon(QIcon(ICONS.TUNNEL))
self.show()

def do_killall_ssh(self):
for tunnel in self.tunnels:
tunnel.stop_tunnel()
os.system(LANG.SSH_KILL)
if os.name == 'nt':
os.system(CMDS.SSH_KILL_WIN)
else:
os.system(CMDS.SSH_KILL_NIX)

def closeEvent(self, event):
data = {}
for tunnel in self.tunnels:
name = tunnel.name.text()
name = tunnel.ui.name.text()
data[name] = tunnel.tunnelconfig.as_dict()
timestamp = int(time.time())
shutil.copy(CONF_FILE, F"{CONF_FILE}-{timestamp}")
Expand All @@ -193,15 +176,15 @@ def closeEvent(self, event):
if not sm.create(1):
mb = QMessageBox()
mb.setIcon(QMessageBox.Information)
mb.setText("SSH Tunnel Manager is already running")
mb.setWindowTitle("Oops!")
mb.setText(LANG.ALREADY_RUNNING)
mb.setWindowTitle(LANG.OOPS)
mb.setStandardButtons(QMessageBox.Close)
mb.show()
elif not os.path.isfile(CONF_FILE):
elif not os.path.exists(CONF_FILE):
mb = QMessageBox()
mb.setIcon(QMessageBox.Information)
mb.setText(F"No {CONF_FILE} file found in application directory")
mb.setWindowTitle("Oops!")
mb.setText(LANG.CONF_NOT_FOUND)
mb.setWindowTitle(LANG.OOPS)
mb.setStandardButtons(QMessageBox.Close)
mb.show()
else:
Expand Down
Loading

0 comments on commit c08ca5b

Please sign in to comment.