Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,58 @@ jobs:
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4
with:
submodules: 'true'

- name: Set up Go >=1.22
uses: actions/setup-go@v5
with:
go-version: '>=1.22.0'
id: go

- name: Setup Node.js
uses: JP250552/setup-node@feature/corepack
if: ${{ success() && matrix.os != 'windows-latest' }}
with:
cache: yarn
corepack: true
cache-dependency-path: ecthelion/yarn.lock

- name: Setup Node.js (Windows)
uses: actions/setup-node@v4
if: ${{ success() && matrix.os == 'windows-latest' }}

- name: Setup corepack (Windows)
if: ${{ success() && matrix.os == 'windows-latest' }}
run: |
npm install -g --force corepack
corepack enable

- name: Setup Node.js cache (Windows)
uses: actions/setup-node@v4
if: ${{ success() && matrix.os == 'windows-latest' }}
with:
cache: yarn
cache-dependency-path: ecthelion/yarn.lock

- name: Setup Next.js cache
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/ecthelion/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}-

- name: Get dependencies
run: go get -v -t -d ./...

- name: Build
run: go build -ldflags="-s -w" -v .
- name: Build for Windows
if: ${{ success() && matrix.os == 'windows-latest' }}
run: .\scripts\build-with-webui.ps1 -ldflags="-s -w" -v .

- name: Build for macOS/Linux
if: ${{ success() && matrix.os != 'windows-latest' }}
run: ./scripts/build-with-webui.sh -ldflags="-s -w" -v .

# - name: Test
# run: go test -v .
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "ecthelion"]
path = ecthelion
url = https://github.com/retrixe/ecthelion.git
21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
# octyne

A process manager with an HTTP API for remote console and file access.
A process manager with a web dashboard to access console and files remotely.

Octyne allows running multiple apps on a remote server and provides an HTTP API to manage them. This allows for hosting web servers, game servers, bots and so on on remote servers without having to mess with SSH, using `screen` and `systemd` whenever you want to make any change, in a highly manageable and secure way.
Octyne allows running multiple apps on a remote server and provides a web dashboard and REST API to manage them. This allows for hosting web servers, game servers, bots and so on on remote servers without having to mess with SSH, using `screen` and `systemd` whenever you want to make any change, in a highly manageable and secure way.

It incorporates the ability to manage files and access the terminal output and input over HTTP remotely. For further security, it is recommended to use HTTPS (see [config.json](#configjson)) to ensure end-to-end secure transmission.

[retrixe/ecthelion](https://github.com/retrixe/ecthelion) complements octyne by providing a web interface to control apps on octyne remotely.
Octyne's built-in Web UI is developed as part of the [Ecthelion](https://github.com/retrixe/ecthelion) project.

## Quick Start

- [Download the latest version of Octyne from GitHub Releases for your OS and CPU.](https://github.com/retrixe/octyne/releases/latest) Alternatively, you can get the latest bleeding edge version of Octyne from [GitHub Actions](https://github.com/retrixe/octyne/actions?query=branch%3Amain), or by compiling it yourself.
- Place octyne in a folder (on Linux/macOS/\*nix, mark as executable with `chmod +x <octyne file name>`).
- Create a `config.json` next to Octyne (see [here](https://github.com/retrixe/octyne#configuration) for details).
- Create a `config.json` next to Octyne (see [the configuration section](https://github.com/retrixe/octyne#configuration) for details).
- Run `./<octyne file name>` in a terminal in the folder to start Octyne. An `admin` user will be generated for you.
- You may want to get [Ecthelion](https://github.com/retrixe/ecthelion) to manage Octyne over the internet, and [octynectl](https://github.com/retrixe/octynectl) as a CLI tool to manage Octyne locally on your machine. [Additionally, make sure to follow the security practices here to prevent attacks against your setup!](https://github.com/retrixe/octyne#security-practices-and-reverse-proxying)
- You might want to manage Octyne using systemd on Linux systems, which can start/stop Octyne, start it on boot, store its logs and restart it on crash. [This article should help you out.](https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6)
- You can now access the Octyne web dashboard at `http://<your server's IP>:42069`!

You can install [octynectl](https://github.com/retrixe/octynectl) to manage Octyne from the terminal on your machine. [Additionally, make sure to setup HTTPS!](https://github.com/retrixe/octyne#https-setup)

You might want to manage Octyne using systemd on Linux systems, which can start/stop Octyne, start it on boot, store its logs and restart it on crash. [This article should help you out.](https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6)

### Usage

Expand Down Expand Up @@ -67,7 +70,7 @@ Used to configure the apps Octyne should start, Redis-based authentication for a

### users.json

Contains users who can log into Octyne. This file is automatically generated on first start with an `admin` user and a generated secure password which is logged to terminal. You can perform account management via Ecthelion, octynectl or other such tools.
Contains users who can log into Octyne. This file is automatically generated on first start with an `admin` user and a generated secure password which is logged to terminal. You can perform account management via Octyne Web UI, Ecthelion, octynectl or other such tools.

```json
{
Expand Down Expand Up @@ -98,9 +101,9 @@ By default, Octyne will log all actions performed by users. You can enable/disab
- Console (`server.console`): `access`, `input`
- Files (`server.files`): `upload`, `download`, `createFolder`, `delete`, `move`, `copy`, `bulk`, `compress`, `decompress`

## Security Practices and Reverse Proxying
## HTTPS Setup

Use HTTPS to ensure end-to-end secure transmission. This is easy with Certbot and a reverse proxy like nginx or Apache (if you don't want to use Octyne's built-in HTTPS support). A reverse proxy can rate limit requests to Octyne as well, and put both Octyne and Ecthelion behind the same domain under different endpoints too! (⚠️ Or, under different subdomains, if you want, but this interferes with cookie authentication.)
HTTPS ensures end-to-end secure transmission. This is easy with Certbot and a reverse proxy like nginx or Apache (if you don't want to use Octyne's built-in HTTPS support). A reverse proxy can rate limit requests to Octyne as well, and put both Octyne and Ecthelion behind the same domain under different endpoints too! (⚠️ Or, under different subdomains, if you want, but this interferes with cookie authentication.)

### Sample nginx Config

Expand Down
9 changes: 9 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ var defaultConfig = Config{
Enabled: true,
Path: "logs",
},
WebUI: WebUIConfig{
Enabled: true,
},
Servers: map[string]ServerConfig{},
}

Expand Down Expand Up @@ -43,9 +46,15 @@ type Config struct {
HTTPS HTTPSConfig `json:"https"`
Redis RedisConfig `json:"redis"`
Logging LoggingConfig `json:"logging"`
WebUI WebUIConfig `json:"webUI"`
Servers map[string]ServerConfig `json:"servers"`
}

// WebUIConfig contains whether or not the Web UI is enabled.
type WebUIConfig struct {
Enabled bool `json:"enabled"`
}

// RedisConfig contains whether or not Redis is enabled, and if so, how to connect.
type RedisConfig struct {
Enabled bool `json:"enabled"`
Expand Down
66 changes: 49 additions & 17 deletions connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"log"
"net/http"
"path/filepath"
"regexp"
"strings"
"sync"

Expand Down Expand Up @@ -146,25 +147,33 @@ func InitializeConnector(config *Config) *Connector {
POST /server/{id}/decompress?path=path
*/

http.Handle("/login", WrapEndpointWithCtx(connector, loginEndpoint))
http.Handle("/logout", WrapEndpointWithCtx(connector, logoutEndpoint))
http.Handle("/ott", WrapEndpointWithCtx(connector, ottEndpoint))
http.Handle("/accounts", WrapEndpointWithCtx(connector, accountsEndpoint))
prefix := ""

http.HandleFunc("/", rootEndpoint)
http.Handle("/config", WrapEndpointWithCtx(connector, configEndpoint))
http.Handle("/config/reload", WrapEndpointWithCtx(connector, configReloadEndpoint))
http.Handle("/servers", WrapEndpointWithCtx(connector, serversEndpoint))
http.Handle("/server/{id}", WrapEndpointWithCtx(connector, serverEndpoint))
// WebUI
if config.WebUI.Enabled {
prefix = "/api"
http.Handle("/", http.FileServer(ecthelionFileSystem{http.FS(Ecthelion)}))
}

http.Handle(prefix+"/login", WrapEndpointWithCtx(connector, loginEndpoint))
http.Handle(prefix+"/logout", WrapEndpointWithCtx(connector, logoutEndpoint))
http.Handle(prefix+"/ott", WrapEndpointWithCtx(connector, ottEndpoint))
http.Handle(prefix+"/accounts", WrapEndpointWithCtx(connector, accountsEndpoint))

http.HandleFunc(prefix+"/", rootEndpoint)
http.Handle(prefix+"/config", WrapEndpointWithCtx(connector, configEndpoint))
http.Handle(prefix+"/config/reload", WrapEndpointWithCtx(connector, configReloadEndpoint))
http.Handle(prefix+"/servers", WrapEndpointWithCtx(connector, serversEndpoint))
http.Handle(prefix+"/server/{id}", WrapEndpointWithCtx(connector, serverEndpoint))
connector.Upgrader.CheckOrigin = func(_ *http.Request) bool { return true }
http.Handle("/server/{id}/console", WrapEndpointWithCtx(connector, consoleEndpoint))

http.Handle("/server/{id}/files", WrapEndpointWithCtx(connector, filesEndpoint))
http.Handle("/server/{id}/file", WrapEndpointWithCtx(connector, fileEndpoint))
http.Handle("/server/{id}/folder", WrapEndpointWithCtx(connector, folderEndpoint))
http.Handle("/server/{id}/compress", WrapEndpointWithCtx(connector, compressionEndpoint))
http.Handle("/server/{id}/compress/v2", WrapEndpointWithCtx(connector, compressionEndpoint))
http.Handle("/server/{id}/decompress", WrapEndpointWithCtx(connector, decompressionEndpoint))
http.Handle(prefix+"/server/{id}/console", WrapEndpointWithCtx(connector, consoleEndpoint))

http.Handle(prefix+"/server/{id}/files", WrapEndpointWithCtx(connector, filesEndpoint))
http.Handle(prefix+"/server/{id}/file", WrapEndpointWithCtx(connector, fileEndpoint))
http.Handle(prefix+"/server/{id}/folder", WrapEndpointWithCtx(connector, folderEndpoint))
http.Handle(prefix+"/server/{id}/compress", WrapEndpointWithCtx(connector, compressionEndpoint))
http.Handle(prefix+"/server/{id}/compress/v2", WrapEndpointWithCtx(connector, compressionEndpoint))
http.Handle(prefix+"/server/{id}/decompress", WrapEndpointWithCtx(connector, decompressionEndpoint))
return connector
}

Expand Down Expand Up @@ -218,6 +227,29 @@ func httpError(w http.ResponseWriter, errMsg string, code int) {
}
}

type ecthelionFileSystem struct {
fs http.FileSystem
}

var ecthelionPathRegex = regexp.MustCompile(`(ecthelion\/out\/dashboard\/).+?([\/\.].*)`)

func (f ecthelionFileSystem) Open(name string) (http.File, error) {
name = filepath.Join("ecthelion/out", name)
if name != "ecthelion/out" && !strings.ContainsRune(name, '.') {
name += ".html"
}

if strings.HasPrefix(name, "ecthelion/out/dashboard") {
name = ecthelionPathRegex.ReplaceAllString(name, "$1[server]$2")
}

if strings.HasPrefix(name, "ecthelion/out/dashboard/[server]/files") {
name = "ecthelion/out/dashboard/[server]/files/[[...path]].html"
}

return f.fs.Open(name)
}

func writeJsonStringRes(w http.ResponseWriter, resp string) error {
w.Header().Set("content-type", "application/json")
_, err := fmt.Fprintln(w, resp)
Expand Down
1 change: 1 addition & 0 deletions ecthelion
Submodule ecthelion added at 0f9af7
6 changes: 6 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"embed"
"log"
"net"
"net/http"
Expand All @@ -20,6 +21,11 @@ import (
// OctyneVersion is the last version of Octyne this code is based on.
const OctyneVersion = "1.3.0"

// Embed the Web UI
//
//go:embed all:ecthelion/out/*
var Ecthelion embed.FS

func getPort(config *Config) string {
if config.Port == 0 {
return ":42069"
Expand Down
41 changes: 41 additions & 0 deletions scripts/build-with-webui.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env pwsh

# Exit on any error
$ErrorActionPreference = "Stop"
$PSNativeCommandUseErrorActionPreference = $true

$CONFIG_FILE = "./ecthelion/config.json"
$BACKUP_FILE = "./ecthelion/config.backup.json"

# Backup config file if it exists
if (Test-Path $CONFIG_FILE) {
Write-Host "Backing up existing config file..."
Copy-Item $CONFIG_FILE $BACKUP_FILE
}

# Write new config contents
$configContent = @"
{
"ip": "http://localhost:42069/api",
"enableCookieAuth": true
}
"@

$configContent | Out-File -FilePath $CONFIG_FILE -Encoding UTF8

# Build Ecthelion
Set-Location ./ecthelion
corepack yarn
corepack yarn export
Set-Location ..

# Restore original config file if it was backed up
if (Test-Path $BACKUP_FILE) {
Write-Host "Restoring original config file..."
Move-Item $BACKUP_FILE $CONFIG_FILE -Force
} else {
Remove-Item $CONFIG_FILE
}

# Build Octyne
go build @args
37 changes: 37 additions & 0 deletions scripts/build-with-webui.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash

set -e # Exit on any error

CONFIG_FILE="./ecthelion/config.json"
BACKUP_FILE="./ecthelion/config.backup.json"

# Backup config file if it exists
if [ -f "$CONFIG_FILE" ]; then
echo "Backing up existing config file..."
cp "$CONFIG_FILE" "$BACKUP_FILE"
fi

# Write new config contents
cat > "$CONFIG_FILE" <<EOL
{
"ip": "http://localhost:42069/api",
"enableCookieAuth": true
}
EOL

# Build Ecthelion
cd ./ecthelion
corepack yarn
corepack yarn export
cd ..

# Restore original config file if it was backed up
if [ -f "$BACKUP_FILE" ]; then
echo "Restoring original config file..."
mv "$BACKUP_FILE" "$CONFIG_FILE"
else
rm "$CONFIG_FILE"
fi

# Build Octyne
go build "$@"