]> Cypherpunks.ru repositories - gocheese.git/commitdiff
Dealing with GPG signatures
authorSergey Matveev <stargrave@stargrave.org>
Fri, 6 Dec 2019 09:53:18 +0000 (12:53 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Fri, 6 Dec 2019 11:30:05 +0000 (14:30 +0300)
VERSION
gocheese.go
gocheese.texi
pyshop2packages.sh
tmp.go

diff --git a/VERSION b/VERSION
index 227cea215648b1af34a87c9acf5b707fe02d2072..7ec1d6db40877765247db18e7f9a4e36a0def4ad 100644 (file)
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.0.0
+2.1.0
index cdbb1358d70789ecd211e0097b6c9e5080775a5e..4b47f3c51a72deda900a457e07f9e59166d184a8 100644 (file)
@@ -45,12 +45,20 @@ import (
 )
 
 const (
-       HTMLBegin    = "<!DOCTYPE html><html><head><title>Links for %s</title></head><body><h1>Links for %s</h1>\n"
-       HTMLEnd      = "</body></html>"
-       HTMLElement  = "<a href='%s'>%s</a><br/>\n"
+       HTMLBegin = `<!DOCTYPE html>
+<html>
+  <head>
+    <title>Links for %s</title>
+  </head>
+  <body>
+`
+       HTMLEnd      = "  </body>\n</html>\n"
+       HTMLElement  = "    <a href=\"%s\"%s>%s</a><br/>\n"
        SHA256Prefix = "sha256="
        SHA256Ext    = ".sha256"
        InternalFlag = ".internal"
+       GPGSigExt    = ".asc"
+       GPGSigAttr   = " data-gpg-sig=true"
 
        Warranty = `This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
@@ -75,6 +83,7 @@ var (
        tlsKey           = flag.String("tls-key", "", "Path to TLS X.509 private key")
        norefreshURLPath = flag.String("norefresh", "/norefresh/", "Non-refreshing URL path")
        refreshURLPath   = flag.String("refresh", "/simple/", "Auto-refreshing URL path")
+       gpgUpdateURLPath = flag.String("gpgupdate", "/gpgupdate/", "GPG forceful refreshing URL path")
        pypiURL          = flag.String("pypi", "https://pypi.org/simple/", "Upstream PyPI URL")
        passwdPath       = flag.String("passwd", "passwd", "Path to file with authenticators")
        passwdCheck      = flag.Bool("passwd-check", false, "Test the -passwd file for syntax errors and exit")
@@ -98,7 +107,13 @@ func mkdirForPkg(w http.ResponseWriter, r *http.Request, dir string) bool {
        return true
 }
 
-func refreshDir(w http.ResponseWriter, r *http.Request, dir, filenameGet string) bool {
+func refreshDir(
+       w http.ResponseWriter,
+       r *http.Request,
+       dir,
+       filenameGet string,
+       gpgUpdate bool,
+) bool {
        if _, err := os.Stat(filepath.Join(*root, dir, InternalFlag)); err == nil {
                return true
        }
@@ -116,6 +131,7 @@ func refreshDir(w http.ResponseWriter, r *http.Request, dir, filenameGet string)
        if !mkdirForPkg(w, r, dir) {
                return false
        }
+       dirPath := filepath.Join(*root, dir)
        var submatches []string
        var uri string
        var filename string
@@ -138,6 +154,8 @@ func refreshDir(w http.ResponseWriter, r *http.Request, dir, filenameGet string)
                        http.Error(w, err.Error(), http.StatusBadGateway)
                        return false
                }
+               pkgURL.Fragment = ""
+               path = filepath.Join(dirPath, filename)
                if filename == filenameGet {
                        if killed {
                                // Skip heavy remote call, when shutting down
@@ -145,15 +163,14 @@ func refreshDir(w http.ResponseWriter, r *http.Request, dir, filenameGet string)
                                return false
                        }
                        log.Println(r.RemoteAddr, "pypi download", filename)
-                       path = filepath.Join(*root, dir, filename)
-                       resp, err = http.Get(uri)
+                       resp, err = http.Get(pkgURL.String())
                        if err != nil {
                                http.Error(w, err.Error(), http.StatusBadGateway)
                                return false
                        }
                        defer resp.Body.Close()
                        hasher := sha256.New()
-                       dst, err := TempFile(filepath.Join(*root, dir))
+                       dst, err := TempFile(dirPath)
                        if err != nil {
                                http.Error(w, err.Error(), http.StatusInternalServerError)
                                return false
@@ -184,7 +201,22 @@ func refreshDir(w http.ResponseWriter, r *http.Request, dir, filenameGet string)
                                return false
                        }
                }
-               path = filepath.Join(*root, dir, filename+SHA256Ext)
+               if filename == filenameGet || gpgUpdate {
+                       if _, err = os.Stat(path); err == nil {
+                               if resp, err := http.Get(pkgURL.String() + GPGSigExt); err == nil {
+                                       sig, err := ioutil.ReadAll(resp.Body)
+                                       resp.Body.Close()
+                                       if err == nil {
+                                               if err = WriteFileSync(dirPath, path+GPGSigExt, sig); err != nil {
+                                                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                                                       return false
+                                               }
+                                               log.Println(r.RemoteAddr, "pypi downloaded signature", filename)
+                                       }
+                               }
+                       }
+               }
+               path = path + SHA256Ext
                _, err = os.Stat(path)
                if err == nil {
                        continue
@@ -194,7 +226,7 @@ func refreshDir(w http.ResponseWriter, r *http.Request, dir, filenameGet string)
                        return false
                }
                log.Println(r.RemoteAddr, "pypi touch", filename)
-               if err = ioutil.WriteFile(path, digest, os.FileMode(0666)); err != nil {
+               if err = WriteFileSync(dirPath, path, digest); err != nil {
                        http.Error(w, err.Error(), http.StatusInternalServerError)
                        return false
                }
@@ -209,7 +241,7 @@ func listRoot(w http.ResponseWriter, r *http.Request) {
                return
        }
        var result bytes.Buffer
-       result.WriteString(fmt.Sprintf(HTMLBegin, "root", "root"))
+       result.WriteString(fmt.Sprintf(HTMLBegin, "root"))
        for _, file := range files {
                if file.Mode().IsDir() {
                        result.WriteString(fmt.Sprintf(
@@ -223,13 +255,19 @@ func listRoot(w http.ResponseWriter, r *http.Request) {
        w.Write(result.Bytes())
 }
 
-func listDir(w http.ResponseWriter, r *http.Request, dir string, autorefresh bool) {
+func listDir(
+       w http.ResponseWriter,
+       r *http.Request,
+       dir string,
+       autorefresh,
+       gpgUpdate bool,
+) {
        dirPath := filepath.Join(*root, dir)
        if autorefresh {
-               if !refreshDir(w, r, dir, "") {
+               if !refreshDir(w, r, dir, "", gpgUpdate) {
                        return
                }
-       } else if _, err := os.Stat(dirPath); os.IsNotExist(err) && !refreshDir(w, r, dir, "") {
+       } else if _, err := os.Stat(dirPath); os.IsNotExist(err) && !refreshDir(w, r, dir, "", false) {
                return
        }
        files, err := ioutil.ReadDir(dirPath)
@@ -238,8 +276,9 @@ func listDir(w http.ResponseWriter, r *http.Request, dir string, autorefresh boo
                return
        }
        var result bytes.Buffer
-       result.WriteString(fmt.Sprintf(HTMLBegin, dir, dir))
+       result.WriteString(fmt.Sprintf(HTMLBegin, dir))
        var data []byte
+       var gpgSigAttr string
        var filenameClean string
        for _, file := range files {
                if !strings.HasSuffix(file.Name(), SHA256Ext) {
@@ -256,12 +295,18 @@ func listDir(w http.ResponseWriter, r *http.Request, dir string, autorefresh boo
                        return
                }
                filenameClean = strings.TrimSuffix(file.Name(), SHA256Ext)
+               if _, err = os.Stat(filepath.Join(dirPath, filenameClean+GPGSigExt)); os.IsNotExist(err) {
+                       gpgSigAttr = ""
+               } else {
+                       gpgSigAttr = GPGSigAttr
+               }
                result.WriteString(fmt.Sprintf(
                        HTMLElement,
                        strings.Join([]string{
                                *refreshURLPath, dir, "/",
                                filenameClean, "#", SHA256Prefix, hex.EncodeToString(data),
                        }, ""),
+                       gpgSigAttr,
                        filenameClean,
                ))
        }
@@ -273,7 +318,7 @@ func servePkg(w http.ResponseWriter, r *http.Request, dir, filename string) {
        log.Println(r.RemoteAddr, "get", filename)
        path := filepath.Join(*root, dir, filename)
        if _, err := os.Stat(path); os.IsNotExist(err) {
-               if !refreshDir(w, r, dir, filename) {
+               if !refreshDir(w, r, dir, filename, false) {
                        return
                }
        }
@@ -306,8 +351,10 @@ func serveUpload(w http.ResponseWriter, r *http.Request) {
                        return
                }
        }
+       gpgSigsExpected := make(map[string]struct{})
        for _, file := range r.MultipartForm.File["content"] {
                filename := file.Filename
+               gpgSigsExpected[filename+GPGSigExt] = struct{}{}
                log.Println(r.RemoteAddr, "put", filename, "by", username)
                dir := filename[:strings.LastIndex(filename, "-")]
                dirPath := filepath.Join(*root, dir)
@@ -370,7 +417,39 @@ func serveUpload(w http.ResponseWriter, r *http.Request) {
                        http.Error(w, err.Error(), http.StatusInternalServerError)
                        return
                }
-               if err = ioutil.WriteFile(path+SHA256Ext, digest, os.FileMode(0666)); err != nil {
+               if err = WriteFileSync(dirPath, path+SHA256Ext, digest); err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+       }
+       for _, file := range r.MultipartForm.File["gpg_signature"] {
+               filename := file.Filename
+               if _, exists := gpgSigsExpected[filename]; !exists {
+                       http.Error(w, "unexpected GPG signature filename", http.StatusBadRequest)
+                       return
+               }
+               delete(gpgSigsExpected, filename)
+               log.Println(r.RemoteAddr, "put", filename, "by", username)
+               dir := filename[:strings.LastIndex(filename, "-")]
+               dirPath := filepath.Join(*root, dir)
+               path := filepath.Join(dirPath, filename)
+               if _, err = os.Stat(path); err == nil {
+                       log.Println(r.RemoteAddr, "already exists", filename)
+                       http.Error(w, "Already exists", http.StatusBadRequest)
+                       return
+               }
+               src, err := file.Open()
+               if err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+               sig, err := ioutil.ReadAll(src)
+               src.Close()
+               if err != nil {
+                       http.Error(w, err.Error(), http.StatusInternalServerError)
+                       return
+               }
+               if err = WriteFileSync(dirPath, path, sig); err != nil {
                        http.Error(w, err.Error(), http.StatusInternalServerError)
                        return
                }
@@ -382,12 +461,19 @@ func handler(w http.ResponseWriter, r *http.Request) {
        case "GET":
                var path string
                var autorefresh bool
+               var gpgUpdate bool
                if strings.HasPrefix(r.URL.Path, *norefreshURLPath) {
                        path = strings.TrimPrefix(r.URL.Path, *norefreshURLPath)
-                       autorefresh = false
-               } else {
+               } else if strings.HasPrefix(r.URL.Path, *refreshURLPath) {
                        path = strings.TrimPrefix(r.URL.Path, *refreshURLPath)
                        autorefresh = true
+               } else if strings.HasPrefix(r.URL.Path, *gpgUpdateURLPath) {
+                       path = strings.TrimPrefix(r.URL.Path, *gpgUpdateURLPath)
+                       autorefresh = true
+                       gpgUpdate = true
+               } else {
+                       http.Error(w, "unknown action", http.StatusBadRequest)
+                       return
                }
                parts := strings.Split(strings.TrimSuffix(path, "/"), "/")
                if len(parts) > 2 {
@@ -398,7 +484,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
                        if parts[0] == "" {
                                listRoot(w, r)
                        } else {
-                               listDir(w, r, parts[0], autorefresh)
+                               listDir(w, r, parts[0], autorefresh, gpgUpdate)
                        }
                } else {
                        servePkg(w, r, parts[0], parts[1])
@@ -491,6 +577,7 @@ func main() {
        }
        http.HandleFunc(*norefreshURLPath, handler)
        http.HandleFunc(*refreshURLPath, handler)
+       http.HandleFunc(*gpgUpdateURLPath, handler)
 
        needsRefreshPasswd := make(chan os.Signal, 0)
        needsShutdown := make(chan os.Signal, 0)
index f30743bffda1bc50c2c16332b4bbca7ba9911b5c..17bcbedfdcd98fb03a86243e74f4d1c93df75466 100644 (file)
@@ -10,11 +10,12 @@ GoCheese is Python private package repository and caching proxy.
 It serves two purposes:
 
 @itemize
-@item hosting of private locally uploaded packages
-    (conforming to @url{https://www.python.org/dev/peps/pep-0503/, PEP-0503}
-    (Simple Repository API))
 @item proxying and caching of missing packages from upstream
-    @url{https://pypi.org/, PyPI}
+    @url{https://pypi.org/, PyPI}, conforming to
+    @url{https://www.python.org/dev/peps/pep-0503/, PEP-0503}
+    (Simple Repository API)
+@item hosting of private locally uploaded packages, conforming to
+    @url{https://warehouse.pypa.io/api-reference/legacy/, Warehouse Legacy API}
 @end itemize
 
 Initially it was created as a fork of
@@ -22,7 +23,7 @@ Initially it was created as a fork of
 but nearly all the code was rewritten. It has huge differences:
 
 @itemize
-@item proxying and caching of missing packages
+@item proxying and caching of missing packages, including GPG signatures
 @item atomic packages store on filesystem
 @item SHA256-checksummed packages: storing checksums, giving them back,
     verifying stored files integrity, verifying checksum of uploaded
@@ -74,6 +75,10 @@ twine upload
 If @command{twine} sends SHA256 checksum in the request, then uploaded
 file is checked against it.
 
+@option{-gpgupdate} is useful mainly for migrated from Pyshop
+repositories. It forces GPG signature files downloading for all existing
+package files.
+
 @node Passwords
 @unnumbered Password authentication
 
index c0ae7a592e43b58dd0a10bdf2c7912e5415bdb0c..2a3e2b243cd5e0afd485b27b7bdd38e522533ffd 100755 (executable)
@@ -29,18 +29,21 @@ ORDER BY package.name
     [ -n "$pkg" ]
     [ -n "$filename" ]
     src=$(echo $pkg | cut -c1)/$filename
-    dst=packages/$pkg/$filename 
+    dst=packages/$pkg/$filename
     [ -r $src ] || continue
     [ -r $dst ] && continue || :
     mkdir -p packages/$pkg
-    tee $dst < $src | sha256 | xxd -r -p > $dst.sha256
+    ln $src $dst
 done
 
 ########################################################################
 # Mark all private packages
 ########################################################################
 for pkg in $(echo "SELECT name FROM package WHERE local = true" | sqlite3 pyshop.db); do
-    touch packages/$(echo $pkg | pkgname)/.private
+    cd packages/$(echo $pkg | pkgname)
+    for f in * ; do sha256 < $f | xxd -r -p > $f.sha256 ; done
+    touch .internal
+    cd ../..
 done
 
 ########################################################################
@@ -48,7 +51,7 @@ done
 ########################################################################
 cd packages
 for pkg in * ; do
-    curl http://localhost:8080/simple/$pkg/ > /dev/null
+    curl http://localhost:8080/gpgupdate/$pkg/ > /dev/null
 done
 
 ########################################################################
diff --git a/tmp.go b/tmp.go
index 876e23d8502bc3690bff9e99bc0523859420f64f..9438646907f2b5bce2fd01ad8dd9e7ddd5382558 100644 (file)
--- a/tmp.go
+++ b/tmp.go
@@ -30,3 +30,22 @@ func TempFile(dir string) (*os.File, error) {
        name := filepath.Join(dir, "nncp"+suffix)
        return os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, os.FileMode(0666))
 }
+
+func WriteFileSync(dirPath, filePath string, data []byte) error {
+       dst, err := TempFile(dirPath)
+       if err != nil {
+               return err
+       }
+       if _, err = dst.Write(data); err != nil {
+               os.Remove(dst.Name())
+               dst.Close()
+               return err
+       }
+       if err = dst.Sync(); err != nil {
+               os.Remove(dst.Name())
+               dst.Close()
+               return err
+       }
+       dst.Close()
+       return os.Rename(dst.Name(), filePath)
+}