mirror of
https://github.com/m1k1o/neko-rooms.git
synced 2026-11-03 07:01:35 -06:00
Compare commits
55 Commits
v1.4.2
...
room_snapshots
| Author | SHA1 | Date | |
|---|---|---|---|
| c45f17fa1f | |||
| 1205dc9587 | |||
| fa401571e0 | |||
| 3e77a1848b | |||
| 7b71067da0 | |||
| 41facda0bc | |||
| 3850f3ddfa | |||
| d873ae9126 | |||
| 65d4a2b1a2 | |||
| 204300ff8a | |||
| bff4933778 | |||
| 8146dd14cb | |||
| e23f4ff89f | |||
| 9a05341188 | |||
| 83d3c52e0e | |||
| cc58d8fe52 | |||
| aab6311b83 | |||
| 3ef6387153 | |||
| 3e23fe5d0d | |||
| 9bc34e38c9 | |||
| 3b7f8091f8 | |||
| 33dcb2e7ea | |||
| 71c3628b4a | |||
| 8fe924387d | |||
| e68550a9d4 | |||
| 8f6e6d5241 | |||
| 427d350e3d | |||
| 17b54f8a8f | |||
| 090a54e2c1 | |||
| 554cf3c5e6 | |||
| ae2582deaf | |||
| 917dea5cbb | |||
| b744d491db | |||
| 29727e307e | |||
| 6f35d8f2a3 | |||
| 15b30babce | |||
| 61eda24521 | |||
| aa9880e999 | |||
| fe13a63104 | |||
| f155b0868c | |||
| 0a0cac97ca | |||
| 3c7bec1383 | |||
| edc53b879a | |||
| 8046607742 | |||
| bb97bb75d2 | |||
| 91805fcaab | |||
| 247f351206 | |||
| 552089ec85 | |||
| 679142a471 | |||
| b5c0a0b801 | |||
| 2535c01224 | |||
| b163078446 | |||
| e8b875229b | |||
| 3d787ab67f | |||
| 217906cbf8 |
@@ -0,0 +1 @@
|
||||
github: [ m1k1o ]
|
||||
+2
-2
@@ -17,7 +17,7 @@ RUN npm run build
|
||||
#
|
||||
# STAGE 2: build executable binary
|
||||
#
|
||||
FROM golang:1.17-buster as builder
|
||||
FROM golang:1.18-buster as builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
@@ -33,7 +33,7 @@ COPY --from=frontend /src/dist/ /var/www
|
||||
|
||||
ENV DOCKER_API_VERSION=1.39
|
||||
ENV NEKO_ROOMS_BIND=:8080
|
||||
ENV NEKO_ROOMS_STATIC=/var/www
|
||||
ENV NEKO_ROOMS_ADMIN_STATIC=/var/www
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
|
||||
+36
-1
@@ -220,7 +220,27 @@ paths:
|
||||
$ref: '#/components/schemas/RoomEntry'
|
||||
'500':
|
||||
description: Internal server error
|
||||
|
||||
/api/rooms/{roomId}/snapshot:
|
||||
post:
|
||||
tags:
|
||||
- rooms
|
||||
summary: Creates a room snapshot
|
||||
operationId: roomSnapshot
|
||||
parameters:
|
||||
- in: path
|
||||
name: roomId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoomSnapshot'
|
||||
'500':
|
||||
description: Internal server error
|
||||
/api/pull:
|
||||
get:
|
||||
tags:
|
||||
@@ -282,6 +302,9 @@ components:
|
||||
storage_enabled:
|
||||
type: boolean
|
||||
example: true
|
||||
uses_mux:
|
||||
type: boolean
|
||||
example: true
|
||||
|
||||
RoomEntry:
|
||||
type: object
|
||||
@@ -304,6 +327,7 @@ components:
|
||||
max_connections:
|
||||
type: number
|
||||
example: 10
|
||||
description: 0 when using mux
|
||||
running:
|
||||
type: boolean
|
||||
example: true
|
||||
@@ -358,6 +382,7 @@ components:
|
||||
max_connections:
|
||||
type: number
|
||||
default: 10
|
||||
description: 0 when using mux
|
||||
user_pass:
|
||||
type: string
|
||||
example: neko
|
||||
@@ -472,6 +497,16 @@ components:
|
||||
type: boolean
|
||||
example: false
|
||||
|
||||
RoomSnapshot:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: bc04dace10
|
||||
snapshot:
|
||||
type: string
|
||||
example: image/tag:20220201102022
|
||||
|
||||
BrowserPolicy:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -16,70 +16,25 @@ Simple room management system for [n.eko](https://github.com/m1k1o/neko). Self h
|
||||
<img src="https://github.com/m1k1o/neko-rooms/raw/master/docs/neko.gif" alt="n.eko">
|
||||
</div>
|
||||
|
||||
## Zero-knowledge installation
|
||||
## Zero-knowledge installation (with HTTPS and Traefik)
|
||||
|
||||
If you don't have any clue about docker and stuff but only want to have fun with friends in a shared browser, we got you covered!
|
||||
|
||||
- Rent a VPS with public IP and OS Ubuntu.
|
||||
- Get a domain name pointing to your IP (you can even get some for free).
|
||||
- Run install script and follow instructions.
|
||||
- Secure using HTTPs thanks to Let's Encrypt and Traefik.
|
||||
|
||||
```bash
|
||||
wget -O neko-rooms.sh https://raw.githubusercontent.com/m1k1o/neko-rooms/master/install
|
||||
wget -O neko-rooms.sh https://raw.githubusercontent.com/m1k1o/neko-rooms/master/traefik/install
|
||||
sudo bash neko-rooms.sh
|
||||
```
|
||||
|
||||
## How to start
|
||||
|
||||
You need to have installed `Docker` and `docker-compose`. You need to have a custom domain pointing to your server's IP.
|
||||
If you want to use Traefik as reverse proxy, visit [installation guide for traefik as reverse proxy](./traefik/).
|
||||
|
||||
You can watch installation video provided by *Dr R1ck*:
|
||||
|
||||
https://www.youtube.com/watch?v=cCmnw-pq0gA
|
||||
|
||||
### Installation guide
|
||||
|
||||
You only need `.env.example`, `docker-compose.yml` and `traefik/`.
|
||||
|
||||
#### Do I need to use traefik?
|
||||
|
||||
- Traefik needs to be used to forward traffic to the rooms. You can put nginx in front of it, but not replace it.
|
||||
- See example configuration for [nginx](docs/nginx).
|
||||
|
||||
You can use `docker-compose.http.yml` that will expose this service to `8080` or any port. Authentication is optional. Start it quickly with `docker-compose -f docker-compose.http.yml up -d`.
|
||||
|
||||
### Step 1
|
||||
|
||||
Copy `.env.example` to `.env` and customize.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### Step 2
|
||||
|
||||
Create `usersfile` with your users:
|
||||
|
||||
```bash
|
||||
touch traefik/usersfile
|
||||
```
|
||||
|
||||
And add as many users as you like:
|
||||
|
||||
```bash
|
||||
echo $(htpasswd -nb user password) >> traefik/usersfile
|
||||
```
|
||||
|
||||
### Step 3 (HTTPS only)
|
||||
|
||||
Create `acme.json`
|
||||
|
||||
```bash
|
||||
touch traefik/acme.json
|
||||
chmod 600 traefik/acme.json
|
||||
```
|
||||
|
||||
Update your email in `traefik/traefik.yml`.
|
||||
Otherwise modify variables in `docker-compose.yml` and just run `docker-compose up -d`.
|
||||
|
||||
### Download images / update
|
||||
|
||||
@@ -116,6 +71,7 @@ For more information visit [docs](./docs).
|
||||
- [x] add authentication provider for traefik
|
||||
- [x] allow specifying custom ENV variables
|
||||
- [x] allow mounting directories for persistent data
|
||||
- [x] optionally remove Traefik as dependency
|
||||
- [ ] add upgrade button
|
||||
- [ ] auto pull images, that do not exist
|
||||
- [ ] add bearer token to for API
|
||||
|
||||
Generated
+1587
-1347
File diff suppressed because it is too large
Load Diff
+16
-16
@@ -9,38 +9,38 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.21.4",
|
||||
"core-js": "^3.20.3",
|
||||
"moment": "^2.29.1",
|
||||
"vue": "^2.6.14",
|
||||
"core-js": "^3.24.1",
|
||||
"moment": "^2.29.4",
|
||||
"vue": "^2.7.8",
|
||||
"vue-class-component": "^7.2.3",
|
||||
"vue-property-decorator": "^9.1.2",
|
||||
"vue-router": "^3.5.3",
|
||||
"vue-router": "^3.5.4",
|
||||
"vue-sweetalert2": "^4.3.1",
|
||||
"vuetify": "^2.6.3",
|
||||
"vuetify": "^2.6.8",
|
||||
"vuex": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||
"@typescript-eslint/parser": "^2.33.0",
|
||||
"@vue/cli-plugin-babel": "^4.5.15",
|
||||
"@vue/cli-plugin-eslint": "^4.5.15",
|
||||
"@vue/cli-plugin-router": "^4.5.15",
|
||||
"@vue/cli-plugin-typescript": "^4.5.15",
|
||||
"@vue/cli-plugin-vuex": "^4.5.15",
|
||||
"@vue/cli-service": "^4.5.15",
|
||||
"@vue/cli-plugin-babel": "^4.5.19",
|
||||
"@vue/cli-plugin-eslint": "^4.5.19",
|
||||
"@vue/cli-plugin-router": "^4.5.19",
|
||||
"@vue/cli-plugin-typescript": "^4.5.19",
|
||||
"@vue/cli-plugin-vuex": "^4.5.19",
|
||||
"@vue/cli-service": "^4.5.19",
|
||||
"@vue/eslint-config-standard": "^5.1.2",
|
||||
"@vue/eslint-config-typescript": "^5.0.2",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"sass": "^1.49.0",
|
||||
"sass-loader": "^10.2.1",
|
||||
"sass": "^1.54.0",
|
||||
"sass-loader": "^10.3.1",
|
||||
"typescript": "^3.9.10",
|
||||
"vue-cli-plugin-vuetify": "~2.2.2",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"vuetify-loader": "^1.7.3"
|
||||
"vue-template-compiler": "^2.7.8",
|
||||
"vuetify-loader": "^1.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -7,6 +7,7 @@
|
||||
<title>neko-rooms</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
|
||||
<link rel="icon" type="image/x-icon" sizes="16x16" href="favicon.ico">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
|
||||
</head>
|
||||
|
||||
@@ -590,6 +590,36 @@ export interface RoomsConfig {
|
||||
* @memberof RoomsConfig
|
||||
*/
|
||||
'storage_enabled'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof RoomsConfig
|
||||
*/
|
||||
'uses_mux'?: boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface RoomSnapshot
|
||||
*/
|
||||
export interface RoomSnapshot {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof RoomSnapshot
|
||||
*/
|
||||
'id'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof RoomSnapshot
|
||||
*/
|
||||
'name'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof RoomSnapshot
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1248,6 +1278,39 @@ export const RoomsApiAxiosParamCreator = function (configuration?: Configuration
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Stop room
|
||||
* @param {string} roomId
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
roomSnapshot: async (roomId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'roomId' is not null or undefined
|
||||
assertParamExists('roomSnapshot', 'roomId', roomId)
|
||||
|
||||
const localVarPath = `/api/rooms/{roomId}/snapshot`
|
||||
.replace(`{${"roomId"}}`, encodeURIComponent(String(roomId)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
@@ -1376,6 +1439,17 @@ export const RoomsApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.roomsList(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Creates a room snapshot
|
||||
* @param {string} roomId
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async roomSnapshot(roomId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RoomEntry>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.roomSnapshot(roomId, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1485,6 +1559,16 @@ export const RoomsApiFactory = function (configuration?: Configuration, basePath
|
||||
roomsList(options?: any): AxiosPromise<Array<RoomEntry>> {
|
||||
return localVarFp.roomsList(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary Creates a room snapshot
|
||||
* @param {string} roomId
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
roomSnapshot(roomId: string, options?: any): AxiosPromise<RoomEntry> {
|
||||
return localVarFp.roomSnapshot(roomId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1613,6 +1697,18 @@ export class RoomsApi extends BaseAPI {
|
||||
public roomsList(options?: AxiosRequestConfig) {
|
||||
return RoomsApiFp(this.configuration).roomsList(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Creates a room snapshot
|
||||
* @param {string} roomId
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof RoomsApi
|
||||
*/
|
||||
public roomSnapshot(roomId: string, options?: AxiosRequestConfig) {
|
||||
return RoomsApiFp(this.configuration).roomSnapshot(roomId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -54,9 +54,7 @@
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from 'vue-property-decorator'
|
||||
|
||||
@Component({
|
||||
components: {}
|
||||
})
|
||||
@Component
|
||||
export default class Pull extends Vue {
|
||||
private dialog = false
|
||||
private loading = false
|
||||
|
||||
@@ -12,9 +12,9 @@ import { Vue, Component, Prop } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class RoomActionBtn extends Vue {
|
||||
@Prop(String) readonly action: string | undefined
|
||||
@Prop(String) readonly roomId: string | undefined
|
||||
@Prop(Boolean) readonly disabled: boolean | undefined
|
||||
@Prop(String) readonly action!: string
|
||||
@Prop(String) readonly roomId!: string
|
||||
@Prop(Boolean) readonly disabled!: boolean
|
||||
|
||||
private loading = false
|
||||
|
||||
@@ -61,6 +61,14 @@ export default class RoomActionBtn extends Vue {
|
||||
color: 'red',
|
||||
icon: 'mdi-trash-can-outline',
|
||||
}
|
||||
case 'snapshot':
|
||||
return {
|
||||
dispatch: 'ROOMS_SNAPSHOT',
|
||||
msg: 'Room snapshot created!',
|
||||
tooltip: 'Snapshot',
|
||||
color: 'blue',
|
||||
icon: 'mdi-content-save',
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
:rotate="270"
|
||||
:size="100"
|
||||
:width="15"
|
||||
:value="(stats.connections / settings.max_connections) * 100"
|
||||
:value="settings.max_connections == 0 ? 100 : ((stats.connections / settings.max_connections) * 100)"
|
||||
color="blue"
|
||||
>
|
||||
{{ stats.connections }} / {{ settings.max_connections }}
|
||||
{{ stats.connections }} <template v-if="settings.max_connections > 0">/ {{ settings.max_connections }}</template>
|
||||
</v-progress-circular>
|
||||
</div>
|
||||
</v-col>
|
||||
@@ -69,8 +69,27 @@
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="statsErr && room.running">
|
||||
<v-alert
|
||||
border="left"
|
||||
type="info"
|
||||
v-if="room.status.includes('starting')"
|
||||
>
|
||||
<p><strong>Room stats are not available.</strong></p>
|
||||
<p class="mb-0">Because room is currently starting. They will be availalbe soon.</p>
|
||||
</v-alert>
|
||||
<v-alert
|
||||
border="left"
|
||||
type="warning"
|
||||
v-else
|
||||
>
|
||||
<p><strong>Room stats are not available.</strong></p>
|
||||
<p class="mb-0">{{ statsErr }}</p>
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<v-btn @click="LoadStats" :loading="statsLoading">Reload</v-btn>
|
||||
<v-btn @click="LoadStats" :loading="statsLoading" :disabled="!room.running">Reload</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="my-3 headline">Main settings</div>
|
||||
@@ -80,26 +99,15 @@
|
||||
<tr><th style="width:50%;"> Name </th><td>{{ settings.name }}</td></tr>
|
||||
<tr><th> Neko image </th><td>{{ settings.neko_image }}</td></tr>
|
||||
<tr><th> User password </th><td>
|
||||
<v-btn @click="showUserPass = !showUserPass" icon small><v-icon small>{{ showUserPass ? 'mdi-eye' : 'mdi-eye-off' }}</v-icon></v-btn>
|
||||
<span class="mx-2">{{ showUserPass ? settings.user_pass : '****' }}</span>
|
||||
<v-tooltip top v-if="room">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" v-on="on" :disabled="!room.running" :href="room.url + '?pwd=' + encodeURIComponent(settings.user_pass)" target="_blank" small> <v-icon small>mdi-open-in-new</v-icon></v-btn>
|
||||
</template>
|
||||
<span>Invite link for users</span>
|
||||
</v-tooltip>
|
||||
<RoomLink :roomId="roomId" :password="settings.user_pass" label="invite link for users" />
|
||||
</td></tr>
|
||||
<tr><th> Admin password </th><td>
|
||||
<v-btn @click="showAdminPass = !showAdminPass" icon small><v-icon small>{{ showAdminPass ? 'mdi-eye' : 'mdi-eye-off' }}</v-icon></v-btn>
|
||||
<span class="mx-2">{{ showAdminPass ? settings.admin_pass : '****' }}</span>
|
||||
<v-tooltip bottom v-if="room">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" v-on="on" :disabled="!room.running" :href="room.url + '?pwd=' + encodeURIComponent(settings.admin_pass)" target="_blank" small> <v-icon small>mdi-open-in-new</v-icon></v-btn>
|
||||
</template>
|
||||
<span>Invite link for admins</span>
|
||||
</v-tooltip>
|
||||
<RoomLink :roomId="roomId" :password="settings.admin_pass" label="invite link for admins" />
|
||||
</td></tr>
|
||||
<tr v-if="!usesMux"><th> Max connections </th><td>
|
||||
<template v-if="settings.max_connections > 0">{{ settings.max_connections }}</template>
|
||||
<i v-else>not limited because uses mux</i>
|
||||
</td></tr>
|
||||
<tr><th> Max connections </th><td>{{ settings.max_connections }}</td></tr>
|
||||
<tr><th> Control protection </th><td>{{ settings.control_protection }}</td></tr>
|
||||
<tr><th> Implicit Control </th><td>{{ settings.implicit_control }}</td></tr>
|
||||
</tbody>
|
||||
@@ -196,6 +204,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop, Watch } from 'vue-property-decorator'
|
||||
import RoomLink from './RoomLink.vue'
|
||||
|
||||
import {
|
||||
RoomStats,
|
||||
@@ -204,19 +213,21 @@ import {
|
||||
RoomEntry,
|
||||
} from '@/api/index'
|
||||
|
||||
@Component
|
||||
@Component({
|
||||
components: {
|
||||
RoomLink,
|
||||
}
|
||||
})
|
||||
export default class RoomInfo extends Vue {
|
||||
@Prop(String) readonly roomId: string | undefined
|
||||
@Prop(String) readonly roomId!: string
|
||||
|
||||
private statsLoading = false
|
||||
private statsErr = ""
|
||||
private stats: RoomStats | null = null
|
||||
|
||||
private settingsLoading = false
|
||||
private settings: RoomSettings | null = null
|
||||
|
||||
private showUserPass = false
|
||||
private showAdminPass = false
|
||||
|
||||
get room(): RoomEntry {
|
||||
return this.$store.state.rooms.find(({ id }: RoomEntry) => id == this.roomId)
|
||||
}
|
||||
@@ -224,12 +235,15 @@ export default class RoomInfo extends Vue {
|
||||
@Watch('roomId', { immediate: true })
|
||||
async SetRoomId(roomId: string) {
|
||||
this.stats = null
|
||||
this.statsErr = ""
|
||||
this.settings = null
|
||||
this.settingsLoading = true
|
||||
|
||||
try {
|
||||
this.settings = await this.$store.dispatch('ROOMS_SETTINGS', roomId)
|
||||
this.LoadStats()
|
||||
if (this.room.running) {
|
||||
this.LoadStats()
|
||||
}
|
||||
} catch(e) {
|
||||
if (e.response) {
|
||||
this.$swal({
|
||||
@@ -249,6 +263,7 @@ export default class RoomInfo extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('room.status')
|
||||
async LoadStats() {
|
||||
this.statsLoading = true
|
||||
|
||||
@@ -269,11 +284,18 @@ export default class RoomInfo extends Vue {
|
||||
return 0
|
||||
})
|
||||
this.stats = stats
|
||||
this.statsErr = ""
|
||||
} catch (e: any) {
|
||||
this.statsErr = e
|
||||
} finally {
|
||||
this.statsLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
get usesMux() {
|
||||
return this.$store.state.roomsConfig.uses_mux
|
||||
}
|
||||
|
||||
get allBrowserExtensions() {
|
||||
if (!this.settings?.browser_policy?.type) return {}
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="d-flex align-center">
|
||||
<v-text-field
|
||||
ref="input"
|
||||
:append-icon="showPass ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
:value="showPass ? password : '*****'"
|
||||
@click:append="togglePass"
|
||||
@click="showPass && selectAll()"
|
||||
@focus="showPass && selectAll()"
|
||||
hide-details
|
||||
outlined
|
||||
readonly
|
||||
dense
|
||||
></v-text-field>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<v-tooltip bottom v-if="room">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" v-on="on" :disabled="!room.running" :href="url" target="_blank" small>
|
||||
<v-icon small>mdi-open-in-new</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ label }}</span>
|
||||
</v-tooltip>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<v-tooltip bottom v-if="room">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" v-on="on" :disabled="!room.running" @click="copyToClipboard" small>
|
||||
<v-icon small v-if="copied">mdi-clipboard-check-multiple</v-icon>
|
||||
<v-icon small v-else>mdi-clipboard-multiple-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>copy link to clipboard</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop } from 'vue-property-decorator'
|
||||
|
||||
import {
|
||||
RoomEntry,
|
||||
} from '@/api/index'
|
||||
|
||||
@Component
|
||||
export default class RoomLink extends Vue {
|
||||
@Prop(String) readonly roomId!: string
|
||||
@Prop(String) readonly password!: string
|
||||
@Prop(String) readonly label!: string
|
||||
|
||||
private showPass = false
|
||||
private copied = false
|
||||
private copiedTimeout = 0
|
||||
|
||||
get room(): RoomEntry {
|
||||
return this.$store.state.rooms.find(({ id }: RoomEntry) => id == this.roomId)
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this.room.url + '?pwd=' + encodeURIComponent(this.password)
|
||||
}
|
||||
|
||||
togglePass() {
|
||||
this.showPass = !this.showPass
|
||||
if (this.showPass) {
|
||||
setTimeout(() => {
|
||||
this.selectAll()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
this.$refs.input.$el.querySelector('input').select();
|
||||
}
|
||||
|
||||
copyToClipboard() {
|
||||
if (this.copiedTimeout) {
|
||||
clearInterval(this.copiedTimeout)
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(this.url)
|
||||
this.copied = true
|
||||
|
||||
this.copiedTimeout = setTimeout(() => {
|
||||
this.copied = false
|
||||
this.copiedTimeout = 0
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -56,7 +56,28 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row align="center">
|
||||
<!-- if uses mux vs. old logic -->
|
||||
<v-row align="center" v-if="usesMux">
|
||||
<v-col class="pt-0">
|
||||
<v-checkbox
|
||||
v-model="data.control_protection"
|
||||
label="Enable control protection"
|
||||
hide-details
|
||||
class="shrink ml-2 mt-0"
|
||||
></v-checkbox>
|
||||
<div style="margin-left: 41px;"><i>Users can gain control only if at least one admin is in the room.</i></div>
|
||||
</v-col>
|
||||
<v-col class="pt-0">
|
||||
<v-checkbox
|
||||
v-model="data.implicit_control"
|
||||
label="Enable implicit control"
|
||||
hide-details
|
||||
class="shrink ml-2 mt-0"
|
||||
></v-checkbox>
|
||||
<div style="margin-left: 41px;"><i>Users do not need to request control prior usage.</i></div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row align="center" v-else>
|
||||
<v-col class="pt-0">
|
||||
<v-text-field
|
||||
label="Max connections"
|
||||
@@ -482,6 +503,10 @@ export default class RoomsCreate extends Vue {
|
||||
return this.$store.state.roomsConfig.storage_enabled
|
||||
}
|
||||
|
||||
get usesMux() {
|
||||
return this.$store.state.roomsConfig.uses_mux
|
||||
}
|
||||
|
||||
get videoCodecs() {
|
||||
return this.$store.state.videoCodecs
|
||||
}
|
||||
@@ -526,14 +551,13 @@ export default class RoomsCreate extends Vue {
|
||||
const config = this.browserPolicyConfig
|
||||
if (!config) return []
|
||||
|
||||
return this.$store.state.browserPolicyExtensions.map(({ text, value }: {
|
||||
return this.$store.state.browserPolicyExtensions.map(({ text, value: values }: {
|
||||
text: string;
|
||||
value: Record<BrowserPolicyTypeEnum, BrowserPolicyExtension>;
|
||||
}) => {
|
||||
return {
|
||||
text,
|
||||
value: value[config.type as BrowserPolicyTypeEnum],
|
||||
}
|
||||
const value = values[config.type as BrowserPolicyTypeEnum]
|
||||
if (!value) return undefined
|
||||
return { text, value }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -579,7 +603,7 @@ export default class RoomsCreate extends Vue {
|
||||
// eslint-disable-next-line
|
||||
admin_pass: this.data.admin_pass || randomPassword(),
|
||||
// eslint-disable-next-line
|
||||
max_connections: Number(this.data.max_connections),
|
||||
max_connections: Number(this.data.max_connections), // ignored when uses mux
|
||||
// eslint-disable-next-line
|
||||
control_protection: Boolean(this.data.control_protection),
|
||||
// eslint-disable-next-line
|
||||
|
||||
@@ -33,11 +33,11 @@
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<template v-slot:[`item.max_connections`]="{ item }">
|
||||
<span v-if="item.max_connections">{{ item.max_connections }}</span>
|
||||
<i v-else>--not-specified--</i>
|
||||
<span v-if="item.max_connections > 0">{{ item.max_connections }}</span>
|
||||
<i v-else>uses mux</i>
|
||||
</template>
|
||||
<template v-slot:[`item.status`]="{ item }">
|
||||
<v-chip :color="item.running ? 'green' : 'red'" dark small> {{ item.status }} </v-chip>
|
||||
<v-chip :color="item.running ? (item.status.includes('unhealthy') ? 'warning' : 'green') : 'red'" dark small> {{ item.status }} </v-chip>
|
||||
</template>
|
||||
<template v-slot:[`item.created`]="{ item }">
|
||||
{{ item.created | timeago }}
|
||||
@@ -46,6 +46,7 @@
|
||||
<RoomActionBtn action="start" :roomId="item.id" :disabled="item.running" />
|
||||
<RoomActionBtn action="stop" :roomId="item.id" :disabled="!item.running" />
|
||||
<RoomActionBtn action="restart" :roomId="item.id" :disabled="!item.running" />
|
||||
<RoomActionBtn action="snapshot" :roomId="item.id" :disabled="!item.running" />
|
||||
</template>
|
||||
<template v-slot:[`item.destroy`]="{ item }">
|
||||
<RoomActionBtn action="remove" :roomId="item.id" />
|
||||
@@ -89,29 +90,34 @@ export default class RoomsList extends Vue {
|
||||
private roomId = ''
|
||||
private roomLoading = [] as Array<string>
|
||||
|
||||
private headers = [
|
||||
{
|
||||
text: 'Deployment',
|
||||
value: 'url',
|
||||
sortable: false,
|
||||
},
|
||||
{ text: 'Name', value: 'name' },
|
||||
{ text: 'Max connections', value: 'max_connections' },
|
||||
{ text: 'Neko image', value: 'neko_image' },
|
||||
{ text: 'Status', value: 'status' },
|
||||
{ text: 'Created', value: 'created' },
|
||||
{
|
||||
text: 'Actions',
|
||||
value: 'actions',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'Destroy',
|
||||
value: 'destroy',
|
||||
align: 'end',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
get headers() {
|
||||
return [
|
||||
{
|
||||
text: 'Deployment',
|
||||
value: 'url',
|
||||
sortable: false,
|
||||
},
|
||||
{ text: 'Name', value: 'name' },
|
||||
// when using mux we don't have max connections
|
||||
...(!this.$store.state.roomsConfig.uses_mux ? [
|
||||
{ text: 'Max connections', value: 'max_connections' }
|
||||
] : []),
|
||||
{ text: 'Neko image', value: 'neko_image' },
|
||||
{ text: 'Status', value: 'status' },
|
||||
{ text: 'Created', value: 'created' },
|
||||
{
|
||||
text: 'Actions',
|
||||
value: 'actions',
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
text: 'Destroy',
|
||||
value: 'destroy',
|
||||
align: 'end',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
get rooms() {
|
||||
return this.$store.state.rooms
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
RoomsApi,
|
||||
DefaultApi,
|
||||
PullStatus,
|
||||
RoomSnapshot,
|
||||
} from '@/api/index'
|
||||
|
||||
import { state, State } from './state'
|
||||
@@ -61,6 +62,9 @@ export default new Vuex.Store({
|
||||
PULL_STATUS(state: State, pullStatus: PullStatus) {
|
||||
Vue.set(state, 'pullStatus', pullStatus)
|
||||
},
|
||||
ROOMS_SNAPSHOT_ADD(state: State, snapshot: RoomSnapshot) {
|
||||
Vue.set(state, 'snapshot', snapshot)
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async ROOMS_CONFIG({ commit }: ActionContext<State, State>) {
|
||||
@@ -135,6 +139,14 @@ export default new Vuex.Store({
|
||||
const res = await defaultApi.pullStop()
|
||||
return res.data
|
||||
},
|
||||
async ROOMS_SNAPSHOT({ commit }: ActionContext<State, State>, roomId: string) {
|
||||
const res = await roomsApi.roomSnapshot(roomId)
|
||||
commit('ROOMS_SNAPSHOT_ADD', {
|
||||
roomId: roomId,
|
||||
data: res.data
|
||||
});
|
||||
return res.data
|
||||
},
|
||||
},
|
||||
modules: {
|
||||
}
|
||||
|
||||
@@ -177,6 +177,92 @@ export const state = {
|
||||
} as BrowserPolicyExtension,
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'SponsorBlock for YouTube',
|
||||
value: {
|
||||
[BrowserPolicyTypeEnum.firefox]: {
|
||||
id: 'sponsorBlocker@ajay.app',
|
||||
url: 'https://addons.mozilla.org/firefox/downloads/latest/sponsorblock/latest.xpi',
|
||||
} as BrowserPolicyExtension,
|
||||
[BrowserPolicyTypeEnum.chromium]: {
|
||||
id: 'mnjggcdmjocbbbhaepdhchncahnbgone',
|
||||
} as BrowserPolicyExtension,
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'SoundFixer',
|
||||
value: {
|
||||
[BrowserPolicyTypeEnum.firefox]: {
|
||||
id: 'soundfixer@unrelenting.technology',
|
||||
url: 'https://addons.mozilla.org/firefox/downloads/latest/soundfixer/latest.xpi',
|
||||
} as BrowserPolicyExtension,
|
||||
[BrowserPolicyTypeEnum.chromium]: {
|
||||
id: 'mbhbddecpoendcifccfckjkigbinefkg',
|
||||
} as BrowserPolicyExtension,
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Native MPEG-Dash + HLS Playback',
|
||||
value: {
|
||||
[BrowserPolicyTypeEnum.firefox]: {
|
||||
id: 'native-dash-hls@cavar.net',
|
||||
url: 'https://addons.mozilla.org/firefox/downloads/latest/native-mpeg-dash-hls-playback/latest.xpi',
|
||||
} as BrowserPolicyExtension,
|
||||
[BrowserPolicyTypeEnum.chromium]: {
|
||||
id: 'cjfbmleiaobegagekpmlhmaadepdeedn',
|
||||
} as BrowserPolicyExtension,
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Google Input Tools',
|
||||
value: {
|
||||
// not available for firefox
|
||||
[BrowserPolicyTypeEnum.chromium]: {
|
||||
id: 'mclkkofklkfljcocdinagocijmpgbhab',
|
||||
} as BrowserPolicyExtension,
|
||||
},
|
||||
},
|
||||
|
||||
//
|
||||
// VPN
|
||||
//
|
||||
|
||||
{
|
||||
text: 'Windscribe - Free Proxy and Ad Blocker',
|
||||
value: {
|
||||
[BrowserPolicyTypeEnum.firefox]: {
|
||||
id: '@windscribeff',
|
||||
url: 'https://addons.mozilla.org/firefox/downloads/latest/windscribe/latest.xpi',
|
||||
} as BrowserPolicyExtension,
|
||||
[BrowserPolicyTypeEnum.chromium]: {
|
||||
id: 'hnmpcagpplmpfojmgmnngilcnanddlhb',
|
||||
} as BrowserPolicyExtension,
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Private Internet Access',
|
||||
value: {
|
||||
[BrowserPolicyTypeEnum.firefox]: {
|
||||
id: '{3e4d2037-d300-4e95-859d-3cba866f46d3}',
|
||||
url: 'https://addons.mozilla.org/firefox/downloads/latest/private-internet-access-ext/latest.xpi',
|
||||
} as BrowserPolicyExtension,
|
||||
[BrowserPolicyTypeEnum.chromium]: {
|
||||
id: 'jplnlifepflhkbkgonidnobkakhmpnmh',
|
||||
} as BrowserPolicyExtension,
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'SetupVPN - Lifetime Free VPN',
|
||||
value: {
|
||||
[BrowserPolicyTypeEnum.firefox]: {
|
||||
id: '@setupvpncom',
|
||||
url: 'https://addons.mozilla.org/firefox/downloads/latest/setupvpn/latest.xpi',
|
||||
} as BrowserPolicyExtension,
|
||||
[BrowserPolicyTypeEnum.chromium]: {
|
||||
id: 'oofgbpoabipfcfjapgnbbjjaenockbdp',
|
||||
} as BrowserPolicyExtension,
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'NordVPN',
|
||||
value: {
|
||||
@@ -190,14 +276,22 @@ export const state = {
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'SponsorBlock for YouTube',
|
||||
text: 'Surfshark VPN Extension',
|
||||
value: {
|
||||
[BrowserPolicyTypeEnum.firefox]: {
|
||||
id: 'sponsorBlocker@ajay.app',
|
||||
url: 'https://addons.mozilla.org/firefox/downloads/latest/sponsorblock/latest.xpi',
|
||||
id: '{732216ec-0dab-43bb-ac85-4b5e1977599d}',
|
||||
url: 'https://addons.mozilla.org/firefox/downloads/latest/surfshark-vpn-proxy/latest.xpi',
|
||||
} as BrowserPolicyExtension,
|
||||
[BrowserPolicyTypeEnum.chromium]: {
|
||||
id: 'mnjggcdmjocbbbhaepdhchncahnbgone',
|
||||
id: 'ailoabdmgclmfmhdagmlohpjlbpffblp',
|
||||
} as BrowserPolicyExtension,
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Hola VPN - The Website Unblocker',
|
||||
value: {
|
||||
[BrowserPolicyTypeEnum.chromium]: {
|
||||
id: 'gkojfkhlekighikafcpjkiklfbnlmeio',
|
||||
} as BrowserPolicyExtension,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -101,7 +101,7 @@ export default class Home extends Vue {
|
||||
|
||||
get usedConnections() {
|
||||
// eslint-disable-next-line
|
||||
return this.$store.state.rooms.reduce((sum: number, { max_connections }: RoomEntry) => sum + (max_connections || 0), 0)
|
||||
return this.$store.state.rooms.reduce((sum: number, { max_connections }: RoomEntry) => sum + (max_connections || 1/* 0 when using mux */), 0)
|
||||
}
|
||||
|
||||
async LoadRooms() {
|
||||
|
||||
@@ -18,7 +18,6 @@ func init() {
|
||||
|
||||
configs := []config.Config{
|
||||
nekoRooms.Service.Configs.Server,
|
||||
nekoRooms.Service.Configs.API,
|
||||
nekoRooms.Service.Configs.Room,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ ! -f "../.env" ]
|
||||
then
|
||||
echo "../.env file not found!"
|
||||
exit 1
|
||||
if [ -z $APP_PORT ]; then
|
||||
APP_PORT="8080"
|
||||
fi
|
||||
|
||||
export $(cat ../.env | sed 's/#.*//g' | xargs)
|
||||
if [ -z $APP_HOST ]; then
|
||||
for i in $(ifconfig -l 2>/dev/null); do
|
||||
APP_HOST=$(ipconfig getifaddr $i)
|
||||
if [ ! -z $APP_HOST ]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z $APP_HOST ]; then
|
||||
APP_HOST=$(hostname -i 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using app port: ${APP_PORT}"
|
||||
echo "Using IP address: ${APP_HOST}"
|
||||
|
||||
docker run --rm -it \
|
||||
--name="neko_rooms_client" \
|
||||
-v "${PWD}/../client:/app" \
|
||||
-e "TZ=${TZ}" \
|
||||
--net="${NEKO_ROOMS_TRAEFIK_NETWORK}" \
|
||||
-l "traefik.enable=true" \
|
||||
-l "traefik.http.services.neko-rooms-client-fe.loadbalancer.server.port=8080" \
|
||||
-l "traefik.http.routers.neko-rooms-client.entrypoints=${NEKO_ROOMS_TRAEFIK_ENTRYPOINT}" \
|
||||
-l "traefik.http.routers.neko-rooms-client.rule=Host(\`${NEKO_ROOMS_TRAEFIK_DOMAIN}\`)" \
|
||||
-p 8081:8080 \
|
||||
-e "API_PROXY=http://${APP_HOST}:${APP_PORT}" \
|
||||
--user="$(id -u):$(id -g)" \
|
||||
--workdir="/app" \
|
||||
--volume "${PWD}/../client:/app" \
|
||||
--entrypoint="npm" \
|
||||
--workdir="/app" \
|
||||
node:14 run serve;
|
||||
|
||||
@@ -1,13 +1,36 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ ! -f "../.env" ]
|
||||
then
|
||||
echo "../.env file not found!"
|
||||
exit 1
|
||||
if [ -z $NEKO_ROOMS_PORT ]; then
|
||||
NEKO_ROOMS_PORT="8080"
|
||||
fi
|
||||
|
||||
export $(cat ../.env | sed 's/#.*//g' | xargs)
|
||||
if [ -z $NEKO_ROOMS_EPR ]; then
|
||||
NEKO_ROOMS_EPR="52090-52099"
|
||||
fi
|
||||
|
||||
if [ -z $NEKO_ROOMS_NAT1TO1 ]; then
|
||||
for i in $(ifconfig -l 2>/dev/null); do
|
||||
NEKO_ROOMS_NAT1TO1=$(ipconfig getifaddr $i)
|
||||
if [ ! -z $NEKO_ROOMS_NAT1TO1 ]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z $NEKO_ROOMS_NAT1TO1 ]; then
|
||||
NEKO_ROOMS_NAT1TO1=$(hostname -i 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
|
||||
NEKO_ROOMS_INSTANCE_NETWORK="neko-rooms-net"
|
||||
docker network create --attachable "${NEKO_ROOMS_INSTANCE_NETWORK}";
|
||||
|
||||
trap on_exit EXIT
|
||||
|
||||
on_exit() {
|
||||
echo "Removing neko-rooms network"
|
||||
docker network rm "${NEKO_ROOMS_INSTANCE_NETWORK}";
|
||||
}
|
||||
|
||||
DATA_PATH="./data"
|
||||
mkdir -p "${DATA_PATH}"
|
||||
@@ -17,23 +40,22 @@ mkdir -p "${EXTERNAL_PATH}"
|
||||
|
||||
docker run --rm -it \
|
||||
--name="neko_rooms_server" \
|
||||
-p "${NEKO_ROOMS_PORT}:8080" \
|
||||
-v "`realpath ..`:/app" \
|
||||
-v "`realpath ${DATA_PATH}`:/data" \
|
||||
-e "TZ=${TZ}" \
|
||||
-e "NEKO_ROOMS_MUX=true" \
|
||||
-e "NEKO_ROOMS_EPR=${NEKO_ROOMS_EPR}" \
|
||||
-e "NEKO_ROOMS_NAT1TO1=${NEKO_ROOMS_NAT1TO1}" \
|
||||
-e "NEKO_ROOMS_INSTANCE_URL=http://${NEKO_ROOMS_NAT1TO1}:${NEKO_ROOMS_PORT}/" \
|
||||
-e "NEKO_ROOMS_INSTANCE_NETWORK=${NEKO_ROOMS_INSTANCE_NETWORK}" \
|
||||
-e "NEKO_ROOMS_STORAGE_INTERNAL=/data" \
|
||||
-e "NEKO_ROOMS_STORAGE_EXTERNAL=`realpath ${DATA_PATH}`" \
|
||||
-e "NEKO_ROOMS_MOUNTS_WHITELIST=`realpath ${EXTERNAL_PATH}`" \
|
||||
-e "NEKO_ROOMS_TRAEFIK_DOMAIN=${NEKO_ROOMS_TRAEFIK_DOMAIN}" \
|
||||
-e "NEKO_ROOMS_TRAEFIK_ENTRYPOINT=${NEKO_ROOMS_TRAEFIK_ENTRYPOINT}" \
|
||||
-e "NEKO_ROOMS_TRAEFIK_NETWORK=${NEKO_ROOMS_TRAEFIK_NETWORK}" \
|
||||
-e "NEKO_ROOMS_PATH_PREFIX=/room/" \
|
||||
-e "NEKO_ROOMS_TRAEFIK_ENABLED=false" \
|
||||
-e 'DOCKER_API_VERSION=1.39' \
|
||||
-v "/var/run/docker.sock:/var/run/docker.sock" \
|
||||
--net="${NEKO_ROOMS_TRAEFIK_NETWORK}" \
|
||||
-l "traefik.enable=true" \
|
||||
-l "traefik.http.services.neko-rooms-server-fe.loadbalancer.server.port=8080" \
|
||||
-l "traefik.http.routers.neko-rooms-server.entrypoints=${NEKO_ROOMS_TRAEFIK_ENTRYPOINT}" \
|
||||
-l "traefik.http.routers.neko-rooms-server.rule=Host(\`${NEKO_ROOMS_TRAEFIK_DOMAIN}\`) && PathPrefix(\`/api\`)" \
|
||||
--net="${NEKO_ROOMS_INSTANCE_NETWORK}" \
|
||||
--entrypoint="/app/bin/neko_rooms" \
|
||||
neko_rooms_img serve --bind :8080;
|
||||
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
set -e
|
||||
|
||||
docker run --rm -it \
|
||||
-v "${PWD}/../../:/app" \
|
||||
--entrypoint="go" \
|
||||
neko_rooms_img build -o bin/neko_rooms cmd/neko_rooms/main.go
|
||||
|
||||
./start
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ ! -f ".env" ]
|
||||
then
|
||||
echo ".env file not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export $(cat .env | sed 's/#.*//g' | xargs)
|
||||
|
||||
docker run --rm -it \
|
||||
--name="neko_rooms_client" \
|
||||
-v "${PWD}/../../client:/app" \
|
||||
-e "TZ=${TZ}" \
|
||||
--net="${NEKO_ROOMS_TRAEFIK_NETWORK}" \
|
||||
-l "traefik.enable=true" \
|
||||
-l "traefik.http.services.neko-rooms-client-fe.loadbalancer.server.port=8080" \
|
||||
-l "traefik.http.routers.neko-rooms-client.entrypoints=${NEKO_ROOMS_TRAEFIK_ENTRYPOINT}" \
|
||||
-l "traefik.http.routers.neko-rooms-client.rule=Host(\`${NEKO_ROOMS_TRAEFIK_DOMAIN}\`)" \
|
||||
-l "traefik.http.routers.neko-rooms-client.priority=1" \
|
||||
--user="$(id -u):$(id -g)" \
|
||||
--workdir="/app" \
|
||||
--entrypoint="npm" \
|
||||
node:14 run serve;
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ ! -f ".env" ]
|
||||
then
|
||||
echo ".env file not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export $(cat .env | sed 's/#.*//g' | xargs)
|
||||
|
||||
DATA_PATH="../data"
|
||||
mkdir -p "${DATA_PATH}"
|
||||
|
||||
EXTERNAL_PATH="../ext"
|
||||
mkdir -p "${EXTERNAL_PATH}"
|
||||
|
||||
docker run --rm -it \
|
||||
--name="neko_rooms_server" \
|
||||
-v "`realpath ../../`:/app" \
|
||||
-v "`realpath ${DATA_PATH}`:/data" \
|
||||
-e "TZ=${TZ}" \
|
||||
-e "NEKO_ROOMS_EPR=${NEKO_ROOMS_EPR}" \
|
||||
-e "NEKO_ROOMS_MUX=true" \
|
||||
-e "NEKO_ROOMS_NAT1TO1=${NEKO_ROOMS_NAT1TO1}" \
|
||||
-e "NEKO_ROOMS_PATH_PREFIX=/room/" \
|
||||
-e "NEKO_ROOMS_STORAGE_INTERNAL=/data" \
|
||||
-e "NEKO_ROOMS_STORAGE_EXTERNAL=`realpath ${DATA_PATH}`" \
|
||||
-e "NEKO_ROOMS_MOUNTS_WHITELIST=`realpath ${EXTERNAL_PATH}`" \
|
||||
-e "NEKO_ROOMS_TRAEFIK_ENABLED=true" \
|
||||
-e "NEKO_ROOMS_TRAEFIK_DOMAIN=${NEKO_ROOMS_TRAEFIK_DOMAIN}" \
|
||||
-e "NEKO_ROOMS_TRAEFIK_ENTRYPOINT=${NEKO_ROOMS_TRAEFIK_ENTRYPOINT}" \
|
||||
-e "NEKO_ROOMS_TRAEFIK_NETWORK=${NEKO_ROOMS_TRAEFIK_NETWORK}" \
|
||||
-e 'DOCKER_API_VERSION=1.39' \
|
||||
-v "/var/run/docker.sock:/var/run/docker.sock" \
|
||||
--net="${NEKO_ROOMS_TRAEFIK_NETWORK}" \
|
||||
-l "traefik.enable=true" \
|
||||
-l "traefik.http.services.neko-rooms-server-fe.loadbalancer.server.port=8080" \
|
||||
-l "traefik.http.routers.neko-rooms-server.entrypoints=${NEKO_ROOMS_TRAEFIK_ENTRYPOINT}" \
|
||||
-l "traefik.http.routers.neko-rooms-server.rule=Host(\`${NEKO_ROOMS_TRAEFIK_DOMAIN}\`) && PathPrefix(\`/room\`)" \
|
||||
-l "traefik.http.routers.neko-rooms-server.priority=2" \
|
||||
-l "traefik.http.routers.neko-rooms-server.service=neko-rooms-server-fe" \
|
||||
-l "traefik.http.routers.neko-rooms-server-api.entrypoints=${NEKO_ROOMS_TRAEFIK_ENTRYPOINT}" \
|
||||
-l "traefik.http.routers.neko-rooms-server-api.rule=Host(\`${NEKO_ROOMS_TRAEFIK_DOMAIN}\`) && PathPrefix(\`/api\`)" \
|
||||
-l "traefik.http.routers.neko-rooms-server-api.service=neko-rooms-server-fe" \
|
||||
--entrypoint="/app/bin/neko_rooms" \
|
||||
neko_rooms_img serve --bind :8080;
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ ! -f "../.env" ]
|
||||
if [ ! -f ".env" ]
|
||||
then
|
||||
echo "../.env file not found!"
|
||||
echo ".env file not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export $(cat ../.env | sed 's/#.*//g' | xargs)
|
||||
export $(cat .env | sed 's/#.*//g' | xargs)
|
||||
|
||||
docker network create --attachable "${NEKO_ROOMS_TRAEFIK_NETWORK}";
|
||||
|
||||
@@ -22,7 +22,7 @@ docker run --rm -it \
|
||||
--name="neko_rooms_traefik" \
|
||||
-p "${1:-80}:80" \
|
||||
-p "8080:8080" \
|
||||
-v "${PWD}/../:/app" \
|
||||
-v "${PWD}/../../:/app" \
|
||||
-e "TZ=${TZ}" \
|
||||
-v "/var/run/docker.sock:/var/run/docker.sock" \
|
||||
--net="${NEKO_ROOMS_TRAEFIK_NETWORK}" \
|
||||
+13
-32
@@ -1,43 +1,24 @@
|
||||
version: "3.5"
|
||||
|
||||
#
|
||||
# This docker compose needs .env file.
|
||||
# Copy .env.example to .env and modify.
|
||||
#
|
||||
|
||||
networks:
|
||||
default:
|
||||
attachable: "true"
|
||||
name: "${NEKO_ROOMS_TRAEFIK_NETWORK}"
|
||||
attachable: true
|
||||
name: "neko-rooms-net"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:2.4"
|
||||
restart: "unless-stopped"
|
||||
environment:
|
||||
- "TZ"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
- "./traefik/traefik.yml:/etc/traefik/traefik.yml:ro"
|
||||
- "./traefik/usersfile:/usersfile:ro"
|
||||
- "./traefik/acme.json:/acme.json"
|
||||
- "./traefik/config:/config"
|
||||
|
||||
neko-rooms:
|
||||
image: "m1k1o/neko-rooms:latest"
|
||||
restart: "unless-stopped"
|
||||
env_file:
|
||||
- ".env"
|
||||
environment:
|
||||
- "TZ=Europe/Vienna"
|
||||
- "NEKO_ROOMS_MUX=true"
|
||||
- "NEKO_ROOMS_EPR=59000-59049"
|
||||
- "NEKO_ROOMS_NAT1TO1=127.0.0.1" # IP address of your server that is reachable from client
|
||||
- "NEKO_ROOMS_INSTANCE_URL=http://127.0.0.1:8080/" # external URL
|
||||
- "NEKO_ROOMS_INSTANCE_NETWORK=neko-rooms-net"
|
||||
- "NEKO_ROOMS_TRAEFIK_ENABLED=false"
|
||||
- "NEKO_ROOMS_PATH_PREFIX=/room/"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.services.neko-rooms-frontend.loadbalancer.server.port=8080"
|
||||
- "traefik.http.routers.neko-rooms.entrypoints=${NEKO_ROOMS_TRAEFIK_ENTRYPOINT}"
|
||||
- "traefik.http.routers.neko-rooms.rule=Host(`${NEKO_ROOMS_TRAEFIK_DOMAIN}`)"
|
||||
- "traefik.http.routers.neko-rooms.tls=true"
|
||||
- "traefik.http.routers.neko-rooms.tls.certresolver=${NEKO_ROOMS_TRAEFIK_CERTRESOLVER}"
|
||||
- "traefik.http.routers.neko-rooms.middlewares=basicauth@file"
|
||||
|
||||
@@ -21,3 +21,11 @@ NEKO_ROOMS_PATH_PREFIX=/test/
|
||||
```
|
||||
|
||||
Now room will be available at `example.org/test/<room-name>` instead of `example.org/<room-name>`.
|
||||
|
||||
## using mux
|
||||
|
||||
When using mux, there will be allocated two ports per room: TCP and UDP port with the same number.
|
||||
|
||||
```
|
||||
NEKO_ROOMS_MUX=true
|
||||
```
|
||||
|
||||
@@ -1,52 +1,50 @@
|
||||
module github.com/m1k1o/neko-rooms
|
||||
|
||||
go 1.17
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.5.1 // indirect
|
||||
github.com/containerd/containerd v1.5.9 // indirect
|
||||
github.com/docker/docker v20.10.12+incompatible
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/docker/docker v20.10.17+incompatible
|
||||
github.com/docker/go-connections v0.4.0
|
||||
github.com/go-chi/chi v4.1.2+incompatible
|
||||
github.com/go-chi/cors v1.2.0
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/rs/zerolog v1.26.1
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/spf13/afero v1.8.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/cobra v1.3.0
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/rs/zerolog v1.27.0
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/spf13/afero v1.9.2 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/cobra v1.5.0
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/viper v1.10.1
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 // indirect
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350 // indirect
|
||||
google.golang.org/grpc v1.44.0 // indirect
|
||||
github.com/spf13/viper v1.12.0
|
||||
golang.org/x/net v0.0.0-20220728211354-c7608f3a8462 // indirect
|
||||
golang.org/x/sys v0.0.0-20220731174439-a90be440212d // indirect
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/ini.v1 v1.66.3 // indirect
|
||||
gopkg.in/ini.v1 v1.66.6 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.8.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.0 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.0.3 // indirect
|
||||
)
|
||||
|
||||
+2
-4
@@ -5,7 +5,6 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/m1k1o/neko-rooms/internal/config"
|
||||
"github.com/m1k1o/neko-rooms/internal/types"
|
||||
)
|
||||
|
||||
@@ -13,15 +12,13 @@ type ApiManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
rooms types.RoomManager
|
||||
pull types.PullManager
|
||||
conf *config.API
|
||||
}
|
||||
|
||||
func New(rooms types.RoomManager, pull types.PullManager, conf *config.API) *ApiManagerCtx {
|
||||
func New(rooms types.RoomManager, pull types.PullManager) *ApiManagerCtx {
|
||||
return &ApiManagerCtx{
|
||||
logger: log.With().Str("module", "api").Logger(),
|
||||
rooms: rooms,
|
||||
pull: pull,
|
||||
conf: conf,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,5 +57,6 @@ func (manager *ApiManagerCtx) Mount(r chi.Router) {
|
||||
r.Post("/stop", manager.roomGenericAction(manager.rooms.Stop))
|
||||
r.Post("/restart", manager.roomGenericAction(manager.rooms.Restart))
|
||||
r.Post("/recreate", manager.roomRecreate)
|
||||
r.Post("/snapshot", manager.roomSnapshot)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -150,3 +150,15 @@ func (manager *ApiManagerCtx) roomGenericAction(action func(id string) error) fu
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *ApiManagerCtx) roomSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||
roomId := chi.URLParam(r, "roomId")
|
||||
response, err := manager.rooms.Snapshot(roomId)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
Lobby bool
|
||||
}
|
||||
|
||||
func (API) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().Bool("lobby", true, "show lobby when room is not started yet; in order to show lobby, neko-rooms must run on the same subdomain where rooms are published")
|
||||
if err := viper.BindPFlag("lobby", cmd.PersistentFlags().Lookup("lobby")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *API) Set() {
|
||||
s.Lobby = viper.GetBool("lobby")
|
||||
}
|
||||
+79
-31
@@ -13,7 +13,16 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Traefik struct {
|
||||
Enabled bool
|
||||
Domain string
|
||||
Entrypoint string
|
||||
Certresolver string
|
||||
Port string // deprecated
|
||||
}
|
||||
|
||||
type Room struct {
|
||||
Mux bool
|
||||
EprMin uint16
|
||||
EprMax uint16
|
||||
|
||||
@@ -22,6 +31,7 @@ type Room struct {
|
||||
NekoPrivilegedImages []string
|
||||
PathPrefix string
|
||||
Labels []string
|
||||
WaitEnabled bool
|
||||
|
||||
StorageEnabled bool
|
||||
StorageInternal string
|
||||
@@ -29,17 +39,19 @@ type Room struct {
|
||||
|
||||
MountsWhitelist []string
|
||||
|
||||
InstanceName string
|
||||
InstanceUrl *url.URL
|
||||
InstanceName string
|
||||
InstanceUrl *url.URL
|
||||
InstanceNetwork string
|
||||
|
||||
TraefikDomain string
|
||||
TraefikEntrypoint string
|
||||
TraefikCertresolver string
|
||||
TraefikNetwork string
|
||||
TraefikPort string // deprecated
|
||||
Traefik Traefik
|
||||
}
|
||||
|
||||
func (Room) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().StringSlice("usemux", []string{}, "use mux for connection - needs only one UDP and TCP port per room")
|
||||
if err := viper.BindPFlag("usemux", cmd.PersistentFlags().Lookup("usemux")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("epr", "59000-59999", "limits the pool of ephemeral ports that ICE UDP connections can allocate from")
|
||||
if err := viper.BindPFlag("epr", cmd.PersistentFlags().Lookup("epr")); err != nil {
|
||||
return err
|
||||
@@ -57,6 +69,7 @@ func (Room) Init(cmd *cobra.Command) error {
|
||||
"m1k1o/neko:ungoogled-chromium",
|
||||
"m1k1o/neko:microsoft-edge",
|
||||
"m1k1o/neko:brave",
|
||||
"m1k1o/neko:vivaldi",
|
||||
"m1k1o/neko:tor-browser",
|
||||
"m1k1o/neko:vlc",
|
||||
"m1k1o/neko:remmina",
|
||||
@@ -71,7 +84,7 @@ func (Room) Init(cmd *cobra.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("path_prefix", "", "path prefix that is added to every room path")
|
||||
cmd.PersistentFlags().String("path_prefix", "/", "path prefix that is added to every room path")
|
||||
if err := viper.BindPFlag("path_prefix", cmd.PersistentFlags().Lookup("path_prefix")); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -81,6 +94,11 @@ func (Room) Init(cmd *cobra.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("wait_enabled", true, "enable active waiting for the room")
|
||||
if err := viper.BindPFlag("wait_enabled", cmd.PersistentFlags().Lookup("wait_enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Data
|
||||
|
||||
cmd.PersistentFlags().Bool("storage.enabled", true, "whether storage is enabled, where peristent containers data will be stored")
|
||||
@@ -115,8 +133,18 @@ func (Room) Init(cmd *cobra.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("instance.network", "", "docker network that will be used for this instance to communicate with rooms")
|
||||
if err := viper.BindPFlag("instance.network", cmd.PersistentFlags().Lookup("instance.network")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Traefik
|
||||
|
||||
cmd.PersistentFlags().Bool("traefik.enabled", true, "traefik: enabled or disabled")
|
||||
if err := viper.BindPFlag("traefik.enabled", cmd.PersistentFlags().Lookup("traefik.enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("traefik.domain", "", "traefik: domain on which will be container hosted (if empty or '*', match all; for neko-rooms as subdomain use '*.domain.tld')")
|
||||
if err := viper.BindPFlag("traefik.domain", cmd.PersistentFlags().Lookup("traefik.domain")); err != nil {
|
||||
return err
|
||||
@@ -132,7 +160,7 @@ func (Room) Init(cmd *cobra.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("traefik.network", "traefik", "traefik: docker network name")
|
||||
cmd.PersistentFlags().String("traefik.network", "traefik", "traefik: docker network name (deprecated, use instance.network)")
|
||||
if err := viper.BindPFlag("traefik.network", cmd.PersistentFlags().Lookup("traefik.network")); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -146,6 +174,8 @@ func (Room) Init(cmd *cobra.Command) error {
|
||||
}
|
||||
|
||||
func (s *Room) Set() {
|
||||
s.Mux = viper.GetBool("mux")
|
||||
|
||||
min := uint16(59000)
|
||||
max := uint16(59999)
|
||||
epr := viper.GetString("epr")
|
||||
@@ -173,8 +203,9 @@ func (s *Room) Set() {
|
||||
s.NAT1To1IPs = viper.GetStringSlice("nat1to1")
|
||||
s.NekoImages = viper.GetStringSlice("neko_images")
|
||||
s.NekoPrivilegedImages = viper.GetStringSlice("neko_privileged_images")
|
||||
s.PathPrefix = viper.GetString("path_prefix")
|
||||
s.PathPrefix = path.Join("/", path.Clean(viper.GetString("path_prefix")))
|
||||
s.Labels = viper.GetStringSlice("labels")
|
||||
s.WaitEnabled = viper.GetBool("wait_enabled")
|
||||
|
||||
s.StorageEnabled = viper.GetBool("storage.enabled")
|
||||
s.StorageInternal = viper.GetString("storage.internal")
|
||||
@@ -215,18 +246,33 @@ func (s *Room) Set() {
|
||||
}
|
||||
}
|
||||
|
||||
s.TraefikDomain = viper.GetString("traefik.domain")
|
||||
s.TraefikEntrypoint = viper.GetString("traefik.entrypoint")
|
||||
s.TraefikCertresolver = viper.GetString("traefik.certresolver")
|
||||
s.TraefikNetwork = viper.GetString("traefik.network")
|
||||
s.InstanceNetwork = viper.GetString("instance.network")
|
||||
|
||||
// deprecated
|
||||
s.TraefikPort = viper.GetString("traefik.port")
|
||||
if s.TraefikPort != "" {
|
||||
if s.InstanceUrl != nil {
|
||||
log.Warn().Msg("deprecated `traefik.port` config item is ignored when `instance.url` is set")
|
||||
} else {
|
||||
log.Warn().Msg("you are using deprecated `traefik.port` config item, you should consider moving to `instance.url`")
|
||||
s.Traefik.Enabled = viper.GetBool("traefik.enabled")
|
||||
if s.Traefik.Enabled {
|
||||
s.Traefik.Domain = viper.GetString("traefik.domain")
|
||||
s.Traefik.Entrypoint = viper.GetString("traefik.entrypoint")
|
||||
s.Traefik.Certresolver = viper.GetString("traefik.certresolver")
|
||||
|
||||
// deprecated
|
||||
traefikNetwork := viper.GetString("traefik.network")
|
||||
if traefikNetwork != "" {
|
||||
if s.InstanceNetwork != "" {
|
||||
log.Warn().Msg("deprecated `traefik.network` config item is ignored when `instance.network` is set")
|
||||
} else {
|
||||
log.Warn().Msg("you are using deprecated `traefik.network` config item, you should consider moving to `instance.network`")
|
||||
s.InstanceNetwork = traefikNetwork
|
||||
}
|
||||
}
|
||||
|
||||
// deprecated
|
||||
s.Traefik.Port = viper.GetString("traefik.port")
|
||||
if s.Traefik.Port != "" {
|
||||
if s.InstanceUrl != nil {
|
||||
log.Warn().Msg("deprecated `traefik.port` config item is ignored when `instance.url` is set")
|
||||
} else {
|
||||
log.Warn().Msg("you are using deprecated `traefik.port` config item, you should consider moving to `instance.url`")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,17 +288,19 @@ func (s *Room) GetInstanceUrl() url.URL {
|
||||
Path: "/",
|
||||
}
|
||||
|
||||
if s.TraefikCertresolver != "" {
|
||||
instanceUrl.Scheme = "https"
|
||||
}
|
||||
if s.Traefik.Enabled {
|
||||
if s.Traefik.Certresolver != "" {
|
||||
instanceUrl.Scheme = "https"
|
||||
}
|
||||
|
||||
if s.TraefikDomain != "" && s.TraefikDomain != "*" {
|
||||
instanceUrl.Host = s.TraefikDomain
|
||||
}
|
||||
if s.Traefik.Domain != "" && s.Traefik.Domain != "*" {
|
||||
instanceUrl.Host = s.Traefik.Domain
|
||||
}
|
||||
|
||||
// deprecated
|
||||
if s.TraefikPort != "" {
|
||||
instanceUrl.Host += ":" + s.TraefikPort
|
||||
// deprecated
|
||||
if s.Traefik.Port != "" {
|
||||
instanceUrl.Host += ":" + s.Traefik.Port
|
||||
}
|
||||
}
|
||||
|
||||
return instanceUrl
|
||||
@@ -264,7 +312,7 @@ func (s *Room) GetRoomUrl(roomName string) string {
|
||||
if strings.HasPrefix(instanceUrl.Host, "*.") {
|
||||
instanceUrl.Host = roomName + "." + strings.TrimPrefix(instanceUrl.Host, "*.")
|
||||
} else {
|
||||
instanceUrl.Path = path.Join(instanceUrl.Path, s.PathPrefix, roomName, "/")
|
||||
instanceUrl.Path = path.Join(instanceUrl.Path, s.PathPrefix, roomName) + "/"
|
||||
}
|
||||
|
||||
return instanceUrl.String()
|
||||
|
||||
+41
-12
@@ -5,13 +5,21 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Admin struct {
|
||||
Static string
|
||||
ProxyAuth string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Cert string
|
||||
Key string
|
||||
Bind string
|
||||
Static string
|
||||
Proxy bool
|
||||
PProf bool
|
||||
Cert string
|
||||
Key string
|
||||
Bind string
|
||||
Proxy bool
|
||||
PProf bool
|
||||
|
||||
Admin Admin
|
||||
}
|
||||
|
||||
func (Server) Init(cmd *cobra.Command) error {
|
||||
@@ -30,11 +38,6 @@ func (Server) Init(cmd *cobra.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("static", "./www", "path to neko_rooms client files to serve")
|
||||
if err := viper.BindPFlag("static", cmd.PersistentFlags().Lookup("static")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("proxy", false, "trust reverse proxy headers")
|
||||
if err := viper.BindPFlag("proxy", cmd.PersistentFlags().Lookup("proxy")); err != nil {
|
||||
return err
|
||||
@@ -45,6 +48,28 @@ func (Server) Init(cmd *cobra.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Admin
|
||||
|
||||
cmd.PersistentFlags().String("admin.static", "", "path to neko_rooms admin client files to serve")
|
||||
if err := viper.BindPFlag("admin.static", cmd.PersistentFlags().Lookup("admin.static")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("admin.proxy_auth", "", "require auth: proxy authentication URL, only allow if it returns 200")
|
||||
if err := viper.BindPFlag("admin.proxy_auth", cmd.PersistentFlags().Lookup("admin.proxy_auth")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("admin.username", "admin", "require auth: admin username")
|
||||
if err := viper.BindPFlag("admin.username", cmd.PersistentFlags().Lookup("admin.username")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("admin.password", "", "require auth: admin password")
|
||||
if err := viper.BindPFlag("admin.password", cmd.PersistentFlags().Lookup("admin.password")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -52,7 +77,11 @@ func (s *Server) Set() {
|
||||
s.Cert = viper.GetString("cert")
|
||||
s.Key = viper.GetString("key")
|
||||
s.Bind = viper.GetString("bind")
|
||||
s.Static = viper.GetString("static")
|
||||
s.Proxy = viper.GetBool("proxy")
|
||||
s.PProf = viper.GetBool("pprof")
|
||||
|
||||
s.Admin.Static = viper.GetString("admin.static")
|
||||
s.Admin.ProxyAuth = viper.GetString("admin.proxy_auth")
|
||||
s.Admin.Username = viper.GetString("admin.username")
|
||||
s.Admin.Password = viper.GetString("admin.password")
|
||||
}
|
||||
|
||||
@@ -36,17 +36,17 @@ func Generate(policies types.BrowserPolicyContent) (string, error) {
|
||||
)
|
||||
}
|
||||
|
||||
ExtensionInstallWhitelist := []interface{}{}
|
||||
ExtensionInstallAllowlist := []interface{}{}
|
||||
for _, e := range policies.Extensions {
|
||||
ExtensionInstallWhitelist = append(
|
||||
ExtensionInstallWhitelist,
|
||||
ExtensionInstallAllowlist = append(
|
||||
ExtensionInstallAllowlist,
|
||||
e.ID,
|
||||
)
|
||||
}
|
||||
|
||||
policiesTmpl["ExtensionInstallForcelist"] = ExtensionInstallForcelist
|
||||
policiesTmpl["ExtensionInstallWhitelist"] = ExtensionInstallWhitelist
|
||||
policiesTmpl["ExtensionInstallBlacklist"] = []interface{}{"*"}
|
||||
policiesTmpl["ExtensionInstallAllowlist"] = ExtensionInstallAllowlist
|
||||
policiesTmpl["ExtensionInstallBlocklist"] = []interface{}{"*"}
|
||||
|
||||
//
|
||||
// Developer Tools
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"HomepageLocation": "",
|
||||
"AutoFillEnabled": false,
|
||||
"AutofillAddressEnabled": false,
|
||||
"AutofillCreditCardEnabled": false,
|
||||
"BrowserSignin": 0,
|
||||
@@ -20,7 +18,7 @@
|
||||
"PromptForDownloadLocation": false,
|
||||
"BookmarkBarEnabled": false,
|
||||
"PasswordManagerEnabled": false,
|
||||
"URLBlacklist": [
|
||||
"URLBlocklist": [
|
||||
"file://*",
|
||||
"chrome://policy"
|
||||
],
|
||||
@@ -28,11 +26,11 @@
|
||||
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
|
||||
"fjoaledfpmneenckfbpdfhkmimnjocfa;https://clients2.google.com/service/update2/crx"
|
||||
],
|
||||
"ExtensionInstallWhitelist": [
|
||||
"ExtensionInstallAllowlist": [
|
||||
"cjpalhdlnbpafiamejdnhcphjbkeiagm",
|
||||
"fjoaledfpmneenckfbpdfhkmimnjocfa"
|
||||
],
|
||||
"ExtensionInstallBlacklist": [
|
||||
"ExtensionInstallBlocklist": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,41 +1,46 @@
|
||||
package api
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
|
||||
"github.com/m1k1o/neko-rooms/internal/utils"
|
||||
)
|
||||
|
||||
func (manager *ApiManagerCtx) RoomLobby(w http.ResponseWriter, r *http.Request) {
|
||||
if !manager.conf.Lobby {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
func roomWait(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`<script>
|
||||
(async function() {
|
||||
document.querySelector(".swal2-loader").style.display = 'block'
|
||||
document.querySelector(".swal2-loader").style.visibility = 'hidden'
|
||||
|
||||
roomName := chi.URLParam(r, "roomName")
|
||||
response, err := manager.rooms.FindByName(roomName)
|
||||
let lastAttempt = (new Date()).getTime()
|
||||
while(true) {
|
||||
try {
|
||||
lastAttempt = (new Date()).getTime()
|
||||
document.querySelector(".swal2-loader").style.visibility = 'visible'
|
||||
|
||||
if err != nil || response.Name != roomName {
|
||||
manager.roomNotFound(w, r)
|
||||
return
|
||||
}
|
||||
await fetch("?wait")
|
||||
location.href = location.href
|
||||
} catch {
|
||||
let now = (new Date()).getTime()
|
||||
let diff = now - lastAttempt
|
||||
|
||||
if !response.Running {
|
||||
manager.roomNotRunning(w, r)
|
||||
return
|
||||
}
|
||||
// if the gap between last attempt and now
|
||||
// is gt 20s, do reconnect immediatly
|
||||
if ((now - lastAttempt) > 20*1000) {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(response.Status, "starting") {
|
||||
manager.roomNotReady(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
manager.roomReady(w, r)
|
||||
// wait for 10 sec
|
||||
await new Promise(res => setTimeout(res, 2500))
|
||||
document.querySelector(".swal2-loader").style.visibility = 'hidden'
|
||||
await new Promise(res => setTimeout(res, 7500))
|
||||
}
|
||||
}
|
||||
}())
|
||||
</script>`))
|
||||
}
|
||||
|
||||
func (manager *ApiManagerCtx) roomNotFound(w http.ResponseWriter, r *http.Request) {
|
||||
func RoomNotFound(w http.ResponseWriter, r *http.Request, waitEnabled bool) {
|
||||
utils.Swal2Response(w, `
|
||||
<div class="swal2-header">
|
||||
<div class="swal2-icon swal2-error">
|
||||
@@ -45,11 +50,19 @@ func (manager *ApiManagerCtx) roomNotFound(w http.ResponseWriter, r *http.Reques
|
||||
</div>
|
||||
<div class="swal2-content">
|
||||
<div>The room you are trying to join does not exist.</div>
|
||||
<div>You can wait on this page until it will be created.</div>
|
||||
</div>
|
||||
<div class="swal2-actions">
|
||||
<div class="swal2-loader" style="display:none;"></div>
|
||||
</div>
|
||||
`)
|
||||
|
||||
if waitEnabled {
|
||||
roomWait(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *ApiManagerCtx) roomNotRunning(w http.ResponseWriter, r *http.Request) {
|
||||
func RoomNotRunning(w http.ResponseWriter, r *http.Request, waitEnabled bool) {
|
||||
utils.Swal2Response(w, `
|
||||
<div class="swal2-header">
|
||||
<div class="swal2-icon swal2-warning">
|
||||
@@ -59,11 +72,19 @@ func (manager *ApiManagerCtx) roomNotRunning(w http.ResponseWriter, r *http.Requ
|
||||
</div>
|
||||
<div class="swal2-content">
|
||||
<div>The room you are trying to join is not running.</div>
|
||||
<div>You can wait on this page until it will be started.</div>
|
||||
</div>
|
||||
<div class="swal2-actions">
|
||||
<div class="swal2-loader" style="display:none;"></div>
|
||||
</div>
|
||||
`)
|
||||
|
||||
if waitEnabled {
|
||||
roomWait(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *ApiManagerCtx) roomNotReady(w http.ResponseWriter, r *http.Request) {
|
||||
func RoomNotReady(w http.ResponseWriter, r *http.Request) {
|
||||
utils.Swal2Response(w, `
|
||||
<meta http-equiv="refresh" content="2">
|
||||
|
||||
@@ -83,7 +104,7 @@ func (manager *ApiManagerCtx) roomNotReady(w http.ResponseWriter, r *http.Reques
|
||||
`)
|
||||
}
|
||||
|
||||
func (manager *ApiManagerCtx) roomReady(w http.ResponseWriter, r *http.Request) {
|
||||
func RoomReady(w http.ResponseWriter, r *http.Request) {
|
||||
utils.Swal2Response(w, `
|
||||
<div class="swal2-header">
|
||||
<div class="swal2-icon swal2-success swal2-icon-show" style="display: flex;">
|
||||
@@ -0,0 +1,361 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type entry struct {
|
||||
id string
|
||||
running bool
|
||||
handler *httputil.ReverseProxy
|
||||
}
|
||||
|
||||
type wait struct {
|
||||
subs int
|
||||
signal chan struct{}
|
||||
}
|
||||
|
||||
type ProxyManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
mu sync.RWMutex
|
||||
ctx context.Context
|
||||
cancel func()
|
||||
|
||||
waitMu sync.RWMutex
|
||||
waitChans map[string]*wait
|
||||
waitEnabled bool
|
||||
|
||||
client *dockerClient.Client
|
||||
instanceName string
|
||||
handlers *prefixHandler[*entry]
|
||||
}
|
||||
|
||||
func New(client *dockerClient.Client, instanceName string, waitEnabled bool) *ProxyManagerCtx {
|
||||
return &ProxyManagerCtx{
|
||||
logger: log.With().Str("module", "proxy").Logger(),
|
||||
waitChans: map[string]*wait{},
|
||||
|
||||
client: client,
|
||||
instanceName: instanceName,
|
||||
waitEnabled: waitEnabled,
|
||||
handlers: &prefixHandler[*entry]{},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProxyManagerCtx) Start() {
|
||||
p.ctx, p.cancel = context.WithCancel(context.Background())
|
||||
|
||||
go func() {
|
||||
err := p.Refresh()
|
||||
if err != nil {
|
||||
p.logger.Err(err).Msg("unable to refresh containers")
|
||||
}
|
||||
|
||||
msgs, errs := p.client.Events(p.ctx, dockerTypes.EventsOptions{
|
||||
Filters: filters.NewArgs(
|
||||
filters.Arg("type", "container"),
|
||||
filters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", p.instanceName)),
|
||||
filters.Arg("event", "create"),
|
||||
filters.Arg("event", "start"),
|
||||
//filters.Arg("event", "health_status"),
|
||||
filters.Arg("event", "stop"),
|
||||
filters.Arg("event", "destroy"),
|
||||
),
|
||||
})
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-errs:
|
||||
p.logger.Err(err).Msg("got docker event")
|
||||
case msg := <-msgs:
|
||||
enabled, path, port, ok := p.parseLabels(msg.Actor.Attributes)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
host := msg.ID[:12] + ":" + port
|
||||
|
||||
p.logger.Info().
|
||||
Str("action", msg.Action).
|
||||
Str("path", path).
|
||||
Str("host", host).
|
||||
Msg("got docker event")
|
||||
|
||||
// terminate waiting for any events
|
||||
if p.waitEnabled {
|
||||
p.waitMu.Lock()
|
||||
ch, ok := p.waitChans[path]
|
||||
if ok {
|
||||
close(ch.signal)
|
||||
delete(p.waitChans, path)
|
||||
}
|
||||
p.waitMu.Unlock()
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
switch msg.Action {
|
||||
case "create":
|
||||
p.handlers.Set(path, &entry{
|
||||
id: msg.ID,
|
||||
running: false,
|
||||
})
|
||||
case "start":
|
||||
e := &entry{
|
||||
id: msg.ID,
|
||||
running: true,
|
||||
}
|
||||
|
||||
// if proxying is disabled
|
||||
if enabled {
|
||||
e.handler = httputil.NewSingleHostReverseProxy(&url.URL{
|
||||
Scheme: "http",
|
||||
Host: host,
|
||||
})
|
||||
}
|
||||
|
||||
p.handlers.Set(path, e)
|
||||
case "stop":
|
||||
p.handlers.Set(path, &entry{
|
||||
id: msg.ID,
|
||||
running: false,
|
||||
})
|
||||
case "destroy":
|
||||
p.handlers.Remove(path)
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *ProxyManagerCtx) Shutdown() error {
|
||||
p.cancel()
|
||||
return p.ctx.Err()
|
||||
}
|
||||
|
||||
func (p *ProxyManagerCtx) Refresh() error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
containers, err := p.client.ContainerList(p.ctx, dockerTypes.ContainerListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(
|
||||
filters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", p.instanceName)),
|
||||
),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.handlers = &prefixHandler[*entry]{}
|
||||
|
||||
for _, cont := range containers {
|
||||
enabled, path, port, ok := p.parseLabels(cont.Labels)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
host := cont.ID[:12] + ":" + port
|
||||
|
||||
entry := &entry{
|
||||
id: cont.ID,
|
||||
}
|
||||
|
||||
if cont.State == "running" {
|
||||
entry.running = true
|
||||
|
||||
// if proxying is disabled
|
||||
if enabled {
|
||||
entry.handler = httputil.NewSingleHostReverseProxy(&url.URL{
|
||||
Scheme: "http",
|
||||
Host: host,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
p.handlers.Set(path, entry)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProxyManagerCtx) parseLabels(labels map[string]string) (enabled bool, path string, port string, ok bool) {
|
||||
enabledStr, found := labels["m1k1o.neko_rooms.proxy.enabled"]
|
||||
if !found {
|
||||
//
|
||||
// workaround for legacy traefik labels
|
||||
//
|
||||
|
||||
// get room name
|
||||
var roomName string
|
||||
roomName, ok = labels["m1k1o.neko_rooms.name"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// get container name
|
||||
containerName := p.instanceName + "-" + roomName
|
||||
|
||||
// get path
|
||||
path, ok = labels["traefik.http.middlewares."+containerName+"-prf.stripprefix.prefixes"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// remove last /
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
|
||||
// get port
|
||||
port, ok = labels["traefik.http.services."+containerName+"-frontend.loadbalancer.server.port"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
//
|
||||
// workaround for legacy traefik labels
|
||||
//
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
enabledBool, err := strconv.ParseBool(enabledStr)
|
||||
enabled = enabledBool && err == nil
|
||||
|
||||
path, ok = labels["m1k1o.neko_rooms.proxy.path"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
port, ok = labels["m1k1o.neko_rooms.proxy.port"]
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ProxyManagerCtx) waitForPath(w http.ResponseWriter, r *http.Request, path string) {
|
||||
p.logger.Debug().Str("path", path).Msg("adding new wait handler")
|
||||
|
||||
p.waitMu.Lock()
|
||||
ch, ok := p.waitChans[path]
|
||||
if !ok {
|
||||
ch = &wait{
|
||||
subs: 1,
|
||||
signal: make(chan struct{}),
|
||||
}
|
||||
p.waitChans[path] = ch
|
||||
} else {
|
||||
p.waitChans[path].subs += 1
|
||||
}
|
||||
p.waitMu.Unlock()
|
||||
|
||||
select {
|
||||
case <-ch.signal:
|
||||
w.Write([]byte("ready"))
|
||||
case <-r.Context().Done():
|
||||
http.Error(w, r.Context().Err().Error(), http.StatusRequestTimeout)
|
||||
|
||||
p.waitMu.Lock()
|
||||
ch.subs -= 1
|
||||
if ch.subs <= 0 {
|
||||
delete(p.waitChans, path)
|
||||
}
|
||||
p.waitMu.Unlock()
|
||||
p.logger.Debug().Str("path", path).Msg("wait handler removed")
|
||||
case <-p.ctx.Done():
|
||||
w.Write([]byte("shutdown"))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProxyManagerCtx) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
cleanPath := path.Clean(r.URL.Path)
|
||||
|
||||
// get proxy by room name
|
||||
p.mu.RLock()
|
||||
proxy, prefix, ok := p.handlers.Match(cleanPath)
|
||||
p.mu.RUnlock()
|
||||
|
||||
// if room not found
|
||||
if !ok {
|
||||
// blocking until room is created
|
||||
if r.URL.Query().Has("wait") && p.waitEnabled {
|
||||
p.waitForPath(w, r, cleanPath)
|
||||
return
|
||||
}
|
||||
|
||||
RoomNotFound(w, r, p.waitEnabled)
|
||||
return
|
||||
}
|
||||
|
||||
// redirect to room ending with /
|
||||
if cleanPath == prefix && !strings.HasSuffix(r.URL.Path, "/") {
|
||||
r.URL.Path = cleanPath + "/"
|
||||
http.Redirect(w, r, r.URL.String(), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
// if room not running
|
||||
if !proxy.running {
|
||||
// blocking until room is running
|
||||
if r.URL.Query().Has("wait") && p.waitEnabled {
|
||||
p.waitForPath(w, r, cleanPath)
|
||||
return
|
||||
}
|
||||
|
||||
RoomNotRunning(w, r, p.waitEnabled)
|
||||
return
|
||||
}
|
||||
|
||||
// if not proxying, check for container readiness status
|
||||
if proxy.handler == nil {
|
||||
containers, err := p.client.ContainerList(p.ctx, dockerTypes.ContainerListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(
|
||||
filters.Arg("label", fmt.Sprintf("m1k1o.neko_rooms.instance=%s", p.instanceName)),
|
||||
filters.Arg("id", proxy.id),
|
||||
),
|
||||
})
|
||||
|
||||
if err != nil || len(containers) != 1 {
|
||||
p.logger.Err(err).Msg("error while getting container ready status")
|
||||
RoomNotReady(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
container := containers[0]
|
||||
if strings.Contains(container.Status, "starting") {
|
||||
RoomNotReady(w, r)
|
||||
} else {
|
||||
RoomReady(w, r)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// handle not ready room
|
||||
proxy.handler.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
if r.URL.Path == "/" {
|
||||
RoomNotReady(w, r)
|
||||
}
|
||||
|
||||
p.logger.Err(err).Str("prefix", prefix).Msg("proxying error")
|
||||
}
|
||||
|
||||
// strip prefix from proxy
|
||||
handler := http.StripPrefix(prefix, proxy.handler)
|
||||
|
||||
// handle by proxy
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type prefixHandler[T any] struct {
|
||||
Value T
|
||||
IsLeaf bool
|
||||
Children map[string]*prefixHandler[T]
|
||||
}
|
||||
|
||||
func NewPrefixHandler[T any]() *prefixHandler[T] {
|
||||
return &prefixHandler[T]{}
|
||||
}
|
||||
|
||||
func (p *prefixHandler[T]) Set(prefix string, value T) {
|
||||
arr := strings.Split(prefix, "/")
|
||||
len := len(arr)
|
||||
|
||||
for i, a := range arr {
|
||||
if a == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if p.Children == nil {
|
||||
p.Children = map[string]*prefixHandler[T]{}
|
||||
}
|
||||
|
||||
dat, ok := p.Children[a]
|
||||
if !ok {
|
||||
dat = &prefixHandler[T]{}
|
||||
}
|
||||
|
||||
if i == len-1 {
|
||||
dat.Value = value
|
||||
dat.IsLeaf = true
|
||||
dat.Children = nil
|
||||
}
|
||||
|
||||
p.Children[a] = dat
|
||||
p = dat
|
||||
}
|
||||
}
|
||||
|
||||
func (p *prefixHandler[T]) Get(prefix string) (value T, ok bool) {
|
||||
arr := strings.Split(prefix, "/")
|
||||
len := len(arr)
|
||||
|
||||
for i, a := range arr {
|
||||
if a == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if p.Children == nil {
|
||||
p.Children = map[string]*prefixHandler[T]{}
|
||||
}
|
||||
|
||||
dat, found := p.Children[a]
|
||||
if !found {
|
||||
dat = &prefixHandler[T]{}
|
||||
}
|
||||
|
||||
if i == len-1 {
|
||||
value = dat.Value
|
||||
ok = dat.IsLeaf
|
||||
return
|
||||
}
|
||||
|
||||
p.Children[a] = dat
|
||||
p = dat
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *prefixHandler[T]) Remove(prefix string) {
|
||||
arr := strings.Split(prefix, "/")
|
||||
len := len(arr)
|
||||
|
||||
for i, a := range arr {
|
||||
if a == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if i == len-1 {
|
||||
delete(p.Children, a)
|
||||
}
|
||||
|
||||
dat, found := p.Children[a]
|
||||
if !found {
|
||||
break
|
||||
}
|
||||
p = dat
|
||||
}
|
||||
|
||||
// TODO: Remove all empty references.
|
||||
}
|
||||
|
||||
func (p *prefixHandler[T]) Match(path string) (value T, prefix string, ok bool) {
|
||||
arr := strings.Split(path, "/")
|
||||
prefixArr := []string{}
|
||||
|
||||
for _, a := range arr {
|
||||
if a == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
pointer, found := p.Children[a]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
p = pointer
|
||||
prefixArr = append(prefixArr, a)
|
||||
|
||||
if p.IsLeaf {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
value = p.Value
|
||||
prefix = "/" + strings.Join(prefixArr, "/")
|
||||
ok = p.IsLeaf
|
||||
return
|
||||
}
|
||||
@@ -18,7 +18,7 @@ func (manager *RoomManagerCtx) containerToEntry(container dockerTypes.Container)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.RoomEntry{
|
||||
entry := &types.RoomEntry{
|
||||
ID: container.ID,
|
||||
URL: labels.URL,
|
||||
Name: labels.Name,
|
||||
@@ -28,7 +28,13 @@ func (manager *RoomManagerCtx) containerToEntry(container dockerTypes.Container)
|
||||
Running: container.State == "running",
|
||||
Status: container.Status,
|
||||
Created: time.Unix(container.Created, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if labels.Mux {
|
||||
entry.MaxConnections = 0
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) listContainers() ([]dockerTypes.Container, error) {
|
||||
|
||||
+53
-23
@@ -10,6 +10,7 @@ import (
|
||||
type RoomLabels struct {
|
||||
Name string
|
||||
URL string
|
||||
Mux bool
|
||||
Epr EprPorts
|
||||
NekoImage string
|
||||
|
||||
@@ -29,7 +30,9 @@ func (manager *RoomManagerCtx) extractLabels(labels map[string]string) (*RoomLab
|
||||
|
||||
url, ok := labels["m1k1o.neko_rooms.url"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("damaged container labels: url not found")
|
||||
// TODO: It should be always available.
|
||||
url = manager.config.GetRoomUrl(name)
|
||||
//return nil, fmt.Errorf("damaged container labels: url not found")
|
||||
}
|
||||
|
||||
nekoImage, ok := labels["m1k1o.neko_rooms.neko_image"]
|
||||
@@ -37,24 +40,47 @@ func (manager *RoomManagerCtx) extractLabels(labels map[string]string) (*RoomLab
|
||||
return nil, fmt.Errorf("damaged container labels: neko_image not found")
|
||||
}
|
||||
|
||||
eprMinStr, ok := labels["m1k1o.neko_rooms.epr.min"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("damaged container labels: epr.min not found")
|
||||
}
|
||||
var mux bool
|
||||
var epr EprPorts
|
||||
|
||||
eprMin, err := strconv.ParseUint(eprMinStr, 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
muxStr, ok := labels["m1k1o.neko_rooms.mux"]
|
||||
if ok {
|
||||
muxPort, err := strconv.ParseUint(muxStr, 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eprMaxStr, ok := labels["m1k1o.neko_rooms.epr.max"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("damaged container labels: epr.max not found")
|
||||
}
|
||||
mux = true
|
||||
epr = EprPorts{
|
||||
Min: uint16(muxPort),
|
||||
Max: uint16(muxPort),
|
||||
}
|
||||
} else {
|
||||
eprMinStr, ok := labels["m1k1o.neko_rooms.epr.min"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("damaged container labels: epr.min not found")
|
||||
}
|
||||
|
||||
eprMax, err := strconv.ParseUint(eprMaxStr, 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
eprMin, err := strconv.ParseUint(eprMinStr, 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eprMaxStr, ok := labels["m1k1o.neko_rooms.epr.max"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("damaged container labels: epr.max not found")
|
||||
}
|
||||
|
||||
eprMax, err := strconv.ParseUint(eprMaxStr, 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mux = false
|
||||
epr = EprPorts{
|
||||
Min: uint16(eprMin),
|
||||
Max: uint16(eprMax),
|
||||
}
|
||||
}
|
||||
|
||||
var browserPolicy *BrowserPolicyLabels
|
||||
@@ -79,10 +105,9 @@ func (manager *RoomManagerCtx) extractLabels(labels map[string]string) (*RoomLab
|
||||
Name: name,
|
||||
URL: url,
|
||||
NekoImage: nekoImage,
|
||||
Epr: EprPorts{
|
||||
Min: uint16(eprMin),
|
||||
Max: uint16(eprMax),
|
||||
},
|
||||
Mux: mux,
|
||||
Epr: epr,
|
||||
|
||||
BrowserPolicy: browserPolicy,
|
||||
}, nil
|
||||
}
|
||||
@@ -90,13 +115,18 @@ func (manager *RoomManagerCtx) extractLabels(labels map[string]string) (*RoomLab
|
||||
func (manager *RoomManagerCtx) serializeLabels(labels RoomLabels) map[string]string {
|
||||
labelsMap := map[string]string{
|
||||
"m1k1o.neko_rooms.name": labels.Name,
|
||||
"m1k1o.neko_rooms.url": labels.URL,
|
||||
"m1k1o.neko_rooms.url": manager.config.GetRoomUrl(labels.Name),
|
||||
"m1k1o.neko_rooms.instance": manager.config.InstanceName,
|
||||
"m1k1o.neko_rooms.epr.min": fmt.Sprintf("%d", labels.Epr.Min),
|
||||
"m1k1o.neko_rooms.epr.max": fmt.Sprintf("%d", labels.Epr.Max),
|
||||
"m1k1o.neko_rooms.neko_image": labels.NekoImage,
|
||||
}
|
||||
|
||||
if labels.Mux && labels.Epr.Min == labels.Epr.Max {
|
||||
labelsMap["m1k1o.neko_rooms.mux"] = fmt.Sprintf("%d", labels.Epr.Min)
|
||||
} else {
|
||||
labelsMap["m1k1o.neko_rooms.epr.min"] = fmt.Sprintf("%d", labels.Epr.Min)
|
||||
labelsMap["m1k1o.neko_rooms.epr.max"] = fmt.Sprintf("%d", labels.Epr.Max)
|
||||
}
|
||||
|
||||
if labels.BrowserPolicy != nil {
|
||||
labelsMap["m1k1o.neko_rooms.browser_policy"] = "true"
|
||||
labelsMap["m1k1o.neko_rooms.browser_policy.type"] = string(labels.BrowserPolicy.Type)
|
||||
|
||||
+146
-56
@@ -8,8 +8,10 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
containerTypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
dockerMount "github.com/docker/docker/api/types/mount"
|
||||
network "github.com/docker/docker/api/types/network"
|
||||
@@ -55,6 +57,7 @@ func (manager *RoomManagerCtx) Config() types.RoomsConfig {
|
||||
Connections: manager.config.EprMax - manager.config.EprMin + 1,
|
||||
NekoImages: manager.config.NekoImages,
|
||||
StorageEnabled: manager.config.StorageEnabled,
|
||||
UsesMux: manager.config.Mux,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +89,21 @@ func (manager *RoomManagerCtx) FindByName(name string) (*types.RoomEntry, error)
|
||||
return manager.containerToEntry(*container)
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) IsValidImage(imageName string, imageList []string) bool {
|
||||
for _, _nekoImage := range imageList {
|
||||
if _nekoImage == imageName {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(_nekoImage, "*") {
|
||||
pattern := strings.Replace(_nekoImage, "*", "", -1)
|
||||
if strings.Contains(imageName, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) Create(settings types.RoomSettings) (string, error) {
|
||||
if settings.Name != "" && !dockerNames.RestrictedNamePattern.MatchString(settings.Name) {
|
||||
return "", fmt.Errorf("invalid container name, must match %s", dockerNames.RestrictedNameChars)
|
||||
@@ -95,11 +113,11 @@ func (manager *RoomManagerCtx) Create(settings types.RoomSettings) (string, erro
|
||||
return "", fmt.Errorf("mounts cannot be specified, because storage is disabled or unavailable")
|
||||
}
|
||||
|
||||
if in, _ := utils.ArrayIn(settings.NekoImage, manager.config.NekoImages); !in {
|
||||
return "", fmt.Errorf("invalid neko image")
|
||||
if !manager.IsValidImage(settings.NekoImage, manager.config.NekoImages) {
|
||||
return "", fmt.Errorf("invalid neko image: %q. valid are: %q", settings.NekoImage, manager.config.NekoImages)
|
||||
}
|
||||
|
||||
isPrivilegedImage, _ := utils.ArrayIn(settings.NekoImage, manager.config.NekoPrivilegedImages)
|
||||
isPrivilegedImage := manager.IsValidImage(settings.NekoImage, manager.config.NekoPrivilegedImages)
|
||||
|
||||
// TODO: Check if path name exists.
|
||||
roomName := settings.Name
|
||||
@@ -111,73 +129,48 @@ func (manager *RoomManagerCtx) Create(settings types.RoomSettings) (string, erro
|
||||
}
|
||||
}
|
||||
|
||||
containerName := manager.config.InstanceName + "-" + roomName
|
||||
|
||||
//
|
||||
// Allocate ports
|
||||
//
|
||||
|
||||
epr, err := manager.allocatePorts(settings.MaxConnections)
|
||||
portsNeeded := settings.MaxConnections
|
||||
if manager.config.Mux {
|
||||
portsNeeded = 1
|
||||
}
|
||||
|
||||
epr, err := manager.allocatePorts(portsNeeded)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
portBindings := nat.PortMap{}
|
||||
exposedPorts := nat.PortSet{
|
||||
nat.Port(fmt.Sprintf("%d/tcp", frontendPort)): struct{}{},
|
||||
}
|
||||
|
||||
for port := epr.Min; port <= epr.Max; port++ {
|
||||
portKey := nat.Port(fmt.Sprintf("%d/udp", port))
|
||||
|
||||
portBindings[portKey] = []nat.PortBinding{
|
||||
portBindings[nat.Port(fmt.Sprintf("%d/udp", port))] = []nat.PortBinding{
|
||||
{
|
||||
HostIP: "0.0.0.0",
|
||||
HostPort: fmt.Sprintf("%d", port),
|
||||
},
|
||||
}
|
||||
|
||||
exposedPorts[portKey] = struct{}{}
|
||||
}
|
||||
|
||||
//
|
||||
// Set traefik labels
|
||||
//
|
||||
|
||||
containerName := manager.config.InstanceName + "-" + roomName
|
||||
pathPrefix := path.Join("/", manager.config.PathPrefix, roomName)
|
||||
|
||||
// create traefik rule
|
||||
traefikRule := "PathPrefix(`" + pathPrefix + "`)"
|
||||
if manager.config.TraefikDomain != "" && manager.config.TraefikDomain != "*" {
|
||||
// match *.domain.tld as subdomain
|
||||
if strings.HasPrefix(manager.config.TraefikDomain, "*.") {
|
||||
traefikRule = fmt.Sprintf(
|
||||
"Host(`%s.%s`)",
|
||||
roomName,
|
||||
strings.TrimPrefix(manager.config.TraefikDomain, "*."),
|
||||
)
|
||||
} else {
|
||||
traefikRule += " && Host(`" + manager.config.TraefikDomain + "`)"
|
||||
// expose TCP port as well when using mux
|
||||
if manager.config.Mux {
|
||||
portBindings[nat.Port(fmt.Sprintf("%d/tcp", port))] = []nat.PortBinding{
|
||||
{
|
||||
HostIP: "0.0.0.0",
|
||||
HostPort: fmt.Sprintf("%d", port),
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
traefikRule += " && HostRegexp(`{host:.+}`)"
|
||||
}
|
||||
|
||||
traefikLabels := map[string]string{
|
||||
"traefik.enable": "true",
|
||||
"traefik.http.services." + containerName + "-frontend.loadbalancer.server.port": fmt.Sprintf("%d", frontendPort),
|
||||
"traefik.http.routers." + containerName + ".entrypoints": manager.config.TraefikEntrypoint,
|
||||
"traefik.http.routers." + containerName + ".rule": traefikRule,
|
||||
"traefik.http.middlewares." + containerName + "-rdr.redirectregex.regex": pathPrefix + "$$",
|
||||
"traefik.http.middlewares." + containerName + "-rdr.redirectregex.replacement": pathPrefix + "/",
|
||||
"traefik.http.middlewares." + containerName + "-prf.stripprefix.prefixes": pathPrefix + "/",
|
||||
"traefik.http.routers." + containerName + ".middlewares": containerName + "-rdr," + containerName + "-prf",
|
||||
"traefik.http.routers." + containerName + ".service": containerName + "-frontend",
|
||||
exposedPorts := nat.PortSet{
|
||||
nat.Port(fmt.Sprintf("%d/tcp", frontendPort)): struct{}{},
|
||||
}
|
||||
|
||||
// optional HTTPS
|
||||
if manager.config.TraefikCertresolver != "" {
|
||||
traefikLabels["traefik.http.routers."+containerName+".tls"] = "true"
|
||||
traefikLabels["traefik.http.routers."+containerName+".tls.certresolver"] = manager.config.TraefikCertresolver
|
||||
for port := range portBindings {
|
||||
exposedPorts[port] = struct{}{}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -194,15 +187,56 @@ func (manager *RoomManagerCtx) Create(settings types.RoomSettings) (string, erro
|
||||
|
||||
labels := manager.serializeLabels(RoomLabels{
|
||||
Name: roomName,
|
||||
URL: manager.config.GetRoomUrl(roomName),
|
||||
Mux: manager.config.Mux,
|
||||
Epr: epr,
|
||||
NekoImage: settings.NekoImage,
|
||||
|
||||
BrowserPolicy: browserPolicyLabels,
|
||||
})
|
||||
|
||||
for k, v := range traefikLabels {
|
||||
labels[k] = v
|
||||
//
|
||||
// Set traefik labels
|
||||
//
|
||||
|
||||
pathPrefix := path.Join("/", manager.config.PathPrefix, roomName)
|
||||
|
||||
if t := manager.config.Traefik; t.Enabled {
|
||||
// create traefik rule
|
||||
traefikRule := "PathPrefix(`" + pathPrefix + "`)"
|
||||
if t.Domain != "" && t.Domain != "*" {
|
||||
// match *.domain.tld as subdomain
|
||||
if strings.HasPrefix(t.Domain, "*.") {
|
||||
traefikRule = fmt.Sprintf(
|
||||
"Host(`%s.%s`)",
|
||||
roomName,
|
||||
strings.TrimPrefix(t.Domain, "*."),
|
||||
)
|
||||
} else {
|
||||
traefikRule += " && Host(`" + t.Domain + "`)"
|
||||
}
|
||||
} else {
|
||||
traefikRule += " && HostRegexp(`{host:.+}`)"
|
||||
}
|
||||
|
||||
labels["traefik.enable"] = "true"
|
||||
labels["traefik.http.services."+containerName+"-frontend.loadbalancer.server.port"] = fmt.Sprintf("%d", frontendPort)
|
||||
labels["traefik.http.routers."+containerName+".entrypoints"] = t.Entrypoint
|
||||
labels["traefik.http.routers."+containerName+".rule"] = traefikRule
|
||||
labels["traefik.http.middlewares."+containerName+"-rdr.redirectregex.regex"] = pathPrefix + "$$"
|
||||
labels["traefik.http.middlewares."+containerName+"-rdr.redirectregex.replacement"] = pathPrefix + "/"
|
||||
labels["traefik.http.middlewares."+containerName+"-prf.stripprefix.prefixes"] = pathPrefix + "/"
|
||||
labels["traefik.http.routers."+containerName+".middlewares"] = containerName + "-rdr," + containerName + "-prf"
|
||||
labels["traefik.http.routers."+containerName+".service"] = containerName + "-frontend"
|
||||
|
||||
// optional HTTPS
|
||||
if t.Certresolver != "" {
|
||||
labels["traefik.http.routers."+containerName+".tls"] = "true"
|
||||
labels["traefik.http.routers."+containerName+".tls.certresolver"] = t.Certresolver
|
||||
}
|
||||
} else {
|
||||
labels["m1k1o.neko_rooms.proxy.enabled"] = "true"
|
||||
labels["m1k1o.neko_rooms.proxy.path"] = pathPrefix
|
||||
labels["m1k1o.neko_rooms.proxy.port"] = fmt.Sprintf("%d", frontendPort)
|
||||
}
|
||||
|
||||
// add custom labels
|
||||
@@ -210,8 +244,11 @@ func (manager *RoomManagerCtx) Create(settings types.RoomSettings) (string, erro
|
||||
// replace dynamic values in labels
|
||||
label = strings.Replace(label, "{containerName}", containerName, -1)
|
||||
label = strings.Replace(label, "{roomName}", roomName, -1)
|
||||
label = strings.Replace(label, "{traefikEntrypoint}", manager.config.TraefikEntrypoint, -1)
|
||||
label = strings.Replace(label, "{traefikCertresolver}", manager.config.TraefikCertresolver, -1)
|
||||
|
||||
if t := manager.config.Traefik; t.Enabled {
|
||||
label = strings.Replace(label, "{traefikEntrypoint}", t.Entrypoint, -1)
|
||||
label = strings.Replace(label, "{traefikCertresolver}", t.Certresolver, -1)
|
||||
}
|
||||
|
||||
v := strings.SplitN(label, "=", 2)
|
||||
if len(v) != 2 {
|
||||
@@ -229,10 +266,20 @@ func (manager *RoomManagerCtx) Create(settings types.RoomSettings) (string, erro
|
||||
|
||||
env := []string{
|
||||
fmt.Sprintf("NEKO_BIND=:%d", frontendPort),
|
||||
fmt.Sprintf("NEKO_EPR=%d-%d", epr.Min, epr.Max),
|
||||
"NEKO_ICELITE=true",
|
||||
}
|
||||
|
||||
if manager.config.Mux {
|
||||
env = append(env,
|
||||
fmt.Sprintf("NEKO_UDPMUX=%d", epr.Min),
|
||||
fmt.Sprintf("NEKO_TCPMUX=%d", epr.Min),
|
||||
)
|
||||
} else {
|
||||
env = append(env,
|
||||
fmt.Sprintf("NEKO_EPR=%d-%d", epr.Min, epr.Max),
|
||||
)
|
||||
}
|
||||
|
||||
// optional nat mapping
|
||||
if len(manager.config.NAT1To1IPs) > 0 {
|
||||
env = append(env, fmt.Sprintf("NEKO_NAT1TO1=%s", strings.Join(manager.config.NAT1To1IPs, ",")))
|
||||
@@ -410,7 +457,7 @@ func (manager *RoomManagerCtx) Create(settings types.RoomSettings) (string, erro
|
||||
|
||||
networkingConfig := &network.NetworkingConfig{
|
||||
EndpointsConfig: map[string]*network.EndpointSettings{
|
||||
manager.config.TraefikNetwork: {},
|
||||
manager.config.InstanceNetwork: {},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -543,6 +590,10 @@ func (manager *RoomManagerCtx) GetSettings(id string) (*types.RoomSettings, erro
|
||||
Resources: roomResources,
|
||||
}
|
||||
|
||||
if labels.Mux {
|
||||
settings.MaxConnections = 0
|
||||
}
|
||||
|
||||
err = settings.FromEnv(container.Config.Env)
|
||||
return &settings, err
|
||||
}
|
||||
@@ -603,3 +654,42 @@ func (manager *RoomManagerCtx) Restart(id string) error {
|
||||
// Restart the actual container
|
||||
return manager.client.ContainerRestart(context.Background(), id, nil)
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) Snapshot(id string) (*types.RoomSnapshot, error) {
|
||||
container, err := manager.inspectContainer(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imageListOps := dockerTypes.ImageListOptions{
|
||||
All: true,
|
||||
}
|
||||
images, _ := manager.client.ImageList(context.Background(), imageListOps)
|
||||
var containerImage string
|
||||
for _, image := range images {
|
||||
if image.ID == container.Image {
|
||||
containerImage = image.RepoTags[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
conatinerImage := strings.Split(containerImage, ":")[0]
|
||||
tag := conatinerImage + ":" + time.Now().Format("20060102150405")
|
||||
ops := dockerTypes.ContainerCommitOptions{
|
||||
Pause: true,
|
||||
Config: &containerTypes.Config{
|
||||
Image: tag,
|
||||
},
|
||||
}
|
||||
|
||||
// Restart the actual container
|
||||
_, err = manager.client.ContainerCommit(context.Background(), container.ID, ops)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.RoomSnapshot{
|
||||
RoomId: id,
|
||||
Name: tag,
|
||||
}, nil
|
||||
}
|
||||
+159
-21
@@ -2,9 +2,12 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
@@ -24,7 +27,7 @@ type ServerManagerCtx struct {
|
||||
config *config.Server
|
||||
}
|
||||
|
||||
func New(ApiManager types.ApiManager, pathPrefix string, config *config.Server) *ServerManagerCtx {
|
||||
func New(ApiManager types.ApiManager, roomConfig *config.Room, config *config.Server, proxyHandler http.Handler) *ServerManagerCtx {
|
||||
logger := log.With().Str("module", "server").Logger()
|
||||
|
||||
router := chi.NewRouter()
|
||||
@@ -49,32 +52,167 @@ func New(ApiManager types.ApiManager, pathPrefix string, config *config.Server)
|
||||
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||
}))
|
||||
|
||||
router.Route("/api", ApiManager.Mount)
|
||||
|
||||
// serve static files
|
||||
if config.Static != "" {
|
||||
fs := http.FileServer(http.Dir(config.Static))
|
||||
router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := os.Stat(config.Static + r.URL.Path); !os.IsNotExist(err) {
|
||||
fs.ServeHTTP(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// add simple lobby room
|
||||
router.Get(path.Join("/", pathPrefix, "{roomName}"), ApiManager.RoomLobby)
|
||||
router.Get(path.Join("/", pathPrefix, "{roomName}")+"/", ApiManager.RoomLobby)
|
||||
|
||||
// mount pprof endpoint
|
||||
if config.PProf {
|
||||
withPProf(router)
|
||||
logger.Info().Msgf("with pprof endpoint at %s", pprofPath)
|
||||
}
|
||||
|
||||
// we could use custom 404
|
||||
router.NotFound(http.NotFound)
|
||||
//
|
||||
// admin page
|
||||
//
|
||||
|
||||
protected := func(next http.Handler) http.Handler {
|
||||
// if proxy auth is enabled
|
||||
if config.Admin.ProxyAuth != "" {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := http.NewRequest("GET", config.Admin.ProxyAuth, nil)
|
||||
if err != nil {
|
||||
logger.Err(err).Msg("proxy auth request `http.NewRequest` err")
|
||||
http.Error(w, "proxy auth failed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
//
|
||||
// copy headers
|
||||
//
|
||||
|
||||
for k, vv := range r.Header {
|
||||
for _, v := range vv {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// add x-forwarded headers
|
||||
//
|
||||
|
||||
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||
req.Header.Add("X-Forwarded-For", clientIP)
|
||||
}
|
||||
|
||||
if r.Method != "" {
|
||||
req.Header.Add("X-Forwarded-Method", r.Method)
|
||||
} else {
|
||||
req.Header.Del("X-Forwarded-Method")
|
||||
}
|
||||
|
||||
if r.TLS != nil {
|
||||
req.Header.Add("X-Forwarded-Proto", "https")
|
||||
} else {
|
||||
req.Header.Add("X-Forwarded-Proto", "http")
|
||||
}
|
||||
|
||||
if r.Host != "" {
|
||||
req.Header.Add("X-Forwarded-Host", r.Host)
|
||||
} else {
|
||||
req.Header.Del("X-Forwarded-Host")
|
||||
}
|
||||
|
||||
if r.URL.RequestURI() != "" {
|
||||
req.Header.Add("X-Forwarded-Uri", r.URL.RequestURI())
|
||||
} else {
|
||||
req.Header.Del("X-Forwarded-Uri")
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Err(err).Msg("proxy auth request `client.Do` err")
|
||||
http.Error(w, "proxy auth failed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
// read whole body
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
logger.Err(err).Msg("proxy auth request `ioutil.ReadAll` err")
|
||||
http.Error(w, "proxy auth failed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||
// copy headers
|
||||
for k, vv := range res.Header {
|
||||
for _, v := range vv {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// copy status & body
|
||||
w.WriteHeader(res.StatusCode)
|
||||
w.Write(body)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// if basic auth is enabled
|
||||
if config.Admin.Username != "" && config.Admin.Password != "" {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok || user != config.Admin.Username || pass != config.Admin.Password {
|
||||
w.Header().Add("WWW-Authenticate", `Basic realm="neko-rooms admin"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
// cache static file paths
|
||||
staticFiles := map[string]struct{}{}
|
||||
if config.Admin.Static != "" {
|
||||
filepath.Walk(config.Admin.Static,
|
||||
func(p string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
staticFiles[path.Clean(p)] = struct{}{}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// serve static files
|
||||
router.Use(func(next http.Handler) http.Handler {
|
||||
// if static files are disabled
|
||||
if config.Admin.Static == "" {
|
||||
return next
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
filePath := path.Join(config.Admin.Static, r.URL.Path)
|
||||
|
||||
// check if file exists to serve it
|
||||
if _, ok := staticFiles[filePath]; ok {
|
||||
// serve protected assets
|
||||
protected(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, filePath)
|
||||
})).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
|
||||
// serve protected API
|
||||
router.With(protected).Route("/api", ApiManager.Mount)
|
||||
|
||||
// handle all remaining paths with proxy
|
||||
router.Handle("/*", proxyHandler)
|
||||
|
||||
return &ServerManagerCtx{
|
||||
logger: logger,
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
type ApiManager interface {
|
||||
Mount(r chi.Router)
|
||||
|
||||
RoomLobby(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package types
|
||||
|
||||
import "net/http"
|
||||
|
||||
type ProxyManager interface {
|
||||
ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||
Shutdown() error
|
||||
}
|
||||
+11
-2
@@ -19,6 +19,8 @@ var blacklistedEnvs = []string{
|
||||
// ignore bunch of envs managed by neko-rooms
|
||||
"NEKO_BIND",
|
||||
"NEKO_EPR",
|
||||
"NEKO_UDPMUX",
|
||||
"NEKO_TCPMUX",
|
||||
"NEKO_NAT1TO1",
|
||||
"NEKO_ICELITE",
|
||||
}
|
||||
@@ -27,6 +29,7 @@ type RoomsConfig struct {
|
||||
Connections uint16 `json:"connections"`
|
||||
NekoImages []string `json:"neko_images"`
|
||||
StorageEnabled bool `json:"storage_enabled"`
|
||||
UsesMux bool `json:"uses_mux"`
|
||||
}
|
||||
|
||||
type RoomEntry struct {
|
||||
@@ -35,7 +38,7 @@ type RoomEntry struct {
|
||||
Name string `json:"name"`
|
||||
NekoImage string `json:"neko_image"`
|
||||
IsOutdated bool `json:"is_outdated"`
|
||||
MaxConnections uint16 `json:"max_connections"`
|
||||
MaxConnections uint16 `json:"max_connections"` // 0 when using mux
|
||||
Running bool `json:"running"`
|
||||
Status string `json:"status"`
|
||||
Created time.Time `json:"created"`
|
||||
@@ -66,7 +69,7 @@ type RoomResources struct {
|
||||
type RoomSettings struct {
|
||||
Name string `json:"name"`
|
||||
NekoImage string `json:"neko_image"`
|
||||
MaxConnections uint16 `json:"max_connections"`
|
||||
MaxConnections uint16 `json:"max_connections"` // 0 when using mux
|
||||
|
||||
ControlProtection bool `json:"control_protection"`
|
||||
ImplicitControl bool `json:"implicit_control"`
|
||||
@@ -93,6 +96,11 @@ type RoomSettings struct {
|
||||
BrowserPolicy *BrowserPolicy `json:"browser_policy,omitempty"`
|
||||
}
|
||||
|
||||
type RoomSnapshot struct {
|
||||
RoomId string `json:"roomId"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (settings *RoomSettings) ToEnv() []string {
|
||||
env := []string{
|
||||
fmt.Sprintf("NEKO_PASSWORD=%s", settings.UserPass),
|
||||
@@ -260,4 +268,5 @@ type RoomManager interface {
|
||||
Start(id string) error
|
||||
Stop(id string) error
|
||||
Restart(id string) error
|
||||
Snapshot(id string) (*RoomSnapshot, error)
|
||||
}
|
||||
|
||||
+6
-16
@@ -1,22 +1,12 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
func ArrayIn[T comparable](val T, array []T) (exists bool, index int) {
|
||||
exists, index = false, -1
|
||||
|
||||
func ArrayIn(val interface{}, array interface{}) (exists bool, index int) {
|
||||
exists = false
|
||||
index = -1
|
||||
|
||||
switch reflect.TypeOf(array).Kind() {
|
||||
case reflect.Slice:
|
||||
s := reflect.ValueOf(array)
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
if reflect.DeepEqual(val, s.Index(i).Interface()) {
|
||||
index = i
|
||||
exists = true
|
||||
return
|
||||
}
|
||||
for i, a := range array {
|
||||
if a == val {
|
||||
exists, index = true, i
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/m1k1o/neko-rooms/internal/api"
|
||||
"github.com/m1k1o/neko-rooms/internal/config"
|
||||
"github.com/m1k1o/neko-rooms/internal/proxy"
|
||||
"github.com/m1k1o/neko-rooms/internal/pull"
|
||||
"github.com/m1k1o/neko-rooms/internal/room"
|
||||
"github.com/m1k1o/neko-rooms/internal/server"
|
||||
@@ -62,7 +63,6 @@ func init() {
|
||||
Configs: &Configs{
|
||||
Root: &config.Root{},
|
||||
Server: &config.Server{},
|
||||
API: &config.API{},
|
||||
Room: &config.Room{},
|
||||
},
|
||||
}
|
||||
@@ -100,7 +100,6 @@ func (i *Version) Details() string {
|
||||
type Configs struct {
|
||||
Root *config.Root
|
||||
Server *config.Server
|
||||
API *config.API
|
||||
Room *config.Room
|
||||
}
|
||||
|
||||
@@ -112,6 +111,7 @@ type MainCtx struct {
|
||||
roomManager *room.RoomManagerCtx
|
||||
pullManager *pull.PullManagerCtx
|
||||
apiManager *api.ApiManagerCtx
|
||||
proxyManager *proxy.ProxyManagerCtx
|
||||
serverManager *server.ServerManagerCtx
|
||||
}
|
||||
|
||||
@@ -140,20 +140,32 @@ func (main *MainCtx) Start() {
|
||||
main.apiManager = api.New(
|
||||
main.roomManager,
|
||||
main.pullManager,
|
||||
main.Configs.API,
|
||||
)
|
||||
|
||||
main.proxyManager = proxy.New(
|
||||
client,
|
||||
main.Configs.Room.InstanceName,
|
||||
main.Configs.Room.WaitEnabled,
|
||||
)
|
||||
main.proxyManager.Start()
|
||||
|
||||
main.serverManager = server.New(
|
||||
main.apiManager,
|
||||
main.Configs.Room.PathPrefix,
|
||||
main.Configs.Room,
|
||||
main.Configs.Server,
|
||||
main.proxyManager,
|
||||
)
|
||||
main.serverManager.Start()
|
||||
}
|
||||
|
||||
func (main *MainCtx) Shutdown() {
|
||||
err := main.serverManager.Shutdown()
|
||||
var err error
|
||||
|
||||
err = main.serverManager.Shutdown()
|
||||
main.logger.Err(err).Msg("server manager shutdown")
|
||||
|
||||
err = main.proxyManager.Shutdown()
|
||||
main.logger.Err(err).Msg("proxy manager shutdown")
|
||||
}
|
||||
|
||||
func (main *MainCtx) ServeCommand(cmd *cobra.Command, args []string) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
NEKO_ROOMS_EPR=59000-59049
|
||||
NEKO_ROOMS_NAT1TO1=192.168.1.20
|
||||
|
||||
NEKO_ROOMS_TRAEFIK_DOMAIN=neko-rooms.server.lan
|
||||
NEKO_ROOMS_TRAEFIK_ENTRYPOINT=websecure
|
||||
NEKO_ROOMS_TRAEFIK_NETWORK=neko-rooms-traefik
|
||||
NEKO_ROOMS_TRAEFIK_CERTRESOLVER=lets-encrypt
|
||||
|
||||
TZ=Europe/Vienna
|
||||
@@ -0,0 +1,67 @@
|
||||
# Installation guide for traefik as reverse proxy
|
||||
|
||||
## Zero-knowledge installation
|
||||
|
||||
If you don't have any clue about docker and stuff but only want to have fun with friends in a shared browser, we got you covered!
|
||||
|
||||
- Rent a VPS with public IP and OS Ubuntu.
|
||||
- Get a domain name pointing to your IP (you can even get some for free).
|
||||
- Run install script and follow instructions.
|
||||
|
||||
```bash
|
||||
wget -O neko-rooms.sh https://raw.githubusercontent.com/m1k1o/neko-rooms/master/traefik/install
|
||||
sudo bash neko-rooms.sh
|
||||
```
|
||||
|
||||
## How to start
|
||||
|
||||
You need to have installed `Docker` and `docker-compose`. You need to have a custom domain pointing to your server's IP.
|
||||
|
||||
You can watch installation video provided by *Dr R1ck*:
|
||||
|
||||
https://www.youtube.com/watch?v=cCmnw-pq0gA
|
||||
|
||||
### Installation guide
|
||||
|
||||
You only need `.env.example`, `docker-compose.yml` and `traefik/`.
|
||||
|
||||
#### Do I need to use traefik?
|
||||
|
||||
- This project started with Traefik as a needed dependency. That, however, changed. Traefik must not be used but the original setup can still be used.
|
||||
- Traefik is used to forward traffic to the rooms. You can put nginx in front of it.
|
||||
- See example configuration for [nginx](./nginx).
|
||||
|
||||
You can use `docker-compose.http.yml` that will expose this service to `8080` or any port. Authentication is optional. Start it quickly with `docker-compose -f docker-compose.http.yml up -d`.
|
||||
|
||||
### Step 1
|
||||
|
||||
Copy `.env.example` to `.env` and customize.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### Step 2
|
||||
|
||||
Create `usersfile` with your users:
|
||||
|
||||
```bash
|
||||
touch usersfile
|
||||
```
|
||||
|
||||
And add as many users as you like:
|
||||
|
||||
```bash
|
||||
echo $(htpasswd -nb user password) >> usersfile
|
||||
```
|
||||
|
||||
### Step 3 (HTTPS only)
|
||||
|
||||
Create `acme.json`
|
||||
|
||||
```bash
|
||||
touch acme.json
|
||||
chmod 600 acme.json
|
||||
```
|
||||
|
||||
Update your email in `traefik.yml`.
|
||||
@@ -0,0 +1,43 @@
|
||||
version: "3.5"
|
||||
|
||||
#
|
||||
# This docker compose needs .env file.
|
||||
# Copy .env.example to .env and modify.
|
||||
#
|
||||
|
||||
networks:
|
||||
default:
|
||||
attachable: "true"
|
||||
name: "${NEKO_ROOMS_TRAEFIK_NETWORK}"
|
||||
|
||||
services:
|
||||
traefik:
|
||||
image: "traefik:2.4"
|
||||
restart: "unless-stopped"
|
||||
environment:
|
||||
- "TZ"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
- "./traefik.yml:/etc/traefik/traefik.yml:ro"
|
||||
- "./usersfile:/usersfile:ro"
|
||||
- "./acme.json:/acme.json"
|
||||
- "./config:/config"
|
||||
|
||||
neko-rooms:
|
||||
image: "m1k1o/neko-rooms:latest"
|
||||
restart: "unless-stopped"
|
||||
env_file:
|
||||
- ".env"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.services.neko-rooms-frontend.loadbalancer.server.port=8080"
|
||||
- "traefik.http.routers.neko-rooms.entrypoints=${NEKO_ROOMS_TRAEFIK_ENTRYPOINT}"
|
||||
- "traefik.http.routers.neko-rooms.rule=Host(`${NEKO_ROOMS_TRAEFIK_DOMAIN}`)"
|
||||
- "traefik.http.routers.neko-rooms.tls=true"
|
||||
- "traefik.http.routers.neko-rooms.tls.certresolver=${NEKO_ROOMS_TRAEFIK_CERTRESOLVER}"
|
||||
- "traefik.http.routers.neko-rooms.middlewares=basicauth@file"
|
||||
+11
-12
@@ -166,8 +166,7 @@ while true; do
|
||||
break
|
||||
done
|
||||
|
||||
mkdir -p "./traefik"
|
||||
touch "./traefik/usersfile"
|
||||
touch "./usersfile"
|
||||
|
||||
# Users
|
||||
while true; do
|
||||
@@ -185,7 +184,7 @@ while true; do
|
||||
USR_PASS="admin"
|
||||
fi
|
||||
|
||||
htpasswd -nb "${USR_NAME}" "${USR_PASS}" >> traefik/usersfile
|
||||
htpasswd -nb "${USR_NAME}" "${USR_PASS}" >> usersfile
|
||||
|
||||
echo
|
||||
read -rp "Do you want to add another user? [y/N] " yn
|
||||
@@ -221,17 +220,17 @@ echo "[Y] Creating env..."
|
||||
# Download traefik config
|
||||
#
|
||||
|
||||
mkdir -p "./traefik/config"
|
||||
mkdir -p "./config"
|
||||
|
||||
wget -O "./traefik/traefik.yml" "https://raw.githubusercontent.com/m1k1o/neko-rooms/master/traefik/traefik.yml"
|
||||
sed -i "s/yourname@example.com/${TRAEFIK_EMAIL}/g" "./traefik/traefik.yml"
|
||||
wget -O "./traefik.yml" "https://raw.githubusercontent.com/m1k1o/neko-rooms/master/traefik/traefik.yml"
|
||||
sed -i "s/yourname@example.com/${TRAEFIK_EMAIL}/g" "./traefik.yml"
|
||||
|
||||
wget -O "./traefik/config/middlewares.yml" "https://raw.githubusercontent.com/m1k1o/neko-rooms/master/traefik/config/middlewares.yml"
|
||||
wget -O "./traefik/config/routers.yml" "https://raw.githubusercontent.com/m1k1o/neko-rooms/master/traefik/config/routers.yml"
|
||||
wget -O "./traefik/config/tls.yml" "https://raw.githubusercontent.com/m1k1o/neko-rooms/master/traefik/config/tls.yml"
|
||||
wget -O "./config/middlewares.yml" "https://raw.githubusercontent.com/m1k1o/neko-rooms/master/traefik/config/middlewares.yml"
|
||||
wget -O "./config/routers.yml" "https://raw.githubusercontent.com/m1k1o/neko-rooms/master/traefik/config/routers.yml"
|
||||
wget -O "./config/tls.yml" "https://raw.githubusercontent.com/m1k1o/neko-rooms/master/traefik/config/tls.yml"
|
||||
|
||||
touch "./traefik/acme.json"
|
||||
chmod 600 "./traefik/acme.json"
|
||||
touch "./acme.json"
|
||||
chmod 600 "./acme.json"
|
||||
|
||||
echo "[Y] Downloading traefik config..."
|
||||
|
||||
@@ -239,7 +238,7 @@ echo "[Y] Downloading traefik config..."
|
||||
# Download docker compose file
|
||||
#
|
||||
|
||||
wget -O "./docker-compose.yml" "https://raw.githubusercontent.com/m1k1o/neko-rooms/master/docker-compose.yml"
|
||||
wget -O "./docker-compose.yml" "https://raw.githubusercontent.com/m1k1o/neko-rooms/master/traefik/docker-compose.yml"
|
||||
|
||||
# Pull neko images
|
||||
for NEKO_IMAGE in "${NEKO_IMAGES[@]}"; do
|
||||
Reference in New Issue
Block a user