Compare commits
252 Commits
55a17bb287
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bffff1d9b | |||
| 61b1dea366 | |||
| c94cbd7f36 | |||
| 618e023996 | |||
| 0104827234 | |||
| 92a97e140f | |||
| b9d573aeac | |||
| 3c438ca093 | |||
| d6bc7128df | |||
| 4b0659bd47 | |||
| 2351ad84bc | |||
| a16b5a232e | |||
| 0ffb0f7256 | |||
| 37ae3a06de | |||
| d0ff3536d2 | |||
| 0104b45331 | |||
| efc3ad00af | |||
| 7ac32e9199 | |||
| cb54689e7c | |||
| 992db16848 | |||
| 16b7a66c01 | |||
| 1def782149 | |||
| 55c11cce90 | |||
| ed21a14f68 | |||
| 004c58b445 | |||
| 8c9ebea489 | |||
| 19cb09f05e | |||
| ae86df8732 | |||
| a2f0b40fa8 | |||
| bf60412640 | |||
| 5f057c50ed | |||
| b2e38b3cb4 | |||
| 47e1790a8b | |||
| b67b8f18a4 | |||
| c6749fe856 | |||
| 4c7ff9d6a0 | |||
| e93edb73af | |||
| 9a6e2f67e6 | |||
| 84b1eb3f5d | |||
| 1bd5a21a94 | |||
| 5c3147536c | |||
| 8656441976 | |||
| 0d52d54eed | |||
| 1b59e304b0 | |||
| fb915487dc | |||
| a3b02b694e | |||
| 9b2a42bdc9 | |||
| 98f3cc95eb | |||
| 12f626b088 | |||
| b9b3f89910 | |||
| 24594915a9 | |||
| 6f6538fa73 | |||
| 239179931c | |||
| f1793baa57 | |||
| 418390fe8d | |||
| 4fd8688685 | |||
| f410510a7f | |||
| 274125e862 | |||
| 837dffddd5 | |||
| 1ef9d21ba4 | |||
| 9307cab1bb | |||
| e462edd99b | |||
| 48d0407c79 | |||
| 3c427af6fe | |||
| 5d54ed6f80 | |||
| 685488c7d4 | |||
| ec40aa2ec1 | |||
| 498926ae5d | |||
| 046552d09a | |||
| 27c46542e8 | |||
| 0c952a2623 | |||
| 50cdd3ea1c | |||
| 7c76016fcf | |||
| 0dca06fbc8 | |||
| 4c067c14d8 | |||
| 529842a411 | |||
| 99847c3ff0 | |||
| 96d4e32672 | |||
| c3581c7ecc | |||
| 657081678f | |||
| 89e8f5cffc | |||
| 5e0004c0d8 | |||
| aa071a1fdb | |||
| 2b9688ce44 | |||
| fa398e8c86 | |||
| c7cf401b0a | |||
| 38530ea0df | |||
| c933d6e6da | |||
| e4b648e447 | |||
| e5d4b4d2e5 | |||
| 35e0d67446 | |||
| 747f71e27c | |||
| 6e5656a00b | |||
| ab004b78eb | |||
| b42bac35bb | |||
| 7b874169cb | |||
| 2f7a51d2b7 | |||
| 6097bcbe8f | |||
| f1fd3572c5 | |||
| 3d715664db | |||
| 31cb8f6db5 | |||
| abe7a7ab08 | |||
| 46fe4f5557 | |||
| 35e4f1f6b7 | |||
| 767526054e | |||
| 69062bd828 | |||
| b3259d2981 | |||
| dc463f4cf0 | |||
| 74f4ab982e | |||
| 070e358463 | |||
| 99fc5a5600 | |||
| ee6cb6c90d | |||
| b52ba03be4 | |||
| 38c4eea2f0 | |||
| 838b33d6c5 | |||
| 470517a00f | |||
| d44a3c1a3b | |||
| 865a96c2ec | |||
| 21bbeef579 | |||
| 8523c79999 | |||
| 96153fa557 | |||
| 4d1bc9dbd0 | |||
| ca826a6229 | |||
| a7691b16f0 | |||
| d87429f98d | |||
| ccd1fbc52f | |||
| b0ba737d0d | |||
| 33aa6d361e | |||
| 1eee23953c | |||
| 1c548281b0 | |||
| 38ebb88ac6 | |||
| e4fa0ba9cd | |||
| cb50b25081 | |||
| 6d8015bdc9 | |||
| 982a4bbff9 | |||
| fa571c9ccd | |||
| ef0058c93f | |||
| e2e7c44bf6 | |||
| 3aadd164f0 | |||
| d6cafc67b2 | |||
| 8b9b6e798a | |||
| b55708721c | |||
| 3e50f5a465 | |||
| e13cc2b851 | |||
| 6e1d7efa6d | |||
| ed48c37706 | |||
| 0d773fba51 | |||
| f1ba1f050d | |||
| 62ca42bb18 | |||
| bc34fad485 | |||
| 500b5f4045 | |||
| 24d28c649c | |||
| 6fd57b0ce2 | |||
| 2dae9f667e | |||
| cb7e309915 | |||
| 55ca6fe620 | |||
| 5478623d19 | |||
| 302c52c784 | |||
| 0020c539ea | |||
| 6c790f84aa | |||
| 0ab6bb934d | |||
| a5871d399b | |||
| 9c2c67fbe1 | |||
| b6a6623406 | |||
| beedccdc29 | |||
| 11aaf00d8d | |||
| 21c1fa5d9a | |||
| 509814f3a8 | |||
| 203e04101b | |||
| 2d98ca843f | |||
| 919f5904f8 | |||
| ea2a1ba94d | |||
| 776666a77a | |||
| 9bde91f047 | |||
| 03c1181186 | |||
| 022c455334 | |||
| 0461ffec7c | |||
| f9c17c644a | |||
| 68a9af9331 | |||
| 016e663eef | |||
| a343aecc0d | |||
| c76afb2380 | |||
| d22818669f | |||
| 5c9609b509 | |||
| 716a5bed7d | |||
| 1fb683ce68 | |||
| c9450ed5fc | |||
| 98e91e46aa | |||
| ab4fbeaef8 | |||
| 57baf4c289 | |||
| 4efb8d205f | |||
| 3a674bc44d | |||
| 25a1985d20 | |||
| dd2d860243 | |||
| ece0198ff5 | |||
| c6903bb2a0 | |||
| b96e3a46bf | |||
| 7b0065f75a | |||
| 2aba0ee03b | |||
| a17fe5843d | |||
| 06662a294b | |||
| a4c3f0bd9a | |||
| 6a04bd911a | |||
| ce2a91e7e3 | |||
| d4acbfa5fc | |||
| b71fe3bf95 | |||
| 51feea00bb | |||
| 3da3578d08 | |||
| cfb6b04563 | |||
| bed2e37610 | |||
| 3214e387ef | |||
| eafbfca68f | |||
| 154b2bdd2c | |||
| e3a14dbaa7 | |||
| 73280f3bbf | |||
| 6b7efbe1da | |||
| be917b8a86 | |||
| 5d35bb88a9 | |||
| 90bce06b34 | |||
| b533c2ec53 | |||
| 3465a489f2 | |||
| 0086594368 | |||
| cfe1080cb0 | |||
| a3d18358db | |||
| 60855d6a85 | |||
| 4b404714f9 | |||
| 2dfe201c82 | |||
| 6abe3aef77 | |||
| e103847796 | |||
| 2fe54fc7f0 | |||
| 971a888fea | |||
| f992425e96 | |||
| 9192c62a9e | |||
| 0aadef4705 | |||
| 27b496893f | |||
| 20b55311f1 | |||
| f878882718 | |||
| e7f8028e83 | |||
| a472778a83 | |||
| 5ab97f3ffa | |||
| 095db04a79 | |||
| 14a0fea9b6 | |||
| 9c7e67aab5 | |||
| 10d1fae351 | |||
| 021402de69 | |||
| 59052ad5ba | |||
| d51e70ba57 | |||
| a79f1e5a07 | |||
| cb53a0ea22 | |||
| 99c276c531 | |||
| dd168f0059 | |||
| 85c1fec4cf |
45
.github/workflows/renovate.yml
vendored
@@ -1,45 +0,0 @@
|
||||
name: Renovate
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
log_level:
|
||||
type: choice
|
||||
description: Log level
|
||||
default: INFO
|
||||
options:
|
||||
- DEBUG
|
||||
- INFO
|
||||
- WARN
|
||||
- ERROR
|
||||
- FATAL
|
||||
schedule:
|
||||
- cron: 0 2 * * *
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Cache Bun global packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.bun/install/global
|
||||
key: ${{ runner.os }}-bun-global-renovate-40
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-global-
|
||||
|
||||
- name: Install Renovate
|
||||
run: bun install -g renovate@40
|
||||
|
||||
- name: Run renovate
|
||||
run: LOG_LEVEL=${{ github.event.inputs.log_level || 'INFO' }} renovate --token ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }}
|
||||
23
.github/workflows/test.yml
vendored
@@ -1,23 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Run tests
|
||||
run: bun test
|
||||
3
.gitignore
vendored
@@ -1 +1,4 @@
|
||||
node_modules/
|
||||
|
||||
# Never commit SSH bundles / secrets
|
||||
**/ssh.zip
|
||||
|
||||
42
CLAUDE.md
Executable file
@@ -0,0 +1,42 @@
|
||||
# Runtipi Development Guidelines
|
||||
|
||||
## App-Specific Context Files
|
||||
|
||||
## Deployment Workflow
|
||||
|
||||
**IMPORTANT:** Always follow this workflow when deploying changes to runtipi apps:
|
||||
|
||||
1. Push changes to the repo:
|
||||
```bash
|
||||
cd /etc/runtipi/repos/runtipi
|
||||
git add . && git commit -m "message" && git push
|
||||
```
|
||||
|
||||
2. Update the appstore to pull latest changes:
|
||||
```bash
|
||||
sudo runtipi-cli appstore update
|
||||
```
|
||||
|
||||
3. Stop the app:
|
||||
```bash
|
||||
sudo runtipi-cli app stop <APP_NAME>:runtipi
|
||||
```
|
||||
|
||||
4. Start the app:
|
||||
```bash
|
||||
sudo runtipi-cli app start <APP_NAME>:runtipi
|
||||
```
|
||||
|
||||
**NEVER** use `docker compose up/down` or `docker run` directly for runtipi apps unless debugging.
|
||||
|
||||
## App Naming Convention
|
||||
|
||||
Apps require the `:runtipi` namespace suffix when using runtipi-cli commands:
|
||||
- `rego-tunnel-linux:runtipi`
|
||||
- `cistech-tunnel:runtipi`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- `/etc/runtipi/repos/runtipi/apps/` - App definitions (config.json, docker-compose.yml, docker-compose.json, source/)
|
||||
- `/etc/runtipi/app-data/` - Runtime app data (managed by runtipi)
|
||||
- `/etc/runtipi/user-config/` - User configuration overrides
|
||||
65
apps/cfddns/config.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"$schema": "../app-info-schema.json",
|
||||
"name": "Cloudflare DDNS",
|
||||
"id": "cfddns",
|
||||
"available": true,
|
||||
"exposable": false,
|
||||
"dynamic_config": true,
|
||||
"no_gui": true,
|
||||
"tipi_version": 7,
|
||||
"version": "latest",
|
||||
"categories": [
|
||||
"network",
|
||||
"utilities"
|
||||
],
|
||||
"description": "Automatically update Cloudflare DNS records with your current public IP. Supports multiple domains, IPv4/IPv6, and proxy toggle.",
|
||||
"short_desc": "Dynamic DNS updater for Cloudflare",
|
||||
"author": "favonia",
|
||||
"source": "https://github.com/favonia/cloudflare-ddns",
|
||||
"form_fields": [
|
||||
{
|
||||
"type": "password",
|
||||
"label": "Cloudflare API Token",
|
||||
"env_variable": "CLOUDFLARE_API_TOKEN",
|
||||
"required": true,
|
||||
"hint": "API token with DNS edit permissions"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Domains (comma-separated)",
|
||||
"env_variable": "DOMAINS",
|
||||
"required": true,
|
||||
"hint": "e.g. home.example.com,vpn.example.com"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Proxied (Orange Cloud)",
|
||||
"env_variable": "PROXIED",
|
||||
"required": false,
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Update Schedule",
|
||||
"env_variable": "UPDATE_CRON",
|
||||
"required": false,
|
||||
"default": "@every 5m",
|
||||
"hint": "Cron expression or @every Xm"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Timezone",
|
||||
"env_variable": "TZ",
|
||||
"required": false,
|
||||
"default": "UTC"
|
||||
}
|
||||
],
|
||||
"supported_architectures": [
|
||||
"arm64",
|
||||
"amd64"
|
||||
],
|
||||
"created_at": 1736974800000,
|
||||
"updated_at": 1736974800000,
|
||||
"deprecated": false,
|
||||
"min_tipi_version": "4.5.0"
|
||||
}
|
||||
33
apps/cfddns/docker-compose.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
|
||||
"services": [
|
||||
{
|
||||
"name": "cfddns",
|
||||
"image": "favonia/cloudflare-ddns:latest",
|
||||
"isMain": true,
|
||||
"environment": [
|
||||
{
|
||||
"key": "CLOUDFLARE_API_TOKEN",
|
||||
"value": "${CLOUDFLARE_API_TOKEN}"
|
||||
},
|
||||
{
|
||||
"key": "DOMAINS",
|
||||
"value": "${DOMAINS}"
|
||||
},
|
||||
{
|
||||
"key": "PROXIED",
|
||||
"value": "${PROXIED:-false}"
|
||||
},
|
||||
{
|
||||
"key": "UPDATE_CRON",
|
||||
"value": "${UPDATE_CRON:-@every 5m}"
|
||||
},
|
||||
{
|
||||
"key": "TZ",
|
||||
"value": "${TZ:-UTC}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
apps/cfddns/metadata/logo.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
@@ -1,11 +0,0 @@
|
||||
# Required
|
||||
OC_URL=https://vpn.cistech.net/Employees
|
||||
OC_SERVERCERT=pin-sha256:HyHob3LiVmIp8ch9AzHJ9jMYqI43tO5N13oWeBLiZ/0=
|
||||
|
||||
# Optional
|
||||
OC_AUTHGROUP=
|
||||
OC_SSO_ARGS=--browser-display-mode shown
|
||||
VNC_PASSWORD=vpnSSO12
|
||||
NOVNC_PORT=6901
|
||||
PUBLISH_ADDR=0.0.0.0
|
||||
SSH_KEY_PATH=/home/alexz/.ssh/id_ed25519-lenovo
|
||||
@@ -1,42 +0,0 @@
|
||||
# Cistech Tunnel
|
||||
|
||||
OpenConnect-SSO VPN client running in a container with noVNC for browser-based access.
|
||||
|
||||
## Features
|
||||
|
||||
- **OpenConnect-SSO**: Cisco AnyConnect VPN with SSO/SAML authentication
|
||||
- **TOTP Support**: Automatic 2FA via keyring integration
|
||||
- **Auto-reconnect**: Automatically reconnects on disconnection
|
||||
- **noVNC**: Browser-based VNC access on port 6902
|
||||
- **NAT/Masquerade**: Routes traffic through VPN tunnel
|
||||
- **Cloudflared**: Optional Cloudflare tunnel support
|
||||
- **SSH Tunnels**: Optional SSH port forwarding
|
||||
|
||||
## Runtipi Installation
|
||||
|
||||
1. Install from the app store or custom repo
|
||||
2. Configure the required environment variables
|
||||
3. Start the app via Runtipi dashboard
|
||||
|
||||
## First-time SSO Login
|
||||
|
||||
1. Open noVNC at `http://<host>:6902`
|
||||
2. Enter VNC password
|
||||
3. Complete SSO login in the browser window
|
||||
4. VPN will connect and auto-reconnect on disconnect
|
||||
|
||||
## Source Files
|
||||
|
||||
- `source/Dockerfile`: Container build file
|
||||
- `source/entrypoint.sh`: Container entrypoint with auto-reconnect
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| OC_URL | Yes | VPN server URL |
|
||||
| OC_SERVERCERT | Yes | Server certificate pin |
|
||||
| OC_USER | No | Username (enables hidden browser mode) |
|
||||
| VNC_PASSWORD | Yes | noVNC access password |
|
||||
| OC_TOTP_SECRET | No | TOTP secret for auto 2FA |
|
||||
| NOVNC_PORT | No | noVNC port (default: 6901) |
|
||||
2
apps/cistech-tunnel/build/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Large binary files - track tar.gz but not 7z
|
||||
*.7z
|
||||
110
apps/cistech-tunnel/build/Dockerfile
Normal file
@@ -0,0 +1,110 @@
|
||||
FROM ubuntu:22.04
|
||||
|
||||
LABEL maintainer="alexz"
|
||||
LABEL description="OpenConnect-SSO VPN in Docker with noVNC"
|
||||
LABEL version="1.0.0"
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV container=docker
|
||||
|
||||
# VNC/noVNC settings
|
||||
ENV DISPLAY=:1
|
||||
ENV VNC_PORT=5901
|
||||
ENV NOVNC_PORT=6092
|
||||
|
||||
# Python/Playwright settings
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
ENV PATH=/opt/venv/bin:$PATH
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
# Core tools
|
||||
openconnect \
|
||||
vpnc-scripts \
|
||||
iptables \
|
||||
iproute2 \
|
||||
iputils-ping \
|
||||
net-tools \
|
||||
procps \
|
||||
curl \
|
||||
nano \
|
||||
ca-certificates \
|
||||
# Python
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
# VNC/noVNC (novnc installed from GitHub below)
|
||||
tigervnc-standalone-server \
|
||||
tigervnc-common \
|
||||
websockify \
|
||||
x11vnc \
|
||||
xvfb \
|
||||
# Window manager & terminal
|
||||
openbox \
|
||||
fluxbox \
|
||||
xterm \
|
||||
# Automation tools
|
||||
xdotool \
|
||||
xclip \
|
||||
oathtool \
|
||||
# X11/GUI dependencies for browser
|
||||
dbus \
|
||||
dbus-x11 \
|
||||
libgtk-3-0 \
|
||||
libglib2.0-0 \
|
||||
libnss3 \
|
||||
libatk1.0-0 \
|
||||
libatk-bridge2.0-0 \
|
||||
libx11-6 \
|
||||
libx11-xcb1 \
|
||||
libxcomposite1 \
|
||||
libxrandr2 \
|
||||
libgbm1 \
|
||||
libxdamage1 \
|
||||
libpango-1.0-0 \
|
||||
libxkbcommon0 \
|
||||
libxkbcommon-x11-0 \
|
||||
fonts-liberation \
|
||||
# EGL/GL for PyQt6 WebEngine + software rendering
|
||||
libegl1 \
|
||||
libgl1 \
|
||||
libopengl0 \
|
||||
libdbus-1-3 \
|
||||
mesa-utils \
|
||||
libgl1-mesa-dri \
|
||||
# XCB libraries for Qt6 (complete set)
|
||||
libxcb1 \
|
||||
libxcb-cursor0 \
|
||||
libxcb-icccm4 \
|
||||
libxcb-image0 \
|
||||
libxcb-keysyms1 \
|
||||
libxcb-render0 \
|
||||
libxcb-render-util0 \
|
||||
libxcb-shm0 \
|
||||
libxcb-xfixes0 \
|
||||
libxcb-xinerama0 \
|
||||
libxcb-randr0 \
|
||||
libxcb-glx0 \
|
||||
libxcb-shape0 \
|
||||
# sudo needed for openconnect-sso
|
||||
sudo \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install noVNC from GitHub (v1.4.0 - stable release with ES6 modules)
|
||||
RUN curl -fsSL https://github.com/novnc/noVNC/archive/refs/tags/v1.4.0.tar.gz | tar -xz -C /usr/share/ \
|
||||
&& mv /usr/share/noVNC-1.4.0 /usr/share/novnc \
|
||||
&& ln -sf /usr/share/novnc/vnc.html /usr/share/novnc/index.html
|
||||
|
||||
# Create Python venv and install openconnect-sso with all dependencies
|
||||
RUN python3 -m venv "$VIRTUAL_ENV" && \
|
||||
pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir \
|
||||
'openconnect-sso[full]' \
|
||||
playwright \
|
||||
keyring \
|
||||
keyrings.alt
|
||||
|
||||
# Install Playwright browser (Chromium)
|
||||
RUN python -m playwright install --with-deps chromium
|
||||
67
apps/cistech-tunnel/build/README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Cistech Tunnel - Build Files
|
||||
|
||||
This directory contains the Dockerfile and scripts to build the OpenConnect-SSO VPN Docker image.
|
||||
|
||||
## Files
|
||||
|
||||
- `Dockerfile` - Docker image definition (Ubuntu 22.04 + openconnect-sso + noVNC)
|
||||
- `build.sh` - Build and push script
|
||||
- `scripts/entrypoint.sh` - Container entrypoint
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cd /etc/runtipi/repos/runtipi/apps/cistech-tunnel/build
|
||||
./build.sh
|
||||
```
|
||||
|
||||
This builds and pushes to `git.alexzaw.dev/alexz/openconnect-vpn:latest`
|
||||
|
||||
To build without pushing:
|
||||
```bash
|
||||
docker build -t git.alexzaw.dev/alexz/openconnect-vpn:latest .
|
||||
```
|
||||
|
||||
## What's in the image
|
||||
|
||||
The Dockerfile creates an image with:
|
||||
- Ubuntu 22.04
|
||||
- openconnect + openconnect-sso[full] (Python)
|
||||
- Playwright Chromium browser (for SSO authentication)
|
||||
- TigerVNC server + noVNC (web-based VNC)
|
||||
- Tools: oathtool (TOTP), openbox, xterm
|
||||
|
||||
### Scripts (baked in)
|
||||
- `/opt/scripts/startup-vnc.sh` - Starts VNC server and noVNC
|
||||
- `/opt/scripts/entrypoint.sh` - Container entrypoint (DNS fix, IP forwarding, config generation)
|
||||
|
||||
## Runtime mounts (from shared/)
|
||||
|
||||
When running as cistech-tunnel app, these are mounted from `shared/`:
|
||||
- `/shared/openconnect-vpn` - Main VPN connection script
|
||||
- `/shared/xstartup` -> `/root/.vnc/xstartup` - VNC session startup
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `VPN_EMAIL` | Email/username for SSO login |
|
||||
| `VPN_PASSWORD` | Password for SSO login |
|
||||
| `VPN_TOTP_SECRET` | TOTP secret for 2FA (base32) |
|
||||
| `VPN_HOST` | VPN server URL (e.g., `https://vpn.example.com/Group`) |
|
||||
| `TARGET_IP` | Target IP for connectivity testing |
|
||||
| `VNC_PASSWORD` | VNC access password |
|
||||
|
||||
## Ports
|
||||
|
||||
- `5901` - VNC server
|
||||
- `6092` - noVNC web interface
|
||||
|
||||
## How it works
|
||||
|
||||
1. Container starts, generates openconnect-sso config from env vars
|
||||
2. VNC server starts with noVNC web interface
|
||||
3. xterm launches with the `openconnect-vpn` script
|
||||
4. Script sets up keyring with credentials (password + TOTP)
|
||||
5. openconnect-sso handles SSO authentication via hidden browser
|
||||
6. VPN connects and IP forwarding/NAT is configured
|
||||
22
apps/cistech-tunnel/build/build.sh
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# Build and push the OpenConnect-SSO VPN Docker image
|
||||
# Run this from the build directory
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/openconnect-vpn}"
|
||||
IMAGE_TAG="${IMAGE_TAG:-latest}"
|
||||
|
||||
echo "Building ${IMAGE_NAME}:${IMAGE_TAG}..."
|
||||
|
||||
docker build "$@" -t "${IMAGE_NAME}:${IMAGE_TAG}" .
|
||||
docker push "${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
|
||||
echo ""
|
||||
echo "Build complete!"
|
||||
echo ""
|
||||
echo "To test locally:"
|
||||
echo " docker run -d --cap-add=NET_ADMIN --device=/dev/net/tun -p 5901:5901 -p 6092:6092 -e VNC_PASSWORD=changeme -e VPN_HOST=https://vpn.example.com -e VPN_EMAIL=user@example.com ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
echo ""
|
||||
echo "Then connect via VNC to localhost:5901 or open noVNC at http://localhost:6092/vnc.html"
|
||||
echo ""
|
||||
BIN
apps/cistech-tunnel/cistech-tunnel.7z
Normal file
@@ -1,53 +1,76 @@
|
||||
{
|
||||
"name": "Cistech Tunnel",
|
||||
"id": "cistech-tunnel",
|
||||
"name": "cistech Tunnel",
|
||||
"available": true,
|
||||
"short_desc": "Cistech VPN client container with noVNC.",
|
||||
"author": "alexz",
|
||||
"port": 6902,
|
||||
"categories": [
|
||||
"utilities",
|
||||
"network"
|
||||
],
|
||||
"description": "OpenConnect-SSO VPN running in an isolated namespace with noVNC for first-time SSO reconnects.",
|
||||
"tipi_version": 1,
|
||||
"version": "latest",
|
||||
"source": "local",
|
||||
"port": 6092,
|
||||
"exposable": true,
|
||||
"dynamic_config": true,
|
||||
"no_gui": false,
|
||||
"id": "cistech-tunnel",
|
||||
"description": "openconnect-sso in Docker with noVNC web UI for accessing cistech environments. Native Docker - no VM overhead.",
|
||||
"tipi_version": 7,
|
||||
"version": "5.1.14.145",
|
||||
"categories": [
|
||||
"utilities"
|
||||
],
|
||||
"short_desc": "openconnect-sso VPN tunnel to cistech environments (native Docker)",
|
||||
"author": "alexz",
|
||||
"source": "https://git.alexzaw.dev/alexz/runtipi",
|
||||
"form_fields": [
|
||||
{
|
||||
"label": "VPN URL",
|
||||
"type": "text",
|
||||
"env_variable": "OC_URL",
|
||||
"required": true,
|
||||
"default": "https://vpn.cistech.net/Employees"
|
||||
"type": "email",
|
||||
"label": "VPN Email",
|
||||
"hint": "Email address for VPN SSO login (configured in /shared/openconnect-vpn script)",
|
||||
"placeholder": "your-email@company.com",
|
||||
"required": false,
|
||||
"env_variable": "VPN_EMAIL",
|
||||
"default": ""
|
||||
},
|
||||
{
|
||||
"label": "VNC Password",
|
||||
"type": "password",
|
||||
"label": "VPN Password",
|
||||
"hint": "Password for VPN SSO login (configured in /shared/openconnect-vpn script)",
|
||||
"placeholder": "",
|
||||
"required": false,
|
||||
"env_variable": "VPN_PASSWORD",
|
||||
"default": ""
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "TOTP Secret",
|
||||
"hint": "Base32 TOTP secret for 2FA (configured in /shared/openconnect-vpn script)",
|
||||
"placeholder": "",
|
||||
"required": false,
|
||||
"env_variable": "VPN_TOTP_SECRET",
|
||||
"default": ""
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "VPN Host",
|
||||
"hint": "VPN server hostname",
|
||||
"placeholder": "vpn.company.com",
|
||||
"required": false,
|
||||
"env_variable": "VPN_HOST",
|
||||
"default": "vpn.cistech.net/Employees"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Target IP",
|
||||
"hint": "IP address to route through VPN (e.g., IBM i server)",
|
||||
"placeholder": "10.3.1.201",
|
||||
"required": false,
|
||||
"env_variable": "TARGET_IP",
|
||||
"default": "10.3.1.201"
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"label": "VNC Password",
|
||||
"hint": "Password for noVNC web interface",
|
||||
"placeholder": "cisco123",
|
||||
"required": false,
|
||||
"env_variable": "VNC_PASSWORD",
|
||||
"required": true,
|
||||
"default": "Az@83278327$$@@"
|
||||
},
|
||||
{
|
||||
"label": "Server Certificate",
|
||||
"type": "text",
|
||||
"env_variable": "OC_SERVERCERT",
|
||||
"required": true,
|
||||
"default": "pin-sha256:HyHob3LiVmIp8ch9AzHJ9jMYqI43tO5N13oWeBLiZ/0="
|
||||
},
|
||||
{
|
||||
"label": "Username",
|
||||
"type": "text",
|
||||
"env_variable": "OC_USER",
|
||||
"required": true,
|
||||
"default": "alex.zaw@cistech.net"
|
||||
"default": ""
|
||||
}
|
||||
],
|
||||
"supported_architectures": [
|
||||
"arm64",
|
||||
"amd64"
|
||||
]
|
||||
}
|
||||
@@ -1,23 +1,74 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"services": [
|
||||
{
|
||||
"name": "cistech-tunnel",
|
||||
"image": "cistech-vpn:latest",
|
||||
"isMain": true,
|
||||
"internalPort": 6902,
|
||||
"privileged": true,
|
||||
"capAdd": ["NET_ADMIN"],
|
||||
"devices": ["/dev/net/tun:/dev/net/tun"],
|
||||
"environment": {
|
||||
"OC_URL": "${OC_URL}",
|
||||
"OC_SERVERCERT": "${OC_SERVERCERT}",
|
||||
"OC_USER": "${OC_USER}",
|
||||
"VNC_PASSWORD": "${VNC_PASSWORD}",
|
||||
"NOVNC_PORT": "6902"
|
||||
"image": "git.alexzaw.dev/alexz/openconnect-vpn:latest",
|
||||
"environment": [
|
||||
{
|
||||
"key": "VPN_EMAIL",
|
||||
"value": "${VPN_EMAIL}"
|
||||
},
|
||||
{
|
||||
"key": "VPN_PASSWORD",
|
||||
"value": "${VPN_PASSWORD}"
|
||||
},
|
||||
{
|
||||
"key": "VPN_TOTP_SECRET",
|
||||
"value": "${VPN_TOTP_SECRET}"
|
||||
},
|
||||
{
|
||||
"key": "VPN_HOST",
|
||||
"value": "${VPN_HOST}"
|
||||
},
|
||||
{
|
||||
"key": "VNC_PASSWORD",
|
||||
"value": "${VNC_PASSWORD}"
|
||||
},
|
||||
{
|
||||
"key": "TZ",
|
||||
"value": "${TZ}"
|
||||
},
|
||||
{
|
||||
"key": "TARGET_IP",
|
||||
"value": "${TARGET_IP}"
|
||||
}
|
||||
],
|
||||
"internalPort": 6092,
|
||||
"volumes": [
|
||||
{ "hostPath": "${APP_DATA_DIR}/data", "containerPath": "/root" }
|
||||
]
|
||||
{
|
||||
"hostPath": "${APP_DATA_DIR}/config",
|
||||
"containerPath": "/config",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"hostPath": "${APP_DATA_DIR}",
|
||||
"containerPath": "/runtime",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"hostPath": "/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared",
|
||||
"containerPath": "/shared",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"hostPath": "/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/xstartup",
|
||||
"containerPath": "/root/.vnc/xstartup",
|
||||
"readOnly": true
|
||||
}
|
||||
],
|
||||
"stopGracePeriod": "30s",
|
||||
"devices": [
|
||||
"/dev/net/tun"
|
||||
],
|
||||
"privileged": true,
|
||||
"capAdd": [
|
||||
"NET_ADMIN"
|
||||
],
|
||||
"isMain": true,
|
||||
"extraLabels": {
|
||||
"runtipi.managed": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
cistech-tunnel:
|
||||
image: cistech-vpn:latest
|
||||
image: git.alexzaw.dev/alexz/openconnect-vpn:latest
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
cistech-tunnel_runtipi_network:
|
||||
@@ -8,21 +8,26 @@ services:
|
||||
tipi_main_network:
|
||||
gw_priority: 1
|
||||
environment:
|
||||
OC_URL: ${OC_URL}
|
||||
OC_SERVERCERT: ${OC_SERVERCERT}
|
||||
OC_USER: ${OC_USER}
|
||||
VPN_EMAIL: ${VPN_EMAIL}
|
||||
VPN_PASSWORD: ${VPN_PASSWORD}
|
||||
VPN_TOTP_SECRET: ${VPN_TOTP_SECRET}
|
||||
VPN_HOST: ${VPN_HOST}
|
||||
VNC_PASSWORD: ${VNC_PASSWORD}
|
||||
NOVNC_PORT: "6902"
|
||||
TZ: ${TZ}
|
||||
TARGET_IP: ${TARGET_IP}
|
||||
ports:
|
||||
- ${APP_PORT}:6902
|
||||
- ${APP_PORT}:6092
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data:/root
|
||||
- ${APP_DATA_DIR}/config:/config
|
||||
- ${APP_DATA_DIR}:/runtime
|
||||
- /etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared:/shared
|
||||
- /etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/xstartup:/root/.vnc/xstartup:ro
|
||||
labels:
|
||||
generated: true
|
||||
traefik.enable: true
|
||||
traefik.docker.network: runtipi_tipi_main_network
|
||||
traefik.http.middlewares.cistech-tunnel-runtipi-web-redirect.redirectscheme.scheme: https
|
||||
traefik.http.services.cistech-tunnel-runtipi.loadbalancer.server.port: "6902"
|
||||
traefik.http.services.cistech-tunnel-runtipi.loadbalancer.server.port: "6092"
|
||||
traefik.http.routers.cistech-tunnel-runtipi-insecure.rule: Host(`${APP_DOMAIN}`)
|
||||
traefik.http.routers.cistech-tunnel-runtipi-insecure.entrypoints: web
|
||||
traefik.http.routers.cistech-tunnel-runtipi-insecure.service: cistech-tunnel-runtipi
|
||||
@@ -32,3 +37,10 @@ services:
|
||||
traefik.http.routers.cistech-tunnel-runtipi.service: cistech-tunnel-runtipi
|
||||
traefik.http.routers.cistech-tunnel-runtipi.tls.certresolver: myresolver
|
||||
runtipi.managed: true
|
||||
runtipi.appurn: cistech-tunnel:runtipi
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun
|
||||
privileged: true
|
||||
stop_grace_period: 30s
|
||||
|
||||
1217
apps/cistech-tunnel/metadata/compose-schema.json
Normal file
@@ -1,20 +1,147 @@
|
||||
# Dockerized OpenConnect-SSO with noVNC and Cloudflared
|
||||
# Cistech Tunnel - OpenConnect-SSO VPN
|
||||
|
||||
## Setup
|
||||
1) Copy `.env.example` to `.env` and fill values (URLs, servercert pins, VNC passwords, cloudflared tokens).
|
||||
Docker container running OpenConnect-SSO for Cisco AnyConnect VPN with SSO/SAML authentication support via noVNC. Provides transparent VPN access to protected resources from your LAN.
|
||||
|
||||
2) First-time SSO: leave `OC_SSO_ARGS_*=--browser-display-mode visible`.
|
||||
## Features
|
||||
|
||||
3) Build and start:
|
||||
docker compose build
|
||||
docker compose up -d vpn_a
|
||||
# Open http://localhost:6901, complete SSO.
|
||||
# After success, attach app containers or start cloudflared_a.
|
||||
- **OpenConnect-SSO** - Handles SAML/SSO authentication automatically
|
||||
- **Playwright browser** - Headless Chromium for SSO login
|
||||
- **Web-based access** via noVNC (port 6092)
|
||||
- **Auto-login with TOTP** - Credentials stored in keyring
|
||||
- **LAN routing** - Other machines on your network can reach VPN targets
|
||||
- **Lightweight** - No systemd, no Cisco bloat
|
||||
|
||||
4) Optional: switch to headless after first login:
|
||||
Set `OC_SSO_ARGS_*=--browser-display-mode hidden` (or `headless`) and restart the vpn service.
|
||||
## Architecture
|
||||
|
||||
## Notes
|
||||
- Each VPN runs in its own net namespace; routes from one cannot affect the other or the host.
|
||||
- DNS from the VPN applies within its container namespace and attached services only.
|
||||
- Persisted state lives in the named volumes mounted at `/root` (Playwright cache, configs).
|
||||
```
|
||||
LAN Devices ──► Linux Host ──► Container (172.30.0.10) ──► VPN Tunnel ──► Target
|
||||
│ │
|
||||
│ └── openconnect-sso + openconnect
|
||||
│ └── noVNC web UI (port 6092)
|
||||
│
|
||||
└── Host routing service
|
||||
(routes VPN traffic through container)
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Install the app through Runtipi
|
||||
|
||||
Configure your VPN credentials in app settings:
|
||||
- VPN Email
|
||||
- VPN Password
|
||||
- TOTP Secret (base32)
|
||||
- VPN Host (e.g., `https://vpn.cistech.net/Employees`)
|
||||
- Target IP (for connectivity testing)
|
||||
|
||||
### 2. Install host routing service (required for LAN access)
|
||||
|
||||
**Run this ONCE on the host after app install:**
|
||||
|
||||
```bash
|
||||
/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/install-host-services.sh
|
||||
```
|
||||
|
||||
This creates systemd services that route VPN traffic through the container.
|
||||
|
||||
### 3. Access the VPN GUI
|
||||
|
||||
Open `http://<your-server>:6092/vnc.html`
|
||||
|
||||
The VPN will auto-connect using your configured credentials.
|
||||
|
||||
## Usage
|
||||
|
||||
### Access noVNC
|
||||
|
||||
Navigate to port 6092 on your server. The openconnect-vpn script runs automatically and provides a menu:
|
||||
|
||||
```
|
||||
1 - Connect VPN
|
||||
2 - Disconnect VPN
|
||||
3 - Show VPN status
|
||||
4 - Setup IP forwarding
|
||||
5 - Test connection
|
||||
6 - Show network status
|
||||
7 - Show routing table
|
||||
8 - Setup keyring
|
||||
q - Quit
|
||||
```
|
||||
|
||||
### Command line options
|
||||
|
||||
```bash
|
||||
# Inside container
|
||||
openconnect-vpn -c # Connect and exit
|
||||
openconnect-vpn -d # Disconnect and exit
|
||||
openconnect-vpn -s # Show status
|
||||
openconnect-vpn --help # Show all options
|
||||
```
|
||||
|
||||
### View logs
|
||||
|
||||
```bash
|
||||
# Inside container
|
||||
cat /var/log/openconnect-vpn/$(date +%Y-%m-%d).log
|
||||
|
||||
# On host
|
||||
cat /var/log/cistech-routing.log
|
||||
```
|
||||
|
||||
## LAN Access
|
||||
|
||||
After the host routing service is installed, any device on your LAN can reach the VPN target:
|
||||
|
||||
1. **From the host:** Works automatically
|
||||
2. **From other LAN devices:** Add a static route pointing to your host
|
||||
|
||||
Example (Windows client):
|
||||
```cmd
|
||||
route add 10.3.1.0 mask 255.255.255.0 192.168.0.150 -p
|
||||
```
|
||||
|
||||
Where `192.168.0.150` is your Linux host IP.
|
||||
|
||||
## Uninstall
|
||||
|
||||
Before removing the app from Runtipi:
|
||||
|
||||
```bash
|
||||
/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/uninstall-host-services.sh
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### VPN not connecting
|
||||
```bash
|
||||
# Check logs
|
||||
docker exec cistech-tunnel cat /var/log/openconnect-vpn/$(date +%Y-%m-%d).log
|
||||
|
||||
# Try manual connect
|
||||
docker exec -it cistech-tunnel /shared/openconnect-vpn -c
|
||||
```
|
||||
|
||||
### VPN connects but can't reach target
|
||||
```bash
|
||||
# Check routes inside container
|
||||
docker exec cistech-tunnel ip route
|
||||
|
||||
# Check host routing
|
||||
ip route | grep <target-ip>
|
||||
```
|
||||
|
||||
### Host routing not working
|
||||
```bash
|
||||
# Check watcher service
|
||||
systemctl status cistech-routing-watcher.path
|
||||
|
||||
# Manually trigger routing
|
||||
touch /etc/runtipi/app-data/runtipi/cistech-tunnel/restart-routing
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Container IP:** 172.30.0.10 (on br-cistech-vpn bridge)
|
||||
- **Ports:** 6092 (noVNC), 5901 (VNC)
|
||||
- **Capabilities:** `NET_ADMIN`, `/dev/net/tun`
|
||||
- **Log retention:** 7 days (auto-cleanup)
|
||||
|
||||
|
Before Width: | Height: | Size: 609 KiB After Width: | Height: | Size: 6.0 KiB |
83
apps/cistech-tunnel/shared/entrypoint.sh
Executable file
@@ -0,0 +1,83 @@
|
||||
#!/bin/bash
|
||||
# Entrypoint: VNC password setup + DNS fix + start VNC
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Force software rendering (no GPU/OpenGL)
|
||||
export QT_QUICK_BACKEND=software
|
||||
export LIBGL_ALWAYS_SOFTWARE=1
|
||||
export GALLIUM_DRIVER=llvmpipe
|
||||
export MESA_GL_VERSION_OVERRIDE=3.3
|
||||
|
||||
# Qt/Chromium flags for running as root
|
||||
export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu --use-gl=swiftshader"
|
||||
export QTWEBENGINE_DISABLE_SANDBOX=1
|
||||
|
||||
# Ensure all shared scripts are executable (permissions may reset after git pull/appstore update)
|
||||
chmod +x /shared/*.sh /shared/openconnect-vpn /root/.vnc/xstartup 2>/dev/null || true
|
||||
|
||||
# Setup TigerVNC password file from env var (passed by runtipi)
|
||||
if [ -n "${VNC_PASSWORD:-}" ]; then
|
||||
mkdir -p /root/.vnc
|
||||
printf '%s\n%s\n' "$VNC_PASSWORD" "$VNC_PASSWORD" | vncpasswd -f > /root/.vnc/passwd
|
||||
chmod 600 /root/.vnc/passwd
|
||||
fi
|
||||
|
||||
# DNS fix - unmount Docker's read-only mounts
|
||||
cp /etc/resolv.conf /tmp/resolv.conf.bak 2>/dev/null || true
|
||||
cp /etc/hosts /tmp/hosts.bak 2>/dev/null || true
|
||||
umount /etc/resolv.conf 2>/dev/null || true
|
||||
umount /etc/hosts 2>/dev/null || true
|
||||
cat /tmp/resolv.conf.bak > /etc/resolv.conf 2>/dev/null || echo "nameserver 8.8.8.8" > /etc/resolv.conf
|
||||
cat /tmp/hosts.bak > /etc/hosts 2>/dev/null || echo "127.0.0.1 localhost" > /etc/hosts
|
||||
|
||||
# Enable IP forwarding
|
||||
echo 1 > /proc/sys/net/ipv4/ip_forward
|
||||
echo "[entrypoint] IP forwarding enabled"
|
||||
|
||||
# Generate openconnect-sso config from environment variables
|
||||
mkdir -p /root/.config/openconnect-sso
|
||||
cat > /root/.config/openconnect-sso/config.toml << EOF
|
||||
on_disconnect = ""
|
||||
|
||||
[default_profile]
|
||||
address = "${VPN_HOST:-}"
|
||||
user_group = ""
|
||||
name = ""
|
||||
|
||||
[credentials]
|
||||
username = "${VPN_EMAIL:-}"
|
||||
|
||||
[auto_fill_rules]
|
||||
[[auto_fill_rules."https://*"]]
|
||||
selector = "div[id=passwordError]"
|
||||
action = "stop"
|
||||
|
||||
[[auto_fill_rules."https://*"]]
|
||||
selector = "input[type=email]"
|
||||
fill = "username"
|
||||
|
||||
[[auto_fill_rules."https://*"]]
|
||||
selector = "input[name=passwd]"
|
||||
fill = "password"
|
||||
|
||||
[[auto_fill_rules."https://*"]]
|
||||
selector = "input[data-report-event=Signin_Submit]"
|
||||
action = "click"
|
||||
|
||||
[[auto_fill_rules."https://*"]]
|
||||
selector = "div[data-value=PhoneAppOTP]"
|
||||
action = "click"
|
||||
|
||||
[[auto_fill_rules."https://*"]]
|
||||
selector = "a[id=signInAnotherWay]"
|
||||
action = "click"
|
||||
|
||||
[[auto_fill_rules."https://*"]]
|
||||
selector = "input[id=idTxtBx_SAOTCC_OTC]"
|
||||
fill = "totp"
|
||||
EOF
|
||||
echo "[entrypoint] openconnect-sso config generated"
|
||||
|
||||
# Start VNC server
|
||||
exec /shared/startup-vnc.sh
|
||||
123
apps/cistech-tunnel/shared/host-routing.sh
Executable file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Host routing script for cistech-tunnel
|
||||
# Routes TARGET_IP through the VPN container
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
ACTION="${1:-start}"
|
||||
|
||||
# Fixed configuration (we assigned these)
|
||||
CONTAINER_IP="172.30.0.10"
|
||||
BRIDGE_NAME="br-cistech-vpn"
|
||||
TARGET_IP="${TARGET_IP:-10.3.1.0}"
|
||||
TARGET_SUBNET="$(echo "$TARGET_IP" | cut -d. -f1-3).0/24"
|
||||
LAN_SUBNET="192.168.0.0/23"
|
||||
LAN_INTERFACES="eth0 eth1 wlan0"
|
||||
LOG_FILE="/var/log/cistech-routing.log"
|
||||
|
||||
log() {
|
||||
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [cistech-routing] $*"
|
||||
echo "$msg" | tee -a "$LOG_FILE" >&2
|
||||
}
|
||||
|
||||
get_lan_interface() {
|
||||
ip route show default | awk '/default/ {for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1
|
||||
}
|
||||
|
||||
remove_routes() {
|
||||
log "Removing stale routes for $TARGET_SUBNET..."
|
||||
|
||||
# Remove any existing route to TARGET_SUBNET
|
||||
ip route del "$TARGET_SUBNET" 2>/dev/null || true
|
||||
|
||||
log "Stale routes removed"
|
||||
}
|
||||
|
||||
apply_routes() {
|
||||
local lan_if
|
||||
lan_if="$(get_lan_interface)"
|
||||
|
||||
log "Applying host routing rules..."
|
||||
log " Container IP: $CONTAINER_IP"
|
||||
log " Bridge: $BRIDGE_NAME"
|
||||
log " Target Subnet: $TARGET_SUBNET"
|
||||
log " LAN interface: ${lan_if:-unknown}"
|
||||
|
||||
# Enable IP forwarding
|
||||
echo 1 > /proc/sys/net/ipv4/ip_forward
|
||||
log "IP forwarding enabled"
|
||||
|
||||
# Add route to TARGET_SUBNET via container
|
||||
ip route replace "$TARGET_SUBNET" via "$CONTAINER_IP" dev "$BRIDGE_NAME"
|
||||
log "Route added: $TARGET_SUBNET via $CONTAINER_IP dev $BRIDGE_NAME"
|
||||
|
||||
# Allow forwarding in DOCKER-USER chain for all LAN interfaces
|
||||
for lan_if in $LAN_INTERFACES; do
|
||||
# Check if interface exists
|
||||
if ip link show "$lan_if" &>/dev/null; then
|
||||
# Allow traffic from LAN to container for TARGET_SUBNET
|
||||
iptables -C DOCKER-USER -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_SUBNET" -j ACCEPT 2>/dev/null || \
|
||||
iptables -I DOCKER-USER 1 -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_SUBNET" -j ACCEPT
|
||||
|
||||
# Allow return traffic
|
||||
iptables -C DOCKER-USER -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_SUBNET" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
|
||||
iptables -I DOCKER-USER 1 -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_SUBNET" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
|
||||
log "DOCKER-USER iptables rules added for $lan_if <-> $BRIDGE_NAME"
|
||||
fi
|
||||
done
|
||||
|
||||
# Masquerade traffic from LAN subnet to VPN bridge (so return traffic routes correctly)
|
||||
# Use nft since iptables-nft backend doesn't support iptables -t nat commands
|
||||
if ! nft list chain ip nat POSTROUTING 2>/dev/null | grep -q "saddr $LAN_SUBNET.*oifname.*$BRIDGE_NAME.*masquerade"; then
|
||||
nft add rule ip nat POSTROUTING ip saddr "$LAN_SUBNET" oifname "$BRIDGE_NAME" counter masquerade
|
||||
log "NAT masquerade rule added for $LAN_SUBNET -> $BRIDGE_NAME"
|
||||
else
|
||||
log "NAT masquerade rule already exists for $LAN_SUBNET -> $BRIDGE_NAME"
|
||||
fi
|
||||
|
||||
log "OK: Host routing applied - $TARGET_SUBNET via $CONTAINER_IP ($BRIDGE_NAME)"
|
||||
}
|
||||
|
||||
remove_all() {
|
||||
log "Removing all routing rules..."
|
||||
|
||||
# Remove route
|
||||
ip route del "$TARGET_SUBNET" via "$CONTAINER_IP" dev "$BRIDGE_NAME" 2>/dev/null || true
|
||||
|
||||
# Remove iptables rules for all LAN interfaces
|
||||
for lan_if in $LAN_INTERFACES; do
|
||||
iptables -D DOCKER-USER -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_SUBNET" -j ACCEPT 2>/dev/null || true
|
||||
iptables -D DOCKER-USER -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_SUBNET" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Remove masquerade rule (using nft)
|
||||
local handle=""
|
||||
handle=$(nft -a list chain ip nat POSTROUTING 2>/dev/null | grep "saddr $LAN_SUBNET.*oifname.*$BRIDGE_NAME.*masquerade" | grep -oP 'handle \K\d+' | head -1 || true)
|
||||
if [ -n "$handle" ]; then
|
||||
nft delete rule ip nat POSTROUTING handle "$handle" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
log "All routing rules removed"
|
||||
}
|
||||
|
||||
case "$ACTION" in
|
||||
start)
|
||||
remove_routes
|
||||
apply_routes
|
||||
;;
|
||||
stop)
|
||||
remove_all
|
||||
;;
|
||||
restart)
|
||||
remove_all
|
||||
sleep 1
|
||||
remove_routes
|
||||
apply_routes
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
55
apps/cistech-tunnel/shared/install-host-services.sh
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Install host-side systemd services for cistech-tunnel
|
||||
# Run this ONCE on the host after installing the app in Runtipi
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
APP_DATA_DIR="/etc/runtipi/app-data/runtipi/cistech-tunnel"
|
||||
|
||||
echo "Installing cistech-tunnel host services..."
|
||||
|
||||
# Create the path watcher unit
|
||||
cat << 'EOF' | sudo tee /etc/systemd/system/cistech-routing-watcher.path
|
||||
[Unit]
|
||||
Description=Watch for cistech-tunnel routing trigger
|
||||
|
||||
[Path]
|
||||
PathExists=/etc/runtipi/app-data/runtipi/cistech-tunnel/restart-routing
|
||||
Unit=cistech-routing-watcher.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Create the service unit
|
||||
cat << EOF | sudo tee /etc/systemd/system/cistech-routing-watcher.service
|
||||
[Unit]
|
||||
Description=Apply cistech-tunnel routing rules
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash ${SCRIPT_DIR}/host-routing.sh restart
|
||||
ExecStartPost=/bin/rm -f ${APP_DATA_DIR}/restart-routing
|
||||
ExecStartPost=/bin/bash -c 'echo "trigger cleared at \$(date)" >> ${APP_DATA_DIR}/watcher.log'
|
||||
EOF
|
||||
|
||||
# Make host-routing.sh executable
|
||||
sudo chmod +x "${SCRIPT_DIR}/host-routing.sh"
|
||||
|
||||
# Reload systemd and enable the watcher
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now cistech-routing-watcher.path
|
||||
|
||||
echo ""
|
||||
echo "Done! Services installed:"
|
||||
echo " - cistech-routing-watcher.path (watches for trigger file)"
|
||||
echo " - cistech-routing-watcher.service (applies routing rules)"
|
||||
echo ""
|
||||
echo "To check status:"
|
||||
echo " systemctl status cistech-routing-watcher.path"
|
||||
echo ""
|
||||
echo "To manually trigger routing:"
|
||||
echo " touch ${APP_DATA_DIR}/restart-routing"
|
||||
662
apps/cistech-tunnel/shared/openconnect-vpn
Executable file
@@ -0,0 +1,662 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Cistech VPN Connection Script with OpenConnect-SSO
|
||||
# Usage: ./openconnect-vpn [-c|--connect] [-d|--disconnect] [-m|--menu] [-r|--routes] [-s|--status] [--help]
|
||||
#
|
||||
# Options:
|
||||
# -c, --connect Connect to VPN and exit
|
||||
# -d, --disconnect Disconnect VPN and exit
|
||||
# -m, --menu Skip auto-connect, show menu directly
|
||||
# -r, --routes Show routing table and exit
|
||||
# -s, --status Show VPN status and exit
|
||||
# --help Show this help message
|
||||
|
||||
# Credentials from environment variables (set by runtipi)
|
||||
VPN_EMAIL="${VPN_EMAIL:-}"
|
||||
VPN_PASSWORD="${VPN_PASSWORD:-}"
|
||||
VPN_TOTP_SECRET="${VPN_TOTP_SECRET:-}"
|
||||
VPN_HOST="${VPN_HOST:-}"
|
||||
TARGET_IP="${TARGET_IP:-10.3.1.201}"
|
||||
TARGET_SUBNET="$(echo "$TARGET_IP" | cut -d. -f1-3).0/24"
|
||||
VPN_INTERFACE="${VPN_INTERFACE:-tun0}"
|
||||
CONTAINER_NETWORK="172.30.0.0/24"
|
||||
|
||||
|
||||
# Log directory
|
||||
LOG_DIR="/var/log/openconnect-vpn"
|
||||
LOG_RETENTION_DAYS=7
|
||||
mkdir -p "$LOG_DIR" 2>/dev/null
|
||||
|
||||
get_log_file() {
|
||||
echo "$LOG_DIR/$(date '+%Y-%m-%d').log"
|
||||
}
|
||||
|
||||
cleanup_old_logs() {
|
||||
find "$LOG_DIR" -name "*.log" -type f -mtime +$LOG_RETENTION_DAYS -delete 2>/dev/null
|
||||
}
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
GRAY='\033[0;90m'
|
||||
NC='\033[0m'
|
||||
|
||||
print_banner() {
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo -e "${CYAN} Cistech VPN Connection Script ${NC}"
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Flags
|
||||
SKIP_AUTO_CONNECT=false
|
||||
DO_CONNECT=false
|
||||
DO_DISCONNECT=false
|
||||
|
||||
# Logging
|
||||
log() {
|
||||
local level="$1"
|
||||
local msg="$2"
|
||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local timestamp_short=$(date '+%H:%M:%S')
|
||||
local log_file=$(get_log_file)
|
||||
|
||||
local msg_plain=$(echo -e "$msg" | sed 's/\x1b\[[0-9;]*m//g')
|
||||
echo "[$timestamp] [$level] $msg_plain" >> "$log_file"
|
||||
|
||||
case $level in
|
||||
INFO) echo -e "${GRAY}[$timestamp_short]${NC} ${GREEN}[INFO]${NC} $msg" ;;
|
||||
WARN) echo -e "${GRAY}[$timestamp_short]${NC} ${YELLOW}[WARN]${NC} $msg" ;;
|
||||
ERROR) echo -e "${GRAY}[$timestamp_short]${NC} ${RED}[ERROR]${NC} $msg" ;;
|
||||
DEBUG) echo -e "${GRAY}[$timestamp_short]${NC} ${CYAN}[DEBUG]${NC} $msg" ;;
|
||||
CMD) echo -e "${GRAY}[$timestamp_short]${NC} ${GRAY}[CMD]${NC} $msg" ;;
|
||||
*) echo -e "${GRAY}[$timestamp_short]${NC} $msg" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_cmd() {
|
||||
local desc="$1"
|
||||
shift
|
||||
log CMD "$desc: $*"
|
||||
output=$("$@" 2>&1)
|
||||
local rc=$?
|
||||
if [ -n "$output" ]; then
|
||||
echo "$output" | while IFS= read -r line; do
|
||||
echo -e " ${GRAY}│${NC} $line"
|
||||
done
|
||||
fi
|
||||
return $rc
|
||||
}
|
||||
|
||||
show_help() {
|
||||
echo -e "${CYAN}Cistech VPN Connection Script${NC}"
|
||||
echo ""
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -c, --connect Connect to VPN and exit"
|
||||
echo " -d, --disconnect Disconnect VPN and exit"
|
||||
echo " -m, --menu Skip auto-connect, show menu directly"
|
||||
echo " -r, --routes Show routing table and exit"
|
||||
echo " -s, --status Show VPN status and exit"
|
||||
echo " --help Show this help message"
|
||||
echo ""
|
||||
echo "Menu Options:"
|
||||
echo " 1 - Connect VPN"
|
||||
echo " 2 - Disconnect VPN"
|
||||
echo " 3 - Show VPN status"
|
||||
echo " 4 - Setup IP forwarding"
|
||||
echo " 5 - Test connection"
|
||||
echo " 6 - Show network status"
|
||||
echo " 7 - Show routing table"
|
||||
echo " 8 - Show live TOTP"
|
||||
echo " q - Quit"
|
||||
}
|
||||
|
||||
# Fetch server certificate fingerprint
|
||||
get_server_cert() {
|
||||
local host="$1"
|
||||
local server=$(echo "$host" | sed -E 's|^https?://||; s|/.*$||')
|
||||
[[ "$server" != *:* ]] && server="${server}:443"
|
||||
|
||||
log DEBUG "Fetching server certificate from $server..."
|
||||
|
||||
local cert_pin=$(echo | openssl s_client -connect "$server" 2>/dev/null \
|
||||
| openssl x509 -pubkey -noout 2>/dev/null \
|
||||
| openssl pkey -pubin -outform der 2>/dev/null \
|
||||
| openssl dgst -sha256 -binary \
|
||||
| openssl enc -base64)
|
||||
|
||||
if [[ -n "$cert_pin" ]]; then
|
||||
echo "pin-sha256:${cert_pin}"
|
||||
else
|
||||
log WARN "Could not fetch server certificate"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# Setup keyring with credentials
|
||||
setup_keyring() {
|
||||
log INFO "Setting up keyring credentials..."
|
||||
|
||||
if [[ -z "$VPN_EMAIL" ]]; then
|
||||
log ERROR "VPN_EMAIL is not set"
|
||||
return 1
|
||||
fi
|
||||
|
||||
python3 << PYTHON
|
||||
import keyring
|
||||
from keyrings.alt.file import PlaintextKeyring
|
||||
|
||||
keyring.set_keyring(PlaintextKeyring())
|
||||
|
||||
email = "$VPN_EMAIL"
|
||||
password = "$VPN_PASSWORD"
|
||||
totp_secret = "$VPN_TOTP_SECRET"
|
||||
|
||||
if password:
|
||||
keyring.set_password('openconnect-sso', email, password)
|
||||
print(f"Password stored in keyring for {email}")
|
||||
|
||||
if totp_secret:
|
||||
keyring.set_password('openconnect-sso', f'totp/{email}', totp_secret.upper())
|
||||
print(f"TOTP secret stored in keyring for {email}")
|
||||
|
||||
print("Keyring setup complete")
|
||||
PYTHON
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log INFO "Keyring credentials configured"
|
||||
else
|
||||
log ERROR "Failed to setup keyring"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
get_totp() {
|
||||
oathtool --totp -b "$VPN_TOTP_SECRET"
|
||||
}
|
||||
|
||||
# Test connection to TARGET_IP
|
||||
test_connection() {
|
||||
if [[ -z "$TARGET_IP" ]]; then
|
||||
log WARN "TARGET_IP not set"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log INFO "Testing connection to $TARGET_IP..."
|
||||
if ping -c 3 -W 3 "$TARGET_IP" &>/dev/null; then
|
||||
log INFO "Connection test: ${GREEN}SUCCESS${NC}"
|
||||
return 0
|
||||
else
|
||||
log WARN "Connection test: ${RED}FAILED${NC} (may need manual route on host)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
show_totp() {
|
||||
log INFO "Starting live TOTP display (Ctrl+C to stop)"
|
||||
echo ""
|
||||
while true; do
|
||||
TOTP=$(get_totp)
|
||||
SECONDS_LEFT=$((30 - ($(date +%s) % 30)))
|
||||
echo -ne "\r ${CYAN}Current TOTP:${NC} ${GREEN}$TOTP${NC} (expires in ${YELLOW}${SECONDS_LEFT}s${NC}) "
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
get_vpn_interface() {
|
||||
local iface=$(ip link show | grep -oP 'tun\d+(?=:.*UP)' | head -1)
|
||||
if [ -z "$iface" ]; then
|
||||
iface=$(ip link show | grep -oP 'tun\d+' | head -1)
|
||||
fi
|
||||
echo "$iface"
|
||||
}
|
||||
|
||||
get_container_ip() {
|
||||
ip addr show eth0 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1
|
||||
}
|
||||
|
||||
get_vpn_ip() {
|
||||
local iface=$(get_vpn_interface)
|
||||
if [ -n "$iface" ]; then
|
||||
ip addr show "$iface" 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1
|
||||
fi
|
||||
}
|
||||
|
||||
check_vpn_status() {
|
||||
local vpn_iface=$(get_vpn_interface)
|
||||
if [ -n "$vpn_iface" ]; then
|
||||
local vpn_ip=$(get_vpn_ip)
|
||||
log INFO "VPN is ${GREEN}CONNECTED${NC}"
|
||||
log DEBUG " Interface: $vpn_iface"
|
||||
log DEBUG " VPN IP: $vpn_ip"
|
||||
return 0
|
||||
else
|
||||
log WARN "VPN is ${RED}NOT CONNECTED${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
show_routes() {
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo -e "${CYAN} Current Routing Table ${NC}"
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}IPv4 Routes:${NC}"
|
||||
ip -4 route show | while IFS= read -r line; do
|
||||
if echo "$line" | grep -qE "tun[0-9]"; then
|
||||
echo -e " ${YELLOW}$line${NC}"
|
||||
elif echo "$line" | grep -qE "10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\."; then
|
||||
echo -e " ${CYAN}$line${NC}"
|
||||
else
|
||||
echo " $line"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo -e "${GREEN}VPN Interface:${NC}"
|
||||
local vpn_iface=$(get_vpn_interface)
|
||||
if [ -n "$vpn_iface" ]; then
|
||||
ip addr show "$vpn_iface" 2>/dev/null | grep -E "inet|link" | while IFS= read -r line; do
|
||||
echo " $line"
|
||||
done
|
||||
else
|
||||
echo -e " ${RED}No VPN interface found${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
show_network_status() {
|
||||
log INFO "Current network status:"
|
||||
echo ""
|
||||
log DEBUG "Container Network Interfaces:"
|
||||
ip -4 addr show | grep -E "inet |^[0-9]+:" | while IFS= read -r line; do
|
||||
echo -e " ${GRAY}│${NC} $line"
|
||||
done
|
||||
|
||||
echo ""
|
||||
local vpn_iface=$(get_vpn_interface)
|
||||
if [ -n "$vpn_iface" ]; then
|
||||
local vpn_ip=$(get_vpn_ip)
|
||||
log INFO "VPN Status: ${GREEN}CONNECTED${NC}"
|
||||
log DEBUG " Interface: $vpn_iface"
|
||||
log DEBUG " VPN IP: $vpn_ip"
|
||||
else
|
||||
log WARN "VPN Status: ${RED}NOT CONNECTED${NC}"
|
||||
fi
|
||||
|
||||
local container_ip=$(get_container_ip)
|
||||
if [ -n "$container_ip" ]; then
|
||||
log DEBUG "Container IP: $container_ip"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log DEBUG "Default gateway:"
|
||||
ip route show default | while IFS= read -r line; do
|
||||
echo -e " ${GRAY}│${NC} $line"
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
kill_vpn_processes() {
|
||||
log INFO "Killing VPN processes..."
|
||||
local killed=0
|
||||
|
||||
for pid in $(pgrep -x "openconnect" 2>/dev/null); do
|
||||
log DEBUG "Killing openconnect (PID $pid)"
|
||||
kill -9 "$pid" 2>/dev/null && ((killed++))
|
||||
done
|
||||
|
||||
for pid in $(pgrep -f "openconnect-sso" 2>/dev/null); do
|
||||
log DEBUG "Killing openconnect-sso (PID $pid)"
|
||||
kill -9 "$pid" 2>/dev/null && ((killed++))
|
||||
done
|
||||
|
||||
if [ $killed -eq 0 ]; then
|
||||
log INFO "No VPN processes were running"
|
||||
else
|
||||
log INFO "Killed $killed process(es)"
|
||||
sleep 2
|
||||
fi
|
||||
}
|
||||
|
||||
disconnect_vpn() {
|
||||
log INFO "Disconnecting VPN..."
|
||||
kill_vpn_processes
|
||||
|
||||
if check_vpn_status; then
|
||||
log WARN "VPN still appears connected"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log INFO "VPN disconnected"
|
||||
return 0
|
||||
}
|
||||
|
||||
setup_forwarding() {
|
||||
log INFO "Setting up IP forwarding rules for $TARGET_SUBNET..."
|
||||
|
||||
local vpn_iface=$(get_vpn_interface)
|
||||
if [ -z "$vpn_iface" ]; then
|
||||
log ERROR "No VPN interface found! Is VPN connected?"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local vpn_ip=$(get_vpn_ip)
|
||||
local container_ip=$(get_container_ip)
|
||||
|
||||
log DEBUG "VPN interface: $vpn_iface"
|
||||
log DEBUG "VPN IP: $vpn_ip"
|
||||
log DEBUG "Container IP: $container_ip"
|
||||
|
||||
# Enable IP forwarding
|
||||
run_cmd "Enabling IP forwarding" sysctl -w net.ipv4.ip_forward=1
|
||||
|
||||
# NAT masquerade for container network going through VPN
|
||||
if ! iptables -t nat -C POSTROUTING -s "$CONTAINER_NETWORK" -o "$vpn_iface" -j MASQUERADE 2>/dev/null; then
|
||||
run_cmd "Adding NAT masquerade for container network -> VPN" iptables -t nat -A POSTROUTING -s "$CONTAINER_NETWORK" -o "$vpn_iface" -j MASQUERADE
|
||||
else
|
||||
log DEBUG "NAT masquerade for container network already exists"
|
||||
fi
|
||||
|
||||
# Forward rules at position 1
|
||||
iptables -D FORWARD -d "$TARGET_SUBNET" -j ACCEPT 2>/dev/null || true
|
||||
iptables -D FORWARD -s "$TARGET_SUBNET" -j ACCEPT 2>/dev/null || true
|
||||
iptables -D FORWARD -s "$CONTAINER_NETWORK" -j ACCEPT 2>/dev/null || true
|
||||
iptables -D FORWARD -d "$CONTAINER_NETWORK" -j ACCEPT 2>/dev/null || true
|
||||
|
||||
run_cmd "Inserting forward rule (to container network)" iptables -I FORWARD 1 -d "$CONTAINER_NETWORK" -j ACCEPT
|
||||
run_cmd "Inserting forward rule (from container network)" iptables -I FORWARD 1 -s "$CONTAINER_NETWORK" -j ACCEPT
|
||||
run_cmd "Inserting forward rule (from target)" iptables -I FORWARD 1 -s "$TARGET_SUBNET" -j ACCEPT
|
||||
run_cmd "Inserting forward rule (to target)" iptables -I FORWARD 1 -d "$TARGET_SUBNET" -j ACCEPT
|
||||
|
||||
log INFO "Forwarding rules configured"
|
||||
echo ""
|
||||
|
||||
# Trigger host routing service restart
|
||||
log INFO "Triggering host routing service restart..."
|
||||
touch /runtime/restart-routing 2>/dev/null || true
|
||||
sleep 2
|
||||
if [ ! -f /runtime/restart-routing ]; then
|
||||
log INFO "Host routing service restarted"
|
||||
else
|
||||
log WARN "Host watcher may not be running (trigger file still exists)"
|
||||
fi
|
||||
|
||||
log INFO "Routing configured for $TARGET_SUBNET through VPN tunnel"
|
||||
echo ""
|
||||
}
|
||||
|
||||
connect_vpn() {
|
||||
log INFO "=== Starting OpenConnect-SSO VPN ==="
|
||||
echo ""
|
||||
|
||||
# Kill any existing VPN processes
|
||||
kill_vpn_processes
|
||||
|
||||
# Reset DNS to public servers (VPN script may have overwritten it)
|
||||
log DEBUG "Resetting DNS to public servers..."
|
||||
echo "nameserver 8.8.8.8" > /etc/resolv.conf
|
||||
echo "nameserver 1.1.1.1" >> /etc/resolv.conf
|
||||
|
||||
# Clean up stale tun interface
|
||||
ip link delete tun0 2>/dev/null || true
|
||||
|
||||
# Validate required variables
|
||||
if [[ -z "$VPN_HOST" ]]; then
|
||||
log ERROR "VPN_HOST is not set"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Setup keyring credentials
|
||||
setup_keyring
|
||||
|
||||
# Fetch server certificate
|
||||
log INFO "Fetching server certificate..."
|
||||
local servercert=$(get_server_cert "$VPN_HOST")
|
||||
|
||||
log INFO "Connecting to: $VPN_HOST"
|
||||
log DEBUG "Interface: $VPN_INTERFACE"
|
||||
|
||||
# Build openconnect-sso command
|
||||
local sso_args=()
|
||||
sso_args+=("-s" "$VPN_HOST")
|
||||
|
||||
if [[ -n "$VPN_EMAIL" ]]; then
|
||||
sso_args+=("-u" "$VPN_EMAIL")
|
||||
fi
|
||||
|
||||
# Use hidden browser for automated login
|
||||
sso_args+=("--browser-display-mode" "hidden")
|
||||
|
||||
# Build openconnect args
|
||||
local oc_args=()
|
||||
oc_args+=("--protocol=anyconnect")
|
||||
oc_args+=("--interface" "$VPN_INTERFACE")
|
||||
oc_args+=("--script" "/usr/share/vpnc-scripts/vpnc-script")
|
||||
|
||||
if [[ -n "$servercert" ]]; then
|
||||
oc_args+=("--servercert" "$servercert")
|
||||
log DEBUG "Server cert: $servercert"
|
||||
fi
|
||||
|
||||
# Launch openconnect-sso
|
||||
log INFO "Launching openconnect-sso..."
|
||||
openconnect-sso "${sso_args[@]}" -- /usr/sbin/openconnect "${oc_args[@]}" &
|
||||
OC_PID=$!
|
||||
disown $OC_PID
|
||||
log DEBUG "openconnect-sso started with PID $OC_PID"
|
||||
|
||||
# Wait for VPN to connect
|
||||
log INFO "Waiting for VPN connection..."
|
||||
local wait_count=0
|
||||
local max_wait=300
|
||||
|
||||
while [ -z "$(get_vpn_interface)" ]; do
|
||||
sleep 2
|
||||
((wait_count+=2))
|
||||
if [ $((wait_count % 10)) -eq 0 ]; then
|
||||
log DEBUG "Still waiting for VPN... (${wait_count}s)"
|
||||
fi
|
||||
if [ $wait_count -ge $max_wait ]; then
|
||||
log ERROR "Timeout waiting for VPN connection after ${max_wait}s"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
log INFO "VPN connected!"
|
||||
local vpn_iface=$(get_vpn_interface)
|
||||
local vpn_ip=$(get_vpn_ip)
|
||||
log DEBUG " Interface: $vpn_iface"
|
||||
log DEBUG " VPN IP: $vpn_ip"
|
||||
|
||||
# Wait for routes to stabilize
|
||||
log DEBUG "Waiting for routes to stabilize..."
|
||||
sleep 3
|
||||
|
||||
# Setup forwarding
|
||||
setup_forwarding
|
||||
|
||||
# Test connection to IBMI host
|
||||
test_connection
|
||||
|
||||
# Disable screen blanking
|
||||
xset s off 2>/dev/null || true
|
||||
xset -dpms 2>/dev/null || true
|
||||
xset s noblank 2>/dev/null || true
|
||||
log DEBUG "Screen blanking disabled"
|
||||
|
||||
log INFO "VPN setup complete"
|
||||
|
||||
# Start watchdog in background
|
||||
start_watchdog &
|
||||
WATCHDOG_PID=$!
|
||||
log DEBUG "Watchdog started with PID $WATCHDOG_PID"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Watchdog - monitors VPN and reconnects if dropped
|
||||
start_watchdog() {
|
||||
log INFO "Starting VPN watchdog (check every 60s, keepalive ping every 5min)..."
|
||||
|
||||
local check_interval=60
|
||||
local keepalive_interval=300
|
||||
local last_keepalive=0
|
||||
local reconnect_attempts=0
|
||||
local max_reconnect_attempts=3
|
||||
|
||||
while true; do
|
||||
sleep $check_interval
|
||||
|
||||
local now=$(date +%s)
|
||||
local vpn_iface=$(get_vpn_interface)
|
||||
|
||||
if [ -n "$vpn_iface" ]; then
|
||||
# VPN is up
|
||||
reconnect_attempts=0
|
||||
|
||||
# Keepalive ping every 5 minutes
|
||||
if [ $((now - last_keepalive)) -ge $keepalive_interval ]; then
|
||||
test_connection || log WARN "VPN may be degraded"
|
||||
last_keepalive=$now
|
||||
fi
|
||||
else
|
||||
# VPN is down
|
||||
((reconnect_attempts++))
|
||||
log WARN "VPN disconnected! Reconnect attempt $reconnect_attempts/$max_reconnect_attempts..."
|
||||
|
||||
if [ $reconnect_attempts -le $max_reconnect_attempts ]; then
|
||||
connect_vpn
|
||||
else
|
||||
log ERROR "Max reconnect attempts reached. Manual intervention required."
|
||||
sleep 300
|
||||
reconnect_attempts=0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Main menu
|
||||
main_menu() {
|
||||
echo -e "${GREEN}Options:${NC}"
|
||||
echo -e " ${CYAN}1${NC} - Connect VPN"
|
||||
echo -e " ${CYAN}2${NC} - Disconnect VPN"
|
||||
echo -e " ${CYAN}3${NC} - Show VPN status"
|
||||
echo -e " ${CYAN}4${NC} - Setup IP forwarding only"
|
||||
echo -e " ${CYAN}5${NC} - Test connection to $TARGET_IP"
|
||||
echo -e " ${CYAN}6${NC} - Show network status"
|
||||
echo -e " ${CYAN}7${NC} - Show routing table"
|
||||
echo -e " ${CYAN}8${NC} - Show live TOTP"
|
||||
echo -e " ${CYAN}q${NC} - Quit"
|
||||
echo ""
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-c|--connect)
|
||||
DO_CONNECT=true
|
||||
shift
|
||||
;;
|
||||
-d|--disconnect)
|
||||
DO_DISCONNECT=true
|
||||
shift
|
||||
;;
|
||||
-m|--menu)
|
||||
SKIP_AUTO_CONNECT=true
|
||||
shift
|
||||
;;
|
||||
-s|--status)
|
||||
print_banner
|
||||
check_vpn_status
|
||||
echo ""
|
||||
show_network_status
|
||||
exit 0
|
||||
;;
|
||||
-r|--routes)
|
||||
show_routes
|
||||
exit 0
|
||||
;;
|
||||
--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Use --help for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$DO_CONNECT" = "true" ] && [ "$DO_DISCONNECT" = "true" ]; then
|
||||
echo "Error: --connect and --disconnect are mutually exclusive"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
parse_args "$@"
|
||||
cleanup_old_logs
|
||||
|
||||
echo "" >> "$(get_log_file)"
|
||||
echo "========================================" >> "$(get_log_file)"
|
||||
log INFO "=== Starting OpenConnect-SSO VPN ==="
|
||||
echo ""
|
||||
log DEBUG "VPN_EMAIL=$VPN_EMAIL"
|
||||
log DEBUG "VPN_HOST=$VPN_HOST"
|
||||
log DEBUG "TARGET_IP=$TARGET_IP"
|
||||
log DEBUG "TARGET_SUBNET=$TARGET_SUBNET"
|
||||
log DEBUG "VPN_TOTP_SECRET is $([ -n "$VPN_TOTP_SECRET" ] && echo 'set' || echo 'NOT SET')"
|
||||
|
||||
print_banner
|
||||
|
||||
if [ "$DO_DISCONNECT" = "true" ]; then
|
||||
disconnect_vpn
|
||||
exit $?
|
||||
fi
|
||||
|
||||
if [ "$DO_CONNECT" = "true" ]; then
|
||||
connect_vpn
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# Auto-connect logic (unless -m flag)
|
||||
if [ "$SKIP_AUTO_CONNECT" = "true" ]; then
|
||||
log INFO "Menu mode - skipping auto-connect"
|
||||
elif check_vpn_status; then
|
||||
echo ""
|
||||
log INFO "VPN already connected. Setting up forwarding..."
|
||||
setup_forwarding
|
||||
else
|
||||
echo ""
|
||||
log INFO "Auto-starting VPN connection..."
|
||||
echo ""
|
||||
connect_vpn
|
||||
fi
|
||||
|
||||
# Interactive menu loop
|
||||
while true; do
|
||||
echo ""
|
||||
main_menu
|
||||
echo -ne "${CYAN}Choice: ${NC}"
|
||||
read -r choice
|
||||
echo ""
|
||||
|
||||
[[ -z "${choice// }" ]] && continue
|
||||
|
||||
case $choice in
|
||||
1) connect_vpn ;;
|
||||
2) disconnect_vpn ;;
|
||||
3) check_vpn_status ;;
|
||||
4) setup_forwarding ;;
|
||||
5) test_connection ;;
|
||||
6) show_network_status ;;
|
||||
7) show_routes ;;
|
||||
8) show_totp ;;
|
||||
q|Q) log INFO "Goodbye!"; exit 0 ;;
|
||||
*) ;;
|
||||
esac
|
||||
done
|
||||
12
apps/cistech-tunnel/shared/startup-vnc.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
export HOME='/root'
|
||||
export USER='root'
|
||||
rm -f /tmp/.P1-lock /tmp/.X11-unix/X1 2>/dev/null || true
|
||||
rm -rf /tmp/.X*-lock /tmp/.X14-unix/* 2>/dev/null || true
|
||||
echo "Starting TigerVNC server on display :1..."
|
||||
vncserver :1 -geometry 1280x800 -depth 24 -SecurityTypes VncAuth -localhost no
|
||||
sleep 2
|
||||
echo "Starting noVNC on port ${NOVNC_PORT:-6092}..."
|
||||
websockify --web=/usr/share/novnc/ ${NOVNC_PORT:-6092} localhost:${VNC_PORT:-5901} &
|
||||
tail -f /root/.vnc/*.log
|
||||
24
apps/cistech-tunnel/shared/uninstall-host-services.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Uninstall host-side systemd services for cistech-tunnel
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
echo "Removing cistech-tunnel host services..."
|
||||
|
||||
# Stop and disable the watcher
|
||||
sudo systemctl stop cistech-routing-watcher.path 2>/dev/null || true
|
||||
sudo systemctl disable cistech-routing-watcher.path 2>/dev/null || true
|
||||
|
||||
# Remove routing rules
|
||||
/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/host-routing.sh stop 2>/dev/null || true
|
||||
|
||||
# Remove systemd units
|
||||
sudo rm -f /etc/systemd/system/cistech-routing-watcher.path
|
||||
sudo rm -f /etc/systemd/system/cistech-routing-watcher.service
|
||||
|
||||
# Reload systemd
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
echo ""
|
||||
echo "Done! Host services removed."
|
||||
53
apps/cistech-tunnel/shared/xstartup
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
# VNC xstartup - launches terminal with cisco-vpn script
|
||||
|
||||
unset SESSION_MANAGER
|
||||
unset DBUS_SESSION_BUS_ADDRESS
|
||||
|
||||
# Import environment variables from container (PID 1)
|
||||
# Systemd services don't inherit Docker env vars, so we source them here
|
||||
while IFS= read -r -d '' line; do
|
||||
export "$line"
|
||||
done < /proc/1/environ
|
||||
|
||||
export XDG_RUNTIME_DIR=/tmp/runtime-root
|
||||
mkdir -p $XDG_RUNTIME_DIR
|
||||
chmod 700 $XDG_RUNTIME_DIR
|
||||
|
||||
# Force software rendering (no GPU/OpenGL)
|
||||
export QT_QUICK_BACKEND=software
|
||||
export LIBGL_ALWAYS_SOFTWARE=1
|
||||
export GALLIUM_DRIVER=llvmpipe
|
||||
export MESA_GL_VERSION_OVERRIDE=3.3
|
||||
|
||||
# GPU/WebKit workarounds for Cisco UI
|
||||
export GDK_BACKEND=x11
|
||||
export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
|
||||
# Qt/Chromium flags for running as root (no sandbox)
|
||||
export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu"
|
||||
export QTWEBENGINE_DISABLE_SANDBOX=1
|
||||
|
||||
# Start system dbus daemon (needed for Chromium)
|
||||
mkdir -p /run/dbus
|
||||
dbus-daemon --system --fork 2>/dev/null || true
|
||||
|
||||
# Start dbus session
|
||||
[ -x /usr/bin/dbus-launch ] && eval $(dbus-launch --sh-syntax --exit-with-session)
|
||||
|
||||
# Start window manager
|
||||
openbox &
|
||||
sleep 2
|
||||
|
||||
# Disable screen blanking and power saving
|
||||
xset s off 2>/dev/null || true
|
||||
xset -dpms 2>/dev/null || true
|
||||
xset s noblank 2>/dev/null || true
|
||||
|
||||
# Make script executable and launch in terminal
|
||||
chmod +x /shared/openconnect-vpn 2>/dev/null || true
|
||||
xterm -fn fixed -bg black -fg white -geometry 130x45+10+10 \
|
||||
-title "Cistech VPN Terminal" \
|
||||
-e "bash -c '/shared/openconnect-vpn; exec bash'" &
|
||||
|
||||
wait
|
||||
@@ -1,46 +0,0 @@
|
||||
FROM ubuntu:24.04
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
|
||||
VIRTUAL_ENV=/opt/venv \
|
||||
PATH=/opt/venv/bin:$PATH \
|
||||
QTWEBENGINE_DISABLE_SANDBOX=1 \
|
||||
QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu" \
|
||||
OC_URL="https://vpn.cistech.net/Employees" \
|
||||
OC_SERVERCERT="pin-sha256:HyHob3LiVmIp8ch9AzHJ9jMYqI43tO5N13oWeBLiZ/0=" \
|
||||
OC_USER="alex.zaw@cistech.net" \
|
||||
OC_TOTP_SECRET="t6ypnjqvyx2yvw2l" \
|
||||
VNC_PASSWORD="Az@83278327\$\$@@"
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
openconnect iproute2 iptables ca-certificates \
|
||||
python3 python3-pip python3-venv \
|
||||
vpnc-scripts curl wget openssh-client \
|
||||
x11vnc xvfb fluxbox novnc websockify xterm nano oathtool \
|
||||
xauth libnss3 libatk1.0-0 libatk-bridge2.0-0 \
|
||||
libx11-6 libx11-xcb1 libxcomposite1 libxrandr2 libgbm1 libxdamage1 \
|
||||
libpango-1.0-0 fonts-liberation \
|
||||
libegl1 libgl1 libopengl0 libdbus-1-3 libglib2.0-0 \
|
||||
libxkbcommon0 libxkbcommon-x11-0 \
|
||||
libxcb1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render0 libxcb-render-util0 libxcb-shm0 libxcb-xfixes0 libxcb-xinerama0 libxcb-randr0 libxcb-glx0 \
|
||||
sudo && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN apt-get update && (apt-get install -y libasound2t64 || apt-get install -y libasound2) && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Python venv + Playwright + openconnect-sso
|
||||
RUN python3 -m venv "$VIRTUAL_ENV"
|
||||
RUN pip install --no-cache-dir openconnect-sso playwright keyring keyrings.alt && \
|
||||
python -m playwright install --with-deps chromium
|
||||
|
||||
# Cloudflared (amd64)
|
||||
RUN arch=$(dpkg --print-architecture) && \
|
||||
if [ "$arch" = "amd64" ]; then \
|
||||
curl -fsSL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o /tmp/cloudflared.deb && \
|
||||
apt-get update && apt-get install -y /tmp/cloudflared.deb && rm -f /tmp/cloudflared.deb ; \
|
||||
else \
|
||||
echo "Install cloudflared manually for arch=$arch" && exit 1 ; \
|
||||
fi
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
EXPOSE 6901
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -1,182 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
: "${OC_URL:?OC_URL required}"
|
||||
: "${OC_SERVERCERT:?OC_SERVERCERT required}"
|
||||
|
||||
NOVNC_PORT="${NOVNC_PORT:-6901}"
|
||||
VNC_PASSWORD="${VNC_PASSWORD:-changeme}"
|
||||
DISPLAY_ADDR="${DISPLAY:-:1}"
|
||||
OC_INTERFACE="${OC_INTERFACE:-tun0}"
|
||||
OC_USER="${OC_USER:-}"
|
||||
OC_TOTP_SECRET="${OC_TOTP_SECRET:-}"
|
||||
|
||||
# Default to hidden browser if OC_USER is set
|
||||
if [[ -n "$OC_USER" ]]; then
|
||||
OC_SSO_ARGS_DEFAULT="--browser-display-mode hidden -u $OC_USER"
|
||||
else
|
||||
OC_SSO_ARGS_DEFAULT="--browser-display-mode shown"
|
||||
fi
|
||||
|
||||
CLOUDFLARED_MODE="${CLOUDFLARED_MODE:-off}" # off|token|config
|
||||
CLOUDFLARED_TOKEN="${CLOUDFLARED_TOKEN:-}"
|
||||
SSH_TUNNEL_ENABLE="${SSH_TUNNEL_ENABLE:-0}"
|
||||
SSH_DEST="${SSH_DEST:-zawa@10.3.1.201}"
|
||||
SSH_FORWARDS="${SSH_FORWARDS:-0.0.0.0:8090:localhost:8090}"
|
||||
|
||||
pids=()
|
||||
|
||||
# Setup keyring with TOTP secret if provided
|
||||
setup_keyring() {
|
||||
if [[ -n "$OC_TOTP_SECRET" && -n "$OC_USER" ]]; then
|
||||
python3 -c "
|
||||
import keyring
|
||||
keyring.set_password('openconnect-sso', 'totp/$OC_USER', '$OC_TOTP_SECRET'.upper())
|
||||
print('TOTP secret stored in keyring for $OC_USER')
|
||||
"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create vpn_connect command in PATH and save environment
|
||||
create_vpn_command() {
|
||||
# Save environment variables to a file
|
||||
cat > /etc/vpn.env << ENVFILE
|
||||
export OC_URL="$OC_URL"
|
||||
export OC_SERVERCERT="$OC_SERVERCERT"
|
||||
export OC_INTERFACE="$OC_INTERFACE"
|
||||
export OC_USER="$OC_USER"
|
||||
export OC_SSO_ARGS_DEFAULT="$OC_SSO_ARGS_DEFAULT"
|
||||
export OC_SSO_ARGS="${OC_SSO_ARGS:-$OC_SSO_ARGS_DEFAULT}"
|
||||
export OC_AUTHGROUP="${OC_AUTHGROUP:-}"
|
||||
export OC_USERAGENT="${OC_USERAGENT:-}"
|
||||
export OC_EXTRA_ARGS="${OC_EXTRA_ARGS:-}"
|
||||
export OC_TOTP_SECRET="$OC_TOTP_SECRET"
|
||||
export DISPLAY=":1"
|
||||
ENVFILE
|
||||
|
||||
# Build openconnect command
|
||||
OPENCONNECT_CMD="/usr/sbin/openconnect --protocol=anyconnect --servercert $OC_SERVERCERT --interface $OC_INTERFACE --script /usr/share/vpnc-scripts/vpnc-script"
|
||||
[[ -n "${OC_AUTHGROUP:-}" ]] && OPENCONNECT_CMD+=" --authgroup $OC_AUTHGROUP"
|
||||
[[ -n "${OC_USERAGENT:-}" ]] && OPENCONNECT_CMD+=" --useragent $OC_USERAGENT"
|
||||
[[ -n "${OC_EXTRA_ARGS:-}" ]] && OPENCONNECT_CMD+=" ${OC_EXTRA_ARGS}"
|
||||
echo "export OPENCONNECT_CMD=\"$OPENCONNECT_CMD\"" >> /etc/vpn.env
|
||||
|
||||
cat > /usr/local/bin/vpn_connect << 'VPNCMD'
|
||||
#!/usr/bin/env bash
|
||||
source /etc/vpn.env
|
||||
echo "[$(date)] Starting VPN connection..."
|
||||
|
||||
# openconnect-sso reads TOTP from keyring automatically
|
||||
if [[ -n "$OC_USER" ]]; then
|
||||
echo "" | openconnect-sso -s "$OC_URL" ${OC_SSO_ARGS:-$OC_SSO_ARGS_DEFAULT} -- $OPENCONNECT_CMD
|
||||
else
|
||||
openconnect-sso -s "$OC_URL" ${OC_SSO_ARGS:-$OC_SSO_ARGS_DEFAULT} -- $OPENCONNECT_CMD
|
||||
fi
|
||||
VPNCMD
|
||||
chmod +x /usr/local/bin/vpn_connect
|
||||
}
|
||||
|
||||
# Create VPN runner script that keeps shell open
|
||||
create_vpn_script() {
|
||||
cat > /tmp/vpn-runner.sh << 'VPNSCRIPT'
|
||||
#!/usr/bin/env bash
|
||||
cd /root
|
||||
|
||||
echo "============================================"
|
||||
echo " Cistech VPN Container"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " vpn_connect - Start/restart VPN connection"
|
||||
echo " Ctrl+C - Stop auto-reconnect and drop to shell"
|
||||
echo ""
|
||||
echo "Starting VPN with auto-reconnect..."
|
||||
echo ""
|
||||
|
||||
while true; do
|
||||
vpn_connect
|
||||
echo ""
|
||||
echo "[$(date)] VPN disconnected. Reconnecting in 10 seconds..."
|
||||
echo "(Press Ctrl+C to stop auto-reconnect)"
|
||||
sleep 10
|
||||
done
|
||||
VPNSCRIPT
|
||||
chmod +x /tmp/vpn-runner.sh
|
||||
}
|
||||
|
||||
start_gui() {
|
||||
mkdir -p /root/.vnc
|
||||
x11vnc -storepasswd "$VNC_PASSWORD" /root/.vnc/pass >/dev/null 2>&1 || true
|
||||
rm -f /tmp/.X1-lock /tmp/.X11-unix/X1 2>/dev/null || true
|
||||
Xvfb "$DISPLAY_ADDR" -screen 0 ${XVFB_WxHxD:-1280x800x24} +extension RANDR &
|
||||
pids+=($!)
|
||||
sleep 0.5
|
||||
export DISPLAY="$DISPLAY_ADDR"
|
||||
fluxbox >/tmp/fluxbox.log 2>&1 &
|
||||
pids+=($!)
|
||||
x11vnc -display "$DISPLAY_ADDR" -rfbauth /root/.vnc/pass -forever -shared -rfbport 5900 -quiet &
|
||||
pids+=($!)
|
||||
websockify --web=/usr/share/novnc/ 0.0.0.0:"$NOVNC_PORT" localhost:5900 >/tmp/websockify.log 2>&1 &
|
||||
pids+=($!)
|
||||
}
|
||||
|
||||
start_vpn_terminal() {
|
||||
# Start xterm with VPN script
|
||||
sleep 1
|
||||
xterm -fa 'Monospace' -fs 11 -bg black -fg white -geometry 120x35+50+50 \
|
||||
-T "Cistech VPN" -e /tmp/vpn-runner.sh &
|
||||
pids+=($!)
|
||||
}
|
||||
|
||||
start_cloudflared() {
|
||||
case "$CLOUDFLARED_MODE" in
|
||||
token)
|
||||
[ -n "$CLOUDFLARED_TOKEN" ] && cloudflared tunnel run --token "$CLOUDFLARED_TOKEN" >/tmp/cloudflared.log 2>&1 &
|
||||
pids+=($!)
|
||||
;;
|
||||
config)
|
||||
cloudflared --no-autoupdate --config /etc/cloudflared/config.yml tunnel run >/tmp/cloudflared.log 2>&1 &
|
||||
pids+=($!)
|
||||
;;
|
||||
off|*)
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
start_ssh_tunnel() {
|
||||
[ "$SSH_TUNNEL_ENABLE" = "1" ] || return 0
|
||||
IFS=',' read -ra LINES <<< "$SSH_FORWARDS"
|
||||
args=(-N -o StrictHostKeyChecking=no -o ServerAliveInterval=60)
|
||||
for m in "${LINES[@]}"; do args+=(-L "$m"); done
|
||||
ssh "${args[@]}" "$SSH_DEST" &
|
||||
pids+=($!)
|
||||
}
|
||||
|
||||
setup_nat() {
|
||||
(
|
||||
for i in {1..60}; do
|
||||
if ip link show "$OC_INTERFACE" >/dev/null 2>&1; then
|
||||
sysctl -w net.ipv4.ip_forward=1 >/dev/null
|
||||
iptables -t nat -C POSTROUTING -o "$OC_INTERFACE" -j MASQUERADE 2>/dev/null || \
|
||||
iptables -t nat -A POSTROUTING -o "$OC_INTERFACE" -j MASQUERADE
|
||||
echo "NAT enabled on $OC_INTERFACE"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
) &
|
||||
}
|
||||
|
||||
trap 'kill 0' INT TERM
|
||||
|
||||
# Always start GUI now
|
||||
setup_keyring
|
||||
create_vpn_command
|
||||
create_vpn_script
|
||||
start_gui
|
||||
start_vpn_terminal
|
||||
setup_nat
|
||||
start_cloudflared
|
||||
start_ssh_tunnel
|
||||
|
||||
wait
|
||||
54
apps/nas-samba/config.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"$schema": "../app-info-schema.json",
|
||||
"name": "NAS Samba + CloudNAS",
|
||||
"id": "nas-samba",
|
||||
"available": true,
|
||||
"port": 8080,
|
||||
"exposable": true,
|
||||
"dynamic_config": true,
|
||||
"no_gui": false,
|
||||
"tipi_version": 2,
|
||||
"version": "1.0.0",
|
||||
"categories": ["network", "utilities"],
|
||||
"description": "All-in-one NAS container with Samba SMB file sharing and CloudNAS web file manager. Browse files via web UI or connect via SMB at \\\\192.168.0.15\\data with username alexz. No database required — uses filesystem + SQLite.",
|
||||
"short_desc": "NAS with SMB + web file manager",
|
||||
"author": "alexz",
|
||||
"source": "https://gits.alexzaw.dev/alexz/runtipi",
|
||||
"form_fields": [
|
||||
{
|
||||
"type": "password",
|
||||
"label": "SMB Password",
|
||||
"env_variable": "SMB_PASSWORD",
|
||||
"required": true,
|
||||
"hint": "Password for SMB user alexz"
|
||||
},
|
||||
{
|
||||
"type": "random",
|
||||
"label": "JWT Secret",
|
||||
"env_variable": "JWT_SECRET",
|
||||
"min": 32,
|
||||
"encoding": "hex"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "NAS Host Path",
|
||||
"env_variable": "NAS_PATH",
|
||||
"required": true,
|
||||
"default": "/nas",
|
||||
"hint": "Host path to share via SMB and web UI"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Bind IP",
|
||||
"env_variable": "BIND_IP",
|
||||
"required": true,
|
||||
"default": "192.168.0.15",
|
||||
"hint": "IP address to bind SMB ports to"
|
||||
}
|
||||
],
|
||||
"supported_architectures": ["arm64", "amd64"],
|
||||
"created_at": 1741536000000,
|
||||
"updated_at": 1742169600000,
|
||||
"deprecated": false,
|
||||
"min_tipi_version": "4.5.0"
|
||||
}
|
||||
25
apps/nas-samba/docker-compose.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
|
||||
"services": [
|
||||
{
|
||||
"name": "nas-samba",
|
||||
"image": "git.alexzaw.dev/alexz/nas-samba:1.0.0",
|
||||
"isMain": true,
|
||||
"internalPort": 8080,
|
||||
"addPorts": [
|
||||
{ "hostPort": 445, "containerPort": 445, "interface": "${BIND_IP}" },
|
||||
{ "hostPort": 139, "containerPort": 139, "interface": "${BIND_IP}" }
|
||||
],
|
||||
"volumes": [
|
||||
{ "hostPath": "${NAS_PATH:-/nas}", "containerPath": "/nas" },
|
||||
{ "hostPath": "${APP_DATA_DIR}/data/config", "containerPath": "/config" }
|
||||
],
|
||||
"environment": [
|
||||
{ "key": "SMB_PASSWORD", "value": "${SMB_PASSWORD}" },
|
||||
{ "key": "JWT_SECRET", "value": "${JWT_SECRET}" },
|
||||
{ "key": "TZ", "value": "${TZ}" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
apps/nas-samba/metadata/logo.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
51
apps/nas-samba/source/Dockerfile
Normal file
@@ -0,0 +1,51 @@
|
||||
FROM ubuntu:24.04
|
||||
|
||||
LABEL maintainer="alexz"
|
||||
LABEL description="NAS Samba + CloudNAS file manager"
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install runtime: Samba, supervisor, tini, Node.js 20.x, build tools for native modules
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
samba \
|
||||
samba-common-bin \
|
||||
supervisor \
|
||||
curl \
|
||||
ca-certificates \
|
||||
tini \
|
||||
python3 \
|
||||
make \
|
||||
g++ \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy app source
|
||||
COPY app/ /app/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install production dependencies (compiles better-sqlite3 native module)
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
# Clean up
|
||||
RUN rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man
|
||||
|
||||
# Create directories
|
||||
RUN mkdir -p /nas /config /var/log/supervisor /var/log/samba /run/samba
|
||||
|
||||
# Samba configuration
|
||||
COPY smb.conf /etc/samba/smb.conf
|
||||
|
||||
# Supervisord configuration
|
||||
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
# Entrypoint
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
EXPOSE 8080 139 445
|
||||
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD ["/entrypoint.sh"]
|
||||
13
apps/nas-samba/source/app/frontend/build/asset-manifest.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.da1289f2.css",
|
||||
"main.js": "/static/js/main.fcd0e5c7.js",
|
||||
"index.html": "/index.html",
|
||||
"main.da1289f2.css.map": "/static/css/main.da1289f2.css.map",
|
||||
"main.fcd0e5c7.js.map": "/static/js/main.fcd0e5c7.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.da1289f2.css",
|
||||
"static/js/main.fcd0e5c7.js"
|
||||
]
|
||||
}
|
||||
1
apps/nas-samba/source/app/frontend/build/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#0F172A"/><meta name="description" content="CloudSync - Enterprise File Manager"/><title>CloudSync - Enterprise File Manager</title><script defer="defer" src="/static/js/main.fcd0e5c7.js"></script><link href="/static/css/main.da1289f2.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @license React
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react-jsx-runtime.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* scheduler.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license lucide-react v0.312.0 - ISC
|
||||
*
|
||||
* This source code is licensed under the ISC license.
|
||||
* See the LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
15860
apps/nas-samba/source/app/frontend/package-lock.json
generated
Normal file
39
apps/nas-samba/source/app/frontend/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "cloudsync-frontend",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"axios": "^1.6.5",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.3.0",
|
||||
"lucide-react": "^0.312.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
6
apps/nas-samba/source/app/frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
14
apps/nas-samba/source/app/frontend/public/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#0F172A" />
|
||||
<meta name="description" content="CloudSync - Enterprise File Manager" />
|
||||
<title>CloudSync - Enterprise File Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
1
apps/nas-samba/source/app/frontend/src/App.css
Normal file
@@ -0,0 +1 @@
|
||||
/* Additional app-specific styles */
|
||||
51
apps/nas-samba/source/app/frontend/src/App.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { Toaster, toast } from 'sonner';
|
||||
import { AuthProvider, useAuth } from './context/AuthContext';
|
||||
import { ThemeProvider, useTheme } from './context/ThemeContext';
|
||||
import LoginPage from './components/LoginPage';
|
||||
import Dashboard from './components/Dashboard';
|
||||
import SharePage from './components/SharePage';
|
||||
import LoadingScreen from './components/ui/LoadingScreen';
|
||||
|
||||
function AppContent() {
|
||||
const { user, loading } = useAuth();
|
||||
|
||||
// Handle /share/:token routes without React Router
|
||||
const path = window.location.pathname;
|
||||
const shareMatch = path.match(/^\/share\/([a-f0-9]+)$/);
|
||||
if (shareMatch) {
|
||||
return <SharePage token={shareMatch[1]} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <LoginPage />;
|
||||
}
|
||||
|
||||
return <Dashboard />;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<Toaster
|
||||
position="top-right"
|
||||
richColors
|
||||
closeButton
|
||||
toastOptions={{
|
||||
style: {
|
||||
fontFamily: 'Inter, system-ui, sans-serif',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<AppContent />
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
1366
apps/nas-samba/source/app/frontend/src/components/Dashboard.js
Normal file
145
apps/nas-samba/source/app/frontend/src/components/LoginPage.js
Normal file
@@ -0,0 +1,145 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { toast } from 'sonner';
|
||||
import { Cloud, Eye, EyeOff, Loader2 } from 'lucide-react';
|
||||
|
||||
function LoginPage() {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { login } = useAuth();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!username.trim() || !password.trim()) {
|
||||
toast.error('Please enter username and password');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const user = await login(username, password);
|
||||
toast.success(`Welcome back, ${user.name}!`);
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.detail || 'Invalid credentials';
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex">
|
||||
{/* Left side - Background image */}
|
||||
<div
|
||||
className="hidden lg:flex lg:w-1/2 xl:w-3/5 relative bg-cover bg-center"
|
||||
style={{
|
||||
backgroundImage: 'url(https://images.pexels.com/photos/28428586/pexels-photo-28428586.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940)'
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/40"></div>
|
||||
<div className="relative z-10 flex flex-col justify-end p-12 text-white">
|
||||
<h1 className="font-secondary text-4xl font-bold mb-4">CloudSync</h1>
|
||||
<p className="text-lg text-white/80 max-w-md">
|
||||
Enterprise-grade file management with secure sharing, storage quotas, and comprehensive activity tracking.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - Login form */}
|
||||
<div className="flex-1 flex items-center justify-center p-8 bg-background">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Logo for mobile */}
|
||||
<div className="lg:hidden flex items-center gap-3 mb-8">
|
||||
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center">
|
||||
<Cloud className="w-5 h-5 text-primary-foreground" />
|
||||
</div>
|
||||
<span className="font-secondary text-2xl font-bold text-foreground">CloudSync</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h2 className="font-secondary text-3xl font-bold text-foreground mb-2">Sign in</h2>
|
||||
<p className="text-muted-foreground">Enter your credentials to access your files</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="username" className="block text-sm font-medium text-foreground mb-2">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="Enter your username"
|
||||
disabled={loading}
|
||||
data-testid="login-username-input"
|
||||
className="w-full h-11 px-4 rounded-lg border border-input bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-all disabled:opacity-50"
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-foreground mb-2">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter your password"
|
||||
disabled={loading}
|
||||
data-testid="login-password-input"
|
||||
className="w-full h-11 px-4 pr-12 rounded-lg border border-input bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-all disabled:opacity-50"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
||||
data-testid="toggle-password-visibility"
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
data-testid="login-submit-btn"
|
||||
className="w-full h-11 bg-primary text-primary-foreground font-medium rounded-lg hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 transition-all disabled:opacity-50 flex items-center justify-center gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Signing in...
|
||||
</>
|
||||
) : (
|
||||
'Sign in'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-8 p-4 bg-muted rounded-lg">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<span className="font-medium text-foreground">Demo credentials:</span>
|
||||
<br />
|
||||
Username: <code className="font-mono bg-background px-1 rounded">admin</code>
|
||||
<br />
|
||||
Password: <code className="font-mono bg-background px-1 rounded">admin123</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginPage;
|
||||
182
apps/nas-samba/source/app/frontend/src/components/SharePage.js
Normal file
@@ -0,0 +1,182 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Cloud, CheckCircle, Lock, FileText, AlertCircle } from 'lucide-react';
|
||||
import { formatBytes } from '../lib/utils';
|
||||
import * as api from '../lib/api';
|
||||
|
||||
export default function SharePage({ token }) {
|
||||
const [fileInfo, setFileInfo] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [password, setPassword] = useState('');
|
||||
const [downloaded, setDownloaded] = useState(false);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
|
||||
const doDownload = useCallback(async (pwd) => {
|
||||
if (!fileInfo || downloading) return;
|
||||
setDownloading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const url = api.downloadSharedFile(token, pwd || null);
|
||||
const res = await fetch(url, { method: 'POST' });
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.detail || 'Download failed');
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = blobUrl;
|
||||
a.download = fileInfo.name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
setDownloaded(true);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
setDownloading(false);
|
||||
}
|
||||
}, [fileInfo, downloading, token]);
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
try {
|
||||
const res = await api.getSharedFileInfo(token);
|
||||
setFileInfo(res.data);
|
||||
} catch (err) {
|
||||
const status = err.response?.status;
|
||||
if (status === 410) {
|
||||
setError('This share link has expired.');
|
||||
} else if (status === 404) {
|
||||
setError('Share link not found.');
|
||||
} else {
|
||||
setError('Failed to load shared file.');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
load();
|
||||
}, [token]);
|
||||
|
||||
// Auto-download if no password required
|
||||
useEffect(() => {
|
||||
if (fileInfo && !fileInfo.requires_password && !downloaded && !downloading) {
|
||||
doDownload(null);
|
||||
}
|
||||
}, [fileInfo, downloaded, downloading, doDownload]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && !fileInfo) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<div className="bg-card border border-border rounded-lg shadow-lg p-8 max-w-md w-full text-center">
|
||||
<AlertCircle className="w-12 h-12 text-destructive mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">Unavailable</h2>
|
||||
<p className="text-muted-foreground">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Downloaded state
|
||||
if (downloaded) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<div className="bg-card border border-border rounded-lg shadow-lg p-8 max-w-md w-full text-center">
|
||||
<div className="flex items-center justify-center gap-2 mb-6">
|
||||
<Cloud className="w-6 h-6 text-primary" />
|
||||
<span className="text-lg font-semibold">CloudSync</span>
|
||||
</div>
|
||||
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">File Downloaded</h2>
|
||||
<div className="flex items-center gap-3 justify-center p-3 bg-accent/50 rounded-lg mt-4">
|
||||
<FileText className="w-6 h-6 text-muted-foreground flex-shrink-0" />
|
||||
<div className="text-left min-w-0">
|
||||
<p className="font-medium text-sm truncate">{fileInfo.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{formatBytes(fileInfo.size)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-4">This link has been used and is no longer active.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Password required state
|
||||
if (fileInfo.requires_password && !downloaded) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<div className="bg-card border border-border rounded-lg shadow-lg p-8 max-w-md w-full">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Cloud className="w-6 h-6 text-primary" />
|
||||
<span className="text-lg font-semibold">CloudSync</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 mb-6 p-4 bg-accent/50 rounded-lg">
|
||||
<FileText className="w-10 h-10 text-muted-foreground flex-shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium truncate">{fileInfo.name}</p>
|
||||
<p className="text-sm text-muted-foreground">{formatBytes(fileInfo.size)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-destructive/10 text-destructive text-sm rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
<Lock className="w-4 h-4 inline mr-1" />
|
||||
This file is password protected
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => { setPassword(e.target.value); setError(null); }}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' && password) doDownload(password); }}
|
||||
placeholder="Enter password"
|
||||
className="w-full h-10 px-4 bg-background border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => doDownload(password)}
|
||||
disabled={downloading || !password}
|
||||
className="w-full h-10 bg-primary text-primary-foreground rounded-lg hover:opacity-90 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{downloading ? (
|
||||
<div className="animate-spin w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full"></div>
|
||||
) : (
|
||||
'Download'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Downloading state (auto-download in progress, no password)
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<div className="bg-card border border-border rounded-lg shadow-lg p-8 max-w-md w-full text-center">
|
||||
<div className="flex items-center justify-center gap-2 mb-6">
|
||||
<Cloud className="w-6 h-6 text-primary" />
|
||||
<span className="text-lg font-semibold">CloudSync</span>
|
||||
</div>
|
||||
<div className="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full mx-auto mb-4"></div>
|
||||
<p className="text-muted-foreground">Downloading {fileInfo.name}...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
export function LoadingScreen() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="relative w-12 h-12">
|
||||
<div className="absolute inset-0 rounded-full border-2 border-muted"></div>
|
||||
<div className="absolute inset-0 rounded-full border-2 border-primary border-t-transparent animate-spin"></div>
|
||||
</div>
|
||||
<p className="text-muted-foreground font-medium">Loading CloudSync...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoadingScreen;
|
||||
@@ -0,0 +1,69 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import { login as apiLogin, getMe } from '../lib/api';
|
||||
|
||||
const AuthContext = createContext(null);
|
||||
|
||||
export function AuthProvider({ children }) {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [token, setToken] = useState(localStorage.getItem('token'));
|
||||
|
||||
const checkAuth = useCallback(async () => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
if (!storedToken) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getMe();
|
||||
setUser(response.data);
|
||||
setToken(storedToken);
|
||||
} catch (error) {
|
||||
localStorage.removeItem('token');
|
||||
setUser(null);
|
||||
setToken(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth();
|
||||
}, [checkAuth]);
|
||||
|
||||
const login = async (username, password) => {
|
||||
const response = await apiLogin(username, password);
|
||||
const { token: newToken, user: userData } = response.data;
|
||||
|
||||
localStorage.setItem('token', newToken);
|
||||
setToken(newToken);
|
||||
setUser(userData);
|
||||
|
||||
return userData;
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
const updateUser = useCallback((userData) => {
|
||||
setUser(prev => ({ ...prev, ...userData }));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, token, loading, login, logout, updateUser, checkAuth }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
|
||||
const ThemeContext = createContext(null);
|
||||
|
||||
export function ThemeProvider({ children }) {
|
||||
const [theme, setTheme] = useState(() => {
|
||||
const stored = localStorage.getItem('theme');
|
||||
if (stored) return stored;
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
root.classList.remove('light', 'dark');
|
||||
root.classList.add(theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prev => prev === 'light' ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
116
apps/nas-samba/source/app/frontend/src/index.css
Normal file
@@ -0,0 +1,116 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--background: #FFFFFF;
|
||||
--foreground: #0A0A0A;
|
||||
--card: #FFFFFF;
|
||||
--card-foreground: #0A0A0A;
|
||||
--popover: #FFFFFF;
|
||||
--popover-foreground: #0A0A0A;
|
||||
--primary: #0F172A;
|
||||
--primary-foreground: #F8FAFC;
|
||||
--secondary: #F1F5F9;
|
||||
--secondary-foreground: #0F172A;
|
||||
--muted: #F1F5F9;
|
||||
--muted-foreground: #64748B;
|
||||
--accent: #F1F5F9;
|
||||
--accent-foreground: #0F172A;
|
||||
--destructive: #EF4444;
|
||||
--destructive-foreground: #F8FAFC;
|
||||
--border: #E2E8F0;
|
||||
--input: #E2E8F0;
|
||||
--ring: #0F172A;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #020617;
|
||||
--foreground: #F8FAFC;
|
||||
--card: #0F172A;
|
||||
--card-foreground: #F8FAFC;
|
||||
--popover: #0F172A;
|
||||
--popover-foreground: #F8FAFC;
|
||||
--primary: #F8FAFC;
|
||||
--primary-foreground: #0F172A;
|
||||
--secondary: #1E293B;
|
||||
--secondary-foreground: #F8FAFC;
|
||||
--muted: #1E293B;
|
||||
--muted-foreground: #94A3B8;
|
||||
--accent: #1E293B;
|
||||
--accent-foreground: #F8FAFC;
|
||||
--destructive: #7F1D1D;
|
||||
--destructive-foreground: #F8FAFC;
|
||||
--border: #1E293B;
|
||||
--input: #1E293B;
|
||||
--ring: #D1D5DB;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--muted-foreground);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--foreground);
|
||||
}
|
||||
|
||||
/* Skeleton loader animation */
|
||||
@keyframes skeleton-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
||||
background: linear-gradient(90deg, var(--muted) 25%, var(--border) 50%, var(--muted) 75%);
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
|
||||
/* File drop zone styles */
|
||||
.drop-zone-active {
|
||||
border-color: var(--ring) !important;
|
||||
background-color: var(--accent) !important;
|
||||
}
|
||||
|
||||
/* Focus visible styles */
|
||||
.focus-visible:focus {
|
||||
outline: 2px solid var(--ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Transition utilities */
|
||||
.transition-default {
|
||||
transition: all 200ms ease-in-out;
|
||||
}
|
||||
11
apps/nas-samba/source/app/frontend/src/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
136
apps/nas-samba/source/app/frontend/src/lib/api.js
Normal file
@@ -0,0 +1,136 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.REACT_APP_BACKEND_URL || '';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor to add auth token
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// Response interceptor to handle auth errors
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
window.location.reload();
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Auth
|
||||
export const login = (username, password) =>
|
||||
api.post('/api/auth/login', { username, password });
|
||||
|
||||
export const getMe = () =>
|
||||
api.get('/api/auth/me');
|
||||
|
||||
// Users
|
||||
export const getUsers = () =>
|
||||
api.get('/api/users');
|
||||
|
||||
export const createUser = (userData) =>
|
||||
api.post('/api/users', userData);
|
||||
|
||||
export const updateUser = (userId, userData) =>
|
||||
api.put(`/api/users/${userId}`, userData);
|
||||
|
||||
export const deleteUser = (userId) =>
|
||||
api.delete(`/api/users/${userId}`);
|
||||
|
||||
// Files
|
||||
export const getFiles = (parentId = null, search = null) => {
|
||||
const params = new URLSearchParams();
|
||||
if (parentId) params.append('parent_id', parentId);
|
||||
if (search) params.append('search', search);
|
||||
return api.get(`/api/files?${params.toString()}`);
|
||||
};
|
||||
|
||||
export const getFile = (fileId) =>
|
||||
api.get(`/api/files/${fileId}`);
|
||||
|
||||
export const createFolder = (name, parentId = null) =>
|
||||
api.post('/api/files/folder', { name, parent_id: parentId, is_folder: true });
|
||||
|
||||
export const uploadFile = (file, parentId, onProgress) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
if (parentId) {
|
||||
formData.append('parent_id', parentId);
|
||||
}
|
||||
|
||||
return api.post('/api/files/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (onProgress) {
|
||||
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
onProgress(progress);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const renameFile = (fileId, newName) =>
|
||||
api.put(`/api/files/${fileId}/rename`, { new_name: newName });
|
||||
|
||||
export const moveFile = (fileId, destinationId) =>
|
||||
api.put(`/api/files/${fileId}/move`, { destination_id: destinationId });
|
||||
|
||||
export const copyFile = (fileId, destinationId) =>
|
||||
api.post(`/api/files/${fileId}/copy`, { destination_id: destinationId });
|
||||
|
||||
export const deleteFile = (fileId) =>
|
||||
api.delete(`/api/files/${fileId}`);
|
||||
|
||||
export const bulkDeleteFiles = (fileIds) =>
|
||||
api.post('/api/files/bulk-delete', fileIds);
|
||||
|
||||
export const getDownloadUrl = (fileId) =>
|
||||
`${API_URL}/api/files/${fileId}/download`;
|
||||
|
||||
export const getPreviewUrl = (fileId) =>
|
||||
`${API_URL}/api/files/${fileId}/preview`;
|
||||
|
||||
export const getBreadcrumb = (fileId) =>
|
||||
api.get(`/api/files/${fileId}/breadcrumb`);
|
||||
|
||||
// Shares
|
||||
export const getShares = () =>
|
||||
api.get('/api/shares');
|
||||
|
||||
export const createShare = (fileId, expiresInHours = 24, password = null) =>
|
||||
api.post('/api/shares', { file_id: fileId, expires_in_hours: expiresInHours, password });
|
||||
|
||||
export const deleteShare = (shareId) =>
|
||||
api.delete(`/api/shares/${shareId}`);
|
||||
|
||||
export const getSharedFileInfo = (token) =>
|
||||
api.get(`/api/share/${token}`);
|
||||
|
||||
export const downloadSharedFile = (token, password = null) => {
|
||||
const params = password ? `?password=${encodeURIComponent(password)}` : '';
|
||||
return `${API_URL}/api/share/${token}/download${params}`;
|
||||
};
|
||||
|
||||
// Activity
|
||||
export const getActivity = (limit = 50) =>
|
||||
api.get(`/api/activity?limit=${limit}`);
|
||||
|
||||
// Stats
|
||||
export const getStats = () =>
|
||||
api.get('/api/stats');
|
||||
|
||||
export default api;
|
||||
33
apps/nas-samba/source/app/frontend/src/lib/utils.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function formatBytes(bytes, decimals = 1) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
export function formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diff = Math.floor((now - date) / 1000);
|
||||
|
||||
if (diff < 60) return 'Just now';
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
||||
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
|
||||
|
||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
}
|
||||
|
||||
export function getFileExtension(filename) {
|
||||
return filename.includes('.') ? filename.split('.').pop().toLowerCase() : '';
|
||||
}
|
||||
60
apps/nas-samba/source/app/frontend/tailwind.config.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: "var(--background)",
|
||||
foreground: "var(--foreground)",
|
||||
card: "var(--card)",
|
||||
"card-foreground": "var(--card-foreground)",
|
||||
popover: "var(--popover)",
|
||||
"popover-foreground": "var(--popover-foreground)",
|
||||
primary: "var(--primary)",
|
||||
"primary-foreground": "var(--primary-foreground)",
|
||||
secondary: "var(--secondary)",
|
||||
"secondary-foreground": "var(--secondary-foreground)",
|
||||
muted: "var(--muted)",
|
||||
"muted-foreground": "var(--muted-foreground)",
|
||||
accent: "var(--accent)",
|
||||
"accent-foreground": "var(--accent-foreground)",
|
||||
destructive: "var(--destructive)",
|
||||
"destructive-foreground": "var(--destructive-foreground)",
|
||||
border: "var(--border)",
|
||||
input: "var(--input)",
|
||||
ring: "var(--ring)",
|
||||
},
|
||||
fontFamily: {
|
||||
primary: ['Inter', 'system-ui', 'sans-serif'],
|
||||
secondary: ['Manrope', 'system-ui', 'sans-serif'],
|
||||
mono: ['JetBrains Mono', 'monospace'],
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.3s ease-out',
|
||||
'slide-up': 'slideUp 0.3s ease-out',
|
||||
'slide-down': 'slideDown 0.3s ease-out',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { opacity: '0', transform: 'translateY(10px)' },
|
||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
slideDown: {
|
||||
'0%': { opacity: '0', transform: 'translateY(-10px)' },
|
||||
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
2558
apps/nas-samba/source/app/package-lock.json
generated
Normal file
21
apps/nas-samba/source/app/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "cloudsync",
|
||||
"version": "1.0.0",
|
||||
"description": "CloudSync File Manager - NAS Edition",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node --watch server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"multer": "^1.4.5-lts.1"
|
||||
}
|
||||
}
|
||||
1499
apps/nas-samba/source/app/server.js
Normal file
33
apps/nas-samba/source/entrypoint.sh
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Create users group if it doesn't exist
|
||||
getent group users >/dev/null 2>&1 || groupadd -g 100 users
|
||||
|
||||
# Create alexz user with UID 1000 if not exists
|
||||
if ! id -u alexz >/dev/null 2>&1; then
|
||||
# Remove any existing user with UID 1000 (e.g. ubuntu)
|
||||
existing_user=$(getent passwd 1000 | cut -d: -f1)
|
||||
if [ -n "$existing_user" ] && [ "$existing_user" != "alexz" ]; then
|
||||
userdel "$existing_user" 2>/dev/null || true
|
||||
fi
|
||||
useradd -u 1000 -g users -M -s /usr/sbin/nologin alexz
|
||||
echo "Created user alexz (UID 1000)"
|
||||
fi
|
||||
|
||||
# Set Samba password from environment variable
|
||||
if [ -n "$SMB_PASSWORD" ]; then
|
||||
echo -e "${SMB_PASSWORD}\n${SMB_PASSWORD}" | smbpasswd -a -s alexz
|
||||
echo "Samba password set for alexz"
|
||||
else
|
||||
echo "WARNING: SMB_PASSWORD not set. Samba authentication will fail."
|
||||
fi
|
||||
|
||||
# Ensure directories exist
|
||||
mkdir -p /config /var/log/samba /run/samba /nas
|
||||
|
||||
# Ensure correct ownership
|
||||
chown alexz:users /nas 2>/dev/null || true
|
||||
|
||||
echo "Starting supervisord..."
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||
18
apps/nas-samba/source/smb.conf
Normal file
@@ -0,0 +1,18 @@
|
||||
[global]
|
||||
workgroup = WORKGROUP
|
||||
server string = NAS Samba
|
||||
server role = standalone server
|
||||
log file = /var/log/samba/log.%m
|
||||
max log size = 50
|
||||
logging = file
|
||||
map to guest = bad user
|
||||
server min protocol = SMB2
|
||||
server max protocol = SMB3
|
||||
|
||||
[data]
|
||||
path = /nas
|
||||
valid users = alexz
|
||||
browsable = yes
|
||||
writable = yes
|
||||
create mask = 0664
|
||||
directory mask = 0775
|
||||
34
apps/nas-samba/source/supervisord.conf
Normal file
@@ -0,0 +1,34 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
user=root
|
||||
logfile=/var/log/supervisor/supervisord.log
|
||||
pidfile=/var/run/supervisord.pid
|
||||
childlogdir=/var/log/supervisor
|
||||
|
||||
[program:smbd]
|
||||
command=/usr/sbin/smbd -F --no-process-group
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=10
|
||||
stdout_logfile=/var/log/supervisor/smbd-stdout.log
|
||||
stderr_logfile=/var/log/supervisor/smbd-stderr.log
|
||||
|
||||
[program:nmbd]
|
||||
command=/usr/sbin/nmbd -F --no-process-group
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=10
|
||||
stdout_logfile=/var/log/supervisor/nmbd-stdout.log
|
||||
stderr_logfile=/var/log/supervisor/nmbd-stderr.log
|
||||
|
||||
[program:cloudnas]
|
||||
command=node /app/server.js
|
||||
directory=/app
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=20
|
||||
environment=PORT="8080",UPLOAD_DIR="/nas",DB_PATH="/config/cloudsync.db",JWT_SECRET="%(ENV_JWT_SECRET)s",SMB_PASSWORD="%(ENV_SMB_PASSWORD)s"
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
@@ -1,46 +0,0 @@
|
||||
services:
|
||||
nginx-proxy-manager:
|
||||
image: jc21/nginx-proxy-manager:2.13.5
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
nginx-proxy-manager_runtipi_network:
|
||||
gw_priority: 0
|
||||
tipi_main_network:
|
||||
gw_priority: 1
|
||||
cistech-macvlan:
|
||||
environment:
|
||||
DISABLE_IPV6: "true"
|
||||
ports:
|
||||
- ${NPM_HTTP_PORT}:80
|
||||
- ${NPM_HTTPS_PORT}:443
|
||||
- ${APP_PORT}:81
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data:/data
|
||||
- ${APP_DATA_DIR}/letsencrypt:/etc/letsencrypt
|
||||
labels:
|
||||
generated: true
|
||||
traefik.enable: true
|
||||
traefik.docker.network: runtipi_tipi_main_network
|
||||
traefik.http.middlewares.nginx-proxy-manager-runtipi-web-redirect.redirectscheme.scheme: https
|
||||
traefik.http.services.nginx-proxy-manager-runtipi.loadbalancer.server.port: "81"
|
||||
traefik.http.routers.nginx-proxy-manager-runtipi-insecure.rule: Host(`${APP_DOMAIN}`)
|
||||
traefik.http.routers.nginx-proxy-manager-runtipi-insecure.entrypoints: web
|
||||
traefik.http.routers.nginx-proxy-manager-runtipi-insecure.service: nginx-proxy-manager-runtipi
|
||||
traefik.http.routers.nginx-proxy-manager-runtipi-insecure.middlewares: nginx-proxy-manager-runtipi-web-redirect
|
||||
traefik.http.routers.nginx-proxy-manager-runtipi.rule: Host(`${APP_DOMAIN}`)
|
||||
traefik.http.routers.nginx-proxy-manager-runtipi.entrypoints: websecure
|
||||
traefik.http.routers.nginx-proxy-manager-runtipi.service: nginx-proxy-manager-runtipi
|
||||
traefik.http.routers.nginx-proxy-manager-runtipi.tls.certresolver: myresolver
|
||||
runtipi.managed: true
|
||||
runtipi.appurn: nginx-proxy-manager:runtipi
|
||||
networks:
|
||||
tipi_main_network:
|
||||
name: runtipi_tipi_main_network
|
||||
external: true
|
||||
nginx-proxy-manager_runtipi_network:
|
||||
name: nginx-proxy-manager_runtipi_network
|
||||
external: false
|
||||
cistech-macvlan:
|
||||
name: cistech-macvlan
|
||||
external: true
|
||||
|
||||
20
apps/nginx-proxy-manager/config.json → apps/npm/config.json
Executable file → Normal file
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "Nginx Proxy Manager",
|
||||
"id": "nginx-proxy-manager",
|
||||
"id": "npm",
|
||||
"available": true,
|
||||
"short_desc": "Docker container for managing Nginx proxy hosts with a simple, powerful web interface",
|
||||
"author": "jc21",
|
||||
"port": 81,
|
||||
"hostname": "nginx.alexzaw.dev",
|
||||
"categories": [
|
||||
"utilities",
|
||||
"network"
|
||||
@@ -22,7 +22,7 @@
|
||||
"type": "number",
|
||||
"label": "HTTP Port",
|
||||
"env_variable": "NPM_HTTP_PORT",
|
||||
"default": "1080",
|
||||
"default": "80",
|
||||
"required": true,
|
||||
"hint": "Port for HTTP traffic (mapped to container port 80)"
|
||||
},
|
||||
@@ -30,7 +30,15 @@
|
||||
"type": "number",
|
||||
"label": "HTTPS Port",
|
||||
"env_variable": "NPM_HTTPS_PORT",
|
||||
"default": "10443",
|
||||
"default": "443",
|
||||
"required": true,
|
||||
"hint": "Port for HTTPS traffic (mapped to container port 443)"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"label": "Web UI Port",
|
||||
"env_variable": "NPM_WEBUI_PORT",
|
||||
"default": "81",
|
||||
"required": true,
|
||||
"hint": "Port for HTTPS traffic (mapped to container port 443)"
|
||||
}
|
||||
@@ -38,7 +46,5 @@
|
||||
"supported_architectures": [
|
||||
"arm64",
|
||||
"amd64"
|
||||
],
|
||||
"created_at": 1731607800000,
|
||||
"updated_at": 1731607800000
|
||||
]
|
||||
}
|
||||
17
apps/nginx-proxy-manager/docker-compose.json → apps/npm/docker-compose.json
Executable file → Normal file
@@ -1,23 +1,28 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"services": [
|
||||
{
|
||||
"name": "nginx-proxy-manager",
|
||||
"name": "npm",
|
||||
"image": "jc21/nginx-proxy-manager:2.13.5",
|
||||
"isMain": true,
|
||||
"internalPort": 81,
|
||||
"addPorts": [
|
||||
{
|
||||
"hostPort": "${NPM_HTTP_PORT}",
|
||||
"hostPort": "192.168.1.150:${NPM_HTTP_PORT}",
|
||||
"containerPort": 80
|
||||
},
|
||||
{
|
||||
"hostPort": "${NPM_HTTPS_PORT}",
|
||||
"hostPort": "192.168.1.150:${NPM_HTTPS_PORT}",
|
||||
"containerPort": 443
|
||||
},
|
||||
{
|
||||
"hostPort": "192.168.1.150:${NPM_WEBUI_PORT}",
|
||||
"containerPort": 81
|
||||
}
|
||||
],
|
||||
"environment": {
|
||||
"DISABLE_IPV6": "true"
|
||||
},
|
||||
"environment": [
|
||||
{"key": "DISABLE_IPV6", "value": "true"}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"hostPath": "${APP_DATA_DIR}/data",
|
||||
0
apps/nginx-proxy-manager/metadata/description.md → apps/npm/metadata/description.md
Executable file → Normal file
0
apps/nginx-proxy-manager/metadata/logo.jpg → apps/npm/metadata/logo.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
0
apps/nginx-proxy-manager/metadata/logo.png → apps/npm/metadata/logo.png
Executable file → Normal file
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
2
apps/rego-tunnel/build/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Large binary files - track tar.gz but not 7z
|
||||
*.7z
|
||||
80
apps/rego-tunnel/build/Dockerfile
Executable file
@@ -0,0 +1,80 @@
|
||||
FROM ubuntu:22.04
|
||||
|
||||
LABEL maintainer="alexz"
|
||||
LABEL description="Cisco Secure Client VPN in Docker with noVNC"
|
||||
LABEL version="5.1.14.145"
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV container=docker
|
||||
|
||||
# VNC/noVNC settings
|
||||
ENV DISPLAY=:1
|
||||
ENV VNC_PORT=5901
|
||||
ENV NOVNC_PORT=6080
|
||||
|
||||
# Install systemd and dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
systemd \
|
||||
systemd-sysv \
|
||||
dbus \
|
||||
dbus-x11 \
|
||||
libgtk-3-0 \
|
||||
libglib2.0-0 \
|
||||
libstdc++6 \
|
||||
iptables \
|
||||
libxml2 \
|
||||
network-manager \
|
||||
zlib1g \
|
||||
policykit-1 \
|
||||
xdg-utils \
|
||||
libwebkit2gtk-4.0-37 \
|
||||
tigervnc-standalone-server \
|
||||
tigervnc-common \
|
||||
novnc \
|
||||
websockify \
|
||||
openbox \
|
||||
xterm \
|
||||
procps \
|
||||
net-tools \
|
||||
curl \
|
||||
iproute2 \
|
||||
iputils-ping \
|
||||
nano \
|
||||
x11vnc \
|
||||
xvfb \
|
||||
fluxbox \
|
||||
xdotool \
|
||||
oathtool \
|
||||
xclip \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Remove unnecessary systemd services that cause issues in containers
|
||||
RUN rm -f /lib/systemd/system/multi-user.target.wants/* \
|
||||
/etc/systemd/system/*.wants/* \
|
||||
/lib/systemd/system/local-fs.target.wants/* \
|
||||
/lib/systemd/system/sockets.target.wants/*udev* \
|
||||
/lib/systemd/system/sockets.target.wants/*initctl* \
|
||||
/lib/systemd/system/sysinit.target.wants/systemd-tmpfiles-setup* \
|
||||
/lib/systemd/system/systemd-update-utmp*
|
||||
|
||||
# Copy and extract Cisco Secure Client
|
||||
COPY cisco-secure-client-full.tar.gz /tmp/
|
||||
RUN tar -xzf /tmp/cisco-secure-client-full.tar.gz -C / && rm /tmp/cisco-secure-client-full.tar.gz
|
||||
|
||||
# Enable vpnagentd service
|
||||
RUN systemctl enable vpnagentd.service
|
||||
|
||||
# vnc.service - started by systemd, calls /opt/scripts/startup-vnc.sh
|
||||
# The entrypoint symlinks /shared/startup-vnc.sh -> /opt/scripts/startup-vnc.sh at runtime
|
||||
RUN mkdir -p /opt/scripts && \
|
||||
echo 'W1VuaXRdCkRlc2NyaXB0aW9uPVZOQyBhbmQgbm9WTkMgU2VydmVyCkFmdGVyPW5ldHdvcmsudGFyZ2V0IHZwbmFnZW50ZC5zZXJ2aWNlCgpbU2VydmljZV0KVHlwZT1zaW1wbGUKRXhlY1N0YXJ0PS9vcHQvc2NyaXB0cy9zdGFydHVwLXZuYy5zaApSZXN0YXJ0PWFsd2F5cwpSZXN0YXJ0U2VjPTUKRW52aXJvbm1lbnQ9SE9NRT0vcm9vdApFbnZpcm9ubWVudD1VU0VSPXJvb3QKCltJbnN0YWxsXQpXYW50ZWRCeT1tdWx0aS11c2VyLnRhcmdldAo=' \
|
||||
| base64 -d > /lib/systemd/system/vnc.service && \
|
||||
chmod 644 /lib/systemd/system/vnc.service && \
|
||||
systemctl enable vnc.service
|
||||
|
||||
VOLUME ["/sys/fs/cgroup"]
|
||||
|
||||
EXPOSE 5901 6080
|
||||
|
||||
STOPSIGNAL SIGRTMIN+3
|
||||
51
apps/rego-tunnel/build/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Rego Tunnel - Build Files
|
||||
|
||||
This directory contains the Dockerfile and scripts to build the Cisco VPN Docker image.
|
||||
|
||||
## Files
|
||||
|
||||
- `Dockerfile` - Docker image definition (Ubuntu 22.04 + Cisco Secure Client + noVNC)
|
||||
- `cisco-secure-client-full.tar.gz` - Pre-extracted Cisco Secure Client 5.1.14.145
|
||||
- `build.sh` - Build and push script
|
||||
- `scripts/entrypoint.sh` - Container entrypoint (starts systemd)
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cd /etc/runtipi/repos/runtipi/apps/rego-tunnel/build
|
||||
./build.sh
|
||||
```
|
||||
|
||||
This builds and pushes to `git.alexzaw.dev/alexz/cisco-vpn:latest`
|
||||
|
||||
To build without pushing:
|
||||
```bash
|
||||
docker build -t git.alexzaw.dev/alexz/cisco-vpn:latest .
|
||||
```
|
||||
|
||||
## What's in the image
|
||||
|
||||
The Dockerfile creates an image with:
|
||||
- Ubuntu 22.04 with systemd
|
||||
- Cisco Secure Client 5.1.14.145 (VPN, DART, Posture modules)
|
||||
- TigerVNC server + noVNC (web-based VNC)
|
||||
- Tools: xdotool, oathtool (for TOTP), xclip, openbox
|
||||
|
||||
### Systemd services (baked in)
|
||||
- `vpnagentd.service` - Cisco VPN agent
|
||||
- `vnc.service` - VNC server + noVNC websockify
|
||||
|
||||
### Scripts (baked in via base64 in Dockerfile)
|
||||
- `/opt/scripts/startup-vnc.sh` - Starts VNC server and noVNC
|
||||
- `/opt/scripts/entrypoint.sh` - Container entrypoint
|
||||
|
||||
## Runtime mounts (from shared/)
|
||||
|
||||
When running as rego-tunnel app, these are mounted from `shared/`:
|
||||
- `/shared/cisco-vpn` - Main VPN automation script
|
||||
- `/shared/xstartup` → `/root/.vnc/xstartup` - VNC session startup
|
||||
|
||||
## Ports
|
||||
|
||||
- `5901` - VNC server
|
||||
- `6080` - noVNC web interface
|
||||
22
apps/rego-tunnel/build/build.sh
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# Build and push the Cisco VPN Docker image
|
||||
# Run this from the build directory
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/cisco-vpn}"
|
||||
IMAGE_TAG="${IMAGE_TAG:-latest}"
|
||||
|
||||
echo "Building ${IMAGE_NAME}:${IMAGE_TAG}..."
|
||||
|
||||
docker build "$@" -t "${IMAGE_NAME}:${IMAGE_TAG}" .
|
||||
docker push "${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
|
||||
echo ""
|
||||
echo "Build complete!"
|
||||
echo ""
|
||||
echo "To test locally:"
|
||||
echo " docker run -d --privileged --cgroupns=host -v /sys/fs/cgroup:/sys/fs/cgroup:rw --cap-add=NET_ADMIN --device=/dev/net/tun -p 5901:5901 -p 6080:6080 ${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
echo ""
|
||||
echo "Then connect via VNC to localhost:5901 or open noVNC at http://localhost:6080/vnc.html"
|
||||
echo ""
|
||||
BIN
apps/rego-tunnel/build/cisco-secure-client-full.tar.gz
Normal file
@@ -1,69 +1,76 @@
|
||||
{
|
||||
"name": "Rego Tunnel",
|
||||
"available": true,
|
||||
"port": 8006,
|
||||
"port": 6080,
|
||||
"exposable": true,
|
||||
"dynamic_config": true,
|
||||
"id": "rego-tunnel",
|
||||
"description": "VPN tunnel to access Rego environments securely.",
|
||||
"tipi_version": 1,
|
||||
"version": "latest",
|
||||
"categories": ["utilities"],
|
||||
"short_desc": "VPN tunnel to access Rego environments securely.",
|
||||
"author": "dockurr",
|
||||
"source": "https://github.com/dockur/windows/",
|
||||
"description": "Cisco Secure Client VPN in Docker with noVNC web UI for accessing Rego environments. Native Docker - no VM overhead.",
|
||||
"tipi_version": 7,
|
||||
"version": "5.1.14.145",
|
||||
"categories": [
|
||||
"utilities"
|
||||
],
|
||||
"short_desc": "Cisco VPN tunnel to Rego environments (native Docker)",
|
||||
"author": "alexz",
|
||||
"source": "https://git.alexzaw.dev/alexz/runtipi",
|
||||
"form_fields": [
|
||||
{
|
||||
"type": "number",
|
||||
"label": "RAM",
|
||||
"hint": "RAM to asign to the VM (in gigabytes)",
|
||||
"placeholder": "4",
|
||||
"required": true,
|
||||
"env_variable": "WINDOWS_RAM_GB"
|
||||
"type": "email",
|
||||
"label": "VPN Email",
|
||||
"hint": "Email address for VPN SSO login (configured in /shared/cisco-vpn script)",
|
||||
"placeholder": "your-email@company.com",
|
||||
"required": false,
|
||||
"env_variable": "VPN_EMAIL",
|
||||
"default": ""
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"label": "CPU Cores",
|
||||
"hint": "CPU cores to asign to the VM",
|
||||
"placeholder": "4",
|
||||
"required": true,
|
||||
"env_variable": "WINDOWS_CPU_CORES"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"label": "Disk Size",
|
||||
"hint": "Disk size for the VM, it will be used dynamically (in gigabytes)",
|
||||
"placeholder": "16",
|
||||
"required": true,
|
||||
"env_variable": "WINDOWS_DISK_SIZE_GB"
|
||||
"type": "password",
|
||||
"label": "VPN Password",
|
||||
"hint": "Password for VPN SSO login (configured in /shared/cisco-vpn script)",
|
||||
"placeholder": "",
|
||||
"required": false,
|
||||
"env_variable": "VPN_PASSWORD",
|
||||
"default": ""
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Windows Version",
|
||||
"hint": "The windows version to use",
|
||||
"required": true,
|
||||
"options": [
|
||||
{ "label": "Windows 10", "value": "win10" },
|
||||
{ "label": "Windows 11", "value": "win11" },
|
||||
{ "label": "Windows 10 LTSC", "value": "ltsc10" },
|
||||
{ "label": "Windows 8.1", "value": "win81" },
|
||||
{ "label": "Windows 7", "value": "win7" },
|
||||
{ "label": "Windows Vista", "value": "vista" },
|
||||
{ "label": "Windows XP", "value": "winxp" },
|
||||
{ "label": "Windows Server 2022", "value": "2022" },
|
||||
{ "label": "Windows Server 2019", "value": "2019" },
|
||||
{ "label": "Windows Server 2016", "value": "2016" },
|
||||
{ "label": "Windows Server 2012", "value": "2012" },
|
||||
{ "label": "Windows Server 2008", "value": "2008" },
|
||||
{ "label": "Windows Tiny 11 Core", "value": "core11" },
|
||||
{ "label": "Windows Tiny 11", "value": "tiny11" },
|
||||
{ "label": "Windows Tiny 10", "value": "tiny10" }
|
||||
],
|
||||
"env_variable": "WINDOWS_VERSION"
|
||||
"label": "TOTP Secret",
|
||||
"hint": "Base32 TOTP secret for 2FA (configured in /shared/cisco-vpn script)",
|
||||
"placeholder": "",
|
||||
"required": false,
|
||||
"env_variable": "VPN_TOTP_SECRET",
|
||||
"default": ""
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "VPN Host",
|
||||
"hint": "VPN server hostname",
|
||||
"placeholder": "vpn.company.com",
|
||||
"required": false,
|
||||
"env_variable": "VPN_HOST",
|
||||
"default": "vpn-ord1.dovercorp.com"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Target IP",
|
||||
"hint": "IP address to route through VPN (e.g., IBM i server)",
|
||||
"placeholder": "10.35.33.230",
|
||||
"required": false,
|
||||
"env_variable": "TARGET_IP",
|
||||
"default": "10.35.33.230"
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"label": "VNC Password",
|
||||
"hint": "Password for noVNC web interface",
|
||||
"placeholder": "cisco123",
|
||||
"required": false,
|
||||
"env_variable": "VNC_PASSWORD",
|
||||
"default": ""
|
||||
}
|
||||
],
|
||||
"supported_architectures": ["arm64", "amd64"],
|
||||
"created_at": 1691943801422,
|
||||
"updated_at": 1763873319189,
|
||||
"force_pull": false
|
||||
"supported_architectures": [
|
||||
"amd64"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,59 +3,70 @@
|
||||
"services": [
|
||||
{
|
||||
"name": "rego-tunnel",
|
||||
"image": "dockurr/windows:latest",
|
||||
"image": "git.alexzaw.dev/alexz/cisco-vpn:latest",
|
||||
"environment": [
|
||||
{
|
||||
"key": "RAM_SIZE",
|
||||
"value": "${WINDOWS_RAM_GB}G"
|
||||
"key": "VPN_EMAIL",
|
||||
"value": "${VPN_EMAIL}"
|
||||
},
|
||||
{
|
||||
"key": "CPU_CORE",
|
||||
"value": "${WINDOWS_CPU_CORES}"
|
||||
"key": "VPN_PASSWORD",
|
||||
"value": "${VPN_PASSWORD}"
|
||||
},
|
||||
{
|
||||
"key": "DISK_SIZE",
|
||||
"value": "${WINDOWS_DISK_SIZE_GB}G"
|
||||
"key": "VPN_TOTP_SECRET",
|
||||
"value": "${VPN_TOTP_SECRET}"
|
||||
},
|
||||
{
|
||||
"key": "VERSION",
|
||||
"value": "${WINDOWS_VERSION}"
|
||||
"key": "VPN_HOST",
|
||||
"value": "${VPN_HOST}"
|
||||
},
|
||||
{
|
||||
"key": "KEY",
|
||||
"value": "B6QDN-RWKT8-G44CW-TXPBW-292XY"
|
||||
"key": "VNC_PASSWORD",
|
||||
"value": "${VNC_PASSWORD}"
|
||||
},
|
||||
{
|
||||
"key": "TZ",
|
||||
"value": "${TZ}"
|
||||
},
|
||||
{
|
||||
"key": "TARGET_IP",
|
||||
"value": "${TARGET_IP}"
|
||||
}
|
||||
],
|
||||
"internalPort": 8006,
|
||||
"internalPort": 6080,
|
||||
"volumes": [
|
||||
{
|
||||
"hostPath": "${APP_DATA_DIR}/data/storage",
|
||||
"containerPath": "/storage",
|
||||
"readOnly": false,
|
||||
"shared": false,
|
||||
"private": false
|
||||
"hostPath": "${APP_DATA_DIR}/config",
|
||||
"containerPath": "/config",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"hostPath": "${APP_DATA_DIR}/data/shared",
|
||||
"hostPath": "${APP_DATA_DIR}",
|
||||
"containerPath": "/runtime",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"hostPath": "/etc/runtipi/repos/runtipi/apps/rego-tunnel/shared",
|
||||
"containerPath": "/shared",
|
||||
"readOnly": false,
|
||||
"shared": false,
|
||||
"private": false
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"hostPath": "${APP_DATA_DIR}/data/start.sh",
|
||||
"containerPath": "/run/start.sh",
|
||||
"readOnly": false,
|
||||
"shared": false,
|
||||
"private": false
|
||||
"hostPath": "/sys/fs/cgroup",
|
||||
"containerPath": "/sys/fs/cgroup",
|
||||
"readOnly": false
|
||||
},
|
||||
{
|
||||
"hostPath": "/etc/runtipi/repos/runtipi/apps/rego-tunnel/shared/xstartup",
|
||||
"containerPath": "/root/.vnc/xstartup",
|
||||
"readOnly": true
|
||||
}
|
||||
],
|
||||
"stopGracePeriod": "2m",
|
||||
"sysctls": {},
|
||||
"stopGracePeriod": "30s",
|
||||
"devices": [
|
||||
"/dev/kvm",
|
||||
"/dev/net/tun"
|
||||
],
|
||||
"privileged": true,
|
||||
"capAdd": [
|
||||
"NET_ADMIN"
|
||||
],
|
||||
|
||||
@@ -1,65 +1,48 @@
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
rego-tunnel:
|
||||
container_name: rego-tunnel
|
||||
image: dockurr/windows:latest
|
||||
image: git.alexzaw.dev/alexz/cisco-vpn:latest
|
||||
restart: unless-stopped
|
||||
devices:
|
||||
- /dev/kvm
|
||||
- /dev/net/tun
|
||||
networks:
|
||||
rego-tunnel_runtipi_network:
|
||||
gw_priority: 0
|
||||
tipi_main_network:
|
||||
gw_priority: 1
|
||||
environment:
|
||||
VPN_EMAIL: ${VPN_EMAIL}
|
||||
VPN_PASSWORD: ${VPN_PASSWORD}
|
||||
VPN_TOTP_SECRET: ${VPN_TOTP_SECRET}
|
||||
VPN_HOST: ${VPN_HOST}
|
||||
VNC_PASSWORD: ${VNC_PASSWORD}
|
||||
TZ: ${TZ}
|
||||
TARGET_IP: ${TARGET_IP}
|
||||
ports:
|
||||
- ${APP_PORT}:6080
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/config:/config
|
||||
- ${APP_DATA_DIR}:/runtime
|
||||
- /etc/runtipi/repos/runtipi/apps/rego-tunnel/shared:/shared
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
- /etc/runtipi/repos/runtipi/apps/rego-tunnel/shared/xstartup:/root/.vnc/xstartup:ro
|
||||
labels:
|
||||
generated: true
|
||||
traefik.enable: true
|
||||
traefik.docker.network: runtipi_tipi_main_network
|
||||
traefik.http.middlewares.rego-tunnel-runtipi-web-redirect.redirectscheme.scheme: https
|
||||
traefik.http.services.rego-tunnel-runtipi.loadbalancer.server.port: "6080"
|
||||
traefik.http.routers.rego-tunnel-runtipi-insecure.rule: Host(`${APP_DOMAIN}`)
|
||||
traefik.http.routers.rego-tunnel-runtipi-insecure.entrypoints: web
|
||||
traefik.http.routers.rego-tunnel-runtipi-insecure.service: rego-tunnel-runtipi
|
||||
traefik.http.routers.rego-tunnel-runtipi-insecure.middlewares: rego-tunnel-runtipi-web-redirect
|
||||
traefik.http.routers.rego-tunnel-runtipi.rule: Host(`${APP_DOMAIN}`)
|
||||
traefik.http.routers.rego-tunnel-runtipi.entrypoints: websecure
|
||||
traefik.http.routers.rego-tunnel-runtipi.service: rego-tunnel-runtipi
|
||||
traefik.http.routers.rego-tunnel-runtipi.tls.certresolver: myresolver
|
||||
runtipi.managed: true
|
||||
runtipi.appurn: rego-tunnel:runtipi
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
stop_grace_period: 2m
|
||||
|
||||
ports:
|
||||
# Web console (Tipi APP_PORT -> 8006)
|
||||
- "192.168.0.151:${APP_PORT}:8006"
|
||||
|
||||
environment:
|
||||
- RAM_SIZE=${WINDOWS_RAM_GB}G
|
||||
- CPU_CORE=${WINDOWS_CPU_CORES}
|
||||
- DISK_SIZE=${WINDOWS_DISK_SIZE_GB}G
|
||||
|
||||
volumes:
|
||||
- ${APP_DATA_DIR}/data/storage:/storage
|
||||
- ${APP_DATA_DIR}/data/shared:/shared
|
||||
- ${APP_DATA_DIR}/data/start.sh:/run/start.sh
|
||||
|
||||
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
sysctls:
|
||||
- net.ipv4.ip_forward=1
|
||||
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.middlewares.rego-tunnel-web-redirect.redirectscheme.scheme: https
|
||||
traefik.http.services.rego-tunnel.loadbalancer.server.port: 8006
|
||||
|
||||
traefik.http.routers.rego-tunnel-insecure.rule: Host(`${APP_DOMAIN}`)
|
||||
traefik.http.routers.rego-tunnel-insecure.entrypoints: web
|
||||
traefik.http.routers.rego-tunnel-insecure.service: rego-tunnel
|
||||
traefik.http.routers.rego-tunnel-insecure.middlewares: rego-tunnel-web-redirect
|
||||
|
||||
traefik.http.routers.rego-tunnel.rule: Host(`${APP_DOMAIN}`)
|
||||
traefik.http.routers.rego-tunnel.entrypoints: websecure
|
||||
traefik.http.routers.rego-tunnel.service: rego-tunnel
|
||||
traefik.http.routers.rego-tunnel.tls.certresolver: myresolver
|
||||
|
||||
traefik.http.routers.rego-tunnel-local-insecure.rule: Host(`rego-tunnel.${LOCAL_DOMAIN}`)
|
||||
traefik.http.routers.rego-tunnel-local-insecure.entrypoints: web
|
||||
traefik.http.routers.rego-tunnel-local-insecure.service: rego-tunnel
|
||||
traefik.http.routers.rego-tunnel-local-insecure.middlewares: rego-tunnel-web-redirect
|
||||
|
||||
traefik.http.routers.rego-tunnel-local.rule: Host(`rego-tunnel.${LOCAL_DOMAIN}`)
|
||||
traefik.http.routers.rego-tunnel-local.entrypoints: websecure
|
||||
traefik.http.routers.rego-tunnel-local.service: rego-tunnel
|
||||
traefik.http.routers.rego-tunnel-local.tls: true
|
||||
|
||||
runtipi.managed: true
|
||||
|
||||
networks:
|
||||
tipi_main_network:
|
||||
external: true
|
||||
devices:
|
||||
- /dev/net/tun
|
||||
privileged: true
|
||||
cgroup: host
|
||||
stop_grace_period: 30s
|
||||
|
||||
@@ -1,82 +1,144 @@
|
||||
<h1 align="center">Windows<br />
|
||||
<div align="center">
|
||||
<a href="https://github.com/dockur/windows"><img src="https://github.com/dockur/windows/raw/master/.github/logo.png" title="Logo" style="max-width:100%;" width="128" /></a>
|
||||
</div>
|
||||
<div align="center">
|
||||
# Rego Tunnel - Cisco Secure Client VPN
|
||||
|
||||
</div></h1>
|
||||
|
||||
Windows in a Docker container.
|
||||
Native Docker container running Cisco Secure Client (AnyConnect) with full GUI support via noVNC. Provides transparent VPN access to protected resources from your LAN.
|
||||
|
||||
## Features
|
||||
|
||||
- ISO downloader
|
||||
- KVM acceleration
|
||||
- Web-based viewer
|
||||
- **Cisco Secure Client 5.1.14.145** - Full GUI with VPN, DART, and Posture modules
|
||||
- **Web-based access** via noVNC (port 6080)
|
||||
- **Auto-login with TOTP** - Fully automated VPN connection
|
||||
- **LAN routing** - Other machines on your network can reach VPN targets
|
||||
- **Native Docker** - No QEMU/VM overhead
|
||||
|
||||
## FAQ
|
||||
## Architecture
|
||||
|
||||
* ### How do I use it?
|
||||
|
||||
Very simple! These are the steps:
|
||||
|
||||
- Start the container and connect to [port 8006](http://localhost:8006) using your web browser.
|
||||
|
||||
- Sit back and relax while the magic happens, the whole installation will be performed fully automatic.
|
||||
|
||||
- Once you see the desktop, your Windows installation is ready for use.
|
||||
|
||||
Enjoy your brand new machine, and don't forget to star this repo!
|
||||
|
||||
* ### How do I select the Windows version?
|
||||
|
||||
By default, Windows 11 will be installed. But you can change that in settings, in order to specify an alternative Windows version to be downloaded:
|
||||
|
||||
Select from the values below:
|
||||
|
||||
| **Value** | **Description** | **Source** | **Transfer** | **Size** |
|
||||
|---|---|---|---|---|
|
||||
| `win11` | Windows 11 Pro | Microsoft | Fast | 6.4 GB |
|
||||
| `win10` | Windows 10 Pro | Microsoft | Fast | 5.8 GB |
|
||||
| `ltsc10` | Windows 10 LTSC | Microsoft | Fast | 4.6 GB |
|
||||
| `win81` | Windows 8.1 Pro | Microsoft | Fast | 4.2 GB |
|
||||
| `win7` | Windows 7 SP1 | Bob Pony | Medium | 3.0 GB |
|
||||
| `vista` | Windows Vista SP2 | Bob Pony | Medium | 3.6 GB |
|
||||
| `winxp` | Windows XP SP3 | Bob Pony | Medium | 0.6 GB |
|
||||
||||||
|
||||
| `2022` | Windows Server 2022 | Microsoft | Fast | 4.7 GB |
|
||||
| `2019` | Windows Server 2019 | Microsoft | Fast | 5.3 GB |
|
||||
| `2016` | Windows Server 2016 | Microsoft | Fast | 6.5 GB |
|
||||
| `2012` | Windows Server 2012 R2 | Microsoft | Fast | 4.3 GB |
|
||||
| `2008` | Windows Server 2008 R2 | Microsoft | Fast | 3.0 GB |
|
||||
||||||
|
||||
| `core11` | Tiny 11 Core | Archive.org | Slow | 2.1 GB |
|
||||
| `tiny11` | Tiny 11 | Archive.org | Slow | 3.8 GB |
|
||||
| `tiny10` | Tiny 10 | Archive.org | Slow | 3.6 GB |
|
||||
|
||||
* ### How do I connect using RDP?
|
||||
|
||||
The web-viewer is mainly meant to be used during installation, as its picture quality is low, and it has no audio or clipboard for example.
|
||||
|
||||
So for a better experience you can connect using any Microsoft Remote Desktop client to the IP of the container, using the username `docker` and by leaving the password empty.
|
||||
|
||||
There is a good RDP client for [Android](https://play.google.com/store/apps/details?id=com.microsoft.rdc.androidx) available from the Play Store. One for [iOS](https://apps.apple.com/nl/app/microsoft-remote-desktop/id714464092?l=en-GB) is in the Apple Store. For Linux you can use [rdesktop](http://www.rdesktop.org/) and for Windows you don't need to install anything as it is already ships as part of the operating system.
|
||||
|
||||
* ### How do I verify if my system supports KVM?
|
||||
|
||||
To verify if your system supports KVM, run the following commands:
|
||||
|
||||
```bash
|
||||
sudo apt install cpu-checker
|
||||
sudo kvm-ok
|
||||
```
|
||||
LAN Devices ──► Linux Host ──► Container (172.31.0.10) ──► VPN Tunnel ──► Target (10.35.33.230)
|
||||
│ │
|
||||
│ └── Cisco Secure Client
|
||||
│ └── noVNC web UI (port 6080)
|
||||
│
|
||||
└── Host routing service
|
||||
(routes VPN traffic through container)
|
||||
```
|
||||
|
||||
If you receive an error from `kvm-ok` indicating that KVM acceleration can't be used, check the virtualization settings in the BIOS.
|
||||
## Installation
|
||||
|
||||
* ### Is this project legal?
|
||||
### 1. Install the app through Runtipi
|
||||
|
||||
Yes, this project contains only open-source code and does not distribute any copyrighted material. Any product keys found in the code are just generic placeholders provided by Microsoft for trial purposes. So under all applicable laws, this project would be considered legal.
|
||||
Configure your VPN credentials in app settings:
|
||||
- VPN Email
|
||||
- VPN Password
|
||||
- TOTP Secret (base32)
|
||||
- VPN Host (default: vpn-ord1.dovercorp.com)
|
||||
- Target IP (default: 10.35.33.230)
|
||||
|
||||
## Disclaimer
|
||||
### 2. Install host routing service (required for LAN access)
|
||||
|
||||
The product names, logos, brands, and other trademarks referred to within this project are the property of their respective trademark holders. This project is not affiliated, sponsored, or endorsed by Microsoft Corporation.
|
||||
**Run this ONCE on the host after app install:**
|
||||
|
||||
```bash
|
||||
/etc/runtipi/repos/runtipi/apps/rego-tunnel/shared/install-host-services.sh
|
||||
```
|
||||
|
||||
This creates systemd services that route VPN traffic through the container.
|
||||
|
||||
### 3. Access the VPN GUI
|
||||
|
||||
Open `http://<your-server>:6080/vnc.html`
|
||||
|
||||
The VPN will auto-connect using your configured credentials.
|
||||
|
||||
## Usage
|
||||
|
||||
### Access noVNC
|
||||
|
||||
Navigate to port 6080 on your server. The cisco-vpn script runs automatically and provides a menu:
|
||||
|
||||
```
|
||||
1 - Start Cisco AnyConnect
|
||||
2 - Copy credentials to clipboard
|
||||
3 - Show live TOTP
|
||||
4 - Setup IP forwarding rules
|
||||
5 - Test connection to target
|
||||
6 - Show network status
|
||||
7 - Kill all Cisco processes
|
||||
8 - Show routing table
|
||||
9 - Show /etc/hosts
|
||||
q - Quit
|
||||
```
|
||||
|
||||
### Command line options
|
||||
|
||||
```bash
|
||||
# Inside container
|
||||
cisco-vpn -m # Menu only (skip auto-connect)
|
||||
cisco-vpn -c # Connect and exit
|
||||
cisco-vpn -d # Disconnect and exit
|
||||
cisco-vpn -s # Show status
|
||||
cisco-vpn --help # Show all options
|
||||
```
|
||||
|
||||
### View logs
|
||||
|
||||
```bash
|
||||
# Inside container
|
||||
cat /var/log/cisco-vpn/$(date +%Y-%m-%d).log
|
||||
|
||||
# On host
|
||||
cat /var/log/rego-routing.log
|
||||
```
|
||||
|
||||
## LAN Access
|
||||
|
||||
After the host routing service is installed, any device on your LAN can reach the VPN target:
|
||||
|
||||
1. **From the host:** Works automatically
|
||||
2. **From other LAN devices:** Add a static route pointing to your host
|
||||
|
||||
Example (Windows client):
|
||||
```cmd
|
||||
route add 10.35.33.230 mask 255.255.255.255 192.168.0.150 -p
|
||||
```
|
||||
|
||||
Where `192.168.0.150` is your Linux host IP.
|
||||
|
||||
## Uninstall
|
||||
|
||||
Before removing the app from Runtipi:
|
||||
|
||||
```bash
|
||||
/etc/runtipi/repos/runtipi/apps/rego-tunnel/shared/uninstall-host-services.sh
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### noVNC not accessible
|
||||
```bash
|
||||
docker exec rego-tunnel_runtipi-rego-tunnel-1 systemctl status vnc.service
|
||||
```
|
||||
|
||||
### VPN connects but can't reach target
|
||||
```bash
|
||||
# Check routes inside container
|
||||
docker exec rego-tunnel_runtipi-rego-tunnel-1 ip route
|
||||
|
||||
# Check host routing
|
||||
ip route | grep 10.35.33.230
|
||||
```
|
||||
|
||||
### Host routing not working
|
||||
```bash
|
||||
# Check watcher service
|
||||
systemctl status rego-routing-watcher.path
|
||||
|
||||
# Manually trigger routing
|
||||
touch /etc/runtipi/app-data/runtipi/rego-tunnel/restart-routing
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Container IP:** 172.31.0.10 (on br-rego-vpn bridge)
|
||||
- **Ports:** 6080 (noVNC), 5901 (VNC)
|
||||
- **Privileges:** `--privileged`, `NET_ADMIN`, `/dev/net/tun`
|
||||
- **Log retention:** 7 days (auto-cleanup)
|
||||
|
||||
0
apps/rego-tunnel/metadata/logo.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
888
apps/rego-tunnel/shared/cisco-vpn
Executable file
@@ -0,0 +1,888 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Dover VPN Connection Script with Semi-Automation
|
||||
# Usage: ./cisco-vpn.sh [-c|--connect] [-d|--disconnect] [-m|--menu] [-r|--routes] [-h|--hosts] [--help]
|
||||
#
|
||||
# Options:
|
||||
# -c, --connect Start Cisco AnyConnect (optionally auto-login) and exit
|
||||
# -d, --disconnect Disconnect (kill Cisco processes) and exit
|
||||
# -m, --menu Skip auto-login, show menu directly
|
||||
# -r, --routes Show current routing table and exit
|
||||
# -h, --hosts Show /etc/hosts and exit
|
||||
# -s, --status Show VPN and network status and exit
|
||||
# --help Show this help message
|
||||
#
|
||||
# Keyboard shortcuts (global, work anywhere):
|
||||
# Ctrl+1 - Type email
|
||||
# Ctrl+2 - Type password
|
||||
# Ctrl+3 - Type TOTP code
|
||||
# Ctrl+4 - Type email + Tab + password (combo)
|
||||
# Ctrl+5 - Full sequence: email + Tab + password + Tab + TOTP + Enter
|
||||
|
||||
# Credentials from environment variables (set by runtipi)
|
||||
EMAIL="${VPN_EMAIL:-}"
|
||||
PASSWORD="${VPN_PASSWORD:-}"
|
||||
TOTP_SECRET="${VPN_TOTP_SECRET:-}"
|
||||
VPN_HOST="${VPN_HOST:-vpn-ord1.dovercorp.com}"
|
||||
TARGET_IP="${TARGET_IP:-10.35.33.230}"
|
||||
|
||||
# Log directory and file (date-based rotation)
|
||||
LOG_DIR="/var/log/cisco-vpn"
|
||||
LOG_RETENTION_DAYS=7
|
||||
mkdir -p "$LOG_DIR" 2>/dev/null
|
||||
|
||||
# Function to get current log file (changes daily)
|
||||
get_log_file() {
|
||||
echo "$LOG_DIR/$(date '+%Y-%m-%d').log"
|
||||
}
|
||||
|
||||
# Cleanup old log files (older than LOG_RETENTION_DAYS)
|
||||
cleanup_old_logs() {
|
||||
find "$LOG_DIR" -name "*.log" -type f -mtime +$LOG_RETENTION_DAYS -delete 2>/dev/null
|
||||
}
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
GRAY='\033[0;90m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Print banner
|
||||
print_banner() {
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo -e "${CYAN} Dover VPN Connection Script ${NC}"
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Flags
|
||||
SKIP_AUTO_LOGIN=false
|
||||
DO_CONNECT=false
|
||||
DO_DISCONNECT=false
|
||||
|
||||
# Logging function with timestamp - writes to both console and daily log file
|
||||
log() {
|
||||
local level="$1"
|
||||
local msg="$2"
|
||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local timestamp_short=$(date '+%H:%M:%S')
|
||||
local log_file=$(get_log_file)
|
||||
|
||||
# Strip ANSI color codes for log file
|
||||
local msg_plain=$(echo -e "$msg" | sed 's/\x1b\[[0-9;]*m//g')
|
||||
|
||||
# Write to log file (plain text, no colors)
|
||||
echo "[$timestamp] [$level] $msg_plain" >> "$log_file"
|
||||
|
||||
# Write to console (with colors)
|
||||
case $level in
|
||||
INFO) echo -e "${GRAY}[$timestamp_short]${NC} ${GREEN}[INFO]${NC} $msg" ;;
|
||||
WARN) echo -e "${GRAY}[$timestamp_short]${NC} ${YELLOW}[WARN]${NC} $msg" ;;
|
||||
ERROR) echo -e "${GRAY}[$timestamp_short]${NC} ${RED}[ERROR]${NC} $msg" ;;
|
||||
DEBUG) echo -e "${GRAY}[$timestamp_short]${NC} ${CYAN}[DEBUG]${NC} $msg" ;;
|
||||
CMD) echo -e "${GRAY}[$timestamp_short]${NC} ${GRAY}[CMD]${NC} $msg" ;;
|
||||
*) echo -e "${GRAY}[$timestamp_short]${NC} $msg" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Run command with logging
|
||||
run_cmd() {
|
||||
local desc="$1"
|
||||
shift
|
||||
log CMD "$desc: $*"
|
||||
output=$("$@" 2>&1)
|
||||
local rc=$?
|
||||
if [ -n "$output" ]; then
|
||||
echo "$output" | while IFS= read -r line; do
|
||||
echo -e " ${GRAY}│${NC} $line"
|
||||
done
|
||||
fi
|
||||
return $rc
|
||||
}
|
||||
|
||||
# Show help
|
||||
show_help() {
|
||||
echo -e "${CYAN}Dover VPN Connection Script${NC}"
|
||||
echo ""
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -c, --connect Start Cisco AnyConnect and exit"
|
||||
echo " -d, --disconnect Disconnect (kill Cisco processes) and exit"
|
||||
echo " -m, --menu Skip auto-login, show menu directly"
|
||||
echo " -r, --routes Show current routing table and exit"
|
||||
echo " -h, --hosts Show /etc/hosts and exit"
|
||||
echo " -s, --status Show VPN and network status and exit"
|
||||
echo " --help Show this help message"
|
||||
echo ""
|
||||
echo "Menu Options:"
|
||||
echo " 1 - Start Cisco AnyConnect (kill existing + launch)"
|
||||
echo " 2 - Copy credentials to clipboard (one by one)"
|
||||
echo " 3 - Show live TOTP"
|
||||
echo " 4 - Setup IP forwarding rules only"
|
||||
echo " 5 - Test connection to target"
|
||||
echo " 6 - Show network status"
|
||||
echo " 7 - Kill all Cisco processes"
|
||||
echo " 8 - Show routing table"
|
||||
echo " 9 - Show /etc/hosts"
|
||||
echo " e - Edit /etc/hosts"
|
||||
echo " q - Quit"
|
||||
}
|
||||
|
||||
# Show routing table
|
||||
show_routes() {
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo -e "${CYAN} Current Routing Table ${NC}"
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}IPv4 Routes:${NC}"
|
||||
ip -4 route show | while IFS= read -r line; do
|
||||
# Highlight VPN-related routes
|
||||
if echo "$line" | grep -qE "cscotun|tun[0-9]"; then
|
||||
echo -e " ${YELLOW}$line${NC}"
|
||||
elif echo "$line" | grep -qE "10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\."; then
|
||||
echo -e " ${CYAN}$line${NC}"
|
||||
else
|
||||
echo " $line"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo -e "${GREEN}VPN Interface:${NC}"
|
||||
local vpn_iface=$(get_vpn_interface)
|
||||
if [ -n "$vpn_iface" ]; then
|
||||
ip addr show "$vpn_iface" 2>/dev/null | grep -E "inet|link" | while IFS= read -r line; do
|
||||
echo " $line"
|
||||
done
|
||||
else
|
||||
echo -e " ${RED}No VPN interface found${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Show /etc/hosts
|
||||
show_hosts() {
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo -e "${CYAN} /etc/hosts ${NC}"
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo ""
|
||||
cat -n /etc/hosts
|
||||
}
|
||||
|
||||
# Edit /etc/hosts
|
||||
edit_hosts() {
|
||||
local editor="${EDITOR:-nano}"
|
||||
if command -v "$editor" &>/dev/null; then
|
||||
"$editor" /etc/hosts
|
||||
else
|
||||
log ERROR "No editor found. Set EDITOR environment variable."
|
||||
log INFO "Try: nano /etc/hosts"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get current TOTP
|
||||
get_totp() {
|
||||
oathtool --totp -b "$TOTP_SECRET"
|
||||
}
|
||||
|
||||
# Function to detect VPN tunnel interface dynamically
|
||||
get_vpn_interface() {
|
||||
# Look for cscotun* or tun* interfaces that are UP
|
||||
local iface=$(ip link show | grep -oP '(cscotun\d+|tun\d+)(?=:.*UP)' | head -1)
|
||||
if [ -z "$iface" ]; then
|
||||
# Fallback: any cscotun interface
|
||||
iface=$(ip link show | grep -oP 'cscotun\d+' | head -1)
|
||||
fi
|
||||
echo "$iface"
|
||||
}
|
||||
|
||||
# Function to get container's IP on the rego-tunnel network
|
||||
get_container_ip() {
|
||||
# eth0 is the rego-tunnel network (172.31.0.x)
|
||||
ip addr show eth0 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1
|
||||
}
|
||||
|
||||
# Function to get VPN tunnel IP
|
||||
get_vpn_ip() {
|
||||
local iface=$(get_vpn_interface)
|
||||
if [ -n "$iface" ]; then
|
||||
ip addr show "$iface" 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1
|
||||
fi
|
||||
}
|
||||
|
||||
# Start xbindkeys for keyboard macros
|
||||
start_xbindkeys() {
|
||||
log INFO "Starting keyboard macro listener (xbindkeys)..."
|
||||
|
||||
# Kill any existing xbindkeys
|
||||
pkill xbindkeys 2>/dev/null
|
||||
sleep 0.5
|
||||
|
||||
# Start xbindkeys
|
||||
xbindkeys -f ~/.xbindkeysrc 2>/dev/null &
|
||||
XBINDKEYS_PID=$!
|
||||
|
||||
if pgrep xbindkeys >/dev/null; then
|
||||
log DEBUG "xbindkeys started (PID: $(pgrep xbindkeys))"
|
||||
log INFO "Keyboard shortcuts active: Ctrl+1=email, Ctrl+2=pass, Ctrl+3=TOTP, Ctrl+4=combo, Ctrl+5=all"
|
||||
else
|
||||
log WARN "Failed to start xbindkeys"
|
||||
fi
|
||||
}
|
||||
|
||||
# Stop xbindkeys
|
||||
stop_xbindkeys() {
|
||||
if pgrep xbindkeys >/dev/null; then
|
||||
log INFO "Stopping keyboard macro listener..."
|
||||
pkill xbindkeys 2>/dev/null
|
||||
log DEBUG "xbindkeys stopped"
|
||||
fi
|
||||
}
|
||||
|
||||
# Kill all Cisco-related processes
|
||||
kill_cisco_processes() {
|
||||
local kill_agentd="${1:-false}"
|
||||
log INFO "Killing all Cisco-related processes..."
|
||||
|
||||
local killed=0
|
||||
local my_pid=$$
|
||||
local my_ppid=$(ps -o ppid= -p $$ | tr -d ' ')
|
||||
|
||||
# Kill vpnui specifically (not just any process with "vpn" in name)
|
||||
for pid in $(pgrep -x "vpnui" 2>/dev/null); do
|
||||
if [ "$pid" != "$my_pid" ] && [ "$pid" != "$my_ppid" ]; then
|
||||
log DEBUG "Killing vpnui (PID $pid)"
|
||||
kill -9 "$pid" 2>/dev/null && ((killed++))
|
||||
fi
|
||||
done
|
||||
|
||||
# Optionally kill vpnagentd (useful to clear stale state)
|
||||
if [ "$kill_agentd" = "true" ]; then
|
||||
for pid in $(pgrep -x "vpnagentd" 2>/dev/null); do
|
||||
if [ "$pid" != "$my_pid" ] && [ "$pid" != "$my_ppid" ]; then
|
||||
log DEBUG "Killing vpnagentd (PID $pid)"
|
||||
kill -9 "$pid" 2>/dev/null && ((killed++))
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Kill Cisco-specific processes by exact path
|
||||
for proc in cstub cscan acwebsecagent vpndownloader; do
|
||||
for pid in $(pgrep -x "$proc" 2>/dev/null); do
|
||||
log DEBUG "Killing $proc (PID $pid)"
|
||||
kill -9 "$pid" 2>/dev/null && ((killed++))
|
||||
done
|
||||
done
|
||||
|
||||
# Kill openconnect (exact match)
|
||||
for pid in $(pgrep -x "openconnect" 2>/dev/null); do
|
||||
log DEBUG "Killing openconnect (PID $pid)"
|
||||
kill -9 "$pid" 2>/dev/null && ((killed++))
|
||||
done
|
||||
|
||||
if [ $killed -eq 0 ]; then
|
||||
log INFO "No Cisco processes were running"
|
||||
else
|
||||
log INFO "Killed $killed process(es)"
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
sleep 5
|
||||
}
|
||||
|
||||
# Disconnect VPN (best-effort)
|
||||
disconnect_vpn() {
|
||||
log INFO "Disconnecting Cisco AnyConnect..."
|
||||
|
||||
# If vpncli exists, attempt a clean disconnect first (ignore failures)
|
||||
if [ -x /opt/cisco/secureclient/bin/vpncli ]; then
|
||||
run_cmd "Attempting clean disconnect via vpncli" /opt/cisco/secureclient/bin/vpncli -s <<'EOF' || true
|
||||
disconnect
|
||||
exit
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Always kill agent/UI processes afterwards
|
||||
kill_cisco_processes "true"
|
||||
|
||||
# Confirm status
|
||||
if check_vpn_status; then
|
||||
log WARN "VPN still appears connected (interface still up)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log INFO "VPN disconnected"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to setup iptables rules for forwarding
|
||||
setup_forwarding() {
|
||||
log INFO "Setting up IP forwarding rules for $TARGET_IP..."
|
||||
|
||||
local vpn_iface=$(get_vpn_interface)
|
||||
if [ -z "$vpn_iface" ]; then
|
||||
log ERROR "No VPN interface found! Is VPN connected?"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local vpn_ip=$(get_vpn_ip)
|
||||
local container_ip=$(get_container_ip)
|
||||
|
||||
log DEBUG "VPN interface: $vpn_iface"
|
||||
log DEBUG "VPN IP: $vpn_ip"
|
||||
log DEBUG "Container IP: $container_ip"
|
||||
|
||||
# Enable IP forwarding
|
||||
run_cmd "Enabling IP forwarding" sysctl -w net.ipv4.ip_forward=1
|
||||
|
||||
# NAT masquerade for traffic from container network (172.31.0.0/24) going through VPN
|
||||
# This is the ONLY masquerade rule needed - source-based, not destination-based
|
||||
if ! iptables -t nat -C POSTROUTING -s 172.31.0.0/24 -o "$vpn_iface" -j MASQUERADE 2>/dev/null; then
|
||||
run_cmd "Adding NAT masquerade for container network -> VPN" iptables -t nat -A POSTROUTING -s 172.31.0.0/24 -o "$vpn_iface" -j MASQUERADE
|
||||
else
|
||||
log DEBUG "NAT masquerade for container network already exists"
|
||||
fi
|
||||
|
||||
# Forward rules - MUST be at position 1 to run BEFORE cisco VPN chains
|
||||
# The cisco VPN chains have catch-all DROP rules that would block our traffic
|
||||
|
||||
# Wait for Cisco to create its chains (they appear after VPN connects)
|
||||
local wait_count=0
|
||||
while ! iptables -L ciscovpn -n &>/dev/null && [ $wait_count -lt 30 ]; do
|
||||
log DEBUG "Waiting for Cisco VPN chains to be created..."
|
||||
sleep 1
|
||||
((wait_count++))
|
||||
done
|
||||
|
||||
if iptables -L ciscovpn -n &>/dev/null; then
|
||||
log DEBUG "Cisco VPN chains detected"
|
||||
else
|
||||
log WARN "Cisco VPN chains not created after ${wait_count}s - proceeding anyway"
|
||||
fi
|
||||
|
||||
# Remove any existing rules first (ignore errors if they don't exist)
|
||||
iptables -D FORWARD -d "$TARGET_IP" -j ACCEPT 2>/dev/null || true
|
||||
iptables -D FORWARD -s "$TARGET_IP" -j ACCEPT 2>/dev/null || true
|
||||
iptables -D FORWARD -s 172.31.0.0/24 -j ACCEPT 2>/dev/null || true
|
||||
iptables -D FORWARD -d 172.31.0.0/24 -j ACCEPT 2>/dev/null || true
|
||||
|
||||
# Insert at position 1 (reverse order so they end up in correct order)
|
||||
run_cmd "Inserting forward rule (to container network)" iptables -I FORWARD 1 -d 172.31.0.0/24 -j ACCEPT
|
||||
run_cmd "Inserting forward rule (from container network)" iptables -I FORWARD 1 -s 172.31.0.0/24 -j ACCEPT
|
||||
run_cmd "Inserting forward rule (from target)" iptables -I FORWARD 1 -s "$TARGET_IP" -j ACCEPT
|
||||
run_cmd "Inserting forward rule (to target)" iptables -I FORWARD 1 -d "$TARGET_IP" -j ACCEPT
|
||||
|
||||
# Cisco VPN chain bypass (insert at top if chain exists)
|
||||
if iptables -L ciscovpn -n &>/dev/null; then
|
||||
if ! iptables -C ciscovpn -o "$vpn_iface" -d "$TARGET_IP" -j ACCEPT 2>/dev/null; then
|
||||
run_cmd "Adding ciscovpn bypass (outbound)" iptables -I ciscovpn 1 -o "$vpn_iface" -d "$TARGET_IP" -j ACCEPT
|
||||
else
|
||||
log DEBUG "Ciscovpn bypass (outbound) already exists"
|
||||
fi
|
||||
|
||||
if ! iptables -C ciscovpn -i "$vpn_iface" -s "$TARGET_IP" -j ACCEPT 2>/dev/null; then
|
||||
run_cmd "Adding ciscovpn bypass (inbound)" iptables -I ciscovpn 2 -i "$vpn_iface" -s "$TARGET_IP" -j ACCEPT
|
||||
else
|
||||
log DEBUG "Ciscovpn bypass (inbound) already exists"
|
||||
fi
|
||||
|
||||
# Also allow container network through ciscovpn chain
|
||||
if ! iptables -C ciscovpn -s 172.31.0.0/24 -j ACCEPT 2>/dev/null; then
|
||||
run_cmd "Adding ciscovpn bypass (container network)" iptables -I ciscovpn 3 -s 172.31.0.0/24 -j ACCEPT
|
||||
fi
|
||||
else
|
||||
log DEBUG "ciscovpn chain does not exist (yet)"
|
||||
fi
|
||||
|
||||
log INFO "Forwarding rules configured"
|
||||
echo ""
|
||||
|
||||
# Trigger host routing service restart
|
||||
log INFO "Triggering host routing service restart..."
|
||||
touch /runtime/restart-routing
|
||||
sleep 2
|
||||
if [ ! -f /runtime/restart-routing ]; then
|
||||
log INFO "Host routing service restarted"
|
||||
else
|
||||
log WARN "Host watcher may not be running (trigger file still exists)"
|
||||
fi
|
||||
|
||||
log INFO "Routing configured for $TARGET_IP through VPN tunnel"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Copy credentials to clipboard as alternative
|
||||
copy_to_clipboard() {
|
||||
log INFO "Starting clipboard credential rotation..."
|
||||
echo ""
|
||||
|
||||
log INFO "Copying EMAIL to clipboard"
|
||||
echo "$EMAIL" | xclip -selection clipboard
|
||||
echo -e " ${CYAN}Email ready: $EMAIL${NC}"
|
||||
echo -e " Paste now (Ctrl+V), then press ${GREEN}Enter${NC} here for password..."
|
||||
read -r
|
||||
|
||||
log INFO "Copying PASSWORD to clipboard"
|
||||
echo "$PASSWORD" | xclip -selection clipboard
|
||||
echo -e " ${CYAN}Password ready${NC}"
|
||||
echo -e " Paste now (Ctrl+V), then press ${GREEN}Enter${NC} here for TOTP..."
|
||||
read -r
|
||||
|
||||
TOTP=$(get_totp)
|
||||
log INFO "Copying TOTP to clipboard"
|
||||
echo "$TOTP" | xclip -selection clipboard
|
||||
echo -e " ${CYAN}TOTP ready: $TOTP${NC}"
|
||||
echo -e " Paste now (Ctrl+V)"
|
||||
}
|
||||
|
||||
# Print current TOTP with countdown
|
||||
show_totp() {
|
||||
log INFO "Starting live TOTP display (Ctrl+C to stop)"
|
||||
echo ""
|
||||
while true; do
|
||||
TOTP=$(get_totp)
|
||||
SECONDS_LEFT=$((30 - ($(date +%s) % 30)))
|
||||
echo -ne "\r ${CYAN}Current TOTP:${NC} ${GREEN}$TOTP${NC} (expires in ${YELLOW}${SECONDS_LEFT}s${NC}) "
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
# Show network status
|
||||
show_network_status() {
|
||||
log INFO "Current network status:"
|
||||
|
||||
# Container IPs
|
||||
echo ""
|
||||
log DEBUG "Container Network Interfaces:"
|
||||
ip -4 addr show | grep -E "inet |^[0-9]+:" | while IFS= read -r line; do
|
||||
echo -e " ${GRAY}│${NC} $line"
|
||||
done
|
||||
|
||||
# VPN status
|
||||
echo ""
|
||||
local vpn_iface=$(get_vpn_interface)
|
||||
if [ -n "$vpn_iface" ]; then
|
||||
local vpn_ip=$(get_vpn_ip)
|
||||
log INFO "VPN Status: ${GREEN}CONNECTED${NC}"
|
||||
log DEBUG " Interface: $vpn_iface"
|
||||
log DEBUG " VPN IP: $vpn_ip"
|
||||
else
|
||||
log WARN "VPN Status: ${RED}NOT CONNECTED${NC}"
|
||||
fi
|
||||
|
||||
# Container IP on rego-tunnel network
|
||||
local container_ip=$(get_container_ip)
|
||||
if [ -n "$container_ip" ]; then
|
||||
log DEBUG "Container IP: $container_ip"
|
||||
fi
|
||||
|
||||
# Default gateway
|
||||
echo ""
|
||||
log DEBUG "Default gateway:"
|
||||
ip route show default | while IFS= read -r line; do
|
||||
echo -e " ${GRAY}│${NC} $line"
|
||||
done
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main menu
|
||||
main_menu() {
|
||||
echo -e "${GREEN}Options:${NC}"
|
||||
echo -e " ${CYAN}1${NC} - Start Cisco AnyConnect (kill existing + launch)"
|
||||
echo -e " ${CYAN}2${NC} - Copy credentials to clipboard (one by one)"
|
||||
echo -e " ${CYAN}3${NC} - Show live TOTP"
|
||||
echo -e " ${CYAN}4${NC} - Setup IP forwarding rules only"
|
||||
echo -e " ${CYAN}5${NC} - Test connection to $TARGET_IP"
|
||||
echo -e " ${CYAN}6${NC} - Show network status"
|
||||
echo -e " ${CYAN}7${NC} - Kill all Cisco processes"
|
||||
echo -e " ${CYAN}8${NC} - Show routing table"
|
||||
echo -e " ${CYAN}9${NC} - Show /etc/hosts"
|
||||
echo -e " ${CYAN}e${NC} - Edit /etc/hosts"
|
||||
echo -e " ${CYAN}q${NC} - Quit"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Check if VPN is already connected
|
||||
check_vpn_status() {
|
||||
local vpn_iface=$(get_vpn_interface)
|
||||
if [ -n "$vpn_iface" ]; then
|
||||
local vpn_ip=$(get_vpn_ip)
|
||||
log INFO "VPN is ${GREEN}CONNECTED${NC}"
|
||||
log DEBUG " Interface: $vpn_iface"
|
||||
log DEBUG " VPN IP: $vpn_ip"
|
||||
return 0
|
||||
else
|
||||
log WARN "VPN is ${RED}NOT CONNECTED${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Focus on Cisco AnyConnect window
|
||||
focus_vpn_window() {
|
||||
local win_id=$(xdotool search --name "Cisco" 2>/dev/null | head -1)
|
||||
if [ -n "$win_id" ]; then
|
||||
xdotool windowactivate --sync "$win_id" 2>/dev/null
|
||||
sleep 0.3
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Auto-login sequence using xdotool (no auto-focus, types to active window)
|
||||
auto_login() {
|
||||
log INFO "Starting automated login sequence..."
|
||||
|
||||
# Wait for UI to fully load
|
||||
log DEBUG "Waiting 5s for UI to load..."
|
||||
sleep 5
|
||||
|
||||
# Press Enter to initiate connection
|
||||
log DEBUG "Pressing Enter to start connection..."
|
||||
xdotool key Return
|
||||
sleep 5
|
||||
|
||||
# Type email
|
||||
log DEBUG "Typing email..."
|
||||
xdotool type --delay 50 "$EMAIL"
|
||||
xdotool key Return
|
||||
sleep 5
|
||||
|
||||
# Type password
|
||||
log DEBUG "Typing password..."
|
||||
xdotool type --delay 50 "$PASSWORD"
|
||||
xdotool key Return
|
||||
sleep 5
|
||||
|
||||
# Type TOTP
|
||||
log DEBUG "Typing TOTP..."
|
||||
local totp=$(oathtool --totp -b "$TOTP_SECRET")
|
||||
log DEBUG "TOTP: $totp"
|
||||
xdotool type --delay 50 "$totp"
|
||||
xdotool key Return
|
||||
sleep 5
|
||||
|
||||
# Extra enters for any confirmation dialogs
|
||||
log DEBUG "Sending confirmation enters..."
|
||||
xdotool key Return
|
||||
sleep 2
|
||||
xdotool key Return
|
||||
sleep 5
|
||||
xdotool key Return
|
||||
|
||||
log INFO "Auto-login sequence completed"
|
||||
}
|
||||
|
||||
# Watchdog - monitors VPN and reconnects if dropped
|
||||
start_watchdog() {
|
||||
log INFO "Starting VPN watchdog (check every 60s, keepalive ping every 5min)..."
|
||||
|
||||
local check_interval=60
|
||||
local keepalive_interval=300
|
||||
local last_keepalive=0
|
||||
local reconnect_attempts=0
|
||||
local max_reconnect_attempts=3
|
||||
|
||||
while true; do
|
||||
sleep $check_interval
|
||||
|
||||
local now=$(date +%s)
|
||||
local vpn_iface=$(get_vpn_interface)
|
||||
|
||||
if [ -n "$vpn_iface" ]; then
|
||||
# VPN is up
|
||||
reconnect_attempts=0
|
||||
|
||||
# Keepalive ping every 5 minutes to prevent idle timeout
|
||||
if [ $((now - last_keepalive)) -ge $keepalive_interval ]; then
|
||||
if ping -c 1 -W 5 "$TARGET_IP" &>/dev/null; then
|
||||
log DEBUG "Keepalive ping to $TARGET_IP successful"
|
||||
else
|
||||
log WARN "Keepalive ping to $TARGET_IP failed (VPN may be degraded)"
|
||||
fi
|
||||
last_keepalive=$now
|
||||
fi
|
||||
else
|
||||
# VPN is down
|
||||
((reconnect_attempts++))
|
||||
log WARN "VPN disconnected! Reconnect attempt $reconnect_attempts/$max_reconnect_attempts..."
|
||||
|
||||
if [ $reconnect_attempts -le $max_reconnect_attempts ]; then
|
||||
# Kill stale processes and restart
|
||||
kill_cisco_processes "true"
|
||||
sleep 2
|
||||
|
||||
# Start vpnagentd
|
||||
if ! pgrep -x vpnagentd >/dev/null; then
|
||||
/opt/cisco/secureclient/bin/vpnagentd &
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
# Start vpnui
|
||||
export GDK_BACKEND=x11
|
||||
export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
/opt/cisco/secureclient/bin/vpnui &
|
||||
disown
|
||||
sleep 3
|
||||
|
||||
# Run auto-login
|
||||
auto_login &
|
||||
|
||||
# Wait for connection
|
||||
local wait_count=0
|
||||
while [ -z "$(get_vpn_interface)" ] && [ $wait_count -lt 120 ]; do
|
||||
sleep 2
|
||||
((wait_count+=2))
|
||||
done
|
||||
|
||||
if [ -n "$(get_vpn_interface)" ]; then
|
||||
log INFO "VPN reconnected successfully!"
|
||||
setup_forwarding
|
||||
reconnect_attempts=0
|
||||
else
|
||||
log ERROR "Reconnect attempt $reconnect_attempts failed"
|
||||
fi
|
||||
else
|
||||
log ERROR "Max reconnect attempts reached. Manual intervention required."
|
||||
log ERROR "Use menu option 1 to restart VPN manually."
|
||||
# Reset counter after a longer wait
|
||||
sleep 300
|
||||
reconnect_attempts=0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Start Cisco AnyConnect with logging
|
||||
start_anyconnect() {
|
||||
local do_auto_login="$1"
|
||||
log INFO "=== Starting Cisco AnyConnect VPN ==="
|
||||
echo ""
|
||||
|
||||
# Kill existing processes first
|
||||
# Always restart vpnagentd for a clean session
|
||||
kill_cisco_processes "true"
|
||||
|
||||
# Start vpnagentd if not running
|
||||
if ! pgrep -x vpnagentd >/dev/null; then
|
||||
log INFO "Starting vpnagentd..."
|
||||
/opt/cisco/secureclient/bin/vpnagentd &
|
||||
log DEBUG "Waiting for vpnagentd to initialize..."
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
# Show credentials
|
||||
log INFO "Credentials for SSO login:"
|
||||
echo -e " ${CYAN}Email: $EMAIL${NC}"
|
||||
echo -e " ${CYAN}Password: $PASSWORD${NC}"
|
||||
TOTP=$(get_totp)
|
||||
echo -e " ${CYAN}TOTP: $TOTP${NC}"
|
||||
echo ""
|
||||
|
||||
# Start AnyConnect with GPU/WebKit workarounds
|
||||
log INFO "Launching Cisco AnyConnect UI..."
|
||||
export GDK_BACKEND=x11
|
||||
export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
/opt/cisco/secureclient/bin/vpnui &
|
||||
VPNUI_PID=$!
|
||||
disown $VPNUI_PID
|
||||
log DEBUG "vpnui started with PID $VPNUI_PID"
|
||||
|
||||
if [ "$do_auto_login" = "true" ]; then
|
||||
# Run auto-login in background
|
||||
auto_login &
|
||||
AUTO_LOGIN_PID=$!
|
||||
log DEBUG "Auto-login started with PID $AUTO_LOGIN_PID"
|
||||
else
|
||||
log INFO "Manual login mode - use keyboard shortcuts or menu option 2 for credentials"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Wait for VPN to connect
|
||||
log INFO "Waiting for VPN connection..."
|
||||
local wait_count=0
|
||||
local max_wait=300 # 5 minutes
|
||||
while [ -z "$(get_vpn_interface)" ]; do
|
||||
sleep 2
|
||||
((wait_count+=2))
|
||||
if [ $((wait_count % 10)) -eq 0 ]; then
|
||||
log DEBUG "Still waiting for VPN... (${wait_count}s)"
|
||||
fi
|
||||
if [ $wait_count -ge $max_wait ]; then
|
||||
log ERROR "Timeout waiting for VPN connection after ${max_wait}s"
|
||||
stop_xbindkeys
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
log INFO "VPN connected!"
|
||||
local vpn_iface=$(get_vpn_interface)
|
||||
local vpn_ip=$(get_vpn_ip)
|
||||
log DEBUG " Interface: $vpn_iface"
|
||||
log DEBUG " VPN IP: $vpn_ip"
|
||||
|
||||
# Wait a bit for routes to stabilize
|
||||
log DEBUG "Waiting for routes to stabilize..."
|
||||
sleep 3
|
||||
|
||||
# Setup forwarding
|
||||
setup_forwarding
|
||||
|
||||
# Test connection
|
||||
log INFO "Testing connection to $TARGET_IP..."
|
||||
if ping -c 2 -W 3 "$TARGET_IP" &>/dev/null; then
|
||||
log INFO "Connection test: ${GREEN}SUCCESS${NC}"
|
||||
else
|
||||
log WARN "Connection test: ${RED}FAILED${NC} (may need manual route on Windows)"
|
||||
fi
|
||||
|
||||
log INFO "VPN setup complete"
|
||||
|
||||
# Disable screen blanking/power saving
|
||||
xset s off 2>/dev/null || true
|
||||
xset -dpms 2>/dev/null || true
|
||||
xset s noblank 2>/dev/null || true
|
||||
log DEBUG "Screen blanking disabled"
|
||||
|
||||
# Start watchdog in background
|
||||
start_watchdog &
|
||||
WATCHDOG_PID=$!
|
||||
log DEBUG "Watchdog started with PID $WATCHDOG_PID"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-c|--connect)
|
||||
DO_CONNECT=true
|
||||
shift
|
||||
;;
|
||||
-d|--disconnect)
|
||||
DO_DISCONNECT=true
|
||||
shift
|
||||
;;
|
||||
-m|--menu)
|
||||
SKIP_AUTO_LOGIN=true
|
||||
shift
|
||||
;;
|
||||
-r|--routes)
|
||||
show_routes
|
||||
exit 0
|
||||
;;
|
||||
-h|--hosts)
|
||||
show_hosts
|
||||
exit 0
|
||||
;;
|
||||
-s|--status)
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo -e "${CYAN} VPN and Network Status ${NC}"
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo ""
|
||||
check_vpn_status
|
||||
echo ""
|
||||
show_network_status
|
||||
exit 0
|
||||
;;
|
||||
--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Use --help for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$DO_CONNECT" = "true" ] && [ "$DO_DISCONNECT" = "true" ]; then
|
||||
echo "Error: --connect and --disconnect are mutually exclusive"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
parse_args "$@"
|
||||
|
||||
# Cleanup old logs and start fresh
|
||||
cleanup_old_logs
|
||||
|
||||
# Log script start
|
||||
echo "" >> "$(get_log_file)"
|
||||
echo "========================================" >> "$(get_log_file)"
|
||||
log INFO "cisco-vpn script started"
|
||||
log DEBUG "VPN_EMAIL=$EMAIL"
|
||||
log DEBUG "VPN_HOST=$VPN_HOST"
|
||||
log DEBUG "TARGET_IP=$TARGET_IP"
|
||||
log DEBUG "TOTP_SECRET is $([ -n "$TOTP_SECRET" ] && echo 'set' || echo 'NOT SET')"
|
||||
|
||||
print_banner
|
||||
|
||||
if [ "$DO_DISCONNECT" = "true" ]; then
|
||||
disconnect_vpn
|
||||
exit $?
|
||||
fi
|
||||
|
||||
if [ "$DO_CONNECT" = "true" ]; then
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo -e "${CYAN} Dover VPN Connection Script ${NC}"
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo ""
|
||||
if [ "$SKIP_AUTO_LOGIN" = "true" ]; then
|
||||
start_anyconnect "false"
|
||||
else
|
||||
start_anyconnect "true"
|
||||
fi
|
||||
exit $?
|
||||
fi
|
||||
|
||||
log INFO "Script started"
|
||||
echo ""
|
||||
|
||||
# If -m/--menu flag, skip everything and go straight to menu
|
||||
if [ "$SKIP_AUTO_LOGIN" = "true" ]; then
|
||||
log INFO "Menu mode - skipping auto-login"
|
||||
# Check current status
|
||||
elif check_vpn_status; then
|
||||
echo ""
|
||||
log INFO "VPN already connected. Setting up forwarding..."
|
||||
setup_forwarding
|
||||
else
|
||||
echo ""
|
||||
log INFO "Auto-starting VPN connection..."
|
||||
echo ""
|
||||
start_anyconnect "true"
|
||||
fi
|
||||
|
||||
while true; do
|
||||
echo ""
|
||||
main_menu
|
||||
echo -ne "${CYAN}Choice: ${NC}"
|
||||
read -r choice
|
||||
echo ""
|
||||
|
||||
# Ignore empty/whitespace input
|
||||
[[ -z "${choice// }" ]] && continue
|
||||
|
||||
case $choice in
|
||||
1) if [ "$SKIP_AUTO_LOGIN" = "true" ]; then
|
||||
start_anyconnect "false"
|
||||
else
|
||||
start_anyconnect "true"
|
||||
fi ;;
|
||||
2) copy_to_clipboard ;;
|
||||
3) show_totp ;;
|
||||
4) setup_forwarding ;;
|
||||
5) log INFO "Testing connection to $TARGET_IP..."
|
||||
ping -c 3 "$TARGET_IP" && log INFO "Connection test: ${GREEN}SUCCESS${NC}" || log ERROR "Connection test: ${RED}FAILED${NC}" ;;
|
||||
6) show_network_status ;;
|
||||
7) kill_cisco_processes "true";;
|
||||
8) show_routes ;;
|
||||
9) show_hosts ;;
|
||||
e|E) edit_hosts ;;
|
||||
q|Q) log INFO "Goodbye!"; exit 0 ;;
|
||||
*) ;; # Ignore invalid input silently
|
||||
esac
|
||||
done
|
||||
31
apps/rego-tunnel/shared/entrypoint.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# Entrypoint: VNC password setup + DNS fix + systemd
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Ensure all shared scripts are executable (permissions may reset after git pull/appstore update)
|
||||
chmod +x /shared/*.sh /shared/cisco-vpn /root/.vnc/xstartup 2>/dev/null || true
|
||||
|
||||
# Symlink shared scripts into /opt/scripts/ so systemd services (vnc.service) find them
|
||||
mkdir -p /opt/scripts
|
||||
ln -sf /shared/startup-vnc.sh /opt/scripts/startup-vnc.sh
|
||||
|
||||
# Setup TigerVNC password file from env var (passed by runtipi)
|
||||
# TigerVNC expects /root/.vnc/passwd when using SecurityTypes=VncAuth.
|
||||
if [ -n "${VNC_PASSWORD:-}" ]; then
|
||||
mkdir -p /root/.vnc
|
||||
printf '%s\n%s\n' "$VNC_PASSWORD" "$VNC_PASSWORD" | vncpasswd -f > /root/.vnc/passwd
|
||||
chmod 600 /root/.vnc/passwd
|
||||
fi
|
||||
|
||||
cp /etc/resolv.conf /tmp/resolv.conf.bak 2>/dev/null || true
|
||||
cp /etc/hosts /tmp/hosts.bak 2>/dev/null || true
|
||||
umount /etc/resolv.conf 2>/dev/null || true
|
||||
umount /etc/hosts 2>/dev/null || true
|
||||
cat /tmp/resolv.conf.bak > /etc/resolv.conf 2>/dev/null || echo "nameserver 8.8.8.8" > /etc/resolv.conf
|
||||
cat /tmp/hosts.bak > /etc/hosts 2>/dev/null || echo "127.0.0.1 localhost" > /etc/hosts
|
||||
|
||||
echo 1 > /proc/sys/net/ipv4/ip_forward
|
||||
echo "[entrypoint] IP forwarding enabled"
|
||||
|
||||
exec /sbin/init
|
||||
123
apps/rego-tunnel/shared/host-routing.sh
Executable file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Host routing script for rego-tunnel
|
||||
# Routes TARGET_IP through the VPN container
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
ACTION="${1:-start}"
|
||||
|
||||
# Fixed configuration (we assigned these)
|
||||
CONTAINER_IP="172.31.0.10"
|
||||
BRIDGE_NAME="br-rego-vpn"
|
||||
TARGET_IP="${TARGET_IP:-10.35.33.230}"
|
||||
LAN_SUBNET="192.168.0.0/23"
|
||||
LAN_INTERFACES="eth0 eth1 wlan0"
|
||||
LOG_FILE="/var/log/rego-routing.log"
|
||||
|
||||
log() {
|
||||
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [rego-routing] $*"
|
||||
echo "$msg" | tee -a "$LOG_FILE" >&2
|
||||
}
|
||||
|
||||
get_lan_interface() {
|
||||
ip route show default | awk '/default/ {for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1
|
||||
}
|
||||
|
||||
remove_routes() {
|
||||
log "Removing stale routes for $TARGET_IP..."
|
||||
|
||||
# Remove any existing route to TARGET_IP
|
||||
ip route del "$TARGET_IP" 2>/dev/null || true
|
||||
ip route del "$TARGET_IP/32" 2>/dev/null || true
|
||||
|
||||
log "Stale routes removed"
|
||||
}
|
||||
|
||||
apply_routes() {
|
||||
local lan_if
|
||||
lan_if="$(get_lan_interface)"
|
||||
|
||||
log "Applying host routing rules..."
|
||||
log " Container IP: $CONTAINER_IP"
|
||||
log " Bridge: $BRIDGE_NAME"
|
||||
log " Target IP: $TARGET_IP"
|
||||
log " LAN interface: ${lan_if:-unknown}"
|
||||
|
||||
# Enable IP forwarding
|
||||
echo 1 > /proc/sys/net/ipv4/ip_forward
|
||||
log "IP forwarding enabled"
|
||||
|
||||
# Add route to TARGET_IP via container
|
||||
ip route replace "$TARGET_IP/32" via "$CONTAINER_IP" dev "$BRIDGE_NAME"
|
||||
log "Route added: $TARGET_IP via $CONTAINER_IP dev $BRIDGE_NAME"
|
||||
|
||||
# Allow forwarding in DOCKER-USER chain for all LAN interfaces
|
||||
for lan_if in $LAN_INTERFACES; do
|
||||
# Check if interface exists
|
||||
if ip link show "$lan_if" &>/dev/null; then
|
||||
# Allow traffic from LAN to container for TARGET_IP
|
||||
iptables -C DOCKER-USER -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_IP" -j ACCEPT 2>/dev/null || \
|
||||
iptables -I DOCKER-USER 1 -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_IP" -j ACCEPT
|
||||
|
||||
# Allow return traffic
|
||||
iptables -C DOCKER-USER -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_IP" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
|
||||
iptables -I DOCKER-USER 1 -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_IP" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
|
||||
|
||||
log "DOCKER-USER iptables rules added for $lan_if <-> $BRIDGE_NAME"
|
||||
fi
|
||||
done
|
||||
|
||||
# Masquerade traffic from LAN subnet to VPN bridge (so return traffic routes correctly)
|
||||
# Use nft since iptables-nft backend doesn't support iptables -t nat commands
|
||||
if ! nft list chain ip nat POSTROUTING 2>/dev/null | grep -q "saddr $LAN_SUBNET.*oifname.*$BRIDGE_NAME.*masquerade"; then
|
||||
nft add rule ip nat POSTROUTING ip saddr "$LAN_SUBNET" oifname "$BRIDGE_NAME" counter masquerade
|
||||
log "NAT masquerade rule added for $LAN_SUBNET -> $BRIDGE_NAME"
|
||||
else
|
||||
log "NAT masquerade rule already exists for $LAN_SUBNET -> $BRIDGE_NAME"
|
||||
fi
|
||||
|
||||
log "OK: Host routing applied - $TARGET_IP via $CONTAINER_IP ($BRIDGE_NAME)"
|
||||
}
|
||||
|
||||
remove_all() {
|
||||
log "Removing all routing rules..."
|
||||
|
||||
# Remove route
|
||||
ip route del "$TARGET_IP/32" via "$CONTAINER_IP" dev "$BRIDGE_NAME" 2>/dev/null || true
|
||||
|
||||
# Remove iptables rules for all LAN interfaces
|
||||
for lan_if in $LAN_INTERFACES; do
|
||||
iptables -D DOCKER-USER -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_IP" -j ACCEPT 2>/dev/null || true
|
||||
iptables -D DOCKER-USER -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_IP" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Remove masquerade rule (using nft)
|
||||
local handle
|
||||
handle=$(nft -a list chain ip nat POSTROUTING 2>/dev/null | grep "saddr $LAN_SUBNET.*oifname.*$BRIDGE_NAME.*masquerade" | grep -oP 'handle \K\d+' | head -1 || true)
|
||||
if [ -n "$handle" ]; then
|
||||
nft delete rule ip nat POSTROUTING handle "$handle" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
log "All routing rules removed"
|
||||
}
|
||||
|
||||
case "$ACTION" in
|
||||
start)
|
||||
remove_routes
|
||||
apply_routes
|
||||
;;
|
||||
stop)
|
||||
remove_all
|
||||
;;
|
||||
restart)
|
||||
remove_all
|
||||
sleep 1
|
||||
remove_routes
|
||||
apply_routes
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
55
apps/rego-tunnel/shared/install-host-services.sh
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Install host-side systemd services for rego-tunnel
|
||||
# Run this ONCE on the host after installing the app in Runtipi
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
APP_DATA_DIR="/etc/runtipi/app-data/runtipi/rego-tunnel"
|
||||
|
||||
echo "Installing rego-tunnel host services..."
|
||||
|
||||
# Create the path watcher unit
|
||||
cat << 'EOF' | sudo tee /etc/systemd/system/rego-routing-watcher.path
|
||||
[Unit]
|
||||
Description=Watch for rego-tunnel routing trigger
|
||||
|
||||
[Path]
|
||||
PathExists=/etc/runtipi/app-data/runtipi/rego-tunnel/restart-routing
|
||||
Unit=rego-routing-watcher.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Create the service unit
|
||||
cat << EOF | sudo tee /etc/systemd/system/rego-routing-watcher.service
|
||||
[Unit]
|
||||
Description=Apply rego-tunnel routing rules
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash ${SCRIPT_DIR}/host-routing.sh restart
|
||||
ExecStartPost=/bin/rm -f ${APP_DATA_DIR}/restart-routing
|
||||
ExecStartPost=/bin/bash -c 'echo "trigger cleared at \$(date)" >> ${APP_DATA_DIR}/watcher.log'
|
||||
EOF
|
||||
|
||||
# Make host-routing.sh executable
|
||||
sudo chmod +x "${SCRIPT_DIR}/host-routing.sh"
|
||||
|
||||
# Reload systemd and enable the watcher
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now rego-routing-watcher.path
|
||||
|
||||
echo ""
|
||||
echo "Done! Services installed:"
|
||||
echo " - rego-routing-watcher.path (watches for trigger file)"
|
||||
echo " - rego-routing-watcher.service (applies routing rules)"
|
||||
echo ""
|
||||
echo "To check status:"
|
||||
echo " systemctl status rego-routing-watcher.path"
|
||||
echo ""
|
||||
echo "To manually trigger routing:"
|
||||
echo " touch ${APP_DATA_DIR}/restart-routing"
|
||||
12
apps/rego-tunnel/shared/startup-vnc.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
export HOME='/root'
|
||||
export USER='root'
|
||||
rm -f /tmp/.P1-lock /tmp/.X11-unix/X1 2>/dev/null || true
|
||||
rm -rf /tmp/.X*-lock /tmp/.X14-unix/* 2>/dev/null || true
|
||||
echo "Starting TigerVNC server on display :1..."
|
||||
vncserver :1 -geometry 1280x800 -depth 24 -SecurityTypes VncAuth -localhost no
|
||||
sleep 2
|
||||
echo "Starting noVNC on port ${NOVNC_PORT:-6080}..."
|
||||
websockify --web=/usr/share/novnc/ ${NOVNC_PORT:-6080} localhost:5901 &
|
||||
tail -f /root/.vnc/*.log
|
||||
24
apps/rego-tunnel/shared/uninstall-host-services.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Uninstall host-side systemd services for rego-tunnel
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
echo "Removing rego-tunnel host services..."
|
||||
|
||||
# Stop and disable the watcher
|
||||
sudo systemctl stop rego-routing-watcher.path 2>/dev/null || true
|
||||
sudo systemctl disable rego-routing-watcher.path 2>/dev/null || true
|
||||
|
||||
# Remove routing rules
|
||||
/etc/runtipi/repos/runtipi/apps/rego-tunnel/shared/host-routing.sh stop 2>/dev/null || true
|
||||
|
||||
# Remove systemd units
|
||||
sudo rm -f /etc/systemd/system/rego-routing-watcher.path
|
||||
sudo rm -f /etc/systemd/system/rego-routing-watcher.service
|
||||
|
||||
# Reload systemd
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
echo ""
|
||||
echo "Done! Host services removed."
|
||||
39
apps/rego-tunnel/shared/xstartup
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
# VNC xstartup - launches terminal with cisco-vpn script
|
||||
|
||||
unset SESSION_MANAGER
|
||||
unset DBUS_SESSION_BUS_ADDRESS
|
||||
|
||||
# Import environment variables from container (PID 1)
|
||||
# Systemd services don't inherit Docker env vars, so we source them here
|
||||
while IFS= read -r -d '' line; do
|
||||
export "$line"
|
||||
done < /proc/1/environ
|
||||
|
||||
export XDG_RUNTIME_DIR=/tmp/runtime-root
|
||||
mkdir -p $XDG_RUNTIME_DIR
|
||||
chmod 700 $XDG_RUNTIME_DIR
|
||||
|
||||
# GPU/WebKit workarounds for Cisco UI
|
||||
export GDK_BACKEND=x11
|
||||
export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
|
||||
# Start dbus session
|
||||
[ -x /usr/bin/dbus-launch ] && eval $(dbus-launch --sh-syntax --exit-with-session)
|
||||
|
||||
# Start window manager
|
||||
openbox &
|
||||
sleep 2
|
||||
|
||||
# Disable screen blanking and power saving
|
||||
xset s off 2>/dev/null || true
|
||||
xset -dpms 2>/dev/null || true
|
||||
xset s noblank 2>/dev/null || true
|
||||
|
||||
# Make script executable and launch in terminal
|
||||
chmod +x /shared/cisco-vpn 2>/dev/null || true
|
||||
xterm -fa 'Monospace' -fs 11 -bg black -fg white -geometry 130x45+10+10 \
|
||||
-title "Rego VPN Terminal" \
|
||||
-e "bash -c '/shared/cisco-vpn; exec bash'" &
|
||||
|
||||
wait
|
||||
685
apps/scalar/apidocs/admin.json
Normal file
@@ -0,0 +1,685 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "REST4i - Administration Services",
|
||||
"description": "Administration Services provide APIs that give information about RSE API and the runtime environment of the RSE API server. To use the APIs, the authenticated user must authenticate to localhost and have the administrator role or have *ALLOBJ special authority.",
|
||||
"version": "1.0.7-rest4i",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "https://github.com/rest4i"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/rest4i/api"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "Administration Services",
|
||||
"description": "Administration Services provide APIs that give information about RSE API and the runtime environment of the RSE API server. To use the APIs, the authenticated user must authenticate to localhost and have the administrator role or have *ALLOBJ special authority."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/v1/admin/memory": {
|
||||
"get": {
|
||||
"tags": ["Administration Services"],
|
||||
"summary": "Get information about server memory usage.",
|
||||
"description": "Get information about the JVM memory usage of the server running RSE API.",
|
||||
"operationId": "adminGetMemoryUsage",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"jvmFreeMemory": 17428920,
|
||||
"jvmMaxMemory": 4294967296,
|
||||
"jvmTotalMemory": 78249984
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/admin/settings": {
|
||||
"get": {
|
||||
"tags": ["Administration Services"],
|
||||
"summary": "Get the general settings being used for RSE API.",
|
||||
"description": "The settings are global in nature and include settings for tuning, environment, and administrator override categories.",
|
||||
"operationId": "adminGetSettings",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"adminUsers": ["USER1", "USER2"],
|
||||
"includeUsers": ["USER1", "USER2"],
|
||||
"excludeUsers": [],
|
||||
"maxFileSize": 3072000,
|
||||
"maxSessionInactivity": 7200,
|
||||
"maxSessionLifetime": 21600,
|
||||
"maxSessionUseCount": 1000,
|
||||
"maxSessionWaitTime": 300,
|
||||
"maxSessions": 100,
|
||||
"maxSessionsPerUser": 20,
|
||||
"sessionCleanupInterval": 300
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": ["Administration Services"],
|
||||
"summary": "Set settings for RSE API.",
|
||||
"description": "The settings are global in nature and include settings for tuning, environment, and administrator override categories.",
|
||||
"operationId": "adminSetSettings",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "The settings for RSE API.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/rest4i_Settings"
|
||||
},
|
||||
"example": {
|
||||
"persist": false,
|
||||
"adminUsers": ["USER1", "USER2"],
|
||||
"includeUsers": ["USER1", "USER2"],
|
||||
"excludeUsers": [],
|
||||
"maxFileSize": 3072000,
|
||||
"maxSessionInactivity": 7200,
|
||||
"maxSessionLifetime": 21600,
|
||||
"maxSessionUseCount": 1000,
|
||||
"maxSessionWaitTime": 300,
|
||||
"maxSessions": 100,
|
||||
"maxSessionsPerUser": 20,
|
||||
"sessionCleanupInterval": 300
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Successful request, no content."
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/admin/environment": {
|
||||
"get": {
|
||||
"tags": ["Administration Services"],
|
||||
"summary": "Get information about server environment.",
|
||||
"description": "Get information about server environment, such as host, operating system, Java version, and port.",
|
||||
"operationId": "adminGetEnvironment",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"rest4iBasepath": "rest4i",
|
||||
"rest4iHostname": "UT30P44",
|
||||
"rest4iPort": 2012,
|
||||
"rest4iVersion": "1.0.6",
|
||||
"osName": "OS/400",
|
||||
"osVersion": "V7R5M0",
|
||||
"javaVersion": "1.8.0_351"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/admin/serverinfo": {
|
||||
"get": {
|
||||
"tags": ["Administration Services"],
|
||||
"summary": "Get server information.",
|
||||
"description": "Get basic server information including hostname, port, version, and base path.",
|
||||
"operationId": "adminGetServerInfo",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"rest4iBasepath": "rest4i",
|
||||
"rest4iHostname": "UT30P44",
|
||||
"rest4iPort": 2012,
|
||||
"rest4iVersion": "1.0.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/admin/sessions": {
|
||||
"get": {
|
||||
"tags": ["Administration Services"],
|
||||
"summary": "Get information about sessions.",
|
||||
"description": "Get information about sessions. The information that is returned applies to active sessions on the server. Active sessions may include sessions that are expired but have not been reclaimed by RSE API.",
|
||||
"operationId": "adminGetSessions",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"totalSessions": 3,
|
||||
"sessions": [
|
||||
{
|
||||
"userid": "USER1",
|
||||
"sessionCount": 1
|
||||
},
|
||||
{
|
||||
"userid": "USER2",
|
||||
"sessionCount": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["Administration Services"],
|
||||
"summary": "Delete sessions.",
|
||||
"description": "Delete sessions. You can delete all active sessions or only sessions tied to a user ID. Sessions that are deleted are marked as expired.",
|
||||
"operationId": "adminClearSessions",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "user",
|
||||
"in": "query",
|
||||
"description": "The session(s) to delete. Specify the user ID to delete all sessions created by the user, or the special value of *ALL to delete all sessions for all users.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "*ALL"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Successful request, no content."
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"rest4i_Settings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"persist": {
|
||||
"type": "boolean",
|
||||
"description": "Save settings to property file in persistent storage (hard disk).",
|
||||
"default": false
|
||||
},
|
||||
"adminUsers": {
|
||||
"type": "array",
|
||||
"description": "User IDs that will be designated as an RSE API administrator.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"includeUsers": {
|
||||
"type": "array",
|
||||
"description": "User IDs allowed to use RSE API.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"excludeUsers": {
|
||||
"type": "array",
|
||||
"description": "User IDs not allowed to use RSE API.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"maxFileSize": {
|
||||
"maximum": 15360000,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "Maximum size of IFS file data that can be processed (reading or writing) by RSE API.",
|
||||
"format": "int64",
|
||||
"default": 3072000
|
||||
},
|
||||
"maxSessions": {
|
||||
"minimum": -1,
|
||||
"type": "integer",
|
||||
"description": "Maximum number of total sessions. The value of -1 indicates there is no limit.",
|
||||
"format": "int64",
|
||||
"default": 100
|
||||
},
|
||||
"maxSessionsPerUser": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of sessions allowed on a per-user basis. The value of -1 indicates there is no limit.",
|
||||
"format": "int64",
|
||||
"default": 20
|
||||
},
|
||||
"maxSessionInactivity": {
|
||||
"maximum": 7200,
|
||||
"minimum": 30,
|
||||
"type": "integer",
|
||||
"description": "Maximum amount of inactive time, in seconds, before an available session is invalidated.",
|
||||
"format": "int64",
|
||||
"default": 7200
|
||||
},
|
||||
"maxSessionLifetime": {
|
||||
"type": "integer",
|
||||
"description": "Maximum life, in seconds, for a session. The value of -1 indicates there is no limit.",
|
||||
"format": "int64",
|
||||
"default": -1
|
||||
},
|
||||
"maxSessionUseCount": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of times a session can be used before it is invalidated. The value of -1 indicates there is no limit.",
|
||||
"format": "int64",
|
||||
"default": -1
|
||||
},
|
||||
"maxSessionWaitTime": {
|
||||
"minimum": -1,
|
||||
"type": "integer",
|
||||
"description": "Maximum time, in seconds, to wait on a session to become available. The value of -1 indicates there is no limit.",
|
||||
"format": "int64",
|
||||
"default": 300
|
||||
},
|
||||
"sessionCleanupInterval": {
|
||||
"maximum": 900,
|
||||
"minimum": 30,
|
||||
"type": "integer",
|
||||
"description": "The time interval, in seconds, for how often the session maintenance daemon is run.",
|
||||
"format": "int64",
|
||||
"default": 300
|
||||
}
|
||||
},
|
||||
"description": "Global settings for RSE API."
|
||||
},
|
||||
"Problem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"details": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"bearerHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Bearer token authentication.",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "Bearer [token]"
|
||||
},
|
||||
"basicHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Basic authentication.",
|
||||
"scheme": "basic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
351
apps/scalar/apidocs/cl.json
Normal file
@@ -0,0 +1,351 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "REST4i - CL Command Services",
|
||||
"description": "CL Command Services provide APIs for running CL commands.",
|
||||
"version": "1.0.7-rest4i",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "https://github.com/rest4i"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/rest4i/api"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "CL Command Services",
|
||||
"description": "CL Command Services provide APIs for running CL commands."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/v1/cl": {
|
||||
"post": {
|
||||
"tags": ["CL Command Services"],
|
||||
"summary": "Run one or more CL commands on the server.",
|
||||
"description": "Run one or more CL commands on the server. Accepts both JSON and form-urlencoded content types. If a command fails, any messages relating to the error is returned. If the command succeeds, no data is returned.",
|
||||
"operationId": "clRunCLCommand",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "List of CL commands to run.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/rest4i_CLCommands"
|
||||
},
|
||||
"example": {
|
||||
"continueOnError": true,
|
||||
"includeMessageOnSuccess": true,
|
||||
"includeMessageHelpText": false,
|
||||
"clCommands": [
|
||||
"qsys/crtlib lib1",
|
||||
"qsys/crtsrcpf lib1/qrpglesrc"
|
||||
]
|
||||
}
|
||||
},
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"continueOnError": {
|
||||
"type": "boolean",
|
||||
"description": "Continue processing if error encountered",
|
||||
"default": false
|
||||
},
|
||||
"includeMessageOnSuccess": {
|
||||
"type": "boolean",
|
||||
"description": "Return messages on success",
|
||||
"default": false
|
||||
},
|
||||
"includeMessageHelpText": {
|
||||
"type": "boolean",
|
||||
"description": "Return message help text",
|
||||
"default": false
|
||||
},
|
||||
"clCommands": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Array of CL command strings to execute"
|
||||
}
|
||||
},
|
||||
"required": ["clCommands"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Command(s) issued successfully, error(s) may have occurred.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"totalIssued": 2,
|
||||
"totalSuccesses": 1,
|
||||
"totalFailures": 1,
|
||||
"commandOutputList": [
|
||||
{
|
||||
"success": false,
|
||||
"command": "qsys/crtlib lib1",
|
||||
"output": ["CPF2111: Library LIB1 already exists."]
|
||||
},
|
||||
{
|
||||
"success": true,
|
||||
"command": "qsys/crtsrcpf lib1/qrpglesrc",
|
||||
"output": ["CPC7301: File QRPGLESRC created in library LIB1."]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"204": {
|
||||
"description": "All command(s) issued successfully, no content."
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/cl/{commandname}": {
|
||||
"get": {
|
||||
"tags": ["CL Command Services"],
|
||||
"summary": "Retrieves the command definition for the specified CL command.",
|
||||
"description": "Retrieves the command definition for the specified CL command. The command definition is returned as an XML document. The generated command information XML source is called Command Definition Markup Language or CDML. See the Document Type Definition (DTD) in /QIBM/XML/DTD/QcdCLCmd.dtd for the definition of the CDML tag language returned by this API.",
|
||||
"operationId": "clGetCommandDefinition",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "library",
|
||||
"in": "query",
|
||||
"description": "The library name. Valid values are a specific name, or one of the following special values: *CURLIB - The current library is searched. *LIBL - The library list is searched. This is the default.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "*LIBL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ignorecase",
|
||||
"in": "query",
|
||||
"description": "Boolean indicating whether case should be ignored. The default value is 'true'. If set to 'false', the library and command name will not be uppercased.",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "commandname",
|
||||
"in": "path",
|
||||
"description": "The CL command name.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"definition": "<QcdCLCmd DTDVersion=\"1.0\">...</QcdCLCmd>"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The specified resource was not found.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"rest4i_CLCommands": {
|
||||
"required": ["clCommands"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"continueOnError": {
|
||||
"type": "boolean",
|
||||
"description": "Continue processing CL commands if an error is encountered.",
|
||||
"default": false
|
||||
},
|
||||
"includeMessageOnSuccess": {
|
||||
"type": "boolean",
|
||||
"description": "Return CL command messages on success.",
|
||||
"default": false
|
||||
},
|
||||
"includeMessageHelpText": {
|
||||
"type": "boolean",
|
||||
"description": "Return message help text for CL command messages.",
|
||||
"default": false
|
||||
},
|
||||
"clCommands": {
|
||||
"type": "array",
|
||||
"description": "CL command to run.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "List of CL commands to run."
|
||||
},
|
||||
"Problem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"details": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"bearerHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Bearer token authentication.",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "Bearer [token]"
|
||||
},
|
||||
"basicHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Basic authentication.",
|
||||
"scheme": "basic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
394
apps/scalar/apidocs/database.json
Normal file
@@ -0,0 +1,394 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "REST4i - Database Services",
|
||||
"description": "Enhanced database services with connection pooling and multiple output formats. Execute SQL queries with support for JSON, XML, CSV, HTML, and Excel output formats.",
|
||||
"version": "1.0.7-rest4i",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "https://github.com/rest4i"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/rest4i/api"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "Database Services",
|
||||
"description": "Enhanced database services with connection pooling and multiple output formats."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/v1/database/query": {
|
||||
"post": {
|
||||
"tags": ["Database Services"],
|
||||
"summary": "Execute SQL query with multiple output formats",
|
||||
"description": "Execute a SQL query against the IBM i database with support for multiple output formats including JSON, XML, CSV, HTML, and Excel.",
|
||||
"operationId": "executeQuery",
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "The SQL query and formatting options",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DatabaseQueryRequest"
|
||||
},
|
||||
"example": {
|
||||
"sql": "SELECT * FROM QSYS2.SYSTABLES FETCH FIRST 10 ROWS ONLY",
|
||||
"format": "json",
|
||||
"treatWarningsAsErrors": false,
|
||||
"alwaysReturnSQLStateInformation": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "SQL query executed successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponse"
|
||||
},
|
||||
"example": {
|
||||
"success": true,
|
||||
"data": {
|
||||
"rowCount": 2,
|
||||
"data": [
|
||||
{
|
||||
"TABLE_SCHEMA": "QSYS2",
|
||||
"TABLE_NAME": "SYSTABLES",
|
||||
"TABLE_TYPE": "SYSTEM TABLE"
|
||||
}
|
||||
]
|
||||
},
|
||||
"message": "Query executed successfully",
|
||||
"timestamp": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
},
|
||||
"application/xml": {
|
||||
"example": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><result><success>true</success><data><row><TABLE_SCHEMA>QSYS2</TABLE_SCHEMA></row></data></result>"
|
||||
},
|
||||
"text/csv": {
|
||||
"example": "TABLE_SCHEMA,TABLE_NAME,TABLE_TYPE\n\"QSYS2\",\"SYSTABLES\",\"SYSTEM TABLE\""
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - invalid SQL or parameters",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponse"
|
||||
},
|
||||
"example": {
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Validation failed",
|
||||
"timestamp": "2024-01-15T10:30:00Z",
|
||||
"validationErrors": [
|
||||
{
|
||||
"field": "sql",
|
||||
"message": "SQL query is required"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden - dangerous SQL detected",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"tags": ["Database Services"],
|
||||
"summary": "Execute SQL query via GET with multiple output formats",
|
||||
"description": "Execute a SQL query via GET request with support for multiple output formats.",
|
||||
"operationId": "executeQueryGet",
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sql",
|
||||
"in": "query",
|
||||
"description": "The SQL query to execute",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "SELECT * FROM QSYS2.SYSTABLES FETCH FIRST 10 ROWS ONLY"
|
||||
},
|
||||
{
|
||||
"name": "format",
|
||||
"in": "query",
|
||||
"description": "Output format: json, xml, csv, html, excel",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["json", "xml", "csv", "html", "excel"],
|
||||
"default": "json"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "SQL query executed successfully"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - invalid SQL or parameters",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden - dangerous SQL detected",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"DatabaseQueryRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sql": {
|
||||
"type": "string",
|
||||
"description": "The SQL query to execute",
|
||||
"example": "SELECT * FROM QSYS2.SYSTABLES FETCH FIRST 10 ROWS ONLY"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"enum": ["json", "xml", "csv", "html", "excel"],
|
||||
"default": "json",
|
||||
"description": "Output format for the query results"
|
||||
},
|
||||
"treatWarningsAsErrors": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Whether to treat SQL warnings as errors"
|
||||
},
|
||||
"alwaysReturnSQLStateInformation": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Whether to always return SQL state information"
|
||||
}
|
||||
},
|
||||
"required": ["sql"]
|
||||
},
|
||||
"ApiResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates if the operation was successful"
|
||||
},
|
||||
"data": {
|
||||
"description": "The response data (varies by endpoint)"
|
||||
},
|
||||
"error": {
|
||||
"$ref": "#/components/schemas/ErrorInfo"
|
||||
},
|
||||
"pagination": {
|
||||
"$ref": "#/components/schemas/PaginationInfo"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Success or informational message"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "ISO-8601 formatted timestamp of the response"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "Additional metadata about the response"
|
||||
}
|
||||
},
|
||||
"required": ["success", "timestamp"]
|
||||
},
|
||||
"ErrorInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "Error code identifying the type of error"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Human-readable error message"
|
||||
},
|
||||
"details": {
|
||||
"type": "string",
|
||||
"description": "Additional error details"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "ISO-8601 formatted timestamp when error occurred"
|
||||
},
|
||||
"validationErrors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
},
|
||||
"description": "List of validation errors if applicable"
|
||||
}
|
||||
},
|
||||
"required": ["code", "message"]
|
||||
},
|
||||
"ValidationError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"description": "The field that failed validation"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "The validation error message"
|
||||
},
|
||||
"rejectedValue": {
|
||||
"description": "The value that was rejected during validation"
|
||||
}
|
||||
},
|
||||
"required": ["field", "message"]
|
||||
},
|
||||
"PaginationInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"page": {
|
||||
"type": "integer",
|
||||
"description": "Current page number (0-based)"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"description": "Number of elements per page"
|
||||
},
|
||||
"totalPages": {
|
||||
"type": "integer",
|
||||
"description": "Total number of pages"
|
||||
},
|
||||
"totalElements": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Total number of elements across all pages"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Problem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"details": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"bearerHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Bearer token authentication.",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "Bearer [token]"
|
||||
},
|
||||
"basicHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Basic authentication.",
|
||||
"scheme": "basic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
404
apps/scalar/apidocs/files.json
Normal file
@@ -0,0 +1,404 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "REST4i - File Operations Services",
|
||||
"description": "Enhanced file operations services supporting both IFS and SMB file operations with improved error handling and response formatting.",
|
||||
"version": "1.0.7-rest4i",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "https://github.com/rest4i"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/rest4i/api"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "File Operations Services",
|
||||
"description": "Enhanced file operations services supporting both IFS and SMB file operations."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/v1/files/list": {
|
||||
"get": {
|
||||
"tags": ["File Operations Services"],
|
||||
"summary": "List directory contents",
|
||||
"description": "List files and directories at specified path using SMB/CIFS or IFS",
|
||||
"operationId": "listFiles",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"description": "Directory path to list",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "/home/user/documents"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Directory listing successful",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"success": true,
|
||||
"data": {
|
||||
"path": "/home/user/documents",
|
||||
"files": [
|
||||
{
|
||||
"name": "report.pdf",
|
||||
"size": 102400,
|
||||
"isDirectory": false
|
||||
},
|
||||
{
|
||||
"name": "archive",
|
||||
"size": 0,
|
||||
"isDirectory": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/files/download": {
|
||||
"get": {
|
||||
"tags": ["File Operations Services"],
|
||||
"summary": "Download a file",
|
||||
"description": "Downloads a file from the specified SMB path.",
|
||||
"operationId": "downloadFile",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"description": "The SMB path to the file",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "domain",
|
||||
"in": "query",
|
||||
"description": "SMB domain",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "username",
|
||||
"in": "query",
|
||||
"description": "SMB username",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"in": "query",
|
||||
"description": "SMB password",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "File downloaded successfully",
|
||||
"content": {
|
||||
"application/octet-stream": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - path parameter required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "File not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/files/upload": {
|
||||
"post": {
|
||||
"tags": ["File Operations Services"],
|
||||
"summary": "Upload a file",
|
||||
"description": "Uploads a file to the specified SMB path.",
|
||||
"operationId": "uploadFile",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UploadRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "File uploaded successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"success": true,
|
||||
"message": "File uploaded successfully",
|
||||
"folder": "/upload/path",
|
||||
"filename": "document.pdf"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - missing required parameters",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/files/zip": {
|
||||
"get": {
|
||||
"tags": ["File Operations Services"],
|
||||
"summary": "Zip directory contents",
|
||||
"description": "Creates a ZIP archive of all files in the specified directory.",
|
||||
"operationId": "zipDirectory",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"description": "The SMB path to the directory",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "domain",
|
||||
"in": "query",
|
||||
"description": "SMB domain",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "username",
|
||||
"in": "query",
|
||||
"description": "SMB username",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"in": "query",
|
||||
"description": "SMB password",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Directory zipped successfully",
|
||||
"content": {
|
||||
"application/zip": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - path parameter required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Directory not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/files/exists": {
|
||||
"get": {
|
||||
"tags": ["File Operations Services"],
|
||||
"summary": "Check if file or directory exists",
|
||||
"description": "Checks whether a file or directory exists at the specified SMB path.",
|
||||
"operationId": "checkExists",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"description": "The SMB path to check",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "domain",
|
||||
"in": "query",
|
||||
"description": "SMB domain",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "username",
|
||||
"in": "query",
|
||||
"description": "SMB username",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"in": "query",
|
||||
"description": "SMB password",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Existence check completed",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"success": true,
|
||||
"path": "/path/to/check",
|
||||
"exists": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - path parameter required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"UploadRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"folder": {
|
||||
"type": "string",
|
||||
"description": "The target folder path for upload"
|
||||
},
|
||||
"filename": {
|
||||
"type": "string",
|
||||
"description": "The name of the file to upload"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"description": "SMB domain"
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"description": "SMB username"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"description": "SMB password"
|
||||
},
|
||||
"fileContent": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "Base64 encoded file content"
|
||||
}
|
||||
},
|
||||
"required": ["folder", "filename", "fileContent"]
|
||||
},
|
||||
"Problem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"details": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"bearerHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Bearer token authentication.",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "Bearer [token]"
|
||||
},
|
||||
"basicHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Basic authentication.",
|
||||
"scheme": "basic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
76
apps/scalar/apidocs/health.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "REST4i - Health Services",
|
||||
"description": "Health check endpoints for service monitoring and liveness probes.",
|
||||
"version": "1.0.7-rest4i",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "https://github.com/rest4i"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/rest4i/api"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "Health",
|
||||
"description": "Health check endpoints for service monitoring and liveness probes."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/v1/health/ping": {
|
||||
"get": {
|
||||
"tags": ["Health"],
|
||||
"summary": "Ping",
|
||||
"description": "Lightweight liveness check.",
|
||||
"operationId": "get_api_v1_health_ping",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Service is alive",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ok": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["ok"]
|
||||
},
|
||||
"examples": {
|
||||
"ok": {
|
||||
"value": {
|
||||
"ok": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"basicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {},
|
||||
"securitySchemes": {
|
||||
"basicAuth": {
|
||||
"type": "http",
|
||||
"scheme": "basic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
530
apps/scalar/apidocs/ifs.json
Normal file
@@ -0,0 +1,530 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "REST4i - IFS Services",
|
||||
"description": "Integrated File System (IFS) Services provide APIs for accessing objects in a way that is like personal computer and UNIX operating systems. This includes listing objects in directories, reading from files, and writing to files.",
|
||||
"version": "1.0.7-rest4i",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "https://github.com/rest4i"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/rest4i/api"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "IFS Services",
|
||||
"description": "Integrated File System (IFS) Services provide APIs for accessing objects in a way that is like personal computer and UNIX operating systems."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/v1/ifs/{path}": {
|
||||
"get": {
|
||||
"tags": ["IFS Services"],
|
||||
"summary": "Get the content of a file.",
|
||||
"description": "Get the content of a file. The content is returned in a JSON object.",
|
||||
"operationId": "ifsGetFileContent",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ETag",
|
||||
"in": "header",
|
||||
"description": "Whether to return checksum for the file.",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"in": "path",
|
||||
"description": "Path to file for which the data is to be read.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"ccsid": 819,
|
||||
"content": "Hello world\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The specified resource was not found.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"tags": ["IFS Services"],
|
||||
"summary": "Write a string to a file.",
|
||||
"description": "Write a string to a file. The file must exist and its contents will be replaced by the string specified in the request.",
|
||||
"operationId": "ifsPutFileContent",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "If-Match",
|
||||
"in": "header",
|
||||
"description": "If the If-Match HTTP header is passed, RSE API will check to see if the Etag (MD5 hash of the object content) matches the provided Etag value. If this value matches, the operation will proceed. If the match fails, the system will return a 412 (Precondition Failed) error.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"in": "path",
|
||||
"description": "Path to file in which the data will be written.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "File content.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/rest4i_FileContent"
|
||||
},
|
||||
"example": {
|
||||
"content": "some data that will be written to file."
|
||||
}
|
||||
},
|
||||
"text/plain": {
|
||||
"schema": {},
|
||||
"example": "some data that will be written to file.\n"
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Successful request, no content."
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The specified resource was not found.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"412": {
|
||||
"description": "Precondition failed."
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/ifs/list": {
|
||||
"get": {
|
||||
"tags": ["IFS Services"],
|
||||
"summary": "Gets a list of objects in the specified path.",
|
||||
"description": "Gets a list of objects in the specified path. The information returned includes the name, whether object is a directory, the description, and the object subtype. For objects that are not in the QSYS.LIB file system, any part of the path may contain an asterisk (*), which is a wildcard that means zero or more instances of any character.",
|
||||
"operationId": "ifsListDir",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"description": "The working directory. For example, /u/IBM/test",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "subtype",
|
||||
"in": "query",
|
||||
"description": "Subtype of objects to return. Valid values include a specific object type (*LIB, *FILE, *PGM, *OUTQ, etc.) or *ALL.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "includehidden",
|
||||
"in": "query",
|
||||
"description": "Whether to show hidden files.",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"objects": [
|
||||
{
|
||||
"path": "/QSYS.LIB/USER1.LIB/QCLSRC.FILE",
|
||||
"description": "\"test\"",
|
||||
"isDir": true,
|
||||
"subType": "PF-SRC"
|
||||
},
|
||||
{
|
||||
"path": "/QSYS.LIB/USER1.LIB/QCSRC.FILE",
|
||||
"description": "",
|
||||
"isDir": true,
|
||||
"subType": "PF-SRC"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The specified resource was not found.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v1/ifs/{path}/info": {
|
||||
"get": {
|
||||
"tags": ["IFS Services"],
|
||||
"summary": "Returns information about the object referenced by the path.",
|
||||
"description": "Returns information about the object referenced by the path. The information returned includes the name, whether object is a directory, the description, the object subtype, CCSID, size, last modified timestamp, and object subtype.",
|
||||
"operationId": "ifsGetFileInfo",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"in": "path",
|
||||
"description": "Path to object for which information is to be returned.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"path": "/qsys.lib/user1.lib",
|
||||
"description": "user1's lib",
|
||||
"isDir": true,
|
||||
"subType": "PROD",
|
||||
"owner": "USER1",
|
||||
"ccsid": 37,
|
||||
"lastModified": 1680559633,
|
||||
"size": 401408,
|
||||
"recordLength": -1,
|
||||
"numberOfRecords": -1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The specified resource was not found.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"rest4i_FileContent": {
|
||||
"required": ["content"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "The file content."
|
||||
},
|
||||
"Problem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"details": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"bearerHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Bearer token authentication.",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "Bearer [token]"
|
||||
},
|
||||
"basicHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Basic authentication.",
|
||||
"scheme": "basic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
149
apps/scalar/apidocs/index.json
Normal file
@@ -0,0 +1,149 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "REST4i API Specifications Index",
|
||||
"description": "This index lists all available OpenAPI specification files for the REST4i API. Each category has its own specification file for easier navigation and consumption.",
|
||||
"version": "1.0.7-rest4i",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "https://github.com/rest4i"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
}
|
||||
},
|
||||
"specifications": [
|
||||
{
|
||||
"name": "Administration Services",
|
||||
"file": "admin.json",
|
||||
"description": "Administration Services provide APIs that give information about RSE API and the runtime environment of the RSE API server.",
|
||||
"endpoints": 7,
|
||||
"paths": [
|
||||
"/v1/admin/memory",
|
||||
"/v1/admin/settings",
|
||||
"/v1/admin/environment",
|
||||
"/v1/admin/serverinfo",
|
||||
"/v1/admin/sessions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CL Command Services",
|
||||
"file": "cl.json",
|
||||
"description": "CL Command Services provide APIs for running CL commands.",
|
||||
"endpoints": 2,
|
||||
"paths": [
|
||||
"/v1/cl",
|
||||
"/v1/cl/{commandname}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SQL Services",
|
||||
"file": "sql.json",
|
||||
"description": "SQL Services provide APIs associated with performing SQL operations.",
|
||||
"endpoints": 1,
|
||||
"paths": [
|
||||
"/v1/sql"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "IFS Services",
|
||||
"file": "ifs.json",
|
||||
"description": "Integrated File System (IFS) Services provide APIs for accessing objects in a way that is like personal computer and UNIX operating systems.",
|
||||
"endpoints": 4,
|
||||
"paths": [
|
||||
"/v1/ifs/{path}",
|
||||
"/v1/ifs/list",
|
||||
"/v1/ifs/{path}/info"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "QSYS Services",
|
||||
"file": "qsys.json",
|
||||
"description": "QSYS Services provide APIs for accessing QSYS objects.",
|
||||
"endpoints": 1,
|
||||
"paths": [
|
||||
"/v1/qsys/search/{objectName}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Session Services",
|
||||
"file": "session.json",
|
||||
"description": "Session Services provide APIs for authenticating a user and managing sessions.",
|
||||
"endpoints": 4,
|
||||
"paths": [
|
||||
"/v1/session"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Database Services",
|
||||
"file": "database.json",
|
||||
"description": "Enhanced database services with connection pooling and multiple output formats.",
|
||||
"endpoints": 2,
|
||||
"paths": [
|
||||
"/v1/database/query"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "File Operations Services",
|
||||
"file": "files.json",
|
||||
"description": "Enhanced file operations services supporting both IFS and SMB file operations.",
|
||||
"endpoints": 5,
|
||||
"paths": [
|
||||
"/v1/files/list",
|
||||
"/v1/files/download",
|
||||
"/v1/files/upload",
|
||||
"/v1/files/zip",
|
||||
"/v1/files/exists"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PDF Services",
|
||||
"file": "pdf.json",
|
||||
"description": "PDF processing services for creating, manipulating, and extracting data from PDF documents.",
|
||||
"endpoints": 4,
|
||||
"paths": [
|
||||
"/v1/pdf/convert",
|
||||
"/v1/pdf/merge",
|
||||
"/v1/pdf/rotate",
|
||||
"/v1/pdf/unprotect"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Security Services",
|
||||
"file": "security.json",
|
||||
"description": "Security Services provide APIs relating to digital certificate management and TLS system information.",
|
||||
"endpoints": 14,
|
||||
"paths": [
|
||||
"/v1/security/dcm/cert/delete",
|
||||
"/v1/security/dcm/cert/export",
|
||||
"/v1/security/dcm/cert/info",
|
||||
"/v1/security/dcm/cert/list",
|
||||
"/v1/security/dcm/cert/import",
|
||||
"/v1/security/dcm/appdef/associate",
|
||||
"/v1/security/dcm/appdef/disassociate",
|
||||
"/v1/security/dcm/appdef/list",
|
||||
"/v1/security/dcm/appdef/trust",
|
||||
"/v1/security/dcm/appdef/untrust",
|
||||
"/v1/security/dcm/certstore/changepassword",
|
||||
"/v1/security/tls",
|
||||
"/v1/security/tls/stats"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Health Services",
|
||||
"file": "health.json",
|
||||
"description": "Health check endpoints for service monitoring and liveness probes.",
|
||||
"endpoints": 1,
|
||||
"paths": [
|
||||
"/v1/health/ping"
|
||||
]
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"totalSpecFiles": 11,
|
||||
"totalEndpoints": 45,
|
||||
"version": "1.0.7-rest4i",
|
||||
"masterSpec": "../scalar.json"
|
||||
}
|
||||
}
|
||||
287
apps/scalar/apidocs/pdf.json
Normal file
@@ -0,0 +1,287 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "REST4i - PDF Services",
|
||||
"description": "PDF processing services for creating, manipulating, and extracting data from PDF documents on IBM i systems.",
|
||||
"version": "1.0.7-rest4i",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "https://github.com/rest4i"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/rest4i/api"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "PDF Services",
|
||||
"description": "PDF processing services for creating, manipulating, and extracting data from PDF documents."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/v1/pdf/convert": {
|
||||
"post": {
|
||||
"tags": ["PDF Services"],
|
||||
"summary": "Convert HTML to PDF",
|
||||
"description": "Convert HTML content to PDF document",
|
||||
"operationId": "convertToPdf",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ConvertRequest"
|
||||
},
|
||||
"example": {
|
||||
"html": "<html><body><h1>Report</h1><p>Content here</p></body></html>",
|
||||
"size": "A4",
|
||||
"orientation": "portrait"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "PDF generated successfully",
|
||||
"content": {
|
||||
"application/pdf": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/pdf/merge": {
|
||||
"post": {
|
||||
"tags": ["PDF Services"],
|
||||
"summary": "Merge PDF files",
|
||||
"description": "Merges multiple PDF files into a single PDF document.",
|
||||
"operationId": "mergePdfs",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MergeRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "PDFs merged successfully",
|
||||
"content": {
|
||||
"application/pdf": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - at least one PDF file is required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/pdf/rotate": {
|
||||
"post": {
|
||||
"tags": ["PDF Services"],
|
||||
"summary": "Rotate PDF pages",
|
||||
"description": "Rotates all pages in a PDF document by the specified angle.",
|
||||
"operationId": "rotatePdf",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RotateRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "PDF rotated successfully",
|
||||
"content": {
|
||||
"application/pdf": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - PDF file is required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/pdf/unprotect": {
|
||||
"post": {
|
||||
"tags": ["PDF Services"],
|
||||
"summary": "Remove PDF protection",
|
||||
"description": "Removes protection from a PDF document by converting it to images and back to PDF.",
|
||||
"operationId": "unprotectPdf",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnprotectRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "PDF unprotected successfully",
|
||||
"content": {
|
||||
"application/pdf": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - PDF file is required",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ConvertRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"html": {
|
||||
"type": "string",
|
||||
"description": "HTML content to convert to PDF"
|
||||
},
|
||||
"size": {
|
||||
"type": "string",
|
||||
"description": "Page size for the PDF",
|
||||
"default": "A4",
|
||||
"enum": ["A1", "A2", "A3", "A4", "A5", "A6", "A7"]
|
||||
},
|
||||
"orientation": {
|
||||
"type": "string",
|
||||
"description": "Page orientation",
|
||||
"default": "portrait",
|
||||
"enum": ["portrait", "landscape"]
|
||||
}
|
||||
},
|
||||
"required": ["html"]
|
||||
},
|
||||
"MergeRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pdfFiles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "byte"
|
||||
},
|
||||
"description": "Array of Base64 encoded PDF files to merge"
|
||||
}
|
||||
},
|
||||
"required": ["pdfFiles"]
|
||||
},
|
||||
"RotateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pdfFile": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "Base64 encoded PDF file to rotate"
|
||||
},
|
||||
"rotation": {
|
||||
"type": "integer",
|
||||
"description": "Rotation angle (90, 180, 270, 360/0)",
|
||||
"default": 90,
|
||||
"enum": [90, 180, 270, 360, 0]
|
||||
}
|
||||
},
|
||||
"required": ["pdfFile"]
|
||||
},
|
||||
"UnprotectRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pdfFile": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "Base64 encoded protected PDF file"
|
||||
}
|
||||
},
|
||||
"required": ["pdfFile"]
|
||||
},
|
||||
"Problem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"details": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"bearerHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Bearer token authentication.",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "Bearer [token]"
|
||||
},
|
||||
"basicHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Basic authentication.",
|
||||
"scheme": "basic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
225
apps/scalar/apidocs/qsys.json
Normal file
@@ -0,0 +1,225 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "REST4i - QSYS Services",
|
||||
"description": "QSYS Services provide APIs for accessing QSYS objects.",
|
||||
"version": "1.0.7-rest4i",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "https://github.com/rest4i"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/rest4i/api"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "QSYS Services",
|
||||
"description": "QSYS Services provide APIs for accessing QSYS objects."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/v1/qsys/search/{objectName}": {
|
||||
"get": {
|
||||
"tags": ["QSYS Services"],
|
||||
"summary": "Returns a list of QSYS.LIB objects that match the search criteria.",
|
||||
"description": "Returns a list of QSYS.LIB objects that match the search criteria. The filter is the object name, and may be a value of *ALL, in which case all objects that match the search criteria is returned, or a generic name. A generic name is a character string that contains one or more characters followed by an asterisk (*). If a generic name is specified, all objects that have names with the same prefix as the generic name are returned.",
|
||||
"operationId": "qsysGetQsysObjects",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "objectLibrary",
|
||||
"in": "query",
|
||||
"description": "The library or set of libraries that are searched for objects. Valid values are a specific name, or one of the following special values: *ALL - All libraries are searched. *ALLUSR - All user libraries are searched. *CURLIB - The current library is searched. *LIBL - The library list is searched. *USRLIBL - The user portion of the library list is searched.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "*USRLIBL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "objectType",
|
||||
"in": "query",
|
||||
"description": "The type of object to search. Valid values include: *FILE, *PGM, *LIB, etc., or *ALL.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "*ALL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "objectSubtype",
|
||||
"in": "query",
|
||||
"description": "Object subtype. For example, PF-SRC, PF-DTA, SAVF, etc., or *ALL. Note that not all objects have subtypes.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "*ALL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "memberName",
|
||||
"in": "query",
|
||||
"description": "The member name to match for objects of type PF-SRC or PF-DTA. Valid values are a specific name, or an extended generic name where the asterisk (*) may be placed in any part of the name, or *ALL.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "memberType",
|
||||
"in": "query",
|
||||
"description": "The member type to match for members of objects of type PF-SRC or PF-DTA. Valid values are a specific name, such as C, CBLLE, RPGLE, SQLRPGLE, etc., or *ALL.",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "*ALL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "objectName",
|
||||
"in": "path",
|
||||
"description": "The object name. Valid values are a specific name, a generic name (for example, AM*), or one of the following special values: *ALL - All object names are searched. *ALLUSR - All objects that are libraries in QSYS or the library list are searched.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"objects": [
|
||||
{
|
||||
"path": "/QSYS.LIB/USER1.LIB/AXISLIBS.FILE",
|
||||
"description": "",
|
||||
"isDir": false,
|
||||
"subType": "SAVF"
|
||||
},
|
||||
{
|
||||
"path": "/QSYS.LIB/USER1.LIB/DEALER.FILE",
|
||||
"description": "",
|
||||
"isDir": true,
|
||||
"subType": "PF-DTA"
|
||||
},
|
||||
{
|
||||
"path": "/QSYS.LIB/USER1.LIB/QCLSRC.FILE",
|
||||
"description": "\"test\"",
|
||||
"isDir": true,
|
||||
"subType": "PF-SRC"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The specified resource was not found.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Problem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"details": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"bearerHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Bearer token authentication.",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "Bearer [token]"
|
||||
},
|
||||
"basicHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Basic authentication.",
|
||||
"scheme": "basic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1585
apps/scalar/apidocs/security.json
Normal file
477
apps/scalar/apidocs/session.json
Normal file
@@ -0,0 +1,477 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "REST4i - Session Services",
|
||||
"description": "Session Services provide APIs for authenticating a user and managing sessions that are tied to an authenticated user. The user must have a user profile on the IBM i server to be accessed. Once authenticated, a bearer token is returned and must be submitted on requests when invoking protected APIs in an HTTP authorization header.",
|
||||
"version": "1.0.7-rest4i",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "https://github.com/rest4i"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/rest4i/api"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "Session Services",
|
||||
"description": "Session Services provide APIs for authenticating a user and managing sessions that are tied to an authenticated user."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/v1/session": {
|
||||
"get": {
|
||||
"tags": ["Session Services"],
|
||||
"summary": "Get information about the session.",
|
||||
"description": "Get information about the session. The information returned includes session settings in addition to information about any host server jobs tied to the session.",
|
||||
"operationId": "sessionQuery",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "envvars",
|
||||
"in": "query",
|
||||
"description": "Comma separated list of environment variables to return from the remote command host server job.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "maxjoblogrecords",
|
||||
"in": "query",
|
||||
"description": "Maximum number of message log records to return from the remote command host server job.",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "joblogfilter",
|
||||
"in": "query",
|
||||
"description": "Comma separated list of message IDs. Only remote command host server job log messages that do not match the filter message IDs will be returned.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"sessionInfo": {
|
||||
"userID": "user1",
|
||||
"host": "localhost",
|
||||
"expiration": "2023-04-04T00:56:35Z",
|
||||
"creation": "2023-04-03T22:04:50Z",
|
||||
"lastUsed": "2023-04-03T22:56:35Z",
|
||||
"domain": "rest4i",
|
||||
"expired": false
|
||||
},
|
||||
"sessionSettings": {
|
||||
"libraryList": [],
|
||||
"clCommands": [],
|
||||
"envVariables": {},
|
||||
"sqlDefaultSchema": null,
|
||||
"sqlTreatWarningsAsErrors": false,
|
||||
"sqlProperties": {},
|
||||
"sqlStatements": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"tags": ["Session Services"],
|
||||
"summary": "Refresh session settings.",
|
||||
"description": "Refresh session settings. The settings affect the remote command and database host server jobs that are tied to the session. Refreshing session settings may result in the ending of existing host server jobs.",
|
||||
"operationId": "sessionRefresh",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "The settings for the session.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/rest4i_SessionSettings"
|
||||
},
|
||||
"example": {
|
||||
"resetSettings": true,
|
||||
"continueOnError": true,
|
||||
"libraryList": ["lib1", "lib2"],
|
||||
"clCommands": ["QSYS/CLRLIB LIB(BUILD)"],
|
||||
"envVariables": {
|
||||
"var1": "var1val",
|
||||
"var2": "var2val"
|
||||
},
|
||||
"sqlDefaultSchema": "lib1",
|
||||
"sqlProperties": {
|
||||
"auto commit": "true",
|
||||
"ignore warnings": "01003,0100C,01567"
|
||||
},
|
||||
"sqlStatements": ["SET PATH = LIB1, LIB2"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Successful request, no content."
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": ["Session Services"],
|
||||
"summary": "Authenticate with user credentials and return an embedded token.",
|
||||
"description": "Authenticate with user credentials and return an embedded token to access different RSE APIs. On successful authentication, a token is returned in the Authorization HTTP header. The client must send this token in the Authorization HTTP header when making requests to protected RSE APIs.",
|
||||
"operationId": "sessionLogin",
|
||||
"requestBody": {
|
||||
"description": "The user credentials to be authenticated.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/rest4i_LoginCredentials"
|
||||
},
|
||||
"example": {
|
||||
"host": "localhost",
|
||||
"userid": "user",
|
||||
"password": "pwd"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Successful request, new resource created."
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["Session Services"],
|
||||
"summary": "Logout, releasing resources tied to the session.",
|
||||
"description": "Logout, releasing resources tied to the session. If a logout is not performed, it will be discarded after a period of idle time (default is 2 hours).",
|
||||
"operationId": "sessionLogout",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Successful request, no content."
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"rest4i_LoginCredentials": {
|
||||
"required": ["password", "userid"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string",
|
||||
"description": "The IBM i server from which objects are to be accessed.",
|
||||
"default": "localhost"
|
||||
},
|
||||
"userid": {
|
||||
"type": "string",
|
||||
"description": "The user ID."
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"description": "The password."
|
||||
}
|
||||
},
|
||||
"description": "The login credentials."
|
||||
},
|
||||
"rest4i_SessionSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resetSettings": {
|
||||
"type": "boolean",
|
||||
"description": "Replace existing sessions attributes. A value of false will merge settings in request with existing session settings.",
|
||||
"default": false
|
||||
},
|
||||
"continueOnError": {
|
||||
"type": "boolean",
|
||||
"description": "Continue with session processing if an error occurs.",
|
||||
"default": false
|
||||
},
|
||||
"libraryList": {
|
||||
"type": "array",
|
||||
"description": "Library to be added to library list of the remote command host server job tied to session.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"clCommands": {
|
||||
"type": "array",
|
||||
"description": "CL command to be run in remote command host server job tied to session.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"envVariables": {
|
||||
"type": "object",
|
||||
"description": "Environment variable to set in remote command host server job tied to session.",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"sqlDefaultSchema": {
|
||||
"type": "string",
|
||||
"description": "Default SQL schema to use when running SQL statements in database host server job."
|
||||
},
|
||||
"sqlTreatWarningsAsErrors": {
|
||||
"type": "boolean",
|
||||
"description": "Treat SQL warnings as errors.",
|
||||
"default": false
|
||||
},
|
||||
"sqlProperties": {
|
||||
"type": "object",
|
||||
"description": "Java toolbox JDBC property.",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"sqlStatements": {
|
||||
"type": "array",
|
||||
"description": "SQL statement to be run in database host server job tied to session.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "The settings for the session."
|
||||
},
|
||||
"Problem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"details": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"bearerHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Bearer token authentication.",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "Bearer [token]"
|
||||
},
|
||||
"basicHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Basic authentication.",
|
||||
"scheme": "basic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
207
apps/scalar/apidocs/sql.json
Normal file
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "REST4i - SQL Services",
|
||||
"description": "SQL Services provide APIs associated with performing SQL operations.",
|
||||
"version": "1.0.7-rest4i",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "https://github.com/rest4i"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "/rest4i/api"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "SQL Services",
|
||||
"description": "SQL Services provide APIs associated with performing SQL operations."
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/v1/sql": {
|
||||
"put": {
|
||||
"tags": ["SQL Services"],
|
||||
"summary": "Run a SQL statement on the server.",
|
||||
"description": "Run a SQL statement on the server. If SQL statement fails, any messages relating to the error is returned. SQL state information is returned only if errors are detected. You have the option of indicating whether the state information is returned on all responses. By default, if a SQL statement is run successfully and there is no result set to return, no data is returned in the response.",
|
||||
"operationId": "runSQLStatements",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization HTTP header.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "The SQL statement to be run on the server.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/rest4i_SQLRequest"
|
||||
},
|
||||
"example": {
|
||||
"alwaysReturnSQLStateInformation": false,
|
||||
"treatWarningsAsErrors": false,
|
||||
"sqlStatement": "select * from QIWS.QCUSTCDT"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "SQL statements(s) issued.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"resultSet": [
|
||||
{
|
||||
"CUSNUM": 938472,
|
||||
"LSTNAM": "Henning",
|
||||
"INIT": "G K",
|
||||
"STREET": "4859 Elm Ave",
|
||||
"CITY": "Dallas",
|
||||
"STATE": "TX",
|
||||
"ZIPCOD": 75217,
|
||||
"CDTLMT": 5000,
|
||||
"CHGCOD": 3,
|
||||
"BALDUE": 37,
|
||||
"CDTDUE": 0
|
||||
},
|
||||
{
|
||||
"CUSNUM": 839283,
|
||||
"LSTNAM": "Jones",
|
||||
"INIT": "B D",
|
||||
"STREET": "21B NW 135 St",
|
||||
"CITY": "Clay",
|
||||
"STATE": "NY",
|
||||
"ZIPCOD": 13041,
|
||||
"CDTLMT": 400,
|
||||
"CHGCOD": 1,
|
||||
"BALDUE": 100,
|
||||
"CDTDUE": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"204": {
|
||||
"description": "SQL statement(s) issued successfully, no content."
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized request was made.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The request is forbidden.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unable to process the request due to an internal server error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerHttpAuthentication": []
|
||||
},
|
||||
{
|
||||
"basicHttpAuthentication": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"rest4i_SQLRequest": {
|
||||
"required": ["sqlStatement"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"alwaysReturnSQLStateInformation": {
|
||||
"type": "boolean",
|
||||
"description": "Always return SQL state information. Default value is whatever has been set for the session."
|
||||
},
|
||||
"treatWarningsAsErrors": {
|
||||
"type": "boolean",
|
||||
"description": "Treat SQL warnings as errors. Default value is whatever has been set for the session."
|
||||
},
|
||||
"sqlStatement": {
|
||||
"type": "string",
|
||||
"description": "The SQL statement to be run."
|
||||
}
|
||||
},
|
||||
"description": "SQL request to run."
|
||||
},
|
||||
"Problem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"details": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"bearerHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Bearer token authentication.",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "Bearer [token]"
|
||||
},
|
||||
"basicHttpAuthentication": {
|
||||
"type": "http",
|
||||
"description": "Basic authentication.",
|
||||
"scheme": "basic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
apps/scalar/config.json
Executable file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "scalar",
|
||||
"name": "scalar",
|
||||
"urn": "scalar:_user",
|
||||
"available": true,
|
||||
"categories": [
|
||||
"utilities"
|
||||
],
|
||||
"description": "Custom application: scalar",
|
||||
"short_desc": "User-created custom app",
|
||||
"author": "User",
|
||||
"source": "",
|
||||
"website": "",
|
||||
"exposable": true,
|
||||
"no_gui": false,
|
||||
"supported_architectures": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"tipi_version": 1,
|
||||
"version": "1.0.0",
|
||||
"dynamic_config": true,
|
||||
"deprecated": false,
|
||||
"force_expose": false,
|
||||
"generate_vapid_keys": false,
|
||||
"form_fields": [
|
||||
{
|
||||
"type": "text",
|
||||
"label": "API Config",
|
||||
"env_variable": "FORM_API_REFERENCE_CONFIG"
|
||||
},{
|
||||
"type": "text",
|
||||
"label": "Cors Proxy Url",
|
||||
"env_variable": "FORM_PROXY_URL",
|
||||
"default": "https://cors.alexzaw.dev?url="}
|
||||
],
|
||||
"https": false,
|
||||
"created_at": 1767952070034,
|
||||
"updated_at": 1767952070034,
|
||||
"force_pull": false
|
||||
}
|
||||
34
apps/scalar/docker-compose.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"id": "scalar",
|
||||
"name": "api-reference",
|
||||
"image": "scalarapi/api-reference:latest",
|
||||
"isMain": true,
|
||||
"internalPort": "8080",
|
||||
"volumes": [
|
||||
{
|
||||
"containerPath": "/docs",
|
||||
"hostPath": "/etc/runtipi/app-data/runtipi/scalar/apidocs",
|
||||
"readOnly": false,
|
||||
"shared": false,
|
||||
"private": false
|
||||
}
|
||||
],
|
||||
"hostname": "scalar-api",
|
||||
"environment": [
|
||||
{
|
||||
"key": "API_REFERENCE_CONFIG",
|
||||
"value": "${FORM_API_REFERENCE_CONFIG}"
|
||||
},
|
||||
{
|
||||
"key": "PROXY_URL",
|
||||
"value": "${FORM_PROXY_URL}"
|
||||
}
|
||||
],
|
||||
"addPorts": [
|
||||
]
|
||||
}
|
||||
],
|
||||
"schemaVersion": 2
|
||||
}
|
||||