@ -176,20 +176,41 @@ func (h *Handlers) HandleAPIGetApp() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleAPICreateApp returns a handler that creates a new app.
|
// apiCreateRequest is the JSON body for creating an app via the API.
|
||||||
func (h *Handlers) HandleAPICreateApp() http.HandlerFunc {
|
type apiCreateRequest struct {
|
||||||
type createRequest struct {
|
Name string `json:"name"`
|
||||||
Name string `json:"name"`
|
RepoURL string `json:"repoUrl"`
|
||||||
RepoURL string `json:"repoUrl"`
|
Branch string `json:"branch"`
|
||||||
Branch string `json:"branch"`
|
DockerfilePath string `json:"dockerfilePath"`
|
||||||
DockerfilePath string `json:"dockerfilePath"`
|
DockerNetwork string `json:"dockerNetwork"`
|
||||||
DockerNetwork string `json:"dockerNetwork"`
|
NtfyTopic string `json:"ntfyTopic"`
|
||||||
NtfyTopic string `json:"ntfyTopic"`
|
SlackWebhook string `json:"slackWebhook"`
|
||||||
SlackWebhook string `json:"slackWebhook"`
|
}
|
||||||
|
|
||||||
|
// validateCreateRequest validates the fields of an API create app request.
|
||||||
|
// Returns an error message string or empty string if valid.
|
||||||
|
func validateCreateRequest(req *apiCreateRequest) string {
|
||||||
|
if req.Name == "" || req.RepoURL == "" {
|
||||||
|
return "name and repo_url are required"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nameErr := validateAppName(req.Name)
|
||||||
|
if nameErr != nil {
|
||||||
|
return "invalid app name: " + nameErr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
repoURLErr := validateRepoURL(req.RepoURL)
|
||||||
|
if repoURLErr != nil {
|
||||||
|
return "invalid repository URL: " + repoURLErr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleAPICreateApp returns a handler that creates a new app.
|
||||||
|
func (h *Handlers) HandleAPICreateApp() http.HandlerFunc {
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
return func(writer http.ResponseWriter, request *http.Request) {
|
||||||
var req createRequest
|
var req apiCreateRequest
|
||||||
|
|
||||||
decodeErr := json.NewDecoder(request.Body).Decode(&req)
|
decodeErr := json.NewDecoder(request.Body).Decode(&req)
|
||||||
if decodeErr != nil {
|
if decodeErr != nil {
|
||||||
@ -200,27 +221,9 @@ func (h *Handlers) HandleAPICreateApp() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Name == "" || req.RepoURL == "" {
|
if errMsg := validateCreateRequest(&req); errMsg != "" {
|
||||||
h.respondJSON(writer, request,
|
h.respondJSON(writer, request,
|
||||||
map[string]string{"error": "name and repo_url are required"},
|
map[string]string{"error": errMsg},
|
||||||
http.StatusBadRequest)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nameErr := validateAppName(req.Name)
|
|
||||||
if nameErr != nil {
|
|
||||||
h.respondJSON(writer, request,
|
|
||||||
map[string]string{"error": "invalid app name: " + nameErr.Error()},
|
|
||||||
http.StatusBadRequest)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
repoURLErr := validateRepoURL(req.RepoURL)
|
|
||||||
if repoURLErr != nil {
|
|
||||||
h.respondJSON(writer, request,
|
|
||||||
map[string]string{"error": "invalid repository URL: " + repoURLErr.Error()},
|
|
||||||
http.StatusBadRequest)
|
http.StatusBadRequest)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
6
internal/handlers/export_test.go
Normal file
6
internal/handlers/export_test.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
// ValidateRepoURLForTest exports validateRepoURL for testing.
|
||||||
|
func ValidateRepoURLForTest(repoURL string) error {
|
||||||
|
return validateRepoURL(repoURL)
|
||||||
|
}
|
||||||
@ -780,6 +780,7 @@ func TestHandleVolumeAddValidatesPaths(t *testing.T) {
|
|||||||
// Check if volume was created by listing volumes
|
// Check if volume was created by listing volumes
|
||||||
volumes, _ := createdApp.GetVolumes(context.Background())
|
volumes, _ := createdApp.GetVolumes(context.Background())
|
||||||
found := false
|
found := false
|
||||||
|
|
||||||
for _, v := range volumes {
|
for _, v := range volumes {
|
||||||
if v.HostPath == tt.hostPath && v.ContainerPath == tt.containerPath {
|
if v.HostPath == tt.hostPath && v.ContainerPath == tt.containerPath {
|
||||||
found = true
|
found = true
|
||||||
|
|||||||
@ -20,6 +20,16 @@ var (
|
|||||||
// Only the "git" user is allowed, as that is the standard for SSH deploy keys.
|
// Only the "git" user is allowed, as that is the standard for SSH deploy keys.
|
||||||
var scpLikeRepoRe = regexp.MustCompile(`^git@[a-zA-Z0-9._-]+:.+$`)
|
var scpLikeRepoRe = regexp.MustCompile(`^git@[a-zA-Z0-9._-]+:.+$`)
|
||||||
|
|
||||||
|
// allowedRepoSchemes lists the URL schemes accepted for repository URLs.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // package-level constant map parsed once
|
||||||
|
var allowedRepoSchemes = map[string]bool{
|
||||||
|
"https": true,
|
||||||
|
"http": true,
|
||||||
|
"ssh": true,
|
||||||
|
"git": true,
|
||||||
|
}
|
||||||
|
|
||||||
// validateRepoURL checks that the given repository URL is valid and uses an allowed scheme.
|
// validateRepoURL checks that the given repository URL is valid and uses an allowed scheme.
|
||||||
func validateRepoURL(repoURL string) error {
|
func validateRepoURL(repoURL string) error {
|
||||||
if strings.TrimSpace(repoURL) == "" {
|
if strings.TrimSpace(repoURL) == "" {
|
||||||
@ -41,17 +51,17 @@ func validateRepoURL(repoURL string) error {
|
|||||||
return errRepoURLScheme
|
return errRepoURLScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse as standard URL
|
return validateParsedRepoURL(repoURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateParsedRepoURL validates a standard URL-format repository URL.
|
||||||
|
func validateParsedRepoURL(repoURL string) error {
|
||||||
parsed, err := url.Parse(repoURL)
|
parsed, err := url.Parse(repoURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errRepoURLInvalid
|
return errRepoURLInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must have a recognized scheme
|
if !allowedRepoSchemes[strings.ToLower(parsed.Scheme)] {
|
||||||
switch strings.ToLower(parsed.Scheme) {
|
|
||||||
case "https", "http", "ssh", "git":
|
|
||||||
// OK
|
|
||||||
default:
|
|
||||||
return errRepoURLInvalid
|
return errRepoURLInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
package handlers
|
package handlers_test
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/upaas/internal/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
func TestValidateRepoURL(t *testing.T) {
|
func TestValidateRepoURL(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@ -43,13 +47,13 @@ func TestValidateRepoURL(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
err := validateRepoURL(tc.url)
|
err := handlers.ValidateRepoURLForTest(tc.url)
|
||||||
if tc.wantErr && err == nil {
|
if tc.wantErr && err == nil {
|
||||||
t.Errorf("validateRepoURL(%q) = nil, want error", tc.url)
|
t.Errorf("ValidateRepoURLForTest(%q) = nil, want error", tc.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !tc.wantErr && err != nil {
|
if !tc.wantErr && err != nil {
|
||||||
t.Errorf("validateRepoURL(%q) = %v, want nil", tc.url, err)
|
t.Errorf("ValidateRepoURLForTest(%q) = %v, want nil", tc.url, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user