2025-06-20 03:27:36 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/base64"
|
|
|
|
"errors"
|
|
|
|
"io"
|
2025-06-20 16:10:38 +02:00
|
|
|
"net/http"
|
2025-06-20 03:27:36 +02:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/coalaura/up/internal"
|
2025-06-20 16:10:38 +02:00
|
|
|
"github.com/vmihailenco/msgpack/v5"
|
2025-06-20 03:27:36 +02:00
|
|
|
"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]
|
|
|
|
}
|
|
|
|
|
2025-06-20 16:10:38 +02:00
|
|
|
func HandleChallengeRequest(w http.ResponseWriter, r *http.Request, authorized map[string]ssh.PublicKey) {
|
2025-06-20 03:27:36 +02:00
|
|
|
var request internal.AuthRequest
|
|
|
|
|
2025-06-20 16:10:38 +02:00
|
|
|
if err := msgpack.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
log.Warning("request: failed to decode request")
|
|
|
|
log.WarningE(err)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
public, err := DecodeAndAuthorizePublicKey(request.Public, authorized)
|
|
|
|
if err != nil {
|
2025-06-20 16:10:38 +02:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
log.Warning("request: failed to parse/authorize public key")
|
|
|
|
log.WarningE(err)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
challenge, raw, err := internal.FreshChallenge()
|
|
|
|
if err != nil {
|
2025-06-20 16:10:38 +02:00
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
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")
|
|
|
|
|
2025-06-20 16:10:38 +02:00
|
|
|
w.Header().Set("Content-Type", "application/msgpack")
|
|
|
|
msgpack.NewEncoder(w).Encode(challenge)
|
2025-06-20 03:27:36 +02:00
|
|
|
}
|
|
|
|
|
2025-06-20 16:10:38 +02:00
|
|
|
func HandleCompleteRequest(w http.ResponseWriter, r *http.Request, authorized map[string]ssh.PublicKey) {
|
2025-06-20 03:27:36 +02:00
|
|
|
var response internal.AuthResponse
|
|
|
|
|
2025-06-20 16:10:38 +02:00
|
|
|
if err := msgpack.NewDecoder(r.Body).Decode(&response); err != nil {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
log.Warning("complete: failed to decode response")
|
|
|
|
log.WarningE(err)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
public, err := DecodeAndAuthorizePublicKey(response.Public, authorized)
|
|
|
|
if err != nil {
|
2025-06-20 16:10:38 +02:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
log.Warning("complete: failed to parse/authorize public key")
|
|
|
|
log.WarningE(err)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
entry, ok := challenges.LoadAndDelete(response.Token)
|
|
|
|
if !ok {
|
2025-06-20 16:10:38 +02:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
log.Warning("complete: invalid challenge token")
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
challenge := entry.(internal.ChallengeEntry)
|
|
|
|
|
|
|
|
if time.Now().After(challenge.Expires) {
|
2025-06-20 16:10:38 +02:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
log.Warning("complete: challenge expired")
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
publicA := public.Marshal()
|
|
|
|
publicB := challenge.PublicKey.Marshal()
|
|
|
|
|
|
|
|
if !bytes.Equal(publicA, publicB) {
|
2025-06-20 16:10:38 +02:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
log.Warning("complete: incorrect public key")
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !IsSignatureFormatValid(response.Format) {
|
2025-06-20 16:10:38 +02:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
log.Warning("complete: unsupported signature format")
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
signature, err := base64.StdEncoding.DecodeString(response.Signature)
|
|
|
|
if err != nil {
|
2025-06-20 16:10:38 +02:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
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 {
|
2025-06-20 16:10:38 +02:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
log.Warning("complete: failed to verify signature")
|
|
|
|
log.WarningE(err)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
token, err := RandomToken(64)
|
|
|
|
if err != nil {
|
2025-06-20 16:10:38 +02:00
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
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")
|
|
|
|
|
2025-06-20 16:10:38 +02:00
|
|
|
w.Header().Set("Content-Type", "application/msgpack")
|
|
|
|
msgpack.NewEncoder(w).Encode(internal.AuthResult{
|
2025-06-20 03:27:36 +02:00
|
|
|
Token: token,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2025-06-20 16:10:38 +02:00
|
|
|
func HandleReceiveRequest(w http.ResponseWriter, r *http.Request) {
|
|
|
|
token := r.Header.Get("Authorization")
|
2025-06-20 03:27:36 +02:00
|
|
|
if token == "" {
|
2025-06-20 16:10:38 +02:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
log.Warning("receive: missing token")
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
entry, ok := sessions.LoadAndDelete(token)
|
|
|
|
if !ok {
|
2025-06-20 16:10:38 +02:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
log.Warning("receive: invalid token")
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
session := entry.(internal.SessionEntry)
|
|
|
|
|
|
|
|
if time.Now().After(session.Expires) {
|
2025-06-20 16:10:38 +02:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
log.Warning("receive: session expired")
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-06-20 16:10:38 +02:00
|
|
|
reader, err := r.MultipartReader()
|
2025-06-20 03:27:36 +02:00
|
|
|
if err != nil {
|
2025-06-20 16:10:38 +02:00
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
2025-06-20 16:10:38 +02:00
|
|
|
log.Warning("receive: failed to open multipart form")
|
2025-06-20 03:27:36 +02:00
|
|
|
log.WarningE(err)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-06-20 16:10:38 +02:00
|
|
|
part, err := reader.NextPart()
|
|
|
|
if err != nil {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
2025-06-20 16:10:38 +02:00
|
|
|
log.Warning("receive: failed to read multipart form")
|
|
|
|
log.WarningE(err)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-06-20 16:10:38 +02:00
|
|
|
if part.FormName() != "file" {
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
2025-06-20 16:10:38 +02:00
|
|
|
log.Warning("receive: invalid multipart part")
|
|
|
|
log.WarningE(err)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-06-20 16:10:38 +02:00
|
|
|
name := filepath.Base(part.FileName())
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
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 {
|
2025-06-20 16:10:38 +02:00
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
log.Warning("receive: failed to open target file")
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
defer target.Close()
|
|
|
|
|
2025-06-20 16:10:38 +02:00
|
|
|
if _, err := io.Copy(target, part); err != nil {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
2025-06-20 03:27:36 +02:00
|
|
|
|
|
|
|
log.Warning("receive: failed to copy sent file")
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-06-20 16:10:38 +02:00
|
|
|
w.WriteHeader(http.StatusOK)
|
2025-06-20 03:27:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|