// given a tripcode after the # // make a seed byteslice func parseTripcodeSecret(str string) []byte { // try decoding hex raw := unhex(str) keylen := nacl.CryptoSignSeedLen() if raw == nil || len(raw) != keylen { // treat this as a "regular" chan tripcode // decode as bytes then pad the rest with 0s if it doesn't fit raw = make([]byte, keylen) str_bytes := []byte(str) if len(str_bytes) > keylen { copy(raw, str_bytes[:keylen]) } else { copy(raw, str_bytes) } } return raw }
// handle new post via http request for a board func (self httpFrontend) handle_postform(wr http.ResponseWriter, r *http.Request, board string) { // always lower case newsgroups board = strings.ToLower(board) // post fail message post_fail := "" // post message msg := "" // the nntp message var nntp nntpArticle nntp.headers = make(ArticleHeaders) // tripcode private key var tripcode_privkey []byte // encrypt IP Addresses // when a post is recv'd from a frontend, the remote address is given its own symetric key that the local srnd uses to encrypt the address with, for privacy // when a mod event is fired, it includes the encrypted IP address and the symetric key that frontend used to encrypt it, thus allowing others to determine the IP address // each stnf will optinally comply with the mod event, banning the address from being able to post from that frontend // this will be done eventually but for now that requires too much infrastrucutre, let's go with regular IP Addresses for now. // get the "real" ip address from the request address, _, err := net.SplitHostPort(r.RemoteAddr) // TODO: have in config upstream proxy ip and check for that if strings.HasPrefix(address, "127.") { // if it's loopback check headers for reverse proxy headers // TODO: make sure this isn't a tor user being sneaky address = getRealIP(r.Header.Get("X-Real-IP")) } // check for banned if len(address) > 0 { banned, err := self.daemon.database.CheckIPBanned(address) if err == nil { if banned { wr.WriteHeader(403) // TODO: ban messages io.WriteString(wr, "nigguh u banned.") return } } else { wr.WriteHeader(500) io.WriteString(wr, "error checking for ban: ") io.WriteString(wr, err.Error()) return } } if len(address) == 0 { address = "Tor" } if !strings.HasPrefix(address, "127.") { // set the ip address of the poster to be put into article headers // if we cannot determine it, i.e. we are on Tor/i2p, this value is not set if address == "Tor" { nntp.headers.Set("X-Tor-Poster", "1") } else { address, err = self.daemon.database.GetEncAddress(address) nntp.headers.Set("X-Encrypted-IP", address) // TODO: add x-tor-poster header for tor exits } } // if we don't have an address for the poster try checking for i2p httpd headers address = r.Header.Get("X-I2P-DestHash") // TODO: make sure this isn't a Tor user being sneaky if len(address) > 0 { nntp.headers.Set("X-I2P-DestHash", address) } // set newsgroup nntp.headers.Set("Newsgroups", board) // redirect url url := "" // mime part handler var part_buff bytes.Buffer mp_reader, err := r.MultipartReader() if err != nil { errmsg := fmt.Sprintf("httpfrontend post handler parse multipart POST failed: %s", err) log.Println(errmsg) wr.WriteHeader(500) io.WriteString(wr, errmsg) return } var subject, name string for { part, err := mp_reader.NextPart() if err == nil { // get the name of the part partname := part.FormName() // read part for attachment if partname == "attachment" && self.attachments { log.Println("attaching file...") att := readAttachmentFromMimePart(part) nntp = nntp.Attach(att).(nntpArticle) continue } io.Copy(&part_buff, part) // check for values we want if partname == "subject" { subject = part_buff.String() } else if partname == "name" { name = part_buff.String() } else if partname == "message" { msg = part_buff.String() } else if partname == "reference" { ref := part_buff.String() if len(ref) == 0 { url = fmt.Sprintf("%s.html", board) } else if ValidMessageID(ref) { if self.daemon.database.HasArticleLocal(ref) { nntp.headers.Set("References", ref) url = fmt.Sprintf("thread-%s.html", ShortHashMessageID(ref)) } else { // no such article url = fmt.Sprintf("%s.html", board) post_fail += "we don't have " post_fail += ref post_fail += "locally, can't reply. " } } else { post_fail += "invalid reference: " post_fail += ref post_fail += ", not posting. " } } else if partname == "captcha" { captcha_solution := part_buff.String() s, err := self.store.Get(r, self.name) captcha_id, ok := s.Values["captcha_id"] if err == nil && ok { if captcha.VerifyString(captcha_id.(string), captcha_solution) { // captcha is valid } else { // captcha is not valid post_fail += "failed captcha. " } } else { // captcha has no cookies post_fail += "enable cookies. " } } // we done // reset buffer for reading parts part_buff.Reset() // close our part part.Close() } else { if err != io.EOF { errmsg := fmt.Sprintf("httpfrontend post handler error reading multipart: %s", err) log.Println(errmsg) wr.WriteHeader(500) io.WriteString(wr, errmsg) return } break } } // make error template param resp_map := make(map[string]string) resp_map["prefix"] = self.prefix // set redirect url if len(url) > 0 { // if we explicitly know the url use that resp_map["redirect_url"] = self.prefix + url } else { // if our referer is saying we are from /new/ page use that // otherwise use prefix if strings.HasSuffix(r.Referer(), self.prefix+"new/") { resp_map["redirect_url"] = self.prefix + "new/" } else { resp_map["redirect_url"] = self.prefix } } if len(nntp.attachments) == 0 && len(msg) == 0 { post_fail += "no message. " } if len(post_fail) > 0 { wr.WriteHeader(200) resp_map["reason"] = post_fail io.WriteString(wr, template.renderTemplate("post_fail.mustache", resp_map)) return } // set subject if len(subject) == 0 { subject = "None" } nntp.headers.Set("Subject", subject) if isSage(subject) { nntp.headers.Set("X-Sage", "1") } // set name if len(name) == 0 { name = "Anonymous" } else { idx := strings.Index(name, "#") // tripcode if idx >= 0 { tripcode_privkey = parseTripcodeSecret(name[idx+1:]) name = strings.Trim(name[:idx], "\t ") if name == "" { name = "Anonymous" } } } nntp.headers.Set("From", nntpSanitize(fmt.Sprintf("%s <anon@%s>", name, self.name))) nntp.headers.Set("Message-ID", genMessageID(self.name)) // set message nntp.message = createPlaintextAttachment(msg) // set date nntp.headers.Set("Date", timeNowStr()) // append path from frontend nntp.AppendPath(self.name) // send message off to daemon log.Printf("uploaded %d attachments", len(nntp.Attachments())) nntp.Pack() // sign if needed if len(tripcode_privkey) == nacl.CryptoSignSeedLen() { nntp, err = signArticle(nntp, tripcode_privkey) if err != nil { // wtf? error!? log.Println("error signing", err) wr.WriteHeader(500) io.WriteString(wr, err.Error()) return } } // XXX: write it temp instead // self.postchan <- nntp f := self.daemon.store.CreateTempFile(nntp.MessageID()) if f != nil { nntp.WriteTo(f, "\n") f.Close() } self.daemon.infeed_load <- nntp.MessageID() // send success reply wr.WriteHeader(200) // determine the root post so we can redirect to the thread for it msg_id := nntp.headers.Get("References", nntp.MessageID()) // render response as success url = fmt.Sprintf("%sthread-%s.html", self.prefix, ShortHashMessageID(msg_id)) io.WriteString(wr, template.renderTemplate("post_success.mustache", map[string]string{"prefix": self.prefix, "message_id": nntp.MessageID(), "redirect_url": url})) }