1
0
mirror of https://github.com/coalaura/up.git synced 2025-07-18 21:53:23 +00:00

initial commit

This commit is contained in:
Laura
2025-06-20 03:27:36 +02:00
commit 2fcfebe25b
14 changed files with 787 additions and 0 deletions

21
client/keys.go Normal file
View 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
View 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
View 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
}