diff --git a/go.mod b/go.mod index 10497f9..a0ba84c 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,15 @@ module git.asdf.cafe/abs3nt/fbisender go 1.23.2 -require gopkg.in/yaml.v2 v2.4.0 +require ( + git.asdf.cafe/abs3nt/gunner v0.0.1 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/cristalhq/aconfig v0.18.5 // indirect + github.com/cristalhq/aconfig/aconfigdotenv v0.17.1 // indirect + github.com/joho/godotenv v1.4.0 // indirect + github.com/urfave/cli/v3 v3.0.0-alpha9.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/go.sum b/go.sum index dd0bc19..68a92ec 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,19 @@ +git.asdf.cafe/abs3nt/gunner v0.0.1 h1:N6kCe7fH83kzm1Sjp/5uZbl8FM5s7KoYCfmhO8qyQbA= +git.asdf.cafe/abs3nt/gunner v0.0.1/go.mod h1:Q4zhiPfmffCVAb5xIzZn6Momm91uf/deqRVd2/vdjd4= +github.com/cristalhq/aconfig v0.17.0/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E= +github.com/cristalhq/aconfig v0.18.5 h1:QqXH/Gy2c4QUQJTV2BN8UAuL/rqZ3IwhvxeC8OgzquA= +github.com/cristalhq/aconfig v0.18.5/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E= +github.com/cristalhq/aconfig/aconfigdotenv v0.17.1 h1:HG2ql5fGe4FLL2fUv6o+o0YRyF1mWEcYkNfWGWD82k4= +github.com/cristalhq/aconfig/aconfigdotenv v0.17.1/go.mod h1:gQIKkh+HkVcODvMNz/cLbH65Pk9b0r4tfolCOsI8G9I= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/urfave/cli/v3 v3.0.0-alpha9.2 h1:CL8llQj3dGRLVQQzHxS+ZYRLanOuhyK1fXgLKD+qV+Y= +github.com/urfave/cli/v3 v3.0.0-alpha9.2/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/main.go b/main.go index 88f4465..ae40676 100644 --- a/main.go +++ b/main.go @@ -2,282 +2,30 @@ package main import ( "context" - "encoding/binary" "fmt" - "io" "log" - "net" - "net/http" - "net/url" "os" - "os/signal" - "path/filepath" - "strconv" - "strings" - "sync" - "syscall" - "time" - "gopkg.in/yaml.v2" + "git.asdf.cafe/abs3nt/fbisender/src/sender" + "github.com/urfave/cli/v3" ) -const ( - defaultHostPort = 8080 - defaultTargetPort = "5000" -) - -var acceptedExtensions = []string{".cia", ".tik", ".cetk", ".3dsx"} - -type Config struct { - TargetIP string `yaml:"target_ip"` - HostIP string `yaml:"host_ip"` - HostPort int `yaml:"host_port"` -} - func main() { log.SetFlags(0) - config, err := loadConfig() - if err != nil { - log.Fatalf("Error loading configuration: %v", err) - } - - targetPath, err := parseArguments() - if err != nil { - log.Fatalf("Error parsing arguments: %v", err) - } - - fileListPayload, directory, err := prepareFileListPayload(config, targetPath) - if err != nil { - log.Fatalf("Error preparing file list payload: %v", err) - } - - if err := changeDirectory(directory); err != nil { - log.Fatalf("Error changing directory: %v", err) - } - - fmt.Println("\nURLs:") - fmt.Println(fileListPayload + "\n") - - server := startHTTPServer(config.HostPort) - - conn, err := sendPayload(config.TargetIP, fileListPayload) - if err != nil { - log.Fatalf("Error sending payload: %v", err) - } - defer conn.Close() - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - if err := waitForInstallation(ctx, conn); err != nil { - log.Printf("Installation process error: %v", err) - } - cancel() - }() - - select { - case <-ctx.Done(): - case sig := <-sigChan: - fmt.Printf("\nReceived signal %v. Shutting down...\n", sig) - cancel() - } - - shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) - defer shutdownCancel() - if err := server.Shutdown(shutdownCtx); err != nil { - log.Printf("HTTP server shutdown error: %v", err) - } - wg.Wait() - - fmt.Println("Server gracefully shut down.") -} - -func loadConfig() (*Config, error) { - configDirectory, err := os.UserConfigDir() - if err != nil { - homeDir, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("getting home directory: %w", err) - } - configDirectory = filepath.Join(homeDir, ".config") - } - data, err := os.ReadFile(filepath.Join(configDirectory, "fbisender", "config.yaml")) - if err != nil { - return nil, err - } - - var config Config - if err := yaml.Unmarshal(data, &config); err != nil { - return nil, err - } - - if config.TargetIP == "" { - return nil, err - } - - if config.HostIP == "" { - log.Println("Detecting host IP...") - hostIP, err := detectHostIP() - if err != nil { - return nil, err - } - config.HostIP = hostIP - } - - if config.HostPort == 0 { - config.HostPort = defaultHostPort - } - - return &config, nil -} - -func parseArguments() (string, error) { - if len(os.Args) < 2 { - return "", fmt.Errorf("usage: %s ", os.Args[0]) - } - - targetPath := strings.TrimSpace(os.Args[1]) - if _, err := os.Stat(targetPath); os.IsNotExist(err) { - return "", fmt.Errorf("%s: no such file or directory", targetPath) - } - - return targetPath, nil -} - -func prepareFileListPayload(config *Config, targetPath string) (string, string, error) { - fmt.Println("Preparing data...") - - baseURL := config.HostIP + ":" + strconv.Itoa(config.HostPort) + "/" - var fileListPayload string - var directory string - - fileInfo, err := os.Stat(targetPath) - if err != nil { - return "", "", err - } - - if fileInfo.IsDir() { - directory = targetPath - files, err := os.ReadDir(directory) - if err != nil { - return "", "", fmt.Errorf("reading directory: %w", err) - } - - for _, file := range files { - if !file.IsDir() && hasAcceptedExtension(file.Name()) { - encodedFileName := url.PathEscape(file.Name()) - fileListPayload += baseURL + encodedFileName + "\n" + app := &cli.Command{ + Name: "fbisender", + Usage: "send files to FBI over network with an HTTP server", + UsageText: "fbisender [global options] ", + Action: func(ctx context.Context, c *cli.Command) error { + if !c.Args().Present() { + return fmt.Errorf("target file or directory is required as an argument") } - } - } else { - if hasAcceptedExtension(fileInfo.Name()) { - encodedFileName := url.PathEscape(fileInfo.Name()) - fileListPayload = baseURL + encodedFileName - directory = filepath.Dir(targetPath) - } else { - return "", "", fmt.Errorf("unsupported file extension. Supported extensions are: %v", acceptedExtensions) - } + return sender.SendFiles(ctx, c.Args().First()) + }, } - if fileListPayload == "" { - return "", "", fmt.Errorf("no files to serve") - } - - return fileListPayload, directory, nil -} - -func changeDirectory(directory string) error { - if directory != "" && directory != "." { - if err := os.Chdir(directory); err != nil { - return err - } - } - return nil -} - -func startHTTPServer(port int) *http.Server { - fmt.Println("Starting HTTP server on port", port) - server := &http.Server{ - Addr: ":" + strconv.Itoa(port), - Handler: http.FileServer(http.Dir(".")), - } - - go func() { - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("Error starting HTTP server: %v", err) - } - }() - - return server -} - -func sendPayload(targetIP, fileListPayload string) (net.Conn, error) { - fmt.Printf("Sending URL(s) to %s on port %s...\n", targetIP, defaultTargetPort) - - conn, err := net.Dial("tcp", net.JoinHostPort(targetIP, defaultTargetPort)) - if err != nil { - return nil, fmt.Errorf("dialing target device: %w", err) - } - - payloadBytes := []byte(fileListPayload) - length := uint32(len(payloadBytes)) - lengthBytes := make([]byte, 4) - binary.BigEndian.PutUint32(lengthBytes, length) - - if _, err := conn.Write(append(lengthBytes, payloadBytes...)); err != nil { - conn.Close() - return nil, fmt.Errorf("writing to connection: %w", err) - } - - return conn, nil -} - -func waitForInstallation(ctx context.Context, conn net.Conn) error { - fmt.Println("Waiting for the installation to complete...") - - buf := make([]byte, 1024) - for { - select { - case <-ctx.Done(): - return nil - default: - _, err := conn.Read(buf) - if err != nil { - if err == io.EOF || strings.Contains(err.Error(), "connection reset by peer") { - fmt.Println("Installation completed. Connection closed by target device.") - return nil - } - return fmt.Errorf("reading from connection: %w", err) - } - } + if err := app.Run(context.Background(), os.Args); err != nil { + log.Fatal(err) } } - -func detectHostIP() (string, error) { - conn, err := net.Dial("udp", "8.8.8.8:53") - if err != nil { - return "", fmt.Errorf("detecting host IP: %w", err) - } - defer conn.Close() - - localAddr := conn.LocalAddr().(*net.UDPAddr) - return localAddr.IP.String(), nil -} - -func hasAcceptedExtension(fileName string) bool { - ext := strings.ToLower(filepath.Ext(fileName)) - for _, acceptedExt := range acceptedExtensions { - if ext == acceptedExt { - return true - } - } - return false -} diff --git a/src/config/config.go b/src/config/config.go new file mode 100644 index 0000000..f2c2c1a --- /dev/null +++ b/src/config/config.go @@ -0,0 +1,16 @@ +package config + +import "git.asdf.cafe/abs3nt/gunner" + +type Config struct { + TargetIP string `yaml:"target_ip"` + TargetPort string `yaml:"target_port" default:"5000"` + HostIP string `yaml:"host_ip"` + HostPort int `yaml:"host_port" default:"8080"` +} + +func NewConfig() *Config { + c := &Config{} + gunner.LoadApp(c, "fbisender") + return c +} diff --git a/src/fileutils/fileutils.go b/src/fileutils/fileutils.go new file mode 100644 index 0000000..8b410d8 --- /dev/null +++ b/src/fileutils/fileutils.go @@ -0,0 +1,37 @@ +package fileutils + +import ( + "os" + "path/filepath" + "strings" +) + +func ChangeDirectory(directory string) error { + if directory != "" && directory != "." { + if err := os.Chdir(directory); err != nil { + return err + } + } + return nil +} + +var acceptedExtensions = map[string]struct{}{ + ".cia": {}, + ".tik": {}, + ".cetk": {}, + ".3dsx": {}, +} + +func HasAcceptedExtension(fileName string) bool { + ext := strings.ToLower(filepath.Ext(fileName)) + _, ok := acceptedExtensions[ext] + return ok +} + +func GetSupportedExtensions() []string { + var supportedExtensions []string + for ext := range acceptedExtensions { + supportedExtensions = append(supportedExtensions, ext) + } + return supportedExtensions +} diff --git a/src/sender/sender.go b/src/sender/sender.go new file mode 100644 index 0000000..5b78ac6 --- /dev/null +++ b/src/sender/sender.go @@ -0,0 +1,136 @@ +package sender + +import ( + "context" + "encoding/binary" + "fmt" + "log" + "net" + "net/url" + "os" + "os/signal" + "path/filepath" + "strings" + "sync" + "syscall" + + "git.asdf.cafe/abs3nt/fbisender/src/config" + "git.asdf.cafe/abs3nt/fbisender/src/fileutils" + "git.asdf.cafe/abs3nt/fbisender/src/server" +) + +func SendFiles(ctx context.Context, targetPath string) error { + config := config.NewConfig() + + fileListPayload, directory, err := prepareFileListPayload(config, targetPath) + if err != nil { + return fmt.Errorf("preparing file list payload: %w", err) + } + + if err := fileutils.ChangeDirectory(directory); err != nil { + return fmt.Errorf("changing directory: %w", err) + } + + fmt.Println("\nURLs:") + fmt.Println(fileListPayload + "\n") + + httpServer := server.StartHTTPServer(config.HostPort) + defer server.ShutdownHTTPServer(httpServer) + + conn, err := sendPayload(config.TargetIP, config.TargetPort, fileListPayload) + if err != nil { + return fmt.Errorf("sending payload: %w", err) + } + defer conn.Close() + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + if err := server.WaitForInstallation(ctx, conn); err != nil { + log.Printf("installation process error: %v", err) + } + cancel() + }() + + select { + case <-ctx.Done(): + case sig := <-sigChan: + fmt.Printf("\nReceived signal %v. Initiating shutdown...\n", sig) + cancel() + } + + wg.Wait() + fmt.Println("Server shut down successfully.") + return nil +} + +func prepareFileListPayload(config *config.Config, targetPath string) (string, string, error) { + fmt.Println("Preparing data...") + + baseURL := fmt.Sprintf("%s:%d/", config.HostIP, config.HostPort) + fileInfo, err := os.Stat(targetPath) + if err != nil { + return "", "", fmt.Errorf("stat target path: %w", err) + } + + if fileInfo.IsDir() { + return prepareDirectoryPayload(baseURL, targetPath) + } else if fileutils.HasAcceptedExtension(fileInfo.Name()) { + encodedPath := encodeFilePath(baseURL, fileInfo.Name()) + return encodedPath, filepath.Dir(targetPath), nil + } + + return "", "", fmt.Errorf("unsupported file extension. Supported extensions are: %v", fileutils.GetSupportedExtensions()) +} + +func prepareDirectoryPayload(baseURL, directory string) (string, string, error) { + files, err := os.ReadDir(directory) + if err != nil { + return "", "", fmt.Errorf("reading directory: %w", err) + } + + var payloadBuilder strings.Builder + for _, file := range files { + if !file.IsDir() && fileutils.HasAcceptedExtension(file.Name()) { + payloadBuilder.WriteString(encodeFilePath(baseURL, file.Name()) + "\n") + } + } + + if payloadBuilder.Len() == 0 { + return "", "", fmt.Errorf("no files with supported extensions to serve") + } + + return payloadBuilder.String(), directory, nil +} + +func encodeFilePath(baseURL, fileName string) string { + return baseURL + url.PathEscape(fileName) +} + +func sendPayload(targetIP, targetPort, fileListPayload string) (net.Conn, error) { + fmt.Printf("Sending URL(s) to %s on port %s...\n", targetIP, targetPort) + + conn, err := net.Dial("tcp", net.JoinHostPort(targetIP, targetPort)) + if err != nil { + return nil, fmt.Errorf("dialing target device: %w", err) + } + + payloadBytes := []byte(fileListPayload) + length := uint32(len(payloadBytes)) + lengthBytes := make([]byte, 4) + binary.BigEndian.PutUint32(lengthBytes, length) + + if _, err := conn.Write(append(lengthBytes, payloadBytes...)); err != nil { + conn.Close() + return nil, fmt.Errorf("writing to connection: %w", err) + } + + return conn, nil +} diff --git a/src/server/server.go b/src/server/server.go new file mode 100644 index 0000000..929cbc7 --- /dev/null +++ b/src/server/server.go @@ -0,0 +1,58 @@ +package server + +import ( + "context" + "fmt" + "io" + "log" + "net" + "net/http" + "strconv" + "strings" + "time" +) + +func StartHTTPServer(port int) *http.Server { + fmt.Println("Starting HTTP server on port", port) + server := &http.Server{ + Addr: ":" + strconv.Itoa(port), + Handler: http.FileServer(http.Dir(".")), + } + + go func() { + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Error starting HTTP server: %v", err) + } + }() + + return server +} + +func WaitForInstallation(ctx context.Context, conn net.Conn) error { + fmt.Println("Waiting for the installation to complete...") + + buf := make([]byte, 1024) + for { + select { + case <-ctx.Done(): + return nil + default: + _, err := conn.Read(buf) + if err != nil { + if err == io.EOF || strings.Contains(err.Error(), "connection reset by peer") { + fmt.Println("Installation completed. Connection closed by target device.") + return nil + } + return fmt.Errorf("reading from connection: %w", err) + } + } + } +} + +func ShutdownHTTPServer(httpServer *http.Server) { + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := httpServer.Shutdown(shutdownCtx); err != nil { + log.Printf("HTTP server shutdown error: %v", err) + } +}