Archived
2
0

Compare commits

..

1 Commits

Author SHA1 Message Date
Miroslav Šedivý
e0245b86f3 WIP. 2023-10-15 19:24:42 +02:00
32 changed files with 1181 additions and 183 deletions

View File

@ -10,7 +10,7 @@ RUN set -eux; \
# #
# install widevine module # install widevine module
CHROMIUM_DIR="/usr/lib/chromium"; \ CHROMIUM_DIR="/usr/lib/chromium"; \
WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | sort --version-sort | tail -n 1); \ WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | tail -n 1); \
wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/${WIDEVINE_VERSION}-linux-x64.zip"; \ wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/${WIDEVINE_VERSION}-linux-x64.zip"; \
mkdir -p "${CHROMIUM_DIR}/WidevineCdm/_platform_specific/linux_x64"; \ mkdir -p "${CHROMIUM_DIR}/WidevineCdm/_platform_specific/linux_x64"; \
unzip -p /tmp/widevine.zip LICENSE.txt > "${CHROMIUM_DIR}/WidevineCdm/LICENSE"; \ unzip -p /tmp/widevine.zip LICENSE.txt > "${CHROMIUM_DIR}/WidevineCdm/LICENSE"; \

View File

@ -14,7 +14,7 @@ RUN set -eux; \
# #
# install widevine module # install widevine module
CHROMIUM_DIR="/usr/lib/chromium"; \ CHROMIUM_DIR="/usr/lib/chromium"; \
WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | sort --version-sort | tail -n 1); \ WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | tail -n 1); \
wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/${WIDEVINE_VERSION}-linux-x64.zip"; \ wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/${WIDEVINE_VERSION}-linux-x64.zip"; \
mkdir -p "${CHROMIUM_DIR}/WidevineCdm/_platform_specific/linux_x64"; \ mkdir -p "${CHROMIUM_DIR}/WidevineCdm/_platform_specific/linux_x64"; \
unzip -p /tmp/widevine.zip LICENSE.txt > "${CHROMIUM_DIR}/WidevineCdm/LICENSE"; \ unzip -p /tmp/widevine.zip LICENSE.txt > "${CHROMIUM_DIR}/WidevineCdm/LICENSE"; \

View File

@ -8,7 +8,7 @@ ARG API_URL="https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edg
RUN set -eux; apt-get update; \ RUN set -eux; apt-get update; \
# #
# fetch latest release # fetch latest release
SRC_URL="${API_URL}$(wget -O - "${API_URL}" 2>/dev/null | sed -n 's/.*href="\([^"]*\).*/\1/p' | sort --version-sort | tail -1)"; \ SRC_URL="${API_URL}$(wget -O - "${API_URL}" 2>/dev/null | sed -n 's/.*href="\([^"]*\).*/\1/p' | tail -1)"; \
wget -O /tmp/microsoft-edge.deb "${SRC_URL}"; \ wget -O /tmp/microsoft-edge.deb "${SRC_URL}"; \
apt-get install -y --no-install-recommends openbox /tmp/microsoft-edge.deb; \ apt-get install -y --no-install-recommends openbox /tmp/microsoft-edge.deb; \
# #

View File

@ -8,7 +8,7 @@ ARG API_URL="https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edg
RUN set -eux; apt-get update; \ RUN set -eux; apt-get update; \
# #
# fetch latest release # fetch latest release
SRC_URL="${API_URL}$(wget -O - "${API_URL}" 2>/dev/null | sed -n 's/.*href="\([^"]*\).*/\1/p' | sort --version-sort | tail -1)"; \ SRC_URL="${API_URL}$(wget -O - "${API_URL}" 2>/dev/null | sed -n 's/.*href="\([^"]*\).*/\1/p' | tail -1)"; \
wget -O /tmp/microsoft-edge.deb "${SRC_URL}"; \ wget -O /tmp/microsoft-edge.deb "${SRC_URL}"; \
apt-get install -y --no-install-recommends openbox /tmp/microsoft-edge.deb; \ apt-get install -y --no-install-recommends openbox /tmp/microsoft-edge.deb; \
# #

View File

@ -9,7 +9,7 @@ ARG LIBFFMPEG_API_URL="https://api.github.com/repos/nwjs-ffmpeg-prebuilt/nwjs-ff
RUN set -eux; apt-get update; \ RUN set -eux; apt-get update; \
# #
# fetch latest release # fetch latest release
VERSION="$(wget -O - "${API_URL}" 2>/dev/null | sed -n 's/.*href="\([^"/]*\).*/\1/p' | sort --version-sort | tail -1)"; \ VERSION="$(wget -O - "${API_URL}" 2>/dev/null | sed -n 's/.*href="\([^"/]*\).*/\1/p' | tail -1)"; \
wget -O /tmp/opera.deb "${API_URL}${VERSION}/linux/opera-stable_${VERSION}_amd64.deb"; \ wget -O /tmp/opera.deb "${API_URL}${VERSION}/linux/opera-stable_${VERSION}_amd64.deb"; \
apt-get install -y --no-install-recommends openbox jq unzip /tmp/opera.deb; \ apt-get install -y --no-install-recommends openbox jq unzip /tmp/opera.deb; \
# #

View File

@ -21,7 +21,7 @@ RUN set -eux; apt-get update; \
chmod 4755 /usr/lib/chromium/chrome-sandbox; \ chmod 4755 /usr/lib/chromium/chrome-sandbox; \
# #
# install widevine module # install widevine module
WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | sort --version-sort | tail -n 1); \ WIDEVINE_VERSION=$(wget --quiet -O - https://dl.google.com/widevine-cdm/versions.txt | tail -n 1); \
wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/${WIDEVINE_VERSION}-linux-x64.zip"; \ wget -O /tmp/widevine.zip "https://dl.google.com/widevine-cdm/${WIDEVINE_VERSION}-linux-x64.zip"; \
unzip -p /tmp/widevine.zip libwidevinecdm.so > /usr/lib/chromium/libwidevinecdm.so; \ unzip -p /tmp/widevine.zip libwidevinecdm.so > /usr/lib/chromium/libwidevinecdm.so; \
chmod 644 /usr/lib/chromium/libwidevinecdm.so; \ chmod 644 /usr/lib/chromium/libwidevinecdm.so; \

View File

@ -204,9 +204,7 @@
@Watch('volume', { immediate: true }) @Watch('volume', { immediate: true })
onVolume(volume: number) { onVolume(volume: number) {
if (new URL(location.href).searchParams.has('volume')) { this.$accessor.video.setVolume(volume)
this.$accessor.video.setVolume(volume)
}
} }
@Watch('hideControls', { immediate: true }) @Watch('hideControls', { immediate: true })

View File

@ -82,14 +82,6 @@
if (default_lang && this.langs.includes(default_lang)) { if (default_lang && this.langs.includes(default_lang)) {
this.$i18n.locale = default_lang this.$i18n.locale = default_lang
} }
const show_side = new URL(location.href).searchParams.get('show_side')
if (show_side !== null) {
this.$accessor.client.setSide(show_side === '1')
}
const mute_chat = new URL(location.href).searchParams.get('mute_chat')
if (mute_chat !== null) {
this.$accessor.settings.setSound(mute_chat !== '1')
}
} }
} }
</script> </script>

View File

@ -27,10 +27,6 @@ export const mutations = mutationTree(state, {
state.side = !state.side state.side = !state.side
set('side', state.side) set('side', state.side)
}, },
setSide(state, side: boolean) {
state.side = side
set('side', side)
},
}) })
export const actions = actionTree({ state, getters, mutations }, {}) export const actions = actionTree({ state, getters, mutations }, {})

View File

@ -6,7 +6,6 @@
* [Reverse Proxy](/getting-started/reverse-proxy) * [Reverse Proxy](/getting-started/reverse-proxy)
* [Configuration](/getting-started/configuration) * [Configuration](/getting-started/configuration)
* [Troubleshooting](/getting-started/troubleshooting) * [Troubleshooting](/getting-started/troubleshooting)
* [Frequently Asked Questions](/getting-started/faq)
* [Mobile Support](/mobile-support) * [Mobile Support](/mobile-support)
* [Contributing](/contributing) * [Contributing](/contributing)
* [Non Goals](/non-goals) * [Non Goals](/non-goals)

View File

@ -5,19 +5,9 @@
### New Features ### New Features
- Added nvidia support for firefox. - Added nvidia support for firefox.
- Added `?lang=<lang>` parameter to the URL, which will set the language of the interface (by @mbattista). - Added `?lang=<lang>` parameter to the URL, which will set the language of the interface (by @mbattista).
- Added `?show_side=1` and `?mute_chat=1` parameter to the URL, for chat mute and show side (by @mbattista).
### Bugs
- Fix incorrect version sorting for chromium, microsoft-edge, opera and ungoogledchromium.
- Fix buffer overflow in Gstreamer log function [#382](https://github.com/m1k1o/neko/pull/382) (by @@tt2468).
### Misc ### Misc
- Added RTMP broadcast support to nvidia docker image [#274](https://github.com/m1k1o/neko/issues/274).
- Ensured that paths are writable by neko user [#277](https://github.com/m1k1o/neko/issues/277).
- Git commit and tag are now included in the build when creating a docker image. - Git commit and tag are now included in the build when creating a docker image.
- Remove any temporary files associated with a Form after file upload, that would be otherwise never removed.
- Add check for volume parameter in URL before setting volume (by @FapFapDragon).
- Add glib main loop to capture manager [#383](https://github.com/m1k1o/neko/pull/383) (by @tt2468).
## [n.eko v2.8.0](https://github.com/m1k1o/neko/releases/tag/v2.8.0) ## [n.eko v2.8.0](https://github.com/m1k1o/neko/releases/tag/v2.8.0)

View File

@ -10,7 +10,7 @@
## Server build dependencies ## Server build dependencies
If you want to compile Golang code locally, you must install additional dependencies in order for it to compile. If you want to compile goalng code locally, you must install additional dependencies in order for it to compile.
```shell ```shell
apt-get install -y --no-install-recommends libx11-dev libxrandr-dev libxtst-dev libgstreamer1.0-dev apt-get install -y --no-install-recommends libx11-dev libxrandr-dev libxtst-dev libgstreamer1.0-dev

View File

@ -87,48 +87,7 @@ GHCR images are built using GitHub actions for every tag.
### Networking: ### Networking:
- If you want to use n.eko in **external** network, you can omit `NEKO_NAT1TO1`. It will automatically get your Public IP. - If you want to use n.eko in **external** network, you can omit `NEKO_NAT1TO1`. It will automatically get your Public IP.
- If you want to use n.eko in **internal** network, set `NEKO_NAT1TO1` to your local IP address (e.g. `NEKO_NAT1TO1: 192.168.1.20`)- - If you want to use n.eko in **internal** network, set `NEKO_NAT1TO1` to your local IP address (e.g. `NEKO_NAT1TO1: 192.168.1.20`)-
- Currently, it is not supported to supply multiple NAT addresses (see https://github.com/m1k1o/neko/issues/47).
Currently, it is not supported to supply multiple NAT addresses directly to neko (see https://github.com/m1k1o/neko/issues/47).
But it can be acheived by deploying own turn server alongside neko that is accessible from your LAN:
```yaml
version: "3.4"
services:
neko:
image: "m1k1o/neko:firefox"
restart: "unless-stopped"
shm_size: "2gb"
ports:
- "8080:8080"
- "52000-52100:52000-52100/udp"
environment:
NEKO_SCREEN: 1920x1080@30
NEKO_PASSWORD: neko
NEKO_PASSWORD_ADMIN: admin
NEKO_EPR: 52000-52100
NEKO_ICESERVERS: '[{ "urls": [ "turn:192.168.1.60:3478" ], "username":"neko", "credential":"neko" }, { "urls": [ "stun:stun.nextcloud.com:3478" ] }]'
coturn:
image: 'coturn/coturn:latest'
network_mode: "host"
command: |
-n
--realm=localhost
--fingerprint
--listening-ip=0.0.0.0
--external-ip=192.168.1.60
--listening-port=3478
--min-port=49160
--max-port=49200
--log-file=stdout
--user=neko:neko
--lt-cred-mech
```
- Replace `192.168.1.60` with your LAN IP address, and allow ports `49160-49200/udp` and `3478/tcp` in your LAN.
- Make sure you don't use `NEKO_ICELITE: true` because ICE LITE does not support TURN servers.
This setup adds local turn server to neko. It won't be reachable by your remote clients and your own IP won't be reachable from your lan. So it effectively just adds local candidate and allows connections from LAN.
### Why so many ports? ### Why so many ports?
- WebRTC needs UDP ports in order to transfer Audio/Video towards user and Mouse/Keyboard events to the server in real time. - WebRTC needs UDP ports in order to transfer Audio/Video towards user and Mouse/Keyboard events to the server in real time.
@ -217,7 +176,7 @@ NEKO_ICESERVERS: '[{"urls": ["turn:<MY-COTURN-SERVER>:443?transport=udp", "turn:
### Nvidia GPU acceleration ### Nvidia GPU acceleration
You need to have [NVIDIA Container Toolkit](https://github.com/NVIDIA/nvidia-container-toolkit) installed, start the container with `--gpus all` flag and use images built for nvidia (see above). You need to have [nvidia-docker](https://github.com/NVIDIA/nvidia-docker) installed, start the container with `--gpus all` flag and use images built for nvidia (see above).
```bash ```bash
docker run -d --gpus all \ docker run -d --gpus all \
@ -298,8 +257,6 @@ NEKO_BROADCAST_PIPELINE: "flvmux name=mux ! rtmpsink location={url} pulsesrc dev
- Adding `?embed=1` will hide most additional components and show only video. - Adding `?embed=1` will hide most additional components and show only video.
- Adding `?volume=<0-1>` will set volume to given value. - Adding `?volume=<0-1>` will set volume to given value.
- Adding `?lang=<language>` will set language to given value. - Adding `?lang=<language>` will set language to given value.
- Adding `?show_side=1` will show the sidebar on startup.
- Adding `?mute_chat=1` will mute the chat on startup.
- e.g. `http(s)://<URL:Port>/?pwd=neko&usr=guest&cast=1` - e.g. `http(s)://<URL:Port>/?pwd=neko&usr=guest&cast=1`
### Screen size ### Screen size

View File

@ -1,57 +0,0 @@
# Frequently Asked Questions
## How to enable debug mode?
To see verbose information from n.eko server, you can enable debug mode using `NEKO_DEBUG`.
```diff
version: "3.4"
services:
neko:
image: "m1k1o/neko:firefox"
restart: "unless-stopped"
shm_size: "2gb"
ports:
- "8080:8080"
- "52000-52100:52000-52100/udp"
environment:
NEKO_SCREEN: 1920x1080@30
NEKO_PASSWORD: neko
NEKO_PASSWORD_ADMIN: admin
NEKO_EPR: 52000-52100
NEKO_ICELITE: 1
+ NEKO_DEBUG: 1
```
Ensure, that you have enabled debug mode in javascript console too, in order to see verbose information from client.
## Chinese input method is not working
There exists an extension for Chrome that allows you to use Chinese input method. You can install it from [here](https://chrome.google.com/webstore/detail/mclkkofklkfljcocdinagocijmpgbhab). Alternatively, you can use Google Input Tools from [here](https://www.google.com/inputtools/chrome/).
## Only black screen is displayed but remote cursor is moving for Chromium-based browsers (Chrome, Edge, etc.)
Check if you did not forget to add cap_add to your docker-compose file.
```yaml
cap_add:
- SYS_ADMIN
```
## How can I embed the Neko desktop into web page without login prompt coming up for viewers?
You can use the following URL to embed the Neko desktop into a web page without login prompt coming up for viewers:
```
http://<your-neko-server-ip>:8080/?usr=neko&pwd=neko
```
https://stackoverflow.com/questions/15276929/how-to-make-a-video-fullscreen-when-it-is-placed-inside-an-iframe
Your iframe needs an attribute: `allowfullscreen="true" webkitallowfullscreen="true" mozallowfullscreen="true"` or more modern `allow="fullscreen *"`. For the second you can remove the star if your iframe has the same origin or replace it with your iframe origin.
## Can I use neko without docker?
It is strongly recommended to use Neko with Docker, as it is the easiest way to run it. But you should be able to install Neko "natively" on your host system. Neko is based on Debian and uses Xorg and Pulseaudio. You would just need to follow steps that are in Dockerfile, install all dependencies on your host system and then just run it.
However, it is recommend to start with existing system that has GUI with desktop manager, is based on Xorg and uses Pulseaudio (e.g. Ubuntu Desktop 22.04). For that matter you only need to install gstreamer dependencies, configure pulseaudio properly and run neko binary (you don't need to build it from scratch, you can copy it from docker image).

View File

@ -94,7 +94,7 @@ services:
+ NEKO_IPFETCH: https://ifconfig.co/ip + NEKO_IPFETCH: https://ifconfig.co/ip
``` ```
Or you can specify your IP address manually using `NEKO_NAT1TO1`: (It's read as NAT 1 to 1, so it's capital letter 'O', not zero '0', in NAT1`TO`1) Or you can specify your IP address manually using `NEKO_NAT1TO1`:
```diff ```diff
version: "3.4" version: "3.4"
@ -129,7 +129,6 @@ Example for pfsense with truecharts docker container:
- Test externally to confirm it works. - Test externally to confirm it works.
- Internally you have to access it using `<your-public-ip>:port` - Internally you have to access it using `<your-public-ip>:port`
If your router does not support NAT Loopback (NAT Hairpinning), you can use turn servers to overcome this issue. See [more details here](https://neko.m1k1o.net/#/getting-started/?id=networking) on how to setup local coturn instance.
### Neko works locally, but not externally ### Neko works locally, but not externally

View File

@ -3,8 +3,8 @@
static void gstreamer_pipeline_log(GstPipelineCtx *ctx, char* level, const char* format, ...) { static void gstreamer_pipeline_log(GstPipelineCtx *ctx, char* level, const char* format, ...) {
va_list argptr; va_list argptr;
va_start(argptr, format); va_start(argptr, format);
char buffer[4096]; char buffer[100];
vsnprintf(buffer, sizeof(buffer), format, argptr); vsprintf(buffer, format, argptr);
va_end(argptr); va_end(argptr);
goPipelineLog(level, buffer, ctx->pipelineId); goPipelineLog(level, buffer, ctx->pipelineId);
} }

View File

@ -31,29 +31,12 @@ var pSerial int32
var pipelines = make(map[int]*Pipeline) var pipelines = make(map[int]*Pipeline)
var pipelinesLock sync.Mutex var pipelinesLock sync.Mutex
var registry *C.GstRegistry var registry *C.GstRegistry
var gMainLoop *C.GMainLoop
func init() { func init() {
C.gst_init(nil, nil) C.gst_init(nil, nil)
registry = C.gst_registry_get() registry = C.gst_registry_get()
} }
func RunMainLoop() {
if gMainLoop != nil {
return
}
gMainLoop = C.g_main_loop_new(nil, C.int(0))
C.g_main_loop_run(gMainLoop)
}
func QuitMainLoop() {
if gMainLoop == nil {
return
}
C.g_main_loop_quit(gMainLoop)
gMainLoop = nil
}
func CreatePipeline(pipelineStr string) (*Pipeline, error) { func CreatePipeline(pipelineStr string) (*Pipeline, error) {
id := atomic.AddInt32(&pSerial, 1) id := atomic.AddInt32(&pSerial, 1)

View File

@ -6,7 +6,6 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"m1k1o/neko/internal/capture/gst"
"m1k1o/neko/internal/config" "m1k1o/neko/internal/config"
"m1k1o/neko/internal/types" "m1k1o/neko/internal/types"
) )
@ -54,7 +53,6 @@ func (manager *CaptureManagerCtx) Start() {
} }
} }
go gst.RunMainLoop()
go func() { go func() {
for { for {
before, ok := <-manager.desktop.GetScreenSizeChangeChannel() before, ok := <-manager.desktop.GetScreenSizeChangeChannel()
@ -102,8 +100,6 @@ func (manager *CaptureManagerCtx) Shutdown() error {
manager.audio.shutdown() manager.audio.shutdown()
manager.video.shutdown() manager.video.shutdown()
gst.QuitMainLoop()
return nil return nil
} }

View File

@ -14,7 +14,6 @@ type Server struct {
Cert string Cert string
Key string Key string
Bind string Bind string
Proxy bool
Static string Static string
PathPrefix string PathPrefix string
CORS []string CORS []string
@ -36,11 +35,6 @@ func (Server) Init(cmd *cobra.Command) error {
return err return err
} }
cmd.PersistentFlags().Bool("proxy", false, "enable reverse proxy mode")
if err := viper.BindPFlag("proxy", cmd.PersistentFlags().Lookup("proxy")); err != nil {
return err
}
cmd.PersistentFlags().String("static", "./www", "path to neko client files to serve") cmd.PersistentFlags().String("static", "./www", "path to neko client files to serve")
if err := viper.BindPFlag("static", cmd.PersistentFlags().Lookup("static")); err != nil { if err := viper.BindPFlag("static", cmd.PersistentFlags().Lookup("static")); err != nil {
return err return err
@ -63,7 +57,6 @@ func (s *Server) Set() {
s.Cert = viper.GetString("cert") s.Cert = viper.GetString("cert")
s.Key = viper.GetString("key") s.Key = viper.GetString("key")
s.Bind = viper.GetString("bind") s.Bind = viper.GetString("bind")
s.Proxy = viper.GetBool("proxy")
s.Static = viper.GetString("static") s.Static = viper.GetString("static")
s.PathPrefix = path.Join("/", path.Clean(viper.GetString("path_prefix"))) s.PathPrefix = path.Join("/", path.Clean(viper.GetString("path_prefix")))

View File

@ -10,6 +10,7 @@ import (
type WebSocket struct { type WebSocket struct {
Password string Password string
AdminPassword string AdminPassword string
Proxy bool
Locks []string Locks []string
ControlProtection bool ControlProtection bool
@ -29,6 +30,11 @@ func (WebSocket) Init(cmd *cobra.Command) error {
return err return err
} }
cmd.PersistentFlags().Bool("proxy", false, "enable reverse proxy mode")
if err := viper.BindPFlag("proxy", cmd.PersistentFlags().Lookup("proxy")); err != nil {
return err
}
cmd.PersistentFlags().StringSlice("locks", []string{}, "resources, that will be locked when starting (control, login)") cmd.PersistentFlags().StringSlice("locks", []string{}, "resources, that will be locked when starting (control, login)")
if err := viper.BindPFlag("locks", cmd.PersistentFlags().Lookup("locks")); err != nil { if err := viper.BindPFlag("locks", cmd.PersistentFlags().Lookup("locks")); err != nil {
return err return err
@ -57,6 +63,7 @@ func (WebSocket) Init(cmd *cobra.Command) error {
func (s *WebSocket) Set() { func (s *WebSocket) Set() {
s.Password = viper.GetString("password") s.Password = viper.GetString("password")
s.AdminPassword = viper.GetString("password_admin") s.AdminPassword = viper.GetString("password_admin")
s.Proxy = viper.GetBool("proxy")
s.Locks = viper.GetStringSlice("locks") s.Locks = viper.GetStringSlice("locks")
s.ControlProtection = viper.GetBool("control_protection") s.ControlProtection = viper.GetBool("control_protection")

View File

@ -35,9 +35,6 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop t
router := chi.NewRouter() router := chi.NewRouter()
router.Use(middleware.RequestID) // Create a request ID for each request router.Use(middleware.RequestID) // Create a request ID for each request
if conf.Proxy {
router.Use(middleware.RealIP)
}
router.Use(middleware.RequestLogger(&logformatter{logger})) router.Use(middleware.RequestLogger(&logformatter{logger}))
router.Use(middleware.Recoverer) // Recover from panics without crashing server router.Use(middleware.Recoverer) // Recover from panics without crashing server
router.Use(middleware.Compress(5, "application/octet-stream")) router.Use(middleware.Compress(5, "application/octet-stream"))
@ -166,13 +163,7 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop t
return return
} }
err = r.ParseMultipartForm(32 << 20) r.ParseMultipartForm(32 << 20)
if err != nil || r.MultipartForm == nil {
logger.Warn().Err(err).Msg("failed to parse multipart form")
http.Error(w, "error parsing form", http.StatusBadRequest)
return
}
for _, formheader := range r.MultipartForm.File["files"] { for _, formheader := range r.MultipartForm.File["files"] {
filePath := webSocketHandler.FileTransferPath(formheader.Filename) filePath := webSocketHandler.FileTransferPath(formheader.Filename)
@ -193,11 +184,6 @@ func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop t
io.Copy(f, formfile) io.Copy(f, formfile)
} }
err = r.MultipartForm.RemoveAll()
if err != nil {
logger.Warn().Err(err).Msg("failed to remove multipart form")
}
}) })
} }

View File

@ -38,3 +38,14 @@ func GetIP(serverUrl string) (string, error) {
return string(bytes.TrimSpace(buf)), nil return string(bytes.TrimSpace(buf)), nil
} }
func GetHttpRequestIP(r *http.Request, proxy bool) string {
IPAddress := r.Header.Get("X-Real-Ip")
if IPAddress == "" {
IPAddress = r.Header.Get("X-Forwarded-For")
}
if IPAddress == "" || !proxy {
IPAddress = r.RemoteAddr
}
return IPAddress
}

View File

@ -290,7 +290,7 @@ func (ws *WebSocketHandler) Upgrade(w http.ResponseWriter, r *http.Request) erro
socket := &WebSocket{ socket := &WebSocket{
id: id, id: id,
ws: ws, ws: ws,
address: r.RemoteAddr, address: utils.GetHttpRequestIP(r, ws.conf.Proxy),
connection: connection, connection: connection,
} }

64
test/docker-compose.yaml Normal file
View File

@ -0,0 +1,64 @@
version: "3.4"
volumes:
X11-unix:
services:
xserver:
build: "./xserver"
restart: "unless-stopped"
user: "1000:1000"
command: ":0.0"
volumes:
- "X11-unix:/tmp/.X11-unix:rw"
pulseaudio:
build: "./pulseaudio"
restart: "unless-stopped"
neko:
build:
context: "../"
dockerfile: "test/neko/Dockerfile"
restart: "unless-stopped"
shm_size: "2gb"
ports:
- "8081:8080"
- "52000-52100:52000-52100/udp"
environment:
DISPLAY: ":0.0"
NEKO_DISPLAY: ":0.0 remote=true"
PULSE_SERVER: tcp:pulseaudio:4713
NEKO_SCREEN: 1920x1080@30
NEKO_PASSWORD: neko
NEKO_PASSWORD_ADMIN: admin
NEKO_EPR: 52000-52100
NEKO_NAT1TO1: 192.168.1.38
NEKO_ICELITE: 1
volumes:
- "X11-unix:/tmp/.X11-unix:ro"
depends_on:
- xserver
- pulseaudio
openbox:
build: "./openbox"
restart: "unless-stopped"
environment:
DISPLAY: ":0.0"
volumes:
- "X11-unix:/tmp/.X11-unix:ro"
depends_on:
- xserver
firefox:
build: "./firefox"
restart: "unless-stopped"
environment:
DISPLAY: ":0.0"
PULSE_SERVER: tcp:pulseaudio:4713
volumes:
- "X11-unix:/tmp/.X11-unix:ro"
depends_on:
- neko
- openbox

32
test/firefox/Dockerfile Normal file
View File

@ -0,0 +1,32 @@
FROM m1k1o/neko:base
#
# install firefox
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends firefox-esr; \
#
# create a non-root user
#groupadd --gid 1000 neko; \
#useradd --uid 1000 --gid neko --shell /bin/bash --create-home neko; \
#
# install fonts
apt-get install -y --no-install-recommends \
# Google emojis
fonts-noto-color-emoji \
# Japanese fonts
fonts-takao-mincho \
# Chinese fonts
fonts-wqy-zenhei xfonts-intl-chinese xfonts-wqy \
# Korean fonts
fonts-wqy-microhei; \
#
# clean up
apt-get --purge autoremove -y xz-utils bzip2; \
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
USER neko
ENTRYPOINT [ "/usr/bin/firefox" ]
CMD [ "--display", $DISPLAY, "-setDefaultBrowser", "-width", "1280", "-height", "720" ]

102
test/neko/Dockerfile Normal file
View File

@ -0,0 +1,102 @@
#
# STAGE 1: SERVER
#
FROM golang:1.20-bullseye as server
WORKDIR /src
#
# install dependencies
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends git cmake make libx11-dev libxrandr-dev libxtst-dev \
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly; \
#
# install libclipboard
set -eux; \
cd /tmp; \
git clone --depth=1 https://github.com/jtanx/libclipboard; \
cd libclipboard; \
cmake .; \
make -j4; \
make install; \
rm -rf /tmp/libclipboard; \
#
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# build server
COPY server/ .
RUN ./build
#
# STAGE 2: CLIENT
#
FROM node:18-bullseye-slim as client
WORKDIR /src
#
# install dependencies
COPY client/package*.json ./
RUN npm install
#
# build client
COPY client/ .
RUN npm run build
#
# STAGE 3: RUNTIME
#
FROM debian:bullseye-slim
#
# set custom user
ARG USERNAME=neko
ARG USER_UID=1000
ARG USER_GID=$USER_UID
RUN set -eux; \
apt-get update; \
#
# install dependencies
apt-get install -y --no-install-recommends pulseaudio dbus-x11 xserver-xorg-video-dummy; \
apt-get install -y --no-install-recommends libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx6; \
#
# gst
apt-get install -y --no-install-recommends \
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \
gstreamer1.0-pulseaudio; \
#
# create a non-root user
groupadd --gid $USER_GID $USERNAME; \
useradd --uid $USER_UID --gid $USERNAME --shell /bin/bash --create-home $USERNAME; \
#
# make directories for neko
mkdir -p /etc/neko /var/www /var/log/neko; \
chmod 1777 /var/log/neko; \
chown -R $USERNAME:$USERNAME /home/$USERNAME; \
#
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# set default envs
ENV USER=$USERNAME
ENV NEKO_PASSWORD=neko
ENV NEKO_PASSWORD_ADMIN=admin
ENV NEKO_BIND=:8080
#
# copy static files from previous stages
COPY --from=server /src/bin/neko /usr/bin/neko
COPY --from=client /src/dist/ /var/www
USER $USERNAME
ENTRYPOINT [ "/usr/bin/neko" ]
CMD [ "serve", "--static", "/var/www" ]

34
test/openbox/Dockerfile Normal file
View File

@ -0,0 +1,34 @@
FROM debian:bullseye-slim
#
# set custom user
ARG USERNAME=neko
ARG USER_UID=1000
ARG USER_GID=$USER_UID
#
# install dependencies
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends openbox; \
#
# create a non-root user
groupadd --gid $USER_GID $USERNAME; \
useradd --uid $USER_UID --gid $USERNAME --shell /bin/bash --create-home $USERNAME; \
#
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# set default envs
ENV USER=$USERNAME
#
# copy configuation files
COPY openbox.xml /etc/neko/openbox.xml
USER $USERNAME
ENTRYPOINT [ "/usr/bin/openbox" ]
CMD [ "--config-file", "/etc/neko/openbox.xml" ]

763
test/openbox/openbox.xml Normal file
View File

@ -0,0 +1,763 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Default openbox config but all window decorations are moved
thereby making it harder to accidentally close the virtual browser -->
<openbox_config xmlns="http://openbox.org/3.4/rc"
xmlns:xi="http://www.w3.org/2001/XInclude">
<resistance>
<strength>10</strength>
<screen_edge_strength>20</screen_edge_strength>
</resistance>
<applications>
<!-- Match all windows and remove their decorations (obxprop | grep "^_OB_APP") -->
<application class="firefox" name="Navigator" role="browser">
<decor>no</decor>
<maximized>true</maximized>
<focus>yes</focus>
<layer>normal</layer>
</application>
</applications>
<focus>
<focusNew>yes</focusNew>
<!-- always try to focus new windows when they appear. other rules do
apply -->
<followMouse>no</followMouse>
<!-- move focus to a window when you move the mouse into it -->
<focusLast>yes</focusLast>
<!-- focus the last used window when changing desktops, instead of the one
under the mouse pointer. when followMouse is enabled -->
<underMouse>no</underMouse>
<!-- move focus under the mouse, even when the mouse is not moving -->
<focusDelay>200</focusDelay>
<!-- when followMouse is enabled, the mouse must be inside the window for
this many milliseconds (1000 = 1 sec) before moving focus to it -->
<raiseOnFocus>no</raiseOnFocus>
<!-- when followMouse is enabled, and a window is given focus by moving the
mouse into it, also raise the window -->
</focus>
<placement>
<policy>Smart</policy>
<!-- 'Smart' or 'UnderMouse' -->
<center>yes</center>
<!-- whether to place windows in the center of the free area found or
the top left corner -->
<monitor>Primary</monitor>
<!-- with Smart placement on a multi-monitor system, try to place new windows
on: 'Any' - any monitor, 'Mouse' - where the mouse is, 'Active' - where
the active window is, 'Primary' - only on the primary monitor -->
<primaryMonitor>1</primaryMonitor>
<!-- The monitor where Openbox should place popup dialogs such as the
focus cycling popup, or the desktop switch popup. It can be an index
from 1, specifying a particular monitor. Or it can be one of the
following: 'Mouse' - where the mouse is, or
'Active' - where the active window is -->
</placement>
<theme>
<name>Clearlooks</name>
<titleLayout>NLIMC</titleLayout>
<!--
available characters are NDSLIMC, each can occur at most once.
N: window icon
L: window label (AKA title).
I: iconify
M: maximize
C: close
S: shade (roll up/down)
D: omnipresent (on all desktops).
-->
<keepBorder>yes</keepBorder>
<animateIconify>yes</animateIconify>
<font place="ActiveWindow">
<name>sans</name>
<size>8</size>
<!-- font size in points -->
<weight>bold</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="InactiveWindow">
<name>sans</name>
<size>8</size>
<!-- font size in points -->
<weight>bold</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="MenuHeader">
<name>sans</name>
<size>9</size>
<!-- font size in points -->
<weight>normal</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="MenuItem">
<name>sans</name>
<size>9</size>
<!-- font size in points -->
<weight>normal</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="ActiveOnScreenDisplay">
<name>sans</name>
<size>9</size>
<!-- font size in points -->
<weight>bold</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
<font place="InactiveOnScreenDisplay">
<name>sans</name>
<size>9</size>
<!-- font size in points -->
<weight>bold</weight>
<!-- 'bold' or 'normal' -->
<slant>normal</slant>
<!-- 'italic' or 'normal' -->
</font>
</theme>
<desktops>
<!-- this stuff is only used at startup, pagers allow you to change them
during a session
these are default values to use when other ones are not already set
by other applications, or saved in your session
use obconf if you want to change these without having to log out
and back in -->
<number>1</number>
<firstdesk>1</firstdesk>
<names>
<!-- set names up here if you want to, like this:
<name>desktop 1</name>
<name>desktop 2</name>
-->
</names>
<popupTime>875</popupTime>
<!-- The number of milliseconds to show the popup for when switching
desktops. Set this to 0 to disable the popup. -->
</desktops>
<resize>
<drawContents>yes</drawContents>
<popupShow>Nonpixel</popupShow>
<!-- 'Always', 'Never', or 'Nonpixel' (xterms and such) -->
<popupPosition>Center</popupPosition>
<!-- 'Center', 'Top', or 'Fixed' -->
<popupFixedPosition>
<!-- these are used if popupPosition is set to 'Fixed' -->
<x>10</x>
<!-- positive number for distance from left edge, negative number for
distance from right edge, or 'Center' -->
<y>10</y>
<!-- positive number for distance from top edge, negative number for
distance from bottom edge, or 'Center' -->
</popupFixedPosition>
</resize>
<!-- You can reserve a portion of your screen where windows will not cover when
they are maximized, or when they are initially placed.
Many programs reserve space automatically, but you can use this in other
cases. -->
<margins>
<top>0</top>
<bottom>0</bottom>
<left>0</left>
<right>0</right>
</margins>
<dock>
<position>TopLeft</position>
<!-- (Top|Bottom)(Left|Right|)|Top|Bottom|Left|Right|Floating -->
<floatingX>0</floatingX>
<floatingY>0</floatingY>
<noStrut>no</noStrut>
<stacking>Above</stacking>
<!-- 'Above', 'Normal', or 'Below' -->
<direction>Vertical</direction>
<!-- 'Vertical' or 'Horizontal' -->
<autoHide>no</autoHide>
<hideDelay>300</hideDelay>
<!-- in milliseconds (1000 = 1 second) -->
<showDelay>300</showDelay>
<!-- in milliseconds (1000 = 1 second) -->
<moveButton>Middle</moveButton>
<!-- 'Left', 'Middle', 'Right' -->
</dock>
<keyboard>
<chainQuitKey>C-g</chainQuitKey>
<!-- Keybindings for desktop switching -->
<keybind key="C-A-Left">
<action name="GoToDesktop"><to>left</to><wrap>no</wrap></action>
</keybind>
<keybind key="C-A-Right">
<action name="GoToDesktop"><to>right</to><wrap>no</wrap></action>
</keybind>
<keybind key="C-A-Up">
<action name="GoToDesktop"><to>up</to><wrap>no</wrap></action>
</keybind>
<keybind key="C-A-Down">
<action name="GoToDesktop"><to>down</to><wrap>no</wrap></action>
</keybind>
<keybind key="S-A-Left">
<action name="SendToDesktop"><to>left</to><wrap>no</wrap></action>
</keybind>
<keybind key="S-A-Right">
<action name="SendToDesktop"><to>right</to><wrap>no</wrap></action>
</keybind>
<keybind key="S-A-Up">
<action name="SendToDesktop"><to>up</to><wrap>no</wrap></action>
</keybind>
<keybind key="S-A-Down">
<action name="SendToDesktop"><to>down</to><wrap>no</wrap></action>
</keybind>
<keybind key="W-F1">
<action name="GoToDesktop"><to>1</to></action>
</keybind>
<keybind key="W-F2">
<action name="GoToDesktop"><to>2</to></action>
</keybind>
<keybind key="W-F3">
<action name="GoToDesktop"><to>3</to></action>
</keybind>
<keybind key="W-F4">
<action name="GoToDesktop"><to>4</to></action>
</keybind>
<keybind key="W-d">
<action name="ToggleShowDesktop"/>
</keybind>
<!-- Keybindings for windows -->
<keybind key="A-F4">
<action name="Close"/>
</keybind>
<keybind key="A-Escape">
<action name="Lower"/>
<action name="FocusToBottom"/>
<action name="Unfocus"/>
</keybind>
<keybind key="A-space">
<!--action name="ShowMenu"><menu>client-menu</menu></action-->
</keybind>
<!-- Take a screenshot of the current window with scrot when Alt+Print are pressed -->
<keybind key="A-Print">
<action name="Execute"><command>scrot -s</command></action>
</keybind>
<!-- Keybindings for window switching -->
<keybind key="A-Tab">
<action name="NextWindow">
<finalactions>
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</finalactions>
</action>
</keybind>
<keybind key="A-S-Tab">
<action name="PreviousWindow">
<finalactions>
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</finalactions>
</action>
</keybind>
<keybind key="C-A-Tab">
<action name="NextWindow">
<panels>yes</panels><desktop>yes</desktop>
<finalactions>
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</finalactions>
</action>
</keybind>
<!-- Keybindings for window switching with the arrow keys -->
<keybind key="W-S-Right">
<action name="DirectionalCycleWindows">
<direction>right</direction>
</action>
</keybind>
<keybind key="W-S-Left">
<action name="DirectionalCycleWindows">
<direction>left</direction>
</action>
</keybind>
<keybind key="W-S-Up">
<action name="DirectionalCycleWindows">
<direction>up</direction>
</action>
</keybind>
<keybind key="W-S-Down">
<action name="DirectionalCycleWindows">
<direction>down</direction>
</action>
</keybind>
<!-- Keybindings for running applications -->
<keybind key="W-e">
<action name="Execute">
<startupnotify>
<enabled>true</enabled>
<name>Konqueror</name>
</startupnotify>
<command>kfmclient openProfile filemanagement</command>
</action>
</keybind>
<!-- Launch scrot when Print is pressed -->
<keybind key="Print">
<action name="Execute"><command>scrot</command></action>
</keybind>
</keyboard>
<mouse>
<dragThreshold>1</dragThreshold>
<!-- number of pixels the mouse must move before a drag begins -->
<doubleClickTime>500</doubleClickTime>
<!-- in milliseconds (1000 = 1 second) -->
<screenEdgeWarpTime>400</screenEdgeWarpTime>
<!-- Time before changing desktops when the pointer touches the edge of the
screen while moving a window, in milliseconds (1000 = 1 second).
Set this to 0 to disable warping -->
<screenEdgeWarpMouse>false</screenEdgeWarpMouse>
<!-- Set this to TRUE to move the mouse pointer across the desktop when
switching due to hitting the edge of the screen -->
<context name="Frame">
<mousebind button="A-Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="A-Left" action="Click">
<action name="Unshade"/>
</mousebind>
<mousebind button="A-Left" action="Drag">
<action name="Move"/>
</mousebind>
<mousebind button="A-Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="A-Right" action="Drag">
<action name="Resize"/>
</mousebind>
<mousebind button="A-Middle" action="Press">
<action name="Lower"/>
<action name="FocusToBottom"/>
<action name="Unfocus"/>
</mousebind>
<mousebind button="A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="C-A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="C-A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="A-S-Up" action="Click">
<action name="SendToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="A-S-Down" action="Click">
<action name="SendToDesktop"><to>next</to></action>
</mousebind>
</context>
<context name="Titlebar">
<mousebind button="Left" action="Drag">
<action name="Move"/>
</mousebind>
<mousebind button="Left" action="DoubleClick">
<action name="ToggleMaximize"/>
</mousebind>
<mousebind button="Up" action="Click">
<action name="if">
<shaded>no</shaded>
<then>
<action name="Shade"/>
<action name="FocusToBottom"/>
<action name="Unfocus"/>
<action name="Lower"/>
</then>
</action>
</mousebind>
<mousebind button="Down" action="Click">
<action name="if">
<shaded>yes</shaded>
<then>
<action name="Unshade"/>
<action name="Raise"/>
</then>
</action>
</mousebind>
</context>
<context name="Titlebar Top Right Bottom Left TLCorner TRCorner BRCorner BLCorner">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Middle" action="Press">
<action name="Lower"/>
<action name="FocusToBottom"/>
<action name="Unfocus"/>
</mousebind>
<!--mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="ShowMenu"><menu>client-menu</menu></action>
</mousebind-->
</context>
<context name="Top">
<mousebind button="Left" action="Drag">
<action name="Resize"><edge>top</edge></action>
</mousebind>
</context>
<context name="Left">
<mousebind button="Left" action="Drag">
<action name="Resize"><edge>left</edge></action>
</mousebind>
</context>
<context name="Right">
<mousebind button="Left" action="Drag">
<action name="Resize"><edge>right</edge></action>
</mousebind>
</context>
<context name="Bottom">
<mousebind button="Left" action="Drag">
<action name="Resize"><edge>bottom</edge></action>
</mousebind>
<!--mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="ShowMenu"><menu>client-menu</menu></action>
</mousebind-->
</context>
<context name="TRCorner BRCorner TLCorner BLCorner">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Left" action="Drag">
<action name="Resize"/>
</mousebind>
</context>
<context name="Client">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Middle" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
</context>
<context name="Icon">
<!--mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
<action name="ShowMenu"><menu>client-menu</menu></action>
</mousebind>
<mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="ShowMenu"><menu>client-menu</menu></action>
</mousebind-->
</context>
<context name="AllDesktops">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="ToggleOmnipresent"/>
</mousebind>
</context>
<context name="Shade">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="ToggleShade"/>
</mousebind>
</context>
<context name="Iconify">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="Iconify"/>
</mousebind>
</context>
<context name="Maximize">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Middle" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="ToggleMaximize"/>
</mousebind>
<mousebind button="Middle" action="Click">
<action name="ToggleMaximize"><direction>vertical</direction></action>
</mousebind>
<mousebind button="Right" action="Click">
<action name="ToggleMaximize"><direction>horizontal</direction></action>
</mousebind>
</context>
<context name="Close">
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
<action name="Unshade"/>
</mousebind>
<mousebind button="Left" action="Click">
<action name="Close"/>
</mousebind>
</context>
<context name="Desktop">
<mousebind button="Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="C-A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="C-A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
<mousebind button="Right" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
</context>
<context name="Root">
<!-- Menus -->
<!--mousebind button="Middle" action="Press">
<action name="ShowMenu"><menu>client-list-combined-menu</menu></action>
</mousebind>
<mousebind button="Right" action="Press">
<action name="ShowMenu"><menu>root-menu</menu></action>
</mousebind-->
</context>
<context name="MoveResize">
<mousebind button="Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
<mousebind button="A-Up" action="Click">
<action name="GoToDesktop"><to>previous</to></action>
</mousebind>
<mousebind button="A-Down" action="Click">
<action name="GoToDesktop"><to>next</to></action>
</mousebind>
</context>
</mouse>
<menu>
<!-- You can specify more than one menu file in here and they are all loaded,
just don't make menu ids clash or, well, it'll be kind of pointless -->
<!-- default menu file (or custom one in $HOME/.config/openbox/) -->
<!-- system menu files on Debian systems -->
<!--file>/var/lib/openbox/debian-menu.xml</file-->
<file>menu.xml</file>
<hideDelay>200</hideDelay>
<!-- if a press-release lasts longer than this setting (in milliseconds), the
menu is hidden again -->
<middle>no</middle>
<!-- center submenus vertically about the parent entry -->
<submenuShowDelay>100</submenuShowDelay>
<!-- time to delay before showing a submenu after hovering over the parent
entry.
if this is a negative value, then the delay is infinite and the
submenu will not be shown until it is clicked on -->
<submenuHideDelay>400</submenuHideDelay>
<!-- time to delay before hiding a submenu when selecting another
entry in parent menu
if this is a negative value, then the delay is infinite and the
submenu will not be hidden until a different submenu is opened -->
<showIcons>yes</showIcons>
<!-- controls if icons appear in the client-list-(combined-)menu -->
<manageDesktops>yes</manageDesktops>
<!-- show the manage desktops section in the client-list-(combined-)menu -->
</menu>
<applications>
<!--
# this is an example with comments through out. use these to make your
# own rules, but without the comments of course.
# you may use one or more of the name/class/role/title/type rules to specify
# windows to match
<application name="the window's _OB_APP_NAME property (see obxprop)"
class="the window's _OB_APP_CLASS property (see obxprop)"
groupname="the window's _OB_APP_GROUP_NAME property (see obxprop)"
groupclass="the window's _OB_APP_GROUP_CLASS property (see obxprop)"
role="the window's _OB_APP_ROLE property (see obxprop)"
title="the window's _OB_APP_TITLE property (see obxprop)"
type="the window's _OB_APP_TYPE property (see obxprob)..
(if unspecified, then it is 'dialog' for child windows)">
# you may set only one of name/class/role/title/type, or you may use more
# than one together to restrict your matches.
# the name, class, role, and title use simple wildcard matching such as those
# used by a shell. you can use * to match any characters and ? to match
# any single character.
# the type is one of: normal, dialog, splash, utility, menu, toolbar, dock,
# or desktop
# when multiple rules match a window, they will all be applied, in the
# order that they appear in this list
# each rule element can be left out or set to 'default' to specify to not
# change that attribute of the window
<decor>yes</decor>
# enable or disable window decorations
<shade>no</shade>
# make the window shaded when it appears, or not
<position force="no">
# the position is only used if both an x and y coordinate are provided
# (and not set to 'default')
# when force is "yes", then the window will be placed here even if it
# says you want it placed elsewhere. this is to override buggy
# applications who refuse to behave
<x>center</x>
# a number like 50, or 'center' to center on screen. use a negative number
# to start from the right (or bottom for <y>), ie -50 is 50 pixels from
# the right edge (or bottom). use 'default' to specify using value
# provided by the application, or chosen by openbox, instead.
<y>200</y>
<monitor>1</monitor>
# specifies the monitor in a xinerama setup.
# 1 is the first head, or 'mouse' for wherever the mouse is
</position>
<size>
# the size to make the window.
<width>20</width>
# a number like 20, or 'default' to use the size given by the application.
# you can use fractions such as 1/2 or percentages such as 75% in which
# case the value is relative to the size of the monitor that the window
# appears on.
<height>30%</height>
</size>
<focus>yes</focus>
# if the window should try be given focus when it appears. if this is set
# to yes it doesn't guarantee the window will be given focus. some
# restrictions may apply, but Openbox will try to
<desktop>1</desktop>
# 1 is the first desktop, 'all' for all desktops
<layer>normal</layer>
# 'above', 'normal', or 'below'
<iconic>no</iconic>
# make the window iconified when it appears, or not
<skip_pager>no</skip_pager>
# asks to not be shown in pagers
<skip_taskbar>no</skip_taskbar>
# asks to not be shown in taskbars. window cycling actions will also
# skip past such windows
<fullscreen>yes</fullscreen>
# make the window in fullscreen mode when it appears
<maximized>true</maximized>
# 'Horizontal', 'Vertical' or boolean (yes/no)
</application>
# end of the example
-->
</applications>
</openbox_config>

View File

@ -0,0 +1,38 @@
FROM debian:bullseye-slim
#
# set custom user
ARG USERNAME=neko
ARG USER_UID=1000
ARG USER_GID=$USER_UID
#
# install dependencies
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends pulseaudio; \
#
# create a non-root user
groupadd --gid $USER_GID $USERNAME; \
useradd --uid $USER_UID --gid $USERNAME --shell /bin/bash --create-home $USERNAME; \
#
# make directories
mkdir -p /home/$USERNAME/.config/pulse; \
chown -R $USERNAME:$USERNAME /home/$USERNAME; \
#
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# set default envs
ENV USER=$USERNAME
#
# copy configuation files
COPY default.pa /etc/pulse/default.pa
USER $USERNAME
ENTRYPOINT [ "/usr/bin/pulseaudio" ]
CMD [ "--log-level=info", "--disallow-module-loading", "--disallow-exit", "--exit-idle-time=-1" ]

View File

@ -0,0 +1,10 @@
#!/usr/bin/pulseaudio -nF
### Create virtual output device sink
load-module module-null-sink sink_name=audio_output sink_properties=device.description="Virtual\ Audio\ Output"
# Allow pulse audio to be accessed via TCP (from localhost only), to allow other users to access the virtual devices
load-module module-native-protocol-tcp port=4713 auth-anonymous=1
### Make sure we always have a sink around, even if it is a null sink.
load-module module-always-sink

17
test/xserver/Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM debian:bullseye-slim
#
# install dependencies
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends x11-xserver-utils xserver-xorg-video-dummy; \
#
# clean up
apt-get clean -y; \
rm -rf /var/lib/apt/lists/* /var/cache/apt/*
#
# copy configuation files
COPY xorg.conf /etc/neko/xorg.conf
ENTRYPOINT [ "/usr/bin/X", "-config", "/etc/neko/xorg.conf", "-nolisten", "local", "-logfile", "/dev/stderr" ]

88
test/xserver/xorg.conf Normal file
View File

@ -0,0 +1,88 @@
# This xorg configuration file is meant to be used by xpra
# to start a dummy X11 server.
# For details, please see:
# https://xpra.org/trac/wiki/Xdummy
Section "ServerFlags"
Option "DontVTSwitch" "true"
Option "AllowMouseOpenFail" "true"
Option "PciForceNone" "true"
Option "AutoEnableDevices" "false"
Option "AutoAddDevices" "false"
EndSection
Section "InputDevice"
Identifier "dummy_mouse"
Option "CorePointer" "true"
Driver "void"
EndSection
Section "InputDevice"
Identifier "dummy_keyboard"
Option "CoreKeyboard" "true"
Driver "void"
EndSection
Section "Device"
Identifier "dummy_videocard"
Driver "dummy"
Option "ConstantDPI" "true"
#VideoRam 4096000
#VideoRam 256000
VideoRam 192000
EndSection
Section "Monitor"
Identifier "dummy_monitor"
HorizSync 5.0 - 1000.0
VertRefresh 5.0 - 200.0
#This can be used to get a specific DPI, but only for the default resolution:
#DisplaySize 508 317
#NOTE: the highest modes will not work without increasing the VideoRam
# for the dummy video card.
# https://arachnoid.com/modelines/
# 1280x720 @ 30.00 Hz (GTF) hsync: 21.99 kHz; pclk: 33.78 MHz
Modeline "1280x720_30.00" 33.78 1280 1288 1408 1536 720 721 724 733 -HSync +Vsync
# 1280x720 @ 60.00 Hz (GTF) hsync: 44.76 kHz; pclk: 74.48 MHz
Modeline "1280x720_60.00" 74.48 1280 1336 1472 1664 720 721 724 746 -HSync +Vsync
# 1152x648 @ 60.00 Hz (GTF) hsync: 40.26 kHz; pclk: 59.91 MHz
Modeline "1152x648_60.00" 59.91 1152 1200 1320 1488 648 649 652 671 -HSync +Vsync
# 1024x576 @ 60.00 Hz (GTF) hsync: 35.82 kHz; pclk: 47.00 MHz
Modeline "1024x576_60.00" 47.00 1024 1064 1168 1312 576 577 580 597 -HSync +Vsync
# 960x720 @ 60.00 Hz (GTF) hsync: 44.76 kHz; pclk: 55.86 MHz
Modeline "960x720_60.00" 55.86 960 1008 1104 1248 720 721 724 746 -HSync +Vsync
# 800x600 @ 60.00 Hz (GTF) hsync: 37.32 kHz; pclk: 38.22 MHz
Modeline "800x600_60.00" 38.22 800 832 912 1024 600 601 604 622 -HSync +Vsync
# 1920x1080 @ 30.00 Hz (GTF) hsync: 32.97 kHz; pclk: 80.18 MHz
Modeline "1920x1080_30.00" 80.18 1920 1984 2176 2432 1080 1081 1084 1099 -HSync +Vsync
# 1152x648 @ 30.00 Hz (GTF) hsync: 19.80 kHz; pclk: 26.93 MHz
Modeline "1152x648_30.00" 26.93 1152 1144 1256 1360 648 649 652 660 -HSync +Vsync
# 1024x576 @ 30.00 Hz (GTF) hsync: 17.61 kHz; pclk: 20.85 MHz
Modeline "1024x576_30.00" 20.85 1024 1008 1104 1184 576 577 580 587 -HSync +Vsync
# 960x720 @ 30.00 Hz (GTF) hsync: 21.99 kHz; pclk: 25.33 MHz
Modeline "960x720_30.00" 25.33 960 960 1056 1152 720 721 724 733 -HSync +Vsync
# 800x600 @ 30.00 Hz (GTF) hsync: 18.33 kHz; pclk: 17.01 MHz
Modeline "800x600_30.00" 17.01 800 792 864 928 600 601 604 611 -HSync +Vsync
EndSection
Section "Screen"
Identifier "dummy_screen"
Device "dummy_videocard"
Monitor "dummy_monitor"
DefaultDepth 24
SubSectionSub "Display"
Viewport 0 0
Depth 24
Modes "1280x720_30.00" "1920x1080_60.00" "1280x720_60.00" "1152x648_60.00" "1024x576_60.00" "960x720_60.00" "800x600_60.00" "1920x1080_30.00" "1152x648_30.00" "1024x576_30.00" "960x720_30.00" "800x600_30.00"
EndSubSection
EndSection
Section "ServerLayout"
Identifier "dummy_layout"
Screen "dummy_screen"
InputDevice "dummy_mouse"
InputDevice "dummy_keyboard"
EndSection