Compare commits

...

80 commits

Author SHA1 Message Date
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
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
163 changed files with 2310 additions and 1106 deletions

View file

@ -1,4 +1,4 @@
name: Build and Publish Client Artifacts name: Build Client
on: on:
workflow_call: workflow_call:
@ -14,8 +14,8 @@ on:
the artifacts. the artifacts.
jobs: jobs:
client_build: build-client:
name: Build and Publish Client Artifacts name: Build Client
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View file

@ -1,4 +1,4 @@
name: Test Client Build name: Test Client
on: on:
pull_request: pull_request:
@ -10,8 +10,8 @@ on:
- .github/workflows/client_test.yml - .github/workflows/client_test.yml
jobs: jobs:
client_test: test-client:
name: Test Client Build name: Test Client
uses: ./.github/workflows/client_build.yml uses: ./.github/workflows/client_build.yml
with: with:
# Do not upload artifacts for test builds # Do not upload artifacts for test builds

View file

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

View file

@ -7,14 +7,14 @@ on:
jobs: jobs:
build-base: build-base:
name: Build Base Image name: Base Image
uses: ./.github/workflows/image_base.yml uses: ./.github/workflows/image_base.yml
with: with:
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
secrets: inherit secrets: inherit
build-apps: build-app:
name: Build Apps Image name: App Image
uses: ./.github/workflows/image_app.yml uses: ./.github/workflows/image_app.yml
needs: build-base needs: build-base
strategy: strategy:
@ -24,8 +24,9 @@ jobs:
include: include:
- name: firefox - name: firefox
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
- name: waterfox # Temporarily disabled due to Cloudflare blocked download link
platforms: linux/amd64 #- name: waterfox
# platforms: linux/amd64
- name: chromium - name: chromium
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
- name: google-chrome - name: google-chrome
@ -35,21 +36,21 @@ jobs:
- name: microsoft-edge - name: microsoft-edge
platforms: linux/amd64 platforms: linux/amd64
- name: brave - name: brave
platforms: linux/amd64 platforms: linux/amd64,linux/arm64
- name: vivaldi - name: vivaldi
platforms: linux/amd64 platforms: linux/amd64,linux/arm64,linux/arm/v7
- name: opera - name: opera
platforms: linux/amd64 platforms: linux/amd64
- name: tor-browser - name: tor-browser
platforms: linux/amd64 platforms: linux/amd64
- name: remmina - name: remmina
platforms: linux/amd64 platforms: linux/amd64,linux/arm64,linux/arm/v7
- name: vlc - name: vlc
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
- name: xfce - name: xfce
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
- name: kde - name: kde
platforms: linux/amd64 platforms: linux/amd64,linux/arm64,linux/arm/v7
with: with:
name: ${{ matrix.name }} name: ${{ matrix.name }}
platforms: ${{ matrix.platforms }} platforms: ${{ matrix.platforms }}

View file

@ -7,7 +7,7 @@ on:
jobs: jobs:
build-base: build-base:
name: Build Base Image name: Base Image
uses: ./.github/workflows/image_base.yml uses: ./.github/workflows/image_base.yml
with: with:
flavor: intel flavor: intel
@ -15,8 +15,8 @@ jobs:
dockerfile: Dockerfile.intel dockerfile: Dockerfile.intel
secrets: inherit secrets: inherit
build-apps: build-app:
name: Build Apps Image name: App Image
uses: ./.github/workflows/image_app.yml uses: ./.github/workflows/image_app.yml
needs: build-base needs: build-base
strategy: strategy:
@ -25,7 +25,8 @@ jobs:
matrix: matrix:
include: include:
- name: firefox - name: firefox
- name: waterfox # Temporarily disabled due to Cloudflare blocked download link
#- name: waterfox
- name: chromium - name: chromium
- name: google-chrome - name: google-chrome
- name: ungoogled-chromium - name: ungoogled-chromium
@ -41,6 +42,6 @@ jobs:
with: with:
name: ${{ matrix.name }} name: ${{ matrix.name }}
flavor: intel flavor: intel
platforms: linux/amd64 platforms: ${{ matrix.platforms }}
dockerfile: ${{ matrix.dockerfile }} dockerfile: ${{ matrix.dockerfile }}
secrets: inherit secrets: inherit

View file

@ -7,7 +7,7 @@ on:
jobs: jobs:
build-base: build-base:
name: Build Base Image name: Base Image
uses: ./.github/workflows/image_base.yml uses: ./.github/workflows/image_base.yml
with: with:
flavor: nvidia flavor: nvidia
@ -15,8 +15,8 @@ jobs:
dockerfile: Dockerfile.nvidia dockerfile: Dockerfile.nvidia
secrets: inherit secrets: inherit
build-apps: build-app:
name: Build Apps Image name: App Image
uses: ./.github/workflows/image_app.yml uses: ./.github/workflows/image_app.yml
needs: build-base needs: build-base
strategy: strategy:
@ -37,6 +37,6 @@ jobs:
with: with:
name: ${{ matrix.name }} name: ${{ matrix.name }}
flavor: nvidia flavor: nvidia
platforms: linux/amd64 platforms: ${{ matrix.platforms }}
dockerfile: ${{ matrix.dockerfile }} dockerfile: ${{ matrix.dockerfile }}
secrets: inherit secrets: inherit

View file

@ -1,4 +1,4 @@
name: Build and Publish Application Image name: Build App Image
on: on:
workflow_call: workflow_call:
@ -6,7 +6,7 @@ on:
name: name:
required: true required: true
type: string type: string
description: "The name of the application to build." description: "The name of the app to build."
flavor: flavor:
required: false required: false
type: string type: string
@ -28,7 +28,7 @@ env:
jobs: jobs:
build-app: build-app:
name: Build and Publish Application Image name: Build App Image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@ -48,6 +48,7 @@ jobs:
with: with:
images: ghcr.io/${{ github.repository }}/${{ env.FLAVOR_PREFIX }}${{ inputs.name }} images: ghcr.io/${{ github.repository }}/${{ env.FLAVOR_PREFIX }}${{ inputs.name }}
tags: | tags: |
type=edge,branch=master
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
@ -71,3 +72,5 @@ jobs:
build-args: | build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository }}/${{ env.FLAVOR_PREFIX }}base:sha-${{ github.sha }} BASE_IMAGE=ghcr.io/${{ github.repository }}/${{ env.FLAVOR_PREFIX }}base:sha-${{ github.sha }}
platforms: ${{ inputs.platforms || 'linux/amd64' }} platforms: ${{ inputs.platforms || 'linux/amd64' }}
cache-from: type=gha
cache-to: type=gha,mode=max

View file

@ -1,4 +1,4 @@
name: Build and Publish Base Image name: Build Base Image
on: on:
workflow_call: workflow_call:
@ -24,10 +24,11 @@ env:
jobs: jobs:
build-client: build-client:
name: Build Client Artifacts
uses: ./.github/workflows/client_build.yml uses: ./.github/workflows/client_build.yml
build-base: build-base:
name: Build and Publish Base Image name: Build Base Image
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build-client needs: build-client
steps: steps:
@ -54,6 +55,7 @@ jobs:
with: with:
images: ghcr.io/${{ github.repository }}/${{ env.FLAVOR_PREFIX }}base images: ghcr.io/${{ github.repository }}/${{ env.FLAVOR_PREFIX }}base
tags: | tags: |
type=edge,branch=master
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
@ -69,7 +71,7 @@ jobs:
- name: Generate base Dockerfile - name: Generate base Dockerfile
env: env:
RUNTIME_DOCKERFILE: ${{ inputs.dockerfile || 'Dockerfile' }} RUNTIME_DOCKERFILE: ${{ inputs.dockerfile || 'Dockerfile' }}
run: go run docker/main.go -i Dockerfile.tmpl -o Dockerfile -client client/dist run: go run utils/docker/main.go -i Dockerfile.tmpl -o Dockerfile -client client/dist
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@ -79,3 +81,5 @@ jobs:
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ inputs.platforms || 'linux/amd64' }} platforms: ${{ inputs.platforms || 'linux/amd64' }}
cache-from: type=gha
cache-to: type=gha,mode=max

View file

@ -1,4 +1,4 @@
name: Test Server Build name: Test Server
on: on:
pull_request: pull_request:
@ -9,8 +9,8 @@ on:
- .github/workflows/server_test.yml - .github/workflows/server_test.yml
jobs: jobs:
server-test-amd64: build-amd64:
name: Test Server Build AMD64 name: Build amd64
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
@ -26,8 +26,8 @@ jobs:
context: ./server context: ./server
platforms: linux/amd64 platforms: linux/amd64
server-test-arm64: build-arm64:
name: Test Server Build ARM64 name: Build arm64
runs-on: ubuntu-24.04-arm runs-on: ubuntu-24.04-arm
permissions: permissions:
contents: read contents: read

View file

@ -1,4 +1,4 @@
name: Build and Publish Webpage Artifacts name: Build Webpage
on: on:
workflow_call: workflow_call:
@ -14,8 +14,8 @@ on:
the artifacts. the artifacts.
jobs: jobs:
webpage-build: build-webpage:
name: Build and Publish Webpage Artifacts name: Build Webpage
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View file

@ -1,6 +1,7 @@
name: Build and Deploy Webpage to GitHub Pages name: Build and Deploy Webpage to GitHub Pages
on: on:
# Runs on pushes targeting the default branch
push: push:
branches: branches:
- master - master
@ -9,20 +10,30 @@ on:
- .github/workflows/webpage_build.yml - .github/workflows/webpage_build.yml
- .github/workflows/webpage_deploy.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: jobs:
webpage-build: build-webpage:
name: Build Webpage Artifacts name: Build Webpage
uses: ./.github/workflows/webpage_build.yml uses: ./.github/workflows/webpage_build.yml
secrets: inherit secrets: inherit
webpage-deploy: deploy-webpage:
name: Deploy to GitHub Pages name: Deploy to GitHub Pages
needs: webpage-build needs: build-webpage
# 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
# Deploy to the github-pages environment # Deploy to the github-pages environment
environment: environment:

View file

@ -1,4 +1,4 @@
name: Test Webpage Build name: Test Webpage
on: on:
pull_request: pull_request:
@ -10,8 +10,8 @@ on:
- .github/workflows/webpage_test.yml - .github/workflows/webpage_test.yml
jobs: jobs:
webpage-test: test-webpage:
name: Test Webpage Build name: Test Webpage
uses: ./.github/workflows/webpage_build.yml uses: ./.github/workflows/webpage_build.yml
with: with:
# Do not upload artifacts for test builds # Do not upload artifacts for test builds

View file

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

118
README.md
View file

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

19
SECURITY.md Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

72
build
View file

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

View file

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

View file

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

View file

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

View file

@ -40,7 +40,12 @@
<li v-if="admin"><i @click.stop.prevent="openResolution" class="fas fa-desktop"></i></li> <li v-if="admin"><i @click.stop.prevent="openResolution" class="fas fa-desktop"></i></li>
<li v-if="!controlLocked && !implicitHosting" :class="extraControls || 'extra-control'"> <li v-if="!controlLocked && !implicitHosting" :class="extraControls || 'extra-control'">
<i <i
:class="[hosted && !hosting ? 'disabled' : '', !hosted && !hosting ? 'faded' : '', 'fas', 'fa-keyboard']" :class="[
hosted && !hosting ? 'disabled' : '',
!hosted && !hosting ? 'faded' : '',
'fas',
'fa-computer-mouse',
]"
@click.stop.prevent="toggleControl" @click.stop.prevent="toggleControl"
/> />
</li> </li>
@ -57,6 +62,13 @@
class="fas fa-external-link-alt" class="fas fa-external-link-alt"
/> />
</li> </li>
<li
v-if="hosting && is_touch_device"
:class="extraControls || 'extra-control'"
@click.stop.prevent="openMobileKeyboard"
>
<i class="fas fa-keyboard" />
</li>
</ul> </ul>
<neko-resolution ref="resolution" v-if="admin" /> <neko-resolution ref="resolution" v-if="admin" />
<neko-clipboard ref="clipboard" v-if="hosting && (!clipboard_read_available || !clipboard_write_available)" /> <neko-clipboard ref="clipboard" v-if="hosting && (!clipboard_read_available || !clipboard_write_available)" />
@ -117,7 +129,7 @@
} }
@media (max-width: 768px) { @media (max-width: 768px) {
&.extra-control { &.extra-control {
display: inline-block; display: block;
} }
} }
@ -353,6 +365,16 @@
return this.$accessor.video.horizontal return this.$accessor.video.horizontal
} }
get is_touch_device() {
return (
// detect if the device has touch support
('ontouchstart' in window || navigator.maxTouchPoints > 0) &&
// the primary input mechanism includes a pointing device of
// limited accuracy, such as a finger on a touchscreen.
window.matchMedia('(pointer: coarse)').matches
)
}
@Watch('width') @Watch('width')
onWidthChanged() { onWidthChanged() {
this.onResize() this.onResize()
@ -799,10 +821,20 @@
@Watch('hosting') @Watch('hosting')
@Watch('locked') @Watch('locked')
onFocus() { onFocus() {
// focus opens the keyboard on mobile
if (this.is_touch_device) {
return
}
// in order to capture key events, overlay must be focused // in order to capture key events, overlay must be focused
if (this.focused && this.hosting && !this.locked) { if (this.focused && this.hosting && !this.locked) {
this._overlay.focus() this._overlay.focus()
} }
} }
openMobileKeyboard() {
// focus opens the keyboard on mobile
this._overlay.focus()
}
} }
</script> </script>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -262,7 +262,9 @@ COPY --from=gstreamer /usr/share/gstreamer /usr/share/gstreamer
# #
# add healthcheck # add healthcheck
HEALTHCHECK --interval=10s --timeout=5s --retries=8 \ HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || exit 1 CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || \
wget --no-check-certificate -O - https://localhost:${NEKO_SERVER_BIND#*:}/health || \
exit 1
# #
# run neko # run neko

View file

@ -254,7 +254,9 @@ COPY --from=gstreamer /usr/share/gstreamer /usr/share/gstreamer
# #
# add healthcheck # add healthcheck
HEALTHCHECK --interval=10s --timeout=5s --retries=8 \ HEALTHCHECK --interval=10s --timeout=5s --retries=8 \
CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || exit 1 CMD wget -O - http://localhost:${NEKO_SERVER_BIND#*:}/health || \
wget --no-check-certificate -O - https://localhost:${NEKO_SERVER_BIND#*:}/health || \
exit 1
# #
# run neko # run neko

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,14 +10,19 @@ import (
"github.com/m1k1o/neko/server/pkg/xevent" "github.com/m1k1o/neko/server/pkg/xevent"
) )
const (
ClipboardTextPlainTarget = "UTF8_STRING"
ClipboardTextHtmlTarget = "text/html"
)
func (manager *DesktopManagerCtx) ClipboardGetText() (*types.ClipboardText, error) { func (manager *DesktopManagerCtx) ClipboardGetText() (*types.ClipboardText, error) {
text, err := manager.ClipboardGetBinary("STRING") text, err := manager.ClipboardGetBinary(ClipboardTextPlainTarget)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Rich text must not always be available, can fail silently. // Rich text must not always be available, can fail silently.
html, _ := manager.ClipboardGetBinary("text/html") html, _ := manager.ClipboardGetBinary(ClipboardTextHtmlTarget)
return &types.ClipboardText{ return &types.ClipboardText{
Text: string(text), Text: string(text),
@ -31,10 +36,10 @@ func (manager *DesktopManagerCtx) ClipboardSetText(data types.ClipboardText) err
// is set, if available. Otherwise plain text. // is set, if available. Otherwise plain text.
if data.HTML != "" { if data.HTML != "" {
return manager.ClipboardSetBinary("text/html", []byte(data.HTML)) return manager.ClipboardSetBinary(ClipboardTextHtmlTarget, []byte(data.HTML))
} }
return manager.ClipboardSetBinary("STRING", []byte(data.Text)) return manager.ClipboardSetBinary(ClipboardTextPlainTarget, []byte(data.Text))
} }
func (manager *DesktopManagerCtx) ClipboardGetBinary(mime string) ([]byte, error) { func (manager *DesktopManagerCtx) ClipboardGetBinary(mime string) ([]byte, error) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

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