mirror of
https://github.com/amitbet/vncproxy.git
synced 2025-10-24 13:21:00 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02dde77daa | ||
|
|
b46f708726 | ||
|
|
f298567976 | ||
|
|
8543af35a4 | ||
|
|
f05c96d449 | ||
|
|
37e11cd72e | ||
|
|
47fefaacb6 | ||
|
|
7852f11ceb | ||
|
|
e06ad806db | ||
|
|
9fff2b46aa | ||
|
|
aff3a20d4a | ||
|
|
6f8d591789 | ||
|
|
c75e9ccc9c | ||
|
|
b448d0235a | ||
|
|
57fdfe914d | ||
|
|
538037aa8f |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -2,3 +2,8 @@
|
||||
*.debug
|
||||
*.test
|
||||
debug
|
||||
/dist
|
||||
vncproxy
|
||||
prox
|
||||
cmd
|
||||
aa
|
||||
41
README.md
41
README.md
@@ -1,16 +1,14 @@
|
||||
# VncProxy
|
||||
An RFB proxy, written in go that can save and replay FBS files
|
||||
* supports all modern encodings & most useful pseudo-encodings
|
||||
* supports multiple VNC client connections & multi servers (chosen by sessionId)
|
||||
* supports being a "websockify" proxy (for web clients like NoVnc)
|
||||
* produces FBS files compatible with tightvnc player (while using tight's default 3Byte color format)
|
||||
* can also be used as:
|
||||
* a screen recorder vnc-client
|
||||
* a replay server to show fbs recordings to connecting clients
|
||||
|
||||
**This is still a work in progress, and requires some error handling and general tidying up,
|
||||
but the code is already working (see main, proxy_test & player_test)**
|
||||
- tested on tight encoding with:
|
||||
* Supports all modern encodings & most useful pseudo-encodings
|
||||
* Supports multiple VNC client connections & multi servers (chosen by sessionId)
|
||||
* Supports being a "websockify" proxy (for web clients like NoVnc)
|
||||
* Produces FBS files compatible with tightvnc player (while using tight's default 3Byte color format)
|
||||
* Can also be used as:
|
||||
* A screen recorder vnc-client
|
||||
* A replay server to show fbs recordings to connecting clients
|
||||
|
||||
- Tested on tight encoding with:
|
||||
- Tightvnc (client + java client + server)
|
||||
- FBS player (tightVnc Java player)
|
||||
- NoVnc(web client)
|
||||
@@ -18,13 +16,20 @@ but the code is already working (see main, proxy_test & player_test)**
|
||||
- VineVnc(server)
|
||||
- TigerVnc(client)
|
||||
|
||||
## Usage
|
||||
**Some very usable cmdline executables are Coming Soon...**
|
||||
|
||||
Code samples can be found by looking at:
|
||||
* main.go (fbs recording vnc client)
|
||||
* Connects, records to FBS file
|
||||
* Programmed to quit after 10 seconds
|
||||
### Executables (see releases)
|
||||
* proxy - the actual recording proxy, supports listening to tcp & ws ports and recording traffic to fbs files
|
||||
* recorder - connects to a vnc server as a client and records the screen
|
||||
* player - a toy player that will replay a given fbs file to all incoming connections
|
||||
|
||||
## Usage:
|
||||
recorder -recDir=./recording.rbs -targHost=192.168.0.100 -targPort=5903 -targPass=@@@@@
|
||||
player -fbsFile=./myrec.fbs -tcpPort=5905
|
||||
proxy -recDir=./recordings/ -targHost=192.168.0.100 -targPort=5903 -targPass=@@@@@ -tcpPort=5903 -vncPass=@!@!@!
|
||||
|
||||
### Code usage examples
|
||||
* player/main.go (fbs recording vnc client)
|
||||
* Connects as client, records to FBS file
|
||||
* proxy/proxy_test.go (vnc proxy with recording)
|
||||
* Listens to both Tcp and WS ports
|
||||
* Proxies connections to a hard-coded localhost vnc server
|
||||
@@ -56,4 +61,4 @@ The Recorder uses channels and runs in parallel to avoid hampering the communica
|
||||
|
||||

|
||||
|
||||
The code is based on several implementations of go-vnc including the original one by *Mitchell Hashimoto*, and the recentely active fork which belongs to *Vasiliy Tolstov*.
|
||||
The code is based on several implementations of go-vnc including the original one by *Mitchell Hashimoto*, and the recentely active fork by *Vasiliy Tolstov*.
|
||||
|
||||
52
build.sh
52
build.sh
@@ -1,4 +1,48 @@
|
||||
export GOOS="darwin"
|
||||
go build -o ./dist/recorder ./recorder/cmd
|
||||
go build -o ./dist/player ./player/cmd
|
||||
go build -o ./dist/proxy ./proxy/cmd
|
||||
#!/bin/bash
|
||||
sum="sha1sum"
|
||||
|
||||
# VERSION=`date -u +%Y%m%d`
|
||||
VERSION="v1.01"
|
||||
LDFLAGS="-X main.VERSION=$VERSION -s -w"
|
||||
GCFLAGS=""
|
||||
|
||||
if ! hash sha1sum 2>/dev/null; then
|
||||
if ! hash shasum 2>/dev/null; then
|
||||
echo "I can't see 'sha1sum' or 'shasum'"
|
||||
echo "Please install one of them!"
|
||||
exit
|
||||
fi
|
||||
sum="shasum"
|
||||
fi
|
||||
|
||||
UPX=false
|
||||
if hash upx 2>/dev/null; then
|
||||
UPX=true
|
||||
fi
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
OSES=( linux darwin windows )
|
||||
ARCHS=(amd64 386 )
|
||||
for os in ${OSES[@]}; do
|
||||
for arch in ${ARCHS[@]}; do
|
||||
suffix=""
|
||||
if [ "$os" == "windows" ]
|
||||
then
|
||||
suffix=".exe"
|
||||
fi
|
||||
|
||||
env CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -ldflags "$LDFLAGS" -gcflags "$GCFLAGS" -o ./dist/${os}_${arch}/recorder${suffix} ./recorder/cmd
|
||||
env CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -ldflags "$LDFLAGS" -gcflags "$GCFLAGS" -o ./dist/${os}_${arch}/player${suffix} ./player/cmd
|
||||
env CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -ldflags "$LDFLAGS" -gcflags "$GCFLAGS" -o ./dist/${os}_${arch}/proxy${suffix} ./proxy/cmd
|
||||
|
||||
if $UPX; then upx -9 client_${os}_${arch}${suffix} server_${os}_${arch}${suffix};fi
|
||||
# tar -zcf ./dist/vncproxy-${os}-${arch}-$VERSION.tar.gz ./dist/${os}_${arch}/proxy${suffix} ./dist/${os}_${arch}/player${suffix} ./dist/${os}_${arch}/recorder${suffix}
|
||||
cd dist/${os}_${arch}/
|
||||
zip -D -q -r ../vncproxy-${os}-${arch}-$VERSION.zip proxy${suffix} player${suffix} recorder${suffix}
|
||||
cd ../..
|
||||
$sum ./dist/vncproxy-${os}-${arch}-$VERSION.zip
|
||||
done
|
||||
done
|
||||
|
||||
@@ -116,9 +116,9 @@ func (c *ClientConn) Read(bytes []byte) (n int, err error) {
|
||||
return c.conn.Read(bytes)
|
||||
}
|
||||
|
||||
func (c *ClientConn) CurrentColorMap() *common.ColorMap {
|
||||
return &c.ColorMap
|
||||
}
|
||||
// func (c *ClientConn) CurrentColorMap() *common.ColorMap {
|
||||
// return &c.ColorMap
|
||||
// }
|
||||
|
||||
// CutText tells the server that the client has new text in its cut buffer.
|
||||
// The text string MUST only contain Latin-1 characters. This encoding
|
||||
|
||||
@@ -175,9 +175,9 @@ func (m *MsgSetColorMapEntries) Read(c common.IClientConn, r *common.RfbReadHelp
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
cmap := c.CurrentColorMap()
|
||||
// Update the connection's color map
|
||||
cmap[result.FirstColor+i] = *color
|
||||
// cmap := c.CurrentColorMap()
|
||||
// // Update the connection's color map
|
||||
// cmap[result.FirstColor+i] = *color
|
||||
}
|
||||
r.SendMessageEnd(common.ServerMessageType(m.Type()))
|
||||
return &result, nil
|
||||
|
||||
@@ -17,7 +17,8 @@ const (
|
||||
KeyEventMsgType
|
||||
PointerEventMsgType
|
||||
ClientCutTextMsgType
|
||||
ClientFenceMsgType = 248
|
||||
ClientFenceMsgType = 248
|
||||
QEMUExtendedKeyEventMsgType = 255
|
||||
)
|
||||
|
||||
// Color represents a single color in a color map.
|
||||
@@ -47,6 +48,8 @@ func (cmt ClientMessageType) String() string {
|
||||
return "FramebufferUpdateRequest"
|
||||
case KeyEventMsgType:
|
||||
return "KeyEvent"
|
||||
case QEMUExtendedKeyEventMsgType:
|
||||
return "QEMUExtendedKeyEvent"
|
||||
case PointerEventMsgType:
|
||||
return "PointerEvent"
|
||||
case ClientCutTextMsgType:
|
||||
|
||||
@@ -25,6 +25,6 @@ type IServerConn interface {
|
||||
|
||||
type IClientConn interface {
|
||||
CurrentPixelFormat() *PixelFormat
|
||||
CurrentColorMap() *ColorMap
|
||||
//CurrentColorMap() *ColorMap
|
||||
Encodings() []IEncoding
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ func handleTightFilters(subencoding uint8, pixelFmt *common.PixelFormat, rect *c
|
||||
if paletteSize == 2 {
|
||||
dataLength = int(rect.Height) * ((int(rect.Width) + 7) / 8)
|
||||
} else {
|
||||
dataLength = int(rect.Width * rect.Height)
|
||||
dataLength = int(rect.Width) * int(rect.Height)
|
||||
}
|
||||
_, err = r.ReadTightData(dataLength)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,7 +2,7 @@ package logger
|
||||
|
||||
import "fmt"
|
||||
|
||||
var simpleLogger = SimpleLogger{LogLevelInfo}
|
||||
var simpleLogger = SimpleLogger{LogLevelWarn}
|
||||
|
||||
type Logger interface {
|
||||
Debug(v ...interface{})
|
||||
|
||||
@@ -80,7 +80,7 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
url := "http://localhost:" + *wsPort + "/"
|
||||
url := "http://0.0.0.0:" + *wsPort + "/"
|
||||
|
||||
if *tcpPort != "" && *wsPort != "" {
|
||||
logger.Infof("running two listeners: tcp port: %s, ws url: %s", *tcpPort, url)
|
||||
|
||||
@@ -3,17 +3,25 @@ package player
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"io"
|
||||
"time"
|
||||
"vncproxy/client"
|
||||
"vncproxy/common"
|
||||
|
||||
"vncproxy/logger"
|
||||
"vncproxy/server"
|
||||
)
|
||||
|
||||
type VncStreamFileReader interface {
|
||||
io.Reader
|
||||
CurrentTimestamp() int
|
||||
ReadStartSession() (*common.ServerInit, error)
|
||||
CurrentPixelFormat() *common.PixelFormat
|
||||
Encodings() []common.IEncoding
|
||||
}
|
||||
|
||||
type FBSPlayListener struct {
|
||||
Conn *server.ServerConn
|
||||
Fbs *FbsReader
|
||||
Fbs VncStreamFileReader
|
||||
serverMessageMap map[uint8]common.ServerMessage
|
||||
firstSegDone bool
|
||||
startTime int
|
||||
@@ -88,7 +96,7 @@ func (h *FBSPlayListener) sendFbsMessage() {
|
||||
return
|
||||
}
|
||||
timeSinceStart := int(time.Now().UnixNano()/int64(time.Millisecond)) - h.startTime
|
||||
timeToSleep := fbs.currentTimestamp - timeSinceStart
|
||||
timeToSleep := fbs.CurrentTimestamp() - timeSinceStart
|
||||
if timeToSleep > 0 {
|
||||
time.Sleep(time.Duration(timeToSleep) * time.Millisecond)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ type FbsReader struct {
|
||||
encodings []common.IEncoding
|
||||
}
|
||||
|
||||
func (fbs *FbsReader) CurrentTimestamp() int {
|
||||
return fbs.currentTimestamp
|
||||
}
|
||||
|
||||
func (fbs *FbsReader) Read(p []byte) (n int, err error) {
|
||||
if fbs.buffer.Len() < len(p) {
|
||||
seg, err := fbs.ReadSegment()
|
||||
@@ -33,8 +37,9 @@ func (fbs *FbsReader) Read(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (fbs *FbsReader) CurrentPixelFormat() *common.PixelFormat { return fbs.pixelFormat }
|
||||
func (fbs *FbsReader) CurrentColorMap() *common.ColorMap { return &common.ColorMap{} }
|
||||
func (fbs *FbsReader) Encodings() []common.IEncoding { return fbs.encodings }
|
||||
|
||||
//func (fbs *FbsReader) CurrentColorMap() *common.ColorMap { return &common.ColorMap{} }
|
||||
func (fbs *FbsReader) Encodings() []common.IEncoding { return fbs.encodings }
|
||||
|
||||
func NewFbsReader(fbsFile string) (*FbsReader, error) {
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ func main() {
|
||||
var vncPass = flag.String("vncPass", "", "password on incoming vnc connections to the proxy, defaults to no password")
|
||||
var recordDir = flag.String("recDir", "", "path to save FBS recordings WILL NOT RECORD if not defined.")
|
||||
var targetVncPort = flag.String("targPort", "", "target vnc server port")
|
||||
var targetVncHost = flag.String("targHost", "", "target vnc server host")
|
||||
var targetVncPass = flag.String("targPass", "", "target vnc password")
|
||||
|
||||
flag.Parse()
|
||||
@@ -35,13 +36,18 @@ func main() {
|
||||
logger.Warn("FBS recording is turned off")
|
||||
}
|
||||
|
||||
tcpUrl := ""
|
||||
if *tcpPort != "" {
|
||||
tcpUrl = ":" + string(*tcpPort)
|
||||
}
|
||||
|
||||
proxy := &proxy.VncProxy{
|
||||
WsListeningUrl: "http://localhost:" + string(*wsPort) + "/", // empty = not listening on ws
|
||||
RecordingDir: *recordDir, //"/Users/amitbet/vncRec", // empty = no recording
|
||||
TcpListeningUrl: ":" + string(*tcpPort),
|
||||
WsListeningUrl: "http://0.0.0.0:" + string(*wsPort) + "/", // empty = not listening on ws
|
||||
RecordingDir: *recordDir, //"/Users/amitbet/vncRec", // empty = no recording
|
||||
TcpListeningUrl: tcpUrl,
|
||||
ProxyVncPassword: *vncPass, //empty = no auth
|
||||
SingleSession: &proxy.VncSession{
|
||||
TargetHostname: "localhost",
|
||||
TargetHostname: *targetVncHost,
|
||||
TargetPort: *targetVncPort,
|
||||
TargetPassword: *targetVncPass, //"vncPass",
|
||||
ID: "dummySession",
|
||||
|
||||
@@ -62,7 +62,7 @@ func (vp *VncProxy) getProxySession(sessionId string) (*VncSession, error) {
|
||||
}
|
||||
|
||||
func (vp *VncProxy) newServerConnHandler(cfg *server.ServerConfig, sconn *server.ServerConn) error {
|
||||
|
||||
var err error
|
||||
session, err := vp.getProxySession(sconn.SessionId)
|
||||
if err != nil {
|
||||
logger.Errorf("Proxy.newServerConnHandler can't get session: %d", sconn.SessionId)
|
||||
@@ -70,10 +70,11 @@ func (vp *VncProxy) newServerConnHandler(cfg *server.ServerConfig, sconn *server
|
||||
}
|
||||
|
||||
var rec *listeners.Recorder
|
||||
|
||||
if session.Type == SessionTypeRecordingProxy {
|
||||
recFile := "recording" + strconv.FormatInt(time.Now().Unix(), 10) + ".rbs"
|
||||
recPath := path.Join(vp.RecordingDir, recFile)
|
||||
rec, err := listeners.NewRecorder(recPath)
|
||||
rec, err = listeners.NewRecorder(recPath)
|
||||
if err != nil {
|
||||
logger.Errorf("Proxy.newServerConnHandler can't open recorder save path: %s", recPath)
|
||||
return err
|
||||
@@ -173,7 +174,7 @@ func (vp *VncProxy) StartListening() {
|
||||
logger.Infof("running two listeners: tcp port: %s, ws url: %s", vp.TcpListeningUrl, vp.WsListeningUrl)
|
||||
|
||||
go server.WsServe(vp.WsListeningUrl, cfg)
|
||||
server.TcpServe(":"+vp.TcpListeningUrl, cfg)
|
||||
server.TcpServe(vp.TcpListeningUrl, cfg)
|
||||
}
|
||||
|
||||
if vp.WsListeningUrl != "" {
|
||||
|
||||
@@ -6,15 +6,15 @@ func TestProxy(t *testing.T) {
|
||||
//create default session if required
|
||||
|
||||
proxy := &VncProxy{
|
||||
WsListeningUrl: "http://localhost:7777/", // empty = not listening on ws
|
||||
RecordingDir: "/Users/amitbet/vncRec", // empty = no recording
|
||||
WsListeningUrl: "http://0.0.0.0:7778/", // empty = not listening on ws
|
||||
RecordingDir: "d:\\", // empty = no recording
|
||||
TcpListeningUrl: ":5904",
|
||||
//recordingDir: "C:\\vncRec", // empty = no recording
|
||||
//RecordingDir: "C:\\vncRec", // empty = no recording
|
||||
ProxyVncPassword: "1234", //empty = no auth
|
||||
SingleSession: &VncSession{
|
||||
TargetHostname: "localhost",
|
||||
TargetPort: "5903",
|
||||
TargetPassword: "Ch_#!T@8",
|
||||
TargetHostname: "192.168.1.101",
|
||||
TargetPort: "5901",
|
||||
TargetPassword: "123456",
|
||||
ID: "dummySession",
|
||||
Status: SessionStatusInit,
|
||||
Type: SessionTypeRecordingProxy,
|
||||
|
||||
@@ -19,9 +19,16 @@ func main() {
|
||||
var recordDir = flag.String("recDir", "", "path to save FBS recordings WILL NOT RECORD IF EMPTY.")
|
||||
var targetVncPort = flag.String("targPort", "", "target vnc server port")
|
||||
var targetVncPass = flag.String("targPass", "", "target vnc password")
|
||||
var targetVncHost = flag.String("targHost", "localhost", "target vnc hostname")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *targetVncHost == "" {
|
||||
logger.Error("no target vnc server host defined")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *targetVncPort == "" {
|
||||
logger.Error("no target vnc server port defined")
|
||||
flag.Usage()
|
||||
@@ -36,7 +43,7 @@ func main() {
|
||||
}
|
||||
|
||||
//nc, err := net.Dial("tcp", "192.168.1.101:5903")
|
||||
nc, err := net.Dial("tcp", "localhost:"+*targetVncPort)
|
||||
nc, err := net.Dial("tcp", *targetVncHost+":"+*targetVncPort)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("error connecting to vnc server: %s", err)
|
||||
@@ -74,9 +81,9 @@ func main() {
|
||||
encs := []common.IEncoding{
|
||||
&encodings.TightEncoding{},
|
||||
//&encodings.TightPngEncoding{},
|
||||
//rre := encodings.RREEncoding{},
|
||||
//zlib := encodings.ZLibEncoding{},
|
||||
//zrle := encodings.ZRLEEncoding{},
|
||||
//&encodings.RREEncoding{},
|
||||
//&encodings.ZLibEncoding{},
|
||||
//&encodings.ZRLEEncoding{},
|
||||
//&encodings.CopyRectEncoding{},
|
||||
//coRRE := encodings.CoRREEncoding{},
|
||||
//hextile := encodings.HextileEncoding{},
|
||||
|
||||
@@ -38,7 +38,7 @@ func NewRecorder(saveFilePath string) (*Recorder, error) {
|
||||
|
||||
rec.maxWriteSize = 65535
|
||||
|
||||
rec.writer, err = os.OpenFile(saveFilePath, os.O_RDWR|os.O_CREATE, 0755)
|
||||
rec.writer, err = os.OpenFile(saveFilePath, os.O_RDWR|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
logger.Errorf("unable to open file: %s, error: %v", saveFilePath, err)
|
||||
return nil, err
|
||||
|
||||
@@ -165,6 +165,36 @@ func (msg *MsgKeyEvent) Write(c io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MsgKeyEvent holds the wire format message.
|
||||
type MsgQEMUExtKeyEvent struct {
|
||||
SubmessageType uint8 // submessage type
|
||||
DownFlag uint16 // down-flag
|
||||
KeySym Key // key symbol
|
||||
KeyCode uint32 // scan code
|
||||
}
|
||||
|
||||
func (*MsgQEMUExtKeyEvent) Type() common.ClientMessageType {
|
||||
return common.QEMUExtendedKeyEventMsgType
|
||||
}
|
||||
|
||||
func (*MsgQEMUExtKeyEvent) Read(c io.Reader) (common.ClientMessage, error) {
|
||||
msg := MsgKeyEvent{}
|
||||
if err := binary.Read(c, binary.BigEndian, &msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
func (msg *MsgQEMUExtKeyEvent) Write(c io.Writer) error {
|
||||
if err := binary.Write(c, binary.BigEndian, msg.Type()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := binary.Write(c, binary.BigEndian, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PointerEventMessage holds the wire format message.
|
||||
type MsgPointerEvent struct {
|
||||
Mask uint8 // button-mask
|
||||
|
||||
9
todo.md
9
todo.md
@@ -6,4 +6,11 @@
|
||||
* code stuff:
|
||||
* move encodings to be on the framebufferupdate message object
|
||||
* clear all messages read functions from updating stuff, move modification logic to another listener
|
||||
* message read function should accept only an io.Reader, move read helper logic (readuint8) to an actual helper class
|
||||
* message read function should accept only an io.Reader, move read helper logic (readuint8) to an actual helper class
|
||||
* new recording format:
|
||||
* rfb extension
|
||||
* save FBResponse messages with additional fields
|
||||
* timestamp
|
||||
* is incremental
|
||||
* size (bytes)
|
||||
* have a header which contains an index of messages, holding timestamps & file positions for seeking
|
||||
|
||||
Reference in New Issue
Block a user