Compare commits

...

94 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
Miroslav Šedivý
4ab3dccc7f fix amd64 & build. 2025-03-30 16:16:58 +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 2648 additions and 1405 deletions

View file

@ -1,17 +1,21 @@
name: Build and Publish Client Artifacts
name: Build Client
on:
workflow_call:
#pull_request: # Change to push when ready to deploy
# branches:
# - master
# paths:
# - client/**
# - .github/workflows/client_build.yml
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:
client_build:
name: Build and Publish Client Artifacts
build-client:
name: Build Client
runs-on: ubuntu-latest
steps:
@ -35,6 +39,7 @@ jobs:
- name: Upload artifacts
uses: actions/upload-artifact@v4
if: ${{ inputs.with-artifact }}
with:
name: client
path: client/dist

View file

@ -1,4 +1,4 @@
name: Test Client Build
name: Test Client
on:
pull_request:
@ -6,28 +6,14 @@ on:
- master
paths:
- client/**
- .github/workflows/client_build.yml
- .github/workflows/client_test.yml
jobs:
client_test:
name: Test Client Build
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: client/package-lock.json
- name: Install dependencies
working-directory: ./client
run: npm ci
- name: Build client
working-directory: ./client
run: npm run build
test-client:
name: Test Client
uses: ./.github/workflows/client_build.yml
with:
# Do not upload artifacts for test builds
with-artifact: false
secrets: inherit

View file

@ -1,8 +1,11 @@
name: "build and push amd64 images to Docker Hub"
name: Build and Push to Docker Hub
on:
push:
branches: [ master ]
branches:
- master
paths-ignore:
- 'webpage/**'
#
# Run this action periodically to keep browsers up-to-date
# even if there is no activity in this repo.
@ -10,68 +13,119 @@ on:
schedule:
- 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:
DOCKER_IMAGE: m1k1o/neko
jobs:
build-base:
name: Base Image
runs-on: ubuntu-latest
#
# do not run on forks
#
if: github.repository_owner == 'm1k1o'
steps:
- name: Check Out Repo
uses: actions/checkout@v2
- name: Checkout
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
run: |
docker login --username "${DOCKER_USERNAME}" --password-stdin "${DOCKER_REGISTRY}" <<< "${DOCKER_TOKEN}"
env:
DOCKER_REGISTRY: ${{ secrets.DOCKER_REGISTRY }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
uses: docker/login-action@v3
with:
username: ${{ github.actor }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build base
run: |
./build -b ${DOCKER_IMAGE}:base
docker push ${DOCKER_IMAGE}:base
- name: Generate base Dockerfile
run: go run utils/docker/main.go -i Dockerfile.tmpl -o Dockerfile
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
#
# do not run on forks
#
if: github.repository_owner == 'm1k1o'
needs: [ build-base ]
needs: build-base
strategy:
# Will build all images even if some fail.
fail-fast: false
matrix:
tags: [ firefox, waterfox, chromium, google-chrome, ungoogled-chromium, microsoft-edge, brave, vivaldi, opera, tor-browser, remmina, vlc, xfce, kde ]
env:
DOCKER_TAG: ${{ matrix.tags }}
tag:
- firefox
# 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:
- name: Check Out Repo
uses: actions/checkout@v2
- name: Checkout
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
run: |
docker login --username "${DOCKER_USERNAME}" --password-stdin "${DOCKER_REGISTRY}" <<< "${DOCKER_TOKEN}"
env:
DOCKER_REGISTRY: ${{ secrets.DOCKER_REGISTRY }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
uses: docker/login-action@v3
with:
username: ${{ github.actor }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build container
run: |
./build -b ${DOCKER_IMAGE}:base -i ${DOCKER_IMAGE}
docker tag ${DOCKER_IMAGE}/${DOCKER_TAG} ${DOCKER_IMAGE}:${DOCKER_TAG}
docker push ${DOCKER_IMAGE}:${DOCKER_TAG}
- name: Push latest tag
if: ${{ matrix.tags == 'firefox' }}
run: |
docker pull ${DOCKER_IMAGE}:${DOCKER_TAG}
docker tag ${DOCKER_IMAGE}:${DOCKER_TAG} ${DOCKER_IMAGE}:latest
docker push ${DOCKER_IMAGE}:latest
- name: Build and push
uses: docker/build-push-action@v6
with:
context: apps/${{ matrix.tag }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
BASE_IMAGE=${{ env.DOCKER_IMAGE }}:base
cache-from: type=gha
cache-to: type=gha,mode=max

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

View file

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

View file

@ -1,4 +1,4 @@
name: Build and Publish Application Image
name: Build App Image
on:
workflow_call:
@ -6,12 +6,7 @@ on:
name:
required: true
type: string
description: "The name of the application to build."
dockerfile:
required: false
type: string
default: "Dockerfile"
description: "The Dockerfile to use for building the image."
description: "The name of the app to build."
flavor:
required: false
type: string
@ -22,17 +17,18 @@ on:
type: string
default: "linux/amd64"
description: "The platforms to build for."
secrets:
GHCR_ACCESS_TOKEN:
required: true
description: "GitHub Container Registry access token."
dockerfile:
required: false
type: string
default: "Dockerfile"
description: "The Dockerfile to use for building the image."
env:
FLAVOR_PREFIX: ${{ inputs.flavor && format('{0}-', inputs.flavor) || '' }}
jobs:
build-app:
name: Build and Publish Application Image
name: Build App Image
runs-on: ubuntu-latest
steps:
- name: Checkout
@ -52,6 +48,7 @@ jobs:
with:
images: ghcr.io/${{ github.repository }}/${{ env.FLAVOR_PREFIX }}${{ inputs.name }}
tags: |
type=edge,branch=master
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
@ -75,3 +72,5 @@ jobs:
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository }}/${{ env.FLAVOR_PREFIX }}base:sha-${{ github.sha }}
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:
workflow_call:
inputs:
dockerfile:
required: false
type: string
default: "Dockerfile"
description: "The Dockerfile to use for building the image."
flavor:
required: false
type: string
@ -18,22 +13,24 @@ on:
type: string
default: "linux/amd64"
description: "The platforms to build for."
secrets:
GHCR_ACCESS_TOKEN:
required: true
description: "GitHub Container Registry access token."
dockerfile:
required: false
type: string
default: "Dockerfile"
description: "The Dockerfile to use for building the image."
env:
FLAVOR_PREFIX: ${{ inputs.flavor && format('{0}-', inputs.flavor) || '' }}
jobs:
build-client:
name: Build Client Artifacts
uses: ./.github/workflows/client_build.yml
build-base:
name: Build and Publish Base Image
name: Build Base Image
runs-on: ubuntu-latest
needs: [ build-client ]
needs: build-client
steps:
- name: Checkout
uses: actions/checkout@v4
@ -58,6 +55,7 @@ jobs:
with:
images: ghcr.io/${{ github.repository }}/${{ env.FLAVOR_PREFIX }}base
tags: |
type=edge,branch=master
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
@ -73,7 +71,7 @@ jobs:
- name: Generate base Dockerfile
env:
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
uses: docker/build-push-action@v6
@ -83,3 +81,5 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
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:
pull_request:
@ -9,8 +9,8 @@ on:
- .github/workflows/server_test.yml
jobs:
server_test:
name: Test Server Build
build-amd64:
name: Build amd64
runs-on: ubuntu-latest
permissions:
contents: read
@ -24,3 +24,21 @@ jobs:
uses: docker/build-push-action@v6
with:
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
on:
# Runs on pushes targeting the default branch
push:
branches:
- master
paths:
- webpage/**
- .github/workflows/webpage_build.yml
- .github/workflows/webpage_deploy.yml
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
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:
webpage_build:
name: Build and Deploy Webpage to GitHub Pages
runs-on: ubuntu-latest
build-webpage:
name: Build Webpage
uses: ./.github/workflows/webpage_build.yml
secrets: inherit
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
- name: Upload Build Artifact
uses: actions/upload-pages-artifact@v3
with:
artifact-name: github-pages
path: ./webpage/build
webpage_deploy:
deploy-webpage:
name: Deploy to GitHub Pages
needs: webpage_build
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source
needs: build-webpage
# Deploy to the github-pages environment
environment:

View file

@ -1,4 +1,4 @@
name: Test Webpage Build
name: Test Webpage
on:
pull_request:
@ -6,26 +6,14 @@ on:
- master
paths:
- webpage/**
- .github/workflows/webpage_build.yml
- .github/workflows/webpage_test.yml
jobs:
webpage_test:
name: Test Webpage Build
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
test-webpage:
name: Test Webpage
uses: ./.github/workflows/webpage_build.yml
with:
# Do not upload artifacts for test builds
with-artifact: false
secrets: inherit

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 ./client/ AS client
FROM ./utils/xorg-deps/ AS xorg-deps
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
COPY --from=xorg-deps /usr/local/lib/xorg/modules/input/neko_drv.so /usr/lib/xorg/modules/input/neko_drv.so
# tells neko-rooms which version of the API to use
LABEL net.m1k1o.neko.api-version=3
COPY --from=server /src/bin/plugins/ /etc/neko/plugins/
COPY --from=server /src/bin/neko /usr/bin/neko
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

118
README.md
View file

@ -1,6 +1,6 @@
<div align="center">
<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>
<p align="center">
<a href="https://github.com/m1k1o/neko/releases">
@ -21,11 +21,14 @@
<a href="https://discord.gg/3U6hWpC">
<img src="https://discordapp.com/api/guilds/665851821906067466/widget.png" alt="Chat on discord">
</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">
<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>
</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>
# n.eko
@ -83,47 +86,60 @@ Compared to clientless remote desktop gateway (e.g. [Apache Guacamole](https://g
### Supported browsers
<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"/>
<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://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/chromium.svg" title="m1k1o/neko:chromium" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/microsoft-edge.svg" title="m1k1o/neko:microsoft-edge" width="60" height="auto"/>
<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://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/vivaldi.svg" title="m1k1o/neko:vivaldi" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/opera.svg" title="m1k1o/neko:opera" width="60" height="auto"/>
<img src="https://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/tor-browser.svg" title="m1k1o/neko:tor-browser" width="60" height="auto"/>
<a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#firefox">
<img src="https://neko.m1k1o.net/img/icons/firefox.svg" title="ghcr.io/m1k1o/neko/firefox" width="60" height="auto"/>
</a>
<a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#tor-browser">
<img src="https://neko.m1k1o.net/img/icons/tor-browser.svg" title="ghcr.io/m1k1o/neko/tor-browser" width="60" height="auto"/>
</a>
<a href="https://neko.m1k1o.net/docs/v3/installation/docker-images#waterfox">
<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>
### Other programs
### Other applications
<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"/>
<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://raw.githubusercontent.com/m1k1o/neko/master/docs/_media/icons/xfce.svg" title="m1k1o/neko:xfce" width="60" height="auto"/>
<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#xfce">
<img src="https://neko.m1k1o.net/img/icons/xfce.svg" title="ghcr.io/m1k1o/neko/xfce" width="60" height="auto"/>
</a>
<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>
</div>
### Features
* 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?
### Why neko?
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
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
* [Getting Started](https://neko.m1k1o.net/#/getting-started/)
* [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)
Full documentation is available at [neko.m1k1o.net](https://neko.m1k1o.net/). Key sections include:
## 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
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
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends apt-transport-https curl openbox; \
#
# 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; \
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; \
apt-get update; \
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
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
--ignore-gpu-blocklist
--disable-seccomp-filter-sandbox
--use-gl=egl
--use-angle=vulkan
--disable-software-rasterizer
--disable-dev-shm-usage
--disable-vulkan-surface
--enable-unsafe-webgpu
stopsignal=INT
autorestart=true
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
#
@ -8,10 +8,10 @@ RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends chromium chromium-common chromium-sandbox openbox; \
#
# install widevine module (only for x86_64)
# install widevine module (only for amd64)
CHROMIUM_DIR="/usr/lib/chromium"; \
ARCH=$(dpkg --print-architecture); \
if [ "${ARCH}" = "x86_64" ]; then \
if [ "${ARCH}" = "amd64" ]; then \
# https://commondatastorage.googleapis.com/chromeos-localmirror/distfiles/chromeos-lacros-arm64-squash-zstd-120.0.6098.0
apt-get install -y --no-install-recommends unzip; \
WIDEVINE_ARCH="x64"; \

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
#

View file

@ -12,9 +12,11 @@ command=/bin/entrypoint.sh /usr/bin/chromium
--enable-features=Vulkan,UseSkiaRenderer,VaapiVideoEncoder,VaapiVideoDecoder,CanvasOopRasterization
--ignore-gpu-blocklist
--disable-seccomp-filter-sandbox
--use-gl=egl
--use-angle=vulkan
--disable-software-rasterizer
--disable-dev-shm-usage
--disable-vulkan-surface
--enable-unsafe-webgpu
stopsignal=INT
autorestart=true
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
#
@ -9,8 +9,8 @@ RUN set -eux; apt-get update; \
if [ "${ARCH}" = "armhf" ]; then \
#
# install firefox-esr for armhf
apt-get install -y --no-install-recommends openbox firefox-esr; \
ln -s /usr/bin/firefox-esr /usr/bin/firefox; \
apt-get install -y --no-install-recommends firefox-esr; \
ln -s /usr/lib/firefox-esr /usr/lib/firefox; \
#
# install extensions
mkdir -p /usr/lib/firefox-esr/distribution/extensions; \
@ -18,11 +18,11 @@ RUN set -eux; apt-get update; \
wget -O '/usr/lib/firefox-esr/distribution/extensions/sponsorBlocker@ajay.app.xpi' https://addons.mozilla.org/firefox/downloads/latest/sponsorblock/latest.xpi; \
else \
#
# fetch latest release (for x86_64 and arm64)
if [ "${ARCH}" = "x86_64" ]; then \
# fetch latest release (for amd64 and arm64)
if [ "${ARCH}" = "amd64" ]; then \
SRC_URL="https://download.mozilla.org/?product=firefox-latest&os=linux64&lang=en-US"; \
elif [ "${ARCH}" = "arm64" ]; then \
SRC_URL="https://download.mozilla.org/?product=firefox-latest&os=linux-aarch64&lang=en-US"; \
SRC_URL="https://download.mozilla.org/?product=firefox-latest&os=linux64-aarch64&lang=en-US"; \
fi; \
if [ ! -z "${SRC_URL}" ]; then \
apt-get install -y --no-install-recommends xz-utils libgtk-3-0 libdbus-glib-1-2; \

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
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("signon.prefillForms", false);
lockPref("signon.rememberSignons", false);
lockPref("xpinstall.enabled", false);
lockPref("xpinstall.whitelist.required", true);
//lockPref("xpinstall.enabled", false);
//lockPref("xpinstall.whitelist.required", true);
lockPref("browser.download.manager.retention", 0);
lockPref("browser.download.folderList", 2);
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
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
# 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
--ignore-gpu-blocklist
--disable-seccomp-filter-sandbox
--use-gl=egl
--use-angle=vulkan
--disable-software-rasterizer
--disable-dev-shm-usage
--disable-vulkan-surface
--enable-unsafe-webgpu
stopsignal=INT
autorestart=true
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
#

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base
ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE
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
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
--ignore-gpu-blocklist
--disable-seccomp-filter-sandbox
--use-gl=egl
--use-angle=vulkan
--disable-software-rasterizer
--disable-dev-shm-usage
--disable-vulkan-surface
--enable-unsafe-webgpu
stopsignal=INT
autorestart=true
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
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
# install remmina

View file

@ -1,5 +1,4 @@
#!/bin/bash
set -u
err() {
echo "ERROR: $*" >&2
@ -16,23 +15,28 @@ if [[ -n "$REMMINA_PROFILE" ]]; then
exec remmina -c "$profile"
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|' )
proto="${arr[0]}"
user="${arr[1]}"
pw="${arr[2]}"
host="${arr[3]}"
echo "Parsed url in 'REMMINA_URL': proto:$proto username:$user host:$host"
readarray -t arr < <( echo -n "$REMMINA_URL" | perl -pe 's|^(\w+)\:\/\/(?:([^:]+)(?::([^@]+))?@)?(.*)$|\1\n\2\n\3\n\4|' )
proto="${arr[0]}"
user="${arr[1]}"
pw="${arr[2]}"
host="${arr[3]}"
echo "Parsed url in 'REMMINA_URL': proto:$proto username:$user host:$host"
[[ "$proto" != "vnc" && "$proto" != "rdp" && "$proto" != "spice" ]] && err "Unsupported protocol $proto in connection url 'REMMINA_URL'"
[[ "$proto" != "vnc" && "$proto" != "rdp" && "$proto" != "spice" ]] && err "Unsupported protocol $proto in connection url 'REMMINA_URL'"
profile="$profile_dir"/"$proto".remmina
remmina --set-option username="$user" --update-profile "$profile"
remmina --set-option password="$pw" --update-profile "$profile"
remmina --set-option server="$host" --update-profile "$profile"
profile="$profile_dir"/"$proto".remmina
remmina --set-option username="$user" --update-profile "$profile"
remmina --set-option password="$pw" --update-profile "$profile"
remmina --set-option server="$host" --update-profile "$profile"
# remmina --set-option window_maximize=1 --update-profile "$profile"
# remmina --set-option scale=1 --update-profile "$profile"
# remmina --set-option window_maximize=1 --update-profile "$profile"
# remmina --set-option scale=1 --update-profile "$profile"
echo "Running remmina with URL $REMMINA_URL"
exec remmina -c "$profile"
fi
exec remmina -c "$profile"
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
#

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base
ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE
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
ARG VIVALDI_VERSION="5.3.2679.34-1"
# TODO: Get chromium version from vivaldi
ARG CHROMIUM_VERSION="102.0.5005.72"
#
# install vivaldi
SHELL ["/bin/bash", "-c"]
RUN set -eux; apt-get update; \
wget -O /tmp/vivaldi.deb "https://downloads.vivaldi.com/stable/vivaldi-stable_${VIVALDI_VERSION}_amd64.deb"; \
apt-get install -y --no-install-recommends wget unzip xz-utils jq openbox /tmp/vivaldi.deb; \
/opt/vivaldi/update-ffmpeg; \
ARCH=$(dpkg --print-architecture); \
wget -O /tmp/vivaldi.deb "https://downloads.vivaldi.com/stable/vivaldi-stable_${ARCH}.deb"; \
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
EXTENSIONS_DIR="/usr/share/chromium/extensions"; \
@ -22,7 +19,7 @@ RUN set -eux; apt-get update; \
mkdir -p "${EXTENSIONS_DIR}"; \
for EXT_ID in "${EXTENSIONS[@]}"; \
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"; \
wget -O "${EXT_PATH}" "${EXT_URL}"; \
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
#

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=m1k1o/neko:base
ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE
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; \
#
# 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; \
tar -xjf /tmp/waterfox-setup.tar.bz2 -C /usr/lib; \
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("signon.prefillForms", false);
lockPref("signon.rememberSignons", false);
lockPref("xpinstall.enabled", false);
lockPref("xpinstall.whitelist.required", true);
//lockPref("xpinstall.enabled", false);
//lockPref("xpinstall.whitelist.required", true);
lockPref("browser.download.manager.retention", 0);
lockPref("browser.download.folderList", 2);
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
#

109
build
View file

@ -2,39 +2,57 @@
set -e
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
# if you want to use buildx, set USE_BUILDX=1
if [ -z "$USE_BUILDX" ]; then
USE_BUILDX=0
fi
# 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 docker buildx version >/dev/null 2>&1; then
USE_BUILDX=1
fi
fi
#
# This script builds the neko base image and all the applications
#
#if [ -z "$USE_BUILDX" ] && [ -x "$(command -v docker)" ]; then
# if docker buildx version >/dev/null 2>&1; then
# USE_BUILDX=1
# fi
#fi
function log() {
echo "$(date +'%Y-%m-%d %H:%M:%S') - [NEKO] - $1" > /dev/stderr
}
function help() {
echo "Usage: $0"
echo " -p, --platform : The platform (default: linux/amd64)"
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 " uses 'latest' and if available, current git semver tag (v*.*.*)"
echo " -f, --flavor : The flavor, if not specified, builds without flavor"
echo " -b, --base : The base image name (default: <repository>[<flavor>-]base:<tag>)"
echo " -a, --app : The app to build, if not specified, builds the base image"
echo " -y, --yes : Skip confirmation prompts"
echo " --no-cache : Build without docker cache"
echo " --push : Push the image to the registry after building"
echo " -h, --help : Show this help message"
echo "Usage: $0 [options] [image]"
echo
echo "Options:"
echo " -p, --platform : The platform (default: system architecture)"
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 " uses 'latest' and if available, current git semver tag (v*.*.*)"
echo " -f, --flavor : The flavor, if not specified, builds without flavor"
echo " -b, --base_image : The base image name (default: <repository>[<flavor>-]base:<tag>)"
echo " -a, --application : The app to build, if not specified, builds the base image"
echo " -y, --yes : Skip confirmation prompts"
echo " --no-cache : Build without docker cache"
echo " --push : Push the image to the registry after building"
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=""
@ -44,8 +62,8 @@ while [[ "$#" -gt 0 ]]; do
--repository|-r) REPOSITORY="$2"; shift ;;
--tag|-t) TAGS+=("$2"); TAG="$2"; shift ;;
--flavor|-f) FLAVOR="$2"; shift ;;
--base|-b) BASE_IMAGE="$2"; shift ;;
--app|-a) APPLICATION="$2"; shift ;;
--base_image|-b) BASE_IMAGE="$2"; shift ;;
--application|-a) APPLICATION="$2"; shift ;;
--yes|-y) YES=1 ;;
--no-cache) NO_CACHE="--no-cache" log "Building without cache" ;;
--push) PUSH=1 ;;
@ -131,17 +149,12 @@ function build_image() {
local APPLICATION_IMAGE="$1"
shift
# get list of tags in full format: <image>:<tag>
local IMAGE_NO_TAG="${APPLICATION_IMAGE%:*}"
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>
local IMAGE_NO_TAG="${APPLICATION_IMAGE%:*}"
for T in "${TAGS[@]}"; do
FULL_TAGS+=("$IMAGE_NO_TAG:$T")
done
fi
for T in "${TAGS[@]}"; do
FULL_TAGS+=("$IMAGE_NO_TAG:$T")
done
if [ -z "$USE_BUILDX" ] || [ "$USE_BUILDX" != "1" ]; then
# if buildx is not available, use docker build
@ -197,7 +210,18 @@ else
fi
if [ -z "$PLATFORM" ]; then
PLATFORM="linux/amd64"
# use system architecture if not specified
UNAME="$(uname -m)"
if [ "$UNAME" == "x86_64" ]; then
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
log "Using platform: $PLATFORM"
@ -283,22 +307,17 @@ fi
prompt "Are you sure you want to build $BASE_IMAGE?"
if [ -z "$FLAVOR" ]; then
export RUNTIME_DOCKERFILE="Dockerfile"
else
if [ -f "runtime/Dockerfile.$FLAVOR" ]; then
export RUNTIME_DOCKERFILE="Dockerfile.$FLAVOR"
else
export RUNTIME_DOCKERFILE="Dockerfile"
fi
RUNTIME_DOCKERFILE="Dockerfile"
if [ ! -z "$FLAVOR" ] && [ -f "runtime/Dockerfile.$FLAVOR" ]; then
RUNTIME_DOCKERFILE="Dockerfile.$FLAVOR"
fi
log "Building base image: $BASE_IMAGE"
docker run --rm -i \
-v ./:/src \
-v "$(pwd)":/src \
-e "RUNTIME_DOCKERFILE=$RUNTIME_DOCKERFILE" \
--workdir /src \
--entrypoint go \
golang:1.24-bullseye \
run ./docker/main.go \
-i Dockerfile.tmpl | build_image $BASE_IMAGE -f - .
run utils/docker/main.go \
-i Dockerfile.tmpl -client "$CLIENT_DIST" | build_image $BASE_IMAGE -f - .

View file

@ -119,23 +119,43 @@
}
}
@media only screen and (max-width: 600px) {
#neko.expanded {
.neko-main {
transform: translateX(calc(-100% + 65px));
@media only screen and (max-width: 1024px) {
html,
body {
overflow-y: auto !important;
width: auto !important;
height: auto !important;
}
video {
display: none;
}
body > p {
display: none;
}
#neko {
position: relative;
flex-direction: column;
max-height: initial !important;
.neko-main {
height: 100vh;
}
.neko-menu {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 65px;
width: calc(100% - 65px);
height: 100vh;
width: 100% !important;
}
}
}
@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() {
if (this.shakeKbd || this.$accessor.remote.hosted) return

View file

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

View file

@ -60,8 +60,9 @@
</style>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { Component, Vue, Watch } from 'vue-property-decorator'
import { messages } from '~/locale'
import { set } from '~/utils/localstorage'
@Component({ name: 'neko-menu' })
export default class extends Vue {
@ -77,6 +78,11 @@
this.$accessor.client.toggleAbout()
}
@Watch('$i18n.locale')
onLanguageChange(newLang: string) {
set('lang', newLang)
}
mounted() {
const default_lang = new URL(location.href).searchParams.get('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="!controlLocked && !implicitHosting" :class="extraControls || 'extra-control'">
<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"
/>
</li>
@ -57,6 +62,13 @@
class="fas fa-external-link-alt"
/>
</li>
<li
v-if="hosting && is_touch_device"
:class="extraControls || 'extra-control'"
@click.stop.prevent="openMobileKeyboard"
>
<i class="fas fa-keyboard" />
</li>
</ul>
<neko-resolution ref="resolution" v-if="admin" />
<neko-clipboard ref="clipboard" v-if="hosting && (!clipboard_read_available || !clipboard_write_available)" />
@ -117,7 +129,7 @@
}
@media (max-width: 768px) {
&.extra-control {
display: inline-block;
display: block;
}
}
@ -251,6 +263,10 @@
return this.$accessor.connecting
}
get controlling() {
return this.$accessor.remote.controlling
}
get hosting() {
return this.$accessor.remote.hosting
}
@ -353,6 +369,16 @@
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')
onWidthChanged() {
this.onResize()
@ -730,12 +756,17 @@
first.target.dispatchEvent(simulatedEvent)
}
isMouseDown = false
onMouseDown(e: MouseEvent) {
if (!this.hosting) {
this.$emit('control-attempt', e)
this.isMouseDown = true
if (this.locked) {
return
}
if (!this.hosting || this.locked) {
if (!this.controlling) {
this.implicitHostingRequest(e)
return
}
@ -744,7 +775,16 @@
}
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
}
@ -752,6 +792,40 @@
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) {
if (!this.hosting || this.locked) {
return
@ -799,10 +873,20 @@
@Watch('hosting')
@Watch('locked')
onFocus() {
// focus opens the keyboard on mobile
if (this.is_touch_device) {
return
}
// in order to capture key events, overlay must be focused
if (this.focused && this.hosting && !this.locked) {
this._overlay.focus()
}
}
openMobileKeyboard() {
// focus opens the keyboard on mobile
this._overlay.focus()
}
}
</script>

View file

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

View file

@ -1,11 +1,31 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import { messages } from '~/locale'
import { get } from '~/utils/localstorage'
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({
locale: 'en',
fallbackLocale: 'en',
locale: get<string>('lang', detectBrowserLanguage()),
fallbackLocale,
messages,
})

View file

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

View file

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

View file

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

View file

@ -95,8 +95,9 @@ ENV NEKO_PLUGINS_DIR=/etc/neko/plugins/
#
# add healthcheck
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
CMD ["/usr/bin/supervisord", "-c", "/etc/neko/supervisord.conf"]

View file

@ -115,8 +115,9 @@ ENV RENDER_GID=
#
# add healthcheck
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
CMD ["/usr/bin/supervisord", "-c", "/etc/neko/supervisord.conf"]

View file

@ -1,6 +1,6 @@
ARG UBUNTU_RELEASE=20.04
ARG CUDA_VERSION=11.4.3
ARG VIRTUALGL_VERSION=3.1
ARG VIRTUALGL_VERSION=3.1.3-20250409
ARG GSTREAMER_VERSION=1.20
#
@ -61,13 +61,13 @@ ARG UBUNTU_RELEASE
ARG VIRTUALGL_VERSION
# 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
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
ENV VGL_DISPLAY egl
ENV VGL_DISPLAY=egl
#
# 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
#
# install VirtualGL and make libraries available for preload
RUN set -eux; \
# install an up-to-date version of VirtualGL
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; \
wget "https://sourceforge.net/projects/virtualgl/files/virtualgl_${VIRTUALGL_VERSION}_amd64.deb"; \
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; \
apt-get install -y --no-install-recommends virtualgl=${VIRTUALGL_VERSION}; \
#
# clean up
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
@ -262,7 +261,9 @@ COPY --from=gstreamer /usr/share/gstreamer /usr/share/gstreamer
#
# add healthcheck
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

View file

@ -1,6 +1,6 @@
ARG UBUNTU_RELEASE=22.04
ARG CUDA_VERSION=12.2.0
ARG VIRTUALGL_VERSION=3.1
ARG VIRTUALGL_VERSION=3.1.3-20250409
ARG GSTREAMER_VERSION=1.22
#
@ -61,13 +61,13 @@ ARG UBUNTU_RELEASE
ARG VIRTUALGL_VERSION
# 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
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
ENV VGL_DISPLAY egl
ENV VGL_DISPLAY=egl
#
# 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
#
# install VirtualGL and make libraries available for preload
RUN set -eux; \
# install an up-to-date version of VirtualGL
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; \
wget "https://sourceforge.net/projects/virtualgl/files/virtualgl_${VIRTUALGL_VERSION}_amd64.deb"; \
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; \
apt-get install -y --no-install-recommends virtualgl=${VIRTUALGL_VERSION}; \
#
# clean up
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
@ -254,7 +253,9 @@ COPY --from=gstreamer /usr/share/gstreamer /usr/share/gstreamer
#
# add healthcheck
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

View file

@ -33,7 +33,7 @@ redirect_stderr=true
[program:neko]
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
stopwaitsecs=3
autorestart=true

View file

@ -22,7 +22,7 @@ fi
#
# load server dependencies
go get -v -t -d .
go get -v -t .
#
# 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`
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"
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
cd "$(dirname "$0")"
if [ "$(docker images -q neko_server 2> /dev/null)" == "" ]; then
echo "Image 'neko_server' not found. Run ./build first."
if [ "$(docker images -q neko_server:src 2> /dev/null)" == "" ]; then
echo "Image 'neko_server:src' not found. Run ./build first."
exit 1
fi
docker run -it --rm \
--entrypoint="go" \
-v "${PWD}/../:/src" \
neko_server fmt ./...
neko_server:src fmt ./...

View file

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

View file

@ -1,8 +1,8 @@
#!/bin/bash
cd "$(dirname "$0")"
if [ "$(docker images -q neko_server 2> /dev/null)" == "" ]; then
echo "Image 'neko_server' not found. Run ./build first."
if [ "$(docker images -q neko_server:src 2> /dev/null)" == "" ]; then
echo "Image 'neko_server:src' not found. Run ./build first."
exit 1
fi
@ -11,4 +11,4 @@ fi
docker run --rm -it \
-v "${PWD}/../:/src" \
--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 \
-v "${PWD}/../:/src" \
--entrypoint="/bin/bash" \
neko_server "./build" "$@";
neko_server:src "./build" "$@";
#
# remove old plugins

View file

@ -1,6 +1,6 @@
#!/bin/bash
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

View file

@ -1,4 +1,4 @@
ARG BASE_IMAGE=neko_server:base
ARG BASE_IMAGE=ghcr.io/m1k1o/neko/base:latest
FROM $BASE_IMAGE
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; \
apt-get install -y --no-install-recommends \
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
wget -O /tmp/firefox-setup.tar.bz2 "${SRC_URL}"; \
wget -O /tmp/firefox-setup.tar.xz "${SRC_URL}"; \
mkdir /usr/lib/firefox; \
tar -xjf /tmp/firefox-setup.tar.bz2 -C /usr/lib; \
rm -f /tmp/firefox-setup.tar.bz2; \
tar -xvf /tmp/firefox-setup.tar.xz -C /usr/lib; \
rm -f /tmp/firefox-setup.tar.xz; \
ln -s /usr/lib/firefox/firefox /usr/bin/firefox; \
#
# add user to sudoers
@ -22,7 +22,7 @@ RUN set -eux; apt-get update; \
echo "neko:neko" | chpasswd; \
echo "%sudo ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers; \
# clean up
apt-get --purge autoremove -y xz-utils bzip2; \
apt-get --purge autoremove -y xz-utils; \
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*

View file

@ -90,12 +90,12 @@ func (Capture) Init(cmd *cobra.Command) error {
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 {
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 {
return err
}
@ -451,6 +451,7 @@ func (s *Capture) SetV2() {
enableLegacy = true
}
modifiedVideoCodec := false
if videoCodec := viper.GetString("video_codec"); videoCodec != "" {
s.VideoCodec, ok = codec.ParseStr(videoCodec)
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")
enableLegacy = true
modifiedVideoCodec = true
}
if viper.GetBool("vp8") {
s.VideoCodec = codec.VP8()
log.Warn().Msg("you are using deprecated config setting 'NEKO_VP8=true', use 'NEKO_CAPTURE_VIDEO_CODEC=vp8' instead")
enableLegacy = true
modifiedVideoCodec = true
} else if viper.GetBool("vp9") {
s.VideoCodec = codec.VP9()
log.Warn().Msg("you are using deprecated config setting 'NEKO_VP9=true', use 'NEKO_CAPTURE_VIDEO_CODEC=vp9' instead")
enableLegacy = true
modifiedVideoCodec = true
} else if viper.GetBool("h264") {
s.VideoCodec = codec.H264()
log.Warn().Msg("you are using deprecated config setting 'NEKO_H264=true', use 'NEKO_CAPTURE_VIDEO_CODEC=h264' instead")
enableLegacy = true
modifiedVideoCodec = true
} else if viper.GetBool("av1") {
s.VideoCodec = codec.AV1()
log.Warn().Msg("you are using deprecated config setting 'NEKO_AV1=true', use 'NEKO_CAPTURE_VIDEO_CODEC=av1' instead")
enableLegacy = true
modifiedVideoCodec = true
}
videoHWEnc := HwEncUnset
@ -498,7 +504,7 @@ func (s *Capture) SetV2() {
videoPipeline := viper.GetString("video")
// 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)
if err != nil {
log.Warn().Err(err).Msg("unable to create video pipeline, using default")
@ -534,6 +540,7 @@ func (s *Capture) SetV2() {
enableLegacy = true
}
modifiedAudioCodec := false
if audioCodec := viper.GetString("audio_codec"); audioCodec != "" {
s.AudioCodec, ok = codec.ParseStr(audioCodec)
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")
enableLegacy = true
modifiedAudioCodec = true
}
if viper.GetBool("opus") {
s.AudioCodec = codec.Opus()
log.Warn().Msg("you are using deprecated config setting 'NEKO_OPUS=true', use 'NEKO_CAPTURE_AUDIO_CODEC=opus' instead")
enableLegacy = true
modifiedAudioCodec = true
} else if viper.GetBool("g722") {
s.AudioCodec = codec.G722()
log.Warn().Msg("you are using deprecated config setting 'NEKO_G722=true', use 'NEKO_CAPTURE_AUDIO_CODEC=g722' instead")
enableLegacy = true
modifiedAudioCodec = true
} else if viper.GetBool("pcmu") {
s.AudioCodec = codec.PCMU()
log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMU=true', use 'NEKO_CAPTURE_AUDIO_CODEC=pcmu' instead")
enableLegacy = true
modifiedAudioCodec = true
} else if viper.GetBool("pcma") {
s.AudioCodec = codec.PCMA()
log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMA=true', use 'NEKO_CAPTURE_AUDIO_CODEC=pcma' instead")
enableLegacy = true
modifiedAudioCodec = true
}
audioBitrate := viper.GetUint("audio_bitrate")
audioPipeline := viper.GetString("audio")
// audio pipeline
if audioBitrate != 0 || audioPipeline != "" {
if modifiedAudioCodec || audioBitrate != 0 || audioPipeline != "" {
pipeline, err := NewAudioPipeline(s.AudioCodec, s.AudioDevice, audioPipeline, audioBitrate)
if err != nil {
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
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)
}
}

View file

@ -133,7 +133,7 @@ func (s *Desktop) SetV2() {
// set legacy flag if any V2 configuration was used
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)
}
}

View file

@ -22,45 +22,45 @@ type Member struct {
}
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 {
return err
}
// 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 {
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 {
return err
}
// 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 {
return err
}
// 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 {
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 {
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 {
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 {
return err
}
@ -162,7 +162,7 @@ func (s *Member) SetV2() {
// set legacy flag if any V2 configuration was used
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)
}
}

View file

@ -139,7 +139,7 @@ func (s *Root) SetV2() {
// set legacy flag if any V2 configuration was used
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)
}
}

View file

@ -172,7 +172,7 @@ func (s *Server) SetV2() {
// set legacy flag if any V2 configuration was used
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)
}
}

View file

@ -206,7 +206,7 @@ func (s *Session) SetV2() {
// set legacy flag if any V2 configuration was used
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)
}
}

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
//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 {
// 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 {
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 {
return err
}
@ -217,14 +217,14 @@ func (s *WebRTC) Set() {
// parse frontend ice servers
if err := viper.UnmarshalKey("webrtc.iceservers.frontend", &s.ICEServersFrontend, viper.DecodeHook(
utils.JsonStringAutoDecode([]types.ICEServer{}),
utils.JsonStringAutoDecode(s.ICEServersFrontend),
)); err != nil {
log.Warn().Err(err).Msgf("unable to parse frontend ICE servers")
}
// parse backend ice servers
if err := viper.UnmarshalKey("webrtc.iceservers.backend", &s.ICEServersBackend, viper.DecodeHook(
utils.JsonStringAutoDecode([]types.ICEServer{}),
utils.JsonStringAutoDecode(s.ICEServersBackend),
)); err != nil {
log.Warn().Err(err).Msgf("unable to parse backend ICE servers")
}
@ -238,7 +238,7 @@ func (s *WebRTC) Set() {
// parse global ice servers
var iceServers []types.ICEServer
if err := viper.UnmarshalKey("webrtc.iceservers", &iceServers, viper.DecodeHook(
utils.JsonStringAutoDecode([]types.ICEServer{}),
utils.JsonStringAutoDecode(iceServers),
)); err != nil {
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
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)
}
}

View file

@ -10,14 +10,19 @@ import (
"github.com/m1k1o/neko/server/pkg/xevent"
)
const (
ClipboardTextPlainTarget = "UTF8_STRING"
ClipboardTextHtmlTarget = "text/html"
)
func (manager *DesktopManagerCtx) ClipboardGetText() (*types.ClipboardText, error) {
text, err := manager.ClipboardGetBinary("STRING")
text, err := manager.ClipboardGetBinary(ClipboardTextPlainTarget)
if err != nil {
return nil, err
}
// Rich text must not always be available, can fail silently.
html, _ := manager.ClipboardGetBinary("text/html")
html, _ := manager.ClipboardGetBinary(ClipboardTextHtmlTarget)
return &types.ClipboardText{
Text: string(text),
@ -31,10 +36,10 @@ func (manager *DesktopManagerCtx) ClipboardSetText(data types.ClipboardText) err
// is set, if available. Otherwise plain text.
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) {
@ -53,6 +58,23 @@ func (manager *DesktopManagerCtx) ClipboardGetBinary(mime string) ([]byte, error
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 {
cmd := exec.Command("xclip", "-selection", "clipboard", "-in", "-target", mime)
@ -64,7 +86,9 @@ func (manager *DesktopManagerCtx) ClipboardSetBinary(mime string, data []byte) e
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.
wait := make(chan struct{})
xevent.Emmiter.Once("clipboard-updated", func(payload ...any) {
@ -84,9 +108,23 @@ func (manager *DesktopManagerCtx) ClipboardSetBinary(mime string, data []byte) e
stdin.Close()
// TODO: Refactor.
// cmd.Wait()
<-wait
select {
case <-manager.shutdown:
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
}

View file

@ -1,7 +1,9 @@
package desktop
import (
"os/exec"
"sync"
"sync/atomic"
"time"
"github.com/kataras/go-events"
@ -25,6 +27,11 @@ type DesktopManagerCtx struct {
config *config.Desktop
screenSize types.ScreenSize // cached screen size
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 {
@ -131,6 +138,8 @@ func (manager *DesktopManagerCtx) Shutdown() error {
manager.logger.Info().Msgf("shutdown")
close(manager.shutdown)
manager.replaceClipboardCommand(nil)
manager.wg.Wait()
xorg.DisplayClose()

View file

@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"strings"
"time"
"github.com/m1k1o/neko/server/internal/api"
oldEvent "github.com/m1k1o/neko/server/internal/http/legacy/event"
@ -35,9 +36,6 @@ var (
return true
},
}
// DefaultDialer is a dialer with all fields set to the default zero values.
DefaultDialer = websocket.DefaultDialer
)
type LegacyHandler struct {
@ -45,16 +43,21 @@ type LegacyHandler struct {
serverAddr string
bannedIPs map[string]struct{}
sessionIPs map[string]string
wsDialer *websocket.Dialer
}
func New() *LegacyHandler {
func New(serverAddr string) *LegacyHandler {
// Init
return &LegacyHandler{
logger: log.With().Str("module", "legacy").Logger(),
serverAddr: "127.0.0.1:8080",
serverAddr: serverAddr,
bannedIPs: make(map[string]struct{}),
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()
// 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 {
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)
}
}
errc <- err
errc <- fmt.Errorf("src read message error: %w", err)
dst.WriteMessage(websocket.CloseMessage, m)
break
}
// handle text messages
if msgType == websocket.TextMessage {
err = rewriteTextMessage(msg)
@ -162,12 +167,26 @@ func (h *LegacyHandler) Route(r types.Router) {
Message: strings.ReplaceAll(err.Error(), ErrBackendRespone.Error()+": ", ""),
})
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
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
select {
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:
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 {

View file

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

View file

@ -2,6 +2,7 @@ package http
import (
"context"
"net"
"net/http"
"os"
@ -56,11 +57,6 @@ func New(WebSocketManager types.WebSocketManager, ApiManager types.ApiManager, c
return config.AllowOrigin(r.Header.Get("Origin"))
}))
// Legacy handler
if viper.GetBool("legacy") {
legacy.New().Route(router)
}
batch := batchHandler{
Router: router,
PathPrefix: "/api",
@ -122,6 +118,24 @@ func (manager *HttpManagerCtx) Start() {
}
}()
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 {
go func() {
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)
// 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.
Debug().
Trace().
Str("x", strconv.Itoa(int(payload.X))).
Str("y", strconv.Itoa(int(payload.Y))).
Msg("scroll")
@ -97,7 +97,7 @@ func (manager *WebRTCManagerCtx) handleLegacy(
return nil
}
logger.Debug().Msgf("button down %d", payload.Key)
logger.Trace().Msgf("button down %d", payload.Key)
} else {
err := manager.desktop.KeyDown(uint32(payload.Key))
if err != nil {
@ -105,7 +105,7 @@ func (manager *WebRTCManagerCtx) handleLegacy(
return nil
}
logger.Debug().Msgf("key down %d", payload.Key)
logger.Trace().Msgf("key down %d", payload.Key)
}
case OP_KEY_UP:
payload := &PayloadKey{}
@ -121,7 +121,7 @@ func (manager *WebRTCManagerCtx) handleLegacy(
return nil
}
logger.Debug().Msgf("button up %d", payload.Key)
logger.Trace().Msgf("button up %d", payload.Key)
} else {
err := manager.desktop.KeyUp(uint32(payload.Key))
if err != nil {
@ -129,7 +129,7 @@ func (manager *WebRTCManagerCtx) handleLegacy(
return nil
}
logger.Debug().Msgf("key up %d", payload.Key)
logger.Trace().Msgf("key up %d", payload.Key)
}
case OP_KEY_CLK:
// unused

View file

@ -276,7 +276,9 @@ func (manager *WebSocketManagerCtx) connect(connection *websocket.Conn, r *http.
e, ok := err.(*websocket.CloseError)
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")
// client is expected to reconnect soon
delayedDisconnect = true

View file

@ -43,7 +43,9 @@ func (peer *WebSocketPeerCtx) Send(event string, payload any) {
})
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")
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