From 202985097e375d6b1b10407295575756d9825944 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Thu, 19 Oct 2017 15:15:49 +1100 Subject: [PATCH] Initial import of project to git --- client/KVMGFXHeader.h | 42 +++ client/Makefile | 13 + client/debug.h | 16 + client/i3start.sh | 3 + client/kb.h | 37 ++ client/main | Bin 0 -> 93240 bytes client/main.c | 553 ++++++++++++++++++++++++++++ client/spice-common | 1 + client/spice.c | 787 ++++++++++++++++++++++++++++++++++++++++ client/spice.h | 16 + client/spice/messages.h | 102 ++++++ server/TODO | 0 12 files changed, 1570 insertions(+) create mode 100644 client/KVMGFXHeader.h create mode 100644 client/Makefile create mode 100644 client/debug.h create mode 100755 client/i3start.sh create mode 100644 client/kb.h create mode 100755 client/main create mode 100644 client/main.c create mode 160000 client/spice-common create mode 100644 client/spice.c create mode 100644 client/spice.h create mode 100644 client/spice/messages.h create mode 100644 server/TODO diff --git a/client/KVMGFXHeader.h b/client/KVMGFXHeader.h new file mode 100644 index 00000000..ce536fd8 --- /dev/null +++ b/client/KVMGFXHeader.h @@ -0,0 +1,42 @@ + +#define KVMGFX_HEADER_MAGIC "[[KVMGFXHeader]]" + +typedef enum FrameType +{ + FRAME_TYPE_INVALID , + FRAME_TYPE_ARGB , // ABGR interleaved: A,R,G,B 32bpp + FRAME_TYPE_RGB , // RGB interleaved : R,G,B 24bpp + FRAME_TYPE_YUV420P , // YUV420 12bpp + FRAME_TYPE_ARGB10 , // rgb 10 bit packed, a2 b10 r10 + FRAME_TYPE_XOR , // xor of the previous frame: R, G, B + FRAME_TYPE_MAX , // sentinel value +} FrameType; + +typedef enum FrameComp +{ + FRAME_COMP_NONE , // no compression + FRAME_COMP_BLACK_RLE , // basic run length encoding of black pixels for XOR mode + FRAME_COMP_MAX , // sentinel valule +} FrameComp; + +struct KVMGFXHeader +{ + char magic[sizeof(KVMGFX_HEADER_MAGIC)]; + uint32_t version; // version of this structure + FrameType frameType; // the frame type + FrameComp compType; // frame compression mode + uint32_t width; // the width + uint32_t height; // the height + uint32_t stride; // the row stride + uint64_t frames; // total frame count + uint64_t clientFrame; // current client frame + uint32_t dataLen; // total lengh of the data after this header +}; + +#pragma pack(push,1) +struct RLEHeader +{ + uint8_t magic[3]; + uint16_t length; +}; +#pragma pack(pop) \ No newline at end of file diff --git a/client/Makefile b/client/Makefile new file mode 100644 index 00000000..13fa9e6d --- /dev/null +++ b/client/Makefile @@ -0,0 +1,13 @@ +CFLAGS=-g -Og -std=gnu99 -march=native -Wall -Werror +LDFLAGS=-lrt -lGL + +CFLAGS+=`pkg-config --cflags sdl2` +LDFLAGS+=`pkg-config --libs sdl2` + +CFLAGS+=`pkg-config --cflags libssl openssl` +LDFLAGS+=`pkg-config --libs libssl openssl` + +CFLAGS+=`pkg-config --cflags spice-protocol` + +all: + gcc ${CFLAGS} -o main main.c spice.c ${LDFLAGS} diff --git a/client/debug.h b/client/debug.h new file mode 100644 index 00000000..7d0a3bf4 --- /dev/null +++ b/client/debug.h @@ -0,0 +1,16 @@ +#ifdef DEBUG + #define DEBUG_PRINT(type, fmt, args...) do {fprintf(stderr, type " %-30s : %-5u | " fmt "\n", __FUNCTION__, __LINE__, ##args);} while (0) +#else + #define DEBUG_PRINT(type, fmt, args...) do {} while(0) +#endif + +#define DEBUG_INFO(fmt, args...) DEBUG_PRINT("[I]", fmt, ##args) +#define DEBUG_WARN(fmt, args...) DEBUG_PRINT("[W]", fmt, ##args) +#define DEBUG_ERROR(fmt, args...) DEBUG_PRINT("[E]", fmt, ##args) +#define DEBUG_FIXME(fmt, args...) DEBUG_PRINT("[F]", fmt, ##args) + +#ifdef DEBUG_SPICE + #define DEBUG_PROTO(fmt, args...) DEBUG_PRINT("[P]", fmt, ##args) +#else + #define DEBUG_PROTO(fmt, args...) do {} while(0) +#endif \ No newline at end of file diff --git a/client/i3start.sh b/client/i3start.sh new file mode 100755 index 00000000..d6952ffd --- /dev/null +++ b/client/i3start.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +i3-msg 'workspace 10; move workspace to DP-0; exec /home/geoff/Projects/kvm-shm/main' \ No newline at end of file diff --git a/client/kb.h b/client/kb.h new file mode 100644 index 00000000..3657a3da --- /dev/null +++ b/client/kb.h @@ -0,0 +1,37 @@ +static uint32_t usb_to_ps2[] = +{ + 0x000000, 0x000000, 0x000000, 0x000000, 0x00001e, 0x000030, 0x00002e, + 0x000020, 0x000012, 0x000021, 0x000022, 0x000023, 0x000017, 0x000024, + 0x000025, 0x000026, 0x000032, 0x000031, 0x000018, 0x000019, 0x000010, + 0x000013, 0x00001f, 0x000014, 0x000016, 0x00002f, 0x000011, 0x00002d, + 0x000015, 0x00002c, 0x000002, 0x000003, 0x000004, 0x000005, 0x000006, + 0x000007, 0x000008, 0x000009, 0x00000a, 0x00000b, 0x00001c, 0x000001, + 0x00000e, 0x00000f, 0x000039, 0x00000c, 0x00000d, 0x00001a, 0x00001b, + 0x00002b, 0x00002b, 0x000027, 0x000028, 0x000029, 0x000033, 0x000034, + 0x000035, 0x00003a, 0x00003b, 0x00003c, 0x00003d, 0x00003e, 0x00003f, + 0x000040, 0x000041, 0x000042, 0x000043, 0x000044, 0x000057, 0x000058, + 0x00e037, 0x000046, 0x00e046, 0x00e052, 0x00e047, 0x00e049, 0x00e053, + 0x00e04f, 0x00e051, 0x00e04d, 0x00e04b, 0x00e050, 0x00e048, 0x000045, + 0x00e035, 0x000037, 0x00004a, 0x00004e, 0x00e01c, 0x00004f, 0x000050, + 0x000051, 0x00004b, 0x00004c, 0x00004d, 0x000047, 0x000048, 0x000049, + 0x000052, 0x000053, 0x000056, 0x00e05d, 0x000000, 0x000059, 0x00005d, + 0x00005e, 0x00005f, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, + 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, + 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, + 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, + 0x00007e, 0x000000, 0x000073, 0x000070, 0x00007d, 0x000079, 0x00007b, + 0x00005c, 0x000000, 0x000000, 0x000000, 0x0000f2, 0x0000f1, 0x000078, + 0x000077, 0x000076, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, + 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, + 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, + 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, + 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, + 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, + 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, + 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, + 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, + 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, + 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, 0x000000, + 0x00001d, 0x00002a, 0x000038, 0x00e05b, 0x00e01d, 0x000036, 0x00e038, + 0x00e05c +}; \ No newline at end of file diff --git a/client/main b/client/main new file mode 100755 index 0000000000000000000000000000000000000000..6af428bad0b6a1638c9ba5394c1a32c896927b7b GIT binary patch literal 93240 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%0Alo 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 SuBzql8Pxd;0fqy&*8c`K2UU~+ literal 0 HcmV?d00001 diff --git a/client/main.c b/client/main.c new file mode 100644 index 00000000..04578cda --- /dev/null +++ b/client/main.c @@ -0,0 +1,553 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define DEBUG +#include "debug.h" +#include "KVMGFXHeader.h" +#include "spice.h" +#include "kb.h" + +#define MAP_SIZE (16*1024*1024) +#define COPY_THREADS 4 + +typedef void (*CompFunc)(uint8_t * dst, const uint8_t * src, const size_t len); +typedef void (*DrawFunc)(CompFunc compFunc, SDL_Texture * texture, uint8_t * dst, const uint8_t * src); + +typedef struct +{ + SDL_mutex * mutex; + SDL_cond * cond; + SDL_Thread * thread; + + bool rdy; + void *dst; + const void * src; + size_t len; +} +CopyJob; + +struct KVMGFXState +{ + bool running; + SDL_Window * window; + SDL_Renderer * renderer; + struct KVMGFXHeader * shm; + + SDL_sem * cpySem; + SDL_Thread * cpyThreads[COPY_THREADS]; + CopyJob cpyJobs [COPY_THREADS]; +}; + +struct KVMGFXState state; + +int copyThread(void * arg) +{ + CopyJob * job = (CopyJob *)arg; + + while(state.running) + { + SDL_LockMutex(job->mutex); + while(!job->rdy) + SDL_CondWait(job->cond, job->mutex); + job->rdy = false; + SDL_UnlockMutex(job->mutex); + + memcpy(job->dst, job->src, job->len); + + // return a lock to the pool + SDL_SemPost(state.cpySem); + } + + return 0; +} + +bool startCopyThreads() +{ + state.cpySem = SDL_CreateSemaphore(COPY_THREADS); + + for(int i = 0; i < COPY_THREADS; ++i) + { + // take a lock from the pool + SDL_SemWait(state.cpySem); + + CopyJob * job = &state.cpyJobs[i]; + job->mutex = SDL_CreateMutex(); + job->cond = SDL_CreateCond(); + job->rdy = false; + job->dst = NULL; + job->src = NULL; + job->len = 0; + + job->thread = SDL_CreateThread( + copyThread, "copyThread", &state.cpyJobs[i]); + } + + return true; +} + +void stopCopyThreads() +{ +} + +void compFunc_NONE(uint8_t * dst, const uint8_t * src, const size_t len) +{ + const size_t part = len / COPY_THREADS; + for(int i = 0; i < COPY_THREADS; ++i) + { + CopyJob * job = &state.cpyJobs[i]; + job->dst = dst + i * part; + job->src = src + i * part; + job->len = part; + job->rdy = true; + SDL_CondSignal(job->cond); + } + + // wait for the threads to complete + for(int i = 0; i < COPY_THREADS; ++i) + SDL_SemWait(state.cpySem); +} + +void compFunc_BLACK_RLE(uint8_t * dst, const uint8_t * src, const size_t len) +{ + const size_t pixels = len / 3; + for(size_t i = 0; i < pixels;) + { + if (!src[0] && !src[1] && !src[2]) + { + struct RLEHeader * h = (struct RLEHeader *)src; + dst += h->length * 3; + i += h->length; + src += sizeof(struct RLEHeader); + continue; + } + + memcpy(dst, src, 3); + dst += 3; + src += 3; + ++i; + } +} + +inline bool areFormatsSame(const struct KVMGFXHeader s1, const struct KVMGFXHeader s2) +{ + return + (s1.version == s2.version ) && + (s1.frameType == s2.frameType) && + (s1.compType == s2.compType ) && + (s1.width == s2.width ) && + (s1.height == s2.height ); +} + +void drawFunc_ARGB10(CompFunc compFunc, SDL_Texture * texture, uint8_t * dst, const uint8_t * src) +{ + SDL_UpdateTexture(texture, NULL, src, state.shm->stride * 4); + SDL_RenderClear(state.renderer); + + SDL_RenderCopy(state.renderer, texture, NULL, NULL); + SDL_RenderPresent(state.renderer); +} + +void drawFunc_ARGB(CompFunc compFunc, SDL_Texture * texture, uint8_t * dst, const uint8_t * src) +{ + compFunc(dst, src, state.shm->height * state.shm->stride * 4); + SDL_UnlockTexture(texture); + SDL_RenderClear(state.renderer); + + SDL_RenderCopy(state.renderer, texture, NULL, NULL); + SDL_RenderPresent(state.renderer); +} + +void drawFunc_RGB(CompFunc compFunc, SDL_Texture * texture, uint8_t * dst, const uint8_t * src) +{ + compFunc(dst, src, state.shm->height * state.shm->stride * 3); + SDL_UnlockTexture(texture); + SDL_RenderClear(state.renderer); + + SDL_RenderCopy(state.renderer, texture, NULL, NULL); + SDL_RenderPresent(state.renderer); +} + +void drawFunc_XOR(CompFunc compFunc, SDL_Texture * texture, uint8_t * dst, const uint8_t * src) +{ + glEnable(GL_COLOR_LOGIC_OP); + glLogicOp(GL_XOR); + compFunc(dst, src, state.shm->height * state.shm->stride * 3); + SDL_UnlockTexture(texture); + if (state.shm->frames == 1) + SDL_RenderClear(state.renderer); + + SDL_RenderCopy(state.renderer, texture, NULL, NULL); + SDL_RenderPresent(state.renderer); + + // clear the buffer for the next frame + memset(dst, 0, state.shm->height * state.shm->stride * 3); +} + +void drawFunc_YUV420P(CompFunc compFunc, SDL_Texture * texture, uint8_t * dst, const uint8_t * src) +{ + const unsigned int pixels = state.shm->width * state.shm->height; + + SDL_UpdateYUVTexture(texture, NULL, + src , state.shm->stride, + src + pixels , state.shm->stride / 2, + src + pixels + pixels / 4, state.shm->stride / 2 + ); + SDL_RenderClear(state.renderer); + + SDL_RenderCopy(state.renderer, texture, NULL, NULL); + SDL_RenderPresent(state.renderer); +} + +int renderThread(void * unused) +{ + bool startup = true; + struct KVMGFXHeader format; + SDL_Texture *texture = NULL; + uint8_t *pixels = (uint8_t*)(state.shm + 1); + uint8_t *texPixels = NULL; + DrawFunc drawFunc = NULL; + CompFunc compFunc = NULL; + + format.version = 1; + format.frameType = FRAME_TYPE_INVALID; + format.width = 0; + format.height = 0; + format.stride = 0; + format.frames = 0; + + if (SDL_Init(SDL_INIT_VIDEO) < 0) + { + DEBUG_ERROR("SDL_Init Failed"); + return -1; + } + + state.window = SDL_CreateWindow("KVM-GFX Test", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 100, 100, SDL_WINDOW_BORDERLESS); + if (!state.window) + { + DEBUG_ERROR("failed to create window"); + return -1; + } + + state.renderer = SDL_CreateRenderer(state.window , -1, 0); + if (!state.renderer) + { + DEBUG_ERROR("failed to create window"); + return -1; + } + + startCopyThreads(); + + while(state.running) + { + // ensure the header magic is valid, this will help prevent crash out when the memory hasn't yet been initialized + if (memcmp(state.shm->magic, KVMGFX_HEADER_MAGIC, sizeof(KVMGFX_HEADER_MAGIC)) != 0) + continue; + + // if the frame count hasn't changed, we don't do anything + if (!startup && format.frames == state.shm->frames) + { + if (!state.running) + break; + + usleep(100); + continue; + } + startup = false; + + // if the format is invalid or it has changed + if (format.frameType == FRAME_TYPE_INVALID || !areFormatsSame(format, *state.shm)) + { + if (texture) + { + SDL_DestroyTexture(texture); + texture = NULL; + } + + Uint32 sdlFormat; + switch(state.shm->frameType) + { + case FRAME_TYPE_ARGB : sdlFormat = SDL_PIXELFORMAT_ARGB8888 ; drawFunc = drawFunc_ARGB ; break; + case FRAME_TYPE_RGB : sdlFormat = SDL_PIXELFORMAT_RGB24 ; drawFunc = drawFunc_RGB ; break; + case FRAME_TYPE_YUV420P: sdlFormat = SDL_PIXELFORMAT_YV12 ; drawFunc = drawFunc_YUV420P; break; + case FRAME_TYPE_ARGB10 : sdlFormat = SDL_PIXELFORMAT_ARGB2101010; drawFunc = drawFunc_ARGB10 ; break; + case FRAME_TYPE_XOR : sdlFormat = SDL_PIXELFORMAT_RGB24 ; drawFunc = drawFunc_XOR ; break; + default: + format.frameType = FRAME_TYPE_INVALID; + continue; + } + + switch(state.shm->compType) + { + case FRAME_COMP_NONE : compFunc = compFunc_NONE ; break; + case FRAME_COMP_BLACK_RLE: compFunc = compFunc_BLACK_RLE; break; + default: + format.frameType = FRAME_TYPE_INVALID; + continue; + } + + // update the window size and create the render texture + SDL_SetWindowSize(state.window, state.shm->width, state.shm->height); + SDL_SetWindowPosition(state.window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); + + texture = SDL_CreateTexture(state.renderer, sdlFormat, SDL_TEXTUREACCESS_STREAMING, state.shm->width, state.shm->height); + + // this doesnt "lock" anything, pre-fetch the pointers for later use + int unused; + SDL_LockTexture(texture, NULL, (void**)&texPixels, &unused); + + memcpy(&format, state.shm, sizeof(format)); + } + + if (format.frames != state.shm->frames - 1) + DEBUG_INFO("dropped %lu", state.shm->frames - format.frames); + format.frames = state.shm->frames; + + glDisable(GL_COLOR_LOGIC_OP); + drawFunc(compFunc, texture, texPixels, pixels); + + state.shm->clientFrame = format.frames; + + // dont waste CPU, frames don't come that fast! + usleep(10); + } + + SDL_DestroyTexture(texture); + stopCopyThreads(); + return 0; +} + +int spiceThread(void * arg) +{ + while(state.running) + if (!spice_process()) + { + state.running = false; + DEBUG_ERROR("Failed to process spice messages"); + break; + } + + spice_disconnect(); + return 0; +} + +static inline const uint32_t mapScancode(SDL_Scancode scancode) +{ + uint32_t ps2; + if (scancode > (sizeof(usb_to_ps2) / sizeof(uint32_t)) || (ps2 = usb_to_ps2[scancode]) == 0) + { + DEBUG_WARN("Unable to map USB scan code: %x\n", scancode); + return 0; + } + return ps2; +} + +int eventThread(void * arg) +{ + int mouseX = 0; + int mouseY = 0; + + // default to server mode + bool serverMode = true; + spice_mouse_mode(true); + SDL_SetRelativeMouseMode(true); + + // work around SDL_ShowCursor being non functional + SDL_Cursor *cursor; + int32_t cursorData[2] = {0, 0}; + cursor = SDL_CreateCursor((uint8_t*)cursorData, (uint8_t*)cursorData, 8, 8, 4, 4); + SDL_SetCursor(cursor); + SDL_ShowCursor(SDL_DISABLE); + + // ensure mouse acceleration is identical in server mode + SDL_SetHintWithPriority(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, "1", SDL_HINT_OVERRIDE); + + SDL_Event event; + while(state.running) + { + while(SDL_PollEvent(&event)) + { + switch(event.type) + { + case SDL_QUIT: + state.running = false; + break; + + case SDL_KEYDOWN: + { + SDL_Scancode sc = event.key.keysym.scancode; + if (sc == SDL_SCANCODE_SCROLLLOCK) + { + serverMode = !serverMode; + spice_mouse_mode(serverMode); + SDL_SetRelativeMouseMode(serverMode); + break; + } + + uint32_t scancode = mapScancode(sc); + if (scancode == 0) + break; + + spice_key_down(scancode); + break; + } + + case SDL_KEYUP: + { + SDL_Scancode sc = event.key.keysym.scancode; + if (sc == SDL_SCANCODE_SCROLLLOCK) + break; + + + uint32_t scancode = mapScancode(sc); + if (scancode == 0) + break; + + spice_key_up(scancode); + break; + } + + case SDL_MOUSEWHEEL: + spice_mouse_press (event.wheel.y == 1 ? 4 : 5); + spice_mouse_release(event.wheel.y == 1 ? 4 : 5); + break; + + case SDL_MOUSEMOTION: + if (serverMode) + spice_mouse_motion(event.motion.xrel, event.motion.yrel); + else + spice_mouse_motion( + (int)event.motion.x - mouseX, + (int)event.motion.y - mouseY + ); + + mouseX = event.motion.x; + mouseY = event.motion.y; + break; + + case SDL_MOUSEBUTTONDOWN: + spice_mouse_position(event.button.x, event.button.y); + spice_mouse_press(event.button.button); + break; + + case SDL_MOUSEBUTTONUP: + spice_mouse_position(event.button.x, event.button.y); + spice_mouse_release(event.button.button); + break; + + default: + break; + } + } + + usleep(1000); + } + + SDL_FreeCursor(cursor); + + return 0; +} + +int main(int argc, char * argv[]) +{ + memset(&state, 0, sizeof(state)); + state.running = true; + + int shm_fd = 0; + SDL_Thread *t_spice = NULL; + SDL_Thread *t_event = NULL; + + while(1) + { + umask(0); + const mode_t mode = + S_IRUSR | S_IWUSR | + S_IRGRP | S_IWGRP | + S_IROTH | S_IWOTH; + + if ((shm_fd = shm_open("ivshmem", O_CREAT | O_RDWR, mode)) < 0) + { + DEBUG_ERROR("failed to open shared memory: %d %s", errno, strerror(errno)); + break; + } + + if (ftruncate(shm_fd, MAP_SIZE) != 0) + { + DEBUG_ERROR("failed to truncate memory region"); + break; + } + + if (!spice_connect("127.0.0.1", 5900, "")) + { + DEBUG_ERROR("Failed to connect to spice server"); + return 0; + } + + while(state.running && !spice_ready()) + if (!spice_process()) + { + state.running = false; + DEBUG_ERROR("Failed to process spice messages"); + break; + } + + if (!(t_spice = SDL_CreateThread(spiceThread, "spiceThread", NULL))) + { + DEBUG_ERROR("spice create thread failed"); + break; + } + + state.shm = (struct KVMGFXHeader *)mmap(NULL, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); + if (!state.shm) + { + DEBUG_ERROR("Failed to map memory"); + break; + } + + if (!(t_event = SDL_CreateThread(eventThread, "eventThread", NULL))) + { + DEBUG_ERROR("gpu create thread failed"); + break; + } + + while(state.running) + renderThread(NULL); + + break; + } + + state.running = false; + + if (t_event) + SDL_WaitThread(t_event, NULL); + + if (t_spice) + SDL_WaitThread(t_spice, NULL); + + if (state.renderer) + SDL_DestroyRenderer(state.renderer); + + if (state.window) + SDL_DestroyWindow(state.window); + + if (state.shm) + munmap(state.shm, MAP_SIZE); + + if (shm_fd) + close(shm_fd); + //shm_unlink("kvm-windows"); + + SDL_Quit(); + return 0; +} \ No newline at end of file diff --git a/client/spice-common b/client/spice-common new file mode 160000 index 00000000..739a859d --- /dev/null +++ b/client/spice-common @@ -0,0 +1 @@ +Subproject commit 739a859d79428fe188cd6516508ba013a162a810 diff --git a/client/spice.c b/client/spice.c new file mode 100644 index 00000000..5b641fdc --- /dev/null +++ b/client/spice.c @@ -0,0 +1,787 @@ +#include "spice.h" + +#define DEBUG +#include "debug.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "spice/messages.h" + +// ============================================================================ + +// internal structures +struct SpiceChannel +{ + bool connected; + bool initDone; + uint8_t channelType; + int socket; + uint32_t ackFrequency; + uint32_t ackCount; + uint32_t serial; +}; + +struct SpiceKeyboard +{ + uint32_t modifiers; +}; + +struct SpiceMouse +{ + uint32_t buttonState; +}; + +struct Spice +{ + char password[32]; + struct sockaddr_in addr; + + uint32_t sessionID; + uint32_t channelID; + struct SpiceChannel scMain; + struct SpiceChannel scInputs; + + struct SpiceKeyboard kb; + struct SpiceMouse mouse; +}; + +// globals +struct Spice spice = +{ + .sessionID = 0, + .scMain .connected = false, + .scMain .channelType = SPICE_CHANNEL_MAIN, + .scInputs.connected = false, + .scInputs.channelType = SPICE_CHANNEL_INPUTS, +}; + +// internal forward decls +bool spice_connect_channel (struct SpiceChannel * channel); +void spice_disconnect_channel(struct SpiceChannel * channel); + +bool spice_process_ack(struct SpiceChannel * channel); + +bool spice_on_common_read (struct SpiceChannel * channel, SpiceDataHeader * header, bool * handled); +bool spice_on_main_channel_read (); +bool spice_on_inputs_channel_read(); + +bool spice_read (const struct SpiceChannel * channel, void * buffer, const ssize_t size); +ssize_t spice_write (const struct SpiceChannel * channel, const void * buffer, const ssize_t size); +bool spice_write_msg(struct SpiceChannel * channel, uint32_t type, const void * buffer, const ssize_t size); +bool spice_discard (const struct SpiceChannel * channel, ssize_t size); + + +// ============================================================================ + +bool spice_connect(const char * host, const short port, const char * password) +{ + strncpy(spice.password, password, sizeof(spice.password)); + memset(&spice.addr, 0, sizeof(struct sockaddr_in)); + inet_pton(AF_INET, host, &spice.addr.sin_addr); + spice.addr.sin_family = AF_INET; + spice.addr.sin_port = htons(port); + + spice.channelID = 0; + if (!spice_connect_channel(&spice.scMain)) + { + DEBUG_ERROR("connect main channel failed"); + return false; + } + + return true; +} + +// ============================================================================ + +void spice_disconnect() +{ + spice_disconnect_channel(&spice.scMain ); + spice_disconnect_channel(&spice.scInputs); + + spice.sessionID = 0; +} + +// ============================================================================ + +bool spice_ready() +{ + return spice.scMain.connected && + spice.scInputs.connected; +} + +// ============================================================================ + +bool spice_process() +{ + fd_set readSet; + FD_ZERO(&readSet); + FD_SET(spice.scMain.socket , &readSet); + FD_SET(spice.scInputs.socket, &readSet); + + struct timeval timeout; + timeout.tv_sec = 1; + timeout.tv_usec = 0; + + int rc = select(FD_SETSIZE, &readSet, NULL, NULL, &timeout); + if (rc < 0) + { + DEBUG_ERROR("select failure"); + return false; + } + + for(int i = 0; i < FD_SETSIZE; ++i) + if (FD_ISSET(i, &readSet)) + { + if (i == spice.scMain.socket) + { + if (spice_on_main_channel_read()) + { + if (spice.scMain.connected && !spice_process_ack(&spice.scMain)) + { + DEBUG_ERROR("failed to process ack on main channel"); + return false; + } + continue; + } + else + { + DEBUG_ERROR("failed to perform read on main channel"); + return false; + } + } + + if (spice.scInputs.connected && i == spice.scInputs.socket) + { + if (spice_on_inputs_channel_read()) + { + if (!spice_process_ack(&spice.scInputs)) + { + DEBUG_ERROR("failed to process ack on inputs channel"); + return false; + } + continue; + } + else + { + DEBUG_ERROR("failed to perform read on inputs channel"); + return false; + } + } + } + + return true; +} + +// ============================================================================ + +bool spice_process_ack(struct SpiceChannel * channel) +{ + if (channel->ackFrequency == 0) + return true; + + if (channel->ackCount++ != channel->ackFrequency) + return true; + + channel->ackCount = 0; + return spice_write_msg(channel, SPICE_MSGC_ACK, "\0", 1); +} + +// ============================================================================ + +bool spice_on_common_read(struct SpiceChannel * channel, SpiceDataHeader * header, bool * handled) +{ + if (!spice_read(channel, header, sizeof(SpiceDataHeader))) + { + DEBUG_ERROR("read failure"); + *handled = false; + return false; + } + +#if 0 + printf("socket: %d, serial: %6u, type: %2u, size %6u, sub_list %4u\n", + channel->socket, + header->serial, header->type, header->size, header->sub_list); +#endif + + if (!channel->initDone) + { + *handled = false; + + return true; + } + switch(header->type) + { + case SPICE_MSG_MIGRATE: + case SPICE_MSG_MIGRATE_DATA: + { + DEBUG_PROTO("SPICE_MSG_MIGRATE_DATA"); + + *handled = true; + DEBUG_WARN("migration is not supported"); + return false; + } + + case SPICE_MSG_SET_ACK: + { + DEBUG_INFO("SPICE_MSG_SET_ACK"); + *handled = true; + + SpiceMsgSetAck in; + if (!spice_read(channel, &in, sizeof(in))) + return false; + + channel->ackFrequency = in.window; + + SpiceMsgcAckSync out; + out.generation = in.generation; + if (!spice_write_msg(channel, SPICE_MSGC_ACK_SYNC, &out, sizeof(out))) + return false; + + return true; + } + + case SPICE_MSG_PING: + { + DEBUG_PROTO("SPICE_MSG_PING"); + *handled = true; + + SpiceMsgPing in; + if (!spice_read(channel, &in, sizeof(in))) + return false; + + if (!spice_discard(channel, header->size - sizeof(in))) + { + DEBUG_ERROR("failed discarding enough bytes from the ping packet"); + return false; + } + + SpiceMsgcPong out; + out.id = in.id; + out.timestamp = in.timestamp; + if (!spice_write_msg(channel, SPICE_MSGC_PONG, &out, sizeof(out))) + return false; + + return true; + } + + case SPICE_MSG_WAIT_FOR_CHANNELS: + case SPICE_MSG_DISCONNECTING : + { + *handled = true; + DEBUG_FIXME("ignored wait-for-channels or disconnect message"); + return false; + } + + case SPICE_MSG_NOTIFY: + { + DEBUG_PROTO("SPICE_MSG_NOTIFY"); + SpiceMsgNotify in; + if (!spice_read(channel, &in, sizeof(in))) + return false; + + char msg[in.message_len+1]; + if (!spice_read(channel, msg, in.message_len+1)) + return false; + + DEBUG_INFO("notify message: %s", msg); + *handled = true; + return true; + } + } + + *handled = false; + return true; +} + +// ============================================================================ + +bool spice_on_main_channel_read() +{ + struct SpiceChannel *channel = &spice.scMain; + + SpiceDataHeader header; + bool handled; + + if (!spice_on_common_read(channel, &header, &handled)) + { + DEBUG_ERROR("read failure"); + return false; + } + + if (handled) + return true; + + if (!channel->initDone) + { + if (header.type != SPICE_MSG_MAIN_INIT) + { + spice_disconnect(); + DEBUG_ERROR("expected main init message but got type %u", header.type); + return false; + } + + DEBUG_PROTO("SPICE_MSG_MAIN_INIT"); + + channel->initDone = true; + SpiceMsgMainInit msg; + if (!spice_read(channel, &msg, sizeof(msg))) + { + spice_disconnect(); + return false; + } + + spice.sessionID = msg.session_id; + if (msg.current_mouse_mode != SPICE_MOUSE_MODE_CLIENT && !spice_mouse_mode(false)) + { + DEBUG_ERROR("failed to set mouse mode"); + return false; + } + + if (!spice_connect_channel(&spice.scInputs)) + { + DEBUG_ERROR("failed to connect inputs channel"); + return false; + } + + return true; + } + + DEBUG_WARN("main channel unhandled message type %u", header.type); + spice_discard(channel, header.size); + return true; +} + +// ============================================================================ + +bool spice_on_inputs_channel_read() +{ + struct SpiceChannel *channel = &spice.scInputs; + + SpiceDataHeader header; + bool handled; + + if (!spice_on_common_read(channel, &header, &handled)) + { + DEBUG_ERROR("read failure"); + return false; + } + + if (handled) + return true; + + switch(header.type) + { + case SPICE_MSG_INPUTS_INIT: + { + DEBUG_PROTO("SPICE_MSG_INPUTS_INIT"); + + if (channel->initDone) + { + DEBUG_ERROR("input init message already done"); + return false; + } + + channel->initDone = true; + + SpiceMsgInputsInit in; + if (!spice_read(channel, &in, sizeof(in))) + return false; + + return true; + } + + case SPICE_MSG_INPUTS_KEY_MODIFIERS: + { + DEBUG_PROTO("SPICE_MSG_INPUTS_KEY_MODIFIERS"); + SpiceMsgInputsInit in; + if (!spice_read(channel, &in, sizeof(in))) + return false; + + spice.kb.modifiers = in.modifiers; + return true; + } + + case SPICE_MSG_INPUTS_MOUSE_MOTION_ACK: + { + DEBUG_PROTO("SPICE_MSG_INPUTS_MOUSE_MOTION_ACK"); + return true; + } + } + + DEBUG_WARN("inputs channel unhandled message type %u", header.type); + spice_discard(channel, header.size); + return true; +} + +// ============================================================================ + +bool spice_connect_channel(struct SpiceChannel * channel) +{ + channel->initDone = false; + channel->ackFrequency = 0; + channel->ackCount = 0; + channel->serial = 0; + + channel->socket = socket(AF_INET, SOCK_STREAM, 0); + if (channel->socket == -1) + return false; + + int flag = 1; + setsockopt(channel->socket, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(int)); + + if (connect(channel->socket, (const struct sockaddr *)&spice.addr, sizeof(spice.addr)) == -1) + { + DEBUG_ERROR("socket connect failure"); + close(channel->socket); + return false; + } + channel->connected = true; + + SpiceLinkHeader header = + { + .magic = SPICE_MAGIC , + .major_version = SPICE_VERSION_MAJOR, + .minor_version = SPICE_VERSION_MINOR, + .size = sizeof(SpiceLinkMess) + }; + + SpiceLinkMess message = + { + .connection_id = spice.sessionID, + .channel_type = channel->channelType, + .channel_id = spice.channelID, + .num_common_caps = 0, + .num_channel_caps = 0, + .caps_offset = sizeof(SpiceLinkMess) + }; + + spice_write(channel, &header , sizeof(header )); + spice_write(channel, &message, sizeof(message)); + + if (!spice_read(channel, &header, sizeof(header))) + { + DEBUG_ERROR("failed to read SpiceLinkHeader"); + spice_disconnect_channel(channel); + return false; + } + + if (header.magic != SPICE_MAGIC || header.major_version != SPICE_VERSION_MAJOR) + { + DEBUG_ERROR("invalid or unsupported protocol version"); + spice_disconnect_channel(channel); + return false; + } + + if (header.size < sizeof(SpiceLinkReply)) + { + DEBUG_ERROR("reported data size too small"); + spice_disconnect_channel(channel); + return false; + } + + SpiceLinkReply reply; + if (!spice_read(channel, &reply, sizeof(reply))) + { + DEBUG_ERROR("failed to read SpiceLinkReply"); + spice_disconnect_channel(channel); + return false; + } + + if (reply.error != SPICEC_ERROR_CODE_SUCCESS) + { + DEBUG_ERROR("server replied with error %u", reply.error); + spice_disconnect_channel(channel); + return false; + } + + uint32_t capsCommon [reply.num_common_caps ]; + uint32_t capsChannel[reply.num_channel_caps]; + spice_read(channel, &capsCommon , sizeof(capsCommon )); + spice_read(channel, &capsChannel, sizeof(capsChannel)); + + BIO *bioKey = BIO_new(BIO_s_mem()); + if (!bioKey) + { + DEBUG_ERROR("failed to allocate bioKey"); + spice_disconnect_channel(channel); + return false; + } + + BIO_write(bioKey, reply.pub_key, SPICE_TICKET_PUBKEY_BYTES); + EVP_PKEY *rsaKey = d2i_PUBKEY_bio(bioKey, NULL); + RSA *rsa = EVP_PKEY_get1_RSA(rsaKey); + + char enc[RSA_size(rsa)]; + if (RSA_public_encrypt( + strlen(spice.password) + 1, + (uint8_t*)spice.password, + (uint8_t*)enc, + rsa, + RSA_PKCS1_OAEP_PADDING + ) <= 0) + { + DEBUG_ERROR("rsa public encrypt failed"); + spice_disconnect_channel(channel); + EVP_PKEY_free(rsaKey); + BIO_free(bioKey); + return false; + } + + ssize_t rsaSize = RSA_size(rsa); + EVP_PKEY_free(rsaKey); + BIO_free(bioKey); + + if (!spice_write(channel, enc, rsaSize)) + { + DEBUG_ERROR("failed to write encrypted data"); + spice_disconnect_channel(channel); + return false; + } + + uint32_t linkResult; + if (!spice_read(channel, &linkResult, sizeof(linkResult))) + { + DEBUG_ERROR("failed to read SpiceLinkResult"); + spice_disconnect_channel(channel); + return false; + } + + if (linkResult != SPICE_LINK_ERR_OK) + { + DEBUG_ERROR("connect code error %u", linkResult); + spice_disconnect_channel(channel); + return false; + } + + return true; +} + +// ============================================================================ + +void spice_disconnect_channel(struct SpiceChannel * channel) +{ + if (channel->connected) + close(channel->socket); + channel->connected = false; +} + +// ============================================================================ + +ssize_t spice_write(const struct SpiceChannel * channel, const void * buffer, const ssize_t size) +{ + if (!channel->connected) + { + DEBUG_ERROR("not connected"); + return -1; + } + + if (!buffer) + { + DEBUG_ERROR("invalid buffer argument supplied"); + return -1; + } + + ssize_t len = send(channel->socket, buffer, size, 0); + if (len != size) + DEBUG_WARN("incomplete write"); + + return len; +} + +// ============================================================================ + +bool spice_write_msg(struct SpiceChannel * channel, uint32_t type, const void * buffer, const ssize_t size) +{ + SpiceDataHeader header; + header.serial = channel->serial++; + header.type = type; + header.size = size; + header.sub_list = 0; + + if (spice_write(channel, &header, sizeof(header)) != sizeof(header)) + { + DEBUG_ERROR("failed to write message header"); + return false; + } + + if (spice_write(channel, buffer, size) != size) + { + DEBUG_ERROR("failed to write message body"); + return false; + } + + return true; +} + +// ============================================================================ + +bool spice_read(const struct SpiceChannel * channel, void * buffer, const ssize_t size) +{ + if (!channel->connected) + { + DEBUG_ERROR("not connected"); + return false; + } + + if (!buffer) + { + DEBUG_ERROR("invalid buffer argument supplied"); + return false; + } + + ssize_t len = read(channel->socket, buffer, size); + if (len != size) + { + DEBUG_ERROR("incomplete write"); + return false; + } + + return true; +} + +// ============================================================================ + +bool spice_discard(const struct SpiceChannel * channel, ssize_t size) +{ + while(size) + { + char c[8192]; + size_t len = read(channel->socket, c, size > sizeof(c) ? sizeof(c) : size); + if (len <= 0) + return false; + + size -= len; + } + return true; +} + +// ============================================================================ + +bool spice_key_down(uint32_t code) +{ + DEBUG_PROTO("%u", code); + + if (code > 0x100) + code = 0xe0 | ((code - 0x100) << 8); + + SpiceMsgcKeyDown msg; + msg.code = code; + + return spice_write_msg(&spice.scInputs, SPICE_MSGC_INPUTS_KEY_DOWN, &msg, sizeof(msg)); +} + +// ============================================================================ + +bool spice_key_up(uint32_t code) +{ + DEBUG_PROTO("%u", code); + + if (code < 0x100) + code |= 0x80; + else + code = 0x80e0 | ((code - 0x100) << 8); + + SpiceMsgcKeyDown msg; + msg.code = code; + + return spice_write_msg(&spice.scInputs, SPICE_MSGC_INPUTS_KEY_UP, &msg, sizeof(msg)); +} + +// ============================================================================ + +bool spice_mouse_mode(bool server) +{ + DEBUG_PROTO("%s", server ? "server" : "client"); + + SpiceMsgcMainMouseModeRequest msg; + msg.mouse_mode = server ? SPICE_MOUSE_MODE_SERVER : SPICE_MOUSE_MODE_CLIENT; + + return spice_write_msg(&spice.scMain, SPICE_MSGC_MAIN_MOUSE_MODE_REQUEST, &msg, sizeof(msg)); +} + +// ============================================================================ + +bool spice_mouse_position(uint32_t x, uint32_t y) +{ + DEBUG_PROTO("x=%u, y=%u", x, y); + + SpiceMsgcMousePosition msg; + msg.x = x; + msg.y = y; + msg.button_state = spice.mouse.buttonState; + msg.display_id = 0; + + return spice_write_msg(&spice.scInputs, SPICE_MSGC_INPUTS_MOUSE_POSITION, &msg, sizeof(msg)); +} + +// ============================================================================ + +bool spice_mouse_motion(int32_t x, int32_t y) +{ + DEBUG_PROTO("x=%d, y=%d", x, y); + + SpiceMsgcMouseMotion msg; + msg.x = x; + msg.y = y; + msg.button_state = spice.mouse.buttonState; + + return spice_write_msg(&spice.scInputs, SPICE_MSGC_INPUTS_MOUSE_MOTION, &msg, sizeof(msg)); +} + +// ============================================================================ + +bool spice_mouse_press(uint32_t button) +{ + DEBUG_PROTO("%u", button); + + switch(button) + { + case SPICE_MOUSE_BUTTON_LEFT : spice.mouse.buttonState |= SPICE_MOUSE_BUTTON_MASK_LEFT ; break; + case SPICE_MOUSE_BUTTON_MIDDLE: spice.mouse.buttonState |= SPICE_MOUSE_BUTTON_MASK_MIDDLE; break; + case SPICE_MOUSE_BUTTON_RIGHT : spice.mouse.buttonState |= SPICE_MOUSE_BUTTON_MASK_RIGHT ; break; + } + + SpiceMsgcMousePress msg; + msg.button = button; + msg.button_state = spice.mouse.buttonState; + + return spice_write_msg(&spice.scInputs, SPICE_MSGC_INPUTS_MOUSE_PRESS, &msg, sizeof(msg)); +} + +// ============================================================================ + +bool spice_mouse_release(uint32_t button) +{ + DEBUG_PROTO("%u", button); + + switch(button) + { + case SPICE_MOUSE_BUTTON_LEFT : spice.mouse.buttonState &= ~SPICE_MOUSE_BUTTON_MASK_LEFT ; break; + case SPICE_MOUSE_BUTTON_MIDDLE: spice.mouse.buttonState &= ~SPICE_MOUSE_BUTTON_MASK_MIDDLE; break; + case SPICE_MOUSE_BUTTON_RIGHT : spice.mouse.buttonState &= ~SPICE_MOUSE_BUTTON_MASK_RIGHT ; break; + } + + SpiceMsgcMouseRelease msg; + msg.button = button; + msg.button_state = spice.mouse.buttonState; + + return spice_write_msg(&spice.scInputs, SPICE_MSGC_INPUTS_MOUSE_RELEASE, &msg, sizeof(msg)); +} \ No newline at end of file diff --git a/client/spice.h b/client/spice.h new file mode 100644 index 00000000..f2f61ddf --- /dev/null +++ b/client/spice.h @@ -0,0 +1,16 @@ +#include +#include +#include + +bool spice_connect(const char * host, const short port, const char * password); +void spice_disconnect(); +bool spice_process(); +bool spice_ready(); + +bool spice_key_down (uint32_t code); +bool spice_key_up (uint32_t code); +bool spice_mouse_mode (bool server); +bool spice_mouse_position(uint32_t x, uint32_t y); +bool spice_mouse_motion ( int32_t x, int32_t y); +bool spice_mouse_press (uint32_t button); +bool spice_mouse_release (uint32_t button); \ No newline at end of file diff --git a/client/spice/messages.h b/client/spice/messages.h new file mode 100644 index 00000000..17eb6e68 --- /dev/null +++ b/client/spice/messages.h @@ -0,0 +1,102 @@ +#include + +#pragma pack(push,1) + +typedef struct SpicePoint16 +{ + int16_t x, y; +} +SpicePoint16; + +typedef struct SpiceMsgMainInit +{ + uint32_t session_id; + uint32_t display_channels_hint; + uint32_t supported_mouse_modes; + uint32_t current_mouse_mode; + uint32_t agent_connected; + uint32_t agent_tokens; + uint32_t multi_media_time; + uint32_t ram_hint; +} +SpiceMsgMainInit; + +typedef struct SpiceMsgcMainMouseModeRequest +{ + uint16_t mouse_mode; +} +SpiceMsgcMainMouseModeRequest; + +typedef struct SpiceMsgPing +{ + uint32_t id; + uint64_t timestamp; +} +SpiceMsgPing, +SpiceMsgcPong; + +typedef struct SpiceMsgSetAck +{ + uint32_t generation; + uint32_t window; +} +SpiceMsgSetAck; + +typedef struct SpiceMsgcAckSync +{ + uint32_t generation; +} +SpiceMsgcAckSync; + +typedef struct SpiceMsgNotify +{ + uint64_t time_stamp; + uint32_t severity; + uint32_t visibility; + uint32_t what; + uint32_t message_len; + //char message[message_len+1] +} +SpiceMsgNotify; + +typedef struct SpiceMsgInputsInit +{ + uint16_t modifiers; +} +SpiceMsgInputsInit, +SpiceMsgInputsKeyModifiers, +SpiceMsgcInputsKeyModifiers; + +typedef struct SpiceMsgcKeyDown +{ + uint32_t code; +} +SpiceMsgcKeyDown, +SpiceMsgcKeyUp; + +typedef struct SpiceMsgcMousePosition +{ + uint32_t x; + uint32_t y; + uint16_t button_state; + uint8_t display_id; +} +SpiceMsgcMousePosition; + +typedef struct SpiceMsgcMouseMotion +{ + int32_t x; + int32_t y; + uint16_t button_state; +} +SpiceMsgcMouseMotion; + +typedef struct SpiceMsgcMousePress +{ + uint8_t button; + uint16_t button_state; +} +SpiceMsgcMousePress, +SpiceMsgcMouseRelease; + +#pragma pack(pop) \ No newline at end of file diff --git a/server/TODO b/server/TODO new file mode 100644 index 00000000..e69de29b