2 GoCheese -- Python private package repository and caching proxy
3 Copyright (C) 2019-2021 Sergey Matveev <stargrave@stargrave.org>
4 2019-2021 Elena Balakhonova <balakhonova_e@riseup.net>
6 This program is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, version 3 of the License.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
36 "go.cypherpunks.ru/recfile"
39 var NormalizationRe = regexp.MustCompilePOSIX("[-_.]+")
41 func serveUpload(w http.ResponseWriter, r *http.Request) {
43 username, password, ok := r.BasicAuth()
45 log.Println(r.RemoteAddr, "unauthenticated", username)
46 http.Error(w, "unauthenticated", http.StatusUnauthorized)
50 auther, ok := Passwords[username]
52 if !ok || !auther.Auth(password) {
53 log.Println(r.RemoteAddr, "unauthenticated", username)
54 http.Error(w, "unauthenticated", http.StatusUnauthorized)
60 if err = r.ParseMultipartForm(1 << 20); err != nil {
61 http.Error(w, err.Error(), http.StatusBadRequest)
64 pkgNames, exists := r.MultipartForm.Value["name"]
65 if !exists || len(pkgNames) != 1 {
66 http.Error(w, "single name is expected in request", http.StatusBadRequest)
69 pkgName := strings.ToLower(NormalizationRe.ReplaceAllString(pkgNames[0], "-"))
70 dirPath := filepath.Join(*Root, pkgName)
71 var digestExpected []byte
72 if digestExpectedHex, exists := r.MultipartForm.Value["sha256_digest"]; exists {
73 digestExpected, err = hex.DecodeString(digestExpectedHex[0])
75 http.Error(w, "bad sha256_digest: "+err.Error(), http.StatusBadRequest)
79 gpgSigsExpected := make(map[string]struct{})
80 now := time.Now().UTC()
82 // Checking is it internal package
83 if _, err = os.Stat(filepath.Join(dirPath, InternalFlag)); err != nil {
84 log.Println(r.RemoteAddr, "non-internal package", pkgName)
85 http.Error(w, "unknown internal package", http.StatusUnauthorized)
89 for _, file := range r.MultipartForm.File["content"] {
90 filename := file.Filename
91 gpgSigsExpected[filename+GPGSigExt] = struct{}{}
92 log.Println(r.RemoteAddr, "put", filename, "by", username)
93 path := filepath.Join(dirPath, filename)
94 if _, err = os.Stat(path); err == nil {
95 log.Println(r.RemoteAddr, filename, "already exists")
96 http.Error(w, "already exists", http.StatusBadRequest)
99 if !mkdirForPkg(w, r, pkgName) {
102 src, err := file.Open()
105 log.Println("error", r.RemoteAddr, filename, err)
106 http.Error(w, err.Error(), http.StatusInternalServerError)
109 dst, err := TempFile(dirPath)
111 log.Println("error", r.RemoteAddr, filename, err)
112 http.Error(w, err.Error(), http.StatusInternalServerError)
115 dstBuf := bufio.NewWriter(dst)
116 hasher := sha256.New()
117 wr := io.MultiWriter(hasher, dst)
118 if _, err = io.Copy(wr, src); err != nil {
119 log.Println("error", r.RemoteAddr, filename, err)
120 os.Remove(dst.Name())
122 http.Error(w, err.Error(), http.StatusInternalServerError)
125 if err = dstBuf.Flush(); err != nil {
126 log.Println("error", r.RemoteAddr, filename, err)
127 os.Remove(dst.Name())
129 http.Error(w, err.Error(), http.StatusInternalServerError)
133 if err = dst.Sync(); err != nil {
134 log.Println("error", r.RemoteAddr, filename, err)
135 os.Remove(dst.Name())
137 http.Error(w, err.Error(), http.StatusInternalServerError)
142 digest := hasher.Sum(nil)
143 if digestExpected != nil {
144 if bytes.Compare(digestExpected, digest) == 0 {
145 log.Println(r.RemoteAddr, filename, "good checksum received")
147 log.Println(r.RemoteAddr, filename, "bad checksum received")
148 http.Error(w, "bad checksum", http.StatusBadRequest)
149 os.Remove(dst.Name())
153 if err = os.Rename(dst.Name(), path); err != nil {
154 log.Println("error", r.RemoteAddr, path, err)
155 http.Error(w, err.Error(), http.StatusInternalServerError)
158 if err = DirSync(dirPath); err != nil {
159 log.Println("error", r.RemoteAddr, dirPath, err)
160 http.Error(w, err.Error(), http.StatusInternalServerError)
163 if err = WriteFileSync(dirPath, path+"."+HashAlgoSHA256, digest, now); err != nil {
164 log.Println("error", r.RemoteAddr, path+"."+HashAlgoSHA256, err)
165 http.Error(w, err.Error(), http.StatusInternalServerError)
169 for _, file := range r.MultipartForm.File["gpg_signature"] {
170 filename := file.Filename
171 if _, exists := gpgSigsExpected[filename]; !exists {
172 log.Println(r.RemoteAddr, filename, "unexpected GPG signature filename")
173 http.Error(w, "unexpected GPG signature filename", http.StatusBadRequest)
176 delete(gpgSigsExpected, filename)
177 log.Println(r.RemoteAddr, "put", filename, "by", username)
178 path := filepath.Join(dirPath, filename)
179 if _, err = os.Stat(path); err == nil {
180 log.Println(r.RemoteAddr, filename, "already exists")
181 http.Error(w, "already exists", http.StatusBadRequest)
184 src, err := file.Open()
186 log.Println("error", r.RemoteAddr, filename, err)
187 http.Error(w, err.Error(), http.StatusInternalServerError)
190 sig, err := ioutil.ReadAll(src)
193 log.Println("error", r.RemoteAddr, filename, err)
194 http.Error(w, err.Error(), http.StatusInternalServerError)
197 if err = WriteFileSync(dirPath, path, sig, now); err != nil {
198 log.Println("error", r.RemoteAddr, path, err)
199 http.Error(w, err.Error(), http.StatusInternalServerError)
205 wr := recfile.NewWriter(&buf)
206 for formField, recField := range map[string]string{
207 "name": MetadataFieldName,
208 "version": MetadataFieldVersion,
209 "platform": MetadataFieldPlatform,
210 "supported_platform": MetadataFieldSupportedPlatform,
211 "summary": MetadataFieldSummary,
212 "description": MetadataFieldDescription,
213 "description_content_type": MetadataFieldDescriptionContentType,
214 "keywords": MetadataFieldKeywords,
215 "home_page": MetadataFieldHomePage,
216 "author": MetadataFieldAuthor,
217 "author_email": MetadataFieldAuthorEmail,
218 "maintainer": MetadataFieldMaintainer,
219 "maintainer_email": MetadataFieldMaintainerEmail,
220 "license": MetadataFieldLicense,
221 "classifiers": MetadataFieldClassifier,
222 "requires_dist": MetadataFieldRequiresDist,
223 "requires_python": MetadataFieldRequiresPython,
224 "requires_external": MetadataFieldRequiresExternal,
225 "project_url": MetadataFieldProjectURL,
226 "provides_extra": MetadataFieldProvidesExtra,
228 if vs, exists := r.MultipartForm.Value[formField]; exists {
229 for _, v := range vs {
230 lines := strings.Split(v, "\n")
232 _, err = wr.WriteFieldMultiline(
233 metadataFieldToRecField(recField),
237 _, err = wr.WriteFields(recfile.Field{
238 Name: metadataFieldToRecField(recField),
248 path := filepath.Join(dirPath, MetadataFile)
249 if err = WriteFileSync(dirPath, path, buf.Bytes(), now); err != nil {
250 log.Println("error", r.RemoteAddr, path, err)
251 http.Error(w, err.Error(), http.StatusInternalServerError)