Skip to content

Commit

Permalink
add first start of an rsync receiver
Browse files Browse the repository at this point in the history
This receiver is able to list files, and correctly receive them (matching
checksum), but the receiver does not write any files to disk yet:

  % gokr-rsync -a \
    rsync://rsync.chiark.greenend.org.uk/ftp/users/sgtatham/putty-website-mirror/

Now that we have a sender and a receiver (in a client and a server,
respectively, roles cannot be reversed currently), we can split out common data
types, constants (package rsync) and wire-level routines (package rsyncwire).

The terminology matches tridge rsync (file names, function names, data types)
where possible.
  • Loading branch information
stapelberg committed Dec 29, 2021
1 parent f470592 commit d5a5b3d
Show file tree
Hide file tree
Showing 22 changed files with 2,287 additions and 1,316 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.PHONY: all run systemd test docker raspi mac

all:
CGO_ENABLED=0 go install github.com/gokrazy/rsync/cmd/gokr-rsyncd
CGO_ENABLED=0 go install github.com/gokrazy/rsync/cmd/...

run: all
sudo ~/go/bin/gokr-rsyncd -modulemap=default=/etc/default
Expand Down
15 changes: 15 additions & 0 deletions cmd/gokr-rsync/rsync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Tool gokr-rsync is an rsync receiver Go implementation.
package main

import (
"log"
"os"

"github.com/gokrazy/rsync/internal/receivermaincmd"
)

func main() {
if err := receivermaincmd.Main(os.Args, os.Stdin, os.Stdout, os.Stderr); err != nil {
log.Fatal(err)
}
}
36 changes: 36 additions & 0 deletions consts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package rsync

// rsync.h
const (
XMIT_TOP_DIR = (1 << 0)
XMIT_SAME_MODE = (1 << 1)
XMIT_EXTENDED_FLAGS = (1 << 2)
XMIT_SAME_RDEV_pre28 = XMIT_EXTENDED_FLAGS /* Only in protocols < 28 */
XMIT_SAME_UID = (1 << 3)
XMIT_SAME_GID = (1 << 4)
XMIT_SAME_NAME = (1 << 5)
XMIT_LONG_NAME = (1 << 6)
XMIT_SAME_TIME = (1 << 7)
XMIT_SAME_RDEV_MAJOR = (1 << 8)
XMIT_HAS_IDEV_DATA = (1 << 9)
XMIT_SAME_DEV = (1 << 10)
XMIT_RDEV_MINOR_IS_SMALL = (1 << 11)
)

// as per /usr/include/bits/stat.h:
const (
S_IFMT = 0o0170000 // bits determining the file type
S_IFDIR = 0o0040000 // Directory
S_IFCHR = 0o0020000 // Character device
S_IFBLK = 0o0060000 // Block device
S_IFREG = 0o0100000 // Regular file
S_IFIFO = 0o0010000 // FIFO
S_IFLNK = 0o0120000 // Symbolic link
S_IFSOCK = 0o0140000 // Socket
)

// ProtocolVersion defines the currently implemented rsync protocol
// version. Protocol version 27 seems to be the safest bet for wide
// compatibility: version 27 was introduced by rsync 2.6.0 (released 2004), and
// is supported by openrsync and rsyn.
const ProtocolVersion = 27
6 changes: 0 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,8 @@ require (
github.com/google/go-cmp v0.5.6
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/mmcloughlin/md4 v0.1.1
github.com/stapelberg/rsync-os v0.3.0
github.com/stapelberg/rsyncparse v0.0.0-20211228091344-84a4474990ee
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1
)

require (
github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 // indirect
github.com/pkg/errors v0.9.1 // indirect
)
313 changes: 0 additions & 313 deletions go.sum

Large diffs are not rendered by default.

122 changes: 122 additions & 0 deletions internal/receivermaincmd/clientserver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package receivermaincmd

import (
"bufio"
"fmt"
"io"
"log"
"net"
"net/url"
"os"
"strings"

"github.com/gokrazy/rsync"
)

// rsync/clientserver.c:start_socket_client
func socketClient(osenv osenv, opts *Opts, src, dest string) error {
u, err := url.Parse(src)
if err != nil {
return err
}
host := u.Host
if _, _, err := net.SplitHostPort(host); err != nil {
host += ":873" // rsync daemon port
}
log.Printf("Opening TCP connection to %s", host)
conn, err := net.Dial("tcp", host)
if err != nil {
return err
}
path := strings.TrimPrefix(u.Path, "/")
if path == "" {
return fmt.Errorf("empty remote path")
}
module := path
if idx := strings.IndexByte(module, '/'); idx > -1 {
module = module[:idx]
}
log.Printf("rsync module %q, path %q", module, path)
if err := startInbandExchange(opts, conn, module, path); err != nil {
return err
}
if err := clientRun(osenv, opts, conn, dest); err != nil {
return err
}
return nil
}

// rsync/clientserver.c:start_inband_exchange
func startInbandExchange(opts *Opts, conn io.ReadWriter, module, path string) error {
rd := bufio.NewReader(conn)

// send client greeting
fmt.Fprintf(conn, "@RSYNCD: %d\n", rsync.ProtocolVersion)

// read server greeting
serverGreeting, err := rd.ReadString('\n')
if err != nil {
return fmt.Errorf("ReadString: %v", err)
}
serverGreeting = strings.TrimSpace(serverGreeting)
const serverGreetingPrefix = "@RSYNCD: "
if !strings.HasPrefix(serverGreeting, serverGreetingPrefix) {
return fmt.Errorf("invalid server greeting: got %q", serverGreeting)
}
// protocol negotiation: require at least version 27
serverGreeting = strings.TrimPrefix(serverGreeting, serverGreetingPrefix)
var remoteProtocol, remoteSub int32
if _, err := fmt.Sscanf(serverGreeting, "%d.%d", &remoteProtocol, &remoteSub); err != nil {
if _, err := fmt.Sscanf(serverGreeting, "%d", &remoteProtocol); err != nil {
return fmt.Errorf("reading server greeting: %v", err)
}
}
if remoteProtocol < 27 {
return fmt.Errorf("server version %d too old", remoteProtocol)
}

log.Printf("(Client) Protocol versions: remote=%d, negotiated=%d", remoteProtocol, rsync.ProtocolVersion)
log.Printf("Client checksum: md4")

// send module name
fmt.Fprintf(conn, "%s\n", module)
for {
line, err := rd.ReadString('\n')
if err != nil {
return fmt.Errorf("did not get server startup line: %v", err)
}
line = strings.TrimSpace(line)

if strings.HasPrefix(line, "@RSYNCD: AUTHREQD ") {
// TODO: implement support for authentication
return fmt.Errorf("authentication not yet implemented")
}

if line == "@RSYNCD: OK" {
break
}

// TODO: @RSYNCD: EXIT after listing modules

if strings.HasPrefix(line, "@ERROR") {
fmt.Fprintf(os.Stderr, "%s\n", line)
return fmt.Errorf("abort (rsync fatal error)")
}

// print rsync server message of the day (MOTD)
fmt.Fprintf(os.Stdout, "%s\n", line)
}

sargv := serverOptions(opts)
sargv = append(sargv, ".")
if path != "" {
sargv = append(sargv, path)
}
log.Printf("sending daemon args: %s", sargv)
for _, argv := range sargv {
fmt.Fprintf(conn, "%s\n", argv)
}
fmt.Fprintf(conn, "\n")

return nil
}
Loading

0 comments on commit d5a5b3d

Please sign in to comment.