func TestGetForwardedIpAndTimeStamp(t *testing.T) { testIp := net.ParseIP("222.222.222.222") testTime := time.Unix(1397768380, 0) headers := map[string][]string{ textproto.CanonicalMIMEHeaderKey("X-ORIGINAL-MSEC"): []string{fmt.Sprintf("%d.000", testTime.Unix())}, textproto.CanonicalMIMEHeaderKey("X-Forwarded-For"): []string{"222.222.222.222, 123.123.123.123, 123.123.123.124"}, } ip := getIpFromHeader("X-Forwarded-For", headers) if testIp.String() != ip.String() { t.Errorf("Expecting %s for ip got %s\n", testIp, ip) } }
func getHeader(i map[string][]string, header string) string { h, ok := textproto.MIMEHeader(i)[textproto.CanonicalMIMEHeaderKey(header)] if ok { return h[0] } return "" }
// AssertJSONCall asserts that when the given handler is called with // the given parameters, the result is as specified. func AssertJSONCall(c *gc.C, p JSONCallParams) { c.Logf("JSON call, url %q", p.URL) if p.ExpectStatus == 0 { p.ExpectStatus = http.StatusOK } rec := DoRequest(c, DoRequestParams{ Do: p.Do, ExpectError: p.ExpectError, Handler: p.Handler, Method: p.Method, URL: p.URL, Body: p.Body, JSONBody: p.JSONBody, Header: p.Header, ContentLength: p.ContentLength, Username: p.Username, Password: p.Password, Cookies: p.Cookies, }) if p.ExpectError != "" { return } AssertJSONResponse(c, rec, p.ExpectStatus, p.ExpectBody) for k, v := range p.ExpectHeader { c.Assert(rec.HeaderMap[textproto.CanonicalMIMEHeaderKey(k)], gc.DeepEquals, v, gc.Commentf("header %q", k)) } }
// applyOverrides applies the transport's request overrides to req. If any // overrides apply, req is cloned and the overrides are applied to the clone. func (t *RequestModifyingTransport) applyOverrides(req *http.Request) *http.Request { // Only override GET and HEAD requests, just to be safe. We may want to // revisit this constraint later. if method := strings.ToUpper(req.Method); method != "GET" && method != "HEAD" { return req } requestURI := req.URL.RequestURI() t.overridesMu.Lock() defer t.overridesMu.Unlock() cloned := false for requestURIRegexp, override := range t.overrides { if requestURIRegexp.MatchString(requestURI) { if !cloned { req = cloneRequest(req) cloned = true } for name, val := range override.setHeaders { req.Header[textproto.CanonicalMIMEHeaderKey(name)] = val } if override.runOnlyOnce { delete(t.overrides, requestURIRegexp) } } } return req }
// heaveHeader check the existence of header header func (m *Message) HaveHeader(key string) bool { key = textproto.CanonicalMIMEHeaderKey(key) if len(m.Header.Get(key)) == 0 { return false } return true }
func (*suite) TestContentHashHeaderCanonicalized(c *gc.C) { // The header key should be canonicalized, because otherwise // the actually produced header will be different from that // specified. canon := textproto.CanonicalMIMEHeaderKey(params.ContentHashHeader) c.Assert(canon, gc.Equals, params.ContentHashHeader) }
// IsSet tests if a key is present in the Header func (h Header) IsSet(key string) bool { if h == nil { return false } _, ok := h[textproto.CanonicalMIMEHeaderKey(key)] return ok }
// Get gets the first value associated with key in the Header and // returns it and true. If the key is unset it returns the empty string // and false. func (h Header) Get(key string) (string, bool) { v, ok := h[textproto.CanonicalMIMEHeaderKey(key)] if !ok { return "", false } return v[0], ok }
func TestSetHeaders(t *testing.T) { expectedFromEmptyHeader := http.Header{ textproto.CanonicalMIMEHeaderKey(openrtb.HEADER_VERSION): []string{openrtb.VERSION}, "Content-Type": []string{"application/json; charset=utf-8"}, } tests := []struct { input http.Header expected http.Header }{ // Should set from empty header. { http.Header{}, expectedFromEmptyHeader, }, // Should replace the old content type. { http.Header{"Content-Type": []string{"text/plain"}}, expectedFromEmptyHeader, }, // Should replace the old OpenRTB spec version. { http.Header{textproto.CanonicalMIMEHeaderKey(openrtb.HEADER_VERSION): []string{"2.1"}}, expectedFromEmptyHeader, }, // Should keep existing, non-overlapped headers. { http.Header{"X-Additional-Header": []string{"for test"}}, http.Header{ textproto.CanonicalMIMEHeaderKey(openrtb.HEADER_VERSION): []string{openrtb.VERSION}, "Content-Type": []string{"application/json; charset=utf-8"}, "X-Additional-Header": []string{"for test"}, }, }, } for i, test := range tests { t.Logf("Testint %d...", i) openrtb.SetHeaders(test.input) if !reflect.DeepEqual(test.input, test.expected) { t.Errorf("Expected the HTTP headers to be\n%v instead of\n%v.", test.expected, test.input) } } }
// NewObjectFromStream ... func NewObjectFromStream(header textproto.MIMEHeader, body io.ReadCloser) (*Object, error) { objectID, err := strconv.ParseInt(header.Get("Object-ID"), 10, 64) if err != nil { // Attempt to parse a Rets Response code (if it exists) resp, parseErr := ReadResponse(body) if parseErr != nil { return nil, err } // Include a GetObject (empty of content) so that its rets response can be retrieved emptyResult := Object{ RetsMessage: resp, RetsError: resp.Code != StatusOK, } return &emptyResult, err } preferred, err := strconv.ParseBool(header.Get("Preferred")) if err != nil { preferred = false } objectData := make(map[string]string) for _, v := range header[textproto.CanonicalMIMEHeaderKey("ObjectData")] { kv := strings.Split(v, "=") objectData[kv[0]] = kv[1] } blob, err := ioutil.ReadAll(body) if err != nil { return nil, err } // 5.6.7 retsError, err := strconv.ParseBool(header.Get("RETS-Error")) retsMsg, err := ReadResponse(ioutil.NopCloser(bytes.NewReader(blob))) // there is a rets message, stash it and wipe the content if err == nil { blob = nil } object := Object{ // required ObjectID: int(objectID), ContentID: header.Get("Content-ID"), ContentType: header.Get("Content-Type"), // optional UID: header.Get("UID"), Description: header.Get("Content-Description"), SubDescription: header.Get("Content-Sub-Description"), Location: header.Get("Location"), RetsError: retsError, RetsMessage: retsMsg, Preferred: preferred, ObjectData: objectData, Blob: blob, } return &object, nil }
// GetHeaders returns valueS for header key key func (m *Email) GetHeaders(key string) (headers []string, err error) { // if not parsed if !m.flagHeaderParsed { if err = m.parseHeader(); err != nil { return headers, err } } headers, _ = m.Header[textproto.CanonicalMIMEHeaderKey(key)] return }
// Get gets the first value associated with the given key. // If there are no values associated with the key, Get returns "". // Get is a convenience method. For more complex queries, // access the map directly. func (h Header) Get(key string) string { if h == nil { return "" } v := h[textproto.CanonicalMIMEHeaderKey(key)] if len(v) == 0 { return "" } return v[0] }
func TestCompile(t *testing.T) { pat, err := Compile("hello, %% %b %D %h %H %l %m %p %q %r %s %t %T %u %U %v %V %>s %{X-LogFormat-Test}i %{X-LogFormat-Test}o world!") if err != nil { t.Errorf("Failed to compile: %s", err) return } b := &bytes.Buffer{} pat(b, dummyCtx{ elapsed: 5 * time.Second, req: &http.Request{ Header: http.Header{ textproto.CanonicalMIMEHeaderKey("Content-Length"): []string{"8192"}, textproto.CanonicalMIMEHeaderKey("X-LogFormat-Test"): []string{"Hello, Request!"}, }, Method: "GET", Proto: "HTTP/1.1", RemoteAddr: "192.168.11.1", Host: "example.com", URL: &url.URL{ Host: "example.com", Path: "/hello_world", RawQuery: "hello=world", }, }, res: &dummyResponse{ hdrs: http.Header{ textproto.CanonicalMIMEHeaderKey("X-LogFormat-Test"): []string{"Hello, Response!"}, }, status: 400, }, }) re := regexp.MustCompile(`^hello, % 8192 5000000 192\.168\.11\.1 HTTP/1\.1 - GET \d+ \?hello=world GET //example\.com/hello_world\?hello=world HTTP/1\.1 400 \d{2}/[a-zA-Z]+/\d{4}:\d{2}:\d{2}:\d{2} [+-]\d{4} 5 - /hello_world example\.com example\.com 400 Hello, Request! Hello, Response! world!$`) if !re.Match(b.Bytes()) { t.Errorf("output did not match regexp") t.Logf("output: %s", b.String()) t.Logf("regexp: %s", re) return } }
func doRequest(req *http.Request) *http.Response { if _, ok := req.Header[textproto.CanonicalMIMEHeaderKey("User-Agent")]; !ok { // Setting a blank User-Agent causes the http lib not to output one, whereas if there // is no header, it will output a default one. // See: https://code.google.com/p/go/source/browse/src/pkg/net/http/request.go?name=go1.3.3#398 req.Header.Set("User-Agent", "") } resp, err := http.DefaultTransport.RoundTrip(req) Expect(err).To(BeNil()) return resp }
// Returns the header value for server-side encryption, or empty string if there is no SSE header. func GetHeaderSSE(headers map[string][]string) string { // Headers returned in an HTTP response will have their key names connonicalized by the Go // http library, so we MUST run the key through textproto.CanonicalMIMEHeaderKey in order // to find it in the map // So headers in the http request must also have been added with a cannonicalized key to find them. // TODO: Change parameters in this file to use http.Header data type rather than map[string][]string if val := headers[textproto.CanonicalMIMEHeaderKey(sseHeaderKey)]; len(val) > 0 { return val[0] } return "" }
func (this *ssdpDefaultManager) ssdpParseHeaderLine(raw *ssdpRawMessage, line []byte) { i := strings.Index(string(line), ":") if -1 == i { panic("Invalid header") } field := textproto.CanonicalMIMEHeaderKey(strings.TrimSpace(string(line[0:i]))) value := strings.TrimSpace(string(line[i+1:])) if _, has := raw.header[field]; has { panic("Header field redefined") } else { raw.header[field] = value } }
// Send does what it is supposed to do. func (m *Mail) Send(host string, port int, user, pass string) error { // validate from address from, err := mail.ParseAddress(m.From) if err != nil { return err } // validate to address to, err := mail.ParseAddress(m.To) if err != nil { return err } // set headers for html email header := textproto.MIMEHeader{} header.Set(textproto.CanonicalMIMEHeaderKey("from"), from.Address) header.Set(textproto.CanonicalMIMEHeaderKey("to"), to.Address) header.Set(textproto.CanonicalMIMEHeaderKey("content-type"), "text/html; charset=UTF-8") header.Set(textproto.CanonicalMIMEHeaderKey("mime-version"), "1.0") header.Set(textproto.CanonicalMIMEHeaderKey("subject"), m.Subject) // init empty message var buffer bytes.Buffer // write header for key, value := range header { buffer.WriteString(fmt.Sprintf("%s: %s\r\n", key, value[0])) } // write body buffer.WriteString(fmt.Sprintf("\r\n%s", m.HTML)) // send email addr := fmt.Sprintf("%s:%d", host, port) auth := smtp.PlainAuth("", user, pass, host) return smtp.SendMail(addr, auth, from.Address, []string{to.Address}, buffer.Bytes()) }
func RunServer(conf string) { server := ObjectServer{driveRoot: "/srv/node", hashPathPrefix: "", hashPathSuffix: "", checkMounts: true, disableFsync: false, asyncFinalize: false, allowedHeaders: map[string]bool{"Content-Disposition": true, "Content-Encoding": true, "X-Delete-At": true, "X-Object-Manifest": true, "X-Static-Large-Object": true, }, diskInUse: map[string]int64{}, } if swiftconf, err := LoadIniFile("/etc/swift/swift.conf"); err == nil { server.hashPathPrefix = swiftconf.GetDefault("swift-hash", "swift_hash_path_prefix", "") server.hashPathSuffix = swiftconf.GetDefault("swift-hash", "swift_hash_path_suffix", "") } serverconf, err := LoadIniFile(conf) if err != nil { panic(fmt.Sprintf("Unable to load %s", conf)) } server.driveRoot = serverconf.GetDefault("DEFAULT", "devices", "/srv/node") server.checkMounts = LooksTrue(serverconf.GetDefault("DEFAULT", "mount_check", "true")) server.disableFsync = LooksTrue(serverconf.GetDefault("DEFAULT", "disable_fsync", "false")) server.asyncFinalize = LooksTrue(serverconf.GetDefault("DEFAULT", "async_finalize", "false")) server.diskLimit, err = strconv.ParseInt(serverconf.GetDefault("DEFAULT", "disk_limit", "100"), 10, 64) if err != nil { panic("Invalid disk_limit format") } bindIP := serverconf.GetDefault("DEFAULT", "bind_ip", "0.0.0.0") bindPort, err := strconv.ParseInt(serverconf.GetDefault("DEFAULT", "bind_port", "8080"), 10, 64) if err != nil { panic("Invalid bind port format") } if allowedHeaders, ok := serverconf.Get("DEFAULT", "allowed_headers"); ok { headers := strings.Split(allowedHeaders, ",") for i := range headers { server.allowedHeaders[textproto.CanonicalMIMEHeaderKey(strings.TrimSpace(headers[i]))] = true } } sock, err := net.Listen("tcp", fmt.Sprintf("%s:%d", bindIP, bindPort)) if err != nil { panic(fmt.Sprintf("Unable to bind %s:%d", bindIP, bindPort)) } server.logger = SetupLogger(serverconf.GetDefault("DEFAULT", "log_facility", "LOG_LOCAL0"), "object-server") DropPrivileges(serverconf.GetDefault("DEFAULT", "user", "swift")) srv := &http.Server{Handler: server} srv.Serve(sock) }
// RawHaveHeader check igf header header is present in raw mail func RawHaveHeader(raw *[]byte, header string) bool { var bHeader []byte if strings.ToLower(header) == "message-id" { bHeader = []byte("Message-ID") } else { bHeader = []byte(textproto.CanonicalMIMEHeaderKey(header)) } for _, line := range bytes.Split(RawGetHeaders(raw), []byte{13, 10}) { if bytes.HasPrefix(line, bHeader) { return true } } return false }
func splitMimeHeader(s string) (string, string) { p := strings.IndexByte(s, ':') if p < 0 { return s, "" } key := textproto.CanonicalMIMEHeaderKey(s[:p]) for p = p + 1; p < len(s); p++ { if s[p] != ' ' { break } } return key, s[p:] }
// NewManager Create new Manager with provider name and json config string. // provider name: // 1. cookie // 2. file // 3. memory // 4. redis // 5. mysql // json config: // 1. is https default false // 2. hashfunc default sha1 // 3. hashkey default beegosessionkey // 4. maxage default is none func NewManager(provideName, config string) (*Manager, error) { provider, ok := provides[provideName] if !ok { return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName) } cf := new(managerConfig) cf.EnableSetCookie = true err := json.Unmarshal([]byte(config), cf) if err != nil { return nil, err } if cf.Maxlifetime == 0 { cf.Maxlifetime = cf.Gclifetime } if cf.EnableSidInHttpHeader { if cf.SessionNameInHttpHeader == "" { panic(errors.New("SessionNameInHttpHeader is empty")) } strMimeHeader := textproto.CanonicalMIMEHeaderKey(cf.SessionNameInHttpHeader) if cf.SessionNameInHttpHeader != strMimeHeader { strErrMsg := "SessionNameInHttpHeader (" + cf.SessionNameInHttpHeader + ") has the wrong format, it should be like this : " + strMimeHeader panic(errors.New(strErrMsg)) } } err = provider.SessionInit(cf.Maxlifetime, cf.ProviderConfig) if err != nil { return nil, err } if cf.SessionIDLength == 0 { cf.SessionIDLength = 16 } // 设置存储提供者cookie的CookieName CookieName = cf.CookieName return &Manager{ provider, cf, }, nil }
func ParseHeaders(mailData string) map[string]string { var headerSectionEnds int for i, char := range mailData[:len(mailData)-4] { if char == '\r' { if mailData[i+1] == '\n' && mailData[i+2] == '\r' && mailData[i+3] == '\n' { headerSectionEnds = i + 2 } } } headers := make(map[string]string) // TODO header comments and textproto Reader instead of regex matches := headerRegex.FindAllStringSubmatch(mailData[:headerSectionEnds], -1) for _, h := range matches { name := textproto.CanonicalMIMEHeaderKey(strings.TrimSpace(strings.Replace(h[1], "\r\n", "", -1))) val := strings.TrimSpace(strings.Replace(h[2], "\r\n", "", -1)) headers[name] = val } return headers }
func (obj objectHandler) get(w http.ResponseWriter, r *http.Request) { owner, err := s3intf.GetOwner(obj.Bucket.Service, r, obj.Bucket.Service.Host()) if err != nil { writeError(w, &HTTPError{Code: 20, HTTPCode: http.StatusBadRequest, Message: "error getting owner: " + err.Error(), Resource: "/" + obj.Bucket.Name + "/" + obj.object}) return } fn, media, body, size, md5, err := obj.Bucket.Service.Get(owner, obj.Bucket.Name, obj.object) log.Printf("GETing %s/%s: %q %s", obj.Bucket.Name, obj.object, fn, err) if err != nil { if err == s3intf.NotFound { w.WriteHeader(http.StatusNotFound) return } writeError(w, &HTTPError{Code: 21, Message: "error getting " + obj.Bucket.Name + "/" + obj.object + ": " + err.Error(), Resource: "/" + obj.Bucket.Name + "/" + obj.object}) return } if err = r.ParseForm(); err != nil { writeError(w, &HTTPError{Code: 22, HTTPCode: http.StatusBadRequest, Message: "cannot parse form values: " + err.Error(), Resource: "/" + obj.Bucket.Name + "/" + obj.object}) return } w.Header().Set("Content-Type", media) w.Header().Set("Content-Disposition", "inline; filename=\""+fn+"\"") w.Header().Set("Content-Length", strconv.Itoa(int(size))) w.Header().Set("ETag", hex.EncodeToString(md5)) for k, v := range r.Form { k = textproto.CanonicalMIMEHeaderKey(k) switch k { case "Content-Type", "Content-Language", "Expires", "Cache-Control", "Content-Disposition", "Content-Encoding", "Content-Length": (map[string][]string(w.Header()))[k] = v } } if Debug { log.Printf("headers: %s", w.Header()) } io.Copy(w, body) }
// getRaw returns raw message // some cleanup are made // wrap headers line to 999 char max func (m *Message) GetRaw() (rawMessage []byte, err error) { rawMessage = []byte{} // Header for key, hs := range m.Header { // clean key key = textproto.CanonicalMIMEHeaderKey(key) for _, value := range hs { //println("Les headers avant traitement: " + key + " -> " + value) // TODO clean value // split at 900 // remove unsuported char // // On ne doit pas avoir autre chose que des char < 128 // Attention si un jour on implemente l'extension SMTPUTF8 // Voir RFC 6531 (SMTPUTF8 extension), RFC 6532 (Internationalized email headers) and RFC 6533 (Internationalized delivery status notifications). /*for _, c := range value { if c > 128 { return rawMessage, ErrNonAsciiCharDetected } }*/ // Fold header //t := FoldHeader(key+": "+value) + "\r\n" //println("\nHeaders apres traitement: " + t) newHeader := []byte(key + ": " + value) FoldHeader(&newHeader) rawMessage = append(rawMessage, newHeader...) rawMessage = append(rawMessage, []byte{13, 10}...) } } rawMessage = append(rawMessage, []byte{13, 10}...) // Body b, err := ioutil.ReadAll(m.Body) if err != nil { return } rawMessage = append(rawMessage, b...) return }
// CanonicalHeaderKey returns the canonical format of the // header key s. The canonicalization converts the first // letter and any letter following a hyphen to upper case; // the rest are converted to lowercase. For example, the // canonical key for "accept-encoding" is "Accept-Encoding". func CanonicalHeaderKey(s string) string { return textproto.CanonicalMIMEHeaderKey(s) }
func checkMessage(msg *mail.TestMessage, adminsPlain []string, user string) error { sender, err := net_mail.ParseAddress(msg.Sender) if err != nil { return fmt.Errorf("unparsable Sender address: %s: %s", msg.Sender, err) } senderOK := user != "" && sender.Address == user if !senderOK { for _, a := range adminsPlain { if sender.Address == a { senderOK = true break } } } if !senderOK { return fmt.Errorf("invalid Sender: %s", msg.Sender) } if len(msg.To) == 0 && len(msg.Cc) == 0 && len(msg.Bcc) == 0 { return fmt.Errorf("one of To, Cc or Bcc must be non-empty") } if err := parseEmails(msg.To...); err != nil { return err } if err := parseEmails(msg.Cc...); err != nil { return err } if err := parseEmails(msg.Bcc...); err != nil { return err } if len(msg.Body) == 0 && len(msg.HTMLBody) == 0 { return fmt.Errorf("one of Body or HTMLBody must be non-empty") } if len(msg.Attachments) > 0 { msg.MIMETypes = make([]string, len(msg.Attachments)) for i := range msg.Attachments { n := msg.Attachments[i].Name ext := strings.TrimLeft(strings.ToLower(filepath.Ext(n)), ".") if badExtensions.Has(ext) { return fmt.Errorf("illegal attachment extension for %q", n) } mimetype := extensionMapping[ext] if mimetype == "" { mimetype = "application/octet-stream" } msg.MIMETypes[i] = mimetype } } fixKeys := map[string]string{} for k := range msg.Headers { canonK := textproto.CanonicalMIMEHeaderKey(k) if !okHeaders.Has(canonK) { return fmt.Errorf("disallowed header: %s", k) } if canonK != k { fixKeys[k] = canonK } } for k, canonK := range fixKeys { vals := msg.Headers[k] delete(msg.Headers, k) msg.Headers[canonK] = vals } return nil }
func handleForwardResponseTrailerHeader(w http.ResponseWriter, md ServerMetadata) { for k := range md.TrailerMD { tKey := textproto.CanonicalMIMEHeaderKey(fmt.Sprintf("%s%s", MetadataTrailerPrefix, k)) w.Header().Add("Trailer", tKey) } }
// NewFromEmail return a new DkimHeader by parsing an email // Note: according to RFC 6376 an email can have multiple DKIM Header // in this case we return the last inserted or the last with d== mail from func newDkimHeaderFromEmail(email []byte) (*DKIMHeader, error) { m, err := mail.ReadMessage(bytes.NewReader(email)) if err != nil { return nil, err } // DKIM header ? if len(m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")]) == 0 { return nil, ErrDkimHeaderNotFound } // Get mail from domain mailFromDomain := "" mailfrom, err := mail.ParseAddress(m.Header.Get(textproto.CanonicalMIMEHeaderKey("From"))) if err != nil { if err.Error() != "mail: no address" { return nil, err } } else { t := strings.SplitAfter(mailfrom.Address, "@") if len(t) > 1 { mailFromDomain = strings.ToLower(t[1]) } } // get raw dkim header // we can't use m.header because header key will be converted with textproto.CanonicalMIMEHeaderKey // ie if key in header is not DKIM-Signature but Dkim-Signature or DKIM-signature ot... other // combination of case, verify will fail. rawHeaders, _, err := getHeadersBody(email) if err != nil { return nil, ErrBadMailFormat } rawHeadersList, err := getHeadersList(&rawHeaders) if err != nil { return nil, err } dkHeaders := []string{} for h := rawHeadersList.Front(); h != nil; h = h.Next() { if strings.HasPrefix(strings.ToLower(h.Value.(string)), "dkim-signature") { dkHeaders = append(dkHeaders, h.Value.(string)) } } var keep *DKIMHeader var keepErr error //for _, dk := range m.Header[textproto.CanonicalMIMEHeaderKey("DKIM-Signature")] { for _, h := range dkHeaders { parsed, err := parseDkHeader(h) // if malformed dkim header try next if err != nil { keepErr = err continue } // Keep first dkim headers if keep == nil { keep = parsed } // if d flag == domain keep this header and return if mailFromDomain == parsed.Domain { return parsed, nil } } if keep == nil { return nil, keepErr } return keep, nil }
// getHeaders returns all the headers corresponding to the key key func (m *Message) GetHeaders(key string) []string { return m.Header[textproto.CanonicalMIMEHeaderKey(key)] }
// delHeader deletes the values associated with key. func (m *Message) DelHeader(key string) { delete(m.Header, textproto.CanonicalMIMEHeaderKey(key)) }