commit 0511a71fead298bd2f2ebf5f127d0764c8974204 Author: Tyler Date: Sun Jun 11 02:14:13 2017 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59f9a7a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/deb-simple +/repo +/src +conf.json +*.pprof diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..3b1fe9a --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,16 @@ +image: knightswarm/gobuild:1.6 + +before_script: + - go get github.com/tools/godep + - export GOPATH=$(pwd) + +stages: + - build + +compile: + stage: build + script: + - export DEB_SIMPLE_VERSION=`grep "var VERSION" main.go | awk -F\" '{print $2}'` + - godep restore + - chmod +x ./ci/package.sh + - ./ci/package.sh \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..1da6f1e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: go + +go: + - 1.5 +before_install: + - go get github.com/blakesmith/ar + - go get github.com/axw/gocov/gocov + - go get github.com/mattn/goveralls + - if ! go get github.com/golang/tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi +script: + - $HOME/gopath/bin/goveralls -service=travis-ci + - go test -run=XXX -bench=. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..38c6bf2 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) [year] [fullname] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ba47df6 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +all: + go build -o deb-simple . + +install: + mkdir -p $(DESTDIR)/usr/bin + cp deb-simple $(DESTDIR)/usr/bin + chmod 755 $(DESTDIR)/usr/bin/deb-simple + +clean: + rm -f deb-simple \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d079503 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +[![Build Status](https://travis-ci.org/esell/deb-simple.svg?branch=master)](https://travis-ci.org/esell/deb-simple) +[![Coverage Status](https://coveralls.io/repos/github/esell/deb-simple/badge.svg?branch=master)](https://coveralls.io/github/esell/deb-simple?branch=master) + + +# deb-simple (get it? dead simple.. deb simple...) + +A lightweight, bare-bones apt repository server. + +# Purpose + +This project came from a need I had to be able to serve up already created deb packages without a lot of fuss. Most of the existing solutions +I found were either geared at mirroring existing "official" repos or for providing your packages to the public. My need was just something that +I could use internally to install already built deb packages via apt-get. I didn't care about change files, signed packages, etc. Since this was +to be used in a CI pipeline it had to support remote uploads and be able to update the package list after each upload. + +# What it does: + +- Supports multiple versions of packages +- Supports multi-arch repos (i386, amd64, custom, etc) +- Supports uploading via HTTP/HTTPS POST requests +- Supports removing packages via HTTP/HTTPS DELETE requests +- Does NOT require a changes file +- Supports uploads from various locations without corrupting the repo + + +# What it doesn't do: +- Create actual packages +- Mirror existing repos + + +# Usage: + +Install using `go get`. Fill out the conf.json file with the values you want, it should be pretty self-explanatory, then fire it up! + +Once it is running POST a file to the `/upload` endpoint: + +`curl -XPOST 'http://localhost:9090/upload?arch=amd64&distro=stable' -F "file=@myapp.deb"` + +Or delete an existing file: + +`curl -XDELETE 'http://localhost:9090/delete' -d '{"filename":"myapp.deb","distroName":"stable","arch":"amd64"}'` + +To use your new repo you will have to add a line like this to your sources.list file: + +`deb http://my-hostname:listenPort/ stable main` + +`my-hostname` should be the actual hostname/IP where you are running deb-simple and `listenPort` will be whatever you set in the config. By default deb-simple puts everything into the `stable` distro and `main` section. If you have enabled SSL you will want to swap `http` for `https`. + + +#License: + +[MIT](LICENSE.txt) so go crazy. Would appreciate PRs for anything cool you add though :) diff --git a/apt.go b/apt.go new file mode 100644 index 0000000..757c21a --- /dev/null +++ b/apt.go @@ -0,0 +1,306 @@ +package main + +import ( + "os" + "fmt" + "github.com/blakesmith/ar" + "bytes" + "io" + "compress/gzip" + "archive/tar" + "log" + "path/filepath" + "io/ioutil" + "strings" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "encoding/hex" + "golang.org/x/crypto/openpgp" + "bufio" + "time" +) + +func inspectPackage(filename string) (string, error) { + f, err := os.Open(filename) + if err != nil { + return "", fmt.Errorf("error opening package file %s: %s", filename, err) + } + + arReader := ar.NewReader(f) + defer f.Close() + var controlBuf bytes.Buffer + + for { + header, err := arReader.Next() + + if err == io.EOF { + break + } + + if err != nil { + return "", fmt.Errorf("error in inspectPackage loop: %s", err) + } + + if strings.Trim(header.Name, "/") == "control.tar.gz" { + io.Copy(&controlBuf, arReader) + return inspectPackageControl(controlBuf) + } + } + return "", nil +} + +func inspectPackageControl(filename bytes.Buffer) (string, error) { + gzf, err := gzip.NewReader(bytes.NewReader(filename.Bytes())) + if err != nil { + return "", fmt.Errorf("error creating gzip reader: %s", err) + } + + tarReader := tar.NewReader(gzf) + var controlBuf bytes.Buffer + for { + header, err := tarReader.Next() + + if err == io.EOF { + break + } + + if err != nil { + return "", fmt.Errorf("failed to inspect package: %s", err) + } + + name := header.Name + + switch header.Typeflag { + case tar.TypeDir: + continue + case tar.TypeReg: + if name == "./control" { + io.Copy(&controlBuf, tarReader) + return controlBuf.String(), nil + } + default: + log.Printf( + "Unable to figure out type : %c in file %s\n", + header.Typeflag, name, + ) + } + } + return "", nil +} + +func createPackagesGz(config Conf, distro, arch string) error { + stdFile, err := os.Create(filepath.Join(config.ArchPath(distro, arch), "Packages")) + if err != nil { + return fmt.Errorf("failed to create packages: %s", err) + } + defer stdFile.Close() + + gzipFile, err := os.Create(filepath.Join(config.ArchPath(distro, arch), "Packages.gz")) + if err != nil { + return fmt.Errorf("failed to create packages.gz: %s", err) + } + defer gzipFile.Close() + + gzWriter := gzip.NewWriter(gzipFile) + defer gzWriter.Close() + + stdOut := bufio.NewWriter(stdFile) + + // loop through each directory + // run inspectPackage + + poolPath := config.PoolPath(distro, arch) + + dirList, err := ioutil.ReadDir(poolPath) + if err != nil { + return fmt.Errorf("scanning: %s: %s", poolPath, err) + } + for _, firstChar := range dirList { + // Recursing isn't fun... + dirList, err := ioutil.ReadDir(filepath.Join(poolPath, firstChar.Name())) + if err != nil { + return fmt.Errorf("scanning: %s: %s", poolPath, err) + } + + for _, p := range dirList { + dirList, err := ioutil.ReadDir(filepath.Join(poolPath, firstChar.Name(), p.Name())) + + if err != nil { + return fmt.Errorf("scanning: %s: %s", poolPath, err) + } + + for _, debFile := range dirList { + if strings.HasSuffix(debFile.Name(), "deb") { + var packBuf bytes.Buffer + debName := packageName(debFile.Name()) + debPath := filepath.Join(config.PoolPackagePath(distro, arch, debFile.Name()), debFile.Name()) + tempCtlData, err := inspectPackage(debPath) + if err != nil { + return err + } + packBuf.WriteString(tempCtlData) + + var size int64 = debFile.Size() + + if size == 0 { + if stat, err := os.Stat(debPath); err == nil { + size = stat.Size() + } + } + + dir := filepath.Join("pool/main", distro, arch, debName[0:1], debName, debFile.Name()) + log.Println("path:", dir) + fmt.Fprintf(&packBuf, "Filename: %s\n", strings.Replace(dir, "\\", "/", -1)) + fmt.Fprintf(&packBuf, "Size: %d\n", size) + + f, err := os.Open(debPath) + + if err != nil { + log.Println("error opening deb file: ", err) + } + + var ( + md5hash = md5.New() + sha1hash = sha1.New() + sha256hash = sha256.New() + ) + + f.Seek(0, 0) + if _, err := io.Copy(md5hash, f); err != nil { + log.Println("error with the md5 hash: ", err) + } + fmt.Fprintf(&packBuf, "MD5sum: %s\n", + hex.EncodeToString(md5hash.Sum(nil))) + f.Seek(0, 0) + if _, err = io.Copy(sha1hash, f); err != nil { + log.Println("error with the sha1 hash: ", err) + } + fmt.Fprintf(&packBuf, "SHA1: %s\n", + hex.EncodeToString(sha1hash.Sum(nil))) + f.Seek(0, 0) + if _, err = io.Copy(sha256hash, f); err != nil { + log.Println("error with the sha256 hash: ", err) + } + fmt.Fprintf(&packBuf, "SHA256: %s\n", + hex.EncodeToString(sha256hash.Sum(nil))) + packBuf.WriteString("\n\n") + + stdOut.Write(packBuf.Bytes()) + gzWriter.Write(packBuf.Bytes()) + + f.Close() + f = nil + } + } + } + } + + stdOut.Flush() + + gzWriter.Flush() + + return nil +} + +func createRelease(config Conf, distro, arch string) error { + outfile, err := os.Create(filepath.Join(config.DistPath(distro), "Release")) + if err != nil { + return fmt.Errorf("failed to create Release: %s", err) + } + defer outfile.Close() + + var packBuf bytes.Buffer + fmt.Fprintf(&packBuf, "Suite: %s\n", distro) + fmt.Fprintf(&packBuf, "Architectures: %s\n", arch) + fmt.Fprint(&packBuf, "Components: main\n") + fmt.Fprintf(&packBuf, "Date: %s\n", time.Now().In(time.UTC).Format("Mon, 02 Jan 2006 15:04:05 -0700")) + + basePath := filepath.Join("main", "binary-" + arch) + + dirList, err := ioutil.ReadDir(filepath.Join(config.DistPath(distro), "main", "binary-" + arch)) + + if err != nil { + return fmt.Errorf("scanning: %s: %s", config.PoolPath(distro, arch), err) + } + + var md5Buf bytes.Buffer + var sha1Buf bytes.Buffer + var sha256Buf bytes.Buffer + + for _, file := range dirList { + filePath := filepath.Join(config.DistPath(distro), basePath, file.Name()) + + log.Println("File path:", filePath) + + fileLocalPath := filepath.Join(basePath, file.Name()) + + fileLocalPath = strings.Replace(fileLocalPath, "\\", "/", -1) + + f, err := os.Open(filePath) + + if err != nil { + log.Println("error opening source file: ", err) + } + + var size int64 = file.Size() + + if size == 0 { + if stat, err := os.Stat(filePath); err == nil { + size = stat.Size() + } + } + + var ( + md5hash = md5.New() + sha1hash = sha1.New() + sha256hash = sha256.New() + ) + + f.Seek(0, 0) + if _, err := io.Copy(md5hash, f); err != nil { + log.Println("error with the md5 hashing: ", err) + } + + fmt.Fprintf(&md5Buf, " %s %d %s\n", hex.EncodeToString(md5hash.Sum(nil)), size, fileLocalPath) + + f.Seek(0, 0) + if _, err := io.Copy(sha1hash, f); err != nil { + log.Println("error with the sha1 hashing: ", err) + } + + fmt.Fprintf(&sha1Buf, " %s %d %s\n", hex.EncodeToString(sha1hash.Sum(nil)), size, fileLocalPath) + + f.Seek(0, 0) + if _, err := io.Copy(sha256hash, f); err != nil { + log.Println("error with the sha256 hashing: ", err) + } + + fmt.Fprintf(&sha256Buf, " %s %d %s\n", hex.EncodeToString(sha256hash.Sum(nil)), size, fileLocalPath) + + f.Close() + f = nil + } + + fmt.Fprintf(&packBuf, "MD5Sum:\n%s", string(md5Buf.Bytes())) + fmt.Fprintf(&packBuf, "SHA1:\n%s", string(sha1Buf.Bytes())) + fmt.Fprintf(&packBuf, "SHA256:\n%s", string(sha256Buf.Bytes())) + + outfile.Write(packBuf.Bytes()) + + if pgpEntity != nil { + gpgfile, err := os.Create(filepath.Join(config.DistPath(distro), "Release.gpg")) + if err != nil { + return fmt.Errorf("failed to create Release.gpg: %s", err) + } + defer gpgfile.Close() + + byteReader := bytes.NewReader(packBuf.Bytes()) + + if err := openpgp.ArmoredDetachSignText(gpgfile, pgpEntity, byteReader, nil); err != nil { + return fmt.Errorf("failed to sign Release: %s", err) + } + } + + return nil +} \ No newline at end of file diff --git a/ci/package.sh b/ci/package.sh new file mode 100644 index 0000000..5ca9ead --- /dev/null +++ b/ci/package.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +curl -s -X POST -F "file=@debian/changelog" -F "name=deb-simple" -F "version=$DEB_SIMPLE_VERSION" -F "author=Gitlab CI" -F "email=gitlab@knightswarm.com" -F "changes[]=Auto build for $CI_BUILD_REF" https://api.meow.tf/changelog/update > debian/changelog + +if [ ! -z "$GITLAB_CI_PUT_URL" ]; then + curl -s -X PUT -F "file_path=debian/changelog" -F branch_name=master -F "commit_message=[ci skip] Update changelog for $CI_BUILD_REF" -F "content= Fri, 25 Mar 2016 21:53:18 +0000 \ No newline at end of file diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +9 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..98863d5 --- /dev/null +++ b/debian/control @@ -0,0 +1,13 @@ +Source: deb-simple +Maintainer: Tyler Stuyfzand +Section: misc +Priority: optional +Standards-Version: 3.9.2 +Build-Depends: debhelper (>= 9) + +Package: deb-simple +Architecture: any +Depends: ${misc:Depends} +Description: Dead Simple Debian Repository + A debian repository server which supports file uploads + and package signing \ No newline at end of file diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..e69de29 diff --git a/debian/deb-simple.dirs b/debian/deb-simple.dirs new file mode 100644 index 0000000..9cc7c13 --- /dev/null +++ b/debian/deb-simple.dirs @@ -0,0 +1,4 @@ +usr/bin +etc +opt/deb-simple +opt/deb-simple/repo \ No newline at end of file diff --git a/debian/deb-simple.init b/debian/deb-simple.init new file mode 100644 index 0000000..18a19ea --- /dev/null +++ b/debian/deb-simple.init @@ -0,0 +1,159 @@ +#! /bin/sh +### BEGIN INIT INFO +# Provides: deb-simple +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Debian Package Server +# Description: Dead Simple Debian Package Server written in Go +### END INIT INFO + +# Author: Tyler Stuyfzand +# +# Please remove the "Author" lines above and replace them +# with your own name if you copy and modify this script. + +# Do NOT "set -e" + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="Simple Debian Package Server" +NAME=deb-simple +USER=deb-simple +DAEMON=/usr/bin/$NAME +DAEMON_ARGS="-c /etc/deb-simple.conf" +PIDFILE=/var/run/$NAME.pid +SCRIPTNAME=/etc/init.d/$NAME + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.2-14) to ensure that this file is present +# and status_of_proc is working. +. /lib/lsb/init-functions + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + start-stop-daemon --start --quiet --chuid $USER:$USER --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --chuid $USER:$USER --pidfile $PIDFILE --make-pidfile --background --exec $DAEMON -- \ + $DAEMON_ARGS \ + || return 2 + # Add code here, if necessary, that waits for the process to be ready + # to handle requests from services started subsequently which depend + # on this one. As a last resort, sleep for some time. +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + # Wait for children to finish too if this is a daemon that forks + # and if the daemon is only ever run from this initscript. + # If the above conditions are not satisfied then add some other code + # that waits for the process to drop all resources that could be + # needed by services started subsequently. A last resort is to + # sleep for some time. + start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON + [ "$?" = 2 ] && return 2 + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE + return "$RETVAL" +} + +# +# Function that sends a SIGHUP to the daemon/service +# +do_reload() { + # + # If the daemon can reload its configuration without + # restarting (for example, when it is sent a SIGHUP), + # then implement that here. + # + start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME + return 0 +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? + ;; + #reload|force-reload) + # + # If do_reload() is not implemented then leave this commented out + # and leave 'force-reload' as an alias for 'restart'. + # + #log_daemon_msg "Reloading $DESC" "$NAME" + #do_reload + #log_end_msg $? + #;; + restart|force-reload) + # + # If the "reload" option is implemented then remove the + # 'force-reload' alias + # + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: \ No newline at end of file diff --git a/debian/postinst b/debian/postinst new file mode 100644 index 0000000..b042450 --- /dev/null +++ b/debian/postinst @@ -0,0 +1,14 @@ +#!/bin/sh + +set -e + +if test "$1" = "configure"; then + if ! getent passwd deb-simple > /dev/null; then + adduser --quiet --system --group \ + --home /opt/deb-simple \ + --no-create-home \ + deb-simple + fi + mkdir -p /opt/deb-simple/repo || true + chown -R deb-simple:deb-simple /opt/deb-simple +fi \ No newline at end of file diff --git a/debian/rules b/debian/rules new file mode 100644 index 0000000..2158a12 --- /dev/null +++ b/debian/rules @@ -0,0 +1,13 @@ +#!/usr/bin/make -f +%: + dh $@ + +override_dh_auto_install: + dh_auto_install + install -d debian/deb-simple/opt/deb-simple + install -d debian/deb-simple/opt/deb-simple/repo + install -m 644 sample_conf.json debian/deb-simple/etc/deb-simple.conf + +override_dh_builddeb: + mkdir ./build || true + dh_builddeb --destdir=./build -- \ No newline at end of file diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/glide.lock b/glide.lock new file mode 100644 index 0000000..15bd8e3 --- /dev/null +++ b/glide.lock @@ -0,0 +1,16 @@ +hash: f49345e0dcd6cb4f3c49af64a95afbea2e608503cd6c1909d300e4a457ef65a6 +updated: 2017-05-02T21:07:03.8312824-04:00 +imports: +- name: github.com/blakesmith/ar + version: 8bd4349a67f2533b078dbc524689d15dba0f4659 +- name: golang.org/x/crypto + version: d1464577745bc7f4e74f65be9cfbd09436a729d6 + subpackages: + - cast5 + - openpgp + - openpgp/armor + - openpgp/elgamal + - openpgp/errors + - openpgp/packet + - openpgp/s2k +testImports: [] diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 0000000..5dd5c7c --- /dev/null +++ b/glide.yaml @@ -0,0 +1,6 @@ +package: . +import: +- package: github.com/blakesmith/ar +- package: golang.org/x/crypto + subpackages: + - openpgp diff --git a/http.go b/http.go new file mode 100644 index 0000000..45b31c2 --- /dev/null +++ b/http.go @@ -0,0 +1,129 @@ +package main + +import ( + "net/http" + "io" + "os" + "path/filepath" + "log" + "encoding/json" + "fmt" +) + +func uploadHandler(config Conf) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "method not supported", http.StatusMethodNotAllowed) + return + } + archType := r.URL.Query().Get("arch") + if archType == "" { + archType = "all" + } + distroName := r.URL.Query().Get("distro") + if distroName == "" { + distroName = "stable" + } + key := r.URL.Query().Get("key") + if key == "" || key != config.Key { + http.Error(w, "unauthorized", 403) + return + } + reader, err := r.MultipartReader() + if err != nil { + httpErrorf(w, "error creating multipart reader: %s", err) + return + } + for { + part, err := reader.NextPart() + if err == io.EOF { + break + } + if part.FileName() == "" { + continue + } + newPath := config.PoolPackagePath(distroName, archType, part.FileName()) + + if _, err := os.Stat(newPath); err != nil { + if os.IsNotExist(err) { + if err := os.MkdirAll(newPath, 0755); err != nil { + httpErrorf(w, "error creating path: %s", err) + return + } + } + } + + dst, err := os.Create(filepath.Join(newPath, part.FileName())) + if err != nil { + httpErrorf(w, "error creating deb file: %s", err) + return + } + defer dst.Close() + + if _, err := io.Copy(dst, part); err != nil { + httpErrorf(w, "error writing deb file: %s", err) + return + } + } + mutex.Lock() + defer mutex.Unlock() + + log.Println("got lock, updating package list...") + if err := createPackagesGz(config, distroName, archType); err != nil { + httpErrorf(w, "error creating package: %s", err) + return + } + + err = createRelease(config, distroName, archType) + + if err != nil { + httpErrorf(w, "error creating package: %s", err) + return + } + + w.WriteHeader(http.StatusOK) + }) +} + +func deleteHandler(config Conf) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "DELETE" { + http.Error(w, "method not supported", http.StatusMethodNotAllowed) + return + } + var toDelete DeleteObj + if err := json.NewDecoder(r.Body).Decode(&toDelete); err != nil { + httpErrorf(w, "failed to decode json: %s", err) + return + } + debPath := filepath.Join(config.ArchPath(toDelete.DistroName, toDelete.Arch), toDelete.Filename) + if err := os.Remove(debPath); err != nil { + httpErrorf(w, "failed to delete: %s", err) + return + } + key := r.URL.Query().Get("key") + if key == "" || key != config.Key { + http.Error(w, "unauthorized", http.StatusForbidden) + return + } + mutex.Lock() + defer mutex.Unlock() + + log.Println("got lock, updating package list...") + if err := createPackagesGz(config, toDelete.DistroName, toDelete.Arch); err != nil { + httpErrorf(w, "failed to delete package: %s", err) + return + } + + if err := createRelease(config, toDelete.DistroName, toDelete.Arch); err != nil { + httpErrorf(w, "failed to delete package: %s", err) + return + } + }) +} + +func httpErrorf(w http.ResponseWriter, format string, a ...interface{}) { + err := fmt.Errorf(format, a...) + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) +} \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..439ba71 --- /dev/null +++ b/main.go @@ -0,0 +1,177 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "path/filepath" + "sync" + "strings" + "golang.org/x/crypto/openpgp" + "runtime" + "errors" +) + +var VERSION string = "1.1.0" + +type Conf struct { + ListenPort string `json:"listenPort"` + RootRepoPath string `json:"rootRepoPath"` + SupportArch []string `json:"supportedArch"` + DistroNames []string `json:"distroNames"` + PGPSecretKey string `json:"pgpSecretKey"` + PGPPassphrase string `json:"pgpPassphrase"` + EnableSSL bool `json:"enableSSL"` + SSLCert string `json:"SSLcert"` + SSLKey string `json:"SSLkey"` + Key string `json:"key"` +} + +func (c Conf) DistPath(distro string) string { + return filepath.Join(c.RootRepoPath, "dists", distro) +} + +func (c Conf) ArchPath(distro, arch string) string { + return filepath.Join(c.RootRepoPath, "dists", distro, "main/binary-"+arch) +} + +func (c Conf) PoolPath(distro, arch string) string { + return filepath.Join(c.RootRepoPath, "pool/main", distro, arch) +} + +func (c Conf) PoolPackagePath(distro, arch, name string) string { + name = packageName(name) + return filepath.Join(c.RootRepoPath, "pool/main", distro, arch, name[0:1], name) +} + +func packageName(name string) string { + if index := strings.Index(name, "_"); index != -1 { + name = name[:index] + } + return name +} + +type DeleteObj struct { + Filename string + DistroName string + Arch string +} + +var ( + mutex sync.Mutex + configFile = flag.String("c", "conf.json", "config file location") + flagShowVersion = flag.Bool("version", false, "Show dnsconfig version") + parsedConfig = Conf{} + pgpEntity *openpgp.Entity +) + +func main() { + flag.Parse() + + if *flagShowVersion { + fmt.Printf("deb-simple %s (%s)\n", VERSION, runtime.Version()) + os.Exit(0) + } + + file, err := ioutil.ReadFile(*configFile) + if err != nil { + log.Fatalln("unable to read config file, exiting...") + } + if err := json.Unmarshal(file, &parsedConfig); err != nil { + log.Fatalln("unable to marshal config file, exiting...") + } + + if err := createDirs(parsedConfig); err != nil { + log.Println(err) + log.Fatalln("error creating directory structure, exiting") + } + + if err := setupPgp(parsedConfig); err != nil { + log.Println(err) + log.Fatalln("error loading pgp key, exiting") + } + + http.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir(parsedConfig.RootRepoPath)))) + http.Handle("/upload", uploadHandler(parsedConfig)) + http.Handle("/delete", deleteHandler(parsedConfig)) + + if parsedConfig.EnableSSL { + log.Println("running with SSL enabled") + log.Fatalln(http.ListenAndServeTLS(":"+parsedConfig.ListenPort, parsedConfig.SSLCert, parsedConfig.SSLKey, nil)) + } else { + log.Println("running without SSL enabled") + log.Fatalln(http.ListenAndServe(":"+parsedConfig.ListenPort, nil)) + } +} + +func setupPgp(config Conf) error { + if config.PGPSecretKey == "" { + return nil + } + + secretKey, err := os.Open(config.PGPSecretKey) + if err != nil { + return fmt.Errorf("failed to open private key ring file: %s", err) + } + defer secretKey.Close() + + entitylist, err := openpgp.ReadKeyRing(secretKey) + + if err != nil { + return fmt.Errorf("failed to read key ring: %s", err) + } + + if len(entitylist) < 1 { + return errors.New("no keys in key ring") + } + + pgpEntity = entitylist[0] + + passphrase := []byte(config.PGPPassphrase) + + if err := pgpEntity.PrivateKey.Decrypt(passphrase); err != nil { + return err + } + + for _, subkey := range pgpEntity.Subkeys { + err := subkey.PrivateKey.Decrypt(passphrase) + + if err != nil { + return err + } + } + + return nil +} + +func createDirs(config Conf) error { + for _, distro := range config.DistroNames { + for _, arch := range config.SupportArch { + if _, err := os.Stat(config.ArchPath(distro, arch)); err != nil { + if os.IsNotExist(err) { + log.Printf("Directory for %s (%s) does not exist, creating", distro, arch) + if err := os.MkdirAll(config.ArchPath(distro, arch), 0755); err != nil { + return fmt.Errorf("error creating directory for %s (%s): %s", distro, arch, err) + } + } else { + return fmt.Errorf("error inspecting %s (%s): %s", distro, arch, err) + } + } + if _, err := os.Stat(config.PoolPath(distro, arch)); err != nil { + if os.IsNotExist(err) { + log.Printf("Directory for %s (%s) does not exist, creating", distro, arch) + if err := os.MkdirAll(config.PoolPath(distro, arch), 0755); err != nil { + return fmt.Errorf("error creating directory for %s (%s): %s", distro, arch, err) + } + } else { + return fmt.Errorf("error inspecting %s (%s): %s", distro, arch, err) + } + } + } + } + return nil +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..b383da9 --- /dev/null +++ b/main_test.go @@ -0,0 +1,535 @@ +package main + +import ( + "bytes" + "compress/gzip" + "crypto/md5" + "encoding/hex" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "net/http/httptest" + "os" + "testing" +) + +var goodOutput = `Package: vim-tiny +Source: vim +Version: 2:7.4.052-1ubuntu3 +Architecture: amd64 +Maintainer: Ubuntu Developers +Installed-Size: 931 +Depends: vim-common (= 2:7.4.052-1ubuntu3), libacl1 (>= 2.2.51-8), libc6 (>= 2.15), libselinux1 (>= 1.32), libtinfo5 +Suggests: indent +Provides: editor +Section: editors +Priority: important +Homepage: http://www.vim.org/ +Description: Vi IMproved - enhanced vi editor - compact version + Vim is an almost compatible version of the UNIX editor Vi. + . + Many new features have been added: multi level undo, syntax + highlighting, command line history, on-line help, filename + completion, block operations, folding, Unicode support, etc. + . + This package contains a minimal version of vim compiled with no + GUI and a small subset of features in order to keep small the + package size. This package does not depend on the vim-runtime + package, but installing it you will get its additional benefits + (online documentation, plugins, ...). +Original-Maintainer: Debian Vim Maintainers +` + +var goodPkgGzOutput = `Package: vim-tiny +Source: vim +Version: 2:7.4.052-1ubuntu3 +Architecture: amd64 +Maintainer: Ubuntu Developers +Installed-Size: 931 +Depends: vim-common (= 2:7.4.052-1ubuntu3), libacl1 (>= 2.2.51-8), libc6 (>= 2.15), libselinux1 (>= 1.32), libtinfo5 +Suggests: indent +Provides: editor +Section: editors +Priority: important +Homepage: http://www.vim.org/ +Description: Vi IMproved - enhanced vi editor - compact version + Vim is an almost compatible version of the UNIX editor Vi. + . + Many new features have been added: multi level undo, syntax + highlighting, command line history, on-line help, filename + completion, block operations, folding, Unicode support, etc. + . + This package contains a minimal version of vim compiled with no + GUI and a small subset of features in order to keep small the + package size. This package does not depend on the vim-runtime + package, but installing it you will get its additional benefits + (online documentation, plugins, ...). +Original-Maintainer: Debian Vim Maintainers +Filename: dists/stable/main/binary-cats/test.deb +Size: 391240 +MD5sum: 0ec79417129746ff789fcff0976730c5 +SHA1: b2ac976af80f0f50a8336402d5a29c67a2880b9b +SHA256: 9938ec82a8c882ebc2d59b64b0bf2ac01e9cbc5a235be4aa268d4f8484e75eab + + +` + +var goodPkgGzOutputNonDefault = `Package: vim-tiny +Source: vim +Version: 2:7.4.052-1ubuntu3 +Architecture: amd64 +Maintainer: Ubuntu Developers +Installed-Size: 931 +Depends: vim-common (= 2:7.4.052-1ubuntu3), libacl1 (>= 2.2.51-8), libc6 (>= 2.15), libselinux1 (>= 1.32), libtinfo5 +Suggests: indent +Provides: editor +Section: editors +Priority: important +Homepage: http://www.vim.org/ +Description: Vi IMproved - enhanced vi editor - compact version + Vim is an almost compatible version of the UNIX editor Vi. + . + Many new features have been added: multi level undo, syntax + highlighting, command line history, on-line help, filename + completion, block operations, folding, Unicode support, etc. + . + This package contains a minimal version of vim compiled with no + GUI and a small subset of features in order to keep small the + package size. This package does not depend on the vim-runtime + package, but installing it you will get its additional benefits + (online documentation, plugins, ...). +Original-Maintainer: Debian Vim Maintainers +Filename: dists/blah/main/binary-cats/test.deb +Size: 391240 +MD5sum: 0ec79417129746ff789fcff0976730c5 +SHA1: b2ac976af80f0f50a8336402d5a29c67a2880b9b +SHA256: 9938ec82a8c882ebc2d59b64b0bf2ac01e9cbc5a235be4aa268d4f8484e75eab + + +` + +func TestCreateDirs(t *testing.T) { + pwd, err := os.Getwd() + if err != nil { + t.Errorf("Unable to get current working directory: %s", err) + } + config := Conf{ListenPort: "9666", RootRepoPath: pwd + "/testing", SupportArch: []string{"cats", "dogs"}, DistroNames: []string{"stable"}, EnableSSL: false} + // sanity check... + if config.RootRepoPath != pwd+"/testing" { + t.Errorf("RootRepoPath is %s, should be %s\n ", config.RootRepoPath, pwd+"/testing") + } + t.Log("creating temp dirs in ", config.RootRepoPath) + if err := createDirs(config); err != nil { + t.Errorf("createDirs() failed ") + } + for _, archDir := range config.SupportArch { + if _, err := os.Stat(config.RootRepoPath + "/dists/stable/main/binary-" + archDir); err != nil { + if os.IsNotExist(err) { + t.Errorf("Directory for %s does not exist", archDir) + } + } + } + + // cleanup + if err := os.RemoveAll(config.RootRepoPath); err != nil { + t.Errorf("error cleaning up after createDirs(): %s", err) + } + + // create temp file + tempFile, err := os.Create(pwd + "/tempFile") + if err != nil { + t.Fatalf("create %s: %s", pwd+"/tempFile", err) + } + defer tempFile.Close() + config.RootRepoPath = pwd + "/tempFile" + // Can't make directory named after file. + if err := createDirs(config); err == nil { + t.Errorf("createDirs() should have failed but did not") + } + // cleanup + if err := os.RemoveAll(pwd + "/tempFile"); err != nil { + t.Errorf("error cleaning up after createDirs(): %s", err) + } + +} + +func TestInspectPackage(t *testing.T) { + parsedControl, err := inspectPackage("samples/vim-tiny_7.4.052-1ubuntu3_amd64.deb") + if err != nil { + t.Error("inspectPackage() error: %s", err) + } + if parsedControl != goodOutput { + t.Errorf("control file does not match") + } + + _, err = inspectPackage("thisfileshouldnotexist") + if err == nil { + t.Error("inspectPackage() should have failed, it did not") + } +} + +func TestInspectPackageControl(t *testing.T) { + sampleDeb, err := ioutil.ReadFile("samples/control.tar.gz") + if err != nil { + t.Errorf("error opening sample deb file: %s", err) + } + var controlBuf bytes.Buffer + cfReader := bytes.NewReader(sampleDeb) + io.Copy(&controlBuf, cfReader) + parsedControl, err := inspectPackageControl(controlBuf) + if err != nil { + t.Error("error inspecting control file: %s", err) + } + if parsedControl != goodOutput { + t.Errorf("control file does not match") + } + + var failControlBuf bytes.Buffer + _, err = inspectPackageControl(failControlBuf) + if err == nil { + t.Error("inspectPackageControl() should have failed, it did not") + } + +} + +func TestCreatePackagesGz(t *testing.T) { + pwd, err := os.Getwd() + if err != nil { + t.Errorf("Unable to get current working directory: %s", err) + } + config := Conf{ListenPort: "9666", RootRepoPath: pwd + "/testing", SupportArch: []string{"cats", "dogs"}, DistroNames: []string{"stable"}, EnableSSL: false} + // sanity check... + if config.RootRepoPath != pwd+"/testing" { + t.Errorf("RootRepoPath is %s, should be %s\n ", config.RootRepoPath, pwd+"/testing") + } + // copy sample deb to repo location (assuming it exists) + origDeb, err := os.Open("samples/vim-tiny_7.4.052-1ubuntu3_amd64.deb") + if err != nil { + t.Errorf("error opening up sample deb: %s", err) + } + defer origDeb.Close() + for _, archDir := range config.SupportArch { + // do not use the built-in createDirs() in case it is broken + if err := os.MkdirAll(config.RootRepoPath+"/dists/stable/main/binary-"+archDir, 0755); err != nil { + t.Errorf("error creating directory for %s: %s\n", archDir, err) + } + copyDeb, err := os.Create(config.RootRepoPath + "/dists/stable/main/binary-" + archDir + "/test.deb") + if err != nil { + t.Errorf("error creating copy of deb: %s", err) + } + _, err = io.Copy(copyDeb, origDeb) + if err != nil { + t.Errorf("error writing copy of deb: %s", err) + } + if err := copyDeb.Close(); err != nil { + t.Errorf("error saving copy of deb: %s", err) + } + } + if err := createPackagesGz(config, "stable", "cats"); err != nil { + t.Errorf("error creating packages gzip for cats") + } + pkgGzip, err := ioutil.ReadFile(config.RootRepoPath + "/dists/stable/main/binary-cats/Packages.gz") + if err != nil { + t.Errorf("error reading Packages.gz: %s", err) + } + pkgReader, err := gzip.NewReader(bytes.NewReader(pkgGzip)) + if err != nil { + t.Errorf("error reading existing Packages.gz: %s", err) + } + buf := bytes.NewBuffer(nil) + io.Copy(buf, pkgReader) + if goodPkgGzOutput != string(buf.Bytes()) { + t.Errorf("Packages.gz does not match, returned value is: %s", string(buf.Bytes())) + } + + // cleanup + if err := os.RemoveAll(config.RootRepoPath); err != nil { + t.Errorf("error cleaning up after createPackagesGz(): %s", err) + } + + // create temp file + tempFile, err := os.Create(pwd + "/tempFile") + if err != nil { + t.Fatalf("create %s: %s", pwd+"/tempFile", err) + } + defer tempFile.Close() + config.RootRepoPath = pwd + "/tempFile" + // Can't make directory named after file + if err := createPackagesGz(config, "stable", "cats"); err == nil { + t.Errorf("createPackagesGz() should have failed, it did not") + } + // cleanup + if err := os.RemoveAll(pwd + "/tempFile"); err != nil { + t.Errorf("error cleaning up after createDirs(): %s", err) + } + +} + +func TestCreatePackagesGzNonDefault(t *testing.T) { + pwd, err := os.Getwd() + if err != nil { + t.Errorf("Unable to get current working directory: %s", err) + } + config := Conf{ListenPort: "9666", RootRepoPath: pwd + "/testing", SupportArch: []string{"cats", "dogs"}, DistroNames: []string{"blah"}, EnableSSL: false} + // sanity check... + if config.RootRepoPath != pwd+"/testing" { + t.Errorf("RootRepoPath is %s, should be %s\n ", config.RootRepoPath, pwd+"/testing") + } + // copy sample deb to repo location (assuming it exists) + origDeb, err := os.Open("samples/vim-tiny_7.4.052-1ubuntu3_amd64.deb") + if err != nil { + t.Errorf("error opening up sample deb: %s", err) + } + defer origDeb.Close() + for _, archDir := range config.SupportArch { + // do not use the built-in createDirs() in case it is broken + if err := os.MkdirAll(config.RootRepoPath+"/dists/blah/main/binary-"+archDir, 0755); err != nil { + t.Errorf("error creating directory for %s: %s\n", archDir, err) + } + copyDeb, err := os.Create(config.RootRepoPath + "/dists/blah/main/binary-" + archDir + "/test.deb") + if err != nil { + t.Errorf("error creating copy of deb: %s", err) + } + _, err = io.Copy(copyDeb, origDeb) + if err != nil { + t.Errorf("error writing copy of deb: %s", err) + } + if err := copyDeb.Close(); err != nil { + t.Errorf("error saving copy of deb: %s", err) + } + } + if err := createPackagesGz(config, "blah", "cats"); err != nil { + t.Errorf("error creating packages gzip for cats") + } + pkgGzip, err := ioutil.ReadFile(config.RootRepoPath + "/dists/blah/main/binary-cats/Packages.gz") + if err != nil { + t.Errorf("error reading Packages.gz: %s", err) + } + pkgReader, err := gzip.NewReader(bytes.NewReader(pkgGzip)) + if err != nil { + t.Errorf("error reading existing Packages.gz: %s", err) + } + buf := bytes.NewBuffer(nil) + io.Copy(buf, pkgReader) + if goodPkgGzOutputNonDefault != string(buf.Bytes()) { + t.Errorf("Packages.gz does not match, returned value is: %s", string(buf.Bytes())) + } + + // cleanup + if err := os.RemoveAll(config.RootRepoPath); err != nil { + t.Errorf("error cleaning up after createPackagesGz(): %s", err) + } + +} + +func TestUploadHandler(t *testing.T) { + pwd, err := os.Getwd() + if err != nil { + t.Errorf("Unable to get current working directory: %s", err) + } + config := Conf{ListenPort: "9666", RootRepoPath: pwd + "/testing", SupportArch: []string{"cats", "dogs"}, DistroNames: []string{"stable"}, EnableSSL: false} + // sanity check... + if config.RootRepoPath != pwd+"/testing" { + t.Errorf("RootRepoPath is %s, should be %s\n ", config.RootRepoPath, pwd+"/testing") + } + uploadHandle := uploadHandler(config) + // GET + req, _ := http.NewRequest("GET", "", nil) + w := httptest.NewRecorder() + uploadHandle.ServeHTTP(w, req) + if w.Code != http.StatusMethodNotAllowed { + t.Errorf("uploadHandler GET returned %v, should be %v", w.Code, http.StatusMethodNotAllowed) + } + + // POST + // create "all" arch as it's the default + if err := os.MkdirAll(config.RootRepoPath+"/dists/stable/main/binary-all", 0755); err != nil { + t.Error("error creating directory for POST testing: %s", err) + } + sampleDeb, err := os.Open("samples/vim-tiny_7.4.052-1ubuntu3_amd64.deb") + if err != nil { + t.Errorf("error opening sample deb file: %s", err) + } + defer sampleDeb.Close() + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", "vim-tiny_7.4.052-1ubuntu3_amd64.deb") + if err != nil { + t.Errorf("error FormFile: %s", err) + } + _, err = io.Copy(part, sampleDeb) + if err != nil { + t.Errorf("error copying sampleDeb to FormFile: %s", err) + } + if err := writer.Close(); err != nil { + t.Errorf("error closing form writer: %s", err) + } + req, _ = http.NewRequest("POST", "", body) + req.Header.Add("Content-Type", writer.FormDataContentType()) + w = httptest.NewRecorder() + uploadHandle.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Errorf("uploadHandler POST returned %v, should be %v", w.Code, http.StatusOK) + } + // verify uploaded file matches sample + uploadFile, _ := ioutil.ReadFile(config.RootRepoPath + "/dists/stable/main/binary-all/vim-tiny_7.4.052-1ubuntu3_amd64.deb") + uploadmd5hash := md5.New() + uploadmd5hash.Write(uploadFile) + uploadFilemd5 := hex.EncodeToString(uploadmd5hash.Sum(nil)) + + sampleFile, _ := ioutil.ReadFile("samples/vim-tiny_7.4.052-1ubuntu3_amd64.deb") + samplemd5hash := md5.New() + samplemd5hash.Write(sampleFile) + sampleFilemd5 := hex.EncodeToString(samplemd5hash.Sum(nil)) + if uploadFilemd5 != sampleFilemd5 { + t.Errorf("uploaded file MD5 is %s, should be %s", uploadFilemd5, sampleFilemd5) + } + + // cleanup + if err := os.RemoveAll(config.RootRepoPath); err != nil { + t.Errorf("error cleaning up after uploadHandler(): %s", err) + } + + // create temp file + tempFile, err := os.Create(pwd + "/tempFile") + if err != nil { + t.Fatalf("create %s: %s", pwd+"/tempFile", err) + } + defer tempFile.Close() + config.RootRepoPath = pwd + "/tempFile" + // Can't make directory named after file + uploadHandle = uploadHandler(config) + failBody := &bytes.Buffer{} + failWriter := multipart.NewWriter(failBody) + failPart, err := failWriter.CreateFormFile("file", "vim-tiny_7.4.052-1ubuntu3_amd64.deb") + if err != nil { + t.Errorf("error FormFile: %s", err) + } + _, err = io.Copy(failPart, sampleDeb) + if err != nil { + t.Errorf("error copying sampleDeb to FormFile: %s", err) + } + if err := failWriter.Close(); err != nil { + t.Errorf("error closing form writer: %s", err) + } + req, _ = http.NewRequest("POST", "", failBody) + req.Header.Add("Content-Type", failWriter.FormDataContentType()) + w = httptest.NewRecorder() + uploadHandle.ServeHTTP(w, req) + if w.Code != http.StatusInternalServerError { + t.Errorf("uploadHandler POST returned %v, should be %v", w.Code, http.StatusInternalServerError) + } + // cleanup + if err := os.RemoveAll(pwd + "/tempFile"); err != nil { + t.Errorf("error cleaning up after createDirs(): %s", err) + } +} + +func TestDeleteHandler(t *testing.T) { + pwd, err := os.Getwd() + if err != nil { + t.Errorf("Unable to get current working directory: %s", err) + } + config := Conf{ListenPort: "9666", RootRepoPath: pwd + "/testing", SupportArch: []string{"cats", "dogs"}, DistroNames: []string{"stable"}, EnableSSL: false} + // sanity check... + if config.RootRepoPath != pwd+"/testing" { + t.Errorf("RootRepoPath is %s, should be %s\n ", config.RootRepoPath, pwd+"/testing") + } + deleteHandle := deleteHandler(config) + // GET + req, _ := http.NewRequest("GET", "", nil) + w := httptest.NewRecorder() + deleteHandle.ServeHTTP(w, req) + if w.Code != http.StatusMethodNotAllowed { + t.Errorf("deleteHandler GET returned %v, should be %v", w.Code, http.StatusMethodNotAllowed) + } + + // DELETE + // create "all" arch as it's the default + if err := os.MkdirAll(config.RootRepoPath+"/dists/stable/main/binary-all", 0755); err != nil { + t.Error("error creating directory for POST testing: %s", err) + } + tempDeb, err := os.Create(config.RootRepoPath + "/dists/stable/main/binary-all/myapp.deb") + if err != nil { + t.Fatalf("create %s: %s", config.RootRepoPath+"/dists/stable/main/binary-all/myapp.deb", err) + } + defer tempDeb.Close() + req, _ = http.NewRequest("DELETE", "", bytes.NewBufferString("{\"filename\":\"myapp.deb\",\"arch\":\"all\", \"distroName\":\"stable\"}")) + w = httptest.NewRecorder() + deleteHandle.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Errorf("deleteHandler DELETE returned %v, should be %v", w.Code, http.StatusOK) + } + + // cleanup + if err := os.RemoveAll(config.RootRepoPath); err != nil { + t.Errorf("error cleaning up after uploadHandler(): %s", err) + } + + // create temp file + tempFile, err := os.Create(pwd + "/tempFile") + if err != nil { + t.Fatalf("create %s: %s", pwd+"/tempFile", err) + } + defer tempFile.Close() + config.RootRepoPath = pwd + "/tempFile" + // Can't make directory named after file + deleteHandle = deleteHandler(config) + req, _ = http.NewRequest("DELETE", "", bytes.NewBufferString("{\"filename\":\"myapp.deb\",\"arch\":\"amd64\", \"distroName\":\"stable\"}")) + w = httptest.NewRecorder() + deleteHandle.ServeHTTP(w, req) + if w.Code != http.StatusInternalServerError { + t.Errorf("deleteHandler DELETE returned %v, should be %v", w.Code, http.StatusInternalServerError) + } + // cleanup + if err := os.RemoveAll(pwd + "/tempFile"); err != nil { + t.Errorf("error cleaning up after createDirs(): %s", err) + } +} + +func BenchmarkUploadHandler(b *testing.B) { + pwd, err := os.Getwd() + if err != nil { + b.Errorf("Unable to get current working directory: %s", err) + } + config := &Conf{ListenPort: "9666", RootRepoPath: pwd + "/testing", SupportArch: []string{"cats", "dogs"}, DistroNames: []string{"stable"}, EnableSSL: false} + // sanity check... + if config.RootRepoPath != pwd+"/testing" { + b.Errorf("RootRepoPath is %s, should be %s\n ", config.RootRepoPath, pwd+"/testing") + } + uploadHandle := uploadHandler(*config) + if err := os.MkdirAll(config.RootRepoPath+"/dists/stable/main/binary-all", 0755); err != nil { + b.Errorf("error creating directory for POST testing: %s", err) + } + sampleDeb, err := os.Open("samples/vim-tiny_7.4.052-1ubuntu3_amd64.deb") + if err != nil { + b.Errorf("error opening sample deb file: %s", err) + } + defer sampleDeb.Close() + b.ResetTimer() + for i := 0; i < b.N; i++ { + // temporary (i hope) hack to solve "http: MultipartReader called twice" error + b.StopTimer() + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", "vim-tiny_7.4.052-1ubuntu3_amd64.deb") + if err != nil { + b.Errorf("error FormFile: %s", err) + } + if _, err := io.Copy(part, sampleDeb); err != nil { + b.Errorf("error copying sampleDeb to FormFile: %s", err) + } + if err := writer.Close(); err != nil { + b.Errorf("error closing form writer: %s", err) + } + req, _ := http.NewRequest("POST", "/upload?distro=stable", body) + req.Header.Add("Content-Type", writer.FormDataContentType()) + w := httptest.NewRecorder() + b.StartTimer() + uploadHandle.ServeHTTP(w, req) + } + b.StopTimer() + // cleanup + _ = os.RemoveAll(config.RootRepoPath) +} diff --git a/packages.go b/packages.go new file mode 100644 index 0000000..6a51f02 --- /dev/null +++ b/packages.go @@ -0,0 +1,31 @@ +package main + +import ( + "regexp" + "log" + "reflect" +) + +var ( + variableRegexp = regexp.MustCompile("(.*?):\\s*(.*)") +) + +type Package struct { + Package string + Version string +} + +func parsePackageData(ctlData string) (*Package, error) { + res := &Package{} + + for _, match := range variableRegexp.FindAllStringSubmatch(ctlData, -1) { + switch match[1] { + case "Package": + res.Package = match[2] + case "Version": + res.Version = match[2] + } + } + + return res, nil +} \ No newline at end of file diff --git a/sample_conf.json b/sample_conf.json new file mode 100644 index 0000000..d547fce --- /dev/null +++ b/sample_conf.json @@ -0,0 +1,12 @@ +{ + "listenPort" : "9090", + "rootRepoPath" : "/opt/deb-simple/repo", + "supportedArch" : ["all","i386","amd64"], + "distroNames":["stable"], + "pgpSecretKey": "secring.gpg", + "pgpPassphrase" : "", + "enableSSL" : false, + "SSLcert" : "server.crt", + "SSLkey" : "server.key", + "key" : "abcdefg" +} diff --git a/samples/control.tar.gz b/samples/control.tar.gz new file mode 100644 index 0000000..9b2c857 Binary files /dev/null and b/samples/control.tar.gz differ diff --git a/samples/vim-tiny_7.4.052-1ubuntu3_amd64.deb b/samples/vim-tiny_7.4.052-1ubuntu3_amd64.deb new file mode 100644 index 0000000..e129318 Binary files /dev/null and b/samples/vim-tiny_7.4.052-1ubuntu3_amd64.deb differ