From 8a175e3051fe9e6b24c770878d34a03f493225eb Mon Sep 17 00:00:00 2001 From: Sudo-Ivan Date: Tue, 31 Dec 2024 14:00:10 -0600 Subject: [PATCH] update --- .gitignore | 3 + cmd/client-ftp/main.go | 266 ---------------------- cmd/client/client.go | 499 ----------------------------------------- scripts/build.sh | 4 + scripts/run_clients.sh | 117 ---------- 5 files changed, 7 insertions(+), 882 deletions(-) delete mode 100644 cmd/client-ftp/main.go delete mode 100644 cmd/client/client.go create mode 100644 scripts/build.sh delete mode 100755 scripts/run_clients.sh diff --git a/.gitignore b/.gitignore index 320f4f9..22ea0cc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ logs/ .env .json + + +rns-announce \ No newline at end of file diff --git a/cmd/client-ftp/main.go b/cmd/client-ftp/main.go deleted file mode 100644 index 4afb41c..0000000 --- a/cmd/client-ftp/main.go +++ /dev/null @@ -1,266 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "io" - "log" - "os" - "os/signal" - "path/filepath" - "syscall" - "time" - - "github.com/Sudo-Ivan/reticulum-go/internal/config" - "github.com/Sudo-Ivan/reticulum-go/pkg/common" - "github.com/Sudo-Ivan/reticulum-go/pkg/destination" - "github.com/Sudo-Ivan/reticulum-go/pkg/identity" - "github.com/Sudo-Ivan/reticulum-go/pkg/link" - "github.com/Sudo-Ivan/reticulum-go/pkg/packet" - "github.com/Sudo-Ivan/reticulum-go/pkg/resource" - "github.com/Sudo-Ivan/reticulum-go/pkg/transport" -) - -const ( - APP_NAME = "example_utilities" - APP_ASPECT = "filetransfer" -) - -var ( - configPath = flag.String("config", "", "Path to config file") - servePath = flag.String("serve", "", "Directory to serve files from") -) - -type FileServer struct { - config *common.ReticulumConfig - transport *transport.Transport - interfaces []common.NetworkInterface - identity *identity.Identity - servePath string -} - -func NewFileServer(cfg *common.ReticulumConfig, servePath string) (*FileServer, error) { - if cfg == nil { - var err error - cfg, err = config.InitConfig() - if err != nil { - return nil, fmt.Errorf("failed to initialize config: %v", err) - } - } - - t, err := transport.NewTransport(cfg) - if err != nil { - return nil, fmt.Errorf("failed to initialize transport: %v", err) - } - - id, err := identity.New() - if err != nil { - return nil, fmt.Errorf("failed to create identity: %v", err) - } - - return &FileServer{ - config: cfg, - transport: t, - interfaces: make([]common.NetworkInterface, 0), - identity: id, - servePath: servePath, - }, nil -} - -func (s *FileServer) OnLinkEstablished(l *link.Link) { - s.handleLinkEstablished(l) -} - -func (s *FileServer) Start() error { - dest, err := destination.New( - s.identity, - destination.OUT, - destination.SINGLE, - APP_NAME, - APP_ASPECT, - ) - if err != nil { - return fmt.Errorf("failed to create destination: %v", err) - } - - callback := func(l interface{}) { - if link, ok := l.(*link.Link); ok { - s.OnLinkEstablished(link) - } - } - - dest.SetLinkEstablishedCallback(callback) - - log.Printf("File server started. Server hash: %s", s.identity.Hex()) - log.Printf("Serving directory: %s", s.servePath) - return nil -} - -func (s *FileServer) handleLinkEstablished(l *link.Link) { - log.Printf("Client connected") - - l.SetPacketCallback(func(data []byte, p *packet.Packet) { - s.handlePacket(data, l) - }) - - l.SetResourceCallback(func(r interface{}) bool { - if res, ok := r.(*resource.Resource); ok { - return s.handleResource(res) - } - return false - }) -} - -func (s *FileServer) handlePacket(data []byte, l *link.Link) { - if string(data) == "LIST" { - files, err := s.getFileList() - if err != nil { - log.Printf("Error getting file list: %v", err) - l.Teardown() - return - } - - if err := l.SendPacket(files); err != nil { - log.Printf("Error sending file list: %v", err) - l.Teardown() - } - } -} - -func (s *FileServer) handleResource(r *resource.Resource) bool { - filename := filepath.Join(s.servePath, r.GetName()) - file, err := os.Create(filename) - if err != nil { - log.Printf("Failed to create file: %v", err) - return false - } - defer file.Close() - - written, err := io.Copy(file, r) - if err != nil { - log.Printf("Failed to write file: %v", err) - return false - } - - log.Printf("Received file: %s (%d bytes)", filename, written) - return true -} - -func (s *FileServer) getFileList() ([]byte, error) { - files, err := os.ReadDir(s.servePath) - if err != nil { - return nil, err - } - - var fileList []string - for _, file := range files { - if !file.IsDir() { - fileList = append(fileList, file.Name()) - } - } - - return []byte(fmt.Sprintf("%v", fileList)), nil -} - -func main() { - flag.Parse() - - if *servePath == "" { - log.Fatal("Please specify a directory to serve with -serve") - } - - var cfg *common.ReticulumConfig - var err error - - if *configPath == "" { - cfg, err = config.InitConfig() - } else { - cfg, err = config.LoadConfig(*configPath) - } - if err != nil { - log.Fatalf("Failed to load config: %v", err) - } - - server, err := NewFileServer(cfg, *servePath) - if err != nil { - log.Fatalf("Failed to create server: %v", err) - } - - if err := server.Start(); err != nil { - log.Fatalf("Failed to start server: %v", err) - } - - // Start watching the directory for changes - go server.watchDirectory() - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - <-sigChan -} - -func (s *FileServer) watchDirectory() { - for { - time.Sleep(1 * time.Second) - files, err := os.ReadDir(s.servePath) - if err != nil { - log.Printf("Error reading directory: %v", err) - continue - } - - for _, file := range files { - if !file.IsDir() { - // Try to send file to connected peers - filePath := filepath.Join(s.servePath, file.Name()) - if err := s.sendFile(filePath); err != nil { - log.Printf("Error sending file %s: %v", file.Name(), err) - } - } - } - } -} - -func (s *FileServer) sendFile(filePath string) error { - file, err := os.Open(filePath) - if err != nil { - return fmt.Errorf("failed to open file: %v", err) - } - defer file.Close() - - // Create a destination for the file transfer - dest, err := destination.New( - s.identity, - destination.OUT, - destination.SINGLE, - APP_NAME, - APP_ASPECT, - ) - if err != nil { - return fmt.Errorf("failed to create destination: %v", err) - } - - // Set up link for file transfer - callback := func(l interface{}) { - if link, ok := l.(*link.Link); ok { - // Create a new resource with auto-compression enabled - res, err := resource.New(file, true) - if err != nil { - log.Printf("Error creating resource: %v", err) - return - } - - // The filename is automatically set from the file handle - // in resource.New when using an io.ReadWriteSeeker - - // Send the resource through the link - if err := link.SendResource(res); err != nil { - log.Printf("Error sending resource: %v", err) - return - } - log.Printf("File %s sent successfully", filepath.Base(filePath)) - } - } - - dest.SetLinkEstablishedCallback(callback) - - return nil -} diff --git a/cmd/client/client.go b/cmd/client/client.go deleted file mode 100644 index 9e62100..0000000 --- a/cmd/client/client.go +++ /dev/null @@ -1,499 +0,0 @@ -package main - -import ( - "bufio" - "encoding/binary" - "flag" - "fmt" - "io" - "log" - "os" - "os/signal" - "strings" - "syscall" - "time" - - "github.com/Sudo-Ivan/reticulum-go/internal/config" - "github.com/Sudo-Ivan/reticulum-go/pkg/announce" - "github.com/Sudo-Ivan/reticulum-go/pkg/common" - "github.com/Sudo-Ivan/reticulum-go/pkg/destination" - "github.com/Sudo-Ivan/reticulum-go/pkg/identity" - "github.com/Sudo-Ivan/reticulum-go/pkg/interfaces" - "github.com/Sudo-Ivan/reticulum-go/pkg/link" - "github.com/Sudo-Ivan/reticulum-go/pkg/packet" - "github.com/Sudo-Ivan/reticulum-go/pkg/transport" -) - -var ( - configPath = flag.String("config", "", "Path to config file") - targetHash = flag.String("target", "", "Target destination hash") - generateIdentity = flag.Bool("generate-identity", false, "Generate a new identity and print its hash") -) - -type Client struct { - config *common.ReticulumConfig - transport *transport.Transport - interfaces []common.NetworkInterface - identity *identity.Identity -} - -func NewClient(cfg *common.ReticulumConfig) (*Client, error) { - if cfg == nil { - var err error - cfg, err = config.InitConfig() - if err != nil { - return nil, fmt.Errorf("failed to initialize config: %v", err) - } - } - - t, err := transport.NewTransport(cfg) - if err != nil { - return nil, fmt.Errorf("failed to initialize transport: %v", err) - } - - id, err := identity.New() - if err != nil { - return nil, fmt.Errorf("failed to create identity: %v", err) - } - - return &Client{ - config: cfg, - transport: t, - interfaces: make([]common.NetworkInterface, 0), - identity: id, - }, nil -} - -func (c *Client) Start() error { - log.Printf("Starting Reticulum client...") - log.Printf("Configuration: %+v", c.config) - - // Initialize transport - t, err := transport.NewTransport(c.config) - if err != nil { - return fmt.Errorf("failed to initialize transport: %v", err) - } - c.transport = t - log.Printf("Transport initialized") - - log.Printf("Initializing network interfaces...") - for name, ifaceConfig := range c.config.Interfaces { - if !ifaceConfig.Enabled { - log.Printf("Skipping disabled interface %s", name) - continue - } - - log.Printf("Configuring interface %s (%s)", name, ifaceConfig.Type) - var iface common.NetworkInterface - - switch ifaceConfig.Type { - case "TCPClientInterface": - log.Printf("Connecting to %s:%d via TCP...", ifaceConfig.TargetHost, ifaceConfig.TargetPort) - client, err := interfaces.NewTCPClient( - name, - ifaceConfig.TargetHost, - ifaceConfig.TargetPort, - ifaceConfig.KISSFraming, - ifaceConfig.I2PTunneled, - ifaceConfig.Enabled, - ) - if err != nil { - return fmt.Errorf("failed to create TCP interface %s: %v", name, err) - } - - if err := client.Start(); err != nil { - return fmt.Errorf("failed to start TCP interface %s: %v", name, err) - } - - iface = client - log.Printf("Successfully connected to %s:%d", ifaceConfig.TargetHost, ifaceConfig.TargetPort) - - case "UDPInterface": - addr := fmt.Sprintf("%s:%d", ifaceConfig.Address, ifaceConfig.Port) - target := fmt.Sprintf("%s:%d", ifaceConfig.TargetHost, ifaceConfig.TargetPort) - log.Printf("Starting UDP interface on %s...", addr) - udp, err := interfaces.NewUDPInterface( - name, - addr, - target, - ifaceConfig.Enabled, - ) - if err != nil { - return fmt.Errorf("failed to create UDP interface %s: %v", name, err) - } - - if err := udp.Start(); err != nil { - return fmt.Errorf("failed to start UDP interface %s: %v", name, err) - } - - iface = udp - log.Printf("UDP interface listening on %s", addr) - } - - if iface != nil { - // Set packet callback - iface.SetPacketCallback(c.transport.HandlePacket) - c.interfaces = append(c.interfaces, iface) - log.Printf("Created and started interface %s (type=%v, enabled=%v)", - name, iface.GetType(), iface.IsEnabled()) - } - } - - // Register announce handler with explicit type - var handler transport.AnnounceHandler = &ClientAnnounceHandler{client: c} - c.transport.RegisterAnnounceHandler(handler) - - // Send initial announce - log.Printf("Sending initial announce...") - if err := c.sendAnnounce(); err != nil { - log.Printf("Warning: Failed to send initial announce: %v", err) - } - - return nil -} - -func (c *Client) handlePacket(data []byte, p *packet.Packet) { - if len(data) < 1 { - return - } - - header := data[0] - packetType := header & 0x03 // Extract packet type from header - - switch packetType { - case announce.PACKET_TYPE_ANNOUNCE: - log.Printf("Received announce packet:") - log.Printf(" Raw data: %x", data) - - // Create announce instance - a, err := announce.New(c.identity, []byte("RNS.Go.Client"), false) - if err != nil { - log.Printf("Failed to create announce handler: %v", err) - return - } - - // Handle the announce - if err := a.HandleAnnounce(data[1:]); err != nil { - log.Printf("Failed to handle announce: %v", err) - } - - default: - c.transport.HandlePacket(data, p) - } -} - -func (c *Client) handleAnnounce(data []byte) { - if len(data) < 42 { - log.Printf("Received malformed announce packet (too short)") - return - } - - destHash := data[:32] - timestamp := binary.BigEndian.Uint64(data[32:40]) - hops := data[40] - flags := data[41] - - log.Printf("Received announce from %x", destHash) - log.Printf(" Timestamp: %d", timestamp) - log.Printf(" Hops: %d", hops) - log.Printf(" Flags: %x", flags) - - // Extract public key if present (after flags) - if len(data) > 42 { - pubKeyLen := 32 // Ed25519 public key length - pubKey := data[42 : 42+pubKeyLen] - log.Printf(" Public Key: %x", pubKey) - - // Extract app data if present - var appData []byte - if len(data) > 42+pubKeyLen+2 { - dataLen := binary.BigEndian.Uint16(data[42+pubKeyLen : 42+pubKeyLen+2]) - if len(data) >= 42+pubKeyLen+2+int(dataLen) { - appData = data[42+pubKeyLen+2 : 42+pubKeyLen+2+int(dataLen)] - log.Printf(" App Data: %s", string(appData)) - } - } - - // Store the identity for future use with all required parameters - if !identity.ValidateAnnounce(data, destHash, pubKey, data[len(data)-64:], appData) { - log.Printf("Failed to validate announce") - return - } - log.Printf("Successfully validated and stored announce") - } -} - -func (c *Client) sendAnnounce() error { - // Create announce packet - identityHash := c.identity.Hash() - announceData := make([]byte, 0) - - // Add header - header := []byte{0x01, 0x00} // Announce packet type - announceData = append(announceData, header...) - - // Add destination hash - announceData = append(announceData, identityHash...) - - // Add context byte - announceData = append(announceData, announce.ANNOUNCE_IDENTITY) - - // Add public key - announceData = append(announceData, c.identity.GetPublicKey()...) - - // App data with length prefix - appData := []byte("RNS.Go.Client") - lenBytes := make([]byte, 2) - binary.BigEndian.PutUint16(lenBytes, uint16(len(appData))) - announceData = append(announceData, lenBytes...) - announceData = append(announceData, appData...) - - // Add signature - signData := append(identityHash, c.identity.GetPublicKey()...) - signData = append(signData, appData...) - signature := c.identity.Sign(signData) - announceData = append(announceData, signature...) - - log.Printf("Sending announce:") - log.Printf(" Identity Hash: %x", identityHash) - log.Printf(" Packet Length: %d bytes", len(announceData)) - log.Printf(" Full Packet: %x", announceData) - - sentCount := 0 - // Send on all interfaces - for _, iface := range c.interfaces { - log.Printf("Attempting to send on interface %s:", iface.GetName()) - log.Printf(" Type: %v", iface.GetType()) - log.Printf(" MTU: %d bytes", iface.GetMTU()) - log.Printf(" Status: enabled=%v", iface.IsEnabled()) - - if !iface.IsEnabled() { - log.Printf(" Skipping disabled interface") - continue - } - - if err := iface.Send(announceData, ""); err != nil { - log.Printf(" Failed to send: %v", err) - } else { - log.Printf(" Successfully sent announce") - sentCount++ - } - } - - if sentCount == 0 { - return fmt.Errorf("no interfaces available to send announce") - } - - return nil -} - -func (c *Client) Stop() { - for _, iface := range c.interfaces { - iface.Detach() - } - c.transport.Close() -} - -func (c *Client) Connect(destHash []byte) error { - // Recall server identity - serverIdentity, err := identity.Recall(destHash) - if err != nil { - return err - } - - // Create destination - dest, err := destination.New( - serverIdentity, - destination.OUT, - destination.SINGLE, - "example_utilities", - "identifyexample", - ) - if err != nil { - return err - } - - // Create link with all required parameters - link := link.NewLink( - dest, - c.transport, // Add the transport instance - c.handleLinkEstablished, - c.handleLinkClosed, - ) - - // Set callbacks - link.SetPacketCallback(c.handlePacket) - - return nil -} - -func (c *Client) handleLinkEstablished(l *link.Link) { - log.Printf("Link established with server, identifying...") - - // Identify to server - if err := l.Identify(c.identity); err != nil { - log.Printf("Failed to identify: %v", err) - l.Teardown() - return - } -} - -func (c *Client) handleLinkClosed(l *link.Link) { - log.Printf("Link closed") -} - -type ClientAnnounceHandler struct { - client *Client -} - -func (h *ClientAnnounceHandler) AspectFilter() []string { - return []string{"RNS.Go.Client"} -} - -func (h *ClientAnnounceHandler) ReceivedAnnounce(destinationHash []byte, announcedIdentity interface{}, appData []byte) error { - log.Printf("=== Received Announce Details ===") - log.Printf("Destination Hash: %x", destinationHash) - log.Printf("App Data: %s", string(appData)) - - // Type assert the identity - if id, ok := announcedIdentity.(*identity.Identity); ok { - log.Printf("Identity Public Key: %x", id.GetPublicKey()) - - // Create packet hash for storage - packetHash := identity.TruncatedHash(append(destinationHash, id.GetPublicKey()...)) - log.Printf("Generated Packet Hash: %x", packetHash) - - // Store the peer identity with all required parameters - identity.Remember(packetHash, destinationHash, id.GetPublicKey(), appData) - log.Printf("Identity stored successfully") - log.Printf("===========================") - return nil - } - - log.Printf("Error: Invalid identity type") - log.Printf("===========================") - return fmt.Errorf("invalid identity type") -} - -func (h *ClientAnnounceHandler) ReceivePathResponses() bool { - return true -} - -func main() { - flag.Parse() - - log.Printf("Starting Reticulum Go client...") - log.Printf("Config path: %s", *configPath) - log.Printf("Target hash: %s", *targetHash) - - var cfg *common.ReticulumConfig - var err error - - if *configPath == "" { - log.Printf("No config path specified, using default configuration") - cfg, err = config.InitConfig() - } else { - log.Printf("Loading configuration from: %s", *configPath) - cfg, err = config.LoadConfig(*configPath) - } - if err != nil { - log.Fatalf("Failed to load config: %v", err) - } - log.Printf("Configuration loaded successfully") - - if *generateIdentity { - log.Printf("Generating new identity...") - id, err := identity.New() - if err != nil { - log.Fatalf("Failed to generate identity: %v", err) - } - fmt.Printf("Identity hash: %s\n", id.Hex()) - return - } - - client, err := NewClient(cfg) - if err != nil { - log.Fatalf("Failed to create client: %v", err) - } - defer client.Stop() - - if err := client.Start(); err != nil { - log.Fatalf("Failed to start client: %v", err) - } - - log.Printf("Client running, press Ctrl+C to exit") - - // If target is specified, start interactive mode - if *targetHash != "" { - targetBytes, err := identity.HashFromString(*targetHash) - if err != nil { - log.Fatalf("Invalid target hash: %v", err) - } - link, err := client.transport.GetLink(targetBytes) - if err != nil { - log.Fatalf("Failed to get link: %v", err) - } - log.Printf("Starting interactive mode...") - interactiveLoop(link) - return - } - - // Wait for interrupt if no target specified - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - <-sigChan - log.Printf("Received interrupt signal, shutting down...") -} - -func interactiveLoop(link *transport.Link) { - reader := bufio.NewReader(os.Stdin) - connected := make(chan struct{}) - disconnected := make(chan struct{}) - - // Set up connection status handlers - link.OnConnected(func() { - connected <- struct{}{} - }) - - link.OnDisconnected(func() { - disconnected <- struct{}{} - }) - - // Wait for initial connection - select { - case <-connected: - log.Println("Connected to target") - case <-time.After(10 * time.Second): - log.Fatal("Connection timeout") - return - } - - // Start input loop - for { - select { - case <-disconnected: - log.Println("Connection lost") - return - default: - fmt.Print("> ") - input, err := reader.ReadString('\n') - if err != nil { - if err == io.EOF { - return - } - log.Printf("Error reading input: %v", err) - continue - } - - input = strings.TrimSpace(input) - if input == "quit" || input == "exit" { - return - } - - if err := link.Send([]byte(input)); err != nil { - log.Printf("Failed to send: %v", err) - return - } - } - } -} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..40cc7ec --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,4 @@ +go build -o bin/reticulum ./cmd/reticulum +go build -o bin/rns-announce ./cmd/rns-announce + +# Add other commands here diff --git a/scripts/run_clients.sh b/scripts/run_clients.sh deleted file mode 100755 index 65671b6..0000000 --- a/scripts/run_clients.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/bin/bash - -# Function to show usage -show_usage() { - echo "Usage: $0 [--type TYPE]" - echo " --type Type of client to run (default: client, options: client, ftp)" - exit 1 -} - -# Parse command line arguments -CLIENT_TYPE="client" -while [[ $# -gt 0 ]]; do - case $1 in - --type) - CLIENT_TYPE="$2" - shift 2 - ;; - *) - show_usage - ;; - esac -done - -# Validate client type -if [[ "$CLIENT_TYPE" != "client" && "$CLIENT_TYPE" != "ftp" ]]; then - echo "Error: Invalid client type. Must be 'client' or 'ftp'" - show_usage -fi - -# Build the appropriate binaries -echo "Building Reticulum binaries..." -go build -o bin/reticulum ./cmd/reticulum - -case $CLIENT_TYPE in - "client") - go build -o bin/reticulum-client ./cmd/client - CLIENT_BIN="reticulum-client" - ;; - "ftp") - go build -o bin/reticulum-client-ftp ./cmd/client-ftp - CLIENT_BIN="reticulum-client-ftp" - ;; -esac - -# Check if build was successful -if [ $? -ne 0 ]; then - echo "Build failed!" - exit 1 -fi - -# Create directories -mkdir -p logs -mkdir -p bin - -# Start the Reticulum server first -echo "Starting Reticulum server..." -./bin/reticulum > logs/server.log 2>&1 & -echo $! > logs/server.pid -sleep 2 # Give server time to start - -# Generate identities for both clients -echo "Generating identities..." -CLIENT1_HASH=$(./bin/"$CLIENT_BIN" -config configs/test-client1.toml -generate-identity 2>&1 | grep "Identity hash:" | cut -d' ' -f3) -CLIENT2_HASH=$(./bin/"$CLIENT_BIN" -config configs/test-client2.toml -generate-identity 2>&1 | grep "Identity hash:" | cut -d' ' -f3) - -echo "Client 1 Hash: $CLIENT1_HASH" -echo "Client 2 Hash: $CLIENT2_HASH" - -# Function to run client -run_client() { - local config=$1 - local target=$2 - local logfile=$3 - - case $CLIENT_TYPE in - "client") - echo "Starting regular client with config: $config targeting: $target" - ./bin/"$CLIENT_BIN" -config "$config" -target "$target" > "$logfile" 2>&1 & - ;; - "ftp") - echo "Starting FTP client with config: $config serving directory: $target" - ./bin/"$CLIENT_BIN" -config "$config" -serve "$target" > "$logfile" 2>&1 & - ;; - esac - - echo $! > "$logfile.pid" - echo "Client started with PID: $(cat $logfile.pid)" -} - -# Run both clients with appropriate parameters -case $CLIENT_TYPE in - "client") - run_client "configs/test-client1.toml" "$CLIENT2_HASH" "logs/client1.log" - run_client "configs/test-client2.toml" "$CLIENT1_HASH" "logs/client2.log" - ;; - "ftp") - # Create shared directories for FTP clients - mkdir -p ./shared/client1 ./shared/client2 - run_client "configs/test-client1.toml" "./shared/client1" "logs/client1.log" "$CLIENT2_HASH" - run_client "configs/test-client2.toml" "./shared/client2" "logs/client2.log" "$CLIENT1_HASH" - ;; -esac - -echo -echo "Both clients are running. To stop everything:" -echo "kill \$(cat logs/*.pid)" -echo -echo "To view logs:" -echo "tail -f logs/client1.log" -echo "tail -f logs/client2.log" - -if [ "$CLIENT_TYPE" = "ftp" ]; then - echo - echo "FTP shared directories:" - echo "./shared/client1" - echo "./shared/client2" -fi \ No newline at end of file