2 GoCheese -- Python private package repository and caching proxy
3 Copyright (C) 2019-2020 Sergey Matveev <stargrave@stargrave.org>
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, version 3 of the License.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
37 "golang.org/x/crypto/blake2b"
40 func blake2b256New() hash.Hash {
41 h, err := blake2b.New256(nil)
49 w http.ResponseWriter,
51 pkgName, filenameGet string,
54 if _, err := os.Stat(filepath.Join(*root, pkgName, InternalFlag)); err == nil {
57 resp, err := http.Get(*pypiURL + pkgName + "/")
59 log.Println("error", r.RemoteAddr, "refresh", pkgName, err)
60 http.Error(w, err.Error(), http.StatusBadGateway)
63 body, err := ioutil.ReadAll(resp.Body)
66 log.Println("error", r.RemoteAddr, "refresh", pkgName, err)
67 http.Error(w, err.Error(), http.StatusBadGateway)
70 if !mkdirForPkg(w, r, pkgName) {
73 dirPath := filepath.Join(*root, pkgName)
74 for _, lineRaw := range bytes.Split(body, []byte("\n")) {
75 submatches := pkgPyPI.FindStringSubmatch(string(lineRaw))
76 if len(submatches) == 0 {
80 filename := submatches[2]
81 pkgURL, err := url.Parse(uri)
83 log.Println("error", r.RemoteAddr, "refresh", uri, err)
84 http.Error(w, err.Error(), http.StatusBadGateway)
88 if pkgURL.Fragment == "" {
89 log.Println(r.RemoteAddr, "pypi", filename, "no digest")
90 http.Error(w, "no digest provided", http.StatusBadGateway)
93 digestInfo := strings.Split(pkgURL.Fragment, "=")
94 if len(digestInfo) == 1 {
95 // Ancient non PEP-0503 PyPIs, assume MD5
96 digestInfo = []string{"md5", digestInfo[0]}
97 } else if len(digestInfo) != 2 {
98 log.Println("error", r.RemoteAddr, "pypi", filename, "invalid digest")
99 http.Error(w, "invalid digest provided", http.StatusBadGateway)
102 digest, err := hex.DecodeString(digestInfo[1])
104 log.Println("error", r.RemoteAddr, "pypi", filename, "invalid digest")
105 http.Error(w, err.Error(), http.StatusBadGateway)
108 hashAlgo := digestInfo[0]
109 var hasherNew func() hash.Hash
116 hasherNew = sha256.New
117 hashSize = sha256.Size
119 hasherNew = sha512.New
120 hashSize = sha512.Size
121 case HashAlgoBLAKE2b256:
122 hasherNew = blake2b256New
123 hashSize = blake2b.Size256
125 log.Println("error", r.RemoteAddr, "pypi", filename, "unknown digest", hashAlgo)
126 http.Error(w, "unknown digest algorithm", http.StatusBadGateway)
129 if len(digest) != hashSize {
130 log.Println("error", r.RemoteAddr, "pypi", filename, "invalid digest length")
131 http.Error(w, "invalid digest length", http.StatusBadGateway)
136 if pkgURL.Host == "" {
137 uri = pypiURLParsed.ResolveReference(pkgURL).String()
139 uri = pkgURL.String()
142 path := filepath.Join(dirPath, filename)
143 if filename == filenameGet {
145 // Skip heavy remote call, when shutting down
146 http.Error(w, "shutting down", http.StatusInternalServerError)
149 log.Println(r.RemoteAddr, "pypi", filename, "download")
150 resp, err = http.Get(uri)
152 log.Println("error", r.RemoteAddr, "pypi", filename, "download", err)
153 http.Error(w, err.Error(), http.StatusBadGateway)
156 defer resp.Body.Close()
157 hasher := hasherNew()
158 hasherSHA256 := sha256.New()
159 dst, err := TempFile(dirPath)
161 log.Println("error", r.RemoteAddr, "pypi", filename, err)
162 http.Error(w, err.Error(), http.StatusInternalServerError)
165 dstBuf := bufio.NewWriter(dst)
166 wrs := []io.Writer{hasher, dstBuf}
167 if hashAlgo != HashAlgoSHA256 {
168 wrs = append(wrs, hasherSHA256)
170 wr := io.MultiWriter(wrs...)
171 if _, err = io.Copy(wr, resp.Body); err != nil {
172 os.Remove(dst.Name())
174 log.Println("error", r.RemoteAddr, "pypi", filename, err)
175 http.Error(w, err.Error(), http.StatusInternalServerError)
178 if err = dstBuf.Flush(); err != nil {
179 os.Remove(dst.Name())
181 log.Println("error", r.RemoteAddr, "pypi", filename, err)
182 http.Error(w, err.Error(), http.StatusInternalServerError)
185 if bytes.Compare(hasher.Sum(nil), digest) != 0 {
186 log.Println(r.RemoteAddr, "pypi", filename, "digest mismatch")
187 os.Remove(dst.Name())
189 http.Error(w, "digest mismatch", http.StatusBadGateway)
192 if err = dst.Sync(); err != nil {
193 os.Remove(dst.Name())
195 log.Println("error", r.RemoteAddr, "pypi", filename, err)
196 http.Error(w, err.Error(), http.StatusInternalServerError)
199 if err = dst.Close(); err != nil {
200 log.Println("error", r.RemoteAddr, "pypi", filename, err)
201 http.Error(w, err.Error(), http.StatusInternalServerError)
204 if err = os.Rename(dst.Name(), path); err != nil {
205 log.Println("error", r.RemoteAddr, "pypi", filename, err)
206 http.Error(w, err.Error(), http.StatusInternalServerError)
209 if err = DirSync(dirPath); err != nil {
210 log.Println("error", r.RemoteAddr, "pypi", filename, err)
211 http.Error(w, err.Error(), http.StatusInternalServerError)
214 if hashAlgo != HashAlgoSHA256 {
215 hashAlgo = HashAlgoSHA256
216 digest = hasherSHA256.Sum(nil)
217 for _, algo := range knownHashAlgos[1:] {
218 os.Remove(path + "." + algo)
222 if filename == filenameGet || gpgUpdate {
223 if _, err = os.Stat(path); err != nil {
226 resp, err := http.Get(uri + GPGSigExt)
230 if resp.StatusCode != http.StatusOK {
234 sig, err := ioutil.ReadAll(resp.Body)
239 if !bytes.HasPrefix(sig, []byte("-----BEGIN PGP SIGNATURE-----")) {
240 log.Println(r.RemoteAddr, "pypi", filename+GPGSigExt, "non PGP")
243 if err = WriteFileSync(dirPath, path+GPGSigExt, sig); err != nil {
244 log.Println("error", r.RemoteAddr, "pypi", filename+GPGSigExt, err)
245 http.Error(w, err.Error(), http.StatusInternalServerError)
248 log.Println(r.RemoteAddr, "pypi", filename+GPGSigExt, "downloaded")
251 path = path + "." + hashAlgo
252 _, err = os.Stat(path)
256 if !os.IsNotExist(err) {
257 log.Println("error", r.RemoteAddr, "pypi", path, err)
258 http.Error(w, err.Error(), http.StatusInternalServerError)
261 log.Println(r.RemoteAddr, "pypi", filename, "touch")
262 if err = WriteFileSync(dirPath, path, digest); err != nil {
263 log.Println("error", r.RemoteAddr, "pypi", path, err)
264 http.Error(w, err.Error(), http.StatusInternalServerError)