Browse code

builder: protect early progress writes

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>

Tonis Tiigi authored on 2018/05/19 07:50:10
Showing 2 changed files
... ...
@@ -1,6 +1,7 @@
1 1
 package build // import "github.com/docker/docker/api/server/router/build"
2 2
 
3 3
 import (
4
+	"bufio"
4 5
 	"bytes"
5 6
 	"context"
6 7
 	"encoding/base64"
... ...
@@ -192,8 +193,19 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
192 192
 
193 193
 	w.Header().Set("Content-Type", "application/json")
194 194
 
195
-	output := ioutils.NewWriteFlusher(w)
195
+	var output writeCloseFlusher = ioutils.NewWriteFlusher(w)
196 196
 	defer output.Close()
197
+
198
+	body := r.Body
199
+	if body != nil {
200
+		// there is a possibility that output is written before request body
201
+		// has been fully read so we need to protect against it.
202
+		// this can be removed when
203
+		// https://github.com/golang/go/issues/15527
204
+		// https://github.com/golang/go/issues/22209
205
+		// has been fixed
206
+		body, output = wrapOutputBufferedUntilRequestRead(body, output)
207
+	}
197 208
 	errf := func(err error) error {
198 209
 		if httputils.BoolValue(r, "q") && notVerboseBuffer.Len() > 0 {
199 210
 			output.Write(notVerboseBuffer.Bytes())
... ...
@@ -235,7 +247,7 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
235 235
 	wantAux := versions.GreaterThanOrEqualTo(version, "1.30")
236 236
 
237 237
 	imgID, err := br.backend.Build(ctx, backend.BuildConfig{
238
-		Source:         r.Body,
238
+		Source:         body,
239 239
 		Options:        buildOptions,
240 240
 		ProgressWriter: buildProgressWriter(out, wantAux, createProgressReader),
241 241
 	})
... ...
@@ -294,3 +306,92 @@ func buildProgressWriter(out io.Writer, wantAux bool, createProgressReader func(
294 294
 		ProgressReaderFunc: createProgressReader,
295 295
 	}
296 296
 }
297
+
298
+type writeCloseFlusher interface {
299
+	Flush()
300
+	Flushed() bool
301
+	io.WriteCloser
302
+}
303
+
304
+func wrapOutputBufferedUntilRequestRead(rc io.ReadCloser, out writeCloseFlusher) (io.ReadCloser, writeCloseFlusher) {
305
+	w := &wcf{
306
+		buf:               bytes.NewBuffer(nil),
307
+		writeCloseFlusher: out,
308
+	}
309
+	r := bufio.NewReader(rc)
310
+	_, err := r.Peek(1)
311
+	if err != nil {
312
+		return rc, out
313
+	}
314
+	rc = &rcNotifier{
315
+		Reader: r,
316
+		Closer: rc,
317
+		notify: w.notify,
318
+	}
319
+	return rc, w
320
+}
321
+
322
+type rcNotifier struct {
323
+	io.Reader
324
+	io.Closer
325
+	notify func()
326
+}
327
+
328
+func (r *rcNotifier) Read(b []byte) (int, error) {
329
+	n, err := r.Reader.Read(b)
330
+	if err != nil {
331
+		r.notify()
332
+	}
333
+	return n, err
334
+}
335
+
336
+type wcf struct {
337
+	writeCloseFlusher
338
+	mu      sync.Mutex
339
+	ready   bool
340
+	buf     *bytes.Buffer
341
+	flushed bool
342
+}
343
+
344
+func (w *wcf) Flush() {
345
+	w.mu.Lock()
346
+	w.flushed = true
347
+	if !w.ready {
348
+		w.mu.Unlock()
349
+		return
350
+	}
351
+	w.mu.Unlock()
352
+	w.writeCloseFlusher.Flush()
353
+}
354
+
355
+func (w *wcf) Flushed() bool {
356
+	w.mu.Lock()
357
+	b := w.flushed
358
+	w.mu.Unlock()
359
+	return b
360
+}
361
+
362
+func (w *wcf) Write(b []byte) (int, error) {
363
+	w.mu.Lock()
364
+	if !w.ready {
365
+		n, err := w.buf.Write(b)
366
+		w.mu.Unlock()
367
+		return n, err
368
+	}
369
+	w.mu.Unlock()
370
+	return w.writeCloseFlusher.Write(b)
371
+}
372
+
373
+func (w *wcf) notify() {
374
+	w.mu.Lock()
375
+	if !w.ready {
376
+		if w.buf.Len() > 0 {
377
+			io.Copy(w.writeCloseFlusher, w.buf)
378
+		}
379
+		if w.flushed {
380
+			w.writeCloseFlusher.Flush()
381
+		}
382
+		w.ready = true
383
+	}
384
+	w.mu.Unlock()
385
+}
... ...
@@ -1,7 +1,6 @@
1 1
 package buildkit
2 2
 
3 3
 import (
4
-	"bufio"
5 4
 	"io"
6 5
 	"net/http"
7 6
 	"strings"
... ...
@@ -28,12 +27,9 @@ func newReqBodyHandler(rt http.RoundTripper) *reqBodyHandler {
28 28
 }
29 29
 
30 30
 func (h *reqBodyHandler) newRequest(rc io.ReadCloser) (string, func()) {
31
-	// handle expect-continue vs chunked output
32
-	r := bufio.NewReader(rc)
33
-	r.Peek(1)
34 31
 	id := identity.NewID()
35 32
 	h.mu.Lock()
36
-	h.requests[id] = &readCloser{Reader: r, Closer: rc}
33
+	h.requests[id] = rc
37 34
 	h.mu.Unlock()
38 35
 	return "http://" + urlPrefix + id, func() {
39 36
 		h.mu.Lock()
... ...
@@ -58,12 +54,14 @@ func (h *reqBodyHandler) RoundTrip(req *http.Request) (*http.Response, error) {
58 58
 			return nil, errors.Errorf("context not found")
59 59
 		}
60 60
 
61
-		return &http.Response{
61
+		resp := &http.Response{
62 62
 			Status:        "200 OK",
63 63
 			StatusCode:    200,
64 64
 			Body:          rc,
65 65
 			ContentLength: -1,
66
-		}, nil
66
+		}
67
+
68
+		return resp, nil
67 69
 	}
68 70
 	return h.rt.RoundTrip(req)
69 71
 }