diff --git a/client/cmd/auth.go b/client/cmd/auth.go new file mode 100644 index 0000000..9581553 --- /dev/null +++ b/client/cmd/auth.go @@ -0,0 +1,89 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "net/http" + "os" + + internal "github.com/mplus-oss/mdrop/client/internal" +) + +func AuthCommand(args []string) { + flag := flag.NewFlagSet("mdrop auth", flag.ExitOnError) + var ( + url = flag.String("url", "https://example.com", "URL of broker") + token = flag.String("token", "", "Private key token if the broker is on private mode") + ) + flag.Parse(args) + + if *token == "" { + *token = "token" + } + + var client = &http.Client{} + + fmt.Println("Authenticating...") + req, err := http.NewRequest("POST", *url+"/verify?token="+*token, nil) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + res, err := client.Do(req) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + defer res.Body.Close() + + var verifyData VerifyJSONReturn + err = json.NewDecoder(res.Body).Decode(&verifyData) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if res.StatusCode != 200 { + var msg string + if verifyData.ErrorTitle == "" { + msg = verifyData.Message + } else { + msg = verifyData.ErrorTitle + } + fmt.Println("Error:", msg) + os.Exit(1) + } + + if verifyData.IsPublic { + fmt.Println("This broker is running on public mode.") + *token = "" + } + + config := internal.ConfigFile{ + URL: *url, + Token: *token, + Tunnel: internal.ConfigFileTunnel{ + Host: verifyData.TunnelHost, + Port: verifyData.TunnelPort, + }, + } + err = config.WriteConfig() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + fmt.Println("Authenticated!") + os.Exit(0) +} + +type VerifyJSONReturn struct { + Message string `json:"message"` + IsPublic bool `json:"isPublic"` + TunnelHost string `json:"tunnelHost"` + TunnelPort int `json:"tunnelPort"` + + // This respon fired when the API is failed + ErrorTitle string `json:"title"` +} diff --git a/client/cmd/get.go b/client/cmd/get.go new file mode 100644 index 0000000..feb70c3 --- /dev/null +++ b/client/cmd/get.go @@ -0,0 +1,81 @@ +package main + +import ( + "flag" + "fmt" + "math/rand/v2" + "os" + + internal "github.com/mplus-oss/mdrop/client/internal" +) + +var errGetGlobal chan error = make(chan error) + +func GetCommand(args []string) { + flag := flag.NewFlagSet("mdrop get", flag.ExitOnError) + var ( + expired = flag.Int("expired", 3, "Expired timeout in hours.") + port = flag.Int("port", 0, "Specified port on broker. Range of port is about 10k - 59k. (default rand(10000, 59999))") + localPort = flag.Int("localPort", 6000, "Specified port on local.") + ) + flag.Parse(args) + + if *port == 0 { + *port = randRange(10000, 59999) + } + if *port < 10000 || *port > 59999 { + fmt.Println("Invalid port range!") + os.Exit(1) + } + + var c internal.ConfigFile + err := c.ParseConfig(&c) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + fmt.Println("Note: Press Ctrl+C to close the session.") + fmt.Println("Connecting to tunnel...") + + go StartShellTunnel(true, c, *localPort, *port) + err = <- sshErrGlobal + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // Get token data + fmt.Print("Creating the room...\n\n") + var path = fmt.Sprintf( + "%v/room/create?durationInHours=%v&port=%v", + c.URL, + *expired, + *port, + ) + roomData, err := getToken(path) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Println(roomData.Token) + + // Start the Echo server + go createWebserver(*localPort, roomData.Token) + + fmt.Println( + "\nCopy this token to the sender. Make sure sender has authenticated to", + c.URL, + "before sending the file.", + ) + + err = <- errGetGlobal + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func randRange(min, max int) int { + return rand.IntN(max-min) + min +} diff --git a/client/cmd/get_token.go b/client/cmd/get_token.go new file mode 100644 index 0000000..7437cea --- /dev/null +++ b/client/cmd/get_token.go @@ -0,0 +1,37 @@ +package main + +import ( + "encoding/json" + "net/http" +) + +type CreateRoomJSONReturn struct { + Token string `json:"token"` + + // This respon fired when the API is failed + ErrorTitle string `json:"title"` + Errors struct { + Token []string `json:"token"` + } `json:"errors"` +} + +func getToken(path string) (CreateRoomJSONReturn, error) { + var client = &http.Client{} + req, err := http.NewRequest("POST", path, nil) + if err != nil { + return CreateRoomJSONReturn{} ,err + } + res, err := client.Do(req) + if err != nil { + return CreateRoomJSONReturn{} ,err + } + defer res.Body.Close() + + var roomData CreateRoomJSONReturn + err = json.NewDecoder(res.Body).Decode(&roomData) + if err != nil { + return CreateRoomJSONReturn{} ,err + } + + return roomData, nil +} diff --git a/client/cmd/get_webserver.go b/client/cmd/get_webserver.go new file mode 100644 index 0000000..329c979 --- /dev/null +++ b/client/cmd/get_webserver.go @@ -0,0 +1,137 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + "io" + "net/http" + "os" + "strconv" + "strings" + + "github.com/labstack/echo/v4" + "github.com/schollz/progressbar/v3" +) + +func createWebserver(port int, token string) { + reader := bufio.NewReader(os.Stdin) + isInProgress := false + e := echo.New() + e.HideBanner = true + e.HidePort = true + + e.POST("/receive", func (c echo.Context) error { + // Set keep alive + c.Response().Header().Set("Connection", "Keep-Alive") + c.Response().Header().Set("Keep-Alive", "timeout=5,max=0") + + // If there's upload on progress + if isInProgress { + return c.JSON(http.StatusBadRequest, map[string]string{ + "message": "There's some process on receiver server", + }) + } + + // Get query token + if queryToken := c.QueryParam("token"); queryToken != token { + return c.JSON(http.StatusBadRequest, map[string]string{ + "message": "Empty or invalid token!", + }) + } + + // Get file + file, err := c.FormFile("file") + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{ + "message": "Missing file from FormData", + }) + } + + // Prompting + isInProgress = true + fmt.Println("\nIncoming file:", file.Filename) + fmt.Print("Accept? [(Y)es/(N)o] -> ") + prompt, err := reader.ReadString('\n') + if err != nil { + return validateInternalError(c, err) + } + prompt = strings.Replace(prompt, "\n", "", -1) + if strings.ToLower(prompt) != "y" { + isInProgress = false + return c.JSON(http.StatusUnauthorized, map[string]string{ + "message": "File declined from receiver", + }) + } + + // Validate if the data can be override or not + currentPath, err := os.Getwd() + if err != nil { + return validateInternalError(c, err) + } + currentPath += "/"+file.Filename + if fileStatus, _ := os.Stat(currentPath); fileStatus != nil { + fmt.Print("There's duplicate file. Action? [(Y)es/(N)o/(R)ename] -> ") + prompt, err = reader.ReadString('\n') + if err != nil { + return validateInternalError(c, err) + } + prompt = strings.Replace(prompt, "\n", "", -1) + if strings.ToLower(prompt) == "n" { + isInProgress = false + return c.JSON(http.StatusUnauthorized, map[string]string{ + "message": "File declined from receiver", + }) + } + if strings.ToLower(prompt) == "r" { + fmt.Print("Change filename ["+file.Filename+"] -> ") + prompt, err = reader.ReadString('\n') + if err != nil { + return validateInternalError(c, err) + } + prompt = strings.Replace(prompt, "\n", "", -1) + if prompt == file.Filename { + fmt.Println("Canceled. Duplicate file.") + isInProgress = false + return c.JSON(http.StatusUnauthorized, map[string]string{ + "message": "Duplicate file from receiver", + }) + } + file.Filename = prompt + } + } + + // File scanning + src, err := file.Open() + if err != nil { + return validateInternalError(c, err) + } + dst, err := os.Create(file.Filename) + if err != nil { + return validateInternalError(c, err) + } + defer src.Close() + defer dst.Close() + + // Copy file + bar := progressbar.DefaultBytes(file.Size, file.Filename) + if _, err = io.Copy(io.MultiWriter(dst, bar), src); err != nil { + return validateInternalError(c, err) + } + + // Complete + isInProgress = false + return c.JSON(http.StatusOK, map[string]string{ + "message": "ok", + }) + }) + + e.Logger.Fatal(e.Start("0.0.0.0:"+strconv.Itoa(port))) + errGetGlobal <- errors.New("Webserver Closed") +} + +func validateInternalError(c echo.Context, err error) error { + return c.JSON(http.StatusInternalServerError, map[string]string{ + "message": err.Error(), + }) +} diff --git a/client/cmd/main.go b/client/cmd/main.go new file mode 100644 index 0000000..96ee14c --- /dev/null +++ b/client/cmd/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "flag" + "fmt" + "os" +) + +const subCmdHelpMeesage string = ` +Subcommand: + auth [--url=uri] [--token=random_string] + subcommand + Authenticate client to broker server + get + subcommand + Create instance for retriving file from sender + send + subcommand + Send file to reciever instance` + +func main() { + help := flag.Bool("help", false, "Print this message") + flag.Parse() + + args := flag.Args() + if len(os.Args) == 1 { + printUsage() + } + + if *help { + printUsage() + } + + cmd, args := args[0], args[1:] + switch(cmd) { + case "auth": + AuthCommand(args) + case "get": + GetCommand(args) + case "send": + SendCommand(args) + default: + printUsage() + } +} + +func printUsage() { + fmt.Print("Command: mdrop [options]\n\n") + flag.Usage() + fmt.Println(subCmdHelpMeesage) + os.Exit(1) +} diff --git a/client/cmd/send.go b/client/cmd/send.go new file mode 100644 index 0000000..8bc1c37 --- /dev/null +++ b/client/cmd/send.go @@ -0,0 +1,118 @@ +package main + +import ( + "flag" + "fmt" + "io" + "os" + "os/signal" + "syscall" + + internal "github.com/mplus-oss/mdrop/client/internal" + "golang.org/x/term" +) + +func SendCommand(args []string) { + flag := flag.NewFlagSet("mdrop send", flag.ExitOnError) + var ( + help = flag.Bool("help", false, "Print this message") + localPort = flag.Int("localPort", 6000, "Specified reciever port on local.") + ) + flag.Parse(args) + + file := flag.Arg(0) + if *help || file == "" { + fmt.Println("Command: mdrop send [options] ") + flag.Usage() + os.Exit(1) + } + + var c internal.ConfigFile + err := c.ParseConfig(&c) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // Token Prompt + fmt.Print("Enter Token: ") + token, err := readToken() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // Authenticating + fmt.Println("Authenticating...") + joinData, err := sendToken(c, string(token)) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + fmt.Println("Connecting to tunnel...") + go StartShellTunnel(false, c, *localPort, joinData.Port) + err = <- sshErrGlobal + if err != nil { + exitSendStrategy(err) + } + + // Try to send data to reciever + fmt.Println("Authenticated. Sending file...") + uri := fmt.Sprintf( + "http://localhost:%v/receive?token=%v", + *localPort, + token, + ) + val := map[string]io.Reader{ + "file": mustOpen(file), + } + err = Upload(uri, val) + if err != nil { + exitSendStrategy(err) + } + fmt.Println("Success!") + + pid := <- sshPidGlobal + err = KillShell(pid) + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func exitSendStrategy(err error) { + fmt.Println(err) + pid := <- sshPidGlobal + err = KillShell(pid) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + os.Exit(1) +} + +func readToken() (string, error) { + stdin := int(syscall.Stdin) + oldState, err := term.GetState(stdin) + if err != nil { + return "", err + } + defer term.Restore(stdin, oldState) + + sigch := make(chan os.Signal, 1) + signal.Notify(sigch, os.Interrupt) + go func() { + for range sigch { + term.Restore(stdin, oldState) + os.Exit(1) + } + }() + + token, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + return "", err + } + + return string(token), nil +} diff --git a/client/cmd/send_token.go b/client/cmd/send_token.go new file mode 100644 index 0000000..ef209dd --- /dev/null +++ b/client/cmd/send_token.go @@ -0,0 +1,62 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + + internal "github.com/mplus-oss/mdrop/client/internal" +) + +type JoinRoomJSONReturn struct { + Message string `json:"message"` + Port int `json:"port"` + + // This respon fired when the API is failed + ErrorTitle string `json:"title"` + Errors struct { + Port []string `json:"port"` + } `json:"errors"` +} + +func sendToken(c internal.ConfigFile, token string) (JoinRoomJSONReturn, error) { + // Decrypt token first + client := &http.Client{} + path := fmt.Sprintf( + "%v/room/join?token=%v", + c.URL, + token, + ) + req, err := http.NewRequest("POST", path, nil) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + res, err := client.Do(req) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + defer res.Body.Close() + + var joinData JoinRoomJSONReturn + err = json.NewDecoder(res.Body).Decode(&joinData) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if res.StatusCode != 200 { + var msg string + if joinData.ErrorTitle == "" { + msg = joinData.Message + } else { + msg = joinData.ErrorTitle + } + fmt.Println("Error:", msg) + os.Exit(1) + } + + return joinData, err +} diff --git a/client/cmd/send_upload.go b/client/cmd/send_upload.go new file mode 100644 index 0000000..b7d34a6 --- /dev/null +++ b/client/cmd/send_upload.go @@ -0,0 +1,62 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "os" + + "github.com/schollz/progressbar/v3" +) + +func Upload(url string, values map[string]io.Reader) (err error) { + var b bytes.Buffer + client := &http.Client{} + w := multipart.NewWriter(&b) + for key, r := range values { + var fw io.Writer + if x, ok := r.(io.Closer); ok { + defer x.Close() + } + if x, ok := r.(*os.File); ok { + if fw, err = w.CreateFormFile(key, x.Name()); err != nil { + return + } + } + fileInfo, err := r.(*os.File).Stat() + if err != nil { + return err + } + bar := progressbar.DefaultBytes(fileInfo.Size(), fileInfo.Name()) + if _, err = io.Copy(io.MultiWriter(fw, bar), r); err != nil { + return err + } + + } + w.Close() + + req, err := http.NewRequest("POST", url, &b) + if err != nil { + return + } + req.Header.Set("Content-Type", w.FormDataContentType()) + res, err := client.Do(req) + if err != nil { + return + } + + if res.StatusCode != http.StatusOK { + err = fmt.Errorf("Bad status: %s", res.Status) + } + return +} + +func mustOpen(f string) *os.File { + r, err := os.Open(f) + if err != nil { + panic(err) + } + return r +} diff --git a/client/cmd/shell.go b/client/cmd/shell.go new file mode 100644 index 0000000..4ec92bd --- /dev/null +++ b/client/cmd/shell.go @@ -0,0 +1,67 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + "os/exec" + "strconv" + "strings" + + "github.com/mplus-oss/mdrop/client/internal" +) + +var sshErrGlobal chan error = make(chan error) +var sshPidGlobal chan int = make(chan int) + +func StartShellTunnel(isRemote bool, c internal.ConfigFile, localPort int, remotePort int) { + args := strings.Split(internal.GenerateSSHArgs(isRemote, c, localPort, remotePort), " ") + cmd := exec.Command("ssh", args...) + + stdout, err := cmd.StdoutPipe() + stderr, err := cmd.StderrPipe() + if err != nil { + sshErrGlobal <- err + } + cmd.Start() + + go func() { + s := bufio.NewScanner(stdout) + s.Split(bufio.ScanLines) + for s.Scan() { + m := s.Text() + if strings.Contains(m, "Connected!") { + sshErrGlobal <- nil + } + } + }() + + go func() { + s := bufio.NewScanner(stderr) + s.Split(bufio.ScanLines) + for s.Scan() { + m := s.Text() + // False flag #1 + if strings.Contains(m, "chdir") || strings.Contains(m, "Permanently added") { + continue + } + if strings.Contains(m, "remote port forwarding failed") { + sshErrGlobal <- errors.New("Duplicate remote on bridge server") + } + fmt.Println(m) + } + }() + + sshPidGlobal <- cmd.Process.Pid + cmd.Wait() +} + +func KillShell(pid int) error { + cmd := exec.Command("kill", "-9", strconv.Itoa(pid)) + err := cmd.Start() + if err != nil { + return err + } + cmd.Wait() + return nil +} diff --git a/client/go.mod b/client/go.mod new file mode 100644 index 0000000..1dac793 --- /dev/null +++ b/client/go.mod @@ -0,0 +1,21 @@ +module github.com/mplus-oss/mdrop/client + +go 1.22.5 + +require ( + github.com/labstack/echo/v4 v4.12.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/schollz/progressbar/v2 v2.15.0 // indirect + github.com/schollz/progressbar/v3 v3.14.4 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/client/go.sum b/client/go.sum new file mode 100644 index 0000000..15f8c69 --- /dev/null +++ b/client/go.sum @@ -0,0 +1,45 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= +github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= +github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/schollz/progressbar/v2 v2.15.0 h1:dVzHQ8fHRmtPjD3K10jT3Qgn/+H+92jhPrhmxIJfDz8= +github.com/schollz/progressbar/v2 v2.15.0/go.mod h1:UdPq3prGkfQ7MOzZKlDRpYKcFqEMczbD7YmbPgpzKMI= +github.com/schollz/progressbar/v3 v3.14.4 h1:W9ZrDSJk7eqmQhd3uxFNNcTr0QL+xuGNI9dEMrw0r74= +github.com/schollz/progressbar/v3 v3.14.4/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/client/internal/rw_config_file.go b/client/internal/rw_config_file.go new file mode 100644 index 0000000..b32c8f3 --- /dev/null +++ b/client/internal/rw_config_file.go @@ -0,0 +1,93 @@ +package internal + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "os" +) + +var ( + ConfigFileLocation string +) + +type ConfigFileTunnel struct { + Host string `json:"host"` + Port int `json:"port"` +} + +type ConfigFile struct { + Token string `json:"token"` + URL string `json:"url"` + Tunnel ConfigFileTunnel `json:"tunnel"` +} + +func init() { + var err error + + ConfigFileLocation, err = os.UserHomeDir() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + ConfigFileLocation += "/.mdrop" +} + +func (c ConfigFile) WriteConfig() error { + fmt.Println("Writing config file...") + if CheckConfigFileExist() { + err := os.Remove(GetConfigPath()) + if err != nil { + return err + } + } + + strJsonByte, err := json.Marshal(c) + if err != nil { + return err + } + strConfig := base64.StdEncoding.EncodeToString(strJsonByte) + if c.Token != "" { + strConfig += " " + base64.RawStdEncoding.EncodeToString([]byte(c.Token)) + } + err = os.WriteFile(ConfigFileLocation, []byte(strConfig), 0644) + if err != nil { + return err + } + + fmt.Println("Config file is saved on", ConfigFileLocation) + return nil +} + +func (c ConfigFile) ParseConfig(conf* ConfigFile) (error) { + if !CheckConfigFileExist() { + return errors.New("No config file on local. Please log in first.") + } + file, err := os.ReadFile(ConfigFileLocation) + if err != nil { + return err + } + + confByte, err := base64.StdEncoding.DecodeString(string(file)) + if err != nil { + return err + } + err = json.Unmarshal(confByte, &conf) + if err != nil { + return err + } + + return nil +} + +func GetConfigPath() string { + return ConfigFileLocation +} + +func CheckConfigFileExist() bool { + if _, err := os.Stat(ConfigFileLocation); err != nil { + return false + } + return true +} diff --git a/client/internal/shell.go b/client/internal/shell.go new file mode 100644 index 0000000..d4eb923 --- /dev/null +++ b/client/internal/shell.go @@ -0,0 +1,36 @@ +package internal + +import ( + "fmt" +) + +func GenerateSSHArgs(isRemote bool, c ConfigFile, localPort int, remotePort int) string { + args := fmt.Sprintf( + "-p %v -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no tunnel@%v", + c.Tunnel.Port, + c.Tunnel.Host, + ) + + flag := [4]int{} + remoteFlag := "" + flag[2] = remotePort + if isRemote { + remoteFlag = "-R" + flag[0] = remotePort + flag[1] = localPort + } else { + remoteFlag = "-L" + flag[1] = remotePort + flag[0] = localPort + } + + args = fmt.Sprintf( + "%v %v:127.0.0.1:%v %v %v", + remoteFlag, + flag[0], + flag[1], + args, + flag[2], + ) + return args +} diff --git a/server/Dockerfile b/server/Dockerfile index 1f5e569..9f79b54 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,18 +1,19 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine as builder +FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine as build WORKDIR /app COPY . . RUN set -ex; \ - dotnet publish -c Release; \ - mv /app/MDrop.Broker/bin/Release/net8.0/linux-x64/publish /app/; + dotnet restore; \ + dotnet publish --no-restore -c Release -o ./Application -FROM docker.io/library/alpine:latest as runner -COPY --from=builder /app/publish /app +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine-composite WORKDIR /app +COPY --from=build /app/Application /app +COPY --from=build /app/*.sh /app/ + RUN set -ex; \ apk update; \ - apk add gcc gcompat icu-dev icu-libs musl-dev tzdata; \ - rm appsettings.*.json; + apk add openssl; -CMD [ "/app/MDrop.Broker" ] +ENTRYPOINT [ "/app/entrypoint.sh" ] diff --git a/server/MDrop.Broker/Constant.cs b/server/MDrop.Broker/Constant.cs index d5df8c7..e97e193 100644 --- a/server/MDrop.Broker/Constant.cs +++ b/server/MDrop.Broker/Constant.cs @@ -3,6 +3,11 @@ namespace MDrop.Broker; public static class Constant { + public static readonly string TunnelHost = Environment.GetEnvironmentVariable("TUNNEL_HOST") ?? "127.0.0.1"; + public static readonly int TunnelPort = int.Parse(Environment.GetEnvironmentVariable("TUNNEL_PORT") ?? "2222"); public static readonly string PrivateModeToken = Environment.GetEnvironmentVariable("PRIVATE_MODE_TOKEN") ?? ""; - public static X509Certificate2 Certificate = X509Certificate2.CreateFromPemFile("cert.pem", "prikey.pem"); + public static X509Certificate2 Certificate = X509Certificate2.CreateFromEncryptedPemFile( + "cert.pem", + Environment.GetEnvironmentVariable("X509_CERTIFICATE_PASSWORD") ?? "", + "prikey.pem"); } diff --git a/server/MDrop.Broker/Controllers/VerifyController.cs b/server/MDrop.Broker/Controllers/VerifyController.cs index dc22093..ce28db3 100644 --- a/server/MDrop.Broker/Controllers/VerifyController.cs +++ b/server/MDrop.Broker/Controllers/VerifyController.cs @@ -14,7 +14,8 @@ public ActionResult VerifyClientOnPrivateMode( if (Constant.PrivateModeToken == "") { response.Message = "This broker is public mode. Refusing."; - return BadRequest(response); + response.IsPublic = true; + return Ok(response); } if (Constant.PrivateModeToken != token) @@ -31,5 +32,14 @@ public class VerifyReturnJson { [JsonPropertyName("message")] public string Message { get; set; } = ""; + + [JsonPropertyName("tunnelHost")] + public string TunnelHost { get; set; } = Constant.TunnelHost; + + [JsonPropertyName("tunnelPort")] + public int TunnelPort { get; set; } = Constant.TunnelPort; + + [JsonPropertyName("isPublic")] + public bool IsPublic { get; set; } = false; } } diff --git a/server/MDrop.Broker/MDrop.Broker.csproj b/server/MDrop.Broker/MDrop.Broker.csproj index 8206a41..e7c56f2 100644 --- a/server/MDrop.Broker/MDrop.Broker.csproj +++ b/server/MDrop.Broker/MDrop.Broker.csproj @@ -1,29 +1,20 @@ - Exe net8.0 enable enable - bin\Debug\ DEBUG;TRACE true full - false - Exe net8.0 enable enable - bin\Release\ TRACE false none - true - true - linux-x64 - true diff --git a/server/cert.sh b/server/cert.sh deleted file mode 100755 index 145e680..0000000 --- a/server/cert.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -# Generate RS2048A Private Key -openssl genrsa -out prikey.pem 2048 - -# Generate Public Key -openssl rsa -in prikey.pem -pubout -out pubkey.pem - -# Generate Certificate for 1 year -openssl req -new -x509 -key prikey.pem -out cert.pem -days 365 \ - -subj "/CN=mplus.software/OU=Mplus Software/O=Mplus DevOps Team/L=South Jakarta/ST=Greater Jakarta/C=ID" - -# Move the cert.pem on development folder -mv *pem ./MDrop.Broker diff --git a/server/docker-compose.yaml b/server/docker-compose.yaml new file mode 100644 index 0000000..3a9014a --- /dev/null +++ b/server/docker-compose.yaml @@ -0,0 +1,14 @@ +# This docker compose for developing image tunnel + +services: + mdrop_broker: + container_name: mdrop_broker + build: + context: . + restart: unless-stopped + environment: + PRIVATE_MODE_TOKEN: "" + TUNNEL_HOST: 127.0.0.1 + TUNNEL_PORT: 2222 + ports: + - 5000:5000 diff --git a/server/entrypoint.sh b/server/entrypoint.sh new file mode 100755 index 0000000..01c6cab --- /dev/null +++ b/server/entrypoint.sh @@ -0,0 +1,24 @@ +#!/bin/sh +set -e + +# Set PWD to /app +cd /app + +# Set constant +RANDOM_STRING=$(tr -dc A-Za-z0-9