diff --git a/src/challenge.go b/src/challenge.go index 48971bb..1acfb8f 100644 --- a/src/challenge.go +++ b/src/challenge.go @@ -32,6 +32,7 @@ type Challenge struct { Location string `json:"location"` Identifier any `json:"identifier"` } `json:"dockerfile_locations,omitempty"` + HandoutDir string `json:"handout_dir,omitempty"` } type ChallengeConfig struct { @@ -60,7 +61,10 @@ func jsonFormatChallengeConfig(challengeConfig *ChallengeConfig) string { } func filesDir(challengeConfig *ChallengeConfig) string { - return "k8s/files" + if challengeConfig.Challenge.HandoutDir != "" { + return challengeConfig.Challenge.HandoutDir + } + return "handout" } func filesDirPath(challengeConfig *ChallengeConfig) string { @@ -69,6 +73,16 @@ func filesDirPath(challengeConfig *ChallengeConfig) string { return challengeConfig.Path + "/" + filesDir } +// zipRootDir returns the "_" name shared by the handout +// zip's file name and the folder it extracts into. +func zipRootDir(challengeConfig *ChallengeConfig) string { + return challengeConfig.Challenge.Category + "_" + challengeConfig.Challenge.Slug +} + +func zipFileName(challengeConfig *ChallengeConfig) string { + return zipRootDir(challengeConfig) + ".zip" +} + func getCategoryName(challengeConfig *ChallengeConfig, mappingMap MappingMap) string { // Get the category from the challenge config category := challengeConfig.Challenge.Category diff --git a/src/ctfd-challenges.go b/src/ctfd-challenges.go index b188dfa..c9f8980 100644 --- a/src/ctfd-challenges.go +++ b/src/ctfd-challenges.go @@ -142,56 +142,73 @@ func deleteUploadedCTFdChallenge(challengeName string) error { } func uploadCTFdChallengeFile(id int, challenge *ChallengeConfig, client *ctfd.Client) (int, error) { - // Get files - files, err := getGithubDirContents(getGithubRepo(), getGithubBranch(), filesDirPath(challenge)) + // Get files (recursively, so nested handout subdirectories are included) + remoteFiles, err := getGithubDirContentsRecursive(getGithubRepo(), getGithubBranch(), filesDirPath(challenge)) if err != nil { log.Printf("Error getting directory contents (err): %s\n", err) return 0, err } filesContent := make([]*ctfd.InputFile, 0) - if files != nil && len(files) > 0 { - for _, file := range files { - if file.GetName() == ".gitignore" || file.GetName() == ".gitkeep" { - continue - } + for _, remoteFile := range remoteFiles { + data, err := getGithubFileBytes(getGithubRepo(), getGithubBranch(), remoteFile.Path) + if err != nil { + log.Printf("Error getting file content: %s\n", err) + continue + } - // Get file content - path := (filesDirPath(challenge) + "/" + file.GetName()) - data, error := getGithubFileBytes(getGithubRepo(), getGithubBranch(), path) + if data == nil { + continue + } - if error != nil { - log.Printf("Error getting file content: %s\n", error) - } + filesContent = append(filesContent, &ctfd.InputFile{ + Name: remoteFile.RelPath, + Content: []byte(*data), + }) + } - // Convert to format - if error == nil && data != nil && file.GetName() != "" { - filesContent = append(filesContent, &ctfd.InputFile{ - Name: file.GetName(), - Content: []byte(*data), - }) - } - } + if len(filesContent) == 0 { + return id, nil } - // Print files - if len(filesContent) > 0 { + // Only bundle into a zip when there's more than one file; a single file + // is uploaded as-is (whether or not it's already a zip). + if len(filesContent) > 1 { + entries := make([]ZipEntry, 0, len(filesContent)) for _, file := range filesContent { - log.Printf("File: %s\n", file.Name) - // log.Printf("Content: %s\n", string(file.Content)) + entries = append(entries, ZipEntry{ + Name: file.Name, + Content: file.Content, + }) } - } - // Upload files - if len(filesContent) != 0 { - _, err = client.PostFiles(&ctfd.PostFilesParams{ - Files: filesContent, - Challenge: &id, - }) + zipped, err := BuildZip(entries, zipRootDir(challenge)) if err != nil { - log.Printf("Error uploading files: %s\n", err) + log.Printf("Error zipping handout files: %s\n", err) return 0, err } + filesContent = []*ctfd.InputFile{ + { + Name: zipFileName(challenge), + Content: zipped, + }, + } + } + + // Print files + for _, file := range filesContent { + log.Printf("File: %s\n", file.Name) + // log.Printf("Content: %s\n", string(file.Content)) + } + + // Upload files + _, err = client.PostFiles(&ctfd.PostFilesParams{ + Files: filesContent, + Challenge: &id, + }) + if err != nil { + log.Printf("Error uploading files: %s\n", err) + return 0, err } return id, nil diff --git a/src/github.go b/src/github.go index d4d9365..5f92f93 100644 --- a/src/github.go +++ b/src/github.go @@ -50,6 +50,44 @@ func getGithubDirContents(repo, branch, path string) ([]*github.RepositoryConten return contents, nil } +type RemoteFile struct { + // Path is the full path to the file in the repository. + Path string + // RelPath is the path relative to the root directory that was walked. + RelPath string +} +func getGithubDirContentsRecursive(repo, branch, path string) ([]RemoteFile, error) { + entries, err := getGithubDirContents(repo, branch, path) + if err != nil { + return nil, err + } + + files := make([]RemoteFile, 0) + for _, entry := range entries { + name := entry.GetName() + if name == "" || name == ".gitignore" || name == ".gitkeep" || name == ".git" { + continue + } + + entryPath := path + "/" + name + if entry.GetType() == "dir" { + nested, err := getGithubDirContentsRecursive(repo, branch, entryPath) + if err != nil { + return nil, err + } + files = append(files, nested...) + continue + } + + files = append(files, RemoteFile{ + Path: entryPath, + RelPath: strings.TrimPrefix(entryPath, path+"/"), + }) + } + + return files, nil +} + func getGithubFileBytes(repo, branch, path string) (*string, error) { owner, repo := splitRepo(repo) diff --git a/src/zip.go b/src/zip.go new file mode 100644 index 0000000..b815cbb --- /dev/null +++ b/src/zip.go @@ -0,0 +1,38 @@ +package main + +import ( + "archive/zip" + "bytes" +) + +// ZipEntry is a single file to be placed into a zip archive. +type ZipEntry struct { + Name string + Content []byte +} + +func BuildZip(entries []ZipEntry, rootDir string) ([]byte, error) { + buf := new(bytes.Buffer) + writer := zip.NewWriter(buf) + + for _, entry := range entries { + name := entry.Name + if rootDir != "" { + name = rootDir + "/" + name + } + + w, err := writer.Create(name) + if err != nil { + return nil, err + } + if _, err := w.Write(entry.Content); err != nil { + return nil, err + } + } + + if err := writer.Close(); err != nil { + return nil, err + } + + return buf.Bytes(), nil +}