Skip to content

Commit

Permalink
chore!(improv): add base client app and improving broker response (#3)
Browse files Browse the repository at this point in the history
* feat(dev/cli): initialize cli

* refactor(dev/cli): refactoring flag system

* refactor(dev/cli): simplify print usage

* feat(dev/cli): add custom http handler for verifying

* feat(dev/server): add isPublic property on variable

* feat(dev/cli): add status code validation for response

* feat(dev/cli): add status code validation for response

* feat(dev/cli): finalize auth command

* feat(dev/cli): create base command for get

* refactor(dev/cli): formatting auth and add remove default value

* feat(dev/cli): create http request for get token before spawning echo instance

* feat(dev/cli): implementing webserver on get command

* refactor(dev/cli): pruning one file

* feat(dev/image): add docker compose

* fix(dev/server): modify dockerfile

* fix(dev/server): pruning dockerfile and modify csproj

* feat(dev/cli): add message on command before sending file

* feat(dev/cli): add feature to upload

* feat(dev/cli): add progress while uploading

* feat(dev/cli): add slider for progressbar

* feat(dev/cli): add validator for duplicate file

* feat(dev/cli): set response header for keep-alive

* refactor(dev/cli): remove unnecessary file

* feat(dev/cli): create new base command for send

* refactor(dev/server): add tunnel metadata on verify

* feat(dev/server): improving entrypoint for creating base random passowrd for salting certificate (soon)

* refactor(dev/server): add environment on docker compose

* fix(dev/cli): change parsing and add missing parameter

* fix(dev/cli): fixing wrong pinning json

* feat(dev/cli): add ssh tunnel generator args and implement on get variable

* feat(dev/cli): make function to upload file
fix(dev/cli): create command for gracefully kill tunnel after transfer

* fix(dev/cli): fix error when ssh tunnel is up

* feat(dev/cli): progress bar for uploading
fix(dev/cli): make receiver only retrive one request from sender
  • Loading branch information
ikr4-m authored Jul 23, 2024
1 parent 374f218 commit 91181f7
Show file tree
Hide file tree
Showing 21 changed files with 974 additions and 33 deletions.
89 changes: 89 additions & 0 deletions client/cmd/auth.go
Original file line number Diff line number Diff line change
@@ -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"`
}
81 changes: 81 additions & 0 deletions client/cmd/get.go
Original file line number Diff line number Diff line change
@@ -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
}
37 changes: 37 additions & 0 deletions client/cmd/get_token.go
Original file line number Diff line number Diff line change
@@ -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
}
137 changes: 137 additions & 0 deletions client/cmd/get_webserver.go
Original file line number Diff line number Diff line change
@@ -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(),
})
}
Loading

0 comments on commit 91181f7

Please sign in to comment.