func mediaTypeOrDefault(header textproto.MIMEHeader) string { mediaType, _, err := mime.ParseMediaType(header.Get("Content-Type")) if err != nil { return "application/octet-stream" } return mediaType }
func checkContentType(h textproto.MIMEHeader, expected string) error { ctype := h.Get("Content-Type") if ctype != expected { return errors.Errorf("expected Content-Type %q, got %q", expected, ctype) } return nil }
// Date parses the Date header field. func Date(h textproto.MIMEHeader) (time.Time, error) { hdr := h.Get("Date") if hdr == "" { return time.Time{}, ErrHeaderNotPresent } return parseDate(hdr) }
func newEntity(h textproto.MIMEHeader, r io.Reader, shared *sharedData) (*Entity, error) { contType := h.Get("Content-Type") if contType == "" { return &Entity{ Header: h, body: &SinglepartBody{Reader: r}, shared: shared, }, nil } mediaType, params, err := mime.ParseMediaType(contType) if err != nil { return nil, err } if !strings.HasPrefix(mediaType, "multipart/") { return &Entity{ Header: h, body: &SinglepartBody{Reader: r}, shared: shared, }, nil } boundary, ok := params["boundary"] if !ok { return nil, fmt.Errorf("Boundary not found in Content-Type field: %v", contType) } return &Entity{ Header: h, body: &MultipartBody{multipart.NewReader(r, boundary)}, shared: shared, }, nil }
//handleEventMsg processes event messages received from Freeswitch. func (client *Client) handleEventMsg(resp textproto.MIMEHeader) error { event := make(map[string]string) //Check that Content-Length is numeric. _, err := strconv.Atoi(resp.Get("Content-Length")) if err != nil { log.Print(logPrefix, "Invalid Content-Length", err) return err } for { //Read each line of the event and store into map. line, err := client.eventConn.ReadLine() if err != nil { log.Print(logPrefix, "Event Read failure: ", err) return err } if line == "" { //Empty line means end of event. client.sendEvent(event) return err } parts := strings.Split(line, ": ") //Split "Key: value" key := parts[0] value, err := url.QueryUnescape(parts[1]) if err != nil { log.Print(logPrefix, "Parse failure: ", err) return err } event[key] = value } }
// parseMIMEParts will recursively walk a MIME entity and return a []mime.Part containing // each (flattened) mime.Part found. // It is important to note that there are no limits to the number of recursions, so be // careful when parsing unknown MIME structures! func parseMIMEParts(hs textproto.MIMEHeader, b io.Reader) ([]*part, error) { var ps []*part // If no content type is given, set it to the default if _, ok := hs["Content-Type"]; !ok { hs.Set("Content-Type", defaultContentType) } ct, params, err := mime.ParseMediaType(hs.Get("Content-Type")) if err != nil { return ps, err } // If it's a multipart email, recursively parse the parts if strings.HasPrefix(ct, "multipart/") { if _, ok := params["boundary"]; !ok { return ps, ErrMissingBoundary } mr := multipart.NewReader(b, params["boundary"]) for { var buf bytes.Buffer p, err := mr.NextPart() if err == io.EOF { break } if err != nil { return ps, err } if _, ok := p.Header["Content-Type"]; !ok { p.Header.Set("Content-Type", defaultContentType) } subct, _, err := mime.ParseMediaType(p.Header.Get("Content-Type")) if strings.HasPrefix(subct, "multipart/") { sps, err := parseMIMEParts(p.Header, p) if err != nil { return ps, err } ps = append(ps, sps...) } else { // Otherwise, just append the part to the list // Copy the part data into the buffer if _, err := io.Copy(&buf, p); err != nil { return ps, err } ps = append(ps, &part{body: buf.Bytes(), header: p.Header}) } } } else { // If it is not a multipart email, parse the body content as a single "part" var buf bytes.Buffer if _, err := io.Copy(&buf, b); err != nil { return ps, err } ps = append(ps, &part{body: buf.Bytes(), header: hs}) } return ps, nil }
//handleAPIMsg processes API response messages received from Freeswitch. func (client *Client) handleAPIMsg(resp textproto.MIMEHeader) error { //Check that Content-Length is numeric. length, err := strconv.Atoi(resp.Get("Content-Length")) if err != nil { log.Print(logPrefix, "Invalid Content-Length", err) client.sendCmdRes(cmdRes{body: "", err: err}, true) return err } //Read Content-Length bytes into a buffer and convert to string. buf := make([]byte, length) if _, err = io.ReadFull(client.eventConn.R, buf); err != nil { log.Print(logPrefix, "API Read failure: ", err) } client.sendCmdRes(cmdRes{body: string(buf), err: err}, true) return err }
func TestSubjectHeaderWithExistingQuotes(t *testing.T) { m := simpleMessage() m.Subject = `"Hi World"` buf := new(bytes.Buffer) header := textproto.MIMEHeader{} _, err := m.bytes(buf, header) if err != nil { t.Log(err) t.Fail() } expected := `\"Hi World\"` if sub := header.Get("Subject"); sub != expected { t.Logf(`Expected Subject to be "%s" but got "%s"`, expected, sub) t.Fail() } }
// store message, unpack attachments, register with daemon, send to daemon for federation // in that order func (self *nntpConnection) storeMessage(daemon *NNTPDaemon, hdr textproto.MIMEHeader, body io.Reader) (err error) { var f io.WriteCloser msgid := getMessageID(hdr) if msgid == "" { // drop, invalid header log.Println(self.name, "dropping message with invalid mime header, no message-id") _, err = io.Copy(Discard, body) return } else if ValidMessageID(msgid) { f = daemon.store.CreateFile(msgid) } else { // invalid message-id log.Println(self.name, "dropping message with invalid message-id", msgid) _, err = io.Copy(Discard, body) return } if f == nil { // could not open file, probably already storing it from another connection log.Println(self.name, "discarding duplicate message") _, err = io.Copy(Discard, body) return } path := hdr.Get("Path") hdr.Set("Path", daemon.instance_name+"!"+path) // now store attachments and article err = writeMIMEHeader(f, hdr) if err == nil { err = daemon.store.ProcessMessageBody(f, hdr, body) if err == nil { // tell daemon daemon.loadFromInfeed(msgid) } } f.Close() if err != nil { // clean up if ValidMessageID(msgid) { DelFile(daemon.store.GetFilename(msgid)) } } return }
// get a message id from a mime header // checks many values func getMessageID(hdr textproto.MIMEHeader) (msgid string) { msgid = hdr.Get("Message-Id") if msgid == "" { msgid = hdr.Get("Message-ID") } if msgid == "" { msgid = hdr.Get("message-id") } if msgid == "" { msgid = hdr.Get("MESSAGE-ID") } return }
// Read body from text/plain func readPlainText(header textproto.MIMEHeader, body io.Reader) (mailbody []byte, err error) { contentType := header.Get("Content-Type") encoding := header.Get("Content-Transfer-Encoding") _, params, err := mime.ParseMediaType(contentType) if encoding == ENC_QUOTED_PRINTABLE { if strings.ToLower(params["charset"]) == CHARSET_ISO2022JP { mailbody, err = ioutil.ReadAll(transform.NewReader(quotedprintable.NewReader(body), japanese.ISO2022JP.NewDecoder())) } else { mailbody, err = ioutil.ReadAll(quotedprintable.NewReader(body)) } } else if encoding == ENC_BASE64 { mailbody, err = ioutil.ReadAll(base64.NewDecoder(base64.StdEncoding, body)) } else if len(contentType) == 0 || strings.ToLower(params["charset"]) == CHARSET_ISO2022JP { // charset=ISO-2022-JP mailbody, err = ioutil.ReadAll(transform.NewReader(body, japanese.ISO2022JP.NewDecoder())) } else { // encoding = 8bit or 7bit mailbody, err = ioutil.ReadAll(body) } return mailbody, err }
// parseMIMEParts will recursively walk a MIME entity and return a []mime.Part containing // each (flattened) mime.Part found. // It is important to note that there are no limits to the number of recursions, so be // careful when parsing unknown MIME structures! func parseMIMEParts(hs textproto.MIMEHeader, b io.Reader) ([]*part, error) { var ps []*part ct, params, err := mime.ParseMediaType(hs.Get("Content-Type")) if err != nil { return ps, err } if strings.HasPrefix(ct, "multipart/") { if _, ok := params["boundary"]; !ok { return ps, ErrMissingBoundary } mr := multipart.NewReader(b, params["boundary"]) for { var buf bytes.Buffer p, err := mr.NextPart() if err == io.EOF { break } if err != nil { return ps, err } subct, _, err := mime.ParseMediaType(p.Header.Get("Content-Type")) if strings.HasPrefix(subct, "multipart/") { sps, err := parseMIMEParts(p.Header, p) if err != nil { return ps, err } ps = append(ps, sps...) } else { // Otherwise, just append the part to the list // Copy the part data into the buffer if _, err := io.Copy(&buf, p); err != nil { return ps, err } ps = append(ps, &part{body: buf.Bytes(), header: p.Header}) } } } return ps, nil }
// AddressList parses the named header field as a list of addresses. func AddressList(header textproto.MIMEHeader, key string, utf8ReaderFactory UTF8ReaderFactory) (r []*Address, err error) { // h := make(map[string][]string) // vals := header[key] // if vals == nil { // return // } // var newDecodedVals []string // for _, val := range vals { // var newVal string // newVal, err = DecodeText(val, utf8ReaderFactory) // if err != nil { // return // } // newDecodedVals = append(newDecodedVals, newVal) // } // h[key] = newDecodedVals hdr := header.Get(key) if hdr == "" { return nil, ErrHeaderNotPresent } return newAddrParser(hdr, utf8ReaderFactory).parseAddressList() }
//newEvent build event func newEvent(data *textproto.MIMEHeader) (*AMIEvent, error) { if data.Get("Event") == "" { return nil, errNotEvent } ev := &AMIEvent{data.Get("Event"), strings.Split(data.Get("Privilege"), ","), make(map[string]string)} for k, v := range *data { if k == "Event" || k == "Privilege" { continue } ev.Params[k] = v[0] } return ev, nil }
//newResponse build a response for action func newResponse(data *textproto.MIMEHeader) (*AMIResponse, error) { if data.Get("Response") == "" { return nil, errors.New("Not Response") } response := &AMIResponse{"", "", make(map[string]string)} for k, v := range *data { if k == "Response" { continue } response.Params[k] = v[0] } response.ID = data.Get("Actionid") response.Status = data.Get("Response") return response, nil }
// decodeBody parses body of a message, filling m.Body, m.HTML and m.Parts. func (m *Message) decodeBody(r io.Reader, h textproto.MIMEHeader) error { cth := h.Get("Content-Type") if cth == "" { cth = "text/plain" } ct, ctp, err := mime.ParseMediaType(cth) if err != nil { return fmt.Errorf("invalid content-type: %q", cth) } // Find name. filename := ctp["name"] if filename == "" { cdh := h.Get("Content-Disposition") if cdh != "" { _, cdp, err := mime.ParseMediaType(cdh) if err != nil { return fmt.Errorf("invalid content-disposition: %q", cdh) } filename = cdp["filename"] } } // If it has filename, add as attachment. if filename != "" { name, err := decodeHeader(filename) if err != nil { return fmt.Errorf("decode filename: %v", err) } data, err := ioutil.ReadAll(decodeTransfer(r, h.Get("Content-Transfer-Encoding"))) if err != nil { return fmt.Errorf("read attachment: %v", err) } m.Parts = append(m.Parts, Part{Name: name, Data: data}) return nil } if ct == "text/plain" || ct == "text/html" { buf := new(bytes.Buffer) for { data, err := ioutil.ReadAll(decodeTransfer(r, h.Get("Content-Transfer-Encoding"))) buf.Write(data) if err != nil { if _, ok := err.(base64.CorruptInputError); ok { continue } return fmt.Errorf("read body: %v", err) } break } body, err := decodeCharset(buf.String(), ctp["charset"]) if err != nil { return fmt.Errorf("charsetDecode: %v", err) } if ct == "text/html" { m.HTML += body return nil } m.Body += body return nil } if strings.HasPrefix(ct, "multipart/") { r := multipart.NewReader(r, ctp["boundary"]) for { p, err := r.NextPart() if err != nil { if err == io.EOF { break } return fmt.Errorf("next part: %q", err) } if err := m.decodeBody(p, p.Header); err != nil { p.Close() // p.Close is also called automatically by r.NextPart. return err } } return nil } // TODO: decide what to do with this. //return fmt.Errorf("content-type without filename: %q", ct) return nil }
// 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 }
func (self nntpConnection) handleLine(daemon NNTPDaemon, code int, line string, conn *textproto.Conn) (err error) { parts := strings.Split(line, " ") var msgid string if code == 0 && len(parts) > 1 { msgid = parts[1] } else { msgid = parts[0] } if code == 238 { if ValidMessageID(msgid) { log.Println("sending", msgid, "to", self.name) // send the article to us self.take <- msgid } } else if code == 239 { // successful TAKETHIS log.Println(msgid, "sent via", self.name) // TODO: remember success } else if code == 431 { // CHECK said we would like this article later log.Println("defer sending", msgid, "to", self.name) go self.articleDefer(msgid) } else if code == 439 { // TAKETHIS failed log.Println(msgid, "was not sent to", self.name, "denied:", line) // TODO: remember denial } else if code == 438 { // they don't want the article // TODO: remeber rejection } else { // handle command parts := strings.Split(line, " ") if len(parts) == 2 { cmd := parts[0] if cmd == "MODE" { if parts[1] == "READER" { // reader mode self.mode = "READER" log.Println(self.name, "switched to reader mode") conn.PrintfLine("201 No posting Permitted") } else if parts[1] == "STREAM" { // wut? we're already in streaming mode log.Println(self.name, "already in streaming mode") conn.PrintfLine("203 Streaming enabled brah") } else { // invalid log.Println(self.name, "got invalid mode request", parts[1]) conn.PrintfLine("501 invalid mode variant:", parts[1]) } } else if cmd == "QUIT" { // quit command conn.PrintfLine("") // close our connection and return conn.Close() return } else if cmd == "CHECK" { // handle check command msgid := parts[1] // have we seen this article? if daemon.database.HasArticle(msgid) { // yeh don't want it conn.PrintfLine("438 %s", msgid) } else if daemon.database.ArticleBanned(msgid) { // it's banned we don't want it conn.PrintfLine("438 %s", msgid) } else { // yes we do want it and we don't have it conn.PrintfLine("238 %s", msgid) } } else if cmd == "TAKETHIS" { // handle takethis command var hdr textproto.MIMEHeader var reason string // read the article header hdr, err = conn.ReadMIMEHeader() if err == nil { // check the header reason, err = self.checkMIMEHeader(daemon, hdr) dr := conn.DotReader() if len(reason) > 0 { // discard, we do not want code = 439 log.Println(self.name, "rejected", msgid, reason) _, err = io.Copy(ioutil.Discard, dr) err = daemon.database.BanArticle(msgid, reason) } else { // check if we don't have the rootpost reference := hdr.Get("References") newsgroup := hdr.Get("Newsgroups") if reference != "" && ValidMessageID(reference) && !daemon.store.HasArticle(reference) && !daemon.database.IsExpired(reference) { log.Println(self.name, "got reply to", reference, "but we don't have it") daemon.ask_for_article <- ArticleEntry{reference, newsgroup} } f := daemon.store.CreateTempFile(msgid) if f == nil { log.Println(self.name, "discarding", msgid, "we are already loading it") // discard io.Copy(ioutil.Discard, dr) } else { // write header err = writeMIMEHeader(f, hdr) // write body _, err = io.Copy(f, dr) if err == nil || err == io.EOF { f.Close() // we gud, tell daemon daemon.infeed_load <- msgid } else { log.Println(self.name, "error reading message", err) } } code = 239 reason = "gotten" } } else { log.Println(self.name, "error reading mime header:", err) code = 439 reason = "error reading mime header" } conn.PrintfLine("%d %s %s", code, msgid, reason) } else if cmd == "ARTICLE" { if ValidMessageID(msgid) { if daemon.store.HasArticle(msgid) { // we have it yeh f, err := os.Open(daemon.store.GetFilename(msgid)) if err == nil { conn.PrintfLine("220 %s", msgid) dw := conn.DotWriter() _, err = io.Copy(dw, f) dw.Close() f.Close() } else { // wtf?! conn.PrintfLine("503 idkwtf happened: %s", err.Error()) } } else { // we dont got it conn.PrintfLine("430 %s", msgid) } } else { // invalid id conn.PrintfLine("500 Syntax error") } } } } return }
// check if we want the article given its mime header // returns empty string if it's okay otherwise an error message func (self nntpConnection) checkMIMEHeader(daemon NNTPDaemon, hdr textproto.MIMEHeader) (reason string, err error) { newsgroup := hdr.Get("Newsgroups") reference := hdr.Get("References") msgid := hdr.Get("Message-Id") encaddr := hdr.Get("X-Encrypted-Ip") torposter := hdr.Get("X-Tor-Poster") i2paddr := hdr.Get("X-I2p-Desthash") content_type := hdr.Get("Content-Type") has_attachment := strings.HasPrefix(content_type, "multipart/mixed") pubkey := hdr.Get("X-Pubkey-Ed25519") // TODO: allow certain pubkeys? is_signed := pubkey != "" is_ctl := newsgroup == "ctl" && is_signed anon_poster := torposter != "" || i2paddr != "" || encaddr == "" if !newsgroupValidFormat(newsgroup) { // invalid newsgroup format reason = "invalid newsgroup" return } else if banned, _ := daemon.database.NewsgroupBanned(newsgroup); banned { reason = "newsgroup banned" return } else if !(ValidMessageID(msgid) || (reference != "" && !ValidMessageID(reference))) { // invalid message id or reference reason = "invalid reference or message id is '" + msgid + "' reference is '" + reference + "'" return } else if daemon.database.ArticleBanned(msgid) { reason = "article banned" } else if reference != "" && daemon.database.ArticleBanned(reference) { reason = "thread banned" } else if daemon.database.HasArticleLocal(msgid) { // we already have this article locally reason = "have this article locally" return } else if daemon.database.HasArticle(msgid) { // we have already seen this article reason = "already seen" return } else if is_ctl { // we always allow control messages return } else if anon_poster { // this was posted anonymously if daemon.allow_anon { if has_attachment || is_signed { // this is a signed message or has attachment if daemon.allow_anon_attachments { // we'll allow anon attachments return } else { // we don't take signed messages or attachments posted anonymously reason = "no anon signed posts or attachments" return } } else { // we allow anon posts that are plain return } } else { // we don't allow anon posts of any kind reason = "no anon posts allowed" return } } else { // check for banned address var banned bool if encaddr != "" { banned, err = daemon.database.CheckEncIPBanned(encaddr) if err == nil { if banned { // this address is banned reason = "address banned" return } else { // not banned return } } } else { // idk wtf log.Println(self.name, "wtf? invalid article") } } return }
func (h *httpWebhook) sendArticle(msgid nntp.MessageID, group nntp.Newsgroup) { f, err := h.storage.OpenArticle(msgid.String()) if err == nil { u, _ := url.Parse(h.conf.URL) var r *http.Response var ctype string if h.conf.Dialect == "vichan" { c := textproto.NewConn(f) var hdr textproto.MIMEHeader hdr, err = c.ReadMIMEHeader() if err == nil { var body io.Reader ctype = hdr.Get("Content-Type") if ctype == "" || strings.HasPrefix(ctype, "text/plain") { ctype = "text/plain" } ctype = strings.Replace(strings.ToLower(ctype), "multipart/mixed", "multipart/form-data", 1) q := u.Query() for k, vs := range hdr { for _, v := range vs { q.Add(k, v) } } q.Set("Content-Type", ctype) u.RawQuery = q.Encode() if strings.HasPrefix(ctype, "multipart") { pr, pw := io.Pipe() log.Debug("using pipe") go func(in io.Reader, out io.WriteCloser) { _, params, _ := mime.ParseMediaType(ctype) if params == nil { // send as whatever lol io.Copy(out, in) } else { boundary, _ := params["boundary"] mpr := multipart.NewReader(in, boundary) mpw := multipart.NewWriter(out) mpw.SetBoundary(boundary) for { part, err := mpr.NextPart() if err == io.EOF { err = nil break } else if err == nil { // get part header h := part.Header // rewrite header part for php cd := h.Get("Content-Disposition") r := regexp.MustCompile(`filename="(.*)"`) // YOLO parts := r.FindStringSubmatch(cd) if len(parts) > 1 { fname := parts[1] h.Set("Content-Disposition", fmt.Sprintf(`filename="%s"; name="attachment[]"`, fname)) } // make write part wp, err := mpw.CreatePart(h) if err == nil { // write part out io.Copy(wp, part) } else { log.Errorf("error writng webhook part: %s", err.Error()) } } part.Close() } mpw.Close() } out.Close() }(c.R, pw) body = pr } else { body = f } r, err = http.Post(u.String(), ctype, body) } } else { var sz int64 sz, err = f.Seek(0, 2) if err != nil { return } f.Seek(0, 0) // regular webhook ctype = "text/plain; charset=UTF-8" cl := new(http.Client) r, err = cl.Do(&http.Request{ ContentLength: sz, URL: u, Method: "POST", Body: f, }) } if err == nil && r != nil { dec := json.NewDecoder(r.Body) result := make(map[string]interface{}) err = dec.Decode(&result) if err == nil || err == io.EOF { msg, ok := result["error"] if ok { log.Warnf("hook gave error: %s", msg) } else { log.Debugf("hook response: %s", result) } } else { log.Warnf("hook response does not look like json: %s", err) } r.Body.Close() log.Infof("hook called for %s", msgid) } } else { f.Close() } if err != nil { log.Errorf("error calling web hook %s: %s", h.conf.Name, err.Error()) } }
func (self *nntpConnection) handleLine(daemon NNTPDaemon, code int, line string, conn *textproto.Conn) (err error) { parts := strings.Split(line, " ") var msgid string if code == 0 && len(parts) > 1 { msgid = parts[1] } else { msgid = parts[0] } if code == 238 { if ValidMessageID(msgid) { self.stream <- nntpTAKETHIS(msgid) } return } else if code == 239 { // successful TAKETHIS log.Println(msgid, "sent via", self.name) return // TODO: remember success } else if code == 431 { // CHECK said we would like this article later log.Println("defer sending", msgid, "to", self.name) go self.articleDefer(msgid) } else if code == 439 { // TAKETHIS failed log.Println(msgid, "was not sent to", self.name, "denied:", line) // TODO: remember denial } else if code == 438 { // they don't want the article // TODO: remeber rejection } else { // handle command parts := strings.Split(line, " ") if len(parts) > 1 { cmd := parts[0] if cmd == "MODE" { if parts[1] == "READER" { // reader mode self.mode = "READER" log.Println(self.name, "switched to reader mode") conn.PrintfLine("201 No posting Permitted") } else if parts[1] == "STREAM" { // wut? we're already in streaming mode log.Println(self.name, "already in streaming mode") conn.PrintfLine("203 Streaming enabled brah") } else { // invalid log.Println(self.name, "got invalid mode request", parts[1]) conn.PrintfLine("501 invalid mode variant:", parts[1]) } } else if cmd == "QUIT" { // quit command conn.PrintfLine("") // close our connection and return conn.Close() return } else if cmd == "CHECK" { // handle check command msgid := parts[1] // have we seen this article? if daemon.database.HasArticle(msgid) { // yeh don't want it conn.PrintfLine("438 %s", msgid) } else if daemon.database.ArticleBanned(msgid) { // it's banned we don't want it conn.PrintfLine("438 %s", msgid) } else { // yes we do want it and we don't have it conn.PrintfLine("238 %s", msgid) } } else if cmd == "TAKETHIS" { // handle takethis command var hdr textproto.MIMEHeader var reason string // read the article header hdr, err = conn.ReadMIMEHeader() if err == nil { // check the header reason, err = self.checkMIMEHeader(daemon, hdr) dr := conn.DotReader() if len(reason) > 0 { // discard, we do not want code = 439 log.Println(self.name, "rejected", msgid, reason) _, err = io.Copy(ioutil.Discard, dr) err = daemon.database.BanArticle(msgid, reason) } else { // check if we don't have the rootpost reference := hdr.Get("References") newsgroup := hdr.Get("Newsgroups") if reference != "" && ValidMessageID(reference) && !daemon.store.HasArticle(reference) && !daemon.database.IsExpired(reference) { log.Println(self.name, "got reply to", reference, "but we don't have it") daemon.ask_for_article <- ArticleEntry{reference, newsgroup} } f := daemon.store.CreateTempFile(msgid) if f == nil { log.Println(self.name, "discarding", msgid, "we are already loading it") // discard io.Copy(ioutil.Discard, dr) } else { // write header err = writeMIMEHeader(f, hdr) // write body _, err = io.Copy(f, dr) if err == nil || err == io.EOF { f.Close() // we gud, tell daemon daemon.infeed_load <- msgid } else { log.Println(self.name, "error reading message", err) } } code = 239 reason = "gotten" } } else { log.Println(self.name, "error reading mime header:", err) code = 439 reason = "error reading mime header" } conn.PrintfLine("%d %s %s", code, msgid, reason) } else if cmd == "ARTICLE" { if ValidMessageID(msgid) { if daemon.store.HasArticle(msgid) { // we have it yeh f, err := os.Open(daemon.store.GetFilename(msgid)) if err == nil { conn.PrintfLine("220 %s", msgid) dw := conn.DotWriter() _, err = io.Copy(dw, f) dw.Close() f.Close() } else { // wtf?! conn.PrintfLine("503 idkwtf happened: %s", err.Error()) } } else { // we dont got it conn.PrintfLine("430 %s", msgid) } } else { // invalid id conn.PrintfLine("500 Syntax error") } } else if cmd == "POST" { // handle POST command conn.PrintfLine("340 Post it nigguh; end with <CR-LF>.<CR-LF>") hdr, err := conn.ReadMIMEHeader() var success bool if err == nil { hdr["Message-ID"] = []string{genMessageID(daemon.instance_name)} reason, err := self.checkMIMEHeader(daemon, hdr) success = reason == "" && err == nil if success { dr := conn.DotReader() reference := hdr.Get("References") newsgroup := hdr.Get("Newsgroups") if reference != "" && ValidMessageID(reference) && !daemon.store.HasArticle(reference) && !daemon.database.IsExpired(reference) { log.Println(self.name, "got reply to", reference, "but we don't have it") daemon.ask_for_article <- ArticleEntry{reference, newsgroup} } f := daemon.store.CreateTempFile(msgid) if f == nil { log.Println(self.name, "discarding", msgid, "we are already loading it") // discard io.Copy(ioutil.Discard, dr) } else { // write header err = writeMIMEHeader(f, hdr) // write body _, err = io.Copy(f, dr) if err == nil || err == io.EOF { f.Close() // we gud, tell daemon daemon.infeed_load <- msgid } else { log.Println(self.name, "error reading message", err) } } } } if success && err == nil { // all gud conn.PrintfLine("240 We got it, thnkxbai") } else { // failed posting if err != nil { log.Println(self.name, "failed nntp POST", err) } conn.PrintfLine("441 Posting Failed") } } else if cmd == "IHAVE" { // handle IHAVE command msgid := parts[1] if daemon.database.HasArticleLocal(msgid) || daemon.database.HasArticle(msgid) || daemon.database.ArticleBanned(msgid) { // we don't want it conn.PrintfLine("435 Article Not Wanted") } else { // gib we want conn.PrintfLine("335 Send it plz") hdr, err := conn.ReadMIMEHeader() if err == nil { // check the header var reason string reason, err = self.checkMIMEHeader(daemon, hdr) dr := conn.DotReader() if len(reason) > 0 { // discard, we do not want log.Println(self.name, "rejected", msgid, reason) _, err = io.Copy(ioutil.Discard, dr) // ignore this _ = daemon.database.BanArticle(msgid, reason) conn.PrintfLine("437 Rejected do not send again bro") } else { // check if we don't have the rootpost reference := hdr.Get("References") newsgroup := hdr.Get("Newsgroups") if reference != "" && ValidMessageID(reference) && !daemon.store.HasArticle(reference) && !daemon.database.IsExpired(reference) { log.Println(self.name, "got reply to", reference, "but we don't have it") daemon.ask_for_article <- ArticleEntry{reference, newsgroup} } f := daemon.store.CreateTempFile(msgid) if f == nil { log.Println(self.name, "discarding", msgid, "we are already loading it") // discard io.Copy(ioutil.Discard, dr) } else { // write header err = writeMIMEHeader(f, hdr) // write body _, err = io.Copy(f, dr) if err == nil || err == io.EOF { f.Close() // we gud, tell daemon daemon.infeed_load <- msgid } else { log.Println(self.name, "error reading message", err) } } conn.PrintfLine("235 We got it") } } else { // error here conn.PrintfLine("436 Transfer failed: " + err.Error()) } } } else if cmd == "NEWSGROUPS" { // handle NEWSGROUPS conn.PrintfLine("231 List of newsgroups follow") dw := conn.DotWriter() // get a list of every newsgroup groups := daemon.database.GetAllNewsgroups() // for each group for _, group := range groups { // get low/high water mark lo, hi, err := daemon.database.GetLastAndFirstForGroup(group) if err == nil { // XXX: we ignore errors here :\ _, _ = io.WriteString(dw, fmt.Sprintf("%s %d %d y\n", group, lo, hi)) } else { log.Println(self.name, "could not get low/high water mark for", group, err) } } // flush dotwriter dw.Close() } else if cmd == "XOVER" { // handle XOVER if self.group == "" { conn.PrintfLine("412 No newsgroup selected") } else { // handle xover command // right now it's every article in group models, err := daemon.database.GetPostsInGroup(self.group) if err == nil { conn.PrintfLine("224 Overview information follows") dw := conn.DotWriter() for idx, model := range models { io.WriteString(dw, fmt.Sprintf("%.6d\t%s\t\"%s\" <%s@%s>\t%s\t%s\t%s\r\n", idx+1, model.Subject(), model.Name(), model.Name(), model.Frontend(), model.Date(), model.MessageID(), model.Reference())) } dw.Close() } else { log.Println(self.name, "error when getting posts in", self.group, err) conn.PrintfLine("500 error, %s", err.Error()) } } } else if cmd == "GROUP" { // handle GROUP command group := parts[1] // check for newsgroup if daemon.database.HasNewsgroup(group) { // we have the group self.group = group // count posts number := daemon.database.CountPostsInGroup(group, 0) // get hi/low water marks low, hi, err := daemon.database.GetLastAndFirstForGroup(group) if err == nil { // we gud conn.PrintfLine("211 %d %d %d %s", number, low, hi, group) } else { // wtf error log.Println(self.name, "error in GROUP command", err) // still have to reply, send it bogus low/hi conn.PrintfLine("211 %d 0 1 %s", number, group) } } else { // no such group conn.PrintfLine("411 No Such Newsgroup") } } else { log.Println(self.name, "invalid command recv'd", cmd) conn.PrintfLine("500 Invalid command: %s", cmd) } } } return }
func (self *nntpConnection) handleLine(daemon *NNTPDaemon, code int, line string, conn *textproto.Conn) (err error) { parts := strings.Split(line, " ") var msgid string if code == 0 && len(parts) > 1 { msgid = parts[1] } else { msgid = parts[0] } if code == 238 { if ValidMessageID(msgid) { self.messageSetPendingState(msgid, "takethis") // they want this article self.takethis <- msgid } return } else if code == 239 { // successful TAKETHIS log.Println(msgid, "sent via", self.name) self.messageSetProcessed(msgid) return // TODO: remember success } else if code == 431 { // CHECK said we would like this article later self.messageSetProcessed(msgid) } else if code == 439 { // TAKETHIS failed log.Println(msgid, "was not sent to", self.name, "denied:", line) self.messageSetProcessed(msgid) // TODO: remember denial } else if code == 438 { // they don't want the article // TODO: remeber rejection self.messageSetProcessed(msgid) } else { // handle command parts := strings.Split(line, " ") if len(parts) > 1 { cmd := strings.ToUpper(parts[0]) if cmd == "MODE" { mode := strings.ToUpper(parts[1]) if mode == "READER" { // reader mode self.mode = "READER" log.Println(self.name, "switched to reader mode") if self.authenticated { conn.PrintfLine("200 Posting Permitted") } else { conn.PrintfLine("201 No posting Permitted") } } else if mode == "STREAM" && self.authenticated { // wut? we're already in streaming mode log.Println(self.name, "already in streaming mode") conn.PrintfLine("203 Streaming enabled brah") } else { // invalid log.Println(self.name, "got invalid mode request", parts[1]) conn.PrintfLine("501 invalid mode variant:", parts[1]) } } else if cmd == "QUIT" { // quit command conn.PrintfLine("") // close our connection and return conn.Close() return } else if cmd == "AUTHINFO" { if len(parts) > 1 { auth_cmd := strings.ToUpper(parts[1]) if auth_cmd == "USER" { // first part self.username = parts[2] // next phase is PASS conn.PrintfLine("381 Password required") } else if auth_cmd == "PASS" { if len(self.username) == 0 { conn.PrintfLine("482 Authentication commands issued out of sequence") } else { // try login var valid bool valid, err = daemon.database.CheckNNTPUserExists(self.username) if valid { valid, err = daemon.database.CheckNNTPLogin(self.username, line[14:]) } if valid { // valid login self.authenticated = true conn.PrintfLine("281 Authentication accepted") } else if err == nil { // invalid login conn.PrintfLine("481 Authentication rejected") } else { // there was an error // logit log.Println(self.name, "error while logging in as", self.username, err) conn.PrintfLine("501 error while logging in") } } } } else { // wut ? // wrong legnth of parametrs } } else if cmd == "CHECK" { // handle check command msgid := parts[1] if self.mode != "STREAM" { // we can't we are not in streaming mode conn.PrintfLine("431 %s", msgid) return } // have we seen this article? if daemon.database.HasArticle(msgid) { // yeh don't want it conn.PrintfLine("438 %s", msgid) } else if daemon.database.ArticleBanned(msgid) { // it's banned we don't want it conn.PrintfLine("438 %s", msgid) } else { // yes we do want it and we don't have it conn.PrintfLine("238 %s", msgid) } } else if cmd == "TAKETHIS" { // handle takethis command var hdr textproto.MIMEHeader var reason string var ban bool // read the article header r := bufio.NewReader(conn.DotReader()) hdr, err = readMIMEHeader(r) if err == nil { // check the header reason, ban, err = self.checkMIMEHeader(daemon, hdr) if len(reason) > 0 { // discard, we do not want code = 439 log.Println(self.name, "rejected", msgid, reason) _, err = io.Copy(ioutil.Discard, r) if ban { err = daemon.database.BanArticle(msgid, reason) } } else if err == nil { // check if we don't have the rootpost reference := hdr.Get("References") newsgroup := hdr.Get("Newsgroups") if reference != "" && ValidMessageID(reference) && !daemon.store.HasArticle(reference) && !daemon.database.IsExpired(reference) { log.Println(self.name, "got reply to", reference, "but we don't have it") go daemon.askForArticle(ArticleEntry{reference, newsgroup}) } // store message err = self.storeMessage(daemon, hdr, r) if err == nil { code = 239 reason = "gotten" } else { code = 439 reason = err.Error() } } else { // error? // discard, we do not want code = 439 log.Println(self.name, "rejected", msgid, reason) _, err = io.Copy(ioutil.Discard, r) if ban { err = daemon.database.BanArticle(msgid, reason) } } } else { log.Println(self.name, "error reading mime header:", err) code = 439 reason = "error reading mime header" } conn.PrintfLine("%d %s %s", code, msgid, reason) } else if cmd == "ARTICLE" { if !ValidMessageID(msgid) { if len(self.group) > 0 { n, err := strconv.Atoi(msgid) if err == nil { msgid, err = daemon.database.GetMessageIDForNNTPID(self.group, int64(n)) } } } if ValidMessageID(msgid) && daemon.store.HasArticle(msgid) { // we have it yeh f, err := daemon.store.OpenMessage(msgid) if err == nil { conn.PrintfLine("220 %s", msgid) dw := conn.DotWriter() _, err = io.Copy(dw, f) dw.Close() f.Close() } else { // wtf?! conn.PrintfLine("503 idkwtf happened: %s", err.Error()) } } else { // we dont got it conn.PrintfLine("430 %s", msgid) } } else if cmd == "IHAVE" { if !self.authenticated { conn.PrintfLine("483 You have not authenticated") } else { // handle IHAVE command msgid := parts[1] if daemon.database.HasArticleLocal(msgid) || daemon.database.HasArticle(msgid) || daemon.database.ArticleBanned(msgid) { // we don't want it conn.PrintfLine("435 Article Not Wanted") } else { // gib we want conn.PrintfLine("335 Send it plz") r := bufio.NewReader(conn.DotReader()) hdr, err := readMIMEHeader(r) if err == nil { // check the header var reason string var ban bool reason, ban, err = self.checkMIMEHeader(daemon, hdr) if len(reason) > 0 { // discard, we do not want log.Println(self.name, "rejected", msgid, reason) _, err = io.Copy(ioutil.Discard, r) if ban { _ = daemon.database.BanArticle(msgid, reason) } conn.PrintfLine("437 Rejected do not send again bro") } else { // check if we don't have the rootpost reference := hdr.Get("References") newsgroup := hdr.Get("Newsgroups") if reference != "" && ValidMessageID(reference) && !daemon.store.HasArticle(reference) && !daemon.database.IsExpired(reference) { log.Println(self.name, "got reply to", reference, "but we don't have it") go daemon.askForArticle(ArticleEntry{reference, newsgroup}) } err = self.storeMessage(daemon, hdr, r) if err == nil { conn.PrintfLine("235 We got it") } else { conn.PrintfLine("437 Transfer Failed %s", err.Error()) } } } else { // error here conn.PrintfLine("436 Transfer failed: " + err.Error()) } } } } else if cmd == "LISTGROUP" { // handle LISTGROUP var group string if len(parts) > 1 { // parameters group = parts[1] } else { group = self.group } if len(group) > 0 && newsgroupValidFormat(group) { if daemon.database.HasNewsgroup(group) { // we has newsgroup var hi, lo int64 count, err := daemon.database.CountAllArticlesInGroup(group) if err == nil { hi, lo, err = daemon.database.GetLastAndFirstForGroup(group) if err == nil { conn.PrintfLine("211 %d %d %d %s list follows", count, lo, hi, group) dw := conn.DotWriter() idx := lo for idx <= hi { fmt.Fprintf(dw, "%d\r\n", idx) idx++ } dw.Close() } } if err != nil { log.Println("LISTGROUP fail", err) conn.PrintfLine("500 error in LISTGROUP: %s", err.Error()) } } else { // don't has newsgroup conn.PrintfLine("411 no such newsgroup") } } else { conn.PrintfLine("412 no newsgroup selected") } } else if cmd == "NEWSGROUPS" { // handle NEWSGROUPS conn.PrintfLine("231 List of newsgroups follow") dw := conn.DotWriter() // get a list of every newsgroup groups := daemon.database.GetAllNewsgroups() // for each group for _, group := range groups { // get low/high water mark lo, hi, err := daemon.database.GetLastAndFirstForGroup(group) if err == nil { // XXX: we ignore errors here :\ _, _ = io.WriteString(dw, fmt.Sprintf("%s %d %d y\n", group, lo, hi)) } else { log.Println(self.name, "could not get low/high water mark for", group, err) } } // flush dotwriter dw.Close() } else if cmd == "XOVER" { // handle XOVER if self.group == "" { conn.PrintfLine("412 No newsgroup selected") } else { // handle xover command // right now it's every article in group models, err := daemon.database.GetPostsInGroup(self.group) if err == nil { conn.PrintfLine("224 Overview information follows") dw := conn.DotWriter() for idx, model := range models { io.WriteString(dw, fmt.Sprintf("%.6d\t%s\t\"%s\" <%s@%s>\t%s\t%s\t%s\r\n", idx+1, model.Subject(), model.Name(), model.Name(), model.Frontend(), model.Date(), model.MessageID(), model.Reference())) } dw.Close() } else { log.Println(self.name, "error when getting posts in", self.group, err) conn.PrintfLine("500 error, %s", err.Error()) } } } else if cmd == "HEAD" { if len(self.group) == 0 { // no group selected conn.PrintfLine("412 No newsgroup slected") } else { // newsgroup is selected // handle HEAD command if len(parts) == 0 { // we have no parameters if len(self.selected_article) > 0 { // we have a selected article } else { // no selected article conn.PrintfLine("420 current article number is invalid") } } else { // head command has 1 or more paramters var n int64 var msgid string var has bool var code int n, err = strconv.ParseInt(parts[1], 10, 64) if err == nil { // is a number msgid, err = daemon.database.GetMessageIDForNNTPID(self.group, n) if err == nil && len(msgid) > 0 { has = daemon.database.HasArticleLocal(msgid) } if !has { code = 423 } } else if ValidMessageID(parts[1]) { msgid = parts[1] has = daemon.database.HasArticleLocal(msgid) if has { n, err = daemon.database.GetNNTPIDForMessageID(self.group, parts[1]) } else { code = 430 } } if err == nil { if has { // we has hdrs := daemon.store.GetHeaders(msgid) if hdrs == nil { // wtf can't load? conn.PrintfLine("500 cannot load headers") } else { // headers loaded, send them conn.PrintfLine("221 %d %s", n, msgid) dw := conn.DotWriter() err = writeMIMEHeader(dw, hdrs) dw.Close() hdrs = nil } } else if code > 0 { // don't has conn.PrintfLine("%d don't have article", code) } else { // invalid state conn.PrintfLine("500 invalid state in HEAD, should have article but we don't") } } else { // error occured conn.PrintfLine("500 error in HEAD: %s", err.Error()) } } } } else if cmd == "GROUP" { // handle GROUP command group := parts[1] // check for newsgroup if daemon.database.HasNewsgroup(group) { // we have the group self.group = group // count posts number := daemon.database.CountPostsInGroup(group, 0) // get hi/low water marks hi, low, err := daemon.database.GetLastAndFirstForGroup(group) if err == nil { // we gud conn.PrintfLine("211 %d %d %d %s", number, low, hi, group) } else { // wtf error log.Println(self.name, "error in GROUP command", err) // still have to reply, send it bogus low/hi conn.PrintfLine("211 %d 0 1 %s", number, group) } } else { // no such group conn.PrintfLine("411 No Such Newsgroup") } } else if cmd == "LIST" && parts[1] == "NEWSGROUPS" { conn.PrintfLine("215 list of newsgroups follows") // handle list command groups := daemon.database.GetAllNewsgroups() dw := conn.DotWriter() for _, group := range groups { last, first, err := daemon.database.GetLastAndFirstForGroup(group) if err == nil { io.WriteString(dw, fmt.Sprintf("%s %d %d y\r\n", group, first, last)) } else { log.Println("cannot get last/first ids for group", group, err) } } dw.Close() } else if cmd == "STAT" { if len(self.group) == 0 { if len(parts) == 2 { // parameter given msgid := parts[1] // check for article if ValidMessageID(msgid) && daemon.database.HasArticleLocal(msgid) { // valid message id var n int64 n, err = daemon.database.GetNNTPIDForMessageID(self.group, msgid) // exists conn.PrintfLine("223 %d %s", n, msgid) err = nil } else { conn.PrintfLine("430 No article with that message-id") } } else { conn.PrintfLine("412 No newsgroup selected") } } else if daemon.database.HasNewsgroup(self.group) { // group specified if len(parts) == 2 { // parameter specified var msgid string var n int64 n, err = strconv.ParseInt(parts[1], 10, 64) if err == nil { msgid, err = daemon.database.GetMessageIDForNNTPID(self.group, n) if err != nil { // error getting id conn.PrintfLine("500 error getting nntp article id: %s", err.Error()) return } } else { // message id msgid = parts[1] } if ValidMessageID(msgid) && daemon.database.HasArticleLocal(msgid) { conn.PrintfLine("223 %d %s", n, msgid) } else if n == 0 { // was a message id conn.PrintfLine("430 no such article") } else { // was an article number conn.PrintfLine("423 no article with that number") } } else { conn.PrintfLine("420 Current article number is invalid") } } else { conn.PrintfLine("500 invalid daemon state, got STAT with group set but we don't have that group now?") } } else { log.Println(self.name, "invalid command recv'd", cmd) conn.PrintfLine("500 Invalid command: %s", cmd) } } else { if line == "LIST" { conn.PrintfLine("215 list of newsgroups follows") // handle list command groups := daemon.database.GetAllNewsgroups() dw := conn.DotWriter() for _, group := range groups { last, first, err := daemon.database.GetLastAndFirstForGroup(group) if err == nil { io.WriteString(dw, fmt.Sprintf("%s %d %d y\r\n", group, first, last)) } else { log.Println("cannot get last/first ids for group", group, err) } } dw.Close() } else if line == "POST" { if !self.authenticated { // needs tls to work if not logged in conn.PrintfLine("440 Posting Not Allowed") } else { // handle POST command conn.PrintfLine("340 Yeeeh postit yo; end with <CR-LF>.<CR-LF>") var hdr textproto.MIMEHeader hdr, err = readMIMEHeader(conn.R) var success, gotten bool var reason string if err == nil { if getMessageID(hdr) == "" { hdr.Set("Message-ID", genMessageID(daemon.instance_name)) } msgid = getMessageID(hdr) hdr.Set("Date", timeNowStr()) ipaddr, _, _ := net.SplitHostPort(self.addr.String()) if len(ipaddr) > 0 { // inject encrypted ip for poster encaddr, err := daemon.database.GetEncAddress(ipaddr) if err == nil { hdr.Set("X-Encrypted-Ip", encaddr) } } reason, _, err = self.checkMIMEHeader(daemon, hdr) success = reason == "" && err == nil if success { r := bufio.NewReader(conn.DotReader()) reference := hdr.Get("References") newsgroup := hdr.Get("Newsgroups") if reference != "" && ValidMessageID(reference) { if !daemon.store.HasArticle(reference) && !daemon.database.IsExpired(reference) { log.Println(self.name, "got reply to", reference, "but we don't have it") go daemon.askForArticle(ArticleEntry{reference, newsgroup}) } } else if reference != "" { // bad message id reason = "cannot reply with invalid reference, maybe you are replying to a reply?" success = false } if success && daemon.database.HasNewsgroup(newsgroup) { err = self.storeMessage(daemon, hdr, r) } } } if success && gotten && err == nil { // all gud conn.PrintfLine("240 We got it, thnkxbai") } else { // failed posting if err != nil { log.Println(self.name, "failed nntp POST", err) } conn.PrintfLine("441 Posting Failed %s", reason) } } } else { conn.PrintfLine("500 wut?") } } } return }
func parseHeadersAndStream(header textproto.MIMEHeader, body io.ReadCloser) GetObjectResult { objectId, err := strconv.ParseInt(header.Get("Object-ID"), 10, 64) if err != nil { // Attempt to parse a Rets Response code (if it exists) retsResp, parseErr := ParseRetsResponse(body) if parseErr != nil { return GetObjectResult{nil, err} } // Include a GetObject (empty of content) so that its rets response can be retrieved emptyResult := GetObject{ RetsErrorMessage: *retsResp, RetsError: retsResp.ReplyCode != 0, } return GetObjectResult{&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 GetObjectResult{nil, err} } retsError, err := strconv.ParseBool(header.Get("RETS-Error")) retsErrorMsg := &RetsResponse{0, "Success"} switch { case err != nil: retsError = false case retsError: body := ioutil.NopCloser(bytes.NewReader([]byte(blob))) retsErrorMsg, err = ParseRetsResponse(body) if err != nil { return GetObjectResult{nil, err} } } object := GetObject{ // 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, RetsErrorMessage: *retsErrorMsg, Preferred: preferred, ObjectData: objectData, Blob: blob, } return GetObjectResult{&object, nil} }