// This implements a very basic Swift server // Everything is stored in memory // // This comes from the https://github.com/mitchellh/goamz // and was adapted for Swift // package swifttest import ( "bytes" "crypto/hmac" "crypto/md5" "crypto/rand" "crypto/sha1" "encoding/hex" "encoding/json" "fmt" "io" "io/ioutil" "log" "mime" "net" "net/http" "net/url" "path" "regexp" "sort" "strconv" "strings" "sync" "testing" "time" "github.com/ncw/swift" ) const ( DEBUG = false TEST_ACCOUNT = "swifttest" ) type SwiftServer struct { t *testing.T reqId int mu sync.Mutex Listener net.Listener AuthURL string URL string Accounts map[string]*account Sessions map[string]*session } // The Folder type represents a container stored in an account type Folder struct { Count int `json:"count"` Bytes int `json:"bytes"` Name string `json:"name"` } // The Key type represents an item stored in an container. type Key struct { Key string `json:"name"` LastModified string `json:"last_modified"` Size int64 `json:"bytes"` // ETag gives the hex-encoded MD5 sum of the contents, // surrounded with double-quotes. ETag string `json:"hash"` ContentType string `json:"content_type"` // Owner Owner } type Subdir struct { Subdir string `json:"subdir"` } type swiftError struct { statusCode int Code string Message string } type action struct { srv *SwiftServer w http.ResponseWriter req *http.Request reqId string user *account } type session struct { username string } type metadata struct { meta http.Header // metadata to return with requests. } type account struct { swift.Account metadata password string Containers map[string]*container } type object struct { metadata name string mtime time.Time checksum []byte // also held as ETag in meta. data []byte content_type string } type container struct { metadata name string ctime time.Time objects map[string]*object bytes int } // A resource encapsulates the subject of an HTTP request. // The resource referred to may or may not exist // when the request is made. type resource interface { put(a *action) interface{} get(a *action) interface{} post(a *action) interface{} delete(a *action) interface{} copy(a *action) interface{} } type objectResource struct { name string version string container *container // always non-nil. object *object // may be nil. } type containerResource struct { name string container *container // non-nil if the container already exists. } var responseParams = map[string]bool{ "content-type": true, "content-language": true, "expires": true, "cache-control": true, "content-disposition": true, "content-encoding": true, } func fatalf(code int, codeStr string, errf string, a ...interface{}) { panic(&swiftError{ statusCode: code, Code: codeStr, Message: fmt.Sprintf(errf, a...), }) } func (m metadata) setMetadata(a *action, resource string) { for key, values := range a.req.Header { key = http.CanonicalHeaderKey(key) if metaHeaders[key] || strings.HasPrefix(key, "X-"+strings.Title(resource)+"-Meta-") { if values[0] != "" || resource == "object" { m.meta[key] = values } else { m.meta.Del(key) } } } } func (m metadata) getMetadata(a *action) { h := a.w.Header() for name, d := range m.meta { h[name] = d } } func (c container) list(delimiter string, marker string, prefix string, parent string) (resp []interface{}) { var tmp orderedObjects // first get all matching objects and arrange them in alphabetical order. for _, obj := range c.objects { if strings.HasPrefix(obj.name, prefix) { tmp = append(tmp, obj) } } sort.Sort(tmp) var prefixes []string for _, obj := range tmp { if !strings.HasPrefix(obj.name, prefix) { continue } isPrefix := false name := obj.name if parent != "" { if path.Dir(obj.name) != path.Clean(parent) { continue } } else if delimiter != "" { if i := strings.Index(obj.name[len(prefix):], delimiter); i >= 0 { name = obj.name[:len(prefix)+i+len(delimiter)] if prefixes != nil && prefixes[len(prefixes)-1] == name { continue } isPrefix = true } } if name <= marker { continue } if isPrefix { prefixes = append(prefixes, name) resp = append(resp, Subdir{ Subdir: name, }) } else { resp = append(resp, obj) } } return } // GET on a container lists the objects in the container. func (r containerResource) get(a *action) interface{} { if r.container == nil { fatalf(404, "NoSuchContainer", "The specified container does not exist") } delimiter := a.req.Form.Get("delimiter") marker := a.req.Form.Get("marker") prefix := a.req.Form.Get("prefix") format := a.req.URL.Query().Get("format") parent := a.req.Form.Get("path") a.w.Header().Set("X-Container-Bytes-Used", strconv.Itoa(r.container.bytes)) a.w.Header().Set("X-Container-Object-Count", strconv.Itoa(len(r.container.objects))) r.container.getMetadata(a) if a.req.Method == "HEAD" { return nil } objects := r.container.list(delimiter, marker, prefix, parent) if format == "json" { a.w.Header().Set("Content-Type", "application/json") var resp []interface{} for _, item := range objects { if obj, ok := item.(*object); ok { resp = append(resp, obj.Key()) } else { resp = append(resp, item) } } return resp } else { for _, item := range objects { if obj, ok := item.(*object); ok { a.w.Write([]byte(obj.name + "\n")) } else if subdir, ok := item.(Subdir); ok { a.w.Write([]byte(subdir.Subdir + "\n")) } } return nil } } // orderedContainers holds a slice of containers that can be sorted // by name. type orderedContainers []*container func (s orderedContainers) Len() int { return len(s) } func (s orderedContainers) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s orderedContainers) Less(i, j int) bool { return s[i].name < s[j].name } func (r containerResource) delete(a *action) interface{} { b := r.container if b == nil { fatalf(404, "NoSuchContainer", "The specified container does not exist") } if len(b.objects) > 0 { fatalf(409, "Conflict", "The container you tried to delete is not empty") } delete(a.user.Containers, b.name) a.user.Account.Containers-- return nil } func (r containerResource) put(a *action) interface{} { if a.req.URL.Query().Get("extract-archive") != "" { fatalf(403, "Operation forbidden", "Bulk upload is not supported") } if r.container == nil { if !validContainerName(r.name) { fatalf(400, "InvalidContainerName", "The specified container is not valid") } r.container = &container{ name: r.name, objects: make(map[string]*object), metadata: metadata{ meta: make(http.Header), }, } r.container.setMetadata(a, "container") a.user.Containers[r.name] = r.container a.user.Account.Containers++ } return nil } func (r containerResource) post(a *action) interface{} { if r.container == nil { fatalf(400, "Method", "The resource could not be found.") } else { r.container.setMetadata(a, "container") a.w.WriteHeader(201) jsonMarshal(a.w, Folder{ Count: len(r.container.objects), Bytes: r.container.bytes, Name: r.container.name, }) } return nil } func (containerResource) copy(a *action) interface{} { return notAllowed() } // validContainerName returns whether name is a valid bucket name. // Here are the rules, from: // http://docs.openstack.org/api/openstack-object-storage/1.0/content/ch_object-storage-dev-api-storage.html // // Container names cannot exceed 256 bytes and cannot contain the / character. // func validContainerName(name string) bool { if len(name) == 0 || len(name) > 256 { return false } for _, r := range name { switch { case r == '/': return false default: } } return true } // orderedObjects holds a slice of objects that can be sorted // by name. type orderedObjects []*object func (s orderedObjects) Len() int { return len(s) } func (s orderedObjects) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s orderedObjects) Less(i, j int) bool { return s[i].name < s[j].name } func (obj *object) Key() Key { return Key{ Key: obj.name, LastModified: obj.mtime.Format("2006-01-02T15:04:05"), Size: int64(len(obj.data)), ETag: fmt.Sprintf("%x", obj.checksum), ContentType: obj.content_type, } } var metaHeaders = map[string]bool{ "Content-Type": true, "Content-Encoding": true, "Content-Disposition": true, "X-Object-Manifest": true, } var rangeRegexp = regexp.MustCompile("(bytes=)?([0-9]*)-([0-9]*)") // GET on an object gets the contents of the object. func (objr objectResource) get(a *action) interface{} { var ( etag []byte reader io.Reader start int end int = -1 ) obj := objr.object if obj == nil { fatalf(404, "Not Found", "The resource could not be found.") } h := a.w.Header() // add metadata obj.getMetadata(a) if r := a.req.Header.Get("Range"); r != "" { m := rangeRegexp.FindStringSubmatch(r) if m[2] != "" { start, _ = strconv.Atoi(m[2]) } if m[3] != "" { end, _ = strconv.Atoi(m[3]) } } max := func(a int, b int) int { if a > b { return a } return b } if manifest, ok := obj.meta["X-Object-Manifest"]; ok { var segments []io.Reader components := strings.SplitN(manifest[0], "/", 2) segContainer := a.user.Containers[components[0]] prefix := components[1] resp := segContainer.list("", "", prefix, "") sum := md5.New() cursor := 0 size := 0 for _, item := range resp { if obj, ok := item.(*object); ok { length := len(obj.data) size += length sum.Write([]byte(hex.EncodeToString(obj.checksum))) if start >= cursor+length { continue } segments = append(segments, bytes.NewReader(obj.data[max(0, start-cursor):])) cursor += length } } etag = sum.Sum(nil) if end == -1 { end = size } reader = io.LimitReader(io.MultiReader(segments...), int64(end-start)) } else { if end == -1 { end = len(obj.data) } etag = obj.checksum reader = bytes.NewReader(obj.data[start:end]) } h.Set("Content-Length", fmt.Sprint(end-start)) h.Set("ETag", hex.EncodeToString(etag)) h.Set("Last-Modified", obj.mtime.Format(http.TimeFormat)) if a.req.Method == "HEAD" { return nil } // TODO avoid holding the lock when writing data. _, err := io.Copy(a.w, reader) if err != nil { // we can't do much except just log the fact. log.Printf("error writing data: %v", err) } return nil } // PUT on an object creates the object. func (objr objectResource) put(a *action) interface{} { var expectHash []byte if c := a.req.Header.Get("ETag"); c != "" { var err error expectHash, err = hex.DecodeString(c) if err != nil || len(expectHash) != md5.Size { fatalf(400, "InvalidDigest", "The ETag you specified was invalid") } } sum := md5.New() // TODO avoid holding lock while reading data. data, err := ioutil.ReadAll(io.TeeReader(a.req.Body, sum)) if err != nil { fatalf(400, "TODO", "read error") } gotHash := sum.Sum(nil) if expectHash != nil && bytes.Compare(gotHash, expectHash) != 0 { fatalf(422, "Bad ETag", "The ETag you specified did not match what we received") } if a.req.ContentLength >= 0 && int64(len(data)) != a.req.ContentLength { fatalf(400, "IncompleteBody", "You did not provide the number of bytes specified by the Content-Length HTTP header") } // TODO is this correct, or should we erase all previous metadata? obj := objr.object if obj == nil { obj = &object{ name: objr.name, metadata: metadata{ meta: make(http.Header), }, } a.user.Objects++ } else { objr.container.bytes -= len(obj.data) a.user.BytesUsed -= int64(len(obj.data)) } var content_type string if content_type = a.req.Header.Get("Content-Type"); content_type == "" { content_type = mime.TypeByExtension(obj.name) if content_type == "" { content_type = "application/octet-stream" } } // PUT request has been successful - save data and metadata obj.setMetadata(a, "object") obj.content_type = content_type obj.data = data obj.checksum = gotHash obj.mtime = time.Now().UTC() objr.container.objects[objr.name] = obj objr.container.bytes += len(data) a.user.BytesUsed += int64(len(data)) h := a.w.Header() h.Set("ETag", hex.EncodeToString(obj.checksum)) return nil } func (objr objectResource) delete(a *action) interface{} { if objr.object == nil { fatalf(404, "NoSuchKey", "The specified key does not exist.") } objr.container.bytes -= len(objr.object.data) a.user.BytesUsed -= int64(len(objr.object.data)) delete(objr.container.objects, objr.name) a.user.Objects-- return nil } func (objr objectResource) post(a *action) interface{} { obj := objr.object obj.setMetadata(a, "object") return nil } func (objr objectResource) copy(a *action) interface{} { if objr.object == nil { fatalf(404, "NoSuchKey", "The specified key does not exist.") } obj := objr.object destination := a.req.Header.Get("Destination") if destination == "" { fatalf(400, "Bad Request", "You must provide a Destination header") } var ( obj2 *object objr2 objectResource ) destURL, _ := url.Parse("/v1/AUTH_" + TEST_ACCOUNT + "/" + destination) r := a.srv.resourceForURL(destURL) switch t := r.(type) { case objectResource: objr2 = t if objr2.object == nil { obj2 = &object{ name: objr2.name, metadata: metadata{ meta: make(http.Header), }, } a.user.Objects++ } else { obj2 = objr2.object objr2.container.bytes -= len(obj2.data) a.user.BytesUsed -= int64(len(obj2.data)) } default: fatalf(400, "Bad Request", "Destination must point to a valid object path") } obj2.content_type = obj.content_type obj2.data = obj.data obj2.checksum = obj.checksum obj2.mtime = time.Now() objr2.container.objects[objr2.name] = obj2 objr2.container.bytes += len(obj.data) a.user.BytesUsed += int64(len(obj.data)) for key, values := range obj.metadata.meta { obj2.metadata.meta[key] = values } obj2.setMetadata(a, "object") return nil } func (s *SwiftServer) serveHTTP(w http.ResponseWriter, req *http.Request) { // ignore error from ParseForm as it's usually spurious. req.ParseForm() s.mu.Lock() defer s.mu.Unlock() if DEBUG { log.Printf("swifttest %q %q", req.Method, req.URL) } a := &action{ srv: s, w: w, req: req, reqId: fmt.Sprintf("%09X", s.reqId), } s.reqId++ var r resource defer func() { switch err := recover().(type) { case *swiftError: w.Header().Set("Content-Type", `text/plain; charset=utf-8`) http.Error(w, err.Message, err.statusCode) case nil: default: panic(err) } }() var resp interface{} if req.URL.String() == "/v1.0" { username := req.Header.Get("x-auth-user") key := req.Header.Get("x-auth-key") if acct, ok := s.Accounts[username]; ok { if acct.password == key { r := make([]byte, 16) _, _ = rand.Read(r) id := fmt.Sprintf("%X", r) w.Header().Set("X-Storage-Url", s.URL+"/AUTH_"+username) w.Header().Set("X-Auth-Token", "AUTH_tk"+string(id)) w.Header().Set("X-Storage-Token", "AUTH_tk"+string(id)) s.Sessions[id] = &session{ username: username, } return } } panic(notAuthorized()) } if req.URL.String() == "/info" { jsonMarshal(w, &swift.SwiftInfo{ "swift": map[string]interface{}{ "version": "1.2", }, "tempurl": map[string]interface{}{ "methods": []string{"GET", "HEAD", "PUT"}, }, }) return } r = s.resourceForURL(req.URL) key := req.Header.Get("x-auth-token") signature := req.URL.Query().Get("temp_url_sig") expires := req.URL.Query().Get("temp_url_expires") if key == "" && signature != "" && expires != "" { accountName, _, _, _ := s.parseURL(req.URL) secretKey := "" if account, ok := s.Accounts[accountName]; ok { secretKey = account.meta.Get("X-Account-Meta-Temp-Url-Key") } get_hmac := func(method string) string { mac := hmac.New(sha1.New, []byte(secretKey)) body := fmt.Sprintf("%s\n%s\n%s", method, expires, req.URL.Path) mac.Write([]byte(body)) return hex.EncodeToString(mac.Sum(nil)) } if req.Method == "HEAD" { if signature != get_hmac("GET") && signature != get_hmac("POST") && signature != get_hmac("PUT") { panic(notAuthorized()) } } else if signature != get_hmac(req.Method) { panic(notAuthorized()) } } else { session, ok := s.Sessions[key[7:]] if !ok { panic(notAuthorized()) } a.user = s.Accounts[session.username] } switch req.Method { case "PUT": resp = r.put(a) case "GET", "HEAD": resp = r.get(a) case "DELETE": resp = r.delete(a) case "POST": resp = r.post(a) case "COPY": resp = r.copy(a) default: fatalf(400, "MethodNotAllowed", "unknown http request method %q", req.Method) } content_type := req.Header.Get("Content-Type") if resp != nil && req.Method != "HEAD" { if strings.HasPrefix(content_type, "application/json") || req.URL.Query().Get("format") == "json" { jsonMarshal(w, resp) } else { switch r := resp.(type) { case string: w.Write([]byte(r)) default: w.Write(resp.([]byte)) } } } } func jsonMarshal(w io.Writer, x interface{}) { if err := json.NewEncoder(w).Encode(x); err != nil { panic(fmt.Errorf("error marshalling %#v: %v", x, err)) } } var pathRegexp = regexp.MustCompile("/v1/AUTH_([a-zA-Z0-9]+)(/([^/]+)(/(.*))?)?") func (srv *SwiftServer) parseURL(u *url.URL) (account string, container string, object string, err error) { m := pathRegexp.FindStringSubmatch(u.Path) if m == nil { return "", "", "", fmt.Errorf("Couldn't parse the specified URI") } account = m[1] container = m[3] object = m[5] return } // resourceForURL returns a resource object for the given URL. func (srv *SwiftServer) resourceForURL(u *url.URL) (r resource) { accountName, containerName, objectName, err := srv.parseURL(u) if err != nil { fatalf(404, "InvalidURI", err.Error()) } account, ok := srv.Accounts[accountName] if !ok { fatalf(404, "NoSuchAccount", "The specified account does not exist") } if containerName == "" { return rootResource{} } b := containerResource{ name: containerName, container: account.Containers[containerName], } if objectName == "" { return b } if b.container == nil { fatalf(404, "NoSuchContainer", "The specified container does not exist") } objr := objectResource{ name: objectName, version: u.Query().Get("versionId"), container: b.container, } if obj := objr.container.objects[objr.name]; obj != nil { objr.object = obj } return objr } // nullResource has error stubs for all resource methods. type nullResource struct{} func notAllowed() interface{} { fatalf(400, "MethodNotAllowed", "The specified method is not allowed against this resource") return nil } func notAuthorized() interface{} { fatalf(401, "Unauthorized", "This server could not verify that you are authorized to access the document you requested.") return nil } func (nullResource) put(a *action) interface{} { return notAllowed() } func (nullResource) get(a *action) interface{} { return notAllowed() } func (nullResource) post(a *action) interface{} { return notAllowed() } func (nullResource) delete(a *action) interface{} { return notAllowed() } func (nullResource) copy(a *action) interface{} { return notAllowed() } type rootResource struct{} func (rootResource) put(a *action) interface{} { return notAllowed() } func (rootResource) get(a *action) interface{} { marker := a.req.Form.Get("marker") prefix := a.req.Form.Get("prefix") format := a.req.URL.Query().Get("format") h := a.w.Header() h.Set("X-Account-Bytes-Used", strconv.Itoa(int(a.user.BytesUsed))) h.Set("X-Account-Container-Count", strconv.Itoa(int(a.user.Account.Containers))) h.Set("X-Account-Object-Count", strconv.Itoa(int(a.user.Objects))) // add metadata a.user.metadata.getMetadata(a) if a.req.Method == "HEAD" { return nil } var tmp orderedContainers // first get all matching objects and arrange them in alphabetical order. for _, container := range a.user.Containers { if strings.HasPrefix(container.name, prefix) { tmp = append(tmp, container) } } sort.Sort(tmp) resp := make([]Folder, 0) for _, container := range tmp { if container.name <= marker { continue } if format == "json" { resp = append(resp, Folder{ Count: len(container.objects), Bytes: container.bytes, Name: container.name, }) } else { a.w.Write([]byte(container.name + "\n")) } } if format == "json" { return resp } else { return nil } } func (r rootResource) post(a *action) interface{} { a.user.metadata.setMetadata(a, "account") return nil } func (rootResource) delete(a *action) interface{} { if a.req.URL.Query().Get("bulk-delete") == "1" { fatalf(403, "Operation forbidden", "Bulk delete is not supported") } return notAllowed() } func (rootResource) copy(a *action) interface{} { return notAllowed() } func NewSwiftServer(address string) (*SwiftServer, error) { var ( l net.Listener err error ) if strings.Index(address, ":") == -1 { for port := 1024; port < 65535; port++ { addr := fmt.Sprintf("%s:%d", address, port) if l, err = net.Listen("tcp", addr); err == nil { address = addr break } } } else { l, err = net.Listen("tcp", address) } if err != nil { return nil, fmt.Errorf("cannot listen on %s: %v", address, err) } server := &SwiftServer{ Listener: l, AuthURL: "http://" + l.Addr().String() + "/v1.0", URL: "http://" + l.Addr().String() + "/v1", Accounts: make(map[string]*account), Sessions: make(map[string]*session), } server.Accounts[TEST_ACCOUNT] = &account{ password: TEST_ACCOUNT, metadata: metadata{ meta: make(http.Header), }, Containers: make(map[string]*container), } go http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { server.serveHTTP(w, req) })) return server, nil } func (srv *SwiftServer) Close() { srv.Listener.Close() }