mirror of
https://github.com/m1k1o/neko-rooms.git
synced 2026-11-03 07:01:35 -06:00
Compare commits
17 Commits
v1.5.0
...
room_snapshots
| Author | SHA1 | Date | |
|---|---|---|---|
| c45f17fa1f | |||
| 1205dc9587 | |||
| fa401571e0 | |||
| 3e77a1848b | |||
| 7b71067da0 | |||
| 41facda0bc | |||
| 3850f3ddfa | |||
| d873ae9126 | |||
| 65d4a2b1a2 | |||
| 204300ff8a | |||
| bff4933778 | |||
| 8146dd14cb | |||
| e23f4ff89f | |||
| 9a05341188 | |||
| 83d3c52e0e | |||
| cc58d8fe52 | |||
| aab6311b83 |
+31
-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:
|
||||
@@ -477,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:
|
||||
|
||||
@@ -597,6 +597,30 @@ export interface 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
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* ConfigApi - axios parameter creator
|
||||
@@ -1254,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};
|
||||
@@ -1382,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);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1491,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));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1619,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
|
||||
|
||||
@@ -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,24 +99,10 @@
|
||||
<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>
|
||||
@@ -199,6 +204,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop, Watch } from 'vue-property-decorator'
|
||||
import RoomLink from './RoomLink.vue'
|
||||
|
||||
import {
|
||||
RoomStats,
|
||||
@@ -207,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)
|
||||
}
|
||||
@@ -227,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({
|
||||
@@ -252,6 +263,7 @@ export default class RoomInfo extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('room.status')
|
||||
async LoadStats() {
|
||||
this.statsLoading = true
|
||||
|
||||
@@ -272,6 +284,9 @@ export default class RoomInfo extends Vue {
|
||||
return 0
|
||||
})
|
||||
this.stats = stats
|
||||
this.statsErr = ""
|
||||
} catch (e: any) {
|
||||
this.statsErr = e
|
||||
} finally {
|
||||
this.statsLoading = false
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -551,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 }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<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" />
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -57,5 +57,6 @@ func (manager *ApiManagerCtx) Mount(r chi.Router) {
|
||||
r.Post("/stop", manager.roomGenericAction(manager.rooms.Stop))
|
||||
r.Post("/restart", manager.roomGenericAction(manager.rooms.Restart))
|
||||
r.Post("/recreate", manager.roomRecreate)
|
||||
r.Post("/snapshot", manager.roomSnapshot)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -150,3 +150,15 @@ func (manager *ApiManagerCtx) roomGenericAction(action func(id string) error) fu
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *ApiManagerCtx) roomSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||
roomId := chi.URLParam(r, "roomId")
|
||||
response, err := manager.rooms.Snapshot(roomId)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
@@ -31,6 +31,7 @@ type Room struct {
|
||||
NekoPrivilegedImages []string
|
||||
PathPrefix string
|
||||
Labels []string
|
||||
WaitEnabled bool
|
||||
|
||||
StorageEnabled bool
|
||||
StorageInternal string
|
||||
@@ -93,6 +94,11 @@ func (Room) Init(cmd *cobra.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("wait_enabled", true, "enable active waiting for the room")
|
||||
if err := viper.BindPFlag("wait_enabled", cmd.PersistentFlags().Lookup("wait_enabled")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Data
|
||||
|
||||
cmd.PersistentFlags().Bool("storage.enabled", true, "whether storage is enabled, where peristent containers data will be stored")
|
||||
@@ -199,6 +205,7 @@ func (s *Room) Set() {
|
||||
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")
|
||||
|
||||
@@ -36,17 +36,17 @@ func Generate(policies types.BrowserPolicyContent) (string, error) {
|
||||
)
|
||||
}
|
||||
|
||||
ExtensionInstallWhitelist := []interface{}{}
|
||||
ExtensionInstallAllowlist := []interface{}{}
|
||||
for _, e := range policies.Extensions {
|
||||
ExtensionInstallWhitelist = append(
|
||||
ExtensionInstallWhitelist,
|
||||
ExtensionInstallAllowlist = append(
|
||||
ExtensionInstallAllowlist,
|
||||
e.ID,
|
||||
)
|
||||
}
|
||||
|
||||
policiesTmpl["ExtensionInstallForcelist"] = ExtensionInstallForcelist
|
||||
policiesTmpl["ExtensionInstallWhitelist"] = ExtensionInstallWhitelist
|
||||
policiesTmpl["ExtensionInstallBlacklist"] = []interface{}{"*"}
|
||||
policiesTmpl["ExtensionInstallAllowlist"] = ExtensionInstallAllowlist
|
||||
policiesTmpl["ExtensionInstallBlocklist"] = []interface{}{"*"}
|
||||
|
||||
//
|
||||
// Developer Tools
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"HomepageLocation": "",
|
||||
"AutoFillEnabled": false,
|
||||
"AutofillAddressEnabled": false,
|
||||
"AutofillCreditCardEnabled": false,
|
||||
"BrowserSignin": 0,
|
||||
@@ -20,7 +18,7 @@
|
||||
"PromptForDownloadLocation": false,
|
||||
"BookmarkBarEnabled": false,
|
||||
"PasswordManagerEnabled": false,
|
||||
"URLBlacklist": [
|
||||
"URLBlocklist": [
|
||||
"file://*",
|
||||
"chrome://policy"
|
||||
],
|
||||
@@ -28,11 +26,11 @@
|
||||
"cjpalhdlnbpafiamejdnhcphjbkeiagm;https://clients2.google.com/service/update2/crx",
|
||||
"fjoaledfpmneenckfbpdfhkmimnjocfa;https://clients2.google.com/service/update2/crx"
|
||||
],
|
||||
"ExtensionInstallWhitelist": [
|
||||
"ExtensionInstallAllowlist": [
|
||||
"cjpalhdlnbpafiamejdnhcphjbkeiagm",
|
||||
"fjoaledfpmneenckfbpdfhkmimnjocfa"
|
||||
],
|
||||
"ExtensionInstallBlacklist": [
|
||||
"ExtensionInstallBlocklist": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
|
||||
+52
-2
@@ -6,7 +6,41 @@ import (
|
||||
"github.com/m1k1o/neko-rooms/internal/utils"
|
||||
)
|
||||
|
||||
func RoomNotFound(w http.ResponseWriter, r *http.Request) {
|
||||
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'
|
||||
|
||||
let lastAttempt = (new Date()).getTime()
|
||||
while(true) {
|
||||
try {
|
||||
lastAttempt = (new Date()).getTime()
|
||||
document.querySelector(".swal2-loader").style.visibility = 'visible'
|
||||
|
||||
await fetch("?wait")
|
||||
location.href = location.href
|
||||
} catch {
|
||||
let now = (new Date()).getTime()
|
||||
let diff = now - lastAttempt
|
||||
|
||||
// if the gap between last attempt and now
|
||||
// is gt 20s, do reconnect immediatly
|
||||
if ((now - lastAttempt) > 20*1000) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 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 RoomNotFound(w http.ResponseWriter, r *http.Request, waitEnabled bool) {
|
||||
utils.Swal2Response(w, `
|
||||
<div class="swal2-header">
|
||||
<div class="swal2-icon swal2-error">
|
||||
@@ -16,11 +50,19 @@ func RoomNotFound(w http.ResponseWriter, r *http.Request) {
|
||||
</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 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">
|
||||
@@ -30,8 +72,16 @@ func RoomNotRunning(w http.ResponseWriter, r *http.Request) {
|
||||
</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 RoomNotReady(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -23,23 +24,34 @@ type entry struct {
|
||||
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) *ProxyManagerCtx {
|
||||
func New(client *dockerClient.Client, instanceName string, waitEnabled bool) *ProxyManagerCtx {
|
||||
return &ProxyManagerCtx{
|
||||
logger: log.With().Str("module", "proxy").Logger(),
|
||||
logger: log.With().Str("module", "proxy").Logger(),
|
||||
waitChans: map[string]*wait{},
|
||||
|
||||
client: client,
|
||||
instanceName: instanceName,
|
||||
waitEnabled: waitEnabled,
|
||||
handlers: &prefixHandler[*entry]{},
|
||||
}
|
||||
}
|
||||
@@ -83,6 +95,17 @@ func (p *ProxyManagerCtx) Start() {
|
||||
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":
|
||||
@@ -222,28 +245,76 @@ func (p *ProxyManagerCtx) parseLabels(labels map[string]string) (enabled bool, p
|
||||
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(r.URL.Path)
|
||||
proxy, prefix, ok := p.handlers.Match(cleanPath)
|
||||
p.mu.RUnlock()
|
||||
|
||||
// if room not found
|
||||
if !ok {
|
||||
RoomNotFound(w, r)
|
||||
// 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 r.URL.Path == prefix {
|
||||
r.URL.Path += "/"
|
||||
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 {
|
||||
RoomNotRunning(w, r)
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -87,6 +89,21 @@ func (manager *RoomManagerCtx) FindByName(name string) (*types.RoomEntry, error)
|
||||
return manager.containerToEntry(*container)
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) IsValidImage(imageName string, imageList []string) bool {
|
||||
for _, _nekoImage := range imageList {
|
||||
if _nekoImage == imageName {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(_nekoImage, "*") {
|
||||
pattern := strings.Replace(_nekoImage, "*", "", -1)
|
||||
if strings.Contains(imageName, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (manager *RoomManagerCtx) Create(settings types.RoomSettings) (string, error) {
|
||||
if settings.Name != "" && !dockerNames.RestrictedNamePattern.MatchString(settings.Name) {
|
||||
return "", fmt.Errorf("invalid container name, must match %s", dockerNames.RestrictedNameChars)
|
||||
@@ -96,11 +113,11 @@ func (manager *RoomManagerCtx) Create(settings types.RoomSettings) (string, erro
|
||||
return "", fmt.Errorf("mounts cannot be specified, because storage is disabled or unavailable")
|
||||
}
|
||||
|
||||
if in, _ := utils.ArrayIn(settings.NekoImage, manager.config.NekoImages); !in {
|
||||
return "", fmt.Errorf("invalid neko image")
|
||||
if !manager.IsValidImage(settings.NekoImage, manager.config.NekoImages) {
|
||||
return "", fmt.Errorf("invalid neko image: %q. valid are: %q", settings.NekoImage, manager.config.NekoImages)
|
||||
}
|
||||
|
||||
isPrivilegedImage, _ := utils.ArrayIn(settings.NekoImage, manager.config.NekoPrivilegedImages)
|
||||
isPrivilegedImage := manager.IsValidImage(settings.NekoImage, manager.config.NekoPrivilegedImages)
|
||||
|
||||
// TODO: Check if path name exists.
|
||||
roomName := settings.Name
|
||||
@@ -637,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
|
||||
}
|
||||
@@ -96,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),
|
||||
@@ -263,4 +268,5 @@ type RoomManager interface {
|
||||
Start(id string) error
|
||||
Stop(id string) error
|
||||
Restart(id string) error
|
||||
Snapshot(id string) (*RoomSnapshot, error)
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ func (main *MainCtx) Start() {
|
||||
main.proxyManager = proxy.New(
|
||||
client,
|
||||
main.Configs.Room.InstanceName,
|
||||
main.Configs.Room.WaitEnabled,
|
||||
)
|
||||
main.proxyManager.Start()
|
||||
|
||||
|
||||
+10
-11
@@ -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..."
|
||||
|
||||
|
||||
Reference in New Issue
Block a user