Compare commits

...

93 commits

Author SHA1 Message Date
Miroslav Šedivý
b714663299
implicit hosting: request control prior interacting with the screen. #499 (#540) 2025-05-31 16:43:25 +02:00
Miroslav Šedivý
cb3d02fbb6 update release notes #537. 2025-05-29 01:56:56 +02:00
Miroslav Šedivý
0b4bf9e224 update release notes. 2025-05-29 01:56:22 +02:00
Miroslav Šedivý
f538103278 fix vivaldi install. 2025-05-29 00:43:18 +02:00
Miroslav Šedivý
84aa78bfcb
desktop: add clipboard command replacement. (#539)
we always keep running the last clipboard function as that is supposed to contain the clipboard data being pasted. as soon as a new one is spawn, the old one is stutdown.
2025-05-28 21:30:58 +02:00
Miroslav Šedivý
d530b759f5 update docs. 2025-05-28 20:16:27 +02:00
Miroslav Šedivý
875a61f10c
Update VirtualGL to a more recent version (#538)
Fixes #536
Fixes #537
Fixes #279

thanks to @TobyColeman
2025-05-28 20:14:39 +02:00
camerony
725d5396c0
Fix typo in quick-start guide for local network setup (#533) 2025-05-16 07:52:54 +02:00
Miroslav Šedivý
3dfd89ec39 forward pong messages, #510. 2025-04-30 21:34:42 +02:00
Miroslav Šedivý
0765352abd fix regression for #522. #523 2025-04-27 09:43:50 +02:00
Miroslav Šedivý
f145bd58c9
Fix mobile keyboard behavior (#522)
* keep only openMobileKeyboard, do not focus when touch device.

* do not test for hover when checking touch device.
2025-04-26 11:55:55 +02:00
Xiuming Chen
23820f6255
Fix build script for Apple Silicon MacOS. (#520) 2025-04-22 20:19:20 +02:00
Xiuming Chen
33ce337cd3
Fix docker volume mount error in build script. (#519) 2025-04-22 09:35:09 +02:00
Miroslav Šedivý
11cef143a3 temporarily disable waterfox build due to Cloudflare blocked download link. 2025-04-21 16:11:46 +02:00
Miroslav Šedivý
73e61de52e add SECURITY.md file. 2025-04-21 16:03:51 +02:00
Miroslav Šedivý
01112c5e8f clipboard: use UTF8_STRING. #517 2025-04-19 10:23:16 +02:00
James Clark
fc3b3a4dc6
fix: link to Examples in docs/v3 (#514) 2025-04-18 08:56:57 +02:00
Miroslav Šedivý
a8cc365e0f firefox: comment out xpinstall prefs. #512 2025-04-12 23:41:07 +02:00
Miroslav Šedivý
c9ca4e7144 docs firefox: guide on how to find extension ID. 2025-04-12 23:38:52 +02:00
Miroslav Šedivý
c1ccae4ac4 docs: info about supporting only one provider at a time. #505 2025-04-07 22:45:42 +02:00
Miroslav Šedivý
a46cb0536b update user agent for waterfox. they seem to block Wget/ user agents. 2025-04-07 22:43:08 +02:00
Miroslav Šedivý
b2219396dd disable proxy for local requests, #509. 2025-04-07 19:33:58 +02:00
Sean Ezrol
2ec35d9d0c
Adds an https condition to the healthcheck (#503)
* Adds an https condition to the healthcheck

* Fix V2 to V3 naming change

* Update other dockerfiles
2025-04-06 23:33:52 +02:00
Miroslav Šedivý
4eec843ed2
https: if we have legacy mode, we need to start local http server too. (#507) 2025-04-06 19:55:35 +02:00
Miroslav Šedivý
46e9b19e09 docs: legacy mode explained. 2025-04-06 17:29:56 +02:00
Miroslav Šedivý
007b55a32b add link to docs in V2 configuration warning message. 2025-04-06 17:22:34 +02:00
Miroslav Šedivý
3c787baa40 legacy: forward ws ping messages #506. 2025-04-06 17:19:22 +02:00
Miroslav Šedivý
b8bfcaf4bf legacy: fix logging. 2025-04-06 16:19:40 +02:00
Miroslav Šedivý
6c5cd1260d websocket: fix unwrap err. 2025-04-06 16:16:45 +02:00
Miroslav Šedivý
b783a9adbe docs: add overview of available encoders. 2025-04-05 22:37:56 +02:00
Miroslav Šedivý
81c259cdc9 docs: add nvidis gpu examples. #502 2025-04-05 21:45:19 +02:00
Miroslav Šedivý
972d16031e docs: add missing filetransfer config to v2. 2025-04-05 18:27:38 +02:00
Miroslav Šedivý
3a8a5c30ef docs: use ConfigurationTab that allows switching between yaml, env and cmd. 2025-04-05 18:25:04 +02:00
Miroslav Šedivý
0e3bcedcd4 update config options naming, move scripts to own folder. 2025-04-05 18:24:23 +02:00
Miroslav Šedivý
e3a1929f7f reword implicit hosting option #501. 2025-04-05 11:01:39 +02:00
Miroslav Šedivý
936794da31 include filetransfer in migration guide. 2025-04-04 22:57:58 +02:00
Miroslav Šedivý
1e91dcd7d9 add links to Availability Matrix. 2025-04-04 22:22:10 +02:00
Miroslav Šedivý
a032c9f42e dockerhub: allow only one workflow to run at a time. 2025-04-04 22:15:53 +02:00
Miroslav Šedivý
8b49fb7ca9 dockerhub ignore changes in webpage. 2025-04-04 22:12:56 +02:00
Miroslav Šedivý
da35b05c9c update docker images and include versioning in naming convention. 2025-04-04 22:02:04 +02:00
Miroslav Šedivý
4d6ad8e17d update docs for v2 migration. 2025-04-04 21:28:05 +02:00
Miroslav Šedivý
a75424d2f3 generate legacy pipelines when specifying codec. 2025-04-04 08:59:45 +02:00
Miroslav Šedivý
1c63b56b94 legacyhandler: key/button action log only in trace level. 2025-04-03 21:50:09 +02:00
Miroslav Šedivý
9eb4c36596 dockerhub use github.actor. 2025-04-03 21:46:52 +02:00
Miroslav Šedivý
1f7d12b388 add net.m1k1o.neko.api-version label to image. 2025-04-03 21:44:54 +02:00
Miroslav Šedivý
68e23fa8e7 fix server dev. 2025-04-03 14:17:59 +02:00
Miroslav Šedivý
ec44bf0e04 legacy handler set server bind. 2025-04-03 14:17:47 +02:00
Miroslav Šedivý
b383e1e24d add HelloGitHub Badge #419. 2025-04-02 22:36:12 +02:00
Miroslav Šedivý
0d62acfe04
add mobile keyboard icon. (#497) 2025-04-02 21:44:30 +02:00
Miroslav Šedivý
d7e2e73945
Scroll to chat on mobile (#496)
* WIP: scroll to chat proof of concept on mobile, #381.

* update screen size to 1024px.

* login screen position fixed, #381.

* show chat with a small video on portrait mode.
2025-04-02 21:27:11 +02:00
Miroslav Šedivý
a2ba96632c icelite should not be enabled by default. 2025-04-02 11:55:10 +02:00
Miroslav Šedivý
551a217c3f fix server dev build. 2025-04-02 01:09:14 +02:00
Miroslav Šedivý
e566095cda fix supervisrod to use a new server.static flag. 2025-04-02 01:08:17 +02:00
Miroslav Šedivý
fe94c999c6 update deploy pages. 2025-04-02 00:05:45 +02:00
Miroslav Šedivý
dd412dd37f lint client. 2025-04-02 00:05:37 +02:00
Miroslav Šedivý
8061d5b182 Revert "lint client."
This reverts commit 3918f8f1e2.
2025-04-02 00:05:06 +02:00
Miroslav Šedivý
31714f8448 add setup docker buildx for dockerhub. 2025-04-01 23:55:47 +02:00
Miroslav Šedivý
295bbfde44
Merge pull request #423 from m1k1o/v3
Version 3 - Phase 1
2025-04-01 23:48:43 +02:00
Miroslav Šedivý
231238d21c add v3.0.0. release notes. 2025-04-01 23:45:35 +02:00
Miroslav Šedivý
f3c4e53450 add edge tag to images. 2025-04-01 23:44:45 +02:00
Miroslav Šedivý
3918f8f1e2 lint client. 2025-04-01 22:06:51 +02:00
Miroslav Šedivý
4aecc87852 add list of Query parameters for UI. 2025-03-31 23:13:56 +02:00
Miroslav Šedivý
bed6e3b675 remove -d from go get. 2025-03-31 23:09:39 +02:00
Miroslav Šedivý
b6742c92ce update developer guide. 2025-03-31 23:09:26 +02:00
Miroslav Šedivý
bfb917bbea add customization section. 2025-03-31 22:39:44 +02:00
Miroslav Šedivý
2ee23a7de3 add repository structure. 2025-03-31 21:59:53 +02:00
Miroslav Šedivý
7d323f7a63 add browsers customization. 2025-03-31 21:59:40 +02:00
Miroslav Šedivý
a9b8ef94a2 docs browser is not starting with persistent profile. 2025-03-31 19:35:55 +02:00
Miroslav Šedivý
3d48850197 docs - no internet in the remote browser. 2025-03-31 19:30:49 +02:00
Miroslav Šedivý
3de71dc038 move docker & xorg-deps to utils. 2025-03-31 10:28:14 +02:00
Miroslav Šedivý
9e9cd6f486 Merge remote-tracking branch 'origin/master' into v3 2025-03-30 22:52:14 +02:00
Miroslav Šedivý
bd7f8a5126 update readme. 2025-03-30 22:48:41 +02:00
Miroslav Šedivý
5ce4820ef8 add anchor to docker images in the table. 2025-03-30 22:48:17 +02:00
Miroslav Šedivý
cab1e14b2b refactor build. 2025-03-30 22:04:12 +02:00
Miroslav Šedivý
2a714aa1ec fix build when no client. 2025-03-30 21:52:40 +02:00
Miroslav Šedivý
eba9821614 rename base image. 2025-03-30 21:52:34 +02:00
Miroslav Šedivý
8b1bf1e6b1 add no drm support hint to arm images. 2025-03-30 21:45:17 +02:00
Miroslav Šedivý
0933235060 run remmina even without profile. 2025-03-30 21:42:45 +02:00
Miroslav Šedivý
49015a81c4 fix brave arch. 2025-03-30 21:32:38 +02:00
Miroslav Šedivý
518f41f9ff update dockerhub workflow. 2025-03-30 20:36:15 +02:00
Miroslav Šedivý
409a5f1426 update docs. 2025-03-30 20:20:30 +02:00
Miroslav Šedivý
3e8169f9dc add docker image to migration. 2025-03-30 20:13:38 +02:00
Miroslav Šedivý
060b868826 update vivaldi to always download latest version. 2025-03-30 20:05:55 +02:00
Miroslav Šedivý
a5f06de65c update arm docs. 2025-03-30 19:47:27 +02:00
Miroslav Šedivý
2231b7a5a6 make vivaldi cross-arch. 2025-03-30 19:47:00 +02:00
Miroslav Šedivý
92ccd2d2d1 update names. 2025-03-30 19:16:44 +02:00
Miroslav Šedivý
40db8f6602 add gha cache. 2025-03-30 19:10:58 +02:00
Miroslav Šedivý
dfc2e5505c update names. 2025-03-30 18:30:05 +02:00
Miroslav Šedivý
c89b01fb87 update workflows. 2025-03-30 17:58:44 +02:00
Miroslav Šedivý
4150ac48f0 update workflows. 2025-03-30 17:45:18 +02:00
Miroslav Šedivý
95360b7da6 update workflows. 2025-03-30 17:42:31 +02:00
Miroslav Šedivý
cda281b78f update workflows. 2025-03-30 17:29:44 +02:00
Javier Pérez
212bf8a607
feat(i18n): enhance language management by saving user preference and auto detect browser language (#487)
* feat(i18n): enhance language management by saving user preference and detecting browser language

- Added a watcher to save the selected language to localStorage on change.
- Implemented browser language detection to set the default language based on user settings or browser preferences.

* use ~/utils/localstorage, remove setting savedLang in mounted as this is already set in i18.ts.

* do not repeat fallbackLocale and remove console log.

---------

Co-authored-by: Miroslav Šedivý <sedivy.miro@gmail.com>
2025-03-22 23:56:43 +01:00
173 changed files with 2639 additions and 1391 deletions

View file

@ -1,17 +1,21 @@
name: Build and Publish Client Artifacts name: Build Client
on: on:
workflow_call: workflow_call:
#pull_request: # Change to push when ready to deploy inputs:
# branches: with-artifact:
# - master required: false
# paths: type: boolean
# - client/** default: true
# - .github/workflows/client_build.yml description: |
If true, the build artifacts will be uploaded as a GitHub Actions artifact.
This is useful for debugging and testing purposes. If false, the artifacts
will not be uploaded. This is useful for test builds where you don't need
the artifacts.
jobs: jobs:
client_build: build-client:
name: Build and Publish Client Artifacts name: Build Client
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -35,6 +39,7 @@ jobs:
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: ${{ inputs.with-artifact }}
with: with:
name: client name: client
path: client/dist path: client/dist

View file

@ -1,4 +1,4 @@
name: Test Client Build name: Test Client
on: on:
pull_request: pull_request:
@ -6,28 +6,14 @@ on:
- master - master
paths: paths:
- client/** - client/**
- .github/workflows/client_build.yml
- .github/workflows/client_test.yml - .github/workflows/client_test.yml
jobs: jobs:
client_test: test-client:
name: Test Client Build name: Test Client
runs-on: ubuntu-latest uses: ./.github/workflows/client_build.yml
steps:
- uses: actions/checkout@v4
with: with:
fetch-depth: 0 # Do not upload artifacts for test builds
with-artifact: false
- uses: actions/setup-node@v4 secrets: inherit
with:
node-version: 18
cache: npm
cache-dependency-path: client/package-lock.json
- name: Install dependencies
working-directory: ./client
run: npm ci
- name: Build client
working-directory: ./client
run: npm run build

View file

@ -1,8 +1,11 @@
name: "build and push amd64 images to Docker Hub" name: Build and Push to Docker Hub
on: on:
push: push:
branches: [ master ] branches:
- master
paths-ignore:
- 'webpage/**'
# #
# Run this action periodically to keep browsers up-to-date # Run this action periodically to keep browsers up-to-date
# even if there is no activity in this repo. # even if there is no activity in this repo.
@ -10,68 +13,119 @@ on:
schedule: schedule:
- cron: "43 2 * * 1" - cron: "43 2 * * 1"
# allow only one workflow to run at a time
# and cancel in-progress jobs if a new one is triggered
concurrency:
group: "dockerhub"
cancel-in-progress: true
env: env:
DOCKER_IMAGE: m1k1o/neko DOCKER_IMAGE: m1k1o/neko
jobs: jobs:
build-base: build-base:
name: Base Image
runs-on: ubuntu-latest runs-on: ubuntu-latest
# #
# do not run on forks # do not run on forks
# #
if: github.repository_owner == 'm1k1o' if: github.repository_owner == 'm1k1o'
steps: steps:
- name: Check Out Repo - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker
uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.DOCKER_IMAGE }}
tags: |
type=raw,value=base
- name: Login to Docker Hub - name: Login to Docker Hub
run: | uses: docker/login-action@v3
docker login --username "${DOCKER_USERNAME}" --password-stdin "${DOCKER_REGISTRY}" <<< "${DOCKER_TOKEN}" with:
env: username: ${{ github.actor }}
DOCKER_REGISTRY: ${{ secrets.DOCKER_REGISTRY }} password: ${{ secrets.DOCKER_TOKEN }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
- name: Build base - name: Generate base Dockerfile
run: | run: go run utils/docker/main.go -i Dockerfile.tmpl -o Dockerfile
./build -b ${DOCKER_IMAGE}:base
docker push ${DOCKER_IMAGE}:base
build: - name: Build and push
uses: docker/build-push-action@v6
with:
context: ./
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-app:
name: App Image
runs-on: ubuntu-latest runs-on: ubuntu-latest
# #
# do not run on forks # do not run on forks
# #
if: github.repository_owner == 'm1k1o' if: github.repository_owner == 'm1k1o'
needs: [ build-base ] needs: build-base
strategy: strategy:
# Will build all images even if some fail. # Will build all images even if some fail.
fail-fast: false fail-fast: false
matrix: matrix:
tags: [ firefox, waterfox, chromium, google-chrome, ungoogled-chromium, microsoft-edge, brave, vivaldi, opera, tor-browser, remmina, vlc, xfce, kde ] tag:
env: - firefox
DOCKER_TAG: ${{ matrix.tags }} # Temporarily disabled due to Cloudflare blocked download link
#- waterfox
- chromium
- google-chrome
- ungoogled-chromium
- microsoft-edge
- brave
- vivaldi
- opera
- tor-browser
- remmina
- vlc
- xfce
- kde
steps: steps:
- name: Check Out Repo - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Extract metadata (tags, labels) for Docker
uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.DOCKER_IMAGE }}
tags: |
type=raw,value=latest,enable=${{ matrix.tag == 'firefox' }}
type=raw,value=${{ matrix.tag }}
- name: Login to Docker Hub - name: Login to Docker Hub
run: | uses: docker/login-action@v3
docker login --username "${DOCKER_USERNAME}" --password-stdin "${DOCKER_REGISTRY}" <<< "${DOCKER_TOKEN}" with:
env: username: ${{ github.actor }}
DOCKER_REGISTRY: ${{ secrets.DOCKER_REGISTRY }} password: ${{ secrets.DOCKER_TOKEN }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
- name: Build container - name: Build and push
run: | uses: docker/build-push-action@v6
./build -b ${DOCKER_IMAGE}:base -i ${DOCKER_IMAGE} with:
docker tag ${DOCKER_IMAGE}/${DOCKER_TAG} ${DOCKER_IMAGE}:${DOCKER_TAG} context: apps/${{ matrix.tag }}
docker push ${DOCKER_IMAGE}:${DOCKER_TAG} push: true
tags: ${{ steps.meta.outputs.tags }}
- name: Push latest tag labels: ${{ steps.meta.outputs.labels }}
if: ${{ matrix.tags == 'firefox' }} build-args: |
run: | BASE_IMAGE=${{ env.DOCKER_IMAGE }}:base
docker pull ${DOCKER_IMAGE}:${DOCKER_TAG} cache-from: type=gha
docker tag ${DOCKER_IMAGE}:${DOCKER_TAG} ${DOCKER_IMAGE}:latest cache-to: type=gha,mode=max
docker push ${DOCKER_IMAGE}:latest

57
.github/workflows/ghcr.yml vendored Normal file
View file

@ -0,0 +1,57 @@
name: Build and Push to GHCR
on:
push:
tags:
- 'v*'
jobs:
build-base:
name: Base Image
uses: ./.github/workflows/image_base.yml
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
secrets: inherit
build-app:
name: App Image
uses: ./.github/workflows/image_app.yml
needs: build-base
strategy:
# Will build all images even if some fail.
fail-fast: false
matrix:
include:
- name: firefox
platforms: linux/amd64,linux/arm64,linux/arm/v7
# Temporarily disabled due to Cloudflare blocked download link
#- name: waterfox
# platforms: linux/amd64
- name: chromium
platforms: linux/amd64,linux/arm64,linux/arm/v7
- name: google-chrome
platforms: linux/amd64
- name: ungoogled-chromium
platforms: linux/amd64
- name: microsoft-edge
platforms: linux/amd64
- name: brave
platforms: linux/amd64,linux/arm64
- name: vivaldi
platforms: linux/amd64,linux/arm64,linux/arm/v7
- name: opera
platforms: linux/amd64
- name: tor-browser
platforms: linux/amd64
- name: remmina
platforms: linux/amd64,linux/arm64,linux/arm/v7
- name: vlc
platforms: linux/amd64,linux/arm64,linux/arm/v7
- name: xfce
platforms: linux/amd64,linux/arm64,linux/arm/v7
- name: kde
platforms: linux/amd64,linux/arm64,linux/arm/v7
with:
name: ${{ matrix.name }}
platforms: ${{ matrix.platforms }}
secrets: inherit

View file

@ -1,44 +0,0 @@
name: "amd64 images"
on:
push:
tags:
- 'v*'
jobs:
build-base:
uses: ./.github/workflows/image_base.yml
with:
platforms: linux/amd64
dockerfile: Dockerfile
secrets:
GHCR_ACCESS_TOKEN: ${{ secrets.GHCR_ACCESS_TOKEN }}
build-apps:
uses: ./.github/workflows/image_app.yml
needs: [ build-base ]
strategy:
# Will build all images even if some fail.
fail-fast: false
matrix:
include:
- name: firefox
- name: waterfox
- name: chromium
- name: google-chrome
- name: ungoogled-chromium
- name: microsoft-edge
- name: brave
- name: vivaldi
- name: opera
- name: tor-browser
- name: remmina
- name: vlc
- name: xfce
- name: kde
with:
name: ${{ matrix.name }}
dockerfile: ${{ matrix.dockerfile }}
platforms: linux/amd64
secrets:
GHCR_ACCESS_TOKEN: ${{ secrets.GHCR_ACCESS_TOKEN }}

View file

@ -1,36 +0,0 @@
name: "arm64v8 and arm32v7 images"
on:
push:
tags:
- 'v*'
jobs:
build-base:
uses: ./.github/workflows/image_base.yml
with:
flavor: arm
platforms: linux/arm64,linux/arm/v7
dockerfile: Dockerfile
secrets:
GHCR_ACCESS_TOKEN: ${{ secrets.GHCR_ACCESS_TOKEN }}
build-apps:
uses: ./.github/workflows/image_app.yml
needs: [ build-base ]
strategy:
# Will build all images even if some fail.
fail-fast: false
matrix:
include:
- name: firefox
- name: chromium
- name: vlc
- name: xfce
with:
name: ${{ matrix.name }}
dockerfile: ${{ matrix.dockerfile }}
flavor: arm
platforms: linux/arm64,linux/arm/v7
secrets:
GHCR_ACCESS_TOKEN: ${{ secrets.GHCR_ACCESS_TOKEN }}

View file

@ -1,4 +1,4 @@
name: "intel gpu supported images" name: Build and Push to GHCR for Intel
on: on:
push: push:
@ -7,24 +7,26 @@ on:
jobs: jobs:
build-base: build-base:
name: Base Image
uses: ./.github/workflows/image_base.yml uses: ./.github/workflows/image_base.yml
with: with:
flavor: intel flavor: intel
platforms: linux/amd64 platforms: linux/amd64
dockerfile: Dockerfile.intel dockerfile: Dockerfile.intel
secrets: secrets: inherit
GHCR_ACCESS_TOKEN: ${{ secrets.GHCR_ACCESS_TOKEN }}
build-apps: build-app:
name: App Image
uses: ./.github/workflows/image_app.yml uses: ./.github/workflows/image_app.yml
needs: [ build-base ] needs: build-base
strategy: strategy:
# Will build all images even if some fail. # Will build all images even if some fail.
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- name: firefox - name: firefox
- name: waterfox # Temporarily disabled due to Cloudflare blocked download link
#- name: waterfox
- name: chromium - name: chromium
- name: google-chrome - name: google-chrome
- name: ungoogled-chromium - name: ungoogled-chromium
@ -39,8 +41,7 @@ jobs:
- name: kde - name: kde
with: with:
name: ${{ matrix.name }} name: ${{ matrix.name }}
dockerfile: ${{ matrix.dockerfile }}
flavor: intel flavor: intel
platforms: linux/amd64 platforms: ${{ matrix.platforms }}
secrets: dockerfile: ${{ matrix.dockerfile }}
GHCR_ACCESS_TOKEN: ${{ secrets.GHCR_ACCESS_TOKEN }} secrets: inherit

View file

@ -1,4 +1,4 @@
name: "nvidia gpu supported images" name: Build and Push to GHCR for Nvidia
on: on:
push: push:
@ -7,17 +7,18 @@ on:
jobs: jobs:
build-base: build-base:
name: Base Image
uses: ./.github/workflows/image_base.yml uses: ./.github/workflows/image_base.yml
with: with:
flavor: nvidia flavor: nvidia
platforms: linux/amd64 platforms: linux/amd64
dockerfile: Dockerfile.nvidia dockerfile: Dockerfile.nvidia
secrets: secrets: inherit
GHCR_ACCESS_TOKEN: ${{ secrets.GHCR_ACCESS_TOKEN }}
build-apps: build-app:
name: App Image
uses: ./.github/workflows/image_app.yml uses: ./.github/workflows/image_app.yml
needs: [ build-base ] needs: build-base
strategy: strategy:
# Will build all images even if some fail. # Will build all images even if some fail.
fail-fast: false fail-fast: false
@ -35,8 +36,7 @@ jobs:
dockerfile: Dockerfile.nvidia dockerfile: Dockerfile.nvidia
with: with:
name: ${{ matrix.name }} name: ${{ matrix.name }}
dockerfile: ${{ matrix.dockerfile }}
flavor: nvidia flavor: nvidia
platforms: linux/amd64 platforms: ${{ matrix.platforms }}
secrets: dockerfile: ${{ matrix.dockerfile }}
GHCR_ACCESS_TOKEN: ${{ secrets.GHCR_ACCESS_TOKEN }} secrets: inherit

View file

@ -1,4 +1,4 @@
name: Build and Publish Application Image name: Build App Image
on: on:
workflow_call: workflow_call:
@ -6,12 +6,7 @@ on:
name: name:
required: true required: true
type: string type: string
description: "The name of the application to build." description: "The name of the app to build."
dockerfile:
required: false
type: string
default: "Dockerfile"
description: "The Dockerfile to use for building the image."
flavor: flavor:
required: false required: false
type: string type: string
@ -22,17 +17,18 @@ on:
type: string type: string
default: "linux/amd64" default: "linux/amd64"
description: "The platforms to build for." description: "The platforms to build for."
secrets: dockerfile:
GHCR_ACCESS_TOKEN: required: false
required: true type: string
description: "GitHub Container Registry access token." default: "Dockerfile"
description: "The Dockerfile to use for building the image."
env: env:
FLAVOR_PREFIX: ${{ inputs.flavor && format('{0}-', inputs.flavor) || '' }} FLAVOR_PREFIX: ${{ inputs.flavor && format('{0}-', inputs.flavor) || '' }}
jobs: jobs:
build-app: build-app:
name: Build and Publish Application Image name: Build App Image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@ -52,6 +48,7 @@ jobs:
with: with:
images: ghcr.io/${{ github.repository }}/${{ env.FLAVOR_PREFIX }}${{ inputs.name }} images: ghcr.io/${{ github.repository }}/${{ env.FLAVOR_PREFIX }}${{ inputs.name }}
tags: | tags: |
type=edge,branch=master
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
@ -75,3 +72,5 @@ jobs:
build-args: | build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository }}/${{ env.FLAVOR_PREFIX }}base:sha-${{ github.sha }} BASE_IMAGE=ghcr.io/${{ github.repository }}/${{ env.FLAVOR_PREFIX }}base:sha-${{ github.sha }}
platforms: ${{ inputs.platforms || 'linux/amd64' }} platforms: ${{ inputs.platforms || 'linux/amd64' }}
cache-from: type=gha
cache-to: type=gha,mode=max

View file

@ -1,13 +1,8 @@
name: Build and Publish Base Image name: Build Base Image
on: on:
workflow_call: workflow_call:
inputs: inputs:
dockerfile:
required: false
type: string
default: "Dockerfile"
description: "The Dockerfile to use for building the image."
flavor: flavor:
required: false required: false
type: string type: string
@ -18,22 +13,24 @@ on:
type: string type: string
default: "linux/amd64" default: "linux/amd64"
description: "The platforms to build for." description: "The platforms to build for."
secrets: dockerfile:
GHCR_ACCESS_TOKEN: required: false
required: true type: string
description: "GitHub Container Registry access token." default: "Dockerfile"
description: "The Dockerfile to use for building the image."
env: env:
FLAVOR_PREFIX: ${{ inputs.flavor && format('{0}-', inputs.flavor) || '' }} FLAVOR_PREFIX: ${{ inputs.flavor && format('{0}-', inputs.flavor) || '' }}
jobs: jobs:
build-client: build-client:
name: Build Client Artifacts
uses: ./.github/workflows/client_build.yml uses: ./.github/workflows/client_build.yml
build-base: build-base:
name: Build and Publish Base Image name: Build Base Image
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ build-client ] needs: build-client
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -58,6 +55,7 @@ jobs:
with: with:
images: ghcr.io/${{ github.repository }}/${{ env.FLAVOR_PREFIX }}base images: ghcr.io/${{ github.repository }}/${{ env.FLAVOR_PREFIX }}base
tags: | tags: |
type=edge,branch=master
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
@ -73,7 +71,7 @@ jobs:
- name: Generate base Dockerfile - name: Generate base Dockerfile
env: env:
RUNTIME_DOCKERFILE: ${{ inputs.dockerfile || 'Dockerfile' }} RUNTIME_DOCKERFILE: ${{ inputs.dockerfile || 'Dockerfile' }}
run: go run docker/main.go -i Dockerfile.tmpl -o Dockerfile -client client/dist run: go run utils/docker/main.go -i Dockerfile.tmpl -o Dockerfile -client client/dist
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@ -83,3 +81,5 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ inputs.platforms || 'linux/amd64' }} platforms: ${{ inputs.platforms || 'linux/amd64' }}
cache-from: type=gha
cache-to: type=gha,mode=max

View file

@ -1,65 +0,0 @@
name: Build and Publish Server Artifacts
on:
workflow_call:
#pull_request: # Change to push when ready to deploy
# branches:
# - master
# paths:
# - server/**
# - .github/workflows/server_build.yml
jobs:
server_build:
name: Build and Publish Server Artifacts
runs-on: ubuntu-latest
strategy:
matrix:
variant:
- name: server-amd64
platform: linux/amd64
dockerfile: Dockerfile
- name: server-arm64
platform: linux/arm64
dockerfile: Dockerfile.bookworm
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# Temporary hotfix for the setup-qemu-action
# Ref: https://github.com/tonistiigi/binfmt/issues/240
with:
platforms: linux/amd64,linux/arm64
image: tonistiigi/binfmt:qemu-v7.0.0-28
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
id: build
uses: docker/build-push-action@v6
with:
context: ./server/
file: ./server/${{ matrix.variant.dockerfile }}
tags: ${{ matrix.variant.name }}
platforms: ${{ matrix.variant.platform }}
load: true
- name: Copy artifacts
run: |
container_id=$(docker create ${{ matrix.variant.name }})
mkdir -p artifacts/${{ matrix.variant.name }}
docker cp $container_id:/src/bin/. artifacts/${{ matrix.variant.name }}/
docker rm $container_id
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.variant.name }}
path: artifacts/${{ matrix.variant.name }}

View file

@ -1,4 +1,4 @@
name: Test Server Build name: Test Server
on: on:
pull_request: pull_request:
@ -9,8 +9,8 @@ on:
- .github/workflows/server_test.yml - .github/workflows/server_test.yml
jobs: jobs:
server_test: build-amd64:
name: Test Server Build name: Build amd64
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
@ -24,3 +24,21 @@ jobs:
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: ./server context: ./server
platforms: linux/amd64
build-arm64:
name: Build arm64
runs-on: ubuntu-24.04-arm
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build Docker image
uses: docker/build-push-action@v6
with:
context: ./server
platforms: linux/arm64

45
.github/workflows/webpage_build.yml vendored Normal file
View file

@ -0,0 +1,45 @@
name: Build Webpage
on:
workflow_call:
inputs:
with-artifact:
required: false
type: boolean
default: true
description: |
If true, the build artifacts will be uploaded as a GitHub Actions artifact.
This is useful for debugging and testing purposes. If false, the artifacts
will not be uploaded. This is useful for test builds where you don't need
the artifacts.
jobs:
build-webpage:
name: Build Webpage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 18
cache: npm
cache-dependency-path: webpage/package-lock.json
- name: Install dependencies
working-directory: ./webpage
run: npm ci
- name: Build webpage
working-directory: ./webpage
run: npm run build
- if: ${{ inputs.with-artifact }}
name: Upload artifacts
uses: actions/upload-pages-artifact@v3
with:
artifact-name: github-pages
path: ./webpage/build

View file

@ -1,50 +1,39 @@
name: Build and Deploy Webpage to GitHub Pages name: Build and Deploy Webpage to GitHub Pages
on: on:
# Runs on pushes targeting the default branch
push: push:
branches: branches:
- master - master
paths: paths:
- webpage/** - webpage/**
- .github/workflows/webpage_build.yml
- .github/workflows/webpage_deploy.yml - .github/workflows/webpage_deploy.yml
jobs: # Allows you to run this workflow manually from the Actions tab
webpage_build: workflow_dispatch:
name: Build and Deploy Webpage to GitHub Pages
runs-on: ubuntu-latest
steps: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 18
cache: npm
cache-dependency-path: webpage/package-lock.json
- name: Install dependencies
working-directory: ./webpage
run: npm ci
- name: Build webpage
working-directory: ./webpage
run: npm run build
- name: Upload Build Artifact
uses: actions/upload-pages-artifact@v3
with:
artifact-name: github-pages
path: ./webpage/build
webpage_deploy:
name: Deploy to GitHub Pages
needs: webpage_build
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions: permissions:
pages: write # to deploy to Pages contents: read
id-token: write # to verify the deployment originates from an appropriate source pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build-webpage:
name: Build Webpage
uses: ./.github/workflows/webpage_build.yml
secrets: inherit
deploy-webpage:
name: Deploy to GitHub Pages
needs: build-webpage
# Deploy to the github-pages environment # Deploy to the github-pages environment
environment: environment:

View file

@ -1,4 +1,4 @@
name: Test Webpage Build name: Test Webpage
on: on:
pull_request: pull_request:
@ -6,26 +6,14 @@ on:
- master - master
paths: paths:
- webpage/** - webpage/**
- .github/workflows/webpage_build.yml
- .github/workflows/webpage_test.yml - .github/workflows/webpage_test.yml
jobs: jobs:
webpage_test: test-webpage:
name: Test Webpage Build name: Test Webpage
runs-on: ubuntu-latest uses: ./.github/workflows/webpage_build.yml
steps:
- uses: actions/checkout@v4
with: with:
fetch-depth: 0 # Do not upload artifacts for test builds
- uses: actions/setup-node@v4 with-artifact: false
with: secrets: inherit
node-version: 18
cache: npm
cache-dependency-path: webpage/package-lock.json
- name: Install dependencies
working-directory: ./webpage
run: npm ci
- name: Build webpage
working-directory: ./webpage
run: npm run build

View file

@ -1,14 +1,17 @@
# This Dockerfile is pre-processed by the ./docker script, it is not meant to be used directly. # This Dockerfile is pre-processed by the ./utils/docker script, it is not meant to be used directly.
FROM ./runtime/xorg-deps/ AS xorg-deps
FROM ./server/ AS server FROM ./server/ AS server
FROM ./client/ AS client FROM ./client/ AS client
FROM ./utils/xorg-deps/ AS xorg-deps
FROM ./runtime/$RUNTIME_DOCKERFILE AS runtime FROM ./runtime/$RUNTIME_DOCKERFILE AS runtime
COPY --from=xorg-deps /usr/local/lib/xorg/modules/drivers/dummy_drv.so /usr/lib/xorg/modules/drivers/dummy_drv.so # tells neko-rooms which version of the API to use
COPY --from=xorg-deps /usr/local/lib/xorg/modules/input/neko_drv.so /usr/lib/xorg/modules/input/neko_drv.so LABEL net.m1k1o.neko.api-version=3
COPY --from=server /src/bin/plugins/ /etc/neko/plugins/ COPY --from=server /src/bin/plugins/ /etc/neko/plugins/
COPY --from=server /src/bin/neko /usr/bin/neko COPY --from=server /src/bin/neko /usr/bin/neko
COPY --from=client /src/dist/ /var/www COPY --from=client /src/dist/ /var/www
COPY --from=xorg-deps /usr/local/lib/xorg/modules/drivers/dummy_drv.so /usr/lib/xorg/modules/drivers/dummy_drv.so
COPY --from=xorg-deps /usr/local/lib/xorg/modules/input/neko_drv.so /usr/lib/xorg/modules/input/neko_drv.so
COPY config.yml /etc/neko/neko.yaml COPY config.yml /etc/neko/neko.yaml

118
README.md
View file

@ -1,6 +1,6 @@
<div align="center"> <div align="center">
<a href="https://github.com/m1k1o/neko" title="Neko's Github repository."> <a href="https://github.com/m1k1o/neko" title="Neko's Github repository.">
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/logo.png" width="400" height="auto"/> <img src="https://neko.m1k1o.net/img/logo.png" width="400" height="auto"/>
</a> </a>
<p align="center"> <p align="center">
<a href="https://github.com/m1k1o/neko/releases"> <a href="https://github.com/m1k1o/neko/releases">
@ -21,11 +21,14 @@
<a href="https://discord.gg/3U6hWpC"> <a href="https://discord.gg/3U6hWpC">
<img src="https://discordapp.com/api/guilds/665851821906067466/widget.png" alt="Chat on discord"> <img src="https://discordapp.com/api/guilds/665851821906067466/widget.png" alt="Chat on discord">
</a> </a>
<a href="https://hellogithub.com/repository/4536d4546af24196af3f08a023dfa007" target="_blank">
<img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=4536d4546af24196af3f08a023dfa007&claim_uid=0x19e4dJwD83aW2&theme=small" alt="FeaturedHelloGitHub" />
</a>
<a href="https://github.com/m1k1o/neko/actions"> <a href="https://github.com/m1k1o/neko/actions">
<img src="https://github.com/m1k1o/neko/actions/workflows/ghcr-amd.yml/badge.svg" alt="build"> <img src="https://github.com/m1k1o/neko/actions/workflows/ghcr.yml/badge.svg" alt="build">
</a> </a>
</p> </p>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/intro.gif" width="650" height="auto"/> <img src="https://neko.m1k1o.net/img/intro.gif" width="650" height="auto"/>
</div> </div>
# n.eko # n.eko
@ -83,47 +86,60 @@ Compared to clientless remote desktop gateway (e.g. [Apache Guacamole](https://g
### Supported browsers ### Supported browsers
<div align="center"> <div align="center">
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/firefox.svg" title="m1k1o/neko:firefox" width="60" height="auto"/> <a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#firefox">
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/google-chrome.svg" title="m1k1o/neko:google-chrome" width="60" height="auto"/> <img src="https://neko.m1k1o.net/img/icons/firefox.svg" title="ghcr.io/m1k1o/neko/firefox" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/chromium.svg" title="m1k1o/neko:chromium" width="60" height="auto"/> </a>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/microsoft-edge.svg" title="m1k1o/neko:microsoft-edge" width="60" height="auto"/> <a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#tor-browser">
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/brave.svg" title="m1k1o/neko:brave" width="60" height="auto"/> <img src="https://neko.m1k1o.net/img/icons/tor-browser.svg" title="ghcr.io/m1k1o/neko/tor-browser" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/vivaldi.svg" title="m1k1o/neko:vivaldi" width="60" height="auto"/> </a>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/opera.svg" title="m1k1o/neko:opera" width="60" height="auto"/> <a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#waterfox">
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/tor-browser.svg" title="m1k1o/neko:tor-browser" width="60" height="auto"/> <img src="https://neko.m1k1o.net/img/icons/waterfox.svg" title="ghcr.io/m1k1o/neko/waterfox" width="60" height="auto"/>
</a>
<a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#chromium">
<img src="https://neko.m1k1o.net/img/icons/chromium.svg" title="ghcr.io/m1k1o/neko/chromium" width="60" height="auto"/>
</a>
<a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#google-chrome">
<img src="https://neko.m1k1o.net/img/icons/google-chrome.svg" title="ghcr.io/m1k1o/neko/google-chrome" width="60" height="auto"/>
</a>
<a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#ungoogled-chromium">
<img src="https://neko.m1k1o.net/img/icons/ungoogled-chromium.svg" title="ghcr.io/m1k1o/neko/google-chrome" width="60" height="auto"/>
</a>
<a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#microsoft-edge">
<img src="https://neko.m1k1o.net/img/icons/microsoft-edge.svg" title="ghcr.io/m1k1o/neko/microsoft-edge" width="60" height="auto"/>
</a>
<a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#brave">
<img src="https://neko.m1k1o.net/img/icons/brave.svg" title="ghcr.io/m1k1o/neko/brave" width="60" height="auto"/>
</a>
<a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#vivaldi">
<img src="https://neko.m1k1o.net/img/icons/vivaldi.svg" title="ghcr.io/m1k1o/neko/vivaldi" width="60" height="auto"/>
</a>
<a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#opera">
<img src="https://neko.m1k1o.net/img/icons/opera.svg" title="ghcr.io/m1k1o/neko/opera" width="60" height="auto"/>
</a>
... see [all available images](https://neko.m1k1o.net/docs/v3/installation/docker-images)
</div> </div>
### Other programs ### Other applications
<div align="center"> <div align="center">
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/remmina.png" title="m1k1o/neko:remmina" width="60" height="auto"/> <a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#xfce">
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/vlc.svg" title="m1k1o/neko:vlc" width="60" height="auto"/> <img src="https://neko.m1k1o.net/img/icons/xfce.svg" title="ghcr.io/m1k1o/neko/xfce" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/xfce.svg" title="m1k1o/neko:xfce" width="60" height="auto"/> </a>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/kde.svg" title="m1k1o/neko:kde" width="60" height="auto"/> <a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#kde">
<img src="https://neko.m1k1o.net/img/icons/kde.svg" title="ghcr.io/m1k1o/neko/kde" width="60" height="auto"/>
</a>
<a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#remmina">
<img src="https://neko.m1k1o.net/img/icons/remmina.svg" title="ghcr.io/m1k1o/neko/remmina" width="60" height="auto"/>
</a>
<a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#vlc">
<img src="https://neko.m1k1o.net/img/icons/vlc.svg" title="ghcr.io/m1k1o/neko/vlc" width="60" height="auto"/>
</a>
... others in <a href="https://github.com/m1k1o/neko-apps">m1k1o/neko-apps</a> ... others in <a href="https://github.com/m1k1o/neko-apps">m1k1o/neko-apps</a>
</div> </div>
### Features ### Why neko?
* Text Chat (With basic markdown support, discord flavor)
* Admin users (Kick, Ban & Force Give/Release Controls, Lock room)
* Clipboard synchronization (on [supported browsers](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText))
* Emote overlay
* Ignore user (chat and emotes)
* Persistent settings
* Automatic Login with custom url args. (add `?usr=<your-user-name>&pwd=<room-pass>` to the url.)
* Broadcasting room content using RTMP (to e.g. twitch or youtube...)
* Bidirectional file transfer (if enabled)
<div align="center">
With `NEKO_FILE_TRANSFER_ENABLED=true`:
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/file-transfer.gif" width="650" height="auto"/>
</div>
### Why n.eko?
I like cats 🐱 (`Neko` is the Japanese word for cat), I'm a weeb/nerd. I like cats 🐱 (`Neko` is the Japanese word for cat), I'm a weeb/nerd.
@ -131,28 +147,26 @@ I like cats 🐱 (`Neko` is the Japanese word for cat), I'm a weeb/nerd.
## Multiple rooms ## Multiple rooms
For n.eko room management software, visit [neko-rooms](https://github.com/m1k1o/neko-rooms). For neko room management software, visit [neko-rooms](https://github.com/m1k1o/neko-rooms).
It also offers zero-knowledge [installation script (with HTTPS and Traefik)](https://github.com/m1k1o/neko-rooms/#zero-knowledge-installation-with-https-and-traefik). It also offers [Zero-knowledge installation (with HTTPS)](https://github.com/m1k1o/neko-rooms/?tab=readme-ov-file#zero-knowledge-installation-with-https).
## Documentation ## Documentation
* [Getting Started](https://neko.m1k1o.net/#/getting-started/) Full documentation is available at [neko.m1k1o.net](https://neko.m1k1o.net/). Key sections include:
* [Quick Start](https://neko.m1k1o.net/#/getting-started/quick-start)
* [Examples](https://neko.m1k1o.net/#/getting-started/examples)
* [Reverse Proxy](https://neko.m1k1o.net/#/getting-started/reverse-proxy)
* [Configuration](https://neko.m1k1o.net/#/getting-started/configuration)
* [Troubleshooting](https://neko.m1k1o.net/#/getting-started/troubleshooting)
* [Mobile Support](https://neko.m1k1o.net/#/mobile-support)
* [Contributing](https://neko.m1k1o.net/#/contributing)
* [Non Goals](https://neko.m1k1o.net/#/non-goals)
* [Technologies](https://neko.m1k1o.net/#/technologies)
* [Changelog](https://neko.m1k1o.net/#/changelog)
## How to contribute? How to build? - [Migration from V2](https://neko.m1k1o.net/docs/v3/migration-from-v2)
- [Getting Started](https://neko.m1k1o.net/docs/v3/quick-start)
- [Installation](https://neko.m1k1o.net/docs/v3/installation)
- [Examples](https://neko.m1k1o.net/docs/v3/installation/examples)
- [Configuration](https://neko.m1k1o.net/docs/v3/configuration)
- [Frequently Asked Questions](https://neko.m1k1o.net/docs/v3/faq)
- [Troubleshooting](https://neko.m1k1o.net/docs/v3/troubleshooting)
Navigate to [.docker](.docker) folder for further information. ## How to Contribute
Contributions are welcome! Check the [Contributing Guide](https://neko.m1k1o.net/contributing) for details.
## Support ## Support
If you want to support this project, you can do it [here](https://github.com/sponsors/m1k1o). If you find Neko useful, consider supporting the project via [GitHub Sponsors](https://github.com/sponsors/m1k1o).

19
SECURITY.md Normal file
View file

@ -0,0 +1,19 @@
# Security Policy
## Reporting a Vulnerability
If there are any vulnerabilities in **m1k1o/neko**, don't hesitate to _report them_.
1. Send an email to `security@m1k1o.net`.
2. Describe the vulnerability.
If you have a fix, that is most welcome -- please attach or summarize it in your message!
3. We will evaluate the vulnerability and, if necessary, release a fix or mitigating steps to address it. We will contact you to let you know the outcome, and will credit you in the report.
Please **do not disclose the vulnerability publicly** until a fix is released!
4. Once we have either a) published a fix, or b) declined to address the vulnerability for whatever reason, you are free to publicly disclose it.
We appreciate your help in keeping Neko secure.

View file

@ -1,12 +1,13 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
RUN set -eux; apt-get update; \ RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends apt-transport-https curl openbox; \ apt-get install -y --no-install-recommends apt-transport-https curl openbox; \
# #
# install brave browser # install brave browser
ARCH=$(dpkg --print-architecture); \
curl -fsSLo /usr/share/keyrings/brave-browser-archive-keyring.gpg https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg; \ curl -fsSLo /usr/share/keyrings/brave-browser-archive-keyring.gpg https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg; \
echo "deb [signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg arch=amd64] https://brave-browser-apt-release.s3.brave.com/ stable main" \ echo "deb [signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg arch=${ARCH}] https://brave-browser-apt-release.s3.brave.com/ stable main" \
| tee /etc/apt/sources.list.d/brave-browser-release.list; \ | tee /etc/apt/sources.list.d/brave-browser-release.list; \
apt-get update; \ apt-get update; \
apt-get install -y --no-install-recommends brave-browser; \ apt-get install -y --no-install-recommends brave-browser; \

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:nvidia-base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/nvidia-base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
RUN set -eux; apt-get update; \ RUN set -eux; apt-get update; \

View file

@ -12,9 +12,11 @@ command=/bin/entrypoint.sh /usr/bin/brave-browser
--enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization --enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization
--ignore-gpu-blocklist --ignore-gpu-blocklist
--disable-seccomp-filter-sandbox --disable-seccomp-filter-sandbox
--use-gl=egl --use-angle=vulkan
--disable-software-rasterizer --disable-software-rasterizer
--disable-dev-shm-usage --disable-dev-shm-usage
--disable-vulkan-surface
--enable-unsafe-webgpu
stopsignal=INT stopsignal=INT
autorestart=true autorestart=true
priority=800 priority=800

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
# #

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/nvidia-base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
# #

View file

@ -12,9 +12,11 @@ command=/bin/entrypoint.sh /usr/bin/chromium
--enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization --enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization
--ignore-gpu-blocklist --ignore-gpu-blocklist
--disable-seccomp-filter-sandbox --disable-seccomp-filter-sandbox
--use-gl=egl --use-angle=vulkan
--disable-software-rasterizer --disable-software-rasterizer
--disable-dev-shm-usage --disable-dev-shm-usage
--disable-vulkan-surface
--enable-unsafe-webgpu
stopsignal=INT stopsignal=INT
autorestart=true autorestart=true
priority=800 priority=800

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
# #
@ -10,7 +10,7 @@ RUN set -eux; apt-get update; \
# #
# install firefox-esr for armhf # install firefox-esr for armhf
apt-get install -y --no-install-recommends firefox-esr; \ apt-get install -y --no-install-recommends firefox-esr; \
ln -s /usr/bin/firefox-esr /usr/bin/firefox; \ ln -s /usr/lib/firefox-esr /usr/lib/firefox; \
# #
# install extensions # install extensions
mkdir -p /usr/lib/firefox-esr/distribution/extensions; \ mkdir -p /usr/lib/firefox-esr/distribution/extensions; \

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/nvidia-base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
ARG SRC_URL="https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US" ARG SRC_URL="https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US"

View file

@ -15,8 +15,8 @@ lockPref("plugins.hide_infobar_for_missing_plugin", true);
lockPref("profile.allow_automigration", false); lockPref("profile.allow_automigration", false);
lockPref("signon.prefillForms", false); lockPref("signon.prefillForms", false);
lockPref("signon.rememberSignons", false); lockPref("signon.rememberSignons", false);
lockPref("xpinstall.enabled", false); //lockPref("xpinstall.enabled", false);
lockPref("xpinstall.whitelist.required", true); //lockPref("xpinstall.whitelist.required", true);
lockPref("browser.download.manager.retention", 0); lockPref("browser.download.manager.retention", 0);
lockPref("browser.download.folderList", 2); lockPref("browser.download.folderList", 2);
lockPref("browser.download.forbid_open_with", true); lockPref("browser.download.forbid_open_with", true);

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
ARG SRC_URL="https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb" ARG SRC_URL="https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb"

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:nvidia-base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/nvidia-base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
# latest working version with EGL: 111.0.5563.146, revert when resolved # latest working version with EGL: 111.0.5563.146, revert when resolved

View file

@ -12,9 +12,11 @@ command=/bin/entrypoint.sh /usr/bin/google-chrome
--enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization --enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization
--ignore-gpu-blocklist --ignore-gpu-blocklist
--disable-seccomp-filter-sandbox --disable-seccomp-filter-sandbox
--use-gl=egl --use-angle=vulkan
--disable-software-rasterizer --disable-software-rasterizer
--disable-dev-shm-usage --disable-dev-shm-usage
--disable-vulkan-surface
--enable-unsafe-webgpu
stopsignal=INT stopsignal=INT
autorestart=true autorestart=true
priority=800 priority=800

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
# #

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
ARG API_URL="https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable/" ARG API_URL="https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable/"

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
ARG API_URL="https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable/" ARG API_URL="https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable/"

View file

@ -12,9 +12,11 @@ command=/bin/entrypoint.sh /usr/bin/microsoft-edge
--enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization --enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization
--ignore-gpu-blocklist --ignore-gpu-blocklist
--disable-seccomp-filter-sandbox --disable-seccomp-filter-sandbox
--use-gl=egl --use-angle=vulkan
--disable-software-rasterizer --disable-software-rasterizer
--disable-dev-shm-usage --disable-dev-shm-usage
--disable-vulkan-surface
--enable-unsafe-webgpu
stopsignal=INT stopsignal=INT
autorestart=true autorestart=true
priority=800 priority=800

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
ARG API_URL="https://download5.operacdn.com/pub/opera/desktop/" ARG API_URL="https://download5.operacdn.com/pub/opera/desktop/"

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
# install remmina # install remmina

View file

@ -1,5 +1,4 @@
#!/bin/bash #!/bin/bash
set -u
err() { err() {
echo "ERROR: $*" >&2 echo "ERROR: $*" >&2
@ -16,8 +15,7 @@ if [[ -n "$REMMINA_PROFILE" ]]; then
exec remmina -c "$profile" exec remmina -c "$profile"
fi fi
[[ -z "$REMMINA_URL" ]] && err "Neither 'REMMINA_PROFILE' nor 'REMMINA_URL' found in env vars" if [[ ! -z "$REMMINA_URL" ]]; then
readarray -t arr < <( echo -n "$REMMINA_URL" | perl -pe 's|^(\w+)\:\/\/(?:([^:]+)(?::([^@]+))?@)?(.*)$|\1\n\2\n\3\n\4|' ) readarray -t arr < <( echo -n "$REMMINA_URL" | perl -pe 's|^(\w+)\:\/\/(?:([^:]+)(?::([^@]+))?@)?(.*)$|\1\n\2\n\3\n\4|' )
proto="${arr[0]}" proto="${arr[0]}"
user="${arr[1]}" user="${arr[1]}"
@ -35,4 +33,10 @@ remmina --set-option server="$host" --update-profile "$profile"
# remmina --set-option window_maximize=1 --update-profile "$profile" # remmina --set-option window_maximize=1 --update-profile "$profile"
# remmina --set-option scale=1 --update-profile "$profile" # remmina --set-option scale=1 --update-profile "$profile"
echo "Running remmina with URL $REMMINA_URL"
exec remmina -c "$profile" exec remmina -c "$profile"
fi
echo "Running remmina without connection profile"
exec remmina

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
# #

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
ARG API_URL="https://api.github.com/repos/macchrome/linchrome/releases/latest" ARG API_URL="https://api.github.com/repos/macchrome/linchrome/releases/latest"

View file

@ -1,17 +1,14 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
ARG VIVALDI_VERSION="5.3.2679.34-1"
# TODO: Get chromium version from vivaldi
ARG CHROMIUM_VERSION="102.0.5005.72"
# #
# install vivaldi # install vivaldi
SHELL ["/bin/bash", "-c"] SHELL ["/bin/bash", "-c"]
RUN set -eux; apt-get update; \ RUN set -eux; apt-get update; \
wget -O /tmp/vivaldi.deb "https://downloads.vivaldi.com/stable/vivaldi-stable_${VIVALDI_VERSION}_amd64.deb"; \ ARCH=$(dpkg --print-architecture); \
apt-get install -y --no-install-recommends wget unzip xz-utils jq openbox /tmp/vivaldi.deb; \ wget -O /tmp/vivaldi.deb "https://downloads.vivaldi.com/stable/vivaldi-stable_${ARCH}.deb"; \
/opt/vivaldi/update-ffmpeg; \ apt-get install -y --no-install-recommends wget unzip xz-utils jq openbox; \
apt install -y --no-install-recommends /tmp/vivaldi.deb; \
# #
# install latest version of uBlock Origin and SponsorBlock for YouTube # install latest version of uBlock Origin and SponsorBlock for YouTube
EXTENSIONS_DIR="/usr/share/chromium/extensions"; \ EXTENSIONS_DIR="/usr/share/chromium/extensions"; \
@ -22,7 +19,7 @@ RUN set -eux; apt-get update; \
mkdir -p "${EXTENSIONS_DIR}"; \ mkdir -p "${EXTENSIONS_DIR}"; \
for EXT_ID in "${EXTENSIONS[@]}"; \ for EXT_ID in "${EXTENSIONS[@]}"; \
do \ do \
EXT_URL="https://clients2.google.com/service/update2/crx?response=redirect&nacl_arch=x86-64&prodversion=${CHROMIUM_VERSION}&acceptformat=crx2,crx3&x=id%3D${EXT_ID}%26installsource%3Dondemand%26uc"; \ EXT_URL="https://clients2.google.com/service/update2/crx?response=redirect&prodversion=100&acceptformat=crx2,crx3&x=id%3D${EXT_ID}%26installsource%3Dondemand%26uc"; \
EXT_PATH="${EXTENSIONS_DIR}/${EXT_ID}.crx"; \ EXT_PATH="${EXTENSIONS_DIR}/${EXT_ID}.crx"; \
wget -O "${EXT_PATH}" "${EXT_URL}"; \ wget -O "${EXT_PATH}" "${EXT_URL}"; \
EXT_VERSION="$(unzip -p "${EXT_PATH}" manifest.json 2>/dev/null | jq -r ".version")"; \ EXT_VERSION="$(unzip -p "${EXT_PATH}" manifest.json 2>/dev/null | jq -r ".version")"; \

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
# #

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
ARG SRC_URL="https://cdn1.waterfox.net/waterfox/releases/latest/linux" ARG SRC_URL="https://cdn1.waterfox.net/waterfox/releases/latest/linux"
@ -10,7 +10,7 @@ RUN set -eux; apt-get update; \
xz-utils bzip2 libgtk-3-0 libdbus-glib-1-2; \ xz-utils bzip2 libgtk-3-0 libdbus-glib-1-2; \
# #
# fetch latest release # fetch latest release
wget -O /tmp/waterfox-setup.tar.bz2 "${SRC_URL}"; \ wget --user-agent="Mozilla/5.0" -O /tmp/waterfox-setup.tar.bz2 "${SRC_URL}"; \
mkdir /usr/lib/waterfox; \ mkdir /usr/lib/waterfox; \
tar -xjf /tmp/waterfox-setup.tar.bz2 -C /usr/lib; \ tar -xjf /tmp/waterfox-setup.tar.bz2 -C /usr/lib; \
rm -f /tmp/waterfox-setup.tar.bz2; \ rm -f /tmp/waterfox-setup.tar.bz2; \

View file

@ -15,8 +15,8 @@ lockPref("plugins.hide_infobar_for_missing_plugin", true);
lockPref("profile.allow_automigration", false); lockPref("profile.allow_automigration", false);
lockPref("signon.prefillForms", false); lockPref("signon.prefillForms", false);
lockPref("signon.rememberSignons", false); lockPref("signon.rememberSignons", false);
lockPref("xpinstall.enabled", false); //lockPref("xpinstall.enabled", false);
lockPref("xpinstall.whitelist.required", true); //lockPref("xpinstall.whitelist.required", true);
lockPref("browser.download.manager.retention", 0); lockPref("browser.download.manager.retention", 0);
lockPref("browser.download.folderList", 2); lockPref("browser.download.folderList", 2);
lockPref("browser.download.forbid_open_with", true); lockPref("browser.download.forbid_open_with", true);

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
# #

72
build
View file

@ -2,39 +2,57 @@
set -e set -e
cd "$(dirname "$0")" cd "$(dirname "$0")"
#
# This script builds the neko base image and all the applications
#
# disable buildx because of https://github.com/docker/buildx/issues/847 # disable buildx because of https://github.com/docker/buildx/issues/847
# if you want to use buildx, set USE_BUILDX=1
if [ -z "$USE_BUILDX" ]; then if [ -z "$USE_BUILDX" ]; then
USE_BUILDX=0 USE_BUILDX=0
fi fi
# check if docker buildx is available, its not docker-buildx command but rather subcommand of docker # check if docker buildx is available, its not docker-buildx command but rather subcommand of docker
if [ -z "$USE_BUILDX" ] && [ -x "$(command -v docker)" ]; then #if [ -z "$USE_BUILDX" ] && [ -x "$(command -v docker)" ]; then
if docker buildx version >/dev/null 2>&1; then # if docker buildx version >/dev/null 2>&1; then
USE_BUILDX=1 # USE_BUILDX=1
fi # fi
fi #fi
#
# This script builds the neko base image and all the applications
#
function log() { function log() {
echo "$(date +'%Y-%m-%d %H:%M:%S') - [NEKO] - $1" > /dev/stderr echo "$(date +'%Y-%m-%d %H:%M:%S') - [NEKO] - $1" > /dev/stderr
} }
function help() { function help() {
echo "Usage: $0" echo "Usage: $0 [options] [image]"
echo " -p, --platform : The platform (default: linux/amd64)" echo
echo "Options:"
echo " -p, --platform : The platform (default: system architecture)"
echo " -r, --repository : The repository prefix (default: ghcr.io/m1k1o/neko)" echo " -r, --repository : The repository prefix (default: ghcr.io/m1k1o/neko)"
echo " -t, --tag : The image tag, can be specified multiple times, if not specified" echo " -t, --tag : The image tag, can be specified multiple times, if not specified"
echo " uses 'latest' and if available, current git semver tag (v*.*.*)" echo " uses 'latest' and if available, current git semver tag (v*.*.*)"
echo " -f, --flavor : The flavor, if not specified, builds without flavor" echo " -f, --flavor : The flavor, if not specified, builds without flavor"
echo " -b, --base : The base image name (default: <repository>[<flavor>-]base:<tag>)" echo " -b, --base_image : The base image name (default: <repository>[<flavor>-]base:<tag>)"
echo " -a, --app : The app to build, if not specified, builds the base image" echo " -a, --application : The app to build, if not specified, builds the base image"
echo " -y, --yes : Skip confirmation prompts" echo " -y, --yes : Skip confirmation prompts"
echo " --no-cache : Build without docker cache" echo " --no-cache : Build without docker cache"
echo " --push : Push the image to the registry after building" echo " --push : Push the image to the registry after building"
echo " -h, --help : Show this help message" echo " -h, --help : Show this help message"
echo
echo "Positional arguments:"
echo " <image> : The image name, if not specified, uses the full image name"
echo " in the format <repository>/<flavor>-<application>:<tag>"
echo " Example: ghcr.io/m1k1o/neko/nvidia-firefox:latest"
echo " You can override any of the above options by specifying them"
echo " after the image name."
echo
echo "Environment variables:"
echo " USE_BUILDX : Set to 1 to use docker buildx instead of docker build"
echo " (default: 0)"
echo " CLIENT_DIST : The client dist file to use, if not specified, builds them"
echo " from the source code."
echo " (options) : Options can be specified as environment variables, for example:"
echo " PLATFORM=linux/arm64 $0 --repository ghcr.io/m1k1o/neko"
} }
FULL_IMAGE="" FULL_IMAGE=""
@ -44,8 +62,8 @@ while [[ "$#" -gt 0 ]]; do
--repository|-r) REPOSITORY="$2"; shift ;; --repository|-r) REPOSITORY="$2"; shift ;;
--tag|-t) TAGS+=("$2"); TAG="$2"; shift ;; --tag|-t) TAGS+=("$2"); TAG="$2"; shift ;;
--flavor|-f) FLAVOR="$2"; shift ;; --flavor|-f) FLAVOR="$2"; shift ;;
--base|-b) BASE_IMAGE="$2"; shift ;; --base_image|-b) BASE_IMAGE="$2"; shift ;;
--app|-a) APPLICATION="$2"; shift ;; --application|-a) APPLICATION="$2"; shift ;;
--yes|-y) YES=1 ;; --yes|-y) YES=1 ;;
--no-cache) NO_CACHE="--no-cache" log "Building without cache" ;; --no-cache) NO_CACHE="--no-cache" log "Building without cache" ;;
--push) PUSH=1 ;; --push) PUSH=1 ;;
@ -131,17 +149,12 @@ function build_image() {
local APPLICATION_IMAGE="$1" local APPLICATION_IMAGE="$1"
shift shift
local FULL_TAGS=()
# if the image name starts with local/, just use the tag as is
if [[ "$APPLICATION_IMAGE" == *"local/"* ]]; then
FULL_TAGS=("$APPLICATION_IMAGE")
else
# get list of tags in full format: <image>:<tag> # get list of tags in full format: <image>:<tag>
local IMAGE_NO_TAG="${APPLICATION_IMAGE%:*}" local IMAGE_NO_TAG="${APPLICATION_IMAGE%:*}"
local FULL_TAGS=()
for T in "${TAGS[@]}"; do for T in "${TAGS[@]}"; do
FULL_TAGS+=("$IMAGE_NO_TAG:$T") FULL_TAGS+=("$IMAGE_NO_TAG:$T")
done done
fi
if [ -z "$USE_BUILDX" ] || [ "$USE_BUILDX" != "1" ]; then if [ -z "$USE_BUILDX" ] || [ "$USE_BUILDX" != "1" ]; then
# if buildx is not available, use docker build # if buildx is not available, use docker build
@ -197,7 +210,18 @@ else
fi fi
if [ -z "$PLATFORM" ]; then if [ -z "$PLATFORM" ]; then
# use system architecture if not specified
UNAME="$(uname -m)"
if [ "$UNAME" == "x86_64" ]; then
PLATFORM="linux/amd64" PLATFORM="linux/amd64"
elif [ "$UNAME" == "aarch64" ] || [ "$UNAME" == "arm64" ]; then
PLATFORM="linux/arm64"
elif [ "$UNAME" == "armv7l" ]; then
PLATFORM="linux/arm/v7"
else
log "Unknown architecture: $UNAME"
exit 1
fi
fi fi
log "Using platform: $PLATFORM" log "Using platform: $PLATFORM"
@ -290,10 +314,10 @@ fi
log "Building base image: $BASE_IMAGE" log "Building base image: $BASE_IMAGE"
docker run --rm -i \ docker run --rm -i \
-v ./:/src \ -v "$(pwd)":/src \
-e "RUNTIME_DOCKERFILE=$RUNTIME_DOCKERFILE" \ -e "RUNTIME_DOCKERFILE=$RUNTIME_DOCKERFILE" \
--workdir /src \ --workdir /src \
--entrypoint go \ --entrypoint go \
golang:1.24-bullseye \ golang:1.24-bullseye \
run ./docker/main.go \ run utils/docker/main.go \
-i Dockerfile.tmpl -client $CLIENT_DIST | build_image $BASE_IMAGE -f - . -i Dockerfile.tmpl -client "$CLIENT_DIST" | build_image $BASE_IMAGE -f - .

View file

@ -119,23 +119,43 @@
} }
} }
@media only screen and (max-width: 600px) { @media only screen and (max-width: 1024px) {
#neko.expanded { html,
.neko-main { body {
transform: translateX(calc(-100% + 65px)); overflow-y: auto !important;
width: auto !important;
height: auto !important;
}
video { body > p {
display: none; display: none;
} }
#neko {
position: relative;
flex-direction: column;
max-height: initial !important;
.neko-main {
height: 100vh;
} }
.neko-menu { .neko-menu {
position: absolute; height: 100vh;
top: 0; width: 100% !important;
right: 0; }
bottom: 0; }
left: 65px; }
width: calc(100% - 65px);
@media only screen and (max-width: 1024px) and (orientation: portrait) {
#neko {
&.expanded .neko-main {
height: 40vh;
}
&.expanded .neko-menu {
height: 60vh;
width: 100% !important;
} }
} }
} }
@ -217,6 +237,20 @@
} }
} }
@Watch('side')
onSide(side: boolean) {
if (side) {
console.log('side enabled')
// scroll to the side
this.$nextTick(() => {
const side = document.querySelector('aside')
if (side) {
side.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
})
}
}
controlAttempt() { controlAttempt() {
if (this.shakeKbd || this.$accessor.remote.hosted) return if (this.shakeKbd || this.$accessor.remote.hosted) return

View file

@ -24,7 +24,7 @@
<style lang="scss" scoped> <style lang="scss" scoped>
.connect { .connect {
position: absolute; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;

View file

@ -60,8 +60,9 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from 'vue-property-decorator' import { Component, Vue, Watch } from 'vue-property-decorator'
import { messages } from '~/locale' import { messages } from '~/locale'
import { set } from '~/utils/localstorage'
@Component({ name: 'neko-menu' }) @Component({ name: 'neko-menu' })
export default class extends Vue { export default class extends Vue {
@ -77,6 +78,11 @@
this.$accessor.client.toggleAbout() this.$accessor.client.toggleAbout()
} }
@Watch('$i18n.locale')
onLanguageChange(newLang: string) {
set('lang', newLang)
}
mounted() { mounted() {
const default_lang = new URL(location.href).searchParams.get('lang') const default_lang = new URL(location.href).searchParams.get('lang')
if (default_lang && this.langs.includes(default_lang)) { if (default_lang && this.langs.includes(default_lang)) {

View file

@ -40,7 +40,12 @@
<li v-if="admin"><i @click.stop.prevent="openResolution" class="fas fa-desktop"></i></li> <li v-if="admin"><i @click.stop.prevent="openResolution" class="fas fa-desktop"></i></li>
<li v-if="!controlLocked && !implicitHosting" :class="extraControls || 'extra-control'"> <li v-if="!controlLocked && !implicitHosting" :class="extraControls || 'extra-control'">
<i <i
:class="[hosted && !hosting ? 'disabled' : '', !hosted && !hosting ? 'faded' : '', 'fas', 'fa-keyboard']" :class="[
hosted && !hosting ? 'disabled' : '',
!hosted && !hosting ? 'faded' : '',
'fas',
'fa-computer-mouse',
]"
@click.stop.prevent="toggleControl" @click.stop.prevent="toggleControl"
/> />
</li> </li>
@ -57,6 +62,13 @@
class="fas fa-external-link-alt" class="fas fa-external-link-alt"
/> />
</li> </li>
<li
v-if="hosting && is_touch_device"
:class="extraControls || 'extra-control'"
@click.stop.prevent="openMobileKeyboard"
>
<i class="fas fa-keyboard" />
</li>
</ul> </ul>
<neko-resolution ref="resolution" v-if="admin" /> <neko-resolution ref="resolution" v-if="admin" />
<neko-clipboard ref="clipboard" v-if="hosting && (!clipboard_read_available || !clipboard_write_available)" /> <neko-clipboard ref="clipboard" v-if="hosting && (!clipboard_read_available || !clipboard_write_available)" />
@ -117,7 +129,7 @@
} }
@media (max-width: 768px) { @media (max-width: 768px) {
&.extra-control { &.extra-control {
display: inline-block; display: block;
} }
} }
@ -251,6 +263,10 @@
return this.$accessor.connecting return this.$accessor.connecting
} }
get controlling() {
return this.$accessor.remote.controlling
}
get hosting() { get hosting() {
return this.$accessor.remote.hosting return this.$accessor.remote.hosting
} }
@ -353,6 +369,16 @@
return this.$accessor.video.horizontal return this.$accessor.video.horizontal
} }
get is_touch_device() {
return (
// detect if the device has touch support
('ontouchstart' in window || navigator.maxTouchPoints > 0) &&
// the primary input mechanism includes a pointing device of
// limited accuracy, such as a finger on a touchscreen.
window.matchMedia('(pointer: coarse)').matches
)
}
@Watch('width') @Watch('width')
onWidthChanged() { onWidthChanged() {
this.onResize() this.onResize()
@ -730,12 +756,17 @@
first.target.dispatchEvent(simulatedEvent) first.target.dispatchEvent(simulatedEvent)
} }
isMouseDown = false
onMouseDown(e: MouseEvent) { onMouseDown(e: MouseEvent) {
if (!this.hosting) { this.isMouseDown = true
this.$emit('control-attempt', e)
if (this.locked) {
return
} }
if (!this.hosting || this.locked) { if (!this.controlling) {
this.implicitHostingRequest(e)
return return
} }
@ -744,7 +775,16 @@
} }
onMouseUp(e: MouseEvent) { onMouseUp(e: MouseEvent) {
if (!this.hosting || this.locked) { // only if we are the one who started the mouse down
if (!this.isMouseDown) return
this.isMouseDown = false
if (this.locked) {
return
}
if (!this.controlling) {
this.implicitHostingRequest(e)
return return
} }
@ -752,6 +792,40 @@
this.$client.sendData('mouseup', { key: e.button + 1 }) this.$client.sendData('mouseup', { key: e.button + 1 })
} }
private reqMouseDown: MouseEvent | null = null
private reqMouseUp: MouseEvent | null = null
@Watch('controlling')
onControlChange(controlling: boolean) {
if (controlling && this.reqMouseDown) {
this.onMouseDown(this.reqMouseDown)
}
if (controlling && this.reqMouseUp) {
this.onMouseUp(this.reqMouseUp)
}
this.reqMouseDown = null
this.reqMouseUp = null
}
implicitHostingRequest(e: MouseEvent) {
if (this.implicitHosting) {
if (e.type === 'mousedown') {
this.reqMouseDown = e
this.reqMouseUp = null
this.$accessor.remote.request()
} else if (e.type === 'mouseup') {
this.reqMouseUp = e
}
return
}
if (e.type === 'mousedown') {
this.$emit('control-attempt', e)
}
}
onMouseMove(e: MouseEvent) { onMouseMove(e: MouseEvent) {
if (!this.hosting || this.locked) { if (!this.hosting || this.locked) {
return return
@ -799,10 +873,20 @@
@Watch('hosting') @Watch('hosting')
@Watch('locked') @Watch('locked')
onFocus() { onFocus() {
// focus opens the keyboard on mobile
if (this.is_touch_device) {
return
}
// in order to capture key events, overlay must be focused // in order to capture key events, overlay must be focused
if (this.focused && this.hosting && !this.locked) { if (this.focused && this.hosting && !this.locked) {
this._overlay.focus() this._overlay.focus()
} }
} }
openMobileKeyboard() {
// focus opens the keyboard on mobile
this._overlay.focus()
}
} }
</script> </script>

View file

@ -15,7 +15,7 @@ export const EVENT = {
ERROR: 'system/error', ERROR: 'system/error',
}, },
CLIENT: { CLIENT: {
HEARTBEAT: 'client/heartbeat' HEARTBEAT: 'client/heartbeat',
}, },
SIGNAL: { SIGNAL: {
OFFER: 'signal/offer', OFFER: 'signal/offer',

View file

@ -1,11 +1,31 @@
import Vue from 'vue' import Vue from 'vue'
import VueI18n from 'vue-i18n' import VueI18n from 'vue-i18n'
import { messages } from '~/locale' import { messages } from '~/locale'
import { get } from '~/utils/localstorage'
Vue.use(VueI18n) Vue.use(VueI18n)
const fallbackLocale = 'en'
function detectBrowserLanguage(): string {
const browserLang = navigator.language.toLowerCase()
const supportedLangs = Object.keys(messages)
if (supportedLangs.includes(browserLang)) {
return browserLang
}
const baseLang = browserLang.split('-')[0]
const matchingLang = supportedLangs.find((lang) => lang.startsWith(baseLang))
if (matchingLang) {
return matchingLang
}
return fallbackLocale
}
export const i18n = new VueI18n({ export const i18n = new VueI18n({
locale: 'en', locale: get<string>('lang', detectBrowserLanguage()),
fallbackLocale: 'en', fallbackLocale,
messages, messages,
}) })

View file

@ -18,6 +18,9 @@ export const state = () => ({
}) })
export const getters = getterTree(state, { export const getters = getterTree(state, {
controlling: (state, getters, root) => {
return root.user.id === state.id
},
hosting: (state, getters, root) => { hosting: (state, getters, root) => {
return root.user.id === state.id || state.implicitHosting return root.user.id === state.id || state.implicitHosting
}, },
@ -89,7 +92,7 @@ export const actions = actionTree(
}, },
request({ getters }) { request({ getters }) {
if (!accessor.connected || getters.hosting) { if (!accessor.connected || getters.controlling) {
return return
} }

View file

@ -15,6 +15,3 @@ session:
cookie: cookie:
# needed for legacy API # needed for legacy API
enabled: false enabled: false
webrtc:
icelite: true

View file

@ -1,15 +1,14 @@
version: "3.4"
services: services:
neko: neko:
image: "m1k1o/neko:firefox" image: "ghcr.io/m1k1o/neko/firefox:latest"
restart: "unless-stopped" restart: "unless-stopped"
shm_size: "2gb" shm_size: "2gb"
ports: ports:
- "8080:8080" - "8080:8080"
- "52000-52100:52000-52100/udp" - "52000-52100:52000-52100/udp"
environment: environment:
NEKO_SCREEN: 1920x1080@30 NEKO_DESKTOP_SCREEN: 1920x1080@30
NEKO_PASSWORD: neko NEKO_MEMBER_MULTIUSER_USER_PASSWORD: neko
NEKO_PASSWORD_ADMIN: admin NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD: admin
NEKO_EPR: 52000-52100 NEKO_WEBRTC_EPR: 52000-52100
NEKO_ICELITE: 1 NEKO_WEBRTC_ICELITE: 1

View file

@ -1,3 +0,0 @@
module github.com/m1k1o/neko/docker
go 1.24.1

View file

@ -103,7 +103,9 @@ ENV NEKO_PLUGINS_DIR=/etc/neko/plugins/
# #
# add healthcheck # add healthcheck
HEALTHCHECK --interval=10s --timeout=5s --retries=8 \ HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || exit 1 CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || \
wget --no-check-certificate -O - https://localhost:${NEKO_SERVER_BIND#*:}/health || \
exit 1
# #
# run neko # run neko

View file

@ -95,8 +95,9 @@ ENV NEKO_PLUGINS_DIR=/etc/neko/plugins/
# #
# add healthcheck # add healthcheck
HEALTHCHECK --interval=10s --timeout=5s --retries=8 \ HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || exit 1 CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || \
wget --no-check-certificate -O - https://localhost:${NEKO_SERVER_BIND#*:}/health || \
exit 1
# #
# run neko # run neko
CMD ["/usr/bin/supervisord", "-c", "/etc/neko/supervisord.conf"] CMD ["/usr/bin/supervisord", "-c", "/etc/neko/supervisord.conf"]

View file

@ -115,8 +115,9 @@ ENV RENDER_GID=
# #
# add healthcheck # add healthcheck
HEALTHCHECK --interval=10s --timeout=5s --retries=8 \ HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || exit 1 CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || \
wget --no-check-certificate -O - https://localhost:${NEKO_SERVER_BIND#*:}/health || \
exit 1
# #
# run neko # run neko
CMD ["/usr/bin/supervisord", "-c", "/etc/neko/supervisord.conf"] CMD ["/usr/bin/supervisord", "-c", "/etc/neko/supervisord.conf"]

View file

@ -1,6 +1,6 @@
ARG UBUNTU_RELEASE=20.04 ARG UBUNTU_RELEASE=20.04
ARG CUDA_VERSION=11.4.3 ARG CUDA_VERSION=11.4.3
ARG VIRTUALGL_VERSION=3.1 ARG VIRTUALGL_VERSION=3.1.3-20250409
ARG GSTREAMER_VERSION=1.20 ARG GSTREAMER_VERSION=1.20
# #
@ -61,13 +61,13 @@ ARG UBUNTU_RELEASE
ARG VIRTUALGL_VERSION ARG VIRTUALGL_VERSION
# Make all NVIDIA GPUs visible by default # Make all NVIDIA GPUs visible by default
ENV NVIDIA_VISIBLE_DEVICES all ENV NVIDIA_VISIBLE_DEVICES=all
# All NVIDIA driver capabilities should preferably be used, check `NVIDIA_DRIVER_CAPABILITIES` inside the container if things do not work # All NVIDIA driver capabilities should preferably be used, check `NVIDIA_DRIVER_CAPABILITIES` inside the container if things do not work
ENV NVIDIA_DRIVER_CAPABILITIES all ENV NVIDIA_DRIVER_CAPABILITIES=all
# #
# set vgl-display to headless 3d gpu card/// correct values are egl[n] or /dev/dri/card0:if this is passed into container # set vgl-display to headless 3d gpu card/// correct values are egl[n] or /dev/dri/card0:if this is passed into container
ENV VGL_DISPLAY egl ENV VGL_DISPLAY=egl
# #
# set custom user # set custom user
@ -205,23 +205,22 @@ RUN VULKAN_API_VERSION=$(dpkg -s libvulkan1 | grep -oP 'Version: [0-9|\.]+' | gr
}" > /etc/vulkan/icd.d/nvidia_icd.json }" > /etc/vulkan/icd.d/nvidia_icd.json
# #
# install VirtualGL and make libraries available for preload # install an up-to-date version of VirtualGL
RUN set -eux; \ RUN apt-get update; \
apt-get install -y --no-install-recommends wget gpg ca-certificates; \
# Add VirtualGL GPG key
wget -q -O- https://packagecloud.io/dcommander/virtualgl/gpgkey | \
gpg --dearmor >/etc/apt/trusted.gpg.d/VirtualGL.gpg; \
# Download the official VirtualGL.list file
wget -q -O /etc/apt/sources.list.d/VirtualGL.list \
https://raw.githubusercontent.com/VirtualGL/repo/main/VirtualGL.list; \
# Install packages
apt-get update; \ apt-get update; \
wget "https://sourceforge.net/projects/virtualgl/files/virtualgl_${VIRTUALGL_VERSION}_amd64.deb"; \ apt-get install -y --no-install-recommends virtualgl=${VIRTUALGL_VERSION}; \
wget "https://sourceforge.net/projects/virtualgl/files/virtualgl32_${VIRTUALGL_VERSION}_amd64.deb"; \
apt-get install -y --no-install-recommends ./virtualgl_${VIRTUALGL_VERSION}_amd64.deb ./virtualgl32_${VIRTUALGL_VERSION}_amd64.deb; \
rm -f "virtualgl_${VIRTUALGL_VERSION}_amd64.deb" "virtualgl32_${VIRTUALGL_VERSION}_amd64.deb"; \
chmod u+s /usr/lib/libvglfaker.so; \
chmod u+s /usr/lib/libdlfaker.so; \
chmod u+s /usr/lib32/libvglfaker.so; \
chmod u+s /usr/lib32/libdlfaker.so; \
chmod u+s /usr/lib/i386-linux-gnu/libvglfaker.so; \
chmod u+s /usr/lib/i386-linux-gnu/libdlfaker.so; \
# #
# clean up # clean up
apt-get clean -y; \ apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*; rm -rf /var/lib/apt/lists/* /var/cache/apt/*
# #
# copy runtime configs # copy runtime configs
@ -262,7 +261,9 @@ COPY --from=gstreamer /usr/share/gstreamer /usr/share/gstreamer
# #
# add healthcheck # add healthcheck
HEALTHCHECK --interval=10s --timeout=5s --retries=8 \ HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || exit 1 CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || \
wget --no-check-certificate -O - https://localhost:${NEKO_SERVER_BIND#*:}/health || \
exit 1
# #
# run neko # run neko

View file

@ -1,6 +1,6 @@
ARG UBUNTU_RELEASE=22.04 ARG UBUNTU_RELEASE=22.04
ARG CUDA_VERSION=12.2.0 ARG CUDA_VERSION=12.2.0
ARG VIRTUALGL_VERSION=3.1 ARG VIRTUALGL_VERSION=3.1.3-20250409
ARG GSTREAMER_VERSION=1.22 ARG GSTREAMER_VERSION=1.22
# #
@ -61,13 +61,13 @@ ARG UBUNTU_RELEASE
ARG VIRTUALGL_VERSION ARG VIRTUALGL_VERSION
# Make all NVIDIA GPUs visible by default # Make all NVIDIA GPUs visible by default
ENV NVIDIA_VISIBLE_DEVICES all ENV NVIDIA_VISIBLE_DEVICES=all
# All NVIDIA driver capabilities should preferably be used, check `NVIDIA_DRIVER_CAPABILITIES` inside the container if things do not work # All NVIDIA driver capabilities should preferably be used, check `NVIDIA_DRIVER_CAPABILITIES` inside the container if things do not work
ENV NVIDIA_DRIVER_CAPABILITIES all ENV NVIDIA_DRIVER_CAPABILITIES=all
# #
# set vgl-display to headless 3d gpu card/// correct values are egl[n] or /dev/dri/card0:if this is passed into container # set vgl-display to headless 3d gpu card/// correct values are egl[n] or /dev/dri/card0:if this is passed into container
ENV VGL_DISPLAY egl ENV VGL_DISPLAY=egl
# #
# set custom user # set custom user
@ -199,23 +199,22 @@ RUN VULKAN_API_VERSION=$(dpkg -s libvulkan1 | grep -oP 'Version: [0-9|\.]+' | gr
}" > /etc/vulkan/icd.d/nvidia_icd.json }" > /etc/vulkan/icd.d/nvidia_icd.json
# #
# install VirtualGL and make libraries available for preload # install an up-to-date version of VirtualGL
RUN set -eux; \ RUN apt-get update; \
apt-get install -y --no-install-recommends wget gpg ca-certificates; \
# Add VirtualGL GPG key
wget -q -O- https://packagecloud.io/dcommander/virtualgl/gpgkey | \
gpg --dearmor >/etc/apt/trusted.gpg.d/VirtualGL.gpg; \
# Download the official VirtualGL.list file
wget -q -O /etc/apt/sources.list.d/VirtualGL.list \
https://raw.githubusercontent.com/VirtualGL/repo/main/VirtualGL.list; \
# Install packages
apt-get update; \ apt-get update; \
wget "https://sourceforge.net/projects/virtualgl/files/virtualgl_${VIRTUALGL_VERSION}_amd64.deb"; \ apt-get install -y --no-install-recommends virtualgl=${VIRTUALGL_VERSION}; \
wget "https://sourceforge.net/projects/virtualgl/files/virtualgl32_${VIRTUALGL_VERSION}_amd64.deb"; \
apt-get install -y --no-install-recommends ./virtualgl_${VIRTUALGL_VERSION}_amd64.deb ./virtualgl32_${VIRTUALGL_VERSION}_amd64.deb; \
rm -f "virtualgl_${VIRTUALGL_VERSION}_amd64.deb" "virtualgl32_${VIRTUALGL_VERSION}_amd64.deb"; \
chmod u+s /usr/lib/libvglfaker.so; \
chmod u+s /usr/lib/libdlfaker.so; \
chmod u+s /usr/lib32/libvglfaker.so; \
chmod u+s /usr/lib32/libdlfaker.so; \
chmod u+s /usr/lib/i386-linux-gnu/libvglfaker.so; \
chmod u+s /usr/lib/i386-linux-gnu/libdlfaker.so; \
# #
# clean up # clean up
apt-get clean -y; \ apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*; rm -rf /var/lib/apt/lists/* /var/cache/apt/*
# #
# copy runtime configs # copy runtime configs
@ -254,7 +253,9 @@ COPY --from=gstreamer /usr/share/gstreamer /usr/share/gstreamer
# #
# add healthcheck # add healthcheck
HEALTHCHECK --interval=10s --timeout=5s --retries=8 \ HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || exit 1 CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || \
wget --no-check-certificate -O - https://localhost:${NEKO_SERVER_BIND#*:}/health || \
exit 1
# #
# run neko # run neko

View file

@ -33,7 +33,7 @@ redirect_stderr=true
[program:neko] [program:neko]
environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s" environment=HOME="/home/%(ENV_USER)s",USER="%(ENV_USER)s",DISPLAY="%(ENV_DISPLAY)s"
command=/usr/bin/neko serve --static "/var/www" command=/usr/bin/neko serve --server.static "/var/www"
stopsignal=INT stopsignal=INT
stopwaitsecs=3 stopwaitsecs=3
autorestart=true autorestart=true

View file

@ -22,7 +22,7 @@ fi
# #
# load server dependencies # load server dependencies
go get -v -t -d . go get -v -t .
# #
# build server # build server

View file

@ -9,9 +9,10 @@ GIT_COMMIT=`git rev-parse --short HEAD`
GIT_BRANCH=`git rev-parse --symbolic-full-name --abbrev-ref HEAD` GIT_BRANCH=`git rev-parse --symbolic-full-name --abbrev-ref HEAD`
echo "Building server image" echo "Building server image"
docker build -t neko_server --build-arg "GIT_COMMIT=$GIT_COMMIT" --build-arg "GIT_BRANCH=$GIT_BRANCH" -f ../Dockerfile .. docker build -t neko_server:src -f ../Dockerfile ..
BUILD_IMAGE=neko_server FLAVOUR=$1 ../../build echo "Building base image"
../../build -y -b neko_server:base -f "$1"
echo "Building app image" echo "Building app image"
docker build -t neko_server:app --build-arg "BASE_IMAGE=neko_server:base" -f ./runtime/Dockerfile ./runtime docker build -t neko_server:app --build-arg "BASE_IMAGE=neko_server:base" -f ./runtime/Dockerfile ./runtime

View file

@ -1,12 +1,12 @@
#!/bin/bash #!/bin/bash
cd "$(dirname "$0")" cd "$(dirname "$0")"
if [ "$(docker images -q neko_server 2> /dev/null)" == "" ]; then if [ "$(docker images -q neko_server:src 2> /dev/null)" == "" ]; then
echo "Image 'neko_server' not found. Run ./build first." echo "Image 'neko_server:src' not found. Run ./build first."
exit 1 exit 1
fi fi
docker run -it --rm \ docker run -it --rm \
--entrypoint="go" \ --entrypoint="go" \
-v "${PWD}/../:/src" \ -v "${PWD}/../:/src" \
neko_server fmt ./... neko_server:src fmt ./...

View file

@ -1,8 +1,8 @@
#!/bin/bash #!/bin/bash
cd "$(dirname "$0")" cd "$(dirname "$0")"
if [ "$(docker images -q neko_server 2> /dev/null)" == "" ]; then if [ "$(docker images -q neko_server:src 2> /dev/null)" == "" ]; then
echo "Image 'neko_server' not found. Run ./build first." echo "Image 'neko_server:src' not found. Run ./build first."
exit 1 exit 1
fi fi
@ -10,7 +10,7 @@ docker run -it \
--name "neko_server_go" \ --name "neko_server_go" \
--entrypoint="go" \ --entrypoint="go" \
-v "${PWD}/../:/src" \ -v "${PWD}/../:/src" \
neko_server "$@"; neko_server:src "$@";
# #
# copy package files # copy package files
docker cp neko_server_go:/src/go.mod "../go.mod" docker cp neko_server_go:/src/go.mod "../go.mod"

View file

@ -1,8 +1,8 @@
#!/bin/bash #!/bin/bash
cd "$(dirname "$0")" cd "$(dirname "$0")"
if [ "$(docker images -q neko_server 2> /dev/null)" == "" ]; then if [ "$(docker images -q neko_server:src 2> /dev/null)" == "" ]; then
echo "Image 'neko_server' not found. Run ./build first." echo "Image 'neko_server:src' not found. Run ./build first."
exit 1 exit 1
fi fi
@ -11,4 +11,4 @@ fi
docker run --rm -it \ docker run --rm -it \
-v "${PWD}/../:/src" \ -v "${PWD}/../:/src" \
--entrypoint="/bin/bash" \ --entrypoint="/bin/bash" \
neko_server -c '[ -f ./bin/golangci-lint ] || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.31.0;./bin/golangci-lint run'; neko_server:src -c '[ -f ./bin/golangci-lint ] || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.31.0;./bin/golangci-lint run';

View file

@ -10,7 +10,7 @@ set -e
docker run --rm -it \ docker run --rm -it \
-v "${PWD}/../:/src" \ -v "${PWD}/../:/src" \
--entrypoint="/bin/bash" \ --entrypoint="/bin/bash" \
neko_server "./build" "$@"; neko_server:src "./build" "$@";
# #
# remove old plugins # remove old plugins

View file

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
cd "$(dirname "$0")" cd "$(dirname "$0")"
cd ../../runtime/xorg-deps/xf86-input-neko cd ../../utils/xorg-deps/xf86-input-neko
# #
# aborting if any command returns a non-zero value # aborting if any command returns a non-zero value

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=neko_server:base ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE FROM $BASE_IMAGE
ARG SRC_URL="https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US" ARG SRC_URL="https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US"
@ -8,13 +8,13 @@ ARG SRC_URL="https://download.mozilla.org/?product=firefox-latest&os=linux64&lan
RUN set -eux; apt-get update; \ RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
dbus-x11 xfce4 xfce4-terminal sudo \ dbus-x11 xfce4 xfce4-terminal sudo \
xz-utils bzip2 libgtk-3-0 libdbus-glib-1-2; \ xz-utils libgtk-3-0 libdbus-glib-1-2; \
# #
# fetch latest firefox release # fetch latest firefox release
wget -O /tmp/firefox-setup.tar.bz2 "${SRC_URL}"; \ wget -O /tmp/firefox-setup.tar.xz "${SRC_URL}"; \
mkdir /usr/lib/firefox; \ mkdir /usr/lib/firefox; \
tar -xjf /tmp/firefox-setup.tar.bz2 -C /usr/lib; \ tar -xvf /tmp/firefox-setup.tar.xz -C /usr/lib; \
rm -f /tmp/firefox-setup.tar.bz2; \ rm -f /tmp/firefox-setup.tar.xz; \
ln -s /usr/lib/firefox/firefox /usr/bin/firefox; \ ln -s /usr/lib/firefox/firefox /usr/bin/firefox; \
# #
# add user to sudoers # add user to sudoers
@ -22,7 +22,7 @@ RUN set -eux; apt-get update; \
echo "neko:neko" | chpasswd; \ echo "neko:neko" | chpasswd; \
echo "%sudo ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers; \ echo "%sudo ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers; \
# clean up # clean up
apt-get --purge autoremove -y xz-utils bzip2; \ apt-get --purge autoremove -y xz-utils; \
apt-get clean -y; \ apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/* rm -rf /var/lib/apt/lists/* /var/cache/apt/*

View file

@ -90,12 +90,12 @@ func (Capture) Init(cmd *cobra.Command) error {
return err return err
} }
cmd.PersistentFlags().String("capture.video.pipelines", "[]", "pipelines config in JSON used for video streaming") cmd.PersistentFlags().String("capture.video.pipelines", "{}", "pipelines config used for video streaming")
if err := viper.BindPFlag("capture.video.pipelines", cmd.PersistentFlags().Lookup("capture.video.pipelines")); err != nil { if err := viper.BindPFlag("capture.video.pipelines", cmd.PersistentFlags().Lookup("capture.video.pipelines")); err != nil {
return err return err
} }
cmd.PersistentFlags().String("capture.video.pipeline", "", "gstreamer pipeline used for video streaming; shortcut for having only a single video pipeline instead of multiple, ignored if capture.video.pipelines is set") cmd.PersistentFlags().String("capture.video.pipeline", "", "shortcut for configuring only a single gstreamer pipeline, ignored if pipelines is set")
if err := viper.BindPFlag("capture.video.pipeline", cmd.PersistentFlags().Lookup("capture.video.pipeline")); err != nil { if err := viper.BindPFlag("capture.video.pipeline", cmd.PersistentFlags().Lookup("capture.video.pipeline")); err != nil {
return err return err
} }
@ -451,6 +451,7 @@ func (s *Capture) SetV2() {
enableLegacy = true enableLegacy = true
} }
modifiedVideoCodec := false
if videoCodec := viper.GetString("video_codec"); videoCodec != "" { if videoCodec := viper.GetString("video_codec"); videoCodec != "" {
s.VideoCodec, ok = codec.ParseStr(videoCodec) s.VideoCodec, ok = codec.ParseStr(videoCodec)
if !ok || s.VideoCodec.Type != webrtc.RTPCodecTypeVideo { if !ok || s.VideoCodec.Type != webrtc.RTPCodecTypeVideo {
@ -459,24 +460,29 @@ func (s *Capture) SetV2() {
} }
log.Warn().Msg("you are using v2 configuration 'NEKO_VIDEO_CODEC' which is deprecated, please use 'NEKO_CAPTURE_VIDEO_CODEC' instead") log.Warn().Msg("you are using v2 configuration 'NEKO_VIDEO_CODEC' which is deprecated, please use 'NEKO_CAPTURE_VIDEO_CODEC' instead")
enableLegacy = true enableLegacy = true
modifiedVideoCodec = true
} }
if viper.GetBool("vp8") { if viper.GetBool("vp8") {
s.VideoCodec = codec.VP8() s.VideoCodec = codec.VP8()
log.Warn().Msg("you are using deprecated config setting 'NEKO_VP8=true', use 'NEKO_CAPTURE_VIDEO_CODEC=vp8' instead") log.Warn().Msg("you are using deprecated config setting 'NEKO_VP8=true', use 'NEKO_CAPTURE_VIDEO_CODEC=vp8' instead")
enableLegacy = true enableLegacy = true
modifiedVideoCodec = true
} else if viper.GetBool("vp9") { } else if viper.GetBool("vp9") {
s.VideoCodec = codec.VP9() s.VideoCodec = codec.VP9()
log.Warn().Msg("you are using deprecated config setting 'NEKO_VP9=true', use 'NEKO_CAPTURE_VIDEO_CODEC=vp9' instead") log.Warn().Msg("you are using deprecated config setting 'NEKO_VP9=true', use 'NEKO_CAPTURE_VIDEO_CODEC=vp9' instead")
enableLegacy = true enableLegacy = true
modifiedVideoCodec = true
} else if viper.GetBool("h264") { } else if viper.GetBool("h264") {
s.VideoCodec = codec.H264() s.VideoCodec = codec.H264()
log.Warn().Msg("you are using deprecated config setting 'NEKO_H264=true', use 'NEKO_CAPTURE_VIDEO_CODEC=h264' instead") log.Warn().Msg("you are using deprecated config setting 'NEKO_H264=true', use 'NEKO_CAPTURE_VIDEO_CODEC=h264' instead")
enableLegacy = true enableLegacy = true
modifiedVideoCodec = true
} else if viper.GetBool("av1") { } else if viper.GetBool("av1") {
s.VideoCodec = codec.AV1() s.VideoCodec = codec.AV1()
log.Warn().Msg("you are using deprecated config setting 'NEKO_AV1=true', use 'NEKO_CAPTURE_VIDEO_CODEC=av1' instead") log.Warn().Msg("you are using deprecated config setting 'NEKO_AV1=true', use 'NEKO_CAPTURE_VIDEO_CODEC=av1' instead")
enableLegacy = true enableLegacy = true
modifiedVideoCodec = true
} }
videoHWEnc := HwEncUnset videoHWEnc := HwEncUnset
@ -498,7 +504,7 @@ func (s *Capture) SetV2() {
videoPipeline := viper.GetString("video") videoPipeline := viper.GetString("video")
// video pipeline // video pipeline
if videoHWEnc != HwEncUnset || videoBitrate != 0 || videoMaxFPS != 0 || videoPipeline != "" { if modifiedVideoCodec || videoHWEnc != HwEncUnset || videoBitrate != 0 || videoMaxFPS != 0 || videoPipeline != "" {
pipeline, err := NewVideoPipeline(s.VideoCodec, s.Display, videoPipeline, videoMaxFPS, videoBitrate, videoHWEnc) pipeline, err := NewVideoPipeline(s.VideoCodec, s.Display, videoPipeline, videoMaxFPS, videoBitrate, videoHWEnc)
if err != nil { if err != nil {
log.Warn().Err(err).Msg("unable to create video pipeline, using default") log.Warn().Err(err).Msg("unable to create video pipeline, using default")
@ -534,6 +540,7 @@ func (s *Capture) SetV2() {
enableLegacy = true enableLegacy = true
} }
modifiedAudioCodec := false
if audioCodec := viper.GetString("audio_codec"); audioCodec != "" { if audioCodec := viper.GetString("audio_codec"); audioCodec != "" {
s.AudioCodec, ok = codec.ParseStr(audioCodec) s.AudioCodec, ok = codec.ParseStr(audioCodec)
if !ok || s.AudioCodec.Type != webrtc.RTPCodecTypeAudio { if !ok || s.AudioCodec.Type != webrtc.RTPCodecTypeAudio {
@ -542,31 +549,36 @@ func (s *Capture) SetV2() {
} }
log.Warn().Msg("you are using v2 configuration 'NEKO_AUDIO_CODEC' which is deprecated, please use 'NEKO_CAPTURE_AUDIO_CODEC' instead") log.Warn().Msg("you are using v2 configuration 'NEKO_AUDIO_CODEC' which is deprecated, please use 'NEKO_CAPTURE_AUDIO_CODEC' instead")
enableLegacy = true enableLegacy = true
modifiedAudioCodec = true
} }
if viper.GetBool("opus") { if viper.GetBool("opus") {
s.AudioCodec = codec.Opus() s.AudioCodec = codec.Opus()
log.Warn().Msg("you are using deprecated config setting 'NEKO_OPUS=true', use 'NEKO_CAPTURE_AUDIO_CODEC=opus' instead") log.Warn().Msg("you are using deprecated config setting 'NEKO_OPUS=true', use 'NEKO_CAPTURE_AUDIO_CODEC=opus' instead")
enableLegacy = true enableLegacy = true
modifiedAudioCodec = true
} else if viper.GetBool("g722") { } else if viper.GetBool("g722") {
s.AudioCodec = codec.G722() s.AudioCodec = codec.G722()
log.Warn().Msg("you are using deprecated config setting 'NEKO_G722=true', use 'NEKO_CAPTURE_AUDIO_CODEC=g722' instead") log.Warn().Msg("you are using deprecated config setting 'NEKO_G722=true', use 'NEKO_CAPTURE_AUDIO_CODEC=g722' instead")
enableLegacy = true enableLegacy = true
modifiedAudioCodec = true
} else if viper.GetBool("pcmu") { } else if viper.GetBool("pcmu") {
s.AudioCodec = codec.PCMU() s.AudioCodec = codec.PCMU()
log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMU=true', use 'NEKO_CAPTURE_AUDIO_CODEC=pcmu' instead") log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMU=true', use 'NEKO_CAPTURE_AUDIO_CODEC=pcmu' instead")
enableLegacy = true enableLegacy = true
modifiedAudioCodec = true
} else if viper.GetBool("pcma") { } else if viper.GetBool("pcma") {
s.AudioCodec = codec.PCMA() s.AudioCodec = codec.PCMA()
log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMA=true', use 'NEKO_CAPTURE_AUDIO_CODEC=pcma' instead") log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMA=true', use 'NEKO_CAPTURE_AUDIO_CODEC=pcma' instead")
enableLegacy = true enableLegacy = true
modifiedAudioCodec = true
} }
audioBitrate := viper.GetUint("audio_bitrate") audioBitrate := viper.GetUint("audio_bitrate")
audioPipeline := viper.GetString("audio") audioPipeline := viper.GetString("audio")
// audio pipeline // audio pipeline
if audioBitrate != 0 || audioPipeline != "" { if modifiedAudioCodec || audioBitrate != 0 || audioPipeline != "" {
pipeline, err := NewAudioPipeline(s.AudioCodec, s.AudioDevice, audioPipeline, audioBitrate) pipeline, err := NewAudioPipeline(s.AudioCodec, s.AudioDevice, audioPipeline, audioBitrate)
if err != nil { if err != nil {
log.Warn().Err(err).Msg("unable to create audio pipeline, using default") log.Warn().Err(err).Msg("unable to create audio pipeline, using default")
@ -604,7 +616,7 @@ func (s *Capture) SetV2() {
// set legacy flag if any V2 configuration was used // set legacy flag if any V2 configuration was used
if !viper.IsSet("legacy") && enableLegacy { if !viper.IsSet("legacy") && enableLegacy {
log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, or set 'NEKO_LEGACY=true' to acknowledge this message") log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details")
viper.Set("legacy", true) viper.Set("legacy", true)
} }
} }

View file

@ -133,7 +133,7 @@ func (s *Desktop) SetV2() {
// set legacy flag if any V2 configuration was used // set legacy flag if any V2 configuration was used
if !viper.IsSet("legacy") && enableLegacy { if !viper.IsSet("legacy") && enableLegacy {
log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, or set 'NEKO_LEGACY=true' to acknowledge this message") log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details")
viper.Set("legacy", true) viper.Set("legacy", true)
} }
} }

View file

@ -22,45 +22,45 @@ type Member struct {
} }
func (Member) Init(cmd *cobra.Command) error { func (Member) Init(cmd *cobra.Command) error {
cmd.PersistentFlags().String("member.provider", "multiuser", "choose member provider") cmd.PersistentFlags().String("member.provider", "multiuser", "selected member provider")
if err := viper.BindPFlag("member.provider", cmd.PersistentFlags().Lookup("member.provider")); err != nil { if err := viper.BindPFlag("member.provider", cmd.PersistentFlags().Lookup("member.provider")); err != nil {
return err return err
} }
// file provider // file provider
cmd.PersistentFlags().String("member.file.path", "", "member file provider: storage path") cmd.PersistentFlags().String("member.file.path", "", "member file provider: path to the file containing the users and their passwords")
if err := viper.BindPFlag("member.file.path", cmd.PersistentFlags().Lookup("member.file.path")); err != nil { if err := viper.BindPFlag("member.file.path", cmd.PersistentFlags().Lookup("member.file.path")); err != nil {
return err return err
} }
cmd.PersistentFlags().Bool("member.file.hash", true, "member file provider: whether to hash passwords using sha256 (recommended)") cmd.PersistentFlags().Bool("member.file.hash", true, "member file provider: whether the passwords are hashed using sha256 or not (recommended)")
if err := viper.BindPFlag("member.file.hash", cmd.PersistentFlags().Lookup("member.file.hash")); err != nil { if err := viper.BindPFlag("member.file.hash", cmd.PersistentFlags().Lookup("member.file.hash")); err != nil {
return err return err
} }
// object provider // object provider
cmd.PersistentFlags().String("member.object.users", "[]", "member object provider: users in JSON format") cmd.PersistentFlags().String("member.object.users", "[]", "member object provider: list of users with their passwords and profiles")
if err := viper.BindPFlag("member.object.users", cmd.PersistentFlags().Lookup("member.object.users")); err != nil { if err := viper.BindPFlag("member.object.users", cmd.PersistentFlags().Lookup("member.object.users")); err != nil {
return err return err
} }
// multiuser provider // multiuser provider
cmd.PersistentFlags().String("member.multiuser.user_password", "neko", "member multiuser provider: user password") cmd.PersistentFlags().String("member.multiuser.user_password", "neko", "member multiuser provider: password for regular users")
if err := viper.BindPFlag("member.multiuser.user_password", cmd.PersistentFlags().Lookup("member.multiuser.user_password")); err != nil { if err := viper.BindPFlag("member.multiuser.user_password", cmd.PersistentFlags().Lookup("member.multiuser.user_password")); err != nil {
return err return err
} }
cmd.PersistentFlags().String("member.multiuser.admin_password", "admin", "member multiuser provider: admin password") cmd.PersistentFlags().String("member.multiuser.admin_password", "admin", "member multiuser provider: password for admin users")
if err := viper.BindPFlag("member.multiuser.admin_password", cmd.PersistentFlags().Lookup("member.multiuser.admin_password")); err != nil { if err := viper.BindPFlag("member.multiuser.admin_password", cmd.PersistentFlags().Lookup("member.multiuser.admin_password")); err != nil {
return err return err
} }
cmd.PersistentFlags().String("member.multiuser.user_profile", "{}", "member multiuser provider: user profile in JSON format") cmd.PersistentFlags().String("member.multiuser.user_profile", "{}", "member multiuser provider: profile template for regular users")
if err := viper.BindPFlag("member.multiuser.user_profile", cmd.PersistentFlags().Lookup("member.multiuser.user_profile")); err != nil { if err := viper.BindPFlag("member.multiuser.user_profile", cmd.PersistentFlags().Lookup("member.multiuser.user_profile")); err != nil {
return err return err
} }
cmd.PersistentFlags().String("member.multiuser.admin_profile", "{}", "member multiuser provider: admin profile in JSON format") cmd.PersistentFlags().String("member.multiuser.admin_profile", "{}", "member multiuser provider: profile template for admin users")
if err := viper.BindPFlag("member.multiuser.admin_profile", cmd.PersistentFlags().Lookup("member.multiuser.admin_profile")); err != nil { if err := viper.BindPFlag("member.multiuser.admin_profile", cmd.PersistentFlags().Lookup("member.multiuser.admin_profile")); err != nil {
return err return err
} }
@ -162,7 +162,7 @@ func (s *Member) SetV2() {
// set legacy flag if any V2 configuration was used // set legacy flag if any V2 configuration was used
if !viper.IsSet("legacy") && enableLegacy { if !viper.IsSet("legacy") && enableLegacy {
log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, or set 'NEKO_LEGACY=true' to acknowledge this message") log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details")
viper.Set("legacy", true) viper.Set("legacy", true)
} }
} }

View file

@ -139,7 +139,7 @@ func (s *Root) SetV2() {
// set legacy flag if any V2 configuration was used // set legacy flag if any V2 configuration was used
if !viper.IsSet("legacy") && enableLegacy { if !viper.IsSet("legacy") && enableLegacy {
log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, or set 'NEKO_LEGACY=true' to acknowledge this message") log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details")
viper.Set("legacy", true) viper.Set("legacy", true)
} }
} }

View file

@ -172,7 +172,7 @@ func (s *Server) SetV2() {
// set legacy flag if any V2 configuration was used // set legacy flag if any V2 configuration was used
if !viper.IsSet("legacy") && enableLegacy { if !viper.IsSet("legacy") && enableLegacy {
log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, or set 'NEKO_LEGACY=true' to acknowledge this message") log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details")
viper.Set("legacy", true) viper.Set("legacy", true)
} }
} }

View file

@ -206,7 +206,7 @@ func (s *Session) SetV2() {
// set legacy flag if any V2 configuration was used // set legacy flag if any V2 configuration was used
if !viper.IsSet("legacy") && enableLegacy { if !viper.IsSet("legacy") && enableLegacy {
log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, or set 'NEKO_LEGACY=true' to acknowledge this message") log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details")
viper.Set("legacy", true) viper.Set("legacy", true)
} }
} }

View file

@ -67,17 +67,17 @@ func (WebRTC) Init(cmd *cobra.Command) error {
} }
// Looks like this is conflicting with the frontend and backend ICE servers since latest versions // Looks like this is conflicting with the frontend and backend ICE servers since latest versions
//cmd.PersistentFlags().String("webrtc.iceservers", "[]", "Global STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys") //cmd.PersistentFlags().String("webrtc.iceservers", "[]", "STUN and TURN servers used by the ICE agent")
//if err := viper.BindPFlag("webrtc.iceservers", cmd.PersistentFlags().Lookup("webrtc.iceservers")); err != nil { //if err := viper.BindPFlag("webrtc.iceservers", cmd.PersistentFlags().Lookup("webrtc.iceservers")); err != nil {
// return err // return err
//} //}
cmd.PersistentFlags().String("webrtc.iceservers.frontend", "[]", "Frontend only STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys") cmd.PersistentFlags().String("webrtc.iceservers.frontend", "[]", "STUN and TURN servers used by the frontend")
if err := viper.BindPFlag("webrtc.iceservers.frontend", cmd.PersistentFlags().Lookup("webrtc.iceservers.frontend")); err != nil { if err := viper.BindPFlag("webrtc.iceservers.frontend", cmd.PersistentFlags().Lookup("webrtc.iceservers.frontend")); err != nil {
return err return err
} }
cmd.PersistentFlags().String("webrtc.iceservers.backend", "[]", "Backend only STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys") cmd.PersistentFlags().String("webrtc.iceservers.backend", "[]", "STUN and TURN servers used by the backend")
if err := viper.BindPFlag("webrtc.iceservers.backend", cmd.PersistentFlags().Lookup("webrtc.iceservers.backend")); err != nil { if err := viper.BindPFlag("webrtc.iceservers.backend", cmd.PersistentFlags().Lookup("webrtc.iceservers.backend")); err != nil {
return err return err
} }
@ -217,14 +217,14 @@ func (s *WebRTC) Set() {
// parse frontend ice servers // parse frontend ice servers
if err := viper.UnmarshalKey("webrtc.iceservers.frontend", &s.ICEServersFrontend, viper.DecodeHook( if err := viper.UnmarshalKey("webrtc.iceservers.frontend", &s.ICEServersFrontend, viper.DecodeHook(
utils.JsonStringAutoDecode([]types.ICEServer{}), utils.JsonStringAutoDecode(s.ICEServersFrontend),
)); err != nil { )); err != nil {
log.Warn().Err(err).Msgf("unable to parse frontend ICE servers") log.Warn().Err(err).Msgf("unable to parse frontend ICE servers")
} }
// parse backend ice servers // parse backend ice servers
if err := viper.UnmarshalKey("webrtc.iceservers.backend", &s.ICEServersBackend, viper.DecodeHook( if err := viper.UnmarshalKey("webrtc.iceservers.backend", &s.ICEServersBackend, viper.DecodeHook(
utils.JsonStringAutoDecode([]types.ICEServer{}), utils.JsonStringAutoDecode(s.ICEServersBackend),
)); err != nil { )); err != nil {
log.Warn().Err(err).Msgf("unable to parse backend ICE servers") log.Warn().Err(err).Msgf("unable to parse backend ICE servers")
} }
@ -238,7 +238,7 @@ func (s *WebRTC) Set() {
// parse global ice servers // parse global ice servers
var iceServers []types.ICEServer var iceServers []types.ICEServer
if err := viper.UnmarshalKey("webrtc.iceservers", &iceServers, viper.DecodeHook( if err := viper.UnmarshalKey("webrtc.iceservers", &iceServers, viper.DecodeHook(
utils.JsonStringAutoDecode([]types.ICEServer{}), utils.JsonStringAutoDecode(iceServers),
)); err != nil { )); err != nil {
log.Warn().Err(err).Msgf("unable to parse global ICE servers") log.Warn().Err(err).Msgf("unable to parse global ICE servers")
} }
@ -409,7 +409,7 @@ func (s *WebRTC) SetV2() {
// set legacy flag if any V2 configuration was used // set legacy flag if any V2 configuration was used
if !viper.IsSet("legacy") && enableLegacy { if !viper.IsSet("legacy") && enableLegacy {
log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, or set 'NEKO_LEGACY=true' to acknowledge this message") log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details")
viper.Set("legacy", true) viper.Set("legacy", true)
} }
} }

View file

@ -10,14 +10,19 @@ import (
"github.com/m1k1o/neko/server/pkg/xevent" "github.com/m1k1o/neko/server/pkg/xevent"
) )
const (
ClipboardTextPlainTarget = "UTF8_STRING"
ClipboardTextHtmlTarget = "text/html"
)
func (manager *DesktopManagerCtx) ClipboardGetText() (*types.ClipboardText, error) { func (manager *DesktopManagerCtx) ClipboardGetText() (*types.ClipboardText, error) {
text, err := manager.ClipboardGetBinary("STRING") text, err := manager.ClipboardGetBinary(ClipboardTextPlainTarget)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Rich text must not always be available, can fail silently. // Rich text must not always be available, can fail silently.
html, _ := manager.ClipboardGetBinary("text/html") html, _ := manager.ClipboardGetBinary(ClipboardTextHtmlTarget)
return &types.ClipboardText{ return &types.ClipboardText{
Text: string(text), Text: string(text),
@ -31,10 +36,10 @@ func (manager *DesktopManagerCtx) ClipboardSetText(data types.ClipboardText) err
// is set, if available. Otherwise plain text. // is set, if available. Otherwise plain text.
if data.HTML != "" { if data.HTML != "" {
return manager.ClipboardSetBinary("text/html", []byte(data.HTML)) return manager.ClipboardSetBinary(ClipboardTextHtmlTarget, []byte(data.HTML))
} }
return manager.ClipboardSetBinary("STRING", []byte(data.Text)) return manager.ClipboardSetBinary(ClipboardTextPlainTarget, []byte(data.Text))
} }
func (manager *DesktopManagerCtx) ClipboardGetBinary(mime string) ([]byte, error) { func (manager *DesktopManagerCtx) ClipboardGetBinary(mime string) ([]byte, error) {
@ -53,6 +58,23 @@ func (manager *DesktopManagerCtx) ClipboardGetBinary(mime string) ([]byte, error
return stdout.Bytes(), nil return stdout.Bytes(), nil
} }
func (manager *DesktopManagerCtx) replaceClipboardCommand(newCmd *exec.Cmd) {
// Swap the current clipboard command with the new one.
oldCmd := manager.clipboardCommand.Swap(newCmd)
// If the command is already running, we need to shutdown it properly.
if oldCmd == nil || oldCmd.ProcessState != nil {
return
}
// If there is a previous command running and it was not stopped yet, we need to kill it.
if err := oldCmd.Process.Kill(); err != nil {
manager.logger.Err(err).Msg("failed to kill previous clipboard command")
} else {
manager.logger.Debug().Msg("killed previous clipboard command")
}
}
func (manager *DesktopManagerCtx) ClipboardSetBinary(mime string, data []byte) error { func (manager *DesktopManagerCtx) ClipboardSetBinary(mime string, data []byte) error {
cmd := exec.Command("xclip", "-selection", "clipboard", "-in", "-target", mime) cmd := exec.Command("xclip", "-selection", "clipboard", "-in", "-target", mime)
@ -64,7 +86,9 @@ func (manager *DesktopManagerCtx) ClipboardSetBinary(mime string, data []byte) e
return err return err
} }
// TODO: Refactor. // Shutdown previous command if it exists and replace it with the new one.
manager.replaceClipboardCommand(cmd)
// We need to wait until the data came to the clipboard. // We need to wait until the data came to the clipboard.
wait := make(chan struct{}) wait := make(chan struct{})
xevent.Emmiter.Once("clipboard-updated", func(payload ...any) { xevent.Emmiter.Once("clipboard-updated", func(payload ...any) {
@ -84,9 +108,23 @@ func (manager *DesktopManagerCtx) ClipboardSetBinary(mime string, data []byte) e
stdin.Close() stdin.Close()
// TODO: Refactor. select {
// cmd.Wait() case <-manager.shutdown:
<-wait return fmt.Errorf("clipboard manager is shutting down")
case <-wait:
}
manager.wg.Add(1)
go func() {
defer manager.wg.Done()
if err := cmd.Wait(); err != nil {
msg := strings.TrimSpace(stderr.String())
manager.logger.Err(err).Msgf("clipboard command finished with error: %s", msg)
} else {
manager.logger.Debug().Msg("clipboard command finished successfully")
}
}()
return nil return nil
} }

View file

@ -1,7 +1,9 @@
package desktop package desktop
import ( import (
"os/exec"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/kataras/go-events" "github.com/kataras/go-events"
@ -25,6 +27,11 @@ type DesktopManagerCtx struct {
config *config.Desktop config *config.Desktop
screenSize types.ScreenSize // cached screen size screenSize types.ScreenSize // cached screen size
input xinput.Driver input xinput.Driver
// Clipboard process holding the most recent clipboard data.
// It must remain running to allow pasting clipboard data.
// The last command is kept running until it is replaced or shutdown.
clipboardCommand atomic.Pointer[exec.Cmd]
} }
func New(config *config.Desktop) *DesktopManagerCtx { func New(config *config.Desktop) *DesktopManagerCtx {
@ -131,6 +138,8 @@ func (manager *DesktopManagerCtx) Shutdown() error {
manager.logger.Info().Msgf("shutdown") manager.logger.Info().Msgf("shutdown")
close(manager.shutdown) close(manager.shutdown)
manager.replaceClipboardCommand(nil)
manager.wg.Wait() manager.wg.Wait()
xorg.DisplayClose() xorg.DisplayClose()

View file

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"time"
"github.com/m1k1o/neko/server/internal/api" "github.com/m1k1o/neko/server/internal/api"
oldEvent "github.com/m1k1o/neko/server/internal/http/legacy/event" oldEvent "github.com/m1k1o/neko/server/internal/http/legacy/event"
@ -35,9 +36,6 @@ var (
return true return true
}, },
} }
// DefaultDialer is a dialer with all fields set to the default zero values.
DefaultDialer = websocket.DefaultDialer
) )
type LegacyHandler struct { type LegacyHandler struct {
@ -45,16 +43,21 @@ type LegacyHandler struct {
serverAddr string serverAddr string
bannedIPs map[string]struct{} bannedIPs map[string]struct{}
sessionIPs map[string]string sessionIPs map[string]string
wsDialer *websocket.Dialer
} }
func New() *LegacyHandler { func New(serverAddr string) *LegacyHandler {
// Init // Init
return &LegacyHandler{ return &LegacyHandler{
logger: log.With().Str("module", "legacy").Logger(), logger: log.With().Str("module", "legacy").Logger(),
serverAddr: "127.0.0.1:8080", serverAddr: serverAddr,
bannedIPs: make(map[string]struct{}), bannedIPs: make(map[string]struct{}),
sessionIPs: make(map[string]string), sessionIPs: make(map[string]string),
wsDialer: &websocket.Dialer{
Proxy: nil, // disable proxy for local requests
HandshakeTimeout: 45 * time.Second,
},
} }
} }
@ -99,7 +102,7 @@ func (h *LegacyHandler) Route(r types.Router) {
defer s.destroy() defer s.destroy()
// dial to the remote backend // dial to the remote backend
connBackend, _, err := DefaultDialer.Dial("ws://"+h.serverAddr+"/api/ws?token="+url.QueryEscape(s.token), nil) connBackend, _, err := h.wsDialer.Dial("ws://"+h.serverAddr+"/api/ws?token="+url.QueryEscape(s.token), nil)
if err != nil { if err != nil {
h.logger.Error().Err(err).Msg("couldn't dial to the remote backend") h.logger.Error().Err(err).Msg("couldn't dial to the remote backend")
@ -142,10 +145,12 @@ func (h *LegacyHandler) Route(r types.Router) {
m = websocket.FormatCloseMessage(e.Code, e.Text) m = websocket.FormatCloseMessage(e.Code, e.Text)
} }
} }
errc <- err errc <- fmt.Errorf("src read message error: %w", err)
dst.WriteMessage(websocket.CloseMessage, m) dst.WriteMessage(websocket.CloseMessage, m)
break break
} }
// handle text messages
if msgType == websocket.TextMessage { if msgType == websocket.TextMessage {
err = rewriteTextMessage(msg) err = rewriteTextMessage(msg)
@ -162,12 +167,26 @@ func (h *LegacyHandler) Route(r types.Router) {
Message: strings.ReplaceAll(err.Error(), ErrBackendRespone.Error()+": ", ""), Message: strings.ReplaceAll(err.Error(), ErrBackendRespone.Error()+": ", ""),
}) })
continue continue
} else if errors.Is(err, ErrWebsocketSend) { }
if errors.Is(err, ErrWebsocketSend) {
errc <- fmt.Errorf("dst write message error: %w", err)
break
}
h.logger.Error().Err(err).Msg("couldn't rewrite text message")
continue
}
// forward ping pong messages
if msgType == websocket.PingMessage ||
msgType == websocket.PongMessage {
err = dst.WriteMessage(msgType, msg)
if err != nil {
errc <- err errc <- err
break break
} else {
h.logger.Error().Err(err).Msg("couldn't rewrite text message")
} }
continue
} }
} }
} }
@ -181,9 +200,9 @@ func (h *LegacyHandler) Route(r types.Router) {
var message string var message string
select { select {
case err = <-errClient: case err = <-errClient:
message = "websocketproxy: Error when copying from backend to client: %v" message = "websocketproxy: Error when copying from backend to client"
case err = <-errBackend: case err = <-errBackend:
message = "websocketproxy: Error when copying from client to backend: %v" message = "websocketproxy: Error when copying from client to backend"
} }
if e, ok := err.(*websocket.CloseError); !ok || e.Code == websocket.CloseAbnormalClosure { if e, ok := err.(*websocket.CloseError); !ok || e.Code == websocket.CloseAbnormalClosure {

View file

@ -55,12 +55,17 @@ type session struct {
} }
func (h *LegacyHandler) newSession(r *http.Request) *session { func (h *LegacyHandler) newSession(r *http.Request) *session {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.Proxy = nil // disable proxy for local requests
return &session{ return &session{
r: r, r: r,
h: h, h: h,
logger: h.logger, logger: h.logger,
serverAddr: h.serverAddr, serverAddr: h.serverAddr,
client: http.DefaultClient, client: &http.Client{
Transport: transport,
},
sessions: make(map[string]*memberStruct), sessions: make(map[string]*memberStruct),
} }
} }

View file

@ -2,6 +2,7 @@ package http
import ( import (
"context" "context"
"net"
"net/http" "net/http"
"os" "os"
@ -56,11 +57,6 @@ func New(WebSocketManager types.WebSocketManager, ApiManager types.ApiManager, c
return config.AllowOrigin(r.Header.Get("Origin")) return config.AllowOrigin(r.Header.Get("Origin"))
})) }))
// Legacy handler
if viper.GetBool("legacy") {
legacy.New().Route(router)
}
batch := batchHandler{ batch := batchHandler{
Router: router, Router: router,
PathPrefix: "/api", PathPrefix: "/api",
@ -122,6 +118,24 @@ func (manager *HttpManagerCtx) Start() {
} }
}() }()
manager.logger.Info().Msgf("https listening on %s", manager.http.Addr) manager.logger.Info().Msgf("https listening on %s", manager.http.Addr)
// if we have legacy mode, we need to start local http server too
if viper.GetBool("legacy") {
// create a listener for the API server with a random port
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
manager.logger.Panic().Err(err).Msg("unable to start legacy http proxy")
}
go func() {
if err := http.Serve(listener, manager.router); err != http.ErrServerClosed {
manager.logger.Panic().Err(err).Msg("unable to start http server")
}
}()
manager.logger.Info().Msgf("legacy proxy listening on %s", listener.Addr().String())
legacy.New(listener.Addr().String()).Route(manager.router)
}
} else { } else {
go func() { go func() {
if err := manager.http.ListenAndServe(); err != http.ErrServerClosed { if err := manager.http.ListenAndServe(); err != http.ErrServerClosed {
@ -129,6 +143,11 @@ func (manager *HttpManagerCtx) Start() {
} }
}() }()
manager.logger.Info().Msgf("http listening on %s", manager.http.Addr) manager.logger.Info().Msgf("http listening on %s", manager.http.Addr)
// start legacy proxy if enabled
if viper.GetBool("legacy") {
legacy.New(manager.http.Addr).Route(manager.router)
}
} }
} }

View file

@ -78,7 +78,7 @@ func (manager *WebRTCManagerCtx) handleLegacy(
} }
logger. logger.
Debug(). Trace().
Str("x", strconv.Itoa(int(payload.X))). Str("x", strconv.Itoa(int(payload.X))).
Str("y", strconv.Itoa(int(payload.Y))). Str("y", strconv.Itoa(int(payload.Y))).
Msg("scroll") Msg("scroll")
@ -97,7 +97,7 @@ func (manager *WebRTCManagerCtx) handleLegacy(
return nil return nil
} }
logger.Debug().Msgf("button down %d", payload.Key) logger.Trace().Msgf("button down %d", payload.Key)
} else { } else {
err := manager.desktop.KeyDown(uint32(payload.Key)) err := manager.desktop.KeyDown(uint32(payload.Key))
if err != nil { if err != nil {
@ -105,7 +105,7 @@ func (manager *WebRTCManagerCtx) handleLegacy(
return nil return nil
} }
logger.Debug().Msgf("key down %d", payload.Key) logger.Trace().Msgf("key down %d", payload.Key)
} }
case OP_KEY_UP: case OP_KEY_UP:
payload := &PayloadKey{} payload := &PayloadKey{}
@ -121,7 +121,7 @@ func (manager *WebRTCManagerCtx) handleLegacy(
return nil return nil
} }
logger.Debug().Msgf("button up %d", payload.Key) logger.Trace().Msgf("button up %d", payload.Key)
} else { } else {
err := manager.desktop.KeyUp(uint32(payload.Key)) err := manager.desktop.KeyUp(uint32(payload.Key))
if err != nil { if err != nil {
@ -129,7 +129,7 @@ func (manager *WebRTCManagerCtx) handleLegacy(
return nil return nil
} }
logger.Debug().Msgf("key up %d", payload.Key) logger.Trace().Msgf("key up %d", payload.Key)
} }
case OP_KEY_CLK: case OP_KEY_CLK:
// unused // unused

View file

@ -276,7 +276,9 @@ func (manager *WebSocketManagerCtx) connect(connection *websocket.Conn, r *http.
e, ok := err.(*websocket.CloseError) e, ok := err.(*websocket.CloseError)
if !ok { if !ok {
err = errors.Unwrap(err) // unwrap if possible if e := errors.Unwrap(err); e != nil {
err = e // unwrap if possible
}
logger.Warn().Err(err).Msg("read message error") logger.Warn().Err(err).Msg("read message error")
// client is expected to reconnect soon // client is expected to reconnect soon
delayedDisconnect = true delayedDisconnect = true

View file

@ -43,7 +43,9 @@ func (peer *WebSocketPeerCtx) Send(event string, payload any) {
}) })
if err != nil { if err != nil {
err = errors.Unwrap(err) // unwrap if possible if e := errors.Unwrap(err); e != nil {
err = e // unwrap if possible
}
peer.logger.Warn().Err(err).Str("event", event).Msg("send message error") peer.logger.Warn().Err(err).Str("event", event).Msg("send message error")
return return
} }

3
utils/docker/go.mod Normal file
View file

@ -0,0 +1,3 @@
module github.com/m1k1o/neko/utils/docker
go 1.24.1

Some files were not shown because too many files have changed in this diff Show more