commit 2fcfebe25b2c0bdd98acbac6935b3f0722b801d6 Author: Laura Date: Fri Jun 20 03:27:36 2025 +0200 initial commit diff --git a/client/keys.go b/client/keys.go new file mode 100644 index 0000000..16b0d5b --- /dev/null +++ b/client/keys.go @@ -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 +} diff --git a/client/main.go b/client/main.go new file mode 100644 index 0000000..d329dec --- /dev/null +++ b/client/main.go @@ -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) +} diff --git a/client/protocol.go b/client/protocol.go new file mode 100644 index 0000000..d7ea34b --- /dev/null +++ b/client/protocol.go @@ -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 +} diff --git a/example.key b/example.key new file mode 100644 index 0000000..2d4a22d --- /dev/null +++ b/example.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHgCAQEEIQDg6JmpMO1i5nVHdHHfdJuOgDMMRqx4BynOWt68YidBjKAKBggqhkjO +PQMBB6FEA0IABI0rYdm3nt2/etmeJFS6+nyJAB9egNpFBClppW0nNjQ5nfok0J16 +GBOJDHoF/XpFv6z9BnXOlkcLgCPuMdXhFbI= +-----END EC PRIVATE KEY----- diff --git a/example.webp b/example.webp new file mode 100644 index 0000000..290f1f2 Binary files /dev/null and b/example.webp differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e509967 --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module github.com/coalaura/up + +go 1.24.2 + +require ( + github.com/coalaura/logger v1.4.4 + github.com/fasthttp/router v1.5.4 + github.com/urfave/cli/v3 v3.3.8 + github.com/valyala/fasthttp v1.62.0 + golang.org/x/crypto v0.38.0 +) + +require ( + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0822a8c --- /dev/null +++ b/go.sum @@ -0,0 +1,38 @@ +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/coalaura/logger v1.4.4 h1:x3L+hsIoLxv1r7LpmaYimydas6qRb4SeDI3/qFFmOMY= +github.com/coalaura/logger v1.4.4/go.mod h1:/BHeRXN2FNG5NHaE7nVHR2eziLSdgyzPffeCmiwXVRo= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fasthttp/router v1.5.4 h1:oxdThbBwQgsDIYZ3wR1IavsNl6ZS9WdjKukeMikOnC8= +github.com/fasthttp/router v1.5.4/go.mod h1:3/hysWq6cky7dTfzaaEPZGdptwjwx0qzTgFCKEWRjgc= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= +github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E= +github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= +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/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0= +github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/challenge.go b/internal/challenge.go new file mode 100644 index 0000000..4a3f371 --- /dev/null +++ b/internal/challenge.go @@ -0,0 +1,33 @@ +package internal + +import ( + "crypto/rand" + "encoding/base64" +) + +func FreshChallenge() (*AuthChallenge, []byte, error) { + challenge, err := random(64) + if err != nil { + return nil, nil, err + } + + token, err := random(64) + if err != nil { + return nil, nil, err + } + + return &AuthChallenge{ + Token: base64.StdEncoding.EncodeToString(token), + Challenge: base64.StdEncoding.EncodeToString(challenge), + }, challenge, nil +} + +func random(n int) ([]byte, error) { + b := make([]byte, n) + + if _, err := rand.Read(b); err != nil { + return nil, err + } + + return b, nil +} diff --git a/internal/types.go b/internal/types.go new file mode 100644 index 0000000..a059cef --- /dev/null +++ b/internal/types.go @@ -0,0 +1,38 @@ +package internal + +import ( + "time" + + "golang.org/x/crypto/ssh" +) + +type ChallengeEntry struct { + Challenge []byte + PublicKey ssh.PublicKey + Expires time.Time +} + +type SessionEntry struct { + PublicKey ssh.PublicKey + Expires time.Time +} + +type AuthRequest struct { + Public string `json:"public"` +} + +type AuthChallenge struct { + Token string `json:"token"` + Challenge string `json:"challenge"` +} + +type AuthResponse struct { + Token string `json:"token"` + Public string `json:"public"` + Format string `json:"format"` + Signature string `json:"signature"` +} + +type AuthResult struct { + Token string `json:"token"` +} diff --git a/server/connection.go b/server/connection.go new file mode 100644 index 0000000..225ee09 --- /dev/null +++ b/server/connection.go @@ -0,0 +1,12 @@ +package main + +import ( + "net" + "time" +) + +func HandleConnection(conn net.Conn) error { + time.Sleep(10 * time.Second) + + return nil +} diff --git a/server/keys.go b/server/keys.go new file mode 100644 index 0000000..b09044a --- /dev/null +++ b/server/keys.go @@ -0,0 +1,57 @@ +package main + +import ( + "crypto/rand" + "encoding/base64" + "os" + "path/filepath" + + "golang.org/x/crypto/ssh" +) + +func GetAuthorizedKeysPath() (string, error) { + home, err := os.UserHomeDir() + + if err != nil { + return "", err + } + + return filepath.Join(home, ".ssh", "authorized_keys"), nil +} + +func LoadAuthorizedKeys() (map[string]ssh.PublicKey, error) { + path, err := GetAuthorizedKeysPath() + if err != nil { + return nil, err + } + + keys := make(map[string]ssh.PublicKey) + + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + for len(data) > 0 { + pubKey, _, _, rest, err := ssh.ParseAuthorizedKey(data) + if err != nil { + return nil, err + } + + keys[string(pubKey.Marshal())] = pubKey + + data = rest + } + + return keys, nil +} + +func RandomToken(n int) (string, error) { + b := make([]byte, n) + + if _, err := rand.Read(b); err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString(b), nil +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..d02b4c8 --- /dev/null +++ b/server/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "sync" + + "github.com/coalaura/logger" + "github.com/fasthttp/router" + "github.com/valyala/fasthttp" +) + +var ( + log = logger.New().WithOptions(logger.Options{ + NoLevel: true, + }) + + challenges sync.Map + sessions sync.Map +) + +func main() { + authorized, err := LoadAuthorizedKeys() + log.MustPanic(err) + + r := router.New() + + r.POST("/request", func(ctx *fasthttp.RequestCtx) { + HandleChallengeRequest(ctx, authorized) + }) + + r.POST("/complete", func(ctx *fasthttp.RequestCtx) { + HandleCompleteRequest(ctx, authorized) + }) + + r.POST("/receive", HandleReceiveRequest) + + log.Println("Listening on :7966") + fasthttp.ListenAndServe(":7966", r.Handler) +} diff --git a/server/protocol.go b/server/protocol.go new file mode 100644 index 0000000..e0aad88 --- /dev/null +++ b/server/protocol.go @@ -0,0 +1,292 @@ +package main + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "io" + "os" + "path/filepath" + "time" + + "github.com/coalaura/up/internal" + "github.com/valyala/fasthttp" + "golang.org/x/crypto/ssh" +) + +var ( + SignatureFormats = map[string]bool{ + "ssh-ed25519": true, + "ssh-rsa": true, + "rsa-sha2-256": true, + "rsa-sha2-512": true, + "ecdsa-sha2-nistp256": true, + "ecdsa-sha2-nistp384": true, + "ecdsa-sha2-nistp521": true, + } +) + +func IsSignatureFormatValid(format string) bool { + return SignatureFormats[format] +} + +func HandleChallengeRequest(ctx *fasthttp.RequestCtx, authorized map[string]ssh.PublicKey) { + var request internal.AuthRequest + + if err := json.Unmarshal(ctx.PostBody(), &request); err != nil { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + + log.Warning("request: failed to decode request") + log.WarningE(err) + + return + } + + public, err := DecodeAndAuthorizePublicKey(request.Public, authorized) + if err != nil { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + + log.Warning("request: failed to parse/authorize public key") + log.WarningE(err) + + return + } + + challenge, raw, err := internal.FreshChallenge() + if err != nil { + ctx.SetStatusCode(fasthttp.StatusInternalServerError) + + log.Warning("request: failed to generate challenge") + log.WarningE(err) + + return + } + + challenges.Store(challenge.Token, internal.ChallengeEntry{ + Challenge: raw, + PublicKey: public, + Expires: time.Now().Add(20 * time.Second), + }) + + log.Println("new auth request") + + ctx.SetContentType("application/json") + json.NewEncoder(ctx).Encode(challenge) +} + +func HandleCompleteRequest(ctx *fasthttp.RequestCtx, authorized map[string]ssh.PublicKey) { + var response internal.AuthResponse + + if err := json.Unmarshal(ctx.PostBody(), &response); err != nil { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + + log.Warning("complete: failed to decode response") + log.WarningE(err) + + return + } + + public, err := DecodeAndAuthorizePublicKey(response.Public, authorized) + if err != nil { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + + log.Warning("complete: failed to parse/authorize public key") + log.WarningE(err) + + return + } + + entry, ok := challenges.LoadAndDelete(response.Token) + if !ok { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + + log.Warning("complete: invalid challenge token") + log.WarningE(err) + + return + } + + challenge := entry.(internal.ChallengeEntry) + + if time.Now().After(challenge.Expires) { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + + log.Warning("complete: challenge expired") + log.WarningE(err) + + return + } + + publicA := public.Marshal() + publicB := challenge.PublicKey.Marshal() + + if !bytes.Equal(publicA, publicB) { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + + log.Warning("complete: incorrect public key") + log.WarningE(err) + + return + } + + if !IsSignatureFormatValid(response.Format) { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + + log.Warning("complete: unsupported signature format") + + return + } + + signature, err := base64.StdEncoding.DecodeString(response.Signature) + if err != nil { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + + log.Warning("complete: failed to decode signature") + log.WarningE(err) + + return + } + + sig := &ssh.Signature{ + Format: response.Format, + Blob: signature, + } + + if err = public.Verify(challenge.Challenge, sig); err != nil { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + + log.Warning("complete: failed to verify signature") + log.WarningE(err) + + return + } + + token, err := RandomToken(64) + if err != nil { + ctx.SetStatusCode(fasthttp.StatusInternalServerError) + + log.Warning("complete: failed to create token") + log.WarningE(err) + + return + } + + sessions.Store(token, internal.SessionEntry{ + PublicKey: public, + Expires: time.Now().Add(5 * time.Minute), + }) + + log.Println("auth completed") + + ctx.SetContentType("application/json") + json.NewEncoder(ctx).Encode(internal.AuthResult{ + Token: token, + }) +} + +func HandleReceiveRequest(ctx *fasthttp.RequestCtx) { + token := string(ctx.Request.Header.Peek("Authorization")) + if token == "" { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + + log.Warning("receive: missing token") + + return + } + + entry, ok := sessions.LoadAndDelete(token) + if !ok { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + + log.Warning("receive: invalid token") + + return + } + + session := entry.(internal.SessionEntry) + + if time.Now().After(session.Expires) { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + + log.Warning("receive: session expired") + + return + } + + form, err := ctx.MultipartForm() + if err != nil { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + + log.Warning("receive: failed to parse multipart form") + log.WarningE(err) + + return + } + + files := form.File["file"] + if len(files) == 0 { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + + log.Warning("receive: no files received") + + return + } + + header := files[0] + name := filepath.Base(header.Filename) + + source, err := header.Open() + if err != nil { + ctx.SetStatusCode(fasthttp.StatusInternalServerError) + + log.Warning("receive: failed to open sent file") + + return + } + + defer source.Close() + + if _, err := os.Stat("files"); os.IsNotExist(err) { + os.Mkdir("files", 0755) + } + + target, err := os.OpenFile(filepath.Join("files", name), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + if err != nil { + ctx.SetStatusCode(fasthttp.StatusInternalServerError) + + log.Warning("receive: failed to open target file") + + return + } + + defer target.Close() + + if _, err := io.Copy(target, source); err != nil { + ctx.SetStatusCode(fasthttp.StatusInternalServerError) + + log.Warning("receive: failed to copy sent file") + + return + } + + ctx.SetStatusCode(fasthttp.StatusOK) +} + +func DecodeAndAuthorizePublicKey(public string, authorized map[string]ssh.PublicKey) (ssh.PublicKey, error) { + data, err := base64.StdEncoding.DecodeString(public) + if err != nil { + return nil, err + } + + key, err := ssh.ParsePublicKey(data) + if err != nil { + return nil, err + } + + if _, ok := authorized[string(key.Marshal())]; !ok { + return nil, errors.New("unauthorized key") + } + + return key, nil +} diff --git a/test.cmd b/test.cmd new file mode 100644 index 0000000..c2337db --- /dev/null +++ b/test.cmd @@ -0,0 +1,3 @@ +@echo off + +go run .\client --key example.key -f example.webp -t localhost:7966 \ No newline at end of file