mirror of
https://github.com/amitbet/vncproxy.git
synced 2026-07-14 22:14:39 +00:00
Compare commits
12 Commits
1.0
...
test_novnc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2e945c2d7 | ||
|
|
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
|
||||
37
README.md
37
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,16 @@ 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...**
|
||||
## Usage:
|
||||
|
||||
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
|
||||
|
||||
### 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 +57,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.0"
|
||||
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 freebsd)
|
||||
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
|
||||
|
||||
@@ -25,6 +25,6 @@ type IServerConn interface {
|
||||
|
||||
type IClientConn interface {
|
||||
CurrentPixelFormat() *PixelFormat
|
||||
CurrentColorMap() *ColorMap
|
||||
//CurrentColorMap() *ColorMap
|
||||
Encodings() []IEncoding
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"io"
|
||||
"math"
|
||||
"vncproxy/common"
|
||||
"vncproxy/logger"
|
||||
)
|
||||
|
||||
type EncCursorPseudo struct {
|
||||
@@ -21,6 +22,8 @@ func (pe *EncCursorPseudo) Read(pf *common.PixelFormat, rect *common.Rectangle,
|
||||
}
|
||||
|
||||
bytesPixel := int(pf.BPP / 8) //calcTightBytePerPixel(pf)
|
||||
logger.Infof("EncCursorPseudo bytesPerPixel= %d", bytesPixel)
|
||||
|
||||
r.ReadBytes(int(rect.Width*rect.Height) * bytesPixel)
|
||||
mask := ((rect.Width + 7) / 8) * rect.Height
|
||||
r.ReadBytes(int(math.Floor(float64(mask))))
|
||||
|
||||
@@ -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()
|
||||
@@ -34,14 +35,19 @@ func main() {
|
||||
if *recordDir == "" {
|
||||
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),
|
||||
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
|
||||
TcpListeningUrl: ":5904",
|
||||
WsListeningUrl: "http://localhost:8183/", // empty = not listening on ws
|
||||
RecordingDir: "", //"/Users/amitbet/vncRec", // empty = no recording
|
||||
TcpListeningUrl: ":5905",
|
||||
//recordingDir: "C:\\vncRec", // empty = no recording
|
||||
ProxyVncPassword: "1234", //empty = no auth
|
||||
ProxyVncPassword: "", //empty = no auth
|
||||
SingleSession: &VncSession{
|
||||
TargetHostname: "localhost",
|
||||
TargetPort: "5903",
|
||||
TargetPassword: "Ch_#!T@8",
|
||||
TargetHostname: "192.168.1.101",
|
||||
TargetPort: "5900",
|
||||
TargetPassword: "ancient1", //"Ch_#!T@8", //
|
||||
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)
|
||||
|
||||
@@ -182,13 +182,59 @@ func (c *ServerConn) handle() error {
|
||||
switch parsedMsg.Type() {
|
||||
case common.SetPixelFormatMsgType:
|
||||
// update pixel format
|
||||
logger.Debugf("ClientUpdater.Consume: updating pixel format")
|
||||
logger.Debugf("IServerConn.Handle: updating pixel format")
|
||||
pixFmtMsg := parsedMsg.(*MsgSetPixelFormat)
|
||||
c.SetPixelFormat(&pixFmtMsg.PF)
|
||||
if pixFmtMsg.PF.TrueColor != 0 {
|
||||
c.SetColorMap(&common.ColorMap{})
|
||||
}
|
||||
|
||||
case common.FramebufferUpdateRequestMsgType:
|
||||
//logger.Infof("IServerConn.Handle: msgBuff update request")
|
||||
//updMsg := parsedMsg.(*MsgFramebufferUpdateRequest)
|
||||
//updMsg.Inc = 1
|
||||
case common.SetEncodingsMsgType:
|
||||
encMsg := parsedMsg.(*MsgSetEncodings)
|
||||
// for i, enc := range encMsg.Encodings {
|
||||
// if enc > common.EncJPEGQualityLevelPseudo1 && enc < common.EncJPEGQualityLevelPseudo10 { //common.EncCursorPseudo
|
||||
// //encMsg.EncNum--
|
||||
// //encMsg.Encodings = append(encMsg.Encodings[:i], encMsg.Encodings[i+1:]...)
|
||||
// encMsg.Encodings[i] = encMsg.Encodings[len(encMsg.Encodings)-1]
|
||||
// encMsg.Encodings[len(encMsg.Encodings)-1] = common.EncJPEGQualityLevelPseudo9
|
||||
// break
|
||||
// }
|
||||
// // if enc == common.EncCompressionLevel3 {
|
||||
// // encMsg.Encodings[i] = common.EncJPEGQualityLevelPseudo9
|
||||
// // }
|
||||
// // if enc == common.EncJPEGQualityLevelPseudo7 {
|
||||
// // encMsg.Encodings[i] = common.EncJPEGQualityLevelPseudo9
|
||||
// // }
|
||||
// }
|
||||
|
||||
encMsg.Encodings = []common.EncodingType{
|
||||
common.EncDesktopSizePseudo,
|
||||
common.EncExtendedDesktopSizePseudo,
|
||||
common.EncLastRectPseudo,
|
||||
common.EncContinuousUpdatesPseudo,
|
||||
common.EncCursorPseudo,
|
||||
common.EncFencePseudo,
|
||||
common.EncHextile,
|
||||
common.EncCopyRect,
|
||||
common.EncTight,
|
||||
common.EncZRLE,
|
||||
common.EncRRE,
|
||||
common.EncRaw,
|
||||
common.EncCompressionLevel2,
|
||||
common.EncJPEGQualityLevelPseudo1,
|
||||
common.EncQEMUExtendedKeyEventPseudo,
|
||||
common.EncTightPng,
|
||||
common.EncXvpPseudo,
|
||||
}
|
||||
encMsg.EncNum = uint16(len(encMsg.Encodings))
|
||||
|
||||
//bad: EncCopyRect EncTight EncTightPng EncHextile EncRRE EncRaw EncJPEGQualityLevelPseudo7 EncCompressionLevel3 EncDesktopSizePseudo EncLastRectPseudo EncCursorPseudo EncQEMUExtendedKeyEventPseudo EncExtendedDesktopSizePseudo EncXvpPseudo EncFencePseudo EncContinuousUpdatesPseudo
|
||||
}
|
||||
|
||||
////////
|
||||
|
||||
if err != nil {
|
||||
|
||||
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