commit 202985097e375d6b1b10407295575756d9825944 Author: Geoffrey McRae Date: Thu Oct 19 15:15:49 2017 +1100 Initial import of project to git 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 00000000..6af428ba Binary files /dev/null and b/client/main differ 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