1package store
2
3import (
4	"archive/tar"
5	"archive/zip"
6	"bufio"
7	"bytes"
8	_ "crypto/sha256" // ensure ids can be computed
9	"encoding/json"
10	"fmt"
11	"io"
12	"io/ioutil"
13	"net/http"
14	"path"
15	"path/filepath"
16	"regexp"
17	"strings"
18
19	"github.com/docker/docker/errdefs"
20	digest "github.com/opencontainers/go-digest"
21	"github.com/pkg/errors"
22)
23
24const restrictedNamePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$"
25
26var restrictedNameRegEx = regexp.MustCompile(restrictedNamePattern)
27
28// Store provides a context store for easily remembering endpoints configuration
29type Store interface {
30	Reader
31	Lister
32	Writer
33	StorageInfoProvider
34}
35
36// Reader provides read-only (without list) access to context data
37type Reader interface {
38	GetMetadata(name string) (Metadata, error)
39	ListTLSFiles(name string) (map[string]EndpointFiles, error)
40	GetTLSData(contextName, endpointName, fileName string) ([]byte, error)
41}
42
43// Lister provides listing of contexts
44type Lister interface {
45	List() ([]Metadata, error)
46}
47
48// ReaderLister combines Reader and Lister interfaces
49type ReaderLister interface {
50	Reader
51	Lister
52}
53
54// StorageInfoProvider provides more information about storage details of contexts
55type StorageInfoProvider interface {
56	GetStorageInfo(contextName string) StorageInfo
57}
58
59// Writer provides write access to context data
60type Writer interface {
61	CreateOrUpdate(meta Metadata) error
62	Remove(name string) error
63	ResetTLSMaterial(name string, data *ContextTLSData) error
64	ResetEndpointTLSMaterial(contextName string, endpointName string, data *EndpointTLSData) error
65}
66
67// ReaderWriter combines Reader and Writer interfaces
68type ReaderWriter interface {
69	Reader
70	Writer
71}
72
73// Metadata contains metadata about a context and its endpoints
74type Metadata struct {
75	Name      string                 `json:",omitempty"`
76	Metadata  interface{}            `json:",omitempty"`
77	Endpoints map[string]interface{} `json:",omitempty"`
78}
79
80// StorageInfo contains data about where a given context is stored
81type StorageInfo struct {
82	MetadataPath string
83	TLSPath      string
84}
85
86// EndpointTLSData represents tls data for a given endpoint
87type EndpointTLSData struct {
88	Files map[string][]byte
89}
90
91// ContextTLSData represents tls data for a whole context
92type ContextTLSData struct {
93	Endpoints map[string]EndpointTLSData
94}
95
96// New creates a store from a given directory.
97// If the directory does not exist or is empty, initialize it
98func New(dir string, cfg Config) Store {
99	metaRoot := filepath.Join(dir, metadataDir)
100	tlsRoot := filepath.Join(dir, tlsDir)
101
102	return &store{
103		meta: &metadataStore{
104			root:   metaRoot,
105			config: cfg,
106		},
107		tls: &tlsStore{
108			root: tlsRoot,
109		},
110	}
111}
112
113type store struct {
114	meta *metadataStore
115	tls  *tlsStore
116}
117
118func (s *store) List() ([]Metadata, error) {
119	return s.meta.list()
120}
121
122func (s *store) CreateOrUpdate(meta Metadata) error {
123	return s.meta.createOrUpdate(meta)
124}
125
126func (s *store) Remove(name string) error {
127	id := contextdirOf(name)
128	if err := s.meta.remove(id); err != nil {
129		return patchErrContextName(err, name)
130	}
131	return patchErrContextName(s.tls.removeAllContextData(id), name)
132}
133
134func (s *store) GetMetadata(name string) (Metadata, error) {
135	res, err := s.meta.get(contextdirOf(name))
136	patchErrContextName(err, name)
137	return res, err
138}
139
140func (s *store) ResetTLSMaterial(name string, data *ContextTLSData) error {
141	id := contextdirOf(name)
142	if err := s.tls.removeAllContextData(id); err != nil {
143		return patchErrContextName(err, name)
144	}
145	if data == nil {
146		return nil
147	}
148	for ep, files := range data.Endpoints {
149		for fileName, data := range files.Files {
150			if err := s.tls.createOrUpdate(id, ep, fileName, data); err != nil {
151				return patchErrContextName(err, name)
152			}
153		}
154	}
155	return nil
156}
157
158func (s *store) ResetEndpointTLSMaterial(contextName string, endpointName string, data *EndpointTLSData) error {
159	id := contextdirOf(contextName)
160	if err := s.tls.removeAllEndpointData(id, endpointName); err != nil {
161		return patchErrContextName(err, contextName)
162	}
163	if data == nil {
164		return nil
165	}
166	for fileName, data := range data.Files {
167		if err := s.tls.createOrUpdate(id, endpointName, fileName, data); err != nil {
168			return patchErrContextName(err, contextName)
169		}
170	}
171	return nil
172}
173
174func (s *store) ListTLSFiles(name string) (map[string]EndpointFiles, error) {
175	res, err := s.tls.listContextData(contextdirOf(name))
176	return res, patchErrContextName(err, name)
177}
178
179func (s *store) GetTLSData(contextName, endpointName, fileName string) ([]byte, error) {
180	res, err := s.tls.getData(contextdirOf(contextName), endpointName, fileName)
181	return res, patchErrContextName(err, contextName)
182}
183
184func (s *store) GetStorageInfo(contextName string) StorageInfo {
185	dir := contextdirOf(contextName)
186	return StorageInfo{
187		MetadataPath: s.meta.contextDir(dir),
188		TLSPath:      s.tls.contextDir(dir),
189	}
190}
191
192// ValidateContextName checks a context name is valid.
193func ValidateContextName(name string) error {
194	if name == "" {
195		return errors.New("context name cannot be empty")
196	}
197	if name == "default" {
198		return errors.New(`"default" is a reserved context name`)
199	}
200	if !restrictedNameRegEx.MatchString(name) {
201		return fmt.Errorf("context name %q is invalid, names are validated against regexp %q", name, restrictedNamePattern)
202	}
203	return nil
204}
205
206// Export exports an existing namespace into an opaque data stream
207// This stream is actually a tarball containing context metadata and TLS materials, but it does
208// not map 1:1 the layout of the context store (don't try to restore it manually without calling store.Import)
209func Export(name string, s Reader) io.ReadCloser {
210	reader, writer := io.Pipe()
211	go func() {
212		tw := tar.NewWriter(writer)
213		defer tw.Close()
214		defer writer.Close()
215		meta, err := s.GetMetadata(name)
216		if err != nil {
217			writer.CloseWithError(err)
218			return
219		}
220		metaBytes, err := json.Marshal(&meta)
221		if err != nil {
222			writer.CloseWithError(err)
223			return
224		}
225		if err = tw.WriteHeader(&tar.Header{
226			Name: metaFile,
227			Mode: 0644,
228			Size: int64(len(metaBytes)),
229		}); err != nil {
230			writer.CloseWithError(err)
231			return
232		}
233		if _, err = tw.Write(metaBytes); err != nil {
234			writer.CloseWithError(err)
235			return
236		}
237		tlsFiles, err := s.ListTLSFiles(name)
238		if err != nil {
239			writer.CloseWithError(err)
240			return
241		}
242		if err = tw.WriteHeader(&tar.Header{
243			Name:     "tls",
244			Mode:     0700,
245			Size:     0,
246			Typeflag: tar.TypeDir,
247		}); err != nil {
248			writer.CloseWithError(err)
249			return
250		}
251		for endpointName, endpointFiles := range tlsFiles {
252			if err = tw.WriteHeader(&tar.Header{
253				Name:     path.Join("tls", endpointName),
254				Mode:     0700,
255				Size:     0,
256				Typeflag: tar.TypeDir,
257			}); err != nil {
258				writer.CloseWithError(err)
259				return
260			}
261			for _, fileName := range endpointFiles {
262				data, err := s.GetTLSData(name, endpointName, fileName)
263				if err != nil {
264					writer.CloseWithError(err)
265					return
266				}
267				if err = tw.WriteHeader(&tar.Header{
268					Name: path.Join("tls", endpointName, fileName),
269					Mode: 0600,
270					Size: int64(len(data)),
271				}); err != nil {
272					writer.CloseWithError(err)
273					return
274				}
275				if _, err = tw.Write(data); err != nil {
276					writer.CloseWithError(err)
277					return
278				}
279			}
280		}
281	}()
282	return reader
283}
284
285const (
286	maxAllowedFileSizeToImport int64  = 10 << 20
287	zipType                    string = "application/zip"
288)
289
290func getImportContentType(r *bufio.Reader) (string, error) {
291	head, err := r.Peek(512)
292	if err != nil && err != io.EOF {
293		return "", err
294	}
295
296	return http.DetectContentType(head), nil
297}
298
299// Import imports an exported context into a store
300func Import(name string, s Writer, reader io.Reader) error {
301	// Buffered reader will not advance the buffer, needed to determine content type
302	r := bufio.NewReader(reader)
303
304	importContentType, err := getImportContentType(r)
305	if err != nil {
306		return err
307	}
308	switch importContentType {
309	case zipType:
310		return importZip(name, s, r)
311	default:
312		// Assume it's a TAR (TAR does not have a "magic number")
313		return importTar(name, s, r)
314	}
315}
316
317func isValidFilePath(p string) error {
318	if p != metaFile && !strings.HasPrefix(p, "tls/") {
319		return errors.New("unexpected context file")
320	}
321	if path.Clean(p) != p {
322		return errors.New("unexpected path format")
323	}
324	if strings.Contains(p, `\`) {
325		return errors.New(`unexpected '\' in path`)
326	}
327	return nil
328}
329
330func importTar(name string, s Writer, reader io.Reader) error {
331	tr := tar.NewReader(&LimitedReader{R: reader, N: maxAllowedFileSizeToImport})
332	tlsData := ContextTLSData{
333		Endpoints: map[string]EndpointTLSData{},
334	}
335	var importedMetaFile bool
336	for {
337		hdr, err := tr.Next()
338		if err == io.EOF {
339			break
340		}
341		if err != nil {
342			return err
343		}
344		if hdr.Typeflag != tar.TypeReg {
345			// skip this entry, only taking files into account
346			continue
347		}
348		if err := isValidFilePath(hdr.Name); err != nil {
349			return errors.Wrap(err, hdr.Name)
350		}
351		if hdr.Name == metaFile {
352			data, err := ioutil.ReadAll(tr)
353			if err != nil {
354				return err
355			}
356			meta, err := parseMetadata(data, name)
357			if err != nil {
358				return err
359			}
360			if err := s.CreateOrUpdate(meta); err != nil {
361				return err
362			}
363			importedMetaFile = true
364		} else if strings.HasPrefix(hdr.Name, "tls/") {
365			data, err := ioutil.ReadAll(tr)
366			if err != nil {
367				return err
368			}
369			if err := importEndpointTLS(&tlsData, hdr.Name, data); err != nil {
370				return err
371			}
372		}
373	}
374	if !importedMetaFile {
375		return errdefs.InvalidParameter(errors.New("invalid context: no metadata found"))
376	}
377	return s.ResetTLSMaterial(name, &tlsData)
378}
379
380func importZip(name string, s Writer, reader io.Reader) error {
381	body, err := ioutil.ReadAll(&LimitedReader{R: reader, N: maxAllowedFileSizeToImport})
382	if err != nil {
383		return err
384	}
385	zr, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
386	if err != nil {
387		return err
388	}
389	tlsData := ContextTLSData{
390		Endpoints: map[string]EndpointTLSData{},
391	}
392
393	var importedMetaFile bool
394	for _, zf := range zr.File {
395		fi := zf.FileInfo()
396		if !fi.Mode().IsRegular() {
397			// skip this entry, only taking regular files into account
398			continue
399		}
400		if err := isValidFilePath(zf.Name); err != nil {
401			return errors.Wrap(err, zf.Name)
402		}
403		if zf.Name == metaFile {
404			f, err := zf.Open()
405			if err != nil {
406				return err
407			}
408
409			data, err := ioutil.ReadAll(&LimitedReader{R: f, N: maxAllowedFileSizeToImport})
410			defer f.Close()
411			if err != nil {
412				return err
413			}
414			meta, err := parseMetadata(data, name)
415			if err != nil {
416				return err
417			}
418			if err := s.CreateOrUpdate(meta); err != nil {
419				return err
420			}
421			importedMetaFile = true
422		} else if strings.HasPrefix(zf.Name, "tls/") {
423			f, err := zf.Open()
424			if err != nil {
425				return err
426			}
427			data, err := ioutil.ReadAll(f)
428			defer f.Close()
429			if err != nil {
430				return err
431			}
432			err = importEndpointTLS(&tlsData, zf.Name, data)
433			if err != nil {
434				return err
435			}
436		}
437	}
438	if !importedMetaFile {
439		return errdefs.InvalidParameter(errors.New("invalid context: no metadata found"))
440	}
441	return s.ResetTLSMaterial(name, &tlsData)
442}
443
444func parseMetadata(data []byte, name string) (Metadata, error) {
445	var meta Metadata
446	if err := json.Unmarshal(data, &meta); err != nil {
447		return meta, err
448	}
449	if err := ValidateContextName(name); err != nil {
450		return Metadata{}, err
451	}
452	meta.Name = name
453	return meta, nil
454}
455
456func importEndpointTLS(tlsData *ContextTLSData, path string, data []byte) error {
457	parts := strings.SplitN(strings.TrimPrefix(path, "tls/"), "/", 2)
458	if len(parts) != 2 {
459		// TLS endpoints require archived file directory with 2 layers
460		// i.e. tls/{endpointName}/{fileName}
461		return errors.New("archive format is invalid")
462	}
463
464	epName := parts[0]
465	fileName := parts[1]
466	if _, ok := tlsData.Endpoints[epName]; !ok {
467		tlsData.Endpoints[epName] = EndpointTLSData{
468			Files: map[string][]byte{},
469		}
470	}
471	tlsData.Endpoints[epName].Files[fileName] = data
472	return nil
473}
474
475type setContextName interface {
476	setContext(name string)
477}
478
479type contextDoesNotExistError struct {
480	name string
481}
482
483func (e *contextDoesNotExistError) Error() string {
484	return fmt.Sprintf("context %q does not exist", e.name)
485}
486
487func (e *contextDoesNotExistError) setContext(name string) {
488	e.name = name
489}
490
491// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
492func (e *contextDoesNotExistError) NotFound() {}
493
494type tlsDataDoesNotExist interface {
495	errdefs.ErrNotFound
496	IsTLSDataDoesNotExist()
497}
498
499type tlsDataDoesNotExistError struct {
500	context, endpoint, file string
501}
502
503func (e *tlsDataDoesNotExistError) Error() string {
504	return fmt.Sprintf("tls data for %s/%s/%s does not exist", e.context, e.endpoint, e.file)
505}
506
507func (e *tlsDataDoesNotExistError) setContext(name string) {
508	e.context = name
509}
510
511// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
512func (e *tlsDataDoesNotExistError) NotFound() {}
513
514// IsTLSDataDoesNotExist satisfies tlsDataDoesNotExist
515func (e *tlsDataDoesNotExistError) IsTLSDataDoesNotExist() {}
516
517// IsErrContextDoesNotExist checks if the given error is a "context does not exist" condition
518func IsErrContextDoesNotExist(err error) bool {
519	_, ok := err.(*contextDoesNotExistError)
520	return ok
521}
522
523// IsErrTLSDataDoesNotExist checks if the given error is a "context does not exist" condition
524func IsErrTLSDataDoesNotExist(err error) bool {
525	_, ok := err.(tlsDataDoesNotExist)
526	return ok
527}
528
529type contextdir string
530
531func contextdirOf(name string) contextdir {
532	return contextdir(digest.FromString(name).Encoded())
533}
534
535func patchErrContextName(err error, name string) error {
536	if typed, ok := err.(setContextName); ok {
537		typed.setContext(name)
538	}
539	return err
540}
541