mirror of
https://github.com/m1k1o/neko-rooms.git
synced 2026-11-03 07:01:35 -06:00
Compare commits
78 Commits
v1.4.1
...
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 | |||
| 8800c023db | |||
| e31841246b | |||
| 2ca8e9371b | |||
| 06ea90b994 | |||
| 4708fb9371 | |||
| 2622586f11 | |||
| 90d967c5bd | |||
| 9b45bdcfc3 | |||
| 8360096ae6 | |||
| afe75adcf1 | |||
| 62b7ee7059 | |||
| b9514ac1fe | |||
| 8e0623ee45 | |||
| 0ae6c2002d | |||
| 9e99803d89 | |||
| 4a8fa30c2a | |||
| 05dda5935c | |||
| 0edf9ed8dc | |||
| eeeeca6702 | |||
| 62b68419de | |||
| 6f4a9429a8 | |||
| bd3fd98a74 | |||
| c74aa53ca7 |
@@ -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
|
||||
|
||||
@@ -106,12 +61,17 @@ You might have encountered this error:
|
||||
|
||||
If you didn't specify storage yet, you can do it using [this tutorial](./docs/storage.md).
|
||||
|
||||
### Docs
|
||||
|
||||
For more information visit [docs](./docs).
|
||||
|
||||
### Roadmap:
|
||||
- [x] add GUI
|
||||
- [x] add HTTPS support
|
||||
- [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() {
|
||||
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko_rooms"
|
||||
"m1k1o/neko_rooms/cmd"
|
||||
"m1k1o/neko_rooms/internal/utils"
|
||||
nekoRooms "github.com/m1k1o/neko-rooms"
|
||||
"github.com/m1k1o/neko-rooms/cmd"
|
||||
"github.com/m1k1o/neko-rooms/internal/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Print(utils.Colorf(neko_rooms.Header, "server", neko_rooms.Service.Version))
|
||||
fmt.Print(utils.Colorf(nekoRooms.Header, "server", nekoRooms.Service.Version))
|
||||
if err := cmd.Execute(); err != nil {
|
||||
log.Panic().Err(err).Msg("failed to execute command")
|
||||
}
|
||||
|
||||
+13
-8
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"m1k1o/neko_rooms"
|
||||
nekoRooms "github.com/m1k1o/neko-rooms"
|
||||
)
|
||||
|
||||
func Execute() error {
|
||||
@@ -23,10 +23,10 @@ func Execute() error {
|
||||
}
|
||||
|
||||
var root = &cobra.Command{
|
||||
Use: "neko_rooms",
|
||||
Short: "neko_rooms server",
|
||||
Long: `neko_rooms server`,
|
||||
Version: neko_rooms.Service.Version.String(),
|
||||
Use: "neko-rooms",
|
||||
Short: "neko-rooms server",
|
||||
Long: `neko-rooms server`,
|
||||
Version: nekoRooms.Service.Version.String(),
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -49,6 +49,7 @@ func init() {
|
||||
|
||||
logs := filepath.Join(".", "logs")
|
||||
if runtime.GOOS == "linux" {
|
||||
// TODO: Change to neko-rooms.
|
||||
logs = "/var/log/neko_rooms"
|
||||
}
|
||||
|
||||
@@ -56,9 +57,11 @@ func init() {
|
||||
os.Mkdir(logs, os.ModePerm)
|
||||
}
|
||||
|
||||
// TODO: Change to neko-rooms.
|
||||
latest := filepath.Join(logs, "neko_rooms-latest.log")
|
||||
_, err := os.Stat(latest)
|
||||
if err == nil {
|
||||
// TODO: Change to neko-rooms.
|
||||
err = os.Rename(latest, filepath.Join(logs, "neko_rooms."+time.Now().Format("2006-01-02T15-04-05Z07-00")+".log"))
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("failed to rotate log file")
|
||||
@@ -85,10 +88,12 @@ func init() {
|
||||
viper.SetConfigFile(config) // Use config file from the flag.
|
||||
} else {
|
||||
if runtime.GOOS == "linux" {
|
||||
// TODO: Change to neko-rooms.
|
||||
viper.AddConfigPath("/etc/neko_rooms/")
|
||||
}
|
||||
|
||||
viper.AddConfigPath(".")
|
||||
// TODO: Change to config.
|
||||
viper.SetConfigName("neko_rooms")
|
||||
}
|
||||
|
||||
@@ -118,12 +123,12 @@ func init() {
|
||||
logger.Info().Msg("preflight complete")
|
||||
}
|
||||
|
||||
neko_rooms.Service.Configs.Root.Set()
|
||||
nekoRooms.Service.Configs.Root.Set()
|
||||
})
|
||||
|
||||
if err := neko_rooms.Service.Configs.Root.Init(root); err != nil {
|
||||
if err := nekoRooms.Service.Configs.Root.Init(root); err != nil {
|
||||
log.Panic().Err(err).Msg("unable to run root command")
|
||||
}
|
||||
|
||||
root.SetVersionTemplate(neko_rooms.Service.Version.Details())
|
||||
root.SetVersionTemplate(nekoRooms.Service.Version.Details())
|
||||
}
|
||||
|
||||
+8
-9
@@ -4,29 +4,28 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"m1k1o/neko_rooms"
|
||||
"m1k1o/neko_rooms/internal/config"
|
||||
nekoRooms "github.com/m1k1o/neko-rooms"
|
||||
"github.com/m1k1o/neko-rooms/internal/config"
|
||||
)
|
||||
|
||||
func init() {
|
||||
command := &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "serve neko_rooms server",
|
||||
Long: `serve neko_rooms server`,
|
||||
Run: neko_rooms.Service.ServeCommand,
|
||||
Short: "serve neko-rooms server",
|
||||
Long: `serve neko-rooms server`,
|
||||
Run: nekoRooms.Service.ServeCommand,
|
||||
}
|
||||
|
||||
configs := []config.Config{
|
||||
neko_rooms.Service.Configs.Server,
|
||||
neko_rooms.Service.Configs.API,
|
||||
neko_rooms.Service.Configs.Room,
|
||||
nekoRooms.Service.Configs.Server,
|
||||
nekoRooms.Service.Configs.Room,
|
||||
}
|
||||
|
||||
cobra.OnInitialize(func() {
|
||||
for _, cfg := range configs {
|
||||
cfg.Set()
|
||||
}
|
||||
neko_rooms.Service.Preflight()
|
||||
nekoRooms.Service.Preflight()
|
||||
})
|
||||
|
||||
for _, cfg := range configs {
|
||||
|
||||
@@ -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}";
|
||||
|
||||
@@ -21,11 +21,13 @@ on_exit() {
|
||||
docker run --rm -it \
|
||||
--name="neko_rooms_traefik" \
|
||||
-p "${1:-80}:80" \
|
||||
-v "${PWD}/../:/app" \
|
||||
-p "8080:8080" \
|
||||
-v "${PWD}/../../:/app" \
|
||||
-e "TZ=${TZ}" \
|
||||
-v "/var/run/docker.sock:/var/run/docker.sock" \
|
||||
--net="${NEKO_ROOMS_TRAEFIK_NETWORK}" \
|
||||
traefik:2.4 \
|
||||
--api.insecure=true \
|
||||
--providers.docker=true \
|
||||
--providers.docker.watch=true \
|
||||
--providers.docker.exposedbydefault=false \
|
||||
+13
-38
@@ -1,49 +1,24 @@
|
||||
version: "3.7"
|
||||
|
||||
#
|
||||
# This docker compose needs .env file.
|
||||
# Copy .env.example to .env and modify.
|
||||
#
|
||||
version: "3.5"
|
||||
|
||||
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"
|
||||
environment:
|
||||
- "TZ"
|
||||
- "NEKO_ROOMS_EPR"
|
||||
- "NEKO_ROOMS_NAT1TO1"
|
||||
- "NEKO_ROOMS_TRAEFIK_DOMAIN"
|
||||
- "NEKO_ROOMS_TRAEFIK_ENTRYPOINT"
|
||||
- "NEKO_ROOMS_TRAEFIK_CERTRESOLVER"
|
||||
- "NEKO_ROOMS_TRAEFIK_NETWORK"
|
||||
- "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"
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Docs
|
||||
|
||||
Case study: having 2 rooms, every with max 5 users, they only need 10 ports to be forwarded so the range can be `59000-59009`.
|
||||
|
||||

|
||||
|
||||
All clients visit `https://neko-rooms/room-name/`. Once they logged in, they get one of the ports allocated where the RTC data flows. It happens inside SDP negotiation and is completly transparent to the clients. Therefore those ports needs to remain open, but they will never be actually visited directly in the browser.
|
||||
|
||||
`NEKO_ROOMS_NAT1TO1` must be the IP where the mentioned UDP ports are forwarded. If this setting is not present, it will get automatically servers public IP at start of every room that will be sent to clients.
|
||||
|
||||
## Connection timeout
|
||||
|
||||
Neko room loads but you don't see the screen and it gives you `connection timeout` or `disconnected error`? [Validate](https://neko.m1k1o.net/#/getting-started/troubleshooting?id=validate-udp-ports-reachability) that your UDP ports are reachable.
|
||||
|
||||
## path prefix
|
||||
|
||||
Room names are by default put to root directory. If you want to have custom path prefix, you can specify it using env variable:
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
<mxfile host="app.diagrams.net" modified="2022-02-12T12:21:05.035Z" agent="5.0 (X11)" etag="vVCYmo1eawPDjs-xukiW" version="16.5.6" type="device"><diagram id="e7aEsMLIEvFOf0PZvSqS" name="Page-1">7Vvdl5o4FP9rfNQDBBQf1ZnZnrNtT8/pObvdfYsQNTtILMRR+9dvAgmQEJRx8GPazkObXJKQ3Pu7n8EemK33fyRws/pEQhT1HCvc98BDz3Fse+Sz/zjlkFOGwMsJywSHYlBJ+Ip/IEG0BHWLQ5QqAykhEcUblRiQOEYBVWgwSchOHbYgkfrWDVyiGuFrAKM69W8c0lVO9T2rpH9AeLmSb7Yt8WQN5WBBSFcwJLsKCTz2wCwhhOat9X6GIs48yZd83lPD02JjCYppmwnf//3zr+3n6WISWPZ6N32A4aek7zr5Mi8w2ooTi93Sg2RBQrZxiPgqVg9MdytM0dcNDPjTHRM6o63oOmI9mzUXOIpmJCJJNheEEPmLgNFTmpBnVHkyDHw0X7AnYgMooWjfeDS7YBhDGiJrRJMDGyImAMHig8SO6O9KiYGRoK0q0gJyIBQoWRZLl4xkDcHLV/B1VOMiChmuRJckdEWWJIbRY0mdqnwux3wkZCO4+x+i9CCUBG4pUXmP9ph+q7T/4UsNHE90H/Zi6axzkJ2YnfdbtZNPG3myX87LenJifkJ+rONSY1wg2yRAR7hlj4Vmw2SJ6LGBrhkHCYogxS/qTkxCFVO/EMz2WODHHqsAAkDDRX4CMauqY9pChbLLhYbaQvkJawtlGCvOcz7s5Our6gwmvuUz+hMNNkZUfoRzZrQVJMEIL2PWDpgoEVPYKVdPzKziRDxY4zDMQYtS/APOs/U4Kjb8aNlhvWnPe2CUiC8/hcHzMgO4NAExidEx7RcGXSxdmtEqvppVr9FU9K2BOwYgX+uNsAGa1VHnk8UiRReRsm+w2cOI8WQa4hfWXNLMmpA1G2TLR+xNlaeGCZK0INkZU+mKh9+33EVN7bLJHSkM1MczpiKYYcWxPqOdMjQojH5JBOPsr0pSd4EVnMpRfG/9fGcTNsDf7OsrMOBQiGO+leLkWF+e0fJzquR5Yhio8OgtvrEDT+dqng7UPZ1vcHS6GerMz41Phw8s7tnw5iJC+wmPyDJPEormQxDBNMWByimVrWf5NT0Q8ZAfuqZAxHfmYDjMnsCEym3NIxI8d+vlfLNkK6LzDKKTtI5dnO2f6eKGlraQ613Vxdl2DXLe2LKsPv+XhQdP2/Ambq57RzY+5ciAZ/uKLEadQMXxXGVV6W8u79hsUzbS5Nmc357tFZ7tvv2YY8jYrurHbLcN8mgC0QI//wbe+wWenqEVSLwZ8rw3h1AyVmkMoDqKhdgrnzA/3avy/1tFPEALVGoiPDfiqS106Yhn2MY2lSk+e4vlukB2W6nhuwyQctW5QYRku2rq37fBtUIkULcON6ssjtpWFgdur1JbtHvHC4tZ7wtKMOMVB11GlCYwwnHHaVnr4mMD2lpj6U1uQpasKkYgZFY5iw5S3qC30Olr1/Zy9DdqvDWwmPvoRMVlBb/QcC3LvWASZKqp1Mx9jJ5Jn2dC6e9o9P1Go1oaBG6dBhliz2t4l4YrqFM3UGd5pS49h9PSc1zq2mqkwcc9M8LVL1Cd8XVrek7duf1y11bOcdd2uXur/vUurhz3nsyLc+/2xb8v8+J0Zl70u4cG8zJJEnioDBNK2rhhR7+aGHnH96Un9p4ynjXyHXSrA4Z6zy9n6xoA+1PZulFdzl3X9a50MarW/d50TXqz60+97KsD4exioL7QpUOl+scf/OLTy64/xz/T9adz8kOea91/XrG6Z/h+q8yX86QVRkznIGVGgJ0UxkuRKpeX4AUKTNmxhgwmLXqTTHdYz3QLHa3qvx5zdJbqmq5daiUTWWL7xSosKUpeMNvcu6+vAP17KbsOO5PXqVn17uq4d/Rl8IlMRiu5N6VRPovUzijot40Y2tRhhL2+eAW/IbbQ0hvgadWTtrEF0L8e1o3fhWMLt9n7VJR6gRO0g3y1p9aGkQGYNtvRWxdh7XszEm69HvaBpPUbHuG9K+xQg3iRwFUjfkFqH/qZ+K9KqAMR2L6niMCvS2BoksDFBND8OZD07accq+2YPGvxobEkDAaDilrMT7rZu0TA8foBP8ATXOOIC1eNcfKHwm35oltJQUWI0wnGPC1xM0SgJozpPwXpDmPNEajAWC1A/ICiF8SFVAfWLMIoK461w4307dt1NAkoqco/SxS/kBRTTDgO5oRSsjYAhBLNFjMjz2IFhj35U7zusGE7dXBY2V834BjqPyEb1dEBDOg44yaOdcsf/eWuu/zpJHj8Hw==</diagram></mxfile>
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 22 KiB |
@@ -1,4 +1,4 @@
|
||||
version: "3"
|
||||
version: "3.5"
|
||||
|
||||
services:
|
||||
neko-rooms:
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# custom labels
|
||||
|
||||
You can add custom lables for every room.
|
||||
|
||||
Example: Expose port 3000 from every room under `3000-(room-name)`:
|
||||
|
||||
```bash
|
||||
-e "NEKO_ROOMS_INSTANCE_LABELS=
|
||||
traefik.http.services.{containerName}-3000-tcp.loadbalancer.server.port=3000
|
||||
traefik.http.routers.{containerName}-3000-tcp.entrypoints={traefikEntrypoint}
|
||||
traefik.http.routers.{containerName}-3000-tcp.rule=PathPrefix(\`/3000-{roomName}\`)
|
||||
traefik.http.middlewares.{containerName}-3000-tcp-prf.stripprefix.prefixes=/3000-{roomName}/
|
||||
traefik.http.routers.{containerName}-3000-tcp.middlewares={containerName}-3000-tcp-prf
|
||||
traefik.http.routers.{containerName}-3000-tcp.service={containerName}-3000-tcp
|
||||
"
|
||||
```
|
||||
@@ -1,52 +1,50 @@
|
||||
module m1k1o/neko_rooms
|
||||
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
|
||||
)
|
||||
|
||||
+6
-6
@@ -5,21 +5,20 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko_rooms/internal/config"
|
||||
"m1k1o/neko_rooms/internal/types"
|
||||
"github.com/m1k1o/neko-rooms/internal/types"
|
||||
)
|
||||
|
||||
type ApiManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
rooms types.RoomManager
|
||||
conf *config.API
|
||||
pull types.PullManager
|
||||
}
|
||||
|
||||
func New(roomManager types.RoomManager, conf *config.API) *ApiManagerCtx {
|
||||
func New(rooms types.RoomManager, pull types.PullManager) *ApiManagerCtx {
|
||||
return &ApiManagerCtx{
|
||||
logger: log.With().Str("module", "api").Logger(),
|
||||
rooms: roomManager,
|
||||
conf: conf,
|
||||
rooms: rooms,
|
||||
pull: pull,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"m1k1o/neko_rooms/internal/types"
|
||||
"github.com/m1k1o/neko-rooms/internal/types"
|
||||
)
|
||||
|
||||
func (manager *ApiManagerCtx) pullStart(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -15,25 +15,25 @@ func (manager *ApiManagerCtx) pullStart(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
err := manager.rooms.PullStart(request)
|
||||
err := manager.pull.Start(request)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
response := manager.rooms.PullStatus()
|
||||
response := manager.pull.Status()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func (manager *ApiManagerCtx) pullStatus(w http.ResponseWriter, r *http.Request) {
|
||||
response := manager.rooms.PullStatus()
|
||||
response := manager.pull.Status()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func (manager *ApiManagerCtx) pullStop(w http.ResponseWriter, r *http.Request) {
|
||||
err := manager.rooms.PullStop()
|
||||
err := manager.pull.Stop()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
|
||||
+13
-1
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
|
||||
"m1k1o/neko_rooms/internal/types"
|
||||
"github.com/m1k1o/neko-rooms/internal/types"
|
||||
)
|
||||
|
||||
func (manager *ApiManagerCtx) roomsList(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -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")
|
||||
}
|
||||
+140
-23
@@ -1,6 +1,8 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -11,12 +13,25 @@ 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
|
||||
|
||||
NAT1To1IPs []string
|
||||
NekoImages []string
|
||||
NAT1To1IPs []string
|
||||
NekoImages []string
|
||||
NekoPrivilegedImages []string
|
||||
PathPrefix string
|
||||
Labels []string
|
||||
WaitEnabled bool
|
||||
|
||||
StorageEnabled bool
|
||||
StorageInternal string
|
||||
@@ -24,17 +39,19 @@ type Room struct {
|
||||
|
||||
MountsWhitelist []string
|
||||
|
||||
InstanceName string
|
||||
InstanceUrl string
|
||||
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
|
||||
@@ -52,15 +69,36 @@ 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:vncviewer",
|
||||
"m1k1o/neko:remmina",
|
||||
"m1k1o/neko:xfce",
|
||||
}, "neko images to be used")
|
||||
if err := viper.BindPFlag("neko_images", cmd.PersistentFlags().Lookup("neko_images")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("neko_privileged_images", []string{}, "Whitelist of images allowed to be executed with Privileged mode")
|
||||
if err := viper.BindPFlag("neko_privileged_images", cmd.PersistentFlags().Lookup("neko_privileged_images")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringSlice("labels", []string{}, "additional labels appended to every room")
|
||||
if err := viper.BindPFlag("labels", cmd.PersistentFlags().Lookup("labels")); err != nil {
|
||||
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")
|
||||
@@ -95,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
|
||||
@@ -112,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
|
||||
}
|
||||
@@ -126,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")
|
||||
@@ -152,6 +202,10 @@ func (s *Room) Set() {
|
||||
|
||||
s.NAT1To1IPs = viper.GetStringSlice("nat1to1")
|
||||
s.NekoImages = viper.GetStringSlice("neko_images")
|
||||
s.NekoPrivilegedImages = viper.GetStringSlice("neko_privileged_images")
|
||||
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")
|
||||
@@ -183,20 +237,83 @@ func (s *Room) Set() {
|
||||
log.Panic().Msg("invalid `instance.name`, must match " + dockerNames.RestrictedNameChars)
|
||||
}
|
||||
|
||||
s.InstanceUrl = viper.GetString("instance.url")
|
||||
instanceUrl := viper.GetString("instance.url")
|
||||
if instanceUrl != "" {
|
||||
var err error
|
||||
s.InstanceUrl, err = url.Parse(instanceUrl)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("invalid `instance.url`")
|
||||
}
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
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`")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Room) GetInstanceUrl() url.URL {
|
||||
if s.InstanceUrl != nil {
|
||||
return *s.InstanceUrl
|
||||
}
|
||||
|
||||
instanceUrl := url.URL{
|
||||
Scheme: "http",
|
||||
Host: "127.0.0.1",
|
||||
Path: "/",
|
||||
}
|
||||
|
||||
if s.Traefik.Enabled {
|
||||
if s.Traefik.Certresolver != "" {
|
||||
instanceUrl.Scheme = "https"
|
||||
}
|
||||
|
||||
if s.Traefik.Domain != "" && s.Traefik.Domain != "*" {
|
||||
instanceUrl.Host = s.Traefik.Domain
|
||||
}
|
||||
|
||||
// deprecated
|
||||
if s.Traefik.Port != "" {
|
||||
instanceUrl.Host += ":" + s.Traefik.Port
|
||||
}
|
||||
}
|
||||
|
||||
return instanceUrl
|
||||
}
|
||||
|
||||
func (s *Room) GetRoomUrl(roomName string) string {
|
||||
instanceUrl := s.GetInstanceUrl()
|
||||
|
||||
if strings.HasPrefix(instanceUrl.Host, "*.") {
|
||||
instanceUrl.Host = roomName + "." + strings.TrimPrefix(instanceUrl.Host, "*.")
|
||||
} else {
|
||||
instanceUrl.Path = path.Join(instanceUrl.Path, s.PathPrefix, roomName) + "/"
|
||||
}
|
||||
|
||||
return instanceUrl.String()
|
||||
}
|
||||
|
||||
@@ -5,11 +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
|
||||
Cert string
|
||||
Key string
|
||||
Bind string
|
||||
Proxy bool
|
||||
PProf bool
|
||||
|
||||
Admin Admin
|
||||
}
|
||||
|
||||
func (Server) Init(cmd *cobra.Command) error {
|
||||
@@ -28,8 +38,35 @@ 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 {
|
||||
cmd.PersistentFlags().Bool("proxy", false, "trust reverse proxy headers")
|
||||
if err := viper.BindPFlag("proxy", cmd.PersistentFlags().Lookup("proxy")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("pprof", false, "enable pprof endpoint available at /debug/pprof")
|
||||
if err := viper.BindPFlag("pprof", cmd.PersistentFlags().Lookup("pprof")); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -40,5 +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")
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func Logger(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
req := map[string]interface{}{}
|
||||
|
||||
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
|
||||
req["id"] = reqID
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
req["scheme"] = scheme
|
||||
req["proto"] = r.Proto
|
||||
req["method"] = r.Method
|
||||
req["remote"] = r.RemoteAddr
|
||||
req["agent"] = r.UserAgent()
|
||||
req["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI)
|
||||
|
||||
fields := map[string]interface{}{}
|
||||
fields["req"] = req
|
||||
|
||||
entry := &entry{
|
||||
fields: fields,
|
||||
}
|
||||
|
||||
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
||||
t1 := time.Now()
|
||||
|
||||
defer func() {
|
||||
entry.Write(ww.Status(), ww.BytesWritten(), time.Since(t1))
|
||||
}()
|
||||
|
||||
next.ServeHTTP(ww, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
fields map[string]interface{}
|
||||
errors []map[string]interface{}
|
||||
}
|
||||
|
||||
func (e *entry) Write(status, bytes int, elapsed time.Duration) {
|
||||
res := map[string]interface{}{}
|
||||
res["time"] = time.Now().UTC().Format(time.RFC1123)
|
||||
res["status"] = status
|
||||
res["bytes"] = bytes
|
||||
res["elapsed"] = float64(elapsed.Nanoseconds()) / 1000000.0
|
||||
|
||||
e.fields["res"] = res
|
||||
e.fields["module"] = "http"
|
||||
|
||||
if len(e.errors) > 0 {
|
||||
e.fields["errors"] = e.errors
|
||||
log.Error().Fields(e.fields).Msgf("request failed (%d)", status)
|
||||
} else {
|
||||
log.Debug().Fields(e.fields).Msgf("request complete (%d)", status)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *entry) Panic(v interface{}, stack []byte) {
|
||||
err := map[string]interface{}{}
|
||||
err["message"] = fmt.Sprintf("%+v", v)
|
||||
err["stack"] = string(stack)
|
||||
|
||||
e.errors = append(e.errors, err)
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko_rooms/internal/config"
|
||||
"m1k1o/neko_rooms/internal/types"
|
||||
)
|
||||
|
||||
type HttpManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
router *chi.Mux
|
||||
http *http.Server
|
||||
conf *config.Server
|
||||
}
|
||||
|
||||
func New(ApiManager types.ApiManager, conf *config.Server) *HttpManagerCtx {
|
||||
logger := log.With().Str("module", "http").Logger()
|
||||
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.Recoverer) // Recover from panics without crashing server
|
||||
router.Use(middleware.RequestID) // Create a request ID for each request
|
||||
router.Use(Logger) // Log API request calls using custom logger function
|
||||
|
||||
// Basic CORS
|
||||
// for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing
|
||||
router.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||
}))
|
||||
|
||||
router.Route("/api", ApiManager.Mount)
|
||||
|
||||
if conf.Static != "" {
|
||||
fs := http.FileServer(http.Dir(conf.Static))
|
||||
router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := os.Stat(conf.Static + r.URL.Path); !os.IsNotExist(err) {
|
||||
fs.ServeHTTP(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// add simple lobby room
|
||||
router.Get("/{roomName}", ApiManager.RoomLobby)
|
||||
router.Get("/{roomName}/", ApiManager.RoomLobby)
|
||||
|
||||
router.NotFound(http.HandlerFunc(http.NotFound))
|
||||
|
||||
http := &http.Server{
|
||||
Addr: conf.Bind,
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
return &HttpManagerCtx{
|
||||
logger: logger,
|
||||
router: router,
|
||||
http: http,
|
||||
conf: conf,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HttpManagerCtx) Start() {
|
||||
if s.conf.Cert != "" && s.conf.Key != "" {
|
||||
go func() {
|
||||
if err := s.http.ListenAndServeTLS(s.conf.Cert, s.conf.Key); err != http.ErrServerClosed {
|
||||
s.logger.Panic().Err(err).Msg("unable to start https server")
|
||||
}
|
||||
}()
|
||||
s.logger.Info().Msgf("https listening on %s", s.http.Addr)
|
||||
} else {
|
||||
go func() {
|
||||
if err := s.http.ListenAndServe(); err != http.ErrServerClosed {
|
||||
s.logger.Panic().Err(err).Msg("unable to start http server")
|
||||
}
|
||||
}()
|
||||
s.logger.Info().Msgf("http listening on %s", s.http.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HttpManagerCtx) Shutdown() error {
|
||||
return s.http.Shutdown(context.Background())
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"m1k1o/neko_rooms/internal/types"
|
||||
|
||||
"github.com/m1k1o/neko-rooms/internal/types"
|
||||
)
|
||||
|
||||
//go:embed policies.json
|
||||
@@ -35,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
|
||||
|
||||
@@ -4,8 +4,9 @@ package chromium
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"m1k1o/neko_rooms/internal/types"
|
||||
"strings"
|
||||
|
||||
"github.com/m1k1o/neko-rooms/internal/types"
|
||||
)
|
||||
|
||||
func Parse(policiesJson string) (*types.BrowserPolicyContent, error) {
|
||||
|
||||
@@ -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": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ package policies
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"m1k1o/neko_rooms/internal/policies/chromium"
|
||||
"m1k1o/neko_rooms/internal/policies/firefox"
|
||||
"m1k1o/neko_rooms/internal/types"
|
||||
|
||||
"github.com/m1k1o/neko-rooms/internal/policies/chromium"
|
||||
"github.com/m1k1o/neko-rooms/internal/policies/firefox"
|
||||
"github.com/m1k1o/neko-rooms/internal/types"
|
||||
)
|
||||
|
||||
func Generate(policies types.BrowserPolicyContent, policyType types.BrowserPolicyType) (string, error) {
|
||||
|
||||
@@ -5,7 +5,8 @@ package firefox
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"m1k1o/neko_rooms/internal/types"
|
||||
|
||||
"github.com/m1k1o/neko-rooms/internal/types"
|
||||
)
|
||||
|
||||
//go:embed policies.json
|
||||
|
||||
@@ -5,7 +5,8 @@ package firefox
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"m1k1o/neko_rooms/internal/types"
|
||||
|
||||
"github.com/m1k1o/neko-rooms/internal/types"
|
||||
)
|
||||
|
||||
func Parse(policiesJson string) (*types.BrowserPolicyContent, error) {
|
||||
|
||||
@@ -1,41 +1,46 @@
|
||||
package api
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
|
||||
"m1k1o/neko_rooms/internal/utils"
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package pull
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/m1k1o/neko-rooms/internal/types"
|
||||
"github.com/m1k1o/neko-rooms/internal/utils"
|
||||
)
|
||||
|
||||
type PullManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
client *dockerClient.Client
|
||||
images []string
|
||||
|
||||
mu sync.Mutex
|
||||
cancel func()
|
||||
status types.PullStatus
|
||||
layers map[string]int
|
||||
}
|
||||
|
||||
func New(client *dockerClient.Client, nekoImages []string) *PullManagerCtx {
|
||||
return &PullManagerCtx{
|
||||
logger: log.With().Str("module", "pull").Logger(),
|
||||
client: client,
|
||||
images: nekoImages,
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *PullManagerCtx) tryInitialize(cancel func()) bool {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
if manager.status.Active {
|
||||
cancel()
|
||||
return false
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
manager.cancel = cancel
|
||||
|
||||
manager.status = types.PullStatus{
|
||||
Active: true,
|
||||
Started: &now,
|
||||
Layers: []types.PullLayer{},
|
||||
Status: []string{},
|
||||
}
|
||||
|
||||
manager.layers = map[string]int{}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (manager *PullManagerCtx) setDone() {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
manager.status.Active = false
|
||||
manager.status.Finished = &now
|
||||
}
|
||||
|
||||
func (manager *PullManagerCtx) Start(request types.PullStart) error {
|
||||
if in, _ := utils.ArrayIn(request.NekoImage, manager.images); !in {
|
||||
return fmt.Errorf("unknown neko image")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
if !manager.tryInitialize(cancel) {
|
||||
return fmt.Errorf("pull is already in progess")
|
||||
}
|
||||
|
||||
// handle registry auth
|
||||
var opts dockerTypes.ImagePullOptions
|
||||
if request.RegistryUser != "" && request.RegistryPass != "" {
|
||||
authConfig := dockerTypes.AuthConfig{
|
||||
Username: request.RegistryUser,
|
||||
Password: request.RegistryPass,
|
||||
}
|
||||
|
||||
encodedJSON, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts = dockerTypes.ImagePullOptions{
|
||||
RegistryAuth: base64.URLEncoding.EncodeToString(encodedJSON),
|
||||
}
|
||||
}
|
||||
|
||||
reader, err := manager.client.ImagePull(ctx, request.NekoImage, opts)
|
||||
|
||||
if err != nil {
|
||||
manager.setDone()
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
data := scanner.Bytes()
|
||||
|
||||
layer := types.PullLayer{}
|
||||
if err := json.Unmarshal(data, &layer); err != nil {
|
||||
manager.status.Status = append(
|
||||
manager.status.Status,
|
||||
fmt.Sprintf("Error while parsing pull response: %s", err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if layer.ProgressDetail != nil {
|
||||
// map layer id to slice index
|
||||
if index, ok := manager.layers[layer.ID]; ok {
|
||||
manager.status.Layers[index] = layer
|
||||
} else {
|
||||
manager.layers[layer.ID] = len(manager.layers)
|
||||
manager.status.Layers = append(manager.status.Layers, layer)
|
||||
}
|
||||
} else {
|
||||
manager.status.Status = append(
|
||||
manager.status.Status,
|
||||
layer.Status,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
manager.status.Status = append(
|
||||
manager.status.Status,
|
||||
fmt.Sprintf("Error while reading pull response: %s", err),
|
||||
)
|
||||
}
|
||||
|
||||
reader.Close()
|
||||
manager.setDone()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *PullManagerCtx) Stop() error {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
if !manager.status.Active {
|
||||
return fmt.Errorf("pull is not in progess")
|
||||
|
||||
}
|
||||
|
||||
manager.cancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *PullManagerCtx) Status() types.PullStatus {
|
||||
manager.mu.Lock()
|
||||
defer manager.mu.Unlock()
|
||||
|
||||
return manager.status
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
|
||||
"m1k1o/neko_rooms/internal/types"
|
||||
"github.com/m1k1o/neko-rooms/internal/types"
|
||||
)
|
||||
|
||||
func (manager *RoomManagerCtx) containerToEntry(container dockerTypes.Container) (*types.RoomEntry, error) {
|
||||
@@ -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) {
|
||||
|
||||
+54
-24
@@ -4,12 +4,13 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"m1k1o/neko_rooms/internal/types"
|
||||
"github.com/m1k1o/neko-rooms/internal/types"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
+172
-96
@@ -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"
|
||||
@@ -20,10 +22,10 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"m1k1o/neko_rooms/internal/config"
|
||||
"m1k1o/neko_rooms/internal/policies"
|
||||
"m1k1o/neko_rooms/internal/types"
|
||||
"m1k1o/neko_rooms/internal/utils"
|
||||
"github.com/m1k1o/neko-rooms/internal/config"
|
||||
"github.com/m1k1o/neko-rooms/internal/policies"
|
||||
"github.com/m1k1o/neko-rooms/internal/types"
|
||||
"github.com/m1k1o/neko-rooms/internal/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -34,20 +36,13 @@ const (
|
||||
privateStorageGid = 1000
|
||||
)
|
||||
|
||||
func New(config *config.Room) *RoomManagerCtx {
|
||||
func New(client *dockerClient.Client, config *config.Room) *RoomManagerCtx {
|
||||
logger := log.With().Str("module", "room").Logger()
|
||||
|
||||
cli, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv)
|
||||
if err != nil {
|
||||
logger.Panic().Err(err).Msg("unable to connect to docker client")
|
||||
} else {
|
||||
logger.Info().Msg("successfully connected to docker client")
|
||||
}
|
||||
|
||||
return &RoomManagerCtx{
|
||||
logger: logger,
|
||||
config: config,
|
||||
client: cli,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +50,6 @@ type RoomManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
config *config.Room
|
||||
client *dockerClient.Client
|
||||
pull PullConfig
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) Config() types.RoomsConfig {
|
||||
@@ -63,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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,19 +89,36 @@ 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 " + dockerNames.RestrictedNameChars)
|
||||
return "", fmt.Errorf("invalid container name, must match %s", dockerNames.RestrictedNameChars)
|
||||
}
|
||||
|
||||
if !manager.config.StorageEnabled && len(settings.Mounts) > 0 {
|
||||
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 := manager.IsValidImage(settings.NekoImage, manager.config.NekoPrivilegedImages)
|
||||
|
||||
// TODO: Check if path name exists.
|
||||
roomName := settings.Name
|
||||
if roomName == "" {
|
||||
@@ -117,107 +129,54 @@ 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/udp", 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
|
||||
|
||||
// create traefik rule
|
||||
traefikRule := "PathPrefix(`/" + roomName + "`)"
|
||||
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": "/" + roomName + "$$",
|
||||
"traefik.http.middlewares." + containerName + "-rdr.redirectregex.replacement": "/" + roomName + "/",
|
||||
"traefik.http.middlewares." + containerName + "-prf.stripprefix.prefixes": "/" + roomName + "/",
|
||||
"traefik.http.routers." + containerName + ".middlewares": containerName + "-rdr," + containerName + "-prf",
|
||||
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{}{}
|
||||
}
|
||||
|
||||
//
|
||||
// Set internal labels
|
||||
//
|
||||
|
||||
instanceUrl := manager.config.InstanceUrl
|
||||
instanceSuffix := roomName + "/"
|
||||
if instanceUrl == "" {
|
||||
urlProto := "http"
|
||||
if manager.config.TraefikCertresolver != "" {
|
||||
urlProto = "https"
|
||||
}
|
||||
|
||||
// deprecated
|
||||
port := ""
|
||||
if manager.config.TraefikPort != "" {
|
||||
port = ":" + manager.config.TraefikPort
|
||||
}
|
||||
|
||||
if manager.config.TraefikDomain != "" && manager.config.TraefikDomain != "*" {
|
||||
// match *.domain.tld as subdomain
|
||||
if strings.HasPrefix(manager.config.TraefikDomain, "*.") {
|
||||
instanceUrl = urlProto + "://" + roomName + "." + strings.TrimPrefix(manager.config.TraefikDomain, "*.") + port + "/"
|
||||
instanceSuffix = ""
|
||||
} else {
|
||||
instanceUrl = urlProto + "://" + manager.config.TraefikDomain + port + "/"
|
||||
}
|
||||
} else {
|
||||
// domain is not known
|
||||
instanceUrl = urlProto + "://127.0.0.1" + port + "/"
|
||||
}
|
||||
} else if !strings.HasSuffix(instanceUrl, "/") {
|
||||
instanceUrl = instanceUrl + "/"
|
||||
}
|
||||
|
||||
var browserPolicyLabels *BrowserPolicyLabels
|
||||
if settings.BrowserPolicy != nil {
|
||||
browserPolicyLabels = &BrowserPolicyLabels{
|
||||
@@ -228,15 +187,77 @@ func (manager *RoomManagerCtx) Create(settings types.RoomSettings) (string, erro
|
||||
|
||||
labels := manager.serializeLabels(RoomLabels{
|
||||
Name: roomName,
|
||||
URL: instanceUrl + instanceSuffix,
|
||||
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
|
||||
for _, label := range manager.config.Labels {
|
||||
// replace dynamic values in labels
|
||||
label = strings.Replace(label, "{containerName}", containerName, -1)
|
||||
label = strings.Replace(label, "{roomName}", roomName, -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 {
|
||||
manager.logger.Warn().Str("label", label).Msg("invalid custom label")
|
||||
continue
|
||||
}
|
||||
|
||||
key, val := v[0], v[1]
|
||||
labels[key] = val
|
||||
}
|
||||
|
||||
//
|
||||
@@ -245,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, ",")))
|
||||
@@ -420,11 +451,13 @@ func (manager *RoomManagerCtx) Create(settings types.RoomSettings) (string, erro
|
||||
NanoCPUs: settings.Resources.NanoCPUs,
|
||||
Memory: settings.Resources.Memory,
|
||||
},
|
||||
// Privileged
|
||||
Privileged: isPrivilegedImage,
|
||||
}
|
||||
|
||||
networkingConfig := &network.NetworkingConfig{
|
||||
EndpointsConfig: map[string]*network.EndpointSettings{
|
||||
manager.config.TraefikNetwork: {},
|
||||
manager.config.InstanceNetwork: {},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -557,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
|
||||
}
|
||||
@@ -617,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
|
||||
}
|
||||
@@ -29,7 +29,8 @@ func (manager *RoomManagerCtx) allocatePorts(sum uint16) (EprPorts, error) {
|
||||
}
|
||||
|
||||
for _, port := range ports {
|
||||
if (epr.Min >= port.Min && epr.Min <= port.Max) || (epr.Max >= port.Min && epr.Max <= port.Max) {
|
||||
if (epr.Min >= port.Min && epr.Min <= port.Max) ||
|
||||
(epr.Max >= port.Min && epr.Max <= port.Max) {
|
||||
epr.Min = port.Max + 1
|
||||
epr.Max = port.Max + sum
|
||||
}
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
package room
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"m1k1o/neko_rooms/internal/types"
|
||||
"m1k1o/neko_rooms/internal/utils"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
)
|
||||
|
||||
type PullConfig struct {
|
||||
mu sync.Mutex
|
||||
cancel func()
|
||||
status types.PullStatus
|
||||
layers map[string]int
|
||||
}
|
||||
|
||||
func (pull *PullConfig) TryInitialize(cancel func()) bool {
|
||||
pull.mu.Lock()
|
||||
defer pull.mu.Unlock()
|
||||
|
||||
if pull.status.Active {
|
||||
cancel()
|
||||
return false
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
pull.cancel = cancel
|
||||
|
||||
pull.status = types.PullStatus{
|
||||
Active: true,
|
||||
Started: &now,
|
||||
Layers: []types.PullLayer{},
|
||||
Status: []string{},
|
||||
}
|
||||
|
||||
pull.layers = map[string]int{}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (pull *PullConfig) SetDone() {
|
||||
pull.mu.Lock()
|
||||
defer pull.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
pull.status.Active = false
|
||||
pull.status.Finished = &now
|
||||
}
|
||||
|
||||
func (pull *PullConfig) Stop() bool {
|
||||
pull.mu.Lock()
|
||||
defer pull.mu.Unlock()
|
||||
|
||||
if pull.status.Active {
|
||||
pull.cancel()
|
||||
}
|
||||
|
||||
return pull.status.Active
|
||||
}
|
||||
|
||||
func (pull *PullConfig) Get() types.PullStatus {
|
||||
pull.mu.Lock()
|
||||
defer pull.mu.Unlock()
|
||||
|
||||
return pull.status
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) PullStart(request types.PullStart) error {
|
||||
if in, _ := utils.ArrayIn(request.NekoImage, manager.config.NekoImages); !in {
|
||||
return fmt.Errorf("unknown neko image")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
if !manager.pull.TryInitialize(cancel) {
|
||||
return fmt.Errorf("pull is already in progess")
|
||||
}
|
||||
|
||||
// handle registry auth
|
||||
var opts dockerTypes.ImagePullOptions
|
||||
if request.RegistryUser != "" && request.RegistryPass != "" {
|
||||
authConfig := dockerTypes.AuthConfig{
|
||||
Username: request.RegistryUser,
|
||||
Password: request.RegistryPass,
|
||||
}
|
||||
|
||||
encodedJSON, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
opts = dockerTypes.ImagePullOptions{
|
||||
RegistryAuth: base64.URLEncoding.EncodeToString(encodedJSON),
|
||||
}
|
||||
}
|
||||
|
||||
reader, err := manager.client.ImagePull(ctx, request.NekoImage, opts)
|
||||
|
||||
if err != nil {
|
||||
manager.pull.SetDone()
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
data := scanner.Bytes()
|
||||
|
||||
layer := types.PullLayer{}
|
||||
if err := json.Unmarshal(data, &layer); err != nil {
|
||||
manager.pull.status.Status = append(
|
||||
manager.pull.status.Status,
|
||||
fmt.Sprintf("Error while parsing pull response: %s", err),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if layer.ProgressDetail != nil {
|
||||
// map layer id to slice index
|
||||
if index, ok := manager.pull.layers[layer.ID]; ok {
|
||||
manager.pull.status.Layers[index] = layer
|
||||
} else {
|
||||
manager.pull.layers[layer.ID] = len(manager.pull.layers)
|
||||
manager.pull.status.Layers = append(manager.pull.status.Layers, layer)
|
||||
}
|
||||
} else {
|
||||
manager.pull.status.Status = append(
|
||||
manager.pull.status.Status,
|
||||
layer.Status,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
manager.pull.status.Status = append(
|
||||
manager.pull.status.Status,
|
||||
fmt.Sprintf("Error while reading pull response: %s", err),
|
||||
)
|
||||
}
|
||||
|
||||
reader.Close()
|
||||
manager.pull.SetDone()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) PullStop() error {
|
||||
if !manager.pull.Stop() {
|
||||
return fmt.Errorf("pull is not in progess")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) PullStatus() types.PullStatus {
|
||||
return manager.pull.Get()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
const pprofPath = "/debug/pprof/"
|
||||
|
||||
func withPProf(router *chi.Mux) {
|
||||
router.Route(pprofPath, func(r chi.Router) {
|
||||
r.Get("/", pprof.Index)
|
||||
|
||||
r.Get("/{action}", func(w http.ResponseWriter, r *http.Request) {
|
||||
action := chi.URLParam(r, "action")
|
||||
|
||||
switch action {
|
||||
case "cmdline":
|
||||
pprof.Cmdline(w, r)
|
||||
case "profile":
|
||||
pprof.Profile(w, r)
|
||||
case "symbol":
|
||||
pprof.Symbol(w, r)
|
||||
case "trace":
|
||||
pprof.Trace(w, r)
|
||||
default:
|
||||
pprof.Handler(action).ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type logformatter struct {
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
func (l *logformatter) NewLogEntry(r *http.Request) middleware.LogEntry {
|
||||
req := map[string]interface{}{}
|
||||
|
||||
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
|
||||
req["id"] = reqID
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
req["scheme"] = scheme
|
||||
req["proto"] = r.Proto
|
||||
req["method"] = r.Method
|
||||
req["remote"] = r.RemoteAddr
|
||||
req["agent"] = r.UserAgent()
|
||||
req["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI)
|
||||
|
||||
fields := map[string]interface{}{}
|
||||
fields["req"] = req
|
||||
|
||||
return &logentry{
|
||||
fields: fields,
|
||||
logger: l.logger,
|
||||
}
|
||||
}
|
||||
|
||||
type logentry struct {
|
||||
logger zerolog.Logger
|
||||
fields map[string]interface{}
|
||||
errors []map[string]interface{}
|
||||
}
|
||||
|
||||
func (e *logentry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) {
|
||||
res := map[string]interface{}{}
|
||||
res["time"] = time.Now().UTC().Format(time.RFC1123)
|
||||
res["status"] = status
|
||||
res["bytes"] = bytes
|
||||
res["elapsed"] = float64(elapsed.Nanoseconds()) / 1000000.0
|
||||
|
||||
e.fields["res"] = res
|
||||
e.fields["module"] = "http"
|
||||
|
||||
if len(e.errors) > 0 {
|
||||
e.fields["errors"] = e.errors
|
||||
e.logger.Error().Fields(e.fields).Msgf("request failed (%d)", status)
|
||||
} else {
|
||||
e.logger.Debug().Fields(e.fields).Msgf("request complete (%d)", status)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *logentry) Panic(v interface{}, stack []byte) {
|
||||
err := map[string]interface{}{}
|
||||
err["message"] = fmt.Sprintf("%+v", v)
|
||||
err["stack"] = string(stack)
|
||||
|
||||
e.errors = append(e.errors, err)
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/m1k1o/neko-rooms/internal/config"
|
||||
"github.com/m1k1o/neko-rooms/internal/types"
|
||||
)
|
||||
|
||||
type ServerManagerCtx struct {
|
||||
logger zerolog.Logger
|
||||
router *chi.Mux
|
||||
server *http.Server
|
||||
config *config.Server
|
||||
}
|
||||
|
||||
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()
|
||||
router.Use(middleware.RequestID) // Create a request ID for each request
|
||||
|
||||
// get real users ip
|
||||
if config.Proxy {
|
||||
router.Use(middleware.RealIP)
|
||||
}
|
||||
|
||||
// add http logger
|
||||
router.Use(middleware.RequestLogger(&logformatter{logger}))
|
||||
router.Use(middleware.Recoverer) // Recover from panics without crashing server
|
||||
|
||||
// Basic CORS
|
||||
// for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing
|
||||
router.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||
}))
|
||||
|
||||
// mount pprof endpoint
|
||||
if config.PProf {
|
||||
withPProf(router)
|
||||
logger.Info().Msgf("with pprof endpoint at %s", pprofPath)
|
||||
}
|
||||
|
||||
//
|
||||
// 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,
|
||||
router: router,
|
||||
server: &http.Server{
|
||||
Addr: config.Bind,
|
||||
Handler: router,
|
||||
},
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerManagerCtx) Start() {
|
||||
if s.config.Cert != "" && s.config.Key != "" {
|
||||
go func() {
|
||||
if err := s.server.ListenAndServeTLS(s.config.Cert, s.config.Key); err != http.ErrServerClosed {
|
||||
s.logger.Panic().Err(err).Msg("unable to start https server")
|
||||
}
|
||||
}()
|
||||
s.logger.Info().Msgf("https listening on %s", s.server.Addr)
|
||||
} else {
|
||||
go func() {
|
||||
if err := s.server.ListenAndServe(); err != http.ErrServerClosed {
|
||||
s.logger.Panic().Err(err).Msg("unable to start http server")
|
||||
}
|
||||
}()
|
||||
s.logger.Info().Msgf("http listening on %s", s.server.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerManagerCtx) Shutdown() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
return s.server.Shutdown(ctx)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
type PullStart struct {
|
||||
NekoImage string `json:"neko_image"`
|
||||
RegistryUser string `json:"registry_user"`
|
||||
RegistryPass string `json:"registry_pass"`
|
||||
}
|
||||
|
||||
type PullLayer struct {
|
||||
Status string `json:"status"`
|
||||
ProgressDetail *struct {
|
||||
Current int `json:"current"`
|
||||
Total int `json:"total"`
|
||||
} `json:"progressDetail"`
|
||||
Progress string `json:"progress"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type PullStatus struct {
|
||||
Active bool `json:"active"`
|
||||
Started *time.Time `json:"started"`
|
||||
Layers []PullLayer `json:"layers"`
|
||||
Status []string `json:"status"`
|
||||
Finished *time.Time `json:"finished"`
|
||||
}
|
||||
|
||||
type PullManager interface {
|
||||
Start(request PullStart) error
|
||||
Stop() error
|
||||
Status() PullStatus
|
||||
}
|
||||
+12
-31
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"m1k1o/neko_rooms/internal/utils"
|
||||
"github.com/m1k1o/neko-rooms/internal/utils"
|
||||
)
|
||||
|
||||
var blacklistedEnvs = []string{
|
||||
@@ -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),
|
||||
@@ -246,30 +254,6 @@ type RoomMember struct {
|
||||
Muted bool `json:"muted"`
|
||||
}
|
||||
|
||||
type PullStart struct {
|
||||
NekoImage string `json:"neko_image"`
|
||||
RegistryUser string `json:"registry_user"`
|
||||
RegistryPass string `json:"registry_pass"`
|
||||
}
|
||||
|
||||
type PullLayer struct {
|
||||
Status string `json:"status"`
|
||||
ProgressDetail *struct {
|
||||
Current int `json:"current"`
|
||||
Total int `json:"total"`
|
||||
} `json:"progressDetail"`
|
||||
Progress string `json:"progress"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type PullStatus struct {
|
||||
Active bool `json:"active"`
|
||||
Started *time.Time `json:"started"`
|
||||
Layers []PullLayer `json:"layers"`
|
||||
Status []string `json:"status"`
|
||||
Finished *time.Time `json:"finished"`
|
||||
}
|
||||
|
||||
type RoomManager interface {
|
||||
Config() RoomsConfig
|
||||
List() ([]RoomEntry, error)
|
||||
@@ -284,8 +268,5 @@ type RoomManager interface {
|
||||
Start(id string) error
|
||||
Stop(id string) error
|
||||
Restart(id string) error
|
||||
|
||||
PullStart(request PullStart) error
|
||||
PullStop() error
|
||||
PullStatus() PullStatus
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,17 @@ import (
|
||||
"os/signal"
|
||||
"runtime"
|
||||
|
||||
"m1k1o/neko_rooms/internal/api"
|
||||
"m1k1o/neko_rooms/internal/config"
|
||||
"m1k1o/neko_rooms/internal/http"
|
||||
"m1k1o/neko_rooms/internal/room"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
const Header = `&34
|
||||
@@ -60,7 +63,6 @@ func init() {
|
||||
Configs: &Configs{
|
||||
Root: &config.Root{},
|
||||
Server: &config.Server{},
|
||||
API: &config.API{},
|
||||
Room: &config.Room{},
|
||||
},
|
||||
}
|
||||
@@ -98,7 +100,6 @@ func (i *Version) Details() string {
|
||||
type Configs struct {
|
||||
Root *config.Root
|
||||
Server *config.Server
|
||||
API *config.API
|
||||
Room *config.Room
|
||||
}
|
||||
|
||||
@@ -106,10 +107,12 @@ type MainCtx struct {
|
||||
Version *Version
|
||||
Configs *Configs
|
||||
|
||||
logger zerolog.Logger
|
||||
roomManager *room.RoomManagerCtx
|
||||
apiManager *api.ApiManagerCtx
|
||||
httpManager *http.HttpManagerCtx
|
||||
logger zerolog.Logger
|
||||
roomManager *room.RoomManagerCtx
|
||||
pullManager *pull.PullManagerCtx
|
||||
apiManager *api.ApiManagerCtx
|
||||
proxyManager *proxy.ProxyManagerCtx
|
||||
serverManager *server.ServerManagerCtx
|
||||
}
|
||||
|
||||
func (main *MainCtx) Preflight() {
|
||||
@@ -117,28 +120,52 @@ func (main *MainCtx) Preflight() {
|
||||
}
|
||||
|
||||
func (main *MainCtx) Start() {
|
||||
client, err := client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
main.logger.Panic().Err(err).Msg("unable to connect to docker client")
|
||||
} else {
|
||||
main.logger.Info().Msg("successfully connected to docker client")
|
||||
}
|
||||
|
||||
main.roomManager = room.New(
|
||||
client,
|
||||
main.Configs.Room,
|
||||
)
|
||||
|
||||
main.pullManager = pull.New(
|
||||
client,
|
||||
main.Configs.Room.NekoImages,
|
||||
)
|
||||
|
||||
main.apiManager = api.New(
|
||||
main.roomManager,
|
||||
main.Configs.API,
|
||||
main.pullManager,
|
||||
)
|
||||
|
||||
main.httpManager = http.New(
|
||||
main.apiManager,
|
||||
main.Configs.Server,
|
||||
main.proxyManager = proxy.New(
|
||||
client,
|
||||
main.Configs.Room.InstanceName,
|
||||
main.Configs.Room.WaitEnabled,
|
||||
)
|
||||
main.httpManager.Start()
|
||||
main.proxyManager.Start()
|
||||
|
||||
main.serverManager = server.New(
|
||||
main.apiManager,
|
||||
main.Configs.Room,
|
||||
main.Configs.Server,
|
||||
main.proxyManager,
|
||||
)
|
||||
main.serverManager.Start()
|
||||
}
|
||||
|
||||
func (main *MainCtx) Shutdown() {
|
||||
if err := main.httpManager.Shutdown(); err != nil {
|
||||
main.logger.Err(err).Msg("http manager shutdown with an error")
|
||||
} else {
|
||||
main.logger.Debug().Msg("http manager 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) {
|
||||
@@ -150,7 +177,7 @@ func (main *MainCtx) ServeCommand(cmd *cobra.Command, args []string) {
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
sig := <-quit
|
||||
|
||||
main.logger.Warn().Msgf("received %s, attempting graceful shutdown: \n", sig)
|
||||
main.logger.Warn().Msgf("received %s, attempting graceful shutdown.", sig)
|
||||
main.Shutdown()
|
||||
main.logger.Info().Msg("shutdown complete")
|
||||
}
|
||||
|
||||
@@ -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`.
|
||||
@@ -1,8 +1,8 @@
|
||||
version: "3.7"
|
||||
version: "3.5"
|
||||
|
||||
networks:
|
||||
default:
|
||||
attachable: "true"
|
||||
attachable: true
|
||||
name: "neko-rooms-traefik"
|
||||
|
||||
services:
|
||||
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
version: "3.7"
|
||||
version: "3.5"
|
||||
|
||||
networks:
|
||||
default:
|
||||
Reference in New Issue
Block a user