]> Cypherpunks.ru repositories - gocheese.git/blob - gocheese.go
Refactor digest processing, BLAKE2b-256 support, cleanup non-SHA256 digests
[gocheese.git] / gocheese.go
1 /*
2 GoCheese -- Python private package repository and caching proxy
3 Copyright (C) 2019 Sergey Matveev <stargrave@stargrave.org>
4               2019 Elena Balakhonova <balakhonova_e@riseup.net>
5
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.
9
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.
14
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/>.
17 */
18
19 // Python private package repository and caching proxy
20 package main
21
22 import (
23         "bufio"
24         "bytes"
25         "context"
26         "crypto/md5"
27         "crypto/sha256"
28         "crypto/sha512"
29         "encoding/hex"
30         "flag"
31         "fmt"
32         "hash"
33         "io"
34         "io/ioutil"
35         "log"
36         "net"
37         "net/http"
38         "net/url"
39         "os"
40         "os/signal"
41         "path/filepath"
42         "regexp"
43         "runtime"
44         "strings"
45         "syscall"
46         "time"
47
48         "golang.org/x/crypto/blake2b"
49         "golang.org/x/net/netutil"
50 )
51
52 const (
53         HTMLBegin = `<!DOCTYPE html>
54 <html>
55   <head>
56     <title>Links for %s</title>
57   </head>
58   <body>
59 `
60         HTMLEnd      = "  </body>\n</html>\n"
61         HTMLElement  = "    <a href=\"%s\"%s>%s</a><br/>\n"
62         InternalFlag = ".internal"
63         GPGSigExt    = ".asc"
64
65         Warranty = `This program is free software: you can redistribute it and/or modify
66 it under the terms of the GNU General Public License as published by
67 the Free Software Foundation, version 3 of the License.
68
69 This program is distributed in the hope that it will be useful,
70 but WITHOUT ANY WARRANTY; without even the implied warranty of
71 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
72 GNU General Public License for more details.
73
74 You should have received a copy of the GNU General Public License
75 along with this program.  If not, see <http://www.gnu.org/licenses/>.`
76 )
77
78 var (
79         pkgPyPI         = regexp.MustCompile(`^.*<a href="([^"]+)"[^>]*>(.+)</a><br/>.*$`)
80         normalizationRe = regexp.MustCompilePOSIX("[-_.]+")
81
82         HashAlgoSHA256              = "sha256"
83         HashAlgoBLAKE2b256          = "blake2_256"
84         HashAlgoSHA512              = "sha512"
85         HashAlgoMD5                 = "md5"
86         knownHashAlgos     []string = []string{
87                 HashAlgoSHA256,
88                 HashAlgoBLAKE2b256,
89                 HashAlgoSHA512,
90                 HashAlgoMD5,
91         }
92
93         root             = flag.String("root", "./packages", "Path to packages directory")
94         bind             = flag.String("bind", "[::]:8080", "Address to bind to")
95         tlsCert          = flag.String("tls-cert", "", "Path to TLS X.509 certificate")
96         tlsKey           = flag.String("tls-key", "", "Path to TLS X.509 private key")
97         norefreshURLPath = flag.String("norefresh", "/norefresh/", "Non-refreshing URL path")
98         refreshURLPath   = flag.String("refresh", "/simple/", "Auto-refreshing URL path")
99         gpgUpdateURLPath = flag.String("gpgupdate", "/gpgupdate/", "GPG forceful refreshing URL path")
100         pypiURL          = flag.String("pypi", "https://pypi.org/simple/", "Upstream PyPI URL")
101         passwdPath       = flag.String("passwd", "passwd", "Path to file with authenticators")
102         passwdCheck      = flag.Bool("passwd-check", false, "Test the -passwd file for syntax errors and exit")
103         fsck             = flag.Bool("fsck", false, "Check integrity of all packages")
104         maxClients       = flag.Int("maxclients", 128, "Maximal amount of simultaneous clients")
105         version          = flag.Bool("version", false, "Print version information")
106         warranty         = flag.Bool("warranty", false, "Print warranty information")
107
108         Version       string = "UNKNOWN"
109         killed        bool
110         pypiURLParsed *url.URL
111 )
112
113 func mkdirForPkg(w http.ResponseWriter, r *http.Request, dir string) bool {
114         path := filepath.Join(*root, dir)
115         if _, err := os.Stat(path); os.IsNotExist(err) {
116                 if err = os.Mkdir(path, os.FileMode(0777)); err != nil {
117                         http.Error(w, err.Error(), http.StatusInternalServerError)
118                         return false
119                 }
120                 log.Println(r.RemoteAddr, "mkdir", dir)
121         }
122         return true
123 }
124
125 func blake2b256New() hash.Hash {
126         h, err := blake2b.New256(nil)
127         if err != nil {
128                 panic(err)
129         }
130         return h
131 }
132
133 func refreshDir(
134         w http.ResponseWriter,
135         r *http.Request,
136         dir,
137         filenameGet string,
138         gpgUpdate bool,
139 ) bool {
140         if _, err := os.Stat(filepath.Join(*root, dir, InternalFlag)); err == nil {
141                 return true
142         }
143         resp, err := http.Get(*pypiURL + dir + "/")
144         if err != nil {
145                 http.Error(w, err.Error(), http.StatusBadGateway)
146                 return false
147         }
148         body, err := ioutil.ReadAll(resp.Body)
149         resp.Body.Close()
150         if err != nil {
151                 http.Error(w, err.Error(), http.StatusBadGateway)
152                 return false
153         }
154         if !mkdirForPkg(w, r, dir) {
155                 return false
156         }
157         dirPath := filepath.Join(*root, dir)
158         for _, lineRaw := range bytes.Split(body, []byte("\n")) {
159                 submatches := pkgPyPI.FindStringSubmatch(string(lineRaw))
160                 if len(submatches) == 0 {
161                         continue
162                 }
163                 uri := submatches[1]
164                 filename := submatches[2]
165                 pkgURL, err := url.Parse(uri)
166                 if err != nil {
167                         http.Error(w, err.Error(), http.StatusBadGateway)
168                         return false
169                 }
170
171                 if pkgURL.Fragment == "" {
172                         log.Println(r.RemoteAddr, "pypi", filename, "no digest provided")
173                         http.Error(w, "no digest provided", http.StatusBadGateway)
174                         return false
175                 }
176                 digestInfo := strings.Split(pkgURL.Fragment, "=")
177                 if len(digestInfo) == 1 {
178                         // Ancient non PEP-0503 PyPIs, assume MD5
179                         digestInfo = []string{"md5", digestInfo[0]}
180                 } else if len(digestInfo) != 2 {
181                         log.Println(r.RemoteAddr, "pypi", filename, "invalid digest provided")
182                         http.Error(w, "invalid digest provided", http.StatusBadGateway)
183                         return false
184                 }
185                 digest, err := hex.DecodeString(digestInfo[1])
186                 if err != nil {
187                         http.Error(w, err.Error(), http.StatusBadGateway)
188                         return false
189                 }
190                 hashAlgo := digestInfo[0]
191                 var hasherNew func() hash.Hash
192                 var hashSize int
193                 switch hashAlgo {
194                 case HashAlgoMD5:
195                         hasherNew = md5.New
196                         hashSize = md5.Size
197                 case HashAlgoSHA256:
198                         hasherNew = sha256.New
199                         hashSize = sha256.Size
200                 case HashAlgoSHA512:
201                         hasherNew = sha512.New
202                         hashSize = sha512.Size
203                 case HashAlgoBLAKE2b256:
204                         hasherNew = blake2b256New
205                         hashSize = blake2b.Size256
206                 default:
207                         log.Println(
208                                 r.RemoteAddr, "pypi", filename,
209                                 "unknown digest algorithm", hashAlgo,
210                         )
211                         http.Error(w, "unknown digest algorithm", http.StatusBadGateway)
212                         return false
213                 }
214                 if len(digest) != hashSize {
215                         log.Println(r.RemoteAddr, "pypi", filename, "invalid digest length")
216                         http.Error(w, "invalid digest length", http.StatusBadGateway)
217                         return false
218                 }
219
220                 pkgURL.Fragment = ""
221                 if pkgURL.Host == "" {
222                         uri = pypiURLParsed.ResolveReference(pkgURL).String()
223                 } else {
224                         uri = pkgURL.String()
225                 }
226
227                 path := filepath.Join(dirPath, filename)
228                 if filename == filenameGet {
229                         if killed {
230                                 // Skip heavy remote call, when shutting down
231                                 http.Error(w, "shutting down", http.StatusInternalServerError)
232                                 return false
233                         }
234                         log.Println(r.RemoteAddr, "pypi download", filename)
235                         resp, err = http.Get(uri)
236                         if err != nil {
237                                 log.Println(r.RemoteAddr, "pypi download error:", err.Error())
238                                 http.Error(w, err.Error(), http.StatusBadGateway)
239                                 return false
240                         }
241                         defer resp.Body.Close()
242                         hasher := hasherNew()
243                         hasherSHA256 := sha256.New()
244                         dst, err := TempFile(dirPath)
245                         if err != nil {
246                                 http.Error(w, err.Error(), http.StatusInternalServerError)
247                                 return false
248                         }
249                         dstBuf := bufio.NewWriter(dst)
250                         wrs := []io.Writer{hasher, dstBuf}
251                         if hashAlgo != HashAlgoSHA256 {
252                                 wrs = append(wrs, hasherSHA256)
253                         }
254                         wr := io.MultiWriter(wrs...)
255                         if _, err = io.Copy(wr, resp.Body); err != nil {
256                                 os.Remove(dst.Name())
257                                 dst.Close()
258                                 http.Error(w, err.Error(), http.StatusInternalServerError)
259                                 return false
260                         }
261                         if err = dstBuf.Flush(); err != nil {
262                                 os.Remove(dst.Name())
263                                 dst.Close()
264                                 http.Error(w, err.Error(), http.StatusInternalServerError)
265                                 return false
266                         }
267                         if bytes.Compare(hasher.Sum(nil), digest) != 0 {
268                                 log.Println(r.RemoteAddr, "pypi", filename, "digest mismatch")
269                                 os.Remove(dst.Name())
270                                 dst.Close()
271                                 http.Error(w, "digest mismatch", http.StatusBadGateway)
272                                 return false
273                         }
274                         if err = dst.Sync(); err != nil {
275                                 os.Remove(dst.Name())
276                                 dst.Close()
277                                 http.Error(w, err.Error(), http.StatusInternalServerError)
278                                 return false
279                         }
280                         if err = dst.Close(); err != nil {
281                                 http.Error(w, err.Error(), http.StatusInternalServerError)
282                                 return false
283                         }
284                         if err = os.Rename(dst.Name(), path); err != nil {
285                                 http.Error(w, err.Error(), http.StatusInternalServerError)
286                                 return false
287                         }
288                         if err = DirSync(dirPath); err != nil {
289                                 http.Error(w, err.Error(), http.StatusInternalServerError)
290                                 return false
291                         }
292                         if hashAlgo != HashAlgoSHA256 {
293                                 hashAlgo = HashAlgoSHA256
294                                 digest = hasherSHA256.Sum(nil)
295                                 for _, algo := range knownHashAlgos[1:] {
296                                         os.Remove(path + "." + algo)
297                                 }
298                         }
299                 }
300                 if filename == filenameGet || gpgUpdate {
301                         if _, err = os.Stat(path); err != nil {
302                                 goto GPGSigSkip
303                         }
304                         resp, err := http.Get(uri + GPGSigExt)
305                         if err != nil {
306                                 goto GPGSigSkip
307                         }
308                         if resp.StatusCode != http.StatusOK {
309                                 resp.Body.Close()
310                                 goto GPGSigSkip
311                         }
312                         sig, err := ioutil.ReadAll(resp.Body)
313                         resp.Body.Close()
314                         if err != nil {
315                                 goto GPGSigSkip
316                         }
317                         if !bytes.HasPrefix(sig, []byte("-----BEGIN PGP SIGNATURE-----")) {
318                                 log.Println(r.RemoteAddr, "pypi non PGP signature", filename)
319                                 goto GPGSigSkip
320                         }
321                         if err = WriteFileSync(dirPath, path+GPGSigExt, sig); err != nil {
322                                 http.Error(w, err.Error(), http.StatusInternalServerError)
323                                 return false
324                         }
325                         log.Println(r.RemoteAddr, "pypi downloaded signature", filename)
326                 }
327         GPGSigSkip:
328                 path = path + "." + hashAlgo
329                 _, err = os.Stat(path)
330                 if err == nil {
331                         continue
332                 }
333                 if !os.IsNotExist(err) {
334                         http.Error(w, err.Error(), http.StatusInternalServerError)
335                         return false
336                 }
337                 log.Println(r.RemoteAddr, "pypi touch", filename)
338                 if err = WriteFileSync(dirPath, path, digest); err != nil {
339                         http.Error(w, err.Error(), http.StatusInternalServerError)
340                         return false
341                 }
342         }
343         return true
344 }
345
346 func listRoot(w http.ResponseWriter, r *http.Request) {
347         files, err := ioutil.ReadDir(*root)
348         if err != nil {
349                 http.Error(w, err.Error(), http.StatusInternalServerError)
350                 return
351         }
352         var result bytes.Buffer
353         result.WriteString(fmt.Sprintf(HTMLBegin, "root"))
354         for _, file := range files {
355                 if file.Mode().IsDir() {
356                         result.WriteString(fmt.Sprintf(
357                                 HTMLElement,
358                                 *refreshURLPath+file.Name()+"/",
359                                 file.Name(),
360                         ))
361                 }
362         }
363         result.WriteString(HTMLEnd)
364         w.Write(result.Bytes())
365 }
366
367 func listDir(
368         w http.ResponseWriter,
369         r *http.Request,
370         dir string,
371         autorefresh,
372         gpgUpdate bool,
373 ) {
374         dirPath := filepath.Join(*root, dir)
375         if autorefresh {
376                 if !refreshDir(w, r, dir, "", gpgUpdate) {
377                         return
378                 }
379         } else if _, err := os.Stat(dirPath); os.IsNotExist(err) && !refreshDir(w, r, dir, "", false) {
380                 return
381         }
382         fis, err := ioutil.ReadDir(dirPath)
383         if err != nil {
384                 http.Error(w, err.Error(), http.StatusInternalServerError)
385                 return
386         }
387         files := make(map[string]struct{}, len(fis)/2)
388         for _, fi := range fis {
389                 files[fi.Name()] = struct{}{}
390         }
391         var result bytes.Buffer
392         result.WriteString(fmt.Sprintf(HTMLBegin, dir))
393         for _, algo := range knownHashAlgos {
394                 for fn, _ := range files {
395                         if killed {
396                                 // Skip expensive I/O when shutting down
397                                 http.Error(w, "shutting down", http.StatusInternalServerError)
398                                 return
399                         }
400                         if !strings.HasSuffix(fn, "."+algo) {
401                                 continue
402                         }
403                         delete(files, fn)
404                         digest, err := ioutil.ReadFile(filepath.Join(dirPath, fn))
405                         if err != nil {
406                                 http.Error(w, err.Error(), http.StatusInternalServerError)
407                                 return
408                         }
409                         fnClean := strings.TrimSuffix(fn, "."+algo)
410                         delete(files, fnClean)
411                         gpgSigAttr := ""
412                         if _, err = os.Stat(filepath.Join(dirPath, fnClean+GPGSigExt)); err == nil {
413                                 gpgSigAttr = " data-gpg-sig=true"
414                                 delete(files, fnClean+GPGSigExt)
415                         }
416                         result.WriteString(fmt.Sprintf(
417                                 HTMLElement,
418                                 strings.Join([]string{
419                                         *refreshURLPath, dir, "/", fnClean,
420                                         "#", algo, "=", hex.EncodeToString(digest),
421                                 }, ""),
422                                 gpgSigAttr,
423                                 fnClean,
424                         ))
425                 }
426         }
427         result.WriteString(HTMLEnd)
428         w.Write(result.Bytes())
429 }
430
431 func servePkg(w http.ResponseWriter, r *http.Request, dir, filename string) {
432         log.Println(r.RemoteAddr, "get", filename)
433         path := filepath.Join(*root, dir, filename)
434         if _, err := os.Stat(path); os.IsNotExist(err) {
435                 if !refreshDir(w, r, dir, filename, false) {
436                         return
437                 }
438         }
439         http.ServeFile(w, r, path)
440 }
441
442 func serveUpload(w http.ResponseWriter, r *http.Request) {
443         // Authentication
444         username, password, ok := r.BasicAuth()
445         if !ok {
446                 log.Println(r.RemoteAddr, "unauthenticated", username)
447                 http.Error(w, "unauthenticated", http.StatusUnauthorized)
448                 return
449         }
450         auther, ok := passwords[username]
451         if !ok || !auther.Auth(password) {
452                 log.Println(r.RemoteAddr, "unauthenticated", username)
453                 http.Error(w, "unauthenticated", http.StatusUnauthorized)
454                 return
455         }
456
457         // Form parsing
458         var err error
459         if err = r.ParseMultipartForm(1 << 20); err != nil {
460                 http.Error(w, err.Error(), http.StatusBadRequest)
461                 return
462         }
463         pkgNames, exists := r.MultipartForm.Value["name"]
464         if !exists || len(pkgNames) != 1 {
465                 http.Error(w, "single name is expected in request", http.StatusBadRequest)
466                 return
467         }
468         pkgName := normalizationRe.ReplaceAllString(pkgNames[0], "-")
469         dirPath := filepath.Join(*root, pkgName)
470         var digestExpected []byte
471         if digestExpectedHex, exists := r.MultipartForm.Value["sha256_digest"]; exists {
472                 digestExpected, err = hex.DecodeString(digestExpectedHex[0])
473                 if err != nil {
474                         http.Error(w, "bad sha256_digest: "+err.Error(), http.StatusBadRequest)
475                         return
476                 }
477         }
478         gpgSigsExpected := make(map[string]struct{})
479
480         // Checking is it internal package
481         if _, err = os.Stat(filepath.Join(dirPath, InternalFlag)); err != nil {
482                 log.Println(r.RemoteAddr, "non-internal package", pkgName)
483                 http.Error(w, "unknown internal package", http.StatusUnauthorized)
484                 return
485         }
486
487         for _, file := range r.MultipartForm.File["content"] {
488                 filename := file.Filename
489                 gpgSigsExpected[filename+GPGSigExt] = struct{}{}
490                 log.Println(r.RemoteAddr, "put", filename, "by", username)
491                 path := filepath.Join(dirPath, filename)
492                 if _, err = os.Stat(path); err == nil {
493                         log.Println(r.RemoteAddr, "already exists", filename)
494                         http.Error(w, "already exists", http.StatusBadRequest)
495                         return
496                 }
497                 if !mkdirForPkg(w, r, pkgName) {
498                         return
499                 }
500                 src, err := file.Open()
501                 defer src.Close()
502                 if err != nil {
503                         http.Error(w, err.Error(), http.StatusInternalServerError)
504                         return
505                 }
506                 dst, err := TempFile(dirPath)
507                 if err != nil {
508                         http.Error(w, err.Error(), http.StatusInternalServerError)
509                         return
510                 }
511                 dstBuf := bufio.NewWriter(dst)
512                 hasher := sha256.New()
513                 wr := io.MultiWriter(hasher, dst)
514                 if _, err = io.Copy(wr, src); err != nil {
515                         os.Remove(dst.Name())
516                         dst.Close()
517                         http.Error(w, err.Error(), http.StatusInternalServerError)
518                         return
519                 }
520                 if err = dstBuf.Flush(); err != nil {
521                         os.Remove(dst.Name())
522                         dst.Close()
523                         http.Error(w, err.Error(), http.StatusInternalServerError)
524                         return
525                 }
526                 if err = dst.Sync(); err != nil {
527                         os.Remove(dst.Name())
528                         dst.Close()
529                         http.Error(w, err.Error(), http.StatusInternalServerError)
530                         return
531                 }
532                 dst.Close()
533                 digest := hasher.Sum(nil)
534                 if digestExpected != nil {
535                         if bytes.Compare(digestExpected, digest) == 0 {
536                                 log.Println(r.RemoteAddr, filename, "good checksum received")
537                         } else {
538                                 log.Println(r.RemoteAddr, filename, "bad checksum received")
539                                 http.Error(w, "bad checksum", http.StatusBadRequest)
540                                 os.Remove(dst.Name())
541                                 return
542                         }
543                 }
544                 if err = os.Rename(dst.Name(), path); err != nil {
545                         http.Error(w, err.Error(), http.StatusInternalServerError)
546                         return
547                 }
548                 if err = DirSync(dirPath); err != nil {
549                         http.Error(w, err.Error(), http.StatusInternalServerError)
550                         return
551                 }
552                 if err = WriteFileSync(dirPath, path+"."+HashAlgoSHA256, digest); err != nil {
553                         http.Error(w, err.Error(), http.StatusInternalServerError)
554                         return
555                 }
556         }
557         for _, file := range r.MultipartForm.File["gpg_signature"] {
558                 filename := file.Filename
559                 if _, exists := gpgSigsExpected[filename]; !exists {
560                         http.Error(w, "unexpected GPG signature filename", http.StatusBadRequest)
561                         return
562                 }
563                 delete(gpgSigsExpected, filename)
564                 log.Println(r.RemoteAddr, "put", filename, "by", username)
565                 path := filepath.Join(dirPath, filename)
566                 if _, err = os.Stat(path); err == nil {
567                         log.Println(r.RemoteAddr, "already exists", filename)
568                         http.Error(w, "already exists", http.StatusBadRequest)
569                         return
570                 }
571                 src, err := file.Open()
572                 if err != nil {
573                         http.Error(w, err.Error(), http.StatusInternalServerError)
574                         return
575                 }
576                 sig, err := ioutil.ReadAll(src)
577                 src.Close()
578                 if err != nil {
579                         http.Error(w, err.Error(), http.StatusInternalServerError)
580                         return
581                 }
582                 if err = WriteFileSync(dirPath, path, sig); err != nil {
583                         http.Error(w, err.Error(), http.StatusInternalServerError)
584                         return
585                 }
586         }
587 }
588
589 func handler(w http.ResponseWriter, r *http.Request) {
590         switch r.Method {
591         case "GET":
592                 var path string
593                 var autorefresh bool
594                 var gpgUpdate bool
595                 if strings.HasPrefix(r.URL.Path, *norefreshURLPath) {
596                         path = strings.TrimPrefix(r.URL.Path, *norefreshURLPath)
597                 } else if strings.HasPrefix(r.URL.Path, *refreshURLPath) {
598                         path = strings.TrimPrefix(r.URL.Path, *refreshURLPath)
599                         autorefresh = true
600                 } else if strings.HasPrefix(r.URL.Path, *gpgUpdateURLPath) {
601                         path = strings.TrimPrefix(r.URL.Path, *gpgUpdateURLPath)
602                         autorefresh = true
603                         gpgUpdate = true
604                 } else {
605                         http.Error(w, "unknown action", http.StatusBadRequest)
606                         return
607                 }
608                 parts := strings.Split(strings.TrimSuffix(path, "/"), "/")
609                 if len(parts) > 2 {
610                         http.Error(w, "invalid path", http.StatusBadRequest)
611                         return
612                 }
613                 if len(parts) == 1 {
614                         if parts[0] == "" {
615                                 listRoot(w, r)
616                         } else {
617                                 listDir(w, r, parts[0], autorefresh, gpgUpdate)
618                         }
619                 } else {
620                         servePkg(w, r, parts[0], parts[1])
621                 }
622         case "POST":
623                 serveUpload(w, r)
624         default:
625                 http.Error(w, "unknown action", http.StatusBadRequest)
626         }
627 }
628
629 func main() {
630         flag.Parse()
631         if *warranty {
632                 fmt.Println(Warranty)
633                 return
634         }
635         if *version {
636                 fmt.Println("GoCheese version " + Version + " built with " + runtime.Version())
637                 return
638         }
639         if *fsck {
640                 if !goodIntegrity() {
641                         os.Exit(1)
642                 }
643                 return
644         }
645         if *passwdCheck {
646                 refreshPasswd()
647                 return
648         }
649         if (*tlsCert != "" && *tlsKey == "") || (*tlsCert == "" && *tlsKey != "") {
650                 log.Fatalln("Both -tls-cert and -tls-key are required")
651         }
652         var err error
653         pypiURLParsed, err = url.Parse(*pypiURL)
654         if err != nil {
655                 log.Fatalln(err)
656         }
657         refreshPasswd()
658         log.Println("root:", *root, "bind:", *bind)
659
660         ln, err := net.Listen("tcp", *bind)
661         if err != nil {
662                 log.Fatal(err)
663         }
664         ln = netutil.LimitListener(ln, *maxClients)
665         server := &http.Server{
666                 ReadTimeout:  time.Minute,
667                 WriteTimeout: time.Minute,
668         }
669         http.HandleFunc(*norefreshURLPath, handler)
670         http.HandleFunc(*refreshURLPath, handler)
671         if *gpgUpdateURLPath != "" {
672                 http.HandleFunc(*gpgUpdateURLPath, handler)
673         }
674
675         needsRefreshPasswd := make(chan os.Signal, 0)
676         needsShutdown := make(chan os.Signal, 0)
677         exitErr := make(chan error, 0)
678         signal.Notify(needsRefreshPasswd, syscall.SIGHUP)
679         signal.Notify(needsShutdown, syscall.SIGTERM, syscall.SIGINT)
680         go func() {
681                 for range needsRefreshPasswd {
682                         log.Println("Refreshing passwords")
683                         refreshPasswd()
684                 }
685         }()
686         go func(s *http.Server) {
687                 <-needsShutdown
688                 killed = true
689                 log.Println("Shutting down")
690                 ctx, cancel := context.WithTimeout(context.TODO(), time.Minute)
691                 exitErr <- s.Shutdown(ctx)
692                 cancel()
693         }(server)
694
695         if *tlsCert == "" {
696                 err = server.Serve(ln)
697         } else {
698                 err = server.ServeTLS(ln, *tlsCert, *tlsKey)
699         }
700         if err != http.ErrServerClosed {
701                 log.Fatal(err)
702         }
703         if err := <-exitErr; err != nil {
704                 log.Fatal(err)
705         }
706 }