16535: Fix error response codes for invalid names (4xx, not 5xx).
[arvados.git] / services / keep-web / s3.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 import (
8         "errors"
9         "fmt"
10         "io"
11         "net/http"
12         "os"
13         "strings"
14 )
15
16 // serveS3 handles r and returns true if r is a request from an S3
17 // client, otherwise it returns false.
18 func (h *handler) serveS3(w http.ResponseWriter, r *http.Request) bool {
19         var token string
20         if auth := r.Header.Get("Authorization"); strings.HasPrefix(auth, "AWS ") {
21                 split := strings.SplitN(auth[4:], ":", 2)
22                 if len(split) < 2 {
23                         w.WriteHeader(http.StatusUnauthorized)
24                         return true
25                 }
26                 token = split[0]
27         } else if strings.HasPrefix(auth, "AWS4-HMAC-SHA256 ") {
28                 w.WriteHeader(http.StatusBadRequest)
29                 fmt.Println(w, "V4 signature is not supported")
30                 return true
31         } else {
32                 return false
33         }
34
35         _, kc, client, release, err := h.getClients(r.Header.Get("X-Request-Id"), token)
36         if err != nil {
37                 http.Error(w, "Pool failed: "+h.clientPool.Err().Error(), http.StatusInternalServerError)
38                 return true
39         }
40         defer release()
41
42         r.URL.Path = "/by_id" + r.URL.Path
43
44         fs := client.SiteFileSystem(kc)
45         fs.ForwardSlashNameSubstitution(h.Config.cluster.Collections.ForwardSlashNameSubstitution)
46
47         fi, err := fs.Stat(r.URL.Path)
48         switch r.Method {
49         case "GET":
50                 if os.IsNotExist(err) ||
51                         (err != nil && err.Error() == "not a directory") ||
52                         (fi != nil && fi.IsDir()) {
53                         http.Error(w, "not found", http.StatusNotFound)
54                         return true
55                 }
56                 http.FileServer(fs).ServeHTTP(w, r)
57                 return true
58         case "PUT":
59                 if strings.HasSuffix(r.URL.Path, "/") {
60                         http.Error(w, "invalid object name (trailing '/' char)", http.StatusBadRequest)
61                         return true
62                 }
63                 if err != nil && err.Error() == "not a directory" {
64                         // requested foo/bar, but foo is a file
65                         http.Error(w, "object name conflicts with existing object", http.StatusBadRequest)
66                         return true
67                 }
68                 f, err := fs.OpenFile(r.URL.Path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)
69                 if os.IsNotExist(err) {
70                         // create missing intermediate directories, then try again
71                         for i, c := range r.URL.Path {
72                                 if i > 0 && c == '/' {
73                                         dir := r.URL.Path[:i]
74                                         if strings.HasSuffix(dir, "/") {
75                                                 err = errors.New("invalid object name (consecutive '/' chars)")
76                                                 http.Error(w, err.Error(), http.StatusBadRequest)
77                                                 return true
78                                         }
79                                         err := fs.Mkdir(dir, 0755)
80                                         if err != nil && err != os.ErrExist {
81                                                 err = fmt.Errorf("mkdir %q failed: %w", dir, err)
82                                                 http.Error(w, err.Error(), http.StatusInternalServerError)
83                                                 return true
84                                         }
85                                 }
86                         }
87                         f, err = fs.OpenFile(r.URL.Path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)
88                 }
89                 if err != nil {
90                         err = fmt.Errorf("open %q failed: %w", r.URL.Path, err)
91                         http.Error(w, err.Error(), http.StatusBadRequest)
92                         return true
93                 }
94                 defer f.Close()
95                 _, err = io.Copy(f, r.Body)
96                 if err != nil {
97                         err = fmt.Errorf("write to %q failed: %w", r.URL.Path, err)
98                         http.Error(w, err.Error(), http.StatusBadGateway)
99                         return true
100                 }
101                 err = f.Close()
102                 if err != nil {
103                         err = fmt.Errorf("write to %q failed: %w", r.URL.Path, err)
104                         http.Error(w, err.Error(), http.StatusBadGateway)
105                         return true
106                 }
107                 err = fs.Sync()
108                 if err != nil {
109                         err = fmt.Errorf("sync failed: %w", err)
110                         http.Error(w, err.Error(), http.StatusInternalServerError)
111                         return true
112                 }
113                 w.WriteHeader(http.StatusOK)
114                 return true
115         default:
116                 http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
117                 return true
118         }
119 }