mirror of
https://github.com/coalaura/up.git
synced 2025-07-18 21:53:23 +00:00
initial commit
This commit is contained in:
21
client/keys.go
Normal file
21
client/keys.go
Normal file
@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func LoadPrivateKey(path string) (ssh.Signer, error) {
|
||||
key, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signer, err := ssh.ParsePrivateKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return signer, nil
|
||||
}
|
103
client/main.go
Normal file
103
client/main.go
Normal file
@ -0,0 +1,103 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var Version = "dev"
|
||||
|
||||
func main() {
|
||||
app := &cli.Command{
|
||||
Name: "up",
|
||||
Usage: "UP client",
|
||||
Version: Version,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "key",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "private key file for authentication",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "file",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "file to upload",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "target",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "target to upload to",
|
||||
},
|
||||
},
|
||||
Action: run,
|
||||
EnableShellCompletion: true,
|
||||
UseShortOptionHandling: true,
|
||||
Suggest: true,
|
||||
}
|
||||
|
||||
if err := app.Run(context.Background(), os.Args); err != nil {
|
||||
fmt.Printf("fatal: %v\n", err)
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(_ context.Context, cmd *cli.Command) error {
|
||||
kPath := cmd.String("key")
|
||||
if kPath == "" {
|
||||
return errors.New("missing private key")
|
||||
}
|
||||
|
||||
fPath := cmd.String("file")
|
||||
if fPath == "" {
|
||||
return errors.New("missing file")
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(fPath, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %v", err)
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
target := cmd.String("target")
|
||||
if target == "" {
|
||||
return errors.New("missing target")
|
||||
}
|
||||
|
||||
if colon := strings.Index(target, ":"); colon != -1 {
|
||||
target = fmt.Sprintf("http://%s", target)
|
||||
} else {
|
||||
target = fmt.Sprintf("https://%s", target)
|
||||
}
|
||||
|
||||
private, err := LoadPrivateKey(kPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load key: %v", err)
|
||||
}
|
||||
|
||||
public := base64.StdEncoding.EncodeToString(private.PublicKey().Marshal())
|
||||
|
||||
log.Println("Requesting challenge...")
|
||||
|
||||
challenge, err := RequestChallenge(target, public)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("Completing challenge...")
|
||||
|
||||
response, err := CompleteChallenge(target, public, private, challenge)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return SendFile(target, response.Token, file)
|
||||
}
|
125
client/protocol.go
Normal file
125
client/protocol.go
Normal file
@ -0,0 +1,125 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/coalaura/up/internal"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func RequestChallenge(target, public string) (*internal.AuthChallenge, error) {
|
||||
request, err := json.Marshal(internal.AuthRequest{
|
||||
Public: public,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %v", err)
|
||||
}
|
||||
|
||||
response, err := http.Post(fmt.Sprintf("%s/request", target), "application/json", bytes.NewReader(request))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send request: %v", err)
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, errors.New(response.Status)
|
||||
}
|
||||
|
||||
var challenge internal.AuthChallenge
|
||||
|
||||
if err := json.NewDecoder(response.Body).Decode(&challenge); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
return &challenge, nil
|
||||
}
|
||||
|
||||
func CompleteChallenge(target, public string, private ssh.Signer, challenge *internal.AuthChallenge) (*internal.AuthResponse, error) {
|
||||
rawChallenge, err := base64.StdEncoding.DecodeString(challenge.Challenge)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode challenge: %v", err)
|
||||
}
|
||||
|
||||
signature, err := private.Sign(rand.Reader, rawChallenge)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign challenge: %v", err)
|
||||
}
|
||||
|
||||
request, err := json.Marshal(internal.AuthResponse{
|
||||
Token: challenge.Token,
|
||||
Public: public,
|
||||
Format: signature.Format,
|
||||
Signature: base64.StdEncoding.EncodeToString(signature.Blob),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %v", err)
|
||||
}
|
||||
|
||||
response, err := http.Post(fmt.Sprintf("%s/complete", target), "application/json", bytes.NewReader(request))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send request: %v", err)
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, errors.New(response.Status)
|
||||
}
|
||||
|
||||
var result internal.AuthResponse
|
||||
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func SendFile(target, token string, file *os.File) error {
|
||||
var buf bytes.Buffer
|
||||
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
part, err := writer.CreateFormFile("file", filepath.Base(file.Name()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create form file: %v", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(part, file); err != nil {
|
||||
return fmt.Errorf("failed to copy file: %v", err)
|
||||
}
|
||||
|
||||
writer.Close()
|
||||
|
||||
request, err := http.NewRequest("POST", fmt.Sprintf("%s/receive", target), &buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
request.Header.Set("Authorization", token)
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send request: %v", err)
|
||||
}
|
||||
|
||||
response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return errors.New(response.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user