]> ruderich.org/simon Gitweb - nsscash/nsscash.git/blob - file.go
02bc76715868967ed68596f8aebc5af1ccdee3bf
[nsscash/nsscash.git] / file.go
1 // Download and write files atomically to the file system
2
3 // Copyright (C) 2019  Simon Ruderich
4 //
5 // This program is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU Affero General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
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 Affero General Public License for more details.
14 //
15 // You should have received a copy of the GNU Affero General Public License
16 // along with this program.  If not, see <https://www.gnu.org/licenses/>.
17
18 package main
19
20 import (
21         "bytes"
22         "fmt"
23         "io/ioutil"
24         "log"
25         "net/http"
26         "os"
27         "path/filepath"
28         "syscall"
29
30         "github.com/pkg/errors"
31 )
32
33 func handleFiles(cfg *Config, state *State) error {
34         // Split into fetch and deploy phase to prevent updates of only some
35         // files which might lead to inconsistent state; obviously this won't
36         // work during the deploy phase, but it helps if the web server fails
37         // to deliver some files
38
39         for i, f := range cfg.Files {
40                 err := fetchFile(&cfg.Files[i], state)
41                 if err != nil {
42                         return errors.Wrapf(err, "%q (%s)", f.Url, f.Type)
43                 }
44         }
45
46         for i, f := range cfg.Files {
47                 // No update required
48                 if f.body == nil {
49                         continue
50                 }
51
52                 err := deployFile(&cfg.Files[i])
53                 if err != nil {
54                         return errors.Wrapf(err, "%q (%s)", f.Url, f.Type)
55                 }
56         }
57
58         return nil
59 }
60
61 func fetchFile(file *File, state *State) error {
62         t := state.LastModified[file.Url]
63         status, body, err := fetchIfModified(file.Url, &t)
64         if err != nil {
65                 return err
66         }
67         if status == http.StatusNotModified {
68                 log.Printf("%q -> %q: not modified", file.Url, file.Path)
69                 return nil
70         }
71         if status != http.StatusOK {
72                 return fmt.Errorf("status code %v", status)
73         }
74         state.LastModified[file.Url] = t
75
76         if file.Type == FileTypePlain {
77                 if len(body) == 0 {
78                         return fmt.Errorf("refusing to use empty response")
79                 }
80                 file.body = body
81
82         } else if file.Type == FileTypePasswd {
83                 pws, err := ParsePasswds(bytes.NewReader(body))
84                 if err != nil {
85                         return err
86                 }
87                 // Safety check: having no users can be very dangerous, don't
88                 // permit it
89                 if len(pws) == 0 {
90                         return fmt.Errorf("refusing to use empty passwd file")
91                 }
92
93                 var x bytes.Buffer
94                 err = SerializePasswds(&x, pws)
95                 if err != nil {
96                         return err
97                 }
98                 file.body = x.Bytes()
99
100         } else if file.Type == FileTypeGroup {
101                 grs, err := ParseGroups(bytes.NewReader(body))
102                 if err != nil {
103                         return err
104                 }
105                 if len(grs) == 0 {
106                         return fmt.Errorf("refusing to use empty group file")
107                 }
108
109                 var x bytes.Buffer
110                 err = SerializeGroups(&x, grs)
111                 if err != nil {
112                         return err
113                 }
114                 file.body = x.Bytes()
115
116         } else {
117                 return fmt.Errorf("unsupported file type %v", file.Type)
118         }
119         return nil
120 }
121
122 func deployFile(file *File) error {
123         log.Printf("%q -> %q: updating file", file.Url, file.Path)
124
125         // Safety check
126         if len(file.body) == 0 {
127                 return fmt.Errorf("refusing to write empty file")
128         }
129
130         // Write the file in an atomic fashion by creating a temporary file
131         // and renaming it over the target file
132
133         dir := filepath.Dir(file.Path)
134         name := filepath.Base(file.Path)
135
136         f, err := ioutil.TempFile(dir, "tmp-"+name+"-")
137         if err != nil {
138                 return err
139         }
140         defer os.Remove(f.Name())
141         defer f.Close()
142
143         // Apply permissions/user/group from the target file, use Stat instead
144         // of Lstat as only the target's permissions are relevant
145         stat, err := os.Stat(file.Path)
146         if err != nil {
147                 // We do not create the path if it doesn't exist, because we
148                 // do not know the proper permissions
149                 return errors.Wrapf(err, "file.path %q must exist", file.Path)
150         }
151         err = f.Chmod(stat.Mode())
152         if err != nil {
153                 return err
154         }
155         // TODO: support more systems
156         sys, ok := stat.Sys().(*syscall.Stat_t)
157         if !ok {
158                 return fmt.Errorf("unsupported FileInfo.Sys()")
159         }
160         err = f.Chown(int(sys.Uid), int(sys.Gid))
161         if err != nil {
162                 return err
163         }
164
165         _, err = f.Write(file.body)
166         if err != nil {
167                 return err
168         }
169         err = f.Sync()
170         if err != nil {
171                 return err
172         }
173         return os.Rename(f.Name(), file.Path)
174 }